--- name: logo-reveal description: Animate a brand mark on a NodeTool timeline — stroke draw-on, mask wipe, staggered build, wordmark type, an idle loop, and landing the settle on a sound logo. Use for an intro sting, a sign-off, an end card, a splash, or a loader loop. Not for designing the mark itself. featured: true --- # Logo Reveal → the sting One idea per logo. Draw it, build it, or wipe it — never all three. The whole reveal runs 800–2500ms and ends on the clean mark, held. `motion-graphics` is the build loop; `timeline-edit-ops` carries the op contract. `motion-direction` decides which personality the sting belongs to; a logo is usually the one place a piece is allowed its overshoot. ## The same three patterns, in code **Draw-on** — a path element and `el.draw()`, which is `enter({from: {trimEnd: 0}})`: ```js // fragment const mark = s.path([["M", -40, 30], ["L", 0, -30], ["L", 40, 30]], { stroke: "#111111" }); mark.draw({ dur: 1.1, ease: "cubic-bezier(0.65,0,0.35,1)" }); ``` **Wordmark stagger** — `by` and `staggerMs` on `el.enter()`, per character for a short mark: ```js // fragment const wordmark = s.text("Northwind", { size: 64, weight: 700 }); wordmark.enter({ from: { scale: 0.9, opacity: 0 }, dur: 0.38, by: "character", staggerMs: 45 }); ``` **Lockup row** — mark beside wordmark, one flex container (`frame-composition` covers the container vocabulary in full): ```js // fragment s.row([mark, wordmark], { gap: 16, align: "center" }); ``` Reveal the container first if the mark and wordmark should feel like one build, or stagger the row's own children through `s.stagger()` for a build-on that still keeps its lockup spacing. ## Pick one technique | Technique | Best for | How, on the timeline | |---|---|---| | Stroke draw-on | Line marks, monograms, signatures | A `path` shape clip, `trimStart`/`trimEnd` animated | | Mask wipe | Filled shapes, gradient marks | `wipe` preset, or a `set_mask` rect animated | | Build-on | Multi-part marks, grids | One clip per part, in a group, `delayMs` staggered | | Wordmark | Logotypes | A text clip with a per-character or per-word `stagger` | | Idle loop | Loaders, ambient splash | `breathe` or `rotate` as a `loop` | | Scale settle | Anything, when in doubt | `pop` with a small `overshoot` | ## Stroke draw-on `trimStart` and `trimEnd` stroke only a sub-range of a path, 0..1. Drawing is `trimEnd` running 0 to 1 with the stroke visible and the fill arriving after: ```json {"op": "add_shape_clip", "shape": {"kind": "path", "d": "M0.1 0.7 L0.3 0.2 L0.5 0.7", "stroke": "#111111", "strokeWidthPx": 6, "lineCap": "round", "trimStart": 0, "trimEnd": 0}, "durationMs": 2000} ``` ```json {"op": "animate_clip", "target": "Path", "animations": [ {"role": "in", "preset": "custom", "durationMs": 1100, "easing": "cubic-bezier(0.65,0,0.35,1)", "curves": [{"property": "trimEnd", "keyframes": [ {"t": 0, "value": 0}, {"t": 1, "value": 1}]}]}]} ``` Both trim channels fold as **replace**: one animation drives one of them, and two overlapping in time means the later wins and the earlier is discarded, reported as `replace_curves_overlap`. Draw with `trimEnd` alone; use `trimStart` chasing it only for a stroke that draws and erases, and put the two in separate windows. Stagger several strokes by giving each path clip's animation a `delayMs` 100–200ms apart, in the order a hand would draw them. A `d` the renderer cannot parse is refused at the call rather than stored, and an unknown `kind` reports `unknown_shape_kind` and draws nothing — a mark that vanished is usually one of those two. ## Mask wipe For a filled mark, reveal rather than draw. `{"role": "in", "preset": "wipe", "durationMs": 700, "params": {"direction": "left", "softness": 0.05}}` sweeps a feathered edge across the layer. `softness` 0 is a hard edge, which reads as graphic; 0.1 reads as light passing over. A `custom` curve driving `wipeProgress` needs a `mask` on the same animation — `{direction, softness}` — because a wipe with no edge to run against cannot be inferred. `wipeProgress` folds as **min**, so two wipes on one layer keep the tighter one. ## Build-on One clip per part, each on its own overlay track, all parented to one group with `add_group`. Reveal the container first, then accents, then the wordmark last so the eye lands on the name. ```json {"op": "animate_clip", "target": "Mark part 2", "animations": [ {"role": "in", "preset": "pop", "durationMs": 500, "delayMs": 160, "params": {"overshoot": 1.12}}]} ``` Steps of 80–120ms. Then animate the **group** for anything that moves the whole assembly — one `fade` out on the group beats three chances for the parts to drift apart. The group's opacity multiplies into each child. When the parts already have animations, `{"op": "stagger_animations", "clip_ids": ["mark-id", "wordmark-id", "tagline-id"], "offset_ms": 100}` adds delay in that order without moving their media. Use clip IDs, with at least two, and give every selected clip an animation first. The first keeps its delay. For a repeating field of identical marks, the document's `repeater` sets copy count, position step, and time step on one clip. Use a repeater for a pattern, and separate clips for parts that need distinct art. Keep a follower aligned by storing `animationLinks` on its clip when it should read the mark's position, scale, rotation, or opacity. A link follows the source's authored animation and does not chain through other links. Put the mark and the wordmark in a flex row (`layout: {display: "flex", flexDirection: "row", gap, alignItems: "center"}` on a group clip, both as its real children via `parentId`) so the wordmark's position follows the mark's changing box; a backing plate is a third child with `flexItem: {position: "absolute", inset: 0}` behind the wordmark. These are document fields, so read and write the full document with `get_timeline` and `set_timeline_document`. `set_clip_params` refuses `repeater`, `animationLinks`, and `layout`. ## Wordmark A text clip with a stagger. Per character for a short mark, per word for a lockup with a tagline: ```json {"role": "in", "preset": "pop", "durationMs": 380, "stagger": {"unit": "character", "offsetMs": 45, "from": "start"}} ``` Work the span out — `durationMs + offsetMs × (units − 1)` — and compare it against the clip. A span that does not fit is compressed silently, and a compressed wordmark is the reason a reveal "does not look staggered". `stagger_compressed` is the warning. Set the family to a bundled one — `Inter`, `Space Grotesk`, `Bebas Neue`, `Playfair Display`, `Lora`, `JetBrains Mono` — or the render and the editor resolve it differently and the lockup's width changes under you. ## What not to do to a mark Scale, opacity, rotation about the mark's own anchor, and position are safe. `scaleX` and `scaleY` on their own are not: `squash` distorts the geometry, and a distorted logo is a brand problem rather than a taste one. Keep motion out of the clearspace — a support element sliding through the mark's margin cheapens it. ## Landing on a sound logo Decide the audio first. A sound logo that does not exist yet is a 1–3 s effects brief — source, action, space — on ElevenLabs sound effects (`elevenlabs-audio-prompting`) or a `TrackType: SFX` prompt on Stable Audio (`stable-audio-prompting`); a 30-second request for a hit is a hit followed by room tone. Then `detect_audio_events` on the sting reports `onsets.times` in **seconds**; multiply by 1000. Time the reveal so the mark reaches full opacity and scale on that millisecond: `delayMs` on the `in` animation = onset ms − the clip's `startMs` − `durationMs`. Then add the accent on the hit itself, as a separate emphasis: `{"role": "emphasis", "preset": "pulse", "durationMs": 220, "delayMs": , "params": {"intensity": 0.04}}`. A settle that floats free of the sound reads as cheap even when both are good. `snap_to_beats` is the blunt version when the whole end card has to sit on the grid. For a mark that should stay on beat if the sequence tempo changes, put a `beat: {index, scope: "sequence", offsetMs?}` anchor on its animation in the full document. The index is one-based and `delayMs` adds to it. The `animate_clip` tool input does not accept `beat`. Keep the stored animation's `id`, `role`, `preset`, and `durationMs` when adding it. If the sound logo's measured onset is the source of truth, keep the absolute delay or use `bake_audio_animation` for audio-driven motion instead. ## Idle loop For a loader or a splash that waits, a `loop` at low amplitude: `breathe` with `intensity` 0.02–0.04, or `rotate` on a mark that is radially symmetric. Set `durationMs` to the cycle, 2000–4000ms, and remember a cycle longer than the clip shows a fragment of the motion. Never loop a wordmark somebody is reading. ## Hold the end frame The last thing on screen is the static mark. Leave at least 800ms after the reveal completes with nothing animating — an end card that cuts the instant it settles reads as a mistake. On a 2500ms clip with a 1100ms draw and a 220ms accent, that is satisfied; check it rather than assuming. ## Start from a rig when it fits `list_compositions` and `get_composition` show the shipped `logo-sting` rig and its parameters. `insert_composition` places it as a group with fresh clip IDs. Inspect those IDs before changing its timing or staggering its parts. Use the `title-slam` or `word-cards` examples for a typographic reveal; they are starting documents rather than tool presets. ## Check it - `validate_timeline`: `replace_curves_overlap` on the trims, `stagger_compressed` on the wordmark, `animation_exceeds_clip` on a reveal longer than its clip, `unknown_shape_kind` on a mark that draws nothing. - `preview_timeline_frame` at the midpoint of the draw, a held moment, and the last frame. The midpoint is where a half-drawn path or a stroke that never closes shows up; the last frame is where you confirm the mark is clean and complete. - Then `render_timeline`, because a sting is judged at speed.