Skip to content

Commit

Permalink
Large update on animation chapter + small notes on Bloc rendering loops
Browse files Browse the repository at this point in the history
  • Loading branch information
rvillemeur committed Sep 5, 2024
1 parent ff7d00f commit 4c791c5
Show file tree
Hide file tree
Showing 3 changed files with 169 additions and 209 deletions.
26 changes: 25 additions & 1 deletion Chapters/bloc/BlocInternals.md
Original file line number Diff line number Diff line change
Expand Up @@ -376,4 +376,28 @@ BlElement >> aeCompositionLayersSortedByElevationDo: aBlock
(each aeCompositionLayersSortedByElevationDo: aBlock) ]
```

Drawing is separated into layers.
Drawing is separated into layers.

Drawing is done on one instance of AeCanvas.
Alexandrie Canvas for bloc, which is not Alexandrie Canvas for Cairo.
You can still view the temporary result with `aeCanvas asForm`.

Start with BASpaceRenderer >> renderSpace: aBlSpace
aBlSpace aeFullDrawOn: aeCanvas.

BlElement >> aeFullDrawOn: aCanvas
BlElement >> aeDrawInSameLayerOn: aCanvas.
BlElement >> aeDrawOn: aeCanvas
BlElement >> aeDrawIgnoringOpacityAndTransformationOn: aeCanvas
BlElement >> aeDrawEffectBelowGeometryOn: aeCanvas.
BlElement >> aeDrawGeometryOn: aeCanvas.
BlElement >> aeDrawEffectAboveGeometryOn: aeCanvas.
BlElement >> aeDrawChildrenOn: aeCanvas. "Z-index: children sorted by elevation"
BlElement >> aeDrawInSameLayerOn: aCanvas "and recursive repeat"
BlElement >> aeCompositionLayersSortedByElevationDo: [ :each | each paintOn: aCanvas ].

BlElement >> aeCompositionLayersSortedByElevationDo: call
BAAxisAlignedCompositionLayer >> paintOn: aCanvas
BAAxisAlignedCompositionLayer>> ensureReadyToPaintOn: aCanvas
BlElement >> aeDrawAsSeparatedLayerOn: layerCanvas
BlElement >> aeDrawIgnoringOpacityAndTransformationOn: layerCanvas
282 changes: 144 additions & 138 deletions Chapters/bloc/animation.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,114 +7,185 @@ be decomposed into multiple **steps**. The total of each steps
composes a **loop** which can be repeated N time or **beInfinite**.

Animation can be started with a certain **delay**, and last for a
specific **duration**. Execution **progress** can be measured.
specific **duration**. Execution **progress** is measured as a normalized
number within [0..1] where:

- 0 means animation is not yet started.
- 1 animation loop is done

```text
Step Step Step Step step
|---------^---------^---------^---------^---------^---------| -> animation loop
0....................progress...............................1
```

The value is normalized from 0 to 1, reaching 1 at the end of animation duration.

When a step or the full loop is complete, animation
will raise `BlAnimationStepEvent` or `BlAnimationLoopDoneEvent`.

Animation is automatically started when added to an element.
Once stopped, an animation is considered as **complete**.
This simple animation will update the opacity of an element, indefinitely,
every 5 seconds, with a delay of 5 seconds between each loop.

```smalltalk
|element animation|
element := BlElement new size: 150 @ 150; background: Color red.
animation := BlAnimation new beInfinite; delay: 5 seconds; duration: 5 seconds.
### Lifetime
Animation is a subclass of `BlTask`, a kind of runnable. You can
define pre-computed activities through BlTask which has different
states.
animation addEventHandler: (BlEventHandler
on: BlAnimationLoopDoneEvent
do: [ :anEvent | element opacity: 0.0 .]).
Tasks go through the following steps:
animation addEventHandler: (BlEventHandler
on: BlAnimationStepEvent
do: [ :anEvent | element opacity: anEvent progress .]).
- new
- queued
- pendingExecution
- executing
- complete
element addAnimation: animation; openInNewSpace.
```

This example is quite limited and don't allow for much customization.
We'll see in the next section how you can define your own animation.

You can run multiple animations, in parallel or in sequence, which are
managed by *BlSequentialAnimation* or *BlParallelAnimation*

Tasks cannot be submitted twice, so you cannot add multiple time
the same animation to the same element.
Here is an example of using `BlSequentialAnimation`:

You can call **stop** to stop an animation. Restarting it is much
less obvious, as your animation will keep its current state.
```smalltalk
| space element translation scale sequential |
translation := (BlTransformAnimation translate: 300 @ 300)
easing: BlEasingElastic new ;
duration: 2 seconds.
scale := (BlTransformAnimation scale: 2 @ 2)
easing: BlEasingElastic new;
duration: 2 seconds.
sequential := BlSequentialAnimation new addAll: {
translation.
scale }.
element := BlElement new
background: Color blue;
size: 100 @ 100;
position: 100 @ 100.
element addAnimation: sequential.
space := BlSpace new.
space root addChild: element.
space show.
^space
```

**Important**
Animation is automatically started when added to an element.
Once stopped, an animation is considered as **complete**.

#### Restarting an animation.
You can call **stop** to stop a running animation.

To restart an animation, you'll have to do this in a specific order,
You cannot add multiple time an animation to an element. If you need to reapply
one, you can restart it. To do so, you'll have to do this in a specific order,
as: `animation reset; start; setNew; enqueue`

- reset will, well, reset the animation internal state.
- start will tell the animation it can start. This is not enough, we also need to enqueue it into BlElement task queue. As you cannot add the same task twice, you need to tell it's new.
- setNew will set the *BlTask* state to #new.
- enqueue will re-enqueue your animation into BlElement task queue.
### Creating your own animation

The base *BlAnimation* give you the basic element for animation, and you can
use it as a base to create more complex animation.

The entry point will be *BlAnimation >> doStep*, which is called at every step.
Let's look at its default implementation:

`self applyValue: (self valueForStep: (easing interpolate: progress))`

### Bloc animation & Task
1. We already know progress is between 0 and 1.
2. Progress value is changed by the easing function.
3. For each steps, the easing value is used to update transformation state.
4. This state is then applied to our target element.

- steps
- loops: the number of loops to execute an animation
- delay: how much time to postpone the actual start after an animation is added
- duration: how much time the animation will last for each step (start time + delay)
- event raised when step is done or loop is done.
the *progress* value can be interpolated by the result of `BlEasing` selected class.
BlEasing represents a mathematical function that describes the rate at which
a value changes. The transition between 0 and 1 may be used to describe how fast
values change during animations. This lets you vary the animation's speed over
the course of its duration.

When animation run, it'll call the `step` which in turn will call the `doStep`
When one step is done, it'll fire the `BlAnimationStepEvent` event.
When an entire loop animation is done, it'll fire the `BlAnimationLoopDoneEvent`event.
Pharo provide those easing function:

Step can be decomposed into multiple sub-step. All those sub-step
comprise the animation loop, which can be repeated multiple time of indefinitely.
- linear (default - BlLinearInterpolator)
- Bounce In (BlEasingBounceIn)
- Bounce Out (BlEasingBounceOut)
- Bounce In Out (BlEasingBounceInOut)
- Elastic (BlEasingElastic)
- Quad (BlEasingQuad)
- Quintic (BlQuinticInterpolator)
- Sine (BlSineInterpolator)
- Viscous Fluid (BlViscousFluidInterpolator)

At every pulse, `doStep` is called. Because of that, you can't
compute any new state during a step. You either have to pre-compute
it, or react to `BlAnimationStepEvent` to get new state.
Other easing function can be implemented easily. As example, look at this page:
easing function: https://easings.net/. You need to have an object which implement
the `interpolate: aNumber` method, aNumber being the *progress* of our animation.
Adding new easing function is left as an exercise to the reader.

You can use pre-defined animation class, or create your own animation
by subclassing `BlAnimation` and overwrite `valueForStep:`
Let's implement our own animation, where we want to rotate an element

We first define our animation class as a subclass of BlAnimation.

```smalltalk
BlAnimation << #BlRotateAnimation
slots: { #angle };
tag: 'Animation';
package: 'BookletGraphics'
BlRotateAnimation >> angle: anAngle
angle := anAngle
```
valueForStep:` receive a progress number:
"a normalized number within [0..1] representing animation progress.
0 - means animation is not yet started.
1 - animation loop is done"

the progress value is the result of `BlEasing`selected class, which
provides different mathematical function to go from 0 to 1
At every step, we need to compute the angle reached by transformation

progress := (elapsedTime / self duration) asFloat
BlEasing: Math function taking progress as argument to show different animation style
self applyValue: (self valueForStep: (easing interpolate: progress))
```smalltalk
BlRotateAnimation >> valueForStep: aNumber
^ (angle * aNumber)
```

"Execute an actual animation step. My subclasses define this hook, and assume it's executed after my internal state has been updated, for example, progress."
Last we need to apply the result of our step to our element

```smalltalk
BlRotateAnimation >> applyValue: anAngle
self target transformDo: [ :t | t rotateBy: anAngle ]
```

Execution is done by *steps*:
you can then use it like:

```smalltalk
animatedBackground
| element animation |
element := BlElement new size: 50@50.
animation := BlNumberTransition new
from: 0;
to: 1;
by: 0.5;
beInfinite;
duration: 3 seconds;
onStepDo: [ :aValue :anElement |
aValue < 0.5
ifTrue: [ anElement background: Color red ]
ifFalse: [ anElement background: Color blue ] ].
element addAnimation: animation.
| elt frame container anim |
elt := BlElement new background: (Color red alpha: 0.5); position: 100 asPoint; size: 100 asPoint.
frame := BlElement new background: Color yellow; position: 100 asPoint; size: 100 asPoint.
container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: {frame. elt}.
anim := BlRotateAnimation new angle: 90; duration: 1 second.
elt addEventHandlerOn: BlClickEvent do: [ elt addAnimation: anim copy ].
container openInSpace
```

### Pre-defined animations

While you can define your own animation, Pharo comes with different pre-defined animation
you should know instead of reinventing the wheel. Here are the different option
readily available for use.

#### Gaussian Effect opacity animation.

Apply a gaussian blur effect with opacity on `BlElement`:

```smalltak
BlGaussianEffectOpacityAnimation new
delay: 1 second;
color: Color red;
width: 25;
opacity: 0.9;
duration: 300 milliSeconds.
delay: 1 second;
color: Color red;
width: 25;
opacity: 0.9;
duration: 300 milliSeconds.
```

#### Opacity animation.
Expand All @@ -123,9 +194,9 @@ Update the opacity of the BlElement from its initial value to specified opacity.

```smalltalk
BlOpacityAnimation new
delay: 1 second;
opacity: 0.1;
duration: 300 milliSeconds.
delay: 1 second;
opacity: 0.1;
duration: 300 milliSeconds.
```

### Transform animation
Expand All @@ -149,18 +220,6 @@ BlTransformAnimation new
easing: BlEasing bounceOut.
```

You can have multiple easing classes. Have a look at:

- `BlViscousFluidInterpolator`
- `BlSineInterpolator`
- `BlQuinticInterpolator`
- `BlLinearInterpolator`
- `BlEasingQuad`
- `BlEasingElastic`
- `BlEasingBounceIn`
- `BlEasingBounceOut`
- `BlEasingBounceInOut`

### Color transition

Transition from one color to another
Expand Down Expand Up @@ -195,57 +254,4 @@ BlNumberTransition new
ifFalse: [ anElement background: Color blue ] ].
```

### Animation composition

You can run multiple animations, in parallel or in sequence.

In `BlAnimationExamplesTest class >> ballsAnim` shows two transformations are applied
in parallel to multiples balls:

- position
- color

In `BlAnimationExamplesTest class >> sequential` two transformations are applied
sequencially to an element:

- position
- scale

### A simple rotation

A custom animation for element rotation.

```smalltalk
BlAnimation << #BlRotateAnimation
slots: { #angle };
tag: 'Animation';
package: 'BookletGraphics'
BlRotateAnimation >> angle: anAngle
angle := anAngle
BlRotateAnimation >> applyValue: anAngle
self target transformDo: [ :t | t rotateBy: anAngle ]
BlRotateAnimation >> valueForStep: aNumber
^ (angle * aNumber)
```

you can then use it like:

```smalltalk
| elt frame container anim |
elt := BlElement new background: (Color red alpha: 0.5); position: 100 asPoint; size: 100 asPoint.
frame := BlElement new background: Color yellow; position: 100 asPoint; size: 100 asPoint.
container := BlElement new background: Color lightGreen; size: 500 asPoint; addChildren: {frame. elt}.
anim := BlRotateAnimation new angle: 90; duration: 1 second.
elt addEventHandlerOn: BlClickEvent do: [ elt addAnimation: anim copy ].
container openInSpace
```



### Conclusion (missing)
### Conclusion (missing)
Loading

0 comments on commit 4c791c5

Please sign in to comment.