Copperplate Engraving — our demo

One example among many. Don't reuse its story, arc, shots, props or timings.

Demo: The Honeybee, Plate VII (40.0 s) · engraving.mp4 · source in demo/ · all words from demo/content.json

Story & structure

A scene demo, not a story: a museum object label for the screen next to a case. One object (the honeybee), its name, three things you did not know about it, and where to see the real one ("see the living hive in Gallery 4").

Structure (40 s): copper ECU → proof pulled → pull out to the plate → three magnified details (eye, hamuli, corbicula), each flying to its slot → silence → hand colouring → locked final plate → tissue-guard end card.

How the native moves served information in this demo:

Use cases, with a shot structure for each (examples, not recipes):

Use Shot structure
Museum object label (the demo) copper ECU → proof pulled → pull out to the plate → 1–4 magnified details, each flying to its slot → silence → hand colouring → locked final plate
"One minute on …" same, with the narrator carrying the notes; end on the plate as the thumbnail
Textbook figure no copper opening; start on paper at 1×, engrave the figure, then label parts with leaders one by one; no colour or colour only on the part being taught
Craft heritage (a watch, a violin, a knife) ECU of the burin on the object's contour → build → roundels on the mechanism → colour only the material that matters (gold, varnish)
Map or site plan engrave the coastline or plan, roundels become insets (a building, a harbour)

Shots

The camera is a loupe over the plate. Beat by beat in the demo:

Beat Camera Why
Hook (0–2.5 s) 4.2× on copper, follows the burin tip, 3° roll one point of attention; the cut is the hook
Proof pulled (2.5–3.2) locked while the sheet peels across the medium's own transition
Build (3.2–6.2) pull out 4× → 1×, log-zoom with screen-linear dolly feel scale reveal: the curve was a bee's abdomen
Detail 1 (9.4–15.0) push to 4.3× on the eye; ride with the roundel as it lifts off and flies to its slot, easing out to a 1.45× reading frame the signature one-take: where → what → label
Detail 2 (15.0–19.4) tilt down at 1.45× from the upper to the lower left, keeping detail 1's note in frame faster, same grammar
Detail 3 (19.4–23.75) pull back to the full plate while the roundel flies, then push in to read (after detail 2's note has had its time) acceleration; reading time respected
Gather, silence, colour full plate; a 2 % breathing push during colour the colour moves, not the camera
Final plate locked off the poster

Motion timings in this demo: the first line is one stroke followed by the camera, 5.4 s long, so at frame 0 it is already a third cut; strokes run 6–90 at once; roundels lift 30 px at mid-flight and grow to radius 120; colour regions bloom over 1.25–1.6 s each and start 0.42 s apart.

Score structure

Three layers: room tone (a quiet workroom with a faint clock on copper; paper room after), foley keyed to picture events, and a baroque chamber score.

Score form, bar by bar

Original, written in code (music/score.py, deterministic: a rebuild gives a byte-identical score.wav); no existing or public-domain tunes. Key D major, 96 BPM in 4/4 (beat 0.625 s, bar 2.5 s, 16 bars = 40.0 s). Forces: harpsichord (continuo and solo figures) and a string quartet (solo violin I, ensemble violin II, violas, cellos, contrabass); the strings play pizzicato up to 25.0 s and use the bow only from 26.25 s. Rebuild from the repo root with .venv/bin/python styles/engraving/demo/music/score.py (about 5 s; writes score.wav, stems/, cues.json, CREDITS_music.txt). Level −18.0 LUFS integrated (BS.1770), peak −1.2 dBFS, 48 kHz stereo 24-bit. Stems harpsichord.wav, pizz.wav, bowed.wav (40.0 s each, same gain as the mix, so they sum to the mix before the limiter, about 1 dB of peak limiting on two hits).

The form is a trio-sonata texture: the grammar of engraving, then colour. Times are for the demo's three details.

Bars / time Harmony What happens
1 (0–2.5) — Silent for burin foley. 1.875 / 2.1875 harpsichord pickup A4 → C#5, damped short (below −28 dBFS by 2.3).
2 (2.5) D, A/C# The print is pulled: tutti downbeat accent (harpsichord D chord, cello, contrabass and viola pizz). Cello walking bass and viola in pizz 8ths.
3 (5.0) Bm, G, A7 5.0 violin II enters in 16th pizz, broken-chord "cross-hatching". 6.25 harpsichord 32nd-note arpeggio flourish (G, then A7) rises into 7.5.
4 (7.5) D, G Full texture. 9.375 it thins to a cello pizz A pedal plus a high harpsichord 8th figure (dominant, "curious").
5 (10.0) A pedal 10.425 ring "ting": solo violin pizz A6 with harpsichord A5. 11.275–12.5875 rising pizz and harpsichord scale A4 → A5 (8 notes, 0.1875 s apart). 12.775 landing on low D (cello, contrabass and harpsichord D).
6 (12.5) D, Bm The pizz texture comes back lighter (it restarts at 13.125).
7 (15.0) Em7, A7, D 15.0 Em7 downbeat. 15.5 ting (B6). 15.8–16.66 faster rise (7 notes, 1/7 s apart). 16.8 landing on D.
8 (17.5) Bm, G, A Fuller texture. 19.375 thins to an A pedal (third detail). 19.875 ting (A6).
9 (20.0) D, G 20.0 the violin I pizz melody, the most tuneful moment: A5 F#5 D6 · B5 G5-A5 B5 C#6-D6 · E6 G5 F#5. 21.475 landing on low G. 22.19 the lift: violin II 16th crescendo plus a harpsichord roll.
10 (22.5) Em, A7 23.75 harpsichord descending 16th run A5 → A4 (in octaves), ending on the dominant. The last note is 90 ms long and everything is gated to zero at 25.0. The expected tonic never comes, which is the joke.
11 (25.0–26.25) — Total digital silence (−inf dBFS, reverb tails included).
11b–13 (26.25) D, Bm, G, Em7, A7 → D 26.25 the first bowed notes of the film. Cellos, violas and violins enter pp on D and swell to mp by 27.6. Soprano line A5 B5 · D6 C#6 → D6, bass line descending in thirds D-B-G-E then A → D. The harpsichord is sparse and high: slow broken chords at 27.5, 28.75 and 30.0. 31.25 perfect cadence V7 → I: a low harpsichord D (not rolled) with the upper chord rolled after it, contrabass pizz, and bowed D chord. The tonic then rings.
14 (32.5) D 33.75 wink: one soft solo-violin pizz D6.
15–16 (35.0–40.0) D 35.625 a very soft harpsichord D-A-D roll for the end card. A fade from 36.6 reaches zero at 38.9. Digital silence from 39.0 to 40.0.

Narration windows. Inside 6.4–9.6, 9.8–14.9, 15.3–19.2 and 19.7–23.3 the pizz is short and dry, which keeps 300 Hz–3 kHz transient rather than sustained. During 26.9–30.9 the bowed chords sit mostly in the bass (cellos and contrabass below 300 Hz) and above 900 Hz (violins at A5–D6), with only violas and violin II in the voice band at pp. The harpsichord plays only four soft broken chords there.

Reverb. S.room per stem, applied before the silence gates, so no tail leaks into 25.0–26.25: harpsichord size .28 / mix .16, pizz .30 / .18, bowed .42 / .24. A 35 Hz high-pass on every stem.

Timing check. Each accent in music/cues.json is measured on its own rendered stem, after reverb: the first 30 % rise from the local trough to the local peak in [t − 60 ms, t + 120 ms]; low landings measured below 300 Hz, tings above 2 kHz; tolerance ±42 ms (one frame at 24 fps). For the bowed entry, the first bowed sample above −100 dBFS.

Palette & props

The sheet: 1920×1080 world units, laid paper #f1e8d2, world-space paper texture (fibres, laid lines, sparse foxing #9a6a34). Plate mark (the pressed, bevelled rectangle) at (70, 44)–(1850, 1036): plate tone #e8ddc2 at 55 % inside; bevel dark on the top/left walls, light on bottom/right (light from the upper left). Ruled border: 2.4 px rule + 0.8 px hairline 9 px inside, (100, 72)–(1820, 1008).

Ink: #1c1510. World line widths at 1× zoom: outlines 1.8–2.4 (swelling ×0.45–1.35), first hatching 0.18–2.0, cross-hatching ×0.78, third family ×0.6, hair and stipple 0.6–0.9. Hatch spacing on a 600-px figure: 3.1–3.7 px (first), ×1.1 (cross), ×1.25 (third). Hatch thresholds on a 0–1 tone: first family from 0.14, crossing from 0.46–0.5, third from 0.74–0.78.

Light: {x: −0.55, y: −0.62, z: 0.56}.

Lettering: Bodoni Moda (title 50 px, tracking 0.16; labels 17 px, tracking 0.14), italics for Latin (20–24 px), Pinyon Script for notes (29 px). Subtitles: Pinyon Script 38 px, #2b1e14, on a deckled paper slip (#f4eddb, soft shadow) at the bottom centre, max width 860, up to two lines; on 0.1 s before the line, off 0.6 s after it; never over the call to action.

Roundels: radius 120, double rule (2 px + 0.75 px, 4 px inside), ruled ground of horizontal lines every 11 local units; content drawn for a 500-unit radius and scaled in. Slots in order: upper left (300, 300), lower left (300, 700), lower right (1620, 700), upper right (1620, 300). An unused slot shows the specimen at natural size (plate ≈ 20 cm wide, so a 13 mm bee is 0.19× the main figure).

Leaders: 0.95 px, from a dot on the part to the roundel's rim, routed around wings and body with ≥ 34 px clearance (one waypoint), a small italic figure number near the root.

Hand colouring: abdomen amber #d9962a, thorax ochre #a8743e, head #8a6242, eyes #6e4a3c, wings pale blue-grey #a9c3cf (alpha 0.5), legs #7a5a3c, pollen #eaa21a (alpha 0.95). Multiply over paper; 3.5 px spill, 2/1.5 px misregister, tide-line at the wet edge, low-frequency density lift (0.55), 1–2 backruns in large regions, granulation.

The copper (opening only): base #5e2a12 → #94502c → #4e220e, a swirl of polishing hairlines (overlay), a soft studio window reflected across the plate (four panes, blurred 14 px, over a 90 px room glow), dark room toward the frame edges. Grooves: dark trough #240c05, a bright burr rgba(255,226,186) offset toward the light, a warm glint on the far wall. Burin: lit face #d9e0e6, shadow face #262b30, sharp ridge highlight, brass ferrule, mushroom-shaped wooden handle with grain; turned 0.22 rad (≤ 15°); swarf a thin, narrowing copper ribbon curling irregularly (turns open out as it grows, an occasional twist).

Subjects: the honeybee (subjects/bee.js) with three drawn magnifications — compound eye, hamuli (wing hooks), corbicula (pollen basket) — in subjects/details.js; a second built-in subject, the great scallop (subjects/scallop.js), used by content_alt.json.

End card

The title is part of the plate (engraved, not a separate card). End card: a translucent tissue guard falls over the plate; on it, engraved: film title, style name, LEMO-OPUSCAR, LemoLab × Claude Opus 5.5, three credit lines. The "LemoLab × Claude Opus 5.5" sign-off belongs to this library's demo only; a user's film carries no LemoLab credit and no copy of this card.

Build notes

sh styles/engraving/demo/build.sh              # voice → check → score → events → mix → cue check → srt → frames → mux → stills

Pitfalls tied to this demo's props:

content.json fields

demo/content.json drives every word, number and colour.

Field Type Range If out of range
title string ≤ 22 characters longer titles run past the latin line's width; shorten or drop to 44 px in film.js
latin string ≤ 30 characters read time grows (chars/12 + 1 s must fit before the first push at 9.4 s)
plate_no, series string ≤ 10 / ≤ 48 characters series collides with the title above ~48 characters
subject "bee" | "scallop" built-in subjects unknown → bee; a new subject is a module returning {ink, regions, focus} with an ol0 first stroke (see subjects/scallop.js)
details[] array of {name, latin, note, focus} 1–4 >4 are ignored; fewer leaves slots → the first empty one shows the natural-size figure; the timeline re-flows (first 9 beats, others 7 beats each)
details[].focus string bee: eye, hamuli, corbicula (drawn magnifications); any subject: its focus keys a key with no drawn magnification gets a live magnification of the figure itself
details[].note string ≤ 60 characters (2 lines at 29 px) a third line starts to meet the next roundel
colors[] {region, color} regions of the subject (bee: abdomen, thorax, head, eyes, wings, legs, pollen; scallop: shell, bands, ears) unknown regions are skipped; order = order of colouring
caption string ≤ 50 characters must stay readable ≥ chars/12 + 1 s inside the 4.4 s final plate
signature.left/right string short —
nat_size_label string ≤ 16 characters —
voice.voice, voice.speed, voice.lines[] Kokoro voice, 0.8–1.0, {id, text, asr?} ids: title, d1…dN, close a line longer than its section overlaps the next one; keep d-lines ≤ 4.5 s
end.film_title, end.style_name, end.credits[] strings credits ≤ 3 lines of ≤ 35 characters longer lines break the end card's reading time

Steps: edit (or copy) the JSON → sh styles/engraving/demo/build.sh my_content.json (TTS + whisper check, events, score re-placed from the events, mix, cue check, subtitles, frames). To keep the main film untouched, sh styles/engraving/demo/tools/build_alt.sh my_content.json builds everything in demo/out/alt/ instead (the demo's content_alt.json, 2 details, 35.6 s, passes the cue check).

Minimal example:

{
  "title": "The Great Scallop", "latin": "Pecten maximus", "plate_no": "Pl. XII",
  "series": "Shore Gallery · Shells of the Atlantic Coast", "subject": "scallop",
  "details": [ { "name": "Hinge", "latin": "umbo", "note": "The oldest part of the shell.", "focus": "umbo" } ],
  "colors": [ { "region": "shell", "color": "#d98a5c" }, { "region": "bands", "color": "#a8423a" } ],
  "caption": "Hold a real shell at the Discovery Table",
  "voice": { "voice": "bm_fable", "speed": 0.92, "lines": [ { "id": "title", "text": "Plate twelve. The great scallop." } ] }
}

Swapping this file and running build.sh checks that the engine re-flows; it is not how a user's film is made.

Engine reference

import { B, engraveShape, Wash, drawSheet, engraveText } from './engine/index.js';

// 1. any shape + a light direction → an engraved figure (contour lines follow the form), cut in order
const { ink, shape: spark } = engraveShape(
  'M0 -120 C12 -40 40 -12 120 0 C40 12 12 40 0 120 C-12 40 -40 12 -120 0 C-40 -12 -12 -40 0 -120 Z',  // a four-pointed spark
  { at: B.xf(860, 560, 1.9), light: [-0.6, -0.7], spacing: 4.2, style: 'contour' });               // or style: 'hatch'
engraveShape('M0 0 H150 V26 H0 Z', { at: B.xf(1140, 740, 1), ink, g: 't' });                        // its cursor tail, same ink
ink.schedule(['ol', 'tol'], 0, 1.1, { conc: 2 }); ink.schedule(['h1', 'th1'], 0.8, 2.2, { conc: 40 });
ink.schedule(['h2', 'th2'], 1.6, 2.7, { conc: 40 }); ink.schedule(['h3', 'th3'], 2.2, 3.0, { conc: 40 }); ink.build();

// 2. the "only colour": hand-colour one element and leave everything else in black ink
const w = new Wash({ scale: 1.5 });
w.add({ path: spark.path, polys: spark.polys, color: '#D97757', origin: [860, 560], t0: 3.0, dur: 1.2, alpha: 0.85 });

window.render = t => {
  drawSheet(ctx);                 // paper, plate tone, plate mark
  w.draw(ctx, t);                 // colour under the ink (omit for pure black-and-white)
  ink.draw(ctx, t);               // everything cut by time t; lines being cut grow
  engraveText(ctx, 'THE SPARK.', 960, 170, { size: 50, weight: 600, track: 0.16, p: t - 1.5 });
};

Runnable as demo/example/index.html (node core/render/still.mjs styles/engraving/demo/example 4.5), output in demo/stills/engine_example.jpg.

Main functions:

Function What it does Key parameters
B.shape(d, xf) SVG path → world rings + Path2D xf(x, y, scale, rot, flip)
B.Ink ordered, width-bucketed stroke store group(name), schedule(group, t0, t1, {conc}), instant(group), draw(ctx, t, {color, wScale, dx, dy}) → tips being cut
B.hatch(ink, polys, o) one family of swelling lines bent to the form angle, spacing, bend, tone(x,y), thr, wMin, wMax, swell, excl
B.engraveTone(ink, polys, o) the engraver's three passes angle, cross, third, t2, t3, g
B.formTone(polys, {light}) any shape → pillow-lit tone field (+ .df distance) light, lz, base, radius, rim
B.contourHatch(ink, polys, o) lines following the form (iso-distance contours) spacing, tone, thr, wMax
B.outline / B.stroke swelling contour / open burin stroke w, light, vary, run, pinch, swell, excl
B.stipple, B.fur dots by tone; combed hair flicks density, r, tone / len, dir(x,y), curl
B.sphereTone, B.cylTone, B.linTone, B.blobTone analytic tone fields light from B.LIGHT
Wash hand colouring, cached per region add({path, polys, color, alpha, origin, t0, dur, spill, offset, lift, pool}), draw(ctx, t)
drawSheet, borderInk paper, plate tone, bevel; ruled border as ink plate: [x0, y0, x1, y1]
engraveText, writeScript, wrapLines cut roman lettering; written script; fixed-size balanced wrap size, weight, italic, track, align, p
roundelFrame double-ruled circle scribed by p x, y, r, p, w
Copper.drawCopper / copperLight / drawGrooves / drawBurin / drawSwarf the copper close-up zoom, lift, skew, s

A minimal runnable example of the whole thing is demo/film.js with content_alt.json (a different subject, two details).