fsniraj
  • Blogs
  • Courses
  • Account
  • Blogs
  • Courses
  • Account
Privacy PolicyTerms of Service

© 2024 Full Stack Niraj. All Rights Reserved.

MotionCanvas Core Concepts - Beginner Guide

Niraj Dhungana
Niraj Dhungana•July 29, 2025
Share:
MotionCanvas Core Concepts - Beginner Guide

This guide explains the essential MotionCanvas functions used in animations, with simple examples and practical usage.

Table of Contents

  • createRef
  • createSignal
  • createComputed
  • loop
  • useRandom
  • useThread
  • waitFor
  • waitUntil

createRef - Reference to Components

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.

Basic Usage

typescript
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

When to Use

  • When you need to animate or modify a component after creating it
  • When you want to get information from a component (like position, size)
  • When you need to reference one component from another

Real Example

typescript
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

createSignal - Reactive Content

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.

Basic Usage

typescript
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

Animated Updates

typescript
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

Typing Animation Example

typescript
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
}

createComputed - Computed Values

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.

Basic Usage

typescript
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)

Cursor Positioning Example

typescript
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"
  />
);

When to Use

  • When you need a value that depends on other changing values
  • To avoid recalculating expensive operations every frame
  • When multiple components need the same calculated value

loop - Continuous Animations

What it does: Runs code continuously in the background.

Think of it like: A timer that keeps running while other things happen.

Basic Usage

typescript
import { loop } from '@motion-canvas/core';

// Simple continuous animation
yield loop(() => {
  // This runs every frame
  myRect().rotation(myRect().rotation() + 1);
});

Blinking Cursor Example

typescript
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;
  }
});

Controlled Loop

typescript
const isAnimating = createSignal(true);

yield loop(() => {
  if (isAnimating()) {
    myCircle().rotation(myCircle().rotation() + 2);
  }
});

// Stop the animation later
isAnimating(false);

Common Use Cases

  • Blinking cursors
  • Rotating objects
  • Breathing/pulsing effects
  • Background animations that run during other actions

useRandom - Random Values

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.

Basic Usage

typescript
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);

Realistic Typing Delays

typescript
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);
}

Particle System Example

typescript
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)}
    />
  );
}

useThread - Timing Control

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.

Basic Usage

typescript
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();

Time-Based Animation

typescript
const startTime = useThread().time();

yield loop(() => {
  const elapsed = useThread().time() - startTime;
  myRect().rotation(elapsed * 90);  // 90 degrees per second
});

Blinking Timer

typescript
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;
  }
});

waitFor - Delays

What it does: Pauses the animation for a specific amount of time.

Think of it like: A sleep command that waits before continuing.

Basic Usage

typescript
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);

Typing Animation

typescript
const message = "Hello!";
const typedText = createSignal('');

for (const char of message) {
  typedText(typedText() + char);
  yield* waitFor(0.1);  // Wait 0.1 seconds between characters
}

Step-by-Step Animation

typescript
// 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);

Variable Delays

typescript
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));
}

waitUntil - Scene Synchronization

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.

How Markers Work

Markers are labels you place in your timeline to synchronize different parts of your animation.

typescript
// 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();

Where Do Markers Come From?

  1. Timeline in MotionCanvas Editor: You can add markers visually in the timeline
  2. Code: You can create them programmatically
  3. Audio synchronization: Markers can sync with music beats or speech

Practical Example

typescript
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);
});

Choosing Marker Names

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:

  • Use descriptive names that explain what happens
  • Use consistent naming patterns
  • Group related markers with prefixes (slide1_start, slide1_end)

Common Pattern

typescript
// Typical scene structure
yield* waitUntil('scene_start');
// Setup animations

yield* waitUntil('main_action');  
// Main content

yield* waitUntil('scene_end');
// Cleanup/transition

Putting It All Together

Here's how these concepts work together in a complete typing animation:

typescript
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.