@pixerate/flair 0.1.1 → 0.3.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 +21 -0
- package/README.md +65 -14
- package/dist/catalog/index.d.ts +5 -4
- package/dist/catalog/index.js +12 -4
- package/dist/{chunk-JWZCPMFG.js → chunk-D7NLHC7K.js} +117 -674
- package/dist/chunk-D7NLHC7K.js.map +1 -0
- package/dist/{chunk-3FAUZOGL.js → chunk-FIJOMGPG.js} +2 -2
- package/dist/{chunk-UQQNZSYL.js → chunk-GTIAOYOK.js} +211 -7
- package/dist/chunk-GTIAOYOK.js.map +1 -0
- package/dist/{chunk-MWVTQFV5.js → chunk-IWKM5FJK.js} +410 -59
- package/dist/chunk-IWKM5FJK.js.map +1 -0
- package/dist/{chunk-HKXOHRCD.js → chunk-VA74HBPK.js} +2 -2
- package/dist/chunk-WAMA45G3.js +998 -0
- package/dist/chunk-WAMA45G3.js.map +1 -0
- package/dist/{chunk-J55UBNAT.js → chunk-WC3VADZC.js} +42 -36
- package/dist/chunk-WC3VADZC.js.map +1 -0
- package/dist/cli.js +12 -11
- package/dist/cli.js.map +1 -1
- package/dist/{controls-DdXMbovi.d.ts → controls-Syr6drzO.d.ts} +4 -3
- package/dist/editor/index.d.ts +101 -49
- package/dist/editor/index.js +198 -99
- package/dist/editor/index.js.map +1 -1
- package/dist/{export-FEBPEEO7.js → export-DH2HMACO.js} +4 -3
- package/dist/index.d.ts +193 -17
- package/dist/index.js +69 -33
- package/dist/index.js.map +1 -1
- package/dist/node.d.ts +1 -1
- package/dist/node.js +2 -2
- package/dist/recorder-KEX3U35D.js +10 -0
- package/dist/{renderer-Oyynz-fs.d.ts → renderer-mzvhKzKa.d.ts} +1 -1
- package/dist/types-CsFUZZIv.d.ts +102 -0
- package/package.json +3 -3
- package/dist/chunk-36DTNSHU.js +0 -160
- package/dist/chunk-36DTNSHU.js.map +0 -1
- package/dist/chunk-J55UBNAT.js.map +0 -1
- package/dist/chunk-JWZCPMFG.js.map +0 -1
- package/dist/chunk-MWVTQFV5.js.map +0 -1
- package/dist/chunk-UQQNZSYL.js.map +0 -1
- package/dist/recorder-WJTWMEQN.js +0 -10
- package/dist/types-CkKkavvB.d.ts +0 -71
- /package/dist/{chunk-3FAUZOGL.js.map → chunk-FIJOMGPG.js.map} +0 -0
- /package/dist/{chunk-HKXOHRCD.js.map → chunk-VA74HBPK.js.map} +0 -0
- /package/dist/{export-FEBPEEO7.js.map → export-DH2HMACO.js.map} +0 -0
- /package/dist/{recorder-WJTWMEQN.js.map → recorder-KEX3U35D.js.map} +0 -0
package/dist/editor/index.js
CHANGED
|
@@ -3,8 +3,9 @@ import {
|
|
|
3
3
|
findEffects,
|
|
4
4
|
getEffect,
|
|
5
5
|
renderVectorEffect,
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
vectorControlIssues,
|
|
7
|
+
vectorEffectControls
|
|
8
|
+
} from "../chunk-IWKM5FJK.js";
|
|
8
9
|
import {
|
|
9
10
|
svgMotionIssues,
|
|
10
11
|
svgView
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
import {
|
|
13
14
|
BUILTIN_UNIFORMS,
|
|
14
15
|
interpolateOklab
|
|
15
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-VA74HBPK.js";
|
|
16
17
|
import "../chunk-EHDJ3CPJ.js";
|
|
17
18
|
import {
|
|
18
19
|
realClock
|
|
@@ -21,88 +22,22 @@ import {
|
|
|
21
22
|
checkControlValue,
|
|
22
23
|
controlValue,
|
|
23
24
|
parseControls
|
|
24
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-WC3VADZC.js";
|
|
25
26
|
|
|
26
27
|
// src/editor/timeline.ts
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"hold"
|
|
37
|
-
];
|
|
38
|
-
var BEZIERS = {
|
|
39
|
-
ease: [0.25, 0.1, 0.25, 1],
|
|
40
|
-
"ease-in": [0.42, 0, 1, 1],
|
|
41
|
-
"ease-out": [0, 0, 0.58, 1],
|
|
42
|
-
"ease-in-out": [0.42, 0, 0.58, 1],
|
|
43
|
-
"ease-in-back": [0.36, 0, 0.66, -0.56],
|
|
44
|
-
"ease-out-back": [0.34, 1.56, 0.64, 1]
|
|
45
|
-
};
|
|
28
|
+
import {
|
|
29
|
+
ANIMATION_EASING_PRESETS,
|
|
30
|
+
cubicBezierAt as editorCubicBezierAt,
|
|
31
|
+
evaluateEasing as evaluateAnimationEasing,
|
|
32
|
+
isAnimationEasing
|
|
33
|
+
} from "@pixerate/editor/animation";
|
|
34
|
+
var EASING_PRESETS = ANIMATION_EASING_PRESETS;
|
|
35
|
+
var evaluateEasing = evaluateAnimationEasing;
|
|
36
|
+
var cubicBezierAt = editorCubicBezierAt;
|
|
46
37
|
function isEasing(value) {
|
|
47
|
-
if (
|
|
48
|
-
if (
|
|
49
|
-
|
|
50
|
-
if (e.type === "cubic-bezier") return ["x1", "y1", "x2", "y2"].every((k) => Number.isFinite(e[k]));
|
|
51
|
-
if (e.type === "spring")
|
|
52
|
-
return ["stiffness", "damping", "mass"].every((k) => e[k] === void 0 || typeof e[k] === "number" && e[k] > 0);
|
|
53
|
-
return false;
|
|
54
|
-
}
|
|
55
|
-
function cubicBezierAt(x1, y1, x2, y2, x) {
|
|
56
|
-
if (x <= 0) return 0;
|
|
57
|
-
if (x >= 1) return 1;
|
|
58
|
-
x1 = Math.min(1, Math.max(0, x1));
|
|
59
|
-
x2 = Math.min(1, Math.max(0, x2));
|
|
60
|
-
const bx = (t2) => 3 * x1 * t2 * (1 - t2) ** 2 + 3 * x2 * t2 * t2 * (1 - t2) + t2 ** 3;
|
|
61
|
-
const by = (t2) => 3 * y1 * t2 * (1 - t2) ** 2 + 3 * y2 * t2 * t2 * (1 - t2) + t2 ** 3;
|
|
62
|
-
const dx = (t2) => 3 * x1 * (1 - t2) ** 2 + 6 * (x2 - x1) * t2 * (1 - t2) + 3 * (1 - x2) * t2 * t2;
|
|
63
|
-
let t = x;
|
|
64
|
-
for (let i = 0; i < 8; i++) {
|
|
65
|
-
const err = bx(t) - x;
|
|
66
|
-
if (Math.abs(err) < 1e-6) return by(t);
|
|
67
|
-
const d = dx(t);
|
|
68
|
-
if (Math.abs(d) < 1e-6) break;
|
|
69
|
-
t -= err / d;
|
|
70
|
-
}
|
|
71
|
-
let lo = 0;
|
|
72
|
-
let hi = 1;
|
|
73
|
-
t = x;
|
|
74
|
-
for (let i = 0; i < 40; i++) {
|
|
75
|
-
if (bx(t) < x) lo = t;
|
|
76
|
-
else hi = t;
|
|
77
|
-
t = (lo + hi) / 2;
|
|
78
|
-
}
|
|
79
|
-
return by(t);
|
|
80
|
-
}
|
|
81
|
-
function springAt(easing, u, durationMs) {
|
|
82
|
-
const k = easing.stiffness ?? 170;
|
|
83
|
-
const c = easing.damping ?? 26;
|
|
84
|
-
const m = easing.mass ?? 1;
|
|
85
|
-
const w0 = Math.sqrt(k / m);
|
|
86
|
-
const zeta = c / (2 * Math.sqrt(k * m));
|
|
87
|
-
const t = u * Math.max(durationMs / 1e3, 0.6);
|
|
88
|
-
if (zeta < 1) {
|
|
89
|
-
const wd = w0 * Math.sqrt(1 - zeta * zeta);
|
|
90
|
-
return 1 - Math.exp(-zeta * w0 * t) * (Math.cos(wd * t) + zeta * w0 / wd * Math.sin(wd * t));
|
|
91
|
-
}
|
|
92
|
-
return 1 - Math.exp(-w0 * t) * (1 + w0 * t);
|
|
93
|
-
}
|
|
94
|
-
function evaluateEasing(easing, u, durationMs = 1e3) {
|
|
95
|
-
if (u <= 0) return 0;
|
|
96
|
-
if (u >= 1) return 1;
|
|
97
|
-
if (!easing || easing === "linear") return u;
|
|
98
|
-
if (easing === "hold") return 0;
|
|
99
|
-
if (easing === "spring") return springAt({ type: "spring" }, u, durationMs);
|
|
100
|
-
if (typeof easing === "string") {
|
|
101
|
-
const b = BEZIERS[easing];
|
|
102
|
-
return b ? cubicBezierAt(b[0], b[1], b[2], b[3], u) : u;
|
|
103
|
-
}
|
|
104
|
-
if (easing.type === "spring") return springAt(easing, u, durationMs);
|
|
105
|
-
return cubicBezierAt(easing.x1, easing.y1, easing.x2, easing.y2, u);
|
|
38
|
+
if (!isAnimationEasing(value)) return false;
|
|
39
|
+
if (typeof value !== "object" || value.type !== "spring") return true;
|
|
40
|
+
return [value.stiffness, value.damping, value.mass].every((v) => v === void 0 || v > 0);
|
|
106
41
|
}
|
|
107
42
|
function interpolateControlValue(control, a, b, t) {
|
|
108
43
|
if (typeof a === "number" && typeof b === "number") {
|
|
@@ -164,7 +99,12 @@ function createEffectDocument(from) {
|
|
|
164
99
|
version: 1,
|
|
165
100
|
name: effect.name,
|
|
166
101
|
basedOn: effect.id,
|
|
167
|
-
effect: effect.kind === "shader" ? { kind: "shader", source: effect.source } : {
|
|
102
|
+
effect: effect.kind === "shader" ? { kind: "shader", source: effect.source } : {
|
|
103
|
+
kind: "vector",
|
|
104
|
+
motion: structuredClone(effect.motion),
|
|
105
|
+
...effect.controls && { controls: effect.controls.map((c) => ({ ...c })) },
|
|
106
|
+
hold: effect.hold
|
|
107
|
+
},
|
|
168
108
|
values: {},
|
|
169
109
|
tracks: {},
|
|
170
110
|
duration: effect.kind === "shader" && effect.loop ? Math.round(effect.loop * 1e3) : DEFAULT_DURATION,
|
|
@@ -177,7 +117,7 @@ function blankDocument(name, source) {
|
|
|
177
117
|
}
|
|
178
118
|
function documentControls(doc) {
|
|
179
119
|
if (doc.effect.kind === "shader") return parseControls(doc.effect.source);
|
|
180
|
-
return doc.effect
|
|
120
|
+
return vectorEffectControls(doc.effect);
|
|
181
121
|
}
|
|
182
122
|
function valuesAt(doc, time, controls = documentControls(doc)) {
|
|
183
123
|
const out = {};
|
|
@@ -217,7 +157,7 @@ function documentIssues(doc) {
|
|
|
217
157
|
for (const preset of doc.presets) for (const [name, value] of Object.entries(preset.values)) check(`preset "${preset.name}"`, name, value);
|
|
218
158
|
if (doc.effect.kind === "vector") {
|
|
219
159
|
const motion = motionAt(doc, 0);
|
|
220
|
-
issues.push(...svgMotionIssues(motion));
|
|
160
|
+
issues.push(...vectorControlIssues(doc.effect.motion), ...svgMotionIssues(motion));
|
|
221
161
|
if (/\{\{[^}]+\}\}/.test(JSON.stringify(motion))) issues.push("the picture has placeholders no control fills");
|
|
222
162
|
}
|
|
223
163
|
return issues;
|
|
@@ -241,7 +181,7 @@ function parseEffectDocument(input) {
|
|
|
241
181
|
else if (effect.kind === "vector") {
|
|
242
182
|
const v = effect;
|
|
243
183
|
if (!v.motion || typeof v.motion.svg !== "string") errors.push("`effect.motion.svg` must be a string");
|
|
244
|
-
if (!Array.isArray(v.controls)) errors.push("`effect.controls` must be a list");
|
|
184
|
+
if (v.controls !== void 0 && !Array.isArray(v.controls)) errors.push("`effect.controls` must be a list");
|
|
245
185
|
}
|
|
246
186
|
const isRecord = (v) => !!v && typeof v === "object" && !Array.isArray(v);
|
|
247
187
|
if (o.values !== void 0 && !isRecord(o.values)) errors.push("`values` must be an object");
|
|
@@ -282,7 +222,7 @@ var easingSchema = {
|
|
|
282
222
|
description: `An easing preset (${EASING_PRESETS.join(", ")}) or {"type":"cubic-bezier","x1","y1","x2","y2"} or {"type":"spring","stiffness","damping","mass"}.`
|
|
283
223
|
};
|
|
284
224
|
var valueSchema = {
|
|
285
|
-
description: 'A control value: a number (float, int), true/false (bool), an array of numbers (vec2\u2013vec4; colours 0\u20131), a "#RRGGBB" colour,
|
|
225
|
+
description: 'A control value: a number (float, int), true/false (bool), an array of numbers (vec2\u2013vec4; colours 0\u20131), a "#RRGGBB" colour, an image URL (sampler2D), or a string (text).'
|
|
286
226
|
};
|
|
287
227
|
function describeEditor(editor) {
|
|
288
228
|
const state = editor.getState();
|
|
@@ -291,13 +231,13 @@ function describeEditor(editor) {
|
|
|
291
231
|
name: doc.name,
|
|
292
232
|
basedOn: doc.basedOn,
|
|
293
233
|
kind: doc.effect.kind,
|
|
294
|
-
...doc.effect.kind === "shader" ? { source: doc.effect.source } : { motion: doc.effect.motion, vectorControls: doc.effect.controls },
|
|
234
|
+
...doc.effect.kind === "shader" ? { source: doc.effect.source } : { motion: doc.effect.motion, ...doc.effect.controls && { vectorControls: doc.effect.controls } },
|
|
295
235
|
controls: state.controls.map((c) => ({
|
|
296
236
|
name: c.name,
|
|
297
237
|
type: c.type,
|
|
298
238
|
label: c.label,
|
|
299
239
|
...c.color && { color: true },
|
|
300
|
-
...c.type !== "bool" && c.type !== "sampler2D" && { min: c.min, max: c.max, step: c.step },
|
|
240
|
+
...c.type !== "bool" && c.type !== "sampler2D" && c.type !== "text" && { min: c.min, max: c.max, step: c.step },
|
|
301
241
|
default: c.default
|
|
302
242
|
})),
|
|
303
243
|
values: state.values,
|
|
@@ -456,6 +396,29 @@ function effectCommands() {
|
|
|
456
396
|
},
|
|
457
397
|
run: (editor, p) => done(editor.setKeyframeEasing(p.name, p.time, p.easing))
|
|
458
398
|
},
|
|
399
|
+
{
|
|
400
|
+
id: "keyframe.setTrack",
|
|
401
|
+
label: "Set keyframes",
|
|
402
|
+
group: "Keyframes",
|
|
403
|
+
description: "Replaces all of a control's keyframes at once (an empty list removes them). Times are ms; each easing shapes the segment leaving its keyframe.",
|
|
404
|
+
params: {
|
|
405
|
+
type: "object",
|
|
406
|
+
properties: {
|
|
407
|
+
name: { type: "string" },
|
|
408
|
+
keyframes: {
|
|
409
|
+
type: "array",
|
|
410
|
+
items: {
|
|
411
|
+
type: "object",
|
|
412
|
+
properties: { time: { type: "number", minimum: 0 }, value: valueSchema, easing: easingSchema },
|
|
413
|
+
required: ["time", "value"]
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
},
|
|
417
|
+
required: ["name", "keyframes"],
|
|
418
|
+
additionalProperties: false
|
|
419
|
+
},
|
|
420
|
+
run: (editor, p) => done(editor.setTrack(p.name, { keyframes: p.keyframes }))
|
|
421
|
+
},
|
|
459
422
|
{
|
|
460
423
|
id: "keyframe.clear",
|
|
461
424
|
label: "Clear keyframes",
|
|
@@ -618,7 +581,7 @@ var EffectEditor = class {
|
|
|
618
581
|
this.commit("Edit source", { ...this.doc, effect: { kind: "shader", source } }, "source");
|
|
619
582
|
return ok;
|
|
620
583
|
}
|
|
621
|
-
/** Replaces a vector effect's picture (and optionally
|
|
584
|
+
/** Replaces a vector effect's picture, whose `{{name: type …}}` placeholders declare its controls (and optionally a `controls` list). */
|
|
622
585
|
setMotion(motion, controls) {
|
|
623
586
|
if (this.doc.effect.kind !== "vector") return fail("This is a shader effect; use setSource.");
|
|
624
587
|
this.commit("Edit picture", { ...this.doc, effect: { ...this.doc.effect, motion, controls: controls ?? this.doc.effect.controls } });
|
|
@@ -669,17 +632,17 @@ var EffectEditor = class {
|
|
|
669
632
|
this.commit("Reset values", { ...this.doc, values });
|
|
670
633
|
return ok;
|
|
671
634
|
}
|
|
672
|
-
/** Random values in each control's range (colours anywhere), for exploring. */
|
|
635
|
+
/** Random values in each control's range (colours anywhere), for exploring. Images and text keep theirs. */
|
|
673
636
|
randomize(names, random = Math.random) {
|
|
674
637
|
const values = {};
|
|
675
638
|
for (const control of this.controls) {
|
|
676
|
-
if (names ? !names.includes(control.name) : control.type === "sampler2D") continue;
|
|
639
|
+
if (names ? !names.includes(control.name) : control.type === "sampler2D" || control.type === "text") continue;
|
|
677
640
|
const pick = () => {
|
|
678
641
|
const v = control.min + random() * (control.max - control.min);
|
|
679
642
|
return control.type === "int" ? Math.round(v) : +v.toFixed(3);
|
|
680
643
|
};
|
|
681
644
|
if (control.type === "bool") values[control.name] = random() < 0.5;
|
|
682
|
-
else if (control.type === "sampler2D") continue;
|
|
645
|
+
else if (control.type === "sampler2D" || control.type === "text") continue;
|
|
683
646
|
else if (control.color) values[control.name] = Array.from({ length: control.type === "vec4" ? 4 : 3 }, () => +random().toFixed(3));
|
|
684
647
|
else if (control.type === "float" || control.type === "int") values[control.name] = pick();
|
|
685
648
|
else values[control.name] = Array.from({ length: Number(control.type.slice(3)) }, pick);
|
|
@@ -757,6 +720,33 @@ var EffectEditor = class {
|
|
|
757
720
|
this.commit("Clear keyframes", { ...this.doc, tracks, values });
|
|
758
721
|
return ok;
|
|
759
722
|
}
|
|
723
|
+
/**
|
|
724
|
+
* Replaces a control's whole track (or removes it with `undefined`), one
|
|
725
|
+
* undo step. Every keyframe is checked like `setKeyframe`; values may be
|
|
726
|
+
* any form the control accepts (colours as "#RRGGBB" too).
|
|
727
|
+
*/
|
|
728
|
+
setTrack(name, track) {
|
|
729
|
+
const control = this.control(name);
|
|
730
|
+
if (!control) return this.unknownControl(name);
|
|
731
|
+
const tracks = { ...this.doc.tracks };
|
|
732
|
+
if (!track || track.keyframes.length === 0) {
|
|
733
|
+
if (!tracks[name]) return ok;
|
|
734
|
+
delete tracks[name];
|
|
735
|
+
this.commit("Clear keyframes", { ...this.doc, tracks });
|
|
736
|
+
return ok;
|
|
737
|
+
}
|
|
738
|
+
const keyframes = [];
|
|
739
|
+
for (const k of track.keyframes) {
|
|
740
|
+
if (!(k.time >= 0)) return fail("A keyframe's time is 0 ms or more.");
|
|
741
|
+
if (k.easing !== void 0 && !isEasing(k.easing)) return fail(`Unknown easing ${JSON.stringify(k.easing)}.`);
|
|
742
|
+
const check = checkControlValue(control, k.value);
|
|
743
|
+
if (!check.ok) return fail(`At ${k.time} ms: ${check.message}`);
|
|
744
|
+
keyframes.push({ time: k.time, value: check.value, ...k.easing !== void 0 && { easing: k.easing } });
|
|
745
|
+
}
|
|
746
|
+
tracks[name] = { keyframes: normalizeKeyframes(keyframes), ...track.enabled === false && { enabled: false } };
|
|
747
|
+
this.commit(`Keyframes ${control.label}`, { ...this.doc, tracks });
|
|
748
|
+
return ok;
|
|
749
|
+
}
|
|
760
750
|
setTiming(timing) {
|
|
761
751
|
if (timing.duration !== void 0 && !(timing.duration > 0)) return fail("The duration must be above 0 ms.");
|
|
762
752
|
this.commit("Set timing", { ...this.doc, duration: timing.duration ?? this.doc.duration, loop: timing.loop ?? this.doc.loop });
|
|
@@ -872,6 +862,10 @@ function createEffectEditor(options = {}) {
|
|
|
872
862
|
|
|
873
863
|
// src/editor/agent.ts
|
|
874
864
|
import { createCommandToolProvider } from "@pixerate/editor/commands";
|
|
865
|
+
import {
|
|
866
|
+
createToolRegistry,
|
|
867
|
+
toToolResult
|
|
868
|
+
} from "@pixerate/editor/tools";
|
|
875
869
|
function createEffectToolProvider(editor, options = {}) {
|
|
876
870
|
const base = createCommandToolProvider(editor.commands, {
|
|
877
871
|
id: options.id ?? "flair-effect",
|
|
@@ -898,6 +892,11 @@ function createEffectToolProvider(editor, options = {}) {
|
|
|
898
892
|
const baseTools = () => typeof base.tools === "function" ? base.tools() : base.tools;
|
|
899
893
|
return { id: base.id, namespace: base.namespace, subscribe: base.subscribe, tools: () => [...baseTools(), ...extra] };
|
|
900
894
|
}
|
|
895
|
+
function createEffectToolRegistry(editor, options = {}) {
|
|
896
|
+
const registry = createToolRegistry({ validateArguments: true, ...options.registry });
|
|
897
|
+
registry.register(createEffectToolProvider(editor, options));
|
|
898
|
+
return registry;
|
|
899
|
+
}
|
|
901
900
|
function toolDefinitions(provider) {
|
|
902
901
|
const tools = typeof provider.tools === "function" ? provider.tools() : provider.tools;
|
|
903
902
|
return tools.map(({ handler: _handler, ...definition }) => definition);
|
|
@@ -907,11 +906,7 @@ async function callTool(provider, name, args = {}) {
|
|
|
907
906
|
const tool = tools.find((t) => t.name === name);
|
|
908
907
|
if (!tool) return { content: [{ type: "text", text: `There is no tool "${name}".` }], isError: true };
|
|
909
908
|
try {
|
|
910
|
-
|
|
911
|
-
if (result && typeof result === "object" && Array.isArray(result.content)) return result;
|
|
912
|
-
if (result === void 0) return { content: [] };
|
|
913
|
-
if (typeof result === "string") return { content: [{ type: "text", text: result }] };
|
|
914
|
-
return { content: [{ type: "text", text: JSON.stringify(result) }], structuredContent: result };
|
|
909
|
+
return toToolResult(await tool.handler(args, { name, providerId: provider.id }));
|
|
915
910
|
} catch (e) {
|
|
916
911
|
return { content: [{ type: "text", text: e.message }], isError: true };
|
|
917
912
|
}
|
|
@@ -932,7 +927,9 @@ Shader effects are GLSL ES 3.00 fragment shaders written Shadertoy-style:
|
|
|
932
927
|
- Write float literals with a decimal point (1.0, not 1); GLSL does not convert int to float implicitly. Use \`texture()\`, not \`texture2D()\`.
|
|
933
928
|
- After \`effect.setSource\`, call \`effect.describe\` and read \`compile\`: fix any errors (their line numbers match your source) before going on.
|
|
934
929
|
|
|
935
|
-
Vector effects are animated SVG pictures
|
|
930
|
+
Vector effects are animated SVG pictures that declare their controls in themselves, like shader uniforms: the first \`{{name: type \u2026}}\` declares a control (types: float, int, bool, color, text; attributes as for \`@control\`, values with spaces in single quotes), and every \`{{name}}\` is replaced by its value, e.g.
|
|
931
|
+
<!-- {{title: text default='Hello' label='Title'}} {{accent: color default=#6366F1}} --><text fill="{{accent}}">{{title}}</text>
|
|
932
|
+
Change their controls' values freely (a text control takes any words); edit the picture with care, keeping element ids that animations refer to.
|
|
936
933
|
|
|
937
934
|
Make effects that loop smoothly and read well behind content: keep motion calm unless asked otherwise, keep contrast where text may sit, and expose the few controls a designer would actually want (colours, speed, scale, intensity) with sensible ranges. Use keyframes (\`keyframe.set\`, in ms) for changes over the timeline. When you can, look at the result with \`view_effect\` and refine it.`;
|
|
938
935
|
|
|
@@ -980,16 +977,114 @@ function bindSvgView(editor, container) {
|
|
|
980
977
|
view?.element.remove();
|
|
981
978
|
};
|
|
982
979
|
}
|
|
980
|
+
|
|
981
|
+
// src/editor/parameters.ts
|
|
982
|
+
import {
|
|
983
|
+
ParametersController,
|
|
984
|
+
rgba01ToHex
|
|
985
|
+
} from "@pixerate/editor/parameters";
|
|
986
|
+
function parameterFieldFor(control, options = {}) {
|
|
987
|
+
const base = { id: control.name, label: control.label };
|
|
988
|
+
switch (control.type) {
|
|
989
|
+
case "bool":
|
|
990
|
+
return { ...base, type: "boolean", default: control.default === true };
|
|
991
|
+
case "int":
|
|
992
|
+
return { ...base, type: "int", min: control.min, max: control.max, step: Math.max(1, Math.round(control.step)), default: Number(control.default) };
|
|
993
|
+
case "float":
|
|
994
|
+
return { ...base, type: "number", min: control.min, max: control.max, step: control.step, default: Number(control.default) };
|
|
995
|
+
case "sampler2D":
|
|
996
|
+
return { ...base, type: "image", default: String(control.default), ...options.images && { options: options.images } };
|
|
997
|
+
case "text":
|
|
998
|
+
return { ...base, type: "text", default: String(control.default) };
|
|
999
|
+
default: {
|
|
1000
|
+
const value = control.default;
|
|
1001
|
+
if (control.color) {
|
|
1002
|
+
const alpha = control.type === "vec4";
|
|
1003
|
+
return { ...base, type: "color", alpha, default: rgba01ToHex(value, alpha) };
|
|
1004
|
+
}
|
|
1005
|
+
return { ...base, type: "vector", size: value.length, min: control.min, max: control.max, step: control.step, default: [...value] };
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
function parameterSchemaFor(controls, options = {}) {
|
|
1010
|
+
return { ...options.title && { title: options.title }, fields: controls.map((c) => parameterFieldFor(c, options)) };
|
|
1011
|
+
}
|
|
1012
|
+
function toParameterValue(control, value) {
|
|
1013
|
+
if (control.color && Array.isArray(value)) return rgba01ToHex(value, control.type === "vec4");
|
|
1014
|
+
return Array.isArray(value) ? [...value] : value;
|
|
1015
|
+
}
|
|
1016
|
+
function toParameterValues(controls, values) {
|
|
1017
|
+
const out = {};
|
|
1018
|
+
for (const control of controls) if (values[control.name] !== void 0) out[control.name] = toParameterValue(control, values[control.name]);
|
|
1019
|
+
return out;
|
|
1020
|
+
}
|
|
1021
|
+
function toParameterTrack(control, track) {
|
|
1022
|
+
if (!track) return void 0;
|
|
1023
|
+
return {
|
|
1024
|
+
...track.enabled === false && { enabled: false },
|
|
1025
|
+
keyframes: track.keyframes.map((k) => ({ ...k, value: control ? toParameterValue(control, k.value) : k.value }))
|
|
1026
|
+
};
|
|
1027
|
+
}
|
|
1028
|
+
function bindEffectParameters(editor, options = {}) {
|
|
1029
|
+
let state = editor.getState();
|
|
1030
|
+
let controls = state.controls;
|
|
1031
|
+
let signature = JSON.stringify(controls);
|
|
1032
|
+
let syncing = false;
|
|
1033
|
+
const byName = () => new Map(controls.map((c) => [c.name, c]));
|
|
1034
|
+
const keyframes = {
|
|
1035
|
+
getTrack: (id) => toParameterTrack(byName().get(id), editor.getState().document.tracks[id]),
|
|
1036
|
+
setTrack: (id, track) => {
|
|
1037
|
+
editor.setTrack(id, track);
|
|
1038
|
+
},
|
|
1039
|
+
subscribe: (listener) => editor.subscribe(() => listener())
|
|
1040
|
+
};
|
|
1041
|
+
const controller = new ParametersController({
|
|
1042
|
+
schema: parameterSchemaFor(controls, options),
|
|
1043
|
+
values: toParameterValues(controls, state.values),
|
|
1044
|
+
time: state.time,
|
|
1045
|
+
keyframes,
|
|
1046
|
+
onChange: (values, changed) => {
|
|
1047
|
+
if (syncing) return;
|
|
1048
|
+
const result = changed.length === 1 ? editor.setValue(changed[0], values[changed[0]]) : editor.setValues(Object.fromEntries(changed.map((id) => [id, values[id]])));
|
|
1049
|
+
if (!result.ok) sync(editor.getState());
|
|
1050
|
+
}
|
|
1051
|
+
});
|
|
1052
|
+
const sync = (next) => {
|
|
1053
|
+
state = next;
|
|
1054
|
+
const nextSignature = JSON.stringify(next.controls);
|
|
1055
|
+
syncing = true;
|
|
1056
|
+
try {
|
|
1057
|
+
if (nextSignature !== signature) {
|
|
1058
|
+
signature = nextSignature;
|
|
1059
|
+
controls = next.controls;
|
|
1060
|
+
controller.setSchema(parameterSchemaFor(controls, options));
|
|
1061
|
+
}
|
|
1062
|
+
controller.setTime(next.time, toParameterValues(controls, next.values));
|
|
1063
|
+
} finally {
|
|
1064
|
+
syncing = false;
|
|
1065
|
+
}
|
|
1066
|
+
};
|
|
1067
|
+
const unsubscribe = editor.subscribe(sync);
|
|
1068
|
+
return {
|
|
1069
|
+
controller,
|
|
1070
|
+
dispose: () => {
|
|
1071
|
+
unsubscribe();
|
|
1072
|
+
controller.destroy();
|
|
1073
|
+
}
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
983
1076
|
export {
|
|
984
1077
|
EASING_PRESETS,
|
|
985
1078
|
EFFECT_AGENT_INSTRUCTIONS,
|
|
986
1079
|
EffectEditor,
|
|
1080
|
+
bindEffectParameters,
|
|
987
1081
|
bindShaderRenderer,
|
|
988
1082
|
bindSvgView,
|
|
989
1083
|
callTool,
|
|
990
1084
|
createEffectDocument,
|
|
991
1085
|
createEffectEditor,
|
|
992
1086
|
createEffectToolProvider,
|
|
1087
|
+
createEffectToolRegistry,
|
|
993
1088
|
cubicBezierAt,
|
|
994
1089
|
describeEditor,
|
|
995
1090
|
documentControls,
|
|
@@ -1002,7 +1097,11 @@ export {
|
|
|
1002
1097
|
keyframeTimes,
|
|
1003
1098
|
motionAt,
|
|
1004
1099
|
normalizeKeyframes,
|
|
1100
|
+
parameterFieldFor,
|
|
1101
|
+
parameterSchemaFor,
|
|
1005
1102
|
parseEffectDocument,
|
|
1103
|
+
toParameterValue,
|
|
1104
|
+
toParameterValues,
|
|
1006
1105
|
toolDefinitions,
|
|
1007
1106
|
valuesAt
|
|
1008
1107
|
};
|