@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.
- package/NOTICE.md +123 -0
- package/README.md +6 -2
- package/dist/animaguru.js +133 -4
- package/package.json +1 -1
- package/skills/animaguru-aurora-glass/SKILL.md +33 -0
- package/skills/animaguru-aurora-glass/references/recipe.md +27 -0
- package/skills/animaguru-bauhaus-geometry/SKILL.md +35 -0
- package/skills/animaguru-bauhaus-geometry/references/recipe.md +30 -0
- package/skills/animaguru-liquid-motion/SKILL.md +35 -0
- package/skills/animaguru-liquid-motion/references/recipe.md +24 -0
- package/skills/animaguru-one-line-art/SKILL.md +34 -0
- package/skills/animaguru-one-line-art/references/recipe.md +28 -0
- package/skills/animaguru-sticker-explainer/SKILL.md +37 -0
- package/skills/animaguru-sticker-explainer/references/recipe.md +27 -0
- package/skills/animaguru-synthwave-y2k/SKILL.md +35 -0
- package/skills/animaguru-synthwave-y2k/references/recipe.md +29 -0
- package/skills/animaguru-variety-captions/SKILL.md +36 -0
- package/skills/animaguru-variety-captions/references/recipe.md +27 -0
- package/templates/video/README.md +4 -2
- package/templates/video/VIDEO-STYLE-NOTICES.txt +24 -0
- package/templates/video/WORKSPACE-AGENTS.md +1 -1
- package/templates/video/scripts/export-mp4.mjs +91 -13
- package/templates/video/scripts/watermark/OFL.txt +93 -0
- package/templates/video/scripts/watermark/instrument-sans.woff2 +0 -0
- package/templates/video/scripts/watermark/logo-mark-512.png +0 -0
- package/templates/video/shared/locales/en/export.json +1 -0
- package/templates/video/shared/locales/en/style-studies.json +56 -0
- package/templates/video/src/exporter.ts +11 -1
- package/templates/video/src/studies/scenes.ts +391 -0
- package/templates/video/src/watermark.ts +50 -0
- 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
|
|
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
|
-
|
|
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
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
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.
|
|
Binary file
|
|
Binary file
|
|
@@ -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}",
|