Code
Click to preview animation
import {makeScene2D, Code} from '@motion-canvas/2d';
import {all, createRef, DEFAULT, waitFor} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
const code = createRef<Code>();
view.add(
<Code
ref={code}
fontSize={28}
fontFamily={'JetBrains Mono, monospace'}
offsetX={-1}
x={-400}
code={'const number = 7;'}
/>,
);
yield* waitFor(0.6);
yield* all(
code().code.replace(code().findFirstRange('number'), 'variable', 0.6),
code().code.prepend(0.6)`function example() {\n `,
code().code.append(0.6)`\n}`,
);
yield* waitFor(0.6);
yield* code().selection(code().findFirstRange('variable'), 0.6);
yield* waitFor(0.6);
yield* all(
code().code('const number = 7;', 0.6),
code().selection(DEFAULT, 0.6),
);
});
The Code node is used for displaying code snippets.
It supports syntax highlighting and a handful of different methods for animating
the code.
Parsing and Highlighting
First things first, if you just copy any of the snippets in this tutorial you'll
notice that the displayed code has a uniform color. The default highlighter uses
Lezer to parse and highlight the code but to
do that it needs the grammar for the language you're using. You can set that up
in your project configuration file.
First, install the grammar for your language. You can check out this
non-exhaustive list of available grammars. For this tutorial,
you should install the javascript grammar:
npm i @lezer/javascript
Then, in your project configuration, instantiate a new LezerHighlighter using
the imported grammar, and set it as the default highlighter:
src/project.tsimport {makeProject} from '@motion-canvas/core';
import example from './scenes/example?scene';
import {Code, LezerHighlighter} from '@motion-canvas/2d';
import {parser} from '@lezer/javascript';
Code.defaultHighlighter = new LezerHighlighter(parser);
export default makeProject({
scenes: [example],
});
Now all Code nodes in your project will use @lezer/javascript to parse and
highlight the snippets. If you want to use more than one language, check out the
Multiple Languages section.
Note that, by default, the JavaScript parser doesn't support JSX or TypeScript. You can enable support for these via dialects. The dialects available for a given parser are usually listed in the documentation of the grammar package.
Code.defaultHighlighter = new LezerHighlighter(
parser.configure({
// Provide a space-separated list of dialects to enable:
dialect: 'jsx ts',
}),
);
Defining Code
The code to display is set via the code
property. In the simplest case, you can just use a string:
Click to preview animation
import {makeScene2D, Code} from '@motion-canvas/2d';
export default makeScene2D(function* (view) {
view.add(
<Code
fontSize={28}
code={'const number = 7;'}
/>,
);
});
However, usually code snippets contain multiple lines of code. It's much more
convenient to use a template string for this (denoted using the backtick
character `):
view.add(
<Code
fontSize={28}
code={`\
function example() {
const number = 7;
}
`}
/>,
);
Notice two things here:
-
The code snippet ignores the indentation of the template string itself. The template string preserves all whitespace characters, so any additional spaces or tabs at the beginning of each line would be included in the snippet.
-
The backslash character (
\) at the very beginning is used to escape the first newline character. This lets the snippet start on a new line without actually including an empty line at the beginning. Without the slash, the equivalent code would have to be written as:view.add(
<Code
fontSize={28}
code={`function example() {
const number = 7;
}
`}
/>,
);
Template strings allow you to easily include variables in your code snippets
with the ${} syntax. In the example below, ${name} is replaced with the
value of the name variable (which is number in this case):
const name = 'number';
view.add(
<Code
fontSize={28}
code={`\
function example() {
const ${name} = 7;
}
`}
/>,
);
Any valid JavaScript expression inside the ${} syntax will be included in the
code snippet:
const isRed = true;
view.add(
<Code
fontSize={28}
code={`\
function example() {
const color = '${isRed ? 'red' : 'blue'}';
}
`}
/>,
);
Using Signals
If you try to use signals inside the ${} syntax, you'll notice that they don't
work as expected. Invoking a signal inside a template string uses its current
value and then never updates the snippet again, even if the signal changes:
Click to preview animation
import {makeScene2D, Code} from '@motion-canvas/2d';
import {waitFor} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
const nameSignal = Code.createSignal('number');
view.add(
<Code
fontSize={28}
code={`const ${nameSignal()} = 7;`}
/>,
);
yield* waitFor(1);
nameSignal('newValue');
// The code snippet still displays "number" instead of "newValue".
yield* waitFor(1);
});
Trying to pass the signal without invoking it is even worse. Since each signal is a function, it will be stringified and included in the snippet:
Click to preview animation
import {makeScene2D, Code} from '@motion-canvas/2d';
import {waitFor} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
const nameSignal = Code.createSignal('number');
view.add(
<Code
fontSize={28}
code={`const ${nameSignal} = 7;`}
/>,
);
yield* waitFor(1);
nameSignal('newValue');
yield* waitFor(1);
});
This happens because template strings are parsed immediately when our code is
executed. To work around this, you can use a custom tag function
called CODE. It allows the Code node to parse the
template string in a custom way and correctly support signals. It's really easy
to use, simply put the CODE tag function before your template string:
Click to preview animation
import {makeScene2D, Code, CODE} from '@motion-canvas/2d';
import {waitFor} from '@motion-canvas/core';
export default makeScene2D(function* (view) {
const nameSignal = Code.createSignal('number');
view.add(
<Code
fontSize={28}
// Note the CODE tag function here:
code={CODE`const ${nameSignal} = 7;`}
/>,
);
yield* waitFor(1);
nameSignal('newValue');
// Now the code snippet is updated accordingly.
yield* waitFor(1);
});
The value returned by CODE can itself be nested in other template strings:
const implementation = CODE`\
console.log('Hello!');
return 7;`;
const method = CODE`\
greet() {
${implementation}
}`;
const klass = CODE`\
class Example {
${method}
}
`;
view.add(<Code code={klass} />);
// class Example {
// greet() {
// console.log('Hello!');
// return 7;
// }
// }
You might have noticed that these examples used a specialized type of signal
created using Code.createSignal().
While the generic createSignal() would work
fine in these simple examples, the specialized signal will shine once you start
animating your code snippets.
Animating Code
The Code node comes with a few different techniques for animating the code
depending on the level of control you need.