frameyard 0.2.0 → 0.4.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 +102 -0
- package/GLOSSARY.md +58 -2
- package/README.md +185 -95
- package/dist/bindings.d.ts +42 -0
- package/dist/bindings.d.ts.map +1 -0
- package/dist/bindings.js +144 -0
- package/dist/bindings.js.map +1 -0
- package/dist/cli.js +37 -14
- package/dist/cli.js.map +1 -1
- package/dist/contracts.d.ts +73 -47
- package/dist/contracts.d.ts.map +1 -1
- package/dist/contracts.js +14 -159
- package/dist/contracts.js.map +1 -1
- package/dist/errors.d.ts +15 -4
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +8 -0
- package/dist/errors.js.map +1 -1
- package/dist/html/index.d.ts +5 -0
- package/dist/html/index.d.ts.map +1 -0
- package/dist/html/index.js +3 -0
- package/dist/html/index.js.map +1 -0
- package/dist/html/parse.d.ts +30 -0
- package/dist/html/parse.d.ts.map +1 -0
- package/dist/html/parse.js +80 -0
- package/dist/html/parse.js.map +1 -0
- package/dist/html/validate.d.ts +14 -0
- package/dist/html/validate.d.ts.map +1 -0
- package/dist/html/validate.js +931 -0
- package/dist/html/validate.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/preview/main.d.ts +12 -3
- package/dist/preview/main.d.ts.map +1 -1
- package/dist/preview/main.js +208 -85
- package/dist/preview/main.js.map +1 -1
- package/dist/project.d.ts +2 -0
- package/dist/project.d.ts.map +1 -1
- package/dist/project.js +4 -0
- package/dist/project.js.map +1 -1
- package/dist/renderer/audio.d.ts +5 -1
- package/dist/renderer/audio.d.ts.map +1 -1
- package/dist/renderer/audio.js +4 -0
- package/dist/renderer/audio.js.map +1 -1
- package/dist/renderer/browser.d.ts +12 -1
- package/dist/renderer/browser.d.ts.map +1 -1
- package/dist/renderer/browser.js +69 -11
- package/dist/renderer/browser.js.map +1 -1
- package/dist/renderer/index.d.ts +30 -5
- package/dist/renderer/index.d.ts.map +1 -1
- package/dist/renderer/index.js +204 -26
- package/dist/renderer/index.js.map +1 -1
- package/dist/runtime/bindings.d.ts +11 -0
- package/dist/runtime/bindings.d.ts.map +1 -0
- package/dist/runtime/bindings.js +164 -0
- package/dist/runtime/bindings.js.map +1 -0
- package/dist/runtime/diagnostics.d.ts +4 -0
- package/dist/runtime/diagnostics.d.ts.map +1 -0
- package/dist/runtime/diagnostics.js +107 -0
- package/dist/runtime/diagnostics.js.map +1 -0
- package/dist/runtime/host.d.ts +12 -0
- package/dist/runtime/host.d.ts.map +1 -0
- package/dist/runtime/host.js +701 -0
- package/dist/runtime/host.js.map +1 -0
- package/dist/runtime/index.d.ts +0 -26
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +0 -286
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/media.d.ts +16 -0
- package/dist/runtime/media.d.ts.map +1 -0
- package/dist/runtime/media.js +307 -0
- package/dist/runtime/media.js.map +1 -0
- package/dist/scaffold.d.ts.map +1 -1
- package/dist/scaffold.js +169 -78
- package/dist/scaffold.js.map +1 -1
- package/dist/server/composition-html.d.ts +20 -0
- package/dist/server/composition-html.d.ts.map +1 -0
- package/dist/server/composition-html.js +45 -0
- package/dist/server/composition-html.js.map +1 -0
- package/dist/server/preview-html.d.ts.map +1 -1
- package/dist/server/preview-html.js +19 -14
- package/dist/server/preview-html.js.map +1 -1
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +94 -6
- package/dist/server/server.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
- package/docs/adr/0003-render-browser-frames-offline.md +2 -0
- package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +3 -0
- package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +11 -0
- package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
- package/docs/migration-0.4.md +279 -0
- package/docs/v0.1-design.md +82 -57
- package/examples/demo/README.md +7 -3
- package/examples/demo/compositions/demo.controller.js +120 -0
- package/examples/demo/compositions/demo.html +253 -0
- package/examples/demo/video.config.js +1 -1
- package/package.json +9 -3
- package/skills/frameyard/README.md +4 -0
- package/skills/frameyard/SKILL.md +41 -11
- package/skills/frameyard/references/audio-assets.md +25 -14
- package/skills/frameyard/references/bindings.md +73 -0
- package/skills/frameyard/references/common-mistakes.md +59 -0
- package/skills/frameyard/references/controllers.md +126 -0
- package/skills/frameyard/references/deterministic-authoring.md +160 -41
- package/skills/frameyard/references/diagnostics.md +90 -0
- package/skills/frameyard/references/failure-recovery.md +38 -12
- package/skills/frameyard/references/json-results.md +43 -8
- package/skills/frameyard/references/subtitles-karaoke.md +10 -18
- package/examples/demo/compositions/demo.js +0 -206
|
@@ -11,7 +11,7 @@ Every non-interactive command supports `--json`. Stdout contains one JSON docume
|
|
|
11
11
|
}
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
Additional fields are command-specific and stable within the 0.
|
|
14
|
+
Additional fields are command-specific and stable within the 0.4 contract.
|
|
15
15
|
|
|
16
16
|
## Error envelope
|
|
17
17
|
|
|
@@ -21,12 +21,37 @@ Additional fields are command-specific and stable within the 0.1 contract.
|
|
|
21
21
|
"command": "render",
|
|
22
22
|
"error": {
|
|
23
23
|
"code": "OUTPUT_EXISTS",
|
|
24
|
-
"message": "Output already exists: ..."
|
|
24
|
+
"message": "Output already exists: ...",
|
|
25
|
+
"details": {}
|
|
26
|
+
},
|
|
27
|
+
"diagnostics": []
|
|
28
|
+
}
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`error.details` and top-level `diagnostics` are optional. Use the error `code` for decisions, not message text. `command` is `null` only for usage failures that occur before a command is known. Exit code `2` means usage; `130` means cancellation; other failures exit `1`.
|
|
32
|
+
|
|
33
|
+
## Diagnostics
|
|
34
|
+
|
|
35
|
+
`check`, `still`, and `render` results contain a `diagnostics` array:
|
|
36
|
+
|
|
37
|
+
```json
|
|
38
|
+
{
|
|
39
|
+
"severity": "warning",
|
|
40
|
+
"code": "BINDING_PATH_MISSING",
|
|
41
|
+
"message": "data-fy-text references prop \"titel\", which is not in the resolved props.",
|
|
42
|
+
"details": {
|
|
43
|
+
"path": "compositions/hello.html",
|
|
44
|
+
"line": 12,
|
|
45
|
+
"column": 5,
|
|
46
|
+
"element": "h1",
|
|
47
|
+
"attribute": "data-fy-text",
|
|
48
|
+
"value": "titel",
|
|
49
|
+
"suggestion": "title"
|
|
25
50
|
}
|
|
26
51
|
}
|
|
27
52
|
```
|
|
28
53
|
|
|
29
|
-
|
|
54
|
+
Static validation diagnostics include source locations. Runtime diagnostics may include a `frame`. Audio and transcript warnings use `AUDIO_WARNING` and `TRANSCRIPT_WARNING`; controller clock/randomness and CSS transition warnings use `DETERMINISM_WARNING`. See [diagnostics](diagnostics.md) for the code table.
|
|
30
55
|
|
|
31
56
|
## check
|
|
32
57
|
|
|
@@ -35,11 +60,12 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
35
60
|
"ok": true,
|
|
36
61
|
"command": "check",
|
|
37
62
|
"composition": "hello",
|
|
63
|
+
"entry": "compositions/hello.html",
|
|
64
|
+
"controller": { "kind": "inline" },
|
|
38
65
|
"metadata": { "width": 1920, "height": 1080, "fps": 30, "durationInFrames": 900 },
|
|
39
66
|
"framesChecked": 900,
|
|
40
67
|
"audio": {
|
|
41
68
|
"clips": 1,
|
|
42
|
-
"warnings": [],
|
|
43
69
|
"details": [
|
|
44
70
|
{
|
|
45
71
|
"src": "assets/voice.wav",
|
|
@@ -54,15 +80,17 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
54
80
|
},
|
|
55
81
|
"transcripts": {
|
|
56
82
|
"clips": 1,
|
|
57
|
-
"warnings": [],
|
|
58
83
|
"details": [
|
|
59
84
|
{ "src": "assets/voice.transcript.json", "language": "en", "segments": 3, "words": 17, "durationMs": 5200, "bytes": 2048 }
|
|
60
85
|
]
|
|
61
86
|
},
|
|
87
|
+
"diagnostics": [],
|
|
62
88
|
"elapsedMs": 1200
|
|
63
89
|
}
|
|
64
90
|
```
|
|
65
91
|
|
|
92
|
+
`entry` is the project-relative HTML path. `controller` is `{ kind: "inline" }`, `{ kind: "external", src }`, or `null`.
|
|
93
|
+
|
|
66
94
|
## still
|
|
67
95
|
|
|
68
96
|
```json
|
|
@@ -70,10 +98,13 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
70
98
|
"ok": true,
|
|
71
99
|
"command": "still",
|
|
72
100
|
"composition": "hello",
|
|
101
|
+
"entry": "compositions/hello.html",
|
|
102
|
+
"controller": { "kind": "external", "src": "compositions/hello.controller.js" },
|
|
73
103
|
"frame": 42,
|
|
74
104
|
"path": "/absolute/path/hello-42.png",
|
|
75
105
|
"width": 1920,
|
|
76
106
|
"height": 1080,
|
|
107
|
+
"diagnostics": [],
|
|
77
108
|
"elapsedMs": 700
|
|
78
109
|
}
|
|
79
110
|
```
|
|
@@ -85,6 +116,8 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
85
116
|
"ok": true,
|
|
86
117
|
"command": "render",
|
|
87
118
|
"composition": "hello",
|
|
119
|
+
"entry": "compositions/hello.html",
|
|
120
|
+
"controller": { "kind": "inline" },
|
|
88
121
|
"path": "/absolute/path/hello.mp4",
|
|
89
122
|
"width": 1920,
|
|
90
123
|
"height": 1080,
|
|
@@ -92,14 +125,16 @@ Use the error `code` for decisions, not message text. `command` is `null` only f
|
|
|
92
125
|
"durationInFrames": 900,
|
|
93
126
|
"durationSeconds": 30,
|
|
94
127
|
"framesRendered": 900,
|
|
128
|
+
"concurrency": 1,
|
|
95
129
|
"bytes": 1234567,
|
|
96
|
-
"audio": { "clips": 1
|
|
97
|
-
"transcripts": { "clips": 1
|
|
130
|
+
"audio": { "clips": 1 },
|
|
131
|
+
"transcripts": { "clips": 1 },
|
|
132
|
+
"diagnostics": [],
|
|
98
133
|
"elapsedMs": 99000
|
|
99
134
|
}
|
|
100
135
|
```
|
|
101
136
|
|
|
102
|
-
A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published.
|
|
137
|
+
A successful render means the final path exists and was published atomically. A nonzero exit means no new partial video was published. `concurrency` is the number of browser sessions actually used; it is capped by `durationInFrames` and by the CLI maximum of 16.
|
|
103
138
|
|
|
104
139
|
## tts
|
|
105
140
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Subtitles and karaoke
|
|
2
2
|
|
|
3
|
-
Frameyard does not burn captions into videos and has no built-in subtitle renderer.
|
|
3
|
+
Frameyard does not burn captions into videos and has no built-in subtitle renderer. A composition controller renders timed text. Transcription produces a canonical transcript; SRT and VTT are derived exports.
|
|
4
4
|
|
|
5
5
|
## Canonical transcript
|
|
6
6
|
|
|
@@ -33,29 +33,21 @@ All timestamps are integer milliseconds relative to the source audio:
|
|
|
33
33
|
|
|
34
34
|
## Declaring transcript requirements
|
|
35
35
|
|
|
36
|
-
```
|
|
37
|
-
|
|
38
|
-
{ src: "assets/voice.transcript.json", words: true },
|
|
39
|
-
],
|
|
36
|
+
```html
|
|
37
|
+
<fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
|
|
40
38
|
```
|
|
41
39
|
|
|
42
|
-
Transcript `src` paths
|
|
40
|
+
Transcript `src` paths resolve from the project root. `check`, `still`, and `render` validate declared transcript files. Preview loads the declarations but leaves visual presentation to the composition controller.
|
|
43
41
|
|
|
44
42
|
## Converting source time to frames
|
|
45
43
|
|
|
46
|
-
Given an audio
|
|
44
|
+
Given an `<audio>` element:
|
|
47
45
|
|
|
48
|
-
```
|
|
49
|
-
|
|
50
|
-
src: "assets/voice.wav",
|
|
51
|
-
startFrame: 30,
|
|
52
|
-
trimStart: 0.25,
|
|
53
|
-
trimDuration: 4.2,
|
|
54
|
-
volume: 0.8,
|
|
55
|
-
};
|
|
46
|
+
```html
|
|
47
|
+
<audio src="assets/voice.wav" data-fy-start="30" data-fy-trim="0.25" data-fy-duration="4.2" data-fy-volume="0.8"></audio>
|
|
56
48
|
```
|
|
57
49
|
|
|
58
|
-
Convert a word or segment time with:
|
|
50
|
+
the controller can read the attributes or keep matching constants. Convert a word or segment time with:
|
|
59
51
|
|
|
60
52
|
```js
|
|
61
53
|
const FPS = 30;
|
|
@@ -64,7 +56,7 @@ const frame = clip.startFrame + Math.round((word.startMs / 1000 - clip.trimStart
|
|
|
64
56
|
|
|
65
57
|
Skip words before the trimmed source range or after `trimStart + trimDuration`. Do not divide by `durationInFrames` or assume a 30 fps composition.
|
|
66
58
|
|
|
67
|
-
A seek-safe karaoke update only reads the current frame:
|
|
59
|
+
A seek-safe karaoke update in the controller only reads the current frame:
|
|
68
60
|
|
|
69
61
|
```js
|
|
70
62
|
function wordProgress(word, frame) {
|
|
@@ -74,7 +66,7 @@ function wordProgress(word, frame) {
|
|
|
74
66
|
}
|
|
75
67
|
```
|
|
76
68
|
|
|
77
|
-
Apply that value to text styling inside `update({ frame })`. Do not use timers, `requestAnimationFrame`, or transitions.
|
|
69
|
+
Apply that value to text styling inside the controller's `update({ frame })`. Do not use timers, `requestAnimationFrame`, or transitions. Declare the transcript with `<fy-transcript src="..." words>` and fetch the canonical JSON in `mount()`.
|
|
78
70
|
|
|
79
71
|
## Check and inspect
|
|
80
72
|
|
|
@@ -1,206 +0,0 @@
|
|
|
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
|
-
});
|