From 4c791c5df87152373d913857054de21ed4540dc7 Mon Sep 17 00:00:00 2001 From: Renaud de Villemeur Date: Thu, 5 Sep 2024 16:19:30 -0400 Subject: [PATCH] Large update on animation chapter + small notes on Bloc rendering loops --- Chapters/bloc/BlocInternals.md | 26 ++- Chapters/bloc/animation.md | 282 ++++++++++++++++---------------- Chapters/bloc/animationNotes.md | 70 -------- 3 files changed, 169 insertions(+), 209 deletions(-) delete mode 100644 Chapters/bloc/animationNotes.md diff --git a/Chapters/bloc/BlocInternals.md b/Chapters/bloc/BlocInternals.md index c340063..88a56bf 100644 --- a/Chapters/bloc/BlocInternals.md +++ b/Chapters/bloc/BlocInternals.md @@ -376,4 +376,28 @@ BlElement >> aeCompositionLayersSortedByElevationDo: aBlock (each aeCompositionLayersSortedByElevationDo: aBlock) ] ``` -Drawing is separated into layers. \ No newline at end of file +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 \ No newline at end of file diff --git a/Chapters/bloc/animation.md b/Chapters/bloc/animation.md index eeb6c22..767308f 100644 --- a/Chapters/bloc/animation.md +++ b/Chapters/bloc/animation.md @@ -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. @@ -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 @@ -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 @@ -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) \ No newline at end of file +### Conclusion (missing) diff --git a/Chapters/bloc/animationNotes.md b/Chapters/bloc/animationNotes.md deleted file mode 100644 index 478ab63..0000000 --- a/Chapters/bloc/animationNotes.md +++ /dev/null @@ -1,70 +0,0 @@ - - -Here is how I currently understand how animation works. - -An animation is a loop that can be repeated 0 to infinite times, during a specific duration, that can be splitted into steps, - - Step Step Step Step Step -|---------^---------^---------^---------^---------^------| animation loop - -When your animation loop end, It'll emit the BlAnimationLoopDoneEvent event. -At each step, it'll emit the BlAnimationStepEvent event. - -Here, an infinite animation, which loop every 1 second. -```smalltalk -animation := BlAnimation new - beInfinite; - duration: 1 seconds. - -animation addEventHandler: (BlEventHandler - on: BlAnimationLoopDoneEvent - do: [ :anEvent | self inform: 'loop done'. self dieValue: (1 to: faces) atRandom ]). -``` - -Each step is called at every BlUniverse pulse, as shown in this example: The random color is updated at every universe pulse, which surprised me at first. - -animation := BlNumberTransition new - from: 0; - to: 1; - by: 0.5; - loops: 5; - duration: 3 seconds; -"onStepDo: is called on every space pulse. Color are updated at each pulse..." - onStepDo: [ :aValue :anElement | - aValue < 0.5 - ifTrue: [ anElement background: Color random ] - ifFalse: [ anElement background: Color blue ] ]. - -You have various BlAnimation subclasses in the image, like BlColorTransition or BlNumberTransition to help transition between colors and a range of number. You also have BlTransformAnimation to help transform a BlElement. The mathematical effect for the transition is given by a subclass of BlEasing - -For a single element, you can apply multiple sequential animations with BlSequentialAnimation . -You can also apply the same animation to multiple elements using BlParallelAnimation - -```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 -``` - -Hope this helps. -Renaud \ No newline at end of file