@pixerate/editor 0.223.9 → 0.224.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/dist/animation-editor/index.cjs +9 -9
- package/dist/animation-editor/index.js +1 -1
- package/dist/canvas/index.cjs +7 -7
- package/dist/canvas/index.js +1 -1
- package/dist/{chunk-6DUEV36F.cjs → chunk-7AAS3JO6.cjs} +14 -14
- package/dist/{chunk-6G2NLMVK.js → chunk-HKPLKISO.js} +58 -9
- package/dist/{chunk-Z64NRDEH.cjs → chunk-MDNYYOBZ.cjs} +7 -7
- package/dist/{chunk-A3A5TI6R.cjs → chunk-NTWR5TQB.cjs} +59 -10
- package/dist/{chunk-OZSA5YEJ.js → chunk-RDTCG5MS.js} +1 -1
- package/dist/{chunk-WMVEYSOA.js → chunk-WA4IS2EY.js} +1 -1
- package/dist/effects/fake.d.cts +6 -1
- package/dist/effects/fake.d.ts +6 -1
- package/dist/effects/helpers.d.cts +24 -1
- package/dist/effects/helpers.d.ts +24 -1
- package/dist/effects/index.cjs +257 -62
- package/dist/effects/index.js +202 -7
- package/dist/effects/types.d.cts +121 -4
- package/dist/effects/types.d.ts +121 -4
- package/dist/image-editor/index.cjs +13 -13
- package/dist/image-editor/index.js +1 -1
- package/dist/paint-editor/index.cjs +7 -7
- package/dist/paint-editor/index.js +2 -2
- package/dist/vector/index.cjs +6 -6
- package/dist/vector/index.js +1 -1
- package/dist/video-editor/index.cjs +309 -309
- package/dist/video-editor/index.js +3 -3
- package/package.json +1 -1
- package/skills/pixerate-animation-editor/SKILL.md +1 -1
- package/skills/pixerate-video-editor/SKILL.md +1 -1
package/dist/effects/index.js
CHANGED
|
@@ -3,11 +3,12 @@ import {
|
|
|
3
3
|
} from "../chunk-N4ZXW3WB.js";
|
|
4
4
|
import {
|
|
5
5
|
createEffectAuthoringSession
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-WA4IS2EY.js";
|
|
7
7
|
import {
|
|
8
8
|
EffectRendererPool
|
|
9
9
|
} from "../chunk-EHLOCVWB.js";
|
|
10
10
|
import {
|
|
11
|
+
CAMERA_MOVE_ROLES,
|
|
11
12
|
EFFECT_ERROR_CODES,
|
|
12
13
|
EFFECT_KINDS,
|
|
13
14
|
EFFECT_USES,
|
|
@@ -22,17 +23,21 @@ import {
|
|
|
22
23
|
countEffectTags,
|
|
23
24
|
createEffectPixels,
|
|
24
25
|
createEffectToolProvider,
|
|
26
|
+
effectOffered,
|
|
25
27
|
effectValueErrors,
|
|
26
28
|
encodeEffectFloat,
|
|
27
29
|
hasEffectTags,
|
|
30
|
+
isCameraView,
|
|
28
31
|
isEffectError,
|
|
29
32
|
isEffectPixels,
|
|
30
33
|
linearToSrgb,
|
|
31
34
|
normaliseEffectTag,
|
|
32
35
|
resolveEffectValues,
|
|
36
|
+
sampleCameraViews,
|
|
37
|
+
sampleEffectCameraMove,
|
|
33
38
|
searchEffectSummaries,
|
|
34
39
|
srgbToLinear
|
|
35
|
-
} from "../chunk-
|
|
40
|
+
} from "../chunk-HKPLKISO.js";
|
|
36
41
|
import "../chunk-V2ZCJRDM.js";
|
|
37
42
|
import "../chunk-RNSANXPT.js";
|
|
38
43
|
import {
|
|
@@ -62,6 +67,20 @@ function checkSummary(e, issues) {
|
|
|
62
67
|
if (typeof e.loop !== "boolean") at("loop is not a boolean");
|
|
63
68
|
if (e.duration !== void 0 && !(e.duration > 0)) at("duration must be a positive number of ms when present");
|
|
64
69
|
if (e.backgroundOpacity !== void 0 && (typeof e.backgroundOpacity !== "string" || !e.backgroundOpacity)) at("backgroundOpacity must name a control when present");
|
|
70
|
+
if (e.kind === "camera") {
|
|
71
|
+
if (e.uses?.length !== 1 || e.uses[0] !== "camera") at('a camera effect must have exactly the use "camera"');
|
|
72
|
+
const m = e.camera;
|
|
73
|
+
if (!m) at("a camera effect needs camera: { role, durationMs }");
|
|
74
|
+
else {
|
|
75
|
+
if (!CAMERA_MOVE_ROLES.includes(m.role)) at(`camera.role "${m.role}" is not one of ${CAMERA_MOVE_ROLES.join(", ")}`);
|
|
76
|
+
if (!(Number.isFinite(m.durationMs) && m.durationMs >= 0)) at("camera.durationMs must be a number of ms \u2265 0");
|
|
77
|
+
if (m.cut !== void 0 && !(m.cut >= 0 && m.cut <= 1)) at("camera.cut must be 0\u20131 when present");
|
|
78
|
+
if (e.duration !== void 0 && e.duration !== m.durationMs) at(`duration ${e.duration} differs from camera.durationMs ${m.durationMs}`);
|
|
79
|
+
}
|
|
80
|
+
} else {
|
|
81
|
+
if (e.uses?.includes("camera")) at('only camera effects may have the use "camera"');
|
|
82
|
+
if (e.camera !== void 0) at("only camera effects may have camera");
|
|
83
|
+
}
|
|
65
84
|
}
|
|
66
85
|
function checkDescription(id, d, issues) {
|
|
67
86
|
const at = (message) => issues.push({ effectId: id, message });
|
|
@@ -102,7 +121,8 @@ async function checkRender(provider, d, size, issues) {
|
|
|
102
121
|
try {
|
|
103
122
|
renderer = await provider.createRenderer(d.id, { width: size, height: size });
|
|
104
123
|
} catch (e) {
|
|
105
|
-
if (!(d.kind === "vector" && codeOf(e) === "unsupported")) at(`createRenderer failed: ${codeOf(e)}`);
|
|
124
|
+
if (!((d.kind === "vector" || d.kind === "camera") && codeOf(e) === "unsupported")) at(`createRenderer failed: ${codeOf(e)}`);
|
|
125
|
+
else if (!(e instanceof Error && e.message.trim())) at("createRenderer refused without saying why");
|
|
106
126
|
return;
|
|
107
127
|
}
|
|
108
128
|
try {
|
|
@@ -118,6 +138,51 @@ async function checkRender(provider, d, size, issues) {
|
|
|
118
138
|
const after = await rejectsWith("disposed", () => renderer.render({ time: 0, inputs }));
|
|
119
139
|
if (after) at(`render after dispose ${after}`);
|
|
120
140
|
}
|
|
141
|
+
var sameView = (a, b) => JSON.stringify(a) === JSON.stringify(b);
|
|
142
|
+
var near = (a, b) => ["zoom", "scrollX", "scrollY", "tilt", "turn", "roll", "perspective", "blur", "blurAngle"].every((k) => Math.abs((a[k] ?? 0) - (b[k] ?? 0)) <= 1e-6 * Math.max(1, Math.abs(a[k] ?? 0)));
|
|
143
|
+
var STAGE = { width: 320, height: 180 };
|
|
144
|
+
var WIDE = { zoom: 1, scrollX: 0, scrollY: 0 };
|
|
145
|
+
var CLOSE = { zoom: 2, scrollX: 0, scrollY: 0 };
|
|
146
|
+
async function checkCamera(provider, d, issues) {
|
|
147
|
+
const at = (message) => issues.push({ effectId: d.id, message });
|
|
148
|
+
const camera = d.camera;
|
|
149
|
+
const to = camera.role === "hold" ? WIDE : CLOSE;
|
|
150
|
+
const ms = camera.durationMs || 1e3;
|
|
151
|
+
const ctx = (u) => ({ from: WIDE, to, u, ms, stage: STAGE });
|
|
152
|
+
const views = [];
|
|
153
|
+
for (const u of [0, 0.25, 0.5, 0.75, 1]) {
|
|
154
|
+
try {
|
|
155
|
+
const view = provider.cameraView(d.id, ctx(u));
|
|
156
|
+
if (view instanceof Promise) {
|
|
157
|
+
at("cameraView must be synchronous; it returned a Promise");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
if (!isCameraView(view)) at(`cameraView at u ${u} is not a CameraView (zoom > 0, finite scrollX and scrollY): ${JSON.stringify(view)}`);
|
|
161
|
+
else views.push(view);
|
|
162
|
+
const again = provider.cameraView(d.id, ctx(u));
|
|
163
|
+
if (isCameraView(view) && !sameView(view, again)) at(`cameraView is not pure: two calls at u ${u} gave different views`);
|
|
164
|
+
} catch (e) {
|
|
165
|
+
at(`cameraView at u ${u} failed: ${codeOf(e)}`);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
const bad = await rejectsWith("invalid_values", () => provider.cameraView(d.id, ctx(0.5), { __not_a_control__: 1 }));
|
|
169
|
+
if (bad) at(`cameraView with an unknown control ${bad}`);
|
|
170
|
+
if (provider.sampleCameraMove && views.length === 5) {
|
|
171
|
+
try {
|
|
172
|
+
const sample = await provider.sampleCameraMove(d.id, { from: WIDE, to, ms, stage: STAGE, fps: 10 });
|
|
173
|
+
const keys = sample?.keys;
|
|
174
|
+
if (!Array.isArray(keys) || keys.length < 2) at("sampleCameraMove returned fewer than two keys");
|
|
175
|
+
else {
|
|
176
|
+
if (keys.some((k, i) => !isCameraView(k.view) || !Number.isFinite(k.t) || i > 0 && k.t < keys[i - 1].t)) at("sampleCameraMove keys must be CameraViews in time order");
|
|
177
|
+
if (keys[0].t !== 0 || keys[keys.length - 1].t !== ms) at(`sampleCameraMove keys must run from 0 to ${ms} ms, got ${keys[0].t} to ${keys[keys.length - 1].t}`);
|
|
178
|
+
else if (!near(keys[0].view, views[0]) || !near(keys[keys.length - 1].view, views[4])) at("sampleCameraMove disagrees with cameraView at the start or end of the move");
|
|
179
|
+
if (camera.cut !== void 0 && sample.cut === void 0) at("sampleCameraMove gave no cut for a move that cuts");
|
|
180
|
+
}
|
|
181
|
+
} catch (e) {
|
|
182
|
+
at(`sampleCameraMove failed: ${codeOf(e)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
121
186
|
async function checkEffectProvider(provider, options = {}) {
|
|
122
187
|
const issues = [];
|
|
123
188
|
const top = (message) => issues.push({ message });
|
|
@@ -129,6 +194,9 @@ async function checkEffectProvider(provider, options = {}) {
|
|
|
129
194
|
if (caps?.supportsAuthoring !== void 0 && typeof caps.supportsAuthoring !== "boolean") top("capabilities.supportsAuthoring is not a boolean");
|
|
130
195
|
if (caps?.supportsVector && typeof provider.applyVectorEffect !== "function") top("supportsVector is set but applyVectorEffect is missing");
|
|
131
196
|
if (caps?.supportsAuthoring && typeof provider.openAuthoring !== "function") top("supportsAuthoring is set but openAuthoring is missing");
|
|
197
|
+
if (caps?.supportsCamera !== void 0 && typeof caps.supportsCamera !== "boolean") top("capabilities.supportsCamera is not a boolean");
|
|
198
|
+
if (caps?.supportsCamera && typeof provider.cameraView !== "function") top("supportsCamera is set but cameraView is missing");
|
|
199
|
+
if (provider.sampleCameraMove && typeof provider.cameraView !== "function") top("sampleCameraMove is present but cameraView is missing");
|
|
132
200
|
const list = await provider.list();
|
|
133
201
|
const ids = /* @__PURE__ */ new Set();
|
|
134
202
|
for (const e of list) {
|
|
@@ -136,6 +204,7 @@ async function checkEffectProvider(provider, options = {}) {
|
|
|
136
204
|
ids.add(e.id);
|
|
137
205
|
checkSummary(e, issues);
|
|
138
206
|
}
|
|
207
|
+
if (!caps?.supportsCamera && list.some((e) => e.kind === "camera")) top("the catalog lists camera effects but capabilities.supportsCamera is not set");
|
|
139
208
|
const all = await provider.search("");
|
|
140
209
|
if (all.length !== list.length) top(`search("") returned ${all.length} effects, list() ${list.length}`);
|
|
141
210
|
const firstUse = list[0]?.uses[0];
|
|
@@ -147,6 +216,14 @@ async function checkEffectProvider(provider, options = {}) {
|
|
|
147
216
|
if (unknown) top(`describe of an unknown id ${unknown}`);
|
|
148
217
|
const unknownRenderer = await rejectsWith("unknown_effect", () => provider.createRenderer("__no_such_effect__", { width: 4, height: 4 }));
|
|
149
218
|
if (unknownRenderer) top(`createRenderer of an unknown id ${unknownRenderer}`);
|
|
219
|
+
if (caps?.supportsCamera && provider.cameraView) {
|
|
220
|
+
const probe = { from: WIDE, to: CLOSE, u: 0.5, ms: 1e3, stage: STAGE };
|
|
221
|
+
const unknownCamera = await rejectsWith("unknown_effect", () => provider.cameraView("__no_such_effect__", probe));
|
|
222
|
+
if (unknownCamera) top(`cameraView of an unknown id ${unknownCamera}`);
|
|
223
|
+
const notCamera = list.find((e) => e.kind !== "camera");
|
|
224
|
+
const wrongKind = notCamera && await rejectsWith("unsupported", () => provider.cameraView(notCamera.id, probe));
|
|
225
|
+
if (wrongKind) issues.push({ effectId: notCamera.id, message: `cameraView of a ${notCamera.kind} effect ${wrongKind}` });
|
|
226
|
+
}
|
|
150
227
|
const size = options.size ?? 8;
|
|
151
228
|
for (const id of options.ids ?? [...ids]) {
|
|
152
229
|
let d;
|
|
@@ -158,6 +235,7 @@ async function checkEffectProvider(provider, options = {}) {
|
|
|
158
235
|
}
|
|
159
236
|
checkDescription(id, d, issues);
|
|
160
237
|
if (options.render !== false && d.kind !== "motion") await checkRender(provider, d, size, issues);
|
|
238
|
+
if (d.kind === "camera" && d.camera && caps.supportsCamera && provider.cameraView) await checkCamera(provider, d, issues);
|
|
161
239
|
if ((d.kind === "vector" || d.kind === "motion") && caps.supportsVector && provider.applyVectorEffect) {
|
|
162
240
|
try {
|
|
163
241
|
const m = await provider.applyVectorEffect(id, '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 10"><rect id="r" width="10" height="10"/></svg>');
|
|
@@ -172,7 +250,7 @@ async function checkEffectProvider(provider, options = {}) {
|
|
|
172
250
|
}
|
|
173
251
|
if (caps?.supportsAuthoring && options.authoring !== false && typeof provider.openAuthoring === "function") {
|
|
174
252
|
const targetEffect = (await Promise.all((options.ids ?? [...ids]).map((id) => provider.describe(id)))).find(
|
|
175
|
-
(d) => d.kind !== "motion" && d.kind !== "vector"
|
|
253
|
+
(d) => d.kind !== "motion" && d.kind !== "vector" && d.kind !== "camera"
|
|
176
254
|
);
|
|
177
255
|
if (targetEffect) {
|
|
178
256
|
try {
|
|
@@ -394,6 +472,71 @@ var DESCRIPTIONS = [
|
|
|
394
472
|
inputs: []
|
|
395
473
|
}
|
|
396
474
|
];
|
|
475
|
+
var CAMERA_DESCRIPTIONS = [
|
|
476
|
+
{
|
|
477
|
+
id: "fake.glide",
|
|
478
|
+
name: "Glide",
|
|
479
|
+
kind: "camera",
|
|
480
|
+
category: "camera",
|
|
481
|
+
uses: ["camera"],
|
|
482
|
+
description: "Moves the camera smoothly from one framing to the next.",
|
|
483
|
+
tags: ["move", "smooth"],
|
|
484
|
+
transparent: false,
|
|
485
|
+
loop: false,
|
|
486
|
+
duration: 800,
|
|
487
|
+
camera: { role: "transition", durationMs: 800 },
|
|
488
|
+
controls: {
|
|
489
|
+
type: "object",
|
|
490
|
+
additionalProperties: false,
|
|
491
|
+
properties: { curve: { type: "string", enum: ["linear", "smooth"], description: "Speed curve: linear, or smooth (ease in and out). Default smooth." } }
|
|
492
|
+
},
|
|
493
|
+
defaults: { curve: "smooth" },
|
|
494
|
+
presets: [],
|
|
495
|
+
inputs: []
|
|
496
|
+
},
|
|
497
|
+
{
|
|
498
|
+
id: "fake.whip",
|
|
499
|
+
name: "Whip pan",
|
|
500
|
+
kind: "camera",
|
|
501
|
+
category: "camera",
|
|
502
|
+
uses: ["camera"],
|
|
503
|
+
description: "Whips the camera away from one framing, cuts halfway in a blur, and lands on the next.",
|
|
504
|
+
tags: ["move", "fast", "cut"],
|
|
505
|
+
transparent: false,
|
|
506
|
+
loop: false,
|
|
507
|
+
duration: 400,
|
|
508
|
+
camera: { role: "transition", durationMs: 400, cut: 0.5 },
|
|
509
|
+
controls: {
|
|
510
|
+
type: "object",
|
|
511
|
+
additionalProperties: false,
|
|
512
|
+
properties: { blur: { type: "number", minimum: 0, maximum: 100, description: "Motion blur at the cut, in stage pixels. Default 40." } }
|
|
513
|
+
},
|
|
514
|
+
defaults: { blur: 40 },
|
|
515
|
+
presets: [{ name: "Clean", values: { blur: 0 } }],
|
|
516
|
+
inputs: []
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
id: "fake.drift",
|
|
520
|
+
name: "Drift",
|
|
521
|
+
kind: "camera",
|
|
522
|
+
category: "camera",
|
|
523
|
+
uses: ["camera"],
|
|
524
|
+
description: "Zooms slowly into the middle of the framing while the shot holds.",
|
|
525
|
+
tags: ["ken burns", "slow"],
|
|
526
|
+
transparent: false,
|
|
527
|
+
loop: false,
|
|
528
|
+
duration: 4e3,
|
|
529
|
+
camera: { role: "hold", durationMs: 4e3 },
|
|
530
|
+
controls: {
|
|
531
|
+
type: "object",
|
|
532
|
+
additionalProperties: false,
|
|
533
|
+
properties: { scale: { type: "number", minimum: 1, maximum: 2, description: "Zoom at the end of the hold, as a multiple of the framing's zoom. Default 1.15." } }
|
|
534
|
+
},
|
|
535
|
+
defaults: { scale: 1.15 },
|
|
536
|
+
presets: [],
|
|
537
|
+
inputs: []
|
|
538
|
+
}
|
|
539
|
+
];
|
|
397
540
|
var summaryOf = ({ controls: _c, defaults: _d, presets: _p, inputs: _i, ...summary }) => summary;
|
|
398
541
|
var hexToRgb = (hex) => [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16));
|
|
399
542
|
var smooth = (t) => t * t * (3 - 2 * t);
|
|
@@ -478,6 +621,35 @@ function renderPixels(d, v, request, width, height) {
|
|
|
478
621
|
}
|
|
479
622
|
return out;
|
|
480
623
|
}
|
|
624
|
+
var lerp = (a, b, t) => a + (b - a) * t;
|
|
625
|
+
var viewAt = (cx, cy, zoom, ctx) => ({ zoom, scrollX: ctx.stage.width / (2 * zoom) - cx, scrollY: ctx.stage.height / (2 * zoom) - cy });
|
|
626
|
+
var centreOf = (v, ctx) => [ctx.stage.width / (2 * v.zoom) - v.scrollX, ctx.stage.height / (2 * v.zoom) - v.scrollY];
|
|
627
|
+
function cameraViewOf(d, ctx, v) {
|
|
628
|
+
const { from, to, u } = ctx;
|
|
629
|
+
const [x0, y0] = centreOf(from, ctx);
|
|
630
|
+
const [x1, y1] = centreOf(to, ctx);
|
|
631
|
+
switch (d.id) {
|
|
632
|
+
case "fake.glide": {
|
|
633
|
+
const k = v.curve === "smooth" ? smooth(u) : u;
|
|
634
|
+
return viewAt(lerp(x0, x1, k), lerp(y0, y1, k), lerp(from.zoom, to.zoom, k), ctx);
|
|
635
|
+
}
|
|
636
|
+
case "fake.whip": {
|
|
637
|
+
const out = u < 0.5;
|
|
638
|
+
const at = out ? from : to;
|
|
639
|
+
const off = out ? Math.pow(u * 2, 3) : 1 - (1 - Math.pow(1 - (u - 0.5) * 2, 3));
|
|
640
|
+
const dist = Math.hypot(x1 - x0, y1 - y0);
|
|
641
|
+
const [dx, dy] = dist > 1e-6 ? [(x1 - x0) / dist, (y1 - y0) / dist] : [1, 0];
|
|
642
|
+
const reach = ctx.stage.width / at.zoom * off * (out ? 1 : -1);
|
|
643
|
+
const [cx, cy] = centreOf(at, ctx);
|
|
644
|
+
const blur = v.blur * off;
|
|
645
|
+
return { ...viewAt(cx + dx * reach, cy + dy * reach, at.zoom, ctx), ...blur > 0 ? { blur, blurAngle: Math.atan2(dy, dx) * 180 / Math.PI } : {} };
|
|
646
|
+
}
|
|
647
|
+
case "fake.drift":
|
|
648
|
+
return viewAt(x0, y0, from.zoom * lerp(1, v.scale, smooth(u)), ctx);
|
|
649
|
+
default:
|
|
650
|
+
throw new EffectUnsupportedError(`"${d.id}" is a ${d.kind} effect, not a camera move.`, d.id);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
481
653
|
function toFloat(p) {
|
|
482
654
|
const f = new Float32Array(p.data.length);
|
|
483
655
|
for (let i = 0; i < f.length; i += 4) {
|
|
@@ -508,16 +680,24 @@ function createFakeEffectProvider(options = {}) {
|
|
|
508
680
|
supportsHostImages: false,
|
|
509
681
|
supportsVector: true,
|
|
510
682
|
supportsAuthoring,
|
|
683
|
+
...options.camera ? { supportsCamera: true } : {},
|
|
511
684
|
maxSize: 8192,
|
|
512
685
|
...options.capabilities
|
|
513
686
|
};
|
|
514
|
-
const
|
|
687
|
+
const all = options.camera ? [...DESCRIPTIONS, ...CAMERA_DESCRIPTIONS] : DESCRIPTIONS;
|
|
688
|
+
const byId = new Map(all.map((d) => [d.id, d]));
|
|
515
689
|
const get = (id) => {
|
|
516
690
|
const d = byId.get(id);
|
|
517
691
|
if (!d) throw new UnknownEffectError(id, "the fake provider");
|
|
518
692
|
return d;
|
|
519
693
|
};
|
|
520
|
-
const summaries =
|
|
694
|
+
const summaries = all.map(summaryOf);
|
|
695
|
+
const camera = (id, ctx, values) => {
|
|
696
|
+
const d = get(id);
|
|
697
|
+
if (d.kind !== "camera") throw new EffectUnsupportedError(`"${id}" is a ${d.kind} effect, not a camera move.`, id);
|
|
698
|
+
if (!(ctx.u >= 0 && ctx.u <= 1)) throw new EffectUnsupportedError(`u must be 0\u20131 through the move, got ${ctx.u}.`, id);
|
|
699
|
+
return cameraViewOf(d, ctx, resolveEffectValues(d, values));
|
|
700
|
+
};
|
|
521
701
|
const provider = {
|
|
522
702
|
id: "fake",
|
|
523
703
|
name: "Fake effects",
|
|
@@ -528,6 +708,7 @@ function createFakeEffectProvider(options = {}) {
|
|
|
528
708
|
describe: (id) => structuredClone(get(id)),
|
|
529
709
|
createRenderer(id, ro) {
|
|
530
710
|
const d = get(id);
|
|
711
|
+
if (d.kind === "camera") throw new EffectUnsupportedError(`"${id}" is a camera move: it moves a scene's camera and draws no pixels. Use cameraView.`, id);
|
|
531
712
|
if (d.kind !== "shader") throw new EffectUnsupportedError(`"${id}" is a ${d.kind} effect; use applyVectorEffect.`, id);
|
|
532
713
|
const { width, height } = ro;
|
|
533
714
|
const depth = ro.depth ?? 8;
|
|
@@ -578,6 +759,14 @@ function createFakeEffectProvider(options = {}) {
|
|
|
578
759
|
loop: false
|
|
579
760
|
};
|
|
580
761
|
},
|
|
762
|
+
...options.camera ? {
|
|
763
|
+
cameraView: camera,
|
|
764
|
+
sampleCameraMove: (id, request) => {
|
|
765
|
+
const d = get(id);
|
|
766
|
+
const { from, to, ms, stage, fps, values } = request;
|
|
767
|
+
return sampleCameraViews((u) => camera(id, { from, to, u, ms, stage }, values), { ms, fps, cut: d.camera?.cut });
|
|
768
|
+
}
|
|
769
|
+
} : {},
|
|
581
770
|
...supportsAuthoring ? {
|
|
582
771
|
openAuthoring: (from) => createEffectAuthoringSession(from, { provider })
|
|
583
772
|
} : {}
|
|
@@ -593,7 +782,8 @@ var EFFECT_USE_LABELS = {
|
|
|
593
782
|
overlay: "Overlay",
|
|
594
783
|
title: "Title",
|
|
595
784
|
motion: "Motion",
|
|
596
|
-
interactive: "Interactive"
|
|
785
|
+
interactive: "Interactive",
|
|
786
|
+
camera: "Camera"
|
|
597
787
|
};
|
|
598
788
|
var HEX2 = /^#[0-9a-f]{6}([0-9a-f]{2})?$/i;
|
|
599
789
|
var typeOf = (s) => Array.isArray(s.type) ? s.type.find((t) => t !== "null") : s.type;
|
|
@@ -1102,6 +1292,7 @@ function createEffectAuthoringToolProvider(host, options) {
|
|
|
1102
1292
|
};
|
|
1103
1293
|
}
|
|
1104
1294
|
export {
|
|
1295
|
+
CAMERA_MOVE_ROLES,
|
|
1105
1296
|
EFFECT_ERROR_CODES,
|
|
1106
1297
|
EFFECT_KINDS,
|
|
1107
1298
|
EFFECT_USES,
|
|
@@ -1125,6 +1316,7 @@ export {
|
|
|
1125
1316
|
effectControlField,
|
|
1126
1317
|
effectFieldValue,
|
|
1127
1318
|
effectFontFamilies,
|
|
1319
|
+
effectOffered,
|
|
1128
1320
|
effectParameterFields,
|
|
1129
1321
|
effectSampleInputs,
|
|
1130
1322
|
effectSamplePicture,
|
|
@@ -1132,6 +1324,7 @@ export {
|
|
|
1132
1324
|
encodeEffectFloat,
|
|
1133
1325
|
groupEffectsByCategory,
|
|
1134
1326
|
hasEffectTags,
|
|
1327
|
+
isCameraView,
|
|
1135
1328
|
isEffectError,
|
|
1136
1329
|
isEffectFontControl,
|
|
1137
1330
|
isEffectPixels,
|
|
@@ -1139,6 +1332,8 @@ export {
|
|
|
1139
1332
|
normaliseEffectTag,
|
|
1140
1333
|
renderEffectThumbnail,
|
|
1141
1334
|
resolveEffectValues,
|
|
1335
|
+
sampleCameraViews,
|
|
1336
|
+
sampleEffectCameraMove,
|
|
1142
1337
|
searchEffectSummaries,
|
|
1143
1338
|
srgbToLinear
|
|
1144
1339
|
};
|
package/dist/effects/types.d.cts
CHANGED
|
@@ -9,8 +9,11 @@
|
|
|
9
9
|
import type { JsonSchema } from '../tools/schema.cjs';
|
|
10
10
|
import type { FlairSvgAnimation } from '../animation-editor/flair.cjs';
|
|
11
11
|
import type { EffectAuthoringSession, EffectAuthoringSource } from './authoring.cjs';
|
|
12
|
-
/**
|
|
13
|
-
|
|
12
|
+
/**
|
|
13
|
+
* How an effect is drawn: a pixel shader, an animated vector picture, motion applied to someone
|
|
14
|
+
* else's vector art, or a camera move, which moves a scene's camera and draws no pixels.
|
|
15
|
+
*/
|
|
16
|
+
export type EffectKind = 'shader' | 'vector' | 'motion' | 'camera';
|
|
14
17
|
/**
|
|
15
18
|
* What an effect is for. An effect may have several uses.
|
|
16
19
|
*
|
|
@@ -21,10 +24,33 @@ export type EffectKind = 'shader' | 'vector' | 'motion';
|
|
|
21
24
|
* - `title`: draws type or a graphic meant as a title.
|
|
22
25
|
* - `motion`: animates vector art (`applyVectorEffect`).
|
|
23
26
|
* - `interactive`: reacts to pointer, scroll or audio input at run time.
|
|
27
|
+
* - `camera`: moves a scene's camera (`cameraView`). Only `camera` effects have it, and they have
|
|
28
|
+
* no other use, so editors that draw pixels never offer them.
|
|
24
29
|
*/
|
|
25
|
-
export type EffectUse = 'filter' | 'transition' | 'generator' | 'overlay' | 'title' | 'motion' | 'interactive';
|
|
30
|
+
export type EffectUse = 'filter' | 'transition' | 'generator' | 'overlay' | 'title' | 'motion' | 'interactive' | 'camera';
|
|
26
31
|
export declare const EFFECT_KINDS: readonly EffectKind[];
|
|
27
32
|
export declare const EFFECT_USES: readonly EffectUse[];
|
|
33
|
+
/**
|
|
34
|
+
* What a camera move does to a shot.
|
|
35
|
+
*
|
|
36
|
+
* - `transition`: moves the camera from one framing (`from`) to the next (`to`): a glide, a whip
|
|
37
|
+
* pan, a punch zoom, a cut.
|
|
38
|
+
* - `hold`: moves the camera within one framing while the shot holds (`from` and `to` are the
|
|
39
|
+
* same): a Ken Burns drift, a crash zoom.
|
|
40
|
+
*/
|
|
41
|
+
export type CameraMoveRole = 'transition' | 'hold';
|
|
42
|
+
export declare const CAMERA_MOVE_ROLES: readonly CameraMoveRole[];
|
|
43
|
+
/** How a `camera` effect moves (`EffectSummary.camera`). */
|
|
44
|
+
export interface EffectCameraMove {
|
|
45
|
+
role: CameraMoveRole;
|
|
46
|
+
/** Length of the move at its default values (ms); 0 for a cut. A hold plays across the shot's hold instead. */
|
|
47
|
+
durationMs: number;
|
|
48
|
+
/**
|
|
49
|
+
* Where the view jumps, 0–1 through the move (0.5 for a whip pan or punch zoom that cuts
|
|
50
|
+
* halfway). Keyframes either side of it must not be tweened across. Absent for a smooth move.
|
|
51
|
+
*/
|
|
52
|
+
cut?: number;
|
|
53
|
+
}
|
|
28
54
|
/** One entry of a provider's catalog. */
|
|
29
55
|
export interface EffectSummary {
|
|
30
56
|
/** Stable id, saved in documents. Never reused for a different effect. */
|
|
@@ -48,6 +74,8 @@ export interface EffectSummary {
|
|
|
48
74
|
* background of its own to fade (an overlay is already transparent).
|
|
49
75
|
*/
|
|
50
76
|
backgroundOpacity?: string;
|
|
77
|
+
/** How a `camera` effect moves. Present exactly when `kind` is `camera`. */
|
|
78
|
+
camera?: EffectCameraMove;
|
|
51
79
|
}
|
|
52
80
|
export interface EffectSearchOptions {
|
|
53
81
|
kind?: EffectKind;
|
|
@@ -161,6 +189,77 @@ export type EffectVectorTarget = string | {
|
|
|
161
189
|
svg: string;
|
|
162
190
|
nodeIds?: readonly string[];
|
|
163
191
|
};
|
|
192
|
+
/**
|
|
193
|
+
* Where a scene's camera looks (the shape of Flair's `View`). The scene is drawn at
|
|
194
|
+
* (point + scroll) × zoom, then turned by `tilt`, `turn` and `roll` round the middle of the stage
|
|
195
|
+
* and seen in perspective, then blurred by `blur` while the camera moves. Positions are in the
|
|
196
|
+
* scene's own units, so a framing is `stage.width / zoom` wide.
|
|
197
|
+
*/
|
|
198
|
+
export interface CameraView {
|
|
199
|
+
/** Scale of the scene on the stage; > 0. */
|
|
200
|
+
zoom: number;
|
|
201
|
+
/** Scene offset before scaling, in scene units. */
|
|
202
|
+
scrollX: number;
|
|
203
|
+
scrollY: number;
|
|
204
|
+
/** Tips the scene back round its horizontal axis (CSS `rotateX`), degrees, −75 to 75. Default 0. */
|
|
205
|
+
tilt?: number;
|
|
206
|
+
/** Swings the scene round its vertical axis (CSS `rotateY`), degrees, −75 to 75. Default 0. */
|
|
207
|
+
turn?: number;
|
|
208
|
+
/** Rolls the scene round the line of sight (CSS `rotateZ`), degrees clockwise, −180 to 180. Default 0. */
|
|
209
|
+
roll?: number;
|
|
210
|
+
/** Distance of the lens from the scene as a multiple of the stage width; smaller is more dramatic. Default the provider's own. */
|
|
211
|
+
perspective?: number;
|
|
212
|
+
/** Motion blur in stage pixels while the camera moves; 0 or absent when still. */
|
|
213
|
+
blur?: number;
|
|
214
|
+
/** Direction the blur streaks, degrees on screen (0 horizontal, 90 vertical); absent when it blurs every way (a zoom or a turn). */
|
|
215
|
+
blurAngle?: number;
|
|
216
|
+
}
|
|
217
|
+
/** The stage a camera looks at, in pixels. */
|
|
218
|
+
export interface CameraStage {
|
|
219
|
+
width: number;
|
|
220
|
+
height: number;
|
|
221
|
+
}
|
|
222
|
+
/** Where a camera move is: what `cameraView` takes. */
|
|
223
|
+
export interface CameraMoveContext {
|
|
224
|
+
/** The framing the move leaves; for a hold, the framing it moves within (the same as `to`). */
|
|
225
|
+
from: CameraView;
|
|
226
|
+
/** The framing the move arrives at. */
|
|
227
|
+
to: CameraView;
|
|
228
|
+
/** How far through the move, 0–1. */
|
|
229
|
+
u: number;
|
|
230
|
+
/** Length of the whole move (ms): its `durationMs`, the editor's override, or for a hold the shot's hold. */
|
|
231
|
+
ms: number;
|
|
232
|
+
stage: CameraStage;
|
|
233
|
+
}
|
|
234
|
+
/** What `sampleCameraMove` takes: the move's framings, length and stage, and how often to key it. */
|
|
235
|
+
export interface CameraMoveSampleRequest {
|
|
236
|
+
from: CameraView;
|
|
237
|
+
to: CameraView;
|
|
238
|
+
/** Length of the move (ms). */
|
|
239
|
+
ms: number;
|
|
240
|
+
stage: CameraStage;
|
|
241
|
+
/** Keys per second; > 0. */
|
|
242
|
+
fps: number;
|
|
243
|
+
/** Control values; missing controls take their defaults. */
|
|
244
|
+
values?: EffectValues;
|
|
245
|
+
}
|
|
246
|
+
/** One key of a sampled camera move. */
|
|
247
|
+
export interface CameraKeyframe {
|
|
248
|
+
/** Ms from the start of the move. */
|
|
249
|
+
t: number;
|
|
250
|
+
view: CameraView;
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* A camera move as keyframes, so an editor can bake it without calling `cameraView` each frame.
|
|
254
|
+
* Keys are in time order from `t` 0 to `ms`, one per frame. At a cut two keys share the cut's
|
|
255
|
+
* `t`: the view just before it, then the view after; tweening between consecutive keys never
|
|
256
|
+
* crosses the jump.
|
|
257
|
+
*/
|
|
258
|
+
export interface CameraMoveSample {
|
|
259
|
+
keys: readonly CameraKeyframe[];
|
|
260
|
+
/** Ms of the cut, when the move has one. */
|
|
261
|
+
cut?: number;
|
|
262
|
+
}
|
|
164
263
|
/** What a provider can do, so editors can degrade cleanly. */
|
|
165
264
|
export interface EffectProviderCapabilities {
|
|
166
265
|
/** Renders off the main thread. */
|
|
@@ -177,6 +276,8 @@ export interface EffectProviderCapabilities {
|
|
|
177
276
|
maxSize?: number;
|
|
178
277
|
/** Implements `openAuthoring`: effects can be edited, not only used. */
|
|
179
278
|
supportsAuthoring?: boolean;
|
|
279
|
+
/** Lists `camera` effects and implements `cameraView` (and maybe `sampleCameraMove`). */
|
|
280
|
+
supportsCamera?: boolean;
|
|
180
281
|
}
|
|
181
282
|
export type MaybePromise<T> = T | Promise<T>;
|
|
182
283
|
/**
|
|
@@ -196,7 +297,10 @@ export interface EffectProvider {
|
|
|
196
297
|
/** Effects matching every word of `query` (in name, id, category, tags or description), best first. */
|
|
197
298
|
search(query: string, options?: EffectSearchOptions): MaybePromise<readonly EffectSummary[]>;
|
|
198
299
|
describe(id: string): MaybePromise<EffectDescription>;
|
|
199
|
-
/**
|
|
300
|
+
/**
|
|
301
|
+
* A pixel renderer. A `vector` effect may refuse with `EffectUnsupportedError` where it cannot
|
|
302
|
+
* lay out SVG (a Worker); a `camera` effect draws no pixels and may refuse with one saying so.
|
|
303
|
+
*/
|
|
200
304
|
createRenderer(id: string, options: EffectRendererOptions): MaybePromise<EffectRenderer>;
|
|
201
305
|
/**
|
|
202
306
|
* Vector and motion effects as an animated SVG. Present when `capabilities.supportsVector`. A
|
|
@@ -210,4 +314,17 @@ export interface EffectProvider {
|
|
|
210
314
|
* `UnknownEffectError`; an effect the provider cannot edit throws `EffectUnsupportedError`.
|
|
211
315
|
*/
|
|
212
316
|
openAuthoring?(from: EffectAuthoringSource): MaybePromise<EffectAuthoringSession>;
|
|
317
|
+
/**
|
|
318
|
+
* Where a camera move puts the camera `ctx.u` of the way through it, with its motion blur.
|
|
319
|
+
* Present when `capabilities.supportsCamera`. Synchronous and pure: the same id, context and
|
|
320
|
+
* values always give the same view, so editors may call it per frame or cache it. Unknown ids
|
|
321
|
+
* throw `UnknownEffectError`, invalid values `EffectControlError`, and an effect that is not a
|
|
322
|
+
* camera move (or a `u` outside 0–1) `EffectUnsupportedError`.
|
|
323
|
+
*/
|
|
324
|
+
cameraView?(id: string, ctx: CameraMoveContext, values?: EffectValues): CameraView;
|
|
325
|
+
/**
|
|
326
|
+
* A camera move as keyframes (see `CameraMoveSample`). Optional even with `supportsCamera`;
|
|
327
|
+
* `sampleEffectCameraMove` falls back to `cameraView` per frame. Errors as for `cameraView`.
|
|
328
|
+
*/
|
|
329
|
+
sampleCameraMove?(id: string, request: CameraMoveSampleRequest): MaybePromise<CameraMoveSample>;
|
|
213
330
|
}
|