frameyard 0.4.0 → 0.6.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 +101 -0
- package/GLOSSARY.md +156 -24
- package/README.md +103 -31
- package/dist/cli.js +112 -19
- package/dist/cli.js.map +1 -1
- package/dist/composition/adapter.d.ts +30 -0
- package/dist/composition/adapter.d.ts.map +1 -0
- package/dist/composition/adapter.js +2 -0
- package/dist/composition/adapter.js.map +1 -0
- package/dist/composition/animation.d.ts +19 -0
- package/dist/composition/animation.d.ts.map +1 -0
- package/dist/composition/animation.js +55 -0
- package/dist/composition/animation.js.map +1 -0
- package/dist/composition/attributes.d.ts +7 -0
- package/dist/composition/attributes.d.ts.map +1 -0
- package/dist/composition/attributes.js +24 -0
- package/dist/composition/attributes.js.map +1 -0
- package/dist/composition/descriptor.d.ts +30 -0
- package/dist/composition/descriptor.d.ts.map +1 -0
- package/dist/composition/descriptor.js +188 -0
- package/dist/composition/descriptor.js.map +1 -0
- package/dist/composition/diagnostics.d.ts +12 -0
- package/dist/composition/diagnostics.d.ts.map +1 -0
- package/dist/composition/diagnostics.js +38 -0
- package/dist/composition/diagnostics.js.map +1 -0
- package/dist/composition/index.d.ts +12 -0
- package/dist/composition/index.d.ts.map +1 -0
- package/dist/composition/index.js +7 -0
- package/dist/composition/index.js.map +1 -0
- package/dist/composition/media.d.ts +27 -0
- package/dist/composition/media.d.ts.map +1 -0
- package/dist/composition/media.js +162 -0
- package/dist/composition/media.js.map +1 -0
- package/dist/composition/paths.d.ts +5 -0
- package/dist/composition/paths.d.ts.map +1 -0
- package/dist/composition/paths.js +16 -0
- package/dist/composition/paths.js.map +1 -0
- package/dist/composition/transcripts.d.ts +19 -0
- package/dist/composition/transcripts.d.ts.map +1 -0
- package/dist/composition/transcripts.js +57 -0
- package/dist/composition/transcripts.js.map +1 -0
- package/dist/composition/walk.d.ts +10 -0
- package/dist/composition/walk.d.ts.map +1 -0
- package/dist/composition/walk.js +32 -0
- package/dist/composition/walk.js.map +1 -0
- package/dist/contracts.d.ts +14 -0
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.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 +5 -0
- package/dist/html/document-adapter.d.ts.map +1 -0
- package/dist/html/document-adapter.js +17 -0
- package/dist/html/document-adapter.js.map +1 -0
- package/dist/html/parse.d.ts +2 -6
- package/dist/html/parse.d.ts.map +1 -1
- package/dist/html/parse.js.map +1 -1
- package/dist/html/validate.d.ts.map +1 -1
- package/dist/html/validate.js +92 -375
- package/dist/html/validate.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +93 -13
- package/dist/preview/main.js.map +1 -1
- package/dist/preview/timeline-data.d.ts +85 -0
- package/dist/preview/timeline-data.d.ts.map +1 -0
- package/dist/preview/timeline-data.js +2 -0
- package/dist/preview/timeline-data.js.map +1 -0
- package/dist/preview/timeline-host.d.ts +19 -0
- package/dist/preview/timeline-host.d.ts.map +1 -0
- package/dist/preview/timeline-host.js +297 -0
- package/dist/preview/timeline-host.js.map +1 -0
- package/dist/preview/timeline-main.d.ts +17 -0
- package/dist/preview/timeline-main.d.ts.map +1 -0
- package/dist/preview/timeline-main.js +364 -0
- package/dist/preview/timeline-main.js.map +1 -0
- package/dist/project.d.ts +16 -1
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +81 -26
- package/dist/project.js.map +1 -1
- package/dist/renderer/browser.d.ts +2 -0
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +38 -6
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/ffmpeg.d.ts +61 -0
- package/dist/renderer/ffmpeg.d.ts.map +1 -1
- package/dist/renderer/ffmpeg.js +198 -0
- package/dist/renderer/ffmpeg.js.map +1 -1
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +18 -7
- package/dist/renderer/index.js.map +1 -1
- package/dist/renderer/timeline.d.ts +109 -0
- package/dist/renderer/timeline.d.ts.map +1 -0
- package/dist/renderer/timeline.js +984 -0
- package/dist/renderer/timeline.js.map +1 -0
- package/dist/runtime/document-adapter.d.ts +4 -0
- package/dist/runtime/document-adapter.d.ts.map +1 -0
- package/dist/runtime/document-adapter.js +19 -0
- package/dist/runtime/document-adapter.js.map +1 -0
- package/dist/runtime/host.d.ts +2 -0
- package/dist/runtime/host.d.ts.map +1 -1
- package/dist/runtime/host.js +78 -183
- package/dist/runtime/host.js.map +1 -1
- package/dist/runtime/media.d.ts.map +1 -1
- package/dist/runtime/media.js +19 -50
- package/dist/runtime/media.js.map +1 -1
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +93 -23
- package/dist/scaffold.js.map +1 -1
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +15 -1
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +39 -1
- package/dist/server/server.js.map +1 -1
- package/dist/server/timeline-preview-data.d.ts +4 -0
- package/dist/server/timeline-preview-data.d.ts.map +1 -0
- package/dist/server/timeline-preview-data.js +97 -0
- package/dist/server/timeline-preview-data.js.map +1 -0
- package/dist/server/timeline-preview-html.d.ts +4 -0
- package/dist/server/timeline-preview-html.d.ts.map +1 -0
- package/dist/server/timeline-preview-html.js +227 -0
- package/dist/server/timeline-preview-html.js.map +1 -0
- package/dist/timeline/plan.d.ts +32 -0
- package/dist/timeline/plan.d.ts.map +1 -0
- package/dist/timeline/plan.js +68 -0
- package/dist/timeline/plan.js.map +1 -0
- package/dist/timeline/resolve.d.ts +60 -0
- package/dist/timeline/resolve.d.ts.map +1 -0
- package/dist/timeline/resolve.js +523 -0
- package/dist/timeline/resolve.js.map +1 -0
- package/dist/timeline/schema.d.ts +71 -0
- package/dist/timeline/schema.d.ts.map +1 -0
- package/dist/timeline/schema.js +425 -0
- package/dist/timeline/schema.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +2 -0
- package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +2 -0
- package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +4 -0
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +1 -1
- package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
- package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
- package/docs/adr/0010-make-timelines-the-render-unit.md +24 -0
- package/docs/adr/0011-compose-timeline-frames-in-a-live-browser-host.md +22 -0
- package/docs/adr/0012-render-timelines-with-segmented-lossless-clip-intermediates.md +31 -0
- package/docs/v0.1-design.md +135 -85
- package/examples/demo/README.md +11 -4
- package/examples/demo/timelines/demo.timeline.json +12 -0
- package/examples/demo/video.config.js +4 -0
- package/examples/video/README.md +105 -0
- package/examples/video/assets/clip.mp4 +0 -0
- package/examples/video/assets/clip.webm +0 -0
- package/examples/video/compositions/video-mp4.html +362 -0
- package/examples/video/compositions/video-overlay.html +350 -0
- package/examples/video/compositions/video.html +362 -0
- package/examples/video/props/video-timeline.json +10 -0
- package/examples/video/props/video.json +8 -0
- package/examples/video/scripts/make-clip.sh +109 -0
- package/examples/video/timelines/video-timeline-mp4.timeline.json +44 -0
- package/examples/video/timelines/video-timeline.timeline.json +44 -0
- package/examples/video/video.config.js +12 -0
- package/examples/webgl/README.md +30 -0
- package/examples/webgl/compositions/webgl.controller.js +200 -0
- package/examples/webgl/compositions/webgl.html +215 -0
- package/examples/webgl/props/webgl.json +6 -0
- package/examples/webgl/video.config.js +5 -0
- package/package.json +4 -2
- package/skills/frameyard/SKILL.md +18 -13
- package/skills/frameyard/references/json-results.md +2 -2
- package/skills/frameyard/references/timelines.md +141 -0
|
@@ -0,0 +1,425 @@
|
|
|
1
|
+
import { assertJsonSerializable, isPlainObject, } from "../contracts.js";
|
|
2
|
+
const ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
3
|
+
const SCHEME = /^[a-zA-Z][a-zA-Z0-9+.-]*:/;
|
|
4
|
+
function isNonEmptyString(value) {
|
|
5
|
+
return typeof value === "string" && value.length > 0;
|
|
6
|
+
}
|
|
7
|
+
function isLocalPath(value) {
|
|
8
|
+
return (isNonEmptyString(value) &&
|
|
9
|
+
!SCHEME.test(value) &&
|
|
10
|
+
!value.startsWith("/") &&
|
|
11
|
+
!value.startsWith("\\") &&
|
|
12
|
+
!value.split(/[\\/]/).includes(".."));
|
|
13
|
+
}
|
|
14
|
+
function isFiniteNumber(value) {
|
|
15
|
+
return typeof value === "number" && Number.isFinite(value);
|
|
16
|
+
}
|
|
17
|
+
function isInteger(value) {
|
|
18
|
+
return typeof value === "number" && Number.isInteger(value);
|
|
19
|
+
}
|
|
20
|
+
function unknownKeys(value, allowed) {
|
|
21
|
+
return Object.keys(value).filter((key) => !allowed.includes(key));
|
|
22
|
+
}
|
|
23
|
+
function jsonPath(base, key) {
|
|
24
|
+
return typeof key === "number" ? `${base}[${key}]` : `${base}.${key}`;
|
|
25
|
+
}
|
|
26
|
+
function makeDiagnostic(context, severity, code, message, path, details = {}) {
|
|
27
|
+
const merged = {
|
|
28
|
+
...(context.path !== undefined ? { path: context.path } : {}),
|
|
29
|
+
...(context.timeline !== undefined ? { timeline: context.timeline } : {}),
|
|
30
|
+
jsonPath: path,
|
|
31
|
+
...details,
|
|
32
|
+
};
|
|
33
|
+
return { severity, code, message, details: merged };
|
|
34
|
+
}
|
|
35
|
+
function parseTransform(value, path, add) {
|
|
36
|
+
const defaults = { fit: "contain", x: 0, y: 0, scale: 1, rotation: 0, opacity: 1 };
|
|
37
|
+
if (!isPlainObject(value)) {
|
|
38
|
+
add("TIMELINE_CLIP_INVALID", "Clip transform must be an object.", path);
|
|
39
|
+
return defaults;
|
|
40
|
+
}
|
|
41
|
+
for (const key of unknownKeys(value, ["fit", "x", "y", "width", "height", "scale", "rotation", "opacity"])) {
|
|
42
|
+
add("TIMELINE_CLIP_INVALID", `Unknown clip transform field "${key}".`, jsonPath(path, key));
|
|
43
|
+
}
|
|
44
|
+
const transform = { ...defaults };
|
|
45
|
+
if (value.fit !== undefined) {
|
|
46
|
+
if (value.fit !== "contain" && value.fit !== "cover" && value.fit !== "stretch") {
|
|
47
|
+
add("TIMELINE_CLIP_INVALID", 'Clip transform.fit must be "contain", "cover", or "stretch".', jsonPath(path, "fit"));
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
transform.fit = value.fit;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
const width = value.width;
|
|
54
|
+
const height = value.height;
|
|
55
|
+
if ((width === undefined) !== (height === undefined)) {
|
|
56
|
+
add("TIMELINE_CLIP_INVALID", "Clip transform.width and transform.height must be provided together.", jsonPath(path, width === undefined ? "width" : "height"));
|
|
57
|
+
}
|
|
58
|
+
for (const [key, field] of [
|
|
59
|
+
["width", width],
|
|
60
|
+
["height", height],
|
|
61
|
+
]) {
|
|
62
|
+
if (field === undefined) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!isInteger(field) || field <= 0) {
|
|
66
|
+
add("TIMELINE_CLIP_INVALID", `Clip transform.${key} must be a positive integer.`, jsonPath(path, key));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (width !== undefined && height !== undefined && value.scale !== undefined) {
|
|
70
|
+
add("TIMELINE_CLIP_INVALID", "Clip transform.scale cannot be combined with transform.width and transform.height.", jsonPath(path, "scale"));
|
|
71
|
+
}
|
|
72
|
+
if (isInteger(width) && width > 0 && isInteger(height) && height > 0) {
|
|
73
|
+
transform.width = width;
|
|
74
|
+
transform.height = height;
|
|
75
|
+
}
|
|
76
|
+
for (const key of ["x", "y", "scale", "rotation", "opacity"]) {
|
|
77
|
+
const field = value[key];
|
|
78
|
+
if (field === undefined) {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (!isFiniteNumber(field)) {
|
|
82
|
+
add("TIMELINE_CLIP_INVALID", `Clip transform.${key} must be a finite number.`, jsonPath(path, key));
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (key === "scale" && field < 0) {
|
|
86
|
+
add("TIMELINE_CLIP_INVALID", "Clip transform.scale must be zero or greater.", jsonPath(path, key));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (key === "opacity" && (field < 0 || field > 1)) {
|
|
90
|
+
add("TIMELINE_CLIP_INVALID", "Clip transform.opacity must be between 0 and 1.", jsonPath(path, key));
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
transform[key] = field;
|
|
94
|
+
}
|
|
95
|
+
return transform;
|
|
96
|
+
}
|
|
97
|
+
function parseAudio(value, path, add) {
|
|
98
|
+
const defaults = { muted: false, gain: 1 };
|
|
99
|
+
if (!isPlainObject(value)) {
|
|
100
|
+
add("TIMELINE_CLIP_INVALID", "Clip audio must be an object.", path);
|
|
101
|
+
return defaults;
|
|
102
|
+
}
|
|
103
|
+
for (const key of unknownKeys(value, ["muted", "gain"])) {
|
|
104
|
+
add("TIMELINE_CLIP_INVALID", `Unknown clip audio field "${key}".`, jsonPath(path, key));
|
|
105
|
+
}
|
|
106
|
+
if (value.muted !== undefined) {
|
|
107
|
+
if (typeof value.muted !== "boolean") {
|
|
108
|
+
add("TIMELINE_CLIP_INVALID", "Clip audio.muted must be a boolean.", jsonPath(path, "muted"));
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
defaults.muted = value.muted;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
if (value.gain !== undefined) {
|
|
115
|
+
if (!isFiniteNumber(value.gain) || value.gain < 0 || value.gain > 1) {
|
|
116
|
+
add("TIMELINE_CLIP_INVALID", "Clip audio.gain must be between 0 and 1.", jsonPath(path, "gain"));
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
defaults.gain = value.gain;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
return defaults;
|
|
123
|
+
}
|
|
124
|
+
function parseSource(value, path, add) {
|
|
125
|
+
if (!isPlainObject(value)) {
|
|
126
|
+
add("TIMELINE_CLIP_INVALID", "Clip source must be an object.", path);
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const kind = value.kind;
|
|
130
|
+
if (kind === "composition" || kind === "timeline") {
|
|
131
|
+
for (const key of unknownKeys(value, ["kind", "id"])) {
|
|
132
|
+
add("TIMELINE_CLIP_INVALID", `Unknown ${kind} source field "${key}".`, jsonPath(path, key));
|
|
133
|
+
}
|
|
134
|
+
if (!isNonEmptyString(value.id) || !ID_PATTERN.test(value.id)) {
|
|
135
|
+
add("TIMELINE_CLIP_INVALID", `Clip ${kind} source.id must be a valid project id.`, jsonPath(path, "id"));
|
|
136
|
+
return null;
|
|
137
|
+
}
|
|
138
|
+
return kind === "composition" ? { kind, id: value.id } : { kind, id: value.id };
|
|
139
|
+
}
|
|
140
|
+
if (kind === "video" || kind === "image" || kind === "audio") {
|
|
141
|
+
for (const key of unknownKeys(value, ["kind", "src"])) {
|
|
142
|
+
add("TIMELINE_CLIP_INVALID", `Unknown ${kind} source field "${key}".`, jsonPath(path, key));
|
|
143
|
+
}
|
|
144
|
+
if (!isLocalPath(value.src)) {
|
|
145
|
+
add("TIMELINE_CLIP_INVALID", `Clip ${kind} source.src must be a local project-relative path.`, jsonPath(path, "src"));
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
return { kind, src: value.src };
|
|
149
|
+
}
|
|
150
|
+
add("TIMELINE_CLIP_INVALID", 'Clip source.kind must be "composition", "timeline", "video", "image", or "audio".', jsonPath(path, "kind"));
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
function isVisualSource(source) {
|
|
154
|
+
return (source.kind === "composition" || source.kind === "timeline" || source.kind === "video" || source.kind === "image");
|
|
155
|
+
}
|
|
156
|
+
function isAudioBearingSource(source) {
|
|
157
|
+
return source.kind !== "image";
|
|
158
|
+
}
|
|
159
|
+
/** Parses and normalizes a timeline JSON document without touching the filesystem. */
|
|
160
|
+
export function parseTimelineDocument(value, context = {}) {
|
|
161
|
+
const diagnostics = [];
|
|
162
|
+
const add = (code, message, path, details = {}) => {
|
|
163
|
+
diagnostics.push(makeDiagnostic(context, "error", code, message, path, details));
|
|
164
|
+
};
|
|
165
|
+
if (!isPlainObject(value)) {
|
|
166
|
+
add("TIMELINE_INVALID", "Timeline document must be a JSON object.", "$");
|
|
167
|
+
return { timeline: null, diagnostics };
|
|
168
|
+
}
|
|
169
|
+
for (const key of unknownKeys(value, ["version", "name", "durationInFrames", "background", "tracks", "clips"])) {
|
|
170
|
+
add("TIMELINE_INVALID", `Unknown timeline field "${key}".`, jsonPath("$", key));
|
|
171
|
+
}
|
|
172
|
+
if (value.version !== 1) {
|
|
173
|
+
add("TIMELINE_INVALID", "Timeline version must be 1.", "$.version");
|
|
174
|
+
}
|
|
175
|
+
if (value.name !== undefined && !isNonEmptyString(value.name)) {
|
|
176
|
+
add("TIMELINE_INVALID", "Timeline name must be a non-empty string.", "$.name");
|
|
177
|
+
}
|
|
178
|
+
if (value.background !== undefined && !isNonEmptyString(value.background)) {
|
|
179
|
+
add("TIMELINE_INVALID", "Timeline background must be a non-empty string.", "$.background");
|
|
180
|
+
}
|
|
181
|
+
if (value.durationInFrames !== undefined) {
|
|
182
|
+
if (!isInteger(value.durationInFrames) || value.durationInFrames <= 0) {
|
|
183
|
+
add("TIMELINE_DURATION_INVALID", "Timeline durationInFrames must be a positive integer.", "$.durationInFrames");
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (!Array.isArray(value.tracks) || value.tracks.length === 0) {
|
|
187
|
+
add("TIMELINE_INVALID", "Timeline tracks must be a non-empty array.", "$.tracks");
|
|
188
|
+
return { timeline: null, diagnostics };
|
|
189
|
+
}
|
|
190
|
+
if (!Array.isArray(value.clips) || value.clips.length === 0) {
|
|
191
|
+
add("TIMELINE_INVALID", "Timeline clips must be a non-empty array.", "$.clips");
|
|
192
|
+
return { timeline: null, diagnostics };
|
|
193
|
+
}
|
|
194
|
+
const tracks = [];
|
|
195
|
+
const trackById = new Map();
|
|
196
|
+
for (const [index, rawTrack] of value.tracks.entries()) {
|
|
197
|
+
const path = `$.tracks[${index}]`;
|
|
198
|
+
if (!isPlainObject(rawTrack)) {
|
|
199
|
+
add("TIMELINE_TRACK_INVALID", "Track must be an object.", path);
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
for (const key of unknownKeys(rawTrack, ["id", "kind", "name", "gain", "muted"])) {
|
|
203
|
+
add("TIMELINE_TRACK_INVALID", `Unknown track field "${key}".`, jsonPath(path, key));
|
|
204
|
+
}
|
|
205
|
+
if (!isNonEmptyString(rawTrack.id) || !ID_PATTERN.test(rawTrack.id)) {
|
|
206
|
+
add("TIMELINE_TRACK_INVALID", "Track id must be a valid project id.", jsonPath(path, "id"));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
if (trackById.has(rawTrack.id)) {
|
|
210
|
+
add("TIMELINE_TRACK_INVALID", `Duplicate track id "${rawTrack.id}".`, jsonPath(path, "id"), {
|
|
211
|
+
track: rawTrack.id,
|
|
212
|
+
});
|
|
213
|
+
continue;
|
|
214
|
+
}
|
|
215
|
+
if (rawTrack.kind !== "video" && rawTrack.kind !== "audio") {
|
|
216
|
+
add("TIMELINE_TRACK_INVALID", 'Track kind must be "video" or "audio".', jsonPath(path, "kind"), {
|
|
217
|
+
track: rawTrack.id,
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (rawTrack.name !== undefined && !isNonEmptyString(rawTrack.name)) {
|
|
222
|
+
add("TIMELINE_TRACK_INVALID", "Track name must be a non-empty string.", jsonPath(path, "name"), {
|
|
223
|
+
track: rawTrack.id,
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
if (rawTrack.kind === "video" && (rawTrack.gain !== undefined || rawTrack.muted !== undefined)) {
|
|
227
|
+
add("TIMELINE_TRACK_INVALID", "Video tracks do not support gain or muted.", path, { track: rawTrack.id });
|
|
228
|
+
}
|
|
229
|
+
if (rawTrack.gain !== undefined && (!isFiniteNumber(rawTrack.gain) || rawTrack.gain < 0 || rawTrack.gain > 1)) {
|
|
230
|
+
add("TIMELINE_TRACK_INVALID", "Audio track gain must be between 0 and 1.", jsonPath(path, "gain"), {
|
|
231
|
+
track: rawTrack.id,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
if (rawTrack.muted !== undefined && typeof rawTrack.muted !== "boolean") {
|
|
235
|
+
add("TIMELINE_TRACK_INVALID", "Audio track muted must be a boolean.", jsonPath(path, "muted"), {
|
|
236
|
+
track: rawTrack.id,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
const trackId = rawTrack.id;
|
|
240
|
+
const trackKind = rawTrack.kind;
|
|
241
|
+
const trackName = isNonEmptyString(rawTrack.name) ? rawTrack.name : undefined;
|
|
242
|
+
const trackGain = trackKind === "audio" && isFiniteNumber(rawTrack.gain) ? rawTrack.gain : undefined;
|
|
243
|
+
const trackMuted = trackKind === "audio" && typeof rawTrack.muted === "boolean" ? rawTrack.muted : undefined;
|
|
244
|
+
const track = {
|
|
245
|
+
id: trackId,
|
|
246
|
+
kind: trackKind,
|
|
247
|
+
...(trackName !== undefined ? { name: trackName } : {}),
|
|
248
|
+
...(trackKind === "audio" ? { gain: trackGain ?? 1 } : {}),
|
|
249
|
+
...(trackKind === "audio" ? { muted: trackMuted ?? false } : {}),
|
|
250
|
+
};
|
|
251
|
+
tracks.push(track);
|
|
252
|
+
trackById.set(track.id, track);
|
|
253
|
+
}
|
|
254
|
+
const clips = [];
|
|
255
|
+
const clipIds = new Set();
|
|
256
|
+
for (const [index, rawClip] of value.clips.entries()) {
|
|
257
|
+
const path = `$.clips[${index}]`;
|
|
258
|
+
if (!isPlainObject(rawClip)) {
|
|
259
|
+
add("TIMELINE_CLIP_INVALID", "Clip must be an object.", path);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
const clipId = isNonEmptyString(rawClip.id) ? rawClip.id : null;
|
|
263
|
+
const details = clipId ? { clip: clipId } : {};
|
|
264
|
+
for (const key of unknownKeys(rawClip, [
|
|
265
|
+
"id",
|
|
266
|
+
"track",
|
|
267
|
+
"source",
|
|
268
|
+
"start",
|
|
269
|
+
"duration",
|
|
270
|
+
"sourceIn",
|
|
271
|
+
"props",
|
|
272
|
+
"transform",
|
|
273
|
+
"audio",
|
|
274
|
+
])) {
|
|
275
|
+
add("TIMELINE_CLIP_INVALID", `Unknown clip field "${key}".`, jsonPath(path, key), details);
|
|
276
|
+
}
|
|
277
|
+
if (!clipId || !ID_PATTERN.test(clipId)) {
|
|
278
|
+
add("TIMELINE_CLIP_INVALID", "Clip id must be a valid project id.", jsonPath(path, "id"), details);
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
if (clipIds.has(clipId)) {
|
|
282
|
+
add("TIMELINE_CLIP_INVALID", `Duplicate clip id "${clipId}".`, jsonPath(path, "id"), details);
|
|
283
|
+
continue;
|
|
284
|
+
}
|
|
285
|
+
clipIds.add(clipId);
|
|
286
|
+
const trackId = isNonEmptyString(rawClip.track) ? rawClip.track : null;
|
|
287
|
+
if (!trackId || !trackById.has(trackId)) {
|
|
288
|
+
add("TIMELINE_CLIP_INVALID", `Clip track must reference a declared track.`, jsonPath(path, "track"), details);
|
|
289
|
+
continue;
|
|
290
|
+
}
|
|
291
|
+
const track = trackById.get(trackId);
|
|
292
|
+
const clipDetails = { ...details, track: trackId };
|
|
293
|
+
const source = parseSource(rawClip.source, jsonPath(path, "source"), (code, message, errorPath, errorDetails) => {
|
|
294
|
+
add(code, message, errorPath, { ...clipDetails, ...errorDetails });
|
|
295
|
+
});
|
|
296
|
+
if (!source) {
|
|
297
|
+
continue;
|
|
298
|
+
}
|
|
299
|
+
if (!isInteger(rawClip.start) || rawClip.start < 0) {
|
|
300
|
+
add("TIMELINE_CLIP_INVALID", "Clip start must be a non-negative integer.", jsonPath(path, "start"), clipDetails);
|
|
301
|
+
}
|
|
302
|
+
const start = isInteger(rawClip.start) && rawClip.start >= 0 ? rawClip.start : 0;
|
|
303
|
+
if (rawClip.duration !== undefined && (!isInteger(rawClip.duration) || rawClip.duration <= 0)) {
|
|
304
|
+
add("TIMELINE_CLIP_INVALID", "Clip duration must be a positive integer.", jsonPath(path, "duration"), clipDetails);
|
|
305
|
+
}
|
|
306
|
+
if (source.kind === "image" && rawClip.duration === undefined) {
|
|
307
|
+
add("TIMELINE_CLIP_INVALID", "Image clips must declare a duration.", jsonPath(path, "duration"), clipDetails);
|
|
308
|
+
}
|
|
309
|
+
const duration = rawClip.duration === undefined
|
|
310
|
+
? undefined
|
|
311
|
+
: isInteger(rawClip.duration) && rawClip.duration > 0
|
|
312
|
+
? rawClip.duration
|
|
313
|
+
: undefined;
|
|
314
|
+
if (rawClip.sourceIn !== undefined && (!isInteger(rawClip.sourceIn) || rawClip.sourceIn < 0)) {
|
|
315
|
+
add("TIMELINE_CLIP_INVALID", "Clip sourceIn must be a non-negative integer.", jsonPath(path, "sourceIn"), clipDetails);
|
|
316
|
+
}
|
|
317
|
+
const sourceIn = isInteger(rawClip.sourceIn) && rawClip.sourceIn >= 0 ? rawClip.sourceIn : 0;
|
|
318
|
+
if (source.kind === "image" && sourceIn !== 0) {
|
|
319
|
+
add("TIMELINE_CLIP_INVALID", "Image clips must use sourceIn 0.", jsonPath(path, "sourceIn"), clipDetails);
|
|
320
|
+
}
|
|
321
|
+
let props;
|
|
322
|
+
if (rawClip.props !== undefined) {
|
|
323
|
+
if (source.kind !== "composition") {
|
|
324
|
+
add("TIMELINE_CLIP_INVALID", "props is only valid on composition clips.", jsonPath(path, "props"), clipDetails);
|
|
325
|
+
}
|
|
326
|
+
else if (!isPlainObject(rawClip.props)) {
|
|
327
|
+
add("TIMELINE_CLIP_INVALID", "Clip props must be a plain JSON object.", jsonPath(path, "props"), clipDetails);
|
|
328
|
+
}
|
|
329
|
+
else {
|
|
330
|
+
try {
|
|
331
|
+
assertJsonSerializable(rawClip.props, "props");
|
|
332
|
+
props = rawClip.props;
|
|
333
|
+
}
|
|
334
|
+
catch (error) {
|
|
335
|
+
add("TIMELINE_CLIP_INVALID", `Clip props are not JSON-serializable: ${error instanceof Error ? error.message : String(error)}`, jsonPath(path, "props"), clipDetails);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const visual = isVisualSource(source);
|
|
340
|
+
const audioBearing = isAudioBearingSource(source);
|
|
341
|
+
let transform;
|
|
342
|
+
if (rawClip.transform !== undefined) {
|
|
343
|
+
if (!visual) {
|
|
344
|
+
add("TIMELINE_CLIP_INVALID", "transform is only valid on visual clips.", jsonPath(path, "transform"), clipDetails);
|
|
345
|
+
}
|
|
346
|
+
else {
|
|
347
|
+
transform = parseTransform(rawClip.transform, jsonPath(path, "transform"), (code, message, errorPath, errorDetails) => {
|
|
348
|
+
add(code, message, errorPath, { ...clipDetails, ...errorDetails });
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else if (visual) {
|
|
353
|
+
transform = { fit: "contain", x: 0, y: 0, scale: 1, rotation: 0, opacity: 1 };
|
|
354
|
+
}
|
|
355
|
+
let audio;
|
|
356
|
+
if (rawClip.audio !== undefined) {
|
|
357
|
+
if (!audioBearing) {
|
|
358
|
+
add("TIMELINE_CLIP_INVALID", "audio is only valid on clips with audio.", jsonPath(path, "audio"), clipDetails);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
audio = parseAudio(rawClip.audio, jsonPath(path, "audio"), (code, message, errorPath, errorDetails) => {
|
|
362
|
+
add(code, message, errorPath, { ...clipDetails, ...errorDetails });
|
|
363
|
+
});
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
else if (audioBearing) {
|
|
367
|
+
audio = { muted: false, gain: 1 };
|
|
368
|
+
}
|
|
369
|
+
const expectedKind = source.kind === "audio" ? "audio" : "video";
|
|
370
|
+
if (track.kind !== expectedKind) {
|
|
371
|
+
add("TIMELINE_CLIP_INVALID", `Clip source ${source.kind} requires a ${expectedKind} track; "${trackId}" is ${track.kind}.`, jsonPath(path, "track"), clipDetails);
|
|
372
|
+
}
|
|
373
|
+
clips.push({
|
|
374
|
+
id: clipId,
|
|
375
|
+
track: trackId,
|
|
376
|
+
source,
|
|
377
|
+
start,
|
|
378
|
+
...(duration !== undefined ? { duration } : {}),
|
|
379
|
+
sourceIn,
|
|
380
|
+
...(props !== undefined ? { props } : {}),
|
|
381
|
+
...(transform !== undefined ? { transform } : {}),
|
|
382
|
+
...(audio !== undefined ? { audio } : {}),
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
for (const track of tracks) {
|
|
386
|
+
const trackClips = clips
|
|
387
|
+
.filter((clip) => clip.track === track.id && clip.duration !== undefined)
|
|
388
|
+
.sort((left, right) => left.start - right.start);
|
|
389
|
+
for (let index = 1; index < trackClips.length; index += 1) {
|
|
390
|
+
const previous = trackClips[index - 1];
|
|
391
|
+
const current = trackClips[index];
|
|
392
|
+
if (current.start < previous.start + previous.duration) {
|
|
393
|
+
add("TIMELINE_CLIP_OVERLAP", `Clip "${current.id}" overlaps "${previous.id}" on track "${track.id}".`, `$.clips[${clips.indexOf(current)}]`, { track: track.id, clip: current.id, suggestion: "No two clips on one track may overlap in 0.6." });
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const explicitEnd = clips.reduce((end, clip) => {
|
|
398
|
+
if (clip.duration === undefined) {
|
|
399
|
+
return end;
|
|
400
|
+
}
|
|
401
|
+
return Math.max(end, clip.start + clip.duration);
|
|
402
|
+
}, 0);
|
|
403
|
+
if (typeof value.durationInFrames === "number" &&
|
|
404
|
+
Number.isInteger(value.durationInFrames) &&
|
|
405
|
+
value.durationInFrames > 0) {
|
|
406
|
+
if (value.durationInFrames < explicitEnd) {
|
|
407
|
+
add("TIMELINE_DURATION_INVALID", `Timeline durationInFrames ${value.durationInFrames} is shorter than the explicit clip end ${explicitEnd}.`, "$.durationInFrames");
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
if (diagnostics.some((diagnostic) => diagnostic.severity === "error")) {
|
|
411
|
+
return { timeline: null, diagnostics };
|
|
412
|
+
}
|
|
413
|
+
return {
|
|
414
|
+
timeline: {
|
|
415
|
+
version: 1,
|
|
416
|
+
...(typeof value.name === "string" ? { name: value.name } : {}),
|
|
417
|
+
...(typeof value.durationInFrames === "number" ? { durationInFrames: value.durationInFrames } : {}),
|
|
418
|
+
...(typeof value.background === "string" ? { background: value.background } : {}),
|
|
419
|
+
tracks,
|
|
420
|
+
clips,
|
|
421
|
+
},
|
|
422
|
+
diagnostics,
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema.js","sourceRoot":"","sources":["../../src/timeline/schema.ts"],"names":[],"mappings":"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"}
|
package/dist/version.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const VERSION = "0.
|
|
1
|
+
export declare const VERSION = "0.6.0";
|
|
2
2
|
//# sourceMappingURL=version.d.ts.map
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const VERSION = "0.
|
|
1
|
+
export const VERSION = "0.6.0";
|
|
2
2
|
//# sourceMappingURL=version.js.map
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# Make frame and props the only animation state
|
|
2
2
|
|
|
3
|
+
**Amended by [ADR-0010](./0010-make-timelines-the-render-unit.md).** Timeline frames are the master clock; each active clip is seeked to its own composition frame, and each composition instance has its own immutable props.
|
|
4
|
+
|
|
3
5
|
Every composition frame must be seek-safe and derivable from its integer frame number and immutable props, with asynchronous work completed before frame production. Frameyard rejects wall-clock animation and history-dependent updates because deterministic seeking is what makes reliable scrubbing, independent frame rendering, and future parallelism possible.
|
|
@@ -1,3 +1,5 @@
|
|
|
1
1
|
# Keep parallel rendering opt-in and frame-ordered
|
|
2
2
|
|
|
3
|
+
**Amended by [ADR-0010](./0010-make-timelines-the-render-unit.md).** Parallel capture remains composition-only in 0.6; timeline rendering is sequential until the live timeline host proves safe to duplicate.
|
|
4
|
+
|
|
3
5
|
Frameyard will offer parallel frame capture as an explicit `render --concurrency <n>` option, defaulting to one browser session and limiting `n` to 16 to bound browser processes and buffered PNG memory. Each worker loads the same composition, props, and preview server in an independent session; workers seek and capture assigned frames while one ordered writer feeds a single FFmpeg encoder. Bounded lookahead limits buffered PNGs, failures stop further frame work and abort publication, and no partial output is published. This keeps sequential rendering as the predictable default and preserves the determinism contract at the capture boundary: concurrency changes scheduling only, never frame order or visual state. The cost is additional CPU and memory proportional to the requested session count.
|
|
@@ -8,4 +8,8 @@ The browser remains the source of truth for video presentation, while production
|
|
|
8
8
|
|
|
9
9
|
WebGL canvases are redrawn from the current frame and captured through the existing screenshot path. Production Chromium pins SwiftShader so WebGL pixels do not vary with the host GPU. The runtime waits two animation frames after a media seek to ensure the decoded video frame and WebGL drawing are composited before capture.
|
|
10
10
|
|
|
11
|
+
Capture sessions also keep the composition hidden until the first seek. Chromium can otherwise reuse the video frame painted during load when an ancestor animates opacity, so a still at a later frame shows the load frame even though the seek succeeded. The first visible paint is therefore the requested frame. Interactive preview does not take this capture-only precaution; a full-scene opacity animation above a video can leave the load frame on screen after a large seek. Fade overlay layers instead, and keep videos outside full-scene opacity animations.
|
|
12
|
+
|
|
13
|
+
WebM frame timestamps are rounded to milliseconds, while composition times are exact frame fractions. Chromium selects the frame at or before `currentTime`, so a 30fps source at 0.067s combined with a composition time of 0.066666s would show the previous source frame. The runtime seeks one millisecond past the frame-derived time so every composition frame selects its intended source frame. `test/video-determinism.test.ts` covers both this mapping and the animated-ancestor capture path.
|
|
14
|
+
|
|
11
15
|
`update()` stays synchronous and seek-safe. Media preparation happens during loading, and only the settle step is asynchronous. This preserves the existing lifecycle contract while extending deterministic frame production to embedded video and WebGL.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Make HTML documents the composition unit
|
|
2
2
|
|
|
3
|
-
**Status:** Accepted
|
|
3
|
+
**Status:** Accepted. Amended by [ADR-0010](./0010-make-timelines-the-render-unit.md): HTML documents remain the composition unit, but compositions become clip sources inside JSON timelines.
|
|
4
4
|
**Date:** 2026-10-30
|
|
5
5
|
|
|
6
6
|
Frameyard compositions will be declarative HTML documents rather than JavaScript modules. A composition declares its metadata, default props, bindings, animation schedule, audio, and transcripts in HTML and CSS, while an optional inline or external controller handles Canvas 2D, WebGL, karaoke, and per-frame computation the declarative layer cannot express. This replaces the imperative `mount → update → dispose` authoring contract with an HTML-first model that is easier for humans and agents to read, edit, and inspect, while preserving frame-and-props determinism and preview/render parity through the same injected host runtime.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Share the composition document grammar
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-10-30
|
|
5
|
+
|
|
6
|
+
Static composition validation and the composition host will read the HTML composition grammar through one browser-safe module with two adapters: parse5 for Node-side validation and the DOM for the host. The reader returns a composition descriptor of fixed facts (metadata, default props, and the controller declaration) and per-kind declarations (animation, media, and transcripts) at the host's existing lifecycle points; it stays pure, leaving path resolution, filesystem checks, and transcript parsing to the validator. Controller `src` remains project-relative in public results, with the authoring value retained as `rawSrc` for the host's module import. This keeps static validation and frame production from drifting while preserving ADR-0007's split between Node-side validation and browser frame production.
|
|
7
|
+
|
|
8
|
+
## Considered Options
|
|
9
|
+
|
|
10
|
+
- **Server-injected descriptor.** Rejected because the host still reads per-element bindings, animation, and media from the DOM; a descriptor would cover only part of the grammar and couple the host to server injection.
|
|
11
|
+
- **Shared rule table with two readers.** Rejected because it centralizes constants but leaves two traversals and two error paths.
|
|
12
|
+
- **Normalized document tree.** Rejected because the host needs the original DOM elements for animation and media application; a lazy adapter avoids the copy.
|
|
13
|
+
|
|
14
|
+
## Consequences
|
|
15
|
+
|
|
16
|
+
- The reader lives in `src/composition/` and is covered by a browser-only tsconfig so it cannot import `node:`.
|
|
17
|
+
- `ControllerDeclaration` gains `rawSrc`; public `src` stays project-relative. The host's `controller.src` now matches check, still, and render.
|
|
18
|
+
- Bindings remain a separate slice; static validation keeps binding-path resolution and runtime application.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Composite production frames in software
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted. Extended by [ADR-0011](./0011-compose-timeline-frames-in-a-live-browser-host.md): timeline frames use the same software-composited capture path.
|
|
4
|
+
**Date:** 2026-10-30
|
|
5
|
+
|
|
6
|
+
Production Chromium sessions keep ANGLE pinned to SwiftShader for WebGL but add
|
|
7
|
+
`--disable-gpu-compositing`. Frame production is already fully software and
|
|
8
|
+
frame-seeked; routing every captured frame through GPU-process compositing adds
|
|
9
|
+
a fixed copy and synchronization cost without improving determinism. Software
|
|
10
|
+
compositing keeps WebGL canvases on SwiftShader while removing that round trip.
|
|
11
|
+
Capture remains a `Page.captureScreenshot` of the composition viewport, so the
|
|
12
|
+
frame, media-settle, and PNG output contracts are unchanged.
|
|
13
|
+
|
|
14
|
+
## Considered Options
|
|
15
|
+
|
|
16
|
+
- **Keep GPU-process compositing.** Rejected after measurement: the 1080p demo
|
|
17
|
+
rendered about 1.4x slower sequentially and 2.3x slower with
|
|
18
|
+
`--concurrency 4` on the reference machine.
|
|
19
|
+
- **Disable the GPU entirely.** Rejected because `--disable-gpu` also removes
|
|
20
|
+
the SwiftShader path that keeps WebGL pixels independent of the host GPU.
|
|
21
|
+
- **Choose the compositor per composition.** Rejected because WebGL still works
|
|
22
|
+
through software compositing and the extra static signal would widen the
|
|
23
|
+
public composition contract. The benchmark's 1080p WebGL shader fixture pays
|
|
24
|
+
a modest capture penalty (roughly 0-25%) with software compositing, while
|
|
25
|
+
video and 2D capture costs improve substantially.
|
|
26
|
+
|
|
27
|
+
## Consequences
|
|
28
|
+
|
|
29
|
+
- Captured 2D pixels come from the software compositor; WebGL pixels still come
|
|
30
|
+
from the pinned SwiftShader rasterizer.
|
|
31
|
+
- Parallel rendering scales better because each session no longer serializes
|
|
32
|
+
captures through its GPU process.
|
|
33
|
+
- The benchmark includes WebGL and video fixtures so the composition-specific
|
|
34
|
+
tradeoff stays visible; run `pnpm bench --fixtures` for current numbers.
|
|
35
|
+
- Capture timing and pixels can change for existing projects. The
|
|
36
|
+
reproducibility guarantee remains at the capture boundary for the same
|
|
37
|
+
pinned runtime and inputs; run `pnpm bench` for current timings.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Make timelines the render unit and compositions clip sources
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Date:** 2026-10-31
|
|
5
|
+
|
|
6
|
+
Frameyard will render timelines as its primary output unit. A timeline is a project-local JSON document with a project-fixed width, height, and frame rate, ordered video and audio tracks, and clips sourced from HTML compositions, local media, or other timelines. A composition remains the reusable HTML authoring unit and becomes a clip source whose source range and props are fixed by each clip. Standalone composition renders remain supported on the existing direct path.
|
|
7
|
+
|
|
8
|
+
## Considered Options
|
|
9
|
+
|
|
10
|
+
- **Keep compositions as the render unit and add a playlist.** Rejected because a playlist produces separate videos, not one composited output, and cannot express layers, trims, or nested timelines.
|
|
11
|
+
- **Make every composition render an implicit one-clip timeline.** Rejected as the internal path because wrapping the existing composition capture in iframes and a stage would risk pixel and result-schema changes for existing projects. Composition renders keep the direct path; the CLI exposes both targets under one ID space.
|
|
12
|
+
- **Make the timeline a composition.** Rejected because it would conflate a reusable, prop-parameterized source with an edit that has tracks, clips, and a project format, and would invite ambiguous recursion. Timelines nest through clips, not through the composition contract.
|
|
13
|
+
- **Allow per-timeline formats.** Rejected because one project format removes frame-rate conversion from clip mapping and makes nested timelines and media composition straightforward. Standalone compositions keep their own metadata.
|
|
14
|
+
- **Allow overlapping clips on one track.** Rejected for 0.6 because transitions are deferred; overlap would create ambiguous audio and visual precedence. Crossfades can use separate tracks until transitions exist.
|
|
15
|
+
|
|
16
|
+
## Consequences
|
|
17
|
+
|
|
18
|
+
- `video.config.js` gains an optional project `format` and an explicit `timelines` map. IDs are unique across compositions and timelines so `render <id>`, `check <id>`, and `still <id>` stay unambiguous.
|
|
19
|
+
- A composition used in a timeline must share the project frame rate exactly; a different size is fitted and transformed by the clip.
|
|
20
|
+
- Timeline duration is derived from the last clip end, with an optional override. A timeline with no clips is invalid; an audio-only timeline is valid and renders black video with audio.
|
|
21
|
+
- Nested timelines are visual and audio sources, are cycle-checked, and do not accept prop overrides for their internal compositions in 0.6.
|
|
22
|
+
- Timeline clips use static fit, position, scale, rotation, and opacity. Transitions, keyframes, effects, track solo/hidden, and audio fades are deferred.
|
|
23
|
+
- Composition rendering keeps its existing result schema; timeline rendering returns a new timeline result schema.
|
|
24
|
+
- The design baseline, glossary, agent skill, scaffold, examples, and tests gain timeline language; existing composition projects remain valid without migration.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Compose timeline frames in a live browser host
|
|
2
|
+
|
|
3
|
+
**Status:** Superseded by [ADR-0012](./0012-render-timelines-with-segmented-lossless-clip-intermediates.md). Rejected for production in 0.6; retained as the preview model.
|
|
4
|
+
**Date:** 2026-10-31
|
|
5
|
+
|
|
6
|
+
> **Spike result (2026-10-31):** the host was deterministic (pixel-identical across seeks and sessions) and a one-clip host was pixel-identical to a direct composition capture, but a 1920×1080 30 fps timeline with four active layers averaged 138.8 ms/frame versus 33.6 ms/frame for a direct composition capture (4.1×). The project prioritizes long-timeline rendering throughput, so the live host is kept only as a preview candidate, not as the production compositor.
|
|
7
|
+
|
|
8
|
+
Timeline frames will be produced by a browser timeline host that loads every clip source as a layer and captures the composed stage. Composition and nested-timeline clips are same-origin iframes, media clips are video and image elements, and static clip transforms and opacity are CSS. For each timeline frame the host seeks only the active clips, waits for their settle, and the renderer captures the stage through CDP. Preview and production drive the same host, so the timeline output has one compositing path.
|
|
9
|
+
|
|
10
|
+
## Considered Options
|
|
11
|
+
|
|
12
|
+
- **Pre-render each clip and assemble with FFmpeg.** Rejected as the primary path because preview must composite live, so preview and render would diverge at scaling, opacity, and blending; intermediates also add generation loss or very large lossless files.
|
|
13
|
+
- **Capture each clip separately and composite PNG frames in Node.** Rejected because it is substantially slower per timeline frame and cannot use browser-native compositing; preview would still need a separate browser compositor.
|
|
14
|
+
- **Composite with CSS in the preview but pre-render for production.** Rejected for the same parity reason; the project's value is that preview and production execute the same behavior.
|
|
15
|
+
|
|
16
|
+
## Consequences
|
|
17
|
+
|
|
18
|
+
- Composition and nested-timeline clips are isolated iframe surfaces. CSS blend modes and backdrop filters cannot blend across clip boundaries; 0.6 has neither.
|
|
19
|
+
- The timeline host preloads all clip sources, then network is blocked for frame production. Memory and WebGL context use grow with the number of loaded clips; the spike must measure a representative timeline before this decision is accepted.
|
|
20
|
+
- Timeline rendering is sequential in 0.6. Composition rendering keeps opt-in concurrency.
|
|
21
|
+
- Audio is not played by the host; preview mixes timeline audio in the parent and production mixes it offline with FFmpeg.
|
|
22
|
+
- If the spike fails its cost budget, this ADR is revised before implementation; the timeline document contract does not depend on the compositor choice.
|