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
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { random } from "frameyard/runtime";
|
|
2
|
+
|
|
3
|
+
const STAR_COUNT = 130;
|
|
4
|
+
const BAR_COUNT = 52;
|
|
5
|
+
const BASELINE = 858;
|
|
6
|
+
|
|
7
|
+
let context;
|
|
8
|
+
let signalPath;
|
|
9
|
+
let signalBars;
|
|
10
|
+
let frameLabel;
|
|
11
|
+
let stars;
|
|
12
|
+
let bars = [];
|
|
13
|
+
let signalLength = 0;
|
|
14
|
+
|
|
15
|
+
function clamp(value) {
|
|
16
|
+
return Math.max(0, Math.min(1, value));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function easeOut(value) {
|
|
20
|
+
const t = clamp(value);
|
|
21
|
+
return 1 - Math.pow(1 - t, 3);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function makeStars() {
|
|
25
|
+
stars = Array.from({ length: STAR_COUNT }, (_, index) => ({
|
|
26
|
+
x: random("star:" + index + ":x"),
|
|
27
|
+
y: random("star:" + index + ":y"),
|
|
28
|
+
radius: 0.6 + random("star:" + index + ":r") * 1.8,
|
|
29
|
+
speed: 6 + random("star:" + index + ":speed") * 34,
|
|
30
|
+
alpha: 0.12 + random("star:" + index + ":alpha") * 0.65,
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildBars(root) {
|
|
35
|
+
signalBars = root.querySelector("#signal-bars");
|
|
36
|
+
bars = [];
|
|
37
|
+
|
|
38
|
+
for (let index = 0; index < BAR_COUNT; index += 1) {
|
|
39
|
+
const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
|
|
40
|
+
const height = 24 + random("bar:" + index + ":height") * 176;
|
|
41
|
+
const x = 168 + index * 31;
|
|
42
|
+
rect.setAttribute("x", String(x));
|
|
43
|
+
rect.setAttribute("width", "15");
|
|
44
|
+
rect.setAttribute("rx", "7.5");
|
|
45
|
+
rect.setAttribute("fill", "var(--fy-prop-accent)");
|
|
46
|
+
rect.setAttribute("height", "0");
|
|
47
|
+
rect.setAttribute("y", String(BASELINE));
|
|
48
|
+
rect.setAttribute("opacity", "0");
|
|
49
|
+
signalBars.append(rect);
|
|
50
|
+
bars.push({ element: rect, height });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function buildSignalPath(root) {
|
|
55
|
+
signalPath = root.querySelector("#signal-path");
|
|
56
|
+
const points = [];
|
|
57
|
+
for (let index = 0; index <= 120; index += 1) {
|
|
58
|
+
const x = 160 + (index / 120) * 1600;
|
|
59
|
+
const wave = Math.sin(index / 7) * 38 + Math.sin(index / 17 + 1.4) * 22 + (random("signal:" + index) - 0.5) * 12;
|
|
60
|
+
points.push([x, 535 + wave]);
|
|
61
|
+
}
|
|
62
|
+
const d = points.map(([x, y], index) => (index === 0 ? "M" : "L") + x + " " + y).join(" ");
|
|
63
|
+
signalPath.setAttribute("d", d);
|
|
64
|
+
signalLength = signalPath.getTotalLength();
|
|
65
|
+
signalPath.style.strokeDasharray = String(signalLength);
|
|
66
|
+
signalPath.style.strokeDashoffset = String(signalLength);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export default {
|
|
70
|
+
mount({ root }) {
|
|
71
|
+
context = root.querySelector("#field").getContext("2d");
|
|
72
|
+
frameLabel = root.querySelector("#frame");
|
|
73
|
+
makeStars();
|
|
74
|
+
buildBars(root);
|
|
75
|
+
buildSignalPath(root);
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
update({ frame }) {
|
|
79
|
+
const intro = easeOut(frame / 52);
|
|
80
|
+
const signalReveal = easeOut((frame - 96) / 260);
|
|
81
|
+
const width = context.canvas.width;
|
|
82
|
+
const height = context.canvas.height;
|
|
83
|
+
|
|
84
|
+
context.clearRect(0, 0, width, height);
|
|
85
|
+
for (const star of stars) {
|
|
86
|
+
const drift = (frame * star.speed) % width;
|
|
87
|
+
const x = (star.x * width + drift) % width;
|
|
88
|
+
const y = star.y * height;
|
|
89
|
+
context.beginPath();
|
|
90
|
+
context.fillStyle = "rgba(214, 236, 255, " + star.alpha * (0.45 + 0.55 * intro) + ")";
|
|
91
|
+
context.arc(x, y, star.radius, 0, Math.PI * 2);
|
|
92
|
+
context.fill();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
signalPath.style.strokeDashoffset = String(signalLength * (1 - signalReveal));
|
|
96
|
+
signalPath.style.opacity = String(0.18 + signalReveal * 0.64);
|
|
97
|
+
|
|
98
|
+
const barReveal = clamp((frame - 120) / 240);
|
|
99
|
+
for (let index = 0; index < bars.length; index += 1) {
|
|
100
|
+
const bar = bars[index];
|
|
101
|
+
const local = clamp(barReveal * BAR_COUNT - index);
|
|
102
|
+
const height = bar.height * local;
|
|
103
|
+
bar.element.setAttribute("height", height.toFixed(2));
|
|
104
|
+
bar.element.setAttribute("y", String(BASELINE - height));
|
|
105
|
+
bar.element.setAttribute("opacity", (0.25 + local * 0.62).toFixed(3));
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
frameLabel.textContent = String(frame).padStart(3, "0");
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
dispose() {
|
|
112
|
+
context = undefined;
|
|
113
|
+
signalPath = undefined;
|
|
114
|
+
signalBars = undefined;
|
|
115
|
+
frameLabel = undefined;
|
|
116
|
+
stars = undefined;
|
|
117
|
+
bars = [];
|
|
118
|
+
signalLength = 0;
|
|
119
|
+
},
|
|
120
|
+
};
|
|
@@ -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>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "frameyard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Local-first, deterministic video composition framework for HTML, CSS, and JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -52,6 +52,7 @@
|
|
|
52
52
|
"node": ">=22.12"
|
|
53
53
|
},
|
|
54
54
|
"dependencies": {
|
|
55
|
+
"parse5": "^8.0.1",
|
|
55
56
|
"playwright": "1.64.1"
|
|
56
57
|
},
|
|
57
58
|
"devDependencies": {
|
|
@@ -65,12 +66,17 @@
|
|
|
65
66
|
"build": "tsc -p tsconfig.json",
|
|
66
67
|
"typecheck": "tsc -p tsconfig.json --noEmit",
|
|
67
68
|
"typecheck:test": "tsc -p tsconfig.test.json",
|
|
69
|
+
"check:scripts": "node scripts/check-scripts.mjs",
|
|
68
70
|
"lint": "oxlint",
|
|
69
71
|
"format": "oxfmt",
|
|
70
72
|
"format:check": "oxfmt --check",
|
|
73
|
+
"check:static": "pnpm check:scripts && pnpm lint && pnpm format:check && pnpm typecheck && pnpm typecheck:test",
|
|
74
|
+
"check": "pnpm check:static && pnpm test",
|
|
71
75
|
"test": "pnpm build && vitest run",
|
|
72
|
-
"test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/scaffold.test.ts test/transcript.test.ts",
|
|
76
|
+
"test:unit": "vitest run test/contracts.test.ts test/docs.test.ts test/random.test.ts test/release.test.ts test/scaffold.test.ts test/transcript.test.ts",
|
|
73
77
|
"bench": "pnpm build && node scripts/bench-render.mjs",
|
|
74
|
-
"set-version": "node scripts/set-version.mjs"
|
|
78
|
+
"set-version": "node scripts/set-version.mjs",
|
|
79
|
+
"verify-release": "node scripts/verify-release.mjs",
|
|
80
|
+
"retag-release": "node scripts/retag-release.mjs"
|
|
75
81
|
}
|
|
76
82
|
}
|
|
@@ -24,6 +24,10 @@ Run the CLI with `npx frameyard`. It resolves the latest published version; pin
|
|
|
24
24
|
|
|
25
25
|
- `SKILL.md`: trigger description and operating workflow
|
|
26
26
|
- `references/deterministic-authoring.md`
|
|
27
|
+
- `references/bindings.md`
|
|
28
|
+
- `references/controllers.md`
|
|
29
|
+
- `references/diagnostics.md`
|
|
30
|
+
- `references/common-mistakes.md`
|
|
27
31
|
- `references/audio-assets.md`
|
|
28
32
|
- `references/subtitles-karaoke.md`
|
|
29
33
|
- `references/json-results.md`
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: frameyard
|
|
3
|
-
description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML, CSS, and
|
|
4
|
-
frameyard-api-version: 0.
|
|
3
|
+
description: Create, validate, inspect, and render deterministic Frameyard videos authored with HTML, CSS, and an optional JavaScript controller. Use when a user asks to build or operate a Frameyard project, produce parameterized video, add a local TTS or transcription asset through Frameyard's wrappers, or diagnose a Frameyard render. Do not use for general-purpose media generation outside a Frameyard project.
|
|
4
|
+
frameyard-api-version: 0.4.0
|
|
5
5
|
---
|
|
6
6
|
|
|
7
7
|
# Frameyard
|
|
@@ -11,19 +11,19 @@ Use Frameyard's public project format and CLI. Do not introduce an agent-only ru
|
|
|
11
11
|
## First decisions
|
|
12
12
|
|
|
13
13
|
1. Find the project: look for `video.config.js` from the working directory upward, or run `npx frameyard init <directory>` to scaffold one.
|
|
14
|
-
2. Read the composition entry points listed in `video.config.js`.
|
|
15
|
-
3. Read the
|
|
14
|
+
2. Read the composition entry points listed in `video.config.js`. They are `.html` documents; there is no directory scanning.
|
|
15
|
+
3. Read the composition HTML, its optional controller, and the props file before changing behavior.
|
|
16
16
|
4. If the task needs speech or transcription, confirm that inference is requested or explicitly approved before producing assets.
|
|
17
17
|
|
|
18
18
|
## Workflow
|
|
19
19
|
|
|
20
|
-
1. **Author** the composition with
|
|
21
|
-
2. **Provide props** as JSON. Validate domain-specific requirements with `validateProps()`.
|
|
20
|
+
1. **Author** the composition as a full HTML document with one `<fy-composition>` root. Put metadata, default props, markup, CSS animation, bindings, audio, and transcripts in the document. Use an optional controller for Canvas 2D, WebGL, karaoke, or per-frame computation CSS cannot express. Read [deterministic authoring](references/deterministic-authoring.md), [bindings](references/bindings.md), and [controllers](references/controllers.md).
|
|
21
|
+
2. **Provide props** as JSON. Validate domain-specific requirements with a controller's `validateProps()`.
|
|
22
22
|
3. **Produce optional assets** with `npx frameyard tts` and `npx frameyard transcribe`, never with the upstream executables. Read [audio assets](references/audio-assets.md). Do not overwrite or regenerate existing generated assets without permission.
|
|
23
|
-
4. **Validate** with `npx frameyard check <id> --json`.
|
|
23
|
+
4. **Validate** with `npx frameyard check <id> --json`. Static validation runs before Chromium; then every frame is evaluated. Read the structured `diagnostics` array. Read [diagnostics](references/diagnostics.md) and [common mistakes](references/common-mistakes.md).
|
|
24
24
|
5. **Inspect** representative frames with `npx frameyard still <id> --frame <n> --out <file.png> --json`. View the PNGs when the host supports image viewing. If visual inspection is impossible, say so explicitly.
|
|
25
|
-
6. **Render** with `npx frameyard render <id> --out <file.mp4> [--props <file>] --json`.
|
|
26
|
-
7. **Report** the project path, composition entry, props, generated assets, provenance manifest, validation results, still paths and inspection status, final video path, and any failures.
|
|
25
|
+
6. **Render** with `npx frameyard render <id> --out <file.mp4> [--props <file>] [--concurrency <n>] --json`.
|
|
26
|
+
7. **Report** the project path, composition entry, controller, props, generated assets, provenance manifest, validation results, diagnostics, still paths and inspection status, final video path, and any failures.
|
|
27
27
|
|
|
28
28
|
Never claim success when `check` fails, a still is missing, a render exits nonzero, or visual inspection was skipped. A partial or unpublished render is not a successful result.
|
|
29
29
|
|
|
@@ -32,21 +32,39 @@ Never claim success when `check` fails, a still is missing, a render exits nonze
|
|
|
32
32
|
- Use `--json` for every non-interactive command. Parse stdout as the structured result; treat stderr as progress and diagnostics.
|
|
33
33
|
- Keep the current output files unless the user explicitly approves replacement. Use `--force` only with that approval.
|
|
34
34
|
- Use `--project <dir>` when not running from the project root.
|
|
35
|
+
- Use `--concurrency <n>` only when faster local rendering is useful; the default is `1` and the maximum is `16`. It changes capture scheduling only, not composition behavior or output frame order.
|
|
35
36
|
- Use `npx frameyard doctor --json` before reporting an environment problem. It inspects without repairing or downloading.
|
|
36
37
|
- Read [JSON results](references/json-results.md) for result and error shapes, and [failure recovery](references/failure-recovery.md) for actionable error codes.
|
|
37
38
|
|
|
39
|
+
## Authoring rules
|
|
40
|
+
|
|
41
|
+
- A composition is a full HTML document with one `<fy-composition width height fps duration-in-frames>` root and a `data-fy="props"` default-props block.
|
|
42
|
+
- Bindings are explicit: `data-fy-text`, `data-fy-attr-*`, `data-fy-class-*`, `data-fy-if`, and `data-fy-repeat` on `<template>`. Primitive top-level props are also `--fy-prop-<name>` CSS variables.
|
|
43
|
+
- CSS `@keyframes` and the Web Animations API are seeked from the frame clock. `data-fy-anim-start`, `data-fy-anim-duration`, and `data-fy-anim-easing` schedule one animation per element in frames.
|
|
44
|
+
- Frame-state variables on `:root`: `--fy-frame`, `--fy-time`, `--fy-progress`, `--fy-fps`, `--fy-duration`, and `--fy-duration-frames`.
|
|
45
|
+
- A composition has exactly one optional controller, inline or external. It may export `mount`, `update`, `dispose`, and `validateProps`; at least one is required. Use inline for small controllers; extract to an external module for Canvas 2D, WebGL, karaoke, reuse, tests, or a large controller.
|
|
46
|
+
- The controller is the only executable code. Other executable scripts and `on*` handlers are rejected.
|
|
47
|
+
- Media and transcript `src` values resolve from the project root. The controller `src` and ordinary HTML asset URLs resolve relative to the composition document.
|
|
48
|
+
- Embedded video/audio use `data-fy-start` (frames), `data-fy-trim` (seconds), optional `data-fy-duration` (seconds), and `data-fy-volume` (0-1). Transcripts use `<fy-transcript src words>`.
|
|
49
|
+
|
|
38
50
|
## Determinism guardrails
|
|
39
51
|
|
|
40
52
|
- A frame may depend only on its integer `frame`, immutable `props`, and assets loaded before frame production.
|
|
41
53
|
- Use `random("stable:key")` from `frameyard/runtime`; never use `Math.random()`.
|
|
42
54
|
- Never use `Date.now()`, `performance.now()`, timers, independent `requestAnimationFrame` loops, or CSS transitions as animation inputs.
|
|
43
|
-
- Finish remote data, fonts, and image loading during
|
|
55
|
+
- Finish remote data, fonts, and image loading during the loading phase. Network requests are blocked during frame capture.
|
|
44
56
|
- Keep `update({ frame, props })` synchronous and seek-safe. Do not carry state between frames.
|
|
57
|
+
- Let the runtime seek embedded `<video>` elements. Do not play, pause, or set `currentTime` in `update()`.
|
|
58
|
+
- Redraw WebGL canvases from the current frame. Production pins software WebGL; do not depend on GPU-specific results or the previous frame's drawing buffer.
|
|
45
59
|
- Do not run model inference inside `mount()`, `update()`, or frame production.
|
|
46
60
|
|
|
61
|
+
## Diagnostics rule
|
|
62
|
+
|
|
63
|
+
`check`, `still`, and `render` run static composition validation before Chromium. Errors fail before a browser starts; warnings allow frame evaluation. Every diagnostic has a stable `code`, a `message`, and optional `details` with source locations and suggestions. Treat warnings as actionable. Do not ignore `BINDING_PATH_MISSING`, `ANIMATION_NOT_FOUND`, `ANIMATION_MULTIPLE`, or `DETERMINISM_WARNING`.
|
|
64
|
+
|
|
47
65
|
## Subtitle and karaoke rule
|
|
48
66
|
|
|
49
|
-
Use canonical transcripts produced by `npx frameyard transcribe`. Convert integer source-relative milliseconds to composition frames with the audio clip's
|
|
67
|
+
Use canonical transcripts produced by `npx frameyard transcribe`. Declare them with `<fy-transcript src="assets/voice.transcript.json" words>`. Convert integer source-relative milliseconds to composition frames with the audio clip's start frame, trim start, and the composition `fps`. Karaoke highlighting requires actual `words` timings; never interpolate segment timings. Read [subtitles and karaoke](references/subtitles-karaoke.md).
|
|
50
68
|
|
|
51
69
|
## CLI invocation
|
|
52
70
|
|
|
@@ -58,3 +76,15 @@ node scripts/frameyard.mjs check <id> --json # fallback
|
|
|
58
76
|
```
|
|
59
77
|
|
|
60
78
|
When the CLI version differs from `frameyard-api-version`, pin it with `npx frameyard@<version>`.
|
|
79
|
+
|
|
80
|
+
## References
|
|
81
|
+
|
|
82
|
+
- [Deterministic HTML authoring](references/deterministic-authoring.md)
|
|
83
|
+
- [Bindings](references/bindings.md)
|
|
84
|
+
- [Controllers](references/controllers.md)
|
|
85
|
+
- [Diagnostics](references/diagnostics.md)
|
|
86
|
+
- [Common mistakes](references/common-mistakes.md)
|
|
87
|
+
- [Audio and generated assets](references/audio-assets.md)
|
|
88
|
+
- [Subtitles and karaoke](references/subtitles-karaoke.md)
|
|
89
|
+
- [JSON results](references/json-results.md)
|
|
90
|
+
- [Failure recovery](references/failure-recovery.md)
|
|
@@ -1,25 +1,36 @@
|
|
|
1
1
|
# Audio and generated assets
|
|
2
2
|
|
|
3
|
-
Audio clips are a declarative timeline. Generated speech and transcripts are frozen local inputs produced before composition loading.
|
|
3
|
+
Audio clips are a declarative `<audio>` timeline. Generated speech and transcripts are frozen local inputs produced before composition loading.
|
|
4
4
|
|
|
5
5
|
## Audio timeline
|
|
6
6
|
|
|
7
|
-
```
|
|
8
|
-
audio
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
},
|
|
16
|
-
];
|
|
7
|
+
```html
|
|
8
|
+
<audio
|
|
9
|
+
src="assets/voice.wav"
|
|
10
|
+
data-fy-start="30"
|
|
11
|
+
data-fy-trim="0.25"
|
|
12
|
+
data-fy-duration="4.2"
|
|
13
|
+
data-fy-volume="0.8"
|
|
14
|
+
></audio>
|
|
17
15
|
```
|
|
18
16
|
|
|
19
|
-
-
|
|
20
|
-
-
|
|
17
|
+
- `src` resolves from the project root.
|
|
18
|
+
- `data-fy-start` is the composition timeline position in frames.
|
|
19
|
+
- `data-fy-trim` is seconds trimmed from the source.
|
|
20
|
+
- `data-fy-duration` optionally limits how many source seconds are used.
|
|
21
|
+
- `data-fy-volume` is `0..1`.
|
|
21
22
|
- Frameyard never time-stretches audio or changes composition duration to fit it.
|
|
22
|
-
- `check` reports clip durations and overrun
|
|
23
|
+
- `check` reports clip durations and overrun diagnostics.
|
|
24
|
+
|
|
25
|
+
An unmuted `<video>` contributes its audio track to the same mix. Use `muted` or `data-fy-audio="off"` for a visual-only video.
|
|
26
|
+
|
|
27
|
+
Declare required transcripts with:
|
|
28
|
+
|
|
29
|
+
```html
|
|
30
|
+
<fy-transcript src="assets/voice.transcript.json" words></fy-transcript>
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`words` requires measured word timings. See [subtitles and karaoke](subtitles-karaoke.md).
|
|
23
34
|
|
|
24
35
|
## Local speech synthesis
|
|
25
36
|
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Bindings
|
|
2
|
+
|
|
3
|
+
Bindings map immutable props into the composition DOM once per load. They are explicit HTML attributes; there is no template expression language.
|
|
4
|
+
|
|
5
|
+
## Attributes
|
|
6
|
+
|
|
7
|
+
| Attribute | Effect |
|
|
8
|
+
| --- | --- |
|
|
9
|
+
| `data-fy-text="path"` | Sets `textContent`. |
|
|
10
|
+
| `data-fy-attr-<name>="path"` | Sets an attribute. |
|
|
11
|
+
| `data-fy-class-<name>="path"` | Toggles one class. |
|
|
12
|
+
| `data-fy-if="path"` | Removes the element when false. |
|
|
13
|
+
| `data-fy-repeat="path"` on `<template>` | Clones the template content for each array item. |
|
|
14
|
+
|
|
15
|
+
```html
|
|
16
|
+
<h1 data-fy-text="title"></h1>
|
|
17
|
+
<img data-fy-attr-src="image" data-fy-attr-alt="alt">
|
|
18
|
+
<div data-fy-class-active="isActive"></div>
|
|
19
|
+
<p data-fy-if="showSubtitle" data-fy-text="subtitle"></p>
|
|
20
|
+
|
|
21
|
+
<template data-fy-repeat="items">
|
|
22
|
+
<span data-fy-text="item.label" data-fy-attr-data-index="index"></span>
|
|
23
|
+
</template>
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Paths
|
|
27
|
+
|
|
28
|
+
- Dot-separated: `title`, `user.name`, `items.0.label`.
|
|
29
|
+
- Numeric segments index arrays.
|
|
30
|
+
- Only own properties are read. `__proto__`, `constructor`, and `prototype` are rejected.
|
|
31
|
+
- Inside a repeat clone, `item` and `index` shadow props.
|
|
32
|
+
- Missing paths are warnings. Text becomes empty, attributes/classes are removed, and `if` is false. A missing or non-array `repeat` path is an error.
|
|
33
|
+
|
|
34
|
+
## Value coercion
|
|
35
|
+
|
|
36
|
+
| Binding | Rule |
|
|
37
|
+
| --- | --- |
|
|
38
|
+
| `text` | Strings, numbers, and booleans are stringified; null/undefined become empty; objects/arrays warn and become empty. |
|
|
39
|
+
| `attr` | `true` writes an empty attribute; `false`/null/undefined remove it; strings/numbers are stringified; objects/arrays warn and remove. |
|
|
40
|
+
| `class` | JavaScript truthiness; objects/arrays warn and remove. |
|
|
41
|
+
| `if` | JavaScript truthiness. Empty arrays are truthy. |
|
|
42
|
+
| `repeat` | Must be an array; otherwise an error. |
|
|
43
|
+
|
|
44
|
+
`data-fy-attr-on*` is rejected. A bound `href`/`src` that resolves to a `javascript:` URL is rejected.
|
|
45
|
+
|
|
46
|
+
## Repeat scope
|
|
47
|
+
|
|
48
|
+
```html
|
|
49
|
+
<ul>
|
|
50
|
+
<template data-fy-repeat="items">
|
|
51
|
+
<li data-fy-text="item.label"></li>
|
|
52
|
+
</template>
|
|
53
|
+
</ul>
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
- The `<template>` is replaced by the clones.
|
|
57
|
+
- `--fy-index` is set on each top-level clone for CSS stagger.
|
|
58
|
+
- Nested repeats are rejected. Use a controller for nested data.
|
|
59
|
+
- A repeat template cannot also use `data-fy-if` or `data-fy-text`.
|
|
60
|
+
|
|
61
|
+
## CSS variables
|
|
62
|
+
|
|
63
|
+
Primitive top-level props are also set as `--fy-prop-<name>` on the composition root:
|
|
64
|
+
|
|
65
|
+
```html
|
|
66
|
+
<div style="background: var(--fy-prop-accent)"></div>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Booleans are `1`/`0`. Null, undefined, objects, and arrays are omitted. Bindings are applied before the controller mounts; controller-created DOM is not re-bound.
|
|
70
|
+
|
|
71
|
+
## Ordering
|
|
72
|
+
|
|
73
|
+
Bindings apply top-down: repeat, then `if`, then text/attribute/class. A removed element does not process its children. `data-fy-if` and `data-fy-text` on the composition root are errors.
|