frameyard 0.3.0 → 0.5.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 (168) hide show
  1. package/CHANGELOG.md +117 -0
  2. package/GLOSSARY.md +46 -2
  3. package/README.md +174 -106
  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 +32 -21
  9. package/dist/cli.js.map +1 -1
  10. package/dist/composition/adapter.d.ts +30 -0
  11. package/dist/composition/adapter.d.ts.map +1 -0
  12. package/dist/composition/adapter.js +2 -0
  13. package/dist/composition/adapter.js.map +1 -0
  14. package/dist/composition/animation.d.ts +19 -0
  15. package/dist/composition/animation.d.ts.map +1 -0
  16. package/dist/composition/animation.js +55 -0
  17. package/dist/composition/animation.js.map +1 -0
  18. package/dist/composition/attributes.d.ts +7 -0
  19. package/dist/composition/attributes.d.ts.map +1 -0
  20. package/dist/composition/attributes.js +24 -0
  21. package/dist/composition/attributes.js.map +1 -0
  22. package/dist/composition/descriptor.d.ts +30 -0
  23. package/dist/composition/descriptor.d.ts.map +1 -0
  24. package/dist/composition/descriptor.js +188 -0
  25. package/dist/composition/descriptor.js.map +1 -0
  26. package/dist/composition/diagnostics.d.ts +12 -0
  27. package/dist/composition/diagnostics.d.ts.map +1 -0
  28. package/dist/composition/diagnostics.js +38 -0
  29. package/dist/composition/diagnostics.js.map +1 -0
  30. package/dist/composition/index.d.ts +12 -0
  31. package/dist/composition/index.d.ts.map +1 -0
  32. package/dist/composition/index.js +7 -0
  33. package/dist/composition/index.js.map +1 -0
  34. package/dist/composition/media.d.ts +27 -0
  35. package/dist/composition/media.d.ts.map +1 -0
  36. package/dist/composition/media.js +162 -0
  37. package/dist/composition/media.js.map +1 -0
  38. package/dist/composition/paths.d.ts +5 -0
  39. package/dist/composition/paths.d.ts.map +1 -0
  40. package/dist/composition/paths.js +16 -0
  41. package/dist/composition/paths.js.map +1 -0
  42. package/dist/composition/transcripts.d.ts +19 -0
  43. package/dist/composition/transcripts.d.ts.map +1 -0
  44. package/dist/composition/transcripts.js +57 -0
  45. package/dist/composition/transcripts.js.map +1 -0
  46. package/dist/composition/walk.d.ts +10 -0
  47. package/dist/composition/walk.d.ts.map +1 -0
  48. package/dist/composition/walk.js +32 -0
  49. package/dist/composition/walk.js.map +1 -0
  50. package/dist/contracts.d.ts +75 -47
  51. package/dist/contracts.d.ts.map +1 -1
  52. package/dist/contracts.js +14 -159
  53. package/dist/contracts.js.map +1 -1
  54. package/dist/errors.d.ts +15 -4
  55. package/dist/errors.d.ts.map +1 -1
  56. package/dist/errors.js +8 -0
  57. package/dist/errors.js.map +1 -1
  58. package/dist/html/document-adapter.d.ts +5 -0
  59. package/dist/html/document-adapter.d.ts.map +1 -0
  60. package/dist/html/document-adapter.js +17 -0
  61. package/dist/html/document-adapter.js.map +1 -0
  62. package/dist/html/index.d.ts +5 -0
  63. package/dist/html/index.d.ts.map +1 -0
  64. package/dist/html/index.js +3 -0
  65. package/dist/html/index.js.map +1 -0
  66. package/dist/html/parse.d.ts +26 -0
  67. package/dist/html/parse.d.ts.map +1 -0
  68. package/dist/html/parse.js +80 -0
  69. package/dist/html/parse.js.map +1 -0
  70. package/dist/html/validate.d.ts +14 -0
  71. package/dist/html/validate.d.ts.map +1 -0
  72. package/dist/html/validate.js +648 -0
  73. package/dist/html/validate.js.map +1 -0
  74. package/dist/index.d.ts +2 -2
  75. package/dist/index.d.ts.map +1 -1
  76. package/dist/index.js +1 -1
  77. package/dist/index.js.map +1 -1
  78. package/dist/preview/main.d.ts +10 -3
  79. package/dist/preview/main.d.ts.map +1 -1
  80. package/dist/preview/main.js +189 -89
  81. package/dist/preview/main.js.map +1 -1
  82. package/dist/project.d.ts +2 -0
  83. package/dist/project.d.ts.map +1 -1
  84. package/dist/project.js +4 -0
  85. package/dist/project.js.map +1 -1
  86. package/dist/renderer/browser.d.ts +11 -1
  87. package/dist/renderer/browser.d.ts.map +1 -1
  88. package/dist/renderer/browser.js +93 -14
  89. package/dist/renderer/browser.js.map +1 -1
  90. package/dist/renderer/index.d.ts +25 -5
  91. package/dist/renderer/index.d.ts.map +1 -1
  92. package/dist/renderer/index.js +82 -16
  93. package/dist/renderer/index.js.map +1 -1
  94. package/dist/runtime/bindings.d.ts +11 -0
  95. package/dist/runtime/bindings.d.ts.map +1 -0
  96. package/dist/runtime/bindings.js +164 -0
  97. package/dist/runtime/bindings.js.map +1 -0
  98. package/dist/runtime/diagnostics.d.ts +4 -0
  99. package/dist/runtime/diagnostics.d.ts.map +1 -0
  100. package/dist/runtime/diagnostics.js +107 -0
  101. package/dist/runtime/diagnostics.js.map +1 -0
  102. package/dist/runtime/document-adapter.d.ts +4 -0
  103. package/dist/runtime/document-adapter.d.ts.map +1 -0
  104. package/dist/runtime/document-adapter.js +19 -0
  105. package/dist/runtime/document-adapter.js.map +1 -0
  106. package/dist/runtime/host.d.ts +14 -0
  107. package/dist/runtime/host.d.ts.map +1 -0
  108. package/dist/runtime/host.js +596 -0
  109. package/dist/runtime/host.js.map +1 -0
  110. package/dist/runtime/index.d.ts +0 -29
  111. package/dist/runtime/index.d.ts.map +1 -1
  112. package/dist/runtime/index.js +0 -360
  113. package/dist/runtime/index.js.map +1 -1
  114. package/dist/runtime/media.d.ts.map +1 -1
  115. package/dist/runtime/media.js +20 -51
  116. package/dist/runtime/media.js.map +1 -1
  117. package/dist/scaffold.d.ts.map +1 -1
  118. package/dist/scaffold.js +177 -84
  119. package/dist/scaffold.js.map +1 -1
  120. package/dist/server/composition-html.d.ts +20 -0
  121. package/dist/server/composition-html.d.ts.map +1 -0
  122. package/dist/server/composition-html.js +45 -0
  123. package/dist/server/composition-html.js.map +1 -0
  124. package/dist/server/preview-html.d.ts.map +1 -1
  125. package/dist/server/preview-html.js +19 -14
  126. package/dist/server/preview-html.js.map +1 -1
  127. package/dist/server/server.d.ts.map +1 -1
  128. package/dist/server/server.js +94 -6
  129. package/dist/server/server.js.map +1 -1
  130. package/dist/version.d.ts +1 -1
  131. package/dist/version.js +1 -1
  132. package/docs/adr/0001-keep-composition-authoring-web-native.md +2 -0
  133. package/docs/adr/0006-seek-embedded-media-from-the-frame-clock.md +7 -1
  134. package/docs/adr/0007-make-html-documents-the-composition-unit.md +31 -0
  135. package/docs/adr/0008-share-the-composition-document-grammar.md +18 -0
  136. package/docs/adr/0009-composite-production-frames-in-software.md +37 -0
  137. package/docs/migration-0.4.md +279 -0
  138. package/docs/v0.1-design.md +78 -56
  139. package/examples/demo/README.md +8 -4
  140. package/examples/demo/compositions/demo.controller.js +120 -0
  141. package/examples/demo/compositions/demo.html +253 -0
  142. package/examples/demo/video.config.js +1 -1
  143. package/examples/video/README.md +61 -0
  144. package/examples/video/assets/clip.mp4 +0 -0
  145. package/examples/video/assets/clip.webm +0 -0
  146. package/examples/video/compositions/video-mp4.html +362 -0
  147. package/examples/video/compositions/video.html +362 -0
  148. package/examples/video/props/video.json +8 -0
  149. package/examples/video/scripts/make-clip.sh +109 -0
  150. package/examples/video/video.config.js +6 -0
  151. package/examples/webgl/README.md +30 -0
  152. package/examples/webgl/compositions/webgl.controller.js +200 -0
  153. package/examples/webgl/compositions/webgl.html +215 -0
  154. package/examples/webgl/props/webgl.json +6 -0
  155. package/examples/webgl/video.config.js +5 -0
  156. package/package.json +7 -3
  157. package/skills/frameyard/README.md +4 -0
  158. package/skills/frameyard/SKILL.md +40 -12
  159. package/skills/frameyard/references/audio-assets.md +25 -14
  160. package/skills/frameyard/references/bindings.md +73 -0
  161. package/skills/frameyard/references/common-mistakes.md +59 -0
  162. package/skills/frameyard/references/controllers.md +126 -0
  163. package/skills/frameyard/references/deterministic-authoring.md +143 -52
  164. package/skills/frameyard/references/diagnostics.md +90 -0
  165. package/skills/frameyard/references/failure-recovery.md +37 -14
  166. package/skills/frameyard/references/json-results.md +43 -9
  167. package/skills/frameyard/references/subtitles-karaoke.md +10 -18
  168. package/examples/demo/compositions/demo.js +0 -206
