frameyard 0.6.0 → 0.8.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/CHANGELOG.md +106 -0
- package/GLOSSARY.md +40 -16
- package/README.md +64 -55
- package/dist/assets/process.js.map +1 -1
- package/dist/assets/provenance.js.map +1 -1
- package/dist/cli.js +255 -59
- package/dist/cli.js.map +1 -1
- package/dist/composition/attributes.d.ts.map +1 -1
- package/dist/composition/descriptor.d.ts +17 -3
- package/dist/composition/descriptor.d.ts.map +1 -1
- package/dist/composition/descriptor.js +51 -28
- package/dist/composition/descriptor.js.map +1 -1
- package/dist/composition/diagnostics.js.map +1 -1
- package/dist/composition/format.d.ts +12 -0
- package/dist/composition/format.d.ts.map +1 -0
- package/dist/composition/format.js +28 -0
- package/dist/composition/format.js.map +1 -0
- package/dist/composition/index.d.ts +2 -1
- package/dist/composition/index.d.ts.map +1 -1
- package/dist/composition/index.js +1 -0
- package/dist/composition/index.js.map +1 -1
- package/dist/contracts.d.ts +55 -6
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +3 -0
- package/dist/contracts.js.map +1 -1
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +79 -0
- package/dist/doctor.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/html/document-adapter.d.ts +3 -3
- package/dist/html/document-adapter.d.ts.map +1 -1
- package/dist/html/document-adapter.js +1 -1
- package/dist/html/document-adapter.js.map +1 -1
- package/dist/html/index.d.ts +1 -1
- package/dist/html/index.d.ts.map +1 -1
- package/dist/html/parse.d.ts +10 -2
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js +14 -5
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts +12 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +233 -61
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -2
- package/dist/index.js.map +1 -1
- package/dist/preview/main.d.ts +2 -2
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +47 -19
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +5 -1
- package/dist/preview/timeline-data.d.ts.map +1 -1
- package/dist/preview/timeline-host.d.ts +2 -0
- package/dist/preview/timeline-host.d.ts.map +1 -1
- package/dist/preview/timeline-host.js +31 -10
- package/dist/preview/timeline-host.js.map +1 -1
- package/dist/preview/timeline-main.js +12 -7
- package/dist/preview/timeline-main.js.map +1 -1
- package/dist/project.d.ts +3 -5
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +2 -2
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +10 -4
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/ffmpeg.d.ts +69 -2
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +381 -69
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.d.ts +66 -5
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +226 -51
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/timeline.d.ts +41 -4
- package/dist/renderer/timeline.d.ts.map +1 -1
- package/dist/renderer/timeline.js +242 -39
- package/dist/renderer/timeline.js.map +1 -1
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +155 -31
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/media.d.ts.map +1 -1
- package/dist/runtime/media.js +3 -6
- package/dist/runtime/media.js.map +1 -1
- package/dist/runtime/video.d.ts +13 -0
- package/dist/runtime/video.d.ts.map +1 -0
- package/dist/runtime/video.js +41 -0
- package/dist/runtime/video.js.map +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +107 -103
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +13 -7
- package/dist/server/composition-html.d.ts.map +1 -1
- package/dist/server/composition-html.js +65 -16
- package/dist/server/composition-html.js.map +1 -1
- package/dist/server/preview-html.js +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +26 -6
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts.map +1 -1
- package/dist/server/timeline-preview-data.js +11 -0
- package/dist/server/timeline-preview-data.js.map +1 -1
- package/dist/server/timeline-preview-html.d.ts.map +1 -1
- package/dist/server/timeline-preview-html.js +2 -1
- package/dist/server/timeline-preview-html.js.map +1 -1
- package/dist/timeline/plan.js.map +1 -1
- package/dist/timeline/resolve.d.ts +4 -3
- package/dist/timeline/resolve.d.ts.map +1 -1
- package/dist/timeline/resolve.js +14 -4
- package/dist/timeline/resolve.js.map +1 -1
- package/dist/timeline/schema.d.ts +7 -0
- package/dist/timeline/schema.d.ts.map +1 -1
- package/dist/timeline/schema.js +50 -0
- package/dist/timeline/schema.js.map +1 -1
- package/dist/transcript.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
- package/docs/adr/0008-share-the-composition-document-grammar.md +1 -1
- package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +1 -1
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +1 -1
- package/docs/adr/0013-capture-bounded-multi-frame-stills-for-inspection.md +33 -0
- package/docs/adr/0014-compose-timeline-clips-with-css-blend-modes.md +37 -0
- package/docs/adr/0015-inherit-composition-format-from-the-project.md +28 -0
- package/docs/adr/0016-author-compositions-as-html-fragments.md +32 -0
- package/docs/adr/0017-render-timeless-compositions-as-stills.md +99 -0
- package/docs/migration-0.4.md +3 -1
- package/docs/migration-0.8.md +163 -0
- package/docs/preview-testing.md +60 -0
- package/docs/v0.1-design.md +52 -42
- package/examples/blend/README.md +106 -0
- package/examples/blend/assets/ink.mp4 +0 -0
- package/examples/blend/compositions/aurora.html +55 -0
- package/examples/blend/compositions/backdrop.html +74 -0
- package/examples/blend/compositions/labels.html +149 -0
- package/examples/blend/scripts/make-ink.sh +26 -0
- package/examples/blend/timelines/blend.timeline.json +155 -0
- package/examples/blend/video.config.js +11 -0
- package/examples/demo/compositions/demo.html +243 -251
- package/examples/static/README.md +17 -0
- package/examples/static/compositions/still.html +37 -0
- package/examples/static/timelines/still.timeline.json +13 -0
- package/examples/static/video.config.js +9 -0
- package/examples/video/compositions/video-mp4.html +349 -357
- package/examples/video/compositions/video-overlay.html +337 -345
- package/examples/video/compositions/video.html +349 -357
- package/examples/webgl/compositions/webgl.html +205 -213
- package/package.json +6 -3
- package/skills/frameyard/README.md +7 -5
- package/skills/frameyard/SKILL.md +17 -13
- package/skills/frameyard/references/common-mistakes.md +30 -6
- package/skills/frameyard/references/controllers.md +5 -4
- package/skills/frameyard/references/deterministic-authoring.md +42 -36
- package/skills/frameyard/references/diagnostics.md +11 -3
- package/skills/frameyard/references/failure-recovery.md +28 -7
- package/skills/frameyard/references/json-results.md +58 -7
- package/skills/frameyard/references/subtitles-karaoke.md +2 -2
- package/skills/frameyard/references/timelines.md +21 -15
- package/skills/frameyard/scripts/frameyard.mjs +20 -3
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Preview testing
|
|
2
|
+
|
|
3
|
+
The preview runs the production composition and timeline hosts in a browser. Use
|
|
4
|
+
the embedded hosts for deterministic Playwright probes; they are same-origin and
|
|
5
|
+
expose the same seek/settle contract the renderer uses.
|
|
6
|
+
|
|
7
|
+
## Composition preview
|
|
8
|
+
|
|
9
|
+
- Page: `/__frameyard/?composition=<id>`.
|
|
10
|
+
- Stage: the `#stage-frame` iframe.
|
|
11
|
+
- API: `window.__frameyardPreview` exposes `load(entry, props?)`, `seek(frame)`,
|
|
12
|
+
`settle()`, and `dispose()`.
|
|
13
|
+
- `seek()` is synchronous. Call `await settle()` before reading pixels; `load()`
|
|
14
|
+
resets the host, so apply props through it.
|
|
15
|
+
- A static composition reports `metadata: { width, height, static: true }`.
|
|
16
|
+
`seek()` is a no-op, `settle()` resolves, and the page disables the transport.
|
|
17
|
+
Wait for the static composition's own marker instead of `waitForFrame()`,
|
|
18
|
+
which expects a controller-updated `#box`.
|
|
19
|
+
|
|
20
|
+
## Timeline preview
|
|
21
|
+
|
|
22
|
+
- Page: `/__frameyard/timeline/<id>`; add `?__fy-embed=1` for the embedded host.
|
|
23
|
+
- API: `window.__fyTimeline` exposes `ready`, `format`, `durationInFrames`,
|
|
24
|
+
`load()`, `seek(frame)`, `settle()`, and `dispose()`.
|
|
25
|
+
- Order: `await ready`, `await load()`, `await seek(frame)`, `await settle()`.
|
|
26
|
+
- The non-embedded page owns transport and audio; only the embedded host exposes
|
|
27
|
+
`window.__fyTimeline`.
|
|
28
|
+
- A static composition source carries `static: true` and no
|
|
29
|
+
`durationInFrames`. The host loads the layer once and skips per-frame seek;
|
|
30
|
+
the layer remains visible while the timeline playhead moves.
|
|
31
|
+
|
|
32
|
+
## Waiting for a frame
|
|
33
|
+
|
|
34
|
+
- `settle()` waits for video presentation through `requestVideoFrameCallback`
|
|
35
|
+
and then for two animation frames for composition and iframe paint. Do not add
|
|
36
|
+
`waitForTimeout` to assertions.
|
|
37
|
+
- For a composition layer, wait for a controller-updated marker. The shared
|
|
38
|
+
helper `test/helpers/preview.ts` exports `waitForFrame(page, frame)`.
|
|
39
|
+
- For a video layer, sample the rendered stage after `settle()`; do not trust
|
|
40
|
+
`video.currentTime` alone. `test/timeline-preview-runtime.test.ts` covers this.
|
|
41
|
+
- The same helper exports `openEmbeddedTimeline`, `loadTimeline`, and
|
|
42
|
+
`seekTimeline`.
|
|
43
|
+
|
|
44
|
+
## Verification
|
|
45
|
+
|
|
46
|
+
For preview-only UI changes, run `pnpm check:preview`. It runs the static checks
|
|
47
|
+
and then builds before running `test/preview-runtime.test.ts`,
|
|
48
|
+
`test/timeline-preview.test.ts`, and `test/timeline-preview-runtime.test.ts`.
|
|
49
|
+
Use the full `pnpm check` when the change reaches shared contracts, the runtime,
|
|
50
|
+
the renderer, or the CLI, and before pushing or publishing.
|
|
51
|
+
|
|
52
|
+
## Notes
|
|
53
|
+
|
|
54
|
+
- Preview iframes and the composition stage pin `color-scheme: light` to match
|
|
55
|
+
the production capture context. A dark inherited scheme makes Chromium paint a
|
|
56
|
+
white base behind transparent compositions.
|
|
57
|
+
- A running `dev` server keeps the `dist/` modules it loaded. Restart it after
|
|
58
|
+
`pnpm build`.
|
|
59
|
+
- The timeline preview host is not the production compositor; compare pixels at
|
|
60
|
+
the capture boundary, not encoded output.
|
package/docs/v0.1-design.md
CHANGED
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
**Status:** Accepted
|
|
4
4
|
**Initial acceptance:** 2026-10-27
|
|
5
|
-
**Last design revision:** 2026-
|
|
5
|
+
**Last design revision:** 2026-11-03
|
|
6
6
|
|
|
7
|
-
This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.
|
|
7
|
+
This document records the decisions confirmed during design sessions. It is the living product and architecture baseline; later changes revise this document explicitly rather than silently broaden the scope. The original v0.1 baseline has been revised through 0.8 and extended with static clip blend modes. Architecture decisions that changed the baseline are recorded in `docs/adr/`.
|
|
8
8
|
|
|
9
9
|
## 1. Product contract
|
|
10
10
|
|
|
@@ -16,7 +16,7 @@ The target use cases are product demos, social clips, animated diagrams, data vi
|
|
|
16
16
|
|
|
17
17
|
The core success criterion is an end-to-end example in which a developer can:
|
|
18
18
|
|
|
19
|
-
1. Create reusable
|
|
19
|
+
1. Create reusable HTML compositions with a fixed duration and a format they either declare or inherit from the project.
|
|
20
20
|
2. Animate them with HTML, CSS, SVG, and, where needed, a seek-safe controller for Canvas 2D and WebGL.
|
|
21
21
|
3. Supply JSON props and synchronized audio.
|
|
22
22
|
4. Assemble compositions and local media into a timeline with tracks, clips, trims, layers, and nested timelines.
|
|
@@ -30,18 +30,20 @@ The agent-enabled success criterion additionally requires an agent using the off
|
|
|
30
30
|
|
|
31
31
|
### Project
|
|
32
32
|
|
|
33
|
-
- Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition
|
|
33
|
+
- Projects use ordinary HTML, CSS, static assets, and native JavaScript controllers, either inline in a composition fragment or in external modules.
|
|
34
34
|
- `video.config.js` explicitly maps project IDs to source files. There is no directory scanning. It has three optional maps:
|
|
35
35
|
- `format`: the project's fixed `width`, `height`, and `fps`.
|
|
36
|
-
- `compositions`: IDs mapped to `.html` composition
|
|
36
|
+
- `compositions`: IDs mapped to `.html` composition fragments.
|
|
37
37
|
- `timelines`: IDs mapped to JSON timeline documents.
|
|
38
|
-
- `format` is required when a project has timelines. Every timeline uses it. A composition
|
|
38
|
+
- `format` is required when a project has timelines. Every timeline uses it. It also fills `width`, `height`, and `fps` for animated compositions that omit them; a declared value always wins. A composition that declares an `fps` must share the project frame rate exactly; a different size is fitted and transformed by the clip. A composition that declares neither `duration-in-frames` nor `fps` is static; it uses width and height, declared or inherited, and ignores the inherited fps. A composition that declares no dimensions in a project without `format` cannot render and fails with `COMPOSITION_FORMAT_UNRESOLVED`.
|
|
39
39
|
- Composition and timeline IDs share one namespace so `render <id>`, `check <id>`, and `still <id>` are unambiguous. The config rejects duplicates.
|
|
40
40
|
- A project may contain any number of compositions and timelines.
|
|
41
41
|
|
|
42
42
|
### Composition
|
|
43
43
|
|
|
44
|
-
- A composition is a
|
|
44
|
+
- A composition is a partial HTML fragment with one top-level `<fy-composition>` root. The framework wraps it in the document shell, import map, sizing style, and host runtime at serve and render time. `width`, `height`, and `fps` are optional and inherit from the project `format` when absent; declared values win. `width` and `height` are declared together or not at all. Props cannot change metadata.
|
|
45
|
+
- A composition that declares neither `duration-in-frames` nor `fps` is static: it has no frame clock, is captured once in its initial state, and renders to a PNG. Its controller may `mount()` and validate props, but `update()` is not called and frame-state variables are not set. CSS animations and transitions are paused at time 0 with a warning; `data-fy-anim-*`, `<video>`, `<audio>`, and `<fy-transcript>` are errors. `fps` without `duration-in-frames` is an error. See [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
|
|
46
|
+
- Props blocks, the controller, and import maps are top-level siblings of the root. Top-level `<style>` and `<link rel="stylesheet">` elements are hoisted into the generated head; styles inside the root stay in the body. A fragment must not contain `<!doctype>`, `<html>`, `<head>`, `<body>`, `<title>`, `<meta>`, or the reserved `data-fy-host` attribute.
|
|
45
47
|
- Default props are a JSON block, `<script type="application/json" data-fy="props">`. A load's JSON props merge shallowly over the defaults, are validated as JSON, deep-frozen, and fixed for that load.
|
|
46
48
|
- Props bind into the document through explicit `data-fy-*` attributes: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat`. Primitive top-level props are also exposed as `--fy-prop-<name>` CSS custom properties. Bindings apply once per load; props are immutable.
|
|
47
49
|
- CSS and the Web Animations API are the animation language. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in composition frames.
|
|
@@ -59,13 +61,14 @@ The agent-enabled success criterion additionally requires an agent using the off
|
|
|
59
61
|
- a composition, identified by its project ID;
|
|
60
62
|
- a nested timeline, identified by its project ID;
|
|
61
63
|
- a local video, image, or audio asset.
|
|
62
|
-
- A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings.
|
|
63
|
-
- A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity and
|
|
64
|
-
- Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images
|
|
65
|
-
- Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box.
|
|
66
|
-
- Visual clips
|
|
64
|
+
- A composition clip fixes props for that instance and may trim the composition's source range. The same composition may appear multiple times with different props, source ranges, transforms, and audio settings. A clip whose composition source is static behaves like an image: it must declare `duration`, must use `sourceIn: 0`, has no intrinsic source duration, and contributes no audio or transcripts. The renderer captures one transparent PNG per composition/props pair and reuses it across segments.
|
|
65
|
+
- A nested timeline clip is a visual and audio source. It uses the project format, trims with the same source in-point and duration, contributes its audio, and is rendered as a group so parent opacity, transform, and blend mode apply to the whole nested result. A blend mode inside a nested timeline sees only that timeline's own backdrop; a transparent nested timeline does not see the parent's layers, and a declared nested background is part of the group. Cycles are rejected statically. Timeline clips do not override props of compositions inside the nested timeline.
|
|
66
|
+
- Media clips use local project files only. Video and audio durations, dimensions, and audio presence are probed before frame production. Images and static composition clips require an explicit duration. A video or audio clip that exceeds its source range is an error; an omitted duration uses the remaining source.
|
|
67
|
+
- Visual clips support a static transform: fit policy (`contain`, `cover`, `stretch`), an optional explicit `width`/`height` box (both fields together, mutually exclusive with `scale`) or a `scale` multiplier, pixel offset, rotation, and opacity. `fit` applies inside the box; without one, the project frame is the box.
|
|
68
|
+
- Visual clips may declare a static `blend` mode. It uses CSS `mix-blend-mode` semantics against the accumulated backdrop of all lower layers and the timeline background, respects the clip's source alpha, and defaults to `normal`. The supported modes are `normal`, `multiply`, `screen`, `overlay`, `darken`, `lighten`, `color-dodge`, `color-burn`, `hard-light`, `soft-light`, `difference`, and `exclusion`; `hue`, `saturation`, `color`, and `luminosity` are rejected. There are no keyframes, masks, filters, or other effects.
|
|
69
|
+
- Visual clips contribute their audio to the timeline mix unless muted: composition audio, embedded video audio, and nested timeline audio. Static compositions contribute no audio; clip audio settings on a static composition are ignored with a warning. Audio-only clips live on audio tracks and may source audio or video files. Per-clip and per-audio-track gain and mute are supported. There are no linked A/V pairs; a J/L cut is expressed by muting a video clip and placing a separate audio clip with a different source in-point.
|
|
67
70
|
- A timeline's duration is derived from the latest clip end. An explicit `durationInFrames` may extend it but not cut clips. A timeline with no clips is invalid; an audio-only timeline is valid and renders black video with audio.
|
|
68
|
-
- A timeline background is transparent by default. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
|
|
71
|
+
- A timeline background is transparent by default. Blend modes see the accumulated backdrop including this background. The final video composites it over black; a nested timeline remains transparent so lower layers can show through. A composition that paints no background is transparent and may be used as an overlay.
|
|
69
72
|
- The preview provides a read-only timeline view: tracks, clips, playhead, program monitor, and diagnostics. It is not a visual editor.
|
|
70
73
|
|
|
71
74
|
## 3. Deterministic execution
|
|
@@ -78,8 +81,9 @@ The timeline frame is the master clock. A composition's frame is derived from th
|
|
|
78
81
|
- JavaScript animation runs in the controller's `update()`, which must be synchronous and seek-safe. It cannot depend on previously visited frames.
|
|
79
82
|
- Declarative bindings apply once per load, before the controller mounts. Controller-created DOM is the controller's responsibility.
|
|
80
83
|
- Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
|
|
81
|
-
- Timeline clips use static transforms. Timeline-level animation, transitions,
|
|
84
|
+
- Timeline clips use static transforms and static blend modes. Timeline-level animation, transitions, and other effects are not supported.
|
|
82
85
|
- Preview and production seek composition clips explicitly. The timeline compositor is deterministic given the captured clip frames, media frames, clip settings, and segment boundaries.
|
|
86
|
+
- The resolved format—declared width, height, and fps combined with the project format—is part of the immutable frame environment, alongside props and loaded assets. Layouts that use viewport units or percentages therefore depend on the resolved format by design; preview and production always resolve the same values.
|
|
83
87
|
- Assets, remote data, images, fonts, generated audio, transcripts, and media are resolved before frame production. For a segmented timeline render, each segment's composition and nested-timeline clips are loaded and settled before that segment's frames are captured; no frame production depends on a new network request. Network is blocked during capture.
|
|
84
88
|
- AI model inference is never part of the loading phase, controller lifecycle, or frame production.
|
|
85
89
|
- The baseline supports DOM, CSS, SVG, Canvas 2D, WebGL, and frame-seeked embedded video. Canvas 2D and WebGL are drawn from the current frame by the controller.
|
|
@@ -90,39 +94,39 @@ The timeline frame is the master clock. A composition's frame is derived from th
|
|
|
90
94
|
|
|
91
95
|
### Reproducibility guarantee
|
|
92
96
|
|
|
93
|
-
For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final
|
|
97
|
+
For the same timeline or composition revision, props, assets, segment plan, and frame under a pinned runtime/browser/FFmpeg environment, the captured composition frames and the deterministic timeline composite are identical. The final output is not promised to be bit-identical: encoded bytes depend on the encoder and its runtime.
|
|
94
98
|
|
|
95
99
|
Preview and production share composition behavior and capture composition frames the same way, but the timeline preview composites layers in Chromium while production composites them with FFmpeg. Small differences in scaling, alpha, and video color conversion are expected and do not violate clip-capture parity.
|
|
96
100
|
|
|
97
101
|
## 4. Preview and production rendering
|
|
98
102
|
|
|
99
|
-
Preview and production execute the same composition
|
|
103
|
+
Preview and production execute the same composition sources and host runtime. They differ in which component drives the frame clock, which compositor combines timeline layers, and whether frames are captured.
|
|
100
104
|
|
|
101
105
|
The preview provides:
|
|
102
106
|
|
|
103
|
-
- Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics.
|
|
104
|
-
- Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics.
|
|
107
|
+
- Composition preview: composition selection, JSON props editing, play/pause, frame-accurate scrubbing, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, current frame and time, audio controls, and diagnostics. A static composition loads once and disables the transport.
|
|
108
|
+
- Timeline preview: timeline selection, a read-only track/clip view, playhead, program monitor, frame-by-frame stepping with buttons or arrow keys, one-second jumps with buttons or Shift+arrow keys, audio controls, and diagnostics. A static composition layer stays loaded and is not seeked per frame.
|
|
105
109
|
- A bounded pool of active composition and nested-timeline clips. It loads clips around the playhead instead of preloading a whole long timeline.
|
|
106
110
|
|
|
107
|
-
The preview server serves each composition
|
|
111
|
+
The preview server serves each composition fragment at its project path, wraps it in the fixed document shell, and injects the framework import map and host runtime. It serves timeline documents and a timeline preview host. The timeline preview host drives active composition/nested-timeline clips, seeks them, and exposes a timeline API to the preview. Preview audio is mixed by the preview page from the resolved timeline audio clips.
|
|
108
112
|
|
|
109
|
-
Production composition rendering
|
|
113
|
+
Production composition rendering uses one Chromium session per composition. An animated composition seeks every frame, captures PNGs, and encodes them with FFmpeg. A static composition loads once, captures one PNG, and publishes it without FFmpeg or an encoder.
|
|
110
114
|
|
|
111
115
|
Production timeline rendering is segmented and parallel:
|
|
112
116
|
|
|
113
117
|
1. Run static timeline validation and resolve the render plan.
|
|
114
118
|
2. Split the timeline into segments at active-layer changes or a maximum segment duration. Each segment has a fixed set of visual layers and a fixed filter graph.
|
|
115
|
-
3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background.
|
|
116
|
-
4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264.
|
|
119
|
+
3. In parallel workers, capture the active composition and nested-timeline clip frames for each segment with Chromium and encode them to lossless FFV1 intermediates. Transparent clips are captured with an alpha-preserving background. A static composition is captured once per props variant as a transparent PNG and reused as an image layer across segments and workers.
|
|
120
|
+
4. Decode media clips, apply trims, fit, static transforms, and opacity, composite the segment with FFmpeg, and encode H.264, HEVC, or VP9.
|
|
117
121
|
5. Join the ordered segments with a stream copy. Segment boundaries never change frame order or timing.
|
|
118
122
|
6. Mix the timeline audio once with FFmpeg and mux it into the final output.
|
|
119
123
|
7. Publish the output atomically; delete scratch intermediates on success and failure.
|
|
120
124
|
|
|
121
|
-
The default timeline worker count is 8, configurable up to the machine's capacity. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
|
|
125
|
+
The default timeline worker count is 8, configurable up to the machine's capacity. With `--encoder nvenc` the count is capped at eight concurrent encode sessions, the consumer driver limit; a larger `--workers` value produces an `ENCODER_WARNING` and the lower count. Scratch is a configurable directory with a bounded budget; it must not depend on `/tmp` alone. The reference long-form target is a representative 60-minute 1920×1080 30 fps timeline with a 50/50 composition/media split by layer-seconds, 60% one active layer, 30% two, and 10% three, rendered in 30 minutes or less on the reference machine. The 10-second benchmark measured 10.7 s for two composition captures, 35.2 MB of FFV1, and 6.1 s to composite and encode. Continuous three-layer stress runs measured 52.4 s and 58.5 s for 60 s of timeline, about 1× real time, and are documented as worst-case rather than the target workload. The representative gate measured 29.39 s for 60 s with four workers, exact 1800-frame output, and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact 108,000-frame / 3,600 s output; it is stricter than the 50/50 target mix because every frame is an HTML composition capture. The fast path copies full-frame, untransformed video whose source matches the project format and has keyframes at segment boundaries; media that needs scaling or transforms is re-encoded and adds cost not included in that projection.
|
|
122
126
|
|
|
123
|
-
The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial
|
|
127
|
+
The renderer uses a bounded segment plan, a worker pool, and a scratch budget. It never publishes a partial output, and it cleans scratch on success and failure. Segment boundaries preserve timeline frame order and timing; the final audio mix is muxed once.
|
|
124
128
|
|
|
125
|
-
`check` and `still` use the same clip capture and timeline composite path for
|
|
129
|
+
`check` and `still` use the same clip capture and timeline composite path for requested frames. `check` evaluates every timeline frame; on long timelines this is a validation cost, not a render. A static composition has one state and is inspected once. `still` captures one or more requested frames as PNGs without encoding a video; a static composition exposes only frame 0.
|
|
126
130
|
|
|
127
131
|
## 5. Audio and output
|
|
128
132
|
|
|
@@ -133,13 +137,15 @@ A timeline has one audio mix. It combines:
|
|
|
133
137
|
- timeline audio clips on audio tracks;
|
|
134
138
|
- audio from nested timeline clips, recursively.
|
|
135
139
|
|
|
136
|
-
Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final
|
|
140
|
+
Per-clip gain and mute, per-audio-track gain and mute, and composition-level volume apply. Audio fades are deferred. The mix is produced once with FFmpeg and muxed into the final output. Preview mixes the same resolved clips in the browser for monitoring; it is not the production source of truth.
|
|
137
141
|
|
|
138
|
-
Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition
|
|
142
|
+
Audio, video, and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition URL.
|
|
139
143
|
|
|
140
144
|
Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change timeline duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or the timeline deliberately.
|
|
141
145
|
|
|
142
|
-
|
|
146
|
+
Timelines and animated compositions produce MP4, MOV, MKV, or WebM files. The default is MP4 with H.264 video and AAC audio; HEVC and VP9 are opt-in, and WebM is VP9 with Opus. HEVC in MP4 and MOV keeps the `hvc1` sample entry and is not playable in browsers without platform HEVC decoding. Frameyard exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. A static composition produces one PNG and uses no encoder.
|
|
147
|
+
|
|
148
|
+
PNG output through `still`, including a bounded set of inspection frames, is a debugging artifact, not general image-sequence support. A standalone static composition renders to one PNG; this is the composition's production artifact, not an image sequence.
|
|
143
149
|
|
|
144
150
|
A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, invalid composition output, segment encoding failure, or scratch failure. Diagnostics identify the timeline, clip, segment, and, where applicable, the frame. The CLI exits nonzero, cleans temporary files, and never reports a partial video as successful. Existing output files are not overwritten unless explicitly requested.
|
|
145
151
|
|
|
@@ -149,16 +155,16 @@ Project and composition commands are:
|
|
|
149
155
|
|
|
150
156
|
- `init`: scaffold a timeline-first project with a composition, a one-clip timeline, a project format, an agent guide, a `.gitignore` for `out/`, and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts.
|
|
151
157
|
- `dev`: start the preview with composition and timeline modes.
|
|
152
|
-
- `render <id> [--out video
|
|
153
|
-
- `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding.
|
|
154
|
-
- `still <id> --frame <
|
|
158
|
+
- `render <id> [--out video]`: render a timeline, an animated composition, or a static composition. Timeline renders accept `--workers <n>` and `--scratch <dir>`; composition renders accept `--concurrency <n>` from 1 to 16. `--container <mp4|mov|mkv|webm>` and `--codec <h264|hevc|vp9>` select the delivery pair (MP4/H.264 by default; WebM defaults to VP9), and `--encoder <auto|software|nvenc|qsv>` selects how it is encoded; `auto` is the default and probes NVENC then Quick Sync before falling back to software. The output extension must match the container. The default output is `out/<id>.<ext>`. A static composition writes `out/<id>.png` and rejects `--container`, `--codec`, `--encoder`, and `--concurrency`. The JSON result is a discriminated union with `kind: "video"` or `kind: "image"`.
|
|
159
|
+
- `check <id>`: run static validation, then validate configuration, props, assets, media, audio, and transcript requirements, load the target, and evaluate every frame without capturing or encoding. A static composition is loaded and inspected once and returns `kind: "static"` with `framesChecked: 1`.
|
|
160
|
+
- `still <id> --frame <spec>`: capture one or more requested frames as PNGs through the clip capture and timeline composite path. A static composition exposes exactly frame 0. `<spec>` is a frame number (`42`), an inclusive range (`2-12`), or a comma-separated list (`0,118-126,240`); duplicates are removed preserving the first occurrence, input order is preserved, and one invocation captures at most 32 unique frames. There is no step or sequence mode. A single frame keeps the existing `--out <file.png>` behavior; multiple frames use `--out <directory>` and write `still-<id>-<frame>.png` files, defaulting to the current working directory. Frames and output conflicts are validated before Chromium, all frames are captured to temporary files before publication, and a failed capture publishes no files. JSON results add `frames` and keep the singular `frame` and `path` only when one frame was captured. See [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md).
|
|
155
161
|
- `doctor`: inspect core requirements and optional integrations without repairing or downloading anything.
|
|
156
162
|
|
|
157
|
-
`check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe document
|
|
163
|
+
`check`, `still`, and `render` run a Node-side static validation before launching Chromium. For a timeline it validates the JSON document, tracks, clips, references, nested timelines, cycles, frame-rate compatibility, overlaps, source bounds, media, transforms, audio settings, props, and every referenced composition. Static validation and the composition host read the composition grammar through one browser-safe reader with document and fragment scopes. Timeline diagnostics carry stable codes and optional source locations or JSON paths, and composition diagnostics from inside a timeline carry the clip and track context.
|
|
158
164
|
|
|
159
165
|
Local asset-production commands are `tts` and `transcribe`. Every non-interactive command supports `--json` with stable result and error schemas. Structured results go to stdout; human diagnostics and inference progress go to stderr.
|
|
160
166
|
|
|
161
|
-
The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It injects
|
|
167
|
+
The built-in server serves native ES modules, static assets, composition entries, timeline documents, and the timeline preview host. It wraps each configured composition fragment in the fixed document shell, injects the framework import map and composition host, and serves an inline controller through a virtual module URL. It performs no dependency pre-bundling. Third-party libraries must initially be browser-ready ES modules referenced explicitly. Running through the CLI server is required; direct `file://` execution is unsupported for deterministic preview or rendering.
|
|
162
168
|
|
|
163
169
|
Frameyard uses one repository and one publishable npm package with four internal layers:
|
|
164
170
|
|
|
@@ -255,20 +261,20 @@ Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` re
|
|
|
255
261
|
Tests prioritize public behavior and end-to-end evidence over implementation-only unit coverage:
|
|
256
262
|
|
|
257
263
|
- HTML compositions validate metadata, default props, bindings, conditionals, repeats, and controller declarations, including inline/external parity and the exactly-one-controller rule.
|
|
258
|
-
- Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium.
|
|
264
|
+
- Static composition validation catches metadata, root/controller count, binding, animation, script, element, and asset-reference errors before Chromium. It resolves declared format values against the project format and reports an unresolved format to the command rather than the document. A composition with neither time attribute is static; `fps` without `duration-in-frames`, `data-fy-anim-*`, media, and transcripts on a static composition are errors.
|
|
259
265
|
- Timeline JSON validation catches schema, track, clip, reference, cycle, frame-rate, overlap, bounds, media, transform, audio, and props errors before Chromium.
|
|
260
266
|
- Diagnostics carry stable codes and source locations or JSON paths; JSON results expose the composition or timeline entry, clips, tracks, and top-level diagnostics.
|
|
261
267
|
- Sequential, individual, random-order, and parallel composition renders produce equivalent images; timeline segment workers produce ordered output and identical captured composition frames.
|
|
262
|
-
- Determinism checks compare captured composition pixels or stills. The final
|
|
263
|
-
- Preview and renderer use the same composition
|
|
268
|
+
- Determinism checks compare captured composition pixels or stills. The final encoded bytes and the FFmpeg composite versus Chromium preview are not reproducibility oracles.
|
|
269
|
+
- Preview and renderer use the same composition source and host behavior; timeline preview uses the same active clip hosts.
|
|
264
270
|
- CSS, controller JavaScript, SVG, Canvas, WebGL, embedded video, and audio frames seek correctly.
|
|
265
271
|
- Frame-scheduled `data-fy-anim-*` animations and frame-state CSS variables produce the expected frames.
|
|
266
272
|
- Timeline capture preserves alpha for transparent compositions; FFV1 intermediates decode pixel-identically to the captured frames.
|
|
267
|
-
- Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected
|
|
273
|
+
- Timeline segment encoding, ordered concatenation, global audio mix, and scratch cleanup produce the expected output dimensions, frame count, duration, and audio stream.
|
|
268
274
|
- Nested timelines preserve group opacity and transform semantics; cycles fail static validation.
|
|
269
275
|
- Media clips trim and fit deterministically; images require durations; source overruns fail.
|
|
270
276
|
- Props overrides for timeline composition clips merge over clip props and are reflected in the render.
|
|
271
|
-
- `check` exercises every frame; `still` follows the clip capture and timeline composite path.
|
|
277
|
+
- `check` exercises every animated frame and inspects a static composition once; `still` follows the clip capture and timeline composite path. A static composition render captures one PNG; a timeline reuses one static image capture per props variant.
|
|
272
278
|
- The 10-second and 60-second long-form benchmarks record capture, FFV1, composite, encode, disk, worker, and audio numbers. Continuous three-layer runs measured 52.4 s and 58.5 s for 60 s of timeline and are documented as worst-case (~1× real time). The representative 50/50, 60/30/10 occupancy gate measured 29.39 s for 60 s with four workers and a 1080p media fast path, extrapolating to about 29.4 minutes for 60 minutes with exact 1800-frame output. The full 60-minute composition-only gate rendered 108,000 frames in 35.35 minutes with exact output and clean scratch, documenting composition-heavy performance above 30 minutes.
|
|
273
279
|
- Golden-frame tests cover representative composition and timeline visual behavior.
|
|
274
280
|
- Normal CI uses fake `qwen-tts` and `whisper-cli` executables and small fixtures to verify arguments, artifacts, manifests, cancellation, and errors without model downloads or GPU hardware.
|
|
@@ -279,13 +285,13 @@ Tests prioritize public behavior and end-to-end evidence over implementation-onl
|
|
|
279
285
|
|
|
280
286
|
These are scope boundaries, not permanent rejections:
|
|
281
287
|
|
|
282
|
-
- Transitions,
|
|
288
|
+
- Transitions, filters, keyframes, masks, and audio fades
|
|
283
289
|
- Full visual timeline editing, drag-and-drop, clip handles, and undo/redo
|
|
284
290
|
- Timeline-level variables and prop overrides for compositions inside nested timelines
|
|
285
291
|
- Track solo and video-track visibility
|
|
286
292
|
- Per-timeline formats and frame-rate conversion/retiming
|
|
287
293
|
- Persistent clip caches; the segmented renderer cleans its intermediates
|
|
288
|
-
-
|
|
294
|
+
- GPU compositing and codecs beyond H.264, HEVC, and VP9
|
|
289
295
|
- Hosted rendering, collaboration, and asset marketplaces
|
|
290
296
|
- Cloud generative-AI services, text-to-video generation, and image generation
|
|
291
297
|
- `qwentts.cpp` reference-audio voice cloning and VoiceDesign
|
|
@@ -301,8 +307,12 @@ These are scope boundaries, not permanent rejections:
|
|
|
301
307
|
- Dynamic composition or timeline metadata derived from props
|
|
302
308
|
- Live streams, remote media, and other visual sources that cannot be deterministically seeked
|
|
303
309
|
- Dynamic per-frame asset loading or AI inference
|
|
304
|
-
-
|
|
310
|
+
- Unbounded batch still capture; `still` accepts at most 32 inspection frames per invocation and is not an image-sequence exporter
|
|
305
311
|
- Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
|
|
312
|
+
- Audio, transcripts, or embedded video in a static composition; use a timeline
|
|
313
|
+
- A `--video` escape hatch for static composition renders; use a timeline image clip
|
|
314
|
+
- Exporting a fully static timeline as a PNG
|
|
315
|
+
- Recursive static optimization of nested timelines
|
|
306
316
|
- Built-in TypeScript transpilation and npm dependency optimization
|
|
307
317
|
- Rendering untrusted compositions
|
|
308
318
|
- Bit-identical cross-platform encoded output or AI regeneration
|
|
@@ -310,4 +320,4 @@ These are scope boundaries, not permanent rejections:
|
|
|
310
320
|
|
|
311
321
|
## 13. Release posture
|
|
312
322
|
|
|
313
|
-
Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md).
|
|
323
|
+
Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, timeline, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize. 0.6.0 adds timelines, project formats, segmented timeline rendering, and a timeline-first scaffold without breaking the existing HTML composition authoring contract. See [ADR 0010](./adr/0010-make-timelines-the-render-unit.md), [ADR 0011](./adr/0011-compose-timeline-frames-in-a-live-browser-host.md), and [ADR 0012](./adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md). 0.7.0 adds delivery container, codec, and encoder selection, bounded multi-frame still inspection, and optional encoder probes in `doctor` without breaking the timeline or composition contracts; bounded still inspection is recorded in [ADR 0013](./adr/0013-capture-bounded-multi-frame-stills-for-inspection.md). Static clip blend modes are recorded in [ADR 0014](./adr/0014-compose-timeline-clips-with-css-blend-modes.md). Optional composition `width`, `height`, and `fps` with project-format inheritance are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md). 0.8.0 changes compositions to HTML fragments, inherits their format from the project, and renders timeless compositions as still images; the contracts are recorded in [ADR 0015](./adr/0015-inherit-composition-format-from-the-project.md), [ADR 0016](./adr/0016-author-compositions-as-html-fragments.md), and [ADR 0017](./adr/0017-render-timeless-compositions-as-stills.md).
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# Frameyard blend example
|
|
2
|
+
|
|
3
|
+
A 12-second, 1920×1080, 30 fps timeline that demonstrates every supported CSS
|
|
4
|
+
blend mode on composition and video clips. Four video tracks build one frame:
|
|
5
|
+
|
|
6
|
+
- `v1` **backdrop** — a static opaque HTML composition with a grid, concentric
|
|
7
|
+
rings, and a dark vignette. It omits the time metadata and all dimensions, so
|
|
8
|
+
it inherits the project format and is captured once at the timeline's size.
|
|
9
|
+
- `v2` **aurora** — a static transparent HTML composition with three blurred
|
|
10
|
+
color blobs. It declares its dimensions but no time metadata, so it is also
|
|
11
|
+
captured once. Its clip uses `blend: "screen"` for the full 12 seconds.
|
|
12
|
+
- `v3` **ink** — a local 1280×720 H.264 clip scaled to fill the project frame.
|
|
13
|
+
Twelve touching clips play one second each, cycling through all 12 supported
|
|
14
|
+
modes in timeline order.
|
|
15
|
+
- `v4` **labels** — an animated transparent HTML composition in normal mode. It
|
|
16
|
+
lists the modes and their time ranges, and draws a progress bar from the
|
|
17
|
+
`--fy-progress` frame-state variable, so it is evaluated per frame.
|
|
18
|
+
|
|
19
|
+
The mode sequence on `v3` is:
|
|
20
|
+
|
|
21
|
+
| Time | Mode |
|
|
22
|
+
| --- | --- |
|
|
23
|
+
| 0–1 s | `normal` |
|
|
24
|
+
| 1–2 s | `multiply` |
|
|
25
|
+
| 2–3 s | `screen` |
|
|
26
|
+
| 3–4 s | `overlay` |
|
|
27
|
+
| 4–5 s | `darken` |
|
|
28
|
+
| 5–6 s | `lighten` |
|
|
29
|
+
| 6–7 s | `color-dodge` |
|
|
30
|
+
| 7–8 s | `color-burn` |
|
|
31
|
+
| 8–9 s | `hard-light` |
|
|
32
|
+
| 9–10 s | `soft-light` |
|
|
33
|
+
| 10–11 s | `difference` |
|
|
34
|
+
| 11–12 s | `exclusion` |
|
|
35
|
+
|
|
36
|
+
The aurora is transparent, so its `screen` blends only where its blobs have
|
|
37
|
+
alpha. The video is opaque, so its whole scaled frame participates in the blend.
|
|
38
|
+
The labels layer stays in `normal` mode so the copy remains readable.
|
|
39
|
+
|
|
40
|
+
Blend modes are static per clip. The example uses consecutive clips on `v3` to
|
|
41
|
+
show every mode in one timeline; it does not animate `blend` itself. `hue`,
|
|
42
|
+
`saturation`, `color`, and `luminosity` are rejected because they need
|
|
43
|
+
cross-channel math and are not part of the supported set.
|
|
44
|
+
|
|
45
|
+
`backdrop` and `aurora` are static compositions: they declare no
|
|
46
|
+
`duration-in-frames` or `fps`, are captured once, and the same image is reused
|
|
47
|
+
for every timeline frame. `labels` remains animated because its progress bar
|
|
48
|
+
reads `--fy-progress`; making it static would freeze the bar.
|
|
49
|
+
|
|
50
|
+
Preview composites with Chromium `mix-blend-mode`; production uses FFmpeg's
|
|
51
|
+
`blend` filter plus an alpha mask. The preview stage stacks by track order and
|
|
52
|
+
is isolated from the preview chrome. Small scaling, alpha, and color differences
|
|
53
|
+
between preview and the final encode are expected.
|
|
54
|
+
|
|
55
|
+
## Files
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
examples/blend/
|
|
59
|
+
compositions/backdrop.html opaque base composition
|
|
60
|
+
compositions/aurora.html transparent screen layer
|
|
61
|
+
compositions/labels.html transparent mode list and progress layer
|
|
62
|
+
timelines/blend.timeline.json
|
|
63
|
+
assets/ink.mp4 procedural 12-second source clip
|
|
64
|
+
scripts/make-ink.sh regenerate assets/ink.mp4
|
|
65
|
+
video.config.js
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
The timeline lives in `timelines/blend.timeline.json`; the project format and
|
|
69
|
+
entries are fixed in `video.config.js`. `backdrop.html` is a static composition
|
|
70
|
+
that omits all dimensions and time metadata, so it inherits the project's
|
|
71
|
+
1920×1080 size. `aurora.html` is a static composition that declares its
|
|
72
|
+
dimensions but no time metadata. `labels.html` is an animated composition and
|
|
73
|
+
keeps its `fps` and `duration-in-frames`.
|
|
74
|
+
|
|
75
|
+
## Commands
|
|
76
|
+
|
|
77
|
+
From the repository root:
|
|
78
|
+
|
|
79
|
+
```sh
|
|
80
|
+
pnpm build
|
|
81
|
+
node dist/cli.js check blend-timeline --project examples/blend
|
|
82
|
+
node dist/cli.js still blend-timeline --frame 15,45,75,105,135,165,195,225,255,285,315,345 --project examples/blend --out blend-stills
|
|
83
|
+
node dist/cli.js still blend-timeline --frame 285 --project examples/blend --out blend-soft-light.png
|
|
84
|
+
node dist/cli.js render blend-timeline --project examples/blend
|
|
85
|
+
node dist/cli.js dev --project examples/blend
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
The batch still captures one representative frame per mode into `blend-stills/`
|
|
89
|
+
using the same clip capture and composite path as the render. `render` writes
|
|
90
|
+
`out/blend-timeline.mp4` inside the project by default; use `--out <file.mp4>`
|
|
91
|
+
to choose another path. `dev` lists the compositions and the timeline in the
|
|
92
|
+
preview.
|
|
93
|
+
|
|
94
|
+
## Source clip
|
|
95
|
+
|
|
96
|
+
`assets/ink.mp4` is a 12-second, 1280×720, 30 fps rotating gradient with a
|
|
97
|
+
silent AAC track, generated by FFmpeg. Regenerate it with:
|
|
98
|
+
|
|
99
|
+
```sh
|
|
100
|
+
bash examples/blend/scripts/make-ink.sh
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Pass an output path as the first argument to write elsewhere. Replace
|
|
104
|
+
`assets/ink.mp4` with any local, seekable MP4 or WebM that is at least 12 seconds
|
|
105
|
+
long; remote URLs and live streams are not supported. If you use a different
|
|
106
|
+
length, update the clip durations and source in-points in the timeline.
|
|
Binary file
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
html,
|
|
3
|
+
body,
|
|
4
|
+
fy-composition {
|
|
5
|
+
margin: 0;
|
|
6
|
+
width: 100%;
|
|
7
|
+
height: 100%;
|
|
8
|
+
background: transparent;
|
|
9
|
+
}
|
|
10
|
+
fy-composition {
|
|
11
|
+
display: block;
|
|
12
|
+
overflow: hidden;
|
|
13
|
+
}
|
|
14
|
+
.scene {
|
|
15
|
+
position: relative;
|
|
16
|
+
width: 100%;
|
|
17
|
+
height: 100%;
|
|
18
|
+
overflow: hidden;
|
|
19
|
+
isolation: isolate;
|
|
20
|
+
}
|
|
21
|
+
.blob {
|
|
22
|
+
position: absolute;
|
|
23
|
+
border-radius: 50%;
|
|
24
|
+
filter: blur(78px);
|
|
25
|
+
opacity: 0.9;
|
|
26
|
+
}
|
|
27
|
+
.pink {
|
|
28
|
+
left: 6%;
|
|
29
|
+
top: -18%;
|
|
30
|
+
width: 980px;
|
|
31
|
+
height: 980px;
|
|
32
|
+
background: radial-gradient(circle, rgba(255, 64, 164, 0.92), rgba(255, 64, 164, 0) 68%);
|
|
33
|
+
}
|
|
34
|
+
.blue {
|
|
35
|
+
right: -12%;
|
|
36
|
+
top: 4%;
|
|
37
|
+
width: 1180px;
|
|
38
|
+
height: 1180px;
|
|
39
|
+
background: radial-gradient(circle, rgba(46, 146, 255, 0.88), rgba(46, 146, 255, 0) 68%);
|
|
40
|
+
}
|
|
41
|
+
.amber {
|
|
42
|
+
left: 30%;
|
|
43
|
+
bottom: -30%;
|
|
44
|
+
width: 900px;
|
|
45
|
+
height: 900px;
|
|
46
|
+
background: radial-gradient(circle, rgba(255, 186, 72, 0.7), rgba(255, 186, 72, 0) 70%);
|
|
47
|
+
}
|
|
48
|
+
</style>
|
|
49
|
+
<fy-composition width="1920" height="1080">
|
|
50
|
+
<div class="scene">
|
|
51
|
+
<div class="blob pink"></div>
|
|
52
|
+
<div class="blob blue"></div>
|
|
53
|
+
<div class="blob amber"></div>
|
|
54
|
+
</div>
|
|
55
|
+
</fy-composition>
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
html,
|
|
3
|
+
body {
|
|
4
|
+
margin: 0;
|
|
5
|
+
width: 100%;
|
|
6
|
+
height: 100%;
|
|
7
|
+
background: #05070d;
|
|
8
|
+
}
|
|
9
|
+
fy-composition {
|
|
10
|
+
display: block;
|
|
11
|
+
width: 100%;
|
|
12
|
+
height: 100%;
|
|
13
|
+
overflow: hidden;
|
|
14
|
+
}
|
|
15
|
+
.scene {
|
|
16
|
+
position: relative;
|
|
17
|
+
width: 100%;
|
|
18
|
+
height: 100%;
|
|
19
|
+
overflow: hidden;
|
|
20
|
+
background:
|
|
21
|
+
radial-gradient(1200px 760px at 72% 34%, rgba(42, 104, 220, 0.5), transparent 62%),
|
|
22
|
+
radial-gradient(860px 620px at 18% 82%, rgba(255, 76, 140, 0.14), transparent 64%),
|
|
23
|
+
linear-gradient(145deg, #060a12 0%, #0b1424 52%, #05070d 100%);
|
|
24
|
+
}
|
|
25
|
+
.grid {
|
|
26
|
+
position: absolute;
|
|
27
|
+
inset: -1px;
|
|
28
|
+
background-image:
|
|
29
|
+
linear-gradient(rgba(150, 190, 255, 0.075) 1px, transparent 1px),
|
|
30
|
+
linear-gradient(90deg, rgba(150, 190, 255, 0.075) 1px, transparent 1px);
|
|
31
|
+
background-size: 72px 72px;
|
|
32
|
+
}
|
|
33
|
+
.fade {
|
|
34
|
+
position: absolute;
|
|
35
|
+
inset: 0;
|
|
36
|
+
background:
|
|
37
|
+
radial-gradient(ellipse at 58% 38%, transparent 18%, rgba(5, 7, 13, 0.55) 62%, rgba(5, 7, 13, 0.92) 100%),
|
|
38
|
+
linear-gradient(90deg, rgba(5, 7, 13, 0.5), transparent 38%, transparent 74%, rgba(5, 7, 13, 0.55));
|
|
39
|
+
}
|
|
40
|
+
.ring {
|
|
41
|
+
position: absolute;
|
|
42
|
+
left: 58%;
|
|
43
|
+
top: 44%;
|
|
44
|
+
width: 1060px;
|
|
45
|
+
height: 1060px;
|
|
46
|
+
border: 1px solid rgba(160, 205, 255, 0.18);
|
|
47
|
+
border-radius: 50%;
|
|
48
|
+
box-shadow:
|
|
49
|
+
0 0 140px rgba(54, 118, 255, 0.16) inset,
|
|
50
|
+
0 0 90px rgba(54, 118, 255, 0.08);
|
|
51
|
+
transform: translate(-50%, -50%);
|
|
52
|
+
}
|
|
53
|
+
.ring::before,
|
|
54
|
+
.ring::after {
|
|
55
|
+
content: "";
|
|
56
|
+
position: absolute;
|
|
57
|
+
border: 1px solid rgba(255, 118, 190, 0.13);
|
|
58
|
+
border-radius: 50%;
|
|
59
|
+
}
|
|
60
|
+
.ring::before {
|
|
61
|
+
inset: 138px;
|
|
62
|
+
}
|
|
63
|
+
.ring::after {
|
|
64
|
+
inset: 286px;
|
|
65
|
+
border-color: rgba(255, 179, 71, 0.12);
|
|
66
|
+
}
|
|
67
|
+
</style>
|
|
68
|
+
<fy-composition>
|
|
69
|
+
<div class="scene">
|
|
70
|
+
<div class="grid"></div>
|
|
71
|
+
<div class="ring"></div>
|
|
72
|
+
<div class="fade"></div>
|
|
73
|
+
</div>
|
|
74
|
+
</fy-composition>
|