Tutorial
Let's learn how to make animations step by step:
Needs["AnimationFramework`" -> "af`"] // Quiet
Create a scene
s = af`Scene[];
Framed[s]
Animation Flow
All animations on the scene should follows the pattern:
- append objects
- animate
- remove objects
By design most of methods and functions of AF framework return Promise expression. In this regard, it makes sense to describe your animation flow using Asynchronous Functions.
Basics
Append and move
Append and move a disk on the scene:
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, Disk[#pos, 0.1], {
"pos" -> {-1,0}
}];
af`Animate[scene, d, "pos"->{1,0}, "QuadOut", 1.5] // Await;
af`Remove[d];
]][s];
Combine animations of two properties:
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, Disk[#pos, #r], {
"pos" -> {-1,0}, "r"->0.1
}];
af`Animate[scene, d, {
"pos"->{1,0}, "r"->1.0
}, "QuadOut", 1.5] // Await;
af`Remove[d];
]][s];
Animate two objects at the same time:
AsyncFunction[scene, Module[{d,r},
r = af`AddTo[scene, {Red, Translate[Rotate[
Rectangle[0.2{-1,-1}, 0.2{1,1}]
, #angle], #pos]}, {
"pos" -> {1.5,1.5},
"angle" -> 0.
}];
d = af`AddTo[scene, {Blue, Disk[#pos, 0.1]}, {
"pos" -> {-1.5,0}
}];
{
af`Animate[scene, d, "pos"->{0,0}, "QuadOut", 1.5],
af`Animate[scene, r, {
"pos"->{0,0}, "angle"->2Pi
}, "QuadOut", 1.5]
}// Await;
af`Remove[d];
af`Remove[r];
]][s];
Timings
Try different easing functions:
AsyncFunction[scene, Module[{d,label},
d = af`AddTo[scene, {Blue, Disk[#pos, 0.1]}, {
"pos" -> {-0.8,0}
}];
label = af`AddTo[scene,
{Text[#text, {0,0.2}, {0,0}]}, {"text"->"QuadOut"}
];
af`Animate[scene, d, "pos"->{0.8,0}, "QuadOut", 2] // Await;
af`Update[scene, d, "pos"->{-0.8,0}];
af`Update[scene, label, "text"->"QuadIn"];
af`Animate[scene, d, "pos"->{0.8,0}, "QuadIn", 2] // Await;
af`Update[scene, d, "pos"->{-0.8,0}];
af`Update[scene, label, "text"->"CubicInOut"];
af`Animate[scene, d, "pos"->{0.8,0}, "CubicInOut", 2] // Await;
af`Update[scene, d, "pos"->{-0.8,0}];
af`Update[scene, label, "text"->"Linear"];
af`Animate[scene, d, "pos"->{0.8,0}, "Linear", 2] // Await;
af`Remove[d];
af`Remove[label];
]][s];
Use pauses:
AsyncFunction[scene, Module[{r},
r = af`AddTo[scene, {Red, Translate[Rotate[
Rectangle[0.2{-1,-1}, 0.2{1,1}]
, #angle], #pos]}, {
"pos" -> {1.5,1.5},
"angle" -> 0.
}];
af`Animate[scene, r, "pos"->{0,0}, "CubicInOut", 1] // Await;
PauseAsync[scene, 0.2] // Await;
af`Animate[scene, r, "angle"->Pi/4, "CubicInOut", 0.4] // Await;
PauseAsync[scene, 0.2] // Await;
af`Animate[scene, r, "pos"->{-0.5,0}, "CubicInOut", 1] // Await;
PauseAsync[scene, 0.2] // Await;
{
af`Animate[scene, r, "angle"->Pi/2, "CubicInOut", 1],
af`Animate[scene, r, "pos"->{-0.5,-0.5}, "CubicInOut", 1]
} // Await;
{
af`Animate[scene, r, "angle"->(Pi/2 + Pi/4), "CubicInOut", 1],
af`Animate[scene, r, "pos"->{0.5,-0.5}, "CubicInOut", 1]
} // Await;
{
af`Animate[scene, r, "angle"->(Pi/2 + Pi/2), "CubicInOut", 1],
af`Animate[scene, r, "pos"->{0.5,0.5}, "CubicInOut", 1]
} // Await;
{
af`Animate[scene, r, "angle"->(10Pi), "CubicInOut", 4],
af`Animate[scene, r, "pos"->{0,0}, "CubicInOut", 1]
} // Await;
PauseAsync[scene, 2] // Await;
af`Remove[r];
]][s];
Turn repeating elements into async functions
Using the pervious examples on Timings we can simplify the code:
animateStuff = AsyncFunction[{s, d, l, type}, (
af`Update[s, d, "pos"->{-0.8,0}];
af`Update[s, l, "text"->type];
af`Animate[s, d, "pos"->{0.8,0}, type, 2] // Await;
)];
AsyncFunction[scene, Module[{d,label},
d = af`AddTo[scene, {Blue, Disk[#pos, 0.1]}, {
"pos" -> {-0.8,0}
}];
label = af`AddTo[scene,
{Text[#text, {0,0.2}, {0,0}]}, {"text"->""}
];
animateStuff[scene, d, label, "QuadOut"] // Await;
animateStuff[scene, d, label, "QuadIn"] // Await;
animateStuff[scene, d, label, "CubicInOut"] // Await;
animateStuff[scene, d, label, "Linear"] // Await;
af`Remove[d];
af`Remove[label];
]][s];
Animating more properties
Animate Opacity for adding or removing an objects:
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, {Opacity[#o], Red, Disk[#pos, 0.1]}, {
"pos" -> {-0.5,0},
"o"->0
}];
af`Animate[scene, d, "o"->1.0, "Linear", 0.2] // Await;
af`Animate[scene, d, "pos"->{0.5,0}, "Ease", 1.5] // Await;
PauseAsync[scene, 0.5] // Await;
af`Animate[scene, d, "o"->0.0, "Linear", 0.2] // Await;
af`Remove[d];
]][s];
To interpolate color of the object we use "LinearEuclidean"
to get the correct gamma of the intermediate colors:
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, {Opacity[#o], RGBColor[#c], Disk[#pos, 0.1]}, {
"pos" -> {-0.5,0},
"o"->0,
"c"->af`Color[Red]
}];
af`Animate[scene, d, "o"->1.0, "Linear", 0.2] // Await;
{
af`Animate[scene, d, "c"->af`Color[Blue], "LinearEuclidean", 1.5],
af`Animate[scene, d, "pos"->{0.5,0}, "Linear", 1.5]
} // Await;
PauseAsync[scene, 0.5] // Await;
af`Animate[scene, d, "o"->0.0, "Linear", 0.2] // Await;
af`Remove[d];
]][s];
Background animation:
AsyncFunction[scene, Module[{bg,text},
bg = af`AddTo[scene, {
RGBColor[#c], Opacity[#o], Rectangle[{-2,-2}, {2,2}]
}, {
"o"->0.,
"c"->af`Color[Pink]
}];
text = af`AddTo[scene, {White, Text[Style["Hello World", FontSize->68], {0,0}, {0,0}]}];
af`Animate[scene, bg, "o"->1.0, "CubicInOut", 0.5] // Await;
PauseAsync[scene, 0.3] // Await;
af`Animate[scene, bg, "c"->af`Color[Lighter[Magenta]], "LinearEuclidean", 0.5] // Await;
PauseAsync[scene, 0.3] // Await;
af`Animate[scene, bg, "c"->af`Color[Lighter[Brown]], "LinearEuclidean", 0.5] // Await;
PauseAsync[scene, 0.3] // Await;
af`Animate[scene, bg, "o"->0.0, "CubicInOut", 0.5] // Await;
af`Remove[bg];
af`Remove[text];
]][s];
Layers
Layer are used for grouping multiple objects. All properties do not propagate to the objects in the group, but applied to the container. You can animate:
Opacity
Rotate
Translate
Scale
SVGAttribute
There is a special key-property for placing children objects #children
. Methods used for operating groups are similar to Scene
and to a regular object. For example:
AsyncFunction[scene, Module[{layer},
layer = af`Layer[scene, {
Opacity[#opacity],
Scale[#children, #scale]
}, {"opacity" -> 0., "scale"->1.0}];
af`AddTo[layer, Table[{RandomColor[], Disk[RandomReal[{-0.2,0.2},2], 0.05]}, {10}]];
af`Animate[scene, layer, "opacity"->1.0, "Linear", 0.5] // Await;
af`Animate[scene, layer, "scale"->3.0, "QuadOut", 0.5] // Await;
af`Animate[scene, layer, "scale"->0.1, "QuadIn", 0.5] // Await;
af`Remove[layer];
]][s];
Apply directly any CSS style:
AsyncFunction[scene, Module[{layer},
layer = af`Layer[scene, {
SVGAttribute[#children, "style"->#style]
}, {"style"->"filter: blur(0px)"}];
af`AddTo[layer, Table[{RandomColor[], Disk[RandomReal[{-0.5,0.5},2], 0.05]}, {10}]];
af`Animate[scene, layer, "style"->"filter: blur(100px)", "Linear", 2] // Await;
af`Remove[layer];
]][s];
Animate
does support string interpolation
By removing the layer object you will removes all children as well
External events
Any EventObject
or Promise
can be used a trigger for your animation:
next = InputButton["Next"]
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, {
Disk[#pos, #r], Hue[0.3], Circle[#pos, #r #r]
},
{"pos"->{0.,0.}, "r"->0.001}
];
next // Await;
af`Animate[scene, d, "r"->0.5, "CubicInOut", 1] // Await;
next // Await;
af`Animate[scene, d, "r"->0.0001, "CubicInOut", 1] // Await;
af`Remove[d];
]][s];
Embed foreign objects
For general case you need to use Inset (see also Inset expressions).
Embed a graph:
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, {
Translate[Scale[
Rotate[Inset[Plot[x, {x,0,1}, ImageSize->Small]], #a],
0.5
], #pos]
},
{"pos"->{2.0,0.}, "a"->0}
];
af`Animate[scene, d, {
"pos"->{0,0},
"a"->2Pi
}, "CubicInOut", 1] // Await;
PauseAsync[scene, 1] // Await;
af`Remove[d];
]][s];
Inset is pixel-perfect and it does not autoscale. Results might depend on pixel-density of the screen. To avoid that use ImageSizeRaw
instead of ImageSize
on Graphics
-like objects
Medium
Combining with Offload
AddTo
method effectively uses Offload technique from general dynamics of WLJS Notebook. It means you can still use it in any animated object.
Animate
method can animate an arbitrary symbol as well:
AsyncFunction[scene, Module[{d, pos = {2,0}, a = 0.},
d = af`AddTo[scene, {
Translate[
Rotate[
Text["Hello World", {0,0}, {0,0}]
, Offload[a]]
, Offload[pos]]
}];
{
af`Animate[scene, Hold[pos], {0,0}, "CubicInOut", 1],
af`Animate[scene, Hold[a], 2Pi, "CubicInOut", 1]
} // Await;
PauseAsync[scene, 1] // Await;
af`Remove[d];
]][s];
Equivalent using AF only
AsyncFunction[scene, Module[{d},
d = af`AddTo[scene, {
Translate[Rotate[Text["Hello World", {0,0}, {0,0}], #a], #pos]
},
{"pos"->{2.0,0.}, "a"->0}
];
af`Animate[scene, d, {
"pos"->{0,0},
"a"->2Pi
}, "CubicInOut", 1] // Await;
PauseAsync[scene, 1] // Await;
af`Remove[d];
]][s];
An example, where we combine both approaches to avoid extra Translate
function:
AsyncFunction[scene, Module[{d, pos = {2,0}, a = 0.},
d = af`AddTo[scene, {
Magenta, Rectangle[
Offload[{-0.5,-0.5} + #pos],
Offload[{0.5,0.5} + #pos]
, RoundingRadius->0.1]
}, {"pos"->{0,-1.5}}];
af`Animate[scene, d, "pos"->{0,0}, "CubicInOut", 1] // Await;
PauseAsync[scene, 1] // Await;
af`Remove[d];
]][s];
Another example:
AsyncFunction[scene, Module[{d, text = "Hello"},
d = af`AddTo[scene, {
Translate[Rotate[Text[text // Offload, {0,0}, {0,0}], #a], #pos]
},
{"pos"->{2.0,0.}, "a"->0}
];
af`Animate[scene, d, {
"pos"->{0,0},
"a"->2Pi
}, "CubicInOut", 1] // Await;
text = "Hello W";
af`PauseAsync[scene, 0.2] // Await;
text = "Hello Wo";
af`PauseAsync[scene, 0.2] // Await;
text = "Hello Wor";
af`PauseAsync[scene, 0.2] // Await;
text = "Hello Worl";
af`PauseAsync[scene, 0.2] // Await;
text = "Hello World";
af`PauseAsync[scene, 0.2] // Await;
af`Remove[d];
]][s];
In general named property slots #pos
and etc can be passed further to other function outside the main one:
Details
fakeWritting[from_, to_, vert_, scale_: 1, freq_: 50] :=
Line[
MovingAverage[
Table[
{
i,
vert + scale 0.065 Sin[freq i] + 0 RandomReal[scale 0.05 {-1, 1}]
},
{i, from, to, (to - from)/300.0}
],
10
]
];
slidersComponent[p1_, p2_] := {
White,
Rectangle[{-0.5, -0.5}, {0.5, 0.2}, RoundingRadius -> 2 0.14],
{
RGBColor[0.9, 0.9, 0.9],
Rectangle[{-0.3, -0.02}, {0.3, 0.02}, RoundingRadius -> 0.04],
RGBColor[0.4, 0.6, 0.9],
Disk[{0.25 p1, 0}, 0.05]
},
Translate[
{
RGBColor[0.9, 0.9, 0.9],
Rectangle[{-0.3, -0.02}, {0.3, 0.02}, RoundingRadius -> 0.04],
RGBColor[0.4, 0.6, 0.9],
Disk[{0.25 p2, 0}, 0.05]
},
{0, -0.3}
]
};
ClearAll[notebookModel]
notebookModel[size_, scale_, contentOpacity_, s_] :=
Scale[
SVGGroup[
{
Lighter[Black],
Text[Style["Notebook", FontSize -> 14], {0, 0.4}, {0, -1}],
RGBColor[0.93, 0.93, 0.90],
Rectangle[size {-1, -1}, size {1, 1}, RoundingRadius -> 0.13],
Opacity[contentOpacity],
SVGGroup[{
{
Translate[
Scale[
slidersComponent[Offload[s[[1]]], Offload[s[[2]]]],
0.2
],
{0.16015625, 0.16022949218750004}
]
},
Black // Lighter // Lighter,
{
AbsoluteThickness[2],
fakeWritting[-0.3, 0.05, 0.27, 0.4, 170/1.8]
},
Table[
fakeWritting[-0.3, 0.3, i, 0.1, 170],
{i, 0.13, 0.3 - 0.07, 0.04}
],
Table[
fakeWritting[-0.3, 0.3, i, 0.1, 170],
{i, -0.3 + 0.03, -0.1, 0.04}
]
}]
}
],
scale
];
AsyncFunction[scene, Module[{d, sliders = {0,0}},
d = af`AddTo[scene, {
Translate[notebookModel[0.33, 2.0, 1.0, sliders // Offload], #pos]
}, {
"pos"->{0,2.0},
"cop"->1.0
}];
af`Animate[scene, d, "pos"->{0,0}, "CubicInOut", 1] // Await;
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
af`Remove[d];
]][s];
This example can be heavily simplified using Workers
Be careful with temporal symbols defined within the Module
. If they are not involved into any animation Wolfram Kernel will collect them as garbage.
Loops
It allows to create custom animation loop on a given property of an object on the scene:
af`Loop[scene_, object_, prop_String, Function[{t, previousValue, n},
...
], duration_]
where t
goes from 0
to 1
every duration
and n
represents the number of a cycle.
Loop function is called every frame (usually 30 times per second) and is running until manually removed using Remove method.
To simulate a delayed event we introduce a button:
next = InputButton["Stop"]
As an example we rotate infinitely a rectangle:
AsyncFunction[scene, Module[{d, loop},
d = af`AddTo[scene, {
Rotate[
Rectangle[{-0.5,-0.1}, {0.5,0.1}]
, #r]
}, {
"r" -> 0.
}];
loop = af`Loop[scene, d, "r", Function[{t, o, n},
Pi t + (n-1) Pi
], 0.5];
next // Await;
af`Remove[loop];
PauseAsync[scene, 0.5] // Await;
af`Remove[d];
]][s]
However, one can see that our animation has been interrupted somewhere mid-cycle, which may not be great for some cases. Using Finish method we can wait extra time until the cycle of animation has been finished:
AsyncFunction[scene, Module[{d, loop},
d = af`AddTo[scene, {
Rotate[
Rectangle[{-0.5,-0.1}, {0.5,0.1}]
, #r]
}, {
"r" -> 0.
}];
loop = af`Loop[scene, d, "r", Function[{t, o, n},
Pi t + (n-1) Pi
], 0.5];
next // Await;
af`Finish[loop] // Await; (* <---- HERE *)
af`Remove[loop];
PauseAsync[scene, 0.5] // Await;
af`Remove[d];
]][s]
Workers
Workers are like parallel threads - a more general and extended version of loops. It allows to execute an arbitrary code concurrently with the main animation on each frame:
af`Worker[scene_Scene, Function[absoluteTime,
...
]]
or
af`Worker[scene_Scene, AsyncFunction[absoluteTime,
...
]]
The last case is the most valuable. You can assign a sub-animation to a worker. Using Finish method on worker one can catch the moment when async sub-animation has been finished. Workers are running until they are manually removed using Remove method.
Taking a previous example, we can modify it slightly to take advantage of workers:
next = InputButton["Stop"]
AsyncFunction[scene, Module[{d, worker},
d = af`AddTo[scene, {
Rotate[
Rectangle[{-0.5,-0.1}, {0.5,0.1}]
, #r]
}, {
"r" -> 0.
}];
worker = af`Worker[scene, AsyncFunction[Null,
af`Animate[scene, d, "r"->Pi/2] // Await;
af`Animate[scene, d, "r"->-Pi/2] // Await;
]];
next // Await;
af`Finish[worker] // Await;
af`Remove[worker];
PauseAsync[scene, 0.5] // Await;
af`Remove[d];
]][s]
Here is an adapter example from Combining with Offload, that becomes much more compact and easier to read using workers:
Details
fakeWritting[from_, to_, vert_, scale_: 1, freq_: 50] :=
Line[
MovingAverage[
Table[
{
i,
vert + scale 0.065 Sin[freq i] + 0 RandomReal[scale 0.05 {-1, 1}]
},
{i, from, to, (to - from)/300.0}
],
10
]
];
slidersComponent[p1_, p2_] := {
White,
Rectangle[{-0.5, -0.5}, {0.5, 0.2}, RoundingRadius -> 2 0.14],
{
RGBColor[0.9, 0.9, 0.9],
Rectangle[{-0.3, -0.02}, {0.3, 0.02}, RoundingRadius -> 0.04],
RGBColor[0.4, 0.6, 0.9],
Disk[{0.25 p1, 0}, 0.05]
},
Translate[
{
RGBColor[0.9, 0.9, 0.9],
Rectangle[{-0.3, -0.02}, {0.3, 0.02}, RoundingRadius -> 0.04],
RGBColor[0.4, 0.6, 0.9],
Disk[{0.25 p2, 0}, 0.05]
},
{0, -0.3}
]
};
ClearAll[notebookModel]
notebookModel[size_, scale_, contentOpacity_, s_] :=
Scale[
SVGGroup[
{
Lighter[Black],
Text[Style["Notebook", FontSize -> 14], {0, 0.4}, {0, -1}],
RGBColor[0.93, 0.93, 0.90],
Rectangle[size {-1, -1}, size {1, 1}, RoundingRadius -> 0.13],
Opacity[contentOpacity],
SVGGroup[{
{
Translate[
Scale[
slidersComponent[Offload[s[[1]]], Offload[s[[2]]]],
0.2
],
{0.16015625, 0.16022949218750004}
]
},
Black // Lighter // Lighter,
{
AbsoluteThickness[2],
fakeWritting[-0.3, 0.05, 0.27, 0.4, 170/1.8]
},
Table[
fakeWritting[-0.3, 0.3, i, 0.1, 170],
{i, 0.13, 0.3 - 0.07, 0.04}
],
Table[
fakeWritting[-0.3, 0.3, i, 0.1, 170],
{i, -0.3 + 0.03, -0.1, 0.04}
]
}]
}
],
scale
];
AsyncFunction[scene, Module[{d, sliders = {0,0}, w},
d = af`AddTo[scene, {
Translate[notebookModel[0.33, 2.0, 1.0, sliders // Offload], #pos]
}, {
"pos"->{0,2.0},
"cop"->1.0
}];
af`Animate[scene, d, "pos"->{0,0}, "CubicInOut", 1] // Await;
w = af`Worker[scene, AsyncFunction[Null,
af`Animate[scene, Hold[sliders], RandomReal[{-1,1},2], "CubicInOut", 0.5] // Await;
]];
PauseAsync[scene, 4] // Await;
af`Remove[w];
af`Remove[d];
]][s];