@@ -0,0 +1,253 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script type="application/json" data-fy="props">
6
+ {
7
+ "title": "Frameyard",
8
+ "subtitle": "Deterministic video, authored with the web platform.",
9
+ "accent": "#b8f36b"
10
+ }
11
+ </script>
12
+ <style>
13
+ @keyframes orbit {
14
+ from {
15
+ transform: rotate(0deg) translateX(210px) rotate(0deg);
16
+ }
17
+ to {
18
+ transform: rotate(360deg) translateX(210px) rotate(-360deg);
19
+ }
20
+ }
21
+ @keyframes breathe {
22
+ 0%,
23
+ 100% {
24
+ opacity: 0.35;
25
+ transform: scale(1);
26
+ }
27
+ 50% {
28
+ opacity: 0.7;
29
+ transform: scale(1.12);
30
+ }
31
+ }
32
+ @keyframes title-in {
33
+ from {
34
+ opacity: 0;
35
+ transform: translateY(46px);
36
+ }
37
+ to {
38
+ opacity: 1;
39
+ transform: translateY(0);
40
+ }
41
+ }
42
+ @keyframes subtitle-in {
43
+ from {
44
+ opacity: 0;
45
+ transform: translateY(26px);
46
+ }
47
+ to {
48
+ opacity: 1;
49
+ transform: translateY(0);
50
+ }
51
+ }
52
+ @keyframes scene-out {
53
+ from {
54
+ opacity: 1;
55
+ }
56
+ to {
57
+ opacity: 0;
58
+ }
59
+ }
60
+ html,
61
+ body,
62
+ fy-composition {
63
+ background: #080a0d;
64
+ }
65
+ #scene {
66
+ position: relative;
67
+ width: 100%;
68
+ height: 100%;
69
+ overflow: hidden;
70
+ background: #080a0d;
71
+ color: #eef2f7;
72
+ font-family: Inter, ui-sans-serif, system-ui, sans-serif;
73
+ animation: scene-out 1s linear both;
74
+ }
75
+ #field {
76
+ position: absolute;
77
+ inset: 0;
78
+ width: 100%;
79
+ height: 100%;
80
+ }
81
+ #orb {
82
+ position: absolute;
83
+ top: 46%;
84
+ left: 38%;
85
+ width: 460px;
86
+ height: 460px;
87
+ margin: -230px 0 0 -230px;
88
+ border-radius: 50%;
89
+ background: var(--fy-prop-accent);
90
+ filter: blur(110px);
91
+ opacity: 0.28;
92
+ animation: orbit 30s linear infinite;
93
+ }
94
+ #glow {
95
+ position: absolute;
96
+ top: 8%;
97
+ right: 6%;
98
+ width: 280px;
99
+ height: 280px;
100
+ border-radius: 50%;
101
+ background: var(--fy-prop-accent);
102
+ filter: blur(90px);
103
+ animation: breathe 9s ease-in-out infinite;
104
+ }
105
+ #rail {
106
+ position: absolute;
107
+ top: 92px;
108
+ left: 160px;
109
+ right: 160px;
110
+ height: 1px;
111
+ background: rgba(255, 255, 255, 0.14);
112
+ }
113
+ #eyebrow {
114
+ position: absolute;
115
+ top: 64px;
116
+ left: 160px;
117
+ font:
118
+ 600 14px/1 ui-monospace,
119
+ monospace;
120
+ letter-spacing: 0.28em;
121
+ text-transform: uppercase;
122
+ color: var(--fy-prop-accent);
123
+ }
124
+ #meta {
125
+ position: absolute;
126
+ top: 64px;
127
+ right: 160px;
128
+ font:
129
+ 500 14px/1 ui-monospace,
130
+ monospace;
131
+ letter-spacing: 0.12em;
132
+ color: #788595;
133
+ }
134
+ #copy {
135
+ position: absolute;
136
+ left: 160px;
137
+ top: 236px;
138
+ width: 1320px;
139
+ }
140
+ #title {
141
+ margin: 0;
142
+ font-size: 156px;
143
+ line-height: 0.92;
144
+ letter-spacing: -0.055em;
145
+ font-weight: 680;
146
+ opacity: 0;
147
+ animation: title-in 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
148
+ }
149
+ #subtitle {
150
+ margin: 34px 0 0;
151
+ max-width: 820px;
152
+ font-size: 31px;
153
+ line-height: 1.45;
154
+ color: #96a2b2;
155
+ font-weight: 420;
156
+ opacity: 0;
157
+ animation: subtitle-in 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
158
+ }
159
+ #signal {
160
+ position: absolute;
161
+ left: 0;
162
+ bottom: 0;
163
+ width: 1920px;
164
+ height: 1080px;
165
+ overflow: visible;
166
+ }
167
+ #caption {
168
+ position: absolute;
169
+ left: 160px;
170
+ bottom: 92px;
171
+ font:
172
+ 500 13px/1 ui-monospace,
173
+ monospace;
174
+ letter-spacing: 0.16em;
175
+ color: #7e8b9b;
176
+ }
177
+ #caption strong {
178
+ color: var(--fy-prop-accent);
179
+ font-weight: 600;
180
+ }
181
+ #progress-track {
182
+ position: absolute;
183
+ left: 160px;
184
+ right: 160px;
185
+ bottom: 72px;
186
+ height: 2px;
187
+ background: rgba(255, 255, 255, 0.1);
188
+ }
189
+ #progress {
190
+ position: absolute;
191
+ inset: 0 auto 0 0;
192
+ width: calc(var(--fy-progress) * 100%);
193
+ background: var(--fy-prop-accent);
194
+ box-shadow: 0 0 18px var(--fy-prop-accent);
195
+ }
196
+ #vignette {
197
+ position: absolute;
198
+ inset: 0;
199
+ background: radial-gradient(ellipse at center, transparent 48%, rgba(0, 0, 0, 0.42) 100%);
200
+ pointer-events: none;
201
+ }
202
+ </style>
203
+ </head>
204
+ <body>
205
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="900">
206
+ <div id="scene" data-fy-anim-start="824" data-fy-anim-duration="70" data-fy-anim-easing="linear">
207
+ <canvas id="field" width="1920" height="1080"></canvas>
208
+ <div id="orb"></div>
209
+ <div id="glow"></div>
210
+ <div id="vignette"></div>
211
+ <div id="rail"></div>
212
+ <div id="eyebrow">Frameyard / Field Notes</div>
213
+ <div id="meta">1920×1080 · 30 FPS · 30.00 S</div>
214
+ <div id="copy">
215
+ <h1
216
+ id="title"
217
+ data-fy-text="title"
218
+ data-fy-anim-start="0"
219
+ data-fy-anim-duration="52"
220
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
221
+ ></h1>
222
+ <p
223
+ id="subtitle"
224
+ data-fy-text="subtitle"
225
+ data-fy-anim-start="26"
226
+ data-fy-anim-duration="62"
227
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
228
+ ></p>
229
+ </div>
230
+ <svg id="signal" viewBox="0 0 1920 1080" fill="none" aria-hidden="true">
231
+ <path
232
+ id="signal-path"
233
+ stroke="var(--fy-prop-accent)"
234
+ stroke-width="3"
235
+ stroke-linecap="round"
236
+ stroke-linejoin="round"
237
+ opacity=".82"
238
+ ></path>
239
+ <g id="signal-bars"></g>
240
+ </svg>
241
+ <div id="caption">FRAME <strong id="frame">000</strong> / 899 · SEEK-SAFE CANVAS + SVG + CSS</div>
242
+ <div id="progress-track"><div id="progress"></div></div>
243
+ </div>
244
+
245
+ <audio src="assets/chime.wav" data-fy-start="18" data-fy-volume="0.5"></audio>
246
+ <audio src="assets/chime.wav" data-fy-start="300" data-fy-volume="0.38"></audio>
247
+ <audio src="assets/chime.wav" data-fy-start="600" data-fy-volume="0.42"></audio>
248
+ <audio src="assets/chime.wav" data-fy-start="840" data-fy-volume="0.55"></audio>
249
+ </fy-composition>
250
+
251
+ <script type="text/fy-controller" src="./demo.controller.js"></script>
252
+ </body>
253
+ </html>
@@ -1,5 +1,5 @@
1
1
  export default {
2
2
  compositions: {
3
- demo: "./compositions/demo.js",
3
+ demo: "./compositions/demo.html",
4
4
  },
5
5
  };
