@docyrus/animaguru 0.24.0 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (31) hide show
  1. package/NOTICE.md +123 -0
  2. package/README.md +6 -2
  3. package/dist/animaguru.js +133 -4
  4. package/package.json +1 -1
  5. package/skills/animaguru-aurora-glass/SKILL.md +33 -0
  6. package/skills/animaguru-aurora-glass/references/recipe.md +27 -0
  7. package/skills/animaguru-bauhaus-geometry/SKILL.md +35 -0
  8. package/skills/animaguru-bauhaus-geometry/references/recipe.md +30 -0
  9. package/skills/animaguru-liquid-motion/SKILL.md +35 -0
  10. package/skills/animaguru-liquid-motion/references/recipe.md +24 -0
  11. package/skills/animaguru-one-line-art/SKILL.md +34 -0
  12. package/skills/animaguru-one-line-art/references/recipe.md +28 -0
  13. package/skills/animaguru-sticker-explainer/SKILL.md +37 -0
  14. package/skills/animaguru-sticker-explainer/references/recipe.md +27 -0
  15. package/skills/animaguru-synthwave-y2k/SKILL.md +35 -0
  16. package/skills/animaguru-synthwave-y2k/references/recipe.md +29 -0
  17. package/skills/animaguru-variety-captions/SKILL.md +36 -0
  18. package/skills/animaguru-variety-captions/references/recipe.md +27 -0
  19. package/templates/video/README.md +4 -2
  20. package/templates/video/VIDEO-STYLE-NOTICES.txt +24 -0
  21. package/templates/video/WORKSPACE-AGENTS.md +1 -1
  22. package/templates/video/scripts/export-mp4.mjs +91 -13
  23. package/templates/video/scripts/watermark/OFL.txt +93 -0
  24. package/templates/video/scripts/watermark/instrument-sans.woff2 +0 -0
  25. package/templates/video/scripts/watermark/logo-mark-512.png +0 -0
  26. package/templates/video/shared/locales/en/export.json +1 -0
  27. package/templates/video/shared/locales/en/style-studies.json +56 -0
  28. package/templates/video/src/exporter.ts +11 -1
  29. package/templates/video/src/studies/scenes.ts +391 -0
  30. package/templates/video/src/watermark.ts +50 -0
  31. package/templates/video/style-source-provenance.json +5 -1
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: animaguru-sticker-explainer
3
+ description: Build Sticker Explainer visuals and motion in an AnimaGuru PixiJS video workspace. Use when this visual style is chosen for an explainer, advertisement or general video.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Sticker Explainer
8
+
9
+ The knowledge-video board: cut-out photos with thick white borders, flat icons and exact charts laid across one huge calm canvas, and a single camera that travels from fact to fact while every narration line brings a new element. It reads serious and dense: restrained colour, one monoline sans, numbers that are always to scale. Subject, audience, emotional tone and story come from the user's brief. This style supports Explainer, Advertisement and General Video.
10
+
11
+ ## Establish direction
12
+
13
+ Follow sibling `animaguru-video` for purpose → style → compact interview before new production. Reuse explicit choices and completed briefs; honor a requested interview skip. Share [the style gallery](https://animaguru.com/animation-styles#sticker-explainer). Fold missing visual questions into the shared interview:
14
+
15
+ - Which photos, product shots or data do you have, and which facts should the camera visit, in what order?
16
+ - What is the one number or conclusion the board pulls back to at the end?
17
+
18
+ ## Keep the facts traceable
19
+
20
+ Ask the user for their own photos and data first; otherwise use licence-clean sources (see the [production recipe](references/recipe.md)). Every number on the board comes from a source or a stated calculation: keep `production/FACTS.md` (each figure, its source URL or the arithmetic behind it) and `production/CREDITS.md` (every photo's source, author and licence), and deliver both with the film. Generated cut-outs may stand in for generic objects (a cup, a sack, a phone), never for a specific real person, place, product or event, and a generated image never carries a figure. Rounded or illustrative values say so on screen.
21
+
22
+ ## Build the look and movement
23
+
24
+ Read `pixijs` and the relevant Container, Sprite, Graphics, Text and Filters skills. Read `animaguru` before generating assets; discover current models and estimates, respect the user's budget, and reuse completed media. Essential numbers, labels and charts stay authored canvas content, never baked into a picture.
25
+
26
+ - One board, several frames wide and tall, on a cool neutral ground (blue-grey or off-white) with a faint dot grid and fibre; facts sit in regions on it, linked by thin dashed threads. Only the camera moves between regions.
27
+ - Every photo becomes a sticker: subject cut out, then one uniform white margin of 8–12 px at delivery size, a soft shadow with a tight contact core and the same light direction on every sticker. Bake the margin into the texture so it never thickens or thins with the camera zoom.
28
+ - Stickers slap on: they drop from about 1.3× scale with a few degrees of turn while their shadow tightens, land in 0.2–0.3 s and settle with a small spring; the board can thump by under 1% on impact.
29
+ - The camera holds with a slow 2–3% push, then travels in 0.7–1 s with a long ease, easing back mid-move so the departing and arriving facts share the frame; it ends pulled back on the whole board with the conclusion set large in the middle.
30
+ - One narration line, one new element: a sticker, a counter, a chart or a labelled leader line lands within about 0.2 s of its line. Keep the subtitle in a fixed pill, a chapter tag in a corner, and two or three colours plus one accent.
31
+ - Charts are exact: zero-based bars to scale from one data model, counters rolling in fixed-width digit cells to the exact final value, and a source or approximation note under each chart.
32
+
33
+ Use the [production recipe](references/recipe.md) for sources, the bundled original study and port-specific decisions. It is a technique example, not a prescribed film. Keep motions a function of absolute seconds. Prepare assets locally, preserve async seeking and register actual resource/cue times. Keep essential type editable and localized.
34
+
35
+ ## Review and delivery
36
+
37
+ Check every figure against `FACTS.md`, bar lengths against their values, sticker margins and shadows for consistency at every zoom, that no frame mid-move is an empty stretch of board, that labels clear the subtitle pill, and that the final pull-back frame reads as a finished poster. Check representative full-resolution frames, moving playback, desktop/mobile fit and cold seeks. Follow `animaguru-video` for production records, audio mixing and HTML5 delivery; export MP4 when requested. Describe the actual 2D/2.5D rendering method accurately. Physically rendered 3D remains deferred.
@@ -0,0 +1,27 @@
1
+ # Sticker Explainer production recipe
2
+
3
+ The workspace includes an executable original study in `src/studies/scenes.ts`, selected with `sticker-explainer`. Its drawing commands are consumed by `src/studies/pixi-renderer.ts`; geometry and time sampling are shared with the gallery renderer. All copy lives in `shared/locales/en/style-studies.json`.
4
+
5
+ In an untouched starter, run `npm run example -- sticker-explainer`, then `npm run dev`. The command refuses to replace an authored film, retains starter backups in `production/example-backup/`, and writes an 8-second study with its own resource tracks. `npm run export:html` and `npm run export:mp4` use this installed film normally. To use the recipe inside an existing film, import `studyFrame()` and `StudyRenderer`, then compose the selected scene into the project's timeline instead of running the installer.
6
+
7
+ ## Photos, data and sources
8
+
9
+ The user's own product shots, photos and data come first. Otherwise: Wikimedia Commons (read the licence on each file page: CC0, public domain or CC BY are fine; not CC BY-SA, "no known restrictions" or unclear licences), U.S. federal agencies (NASA, NOAA, USDA, USGS: works of federal employees are public domain), Openverse results filtered to CC0 or public domain, and official statistics from the publishing agency's own site. Download into the film's `public/` or `production/` folders, log every request, record each photo in `production/CREDITS.md` and each figure, with its source or arithmetic, in `production/FACTS.md`. When the brief allows generated cut-outs for generic objects, generate them on a plain background so the cut-out is clean, and list them in `CREDITS.md` as generated.
10
+
11
+ ## Adaptation decisions
12
+
13
+ - Build the board as one `Container` holding every region; the camera is that container's position and scale, computed each frame from a table of shots (centre and zoom) and absolute time. Interpolate zoom in log space and multiply it by a `1 − k·sin(πu)` dip during a move so both regions stay in view. Keep the subtitle pill, chapter tag and honesty labels in a separate screen-space container above the board.
14
+ - Make each sticker texture once at load: cut the subject out (or use the photo's alpha), dilate its silhouette by a distance pass to a uniform 8–12 px margin at delivery scale, fill the margin white and render it to a `RenderTexture` with `renderer.generateTexture`. Draw the shadow as a second, blurred sprite underneath, offset down-right. Never use a live outline filter: its width would scale with the camera.
15
+ - Animate the slap on the sticker's own container (scale, rotation, alpha) and the shadow's offset and opacity separately; the board thump is a brief extra factor on the camera zoom.
16
+ - Draw charts with `Graphics` from one data model and redraw only while values change; set counters in per-digit `Text` or `BitmapText` cells so they never jitter, and finish on the exact formatted value.
17
+ - Draw threads and route lines as polylines truncated by arc length (`strokePoints` in `src/studies/timing.ts`) so they grow along the camera's path; a dashed line is a sequence of short segments.
18
+
19
+ The study is silent, original procedural art with illustrative values, labelled as such; it demonstrates the board, sticker, camera and chart system, not a real subject. Replace its stand-ins with the user's photos and sourced figures described above. The gallery preview renders its "photo" stickers procedurally (shaded height fields of a cup, beans, a coffee branch and a jute sack) with rounded, approximate figures, and says so on screen. Configure the subject, palette, scene sequence, duration and soundtrack from the brief.
20
+
21
+ Use `src/studies/timing.ts` for deterministic easing, seeded variation, measured stroke truncation and cue helpers. `window.animaguru.texts` exposes visible study text bounds for `npm run review`; text declarations alone do not count as visual proof. Inspect the actual frames as well.
22
+
23
+ ## Sources and attribution
24
+
25
+ Inspired by the following pinned reference, which describes the PaperClip-style knowledge-video format; no code, footage, music or assets from it are bundled. New study art and scene geometry are original. The workspace's `VIDEO-STYLE-NOTICES.txt` preserves the repository notices and clarifies third-party asset scope.
26
+
27
+ - [prompts/19-paperclip.md](https://github.com/Vincentwei1021/mg-styles-15/blob/49052d80bfaf9fa8412cc9710530e5e95a3b8d8c/prompts/19-paperclip.md)
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: animaguru-synthwave-y2k
3
+ description: Build Synthwave & Y2K visuals and motion in an AnimaGuru PixiJS video workspace. Use when this visual style is chosen for an explainer, advertisement or general video.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Synthwave & Y2K
8
+
9
+ A whole-film quote of a media era: a neon perspective grid running at a slatted sunset, chrome lettering with a flare and a glint, and Y2K desktop pop-ups driven by a cursor, all played back through one VHS deck. Subject, audience, emotional tone and story come from the user's brief. This style supports Explainer, Advertisement and General Video.
10
+
11
+ ## Establish direction
12
+
13
+ Follow sibling `animaguru-video` for purpose → style → compact interview before new production. Reuse explicit choices and completed briefs; honor a requested interview skip. Share [the style gallery](https://animaguru.com/animation-styles#synthwave-y2k). Fold missing visual questions into the shared interview:
14
+
15
+ - Pure 80s synthwave (grid, sun, chrome), Y2K desktop (pop-ups, acid chrome, cursor), or both in one film?
16
+ - How worn should the tape be: a light vintage finish, or a tired cassette with tracking errors and stutters?
17
+ - Which words get chrome and which get neon, and is there an on-screen action the cursor should perform?
18
+
19
+ ## Build the look and movement
20
+
21
+ Read `pixijs` and the relevant Graphics, Text, Mesh, Filters and Blend Modes skills. Read `animaguru` before generating assets: generation can supply a soundtrack or a backdrop plate, but the grid, sun, type and windows are drawn in code. Discover current models and estimates, respect the user's budget, and reuse completed media. Essential text belongs in authored canvas content, never baked into an image.
22
+
23
+ - The grid is a one-point perspective floor computed in world space (lines at fixed world spacing, scrolled by camera travel), so lines thicken toward the lens and stay locked to the horizon. Fade lines into their average glow where a cell shrinks below about 4 px; never let them shimmer into moiré at the horizon.
24
+ - The sun is a multi-stop vertical gradient (pale yellow → orange → magenta) with slats cut from its lower half, widening toward the bottom and drifting downward; mountains, palms and cloud strips may cross it, its reflection ripples on the floor.
25
+ - Glow is double: a tight bloom of about 3–4 px and a wide one of about 30 px, both from an emission layer that holds only the neon (grid, rims, sun, script tubes), added on top. Glow never lifts the whole frame; keep the sky top near navy-black.
26
+ - Chrome lettering is a heavy italic sans with a high-contrast metal gradient that breaks at a hard horizon band near its middle (sky blue → white → dark band → warm tones), a stepped extrusion, a thin bevel highlight, one shine sweep and one star glint. A title slam lands with a flare and, if wanted, a short flash; neon script strikes on left to right with a hot leading edge and a brief sputter.
27
+ - Y2K desktop language: glossy pop-ups with gradient title bars, bevelled buttons and candy icons; they pop with overshoot, a cursor moves, presses, drags (an outline may stand in for the window while dragging), cascades more pop-ups and clicks; windows close by collapsing to a line like a switched-off tube. Invent the operating system: no real OS logos, chrome or sounds.
28
+ - The VHS chain runs in a fixed order: row wobble and tears, chroma bleed (luma sharp; colour blurred over about 9 px and delayed 1–2 px, or red and blue split 1–2 px), luma noise and dropouts, tracking bars and bottom head-switching noise, then scanlines (every other row 10–15% darker) and rounded tube corners. An on-screen display (PLAY ▶, tape counter, SP) sits above the tape damage and below the scanlines. An optional 4:3 rounded mask is a framing choice, not a requirement.
29
+ - Cadence is part of the medium: one or two glitches a second at most, a few passages stepped to 15 fps, and a two-frame reverse stutter on a hit. Keep essential reading moments clean.
30
+
31
+ Use the [production recipe](references/recipe.md) for the bundled original study and port-specific decisions. It is a technique example, not a prescribed film. Keep motions a function of absolute seconds; separate the scene clock (stepped, stuttered) from the tape clock (noise and glitches always moving). Prepare assets locally, preserve async seeking and register actual resource/cue times. Keep essential type editable and localized.
32
+
33
+ ## Review and delivery
34
+
35
+ Inspect grid stability near the horizon, chrome legibility under glow, the slam and stutter frames one by one, cursor targets (it must actually reach the buttons it clicks), and that glitches avoid essential text. Check representative full-resolution frames, moving playback, desktop/mobile fit and cold seeks. Fonts must be licensed for embedding (OFL or system-bundled for local rendering); music and sound must be licence-clean or generated. Follow `animaguru-video` for production records, audio mixing and HTML5 delivery; export MP4 when requested. Describe the actual 2D/2.5D rendering method accurately. Physically rendered 3D remains deferred.
@@ -0,0 +1,29 @@
1
+ # Synthwave & Y2K production recipe
2
+
3
+ The workspace includes an executable original study in `src/studies/scenes.ts`, selected with `synthwave-y2k`. Its drawing commands are consumed by `src/studies/pixi-renderer.ts`; geometry and time sampling are shared with the gallery renderer. All copy lives in `shared/locales/en/style-studies.json`.
4
+
5
+ In an untouched starter, run `npm run example -- synthwave-y2k`, then `npm run dev`. The command refuses to replace an authored film, retains starter backups in `production/example-backup/`, and writes an 8-second study with its own resource tracks. `npm run export:html` and `npm run export:mp4` use this installed film normally. To use the recipe inside an existing film, import `studyFrame()` and `StudyRenderer`, then compose the selected scene into the project's timeline instead of running the installer.
6
+
7
+ ## Assets and licences
8
+
9
+ The look needs no media: the grid, sun, mountains, palms, chrome type, windows and tape damage are all drawn. Generated media fits around it, never in place of editable type: a generated synthwave track, a narration, or a painted sky plate behind the code-drawn grid. Fonts must be OFL or bundled with the system that renders the film; record each in `production/CREDITS.md` with its licence. Do not reproduce real operating system interfaces, logos, startup sounds or cursors from commercial products; invent the desktop, its file names and its colours.
10
+
11
+ ## Adaptation decisions
12
+
13
+ - Draw the floor grid as a `Mesh` or a full-screen `Filter` that intersects each pixel's ray with the ground plane (`z = eyeHeight · focal / (y − horizon)`), measures the distance to the nearest world-space line in screen pixels and fades lines into their average once a cell is under about 4 px. Scroll by adding camera travel to world `z`, not by moving sprites.
14
+ - Keep emissive parts (grid, horizon line, sun, crests, neon script, title outline) in one container rendered to a `RenderTexture`. Blur two copies (`BlurFilter` of about 4 and, at quarter resolution, about 30) and add them with `blendMode: "add"`; mask the emission under solid objects (palms, chrome face, windows) before blurring, so glow wraps their edges instead of washing over them.
15
+ - Build the chrome title once as a texture: stepped extrusion copies, a `FillGradient` face with a hard dark band at its middle, a thin light stroke for the bevel. Animate the slam by scaling that texture (average a few sub-frame scales for motion blur) and run the shine as a masked gradient sprite clipped to the face.
16
+ - Write the VHS chain as one custom `Filter` on the stage: per-row horizontal offsets from a hash of row and absolute time, YIQ chroma blur and delay (or an RGB split), luma noise, a moving tracking band, bottom-row skew and every-other-row darkening. Feed it a `glitch` uniform from a cue table so tears happen on purpose. Draw the on-screen display after the filter's damage stage, before scanlines.
17
+ - Quantise the scene clock for stepped passages (`Math.floor(t * 15) / 15`) and map a hit's next two frames to earlier scene times for the reverse stutter; the tape noise keeps reading the true time.
18
+ - Drive windows and the cursor from one cue table: pop (overshoot spring), move, press, drag outline, release, click (button down, ripple) and collapse (scale Y to a line, then X to a point).
19
+
20
+ The study is silent, original procedural art. It demonstrates material, transforms and layout rather than any real product or operating system. Configure an original subject, palette, scene sequence, duration and soundtrack from the brief.
21
+
22
+ Use `src/studies/timing.ts` for deterministic easing, seeded variation, measured stroke truncation and cue helpers. `window.animaguru.texts` exposes visible study text bounds for `npm run review`; text declarations alone do not count as visual proof. Inspect the actual frames as well.
23
+
24
+ ## Sources and attribution
25
+
26
+ Inspired by the following pinned reference; no source code, assets or audio are bundled. New study art and scene geometry are original. The workspace's `VIDEO-STYLE-NOTICES.txt` preserves the repository notices and clarifies third-party asset scope.
27
+
28
+ - [prompts/10-synthwave.md](https://github.com/Vincentwei1021/mg-styles-15/blob/49052d80bfaf9fa8412cc9710530e5e95a3b8d8c/prompts/10-synthwave.md)
29
+ - [demos/10-synthwave/index.html](https://github.com/Vincentwei1021/mg-styles-15/blob/49052d80bfaf9fa8412cc9710530e5e95a3b8d8c/demos/10-synthwave/index.html)
@@ -0,0 +1,36 @@
1
+ ---
2
+ name: animaguru-variety-captions
3
+ description: Build Variety Captions visuals and motion in an AnimaGuru PixiJS video workspace. Use when this visual style is chosen for an explainer, advertisement or general video.
4
+ disable-model-invocation: true
5
+ ---
6
+
7
+ # Variety Captions
8
+
9
+ TV variety-show packaging: chunky rounded captions in layered outlines pop onto the footage a beat after every gag, and each feeling gets its own treatment, from bouncing question marks to a burst frame on a freeze. It turns a vlog, a pet clip, an interview or a product demo into an episode. Subject, audience, emotional tone and story come from the user's brief. This style supports Explainer, Advertisement and General Video.
10
+
11
+ ## Establish direction
12
+
13
+ Follow sibling `animaguru-video` for purpose → style → compact interview before new production. Reuse explicit choices and completed briefs; honor a requested interview skip. Share [the style gallery](https://animaguru.com/animation-styles#variety-captions). Fold missing visual questions into the shared interview:
14
+
15
+ - Which footage carries the film, and which three to five moments are the gags or turns the captions answer?
16
+ - Which language and voice do the captions speak in (the narrator, the subject's inner voice, a commentator), and how cheeky may they be?
17
+
18
+ ## Footage and assets
19
+
20
+ Ask for the user's own footage first; the captions are written for real moments, so the clip decides the gags. Otherwise use licence-clean stock (CC0 or public domain, or a licence the user confirms) and record each file in `production/CREDITS.md`. A generated clip is allowed only when the user agrees, and it must never pose as a real recorded moment of a real person or animal. Bundle every font locally with its licence: heavy rounded faces such as Nunito Black, Baloo 2, M PLUS Rounded 1c, ZCOOL KuaiLe or Smiley Sans (all OFL), and a hand-lettering face such as Caveat or Patrick Hand for asides.
21
+
22
+ ## Build the look and movement
23
+
24
+ Read `pixijs` and the relevant Text, Graphics, Container and Filters skills. Read `animaguru` before generating assets: generation can supply music, sound effects or narration; the captions themselves stay authored, editable canvas text.
25
+
26
+ - Every caption is three layers plus a shadow: a fill with a two-stop gloss (a hard step at about half height), a white outline of about 0.18 em and a coloured outer outline of about 0.34 em, over an offset drop shadow (about 0.04 em right, 0.08 em down, half opacity). Draw each layer across the whole line before the next, so no letter's outline covers its neighbour's fill.
27
+ - Entrances overshoot: scale 0 → 1.15 → 1.0 over about eight frames at 24 fps, or letter by letter with a two-frame stagger and a short hop. Exits are a three-to-four-frame pop. Long holds keep a gentle wiggle (±2–3°, ±2–3 px).
28
+ - A caption lands two to three frames after its beat (the visual moment or the sound cue), never before, and the edit supports it: a two-frame snap punch-in, a freeze with a slow push, a jump cut to the reaction.
29
+ - Match each feeling to one treatment and one palette: curiosity gets bouncing question marks; shock a freeze, a two-frame white flash, a jagged burst frame, converging speed lines redrawn every two frames and a decaying shake; awkwardness a sliding sweat drop, gloom lines and cool lavender; delight twinkling sparkles in pink and magenta; a verdict a worn rubber stamp slammed from about 2.3× to 1× in three frames; a snarky aside small slanted hand lettering in parentheses.
30
+ - Keep faces and the gag itself clear, keep a show bug in one corner, and stay inside about 5 % safe margins. Use idiomatic copy and correct punctuation in the captions' language, including CJK punctuation.
31
+
32
+ Use the [production recipe](references/recipe.md) for the bundled original study and port-specific decisions. It is a technique example, not a prescribed film. Keep motions a function of absolute seconds; separate drawing cadence from camera/light cadence when the material calls for it. Prepare assets locally, preserve async seeking and register actual resource/cue times. Keep essential type editable and localized.
33
+
34
+ ## Review and delivery
35
+
36
+ Inspect each caption's landing against its beat (two to three frames late, never early), reading time on every caption, overlap with faces and the show bug, outline order between neighbouring letters, fallback fonts and phone-size legibility. Check representative full-resolution frames, moving playback, desktop/mobile fit and cold seeks. Follow `animaguru-video` for production records, audio mixing and HTML5 delivery; export MP4 when requested. Describe the actual 2D/2.5D rendering method accurately. Physically rendered 3D remains deferred.
@@ -0,0 +1,27 @@
1
+ # Variety Captions production recipe
2
+
3
+ The workspace includes an executable original study in `src/studies/scenes.ts`, selected with `variety-captions`. Its drawing commands are consumed by `src/studies/pixi-renderer.ts`; geometry and time sampling are shared with the gallery renderer. All copy lives in `shared/locales/en/style-studies.json`.
4
+
5
+ In an untouched starter, run `npm run example -- variety-captions`, then `npm run dev`. The command refuses to replace an authored film, retains starter backups in `production/example-backup/`, and writes an 8-second study with its own resource tracks. `npm run export:html` and `npm run export:mp4` use this installed film normally. To use the recipe inside an existing film, import `studyFrame()` and `StudyRenderer`, then compose the selected scene into the project's timeline instead of running the installer.
6
+
7
+ ## Footage, fonts and sound
8
+
9
+ The user's own clip comes first. Otherwise: Wikimedia Commons (CC0 or public domain; read each file page, and not CC BY-SA or "no known restrictions"), Pexels or Pixabay only under a licence the user accepts, and the Internet Archive for public-domain material. Fonts come from Google Fonts or the foundry under the SIL Open Font License, bundled in `public/` with the licence text. Comedy sound effects (boing, pop, ding, slide whistle, record scratch) are generated or taken from CC0 libraries such as Freesound's CC0 filter. Record every asset's source, author and licence in `production/CREDITS.md`, and write a cue sheet in `production/` listing each gag beat and the caption that answers it.
10
+
11
+ ## Adaptation decisions
12
+
13
+ - Build each caption line from prebuilt per-glyph `Text` objects measured after the fonts load, in four sibling containers (shadow, outer outline, white outline, fill) so every outline sits under every fill. Animate the glyph containers' position, rotation and scale, never the font size, and use a `FillGradient` with a hard stop for the gloss.
14
+ - Draw the burst frame, the converging speed lines, sweat drops and sparkles as `Graphics`, reseeded from the frame index every two frames so the hand-drawn flicker repeats exactly on seeks.
15
+ - Make the freeze by holding the footage at its source time (a still captured to a `RenderTexture` for video), push in with a container transform, and run the white flash and the shake on the same footage container, so captions stay locked above the picture.
16
+ - Prepare the rubber stamp once as an ink-worn `Sprite` texture and slam it with a scale tween and a short shake of the footage container.
17
+
18
+ The study is silent, original procedural art: a flat stand-in cat, mug and counter, labelled as illustrative footage. It demonstrates the caption layers, timing and reaction graphics, not a finished episode; replace its stand-ins with the user's footage. The gallery preview uses a procedurally rendered stand-in cat and kitchen, labelled as not real footage, to show the system at full finish. Configure the subject, palette, scene sequence, duration and soundtrack from the brief.
19
+
20
+ Use `src/studies/timing.ts` for deterministic easing, seeded variation, measured stroke truncation and cue helpers. `window.animaguru.texts` exposes visible study text bounds for `npm run review`; text declarations alone do not count as visual proof. Inspect the actual frames as well.
21
+
22
+ ## Sources and attribution
23
+
24
+ Inspired by the following pinned references; no source code, fonts, footage or audio from them is bundled. New study art and scene geometry are original. The workspace's `VIDEO-STYLE-NOTICES.txt` preserves the repository notices and clarifies third-party asset scope.
25
+
26
+ - [prompts/18-hanazi.md](https://github.com/Vincentwei1021/mg-styles-15/blob/49052d80bfaf9fa8412cc9710530e5e95a3b8d8c/prompts/18-hanazi.md)
27
+ - [demos/18-hanazi/index.html](https://github.com/Vincentwei1021/mg-styles-15/blob/49052d80bfaf9fa8412cc9710530e5e95a3b8d8c/demos/18-hanazi/index.html)
@@ -67,11 +67,13 @@ npm run export:mp4
67
67
  npm run export:mp4 -- --port 5197
68
68
  # Captions drawn into the picture, e.g. for social feeds (saved as SLUG-LANGUAGE-captioned.mp4):
69
69
  npm run export:mp4 -- --burn-captions
70
+ # Also save a copy with AnimaGuru's "Made with AnimaGuru" badge (SLUG-LANGUAGE-watermarked.mp4):
71
+ npm run export:mp4 -- --watermark
70
72
  ```
71
73
 
72
74
  Open the local capture URL printed by the command and keep it open until complete. The script runs a separate loopback Vite server (default port 5196), streams high-quality JPEG frames from the Pixi canvas to the bundled FFmpeg binary, and mixes the configured soundtrack. The initial starter exports with a silent AAC track. Exporting local media does not call AnimaGuru or spend credits. FFmpeg/FFprobe binaries are supplied through platform-specific npm dependencies; installation needs a supported platform and network access.
73
75
 
74
- Completed H.264/AAC MP4s and JSON verification reports appear in `production/exports/`. Captions are not drawn into the MP4 picture: the film's `captions` resource track is saved beside it as `SLUG-LANGUAGE.srt` (for YouTube and desktop players, which load a same-named file automatically) and `SLUG-LANGUAGE.vtt` (for an HTML5 `<track>`). `--burn-captions` instead draws them into the picture and writes no subtitle files, so players cannot show them twice. `"poster"` in `film.config.json` (seconds) picks the frame saved as `SLUG-LANGUAGE.jpg`, a caption-free thumbnail 1280 pixels on its longer side and under YouTube's 2 MB limit; without it, no thumbnail is saved. The live player and HTML5 export keep their CC toggle. Existing files are preserved. Frame retries are deduplicated. Ctrl+C removes only this session's unfinished output. The verification checks duration, exact frame count, dimensions, pixel format, codecs and audio language tag. The exporter supports even dimensions up to 4096 pixels, 24/25/30/60 fps and films up to ten minutes. Set duration to an exact number of frames.
76
+ Completed H.264/AAC MP4s and JSON verification reports appear in `production/exports/`. Captions are not drawn into the MP4 picture: the film's `captions` resource track is saved beside it as `SLUG-LANGUAGE.srt` (for YouTube and desktop players, which load a same-named file automatically) and `SLUG-LANGUAGE.vtt` (for an HTML5 `<track>`). `--burn-captions` instead draws them into the picture and writes no subtitle files, so players cannot show them twice. `"poster"` in `film.config.json` (seconds) picks the frame saved as `SLUG-LANGUAGE.jpg`, a caption-free thumbnail 1280 pixels on its longer side and under YouTube's 2 MB limit; without it, no thumbnail is saved. The live player and HTML5 export keep their CC toggle. `--watermark` also saves `SLUG-LANGUAGE-watermarked.mp4`, the same film with AnimaGuru's "Made with AnimaGuru" badge in its bottom-right corner, drawn with the font, logo and layout AnimaGuru's web app uses; it is the preview AnimaGuru's lead pipeline plays before a lead claims the clean film. The picture is encoded again with the badge and verified like the film; the sound is copied. Existing files are preserved. Frame retries are deduplicated. Ctrl+C removes only this session's unfinished output. The verification checks duration, exact frame count, dimensions, pixel format, codecs and audio language tag. The exporter supports even dimensions up to 4096 pixels, 24/25/30/60 fps and films up to ten minutes. Set duration to an exact number of frames.
75
77
 
76
78
  ## Capture API
77
79
 
@@ -102,7 +104,7 @@ The installed style skills cover visual treatment and motion technique across su
102
104
 
103
105
  ## Original studies and production review
104
106
 
105
- Browse `animaguru.templates.json` for all 48 styles and purpose workflows. The 37 expanded, evidence, Machine Anatomy and documentary & broadcast styles include a runnable original study; use `npm run example -- blueprint-drafting` in an untouched starter. Evidence- and documentary-style studies use illustrative values, labelled as such; a production film replaces them with real data, imagery, footage or scans. The command backs up the starter, refuses to replace an authored film, and connects the study to the normal player, timeline and HTML5/MP4 exporters. To compose studies in an existing film, import their scene and renderer instead.
107
+ Browse `animaguru.templates.json` for all 55 styles and purpose workflows. The 44 expanded, evidence, Machine Anatomy, documentary & broadcast and contemporary motion-design styles include a runnable original study; use `npm run example -- blueprint-drafting` in an untouched starter. Evidence- and documentary-style studies use illustrative values, labelled as such; a production film replaces them with real data, imagery, footage or scans. The command backs up the starter, refuses to replace an authored film, and connects the study to the normal player, timeline and HTML5/MP4 exporters. To compose studies in an existing film, import their scene and renderer instead.
106
108
 
107
109
  `npm run review` starts a local browser review session. Open the printed URL; it saves representative JPEG frames, actual text bounds, reading-hold notes and repeated-seek comparisons in `production/review/`. The numerical report complements frame inspection and full-speed listening. Optional media trim/time mapping and cue helpers are in `src/studies/`. All procedural example art is original; source inspirations and MIT notices are recorded in `VIDEO-STYLE-NOTICES.txt`.
108
110
 
@@ -76,3 +76,27 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
76
76
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
77
77
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
78
78
  SOFTWARE.
79
+
80
+
81
+ mg-styles-15
82
+ MIT License
83
+
84
+ Copyright (c) 2026 Vincentwei1021
85
+
86
+ Permission is hereby granted, free of charge, to any person obtaining a copy
87
+ of this software and associated documentation files (the "Software"), to deal
88
+ in the Software without restriction, including without limitation the rights
89
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
90
+ copies of the Software, and to permit persons to whom the Software is
91
+ furnished to do so, subject to the following conditions:
92
+
93
+ The above copyright notice and this permission notice shall be included in all
94
+ copies or substantial portions of the Software.
95
+
96
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
97
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
98
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
99
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
100
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
101
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
102
+ SOFTWARE.
@@ -16,7 +16,7 @@ Run commands from the workspace root:
16
16
  - `npm run dev`: the workspace hub and separate `/animations/NAME/` players.
17
17
  - `npm run build -- --animation NAME`: typecheck and build that film.
18
18
  - `npm run export:html -- --animation NAME`: portable HTML5 ZIP with local media and common branding.
19
- - `npm run export:mp4 -- --animation NAME` (not for a branching story or a quiz film): verified MP4 without burned-in captions, plus `.srt`/`.vtt` subtitle files from the captions track (`--burn-captions` draws them into the picture instead, as `NAME-LANG-captioned.mp4`), and a `.jpg` thumbnail of the `poster` frame set in film.config.json. Add `--headless` to capture in local headless Chrome; otherwise open the printed capture page.
19
+ - `npm run export:mp4 -- --animation NAME` (not for a branching story or a quiz film): verified MP4 without burned-in captions, plus `.srt`/`.vtt` subtitle files from the captions track (`--burn-captions` draws them into the picture instead, as `NAME-LANG-captioned.mp4`), and a `.jpg` thumbnail of the `poster` frame set in film.config.json. `--watermark` also saves `NAME-LANG-watermarked.mp4` with AnimaGuru's "Made with AnimaGuru" badge, for an AnimaGuru lead film's preview. Add `--headless` to capture in local headless Chrome; otherwise open the printed capture page.
20
20
  - `npm run review -- --animation NAME --headless`: frame, text, layout (overlap, safe area, text size), seeking and soundtrack checks, one line per finding.
21
21
  - `npm run frames -- --animation NAME --at 1.2,4.8`: labelled contact sheets in `production/review/frames/` plus layout notes for those times.
22
22
  - `npm run audio:vo|audio:music|audio:mix -- --animation NAME`: cut and place the voiceover (`timing.json`), fit the music bed, and master the soundtrack.
@@ -23,10 +23,13 @@ const headless = takeHeadlessFlag(args);
23
23
  let port = 5196;
24
24
  // Captions become subtitle files beside the MP4 unless --burn-captions draws them into the picture, e.g. for social feeds.
25
25
  let burnCaptions = false;
26
+ // --watermark also saves NAME-watermarked.mp4, the film with AnimaGuru's "Made with AnimaGuru" badge, e.g. a lead film's preview.
27
+ let watermark = false;
26
28
  for (let index = 0; index < args.length; index++) {
27
29
  if (args[index] === "--burn-captions") burnCaptions = true;
30
+ else if (args[index] === "--watermark") watermark = true;
28
31
  else if (args[index] === "--port" && /^\d+$/.test(args[index + 1] ?? "")) port = Number(args[++index]);
29
- else throw new Error("Usage: npm run export:mp4 -- [--animation NAME] [--headless] [--port 5196] [--burn-captions]");
32
+ else throw new Error("Usage: npm run export:mp4 -- [--animation NAME] [--headless] [--port 5196] [--burn-captions] [--watermark]");
30
33
  }
31
34
  if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new Error("Export port must be 1024–65535.");
32
35
  const context = await projectContext(root, selection.animation);
@@ -47,11 +50,15 @@ if (config.audio !== null) {
47
50
  if (config.poster != null && (typeof config.poster !== "number" || !Number.isFinite(config.poster) || config.poster < 0 || config.poster >= duration)) throw new Error("Set poster in film.config.json to a time within the film, in seconds, or leave it out.");
48
51
  const thumbnailScale = Math.min(1, 1280 / Math.max(width, height));
49
52
  const thumbnail = config.poster == null ? null : { time: config.poster, width: Math.round(width * thumbnailScale), height: Math.round(height * thumbnailScale) };
53
+ // The capture page draws the badge with the font and logo AnimaGuru's web app uses, so they are sent to it as base64.
54
+ const watermarkFiles = watermark ? Object.fromEntries(await Promise.all([["font", "instrument-sans.woff2"], ["logo", "logo-mark-512.png"]]
55
+ .map(async ([key, file]) => [key, (await readFile(new URL(`./watermark/${file}`, import.meta.url))).toString("base64")]))) : null;
50
56
  const outputDirectory = context.output;
51
57
  await mkdir(outputDirectory, { recursive: true });
52
58
  const name = `${config.slug}-${language}${burnCaptions ? "-captioned" : ""}`;
53
59
  let destination = path.join(outputDirectory, `${name}.mp4`);
54
- for (const candidate of [destination, ...[".partial.mp4", ".srt", ".vtt", ".jpg"].map((extension) => destination.replace(/\.mp4$/, extension))]) {
60
+ const watermarkedFiles = watermark ? ["-watermarked.mp4", "-watermarked.partial.mp4"] : [];
61
+ for (const candidate of [destination, ...[".partial.mp4", ".srt", ".vtt", ".jpg", ...watermarkedFiles].map((extension) => destination.replace(/\.mp4$/, extension))]) {
55
62
  try {
56
63
  await access(candidate);
57
64
  destination = path.join(outputDirectory, `${name}-${randomUUID()}.mp4`);
@@ -59,6 +66,8 @@ for (const candidate of [destination, ...[".partial.mp4", ".srt", ".vtt", ".jpg"
59
66
  } catch (error) { if (error.code !== "ENOENT") throw error; }
60
67
  }
61
68
  const partial = destination.replace(/\.mp4$/, ".partial.mp4");
69
+ const watermarked = destination.replace(/\.mp4$/, "-watermarked.mp4");
70
+ const watermarkedPartial = destination.replace(/\.mp4$/, "-watermarked.partial.mp4");
62
71
  const token = randomUUID();
63
72
  let encoder;
64
73
  let encoderDone;
@@ -67,6 +76,9 @@ let encoderLog = "";
67
76
  let settings;
68
77
  let subtitles = [];
69
78
  let thumbnailImage;
79
+ let badge;
80
+ let watermarking;
81
+ let ownsWatermarkedPartial = false;
70
82
  let received = 0;
71
83
  let lastFrameHash;
72
84
  let frameQueue = Promise.resolve();
@@ -103,10 +115,62 @@ function jpegDimensions(bytes) {
103
115
  return null;
104
116
  }
105
117
 
118
+ function pngDimensions(bytes) {
119
+ const signature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
120
+ if (bytes.length < 24 || signature.some((byte, index) => bytes[index] !== byte) || bytes.toString("latin1", 12, 16) !== "IHDR") return null;
121
+ return { width: bytes.readUInt32BE(16), height: bytes.readUInt32BE(20) };
122
+ }
123
+
124
+ /** Reads an encoded MP4 back and checks that it is the whole film: frames, duration, dimensions, codecs and language. */
125
+ async function verify(file, what) {
126
+ const { stdout } = await run(ffprobe.path, ["-v", "error", "-count_frames", "-show_streams", "-show_format", "-of", "json", file], { maxBuffer: 1024 * 1024 });
127
+ const probe = JSON.parse(stdout);
128
+ const video = probe.streams.find((stream) => stream.codec_type === "video");
129
+ const audio = probe.streams.find((stream) => stream.codec_type === "audio");
130
+ if (!video || video.codec_name !== "h264" || video.width !== width || video.height !== height || Number(video.nb_read_frames) !== settings.frames || video.pix_fmt !== "yuv420p" || !audio || audio.codec_name !== "aac" || audio.tags?.language !== languageTag || Math.abs(Number(probe.format.duration) - settings.duration) > 0.15) throw new Error(`The encoded ${what} did not pass duration, frame count, dimensions, language, or codec verification.`);
131
+ return probe;
132
+ }
133
+
134
+ /**
135
+ Lays the badge over a copy of the encoded film. The picture is encoded again at the same quality and the sound is
136
+ copied. The badge is converted to the film's BT.709 limited-range colours before it is laid over, so it keeps the
137
+ colours of AnimaGuru's own watermarked copies.
138
+ */
139
+ async function watermarkCopy() {
140
+ await writeFile(watermarkedPartial, "", { flag: "wx" });
141
+ ownsWatermarkedPartial = true;
142
+ // The badge arrives on stdin as a single PNG frame, which the overlay keeps over the whole film.
143
+ const child = spawn(ffmpeg.path, [
144
+ "-hide_banner", "-loglevel", "warning", "-y", "-i", partial, "-f", "png_pipe", "-i", "pipe:0",
145
+ "-filter_complex", `[1:v]scale=out_color_matrix=bt709:out_range=tv,format=yuva420p[mark];[0:v][mark]overlay=x=${badge.x}:y=${badge.y}:format=yuv420,format=yuv420p[film]`,
146
+ "-map", "[film]", "-map", "0:a:0",
147
+ "-c:v", "libx264", "-preset", "medium", "-crf", "18",
148
+ "-color_primaries", "bt709", "-color_trc", "bt709", "-colorspace", "bt709",
149
+ "-c:a", "copy", "-metadata:s:a:0", `language=${languageTag}`, "-metadata:s:v:0", `language=${languageTag}`,
150
+ "-movflags", "+faststart", watermarkedPartial,
151
+ ], { stdio: ["pipe", "ignore", "pipe"] });
152
+ let log = "";
153
+ child.stderr.on("data", (chunk) => { log = (log + chunk.toString()).slice(-8000); });
154
+ // An FFmpeg that fails early closes its input; its exit code reports why.
155
+ child.stdin.on("error", () => {});
156
+ child.stdin.end(badge.image);
157
+ const done = new Promise((resolve, reject) => {
158
+ child.once("error", reject);
159
+ child.once("close", (code, signal) => code === 0 ? resolve() : reject(new Error(`FFmpeg could not make the watermarked copy (${signal ?? `exit ${code}`}): ${log}`)));
160
+ });
161
+ watermarking = { child, done };
162
+ await done;
163
+ }
164
+
106
165
  async function cleanup(error) {
107
166
  if (aborted || finished) return;
108
167
  aborted = true;
109
168
  console.error(`Export stopped: ${error.message ?? error}`);
169
+ // SIGKILL, because an FFmpeg stopped any other way finishes the MP4 and would write the partial copy again; and at
170
+ // once, because Playwright's own Ctrl+C handler exits as soon as the headless browser has closed.
171
+ const marker = watermarking?.child;
172
+ if (marker && marker.exitCode === null && marker.signalCode === null) marker.kill("SIGKILL");
173
+ if (ownsWatermarkedPartial) await rm(watermarkedPartial, { force: true });
110
174
  if (encoder && encoder.exitCode === null) {
111
175
  encoder.stdin.destroy();
112
176
  encoder.kill("SIGTERM");
@@ -156,8 +220,17 @@ async function route(request, response, url) {
156
220
  encoder.once("close", (code) => code === 0 ? resolve() : reject(new Error(`FFmpeg exited ${code}: ${encoderLog}`)));
157
221
  });
158
222
  encoderDone.catch((error) => { encoderError = error; });
159
- console.log(`Rendering ${language.toUpperCase()}: ${settings.frames} frames (${settings.duration}s), ${width} × ${height} / ${fps} fps, ${subtitles.length ? `${subtitles.length} captions ${burnCaptions ? "drawn into the picture" : "as subtitle files"}` : "no captions"}.`);
160
- send({ fps, frames: settings.frames, burnCaptions, thumbnail }); return;
223
+ console.log(`Rendering ${language.toUpperCase()}: ${settings.frames} frames (${settings.duration}s), ${width} × ${height} / ${fps} fps, ${subtitles.length ? `${subtitles.length} captions ${burnCaptions ? "drawn into the picture" : "as subtitle files"}` : "no captions"}${watermark ? ", plus a watermarked copy" : ""}.`);
224
+ send({ fps, frames: settings.frames, burnCaptions, thumbnail, watermark: watermarkFiles }); return;
225
+ }
226
+ if (url.pathname === "/__export/watermark") {
227
+ if (!watermark || !encoder || encoding || badge || received) throw new Error("Send the watermark once, before the first frame.");
228
+ const image = await readBody(request, 1024 * 1024);
229
+ const size = pngDimensions(image);
230
+ const x = Number(url.searchParams.get("x")), y = Number(url.searchParams.get("y"));
231
+ if (request.headers["content-type"] !== "image/png" || !size || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || y < 0 || x + size.width > width || y + size.height > height) throw new Error("Expected a PNG watermark inside the picture.");
232
+ badge = { image, x, y, ...size };
233
+ send({ bytes: image.length }); return;
161
234
  }
162
235
  if (url.pathname === "/__export/frame") {
163
236
  if (!encoder || encoding || finished || aborted) throw new Error("No active frame capture.");
@@ -196,30 +269,34 @@ async function route(request, response, url) {
196
269
  if (url.pathname === "/__export/finish") {
197
270
  if (!encoder || encoding || received !== settings.frames) throw new Error("The capture is incomplete.");
198
271
  if (thumbnail && !thumbnailImage) throw new Error("The thumbnail was not captured.");
272
+ if (watermark && !badge) throw new Error("The watermark was not drawn.");
199
273
  encoding = true;
200
274
  encoder.stdin.end();
201
275
  await encoderDone;
202
276
  console.log("Checking the encoded video and soundtrack…");
203
- const { stdout } = await run(ffprobe.path, ["-v", "error", "-count_frames", "-show_streams", "-show_format", "-of", "json", partial], { maxBuffer: 1024 * 1024 });
204
- const probe = JSON.parse(stdout);
205
- const video = probe.streams.find((stream) => stream.codec_type === "video");
206
- const audio = probe.streams.find((stream) => stream.codec_type === "audio");
207
- if (!video || video.codec_name !== "h264" || video.width !== width || video.height !== height || Number(video.nb_read_frames) !== settings.frames || video.pix_fmt !== "yuv420p" || !audio || audio.codec_name !== "aac" || audio.tags?.language !== languageTag || Math.abs(Number(probe.format.duration) - settings.duration) > 0.15) throw new Error("The encoded file did not pass duration, frame count, dimensions, language, or codec verification.");
277
+ const probe = await verify(partial, "file");
278
+ if (watermark) {
279
+ console.log("Making the watermarked copy…");
280
+ await watermarkCopy();
281
+ await verify(watermarkedPartial, "watermarked copy");
282
+ }
208
283
  await rename(partial, destination);
284
+ if (watermark) await rename(watermarkedPartial, watermarked);
209
285
  // A player would show a same-named subtitle file on top of burned-in captions, so those exports get none.
210
286
  const subtitleFiles = subtitles.length && !burnCaptions ? [[".srt", toSrt(subtitles)], [".vtt", toVtt(subtitles, language)]].map(([extension, text]) => ({ path: destination.replace(/\.mp4$/, extension), text })) : [];
211
287
  for (const file of subtitleFiles) await writeFile(file.path, file.text);
212
288
  const thumbnailFile = thumbnailImage ? destination.replace(/\.mp4$/, ".jpg") : null;
213
289
  if (thumbnailFile) await writeFile(thumbnailFile, thumbnailImage);
214
- await writeFile(destination.replace(/\.mp4$/, ".json"), JSON.stringify({ file: path.basename(destination), title: config.title, audio: config.audio, subtitles: subtitleFiles.map((file) => path.basename(file.path)), thumbnail: thumbnailFile && { file: path.basename(thumbnailFile), time: thumbnail.time }, settings, verification: probe }, null, 2) + "\n");
290
+ await writeFile(destination.replace(/\.mp4$/, ".json"), JSON.stringify({ file: path.basename(destination), title: config.title, audio: config.audio, subtitles: subtitleFiles.map((file) => path.basename(file.path)), thumbnail: thumbnailFile && { file: path.basename(thumbnailFile), time: thumbnail.time }, ...(watermark ? { watermarked: { file: path.basename(watermarked), badge: { x: badge.x, y: badge.y, width: badge.width, height: badge.height } } } : {}), settings, verification: probe }, null, 2) + "\n");
215
291
  const bytes = (await stat(destination)).size;
216
292
  finished = true;
217
293
  clearInterval(idleCheck);
218
294
  console.log(`Saved: ${destination}\nSize: ${(bytes / 1024 / 1024).toFixed(1)} MiB\nVerified: ${settings.frames} frames, ${settings.duration}s, H.264 video and AAC audio.`);
219
295
  if (subtitleFiles.length) console.log(`Subtitles (${subtitles.length} captions, ${language}): ${subtitleFiles.map((file) => file.path).join(", ")}`);
220
296
  console.log(thumbnailFile ? `Thumbnail (${thumbnail.time}s): ${thumbnailFile}` : 'No thumbnail: set "poster" in film.config.json to the seconds of the frame to save as one.');
297
+ if (watermark) console.log(`Watermarked copy ("Made with AnimaGuru"): ${watermarked}`);
221
298
  response.once("finish", () => { void server.close(); });
222
- send({ path: destination, bytes, subtitles: subtitleFiles.map((file) => file.path), thumbnail: thumbnailFile }); return;
299
+ send({ path: destination, bytes, subtitles: subtitleFiles.map((file) => file.path), thumbnail: thumbnailFile, watermarked: watermark ? watermarked : null }); return;
223
300
  }
224
301
  if (url.pathname === "/__export/abort") {
225
302
  const message = JSON.parse((await readBody(request, 4096)).toString());
@@ -244,13 +321,14 @@ server = await createServer({
244
321
  });
245
322
  } }],
246
323
  });
324
+ // Finishing, which for a long film with a watermarked copy can take minutes, waits for no capture request.
247
325
  const idleCheck = setInterval(() => {
248
- if (Date.now() - lastActivity > 10 * 60 * 1000) void cleanup(new Error("No capture activity for ten minutes."));
326
+ if (!encoding && Date.now() - lastActivity > 10 * 60 * 1000) void cleanup(new Error("No capture activity for ten minutes."));
249
327
  }, 30000);
250
328
  process.once("SIGINT", () => { void cleanup(new Error("Cancelled.")); });
251
329
  process.once("SIGTERM", () => { void cleanup(new Error("Cancelled.")); });
252
330
  try { await server.listen(); } catch (error) { await cleanup(error); throw error; }
253
- console.log(`Open this local export page and keep it open until complete:\nhttp://127.0.0.1:${port}${context.route}?capture=1&export=${token}\nOutput: ${destination}\nThe preview can stay open. This capture runs locally without generation credits.`);
331
+ console.log(`Open this local export page and keep it open until complete:\nhttp://127.0.0.1:${port}${context.route}?capture=1&export=${token}\nOutput: ${destination}${watermark ? `\nWatermarked copy: ${watermarked}` : ""}\nThe preview can stay open. This capture runs locally without generation credits.`);
254
332
  if (headless) {
255
333
  try { await captureHeadless(`http://127.0.0.1:${port}${context.route}?capture=1&export=${token}`, { endpoint: "/__export/finish" }); }
256
334
  catch (error) { await cleanup(error); }
@@ -0,0 +1,93 @@
1
+ Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans) InstrumentSans-Italic[wdth,wght].ttf: Copyright 2022 The Instrument Sans Project Authors (https://github.com/Instrument/instrument-sans)
2
+
3
+ This Font Software is licensed under the SIL Open Font License, Version 1.1.
4
+ This license is copied below, and is also available with a FAQ at:
5
+ http://scripts.sil.org/OFL
6
+
7
+
8
+ -----------------------------------------------------------
9
+ SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
10
+ -----------------------------------------------------------
11
+
12
+ PREAMBLE
13
+ The goals of the Open Font License (OFL) are to stimulate worldwide
14
+ development of collaborative font projects, to support the font creation
15
+ efforts of academic and linguistic communities, and to provide a free and
16
+ open framework in which fonts may be shared and improved in partnership
17
+ with others.
18
+
19
+ The OFL allows the licensed fonts to be used, studied, modified and
20
+ redistributed freely as long as they are not sold by themselves. The
21
+ fonts, including any derivative works, can be bundled, embedded,
22
+ redistributed and/or sold with any software provided that any reserved
23
+ names are not used by derivative works. The fonts and derivatives,
24
+ however, cannot be released under any other type of license. The
25
+ requirement for fonts to remain under this license does not apply
26
+ to any document created using the fonts or their derivatives.
27
+
28
+ DEFINITIONS
29
+ "Font Software" refers to the set of files released by the Copyright
30
+ Holder(s) under this license and clearly marked as such. This may
31
+ include source files, build scripts and documentation.
32
+
33
+ "Reserved Font Name" refers to any names specified as such after the
34
+ copyright statement(s).
35
+
36
+ "Original Version" refers to the collection of Font Software components as
37
+ distributed by the Copyright Holder(s).
38
+
39
+ "Modified Version" refers to any derivative made by adding to, deleting,
40
+ or substituting -- in part or in whole -- any of the components of the
41
+ Original Version, by changing formats or by porting the Font Software to a
42
+ new environment.
43
+
44
+ "Author" refers to any designer, engineer, programmer, technical
45
+ writer or other person who contributed to the Font Software.
46
+
47
+ PERMISSION & CONDITIONS
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of the Font Software, to use, study, copy, merge, embed, modify,
50
+ redistribute, and sell modified and unmodified copies of the Font
51
+ Software, subject to the following conditions:
52
+
53
+ 1) Neither the Font Software nor any of its individual components,
54
+ in Original or Modified Versions, may be sold by itself.
55
+
56
+ 2) Original or Modified Versions of the Font Software may be bundled,
57
+ redistributed and/or sold with any software, provided that each copy
58
+ contains the above copyright notice and this license. These can be
59
+ included either as stand-alone text files, human-readable headers or
60
+ in the appropriate machine-readable metadata fields within text or
61
+ binary files as long as those fields can be easily viewed by the user.
62
+
63
+ 3) No Modified Version of the Font Software may use the Reserved Font
64
+ Name(s) unless explicit written permission is granted by the corresponding
65
+ Copyright Holder. This restriction only applies to the primary font name as
66
+ presented to the users.
67
+
68
+ 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
69
+ Software shall not be used to promote, endorse or advertise any
70
+ Modified Version, except to acknowledge the contribution(s) of the
71
+ Copyright Holder(s) and the Author(s) or with their explicit written
72
+ permission.
73
+
74
+ 5) The Font Software, modified or unmodified, in part or in whole,
75
+ must be distributed entirely under this license, and must not be
76
+ distributed under any other license. The requirement for fonts to
77
+ remain under this license does not apply to any document created
78
+ using the Font Software.
79
+
80
+ TERMINATION
81
+ This license becomes null and void if any of the above conditions are
82
+ not met.
83
+
84
+ DISCLAIMER
85
+ THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
86
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
87
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
88
+ OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
89
+ COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
90
+ INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
91
+ DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
92
+ FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
93
+ OTHER DEALINGS IN THE FONT SOFTWARE.
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "preparing": "Preparing MP4 export…",
3
3
  "rendering": "Rendering MP4 · {percent}% · {frame} / {total} frames. Keep this tab open.",
4
+ "watermark": "Drawing the Made with AnimaGuru watermark…",
4
5
  "thumbnail": "Saving the thumbnail…",
5
6
  "finishing": "Finishing the MP4 and checking its video and audio…",
6
7
  "saved": "MP4 saved: {path}",