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

© 2024 Full Stack Niraj. All Rights Reserved.

MotionCanvas Typing Animation Complete Guide

Niraj Dhungana
Niraj Dhungana•July 29, 2025
Share:
MotionCanvas Typing Animation Complete Guide

This guide will teach you how to create realistic typing animations in MotionCanvas, from basic text typing to advanced code editors with cursors and syntax highlighting.

Table of Contents

  • Basic Concepts
  • Simple Text Typing
  • Code Typing with Cursor
  • Advanced Features
  • Real-World Examples
  • Customization Options
  • Tips and Best Practices

Basic Concepts

What You Need

typescript
import {
  Code,          // For syntax-highlighted code
  Txt,           // For plain text
  CodePoint,     // For cursor positioning [line, column]
  makeScene2D,
  Rect,          // For the cursor rectangle
} from '@motion-canvas/2d';

import {
  createRef,     // Reference to components
  createSignal,  // Reactive text content
  createComputed,// Computed cursor position
  loop,          // For cursor blinking
  useRandom,     // Random typing delays
  useThread,     // For timing
  waitFor,       // Delays between characters
  waitUntil,     // Scene synchronization
} from '@motion-canvas/core';

Key Components

  • Signal: Holds the current text being typed
  • CodePoint: [line, column] position for cursor tracking
  • getPointBBox(): Gets bounding box for cursor positioning
  • Random delays: Makes typing feel human-like

Simple Text Typing

Basic Text Animation

typescript
export default makeScene2D(function* (view) {
  const textSignal = createSignal('');
  const textToType = "Hello, World!";

  view.add(
    <Txt
      fontSize={48}
      fill="white"
      text={textSignal}
    />
  );

  yield* waitUntil('start');

  const random = useRandom();
  for (const char of textToType) {
    textSignal(textSignal() + char);
    yield* waitFor(random.nextFloat(0.05, 0.15));
  }
});

Multi-line Text with Pauses

typescript
const textToType = `Welcome to MotionCanvas!
This is line two.
And this is the final line.`;

// In your animation loop:
for (const char of textToType) {
  textSignal(textSignal() + char);

  const delay = char === '\n' ? 
    random.nextFloat(0.3, 0.8) :  // Longer pause for new lines
    char === '.' ? 
    random.nextFloat(0.2, 0.5) :  // Pause after sentences
    random.nextFloat(0.05, 0.15); // Normal typing speed

  yield* waitFor(delay);
}

Code Typing with Cursor

Complete Setup

typescript
export default makeScene2D(function* (view) {
  view.fill('#1a1a1a');

  // Refs and signals
  const codeRef = createRef<Code>();
  const cursorRef = createRef<Rect>();
  const codeText = createSignal('');
  const cursorPosition = createSignal<CodePoint>([0, 0]);

  // Cursor positioning
  const cursorBBox = createComputed(() => 
    codeRef().getPointBBox(cursorPosition())
  );

  // Add components to view
  view.add(
    <>
      <Code
        ref={codeRef}
        fontSize={24}
        fontFamily="'Courier New', monospace"
        fill="white"
        code={codeText}
      />
      <Rect
        ref={cursorRef}
        width={3}
        height={30}
        fill="#00ff00"
        position={() => cursorBBox().center}
      />
    </>
  );

  // Blinking cursor
  const time = useThread().time;
  let lastBlinkTime = time();

  yield loop(() => {
    const now = time();
    if (now - lastBlinkTime > 0.5) {
      cursorRef().opacity(1 - cursorRef().opacity());
      lastBlinkTime = now;
    }
  });

  function resetCursor() {
    lastBlinkTime = time();
    cursorRef().opacity(1);
  }

  // Your typing animation here...
});

Typing Logic

typescript
const textToType = `function hello() {
  console.log("Hello!");
}`;

yield* waitUntil('start_typing');

const random = useRandom();
let currentLine = 0;
let currentColumn = 0;

for (const char of textToType) {
  codeText(codeText() + char);

  // Update cursor position
  if (char === '\n') {
    currentLine++;
    currentColumn = 0;
  } else {
    currentColumn++;
  }

  cursorPosition([currentLine, currentColumn]);
  resetCursor();

  // Variable delays for realism
  const delay = char === ' ' ? 
    random.nextFloat(0.02, 0.05) : 
    char === '\n' ?
    random.nextFloat(0.1, 0.3) :
    random.nextFloat(0.05, 0.15);

  yield* waitFor(delay);
}

