pixi-effects 0.9.0 → 0.11.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 +36 -0
- package/README.md +6 -6
- package/ai/SKILL.md +4 -3
- package/ai/reference/cheatsheet.md +17 -6
- package/ai/reference/pitfalls.md +3 -3
- package/ai/reference/recipes.md +141 -11
- package/ai/template.html +11 -8
- package/ai/tools/render.mjs +14 -5
- package/dist/{Base-CSIulyIq.d.cts → Base-CwUxTfja.d.cts} +1 -1
- package/dist/{Base-DVgwyIUj.d.ts → Base-CyvSLoCP.d.ts} +1 -1
- package/dist/Composition-7TEKHZNQ.cjs +14 -0
- package/dist/{Composition-CM62XJKT.cjs.map → Composition-7TEKHZNQ.cjs.map} +1 -1
- package/dist/Composition-HIISJAX2.js +5 -0
- package/dist/{Composition-RJDSQYTV.js.map → Composition-HIISJAX2.js.map} +1 -1
- package/dist/Controller.cjs +34 -0
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +12 -2
- package/dist/Controller.d.ts +12 -2
- package/dist/Controller.js +34 -0
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-qGHp15c_.d.cts → Movie-DSSNOF3T.d.cts} +61 -2
- package/dist/{Movie-QMlFyHkf.d.ts → Movie-nzmWsxWX.d.ts} +61 -2
- package/dist/{chunk-O4AJYUPK.js → chunk-3IPDNC3E.js} +90 -14
- package/dist/chunk-3IPDNC3E.js.map +1 -0
- package/dist/{chunk-AGMNSJJH.cjs → chunk-6NWJLVCA.cjs} +196 -12
- package/dist/chunk-6NWJLVCA.cjs.map +1 -0
- package/dist/{chunk-FNKSXBKB.js → chunk-C6QHKYC5.js} +193 -14
- package/dist/chunk-C6QHKYC5.js.map +1 -0
- package/dist/{chunk-PDBDDGG5.cjs → chunk-F7NCHS6Q.cjs} +108 -32
- package/dist/chunk-F7NCHS6Q.cjs.map +1 -0
- package/dist/index.cjs +542 -38
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +303 -5
- package/dist/index.d.ts +303 -5
- package/dist/index.js +511 -27
- package/dist/index.js.map +1 -1
- package/dist/loader.css +110 -0
- package/dist/three.cjs +5 -5
- package/dist/three.d.cts +2 -2
- package/dist/three.d.ts +2 -2
- package/dist/three.js +1 -1
- package/dist/{types-oJijbn-K.d.ts → types-BXu28nJj.d.cts} +26 -2
- package/dist/{types-oJijbn-K.d.cts → types-BXu28nJj.d.ts} +26 -2
- package/docs/api.md +16 -1
- package/docs/dsl.md +137 -2
- package/llms-full.txt +318 -26
- package/package.json +5 -2
- package/dist/Composition-CM62XJKT.cjs +0 -14
- package/dist/Composition-RJDSQYTV.js +0 -5
- package/dist/chunk-AGMNSJJH.cjs.map +0 -1
- package/dist/chunk-FNKSXBKB.js.map +0 -1
- package/dist/chunk-O4AJYUPK.js.map +0 -1
- package/dist/chunk-PDBDDGG5.cjs.map +0 -1
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { CompositionSequence } from './chunk-
|
|
2
|
-
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-
|
|
3
|
-
export { registerSequenceType } from './chunk-
|
|
1
|
+
import { CompositionSequence } from './chunk-3IPDNC3E.js';
|
|
2
|
+
import { ensureFilterLibrary, homeDistance, DEFAULT_FOV, random, noise, flattenSvgPath, rand, DEG, resolveAt, collectPropKeys } from './chunk-C6QHKYC5.js';
|
|
3
|
+
export { noise, rand, random, registerSequenceType } from './chunk-C6QHKYC5.js';
|
|
4
4
|
import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
|
|
5
5
|
import * as PIXI from 'pixi.js';
|
|
6
6
|
import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
@@ -276,6 +276,24 @@ function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
|
276
276
|
issues
|
|
277
277
|
};
|
|
278
278
|
}
|
|
279
|
+
|
|
280
|
+
// src/core/motionBlur.ts
|
|
281
|
+
var DEFAULTS = { samples: 8, shutter: 0.5 };
|
|
282
|
+
function resolveMotionBlur(spec, where) {
|
|
283
|
+
if (spec === void 0 || spec === null || spec === false) return null;
|
|
284
|
+
const o = spec === true ? {} : typeof spec === "number" ? { samples: spec } : spec;
|
|
285
|
+
if (typeof spec === "object") warnUnknownOptions(`${where} motionBlur`, o, ["samples", "shutter"]);
|
|
286
|
+
const samples = o.samples ?? DEFAULTS.samples, shutter = o.shutter ?? DEFAULTS.shutter;
|
|
287
|
+
if (!(Number.isInteger(samples) && samples >= 2 && samples <= 64)) throw new Error(`pixi-effects: ${where}: motionBlur samples must be a whole number from 2 to 64, got ${samples}`);
|
|
288
|
+
if (!(Number.isFinite(shutter) && shutter > 0 && shutter <= 1)) throw new Error(`pixi-effects: ${where}: motionBlur shutter must be a fraction of the frame time above 0 and up to 1 (0.5 is a 180\xB0 shutter), got ${shutter}`);
|
|
289
|
+
return { samples, shutter };
|
|
290
|
+
}
|
|
291
|
+
function blurTimes(frame, frameRate, mb, duration) {
|
|
292
|
+
const t = frame / frameRate, width = mb.shutter / frameRate;
|
|
293
|
+
return Array.from({ length: mb.samples }, (_, k) => Math.min(Math.max(t + ((k + 0.5) / mb.samples - 0.5) * width, 0), duration));
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// src/core/Renderer.ts
|
|
279
297
|
var VIDEO_CODEC_BY_FORMAT = {
|
|
280
298
|
mp4: "avc",
|
|
281
299
|
mov: "avc",
|
|
@@ -348,7 +366,10 @@ async function exportFrames(movie, options = {}) {
|
|
|
348
366
|
format: container,
|
|
349
367
|
target: new BufferTarget()
|
|
350
368
|
});
|
|
351
|
-
const
|
|
369
|
+
const mb = options.motionBlur !== void 0 ? resolveMotionBlur(options.motionBlur, "movie.render()") : movie.motionBlur ?? null;
|
|
370
|
+
const stage = movie.app.canvas;
|
|
371
|
+
const blurCanvas = mb ? Object.assign(document.createElement("canvas"), { width: stage.width, height: stage.height }) : null;
|
|
372
|
+
const canvasSource = new CanvasSource(blurCanvas ?? stage, {
|
|
352
373
|
codec: opts.video.codec,
|
|
353
374
|
bitrate: opts.video.bitrate
|
|
354
375
|
});
|
|
@@ -369,7 +390,8 @@ async function exportFrames(movie, options = {}) {
|
|
|
369
390
|
const keyframeIntervalFrames = Math.max(1, Math.round(2 * movie.frameRate));
|
|
370
391
|
try {
|
|
371
392
|
for (let frame = 0; frame <= movie.totalFrames; frame++) {
|
|
372
|
-
await movie.
|
|
393
|
+
if (mb && blurCanvas) await movie._exposeFrame(frame, mb, blurCanvas, false);
|
|
394
|
+
else await movie.gotoFrame(frame, true);
|
|
373
395
|
const isKey = frame === 0 || frame % keyframeIntervalFrames === 0;
|
|
374
396
|
const addOpts = isKey ? { keyFrame: true } : void 0;
|
|
375
397
|
await canvasSource.add(frame / movie.frameRate, 1 / movie.frameRate, addOpts);
|
|
@@ -378,6 +400,7 @@ async function exportFrames(movie, options = {}) {
|
|
|
378
400
|
}
|
|
379
401
|
await canvasSource.close();
|
|
380
402
|
await output.finalize();
|
|
403
|
+
if (mb) await movie.gotoFrame(movie.totalFrames, true);
|
|
381
404
|
return new Blob([output.target.buffer], { type: output.format.mimeType });
|
|
382
405
|
} finally {
|
|
383
406
|
movie.app.ticker.start();
|
|
@@ -1153,12 +1176,12 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1153
1176
|
blending.add(w.to);
|
|
1154
1177
|
}
|
|
1155
1178
|
}
|
|
1156
|
-
comp.layers().forEach(({ seq, display, threeD }, i) => {
|
|
1179
|
+
comp.layers().forEach(({ seq, display, threeD, carriers }, i) => {
|
|
1157
1180
|
const t = seq.target;
|
|
1158
1181
|
const name = seq.spec.name;
|
|
1159
1182
|
const label = name ?? `${seq.spec.type}#${i}`;
|
|
1160
|
-
const alpha = t?.alpha ?? 1;
|
|
1161
|
-
const alive = threeD ? display.visible : t?.renderable ?? true;
|
|
1183
|
+
const alpha = (t?.alpha ?? 1) * carriers.reduce((a, c) => a * (c.target?.alpha ?? 1), 1);
|
|
1184
|
+
const alive = (threeD ? display.visible : t?.renderable ?? true) && carriers.every((c) => c.target?.renderable ?? true);
|
|
1162
1185
|
const visible = parentVisible && alive && alpha > 0;
|
|
1163
1186
|
let bounds = null;
|
|
1164
1187
|
if (!insideThreeD) {
|
|
@@ -1173,9 +1196,16 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
1173
1196
|
if (area === 0) onCanvas = "none";
|
|
1174
1197
|
}
|
|
1175
1198
|
const keys = collectPropKeys(seq.spec);
|
|
1176
|
-
const
|
|
1177
|
-
|
|
1178
|
-
|
|
1199
|
+
const carried = carriers.some((c) => {
|
|
1200
|
+
const k = collectPropKeys(c.spec);
|
|
1201
|
+
return (c.spec.keyframes ?? []).length > 0 && (k.has("x") || k.has("y") || k.has("rotation") || k.has("scale"));
|
|
1202
|
+
});
|
|
1203
|
+
const moving = parentMoving || carried || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
1204
|
+
const zeroScale = (o) => {
|
|
1205
|
+
const s = o?.scale;
|
|
1206
|
+
return !!s && (s.x === 0 || s.y === 0);
|
|
1207
|
+
};
|
|
1208
|
+
const zero = parentZero || zeroScale(t) || carriers.some((c) => zeroScale(c.target));
|
|
1179
1209
|
const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
|
|
1180
1210
|
layers.push(info);
|
|
1181
1211
|
let shown = bounds ? clip(bounds, view) : null;
|
|
@@ -1308,7 +1338,8 @@ var COLORS = {
|
|
|
1308
1338
|
audio: "#ff7aa8",
|
|
1309
1339
|
composition: "#8ea4c8",
|
|
1310
1340
|
three: "#ff9f68",
|
|
1311
|
-
camera: "#b0b0b0"
|
|
1341
|
+
camera: "#b0b0b0",
|
|
1342
|
+
null: "#b0b0b0"
|
|
1312
1343
|
};
|
|
1313
1344
|
var esc = (s) => s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
1314
1345
|
var num = (n) => String(Math.round(n * 100) / 100);
|
|
@@ -1444,6 +1475,41 @@ function normalizePoster(poster, duration, frameRate) {
|
|
|
1444
1475
|
return { seconds, frame: Math.round(seconds * frameRate) };
|
|
1445
1476
|
}
|
|
1446
1477
|
|
|
1478
|
+
// src/core/loader.ts
|
|
1479
|
+
var FADE_FALLBACK_MS = 900;
|
|
1480
|
+
function resolveLoader(option, canvas) {
|
|
1481
|
+
if (option === false) return null;
|
|
1482
|
+
if (option instanceof HTMLElement) return option;
|
|
1483
|
+
if (typeof option === "string") return document.querySelector(option);
|
|
1484
|
+
let box = canvas?.parentElement ?? null;
|
|
1485
|
+
for (let level = 0; box && level < 4; level++, box = box.parentElement) {
|
|
1486
|
+
if (level > 0 && (box === document.body || box === document.documentElement)) break;
|
|
1487
|
+
const found = box.querySelector(".pe-loader");
|
|
1488
|
+
if (found) return found;
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
function dismissLoader(loader) {
|
|
1493
|
+
if (!loader || loader.classList.contains("pe-loader--done")) return;
|
|
1494
|
+
loader.classList.add("pe-loader--done");
|
|
1495
|
+
loader.setAttribute("aria-hidden", "true");
|
|
1496
|
+
let removed = false;
|
|
1497
|
+
const remove = () => {
|
|
1498
|
+
if (!removed) {
|
|
1499
|
+
removed = true;
|
|
1500
|
+
loader.remove();
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
loader.addEventListener("transitionend", remove, { once: true });
|
|
1504
|
+
setTimeout(remove, FADE_FALLBACK_MS);
|
|
1505
|
+
}
|
|
1506
|
+
function failLoader(loader, message) {
|
|
1507
|
+
if (!loader) return;
|
|
1508
|
+
loader.setAttribute("data-state", "error");
|
|
1509
|
+
if (loader.hasAttribute("data-label")) loader.setAttribute("data-label", message);
|
|
1510
|
+
loader.setAttribute("aria-busy", "false");
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1447
1513
|
// src/core/Movie.ts
|
|
1448
1514
|
extensions.add(CullerPlugin);
|
|
1449
1515
|
gsap.registerPlugin(PixiPlugin);
|
|
@@ -1459,6 +1525,8 @@ var Movie = class {
|
|
|
1459
1525
|
_cancelAudioStart = null;
|
|
1460
1526
|
/** The poster time in seconds (`movie.init({ poster })`), or null. */
|
|
1461
1527
|
poster = null;
|
|
1528
|
+
/** The motion blur `render()`, `snapshot()` and `contactSheet()` apply unless told otherwise (`movie.init({ motionBlur })`), or null. */
|
|
1529
|
+
motionBlur = null;
|
|
1462
1530
|
_posterFrame = null;
|
|
1463
1531
|
/** The canvas shows the poster picture while the playhead is still at frame 0 (like `<video poster>`): the first seek or play replaces it. */
|
|
1464
1532
|
_atPoster = false;
|
|
@@ -1515,7 +1583,7 @@ var Movie = class {
|
|
|
1515
1583
|
const back = this.currentFrame;
|
|
1516
1584
|
this.currentFrame = this._posterFrame;
|
|
1517
1585
|
try {
|
|
1518
|
-
this.timeline.time(this.
|
|
1586
|
+
this.timeline.time(this._timeOf(this._posterFrame));
|
|
1519
1587
|
await this._awaitVideoFrames();
|
|
1520
1588
|
this._updateSpace();
|
|
1521
1589
|
this._renderNow();
|
|
@@ -1543,7 +1611,7 @@ var Movie = class {
|
|
|
1543
1611
|
return this._audioContext;
|
|
1544
1612
|
}
|
|
1545
1613
|
async init(options = {}) {
|
|
1546
|
-
warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster"]);
|
|
1614
|
+
warnUnknownOptions("movie.init()", options, ["width", "height", "duration", "frameRate", "background", "canvas", "assets", "composition", "poster", "motionBlur", "loader"]);
|
|
1547
1615
|
this._initState = "pending";
|
|
1548
1616
|
try {
|
|
1549
1617
|
this.width = options.width ?? 1920;
|
|
@@ -1553,6 +1621,7 @@ var Movie = class {
|
|
|
1553
1621
|
this.totalFrames = Math.round(this.duration * this.frameRate);
|
|
1554
1622
|
const poster = normalizePoster(options.poster, this.duration, this.frameRate);
|
|
1555
1623
|
this.poster = poster.seconds;
|
|
1624
|
+
this.motionBlur = resolveMotionBlur(options.motionBlur, "movie.init()");
|
|
1556
1625
|
this._posterFrame = poster.frame;
|
|
1557
1626
|
this.background = options.background ?? "#000000";
|
|
1558
1627
|
this.timeline = gsap.timeline({ paused: true, defaults: { ease: "none" } });
|
|
@@ -1619,7 +1688,9 @@ var Movie = class {
|
|
|
1619
1688
|
this._initState = "ready";
|
|
1620
1689
|
if (this._posterFrame !== null) await this._showPoster();
|
|
1621
1690
|
this.emit("ready");
|
|
1691
|
+
dismissLoader(resolveLoader(options.loader, options.canvas ?? this.app?.canvas ?? null));
|
|
1622
1692
|
} catch (err) {
|
|
1693
|
+
failLoader(resolveLoader(options.loader, options.canvas ?? null), "COULD NOT LOAD");
|
|
1623
1694
|
try {
|
|
1624
1695
|
await this.destroy();
|
|
1625
1696
|
} catch (cleanupErr) {
|
|
@@ -1653,17 +1724,24 @@ var Movie = class {
|
|
|
1653
1724
|
async _goto(frame) {
|
|
1654
1725
|
this._atPoster = false;
|
|
1655
1726
|
this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1656
|
-
this.timeline.time(this.
|
|
1727
|
+
this.timeline.time(this._timeOf(this.currentFrame));
|
|
1657
1728
|
await this._awaitVideoFrames();
|
|
1658
1729
|
this._updateSpace();
|
|
1659
1730
|
this._renderNow();
|
|
1660
1731
|
this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
|
|
1661
1732
|
}
|
|
1662
|
-
|
|
1733
|
+
/**
|
|
1734
|
+
* The moment frame `frame` shows. The very last frame (`totalFrames`) is at the end of the movie, where every layer's lifespan
|
|
1735
|
+
* has just closed: it is drawn a hair before, so a layer that lasts to the end is still on screen in the final frame
|
|
1736
|
+
* (it used to be an empty one, the last frame of every export).
|
|
1737
|
+
*/
|
|
1738
|
+
_timeOf(frame) {
|
|
1739
|
+
return Math.min(frame / this.frameRate, Math.max(0, this.duration - END_MARGIN));
|
|
1740
|
+
}
|
|
1741
|
+
async _awaitVideoFrames(t = this._timeOf(this.currentFrame)) {
|
|
1663
1742
|
if (!this._rootSequence) return;
|
|
1664
1743
|
const collected = [];
|
|
1665
1744
|
collectVideoSequences(this._rootSequence, collected);
|
|
1666
|
-
const t = this.currentFrame / this.frameRate;
|
|
1667
1745
|
await Promise.all(collected.map((v) => {
|
|
1668
1746
|
const local = t - (v.absoluteStart ?? v.at);
|
|
1669
1747
|
if (local < 0 || local > (v.duration ?? 0)) return Promise.resolve();
|
|
@@ -1671,19 +1749,27 @@ var Movie = class {
|
|
|
1671
1749
|
}));
|
|
1672
1750
|
}
|
|
1673
1751
|
async snapshot(frame = this.currentFrame, opts = {}) {
|
|
1674
|
-
warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as"]);
|
|
1752
|
+
warnUnknownOptions("movie.snapshot()", opts, ["scale", "type", "as", "motionBlur"]);
|
|
1675
1753
|
this._requireReady("snapshot");
|
|
1676
|
-
|
|
1754
|
+
const mb = this._blurFor(opts.motionBlur, "movie.snapshot()");
|
|
1755
|
+
const f = clampFrame(frame, this.totalFrames);
|
|
1756
|
+
let source = this.app.canvas;
|
|
1757
|
+
if (mb) {
|
|
1758
|
+
source = this._blurCanvas();
|
|
1759
|
+
await this._quietly(() => this._exposeFrame(f, mb, source, true));
|
|
1760
|
+
} else await this._quietly(() => this.gotoFrame(f, true));
|
|
1677
1761
|
const scale = opts.scale ?? 1;
|
|
1678
1762
|
const out = document.createElement("canvas");
|
|
1679
1763
|
out.width = Math.max(1, Math.round(this.width * scale));
|
|
1680
1764
|
out.height = Math.max(1, Math.round(this.height * scale));
|
|
1681
|
-
out.getContext("2d").drawImage(
|
|
1765
|
+
out.getContext("2d").drawImage(source, 0, 0, out.width, out.height);
|
|
1682
1766
|
return encodeCanvas(out, opts.type ?? "image/png", opts.as ?? "blob");
|
|
1683
1767
|
}
|
|
1684
1768
|
async contactSheet(opts = {}) {
|
|
1685
|
-
warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as"]);
|
|
1769
|
+
warnUnknownOptions("movie.contactSheet()", opts, ["frames", "times", "count", "columns", "cellWidth", "as", "motionBlur"]);
|
|
1686
1770
|
this._requireReady("contactSheet");
|
|
1771
|
+
const mb = this._blurFor(opts.motionBlur, "movie.contactSheet()");
|
|
1772
|
+
const blurred = mb ? this._blurCanvas() : null;
|
|
1687
1773
|
const frames = (opts.frames ?? opts.times?.map((t) => Math.round(t * this.frameRate)) ?? pickFrames(this.totalFrames, opts.count ?? 6)).map((f) => clampFrame(f, this.totalFrames));
|
|
1688
1774
|
const LABEL_H = 26;
|
|
1689
1775
|
const L = sheetLayout(frames.length, opts.columns ?? 3, opts.cellWidth ?? 480, this.width, this.height, LABEL_H);
|
|
@@ -1697,9 +1783,10 @@ var Movie = class {
|
|
|
1697
1783
|
try {
|
|
1698
1784
|
for (let i = 0; i < frames.length; i++) {
|
|
1699
1785
|
const f = frames[i];
|
|
1700
|
-
await this._quietly(() => this.
|
|
1786
|
+
if (mb && blurred) await this._quietly(() => this._exposeFrame(f, mb, blurred, true));
|
|
1787
|
+
else await this._quietly(() => this.gotoFrame(f, true));
|
|
1701
1788
|
const { x, y } = L.positions[i];
|
|
1702
|
-
g.drawImage(this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
|
|
1789
|
+
g.drawImage(blurred ?? this.app.canvas, x, y + LABEL_H, L.cellW, L.cellH);
|
|
1703
1790
|
g.fillStyle = "#e8ecf8";
|
|
1704
1791
|
g.font = "600 15px system-ui, sans-serif";
|
|
1705
1792
|
g.textBaseline = "middle";
|
|
@@ -1769,9 +1856,46 @@ var Movie = class {
|
|
|
1769
1856
|
}
|
|
1770
1857
|
}
|
|
1771
1858
|
/** Projects every `threeD` layer for the current frame (see src/space). */
|
|
1772
|
-
_updateSpace() {
|
|
1859
|
+
_updateSpace(t = this._timeOf(this.currentFrame)) {
|
|
1773
1860
|
if (!this._rootSequence || !this.app) return;
|
|
1774
|
-
this._rootSequence.updateSpace(
|
|
1861
|
+
this._rootSequence.updateSpace(t, this.app.renderer);
|
|
1862
|
+
}
|
|
1863
|
+
/** The motion blur an operation uses: its own option when given (`false` is off), otherwise the movie's. */
|
|
1864
|
+
_blurFor(option, where) {
|
|
1865
|
+
return option === void 0 ? this.motionBlur : resolveMotionBlur(option, where);
|
|
1866
|
+
}
|
|
1867
|
+
/**
|
|
1868
|
+
* @internal Draw `frame` with motion blur into `target` (a canvas the size of the stage): the average of `mb.samples` renders at moments
|
|
1869
|
+
* spread over the shutter interval around the frame time. With `settle` the playhead and the stage are left exactly on the frame.
|
|
1870
|
+
*/
|
|
1871
|
+
async _exposeFrame(frame, mb, target, settle) {
|
|
1872
|
+
const g = target.getContext("2d");
|
|
1873
|
+
const stage = this.app.canvas;
|
|
1874
|
+
this._atPoster = false;
|
|
1875
|
+
const times = blurTimes(frame, this.frameRate, mb, Math.max(0, this.duration - END_MARGIN));
|
|
1876
|
+
for (let k = 0; k < times.length; k++) {
|
|
1877
|
+
const t = times[k];
|
|
1878
|
+
this.timeline.time(t);
|
|
1879
|
+
await this._awaitVideoFrames(t);
|
|
1880
|
+
this._updateSpace(t);
|
|
1881
|
+
this._renderNow();
|
|
1882
|
+
if (k === 0) g.clearRect(0, 0, target.width, target.height);
|
|
1883
|
+
g.globalAlpha = 1 / (k + 1);
|
|
1884
|
+
g.drawImage(stage, 0, 0, target.width, target.height);
|
|
1885
|
+
}
|
|
1886
|
+
g.globalAlpha = 1;
|
|
1887
|
+
if (settle) await this._goto(frame);
|
|
1888
|
+
else {
|
|
1889
|
+
this.currentFrame = Math.max(0, Math.min(frame, this.totalFrames));
|
|
1890
|
+
this.emit("frame", { frame: this.currentFrame, totalFrames: this.totalFrames });
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
_blurCanvas() {
|
|
1894
|
+
const stage = this.app.canvas;
|
|
1895
|
+
const c = document.createElement("canvas");
|
|
1896
|
+
c.width = stage.width;
|
|
1897
|
+
c.height = stage.height;
|
|
1898
|
+
return c;
|
|
1775
1899
|
}
|
|
1776
1900
|
play() {
|
|
1777
1901
|
if (this._initState !== "ready") return;
|
|
@@ -1870,7 +1994,7 @@ var Movie = class {
|
|
|
1870
1994
|
return this.muted;
|
|
1871
1995
|
}
|
|
1872
1996
|
async render(options) {
|
|
1873
|
-
warnUnknownOptions("movie.render()", options, ["format", "video", "audio"]);
|
|
1997
|
+
warnUnknownOptions("movie.render()", options, ["format", "video", "audio", "motionBlur"]);
|
|
1874
1998
|
try {
|
|
1875
1999
|
return await this._quietly(() => exportFrames(this, options));
|
|
1876
2000
|
} catch (err) {
|
|
@@ -1895,6 +2019,7 @@ var Movie = class {
|
|
|
1895
2019
|
this._initState = "destroyed";
|
|
1896
2020
|
}
|
|
1897
2021
|
};
|
|
2022
|
+
var END_MARGIN = 1e-4;
|
|
1898
2023
|
var clampFrame = (f, last) => Math.max(0, Math.min(Math.round(f), last));
|
|
1899
2024
|
function encodeCanvas(canvas, type, as) {
|
|
1900
2025
|
if (as === "dataURL") return Promise.resolve(canvas.toDataURL(type));
|
|
@@ -2136,6 +2261,365 @@ function orbit(opts) {
|
|
|
2136
2261
|
return spec;
|
|
2137
2262
|
}
|
|
2138
2263
|
|
|
2264
|
+
// src/presets/particles.ts
|
|
2265
|
+
var KEYS = ["count", "at", "emit", "life", "area", "angle", "speed", "gravity", "wind", "drag", "sway", "size", "scale", "fade", "colors", "shape", "spin", "seed", "name", "blendMode", "template", "sampleRate"];
|
|
2266
|
+
var SHAPES = ["circle", "rect", "star"];
|
|
2267
|
+
var MAX_KEYFRAMES = 3e4;
|
|
2268
|
+
function particles(opts) {
|
|
2269
|
+
warnUnknownOptions("particles()", opts, KEYS);
|
|
2270
|
+
const { count, at = 0, emit = 0, gravity = 0, wind = 0, drag = 0, sway, fade, colors = ["#ffffff"], shape = "circle", seed = 0, name = "particle", template, sampleRate = 20 } = opts;
|
|
2271
|
+
if (!(Number.isInteger(count) && count > 0)) throw new Error(`particles(): count must be a whole number of particles (at least 1), got ${count}`);
|
|
2272
|
+
const life = range("life", opts.life, 1e-6);
|
|
2273
|
+
if (!opts.area || !Number.isFinite(opts.area.x) || !Number.isFinite(opts.area.y)) throw new Error("particles(): area is required: { x, y } (the centre) and optionally width / height to spread over");
|
|
2274
|
+
if (!(Number.isFinite(emit) && emit >= 0)) throw new Error(`particles(): emit must be a number of seconds >= 0, got ${emit}`);
|
|
2275
|
+
if (!(Number.isFinite(sampleRate) && sampleRate > 0)) throw new Error(`particles(): sampleRate must be a positive number (samples per second), got ${sampleRate}`);
|
|
2276
|
+
if (!SHAPES.includes(shape)) {
|
|
2277
|
+
const g = suggestName(String(shape), SHAPES);
|
|
2278
|
+
throw new Error(`particles(): shape "${shape}" is not one of ${SHAPES.map((s) => `"${s}"`).join(", ")}${g ? `; did you mean "${g}"?` : ""}`);
|
|
2279
|
+
}
|
|
2280
|
+
const angle = range("angle", opts.angle ?? [0, 360], -Infinity);
|
|
2281
|
+
const speed = range("speed", opts.speed ?? [100, 300], 0);
|
|
2282
|
+
const size = range("size", opts.size ?? [3, 6], 0);
|
|
2283
|
+
const spin = opts.spin === void 0 ? null : range("spin", opts.spin, -Infinity);
|
|
2284
|
+
const curved = gravity !== 0 || wind !== 0 || drag !== 0 || !!sway;
|
|
2285
|
+
const worst = curved ? Math.max(1, Math.ceil(life[1] * sampleRate)) : 1;
|
|
2286
|
+
if (count * worst > MAX_KEYFRAMES) {
|
|
2287
|
+
throw new Error(`particles(): too many keyframes (${count} particles \xD7 up to ${worst} samples each): lower count or life, or lower sampleRate (now ${sampleRate})`);
|
|
2288
|
+
}
|
|
2289
|
+
const round2 = (v) => Math.round(v * 1e6) / 1e6;
|
|
2290
|
+
const out = [];
|
|
2291
|
+
for (let i = 0; i < count; i++) {
|
|
2292
|
+
const r = random(seed * 1000003 + i * 7919 + 17);
|
|
2293
|
+
const pick = ([a2, b]) => a2 + r() * (b - a2);
|
|
2294
|
+
const born = at + r() * emit;
|
|
2295
|
+
const lifetime = pick(life);
|
|
2296
|
+
const ax = (r() - 0.5) * (opts.area.width ?? 0), ay = (r() - 0.5) * (opts.area.height ?? 0);
|
|
2297
|
+
const x0 = opts.area.x + ax, y0 = opts.area.y + ay;
|
|
2298
|
+
const a = pick(angle) * Math.PI / 180, v = pick(speed);
|
|
2299
|
+
const vx = v * Math.cos(a), vy = v * Math.sin(a);
|
|
2300
|
+
const s = pick(size);
|
|
2301
|
+
const color = colors[Math.min(colors.length - 1, Math.floor(r() * colors.length))];
|
|
2302
|
+
const turn = spin ? pick(spin) : 0;
|
|
2303
|
+
const startAngle = spin ? r() * 360 : 0;
|
|
2304
|
+
const swayAmp = sway?.amp ?? 0, swayFreq = sway?.freq ?? 1;
|
|
2305
|
+
const n0 = sway ? noise(0, seed * 1000003 + i) : 0;
|
|
2306
|
+
const at_ = (t) => {
|
|
2307
|
+
const axis = (p0, v0, acc) => {
|
|
2308
|
+
if (drag > 0) {
|
|
2309
|
+
const vInf = acc / drag;
|
|
2310
|
+
return p0 + vInf * t + (v0 - vInf) * (1 - Math.exp(-drag * t)) / drag;
|
|
2311
|
+
}
|
|
2312
|
+
return p0 + v0 * t + 0.5 * acc * t * t;
|
|
2313
|
+
};
|
|
2314
|
+
const sw = sway ? swayAmp * (noise(t * swayFreq, seed * 1000003 + i) - n0) : 0;
|
|
2315
|
+
return { x: axis(x0, vx, wind) + sw, y: axis(y0, vy, gravity) };
|
|
2316
|
+
};
|
|
2317
|
+
const kfs = [];
|
|
2318
|
+
const steps = curved ? Math.max(1, Math.ceil(lifetime * sampleRate)) : 1;
|
|
2319
|
+
const step = lifetime / steps;
|
|
2320
|
+
for (let k = 1; k <= steps; k++) {
|
|
2321
|
+
const p = at_(k * step);
|
|
2322
|
+
kfs.push({ at: round2((k - 1) * step), to: { x: round2(p.x), y: round2(p.y) }, duration: round2(step), ease: "none" });
|
|
2323
|
+
}
|
|
2324
|
+
if (opts.scale) kfs.push({ at: 0, to: { scale: opts.scale[1] }, duration: round2(lifetime), ease: "none" });
|
|
2325
|
+
if (turn !== 0) kfs.push({ at: 0, to: { rotation: round2(startAngle + turn * lifetime) }, duration: round2(lifetime), ease: "none" });
|
|
2326
|
+
if (fade?.in) kfs.push({ at: 0, to: { alpha: 1 }, duration: fade.in, ease: "none" });
|
|
2327
|
+
if (fade?.out) kfs.push({ at: -fade.out, from: { alpha: 1 }, to: { alpha: 0 }, duration: fade.out, ease: "none" });
|
|
2328
|
+
const initial = { x: round2(x0), y: round2(y0) };
|
|
2329
|
+
if (opts.scale) initial.scale = opts.scale[0];
|
|
2330
|
+
if (fade?.in) initial.alpha = 0;
|
|
2331
|
+
if (spin) initial.rotation = round2(startAngle);
|
|
2332
|
+
const common = { name: `${name}-${i}`, at: round2(born), duration: round2(lifetime), ...opts.blendMode ? { blendMode: opts.blendMode } : {} };
|
|
2333
|
+
if (template) {
|
|
2334
|
+
const t = template;
|
|
2335
|
+
out.push({ ...t, ...common, initial: { ...t.initial, ...initial }, keyframes: [...t.keyframes ?? [], ...kfs] });
|
|
2336
|
+
continue;
|
|
2337
|
+
}
|
|
2338
|
+
const style = { ...initial, fillColor: color };
|
|
2339
|
+
if (shape === "circle") out.push({ type: "shape", shape: "circle", radius: round2(s), ...common, initial: style, keyframes: kfs });
|
|
2340
|
+
else if (shape === "rect") out.push({ type: "shape", shape: "rect", width: round2(s * 2), height: round2(s * 2), ...common, initial: style, keyframes: kfs });
|
|
2341
|
+
else {
|
|
2342
|
+
const points = [];
|
|
2343
|
+
for (let p = 0; p < 10; p++) {
|
|
2344
|
+
const rad = p % 2 === 0 ? s : s * 0.45, ang = -Math.PI / 2 + p * Math.PI / 5;
|
|
2345
|
+
points.push([round2(rad * Math.cos(ang)), round2(rad * Math.sin(ang))]);
|
|
2346
|
+
}
|
|
2347
|
+
out.push({ type: "shape", shape: "polygon", points, ...common, initial: style, keyframes: kfs });
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
return out;
|
|
2351
|
+
}
|
|
2352
|
+
function range(name, v, min) {
|
|
2353
|
+
const [a, b] = typeof v === "number" ? [v, v] : v;
|
|
2354
|
+
if (!(Number.isFinite(a) && Number.isFinite(b)) || a > b || a < min || min > 0 && a <= 0) {
|
|
2355
|
+
throw new Error(`particles(): ${name} must be a number or [min, max] with min <= max${min > -Infinity ? ` and min >= ${min}` : ""}, got ${JSON.stringify(v)}`);
|
|
2356
|
+
}
|
|
2357
|
+
return [a, b];
|
|
2358
|
+
}
|
|
2359
|
+
|
|
2360
|
+
// src/presets/followPath.ts
|
|
2361
|
+
var KEYS2 = ["d", "at", "duration", "ease", "from", "to", "orient", "rotate", "frameRate"];
|
|
2362
|
+
var MAX_STEPS = 2e3;
|
|
2363
|
+
function followPath(opts) {
|
|
2364
|
+
warnUnknownOptions("followPath()", opts, KEYS2);
|
|
2365
|
+
const { d, at = 0, duration, ease = "none", from = 0, to = 1, orient = false, rotate = 0, frameRate = 30 } = opts;
|
|
2366
|
+
if (typeof d !== "string" || d.trim() === "") throw new Error('followPath(): d must be SVG path data, e.g. "M 100 600 C 300 100 900 100 1180 600"');
|
|
2367
|
+
if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`followPath(): duration must be a positive number of seconds, got ${duration}`);
|
|
2368
|
+
if (!(Number.isFinite(frameRate) && frameRate > 0)) throw new Error(`followPath(): frameRate must be a positive number (samples per second), got ${frameRate}`);
|
|
2369
|
+
for (const [n, v] of [["from", from], ["to", to]]) {
|
|
2370
|
+
if (!(Number.isFinite(v) && v >= 0 && v <= 1)) throw new Error(`followPath(): ${n} must be a fraction of the path between 0 and 1, got ${v}`);
|
|
2371
|
+
}
|
|
2372
|
+
const steps = Math.max(1, Math.round(duration * frameRate));
|
|
2373
|
+
if (steps > MAX_STEPS) throw new Error(`followPath(): ${steps} samples is too many (max ${MAX_STEPS}); lower frameRate or duration`);
|
|
2374
|
+
const segments = [];
|
|
2375
|
+
let total = 0;
|
|
2376
|
+
for (const line of flattenSvgPath(d)) {
|
|
2377
|
+
const p = line.closed ? [...line.pts, line.pts[0], line.pts[1]] : line.pts;
|
|
2378
|
+
for (let i = 2; i < p.length; i += 2) {
|
|
2379
|
+
const len = Math.hypot(p[i] - p[i - 2], p[i + 1] - p[i - 1]);
|
|
2380
|
+
if (len === 0) continue;
|
|
2381
|
+
segments.push({ x0: p[i - 2], y0: p[i - 1], x1: p[i], y1: p[i + 1], start: total, len });
|
|
2382
|
+
total += len;
|
|
2383
|
+
}
|
|
2384
|
+
}
|
|
2385
|
+
if (!(total > 0)) throw new Error(`followPath(): the path "${d}" has no length (it needs at least two different points)`);
|
|
2386
|
+
const find = (s) => {
|
|
2387
|
+
let lo = 0, hi = segments.length - 1;
|
|
2388
|
+
while (lo < hi) {
|
|
2389
|
+
const mid = lo + hi >> 1;
|
|
2390
|
+
if (segments[mid].start + segments[mid].len < s) lo = mid + 1;
|
|
2391
|
+
else hi = mid;
|
|
2392
|
+
}
|
|
2393
|
+
return segments[lo];
|
|
2394
|
+
};
|
|
2395
|
+
const at_ = (fraction) => {
|
|
2396
|
+
const s = Math.min(Math.max(fraction, 0), 1) * total;
|
|
2397
|
+
const g = find(s);
|
|
2398
|
+
const t = Math.min(Math.max((s - g.start) / g.len, 0), 1);
|
|
2399
|
+
return { x: g.x0 + (g.x1 - g.x0) * t, y: g.y0 + (g.y1 - g.y0) * t, heading: Math.atan2(g.y1 - g.y0, g.x1 - g.x0) * 180 / Math.PI };
|
|
2400
|
+
};
|
|
2401
|
+
const shape = parseEase(ease);
|
|
2402
|
+
const round2 = (v) => Math.round(v * 1e6) / 1e6;
|
|
2403
|
+
let turn = 0;
|
|
2404
|
+
const sample = (k) => {
|
|
2405
|
+
const p = from + (to - from) * shape(k / steps);
|
|
2406
|
+
const pt = at_(p);
|
|
2407
|
+
const out2 = { x: round2(pt.x), y: round2(pt.y) };
|
|
2408
|
+
if (orient) {
|
|
2409
|
+
let h = pt.heading + rotate;
|
|
2410
|
+
if (k === 0) turn = h;
|
|
2411
|
+
else {
|
|
2412
|
+
let delta = h - turn;
|
|
2413
|
+
delta -= 360 * Math.round(delta / 360);
|
|
2414
|
+
turn += delta;
|
|
2415
|
+
}
|
|
2416
|
+
out2.rotation = round2(turn);
|
|
2417
|
+
}
|
|
2418
|
+
return out2;
|
|
2419
|
+
};
|
|
2420
|
+
const out = [{ at, set: sample(0) }];
|
|
2421
|
+
const step = duration / steps;
|
|
2422
|
+
for (let k = 1; k <= steps; k++) out.push({ at: round2(at + (k - 1) * step), to: sample(k), duration: round2(step), ease: "none" });
|
|
2423
|
+
return out;
|
|
2424
|
+
}
|
|
2425
|
+
|
|
2426
|
+
// src/presets/stagger.ts
|
|
2427
|
+
var STAGGER_KEYS = ["each", "amount", "from", "grid", "ease", "seed"];
|
|
2428
|
+
var FROM_NAMES = ["start", "end", "center", "edges", "random"];
|
|
2429
|
+
function stagger(items, options = {}) {
|
|
2430
|
+
const isCount = typeof items === "number";
|
|
2431
|
+
const n = isCount ? items : items.length;
|
|
2432
|
+
if (!Number.isInteger(n) || n < 0) throw new Error(`stagger(): count must be a whole number of items, got ${n}`);
|
|
2433
|
+
warnUnknownOptions("stagger()", options, STAGGER_KEYS);
|
|
2434
|
+
const { each, amount, from = "start", grid, ease = "none", seed = 0 } = options;
|
|
2435
|
+
if (each !== void 0 && amount !== void 0) throw new Error("stagger(): give `each` (seconds between neighbours) or `amount` (seconds in total), not both");
|
|
2436
|
+
if (each !== void 0 && !(Number.isFinite(each) && each >= 0)) throw new Error(`stagger(): each must be a number of seconds >= 0, got ${each}`);
|
|
2437
|
+
if (amount !== void 0 && !(Number.isFinite(amount) && amount >= 0)) throw new Error(`stagger(): amount must be a number of seconds >= 0, got ${amount}`);
|
|
2438
|
+
if (typeof from === "string" && !FROM_NAMES.includes(from)) {
|
|
2439
|
+
throw new Error(`stagger(): from "${from}" is not one of ${FROM_NAMES.map((f) => `"${f}"`).join(", ")} or an item index`);
|
|
2440
|
+
}
|
|
2441
|
+
if (typeof from === "number" && !(Number.isInteger(from) && from >= 0 && from < Math.max(n, 1))) throw new Error(`stagger(): from ${from} is not an index of the ${n} items`);
|
|
2442
|
+
if (grid && !(Array.isArray(grid) && grid.length === 2 && grid.every((g) => Number.isInteger(g) && g > 0) && grid[0] * grid[1] >= n)) {
|
|
2443
|
+
throw new Error(`stagger(): grid must be [columns, rows] and hold all ${n} items, got ${JSON.stringify(grid)}`);
|
|
2444
|
+
}
|
|
2445
|
+
const dist = distances(n, from, grid, seed);
|
|
2446
|
+
const max = dist.reduce((m, d) => Math.max(m, d), 0);
|
|
2447
|
+
const total = amount !== void 0 ? amount : (each ?? 0.1) * max;
|
|
2448
|
+
const shape = parseEase(ease);
|
|
2449
|
+
const delays = dist.map((d) => max === 0 ? 0 : Math.round(total * shape(d / max) * 1e6) / 1e6);
|
|
2450
|
+
if (isCount) return delays;
|
|
2451
|
+
return items.map((layer, i) => ({ ...layer, at: Math.round(((layer.at ?? 0) + delays[i]) * 1e6) / 1e6 }));
|
|
2452
|
+
}
|
|
2453
|
+
function distances(n, from, grid, seed) {
|
|
2454
|
+
if (n === 0) return [];
|
|
2455
|
+
if (from === "random") {
|
|
2456
|
+
const order = Array.from({ length: n }, (_, i) => i);
|
|
2457
|
+
const r = random(seed);
|
|
2458
|
+
for (let i = n - 1; i > 0; i--) {
|
|
2459
|
+
const j = Math.floor(r() * (i + 1));
|
|
2460
|
+
[order[i], order[j]] = [order[j], order[i]];
|
|
2461
|
+
}
|
|
2462
|
+
return order;
|
|
2463
|
+
}
|
|
2464
|
+
if (!grid) {
|
|
2465
|
+
const mid = (n - 1) / 2;
|
|
2466
|
+
return Array.from({ length: n }, (_, i) => {
|
|
2467
|
+
switch (from) {
|
|
2468
|
+
case "start":
|
|
2469
|
+
return i;
|
|
2470
|
+
case "end":
|
|
2471
|
+
return n - 1 - i;
|
|
2472
|
+
case "center":
|
|
2473
|
+
return Math.abs(i - mid);
|
|
2474
|
+
case "edges":
|
|
2475
|
+
return mid - Math.abs(i - mid);
|
|
2476
|
+
default:
|
|
2477
|
+
return Math.abs(i - from);
|
|
2478
|
+
}
|
|
2479
|
+
});
|
|
2480
|
+
}
|
|
2481
|
+
const [cols, rows] = grid;
|
|
2482
|
+
const at = (i) => ({ x: i % cols, y: Math.floor(i / cols) });
|
|
2483
|
+
const origin = from === "start" ? { x: 0, y: 0 } : from === "end" ? { x: cols - 1, y: rows - 1 } : from === "center" || from === "edges" ? { x: (cols - 1) / 2, y: (rows - 1) / 2 } : at(from);
|
|
2484
|
+
const d = Array.from({ length: n }, (_, i) => Math.hypot(at(i).x - origin.x, at(i).y - origin.y));
|
|
2485
|
+
if (from !== "edges") return d;
|
|
2486
|
+
const far = Math.max(...d);
|
|
2487
|
+
return d.map((v) => far - v);
|
|
2488
|
+
}
|
|
2489
|
+
|
|
2490
|
+
// src/presets/wiggle.ts
|
|
2491
|
+
var WIGGLE_KEYS = ["at", "duration", "freq", "seed", "ease", "props"];
|
|
2492
|
+
var MAX_STEPS2 = 2e3;
|
|
2493
|
+
function wiggle(opts) {
|
|
2494
|
+
warnUnknownOptions("wiggle()", opts, WIGGLE_KEYS);
|
|
2495
|
+
const { at = 0, duration, freq = 3, seed = 0, ease = "sine.inOut", props } = opts;
|
|
2496
|
+
if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`wiggle(): duration must be a positive number of seconds, got ${duration}`);
|
|
2497
|
+
if (!(Number.isFinite(freq) && freq > 0)) throw new Error(`wiggle(): freq must be a positive number (steps per second), got ${freq}`);
|
|
2498
|
+
const names = Object.keys(props ?? {});
|
|
2499
|
+
if (names.length === 0) throw new Error("wiggle(): props is empty; name at least one property, e.g. props: { x: { around: 100, amp: 8 } }");
|
|
2500
|
+
for (const n of names) {
|
|
2501
|
+
const p = props[n];
|
|
2502
|
+
if (!Number.isFinite(p.amp)) throw new Error(`wiggle(): props.${n}.amp must be a number, got ${p.amp}`);
|
|
2503
|
+
if (p.around === void 0 || p.around === null || p.around === "") throw new Error(`wiggle(): props.${n}.around is missing`);
|
|
2504
|
+
}
|
|
2505
|
+
const steps = Math.ceil(duration * freq - 1e-9);
|
|
2506
|
+
if (steps > MAX_STEPS2) throw new Error(`wiggle(): ${steps} steps is too many (max ${MAX_STEPS2}); lower freq or duration`);
|
|
2507
|
+
const set = {};
|
|
2508
|
+
for (const n of names) set[n] = props[n].around;
|
|
2509
|
+
const out = [{ at, set }];
|
|
2510
|
+
const stepLen = duration / steps;
|
|
2511
|
+
for (let s = 0; s < steps; s++) {
|
|
2512
|
+
const to = {};
|
|
2513
|
+
names.forEach((n, pi) => {
|
|
2514
|
+
const p = props[n];
|
|
2515
|
+
if (s === steps - 1) {
|
|
2516
|
+
to[n] = p.around;
|
|
2517
|
+
return;
|
|
2518
|
+
}
|
|
2519
|
+
const off = (rand(seed * 7919 + pi * 104729 + s * 31 + 1) * 2 - 1) * p.amp;
|
|
2520
|
+
to[n] = typeof p.around === "number" ? p.around + off : `(${p.around}) ${off < 0 ? "-" : "+"} ${Math.abs(off)}`;
|
|
2521
|
+
});
|
|
2522
|
+
out.push({ at: at + s * stepLen, to, duration: stepLen, ease });
|
|
2523
|
+
}
|
|
2524
|
+
return out;
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
// src/presets/animateText.ts
|
|
2528
|
+
var KEYS3 = ["by", "x", "y", "align", "name", "at", "duration", "in", "out", "stagger", "idle", "styleFor"];
|
|
2529
|
+
var PRESETS = {
|
|
2530
|
+
rise: { from: { y: 40, alpha: 0 }, duration: 0.5, ease: "back.out(2)" },
|
|
2531
|
+
drop: { from: { y: -50, alpha: 0 }, duration: 0.7, ease: "bounce.out" },
|
|
2532
|
+
fade: { from: { alpha: 0 }, duration: 0.5, ease: "power1.out" },
|
|
2533
|
+
pop: { from: { scale: 0, alpha: 0 }, duration: 0.45, ease: "back.out(2.4)" },
|
|
2534
|
+
zoom: { from: { scale: 2.5, alpha: 0 }, duration: 0.6, ease: "power3.out" },
|
|
2535
|
+
slide: { from: { x: -60, alpha: 0 }, duration: 0.5, ease: "power3.out" },
|
|
2536
|
+
spin: { from: { rotation: -90, scale: 0, alpha: 0 }, duration: 0.6, ease: "back.out(1.7)" }
|
|
2537
|
+
};
|
|
2538
|
+
var PRESET_NAMES = Object.keys(PRESETS);
|
|
2539
|
+
function animateText(text, style, options) {
|
|
2540
|
+
warnUnknownOptions("animateText()", options, KEYS3);
|
|
2541
|
+
const { by = "chars", x = 0, y = 0, align = "left", name = "text", at = 0, duration, idle, styleFor } = options;
|
|
2542
|
+
if (!(Number.isFinite(duration) && duration > 0)) throw new Error(`animateText(): duration must be a positive number of seconds (how long until the last piece is gone), got ${duration}`);
|
|
2543
|
+
const pieces = splitText(text, style, { by, x, y, align });
|
|
2544
|
+
if (pieces.length === 0) return [];
|
|
2545
|
+
const lineHeight = measureText("M", style).height;
|
|
2546
|
+
const inTween = options.in === false ? null : resolveTween(options.in ?? "rise", "in");
|
|
2547
|
+
const outTween = options.out === void 0 || options.out === false ? null : resolveTween(options.out, "out");
|
|
2548
|
+
const delays = stagger(pieces.length, options.stagger ?? { each: 0.04 });
|
|
2549
|
+
const wave = Math.max(...delays);
|
|
2550
|
+
const need = (inTween?.duration ?? 0) + (outTween?.duration ?? 0) + wave;
|
|
2551
|
+
if (duration < need - 1e-9) {
|
|
2552
|
+
throw new Error(`animateText(): duration ${duration} is too short for this wave: the entrance${outTween ? ", the exit" : ""} and the stagger need at least ${+need.toFixed(3)} s`);
|
|
2553
|
+
}
|
|
2554
|
+
const end = at + duration;
|
|
2555
|
+
return pieces.map((p, i) => {
|
|
2556
|
+
const cx = p.x + p.width / 2, cy = p.y + lineHeight / 2;
|
|
2557
|
+
const rest = { x: cx, y: cy, alpha: 1, scale: 1, scaleX: 1, scaleY: 1, rotation: 0 };
|
|
2558
|
+
const start = at + delays[i];
|
|
2559
|
+
const len = end - start;
|
|
2560
|
+
const keyframes = [];
|
|
2561
|
+
const place = (key, v) => key === "x" || key === "y" ? rest[key] + v : v;
|
|
2562
|
+
if (inTween) {
|
|
2563
|
+
keyframes.push({
|
|
2564
|
+
at: 0,
|
|
2565
|
+
duration: inTween.duration,
|
|
2566
|
+
ease: inTween.ease,
|
|
2567
|
+
from: Object.fromEntries(Object.entries(inTween.from).map(([k, v]) => [k, place(k, v)])),
|
|
2568
|
+
to: Object.fromEntries(Object.keys(inTween.from).map((k) => [k, inTween.to[k] ?? rest[k]]))
|
|
2569
|
+
});
|
|
2570
|
+
}
|
|
2571
|
+
let idleEnd = len;
|
|
2572
|
+
if (outTween) {
|
|
2573
|
+
const begin = outTween.duration + (wave - delays[i]);
|
|
2574
|
+
idleEnd = len - begin;
|
|
2575
|
+
keyframes.push({
|
|
2576
|
+
at: -begin,
|
|
2577
|
+
duration: outTween.duration,
|
|
2578
|
+
ease: outTween.ease,
|
|
2579
|
+
from: Object.fromEntries(Object.keys(outTween.from).map((k) => [k, outTween.to[k] ?? rest[k]])),
|
|
2580
|
+
to: Object.fromEntries(Object.entries(outTween.from).map(([k, v]) => [k, place(k, v)]))
|
|
2581
|
+
});
|
|
2582
|
+
}
|
|
2583
|
+
if (idle) {
|
|
2584
|
+
const props = {};
|
|
2585
|
+
for (const k of ["x", "y", "rotation", "scale"]) if (idle[k] !== void 0) props[k] = { around: rest[k], amp: idle[k] };
|
|
2586
|
+
const freq = idle.freq ?? 1;
|
|
2587
|
+
const from = inTween?.duration ?? 0;
|
|
2588
|
+
if (Object.keys(props).length > 0 && idleEnd - from >= 1 / freq) {
|
|
2589
|
+
keyframes.push(...wiggle({ at: from, duration: idleEnd - from, freq, seed: (idle.seed ?? 0) * 1e3 + i, props }));
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
return {
|
|
2593
|
+
type: "text",
|
|
2594
|
+
name: `${name}-${i}`,
|
|
2595
|
+
text: p.text,
|
|
2596
|
+
style: styleFor ? { ...style, ...styleFor(p) } : style,
|
|
2597
|
+
at: start,
|
|
2598
|
+
duration: len,
|
|
2599
|
+
initial: { x: cx, y: cy, anchorX: 0.5, anchorY: 0.5 },
|
|
2600
|
+
keyframes
|
|
2601
|
+
};
|
|
2602
|
+
});
|
|
2603
|
+
}
|
|
2604
|
+
function resolveTween(spec, which) {
|
|
2605
|
+
const t = typeof spec === "string" ? { preset: spec } : spec;
|
|
2606
|
+
if (t.preset !== void 0 && !(t.preset in PRESETS)) {
|
|
2607
|
+
const guess = suggestName(String(t.preset), PRESET_NAMES);
|
|
2608
|
+
throw new Error(`animateText(): ${which} "${t.preset}" is not a preset${guess ? `; did you mean "${guess}"?` : ""} (presets: ${PRESET_NAMES.map((n) => `"${n}"`).join(", ")})`);
|
|
2609
|
+
}
|
|
2610
|
+
const base = t.preset ? PRESETS[t.preset] : void 0;
|
|
2611
|
+
const from = { ...base?.from ?? {}, ...t.from ?? {} };
|
|
2612
|
+
const to = t.to ?? {};
|
|
2613
|
+
const known = /* @__PURE__ */ new Set(["x", "y", "alpha", "scale", "scaleX", "scaleY", "rotation"]);
|
|
2614
|
+
for (const k of Object.keys(from)) {
|
|
2615
|
+
if (!known.has(k) && to[k] === void 0) throw new Error(`animateText(): ${which}.from has "${k}", which has no resting value; give ${which}.to: { ${k}: \u2026 }`);
|
|
2616
|
+
}
|
|
2617
|
+
if (Object.keys(from).length === 0) throw new Error(`animateText(): ${which} needs a preset or a from`);
|
|
2618
|
+
let ease = t.ease ?? base?.ease ?? "power2.out";
|
|
2619
|
+
if (which === "out" && t.ease === void 0) ease = ease.replace(".out", ".in");
|
|
2620
|
+
return { from, to: { ...to }, duration: t.duration ?? base?.duration ?? 0.5, ease };
|
|
2621
|
+
}
|
|
2622
|
+
|
|
2139
2623
|
// src/transforms/withFade.ts
|
|
2140
2624
|
function withFade(spec, opts) {
|
|
2141
2625
|
warnUnknownOptions("withFade()", opts, ["in", "out"]);
|
|
@@ -2156,6 +2640,6 @@ function withFade(spec, opts) {
|
|
|
2156
2640
|
return spec;
|
|
2157
2641
|
}
|
|
2158
2642
|
|
|
2159
|
-
export { Movie, kenBurns, measureText, orbit, splitText, withFade };
|
|
2643
|
+
export { Movie, animateText, dismissLoader, failLoader, followPath, kenBurns, measureText, orbit, particles, resolveLoader, splitText, stagger, wiggle, withFade };
|
|
2160
2644
|
//# sourceMappingURL=index.js.map
|
|
2161
2645
|
//# sourceMappingURL=index.js.map
|