2. Static decorations
One of the feature advantage of Wolfram Mathematica and WLJS Notebook is a multimodal cells with a powerful syntax sugar. A visual representation of an instance of an object makes the programming experience more educative for sure.
Summary Item
The easiest way of providing a bit more information, but still keeping the actual expression intact is to use ArrangeSummaryBox
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]}
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
None,
summary,
Null
]
]
]
Here we redefined a standard output form to a decorated summary box, providing the visible state field
StateMachine["State" -> 3]
Despite the fact of looking different, you can still work with it normally: setting and getting properties, i.e.
is 100% valid
Custom decorations
One can do all decorations from scratch using Graphics for instance
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
g = Graphics[{ Opacity[0.5],
Table[
Rotate[{
Hue[i/12.0, 1.0, 0.5],
Rectangle[{-1,-1}, {1,1}]
}, i / (s["State"]+1)]
, {i, 0, 6Pi, Pi}]
}, ImageSize->{100,100}, ImagePadding->None]
},
ViewBox[s, g]
]
]
The result will look like
machine = StateMachine[]
StateMachineChange[machine, 2]
Summary Item and Custom decoration
Why not to merge both leaving the graphics as an icon?
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{ Opacity[0.5],
Table[
Rotate[{
Hue[i/12.0, 1.0, 0.5],
Rectangle[{-1,-1}, {1,1}]
}, i / (s["State"]+1), {0.,0.}]
, {i, 0, 6Pi, Pi}]
}, ImageSize->{50,50}, AspectRatio->1, ImagePadding->None]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null
]
]
]
machine = StateMachine["State"->2]
Javascript decoration
There is also an option to use pure Javascript to render an object, let us make our layout much simpler starting with
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{},
ViewBox[s, CustomDecorator[s["State"]]]
]
Here we mentioned CustomDecorator
which is going to be our WLJS Function
Then, create a new cell
.js
core.CustomDecorator = async (args, env) => {
const state = await interpretate(args[0], env);
const element = env.element;
element.classList.add('flex', 'rounded-md', 'p-2');
element.style.border = "1px solid #999";
element.style.boxShadow = "inset 0 2px 4px 0 rgb(0 0 0 / 0.05)";
element.style.transitionDuration = '0.8s';
element.style.transitionProperty = 'transform';
setTimeout(() => {
element.style.transform = "rotate(360deg)";
}, 100);
element.innerText = state;
}
The result will be following
Animated decoration in Summary Item
Why not also animate it using Wolfram Language?
Let it be many balls bouncing the walls. Firstly let us make proof of concept
balls = RandomReal[{-1,1}, {4,2}];
velocities = RandomReal[{-1,1}, {4,2}];
EventHandler["animate", Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
]]
Graphics[{PointSize[0.03], Point[balls // Offload], AnimationFrameListener[balls // Offload, "Event"->"animate"]}, PlotRange->{{-1,1}, {-1,1}}, TransitionType->None]
Now we need only to scope our variables and embed it to summary item
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{
balls = RandomReal[{-1,1}, {s["State"],2}],
velocities = RandomReal[{-1,1}, {s["State"],2}],
animateEvent = CreateUUID[]
},
EventHandler[animateEvent, Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
]];
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{
PointSize[0.03], Point[balls // Offload],
AnimationFrameListener[balls // Offload, "Event"->animateEvent]
},
PlotRange->{{-1,1}, {-1,1}},
TransitionType->None,
ImageSize->{50,50},
AspectRatio->1,
ImagePadding->None
]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null
]
]
]
Then let us see the result
machine = StateMachine["State"->2]
If you want to see an optimized version, please, follow below
Optimized version
Since it relies on AnimationFrameListener, it runs as fast as possible, which might be an issue for a lot of those objects on the screen.
Just using SetInterval
is not an options, since we need something to remove this timer, when there is no visible widgets.
ArrangeSummaryBox is a wrapper over ViewBox, which has an event generator. A user can attach EventHandler
to it and check if a widget was destroyed or created.
StateMachine /: MakeBoxes[s: StateMachine[symbol_Symbol?AssociationQ], form: (StandardForm | TraditionalForm)] := Module[{
balls = RandomReal[{-1,1}, {s["State"],2}],
velocities = RandomReal[{-1,1}, {s["State"],2}],
task, instances = 0,
calculate,
controller = CreateUUID[],
construct,
notebook = EvaluationNotebook[],
destruct
},
(* if someone closed notebook *)
With[{cloned = EventClone[notebook]},
EventHandler[cloned, {"OnClose" -> Function[Null,
destruct;
]}];
];
construct := With[{},
task = SetInterval[calculate[], 100];
];
destruct := With[{},
TaskRemove[task];
];
EventHandler[controller, {
"Mounted" -> Function[Null,
If[instances === 0, construct];
instances = instances + 1;
],
"Destroy" -> Function[Null,
instances = instances - 1;
(* unsubscribe when there is no instances *)
If[instances === 0, destruct];
]
}];
calculate = Function[Null,
{balls, velocities} = Map[With[{
v = {If[Abs[#[[1,1]]] >= 1, -1, 1], If[Abs[#[[1, 2]]] >= 1, -1, 1]} #[[2]],
p = #[[1]]
},
{p + 0.2 v, v}
]&, Transpose[{balls, velocities}]] // Transpose;
];
With[{
summary = {BoxForm`SummaryItem[{"State: ", s["State"]}]},
icon = Graphics[{
PointSize[0.03], Point[balls // Offload]
},
PlotRange->{{-1,1}, {-1,1}},
TransitionType->"Linear",
TransitionDuration -> 100,
ImageSize->{50,50},
AspectRatio->1,
ImagePadding->None
]
},
BoxForm`ArrangeSummaryBox[
StateMachine,
s,
icon,
summary,
Null,
"Event" -> controller
]
]
]
The following changes were made
SetInterval
drives the calculations with100 ms
interval- TransitionDuration interpolates the results with
100 ms
window - We listen an events of creation and destruction of widgets using
"Event"
option of ArrangeSummaryBox - We remove timers, when there is no visible instances on the screen
- We remove timers, when the connection to the notebook was lost (a user closed notebook)