Advanced Features

Backspacing Animation

typescript
function* backspaceText(count: number) {
  const random = useRandom();

  for (let i = 0; i < count; i++) {
    const current = codeText();
    const lastChar = current[current.length - 1];

    // Remove character
    codeText(current.slice(0, -1));

    // Update cursor position
    if (lastChar === '\n') {
      currentLine--;
      // Calculate column position of previous line
      const lines = codeText().split('\n');
      currentColumn = lines[currentLine]?.length || 0;
    } else {
      currentColumn--;
    }

    cursorPosition([currentLine, currentColumn]);
    resetCursor();

    yield* waitFor(random.nextFloat(0.03, 0.08));
  }
}

// Usage:
yield* backspaceText(5); // Delete 5 characters

Typing with Corrections

typescript
const textToType = "console.log('Helllo World!');";
const correction = "console.log('Hello World!');";

// Type original (with typo)
yield* typeText(textToType);
yield* waitFor(0.5);

// Backspace to fix typo
yield* backspaceText(8); // Remove "llo World!);"
yield* waitFor(0.2);

// Type correction
yield* typeText("lo World!');");


Multiple Cursors

const cursors = createRefMap<Rect>();
const cursorPositions = createSignal<CodePoint[]>([[0, 0], [1, 0]]);

// Create multiple cursors
cursorPositions().forEach((_, index) => {
  const cursorBBox = createComputed(() => 
    codeRef().getPointBBox(cursorPositions()[index])
  );

  view.add(
    <Rect ref={cursors[`cursor${index}`]} width={3} height={30} fill="#00ff00" position={() => cursorBBox().center} /> ); });

Real-World Examples

1. Terminal Command Typing

typescript
const terminalTheme = {
  bg: '#0c0c0c',
  text: '#00ff00',
  prompt: '#ffff00',
  cursor: '#ffffff'
};

const commands = [
  '$ npm install motion-canvas',
  '$ npm run build',
  '$ npm start'
];

for (const command of commands) {
  yield* typeText(command);
  yield* waitFor(0.5);
  textSignal(textSignal() + '\n');
  currentLine++;
  currentColumn = 0;
  yield* waitFor(0.3);
}


2. Live Coding Session

const codeSteps = [
  `import React from 'react';`,
  `import React from 'react'; function App() {`,
  `import React from 'react'; function App() { return (`,
  `import React from 'react'; function App() { return ( <div>Hello World!</div>`,
  `import React from 'react'; function App() { return ( <div>Hello World!</div> ); }`
];

for (let i = 0; i < codeSteps.length; i++) {
  const targetText = codeSteps[i];
  const currentText = codeText();

  // Calculate what to add
  const toAdd = targetText.slice(currentText.length);
  yield* typeText(toAdd);
  yield* waitFor(1.0); // Pause between steps
}

3. Error and Fix Simulation

typescript
// Type code with bug
yield* typeText(`function calculate(a, b) {
  return a + b
}`);

yield* waitFor(1.0);

// Show error (add red highlighting)
const errorOverlay = createRef<Rect>();
view.add(
  <Rect
    ref={errorOverlay}
    fill="rgba(255, 0, 0, 0.3)"
    position={() => codeRef().getPointBBox([2, 13]).center}
    size={() => codeRef().getPointBBox([2, 13]).size}
  />
);

yield* waitFor(2.0);

// Fix the error
yield* moveCursorTo([2, 13]);
yield* typeText(';');

// Remove error highlight
errorOverlay().remove();

4. IDE-Style Features

typescript
// Auto-completion popup
const suggestions = ['console', 'const', 'constructor'];

yield* typeText('con');
yield* waitFor(0.5);

// Show autocomplete
const popup = createRef<Rect>();
view.add(
  <Rect
    ref={popup}
    fill="#2d2d2d"
    stroke="#555"
    padding={10}
    radius={4}
    position={() => cursorBBox().center.add([0, 50])}
  >
    {suggestions.map(suggestion => 
      <Txt text={suggestion} fill="white" />
    )}
  </Rect>
);

yield* waitFor(1.0);

// Accept suggestion
popup().remove();
yield* backspaceText(3);
yield* typeText('console.log("Hello!");');

Customization Options

Timing Variations

