Highlight and focus code
Spotlight lines, dim others, underline, surround, and point at code to direct attention.
Goal
Direct the viewer's eye to exactly the right part of a code block.
All of these methods are called on a code node returned by s.code(...), and
all line numbers are 1-based.
Spotlight a line
Raise one line to full brightness and dim the rest. spotlight is dim-only โ
it focuses a line and fades the others back, the contrast alone carrying the
focus (no accent bar):
let code = s.code(read("src/main.rs"), "rust", 96, 160);
code.fade_in(0, 12).typewriter(6, 60);
code.spotlight(3, 0.6, 80, 24); // focus line 3, dim others by 0.6Call it again to walk focus down the block โ each call animates from the last:
code.spotlight(3, 0.6, 80, 24);
code.spotlight(7, 0.6, 120, 24);
code.unfocus(170, 12); // release everyoneHighlight bar
When you want an accent bar behind a line, use highlight(line, start, dur).
It returns a chainable Highlight builder so you can style the bar Tailwind/
GPUI-style:
code.highlight(3, 80, 24); // accent bar behind line 3Layer it with spotlight for the classic "bar + dim" look:
code.highlight(3, 80, 24).rounded(6);
code.spotlight(3, 0.6, 80, 24);Style the bar
Every setter returns the builder, so they chain. Colors are 0โ1 RGBA:
code.highlight(3, 80, 24)
.bg(0.20, 0.55, 1.0, 0.25) // translucent blue fill (alias: .color(...))
.rounded(8) // corner radius (no-arg defaults to 8)
.opacity(0.4) // set just the fill alpha, keep the rgb
.pad(8, 4); // extra padding around the line (x, y).opacity(a) / .alpha(a) on the builder sets only the bar's fill alpha, so
you can pick a color with .bg(r, g, b) and dial transparency separately.
(The whole node also has its own .opacity(a) โ see
Code & node animations.)
Borders (per-side, Tailwind/GPUI-style)
code.highlight(3, 80, 24)
.bg(0.0, 0.0, 0.0, 0.0) // transparent fill โ borders only
.border_left() // single accent edge
.border_color(0.20, 0.55, 1.0)
.border_dashed(); // .border_solid() / .border_dotted()| Method | Effect |
|---|---|
border(w) / border() | All four edges (width defaults to 2). |
border_top/right/bottom/left() | Enable a single edge. |
border_x() / border_y() | Left+right / top+bottom edges. |
border_color(r,g,b[,a]) | Border color (defaults to the fill). |
border_width(w) | Set the stroke width. |
border_solid/dashed/dotted() | Border line style. |
no_border() | Clear all borders. |
Highlight without dimming
To raise the accent bar without dimming the others:
code.highlight(5, 90, 16);Or invert it โ dim everything except a line:
code.dim_others(5, 0.5, 90, 16);Walk through execution
To make the viewer watch code run, step a cursor through the lines in the
order they execute. execution_cursor(line, โฆ) marks the line currently running
(accent bar plus dimming the rest); execution_flow([...], start, step)
chains those steps for you โ including loops, where a line repeats:
let code = s.code(read("src/factorial.rs"), "rust", 110, 130);
code.typewriter(at("in"), sec(1.6));
// Enter, init, loop body x3, then return. The bar slides line-to-line.
code.execution_flow([1, 2, 3, 4, 3, 4, 3, 4, 6, 7], at("run"), ms(420));
code.unfocus(at("end"), ms(500));Each entry holds for step; repeating a line number (the 3, 4 pairs above)
replays the loop body, so control flow reads exactly like a debugger stepping.
No other programmatic video tool can do this โ it understands your code as
lines, not pixels.
Show variable values
Pair the cursor with variable_update(code, line, label, start, dur) โ it pops a
small value badge to the right of a line, so the viewer watches state change as
the code runs. It returns the badge node, so you can fade each one out as the
next appears:
// As the loop body (line 4) runs, acc goes 1 โ 2 โ 6.
let v1 = s.variable_update(code, 4, "acc = 1", at("step1"), ms(250));
v1.dissolve(at("step2"), ms(200));
let v2 = s.variable_update(code, 4, "acc = 2", at("step2"), ms(250));
v2.dissolve(at("step3"), ms(200));
s.variable_update(code, 4, "acc = 6", at("step3"), ms(250));Show a diff (watch a refactor)
Visualize a code change like a review: diff(code, added, removed, start, dur)
marks added lines green and removed lines red โ each gets a subtle full-line
tint, an opaque colored gutter stripe, and a +/- sign. Use diff_add /
diff_remove for single lines.
let code = s.code(read("src/parse.rs"), "rust", 150, 150);
code.typewriter(at("in"), sec(1.6));
// Replace the unsafe line (2) with the safe one (3).
s.diff(code, [3], [2], at("patch"), ms(450));The colored gutter stripe carries the diff color unambiguously even over syntax-highlighted text, so green/red read clearly at a glance.
Trace a value's flow
token_trace(code, from_line, from_token, to_line, to_token, start, dur) draws an
arrow from one token to another โ follow a value from its binding to each use.
Tokens are matched by substring, so you don't count columns:
// Trace `total` from its definition (line 1) to each use (lines 2, 3).
s.token_trace(code, 1, "total", 2, "total", at("t1"), ms(450));
s.token_trace(code, 1, "total", 3, "total", at("t2"), ms(450));Move and morph tokens
Transform individual tokens for refactor stories. morph_token swaps a token's
text in place (an odometer roll โ great for a rename); move_token glides a
ghost copy of a token out of the code, leaving its slot empty (extract / hoist).
Both occlude the original with a chip matching the code panel, so there's no
doubling.
// Rename `vat` -> `tax` in place.
s.morph_token(code, 2, "vat", "tax", at("rename"), ms(500));
// Extract `total` to a callout on the right.
s.move_token(code, 1, "total", 900, 300, at("extract"), ms(650));These are visual annotations โ the code node's text is unchanged. For a real
edit (the source string actually changing), use retype / backspace from
Edit code live.
Underline a line
s.underline(code, 4, 100, 20); // animate an underline under line 4Surround a range of lines
Draw an animated box around lines fromโto:
s.surround(code, 2, 5, 100, 24);Point at a line
Drop an arrow that points at a line from a screen position:
// point_at(code, line, from_x, from_y, start, dur)
s.point_at(code, 6, 760, 300, 120, 20);Gate focus on a marker (after the code finishes typing)
A spotlight reads best after the line it points to is fully on screen. Drive the timing with named markers so the highlight fires exactly when the typewriter completes โ no frame math, and it stays correct if you change the typing speed:
mark("type", ms(260)); // typewriter starts
mark("typed", after("type", sec(2.4))); // ...and finishes here
code.typewriter(at("type"), sec(2.4));
// Spotlight + camera only once the code is fully rendered.
code.highlight(5, at("typed"), ms(450)).rounded(8);
code.spotlight(5, 0.55, at("typed"), ms(450));
let cam = s.camera();
cam.focus_on(code, at("typed"), ms(700), 1.4, 5);Because typed is defined as after("type", sec(2.4)) โ the same duration the
typewriter runs โ the focus always lands on the first fully-typed frame.
Putting it together
A typical walkthrough types the code, then choreographs focus over it:
let code = s.code(read("src/main.rs"), "rust", 96, 160);
code.font_size(36).fade_in(0, 12).typewriter(6, 70);
s.surround(code, 1, 3, 90, 20); // box the signature
code.spotlight(2, 0.6, 120, 24); // focus the key line
s.point_at(code, 2, 760, 320, 150, 18);
code.unfocus(200, 14);Files that made this video
A scene script that creates insert, shift, and spotlight animations.
scenes = ["scenes/intro.ccs", "scenes/terrain.ccs", "scenes/pathfind.ccs", "scenes/particles.ccs", "scenes/graph.ccs", "scenes/life.ccs", "scenes/boids.ccs", "scenes/tweens.ccs", "scenes/inserts.ccs", "scenes/edit.ccs", "scenes/edit_source.ccs", "scenes/editor_theme.ccs", "scenes/font_set.ccs", "scenes/remove_styles.ccs", "scenes/morph.ccs", "scenes/shapes.ccs", "scenes/effects.ccs"]
theme = "dark_documentary"
[episode]
title = "How Rivers Form"
[output]
fps = 30
width = 1280
height = 720
[shaders]
aurora = "shaders/aurora.wgsl"
glitch = "shaders/glitch.wgsl"
scanlines = "shaders/scanlines.wgsl"
[transition]
kind = "crossfade"
frames = 18Reference
The complete list of focus, style, and shape methods on a node is in Code & node animations.