frameyard 0.0.0-stage → 0.2.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 +69 -0
- package/GLOSSARY.md +145 -0
- package/LICENSE +21 -0
- package/README.md +311 -2
- package/dist/assets/executable.d.ts +16 -0
- package/dist/assets/executable.d.ts.map +1 -0
- package/dist/assets/executable.js +59 -0
- package/dist/assets/executable.js.map +1 -0
- package/dist/assets/files.d.ts +4 -0
- package/dist/assets/files.d.ts.map +1 -0
- package/dist/assets/files.js +30 -0
- package/dist/assets/files.js.map +1 -0
- package/dist/assets/integrations.d.ts +15 -0
- package/dist/assets/integrations.d.ts.map +1 -0
- package/dist/assets/integrations.js +15 -0
- package/dist/assets/integrations.js.map +1 -0
- package/dist/assets/process.d.ts +29 -0
- package/dist/assets/process.d.ts.map +1 -0
- package/dist/assets/process.js +97 -0
- package/dist/assets/process.js.map +1 -0
- package/dist/assets/provenance.d.ts +77 -0
- package/dist/assets/provenance.d.ts.map +1 -0
- package/dist/assets/provenance.js +302 -0
- package/dist/assets/provenance.js.map +1 -0
- package/dist/assets/transcribe.d.ts +38 -0
- package/dist/assets/transcribe.d.ts.map +1 -0
- package/dist/assets/transcribe.js +357 -0
- package/dist/assets/transcribe.js.map +1 -0
- package/dist/assets/tts.d.ts +29 -0
- package/dist/assets/tts.d.ts.map +1 -0
- package/dist/assets/tts.js +180 -0
- package/dist/assets/tts.js.map +1 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +795 -0
- package/dist/cli.js.map +1 -0
- package/dist/contracts.d.ts +106 -0
- package/dist/contracts.d.ts.map +1 -0
- package/dist/contracts.js +297 -0
- package/dist/contracts.js.map +1 -0
- package/dist/doctor.d.ts +56 -0
- package/dist/doctor.d.ts.map +1 -0
- package/dist/doctor.js +476 -0
- package/dist/doctor.js.map +1 -0
- package/dist/errors.d.ts +10 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +18 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/preview/main.d.ts +19 -0
- package/dist/preview/main.d.ts.map +1 -0
- package/dist/preview/main.js +480 -0
- package/dist/preview/main.js.map +1 -0
- package/dist/project.d.ts +16 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.js +88 -0
- package/dist/project.js.map +1 -0
- package/dist/renderer/audio.d.ts +31 -0
- package/dist/renderer/audio.d.ts.map +1 -0
- package/dist/renderer/audio.js +88 -0
- package/dist/renderer/audio.js.map +1 -0
- package/dist/renderer/browser.d.ts +22 -0
- package/dist/renderer/browser.d.ts.map +1 -0
- package/dist/renderer/browser.js +175 -0
- package/dist/renderer/browser.js.map +1 -0
- package/dist/renderer/ffmpeg.d.ts +58 -0
- package/dist/renderer/ffmpeg.d.ts.map +1 -0
- package/dist/renderer/ffmpeg.js +268 -0
- package/dist/renderer/ffmpeg.js.map +1 -0
- package/dist/renderer/index.d.ts +72 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +281 -0
- package/dist/renderer/index.js.map +1 -0
- package/dist/renderer/transcripts.d.ts +30 -0
- package/dist/renderer/transcripts.d.ts.map +1 -0
- package/dist/renderer/transcripts.js +86 -0
- package/dist/renderer/transcripts.js.map +1 -0
- package/dist/runtime/clock.d.ts +5 -0
- package/dist/runtime/clock.d.ts.map +1 -0
- package/dist/runtime/clock.js +16 -0
- package/dist/runtime/clock.js.map +1 -0
- package/dist/runtime/index.d.ts +29 -0
- package/dist/runtime/index.d.ts.map +1 -0
- package/dist/runtime/index.js +289 -0
- package/dist/runtime/index.js.map +1 -0
- package/dist/runtime/random.d.ts +8 -0
- package/dist/runtime/random.d.ts.map +1 -0
- package/dist/runtime/random.js +21 -0
- package/dist/runtime/random.js.map +1 -0
- package/dist/scaffold.d.ts +3 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +176 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/server/preview-html.d.ts +2 -0
- package/dist/server/preview-html.d.ts.map +1 -0
- package/dist/server/preview-html.js +229 -0
- package/dist/server/preview-html.js.map +1 -0
- package/dist/server/server.d.ts +17 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +237 -0
- package/dist/server/server.js.map +1 -0
- package/dist/transcript.d.ts +44 -0
- package/dist/transcript.d.ts.map +1 -0
- package/dist/transcript.js +130 -0
- package/dist/transcript.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/docs/adr/0001-keep-composition-authoring-web-native.md +3 -0
- package/docs/adr/0002-make-frame-and-props-the-only-animation-state.md +3 -0
- package/docs/adr/0003-render-browser-frames-offline.md +3 -0
- package/docs/adr/0004-produce-local-ai-assets-outside-rendering.md +3 -0
- package/docs/v0.1-design.md +238 -0
- package/examples/demo/README.md +24 -0
- package/examples/demo/assets/chime.wav +0 -0
- package/examples/demo/compositions/demo.js +206 -0
- package/examples/demo/props/demo.json +5 -0
- package/examples/demo/video.config.js +5 -0
- package/package.json +73 -3
- package/skills/frameyard/README.md +35 -0
- package/skills/frameyard/SKILL.md +60 -0
- package/skills/frameyard/references/audio-assets.md +86 -0
- package/skills/frameyard/references/deterministic-authoring.md +107 -0
- package/skills/frameyard/references/failure-recovery.md +57 -0
- package/skills/frameyard/references/json-results.md +203 -0
- package/skills/frameyard/references/subtitles-karaoke.md +95 -0
- package/skills/frameyard/scripts/frameyard.mjs +60 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { isPlainObject } from "./contracts.js";
|
|
2
|
+
import { FrameyardError } from "./errors.js";
|
|
3
|
+
export const TRANSCRIPT_SCHEMA_VERSION = 1;
|
|
4
|
+
const SHA256 = /^[a-f0-9]{64}$/i;
|
|
5
|
+
function invalid(message) {
|
|
6
|
+
throw new FrameyardError("TRANSCRIPT_INVALID", message);
|
|
7
|
+
}
|
|
8
|
+
function requireString(value, label) {
|
|
9
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
10
|
+
invalid(`${label} must be a non-empty string.`);
|
|
11
|
+
}
|
|
12
|
+
return value;
|
|
13
|
+
}
|
|
14
|
+
function requireMilliseconds(value, label) {
|
|
15
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 0) {
|
|
16
|
+
invalid(`${label} must be an integer milliseconds value greater than or equal to zero.`);
|
|
17
|
+
}
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function parseTimedText(value, label) {
|
|
21
|
+
if (!isPlainObject(value)) {
|
|
22
|
+
invalid(`${label} must be a plain object.`);
|
|
23
|
+
}
|
|
24
|
+
const startMs = requireMilliseconds(value.startMs, `${label}.startMs`);
|
|
25
|
+
const endMs = requireMilliseconds(value.endMs, `${label}.endMs`);
|
|
26
|
+
if (endMs < startMs) {
|
|
27
|
+
invalid(`${label}.endMs must be greater than or equal to ${label}.startMs.`);
|
|
28
|
+
}
|
|
29
|
+
return {
|
|
30
|
+
startMs,
|
|
31
|
+
endMs,
|
|
32
|
+
text: requireString(value.text, `${label}.text`),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function parseTimedTextList(value, label) {
|
|
36
|
+
if (!Array.isArray(value)) {
|
|
37
|
+
invalid(`${label} must be an array.`);
|
|
38
|
+
}
|
|
39
|
+
const result = [];
|
|
40
|
+
let previousStart = -1;
|
|
41
|
+
for (const [index, item] of value.entries()) {
|
|
42
|
+
const parsed = parseTimedText(item, `${label}[${index}]`);
|
|
43
|
+
if (parsed.startMs < previousStart) {
|
|
44
|
+
invalid(`${label} must be ordered by startMs.`);
|
|
45
|
+
}
|
|
46
|
+
previousStart = parsed.startMs;
|
|
47
|
+
result.push(parsed);
|
|
48
|
+
}
|
|
49
|
+
return result;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Validates a canonical Frameyard transcript and returns a normalized copy.
|
|
53
|
+
* Timestamps stay in integer milliseconds relative to the source audio.
|
|
54
|
+
*/
|
|
55
|
+
export function parseTranscript(value, options = {}) {
|
|
56
|
+
if (!isPlainObject(value)) {
|
|
57
|
+
invalid("A transcript must be a plain JSON object.");
|
|
58
|
+
}
|
|
59
|
+
if (value.schemaVersion !== TRANSCRIPT_SCHEMA_VERSION) {
|
|
60
|
+
invalid(`Transcript schemaVersion must be ${TRANSCRIPT_SCHEMA_VERSION}; received ${String(value.schemaVersion)}.`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof value.text !== "string") {
|
|
63
|
+
invalid("Transcript text must be a string.");
|
|
64
|
+
}
|
|
65
|
+
const source = value.source;
|
|
66
|
+
if (!isPlainObject(source)) {
|
|
67
|
+
invalid("Transcript source must be a plain object.");
|
|
68
|
+
}
|
|
69
|
+
const sourcePath = requireString(source.path, "Transcript source.path");
|
|
70
|
+
const sourceSha256 = requireString(source.sha256, "Transcript source.sha256");
|
|
71
|
+
if (!SHA256.test(sourceSha256)) {
|
|
72
|
+
invalid("Transcript source.sha256 must be a 64-character SHA-256 hex digest.");
|
|
73
|
+
}
|
|
74
|
+
const sourceBytes = source.bytes;
|
|
75
|
+
if (typeof sourceBytes !== "number" || !Number.isInteger(sourceBytes) || sourceBytes < 0) {
|
|
76
|
+
invalid("Transcript source.bytes must be a non-negative integer.");
|
|
77
|
+
}
|
|
78
|
+
const transcript = {
|
|
79
|
+
schemaVersion: TRANSCRIPT_SCHEMA_VERSION,
|
|
80
|
+
text: value.text,
|
|
81
|
+
source: {
|
|
82
|
+
path: sourcePath,
|
|
83
|
+
sha256: sourceSha256,
|
|
84
|
+
bytes: sourceBytes,
|
|
85
|
+
},
|
|
86
|
+
segments: parseTimedTextList(value.segments, "Transcript segments"),
|
|
87
|
+
};
|
|
88
|
+
if (value.language !== undefined) {
|
|
89
|
+
transcript.language = requireString(value.language, "Transcript language");
|
|
90
|
+
}
|
|
91
|
+
if (value.words !== undefined) {
|
|
92
|
+
const words = parseTimedTextList(value.words, "Transcript words");
|
|
93
|
+
for (const [index, word] of words.entries()) {
|
|
94
|
+
if (word.text.trim().length === 0) {
|
|
95
|
+
invalid(`Transcript words[${index}].text must be a non-empty string.`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
transcript.words = words;
|
|
99
|
+
}
|
|
100
|
+
if (options.requireWords && (transcript.words === undefined || transcript.words.length === 0)) {
|
|
101
|
+
throw new FrameyardError("TRANSCRIPT_WORDS_UNAVAILABLE", "This transcript has no measured word timings. Karaoke highlighting requires word timings; re-run `frameyard transcribe --words` against the source audio.");
|
|
102
|
+
}
|
|
103
|
+
return Object.freeze(transcript);
|
|
104
|
+
}
|
|
105
|
+
function timestamp(milliseconds, separator) {
|
|
106
|
+
const totalSeconds = Math.floor(milliseconds / 1000);
|
|
107
|
+
const ms = milliseconds % 1000;
|
|
108
|
+
const seconds = totalSeconds % 60;
|
|
109
|
+
const minutes = Math.floor(totalSeconds / 60) % 60;
|
|
110
|
+
const hours = Math.floor(totalSeconds / 3600);
|
|
111
|
+
const pad = (value, width = 2) => String(value).padStart(width, "0");
|
|
112
|
+
return `${pad(hours)}:${pad(minutes)}:${pad(seconds)}${separator}${pad(ms, 3)}`;
|
|
113
|
+
}
|
|
114
|
+
/** Renders the canonical transcript's segment timings as SRT. */
|
|
115
|
+
export function transcriptToSrt(transcript) {
|
|
116
|
+
if (transcript.segments.length === 0) {
|
|
117
|
+
return "";
|
|
118
|
+
}
|
|
119
|
+
return `${transcript.segments
|
|
120
|
+
.map((segment, index) => `${index + 1}\n${timestamp(segment.startMs, ",")} --> ${timestamp(segment.endMs, ",")}\n${segment.text}`)
|
|
121
|
+
.join("\n\n")}\n`;
|
|
122
|
+
}
|
|
123
|
+
/** Renders the canonical transcript's segment timings as WebVTT. */
|
|
124
|
+
export function transcriptToVtt(transcript) {
|
|
125
|
+
const cues = transcript.segments
|
|
126
|
+
.map((segment) => `${timestamp(segment.startMs, ".")} --> ${timestamp(segment.endMs, ".")}\n${segment.text}`)
|
|
127
|
+
.join("\n\n");
|
|
128
|
+
return cues.length === 0 ? "WEBVTT\n" : `WEBVTT\n\n${cues}\n`;
|
|
129
|
+
}
|
|
130
|
+
//# sourceMappingURL=transcript.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"transcript.js","sourceRoot":"","sources":["../src/transcript.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.d.ts","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,OAAO,UAAU,CAAC"}
|
package/dist/version.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"version.js","sourceRoot":"","sources":["../src/version.ts"],"names":[],"mappings":"AAAA,MAAM,CAAC,MAAM,OAAO,GAAG,OAAO,CAAC"}
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Keep composition authoring web-native
|
|
2
|
+
|
|
3
|
+
Frameyard will use ordinary HTML, CSS, and native JavaScript modules with a framework-agnostic composition lifecycle rather than coupling the core to React or another UI framework. This preserves the project's primary differentiator and avoids freezing a UI-framework dependency into the public composition contract; adapters may be added later without changing that contract.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Make frame and props the only animation state
|
|
2
|
+
|
|
3
|
+
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.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Render browser frames offline
|
|
2
|
+
|
|
3
|
+
Production rendering will drive pinned Chromium through explicit frame seeks, capture each frame, and encode frames and timeline audio with FFmpeg instead of recording real-time browser playback. This accepts slower sequential rendering in exchange for frame accuracy under load, reproducible visual state, and parity between preview and production composition behavior.
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
# Produce local AI assets outside rendering
|
|
2
|
+
|
|
3
|
+
Frameyard integrates local speech synthesis and transcription as explicit CLI operations that freeze ordinary project assets before composition loading; model inference is never part of frame production. This separates variable model and hardware behavior from deterministic rendering and keeps the AI integrations optional, at the cost of requiring users and agents to manage generated inputs and their provenance explicitly.
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
# Frameyard v0.1 Design Baseline
|
|
2
|
+
|
|
3
|
+
**Status:** Accepted
|
|
4
|
+
**Initial acceptance:** 2026-10-27
|
|
5
|
+
**Last design revision:** 2026-10-29
|
|
6
|
+
|
|
7
|
+
This document records the decisions confirmed during the initial design sessions. It defines the v0.1 product and architecture baseline; later changes should revise this document explicitly rather than silently broaden the scope.
|
|
8
|
+
|
|
9
|
+
## 1. Product contract
|
|
10
|
+
|
|
11
|
+
Frameyard is a local-first, open-source framework for frontend developers and creative coders who build short, parameterized videos with HTML, CSS, and JavaScript. AI agents can perform the same workflows on a user's behalf through an official skill. Its differentiator is web-native authoring combined with deterministic, frame-accurate output—not a React-centric composition model or a feature-for-feature clone of an existing tool.
|
|
12
|
+
|
|
13
|
+
The initial use cases are product demos, social clips, animated diagrams, data visualizations, karaoke-style subtitles, and personalized videos. Optional local speech synthesis and transcription produce assets for these workflows. Professional long-form editing is outside the target audience.
|
|
14
|
+
|
|
15
|
+
The core v0.1 success criterion is an end-to-end example in which a developer can:
|
|
16
|
+
|
|
17
|
+
1. Create a reusable 30-second, 1920×1080 composition.
|
|
18
|
+
2. Animate it with HTML, CSS, SVG, JavaScript, and seek-safe Canvas 2D.
|
|
19
|
+
3. Supply JSON props and synchronized audio.
|
|
20
|
+
4. Preview and scrub it interactively.
|
|
21
|
+
5. Render it reproducibly to MP4 without dropped frames or timing drift.
|
|
22
|
+
|
|
23
|
+
The agent-enabled success criterion additionally requires an agent using the official skill to create and preserve a valid source project, optionally produce local audio and transcript assets, validate and inspect the composition, render it, and report all resulting artifact paths without concealing validation failures or skipped visual review.
|
|
24
|
+
|
|
25
|
+
## 2. Project and authoring model
|
|
26
|
+
|
|
27
|
+
- Projects use ordinary HTML, CSS, static assets, and native JavaScript ES modules.
|
|
28
|
+
- `video.config.js` explicitly lists composition module entry points. There is no directory-scanning discovery convention.
|
|
29
|
+
- A project may contain multiple compositions, each with a unique ID.
|
|
30
|
+
- Each composition has fixed `width`, `height`, `fps`, and `durationInFrames` metadata. Props cannot change this metadata in v0.1.
|
|
31
|
+
- A composition follows a `mount → update({ frame, props }) → dispose` lifecycle. Mounting creates the visual structure; updating makes that structure represent the requested frame.
|
|
32
|
+
- `update()` must be seek-safe and synchronous. It cannot depend on previously visited frames.
|
|
33
|
+
- Props are JSON-serializable, immutable, and fixed for one render. Composition defaults may be overridden by a JSON file or CLI option.
|
|
34
|
+
- The core validates metadata and JSON-serializability. An optional composition-defined `validateProps()` hook handles domain-specific validation before loading.
|
|
35
|
+
- User compositions are native JavaScript. The framework itself is authored in TypeScript and ships declarations, but v0.1 does not transpile user TypeScript.
|
|
36
|
+
|
|
37
|
+
## 3. Deterministic execution
|
|
38
|
+
|
|
39
|
+
The only sources of animation state are the integer frame number and immutable props. Composition frame rate establishes the mapping between frames and time.
|
|
40
|
+
|
|
41
|
+
- The runtime owns the frame clock and deterministic randomness.
|
|
42
|
+
- CSS `@keyframes` and Web Animations API animations are paused and positioned from the shared frame clock.
|
|
43
|
+
- JavaScript animation runs inside `update()`.
|
|
44
|
+
- Independent `requestAnimationFrame` loops, timers, CSS transitions, wall-clock reads, and unseeded randomness are not supported animation drivers.
|
|
45
|
+
- Deterministic random values are stateless and key-based, such as `random("star:17")`, so they do not depend on call order or rendering history.
|
|
46
|
+
- Development mode provides best-effort diagnostics for forbidden clocks and randomness; it does not claim to police every third-party library perfectly.
|
|
47
|
+
- Assets, remote data, images, fonts, generated audio, and transcripts are resolved during an explicit loading phase before frame production.
|
|
48
|
+
- Required network access is limited to the loading phase. Network requests are blocked during frame capture.
|
|
49
|
+
- AI model inference is never part of `mount()`, `update()`, or frame production.
|
|
50
|
+
- v0.1 supports DOM, CSS, SVG, and Canvas 2D that is redrawn from the current frame.
|
|
51
|
+
- Composition code is trusted local developer code. Frameyard does not claim to render untrusted compositions safely.
|
|
52
|
+
|
|
53
|
+
### Reproducibility guarantee
|
|
54
|
+
|
|
55
|
+
For the same composition revision, props, frame, assets, and pinned runtime/browser environment, Frameyard guarantees identical visual state. It does not promise bit-identical MP4 bytes, even across runs on the same machine; encoded output depends on the FFmpeg build, encoder threading, and runtime environment.
|
|
56
|
+
|
|
57
|
+
Generated assets are covered once frozen. Frameyard does not promise that repeating speech synthesis or transcription produces identical artifacts across different binaries, models, parameters, or hardware.
|
|
58
|
+
|
|
59
|
+
## 4. Preview and production rendering
|
|
60
|
+
|
|
61
|
+
Preview and production execute the same composition bundle and browser runtime. They differ only in which component drives the frame clock and whether frames are captured.
|
|
62
|
+
|
|
63
|
+
The preview provides:
|
|
64
|
+
|
|
65
|
+
- Composition selection
|
|
66
|
+
- JSON props editing
|
|
67
|
+
- Play/pause and frame-accurate scrubbing
|
|
68
|
+
- Current frame and time display
|
|
69
|
+
- Audio controls
|
|
70
|
+
|
|
71
|
+
It is a developer inspection tool, not a visual composition editor.
|
|
72
|
+
|
|
73
|
+
Production rendering:
|
|
74
|
+
|
|
75
|
+
1. Uses a pinned, Playwright-managed Chromium through the Chrome DevTools Protocol.
|
|
76
|
+
2. Seeks explicitly to each requested frame.
|
|
77
|
+
3. Waits for composition readiness.
|
|
78
|
+
4. Captures the frame.
|
|
79
|
+
5. Pipes frames to FFmpeg for encoding and audio muxing.
|
|
80
|
+
|
|
81
|
+
The reference renderer is sequential and has no frame cache. Its design nevertheless permits future parallel and out-of-order rendering because each frame is independently seekable. Simple scenes target real-time preview, but v0.1 has no production rendering-speed guarantee; it reports progress, current frame, and elapsed time.
|
|
82
|
+
|
|
83
|
+
## 5. Audio and output
|
|
84
|
+
|
|
85
|
+
Audio is a declarative timeline of clips. Each clip supports a source, start frame, source trim, and volume. Preview synchronizes this timeline in the browser; production mixes it offline with FFmpeg rather than treating live `<audio>` or Web Audio playback as the production source of truth.
|
|
86
|
+
|
|
87
|
+
Generated speech behaves like any other audio clip. Frameyard does not automatically time-stretch it, change composition duration, or regenerate it to fit. `check` reports clip durations and overrun warnings so users and agents can revise text, timing, or composition deliberately.
|
|
88
|
+
|
|
89
|
+
v0.1 produces portable MP4 files with H.264 video and AAC audio. It exposes only a small set of validated encoding options, not arbitrary FFmpeg arguments. Single-frame PNG output through `still` is a debugging artifact, not general image-sequence support.
|
|
90
|
+
|
|
91
|
+
A render fails immediately on an uncaught exception, unhandled rejection, required-asset failure, or invalid composition output. Diagnostics identify the composition 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.
|
|
92
|
+
|
|
93
|
+
## 6. CLI, tooling, and package boundaries
|
|
94
|
+
|
|
95
|
+
Project and composition commands are:
|
|
96
|
+
|
|
97
|
+
- `init`: scaffold a minimal project, including an agent guide (`AGENTS.md`) and a private `package.json` with pinned `dev`, `check`, `render`, and `still` scripts
|
|
98
|
+
- `dev`: start the preview
|
|
99
|
+
- `render <composition-id> --props props.json --out video.mp4`: render a composition
|
|
100
|
+
- `check <composition-id>`: validate configuration, props, assets, audio, and transcript requirements, then mount the composition and evaluate every frame without capturing or encoding
|
|
101
|
+
- `still <composition-id> --frame <n>`: capture one requested frame as PNG through the production rendering path
|
|
102
|
+
- `doctor`: inspect core requirements and optional integrations without repairing or downloading anything
|
|
103
|
+
|
|
104
|
+
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.
|
|
105
|
+
|
|
106
|
+
The built-in server serves native ES modules and static assets and injects an import map for the framework runtime. 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.
|
|
107
|
+
|
|
108
|
+
v0.1 uses one repository and one publishable npm package with three internal layers:
|
|
109
|
+
|
|
110
|
+
1. Shared composition contracts
|
|
111
|
+
2. Browser runtime
|
|
112
|
+
3. Node-based CLI and renderer
|
|
113
|
+
|
|
114
|
+
These layers have clean internal boundaries but no public plugin API. FFmpeg is discovered on the user's PATH and checked for required capabilities, with actionable installation and version diagnostics. The project supports the current active Node.js LTS; Linux is the reference and CI platform, while macOS and Windows remain best-effort until covered by real rendering tests.
|
|
115
|
+
|
|
116
|
+
## 7. Official agent skill
|
|
117
|
+
|
|
118
|
+
Frameyard ships an optional, host-neutral agent skill with a `SKILL.md`, supporting references, and thin scripts. The npm package includes skill files matching its API version, and documentation explains host-specific installation or referencing. v0.1 does not add a skill-installer command.
|
|
119
|
+
|
|
120
|
+
The skill:
|
|
121
|
+
|
|
122
|
+
- Uses the normal project format, composition API, and CLI available to humans.
|
|
123
|
+
- Does not introduce an agent-only runtime, MCP server, hosted control plane, or parallel rendering API.
|
|
124
|
+
- Triggers for Frameyard authoring and operation rather than claiming to be a general-purpose media-generation skill.
|
|
125
|
+
- Uses Frameyard's audio wrappers instead of invoking `qwen-tts` or `whisper-cli` directly.
|
|
126
|
+
- Includes references for deterministic authoring, audio assets, subtitle and karaoke recipes, JSON command results, and failure recovery.
|
|
127
|
+
- Invokes speech synthesis or transcription only when requested or explicitly approved as part of the brief.
|
|
128
|
+
- Does not overwrite or regenerate existing generated assets without permission.
|
|
129
|
+
- Runs `check`, inspects representative `still` images when the host supports image viewing, and discloses when visual inspection was impossible.
|
|
130
|
+
- Does not claim success when checks fail or rendering is partial.
|
|
131
|
+
|
|
132
|
+
A successful agent workflow preserves and reports the source project, props, generated assets, provenance manifest, validation results, and final video path. The user must be able to inspect, modify, and reproduce the result rather than receiving only an opaque MP4.
|
|
133
|
+
|
|
134
|
+
## 8. Local speech synthesis
|
|
135
|
+
|
|
136
|
+
The `tts` command integrates [`qwentts.cpp`](https://github.com/ServeurpersoCom/qwentts.cpp) through its external `qwen-tts` executable. It does not link the native library or require the upstream server.
|
|
137
|
+
|
|
138
|
+
v0.1 supports CustomVoice mode only:
|
|
139
|
+
|
|
140
|
+
- A named speaker is required.
|
|
141
|
+
- An optional style instruction and language may be supplied.
|
|
142
|
+
- Reference-audio voice cloning and free-form VoiceDesign are deferred.
|
|
143
|
+
- Text is supplied by `--text-file` or stdin.
|
|
144
|
+
- Talker GGUF, codec GGUF, and output paths are required.
|
|
145
|
+
- One invocation produces one frozen 24 kHz mono WAV from one complete text input.
|
|
146
|
+
- Automatic long-text chunking, streaming, and alternative audio formats are deferred.
|
|
147
|
+
|
|
148
|
+
Users acquire and provide models explicitly. Frameyard does not build `qwentts.cpp`, download or redistribute models, or imply that upstream code licenses settle model-weight licensing. Executable resolution uses an explicit option, then environment configuration, then PATH.
|
|
149
|
+
|
|
150
|
+
## 9. Local transcription and timed text
|
|
151
|
+
|
|
152
|
+
The `transcribe` command integrates [`whisper.cpp`](https://github.com/ggml-org/whisper.cpp) through its external `whisper-cli` executable. Before transcription, Frameyard uses FFmpeg to normalize source audio or video to 16 kHz mono PCM WAV, avoiding dependence on optional upstream decoder builds and inconsistent input-format documentation.
|
|
153
|
+
|
|
154
|
+
Users provide a Whisper GGML model explicitly. The command requires source and output paths and supports optional word timings through `--words` and derived subtitle exports through `--export srt,vtt`. Executable resolution follows the same option, environment, then PATH order as speech synthesis.
|
|
155
|
+
|
|
156
|
+
The canonical artifact is a versioned Frameyard JSON document containing:
|
|
157
|
+
|
|
158
|
+
- Full text
|
|
159
|
+
- Source identity and content hash
|
|
160
|
+
- Language when known
|
|
161
|
+
- Timed segments
|
|
162
|
+
- Optional word timings
|
|
163
|
+
|
|
164
|
+
All timestamps are integer milliseconds relative to the source audio, not composition frame numbers. Composition code converts them using the audio clip's start frame, source trim, and composition frame rate.
|
|
165
|
+
|
|
166
|
+
Compositions may declare required transcript assets with a `transcripts` field analogous to `audio`:
|
|
167
|
+
|
|
168
|
+
```js
|
|
169
|
+
transcripts: [{ src: "assets/voice.transcript.json", words: true }]
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
`check` and production validate declared transcript files before frame production. `words: true` requires measured word timings and fails when they are unavailable; it never triggers interpolation.
|
|
173
|
+
|
|
174
|
+
SRT and VTT are derived exports, not the canonical contract. Word-level karaoke requires actual word timings and fails validation when they are unavailable. Frameyard never fabricates word timings by interpolating segment timestamps and never executes Whisper's generated bash/FFmpeg karaoke script.
|
|
175
|
+
|
|
176
|
+
The official skill documents seek-safe subtitle and karaoke recipes. Compositions retain responsibility for visual presentation; v0.1 has no built-in subtitle renderer and does not automatically burn captions into videos.
|
|
177
|
+
|
|
178
|
+
## 10. Generated assets and inference operation
|
|
179
|
+
|
|
180
|
+
Speech synthesis and transcription are separate asset-production operations that complete before composition loading and rendering. Their outputs are ordinary local project assets. Once those assets are frozen, ordinary authoring, preview, checking, and rendering require neither AI executable nor model.
|
|
181
|
+
|
|
182
|
+
Both integrations are optional and local-only, with no cloud fallback. Network activity needed to acquire tools or models is a separate explicit setup operation, never a hidden side effect of generation or rendering.
|
|
183
|
+
|
|
184
|
+
Generated artifacts follow a shared contract:
|
|
185
|
+
|
|
186
|
+
- Outputs are written atomically to explicit paths.
|
|
187
|
+
- Existing outputs are not overwritten unless `--force` is supplied.
|
|
188
|
+
- A versioned project-local provenance manifest records artifact content hashes, generator details, model identities, and parameters. The default manifest is `frameyard.assets.json` in the project root; `--manifest` selects another path.
|
|
189
|
+
- Transcription provenance also identifies the source asset by content hash.
|
|
190
|
+
- Partial operations leave neither apparently valid artifacts nor manifest entries.
|
|
191
|
+
- `doctor` validates executables, model readability, capabilities, and supported integration versions without modifying the environment.
|
|
192
|
+
|
|
193
|
+
Each operation runs one inference subprocess, and the official agent workflow executes inference sequentially to avoid unexpected CPU/GPU contention. There is no arbitrary fixed timeout. Cancellation terminates the child process and removes partial outputs.
|
|
194
|
+
|
|
195
|
+
Release documentation must identify the exact `qwentts.cpp` and `whisper.cpp` revisions against which the wrappers are tested. Frameyard does not claim compatibility with arbitrary upstream revisions.
|
|
196
|
+
|
|
197
|
+
## 11. Verification strategy
|
|
198
|
+
|
|
199
|
+
Tests prioritize public behavior and end-to-end evidence over implementation-only unit coverage:
|
|
200
|
+
|
|
201
|
+
- Sequential, individual, and random-order frame renders produce equivalent images.
|
|
202
|
+
- Determinism checks compare captured pixels or stills; encoded MP4 bytes are not a reproducibility oracle.
|
|
203
|
+
- Preview and renderer use the same runtime.
|
|
204
|
+
- CSS, JavaScript, SVG, and Canvas animations seek correctly.
|
|
205
|
+
- Asset and runtime failures produce nonzero CLI exits.
|
|
206
|
+
- Rendered MP4 files have the expected dimensions, frame count, duration, and audio stream.
|
|
207
|
+
- Golden-frame tests cover representative visual behavior.
|
|
208
|
+
- `check` exercises every frame, and `still` follows the production capture path.
|
|
209
|
+
- 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.
|
|
210
|
+
- Real-model integration tests are explicitly opt-in and use documented upstream revisions.
|
|
211
|
+
- Agent-workflow evaluations verify source preservation, provenance, validation, still inspection behavior, failure reporting, and final renders.
|
|
212
|
+
|
|
213
|
+
## 12. Deferred from v0.1
|
|
214
|
+
|
|
215
|
+
These are scope boundaries, not permanent rejections:
|
|
216
|
+
|
|
217
|
+
- Visual editing, hosted rendering, collaboration, and asset marketplaces
|
|
218
|
+
- Cloud generative-AI services, text-to-video generation, and image generation
|
|
219
|
+
- `qwentts.cpp` reference-audio voice cloning and VoiceDesign
|
|
220
|
+
- Automatic long-text chunking, streaming speech synthesis, and alternative generated-audio formats
|
|
221
|
+
- Built-in subtitle rendering and automatic caption burn-in
|
|
222
|
+
- Automatic upstream builds, model downloads, weight redistribution, and environment repair
|
|
223
|
+
- Agent-only runtimes, MCP servers, hosted agent control planes, and host-specific skill installers
|
|
224
|
+
- React or other UI-framework adapters in the core
|
|
225
|
+
- Public plugin APIs and a broad integration ecosystem
|
|
226
|
+
- Dynamic composition metadata derived from props
|
|
227
|
+
- Embedded video, WebGL, live streams, and other independently timed visual content
|
|
228
|
+
- Dynamic per-frame asset loading or AI inference
|
|
229
|
+
- Parallel rendering and frame caches
|
|
230
|
+
- Additional final-video formats, general image-sequence output, and arbitrary FFmpeg argument passthrough
|
|
231
|
+
- Built-in TypeScript transpilation and npm dependency optimization
|
|
232
|
+
- Rendering untrusted compositions
|
|
233
|
+
- Bit-identical cross-platform encoded output or AI regeneration
|
|
234
|
+
- A minimum production rendering-speed guarantee
|
|
235
|
+
|
|
236
|
+
## 13. Release posture
|
|
237
|
+
|
|
238
|
+
Frameyard is MIT-licensed. Initial releases use `0.x` versioning, and composition, configuration, CLI, JSON-result, transcript, and skill-contract changes are documented explicitly until those contracts stabilize.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Frameyard demo
|
|
2
|
+
|
|
3
|
+
A 30-second, 1920×1080 composition that exercises the v0.1 core:
|
|
4
|
+
|
|
5
|
+
- HTML and CSS layout, including seeked CSS keyframe animation
|
|
6
|
+
- deterministic Canvas 2D starfield
|
|
7
|
+
- SVG path drawing and data bars updated from the frame
|
|
8
|
+
- JSON props for the title, subtitle, and accent color
|
|
9
|
+
- a declarative audio timeline with repeated local chime clips
|
|
10
|
+
|
|
11
|
+
From the repository root:
|
|
12
|
+
|
|
13
|
+
```sh
|
|
14
|
+
pnpm build
|
|
15
|
+
node dist/cli.js check demo --project examples/demo
|
|
16
|
+
node dist/cli.js still demo --frame 240 --project examples/demo --out demo-still.png
|
|
17
|
+
node dist/cli.js render demo --project examples/demo --props examples/demo/props/demo.json --out demo.mp4
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Start the interactive preview with:
|
|
21
|
+
|
|
22
|
+
```sh
|
|
23
|
+
node dist/cli.js dev --project examples/demo
|
|
24
|
+
```
|
|
Binary file
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { defineComposition } from "frameyard";
|
|
2
|
+
import { random } from "frameyard/runtime";
|
|
3
|
+
|
|
4
|
+
let scene;
|
|
5
|
+
let title;
|
|
6
|
+
let subtitle;
|
|
7
|
+
let kaption;
|
|
8
|
+
let progress;
|
|
9
|
+
let signalPath;
|
|
10
|
+
let bars = [];
|
|
11
|
+
let starfield;
|
|
12
|
+
let stars;
|
|
13
|
+
let context;
|
|
14
|
+
let signalLength = 0;
|
|
15
|
+
|
|
16
|
+
const STAR_COUNT = 130;
|
|
17
|
+
const BAR_COUNT = 52;
|
|
18
|
+
const BASELINE = 858;
|
|
19
|
+
|
|
20
|
+
function clamp(value) {
|
|
21
|
+
return Math.max(0, Math.min(1, value));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function easeOut(value) {
|
|
25
|
+
const t = clamp(value);
|
|
26
|
+
return 1 - Math.pow(1 - t, 3);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeStars() {
|
|
30
|
+
stars = Array.from({ length: STAR_COUNT }, (_, index) => ({
|
|
31
|
+
x: random("star:" + index + ":x"),
|
|
32
|
+
y: random("star:" + index + ":y"),
|
|
33
|
+
radius: 0.6 + random("star:" + index + ":r") * 1.8,
|
|
34
|
+
speed: 6 + random("star:" + index + ":speed") * 34,
|
|
35
|
+
alpha: 0.12 + random("star:" + index + ":alpha") * 0.65,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function buildBars() {
|
|
40
|
+
const svg = document.querySelector("#signal");
|
|
41
|
+
const group = document.createElementNS("http://www.w3.org/2000/svg", "g");
|
|
42
|
+
bars = [];
|
|
43
|
+
|
|
44
|
+
for (let index = 0; index < BAR_COUNT; index += 1) {
|
|
45
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
46
|
+
const height = 24 + random("bar:" + index + ":height") * 176;
|
|
47
|
+
const x = 168 + index * 31;
|
|
48
|
+
rect.setAttribute("x", String(x));
|
|
49
|
+
rect.setAttribute("width", "15");
|
|
50
|
+
rect.setAttribute("rx", "7.5");
|
|
51
|
+
rect.setAttribute("fill", "var(--accent)");
|
|
52
|
+
rect.setAttribute("height", "0");
|
|
53
|
+
rect.setAttribute("y", String(BASELINE));
|
|
54
|
+
rect.setAttribute("opacity", "0");
|
|
55
|
+
group.append(rect);
|
|
56
|
+
bars.push({ element: rect, height });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
svg.append(group);
|
|
60
|
+
return group;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function buildSignalPath() {
|
|
64
|
+
const points = [];
|
|
65
|
+
for (let index = 0; index <= 120; index += 1) {
|
|
66
|
+
const x = 160 + (index / 120) * 1600;
|
|
67
|
+
const wave = Math.sin(index / 7) * 38 + Math.sin(index / 17 + 1.4) * 22 + (random("signal:" + index) - 0.5) * 12;
|
|
68
|
+
points.push([x, 535 + wave]);
|
|
69
|
+
}
|
|
70
|
+
const d = points.map(([x, y], index) => (index === 0 ? "M" : "L") + x + " " + y).join(" ");
|
|
71
|
+
signalPath.setAttribute("d", d);
|
|
72
|
+
signalLength = signalPath.getTotalLength();
|
|
73
|
+
signalPath.style.strokeDasharray = String(signalLength);
|
|
74
|
+
signalPath.style.strokeDashoffset = String(signalLength);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default defineComposition({
|
|
78
|
+
width: 1920,
|
|
79
|
+
height: 1080,
|
|
80
|
+
fps: 30,
|
|
81
|
+
durationInFrames: 900,
|
|
82
|
+
defaultProps: {
|
|
83
|
+
title: "Frameyard",
|
|
84
|
+
subtitle: "Deterministic video, authored with the web platform.",
|
|
85
|
+
accent: "#b8f36b",
|
|
86
|
+
},
|
|
87
|
+
validateProps(props) {
|
|
88
|
+
const errors = [];
|
|
89
|
+
if (typeof props.title !== "string" || props.title.length === 0) errors.push("title must be a non-empty string");
|
|
90
|
+
if (typeof props.subtitle !== "string") errors.push("subtitle must be a string");
|
|
91
|
+
if (typeof props.accent !== "string") errors.push("accent must be a string");
|
|
92
|
+
return errors;
|
|
93
|
+
},
|
|
94
|
+
audio: [
|
|
95
|
+
{ src: "assets/chime.wav", startFrame: 18, volume: 0.5 },
|
|
96
|
+
{ src: "assets/chime.wav", startFrame: 300, volume: 0.38 },
|
|
97
|
+
{ src: "assets/chime.wav", startFrame: 600, volume: 0.42 },
|
|
98
|
+
{ src: "assets/chime.wav", startFrame: 840, volume: 0.55 },
|
|
99
|
+
],
|
|
100
|
+
mount({ container, props }) {
|
|
101
|
+
container.style.setProperty("--accent", props.accent);
|
|
102
|
+
container.innerHTML =
|
|
103
|
+
"<style>" +
|
|
104
|
+
"@keyframes orbit { from { transform: rotate(0deg) translateX(210px) rotate(0deg); } to { transform: rotate(360deg) translateX(210px) rotate(-360deg); } }" +
|
|
105
|
+
"@keyframes breathe { 0%, 100% { opacity: .35; transform: scale(1); } 50% { opacity: .7; transform: scale(1.12); } }" +
|
|
106
|
+
"#scene { position: relative; width: 100%; height: 100%; overflow: hidden; background: #080a0d; color: #eef2f7; font-family: Inter, ui-sans-serif, system-ui, sans-serif; }" +
|
|
107
|
+
"#field { position: absolute; inset: 0; width: 100%; height: 100%; }" +
|
|
108
|
+
"#orb { position: absolute; top: 46%; left: 38%; width: 460px; height: 460px; margin: -230px 0 0 -230px; border-radius: 50%; background: var(--accent); filter: blur(110px); opacity: .28; animation: orbit 30s linear infinite;" +
|
|
109
|
+
" }" +
|
|
110
|
+
"#glow { position: absolute; top: 8%; right: 6%; width: 280px; height: 280px; border-radius: 50%; background: var(--accent); filter: blur(90px); animation: breathe 9s ease-in-out infinite; }" +
|
|
111
|
+
"#rail { position: absolute; top: 92px; left: 160px; right: 160px; height: 1px; background: rgba(255,255,255,.14); }" +
|
|
112
|
+
"#eyebrow { position: absolute; top: 64px; left: 160px; font: 600 14px/1 ui-monospace, monospace; letter-spacing: .28em; text-transform: uppercase; color: var(--accent); }" +
|
|
113
|
+
"#meta { position: absolute; top: 64px; right: 160px; font: 500 14px/1 ui-monospace, monospace; letter-spacing: .12em; color: #788595; }" +
|
|
114
|
+
"#copy { position: absolute; left: 160px; top: 236px; width: 1320px; }" +
|
|
115
|
+
"#title { margin: 0; font-size: 156px; line-height: .92; letter-spacing: -.055em; font-weight: 680; }" +
|
|
116
|
+
"#subtitle { margin: 34px 0 0; max-width: 820px; font-size: 31px; line-height: 1.45; color: #96a2b2; font-weight: 420; }" +
|
|
117
|
+
"#signal { position: absolute; left: 0; bottom: 0; width: 1920px; height: 1080px; overflow: visible; }" +
|
|
118
|
+
"#caption { position: absolute; left: 160px; bottom: 92px; font: 500 13px/1 ui-monospace, monospace; letter-spacing: .16em; color: #7e8b9b; }" +
|
|
119
|
+
"#caption strong { color: var(--accent); font-weight: 600; }" +
|
|
120
|
+
"#progress-track { position: absolute; left: 160px; right: 160px; bottom: 72px; height: 2px; background: rgba(255,255,255,.1); }" +
|
|
121
|
+
"#progress { position: absolute; inset: 0 auto 0 0; width: 0%; background: var(--accent); box-shadow: 0 0 18px var(--accent); }" +
|
|
122
|
+
"#vignette { position: absolute; inset: 0; background: radial-gradient(ellipse at center, transparent 48%, rgba(0,0,0,.42) 100%); pointer-events: none; }" +
|
|
123
|
+
"</style>" +
|
|
124
|
+
'<div id="scene">' +
|
|
125
|
+
'<canvas id="field" width="1920" height="1080"></canvas>' +
|
|
126
|
+
'<div id="orb"></div><div id="glow"></div><div id="vignette"></div>' +
|
|
127
|
+
'<div id="rail"></div><div id="eyebrow">Frameyard / Field Notes</div><div id="meta">1920×1080 · 30 FPS · 30.00 S</div>' +
|
|
128
|
+
'<div id="copy"><h1 id="title"></h1><p id="subtitle"></p></div>' +
|
|
129
|
+
'<svg id="signal" viewBox="0 0 1920 1080" fill="none" aria-hidden="true">' +
|
|
130
|
+
'<path id="signal-path" stroke="var(--accent)" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" opacity=".82"/>' +
|
|
131
|
+
"</svg>" +
|
|
132
|
+
'<div id="caption">FRAME <strong id="frame">000</strong> / 899 · SEEK-SAFE CANVAS + SVG + CSS</div>' +
|
|
133
|
+
'<div id="progress-track"><div id="progress"></div></div>' +
|
|
134
|
+
"</div>";
|
|
135
|
+
|
|
136
|
+
scene = container.querySelector("#scene");
|
|
137
|
+
title = container.querySelector("#title");
|
|
138
|
+
subtitle = container.querySelector("#subtitle");
|
|
139
|
+
kaption = container.querySelector("#frame");
|
|
140
|
+
progress = container.querySelector("#progress");
|
|
141
|
+
signalPath = container.querySelector("#signal-path");
|
|
142
|
+
starfield = container.querySelector("#field");
|
|
143
|
+
context = starfield.getContext("2d");
|
|
144
|
+
makeStars();
|
|
145
|
+
buildBars();
|
|
146
|
+
buildSignalPath();
|
|
147
|
+
title.textContent = props.title;
|
|
148
|
+
subtitle.textContent = props.subtitle;
|
|
149
|
+
},
|
|
150
|
+
update({ frame, props }) {
|
|
151
|
+
const intro = easeOut(frame / 52);
|
|
152
|
+
const copyIntro = easeOut((frame - 26) / 62);
|
|
153
|
+
const signalReveal = easeOut((frame - 96) / 260);
|
|
154
|
+
const outro = 1 - clamp((frame - 824) / 70);
|
|
155
|
+
const total = 899;
|
|
156
|
+
|
|
157
|
+
scene.style.opacity = String(outro);
|
|
158
|
+
title.textContent = props.title;
|
|
159
|
+
subtitle.textContent = props.subtitle;
|
|
160
|
+
title.style.opacity = String(intro);
|
|
161
|
+
title.style.transform = "translateY(" + (1 - intro) * 46 + "px)";
|
|
162
|
+
subtitle.style.opacity = String(copyIntro);
|
|
163
|
+
subtitle.style.transform = "translateY(" + (1 - copyIntro) * 26 + "px)";
|
|
164
|
+
kaption.textContent = String(frame).padStart(3, "0");
|
|
165
|
+
progress.style.width = (frame / total) * 100 + "%";
|
|
166
|
+
|
|
167
|
+
const width = starfield.width;
|
|
168
|
+
const height = starfield.height;
|
|
169
|
+
context.clearRect(0, 0, width, height);
|
|
170
|
+
for (const star of stars) {
|
|
171
|
+
const drift = (frame * star.speed) % width;
|
|
172
|
+
const x = (star.x * width + drift) % width;
|
|
173
|
+
const y = star.y * height;
|
|
174
|
+
context.beginPath();
|
|
175
|
+
context.fillStyle = "rgba(214, 236, 255, " + star.alpha * (0.45 + 0.55 * intro) + ")";
|
|
176
|
+
context.arc(x, y, star.radius, 0, Math.PI * 2);
|
|
177
|
+
context.fill();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
signalPath.style.strokeDashoffset = String(signalLength * (1 - signalReveal));
|
|
181
|
+
signalPath.style.opacity = String(0.18 + signalReveal * 0.64);
|
|
182
|
+
|
|
183
|
+
const barReveal = clamp((frame - 120) / 240);
|
|
184
|
+
for (let index = 0; index < bars.length; index += 1) {
|
|
185
|
+
const bar = bars[index];
|
|
186
|
+
const local = clamp(barReveal * BAR_COUNT - index);
|
|
187
|
+
const height = bar.height * local;
|
|
188
|
+
bar.element.setAttribute("height", height.toFixed(2));
|
|
189
|
+
bar.element.setAttribute("y", String(BASELINE - height));
|
|
190
|
+
bar.element.setAttribute("opacity", (0.25 + local * 0.62).toFixed(3));
|
|
191
|
+
}
|
|
192
|
+
},
|
|
193
|
+
dispose() {
|
|
194
|
+
scene = undefined;
|
|
195
|
+
title = undefined;
|
|
196
|
+
subtitle = undefined;
|
|
197
|
+
kaption = undefined;
|
|
198
|
+
progress = undefined;
|
|
199
|
+
signalPath = undefined;
|
|
200
|
+
starfield = undefined;
|
|
201
|
+
context = undefined;
|
|
202
|
+
bars = [];
|
|
203
|
+
stars = undefined;
|
|
204
|
+
signalLength = 0;
|
|
205
|
+
},
|
|
206
|
+
});
|