typescript
const typingProfiles = {
  slow: { min: 0.1, max: 0.3 },
  normal: { min: 0.05, max: 0.15 },
  fast: { min: 0.02, max: 0.08 },
  hunt_and_peck: { min: 0.2, max: 0.8 }
};

const profile = typingProfiles.normal;
const delay = random.nextFloat(profile.min, profile.max);

Character-Specific Delays

typescript
function getTypingDelay(char: string, random: any) {
  const delays = {
    ' ': [0.02, 0.05],   // Spaces are quick
    '\n': [0.1, 0.3],    // Line breaks need thought
    '.': [0.1, 0.25],    // Punctuation pauses
    ',': [0.05, 0.1],    // Commas are brief
    ';': [0.05, 0.12],   // Semicolons
    '{': [0.08, 0.15],   // Opening braces
    '}': [0.1, 0.2],     // Closing braces (think more)
  };

  const range = delays[char] || [0.05, 0.15];
  return random.nextFloat(range[0], range[1]);
}

Cursor Styles

typescript
// Block cursor
<Rect
  width={() => cursorBBox().width || 15}
  height={() => cursorBBox().height}
  fill="rgba(255, 255, 255, 0.5)"
  position={() => cursorBBox().center}
/>

// Underline cursor
<Rect
  width={() => cursorBBox().width || 15}
  height={2}
  fill="white"
  position={() => cursorBBox().center.add([0, cursorBBox().height/2])}
/>

// Animated cursor
<Rect
  ref={cursorRef}
  width={3}
  height={30}
  fill="#00ff00"
  position={() => cursorBBox().center}
  scaleX={() => Math.sin(time() * 8) * 0.1 + 1} // Pulsing effect
/>

Tips and Best Practices

Performance

typescript
// Use createComputed for expensive calculations
const cursorBBox = createComputed(() => 
  codeRef().getPointBBox(cursorPosition())
);

// Avoid recalculating in every frame
const memoizedPosition = createSignal(Vector2.zero);

Realism

typescript
// Add occasional longer pauses (thinking)
if (Math.random() < 0.1) { // 10% chance
  yield* waitFor(random.nextFloat(0.5, 1.5));
}

// Vary typing speed based on complexity
const complexity = char.match(/[{}();]/) ? 'complex' : 'simple';
const delay = complexity === 'complex' ? 
  random.nextFloat(0.1, 0.25) : 
  random.nextFloat(0.05, 0.15);

Error Handling

typescript
// Safeguard cursor position
function updateCursorPosition(line: number, column: number) {
  const lines = codeText().split('\n');
  const maxLine = lines.length - 1;
  const maxColumn = lines[line]?.length || 0;

  currentLine = Math.min(line, maxLine);
  currentColumn = Math.min(column, maxColumn);
  cursorPosition([currentLine, currentColumn]);
}

Modular Functions

typescript
// Reusable typing function
function* typeText(text: string, speed: 'slow' | 'normal' | 'fast' = 'normal') {
  const random = useRandom();
  const profile = typingProfiles[speed];

  for (const char of text) {
    codeText(codeText() + char);
    updateCursorPosition(currentLine, currentColumn + 1);
    resetCursor();

    const delay = getTypingDelay(char, random);
    yield* waitFor(delay);
  }
}

// Usage
yield* typeText("Hello World!", 'fast');
yield* typeText("This is slower...", 'slow');

Common Issues and Solutions

Issue: Cursor not positioning correctly

Solution: Make sure to use getPointBBox() instead of deprecated methods

typescript
// ❌ Don't use
const bbox = codeRef().getCacheBBox();

// ✅ Use instead
const bbox = codeRef().getPointBBox(cursorPosition());

Issue: Cursor jumping around

Solution: Always update cursor position when text changes

typescript
if (char === '\n') {
  currentLine++;
  currentColumn = 0;
} else {
  currentColumn++;
}
cursorPosition([currentLine, currentColumn]);

Issue: Performance problems with long text

Solution: Use signals and computed values efficiently

typescript
// ❌ Avoid recalculating every frame
position={() => codeRef().getPointBBox([line, col]).center}

// ✅ Use computed values
const cursorBBox = createComputed(() => 
  codeRef().getPointBBox(cursorPosition())
);

That's it for this guide these are everything you need to create professional typing animations in MotionCanvas. Start with the basic examples and gradually add more advanced features as needed!