Code Animations

import {makeScene2D, Code} from '@revideo/2d';
import {all, createRef, DEFAULT, waitFor} from '@revideo/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.

For this tutorial, you should install the javascript grammar:

Then, in your project configuration, instantiate a new LezerHighlighter using the imported grammar, and set it as the default highlighter:

src/project.ts

Now all Code nodes in your project will use @lezer/javascript to parse and highlight the snippets.

info

Note that, by default, the JavaScript parser doesn't support JSX or TypeScript. You can enable support for these via [dialects][dialects]. The dialects available for a given parser are usually listed in the documentation of the grammar package.

Defining Code​

The code to display is set via the code property. In the simplest case, you can just use a string:

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 `):

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:

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

Any valid JavaScript expression inside the ${} syntax will be included in the code snippet:

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:

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:

This happens because template strings are parsed immediately when our code is executed. To work around this, you can use a custom [tag function][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:

The value returned by CODE can itself be nested in other template strings:

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.

Diffing​

The default method for animating code is diffing. It's used whenever you tween the code property:

This method uses the patience diff algorithm to determine the differences between 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 the snippet, you can use the append and prepend methods. They can either modify the code immediately or animate the changes over time:

insert, replace, and remove​

For more granular control over the changes, you can use insert, replace, and remove to modify the code at specific points. Check out Code Ranges for more information on how to specify points in your code snippets.

edit​

The edit method offers a different way of defining code transitions. It's used together with the replace, insert, and remove helper functions that are inserted into the template string. They let you specify the changes in a more visual way, without having to know the exact positions in the code:

Signals​

Notice that all the methods used above are not invoked on the Code node but rather on its code property. It may seem unnecessarily verbose but there's a good reason for it: the code property is a specialized code signal, just like the ones created by Code.createSignal(). This means that all the animation methods are also available on your own signals:

Code signals can also be nested in the template strings passed to the animation methods:

Code Ranges​

A CodeRange is used to specify a continuous span of characters 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 of characters.

Code ranges have the following structure:

For example, to select the first three characters of the second line, you would use the following range:

Keep in mind that both lines and columns are zero-based. Additionally, you should 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 of the common types of ranges:

Once you create a Code node, you can use its findFirstRange, findAllRanges, and findLastRange methods to find the ranges that contain a specific string or match the given regular expression:

Code Selection​

The selection property can be used to visually distinguish a part of the code snippet. The selection is specified using an individual code range or an array of ranges:

Querying Positions and Sizes​

getPointBBox and getSelectionBBox can be used to retrieve the position and size of a specific character or a range of characters, respectively. The returned value is a bounding box in the local space of the Code node.

The following example uses getSelectionBBox to draw a rectangle around the word log:

Custom Themes​

LezerHighlighter uses CodeMirror's HighlightStyle to assign colors to specific code tokens. By default, the DefaultHighlightStyle is used. You can specify your own style by passing it as the second argument to the LezerHighlighter constructor:

Multiple Languages​

You can configure highlighters on a per-node basis using the highlighter property. This will override the default highlighter set in the project configuration file:

It can be useful to create a custom component for the languages you often use. You can use the withDefaults helper function to quickly extend any node with your own defaults:

src/nodes/RustCode.ts

src/scenes/example.tsx

Last updated