Code.Movie
Decorations
Decorations highlight or obscure select lines or sections of code. Just like your IDE can highlight the current line, underline type errors or place icons the gutter, so can you!
function add(a: number, b: number): number {
return a + b;
}
add(23, "42"); // ← This is wrongfunction add(a: number, b: number): number {
return a + b; // ← This is important
}
add(23, "42");function add(a: number, b: number): number {
return a + b;
}
add(23, "42"); // ← Watch thisfunction add(a: number, b: number): number {
return a + b;
}
add(23, 42); // ← This works now!function add(a: number, b: number): number {
return a + b;
}
add(23, 42); // ← This returns ??function add(a: number, b: number): number {
return a + b;
}
add(23, 42); // ← This returns 65Like with code, you add decorations declaratively to each code frame and let the library worry about computing the animation. The following example shows how a basic text decoration works:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
import {
fromStringsToScene,
toAnimationHTML,
} from "@codemovie/code-movie/dist/index.js";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;123456789101112131415161718192021222324252627282930let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 15 to 21)
{ kind: "TEXT", data: {}, from: 15, to: 22 },
],
},
];
import {
fromStringsToScene,
toAnimationHTML,
} from "@codemovie/code-movie/dist/index.js";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;Result:
[ "Hello", "World" ]
[ "Hello", "World" ]
Decorations are a fairly powerful feature and not without some complexity. But since they can really elevate your animations to the next level, learning how they work is definitely worth it.
Decoration kinds
Gutter decorations
A gutter decoration renders a single unicode character somewhere near the line number column. You can use the full range of unicode in general and emoji in particular to express things like errors ⛔, approval ✅, personal opinion 🤮, additions ➕, deletions ➖, and much more.
[ "Hello", "World" ]
[ "Hello", "World" ]
The above example illustrates how the default gutter decoration style is pretty much just a static character. The example can be recreated like this:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "Hello"
{ kind: "GUTTER", data: {}, text: "➡️", line: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "World"
{ kind: "GUTTER", data: {}, text: "➡️", line: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;123456789101112131415161718192021222324252627let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "Hello"
{ kind: "GUTTER", data: {}, text: "➡️", line: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Points to the line containing "World"
{ kind: "GUTTER", data: {}, text: "➡️", line: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1">${toAnimationHTML(scene)}</code-movie-runtime>`;Some tips and tricks for gutter decorations:
- Use CSS to position gutter decorations on whichever side of the line number column you prefer.
- Change the font on gutter decorations to switch emoji styles or maybe even use a custom icon font. Remember that you can use the
dataobject to set whatever attributes you want. You might want to usedata.classto add a custom class name that your CSS can then target. - Gutter decorations render their contents in a nested
<span>, which is a great attachment point for more custom styles (eg. the aforementioned custom font) or maybe even an idle animation.
Line decorations
A line decoration renders up to two boxes (one in the text foreground, one in the background) across one or more entire lines. You can use the background to highlight lines and the foreground to obscure content.
[ "Hello", "World" ]
[ "Hello", "World" ]
[ "Hello", "World" ]
The above example illustrates how the default line decoration style is a subtle yellow highlight. The example can be recreated like this:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: {}, fromLine: 2, toLine: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "World"
{ kind: "LINE", data: {}, fromLine: 3, toLine: 3 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers both both lines
{ kind: "LINE", data: {}, fromLine: 2, toLine: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1 2">${toAnimationHTML(scene)}</code-movie-runtime>`;12345678910111213141516171819202122232425262728293031323334let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: {}, fromLine: 2, toLine: 2 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers the line containing "World"
{ kind: "LINE", data: {}, fromLine: 3, toLine: 3 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Covers both both lines
{ kind: "LINE", data: {}, fromLine: 2, toLine: 3 },
],
},
];
import { fromStringsToScene, toAnimationHTML } from "@codemovie/code-movie";
import json from "@codemovie/code-movie/languages/json";
let scene = fromStringsToScene(keyframes, {
tabSize: 2,
language: json(),
});
import "@codemovie/code-movie-runtime";
document.body.innerHTML = `<code-movie-runtime controls keyframes="0 1 2">${toAnimationHTML(scene)}</code-movie-runtime>`;Two line decorations count as equal when their data fields contain equal values.
The default line decoration highlights a line with a light yellow background. To add variants to this, write CSS rules targeting selectors that match any of the tag names and/or attributes set in the decoration's data fields:
/* Decoration for dead code */
.deadcode {
/* No line decoration background color */
--cm-decoration-line-background-background: none;
/* Backdrop filter to desaturate and blur the affected tokens */
--cm-decoration-line-foreground-backdrop-filter: blur(0.75px) saturate(0);
/* Use an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: var(--line-numbers-column-width);
}123456789/* Decoration for dead code */
.deadcode {
/* No line decoration background color */
--cm-decoration-line-background-background: none;
/* Backdrop filter to desaturate and blur the affected tokens */
--cm-decoration-line-foreground-backdrop-filter: blur(0.75px) saturate(0);
/* Use an offset in order to not blur the line number */
--cm-decoration-line-foreground-offset-left: var(--line-numbers-column-width);
}To use this class just make sure that the decoration's data field has an entry for class or className
let keyframes = [
{
code: `\n// Everthing after line 3 is dead code\nfunction example(x) {\n return null;\n return x;\n}`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: { className: "deadcode" }, fromLine: 4, toLine: 4 },
],
},
];123456789let keyframes = [
{
code: `\n// Everthing after line 3 is dead code\nfunction example(x) {\n return null;\n return x;\n}`,
decorations: [
// Covers the line containing "Hello"
{ kind: "LINE", data: { className: "deadcode" }, fromLine: 4, toLine: 4 },
],
},
];Result:
// Everthing after line 3 is dead code
function example(x) {
return null;
return x;
}// This makes more sense!
function example(x) {
// return null;
return x;
}The same approach works with text decorations. Check out the CSS variables that are available to tweak line decorations! The classes error and ok are available as built-in extra styles for both line and text decorations.
Some more tips and tricks for line decorations:
- Don't forget the foreground! Hiding or obscuring a few lines of code behind a line decoration foreground when teaching or presenting can be very useful.
- Combine decorations! An unobtrusive line background works great with a matching gutter decoration. If you want to indicate an error, add a red background and a gutter decoration like ❌ to direct your audience's attention.
Text decorations
Text decorations decorate specific sections of code with backgrounds and/or underlines:
function add(a: number, b: number): number {
return a + b;
}
add(23, 42);
add(23n, 42n); // ← only accepts numbersfunction add(a: number, b: number): number {
return a + b;
}
add(23, 42);
add(23n, 42n); // ← only accepts numbersfunction add<T>(a: T, b: T): T {
return a + b; // ← can't add ANY type
}
add(23, 42);
add(23n, 42n); // ← this works nowfunction add<T>(a: T, b: T): T { // ← TOO generic
return a + b; // ← can't add ANY type
}
add(23, 42);
add(23n, 42n);function add<T extends number | bigint>(a: T, b: T): T {
return a + b;
}
add(23, 42);
add(23n, 42n);Every text decoration renders up to four elements per decorated section. Going from the background to the foreground this can include:
- A background backdrop layer
- A background underline layer
- A foreground backdrop layer
- A foreground underline layer
The highlighted code is sandwiched between the background and foreground layers.
The above example illustrates how text decorations are made up of backgrounds and underlines. The example borders on non-trivial, but can be recreated like this:
Show keyframes for the above example
let keyframes = [
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 16,
to: 22,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 27,
to: 33,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 36,
to: 42,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add<T>(a: T, b: T): T {\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← this works now",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 19,
to: 20,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 25,
to: 26,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 29,
to: 30,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 35,
to: 48,
},
],
},
{
code: "function add<T>(a: T, b: T): T { // ← TOO generic\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 12,
to: 15,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 52,
to: 65,
},
],
},
{
code: "function add<T extends number | bigint>(a: T, b: T): T {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "ok",
tagName: "mark",
},
from: 15,
to: 38,
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 5,
text: "✅",
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 6,
text: "✅",
},
],
},
];123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151let keyframes = [
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add(a: number, b: number): number {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← only accepts numbers",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 16,
to: 22,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 27,
to: 33,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 36,
to: 42,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 77,
to: 91,
},
],
},
{
code: "function add<T>(a: T, b: T): T {\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n); // ← this works now",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 19,
to: 20,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 25,
to: 26,
},
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 29,
to: 30,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 35,
to: 48,
},
],
},
{
code: "function add<T>(a: T, b: T): T { // ← TOO generic\n return a + b; // ← can't add ANY type\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
tagName: "mark",
},
from: 12,
to: 15,
},
{
kind: "TEXT",
data: {
class: "error",
tagName: "mark",
},
from: 52,
to: 65,
},
],
},
{
code: "function add<T extends number | bigint>(a: T, b: T): T {\n return a + b;\n}\n\nadd(23, 42);\nadd(23n, 42n);",
ranges: [],
decorations: [
{
kind: "TEXT",
data: {
class: "ok",
tagName: "mark",
},
from: 15,
to: 38,
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 5,
text: "✅",
},
{
kind: "GUTTER",
data: {
class: "gutter",
tagName: "mark",
},
line: 6,
text: "✅",
},
],
},
];Note that the start and end indices from and to are the offsets of unicode code points, which may not correspond 1:1 with the character count. Two text decorations count as equal when their data fields contain equal values.
The default text decoration highlights its range with a bright yellow background like a <mark> element. Like with line decorations, you can write CSS rules targeting selectors that match any of the tag names and/or attributes set in the decoration's data fields. The classes error and ok are available as built-in extra styles for both line and text decorations.
Some more tips and tricks for line decorations:
- Combine decorations! Text decorations works great with matching gutter decorations. If you want to indicate an error, add a red underline and a gutter decoration like ❌ to direct your audience's attention.
Decoration customization
Decorations are rendered as regular HTML elements. To customize the element specifics you can set properties on the decorator's data field.
- specify
tagNameto pick which HTML tag to use when rendering the element - every other property that does not start with
_is used as an HTML attribute
This allows you to set up decorations as targets for your custom CSS and you can even attach scripted behavior by using a custom element as data.tagName:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{
kind: "TEXT",
data: {
tagName: "mark", // use <mark> instead of the default <span>,
class: "customClass" // add "customClass" to the class attribute
"data-something": "42" // set attribute data-something="42"
"_invisible": "42" // will not show up as an attribute
},
from: 4,
to: 11
},
],
},
];12345678910111213141516171819let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{
kind: "TEXT",
data: {
tagName: "mark", // use <mark> instead of the default <span>,
class: "customClass" // add "customClass" to the class attribute
"data-something": "42" // set attribute data-something="42"
"_invisible": "42" // will not show up as an attribute
},
from: 4,
to: 11
},
],
},
];Every kind of decoration (gutter, line, text) has a different set of CSS variables available for styling purposes. By setting custom classes (or other attributes) on data.attributes you can create your own custom set of differently-styled decorations.
Note that different kinds of decorations do not share any CSS variable names. You can get by with just one class named .something that hosts the settings for both text and line decorations:
.something {
/* Text */
--cm-decoration-text-foreground-underline-offset-y: -0.5;
--cm-decoration-text-foreground-underline-scale: 0.15;
--cm-decoration-text-foreground-underline-width: 10;
--cm-decoration-text-foreground-underline-squiggly: 1;
--cm-decoration-text-foreground-underline-color: #c00;
--cm-decoration-text-background-background: none;
/* Line */
--cm-decoration-line-background-background: #ffeeee;
}1234567891011.something {
/* Text */
--cm-decoration-text-foreground-underline-offset-y: -0.5;
--cm-decoration-text-foreground-underline-scale: 0.15;
--cm-decoration-text-foreground-underline-width: 10;
--cm-decoration-text-foreground-underline-squiggly: 1;
--cm-decoration-text-foreground-underline-color: #c00;
--cm-decoration-text-background-background: none;
/* Line */
--cm-decoration-line-background-background: #ffeeee;
}Decoration defaults
Apart from the default styles for line and text decorations (a yellow-ish highlight), the built-in themes come with two additional presets for line and text decorations each: error and ok:
Default text decoration
"ok" text decoration
"error" text decoration
Default line decoration
"ok" line decoration
"error" line decorationDefault text decoration
"ok" text decoration
"error" text decoration
Default line decoration
"ok" line decoration
"error" line decorationYou can use the presets by setting data.class on line or text decorations to contain the class names error or ok.
Decorations and animation heuristics
From the perspective of the diffing algorithm, every decoration is represented as a hash constructed from its kind and data fields. Two decorations of the same kind and with equivalent data fields are therefore treated as interchangeable. Consider how the text decoration in the following example moves between frames:
[ "Hello", 42, "World" ]
[ "Hello", 42, "World" ]
This is happens because the decoration object has the same kind and equivalent data in both frames:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 21 to 28)
{ kind: "TEXT", data: {}, from: 21, to: 28 },
],
},
];
// ... do something with the keyframes123456789101112131415161718let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 21 to 28)
{ kind: "TEXT", data: {}, from: 21, to: 28 },
],
},
];
// ... do something with the keyframesIf we add something to the data object in the second frame, we "salt" the decoration's hash and cause the diffing algorithm to consider the decoration object to be distinct:
let keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 21 to 28)
{ kind: "TEXT", data: { class: "salt" }, from: 21, to: 28 },
],
},
];
// ... do something with the keyframeslet keyframes = [
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "Hello" (code points 4 to 11)
{ kind: "TEXT", data: {}, from: 4, to: 11 },
],
},
{
code: `[\n "Hello",\n "World"\n]`,
decorations: [
// Highlights the code section "World" (code points 21 to 28)
{ kind: "TEXT", data: { class: "salt" }, from: 21, to: 28 },
],
},
];
// ... do something with the keyframesIt will therefore no longer transition a single decoration between frames, but rather remove the decoration from frame 1 and fade in a new yellow box:
[ "Hello", 42, "World" ]
[ "Hello", 42, "World" ]
This enables opt-in manual control over how decorations behave. In extremis, you could just add an ID to every decoration and exert maximum manual control, but the diffing algorithm should usually do something reasonable by default.