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.
Files changed (112) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/GLOSSARY.md +58 -2
  3. package/README.md +185 -95
  4. package/dist/bindings.d.ts +42 -0
  5. package/dist/bindings.d.ts.map +1 -0
  6. package/dist/bindings.js +144 -0
  7. package/dist/bindings.js.map +1 -0
  8. package/dist/cli.js +37 -14
  9. package/dist/cli.js.map +1 -1
  10. package/dist/contracts.d.ts +73 -47
  11. package/dist/contracts.d.ts.map +1 -1
  12. package/dist/contracts.js +14 -159
  13. package/dist/contracts.js.map +1 -1
  14. package/dist/errors.d.ts +15 -4
  15. package/dist/errors.d.ts.map +1 -1
  16. package/dist/errors.js +8 -0
  17. package/dist/errors.js.map +1 -1
  18. package/dist/html/index.d.ts +5 -0
  19. package/dist/html/index.d.ts.map +1 -0
  20. package/dist/html/index.js +3 -0
  21. package/dist/html/index.js.map +1 -0
  22. package/dist/html/parse.d.ts +30 -0
  23. package/dist/html/parse.d.ts.map +1 -0
  24. package/dist/html/parse.js +80 -0
  25. package/dist/html/parse.js.map +1 -0
  26. package/dist/html/validate.d.ts +14 -0
  27. package/dist/html/validate.d.ts.map +1 -0
  28. package/dist/html/validate.js +931 -0
  29. package/dist/html/validate.js.map +1 -0
  30. package/dist/index.d.ts +2 -2
  31. package/dist/index.d.ts.map +1 -1
  32. package/dist/index.js +1 -1
  33. package/dist/index.js.map +1 -1
  34. package/dist/preview/main.d.ts +12 -3
  35. package/dist/preview/main.d.ts.map +1 -1
  36. package/dist/preview/main.js +208 -85
  37. package/dist/preview/main.js.map +1 -1
  38. package/dist/project.d.ts +2 -0
  39. package/dist/project.d.ts.map +1 -1
  40. package/dist/project.js +4 -0
  41. package/dist/project.js.map +1 -1
  42. package/dist/renderer/audio.d.ts +5 -1
  43. package/dist/renderer/audio.d.ts.map +1 -1
  44. package/dist/renderer/audio.js +4 -0
  45. package/dist/renderer/audio.js.map +1 -1
  46. package/dist/renderer/browser.d.ts +12 -1
  47. package/dist/renderer/browser.d.ts.map +1 -1
  48. package/dist/renderer/browser.js +69 -11
  49. package/dist/renderer/browser.js.map +1 -1
  50. package/dist/renderer/index.d.ts +30 -5
  51. package/dist/renderer/index.d.ts.map +1 -1
  52. package/dist/renderer/index.js +204 -26
  53. package/dist/renderer/index.js.map +1 -1
  54. package/dist/runtime/bindings.d.ts +11 -0
  55. package/dist/runtime/bindings.d.ts.map +1 -0
  56. package/dist/runtime/bindings.js +164 -0
  57. package/dist/runtime/bindings.js.map +1 -0
  58. package/dist/runtime/diagnostics.d.ts +4 -0
  59. package/dist/runtime/diagnostics.d.ts.map +1 -0
  60. package/dist/runtime/diagnostics.js +107 -0
  61. package/dist/runtime/diagnostics.js.map +1 -0
  62. package/dist/runtime/host.d.ts +12 -0
  63. package/dist/runtime/host.d.ts.map +1 -0
  64. package/dist/runtime/host.js +701 -0
  65. package/dist/runtime/host.js.map +1 -0
  66. package/dist/runtime/index.d.ts +0 -26
  67. package/dist/runtime/index.d.ts.map +1 -1
  68. package/dist/runtime/index.js +0 -286
  69. package/dist/runtime/index.js.map +1 -1
  70. package/dist/runtime/media.d.ts +16 -0
  71. package/dist/runtime/media.d.ts.map +1 -0
  72. package/dist/runtime/media.js +307 -0
  73. package/dist/runtime/media.js.map +1 -0
  74. package/dist/scaffold.d.ts.map +1 -1
  75. package/dist/scaffold.js +169 -78
  76. package/dist/scaffold.js.map +1 -1
  77. package/dist/server/composition-html.d.ts +20 -0
  78. package/dist/server/composition-html.d.ts.map +1 -0
  79. package/dist/server/composition-html.js +45 -0
  80. package/dist/server/composition-html.js.map +1 -0
  81. package/dist/server/preview-html.d.ts.map +1 -1
  82. package/dist/server/preview-html.js +19 -14
  83. package/dist/server/preview-html.js.map +1 -1
  84. package/dist/server/server.d.ts.map +1 -1
  85. package/dist/server/server.js +94 -6
  86. package/dist/server/server.js.map +1 -1
  87. package/dist/version.d.ts +1 -1
  88. package/dist/version.js +1 -1
  89. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  90. package/docs/adr/0003-render-browser-frames-offline.md +2 -0
  91. package/docs/adr/0005-keep-parallel-rendering-opt-in-and-frame-ordered.md +3 -0
  92. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +11 -0
  93. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  94. package/docs/migration-0.4.md +279 -0
  95. package/docs/v0.1-design.md +82 -57
  96. package/examples/demo/README.md +7 -3
  97. package/examples/demo/compositions/demo.controller.js +120 -0
  98. package/examples/demo/compositions/demo.html +253 -0
  99. package/examples/demo/video.config.js +1 -1
  100. package/package.json +9 -3
  101. package/skills/frameyard/README.md +4 -0
  102. package/skills/frameyard/SKILL.md +41 -11
  103. package/skills/frameyard/references/audio-assets.md +25 -14
  104. package/skills/frameyard/references/bindings.md +73 -0
  105. package/skills/frameyard/references/common-mistakes.md +59 -0
  106. package/skills/frameyard/references/controllers.md +126 -0
  107. package/skills/frameyard/references/deterministic-authoring.md +160 -41
  108. package/skills/frameyard/references/diagnostics.md +90 -0
  109. package/skills/frameyard/references/failure-recovery.md +38 -12
  110. package/skills/frameyard/references/json-results.md +43 -8
  111. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  112. 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.1 contract.
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
- 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`.
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, "warnings": [] },
97
- "transcripts": { "clips": 1, "warnings": [] },
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. Compositions render timed text themselves. Transcription produces a canonical transcript; SRT and VTT are derived exports.
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
- ```js
37
- transcripts: [
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 are project-relative. `check`, `still`, and `render` validate declared transcript files. Preview loads the declarations but leaves visual presentation to the composition.
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 clip:
44
+ Given an `<audio>` element:
47
45
 
48
- ```js
49
- const clip = {
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
- });