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.
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';
[line, column]
position for cursor trackingexport 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));
}
});
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);
}
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...
});
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);
}
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
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} /> ); });
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
}
// 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();
// 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!");');
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);
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]);
}
// 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
/>
// Use createComputed for expensive calculations
const cursorBBox = createComputed(() =>
codeRef().getPointBBox(cursorPosition())
);
// Avoid recalculating in every frame
const memoizedPosition = createSignal(Vector2.zero);
// 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);
// 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]);
}
// 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');
Solution: Make sure to use getPointBBox()
instead of deprecated methods
// ❌ Don't use
const bbox = codeRef().getCacheBBox();
// ✅ Use instead
const bbox = codeRef().getPointBBox(cursorPosition());
Solution: Always update cursor position when text changes
if (char === '\n') {
currentLine++;
currentColumn = 0;
} else {
currentColumn++;
}
cursorPosition([currentLine, currentColumn]);
Solution: Use signals and computed values efficiently
// ❌ 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!