reframe-video 0.6.0 → 0.6.1
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/dist/bin.js +31 -0
- package/dist/browserEntry.js +67 -11
- package/dist/cli.js +22 -0
- package/dist/index.js +86 -5
- package/dist/labels.js +22 -0
- package/dist/renderer-canvas.js +34 -6
- package/dist/types/evaluate.d.ts +9 -7
- package/dist/types/gradient.d.ts +28 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/ir.d.ts +35 -6
- package/dist/types/path.d.ts +6 -0
- package/guides/edsl-guide.md +22 -0
- package/package.json +1 -1
package/dist/bin.js
CHANGED
|
@@ -349,12 +349,34 @@ var init_compile = __esm({
|
|
|
349
349
|
function validateScene(ir) {
|
|
350
350
|
const problems = [];
|
|
351
351
|
const nodeById = /* @__PURE__ */ new Map();
|
|
352
|
+
const checkPaint = (where, value) => {
|
|
353
|
+
if (typeof value !== "object" || value === null) return;
|
|
354
|
+
const g = value;
|
|
355
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
356
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
360
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
g.stops.forEach((s, i) => {
|
|
364
|
+
const st = s;
|
|
365
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
366
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
367
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
};
|
|
352
371
|
const collect = (nodes) => {
|
|
353
372
|
for (const node of nodes) {
|
|
354
373
|
if (nodeById.has(node.id)) {
|
|
355
374
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
356
375
|
}
|
|
357
376
|
nodeById.set(node.id, node);
|
|
377
|
+
const props = node.props;
|
|
378
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
379
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
358
380
|
if (node.type === "group") {
|
|
359
381
|
const clip = node.props.clip;
|
|
360
382
|
if (clip) {
|
|
@@ -899,6 +921,13 @@ var init_camera = __esm({
|
|
|
899
921
|
}
|
|
900
922
|
});
|
|
901
923
|
|
|
924
|
+
// ../core/src/gradient.ts
|
|
925
|
+
var init_gradient = __esm({
|
|
926
|
+
"../core/src/gradient.ts"() {
|
|
927
|
+
"use strict";
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
|
|
902
931
|
// ../core/src/presets.ts
|
|
903
932
|
function makeRng(seed) {
|
|
904
933
|
let a = seed >>> 0 || 2654435769;
|
|
@@ -1292,6 +1321,7 @@ var init_evaluate = __esm({
|
|
|
1292
1321
|
"use strict";
|
|
1293
1322
|
init_behaviors();
|
|
1294
1323
|
init_camera();
|
|
1324
|
+
init_gradient();
|
|
1295
1325
|
init_interpolate();
|
|
1296
1326
|
init_path();
|
|
1297
1327
|
}
|
|
@@ -1354,6 +1384,7 @@ var init_src = __esm({
|
|
|
1354
1384
|
init_compile();
|
|
1355
1385
|
init_path();
|
|
1356
1386
|
init_camera();
|
|
1387
|
+
init_gradient();
|
|
1357
1388
|
init_presets();
|
|
1358
1389
|
init_devicePreset();
|
|
1359
1390
|
init_cursor();
|
package/dist/browserEntry.js
CHANGED
|
@@ -6,6 +6,22 @@
|
|
|
6
6
|
var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
7
7
|
|
|
8
8
|
// ../core/src/path.ts
|
|
9
|
+
function pathBBox(d) {
|
|
10
|
+
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
|
|
11
|
+
if (!nums || nums.length < 2) return [0, 0, 1, 1];
|
|
12
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
13
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
14
|
+
const x = parseFloat(nums[i]);
|
|
15
|
+
const y = parseFloat(nums[i + 1]);
|
|
16
|
+
if (x < minx) minx = x;
|
|
17
|
+
if (x > maxx) maxx = x;
|
|
18
|
+
if (y < miny) miny = y;
|
|
19
|
+
if (y > maxy) maxy = y;
|
|
20
|
+
}
|
|
21
|
+
const w = maxx - minx;
|
|
22
|
+
const h = maxy - miny;
|
|
23
|
+
return [minx, miny, w > 0 ? w : 1, h > 0 ? h : 1];
|
|
24
|
+
}
|
|
9
25
|
function locate(segCount, u) {
|
|
10
26
|
if (segCount <= 0) return { i: 0, t: 0 };
|
|
11
27
|
const clamped = Math.max(0, Math.min(1, u));
|
|
@@ -344,6 +360,11 @@
|
|
|
344
360
|
return [a, b, c, d, W / 2 - a * x - c * y, H / 2 - b * x - d * y];
|
|
345
361
|
}
|
|
346
362
|
|
|
363
|
+
// ../core/src/gradient.ts
|
|
364
|
+
function isGradient(p) {
|
|
365
|
+
return typeof p === "object" && p !== null && (p.kind === "linear" || p.kind === "radial" || p.kind === "conic");
|
|
366
|
+
}
|
|
367
|
+
|
|
347
368
|
// ../core/src/presets.ts
|
|
348
369
|
var SET = 1 / 120;
|
|
349
370
|
|
|
@@ -709,8 +730,10 @@
|
|
|
709
730
|
const height = num(id, "height", node.props.height);
|
|
710
731
|
const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
|
|
711
732
|
const strokeWidth = num(id, "strokeWidth", node.props.strokeWidth ?? 1);
|
|
712
|
-
const
|
|
713
|
-
const
|
|
733
|
+
const fillP = node.props.fill;
|
|
734
|
+
const strokeP = node.props.stroke;
|
|
735
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
736
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
714
737
|
ops.push({
|
|
715
738
|
type: node.type,
|
|
716
739
|
id,
|
|
@@ -748,17 +771,22 @@
|
|
|
748
771
|
case "path": {
|
|
749
772
|
const ox = num(id, "originX", node.props.originX ?? 0);
|
|
750
773
|
const oy = num(id, "originY", node.props.originY ?? 0);
|
|
751
|
-
const
|
|
752
|
-
const
|
|
774
|
+
const fillP = node.props.fill;
|
|
775
|
+
const strokeP = node.props.stroke;
|
|
776
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
777
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
778
|
+
const dStr = str(id, "d", node.props.d);
|
|
779
|
+
const needsBox = isGradient(fill) || isGradient(stroke);
|
|
753
780
|
ops.push({
|
|
754
781
|
type: "path",
|
|
755
782
|
id,
|
|
756
783
|
transform: ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]),
|
|
757
784
|
opacity,
|
|
758
|
-
d:
|
|
785
|
+
d: dStr,
|
|
759
786
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
760
787
|
...fill !== void 0 && { fill },
|
|
761
788
|
...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
|
|
789
|
+
...needsBox && { bbox: pathBBox(dStr) },
|
|
762
790
|
...clipSpread
|
|
763
791
|
});
|
|
764
792
|
return;
|
|
@@ -806,6 +834,31 @@
|
|
|
806
834
|
}
|
|
807
835
|
|
|
808
836
|
// ../renderer-canvas/src/index.ts
|
|
837
|
+
function resolvePaint(ctx2, paint, box) {
|
|
838
|
+
if (typeof paint === "string") return paint;
|
|
839
|
+
const { x, y, w, h } = box;
|
|
840
|
+
let g;
|
|
841
|
+
if (paint.kind === "linear") {
|
|
842
|
+
const a = (paint.angle ?? 0) * Math.PI / 180;
|
|
843
|
+
const dx = Math.cos(a);
|
|
844
|
+
const dy = Math.sin(a);
|
|
845
|
+
const cx = x + w / 2;
|
|
846
|
+
const cy = y + h / 2;
|
|
847
|
+
const half = Math.abs(dx) * (w / 2) + Math.abs(dy) * (h / 2);
|
|
848
|
+
g = ctx2.createLinearGradient(cx - dx * half, cy - dy * half, cx + dx * half, cy + dy * half);
|
|
849
|
+
} else if (paint.kind === "radial") {
|
|
850
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
851
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
852
|
+
const r = Math.max((paint.r ?? 0.5) * Math.max(w, h), 1e-4);
|
|
853
|
+
g = ctx2.createRadialGradient(cx, cy, 0, cx, cy, r);
|
|
854
|
+
} else {
|
|
855
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
856
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
857
|
+
g = ctx2.createConicGradient((paint.angle ?? 0) * Math.PI / 180, cx, cy);
|
|
858
|
+
}
|
|
859
|
+
for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
|
|
860
|
+
return g;
|
|
861
|
+
}
|
|
809
862
|
function renderFrame(ctx2, compiled2, t, images2) {
|
|
810
863
|
const { size, background } = compiled2.ir;
|
|
811
864
|
ctx2.setTransform(1, 0, 0, 1, 0, 0);
|
|
@@ -838,6 +891,7 @@
|
|
|
838
891
|
ctx2.globalAlpha = Math.max(0, Math.min(1, op.opacity));
|
|
839
892
|
switch (op.type) {
|
|
840
893
|
case "rect": {
|
|
894
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
841
895
|
ctx2.beginPath();
|
|
842
896
|
if (op.radius && op.radius > 0) {
|
|
843
897
|
ctx2.roundRect(op.offsetX, op.offsetY, op.width, op.height, op.radius);
|
|
@@ -845,17 +899,18 @@
|
|
|
845
899
|
ctx2.rect(op.offsetX, op.offsetY, op.width, op.height);
|
|
846
900
|
}
|
|
847
901
|
if (op.fill) {
|
|
848
|
-
ctx2.fillStyle = op.fill;
|
|
902
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
849
903
|
ctx2.fill();
|
|
850
904
|
}
|
|
851
905
|
if (op.stroke) {
|
|
852
|
-
ctx2.strokeStyle = op.stroke;
|
|
906
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
853
907
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
854
908
|
ctx2.stroke();
|
|
855
909
|
}
|
|
856
910
|
break;
|
|
857
911
|
}
|
|
858
912
|
case "ellipse": {
|
|
913
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
859
914
|
ctx2.beginPath();
|
|
860
915
|
ctx2.ellipse(
|
|
861
916
|
op.offsetX + op.width / 2,
|
|
@@ -867,11 +922,11 @@
|
|
|
867
922
|
Math.PI * 2
|
|
868
923
|
);
|
|
869
924
|
if (op.fill) {
|
|
870
|
-
ctx2.fillStyle = op.fill;
|
|
925
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
871
926
|
ctx2.fill();
|
|
872
927
|
}
|
|
873
928
|
if (op.stroke) {
|
|
874
|
-
ctx2.strokeStyle = op.stroke;
|
|
929
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
875
930
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
876
931
|
ctx2.stroke();
|
|
877
932
|
}
|
|
@@ -908,12 +963,13 @@
|
|
|
908
963
|
}
|
|
909
964
|
case "path": {
|
|
910
965
|
const p = new Path2D(op.d);
|
|
966
|
+
const box = op.bbox ? { x: op.bbox[0], y: op.bbox[1], w: op.bbox[2], h: op.bbox[3] } : { x: 0, y: 0, w: 1, h: 1 };
|
|
911
967
|
if (op.fill) {
|
|
912
|
-
ctx2.fillStyle = op.fill;
|
|
968
|
+
ctx2.fillStyle = resolvePaint(ctx2, op.fill, box);
|
|
913
969
|
ctx2.fill(p);
|
|
914
970
|
}
|
|
915
971
|
if (op.stroke && (op.strokeWidth ?? 1) > 0) {
|
|
916
|
-
ctx2.strokeStyle = op.stroke;
|
|
972
|
+
ctx2.strokeStyle = resolvePaint(ctx2, op.stroke, box);
|
|
917
973
|
ctx2.lineWidth = op.strokeWidth ?? 1;
|
|
918
974
|
ctx2.lineJoin = "round";
|
|
919
975
|
ctx2.lineCap = "round";
|
package/dist/cli.js
CHANGED
|
@@ -347,12 +347,34 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
347
347
|
function validateScene(ir) {
|
|
348
348
|
const problems = [];
|
|
349
349
|
const nodeById = /* @__PURE__ */ new Map();
|
|
350
|
+
const checkPaint = (where, value) => {
|
|
351
|
+
if (typeof value !== "object" || value === null) return;
|
|
352
|
+
const g = value;
|
|
353
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
354
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
358
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
g.stops.forEach((s, i) => {
|
|
362
|
+
const st = s;
|
|
363
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
364
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
365
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
366
|
+
}
|
|
367
|
+
});
|
|
368
|
+
};
|
|
350
369
|
const collect = (nodes) => {
|
|
351
370
|
for (const node of nodes) {
|
|
352
371
|
if (nodeById.has(node.id)) {
|
|
353
372
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
354
373
|
}
|
|
355
374
|
nodeById.set(node.id, node);
|
|
375
|
+
const props = node.props;
|
|
376
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
377
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
356
378
|
if (node.type === "group") {
|
|
357
379
|
const clip = node.props.clip;
|
|
358
380
|
if (clip) {
|
package/dist/index.js
CHANGED
|
@@ -6,6 +6,22 @@ var DEFAULT_MOTIONPATH_DURATION = 1;
|
|
|
6
6
|
var DEFAULT_FPS = 30;
|
|
7
7
|
|
|
8
8
|
// ../core/src/path.ts
|
|
9
|
+
function pathBBox(d) {
|
|
10
|
+
const nums = d.match(/-?\d*\.?\d+(?:e[-+]?\d+)?/gi);
|
|
11
|
+
if (!nums || nums.length < 2) return [0, 0, 1, 1];
|
|
12
|
+
let minx = Infinity, miny = Infinity, maxx = -Infinity, maxy = -Infinity;
|
|
13
|
+
for (let i = 0; i + 1 < nums.length; i += 2) {
|
|
14
|
+
const x = parseFloat(nums[i]);
|
|
15
|
+
const y = parseFloat(nums[i + 1]);
|
|
16
|
+
if (x < minx) minx = x;
|
|
17
|
+
if (x > maxx) maxx = x;
|
|
18
|
+
if (y < miny) miny = y;
|
|
19
|
+
if (y > maxy) maxy = y;
|
|
20
|
+
}
|
|
21
|
+
const w = maxx - minx;
|
|
22
|
+
const h = maxy - miny;
|
|
23
|
+
return [minx, miny, w > 0 ? w : 1, h > 0 ? h : 1];
|
|
24
|
+
}
|
|
9
25
|
function locate(segCount, u) {
|
|
10
26
|
if (segCount <= 0) return { i: 0, t: 0 };
|
|
11
27
|
const clamped = Math.max(0, Math.min(1, u));
|
|
@@ -341,12 +357,34 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
341
357
|
function validateScene(ir) {
|
|
342
358
|
const problems = [];
|
|
343
359
|
const nodeById = /* @__PURE__ */ new Map();
|
|
360
|
+
const checkPaint = (where, value) => {
|
|
361
|
+
if (typeof value !== "object" || value === null) return;
|
|
362
|
+
const g = value;
|
|
363
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
364
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
368
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
g.stops.forEach((s, i) => {
|
|
372
|
+
const st = s;
|
|
373
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
374
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
375
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
};
|
|
344
379
|
const collect = (nodes) => {
|
|
345
380
|
for (const node of nodes) {
|
|
346
381
|
if (nodeById.has(node.id)) {
|
|
347
382
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
348
383
|
}
|
|
349
384
|
nodeById.set(node.id, node);
|
|
385
|
+
const props = node.props;
|
|
386
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
387
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
350
388
|
if (node.type === "group") {
|
|
351
389
|
const clip = node.props.clip;
|
|
352
390
|
if (clip) {
|
|
@@ -917,6 +955,38 @@ function cameraTo(props, opts = {}) {
|
|
|
917
955
|
return tween(CAMERA_ID, props, opts);
|
|
918
956
|
}
|
|
919
957
|
|
|
958
|
+
// ../core/src/gradient.ts
|
|
959
|
+
function isGradient(p) {
|
|
960
|
+
return typeof p === "object" && p !== null && (p.kind === "linear" || p.kind === "radial" || p.kind === "conic");
|
|
961
|
+
}
|
|
962
|
+
function toStops(stops) {
|
|
963
|
+
if (stops.length > 0 && typeof stops[0] === "object") return stops;
|
|
964
|
+
const cs = stops;
|
|
965
|
+
const n3 = cs.length;
|
|
966
|
+
return cs.map((color, i) => ({ offset: n3 <= 1 ? 0 : i / (n3 - 1), color }));
|
|
967
|
+
}
|
|
968
|
+
function linearGradient(stops, opts = {}) {
|
|
969
|
+
return { kind: "linear", ...opts.angle !== void 0 && { angle: opts.angle }, stops: toStops(stops) };
|
|
970
|
+
}
|
|
971
|
+
function radialGradient(stops, opts = {}) {
|
|
972
|
+
return {
|
|
973
|
+
kind: "radial",
|
|
974
|
+
...opts.cx !== void 0 && { cx: opts.cx },
|
|
975
|
+
...opts.cy !== void 0 && { cy: opts.cy },
|
|
976
|
+
...opts.r !== void 0 && { r: opts.r },
|
|
977
|
+
stops: toStops(stops)
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
function conicGradient(stops, opts = {}) {
|
|
981
|
+
return {
|
|
982
|
+
kind: "conic",
|
|
983
|
+
...opts.angle !== void 0 && { angle: opts.angle },
|
|
984
|
+
...opts.cx !== void 0 && { cx: opts.cx },
|
|
985
|
+
...opts.cy !== void 0 && { cy: opts.cy },
|
|
986
|
+
stops: toStops(stops)
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
|
|
920
990
|
// ../core/src/presets.ts
|
|
921
991
|
var PRESET_NAMES = [
|
|
922
992
|
"draw-bloom",
|
|
@@ -2919,8 +2989,10 @@ function evaluate(compiled, t) {
|
|
|
2919
2989
|
const height = num(id, "height", node.props.height);
|
|
2920
2990
|
const [ax, ay] = ANCHOR_FACTORS[node.props.anchor ?? "top-left"];
|
|
2921
2991
|
const strokeWidth = num(id, "strokeWidth", node.props.strokeWidth ?? 1);
|
|
2922
|
-
const
|
|
2923
|
-
const
|
|
2992
|
+
const fillP = node.props.fill;
|
|
2993
|
+
const strokeP = node.props.stroke;
|
|
2994
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
2995
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
2924
2996
|
ops.push({
|
|
2925
2997
|
type: node.type,
|
|
2926
2998
|
id,
|
|
@@ -2958,17 +3030,22 @@ function evaluate(compiled, t) {
|
|
|
2958
3030
|
case "path": {
|
|
2959
3031
|
const ox = num(id, "originX", node.props.originX ?? 0);
|
|
2960
3032
|
const oy = num(id, "originY", node.props.originY ?? 0);
|
|
2961
|
-
const
|
|
2962
|
-
const
|
|
3033
|
+
const fillP = node.props.fill;
|
|
3034
|
+
const strokeP = node.props.stroke;
|
|
3035
|
+
const fill = isGradient(fillP) ? fillP : opt(id, "fill", fillP);
|
|
3036
|
+
const stroke = isGradient(strokeP) ? strokeP : opt(id, "stroke", strokeP);
|
|
3037
|
+
const dStr = str(id, "d", node.props.d);
|
|
3038
|
+
const needsBox = isGradient(fill) || isGradient(stroke);
|
|
2963
3039
|
ops.push({
|
|
2964
3040
|
type: "path",
|
|
2965
3041
|
id,
|
|
2966
3042
|
transform: ox === 0 && oy === 0 ? matrix : multiply(matrix, [1, 0, 0, 1, -ox, -oy]),
|
|
2967
3043
|
opacity,
|
|
2968
|
-
d:
|
|
3044
|
+
d: dStr,
|
|
2969
3045
|
progress: Math.max(0, Math.min(1, num(id, "progress", node.props.progress ?? 1))),
|
|
2970
3046
|
...fill !== void 0 && { fill },
|
|
2971
3047
|
...stroke !== void 0 && { stroke, strokeWidth: num(id, "strokeWidth", node.props.strokeWidth ?? 1) },
|
|
3048
|
+
...needsBox && { bbox: pathBBox(dStr) },
|
|
2972
3049
|
...clipSpread
|
|
2973
3050
|
});
|
|
2974
3051
|
return;
|
|
@@ -3116,6 +3193,7 @@ export {
|
|
|
3116
3193
|
compileScene,
|
|
3117
3194
|
composeScene,
|
|
3118
3195
|
composition,
|
|
3196
|
+
conicGradient,
|
|
3119
3197
|
cursor,
|
|
3120
3198
|
cursorClick,
|
|
3121
3199
|
cursorDouble,
|
|
@@ -3135,8 +3213,10 @@ export {
|
|
|
3135
3213
|
ikReach,
|
|
3136
3214
|
image,
|
|
3137
3215
|
isColor,
|
|
3216
|
+
isGradient,
|
|
3138
3217
|
lerpValue,
|
|
3139
3218
|
line,
|
|
3219
|
+
linearGradient,
|
|
3140
3220
|
motionOp,
|
|
3141
3221
|
motionOpLabel,
|
|
3142
3222
|
motionPath,
|
|
@@ -3149,6 +3229,7 @@ export {
|
|
|
3149
3229
|
pathPoint,
|
|
3150
3230
|
pathTangentAngle,
|
|
3151
3231
|
poseTo,
|
|
3232
|
+
radialGradient,
|
|
3152
3233
|
rect,
|
|
3153
3234
|
resolveAudioPlan,
|
|
3154
3235
|
resolveCompositionAudioPlan,
|
package/dist/labels.js
CHANGED
|
@@ -341,12 +341,34 @@ ${problems.map((p) => ` - ${p}`).join("\n")}`);
|
|
|
341
341
|
function validateScene(ir) {
|
|
342
342
|
const problems = [];
|
|
343
343
|
const nodeById = /* @__PURE__ */ new Map();
|
|
344
|
+
const checkPaint = (where, value) => {
|
|
345
|
+
if (typeof value !== "object" || value === null) return;
|
|
346
|
+
const g = value;
|
|
347
|
+
if (g.kind !== "linear" && g.kind !== "radial" && g.kind !== "conic") {
|
|
348
|
+
problems.push(`${where}: a paint object must be a gradient with kind "linear" / "radial" / "conic"`);
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
if (!Array.isArray(g.stops) || g.stops.length === 0) {
|
|
352
|
+
problems.push(`${where}: gradient "${g.kind}" needs at least one color stop`);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
g.stops.forEach((s, i) => {
|
|
356
|
+
const st = s;
|
|
357
|
+
if (typeof st?.color !== "string") problems.push(`${where}: gradient stop ${i} needs a color string`);
|
|
358
|
+
if (typeof st?.offset !== "number" || st.offset < 0 || st.offset > 1) {
|
|
359
|
+
problems.push(`${where}: gradient stop ${i} "offset" must be a number in 0..1`);
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
};
|
|
344
363
|
const collect = (nodes) => {
|
|
345
364
|
for (const node of nodes) {
|
|
346
365
|
if (nodeById.has(node.id)) {
|
|
347
366
|
problems.push(`duplicate node id "${node.id}" \u2014 every node id must be unique`);
|
|
348
367
|
}
|
|
349
368
|
nodeById.set(node.id, node);
|
|
369
|
+
const props = node.props;
|
|
370
|
+
checkPaint(`node "${node.id}" fill`, props.fill);
|
|
371
|
+
checkPaint(`node "${node.id}" stroke`, props.stroke);
|
|
350
372
|
if (node.type === "group") {
|
|
351
373
|
const clip = node.props.clip;
|
|
352
374
|
if (clip) {
|
package/dist/renderer-canvas.js
CHANGED
|
@@ -1,5 +1,30 @@
|
|
|
1
1
|
// ../renderer-canvas/src/index.ts
|
|
2
2
|
import { evaluate } from "@reframe/core";
|
|
3
|
+
function resolvePaint(ctx, paint, box) {
|
|
4
|
+
if (typeof paint === "string") return paint;
|
|
5
|
+
const { x, y, w, h } = box;
|
|
6
|
+
let g;
|
|
7
|
+
if (paint.kind === "linear") {
|
|
8
|
+
const a = (paint.angle ?? 0) * Math.PI / 180;
|
|
9
|
+
const dx = Math.cos(a);
|
|
10
|
+
const dy = Math.sin(a);
|
|
11
|
+
const cx = x + w / 2;
|
|
12
|
+
const cy = y + h / 2;
|
|
13
|
+
const half = Math.abs(dx) * (w / 2) + Math.abs(dy) * (h / 2);
|
|
14
|
+
g = ctx.createLinearGradient(cx - dx * half, cy - dy * half, cx + dx * half, cy + dy * half);
|
|
15
|
+
} else if (paint.kind === "radial") {
|
|
16
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
17
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
18
|
+
const r = Math.max((paint.r ?? 0.5) * Math.max(w, h), 1e-4);
|
|
19
|
+
g = ctx.createRadialGradient(cx, cy, 0, cx, cy, r);
|
|
20
|
+
} else {
|
|
21
|
+
const cx = x + (paint.cx ?? 0.5) * w;
|
|
22
|
+
const cy = y + (paint.cy ?? 0.5) * h;
|
|
23
|
+
g = ctx.createConicGradient((paint.angle ?? 0) * Math.PI / 180, cx, cy);
|
|
24
|
+
}
|
|
25
|
+
for (const s of paint.stops) g.addColorStop(Math.max(0, Math.min(1, s.offset)), s.color);
|
|
26
|
+
return g;
|
|
27
|
+
}
|
|
3
28
|
function renderFrame(ctx, compiled, t, images) {
|
|
4
29
|
const { size, background } = compiled.ir;
|
|
5
30
|
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
@@ -32,6 +57,7 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
32
57
|
ctx.globalAlpha = Math.max(0, Math.min(1, op.opacity));
|
|
33
58
|
switch (op.type) {
|
|
34
59
|
case "rect": {
|
|
60
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
35
61
|
ctx.beginPath();
|
|
36
62
|
if (op.radius && op.radius > 0) {
|
|
37
63
|
ctx.roundRect(op.offsetX, op.offsetY, op.width, op.height, op.radius);
|
|
@@ -39,17 +65,18 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
39
65
|
ctx.rect(op.offsetX, op.offsetY, op.width, op.height);
|
|
40
66
|
}
|
|
41
67
|
if (op.fill) {
|
|
42
|
-
ctx.fillStyle = op.fill;
|
|
68
|
+
ctx.fillStyle = resolvePaint(ctx, op.fill, box);
|
|
43
69
|
ctx.fill();
|
|
44
70
|
}
|
|
45
71
|
if (op.stroke) {
|
|
46
|
-
ctx.strokeStyle = op.stroke;
|
|
72
|
+
ctx.strokeStyle = resolvePaint(ctx, op.stroke, box);
|
|
47
73
|
ctx.lineWidth = op.strokeWidth ?? 1;
|
|
48
74
|
ctx.stroke();
|
|
49
75
|
}
|
|
50
76
|
break;
|
|
51
77
|
}
|
|
52
78
|
case "ellipse": {
|
|
79
|
+
const box = { x: op.offsetX, y: op.offsetY, w: op.width, h: op.height };
|
|
53
80
|
ctx.beginPath();
|
|
54
81
|
ctx.ellipse(
|
|
55
82
|
op.offsetX + op.width / 2,
|
|
@@ -61,11 +88,11 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
61
88
|
Math.PI * 2
|
|
62
89
|
);
|
|
63
90
|
if (op.fill) {
|
|
64
|
-
ctx.fillStyle = op.fill;
|
|
91
|
+
ctx.fillStyle = resolvePaint(ctx, op.fill, box);
|
|
65
92
|
ctx.fill();
|
|
66
93
|
}
|
|
67
94
|
if (op.stroke) {
|
|
68
|
-
ctx.strokeStyle = op.stroke;
|
|
95
|
+
ctx.strokeStyle = resolvePaint(ctx, op.stroke, box);
|
|
69
96
|
ctx.lineWidth = op.strokeWidth ?? 1;
|
|
70
97
|
ctx.stroke();
|
|
71
98
|
}
|
|
@@ -102,12 +129,13 @@ function drawDisplayList(ctx, ops, images) {
|
|
|
102
129
|
}
|
|
103
130
|
case "path": {
|
|
104
131
|
const p = new Path2D(op.d);
|
|
132
|
+
const box = op.bbox ? { x: op.bbox[0], y: op.bbox[1], w: op.bbox[2], h: op.bbox[3] } : { x: 0, y: 0, w: 1, h: 1 };
|
|
105
133
|
if (op.fill) {
|
|
106
|
-
ctx.fillStyle = op.fill;
|
|
134
|
+
ctx.fillStyle = resolvePaint(ctx, op.fill, box);
|
|
107
135
|
ctx.fill(p);
|
|
108
136
|
}
|
|
109
137
|
if (op.stroke && (op.strokeWidth ?? 1) > 0) {
|
|
110
|
-
ctx.strokeStyle = op.stroke;
|
|
138
|
+
ctx.strokeStyle = resolvePaint(ctx, op.stroke, box);
|
|
111
139
|
ctx.lineWidth = op.strokeWidth ?? 1;
|
|
112
140
|
ctx.lineJoin = "round";
|
|
113
141
|
ctx.lineCap = "round";
|
package/dist/types/evaluate.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* always. Renderers only draw; they never compute animation.
|
|
5
5
|
*/
|
|
6
6
|
import type { CompiledScene } from "./compile.js";
|
|
7
|
-
import type { ClipShape, PropValue } from "./ir.js";
|
|
7
|
+
import type { ClipShape, Paint, PropValue } from "./ir.js";
|
|
8
8
|
/** Canvas-style 2D affine matrix [a, b, c, d, e, f]. */
|
|
9
9
|
export type Mat2D = [number, number, number, number, number, number];
|
|
10
10
|
/** A clip from an ancestor group: its shape in the group's coordinate space,
|
|
@@ -31,8 +31,8 @@ export type DisplayOp = (OpBase & {
|
|
|
31
31
|
height: number;
|
|
32
32
|
offsetX: number;
|
|
33
33
|
offsetY: number;
|
|
34
|
-
fill?:
|
|
35
|
-
stroke?:
|
|
34
|
+
fill?: Paint;
|
|
35
|
+
stroke?: Paint;
|
|
36
36
|
strokeWidth?: number;
|
|
37
37
|
radius?: number;
|
|
38
38
|
}) | (OpBase & {
|
|
@@ -41,8 +41,8 @@ export type DisplayOp = (OpBase & {
|
|
|
41
41
|
height: number;
|
|
42
42
|
offsetX: number;
|
|
43
43
|
offsetY: number;
|
|
44
|
-
fill?:
|
|
45
|
-
stroke?:
|
|
44
|
+
fill?: Paint;
|
|
45
|
+
stroke?: Paint;
|
|
46
46
|
strokeWidth?: number;
|
|
47
47
|
}) | (OpBase & {
|
|
48
48
|
type: "line";
|
|
@@ -76,9 +76,11 @@ export type DisplayOp = (OpBase & {
|
|
|
76
76
|
d: string;
|
|
77
77
|
/** 0..1 fraction of the stroke outline drawn (draw-on). */
|
|
78
78
|
progress: number;
|
|
79
|
-
fill?:
|
|
80
|
-
stroke?:
|
|
79
|
+
fill?: Paint;
|
|
80
|
+
stroke?: Paint;
|
|
81
81
|
strokeWidth?: number;
|
|
82
|
+
/** Local-space bbox [x,y,w,h] for mapping a gradient paint (set only when one is used). */
|
|
83
|
+
bbox?: [number, number, number, number];
|
|
82
84
|
});
|
|
83
85
|
export type DisplayList = DisplayOp[];
|
|
84
86
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Gradient paints — the structured alternative to a solid color `fill`/`stroke`.
|
|
3
|
+
*
|
|
4
|
+
* Coordinates are normalized to the node's bounding box (0..1), so a gradient is
|
|
5
|
+
* just an angle + color stops, independent of the node's size. The renderer maps
|
|
6
|
+
* 0..1 → the node's local box and the paint is applied in node-local space, so
|
|
7
|
+
* rotating/scaling the NODE moves the gradient with it (the "animated gradient"
|
|
8
|
+
* idiom — gradients themselves are static this version). Pure + deterministic.
|
|
9
|
+
*/
|
|
10
|
+
import type { ColorStop, Gradient, Paint } from "./ir.js";
|
|
11
|
+
/** True when a paint is a gradient object (vs a plain color string). */
|
|
12
|
+
export declare function isGradient(p: Paint | undefined): p is Gradient;
|
|
13
|
+
/** A linear gradient. `angle` in degrees: 0 = left→right, 90 = top→bottom. */
|
|
14
|
+
export declare function linearGradient(stops: (string | ColorStop)[], opts?: {
|
|
15
|
+
angle?: number;
|
|
16
|
+
}): Gradient;
|
|
17
|
+
/** A radial gradient. `cx/cy` centre (0..1 of the box, default 0.5), `r` radius (0..1, default 0.5). */
|
|
18
|
+
export declare function radialGradient(stops: (string | ColorStop)[], opts?: {
|
|
19
|
+
cx?: number;
|
|
20
|
+
cy?: number;
|
|
21
|
+
r?: number;
|
|
22
|
+
}): Gradient;
|
|
23
|
+
/** A conic (angular sweep) gradient. `angle` start in degrees, `cx/cy` centre (0..1). */
|
|
24
|
+
export declare function conicGradient(stops: (string | ColorStop)[], opts?: {
|
|
25
|
+
angle?: number;
|
|
26
|
+
cx?: number;
|
|
27
|
+
cy?: number;
|
|
28
|
+
}): Gradient;
|
package/dist/types/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export { composeScene, formatComposeReport, type OverlayDoc, type ComposeReport,
|
|
|
6
6
|
export { compileScene, type CompiledScene, type PropertySegment, type LabelSpan, type MotionDriver } from "./compile.js";
|
|
7
7
|
export { pathPoint, pathTangentAngle, type Pt } from "./path.js";
|
|
8
8
|
export { cameraTo, cameraMatrix, CAMERA_ID, CAMERA_PROPS } from "./camera.js";
|
|
9
|
+
export { linearGradient, radialGradient, conicGradient, isGradient } from "./gradient.js";
|
|
9
10
|
export { motionPreset, PRESET_NAMES, type PresetName, type PresetRig, type PresetOpts } from "./presets.js";
|
|
10
11
|
export { devicePreset, deviceScreen, deviceScreenCenter, deviceBounds, deviceScreenPoint, DEVICE_PRESET_NAMES, type DevicePresetName, type DevicePresetOpts } from "./devicePreset.js";
|
|
11
12
|
export { cursor, cursorTo, cursorPath, cursorClick, cursorDouble, type CursorStyle, type CursorOpts, type CursorToOpts, type CursorPathOpts, type CursorClickOpts } from "./cursor.js";
|
package/dist/types/ir.d.ts
CHANGED
|
@@ -37,19 +37,48 @@ export interface BaseProps {
|
|
|
37
37
|
*/
|
|
38
38
|
fixed?: boolean;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* A paint is a solid color string OR a gradient. Coordinates are normalized to the
|
|
42
|
+
* node's bounding box (0..1, SVG `objectBoundingBox` style) so a gradient is just an
|
|
43
|
+
* angle + stops, size-independent. Applied in node-local space, so animating the
|
|
44
|
+
* node's transform (rotation/scale) moves the gradient with it. Build with
|
|
45
|
+
* `linearGradient`/`radialGradient`/`conicGradient` (`gradient.ts`).
|
|
46
|
+
*/
|
|
47
|
+
export interface ColorStop {
|
|
48
|
+
offset: number;
|
|
49
|
+
color: string;
|
|
50
|
+
}
|
|
51
|
+
export type Gradient = {
|
|
52
|
+
kind: "linear";
|
|
53
|
+
angle?: number;
|
|
54
|
+
stops: ColorStop[];
|
|
55
|
+
} | {
|
|
56
|
+
kind: "radial";
|
|
57
|
+
cx?: number;
|
|
58
|
+
cy?: number;
|
|
59
|
+
r?: number;
|
|
60
|
+
stops: ColorStop[];
|
|
61
|
+
} | {
|
|
62
|
+
kind: "conic";
|
|
63
|
+
angle?: number;
|
|
64
|
+
cx?: number;
|
|
65
|
+
cy?: number;
|
|
66
|
+
stops: ColorStop[];
|
|
67
|
+
};
|
|
68
|
+
export type Paint = string | Gradient;
|
|
40
69
|
export interface RectProps extends BaseProps {
|
|
41
70
|
width: number;
|
|
42
71
|
height: number;
|
|
43
|
-
fill?:
|
|
44
|
-
stroke?:
|
|
72
|
+
fill?: Paint;
|
|
73
|
+
stroke?: Paint;
|
|
45
74
|
strokeWidth?: number;
|
|
46
75
|
radius?: number;
|
|
47
76
|
}
|
|
48
77
|
export interface EllipseProps extends BaseProps {
|
|
49
78
|
width: number;
|
|
50
79
|
height: number;
|
|
51
|
-
fill?:
|
|
52
|
-
stroke?:
|
|
80
|
+
fill?: Paint;
|
|
81
|
+
stroke?: Paint;
|
|
53
82
|
strokeWidth?: number;
|
|
54
83
|
}
|
|
55
84
|
export interface LineProps {
|
|
@@ -104,8 +133,8 @@ export interface GroupProps extends BaseProps {
|
|
|
104
133
|
export interface PathProps extends BaseProps {
|
|
105
134
|
/** SVG path data (the `d` attribute). Drawn as a true vector — crisp at any zoom. */
|
|
106
135
|
d: string;
|
|
107
|
-
fill?:
|
|
108
|
-
stroke?:
|
|
136
|
+
fill?: Paint;
|
|
137
|
+
stroke?: Paint;
|
|
109
138
|
strokeWidth?: number;
|
|
110
139
|
/**
|
|
111
140
|
* 0..1 — fraction of the OUTLINE drawn, for a self-drawing "draw-on" effect
|
package/dist/types/path.d.ts
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
* spacing ever overshoots.
|
|
10
10
|
*/
|
|
11
11
|
export type Pt = [number, number];
|
|
12
|
+
/**
|
|
13
|
+
* A loose bounding box `[x, y, w, h]` from a path `d`'s coordinate extents — used
|
|
14
|
+
* only to map a gradient across the shape. Exact for M/L/C/Q/S/T paths (every
|
|
15
|
+
* number is an x/y coord, control points included); loose for the rare H/V/A.
|
|
16
|
+
*/
|
|
17
|
+
export declare function pathBBox(d: string): [number, number, number, number];
|
|
12
18
|
/**
|
|
13
19
|
* Position on the spline at progress u in [0,1]. `curviness` scales the
|
|
14
20
|
* Catmull-Rom tangents (GSAP's idea): 1 = standard smooth (the default and the
|
package/guides/edsl-guide.md
CHANGED
|
@@ -150,6 +150,28 @@ scene({
|
|
|
150
150
|
|
|
151
151
|
See `examples/scenes/camera-demo.ts`.
|
|
152
152
|
|
|
153
|
+
## Gradients (fill / stroke)
|
|
154
|
+
|
|
155
|
+
`fill` and `stroke` on **rect / ellipse / path** accept a gradient as well as a
|
|
156
|
+
color string. Coordinates are normalized to the node's bounding box (0..1), so a
|
|
157
|
+
gradient is just an angle + stops:
|
|
158
|
+
|
|
159
|
+
```ts
|
|
160
|
+
rect({ id: "card", x, y, width: 300, height: 300, anchor: "center",
|
|
161
|
+
fill: linearGradient(["#FF5C3A", "#FFC24B"], { angle: 60 }) }) // 0=L→R, 90=T→B
|
|
162
|
+
ellipse({ id: "orb", /* … */ fill: radialGradient(["#9B7CFF", "#221A4A"], { cx: 0.4, cy: 0.4, r: 0.6 }) })
|
|
163
|
+
path({ id: "star", d, fill: conicGradient(["#00C2A8", "#3AA0FF", "#7C5CFF", "#00C2A8"], { angle: -90 }) })
|
|
164
|
+
ellipse({ id: "ring", /* … */ fill: "none", stroke: linearGradient(["#3AA0FF", "#46E5A0"]), strokeWidth: 10 })
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
- `linearGradient(stops, { angle })`, `radialGradient(stops, { cx, cy, r })`,
|
|
168
|
+
`conicGradient(stops, { angle, cx, cy })`. `stops` is a color array (even offsets)
|
|
169
|
+
or `[{ offset, color }]`. `cx/cy/r` are 0..1 of the box (centre defaults to 0.5).
|
|
170
|
+
- **Gradients are static** (not keyframed). The gradient lives in the node's local
|
|
171
|
+
space, so **animate the NODE** (`tween(id, { rotation: 360 })`, scale, move) and the
|
|
172
|
+
gradient sweeps/stretches with it. Color-string fills still tween as today.
|
|
173
|
+
- text fill and line stroke are color-only for now. See `examples/scenes/gradient-demo.ts`.
|
|
174
|
+
|
|
153
175
|
## Character rig (skeleton, poses, IK)
|
|
154
176
|
|
|
155
177
|
A first-class, declarative character rig that **compiles to plain IR** (nested
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "reframe-video",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Declarative motion graphics that AI can write and humans can tweak — human edits survive AI regeneration. Deterministic mp4 renders from a plain-data scene format.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"motion-graphics",
|