Skip to content
explainroo

Docs

Scene reference

Every element and option you can use in scenes.js, with examples.

Which time helpers can I use?

Member What it gives you
s.t seconds since the scene started
s.T seconds since the video started
s.dur how long the scene is, in seconds
s.voice when the voice starts and ends in the scene, as { start, end }
s.words every spoken word as { text, start, end }, in seconds since the scene started
s.cue(word, n) the moment the voice starts saying a word or phrase. n picks the n-th time it is said, and 1 is the default
s.cueEnd(word, n) the moment the voice finishes that word or phrase
s.mark(name) the time of a [#name] marker in the script
s.time(v) turns a number, a word or "#marker" into seconds
s.pace the pace of the video. 1 is normal
s.p(at, dur, ease) a number that goes from 0 to 1 while your own animation runs. dur is 0.6 and ease is inOut by default
s.since(at) how many seconds have passed since a time. It is negative before that time
s.between(a, b) true while the time is between a and b
s.video facts about the whole video, as { duration, frames, fps, width, height, scenes }

A word that is spoken more than once means its first time. Add # and a number for a later time: 'fills#2' is the second time “fills” is spoken in the scene, the same as s.cue('fills', 2). This works wherever a time goes. Only digits after the last # count, so 'C#' stays a word and '#ask' stays a marker.

The easing names are linear, in, out, inOut, outBack, outElastic, outQuart and inOutSine. For your own math you can use s.ease.out(p), s.lerp(a, b, p) and s.clamp(v, lo, hi).

Which layout helpers can I use?

Member What it gives you
s.W, s.H the width and height of the canvas
s.cx, s.cy the middle of the frame. On Shorts, TikTok and Reels it is the middle of s.safe
s.portrait true when the frame is taller than wide
s.safe the area where content belongs, as { x, y, w, h, left, top, right, bottom }. It leaves a 7% margin and keeps out of the captions and the spots a platform app covers
s.full the whole frame in the same form, for pictures that fill a tall frame. See filling the frame
s.row(n, { width, x }) n x positions spread over a width
s.col(n, { height, y }) n y positions spread over a height, centered in s.safe
s.grid(cols, rows, { x, y, w, h, gap }) grid cells as { x, y, w, h }, row by row. Without a size, the grid fills s.safe
s.get(id) the position and size of an element you drew earlier with that id

Most elements return { x, y, w, h, left, right, top, bottom }, so you can put the next element below or beside them. The box is the element once it has arrived, without its entrance or its shadow.

For sizes that fit wide and tall frames alike, take a unit from the short side of s.safe, like const u = Math.min(s.safe.w, s.safe.h) / 100. Then size text with it, like size: u * 7. The scenes.js that init writes sizes its text this way.

Which options do most elements take?

Option What it does
at, out when the element appears and when it leaves
enter how the element comes in: draw, write, type, words, sync, pop, rise, fade, zoom, drop, slide-left, slide-right, slide-up, slide-down or none
exit how it leaves: fade, pop, rise, drop, slide-left, slide-right or none
dur how long the entrance takes, in seconds
outDur how long the exit takes, 0.45 seconds by default
id a name, so that arrows, annotate and s.get can find the element
color a color name like blue, accent or muted, or any CSS color
opacity, scale, rotate make it see-through, change its size, or turn it. rotate is in degrees
float how many pixels the element drifts slowly, so a still element does not look frozen
sfx the sound it makes when it comes in, or false for no sound. After s.quiet(), true gives it its usual sound
overlap true when the shape may cover other shapes, so check says nothing about it
camera false keeps the automatic camera from moving to the element

at and out take seconds, a spoken word or a "#marker". out is when the exit starts. To swap text in the same place, use exit: "none" on the old text and the same time for the new text’s at.

Use at: -1 for an element that was already on screen in the scene before. It is there from the start of the scene and makes no sound.

How do I draw text?

s.title("How DNS works", { at: 0 });
s.subtitle("the phone book of the internet", { at: 1 });
s.text("Every site has an *address*", { y: 300, size: 64, at: "address" });
s.note("about 20 ms", { x: 1400, y: 820, at: 3 });

s.title uses the display font at 104 pixels and sits centered a little above the middle. s.subtitle is 46 pixels in the muted color. s.note is small text at 32 pixels in the muted color. All three take the same options as s.text:

Option What it does
x, y where the text sits. By default this is the center of the text
size the font size in pixels, 48 by default
font which font: display, body, hand or mono
weight, bold how heavy the font is, or bold: true
align left, center or right. With left, x is the left edge
valign top, middle or bottom. With top, y is the top edge
maxWidth, lineHeight the width where the text wraps to a new line, and the space between lines
color, mark the text color, and the color of words in *stars*
bg, padding, radius, border a card behind the text, with its padding, corner radius and border

With "defaults": "0.6", a title or a subtitle that wraps breaks into lines of about the same length, so no word is left alone on the last line. Text on a card of its own (bg) does the same. balance: false keeps the old lines. Notes are then 34 pixels. See the defaults of 0.6.

Text has a few extra ways to come in. write writes it on the screen. type types it letter by letter, and cps sets the typing speed. words shows one word after another. sync shows each word the moment the voice says it. Words in *stars* get the accent color. \n starts a new line.

In a video in several languages, s.str('key') gives the text for the language being drawn, from its strings file. s.lang is that language’s code.

How do I show a math formula?

Use s.math(tex, options) for a TeX formula:

s.math("E = mc^2", { y: 420, size: 96, at: "energy" });
s.math(String.raw`\int_0^1 x^2\,dx = \tfrac{1}{3}`, {
  x: 960,
  y: 700,
  size: 72,
  color: "accent",
  at: "#integral",
  id: "area",
});
Option What it does
x, y the center of the formula
size the font size, 72 by default
color the formula’s color, ink by default
align center or left. With left, x is the left edge
display true for display style, the default

The common element options work too. It returns a box like text, and its id works with arrows and s.annotate. In paper, chalk and blueprint, the outlines draw first and then fill in. Other looks use their text entrance. fade, pop and rise also work.

Double backslashes in a regular string, such as "\\frac{1}{2}". String.raw keeps them as written. Keep every formula fixed and always call s.math. Use at and out to change formulas. check reports formulas that change with time or appear only later. They are typeset on your computer and cached in build/math/.

How do I draw shapes and arrows?

s.box("Resolver", {
  id: "res",
  x: 960,
  y: 540,
  icon: "server",
  color: "blue",
  at: "resolver",
});
s.circle({ id: "you", x: 400, y: 540, r: 90, label: "You", at: 0.5 });
s.arrow("you", "res", { label: "asks", at: "asks" });
s.arrow([300, 900], [1600, 900], { bend: 0.3, dashed: true, at: 5 });
s.line(
  [
    [200, 800],
    [900, 700],
  ],
  { color: "accent", at: 2 },
);
s.path("M0 0 C 200 -150 400 150 600 0", { x: 660, y: 540, at: 3 });

s.box(label, options) is a card that fits its label:

Option What it does
w, h, minW, minH a fixed size, or the smallest size the box may have. minW only widens the box, and the label still wraps
maxWidth where the label wraps, 560 pixels by default. With a fixed w, it wraps inside that width. To keep a long label on one line, raise maxWidth
size, font, weight the font of the label. The size is 44 pixels by default
icon, iconSize, iconColor an icon above the label, with its size and color
color the color of the outline. The box gets a light fill in the same color
fill your own fill color, or 'none' for no fill
stroke, width, dashed, border the color of the outline, how thick it is, and whether it is dashed. border: false removes it
radius, shape how round the corners are, or shape: 'ellipse' for an oval
textColor the color of the label. Without it, explainroo picks a color that reads well on the fill
fillStyle how the fill is drawn in the hand-drawn looks: solid, hachure, cross-hatch, zigzag or dots

s.circle({ r, label }) takes the same options. r is the radius.

s.arrow(from, to, options) draws an arrow from one point to another. A point can be [x, y], { x, y } or the id of an element. With an id, the arrow stops at the edge of that element. bend curves the arrow, from about -1 to 1. A positive bend curves to the right of the way the arrow points, so an arrow from left to right sags down. A negative one curves to the left. head puts the arrow head at the end, at the start, at both ends or nowhere with none. The other options are label, labelSize, labelColor, labelFont, labelOffset, labelSide, gap, dashed, width and headSize. s.connect does the same thing. labelFont defaults to the handwritten font. Use "body" for a label that suits the clean look.

A label sits above a straight arrow, left of a vertical one and on the outer side of a curve. labelSide: 'right' puts it on the right instead, and labelSide: 'below' puts it under the arrow. 'left' and 'above' work the same way. That helps when the usual side is near the edge or full. check treats the label as text.

With "defaults": "0.5" in video.json, a label is 48 pixels and sits 44 pixels from the arrow, and an arrow leaves or reaches a labelled icon outside its label. gap is the space in pixels between each end and what it points at, 14 by default at an element. With the line, gap also counts at an end that is a point. With "defaults": "0.6", the label is 40 pixels, a little smaller than the words on a card. A project without the line keeps labels of 34 pixels. See the defaults line.

s.line(points, options) draws a line through several points. s.path(d, options) draws an SVG path.

How do I show icons and images?

s.icon("database", {
  x: 1500,
  y: 540,
  size: 140,
  color: "purple",
  at: "database",
});
s.icon("lock", { bg: "circle", color: "green", label: "Encrypted", at: 4 });
s.image("assets/app.png", {
  w: 1100,
  frame: "browser",
  url: "app.example.com",
  at: 0.5,
});
s.image("assets/cables.png", {
  w: 1000,
  frame: "card",
  at: "cables",
  kenburns: true,
});

There are 1,854 icons from Lucide. explainroo icons <word> searches them. Older Lucide names work too. The icon options are size (120 by default), color, weight (the line thickness, 2 by default), bg (true, 'circle', 'square' or a color, for a shape behind the icon), bgScale, label, labelSize, labelColor and labelFont.

An icon returns a box like other elements, and its bottom is the icon’s own, above its label. An icon with a label also returns label, the label’s box, and outer, a box around both. So y: icon.outer.bottom + 40 puts the next thing below the label.

s.image(src, options) draws a file from the project’s assets folder:

Option What it does
w, h the size. If you give only one of them, the image keeps its shape
fit cover fills the box and crops the rest, contain shows the whole image
frame a frame around the image: none, card (a white border), browser, window or phone
url the web address shown in the address bar of the browser frame
radius, border, shadow round corners, an outline, and shadow: false to remove the shadow
kenburns true for a slow zoom
crop [x, y, w, h] in the file’s own pixels, to show only that part of the image

How do I show a video clip?

s.clip(src, options) draws a video from assets/. It takes the same layout, frame and timing options as s.image and returns the same box. Use .mp4, .mov, .webm or .m4v files.

s.clip("assets/signup.mp4", {
  w: 1200,
  frame: "browser",
  url: "app.example.com",
  from: 12.5,
  speed: 1,
  volume: 0.5,
  at: "#demo",
  out: 9,
});
Option What it does
full true fills the whole frame, edge to edge, in any size. It has no frame, shadow or entrance, and it ignores x, y, w and h. Use it for a clip behind the whole scene
from the second in the file where playback starts, 0 by default
to the second in the file where playback stops, the end of the file by default. speed and loop take the part from from to to as the clip
crop [x, y, w, h] in the file’s own pixels, to show only that part of the picture
speed playback speed, from 0.25 to 4, with 1 as the default. Playback starts at at. 'fit' picks the speed that makes the clip end with the scene, or at out, from half speed to twice the speed
loop true repeats the whole clip. The default is false, which holds the last frame
volume sound level from 0 to 1, with 0 as the default. Sound plays only at speed: 1

Sound stops at out, the clip’s end or the scene’s end. Loops repeat the sound too. Music gets quieter under clip sound. check warns when the last frame stays frozen for more than 0.3 seconds, and says how long it is. An AI video clip plays the same way.

Frames are saved once at the project’s frame rate in build/clips/, at most 1920 pixels wide. Renders, stills and sheets use those frames. The preview may briefly show a nearby frame while one loads. Trim long footage before adding it to save disk space. You can delete build/clips/. Record at a fixed frame rate when the motion matters, since conversion can repeat or drop frames.

One long screen recording can serve several scenes, each with its own from and to. See product demos. s.video still holds facts about the whole video.

s.presenter(name) plays a clip of an AI presenter next to the content, with the layout options of a clip. See presenters.

How do I play a recording of the app?

s.recording(name, options) plays a recording that explainroo record made of the real app. A pointer moves from step to step, and a ring shows each click.

const r = s.recording("new-invoice", {
  frame: "browser",
  url: "app.example.com",
  cues: { saved: "#saved" },
});
s.text("Saved", { y: 120, at: r.times.saved });

It takes the layout, frame and timing options of s.clip, apart from crop. Without w and h, it fills s.safe. from and to play part of the recording, so one recording can play in several scenes.

A step with a cue happens on its word. The video holds the frame before the step until its action lands on the cue, or it plays up to twice as fast when the steps before take longer than the words. check warns about a step that is still late. Steps that share a cue run in order, and the first one lands on it.

Option What it does
cues cues set in the scene, by a mark’s name or a step’s number, like { saved: "#saved" }
cursor: false no pointer
zoom: false no zooms from the zoom steps
sound: false no click sounds

r.times has the scene time of each step, by its number and by the name of its mark. The zoom steps are the camera of the scene, so the automatic camera keeps out.

How do I point something out?

s.text("It is *not* magic", { id: "claim", y: 400, at: 0 });
s.annotate("claim", { type: "underline", at: "magic" });
s.annotate(
  { x: 960, y: 700, w: 400, h: 90 },
  { type: "circle", color: "red", at: 3 },
);

s.annotate(target, options) draws a mark on an element or on an area. The target is an id or a box like { x, y, w, h }. The type of mark is underline, circle, box, highlight, strike, cross or bracket. The other options are color, padding, width and dur, and out says when the mark leaves.

A mark stays until its own out. With "defaults": "0.5" in video.json, a mark on an element’s id without an out of its own leaves with that element. Without the line, give the mark the element’s out. check warns about a mark that stays more than 0.3 seconds after its element has left.

An icon’s id covers the icon without its label, so a circle around it can run through the label. check warns about that. Mark a box that holds both instead.

How do I show lists, numbers and charts?

s.list(["Browser cache", "Resolver", "Root server"], {
  x: 360,
  y: 280,
  at: "first",
  stagger: 1.1,
});
s.number(1500000, { suffix: " requests", at: "million", y: 480 });
s.bars(
  [
    { label: "2023", value: 19 },
    { label: "2024", value: 31 },
  ],
  { at: 1, suffix: "k" },
);
s.lineChart([3, 5, 4, 8, 13, 21], {
  labels: ["M", "T", "W", "T", "F", "S"],
  at: 1,
});
s.pie(
  [
    { label: "Images", value: 55 },
    { label: "Other", value: 45 },
  ],
  { donut: 0.55, at: 1 },
);
  • s.list(items, options) shows a list. Each item is text or { text, at, icon, color, out }. For a list, x is the left edge and y is the top. The options are size (50 by default), width, gap, bullet (dot, dash, number, badge, check, arrow or an icon name), card: false (no cards in studio), bulletColor, at (one start time, or one time per item), stagger (the seconds between items, 0.7 by default) and enter. Each item plays a note, and the notes go up as the list grows.
  • s.number(value, options) counts up to a number with a ticking sound. The options are from, dur (1.4 seconds by default), prefix, suffix, decimals, separator, group: false and locale, like 'de-DE', plus the text options. With "defaults": "0.6", it writes the number the way the video’s language does. In a French video, 1050 with suffix: " €" shows as 1 050 €, and the unit stays with the number.
  • s.bars(data, options) draws a bar chart. The options are x, y, w, h, max, stagger, growDur, values: false, prefix, suffix, format(v), labelSize, valueSize and fillStyle. Each bar is { label, value, color, at }. Its own at grows the bar on that cue.
  • s.lineChart(values, options) draws a line chart. The values are a list of numbers, or several lines on one scale, like { Compound: [...], Simple: [...] } or [{ label, values, color }]. Each line gets its name at its end, in the look’s colors. The options are x, y, w, h, min, max, labels, dots: false, area (on for a single line), color and dur.
  • s.pie(data, options) draws a pie chart. The options are x, y, r, donut (from 0 to 0.8, for a ring with a hole in the middle), labels: false, labelOffset and dur.

In charts, x and y are the center of the plot, and w and h its size. Two lines on one scale make a comparison easy to see:

const years = Array.from({ length: 21 }, (_, n) => n);
s.lineChart(
  {
    "Compound interest": years.map((n) => 1000 * 1.05 ** n),
    "Simple interest": years.map((n) => 1000 + 50 * n),
  },
  { labels: years.map((n) => (n % 5 ? "" : `${n} years`)), at: "twenty" },
);

How do I show code?

s.code("const res = await fetch(url);", {
  lang: "js",
  title: "app.js",
  at: 0.5,
});
s.terminal(["$ npm install", "added 42 packages in 3s"], { at: 1 });

s.code(source, options) shows code in a window. It colors the code for js, ts, py, go, rust, php, sh, sql, json and md. The options are x and y (the window’s center), lang, size (32 by default), w, title (a file name, or false for no title bar), lineNumbers: false, reveal (type, lines or none), cps and lineDelay. highlight picks lines to highlight and highlightAt says when. explainroo check tells you when a line is too wide for the window.

How do I show code changing?

Pass a list of steps to s.code when you want to explain a fix or a refactor:

s.code(
  [
    { code: "const total = items.length;", at: 0.5 },
    {
      code: "const total = items\n  .filter((i) => i.active)\n  .length;",
      at: "filter",
      highlight: [2],
    },
    {
      code: "const activeCount = items\n  .filter((i) => i.active)\n  .length;",
      at: "#rename",
    },
  ],
  { lang: "js", title: "stats.js", morphDur: 0.8 },
);

Each step has code and at, and can set highlight. Parts that stay slide into place. Removed parts fade out, and new parts fade in. morphDur sets the change’s length, 0.8 seconds by default, shortened by pace.

The element’s at defaults to the first step’s time. reveal applies only to that step. A step’s highlight replaces the earlier one. The window fits the widest step, up to 86% of the frame, unless you set w. Its height follows the line count. check checks every step’s lines.

How do I show a terminal?

s.terminal(lines, options) shows a terminal window. Lines that start with $ are typed in as commands. The other lines appear as output. A line can also be { cmd, at } or { out, at, color }. { cmd } adds the prompt itself, so write { cmd: "npm test" } and not "$ npm test". The options are w, size, rows, title, prompt, cps (22 by default), outputDelay, lineGap and cursor: false, which hides the cursor. With "defaults": "0.6", no cursor waits under the output that ends a terminal. With "defaults": "0.5", a terminal without an at of its own comes in with its first line. check warns about a terminal or a code window that is empty for more than a second.

How do I draw app screens?

s.ui has the parts you need for a product demo. Set the app’s brand in video.json, then use const u = s.ui. Positions count from the frame’s top left. Times take seconds, words or "#markers".

Call What it draws
u.backdrop(color?), u.floaters({ alpha, tint }) the brand background, and faint cards drifting around the edges (tint: 'white' on a colored background)
u.panel(x, y, w, h, { at, out, from, rise, sfx, fill, border, lift }, fn) a card that pops in at at; fn(x, y, w, h) draws inside it
u.card(x, y, w, h, o) a card without an entrance
u.browser(x, y, w, h, { url, at }, fn) a panel with a browser bar; fn gets the page area
u.modal(x, y, w, h, { at, out }, fn), u.over(fn) a dialog over a dimmed frame, and a layer for your own menus or popups; check does not report text in one layer as overlapping text in another
u.toast(text, { at, out, icon, tone, size }) a message that slides up, with a chime
u.headline(text, x, y, { size, at, out, align, weight, italic, color, accent, tracking, lineHeight }) big text that blurs in word by word; *stars* mark words in the accent color, and with "defaults": "0.6" \n starts a new line
u.text(text, x, y, { size, weight, color, accent, align, base, lh, font, italic, tracking, alpha, maxW, check }), u.para(text, x, y, maxW, o), u.eyebrow(text, x, y) text, wrapped text (\n starts a new line), and a small uppercase label; font is 'ui', 'headline' or a family; check: false hides text from check; u.measure(text, size, weight) gives the width. With "defaults": "0.6", *stars* and \n work in all three
u.button(label, x, y, w, h, { variant, press, icon, loading, color, caps }) primary, secondary or ghost
u.input(x, y, w, h, { label, value, placeholder, focus, caret, icon }), u.textarea(x, y, w, h, o) a text field, and a taller one where the text wraps
u.select(x, y, w, h, { label, value, open, options, hover, selected }) a dropdown; returns the y of each option row
u.toggle(x, y, on), u.checkbox(x, y, on), u.radio(x, y, on) on goes from 0 to 1, so it can animate
u.chip(label, x, y, { selected, size }), u.chipWidth(label, o, selected) option chips; a selected chip grows by its check mark, so leave room
u.pill(label, x, y, tone, { align, dot, pulse, size }) a status badge: gray, blue, yellow, green, red, purple or accent
u.spinner(x, y, r), u.progress(x, y, w, p), u.avatar(x, y, r, initials, { image }), u.icon(name, x, y, size, color), u.lines(x, y, w, n), u.logo(x, y, w) small parts; lines draws grey bars in place of body text
u.pop(at, cx, cy, fn, { sfx }) pops anything in around a point
u.wipe(at, { color }) the accent color spreads out until it fills the frame
u.cursor(keys, { out }) the mouse; a key { at, x, y, dur, arc } moves it (arc bends the path, 0 is straight), { at, click: true } clicks with a sound
u.press(at), u.typed(text, at, cps, { gain, sfx }), u.stream(text, at, wps), u.count(to, at, dur) a button press (0 to 1 and back), text typed so far with typing sounds, text appearing word by word like an AI reply, a number counting up
u.colors accent, accentDark, accentTint, background, ink, text, soft, muted, faint, line, field, surface, panel; brand.colors in video.json replaces the grays, for example { "text": "#141413", "line": "#e8e6dc" } for a warm brand

Draw the background first, then set the camera, draw the headline and screens, and draw the cursor last. A cursor move starts at at and arrives after dur, 0.7 seconds by default. Start the move before the click, and leave the first 0.7 seconds of a scene free of clicks while the transition finishes.

check measures the words of pills, chips and toasts like any text, so they must read on a phone. With "defaults": "0.6", they are 40 pixels or more in a tall frame, and size sets them.

Panels and headlines make no sound unless you pass sfx. Keep long typing quiet with u.typed(text, at, cps, { gain: 0.25 }). check sees UI text, but it cannot tell whether clipping hides it. Look at stills during camera zooms and make sure the cursor does not hide words.

How do I move the camera or draw something myself?

  • s.camera([{ at, x, y, zoom, dur, rotate }]) moves the view by hand instead of the automatic camera, for example to zoom in on one part of a diagram. Call it first in the scene function, because it moves only what is drawn after it. check gives a hint when it comes later. s.camera([]) keeps the scene still.
  • s.focus(target, { at, out }) points the automatic camera at an element or a region on a cue. target is an id drawn earlier, { x, y, w, h } (center and size) or [x, y, w, h]. It draws nothing.
  • s.group({ x, y, scale, rotate, at, out, enter }, () => { ... }) moves a set of elements together. Positions inside the group count from its x and y.
  • s.draw({ x, y, at, out }, (ctx, life) => { ... }) gives you the canvas, so you can draw anything the other methods cannot. life.p goes from 0 to 1 while the element comes in. s.ctx is the canvas too. check only knows that a drawing is there. Give s.draw a box: { x, y, w, h }, the center and the size of what you draw, around 0, 0 like the drawing itself. Then check treats the drawing as a shape. It finds overlaps and text over its edge, and a drawing that runs off the frame or out of the safe area. round: true in the box makes it an ellipse.
  • s.burst({ x, y, at, count, colors }) throws confetti.
  • s.bg(color) paints over the background.
  • s.rand(i), s.noise(x) and s.wiggle(amount, speed) give random values that are the same on every render.

How do I make a Short, a TikTok or a Reel?

s.short draws the parts these videos often share. It works best with a platform size like tiktok and with "captions": "bold".

Call What it draws
s.short.backdrop(options) a dark background for the whole frame, with a gradient and an optional glow behind the subject. Call it first in the scene
s.short.hook(text, options) the title for the first frame, big and uppercase, at most two lines. It is there from the first frame, so the frame says what the video is about. Words in *stars* are keywords
s.short.kicker(text, options) a small uppercase label in a pill, like “QUICK TIP”
s.short.sticker(text, options) a tilted box with bold text for one fact, like "*2* days"
s.short.step(n, total, options) “STEP 2/4” with dots at the top of the content area
s.short.progress(options) a thin bar that fills over the whole video
s.short.payoff(options) the closing card, with an icon, a bold title and up to three short lines with check marks
s.short.punch(at, options), s.short.drift(options) camera keys for s.camera([...]): a fast zoom onto one spot and back, or a slow push in
s.short.jolt(at), s.short.flash(at), s.short.interrupt(at) jolt shakes the frame for a moment and flash covers it with white. interrupt does both with one sound, for the start of a new part

The kit draws straight on the canvas, so the automatic camera keeps what it draws as one block. A scene that wants a move sets its own camera first, with keys from punch or drift. The backdrop is dark, so give text on it a light color, like color: '#ffffff'. check measures text against the backdrop.

hook(s) {
  s.short.backdrop({ color: "#1b2440", glow: "#f59f00", pattern: "dots" });
  s.short.hook("Your bread is *stale* again?", { kicker: "Kitchen tip" });
  s.short.sticker("*2* days", { x: s.cx + s.safe.w * 0.22, y: s.safe.y + s.safe.h * 0.8, at: "#days" });
},

With "defaults": "0.6", the words the kit draws by itself, like “STEP” in s.short.step(), come in the video’s language, like “PASO 2/4” in Spanish. The kit’s keywords and the pill of a kicker take brand.accent from video.json, made darker or lighter until they read.

How do I fill the whole tall frame?

On Shorts, TikTok and Reels, s.safe ends above the captions. A scene placed only from it leaves the lower part of the frame empty. With "defaults": "0.6", pictures and backgrounds may run into the parts that the app covers with its buttons and the post text. Words, and the details a viewer must see, stay in s.safe.

s.full is the whole frame, as { x, y, w, h, left, top, right, bottom }. This band fills the bottom of the frame behind the captions:

const band = s.full.bottom - s.safe.bottom;
s.box({
  x: s.full.w / 2,
  y: s.safe.bottom + band / 2,
  w: s.full.w,
  h: band,
  fill: s.tint("blue"),
  border: false,
  radius: 0,
  at: "hum",
});

check still warns about text outside s.safe, and about a picture that runs off the frame. Look at the frame with still --platform tiktok, because the app’s buttons may cover part of a picture down there. Keep what matters higher up.

How do I add a sound?

s.sfx(name, at, { gain, dur, pitch }) plays a sound effect at a time. The names are pop, click, whoosh, swipe, tick, type, ding, chime, thud, scribble, chalk, rise, sparkle, blip and error. dur sets how long type, scribble, chalk and rise last. A rise ends at at + dur, so start it a little before the thing it leads up to.

s.sound('assets/door.mp3', { at, out, volume }) plays your own sound file on a cue. It takes the same files as a music file. volume goes from 0 to 1, with 0.5 as the default. It stops at out, at the end of the file or at the end of the scene, whichever comes first. It is lowered while the voice speaks, so put a sound that matters in a pause. Always call it, and use at and out for the timing, as for other elements. "sfx": false doesn’t turn it off.

s.quiet() at the top of a scene turns off the entrance sounds of everything after it in that scene. That helps when twelve circles appear and one sound is enough. Give that one sfx: true. The chime of u.toast and the swipe of u.wipe count as entrance sounds too.

Which colors and fonts can I use?

The color names are accent, ink, muted, bg, surface, red, orange, yellow, green, teal, blue, purple, pink and gray. Each look has its own shade of each color. So a scene keeps working when you change the look. s.color(c) takes a color name or any CSS color, like '#c2410c', and gives you the CSS color. s.tint(c) gives you the light version of it that is used for fills.

There are four fonts. display is for titles, body for normal text, hand for handwriting and mono for code.