Silkscreen Travel Poster — our demo
One example among many. Don't reuse its story, arc, shots, props or timings.
Demo: The Long Way Home 繞遠路回家 (50 s) · silkscreen-poster.mp4 · subtitles silkscreen-poster.srt · source in demo/
Story & structure
A traveller wants to get "as far from home as the world allows". Every city is one full-bleed screen print with its landmark as the hero, and a ticket stub on the right counts the distance from home: Beijing 0, Istanbul 7,060, Paris 8,220, Buenos Aires 19,270 km. No point on Earth is more than about 20,015 km from Beijing, so Buenos Aires is almost the far side of the world. The turn: on a round world, past that point every road leads home. The ending: the same Temple of Heaven print is pulled again in dusk inks, its loose layers snap into register, and the stub reads FROM HOME 0 KM · TRAVELLED 39,630 KM, almost once around the Earth. Narrated in English (Kokoro bf_emma), Chinese and English subtitles.
- Hook (0–10 s): the first pull prints a lone vermilion sun on bare paper; the dawn sky, the woods, the blue roofs and the cypresses arrive one ink per beat while the camera steps back; the stub lands DAY 1 · 0 KM on "Day one. Beijing. Zero kilometres."
- Rise (10–21.9 s): Istanbul, Paris, Buenos Aires. The number grows; the layers get looser city by city.
- Turn (21.9–27.8 s): the camera pushes into "19,270 KM" on the stub until the mesh weave shows; the music leaves part by part, then a second of wind only; the first sound after it is the clamp.
- Return (27.8–36.7 s): a night sea in one long truck while FROM HOME counts down and TRAVELLED counts up; a sweep prints Beijing at dusk, its layers still floating.
- Landing (36.7–50 s): the snap (the loudest downbeat), a night re-ink where the hall turns gold, the end card printed as a wider ticket.
The signature moves, each pushed further than the time before:
| Beijing | Istanbul / Paris | Buenos Aires | Beijing again | |
|---|---|---|---|---|
| Layout | full-bleed print, city name in the sky, ticket stub printed last | same stub template, bigger number | the stub's number fills the screen until the weave shows | the stub gains TRAVELLED; the end card is a wider ticket |
| Colour | accent first, one ink per pull, bare paper as marble | a diagonal sweep prints the next city behind the blade | La Boca's houses in one split-fountain pull | the same geometry re-inked for dusk, then night |
| Registration | normal 2 px | Paris: layers loosen along a crane up the tower | layers drift tens of units, paper gaps show | one snap to zero, the only perfect register in the film |
Shots
24 fps, 108 BPM waltz (beat 0.556 s, bar 1.667 s, 30 bars). The camera never leaves the print: z ≥ 1, the view clamped inside the art.
| # | Time (s) | Move | Picture | Why |
|---|---|---|---|---|
| 1 | 0–3.33 | close-up inside the print, stepping back one level per pull (log zoom) | sun alone, then gold sky with paper rings, then stepped rose bands | the hook: colour arrives in the first second |
| 2 | 3.33–5.56 | to the full frame; the screen lifts at 4.44 (the frame's bottom bar sweeps up through the picture) | woods, three blue roofs, marble terraces (paper), cypresses; red drums and gold finial last | a picture built from layers |
| 3 | 5.56–10 | slow push toward the stub | BEIJING 北京 in the sky; DAY 1 · FROM HOME 0 KM · TEMPLE OF HEAVEN BUILT 1420 | the words and the narration land together |
| 4 | 10–14.17 | a cypress silhouette crosses the screen (full cover for about one frame), then Istanbul prints on sixteenth notes | Hagia Sophia across the water, a ferry; 7,060 KM · BUILT 537 | faster by subdividing the beat, same tempo |
| 5 | 14.17–17.78 | diagonal sweep prints Paris behind the blade; crane up the tower on a 1920×1620 print | Eiffel Tower, mansard roofs; layers loosen with parallax; 8,220 KM · BUILT 1889 | a city change is a colour change |
| 6 | 17.78–21.94 | split-fountain sweep (six inks in one bead); lateral truck over a 2600-wide print | La Boca houses, the Obelisco as bare paper; 19,270 KM · BUILT 1936 | the far side, everything loose |
| 7 | 21.94–27.78 | screen-space push into the stub's number (×5.9); the screen comes down, mesh visible | music thins out; 26.86–27.78 wind only | the turn needs silence |
| 8 | 27.78–33.33 | clamp jolt; night inks swept on while still close; pull out; long truck over a 5200-wide night sea | moon, stars as bare paper; counters running | the way home in one breath |
| 9 | 33.33–36.67 | sweep to the Temple of Heaven at dusk; layers float with paper gaps | counters land: 0 KM · 39,630 KM | back where it began |
| 10 | 36.67–42.22 | snap to register on the downbeat; slow push | "Beijing again. Zero kilometres." | the only perfect register |
| 11 | 42.22–50 | night re-ink sweep; the end card prints at 46.2 (panel) and 46.6 (type) | gold hall, moon; THE LONG WAY HOME 繞遠路回家 | the ending echoes the opening |
Transitions are made in the medium: a squeegee pull, the screen lifting and coming down, a foreground silhouette, a diagonal sweep that prints the next poster, the clamp. No fades, no bare cuts.
Score structure
- Original waltz in F major, 3/4, 108 BPM: nylon guitar (bass note plus two chord beats), jazz double bass, brushes and shaker; the glockenspiel answers only the accent ink (the sun, the gold finials, the key number).
- A colour per city: darbuka in Istanbul, an accordion musette tune in Paris, the bass leaning on the "and" of two in Buenos Aires.
- Bars 13–15: parts leave one at a time; from 25.0 s nothing, 26.86–27.78 wind only. The clamp brings bass, guitar and cajón back on beat 3 of bar 16.
- Bars 17–21: the guitar in eighth notes (acceleration by subdivision), harmonica enters. Bar 22 (36.67 s): the snap, the loudest downbeat (cajón, suspended cymbal, glockenspiel). Bars 22–29: the harmonica tune; the last F chord rings out.
- Foley by material: squeegee over mesh (band-passed noise with a 230–280 Hz grain), wet ink, screen clack and hinge, paper stamps for the stub, ticks while the counters run, a wood-and-metal clamp. Each city wakes up inside its print (park birds, the strait with gulls and a ferry horn, a Paris street with a bicycle bell, harbour wind, the night sea, evening crickets), J-cut in and handed over to the next.
- Voice compressed and set about 10 dB (RMS) above the music; the music dips 300 Hz–4 kHz and ducks under speech. Master −14 LUFS, true peak ≤ −1 dB.
Palette & props
One palette per print, ≤ 6 inks plus a lit-face ink; paper #F3EAD8. The ticket stub is always the print's near ink; its text inks are picked by contrast (pickInk).
| sky1 | sky2 | far | mid | near | accent | lit face | |
|---|---|---|---|---|---|---|---|
| Beijing dawn | #F5C978 |
#E8866A |
#9A88AE |
#22447C |
#1B1F34 |
#D8432B |
#5D86C4 |
| Istanbul | #F6DFB2 |
#E9A27A |
#8DB9BE |
#C9705A |
#1F2D3A |
#F2C14E |
#6E8BA3 |
| Paris | #F4E6CC |
#A9C1DE |
#C3B3D2 |
#5B6E9C |
#252B42 |
#E4553F |
#EDC77E |
| Buenos Aires | #F8E7B0 |
#82C7D8 |
#B9A9C6 |
#3B5B7A |
#232B3A |
#E2543D |
#F7E2A6 |
| Night sea | #2C3B69 |
#1C2650 |
#46598A |
#203A63 |
#111A2F |
#F2D28A |
#3F6395 |
| Beijing dusk | #F2A766 |
#A25A84 |
#6E5C8E |
#26386E |
#151A2C |
#E4502F |
#5578B4 |
| Beijing night | #2A3762 |
#18204A |
#3A4778 |
#1F2D5C |
#0F1426 |
#F2BE4E |
#3D5A97 |
La Boca's split-fountain bead: #D9483B #F2B63D #2F86B5 #4E9C5E #E07B4A #8E5BA6.
Landmarks are stylised silhouettes, built from paths in engine/scenes.js, not traced from photographs; no logos, no brands, no maps. Bare paper does the bright work: the marble terraces and the walkway in Beijing, the Obelisco, the stars.
Titles & text
- City names are printed into the sky (Big Shoulders Display 900, tracked) with a short Chinese name beside them (Noto Sans SC 600).
- The ticket stub (350×556 at 1478, 236; two half-round notches and a row of perforations knocked out to paper): DAY · FROM HOME n KM (accent ink) · landmark and year, or TRAVELLED on the way home. It is printed last, row by row, and stays at the same place on screen in every city.
- Subtitles: a dark ink strip at the bottom centre, Chinese line over English line, type knocked out to paper colour, pulled on in 0.12 s. Each cue holds ≥ 1.8 s and ≥ its line + 0.6 s.
- Reading times are checked by
core/render/readcheck.mjs(window.TEXTS); city names and stub rows are separate texts, and running counters are only reported once they stop.
End card
At 46.2 s a wider ticket (560×600) is pulled over the right side of the night print, then its type: THE LONG WAY HOME / 繞遠路回家, SILKSCREEN TRAVEL POSTER / 絲印旅行海報 and LemoLab × Claude Opus 5.5. It holds to the end (3 s). The sign-off belongs to this library's demo only; a user's film carries no LemoLab credit.
Build
sh styles/silkscreen-poster/demo/build.sh (or LIB=<library> sh demo/build.sh) rebuilds everything: fonts (tools/fetch_fonts.sh), narration if missing (core/tts/tts.py → demo/out/voice/) and the speech-to-text check, subtitles (tools/cues.py → voice/cues.json and ../silkscreen-poster.srt), events, score (music/compose.py), mix (mix.py), picture (--workers 2), master (−14 LUFS), readcheck, poster.jpg and demo/stills/styleframe.jpg. sh demo/build.sh 28 36 renders a span. About one minute on Apple silicon.
Facts on the stubs (distances by the haversine formula, landmark years) and their sources: demo/FACTS.md. Third-party material: demo/CREDITS.
Rules the build follows:
- A knocked-out shape (knock) is filled even-odd together with its parent, so its sub-paths must not overlap; to cut one layer out of another use cut, which clips with the inverse.
- Every ellipse added to a Path2D starts with its own moveTo and ends with closePath, or the arcs join into stray wedges.
- Stub type, header type and end card type pick inks by contrast against near with a light fallback, so night palettes stay legible.
- The ticket stub and the push into it live in screen space (OZ), so a crane or a truck over a tall or wide print doesn't move them.
- Bars, beats and every hit come from one timeline in film.js; events.json drives the score and the foley.
Engine reference
engine/silk.js prints any shape in this style: it only needs a Path2D in whatever units the current transform uses.
| Function | What it does |
|---|---|
makeTextures(paperHex, seed) |
paper, mottle and pinhole tiles (once) |
paperSheet(ctx, T, x, y, w, h) |
paper stock |
ink(ctx, part, o) |
one ink layer. part = { path, knock?, paint? } (knock = knocked-out holes, paint(ctx) = custom fill such as text). o = { color, reg:[dx,dy], clip, sheen, trap } |
wipeRegion(box, dir, p, seed) / wipeFront(...) |
the ragged squeegee front as a clip path, and where the blade is |
squeegee(ctx, a, b, lead, o) |
blade from a to b moving along lead; o.beads=[{t0,t1,color}] (split fountain), roll, handle 0–1, scale |
screenFrame(ctx, box, o) |
hinged screen with mesh, emulsion and tape; lift 0–1 |
inkFinish(ctx, T, x, y, w, h) |
mottle and pinholes over the sheet |
bandSteps(x, w, y0, y1, n) |
stepped colour bands (the gradient substitute) |
ridgeLine, polyPath, circlePath, ringPath, rectPath, unionPath |
shape helpers |
engine/scenes.js: temple, istanbul, paris, boca, sea return { w, h, header, depth, parts: [{ pull, ink, path, knock?, cut?, lit? }] } (a print of any size; pull names the squeegee pass, depth drives parallax and paper gaps).
engine/poster.js: drawArt(ctx, scene, { pal, T, prog, off, sep, header }) prints a scene (partial pulls, loose layers, split fountain via ink: 'fountain'); drawStub, drawEndCard, pickInk; drawPrint is the opening's Beijing print with the stub in art units.
film.js: the timeline, cameras (camS art camera inside a screen-space zoom OZ), sweep() (a diagonal pull that prints the next print behind the blade), treeWipe(), subtitles, window.TEXTS.
Minimal example: a warm-orange #D97757 four-point spark with a cursor tail, printed in two inks and knocked out of a sky, then pulled in by the squeegee:
import { makeTextures, paperSheet, ink, wipeRegion, wipeFront, squeegee, inkFinish, polyPath, rectPath, circlePath } from './engine/silk.js';
const T = makeTextures('#F2E8D2');
paperSheet(ctx, T, 0, 0, 800, 600);
const spark = polyPath([[400,180],[430,270],[520,300],[430,330],[400,420],[370,330],[280,300],[370,270]]);
const tail = rectPath(540, 285, 160, 30);
const p = 0.7; // 70 % of the pull done
const clip = wipeRegion([0, 0, 800, 600], 'right', p, 3);
ink(ctx, { path: rectPath(0, 0, 800, 600), knock: circlePath(400, 300, 150) }, { color: '#1C2A33', clip, sheen: 1 }); // sky with a knocked-out halo
ink(ctx, { path: spark }, { color: '#D97757', reg: [2, -1], clip, sheen: 1, trap: 1 });
ink(ctx, { path: tail }, { color: '#D97757', reg: [2, -1], clip, sheen: 1 });
const x = wipeFront([0, 0, 800, 600], 'right', p);
squeegee(ctx, [x + 10, -20], [x + 10, 620], [1, 0], { color: '#D97757', roll: x / 30 });
inkFinish(ctx, T, 0, 0, 800, 600);
A new place is a new scene in scenes.js (its parts in print order, one palette in film.js); the story and timeline stay yours to write.