Appearance
Animation
CanvasEngine provides powerful tools for creating animations. At the core of the animation system are animatedSignal
and animatedSequence
.
animatedSignal
An animatedSignal
is a special type of signal whose changes can be animated over time. It's built on top of the reactive system and integrates with popmotion
for the animation logic.
Creation
You create an animatedSignal
by providing an initial value and optional animation options:
html
<script>
import { animatedSignal } from 'canvasengine';
const opacity = animatedSignal(1, { duration: 500 }); // Initial opacity 1, animates over 500ms
const positionX = animatedSignal(0, { duration: 1000, ease: (t) => t * t }); // Animates x and y
const click = () => {
opacity.set(0)
positionX.set(100)
}
</script>
<Rect width={300} height={300} color="green" x={positionX} click alpha={opacity} />
Updating Value
You can update the value of an animatedSignal
in two ways:
set(newValue, options?)
: This method animates the signal from its current value tonewValue
. It returns a Promise that resolves when the animation is complete. You can optionally provide animation options specific to this transition.html<script> import { animatedSignal } from 'canvasengine'; const opacity = animatedSignal(1, { duration: 500 }); const positionX = animatedSignal(0, { duration: 1000 }); // Example of using set async function animateElement() { await opacity.set(0.5); // Fades to 50% await positionX.set(100, { duration: 2000 }); // Moves to 100 over 2 seconds } // Call it or attach to an event // animateElement(); </script> <Rect width="100" height="100" color="blue" alpha={opacity} x={positionX} click={animateElement} />
update(updaterFn)
: This method takes a function that receives the current value and should return the new value. The transition to the new value will be animated using the default or previously set animation options for the signal.html<script> import { animatedSignal } from 'canvasengine'; const scale = animatedSignal(1, { duration: 300 }); const rotation = animatedSignal(0, { duration: 500 }); // Example of using update function modifyElement() { scale.update(currentScale => currentScale * 1.2); // Scales up by 20% rotation.update(currentRotation => currentRotation + 45); // Rotates by 45 degrees } // Call it or attach to an event // modifyElement(); </script> <Rect width="100" height="100" color="red" scale={scale} rotation={rotation} click={modifyElement} />
Accessing Value
To get the current value of an animatedSignal
, you call it as a function:
html
<script>
import { animatedSignal, effect } from 'canvasengine';
const rectOpacity = animatedSignal(1);
const rectPositionX = animatedSignal(50);
effect(() => {
// Logs the current opacity value whenever it changes
console.log('rectOpacity:', rectOpacity());
});
effect(() => {
// Logs the current x position whenever it changes
console.log('rectPositionX:', rectPositionX());
});
const toggleValues = () => {
if (rectOpacity() === 1) {
rectOpacity.set(0.5);
rectPositionX.set(150);
} else {
rectOpacity.set(1);
rectPositionX.set(50);
}
};
</script>
<Rect width="80" height="80" color="purple" alpha={rectOpacity} x={rectPositionX} click={toggleValues} />
Animation State
Each animatedSignal
has an animatedState
property. This is a WritableSignal
that holds an object with the following properties:
current
: The current value of the signal during an animation.start
: The value at the beginning of the current or last animation.end
: The target value of the current or last animation.
You can subscribe to this state to react to changes during an animation:
html
<script>
import { animatedSignal, effect } from 'canvasengine';
const progressWidth = animatedSignal(0, { duration: 2000 });
let currentProgress = 0;
effect(() => {
const state = progressWidth.animatedState();
currentProgress = state.current;
console.log(`Animation progress: from ${state.start} to ${state.end}, current is ${state.current}`);
// Here currentProgress could be used to set the width of a visual element
});
const startProgressAnimation = () => {
progressWidth.set(100);
};
const resetProgressAnimation = () => {
progressWidth.set(0);
}
const handleProgressClick = () => {
if (progressWidth() === 0) {
startProgressAnimation();
} else {
resetProgressAnimation();
}
}
</script>
<!-- Conceptual Rect acting as a progress bar -->
<Rect width={progressWidth} height="20" color="orange" click={handleProgressClick} />
<!-- Text to display progress -->
<Text text={`Progress: ${progressWidth()}%`} x="10" y="40" />
Example
html
<script>
import { animatedSignal, effect } from 'canvasengine';
const xPosition = animatedSignal(10, { duration: 1000 });
const boxOpacity = animatedSignal(1, { duration: 750 });
const boxColor = signal('blue'); // Non-animated signal for color
// Log the value whenever it changes
effect(() => {
console.log('xPosition is now:', xPosition());
});
effect(() => {
console.log('boxOpacity is now:', boxOpacity());
});
// Log the detailed animation state for xPosition
effect(() => {
const animState = xPosition.animatedState();
console.log(
`xPosition Animation: from ${animState.start} to ${animState.end}. Current: ${animState.current}`
);
});
async function animateMyBox() {
console.log('Starting animation for MyBox...');
boxColor.set('red'); // Change color immediately
// Parallel animation using Promise.all
await Promise.all([
xPosition.set(150), // Move right
boxOpacity.set(0.3) // Fade out a bit
]);
console.log('Animation to x:150, opacity:0.3 finished.');
boxColor.set('green');
await xPosition.set(10, { duration: 500 }); // Animate back faster
console.log('Animation back to x:10 finished.');
boxColor.set('blue');
boxOpacity.set(1); // Fade back in, default duration
xPosition.update(val => val + 70); // Animate to 80 using default duration (1000ms)
console.log('Fade in and x-update to 80 initiated.');
}
</script>
<Rect x={xPosition} y="50" width="50" height="50" color={boxColor} alpha={boxOpacity} click={animateMyBox} />
animatedSequence
The animatedSequence
function allows you to orchestrate multiple animations, running them sequentially or in parallel. This is particularly useful for creating complex animation timelines.
How it Works
animatedSequence
takes an array as its argument. Each element in this array can be either:
- A function that returns a
Promise
(typically ananimatedSignal.set()
call). These functions are executed one after another (sequentially). - An array of such promise-returning functions. All functions within this inner array are executed simultaneously (in parallel). The sequence will only proceed to the next step once all promises in the parallel block have resolved.
Usage
html
<script>
import { animatedSignal, animatedSequence, signal } from 'canvasengine';
// Define some animated signals for our Rects
const rect1X = animatedSignal(10, { duration: 500 });
const rect2Y = animatedSignal(10, { duration: 700 });
const rect3Scale = animatedSignal(1, { duration: 300 });
const rectsVisible = signal(true);
async function runRectSequence() {
console.log('Starting Rect animation sequence...');
rectsVisible.set(true);
await animatedSequence([
// Step 1: Animate rect1X to 100
() => rect1X.set(100),
// Step 2: Animate rect2Y to 50 and rect3Scale to 1.5 in parallel
[
() => rect2Y.set(50),
() => rect3Scale.set(1.5)
],
// Step 3: Animate rect1X back to 10
() => rect1X.set(10),
// Step 4: Animate all three values simultaneously to new targets
[
() => rect1X.set(30),
() => rect2Y.set(20),
() => rect3Scale.set(0.8)
],
// Step 5: Hide the rects (not animated, but part of sequence)
async () => {
// Example of a non-animated action within the sequence
await Promise.all([rect1X.set(200, {duration: 300}), rect2Y.set(200, {duration: 300}), rect3Scale.set(0.1, {duration: 300})]);
rectsVisible.set(false);
}
]);
console.log('Rect sequence complete!');
// console.log(`Final values: rect1X=${rect1X()}, rect2Y=${rect2Y()}, rect3Scale=${rect3Scale()}`);
}
const resetRects = () => {
rect1X.set(10, {duration: 0});
rect2Y.set(10, {duration: 0});
rect3Scale.set(1, {duration: 0});
rectsVisible.set(true);
}
</script>
Key Features:
- Sequential Execution: Animations in the main array are performed one by one.
- Parallel Execution: Animations within a nested array are performed concurrently.
- Promise-based: It leverages Promises to manage the timing and completion of animations. The
animatedSequence
function itself returns a Promise that resolves when the entire sequence is finished.
This structure provides a flexible way to define intricate animation chains.