reframe-video 0.1.0 → 0.1.2
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/assets/sfx/LICENSE.md +5 -0
- package/assets/sfx/bgm-song21.mp3 +0 -0
- package/assets/sfx/pop.wav +0 -0
- package/assets/sfx/rise.wav +0 -0
- package/assets/sfx/shimmer.wav +0 -0
- package/assets/sfx/thud.wav +0 -0
- package/assets/sfx/tick.wav +0 -0
- package/assets/sfx/whoosh.wav +0 -0
- package/dist/analyze.js +44 -1
- package/dist/bin.js +371 -35
- package/dist/browserEntry.js +311 -8
- package/dist/cli.js +305 -21
- package/dist/index.js +566 -16
- package/dist/renderer-canvas.js +58 -3
- package/dist/trace-cli.js +677 -0
- package/dist/types/assets.d.ts +9 -0
- package/dist/types/compile.d.ts +15 -1
- package/dist/types/compose.d.ts +12 -2
- package/dist/types/dsl.d.ts +30 -1
- package/dist/types/evaluate.d.ts +17 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/types/ir.d.ts +82 -1
- package/dist/types/motion.d.ts +54 -0
- package/dist/types/path.d.ts +15 -0
- package/dist/types/presets.d.ts +41 -0
- package/guides/edsl-guide.md +24 -0
- package/package.json +1 -1
- package/preview/src/main.ts +111 -6
- package/preview/src/panel.ts +37 -3
- package/preview/src/store.ts +28 -2
- package/preview/src/virtual.d.ts +9 -1
- package/preview/vite.config.ts +3 -2
|
@@ -0,0 +1,677 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
|
|
3
|
+
// ../../benchmark/harness/motion/trace-cli.ts
|
|
4
|
+
import { writeFile as writeFile2 } from "node:fs/promises";
|
|
5
|
+
import { resolve as resolve2 } from "node:path";
|
|
6
|
+
import { pathToFileURL } from "node:url";
|
|
7
|
+
|
|
8
|
+
// ../core/src/validate.ts
|
|
9
|
+
var COMMON_PROPS = ["x", "y", "opacity", "rotation", "scale", "anchor"];
|
|
10
|
+
var PROPS_BY_TYPE = {
|
|
11
|
+
rect: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth", "radius"],
|
|
12
|
+
ellipse: [...COMMON_PROPS, "width", "height", "fill", "stroke", "strokeWidth"],
|
|
13
|
+
line: ["x1", "y1", "x2", "y2", "stroke", "strokeWidth", "opacity", "progress"],
|
|
14
|
+
text: [...COMMON_PROPS, "content", "contentDecimals", "fontFamily", "fontSize", "fontWeight", "fill", "letterSpacing"],
|
|
15
|
+
image: [...COMMON_PROPS, "src", "width", "height"],
|
|
16
|
+
path: [...COMMON_PROPS, "d", "fill", "stroke", "strokeWidth", "progress", "originX", "originY"],
|
|
17
|
+
group: COMMON_PROPS
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// ../core/src/dsl.ts
|
|
21
|
+
function seq(...children) {
|
|
22
|
+
return { kind: "seq", children };
|
|
23
|
+
}
|
|
24
|
+
function par(...children) {
|
|
25
|
+
return { kind: "par", children };
|
|
26
|
+
}
|
|
27
|
+
function tween(target, props, opts = {}) {
|
|
28
|
+
return { kind: "tween", target, props, ...opts };
|
|
29
|
+
}
|
|
30
|
+
function wait(duration, label) {
|
|
31
|
+
return { kind: "wait", duration, ...label !== void 0 && { label } };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// ../core/src/presets.ts
|
|
35
|
+
var SET = 1 / 120;
|
|
36
|
+
|
|
37
|
+
// ../core/src/interpolate.ts
|
|
38
|
+
var BACK_C1 = 1.70158;
|
|
39
|
+
var BACK_C2 = BACK_C1 * 1.525;
|
|
40
|
+
var BACK_C3 = BACK_C1 + 1;
|
|
41
|
+
var ELASTIC_C4 = 2 * Math.PI / 3;
|
|
42
|
+
var ELASTIC_C5 = 2 * Math.PI / 4.5;
|
|
43
|
+
function easeOutBounce(u) {
|
|
44
|
+
const n1 = 7.5625;
|
|
45
|
+
const d1 = 2.75;
|
|
46
|
+
if (u < 1 / d1) return n1 * u * u;
|
|
47
|
+
if (u < 2 / d1) return n1 * (u -= 1.5 / d1) * u + 0.75;
|
|
48
|
+
if (u < 2.5 / d1) return n1 * (u -= 2.25 / d1) * u + 0.9375;
|
|
49
|
+
return n1 * (u -= 2.625 / d1) * u + 0.984375;
|
|
50
|
+
}
|
|
51
|
+
var EASE_TABLE = {
|
|
52
|
+
linear: (u) => u,
|
|
53
|
+
easeInQuad: (u) => u * u,
|
|
54
|
+
easeOutQuad: (u) => 1 - (1 - u) * (1 - u),
|
|
55
|
+
easeInOutQuad: (u) => u < 0.5 ? 2 * u * u : 1 - (-2 * u + 2) ** 2 / 2,
|
|
56
|
+
easeInCubic: (u) => u ** 3,
|
|
57
|
+
easeOutCubic: (u) => 1 - (1 - u) ** 3,
|
|
58
|
+
easeInOutCubic: (u) => u < 0.5 ? 4 * u ** 3 : 1 - (-2 * u + 2) ** 3 / 2,
|
|
59
|
+
easeInQuart: (u) => u ** 4,
|
|
60
|
+
easeOutQuart: (u) => 1 - (1 - u) ** 4,
|
|
61
|
+
easeInOutQuart: (u) => u < 0.5 ? 8 * u ** 4 : 1 - (-2 * u + 2) ** 4 / 2,
|
|
62
|
+
easeInExpo: (u) => u === 0 ? 0 : 2 ** (10 * u - 10),
|
|
63
|
+
easeOutExpo: (u) => u === 1 ? 1 : 1 - 2 ** (-10 * u),
|
|
64
|
+
easeInOutExpo: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? 2 ** (20 * u - 10) / 2 : (2 - 2 ** (-20 * u + 10)) / 2,
|
|
65
|
+
// --- expressive eases (GSAP's signature feel) — standard Penner equations ---
|
|
66
|
+
// back: overshoots past the target then settles (pop / snap)
|
|
67
|
+
easeInBack: (u) => BACK_C3 * u ** 3 - BACK_C1 * u * u,
|
|
68
|
+
easeOutBack: (u) => 1 + BACK_C3 * (u - 1) ** 3 + BACK_C1 * (u - 1) ** 2,
|
|
69
|
+
easeInOutBack: (u) => u < 0.5 ? (2 * u) ** 2 * ((BACK_C2 + 1) * 2 * u - BACK_C2) / 2 : ((2 * u - 2) ** 2 * ((BACK_C2 + 1) * (2 * u - 2) + BACK_C2) + 2) / 2,
|
|
70
|
+
// elastic: rings around the target before settling (playful spring)
|
|
71
|
+
easeInElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : -(2 ** (10 * u - 10)) * Math.sin((u * 10 - 10.75) * ELASTIC_C4),
|
|
72
|
+
easeOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : 2 ** (-10 * u) * Math.sin((u * 10 - 0.75) * ELASTIC_C4) + 1,
|
|
73
|
+
easeInOutElastic: (u) => u === 0 ? 0 : u === 1 ? 1 : u < 0.5 ? -(2 ** (20 * u - 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5)) / 2 : 2 ** (-20 * u + 10) * Math.sin((20 * u - 11.125) * ELASTIC_C5) / 2 + 1,
|
|
74
|
+
// bounce: drops and bounces to rest (lands without overshoot)
|
|
75
|
+
easeInBounce: (u) => 1 - easeOutBounce(1 - u),
|
|
76
|
+
easeOutBounce,
|
|
77
|
+
easeInOutBounce: (u) => u < 0.5 ? (1 - easeOutBounce(1 - 2 * u)) / 2 : (1 + easeOutBounce(2 * u - 1)) / 2
|
|
78
|
+
};
|
|
79
|
+
var EASE_NAMES = Object.keys(EASE_TABLE);
|
|
80
|
+
|
|
81
|
+
// ../core/src/motion.ts
|
|
82
|
+
var EASE_BY_CLASS = {
|
|
83
|
+
accelerating: "easeInCubic",
|
|
84
|
+
decelerating: "easeOutCubic",
|
|
85
|
+
linear: "linear"
|
|
86
|
+
};
|
|
87
|
+
function easeFor(easing) {
|
|
88
|
+
return EASE_BY_CLASS[easing.class] ?? "easeOutCubic";
|
|
89
|
+
}
|
|
90
|
+
function sketchToTimeline(sketch, nodeIds) {
|
|
91
|
+
if (nodeIds.length === 0) return seq();
|
|
92
|
+
const events = [...sketch.events].sort((a, b) => a.t0 - b.t0);
|
|
93
|
+
const steps = [];
|
|
94
|
+
events.forEach((ev, i) => {
|
|
95
|
+
const node = nodeIds[i % nodeIds.length];
|
|
96
|
+
const dur = Math.max(0.05, ev.t1 - ev.t0);
|
|
97
|
+
const ease = easeFor(ev.easing);
|
|
98
|
+
let motion;
|
|
99
|
+
switch (ev.kind) {
|
|
100
|
+
case "enter":
|
|
101
|
+
motion = tween(node, { opacity: 1 }, { duration: dur, ease });
|
|
102
|
+
break;
|
|
103
|
+
case "exit":
|
|
104
|
+
motion = tween(node, { opacity: 0 }, { duration: dur, ease });
|
|
105
|
+
break;
|
|
106
|
+
case "emphasis": {
|
|
107
|
+
const peak = 1 + Math.max(0.08, Math.min(0.5, ev.magnitude));
|
|
108
|
+
motion = seq(
|
|
109
|
+
tween(node, { scale: peak }, { duration: dur / 2, ease: "easeOutCubic" }),
|
|
110
|
+
tween(node, { scale: 1 }, { duration: dur / 2, ease: "easeInOutQuad" })
|
|
111
|
+
);
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
case "scale":
|
|
115
|
+
motion = tween(node, { scale: 1 + Math.max(-0.5, Math.min(0.5, ev.magnitude)) }, { duration: dur, ease });
|
|
116
|
+
break;
|
|
117
|
+
case "move":
|
|
118
|
+
motion = tween(node, { opacity: 1 }, { duration: dur, ease });
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
steps.push(ev.t0 > 0 ? seq(wait(ev.t0), motion) : motion);
|
|
122
|
+
});
|
|
123
|
+
return par(...steps);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// ../../benchmark/harness/motion/analyze.ts
|
|
127
|
+
import { writeFile } from "node:fs/promises";
|
|
128
|
+
import { resolve } from "node:path";
|
|
129
|
+
import { readFileSync } from "node:fs";
|
|
130
|
+
|
|
131
|
+
// ../../benchmark/harness/motion/blockflow.ts
|
|
132
|
+
var DEFAULT_OPTIONS = {
|
|
133
|
+
scale: 4,
|
|
134
|
+
// 480x270 analysis of 1920x1080
|
|
135
|
+
blockSize: 16,
|
|
136
|
+
searchRadius: 12,
|
|
137
|
+
changedThreshold: 2,
|
|
138
|
+
blockActivityThreshold: 1.5,
|
|
139
|
+
movingThreshold: 0.3
|
|
140
|
+
};
|
|
141
|
+
function blockSad(prev, next, width, bx, by, dx, dy, size) {
|
|
142
|
+
let sad = 0;
|
|
143
|
+
for (let y = 0; y < size; y++) {
|
|
144
|
+
let nextRow = (by + y) * width + bx;
|
|
145
|
+
let prevRow = (by + y + dy) * width + bx + dx;
|
|
146
|
+
for (let x = 0; x < size; x++) {
|
|
147
|
+
sad += Math.abs(next[nextRow + x] - prev[prevRow + x]);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
return sad;
|
|
151
|
+
}
|
|
152
|
+
function parabola(left, best, right) {
|
|
153
|
+
const denom = left - 2 * best + right;
|
|
154
|
+
if (denom <= 0) return 0;
|
|
155
|
+
return Math.max(-0.5, Math.min(0.5, (left - right) / (2 * denom)));
|
|
156
|
+
}
|
|
157
|
+
function analyzePair(prev, next, opts) {
|
|
158
|
+
const { width, height, scale, blockSize, searchRadius: R, changedThreshold } = opts;
|
|
159
|
+
let diffSum = 0;
|
|
160
|
+
let changed = 0;
|
|
161
|
+
const n = width * height;
|
|
162
|
+
for (let i = 0; i < n; i++) {
|
|
163
|
+
const d = Math.abs(next[i] - prev[i]);
|
|
164
|
+
diffSum += d;
|
|
165
|
+
if (d > changedThreshold) changed++;
|
|
166
|
+
}
|
|
167
|
+
const diffMean = diffSum / n;
|
|
168
|
+
const changedFraction = changed / n;
|
|
169
|
+
const speeds = [];
|
|
170
|
+
let movingCount = 0;
|
|
171
|
+
let saturatedCount = 0;
|
|
172
|
+
let residualSum = 0;
|
|
173
|
+
let zeroSadSum = 0;
|
|
174
|
+
let bestSadSum = 0;
|
|
175
|
+
let activeBlocks = 0;
|
|
176
|
+
const pxPerBlock = blockSize * blockSize;
|
|
177
|
+
for (let by = R; by + blockSize + R <= height; by += blockSize) {
|
|
178
|
+
for (let bx = R; bx + blockSize + R <= width; bx += blockSize) {
|
|
179
|
+
const zeroSad = blockSad(prev, next, width, bx, by, 0, 0, blockSize);
|
|
180
|
+
if (zeroSad / pxPerBlock < opts.blockActivityThreshold) continue;
|
|
181
|
+
activeBlocks++;
|
|
182
|
+
const PENALTY = 2;
|
|
183
|
+
let best = zeroSad;
|
|
184
|
+
let bestScore = zeroSad;
|
|
185
|
+
let bestDx = 0;
|
|
186
|
+
let bestDy = 0;
|
|
187
|
+
for (let dy = -R; dy <= R; dy++) {
|
|
188
|
+
for (let dx = -R; dx <= R; dx++) {
|
|
189
|
+
if (dx === 0 && dy === 0) continue;
|
|
190
|
+
const sad = blockSad(prev, next, width, bx, by, dx, dy, blockSize);
|
|
191
|
+
const score = sad + PENALTY * (Math.abs(dx) + Math.abs(dy));
|
|
192
|
+
if (score < bestScore) {
|
|
193
|
+
bestScore = score;
|
|
194
|
+
best = sad;
|
|
195
|
+
bestDx = dx;
|
|
196
|
+
bestDy = dy;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
let fx = bestDx;
|
|
201
|
+
let fy = bestDy;
|
|
202
|
+
if (Math.abs(bestDx) < R) {
|
|
203
|
+
fx += parabola(
|
|
204
|
+
blockSad(prev, next, width, bx, by, bestDx - 1, bestDy, blockSize),
|
|
205
|
+
best,
|
|
206
|
+
blockSad(prev, next, width, bx, by, bestDx + 1, bestDy, blockSize)
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
if (Math.abs(bestDy) < R) {
|
|
210
|
+
fy += parabola(
|
|
211
|
+
blockSad(prev, next, width, bx, by, bestDx, bestDy - 1, blockSize),
|
|
212
|
+
best,
|
|
213
|
+
blockSad(prev, next, width, bx, by, bestDx, bestDy + 1, blockSize)
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
const speed = Math.hypot(fx, fy);
|
|
217
|
+
residualSum += best / pxPerBlock;
|
|
218
|
+
zeroSadSum += zeroSad;
|
|
219
|
+
bestSadSum += best;
|
|
220
|
+
if (Math.abs(bestDx) >= R || Math.abs(bestDy) >= R) saturatedCount++;
|
|
221
|
+
if ((bestDx !== 0 || bestDy !== 0) && speed > opts.movingThreshold && best < 0.6 * zeroSad) {
|
|
222
|
+
movingCount++;
|
|
223
|
+
speeds.push(speed * scale);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
speeds.sort((a, b) => a - b);
|
|
228
|
+
const matchResidual = activeBlocks > 0 ? residualSum / activeBlocks : 0;
|
|
229
|
+
return {
|
|
230
|
+
blockSpeedMean: speeds.length ? speeds.reduce((a, b) => a + b, 0) / speeds.length : 0,
|
|
231
|
+
blockSpeedP95: speeds.length ? speeds[Math.min(speeds.length - 1, Math.floor(speeds.length * 0.95))] : 0,
|
|
232
|
+
movingFraction: activeBlocks > 0 ? movingCount / activeBlocks : 0,
|
|
233
|
+
diffMean,
|
|
234
|
+
changedFraction,
|
|
235
|
+
matchResidual,
|
|
236
|
+
nonGeometricRatio: zeroSadSum > 0 ? Math.min(1, bestSadSum / zeroSadSum) : 0,
|
|
237
|
+
saturatedFraction: activeBlocks > 0 ? saturatedCount / activeBlocks : 0,
|
|
238
|
+
activeBlocks
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// ../../benchmark/harness/motion/frames.ts
|
|
243
|
+
import { spawn } from "node:child_process";
|
|
244
|
+
import { stat } from "node:fs/promises";
|
|
245
|
+
import { join } from "node:path";
|
|
246
|
+
async function resolveSource(path) {
|
|
247
|
+
const s = await stat(path);
|
|
248
|
+
if (s.isDirectory()) return { kind: "png", input: join(path, "%05d.png") };
|
|
249
|
+
return { kind: "mp4", input: path };
|
|
250
|
+
}
|
|
251
|
+
async function* frameStream(source, opts) {
|
|
252
|
+
const frameBytes = opts.width * opts.height;
|
|
253
|
+
const args = [
|
|
254
|
+
"-loglevel",
|
|
255
|
+
"error",
|
|
256
|
+
...source.kind === "png" ? ["-framerate", "30"] : [],
|
|
257
|
+
"-i",
|
|
258
|
+
source.input,
|
|
259
|
+
"-f",
|
|
260
|
+
"rawvideo",
|
|
261
|
+
"-pix_fmt",
|
|
262
|
+
"gray",
|
|
263
|
+
"-s",
|
|
264
|
+
`${opts.width}x${opts.height}`,
|
|
265
|
+
"-"
|
|
266
|
+
];
|
|
267
|
+
const proc = spawn("ffmpeg", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
268
|
+
let stderr = "";
|
|
269
|
+
proc.stderr.on("data", (d) => stderr += d.toString());
|
|
270
|
+
let pending = Buffer.alloc(0);
|
|
271
|
+
for await (const chunk of proc.stdout) {
|
|
272
|
+
pending = pending.length === 0 ? chunk : Buffer.concat([pending, chunk]);
|
|
273
|
+
while (pending.length >= frameBytes) {
|
|
274
|
+
yield new Uint8Array(pending.subarray(0, frameBytes));
|
|
275
|
+
pending = pending.subarray(frameBytes);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const code = await new Promise((res) => proc.on("close", (c) => res(c ?? 1)));
|
|
279
|
+
if (code !== 0) throw new Error(`ffmpeg exited ${code}: ${stderr.slice(-1e3)}`);
|
|
280
|
+
if (pending.length !== 0) throw new Error(`trailing partial frame (${pending.length} bytes)`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ../../benchmark/harness/motion/grid.ts
|
|
284
|
+
var DEFAULT_GRID = { cols: 8, rows: 5 };
|
|
285
|
+
var cellOf = (x, y, w, h, spec) => {
|
|
286
|
+
const c = Math.min(spec.cols - 1, Math.floor(x * spec.cols / w));
|
|
287
|
+
const r = Math.min(spec.rows - 1, Math.floor(y * spec.rows / h));
|
|
288
|
+
return r * spec.cols + c;
|
|
289
|
+
};
|
|
290
|
+
function cellDiff(prev, next, w, h, spec) {
|
|
291
|
+
const n = spec.cols * spec.rows;
|
|
292
|
+
const sum = new Array(n).fill(0);
|
|
293
|
+
const cnt = new Array(n).fill(0);
|
|
294
|
+
for (let y = 0; y < h; y++) {
|
|
295
|
+
for (let x = 0; x < w; x++) {
|
|
296
|
+
const idx = cellOf(x, y, w, h, spec);
|
|
297
|
+
sum[idx] += Math.abs(next[y * w + x] - prev[y * w + x]);
|
|
298
|
+
cnt[idx]++;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
return sum.map((s, i) => cnt[i] ? s / cnt[i] : 0);
|
|
302
|
+
}
|
|
303
|
+
function frameOccupancy(frame, w, h, spec) {
|
|
304
|
+
const n = spec.cols * spec.rows;
|
|
305
|
+
const sum = new Array(n).fill(0);
|
|
306
|
+
const cnt = new Array(n).fill(0);
|
|
307
|
+
for (let y = 0; y < h; y++) {
|
|
308
|
+
for (let x = 0; x < w; x++) {
|
|
309
|
+
const here = frame[y * w + x];
|
|
310
|
+
const gx = x + 1 < w ? Math.abs(frame[y * w + x + 1] - here) : 0;
|
|
311
|
+
const gy = y + 1 < h ? Math.abs(frame[(y + 1) * w + x] - here) : 0;
|
|
312
|
+
const idx = cellOf(x, y, w, h, spec);
|
|
313
|
+
sum[idx] += gx + gy;
|
|
314
|
+
cnt[idx]++;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
return sum.map((s, i) => cnt[i] ? s / cnt[i] : 0);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ../../benchmark/harness/motion/analyze.ts
|
|
321
|
+
var ANALYSIS_WIDTH = 480;
|
|
322
|
+
var ANALYSIS_HEIGHT = 270;
|
|
323
|
+
var mean = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
324
|
+
function rollingMedian(xs, i, w) {
|
|
325
|
+
const half = Math.floor(w / 2);
|
|
326
|
+
const window = [];
|
|
327
|
+
for (let j = Math.max(0, i - half); j <= Math.min(xs.length - 1, i + half); j++) {
|
|
328
|
+
if (j !== i) window.push(xs[j]);
|
|
329
|
+
}
|
|
330
|
+
window.sort((a, b) => a - b);
|
|
331
|
+
return window.length ? window[Math.floor(window.length / 2)] : 0;
|
|
332
|
+
}
|
|
333
|
+
function spearman(xs, ys) {
|
|
334
|
+
const n = xs.length;
|
|
335
|
+
if (n < 4) return null;
|
|
336
|
+
const rank = (vs) => {
|
|
337
|
+
const sorted = vs.map((v, i) => [v, i]).sort((a, b) => a[0] - b[0]);
|
|
338
|
+
const ranks = new Array(n);
|
|
339
|
+
sorted.forEach(([, originalIndex], r) => ranks[originalIndex] = r);
|
|
340
|
+
return ranks;
|
|
341
|
+
};
|
|
342
|
+
const rx = rank(xs);
|
|
343
|
+
const ry = rank(ys);
|
|
344
|
+
const mx = mean(rx);
|
|
345
|
+
const my = mean(ry);
|
|
346
|
+
let cov = 0;
|
|
347
|
+
let vx = 0;
|
|
348
|
+
let vy = 0;
|
|
349
|
+
for (let i = 0; i < n; i++) {
|
|
350
|
+
cov += (rx[i] - mx) * (ry[i] - my);
|
|
351
|
+
vx += (rx[i] - mx) ** 2;
|
|
352
|
+
vy += (ry[i] - my) ** 2;
|
|
353
|
+
}
|
|
354
|
+
return vx > 0 && vy > 0 ? cov / Math.sqrt(vx * vy) : null;
|
|
355
|
+
}
|
|
356
|
+
function dominantPeriodicity(xs, fps) {
|
|
357
|
+
const n = xs.length;
|
|
358
|
+
if (n < 24) return null;
|
|
359
|
+
const m = mean(xs);
|
|
360
|
+
const centered = xs.map((x) => x - m);
|
|
361
|
+
const var0 = mean(centered.map((x) => x * x));
|
|
362
|
+
if (var0 < 1e-6) return null;
|
|
363
|
+
const maxLag = Math.floor(n / 2);
|
|
364
|
+
const corr = [];
|
|
365
|
+
for (let lag = 0; lag <= maxLag; lag++) {
|
|
366
|
+
let c = 0;
|
|
367
|
+
for (let i = 0; i + lag < n; i++) c += centered[i] * centered[i + lag];
|
|
368
|
+
corr.push(c / ((n - lag) * var0));
|
|
369
|
+
}
|
|
370
|
+
let bestLag = 0;
|
|
371
|
+
let bestCorr = 0;
|
|
372
|
+
for (let lag = 4; lag < maxLag; lag++) {
|
|
373
|
+
const isLocalMax = corr[lag] > corr[lag - 1] && corr[lag] >= corr[lag + 1];
|
|
374
|
+
if (isLocalMax && corr[lag] > bestCorr) {
|
|
375
|
+
bestCorr = corr[lag];
|
|
376
|
+
bestLag = lag;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return bestCorr > 0.3 && bestLag > 0 ? fps / bestLag : null;
|
|
380
|
+
}
|
|
381
|
+
function classifyEasing(speeds, nonGeometricRatios, saturatedFractions) {
|
|
382
|
+
const reliable = speeds.length >= 6 && mean(nonGeometricRatios) < 0.5 && saturatedFractions.filter((s) => s > 0.3).length / saturatedFractions.length < 0.2 && mean(speeds) > 0.5;
|
|
383
|
+
if (!reliable) {
|
|
384
|
+
return { class: "unreliable", thirdsRatio: null, spearman: null, reliable: false };
|
|
385
|
+
}
|
|
386
|
+
const third = Math.max(1, Math.floor(speeds.length / 3));
|
|
387
|
+
const first = mean(speeds.slice(0, third));
|
|
388
|
+
const last = mean(speeds.slice(-third));
|
|
389
|
+
const R = last > 0.01 ? first / last : Infinity;
|
|
390
|
+
const rho = spearman(
|
|
391
|
+
speeds.map((_, i) => i),
|
|
392
|
+
speeds
|
|
393
|
+
);
|
|
394
|
+
let cls = "other";
|
|
395
|
+
if (R > 1.8 && rho !== null && rho < -0.5) cls = "decelerating";
|
|
396
|
+
else if (R < 0.55 && rho !== null && rho > 0.5) cls = "accelerating";
|
|
397
|
+
else if (R >= 0.7 && R <= 1.4 && rho !== null && Math.abs(rho) < 0.4) cls = "linear";
|
|
398
|
+
return { class: cls, thirdsRatio: Number.isFinite(R) ? R : null, spearman: rho, reliable: true };
|
|
399
|
+
}
|
|
400
|
+
async function analyzeMotion(inputPath, opts = {}) {
|
|
401
|
+
const fps = opts.fps ?? 30;
|
|
402
|
+
const blockOpts = {
|
|
403
|
+
...DEFAULT_OPTIONS,
|
|
404
|
+
width: ANALYSIS_WIDTH,
|
|
405
|
+
height: ANALYSIS_HEIGHT,
|
|
406
|
+
...opts.changedThreshold !== void 0 && { changedThreshold: opts.changedThreshold }
|
|
407
|
+
};
|
|
408
|
+
const gridSpec = opts.grid ? typeof opts.grid === "object" ? opts.grid : DEFAULT_GRID : null;
|
|
409
|
+
const source = await resolveSource(resolve(inputPath));
|
|
410
|
+
const keys = [
|
|
411
|
+
"blockSpeedMean",
|
|
412
|
+
"blockSpeedP95",
|
|
413
|
+
"movingFraction",
|
|
414
|
+
"diffMean",
|
|
415
|
+
"changedFraction",
|
|
416
|
+
"matchResidual",
|
|
417
|
+
"nonGeometricRatio",
|
|
418
|
+
"saturatedFraction",
|
|
419
|
+
"activeBlocks"
|
|
420
|
+
];
|
|
421
|
+
const series = Object.fromEntries([...keys.map((k) => [k, []]), ["t", []]]);
|
|
422
|
+
const gridDiff = [];
|
|
423
|
+
const gridOcc = [];
|
|
424
|
+
let prev = null;
|
|
425
|
+
let pair = 0;
|
|
426
|
+
for await (const frame of frameStream(source, blockOpts)) {
|
|
427
|
+
if (gridSpec) gridOcc.push(frameOccupancy(frame, ANALYSIS_WIDTH, ANALYSIS_HEIGHT, gridSpec));
|
|
428
|
+
if (prev) {
|
|
429
|
+
const stats = analyzePair(prev, frame, blockOpts);
|
|
430
|
+
for (const k of keys) series[k].push(stats[k]);
|
|
431
|
+
series.t.push((pair + 0.5) / fps);
|
|
432
|
+
if (gridSpec) gridDiff.push(cellDiff(prev, frame, ANALYSIS_WIDTH, ANALYSIS_HEIGHT, gridSpec));
|
|
433
|
+
pair++;
|
|
434
|
+
}
|
|
435
|
+
prev = frame;
|
|
436
|
+
}
|
|
437
|
+
const d = series.diffMean;
|
|
438
|
+
const staticFlags = series.changedFraction.map((c) => c < 5e-3);
|
|
439
|
+
let longestRun = 0;
|
|
440
|
+
let run = 0;
|
|
441
|
+
for (const isStatic of staticFlags) {
|
|
442
|
+
run = isStatic ? run + 1 : 0;
|
|
443
|
+
longestRun = Math.max(longestRun, run);
|
|
444
|
+
}
|
|
445
|
+
const spikes = [];
|
|
446
|
+
for (let i = 0; i < d.length; i++) {
|
|
447
|
+
const med = rollingMedian(d, i, 9);
|
|
448
|
+
const neighbors = Math.max(i > 0 ? d[i - 1] : 0, i + 1 < d.length ? d[i + 1] : 0);
|
|
449
|
+
if (d[i] > Math.max(4 * med, 1) && series.changedFraction[i] > 0.01 && neighbors < d[i] / 2) {
|
|
450
|
+
spikes.push({ pair: i, t: series.t[i], ratio: med > 0 ? d[i] / med : Infinity });
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
const segments = (opts.segments ?? []).map((seg) => {
|
|
454
|
+
const idx = series.t.map((t, i) => t >= seg.t0 && t <= seg.t1 ? i : -1).filter((i) => i >= 0);
|
|
455
|
+
return {
|
|
456
|
+
...seg,
|
|
457
|
+
easing: classifyEasing(
|
|
458
|
+
idx.map((i) => series.blockSpeedMean[i]),
|
|
459
|
+
idx.map((i) => series.nonGeometricRatio[i]),
|
|
460
|
+
idx.map((i) => series.saturatedFraction[i])
|
|
461
|
+
)
|
|
462
|
+
};
|
|
463
|
+
});
|
|
464
|
+
return {
|
|
465
|
+
params: { ...blockOpts, analysisWidth: ANALYSIS_WIDTH, analysisHeight: ANALYSIS_HEIGHT, source: source.kind },
|
|
466
|
+
fps,
|
|
467
|
+
framePairs: series.t.length,
|
|
468
|
+
series,
|
|
469
|
+
summary: {
|
|
470
|
+
meanSpeed: mean(series.blockSpeedMean.filter((s) => s > 0)),
|
|
471
|
+
peakSpeed: Math.max(0, ...series.blockSpeedP95),
|
|
472
|
+
meanDiff: mean(d),
|
|
473
|
+
staticFraction: staticFlags.filter(Boolean).length / Math.max(1, staticFlags.length),
|
|
474
|
+
longestStaticRunSec: longestRun / fps,
|
|
475
|
+
allStatic: staticFlags.every(Boolean),
|
|
476
|
+
spikes,
|
|
477
|
+
saturatedPairs: series.saturatedFraction.map((s, i) => s > 0.3 ? i : -1).filter((i) => i >= 0),
|
|
478
|
+
diffPeriodicityHz: dominantPeriodicity(d, fps)
|
|
479
|
+
},
|
|
480
|
+
segments,
|
|
481
|
+
...gridSpec && { grid: { spec: gridSpec, diff: gridDiff, occupancy: gridOcc } }
|
|
482
|
+
};
|
|
483
|
+
}
|
|
484
|
+
async function main() {
|
|
485
|
+
const [input, ...rest] = process.argv.slice(2);
|
|
486
|
+
if (!input) {
|
|
487
|
+
console.error("usage: tsx analyze.ts <mp4|framesDir> [-o out.json] [--fps N] [--segments seg.json] [--changed-threshold N]");
|
|
488
|
+
process.exit(2);
|
|
489
|
+
}
|
|
490
|
+
let out = null;
|
|
491
|
+
const opts = {};
|
|
492
|
+
for (let i = 0; i < rest.length; i++) {
|
|
493
|
+
if (rest[i] === "-o") out = rest[++i];
|
|
494
|
+
else if (rest[i] === "--fps") opts.fps = Number(rest[++i]);
|
|
495
|
+
else if (rest[i] === "--segments") opts.segments = JSON.parse(readFileSync(rest[++i], "utf8"));
|
|
496
|
+
else if (rest[i] === "--changed-threshold") opts.changedThreshold = Number(rest[++i]);
|
|
497
|
+
}
|
|
498
|
+
const profile = await analyzeMotion(input, opts);
|
|
499
|
+
const json = JSON.stringify(profile, null, 1);
|
|
500
|
+
if (out) await writeFile(out, json);
|
|
501
|
+
else console.log(json);
|
|
502
|
+
}
|
|
503
|
+
if (/analyze\.(ts|js)$/.test(process.argv[1] ?? "")) {
|
|
504
|
+
main().catch((err) => {
|
|
505
|
+
console.error(err);
|
|
506
|
+
process.exit(1);
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
// ../../benchmark/harness/motion/sketch.ts
|
|
511
|
+
var HI_FLOOR = 1.5;
|
|
512
|
+
var HI_LO_RATIO = 0.55;
|
|
513
|
+
var NOISE_MARGIN = 1.5;
|
|
514
|
+
var MIN_RUN_PAIRS = 2;
|
|
515
|
+
var CONTINUOUS_FRAC = 0.35;
|
|
516
|
+
var TEMPORAL_SLACK = 7;
|
|
517
|
+
var MOVE_FRACTION = 0.04;
|
|
518
|
+
var OCC_RATIO = 1.4;
|
|
519
|
+
var INPLACE_RATIO = 0.3;
|
|
520
|
+
var mean2 = (xs) => xs.length ? xs.reduce((a, b) => a + b, 0) / xs.length : 0;
|
|
521
|
+
function backgroundLevel(diff) {
|
|
522
|
+
const flat = [];
|
|
523
|
+
for (const row of diff) for (const v of row) flat.push(v);
|
|
524
|
+
if (flat.length === 0) return 0;
|
|
525
|
+
flat.sort((a, b) => a - b);
|
|
526
|
+
return flat[Math.floor(flat.length / 2)];
|
|
527
|
+
}
|
|
528
|
+
function findRuns(diff, nCells, hi, lo) {
|
|
529
|
+
const runs = [];
|
|
530
|
+
const nPairs = diff.length;
|
|
531
|
+
for (let cell = 0; cell < nCells; cell++) {
|
|
532
|
+
let active = false;
|
|
533
|
+
let start = 0;
|
|
534
|
+
for (let p = 0; p < nPairs; p++) {
|
|
535
|
+
const v = diff[p][cell];
|
|
536
|
+
if (!active && v > hi) {
|
|
537
|
+
active = true;
|
|
538
|
+
start = p;
|
|
539
|
+
} else if (active && v < lo) {
|
|
540
|
+
if (p - start >= MIN_RUN_PAIRS) runs.push({ cell, p0: start, p1: p - 1 });
|
|
541
|
+
active = false;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (active && nPairs - start >= MIN_RUN_PAIRS) runs.push({ cell, p0: start, p1: nPairs - 1 });
|
|
545
|
+
}
|
|
546
|
+
return runs;
|
|
547
|
+
}
|
|
548
|
+
function groupRuns(runs, cols) {
|
|
549
|
+
const parent = runs.map((_, i) => i);
|
|
550
|
+
const find = (i) => parent[i] === i ? i : parent[i] = find(parent[i]);
|
|
551
|
+
const union = (a, b) => {
|
|
552
|
+
parent[find(a)] = find(b);
|
|
553
|
+
};
|
|
554
|
+
const rc = (cell) => ({ r: Math.floor(cell / cols), c: cell % cols });
|
|
555
|
+
for (let i = 0; i < runs.length; i++) {
|
|
556
|
+
for (let j = i + 1; j < runs.length; j++) {
|
|
557
|
+
const a = runs[i];
|
|
558
|
+
const b = runs[j];
|
|
559
|
+
const ra = rc(a.cell);
|
|
560
|
+
const rb = rc(b.cell);
|
|
561
|
+
const sameCell = a.cell === b.cell;
|
|
562
|
+
const adjacent = sameCell || Math.abs(ra.r - rb.r) + Math.abs(ra.c - rb.c) === 1;
|
|
563
|
+
const slack = sameCell ? TEMPORAL_SLACK : 1;
|
|
564
|
+
const temporal = a.p0 <= b.p1 + slack && b.p0 <= a.p1 + slack;
|
|
565
|
+
if (adjacent && temporal) union(i, j);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const groups = /* @__PURE__ */ new Map();
|
|
569
|
+
runs.forEach((run, i) => {
|
|
570
|
+
const root = find(i);
|
|
571
|
+
(groups.get(root) ?? groups.set(root, []).get(root)).push(run);
|
|
572
|
+
});
|
|
573
|
+
return [...groups.values()];
|
|
574
|
+
}
|
|
575
|
+
function extractMotionSketch(profile) {
|
|
576
|
+
const fps = profile.fps;
|
|
577
|
+
const duration = (profile.framePairs + 1) / fps;
|
|
578
|
+
const rhythm = {
|
|
579
|
+
periodicityHz: profile.summary.diffPeriodicityHz,
|
|
580
|
+
beatCount: 0
|
|
581
|
+
};
|
|
582
|
+
if (!profile.grid) {
|
|
583
|
+
return { duration, fps, events: [], rhythm };
|
|
584
|
+
}
|
|
585
|
+
const { spec, diff, occupancy } = profile.grid;
|
|
586
|
+
const nCells = spec.cols * spec.rows;
|
|
587
|
+
const nFrames = occupancy.length;
|
|
588
|
+
const hi = Math.max(HI_FLOOR, backgroundLevel(diff) + NOISE_MARGIN);
|
|
589
|
+
const lo = hi * HI_LO_RATIO;
|
|
590
|
+
const groups = groupRuns(findRuns(diff, nCells, hi, lo), spec.cols);
|
|
591
|
+
const events = groups.map((group) => {
|
|
592
|
+
const cells = [...new Set(group.map((r) => r.cell))];
|
|
593
|
+
const p0 = Math.min(...group.map((r) => r.p0));
|
|
594
|
+
const p1 = Math.max(...group.map((r) => r.p1));
|
|
595
|
+
const cols = cells.map((c) => c % spec.cols);
|
|
596
|
+
const rows = cells.map((c) => Math.floor(c / spec.cols));
|
|
597
|
+
const minC = Math.min(...cols);
|
|
598
|
+
const maxC = Math.max(...cols);
|
|
599
|
+
const minR = Math.min(...rows);
|
|
600
|
+
const maxR = Math.max(...rows);
|
|
601
|
+
const region = {
|
|
602
|
+
x: minC / spec.cols,
|
|
603
|
+
y: minR / spec.rows,
|
|
604
|
+
w: (maxC - minC + 1) / spec.cols,
|
|
605
|
+
h: (maxR - minR + 1) / spec.rows
|
|
606
|
+
};
|
|
607
|
+
const win = (arr) => arr.slice(p0, p1 + 1);
|
|
608
|
+
const speeds = win(profile.series.blockSpeedMean);
|
|
609
|
+
const ngrs = win(profile.series.nonGeometricRatio);
|
|
610
|
+
const sats = win(profile.series.saturatedFraction);
|
|
611
|
+
const movingFrac = mean2(win(profile.series.movingFraction));
|
|
612
|
+
const occFrame = (f) => occupancy[Math.max(0, Math.min(nFrames - 1, f))];
|
|
613
|
+
const occAt = (f) => mean2(cells.map((c) => occFrame(f)[c]));
|
|
614
|
+
const occStart = occAt(p0);
|
|
615
|
+
const occEnd = occAt(p1 + 1);
|
|
616
|
+
const occStartF = occFrame(p0);
|
|
617
|
+
const occEndF = occFrame(p1 + 1);
|
|
618
|
+
const perCellDelta = mean2(cells.map((c) => Math.abs(occEndF[c] - occStartF[c])));
|
|
619
|
+
const inPlace = perCellDelta < INPLACE_RATIO * Math.max(occStart, occEnd, 1);
|
|
620
|
+
let kind;
|
|
621
|
+
if (occEnd > occStart * OCC_RATIO) kind = "enter";
|
|
622
|
+
else if (occStart > occEnd * OCC_RATIO) kind = "exit";
|
|
623
|
+
else if (inPlace) {
|
|
624
|
+
const occPeak = Math.max(...Array.from({ length: p1 - p0 + 1 }, (_, k) => occAt(p0 + k)));
|
|
625
|
+
const transient = occPeak > Math.max(occStart, occEnd) * 1.1 || Math.max(occStart, occEnd) > 1;
|
|
626
|
+
kind = transient ? "emphasis" : "scale";
|
|
627
|
+
} else if (movingFrac > MOVE_FRACTION) kind = "move";
|
|
628
|
+
else kind = "scale";
|
|
629
|
+
const e = classifyEasing(speeds, ngrs, sats);
|
|
630
|
+
const peakSpeed = Math.max(0, ...win(profile.series.blockSpeedP95));
|
|
631
|
+
const magnitude = kind === "enter" || kind === "exit" || kind === "move" ? Math.min(1, peakSpeed / 200) : Math.min(0.5, Math.abs(occEnd - occStart) / Math.max(1, occStart) + 0.1);
|
|
632
|
+
return {
|
|
633
|
+
t0: p0 / fps,
|
|
634
|
+
t1: (p1 + 1) / fps,
|
|
635
|
+
kind,
|
|
636
|
+
region,
|
|
637
|
+
magnitude,
|
|
638
|
+
easing: { class: e.class, thirdsRatio: e.thirdsRatio, reliable: e.reliable }
|
|
639
|
+
};
|
|
640
|
+
});
|
|
641
|
+
const maxEventSec = CONTINUOUS_FRAC * duration;
|
|
642
|
+
const discrete = events.filter((e) => e.t1 - e.t0 <= maxEventSec);
|
|
643
|
+
discrete.sort((a, b) => a.t0 - b.t0);
|
|
644
|
+
rhythm.beatCount = discrete.length;
|
|
645
|
+
return { duration, fps, events: discrete, rhythm };
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ../../benchmark/harness/motion/trace-cli.ts
|
|
649
|
+
async function main2() {
|
|
650
|
+
const [input, ...rest] = process.argv.slice(2);
|
|
651
|
+
if (!input) {
|
|
652
|
+
console.error("usage: reframe trace <ref.mp4|framesDir> [--fps N] [--apply scene.ts] [-o out.json]");
|
|
653
|
+
process.exit(2);
|
|
654
|
+
}
|
|
655
|
+
let fps;
|
|
656
|
+
let apply;
|
|
657
|
+
let out;
|
|
658
|
+
for (let i = 0; i < rest.length; i++) {
|
|
659
|
+
if (rest[i] === "--fps") fps = Number(rest[++i]);
|
|
660
|
+
else if (rest[i] === "--apply") apply = rest[++i];
|
|
661
|
+
else if (rest[i] === "-o") out = rest[++i];
|
|
662
|
+
}
|
|
663
|
+
const profile = await analyzeMotion(resolve2(input), { grid: true, ...fps !== void 0 && { fps } });
|
|
664
|
+
const sketch = extractMotionSketch(profile);
|
|
665
|
+
let result = sketch;
|
|
666
|
+
if (apply) {
|
|
667
|
+
const ir = (await import(pathToFileURL(resolve2(apply)).href)).default;
|
|
668
|
+
result = sketchToTimeline(sketch, ir.nodes.map((n) => n.id));
|
|
669
|
+
}
|
|
670
|
+
const json = JSON.stringify(result, null, 2);
|
|
671
|
+
if (out) await writeFile2(resolve2(out), json);
|
|
672
|
+
else console.log(json);
|
|
673
|
+
}
|
|
674
|
+
main2().catch((err) => {
|
|
675
|
+
console.error(err instanceof Error ? err.message : err);
|
|
676
|
+
process.exit(1);
|
|
677
|
+
});
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asset discovery shared by every consumer that must preload images before
|
|
3
|
+
* rendering (the capture page and the preview). One walker means the two
|
|
4
|
+
* sides can never disagree about which srcs a scene uses — including srcs
|
|
5
|
+
* introduced only mid-scene by a state override or a tween.
|
|
6
|
+
*/
|
|
7
|
+
import type { SceneIR } from "./ir.js";
|
|
8
|
+
/** All image srcs a scene can ever display, deduped, in discovery order. */
|
|
9
|
+
export declare function collectImageSrcs(ir: SceneIR): string[];
|