This guide explains the essential MotionCanvas functions used in animations, with simple examples and practical usage.
What it does: Creates a reference to access and control components in your scene.
Think of it like: A remote control for your TV - you need it to change channels, volume, etc.
import { createRef } from '@motion-canvas/core';
import { Rect, Txt } from '@motion-canvas/2d';
// Create the reference
const myRect = createRef<Rect>();
const myText = createRef<Txt>();
// Use it in JSX with ref={myRect}
view.add(
<>
<Rect ref={myRect} width={100} height={100} fill="red" />
<Txt ref={myText} text="Hello" fontSize={48} />
</>
);
// Now you can control them
myRect().fill('blue'); // Change color
myText().text('World'); // Change text
myRect().rotation(45); // Rotate rectangle
const button = createRef<Rect>();
// Create button
view.add(<Rect ref={button} width={200} height={60} fill="green" />);
// Animate it later
yield* button().scale(1.2, 0.5); // Make it bigger
yield* button().fill('red', 1.0); // Change color over 1 second
What it does: Creates a value that automatically updates the UI when changed.
Think of it like: A smart variable that tells everyone when it changes.
import { createSignal } from '@motion-canvas/core';
// Create a signal with initial value
const counter = createSignal(0);
const message = createSignal('Hello');
const isVisible = createSignal(true);
// Use in components
view.add(
<Txt
text={counter} // Will show current counter value
opacity={() => isVisible() ? 1 : 0} // Changes when isVisible changes
/>
);
// Update the signal (UI updates automatically)
counter(10); // Now shows "10"
message('Goodbye'); // Text changes to "Goodbye"
isVisible(false); // Component becomes invisible
const progress = createSignal(0);
view.add(
<Rect
width={() => progress() * 400} // Width changes with progress
height={50}
fill="blue"
/>
);
// Animate the signal
yield* progress(100, 2.0); // Go from 0 to 100 over 2 seconds
const typedText = createSignal('');
view.add(<Txt text={typedText} fontSize={32} />);
// Type letter by letter
const fullText = "Hello World!";
for (const char of fullText) {
typedText(typedText() + char); // Add one character
yield* waitFor(0.1); // Wait before next character
}
What it does: Creates a value that automatically recalculates when its dependencies change.
Think of it like: A formula in Excel that updates when you change the input cells.
import { createComputed } from '@motion-canvas/core';
const width = createSignal(100);
const height = createSignal(50);
// Computed value that depends on width and height
const area = createComputed(() => width() * height());
console.log(area()); // 5000
// When width changes, area automatically updates
width(200);
console.log(area()); // 10000 (automatically recalculated)
const codeRef = createRef<Code>();
const cursorPosition = createSignal<CodePoint>([0, 0]);
// Computed cursor bounding box
const cursorBBox = createComputed(() =>
codeRef().getPointBBox(cursorPosition())
);
// Cursor automatically follows the computed position
view.add(
<Rect
width={3}
height={30}
position={() => cursorBBox().center} // Updates when cursor moves
fill="white"
/>
);
What it does: Runs code continuously in the background.
Think of it like: A timer that keeps running while other things happen.
import { loop } from '@motion-canvas/core';
// Simple continuous animation
yield loop(() => {
// This runs every frame
myRect().rotation(myRect().rotation() + 1);
});
const cursor = createRef<Rect>();
const time = useThread().time;
let lastBlinkTime = time();
// Start blinking loop
yield loop(() => {
const now = time();
if (now - lastBlinkTime > 0.5) { // Every 0.5 seconds
cursor().opacity(1 - cursor().opacity()); // Toggle visibility
lastBlinkTime = now;
}
});
const isAnimating = createSignal(true);
yield loop(() => {
if (isAnimating()) {
myCircle().rotation(myCircle().rotation() + 2);
}
});
// Stop the animation later
isAnimating(false);
What it does: Provides consistent random numbers for your animation.
Think of it like: A dice that gives you random numbers, but the same sequence every time you restart.
import { useRandom } from '@motion-canvas/core';
const random = useRandom();
// Random float between 0 and 1
const randomValue = random.nextFloat();
// Random float in a range
const speed = random.nextFloat(0.5, 2.0); // Between 0.5 and 2.0
// Random integer
const diceRoll = random.nextInt(1, 7); // 1 to 6
// Random from array
const colors = ['red', 'blue', 'green'];
const randomColor = random.nextChoice(colors);
const random = useRandom();
const textToType = "Hello World!";
for (const char of textToType) {
typedText(typedText() + char);
// Random delay between characters (feels more human)
const delay = char === ' ' ?
random.nextFloat(0.02, 0.05) : // Spaces are quick
random.nextFloat(0.05, 0.15); // Letters take longer
yield* waitFor(delay);
}
const random = useRandom();
// Create 10 particles with random properties
for (let i = 0; i < 10; i++) {
const particle = createRef<Circle>();
view.add(
<Circle
ref={particle}
size={random.nextFloat(10, 30)} // Random size
fill={random.nextChoice(['red', 'blue'])} // Random color
x={random.nextFloat(-400, 400)} // Random position
y={random.nextFloat(-300, 300)}
/>
);
}
What it does: Gives you access to timing information and control.
Think of it like: A stopwatch that tells you how much time has passed.
import { useThread } from '@motion-canvas/core';
const thread = useThread();
// Get current time
const currentTime = thread.time();
// Get time since last frame
const deltaTime = thread.deltaTime();
const startTime = useThread().time();
yield loop(() => {
const elapsed = useThread().time() - startTime;
myRect().rotation(elapsed * 90); // 90 degrees per second
});
const time = useThread().time;
let lastBlink = time();
yield loop(() => {
const now = time();
if (now - lastBlink > 0.5) { // Blink every 0.5 seconds
cursor().opacity(1 - cursor().opacity());
lastBlink = now;
}
});
What it does: Pauses the animation for a specific amount of time.
Think of it like: A sleep command that waits before continuing.
import { waitFor } from '@motion-canvas/core';
// Wait for 1 second
yield* waitFor(1.0);
// Wait for half a second
yield* waitFor(0.5);
// Wait for 2.5 seconds
yield* waitFor(2.5);
const message = "Hello!";
const typedText = createSignal('');
for (const char of message) {
typedText(typedText() + char);
yield* waitFor(0.1); // Wait 0.1 seconds between characters
}
// Show title
title().opacity(1, 0.5);
yield* waitFor(1.0); // Wait 1 second
// Show subtitle
subtitle().opacity(1, 0.5);
yield* waitFor(0.5); // Wait 0.5 seconds
// Start main animation
yield* mainContent().scale(1, 1.0);
const random = useRandom();
for (let i = 0; i < 5; i++) {
console.log(`Step ${i + 1}`);
// Random delay between 0.2 and 0.8 seconds
yield* waitFor(random.nextFloat(0.2, 0.8));
}
What it does: Wait for a specific marker/cue in your timeline.
Think of it like: A conductor's baton - wait for the cue to start your part.
Markers are labels you place in your timeline to synchronize different parts of your animation.
// In your scene
yield* waitUntil('intro_start'); // Wait for 'intro_start' marker
yield* titleAnimation();
yield* waitUntil('main_content'); // Wait for 'main_content' marker
yield* showMainContent();
yield* waitUntil('conclusion'); // Wait for 'conclusion' marker
yield* concludeAnimation();
export default makeScene2D(function* (view) {
const title = createRef<Txt>();
const code = createRef<Code>();
view.add(
<>
<Txt ref={title} text="Welcome!" fontSize={48} opacity={0} />
<Code ref={code} code="" opacity={0} y={100} />
</>
);
// Wait for the "show_title" marker in timeline
yield* waitUntil('show_title');
yield* title().opacity(1, 0.5);
// Wait for the "start_typing" marker
yield* waitUntil('start_typing');
yield* code().opacity(1, 0.3);
// Start typing animation
const codeText = createSignal('');
code().code(codeText);
const textToType = "console.log('Hello!');";
for (const char of textToType) {
codeText(codeText() + char);
yield* waitFor(0.1);
}
// Wait for "end_scene" marker
yield* waitUntil('end_scene');
yield* title().opacity(0, 0.5);
yield* code().opacity(0, 0.5);
});
Good marker names:
intro_start
, intro_end
show_title
, hide_title
start_typing
, typing_done
reveal_answer
, next_slide
beat_drop
, chorus_start
(for music sync)Tips:
slide1_start
, slide1_end
)// Typical scene structure
yield* waitUntil('scene_start');
// Setup animations
yield* waitUntil('main_action');
// Main content
yield* waitUntil('scene_end');
// Cleanup/transition
Here's how these concepts work together in a complete typing animation:
export default makeScene2D(function* (view) {
// 1. Create references and signals
const codeRef = createRef<Code>();
const cursorRef = createRef<Rect>();
const codeText = createSignal('');
const cursorPos = createSignal<CodePoint>([0, 0]);
// 2. Create computed values
const cursorBBox = createComputed(() =>
codeRef().getPointBBox(cursorPos())
);
// 3. Add to view
view.add(
<>
<Code ref={codeRef} code={codeText} />
<Rect
ref={cursorRef}
position={() => cursorBBox().center}
width={3} height={30} fill="white"
/>
</>
);
// 4. Start cursor blinking loop
const time = useThread().time;
let lastBlink = time();
yield loop(() => {
const now = time();
if (now - lastBlink > 0.5) {
cursorRef().opacity(1 - cursorRef().opacity());
lastBlink = now;
}
});
// 5. Wait for synchronization point
yield* waitUntil('start_typing');
// 6. Type with random delays
const random = useRandom();
const textToType = "console.log('Hello World!');";
let line = 0, col = 0;
for (const char of textToType) {
codeText(codeText() + char);
if (char === '\n') {
line++;
col = 0;
} else {
col++;
}
cursorPos([line, col]);
// 7. Wait with random delay
yield* waitFor(random.nextFloat(0.05, 0.15));
}
// 8. Wait for next synchronization point
yield* waitUntil('typing_done');
});
This example shows all concepts working together to create a realistic typing animation with proper synchronization and timing control.