@@ -0,0 +1,61 @@
1
+ # Frameyard video example
2
+
3
+ A 12-second, 1920×1080 composition that treats a local clip as source footage
4
+ and seeks it twice. The project includes the same layout for two source codecs:
5
+
6
+ - `video` reads `assets/clip.webm` (VP9 + Opus)
7
+ - `video-mp4` reads `assets/clip.mp4` (H.264 + AAC)
8
+
9
+ Both compositions include:
10
+
11
+ - a full-bleed hero pass whose unmuted soundtrack joins the composition audio timeline
12
+ - a persistent detail pass with its own `data-fy-start`, `data-fy-trim`, and `data-fy-duration`
13
+ - `data-fy-audio="off"` on the detail pass so only the hero contributes audio
14
+ - a frame counter derived from `--fy-frame` in CSS, with static source-range labels
15
+ - frame-scheduled CSS typography and a frame-state progress bar
16
+ - a one-second fade to black over the final second of the composition
17
+
18
+ The hero pass uses the full 12-second source at volume `0.85`. The detail window
19
+ is visible for the whole composition: it holds the source at 5.8 seconds until
20
+ composition frame 120, then plays 5 seconds of the same file, and holds the final
21
+ source frame after frame 270. The result shows two source moments side by side.
22
+ The example is entirely declarative: the runtime owns loading, pausing, and
23
+ frame seeking, and CSS derives the frame counter from the frame state. No
24
+ controller is needed. The header, copy, and footer fade individually rather
25
+ than the whole scene; a full-scene opacity animation above a video can leave
26
+ the load frame on screen in interactive preview after a large seek.
27
+
28
+ The compositions live in `compositions/video.html` and
29
+ `compositions/video-mp4.html`. Both clips are 12-second 1280×720 files generated
30
+ procedurally for this example; the MP4 is transcoded from the WebM source.
31
+ Replace either asset with a local, seekable video file that is at least
32
+ 12 seconds long; remote URLs and live streams are not supported.
33
+
34
+ Regenerate both clips from scratch with the bundled FFmpeg script:
35
+
36
+ ```sh
37
+ bash examples/video/scripts/make-clip.sh
38
+ ```
39
+
40
+ The script writes both files into `assets/`; pass `--out-dir <directory>` to
41
+ write elsewhere. It requires FFmpeg with `libvpx-vp9`, `libx264`, Opus, and AAC
42
+ encoders, and fixed noise seeds keep the generated grain stable across runs on
43
+ the same FFmpeg build.
44
+
45
+ From the repository root:
46
+
47
+ ```sh
48
+ pnpm build
49
+ node dist/cli.js check video --project examples/video
50
+ node dist/cli.js check video-mp4 --project examples/video
51
+ node dist/cli.js still video --frame 210 --project examples/video --out video-still.png
52
+ node dist/cli.js still video-mp4 --frame 210 --project examples/video --out video-mp4-still.png
53
+ node dist/cli.js render video --project examples/video --props examples/video/props/video.json
54
+ node dist/cli.js render video-mp4 --project examples/video --props examples/video/props/video.json
55
+ ```
56
+
57
+ Start the interactive preview with:
58
+
59
+ ```sh
60
+ node dist/cli.js dev --project examples/video
61
+ ```
Binary file
Binary file
@@ -0,0 +1,362 @@
1
+ <!doctype html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <script type="application/json" data-fy="props">
6
+ {
7
+ "kicker": "Frameyard / Clip Study 001",
8
+ "title": "Moving",
9
+ "titleAccent": "Image",
10
+ "subtitle": "A local clip, loaded once and seeked twice from the composition clock.",
11
+ "detail": "Detail / second pass",
12
+ "accent": "#ffc37a"
13
+ }
14
+ </script>
15
+ <style>
16
+ @keyframes fade-in {
17
+ from {
18
+ opacity: 0;
19
+ }
20
+ to {
21
+ opacity: 1;
22
+ }
23
+ }
24
+ @keyframes rise {
25
+ from {
26
+ opacity: 0;
27
+ transform: translateY(42px);
28
+ }
29
+ to {
30
+ opacity: 1;
31
+ transform: translateY(0);
32
+ }
33
+ }
34
+ @keyframes draw {
35
+ from {
36
+ opacity: 0;
37
+ transform: scaleX(0);
38
+ }
39
+ to {
40
+ opacity: 1;
41
+ transform: scaleX(1);
42
+ }
43
+ }
44
+ @keyframes outro-fade {
45
+ from {
46
+ opacity: 0;
47
+ }
48
+ to {
49
+ opacity: 1;
50
+ }
51
+ }
52
+ html,
53
+ body,
54
+ fy-composition {
55
+ background: #04080b;
56
+ }
57
+ #scene {
58
+ position: relative;
59
+ width: 100%;
60
+ height: 100%;
61
+ overflow: hidden;
62
+ isolation: isolate;
63
+ background: #04080b;
64
+ color: #eef4f1;
65
+ font-family: Inter, "Helvetica Neue", ui-sans-serif, system-ui, sans-serif;
66
+ }
67
+ #hero {
68
+ position: absolute;
69
+ inset: 0;
70
+ display: block;
71
+ width: 100%;
72
+ height: 100%;
73
+ object-fit: cover;
74
+ transform: scale(calc(1 + var(--fy-progress) * 0.035));
75
+ transform-origin: center;
76
+ }
77
+ #scrim {
78
+ position: absolute;
79
+ inset: 0;
80
+ background:
81
+ linear-gradient(
82
+ 90deg,
83
+ rgba(4, 8, 11, 0.94) 0%,
84
+ rgba(4, 8, 11, 0.8) 24%,
85
+ rgba(4, 8, 11, 0.24) 52%,
86
+ rgba(4, 8, 11, 0) 74%
87
+ ),
88
+ linear-gradient(0deg, rgba(4, 8, 11, 0.9) 0%, rgba(4, 8, 11, 0) 34%);
89
+ }
90
+ #vignette {
91
+ position: absolute;
92
+ inset: 0;
93
+ background: radial-gradient(ellipse at 62% 44%, transparent 42%, rgba(0, 0, 0, 0.3) 100%);
94
+ pointer-events: none;
95
+ }
96
+ #frame-border {
97
+ position: absolute;
98
+ inset: 36px;
99
+ border: 1px solid rgba(238, 244, 241, 0.16);
100
+ pointer-events: none;
101
+ }
102
+ #header {
103
+ position: absolute;
104
+ top: 80px;
105
+ left: 148px;
106
+ right: 148px;
107
+ display: flex;
108
+ justify-content: space-between;
109
+ align-items: center;
110
+ gap: 32px;
111
+ animation: fade-in 1s linear both;
112
+ }
113
+ #kicker {
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 15px;
117
+ font:
118
+ 600 14px/1 ui-monospace,
119
+ monospace;
120
+ letter-spacing: 0.24em;
121
+ text-transform: uppercase;
122
+ color: var(--fy-prop-accent);
123
+ }
124
+ #kicker::before {
125
+ content: "";
126
+ width: 8px;
127
+ height: 8px;
128
+ background: var(--fy-prop-accent);
129
+ box-shadow: 0 0 16px var(--fy-prop-accent);
130
+ }
131
+ #format {
132
+ font:
133
+ 500 12px/1 ui-monospace,
134
+ monospace;
135
+ letter-spacing: 0.16em;
136
+ text-transform: uppercase;
137
+ color: #8b999c;
138
+ }
139
+ #copy {
140
+ position: absolute;
141
+ top: 278px;
142
+ left: 148px;
143
+ width: 860px;
144
+ }
145
+ #title {
146
+ margin: 0;
147
+ font-size: 174px;
148
+ line-height: 0.84;
149
+ letter-spacing: -0.064em;
150
+ font-weight: 660;
151
+ }
152
+ #title span,
153
+ #title em {
154
+ display: block;
155
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
156
+ }
157
+ #title em {
158
+ margin-top: 0.06em;
159
+ font-family: "Iowan Old Style", "Palatino Linotype", Georgia, ui-serif, serif;
160
+ font-style: italic;
161
+ font-weight: 400;
162
+ letter-spacing: -0.035em;
163
+ color: var(--fy-prop-accent);
164
+ }
165
+ #subtitle {
166
+ width: 720px;
167
+ margin: 38px 0 0;
168
+ font-size: 29px;
169
+ line-height: 1.5;
170
+ font-weight: 420;
171
+ color: #a6b3b6;
172
+ animation: rise 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
173
+ }
174
+ #footer {
175
+ position: absolute;
176
+ left: 148px;
177
+ right: 148px;
178
+ bottom: 70px;
179
+ contain: layout paint style;
180
+ animation: fade-in 1s linear both;
181
+ }
182
+ #rule {
183
+ position: relative;
184
+ height: 1px;
185
+ background: rgba(238, 244, 241, 0.16);
186
+ transform-origin: left center;
187
+ animation: draw 1s cubic-bezier(0.215, 0.61, 0.355, 1) both;
188
+ }
189
+ #progress {
190
+ position: absolute;
191
+ inset: 0 auto 0 0;
192
+ width: calc(var(--fy-progress) * 100%);
193
+ background: var(--fy-prop-accent);
194
+ box-shadow: 0 0 14px var(--fy-prop-accent);
195
+ }
196
+ #footer-row {
197
+ display: flex;
198
+ justify-content: space-between;
199
+ align-items: baseline;
200
+ gap: 24px;
201
+ margin-top: 18px;
202
+ font:
203
+ 500 12px/1 ui-monospace,
204
+ monospace;
205
+ letter-spacing: 0.14em;
206
+ color: #879598;
207
+ text-transform: uppercase;
208
+ }
209
+ #footer-row strong {
210
+ color: #edf5f2;
211
+ font-weight: 600;
212
+ }
213
+ #footer-row .accent {
214
+ color: var(--fy-prop-accent);
215
+ }
216
+ #frame-counter {
217
+ display: inline-block;
218
+ min-width: 3ch;
219
+ text-align: right;
220
+ }
221
+ #frame-counter::after {
222
+ counter-reset: fy-frame var(--fy-frame);
223
+ content: counter(fy-frame);
224
+ }
225
+ #detail {
226
+ position: absolute;
227
+ right: 148px;
228
+ bottom: 154px;
229
+ width: 440px;
230
+ }
231
+ #detail-bar {
232
+ display: flex;
233
+ justify-content: space-between;
234
+ align-items: baseline;
235
+ gap: 24px;
236
+ margin-bottom: 10px;
237
+ contain: layout paint style;
238
+ font:
239
+ 600 11px/1 ui-monospace,
240
+ monospace;
241
+ letter-spacing: 0.18em;
242
+ text-transform: uppercase;
243
+ }
244
+ #detail-bar > span:first-child {
245
+ color: var(--fy-prop-accent);
246
+ }
247
+ #detail-time {
248
+ color: #98a5a6;
249
+ font-weight: 500;
250
+ }
251
+ #detail-window {
252
+ position: relative;
253
+ width: 440px;
254
+ height: 248px;
255
+ overflow: hidden;
256
+ border: 1px solid rgba(255, 195, 122, 0.65);
257
+ background: #000;
258
+ box-shadow: 0 24px 90px rgba(0, 0, 0, 0.55);
259
+ }
260
+ #detail-clip {
261
+ display: block;
262
+ width: 100%;
263
+ height: 100%;
264
+ object-fit: cover;
265
+ object-position: 68% 50%;
266
+ transform: scale(1.3);
267
+ }
268
+ #detail-tint {
269
+ position: absolute;
270
+ inset: 0;
271
+ background: linear-gradient(0deg, rgba(4, 8, 11, 0.28) 0%, rgba(4, 8, 11, 0) 45%);
272
+ pointer-events: none;
273
+ }
274
+ #outro-fade {
275
+ position: absolute;
276
+ inset: 0;
277
+ background: #04080b;
278
+ pointer-events: none;
279
+ opacity: 0;
280
+ animation: outro-fade 1s linear both;
281
+ }
282
+ </style>
283
+ </head>
284
+ <body>
285
+ <fy-composition width="1920" height="1080" fps="30" duration-in-frames="360">
286
+ <div id="scene">
287
+ <video
288
+ id="hero"
289
+ src="assets/clip.mp4"
290
+ data-fy-start="0"
291
+ data-fy-trim="0"
292
+ data-fy-duration="12"
293
+ data-fy-volume="0.85"
294
+ ></video>
295
+ <div id="scrim"></div>
296
+ <div id="vignette"></div>
297
+ <div id="frame-border"></div>
298
+
299
+ <header id="header" data-fy-anim-start="8" data-fy-anim-duration="28">
300
+ <div id="kicker" data-fy-text="kicker"></div>
301
+ <div id="format">Embedded video / H.264 + AAC / 30 FPS</div>
302
+ </header>
303
+
304
+ <div id="copy">
305
+ <h1 id="title">
306
+ <span
307
+ data-fy-text="title"
308
+ data-fy-anim-start="14"
309
+ data-fy-anim-duration="44"
310
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
311
+ ></span>
312
+ <em
313
+ data-fy-text="titleAccent"
314
+ data-fy-anim-start="26"
315
+ data-fy-anim-duration="44"
316
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
317
+ ></em>
318
+ </h1>
319
+ <p
320
+ id="subtitle"
321
+ data-fy-text="subtitle"
322
+ data-fy-anim-start="40"
323
+ data-fy-anim-duration="40"
324
+ data-fy-anim-easing="cubic-bezier(0.215, 0.61, 0.355, 1)"
325
+ ></p>
326
+ </div>
327
+
328
+ <div id="detail">
329
+ <div id="detail-bar">
330
+ <span data-fy-text="detail"></span>
331
+ <span id="detail-time">SRC 5.8–10.8 S</span>
332
+ </div>
333
+ <div id="detail-window">
334
+ <video
335
+ id="detail-clip"
336
+ src="assets/clip.mp4"
337
+ data-fy-start="120"
338
+ data-fy-trim="5.8"
339
+ data-fy-duration="5"
340
+ data-fy-audio="off"
341
+ ></video>
342
+ <div id="detail-tint"></div>
343
+ </div>
344
+ </div>
345
+
346
+ <div id="footer" data-fy-anim-start="46" data-fy-anim-duration="30">
347
+ <div id="rule" data-fy-anim-start="52" data-fy-anim-duration="50">
348
+ <div id="progress"></div>
349
+ </div>
350
+ <div id="footer-row">
351
+ <span>Hero / SRC <strong>0.0–12.0 S</strong></span>
352
+ <span>Frame <strong id="frame-counter"></strong> / 359</span>
353
+ <span class="accent">Frame-seeked</span>
354
+ <span>12.00 S · 30 FPS</span>
355
+ </div>
356
+ </div>
357
+
358
+ <div id="outro-fade" data-fy-anim-start="329" data-fy-anim-duration="30"></div>
359
+ </div>
360
+ </fy-composition>
361
+ </body>
362
+ </html>