Code
import
...
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 animatingthe code.
Parsing and Highlighting
First things first, if you just copy any of the snippets in this tutorial you'llnotice that the displayed code has a uniform color. The default highlighter uses
Lezer
to parse and highlight the code but todo that it needs the grammar for the language you're using. You can set that upin your project configuration file.
First, install the grammar for your language. You can check out thisnon-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
usingthe imported grammar, and set it as the default highlighter:
import
{
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 andhighlight 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 dialectsavailable for a given parser are usually listed in the documentation of thegrammar 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:
import
...
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 moreconvenient to use a template string for this (denoted using the backtickcharacter
`
):
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. Thetemplate string preserves all whitespace characters, so any additional spacesor tabs at the beginning of each line would be included in the snippet.
-
The backslash character (
\) at the very beginning is used to escape thefirst newline character. This lets the snippet start on a new line withoutactually including an empty line at the beginning. Without the slash, theequivalent 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 snippetswith the
${}
syntax. In the example below,
${name}
is replaced with thevalue 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 thecode 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'twork as expected. Invoking a signal inside a template string uses its currentvalue and then never updates the snippet again, even if the signal changes:
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 signalis a function, it will be stringified and included in the snippet:
import
...
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 isexecuted. To work around this, you can use a custom
tag function
called
CODE
. It allows the
Code
node to parse thetemplate string in a custom way and correctly support signals. It's really easyto use, simply put the
CODE
tag function before your template string:
import
...
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 signalcreated using
Code.createSignal()
.While the generic
createSignal()
would workfine in these simple examples, the specialized signal will shine once you startanimating your code snippets.
Animating Code
The
Code
node comes with a few different techniques for animating the codedepending on the level of control you need.
Diffing
The default method for animating code is diffing. It's used whenever you tweenthe
code
property:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
\
function example() {
const number = 9;
}
`
}
/>
,
)
;
yield
*
code
(
)
.
code
(
'const nine = 9;'
,
0.6
)
.
wait
(
0.6
)
.
back
(
0.6
)
.
wait
(
0.6
)
;
}
)
;
This method uses the patience diff algorithm to determine the differencesbetween the old and new code snippets. It then animates the changes accordingly.
append
and
prepend
For cases where you want to add some code at the beginning or end of thesnippet, you can use the
append
and
prepend
methods. They can eithermodify the code immediately or animate the changes over time:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
/>
,
)
;
// append immediately
code
(
)
.
code
.
append
(
`
const one = 1;
`
)
;
// animate using the signal signature
yield
*
code
(
)
.
code
.
append
(
'\nconst two = 2;'
,
0.6
)
;
// animate using the template tag signature
yield
*
code
(
)
.
code
.
append
(
0.6
)
`
const three = 3;
`
;
// prepend works analogically
yield
*
code
(
)
.
code
.
prepend
(
'// example\n'
,
0.6
)
;
yield
*
waitFor
(
0.6
)
;
}
)
;
insert
,
replace
, and
remove
For more granular control over the changes, you can use
insert
,
replace
, and
remove
to modify the code at specificpoints. Check out
Code Ranges
for more information on how tospecify points in your code snippets.
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
\
function example() {
console.log('Hello!');
}
`
}
/>
,
)
;
// insert code at line 2, column 0
yield
*
code
(
)
.
code
.
insert
(
[
2
,
0
]
,
' return 7;\n'
,
0.6
)
;
// replace the word "Hello!" with "Goodbye!"
yield
*
code
(
)
.
code
.
replace
(
word
(
1
,
15
,
6
)
,
'Goodbye!'
,
0.6
)
;
// remove line 2
yield
*
code
(
)
.
code
.
remove
(
lines
(
2
)
,
0.6
)
;
// animate multiple changes at the same time
yield
*
all
(
code
(
)
.
code
.
replace
(
word
(
0
,
9
,
7
)
,
'greet'
,
0.6
)
,
code
(
)
.
code
.
replace
(
word
(
1
,
15
,
8
)
,
'Hello!'
,
0.6
)
,
)
;
yield
*
waitFor
(
0.6
)
;
}
)
;
edit
The
edit
method offers a different wayof defining code transitions. It's used together with the
replace
,
insert
, and
remove
helper functions that are inserted into thetemplate string. They let you specify the changes in a more visual way, withouthaving to know the exact positions in the code:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
/>
,
)
;
yield
*
code
(
)
.
code
.
edit
(
0.6
)
`
\
function example() {
${
insert
(
`
// This is a comment
`
)
}
console.log("
${
replace
(
'Hello!'
,
'Goodbye!'
)
}
");
${
remove
(
`
return 7;
`
)
}
}
`
;
yield
*
waitFor
(
0.6
)
;
}
)
;
Signals
Notice that all the methods used above are not invoked on the
Code
node butrather on its
code
property. It may seem unnecessarily verbose but there's agood reason for it: the
code
property is a specialized code signal, just likethe ones created by
Code.createSignal()
. This means thatall the animation methods are also available on your own signals:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
message
=
Code
.
createSignal
(
`
Hello, world!
`
)
;
const
body
=
Code
.
createSignal
(
CODE
`
console.log('
${
message
}
');
`
)
;
view
.
add
(
<
Code
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
CODE
`
\
function hello() {
${
body
}
}
`
}
/>
,
)
;
yield
*
waitFor
(
0.3
)
;
yield
*
all
(
message
(
'Goodbye, world!'
,
0.6
)
,
body
.
append
(
0.6
)
`
\n return 7;
`
,
)
;
yield
*
waitFor
(
0.3
)
;
}
)
;
Code signals can also be nested in the template strings passed to the animationmethods:
import
{
makeScene2D
,
Code
,
CODE
}
from
'@motion-canvas/2d'
;
import
{
createRef
,
waitFor
}
from
'@motion-canvas/core'
;
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
'// example'
}
/>
,
)
;
const
body
=
Code
.
createSignal
(
CODE
`
console.log('Hello!');
`
)
;
yield
*
waitFor
(
0.3
)
;
// Notice how the CODE tag is not used here because
// "append" already supports nested signals:
yield
*
code
(
)
.
code
.
append
(
0.6
)
`
function hello() {
${
body
}
}
`
;
// The "body" signal remains reactive after the append animation:
yield
*
body
(
`
console.log('Goodbye!');
`
,
0.6
)
;
yield
*
waitFor
(
0.3
)
;
}
)
;
Code Ranges
A
CodeRange
is used to specify a continuous span ofcharacters using line and column numbers. It can be used for editing the code,visually selecting a part of it, or querying the positions and sizes ofcharacters.
Code ranges have the following structure:
[
[
startLine
,
startColumn
]
,
[
endLine
,
endColumn
]
]
;
For example, to select the first three characters of the second line, you woulduse the following range:
[
[
1
,
0
]
,
[
1
,
3
]
]
;
Keep in mind that both lines and columns are zero-based. Additionally, youshould think of columns as being located on the left side of the characters,meaning that if you want to include the character at column
n
you should use
n + 1
as the end column.
For convenience, the
word
and
lines
helper functions are provided to create some ofthe common types of ranges:
// a range starting at line 1, column 3,
// spanning 3 characters:
word
(
1
,
3
,
3
)
;
// a range starting at line 1, column 3,
// spanning until the end of the line:
word
(
1
,
3
)
;
// a range containing lines from 1 to 3 (inclusive):
lines
(
1
,
3
)
;
// a range containing line 2
lines
(
2
)
;
Once you create a
Code
node, you can use its
findFirstRange
,
findAllRanges
, and
findLastRange
methods to find theranges that contain a specific string or match the given regular expression:
import
{
makeScene2D
,
Code
}
from
'@motion-canvas/2d'
;
import
{
createRef
,
waitFor
}
from
'@motion-canvas/core'
;
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
\
function example() {
console.log('Hello!');
}
`
}
/>
,
)
;
yield
*
code
(
)
.
code
.
replace
(
// find the range of "example" and replace it with "greet"
code
(
)
.
findFirstRange
(
'example'
)
,
'greet'
,
0.6
,
)
;
yield
*
waitFor
(
0.6
)
;
}
)
;
Code Selection
The
selection
property can be usedto visually distinguish a part of the code snippet. The selection is specifiedusing an individual
code range
or an array of ranges:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
\
function hello() {
console.log('Hello');
}
`
}
/>
,
)
;
// select all instances of "hello" (case-insensitive)
yield
*
code
(
)
.
selection
(
code
(
)
.
findAllRanges
(
/
hello
/
gi
)
,
0.6
)
;
yield
*
waitFor
(
0.3
)
;
// select line 1
yield
*
code
(
)
.
selection
(
lines
(
1
)
,
0.6
)
;
yield
*
waitFor
(
0.3
)
;
// reset the selection
yield
*
code
(
)
.
selection
(
DEFAULT
,
0.6
)
;
yield
*
waitFor
(
0.3
)
;
}
)
;
Querying Positions and Sizes
getPointBBox
and
getSelectionBBox
can be used toretrieve the position and size of a specific character or a range of characters,respectively. The returned value is a
bounding box
inthe local space of the
Code
node.
The following example uses
getSelectionBBox
to draw a rectangle around theword
log
:
import
...
export
default
makeScene2D
(
function
*
(
view
)
{
const
code
=
createRef
<
Code
>
(
)
;
view
.
add
(
<
Code
ref
=
{
code
}
fontSize
=
{
28
}
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
\
function hello() {
console.log('Hello');
}
`
}
/>
,
)
;
const
range
=
createSignal
(
(
)
=>
{
const
range
=
code
(
)
.
findFirstRange
(
'log'
)
;
const
bboxes
=
code
(
)
.
getSelectionBBox
(
range
)
;
// "getSelectionBBox" returns an array of bboxes,
// one for each line in the range. You can just
// use the first one for this example.
const
first
=
bboxes
[
0
]
;
return
first
.
expand
(
[
4
,
8
]
)
;
}
)
;
code
(
)
.
add
(
<
Rect
offset
=
{
-
1
}
position
=
{
range
(
)
.
position
}
size
=
{
range
(
)
.
size
}
lineWidth
=
{
4
}
stroke
=
{
'white'
}
radius
=
{
8
}
/>
,
)
;
}
)
;
Custom Themes
LezerHighlighter
uses CodeMirror's
HighlightStyle
to assign colors tospecific code tokens. By default, the
DefaultHighlightStyle
is used. You canspecify your own style by passing it as the second argument to the
LezerHighlighter
constructor:
import
{
Code
,
LezerHighlighter
}
from
'@motion-canvas/2d'
;
import
{
HighlightStyle
}
from
'@codemirror/language'
;
import
{
tags
}
from
'@lezer/highlight'
;
import
{
parser
}
from
'@lezer/javascript'
;
const
MyStyle
=
HighlightStyle
.
define
(
[
{
tag
:
tags
.
keyword
,
color
:
'red'
}
,
{
tag
:
tags
.
function
(
tags
.
variableName
)
,
color
:
'yellow'
}
,
{
tag
:
tags
.
number
,
color
:
'blue'
}
,
{
tag
:
tags
.
string
,
color
:
'green'
}
,
// ...
]
)
;
Code
.
defaultHighlighter
=
new
LezerHighlighter
(
parser
,
MyStyle
)
;
Multiple Languages
You can configure highlighters on a per-node basis using the
highlighter
property. This willoverride the default highlighter set in the project configuration file:
import
{
Code
,
LezerHighlighter
}
from
'@motion-canvas/2d'
;
import
{
parser
}
from
'@lezer/rust'
;
const
RustHighlighter
=
new
LezerHighlighter
(
parser
)
;
// ...
view
.
add
(
<
Code
// this node uses the default parser
offsetX
=
{
-
1
}
x
=
{
-
400
}
code
=
{
`
function hello() {
console.log('Hello!');
}
`
}
/>
,
)
;
view
.
add
(
<
Code
// this node uses the Rust parser
highlighter
=
{
RustHighlighter
}
offsetX
=
{
1
}
x
=
{
400
}
code
=
{
`
fn hello() {
println!("Hello!");
}
`
}
/>
,
)
;
It can be useful to create a custom component for the languages you often use.You can use the
withDefaults
helper function toquickly extend any node with your own defaults:
import
{
Code
,
LezerHighlighter
,
withDefaults
}
from
'@motion-canvas/2d'
;
import
{
parser
}
from
'@lezer/rust'
;
const
RustHighlighter
=
new
LezerHighlighter
(
parser
)
;
export
const
RustCode
=
withDefaults
(
Code
,
{
highlighter
:
RustHighlighter
,
}
)
;
import
{
RustCode
}
from
'../nodes/RustCode'
;
// ...
view
.
add
(
<
RustCode
code
=
{
`
fn hello() {
println!("Hello!");
}
`
}
/>
,
)
;
Rendering
If you're using a Chromium-based browser, you might notice that the codeanimations jitter a bit. This happens because the canvas snaps the rendered textto the nearest pixel which prevents it from moving smoothly. Until we find a wayto fix this, we recommend you use Firefox for rendering.