pixi-effects 0.4.0 → 0.5.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 +30 -1
- package/README.md +9 -5
- package/ai/SKILL.md +7 -4
- package/ai/reference/cheatsheet.md +16 -5
- package/ai/reference/pitfalls.md +15 -11
- package/ai/reference/recipes.md +117 -0
- package/ai/template.html +6 -3
- package/ai/tools/check.mjs +385 -0
- package/dist/{Base-5R7C5pHR.d.cts → Base-CPSFBkPm.d.ts} +6 -1
- package/dist/{Base-DhY9oMxr.d.ts → Base-DnRAxroQ.d.cts} +6 -1
- package/dist/Composition-63EXHNJJ.cjs +13 -0
- package/dist/{Composition-D7RCAGEA.cjs.map → Composition-63EXHNJJ.cjs.map} +1 -1
- package/dist/Composition-TUCRSMJU.js +4 -0
- package/dist/{Composition-RGTRZ2JX.js.map → Composition-TUCRSMJU.js.map} +1 -1
- package/dist/Controller.cjs +37 -13
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +4 -2
- package/dist/Controller.d.ts +4 -2
- package/dist/Controller.js +37 -13
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-DRZtIGU7.d.ts → Movie-CuU_aaCm.d.cts} +56 -2
- package/dist/{Movie-CghFXprP.d.cts → Movie-VyDiUYOj.d.ts} +56 -2
- package/dist/{chunk-QGBOC5YL.cjs → chunk-23M5QTWW.cjs} +33 -21
- package/dist/chunk-23M5QTWW.cjs.map +1 -0
- package/dist/{chunk-6BQ6IEW2.cjs → chunk-2RR43OIN.cjs} +143 -30
- package/dist/chunk-2RR43OIN.cjs.map +1 -0
- package/dist/{chunk-A574IA4F.js → chunk-5Q5XZG7Z.js} +18 -6
- package/dist/chunk-5Q5XZG7Z.js.map +1 -0
- package/dist/chunk-DKGG7WD4.js +61 -0
- package/dist/chunk-DKGG7WD4.js.map +1 -0
- package/dist/{chunk-ZQX7WIYL.js → chunk-UBPZLGLU.js} +143 -31
- package/dist/chunk-UBPZLGLU.js.map +1 -0
- package/dist/chunk-UQBGW5DO.cjs +64 -0
- package/dist/chunk-UQBGW5DO.cjs.map +1 -0
- package/dist/index.cjs +291 -86
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +54 -5
- package/dist/index.d.ts +54 -5
- package/dist/index.js +275 -72
- package/dist/index.js.map +1 -1
- package/dist/sfx-MZJZXYY3.cjs +446 -0
- package/dist/sfx-MZJZXYY3.cjs.map +1 -0
- package/dist/sfx-S37DDC66.js +434 -0
- package/dist/sfx-S37DDC66.js.map +1 -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-CRE9WKd4.d.ts → types-7VtTEKUW.d.cts} +96 -6
- package/dist/{types-CRE9WKd4.d.cts → types-7VtTEKUW.d.ts} +96 -6
- package/docs/api.md +36 -0
- package/docs/dsl.md +44 -3
- package/llms-full.txt +234 -22
- package/llms.txt +1 -1
- package/package.json +2 -1
- package/dist/Composition-D7RCAGEA.cjs +0 -13
- package/dist/Composition-RGTRZ2JX.js +0 -4
- package/dist/chunk-6BQ6IEW2.cjs.map +0 -1
- package/dist/chunk-A574IA4F.js.map +0 -1
- package/dist/chunk-QGBOC5YL.cjs.map +0 -1
- package/dist/chunk-ZQX7WIYL.js.map +0 -1
package/dist/index.d.cts
CHANGED
|
@@ -1,10 +1,59 @@
|
|
|
1
|
-
export { C as ContactSheetOptions, F as FrameEvent, I as InspectOptions,
|
|
2
|
-
import { S as Sequence } from './Base-
|
|
3
|
-
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-
|
|
4
|
-
export { A as
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure } from './Movie-CuU_aaCm.cjs';
|
|
2
|
+
import { S as Sequence } from './Base-DnRAxroQ.cjs';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-7VtTEKUW.cjs';
|
|
4
|
+
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-7VtTEKUW.cjs';
|
|
5
5
|
import 'pixi.js';
|
|
6
6
|
import 'gsap';
|
|
7
7
|
|
|
8
|
+
/** A text style as written in a `text` layer's `style` (numbers, not expressions, for the fields that size the text). */
|
|
9
|
+
type MeasureStyle = Record<string, unknown>;
|
|
10
|
+
interface TextSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
/** Number of lines. */
|
|
14
|
+
lines: number;
|
|
15
|
+
}
|
|
16
|
+
interface SplitOptions {
|
|
17
|
+
/** What a piece is: one visible character (default), one word, or one line. */
|
|
18
|
+
by?: 'chars' | 'words' | 'lines';
|
|
19
|
+
/** Where the text goes: `x` is its left edge, or its centre / right edge with `align`. Default 0. */
|
|
20
|
+
x?: number;
|
|
21
|
+
/** Top of the first line. Default 0. */
|
|
22
|
+
y?: number;
|
|
23
|
+
/** How each line sits on `x`. Default `'left'`. */
|
|
24
|
+
align?: 'left' | 'center' | 'right';
|
|
25
|
+
}
|
|
26
|
+
interface TextPiece {
|
|
27
|
+
text: string;
|
|
28
|
+
/** Left edge of the piece, for a text layer with `anchorX: 0` (the default for text). */
|
|
29
|
+
x: number;
|
|
30
|
+
/** Top of the piece's line, for a text layer with `anchorY: 0`. */
|
|
31
|
+
y: number;
|
|
32
|
+
width: number;
|
|
33
|
+
/** Position in the returned list, 0-based. */
|
|
34
|
+
index: number;
|
|
35
|
+
/** Which line (0-based) the piece is on. */
|
|
36
|
+
line: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* How big `text` is drawn with `style`: the same numbers a text layer with that style has, measured with the font the
|
|
40
|
+
* renderer will use. A web font must be loaded first (`await document.fonts.load('24px "Name"')`), or the width is a
|
|
41
|
+
* fallback font's.
|
|
42
|
+
*/
|
|
43
|
+
declare function measureText(text: string, style?: MeasureStyle): TextSize;
|
|
44
|
+
/**
|
|
45
|
+
* Cut `text` into characters, words or lines and give each piece the x / y it needs so that one text layer per piece
|
|
46
|
+
* (same `style`, `anchorX: 0`, `anchorY: 0`) reads exactly like the whole text: per-letter and per-word animation without
|
|
47
|
+
* measuring anything by hand. Kerning is kept (a piece's x is the measured width of everything before it). Whitespace is
|
|
48
|
+
* not returned as a piece, but it takes its room. Lines are split on `\n` and go down by the style's line height.
|
|
49
|
+
*
|
|
50
|
+
* ```js
|
|
51
|
+
* splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' })
|
|
52
|
+
* .map(p => ({ type: 'text', text: p.text, style, initial: { x: p.x, y: p.y, alpha: 0 }, at: 0.2 * p.index, ... }))
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function splitText(text: string, style?: MeasureStyle, opts?: SplitOptions): TextPiece[];
|
|
56
|
+
|
|
8
57
|
type SequenceCtor = {
|
|
9
58
|
new (spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape): Sequence;
|
|
10
59
|
};
|
|
@@ -147,4 +196,4 @@ interface WithFadeOptions {
|
|
|
147
196
|
*/
|
|
148
197
|
declare function withFade<T extends SequenceCommon>(spec: T, opts: WithFadeOptions): T;
|
|
149
198
|
|
|
150
|
-
export { CameraSequenceSpec, ImageSequenceSpec, type KenBurnsOptions, type OrbitOptions, SequenceCommon, type SequenceCtor, SequenceSpec, type WithFadeOptions, kenBurns, orbit, registerSequenceType, withFade };
|
|
199
|
+
export { CameraSequenceSpec, ImageSequenceSpec, type KenBurnsOptions, type MeasureStyle, type OrbitOptions, SequenceCommon, type SequenceCtor, SequenceSpec, type SplitOptions, type TextPiece, type TextSize, type WithFadeOptions, kenBurns, measureText, orbit, registerSequenceType, splitText, withFade };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,59 @@
|
|
|
1
|
-
export { C as ContactSheetOptions, F as FrameEvent, I as InspectOptions,
|
|
2
|
-
import { S as Sequence } from './Base-
|
|
3
|
-
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-
|
|
4
|
-
export { A as
|
|
1
|
+
export { A as AudioInspectOptions, a as AudioReport, b as AudioSourceReport, C as ContactSheetOptions, F as FrameEvent, I as InspectOptions, c as InspectReport, L as LayerInfo, M as Movie, d as MovieOptions, P as ProgressEvent, R as RenderOptions, S as SnapshotOptions, e as SoundMeasure } from './Movie-VyDiUYOj.js';
|
|
2
|
+
import { S as Sequence } from './Base-CPSFBkPm.js';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-7VtTEKUW.js';
|
|
4
|
+
export { A as ArcShapeSpec, c as AssetSpec, d as AudioAssetSpec, e as AudioSequenceSpec, f as AudioSfxSpec, g as ChromaKeyFilterSpec, h as CircleShapeSpec, i as CompositionSequenceSpec, j as CompositionSpec, k as CrossfadeTransition, l as CustomFilterSpec, D as DipTransition, m as DissolveTransition, E as EllipseShapeSpec, n as Expr, F as FilterSpec, G as GradientSpec, o as IrisTransition, K as Keyframe, L as LineShapeSpec, P as PathShapeSpec, p as PolygonShapeSpec, q as PropValue, r as Props, R as RectShapeSpec, s as SfxKnobs, t as SfxOptions, u as SfxPreset, v as SfxVoice, w as ShapeSequenceSpec, x as SlideTransition, T as TextSequenceSpec, y as TransitionCommon, z as TransitionSpec, V as VideoSequenceSpec, W as WipeTransition, Z as ZoomTransition } from './types-7VtTEKUW.js';
|
|
5
5
|
import 'pixi.js';
|
|
6
6
|
import 'gsap';
|
|
7
7
|
|
|
8
|
+
/** A text style as written in a `text` layer's `style` (numbers, not expressions, for the fields that size the text). */
|
|
9
|
+
type MeasureStyle = Record<string, unknown>;
|
|
10
|
+
interface TextSize {
|
|
11
|
+
width: number;
|
|
12
|
+
height: number;
|
|
13
|
+
/** Number of lines. */
|
|
14
|
+
lines: number;
|
|
15
|
+
}
|
|
16
|
+
interface SplitOptions {
|
|
17
|
+
/** What a piece is: one visible character (default), one word, or one line. */
|
|
18
|
+
by?: 'chars' | 'words' | 'lines';
|
|
19
|
+
/** Where the text goes: `x` is its left edge, or its centre / right edge with `align`. Default 0. */
|
|
20
|
+
x?: number;
|
|
21
|
+
/** Top of the first line. Default 0. */
|
|
22
|
+
y?: number;
|
|
23
|
+
/** How each line sits on `x`. Default `'left'`. */
|
|
24
|
+
align?: 'left' | 'center' | 'right';
|
|
25
|
+
}
|
|
26
|
+
interface TextPiece {
|
|
27
|
+
text: string;
|
|
28
|
+
/** Left edge of the piece, for a text layer with `anchorX: 0` (the default for text). */
|
|
29
|
+
x: number;
|
|
30
|
+
/** Top of the piece's line, for a text layer with `anchorY: 0`. */
|
|
31
|
+
y: number;
|
|
32
|
+
width: number;
|
|
33
|
+
/** Position in the returned list, 0-based. */
|
|
34
|
+
index: number;
|
|
35
|
+
/** Which line (0-based) the piece is on. */
|
|
36
|
+
line: number;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* How big `text` is drawn with `style`: the same numbers a text layer with that style has, measured with the font the
|
|
40
|
+
* renderer will use. A web font must be loaded first (`await document.fonts.load('24px "Name"')`), or the width is a
|
|
41
|
+
* fallback font's.
|
|
42
|
+
*/
|
|
43
|
+
declare function measureText(text: string, style?: MeasureStyle): TextSize;
|
|
44
|
+
/**
|
|
45
|
+
* Cut `text` into characters, words or lines and give each piece the x / y it needs so that one text layer per piece
|
|
46
|
+
* (same `style`, `anchorX: 0`, `anchorY: 0`) reads exactly like the whole text: per-letter and per-word animation without
|
|
47
|
+
* measuring anything by hand. Kerning is kept (a piece's x is the measured width of everything before it). Whitespace is
|
|
48
|
+
* not returned as a piece, but it takes its room. Lines are split on `\n` and go down by the style's line height.
|
|
49
|
+
*
|
|
50
|
+
* ```js
|
|
51
|
+
* splitText('Hello world', style, { by: 'words', x: 640, y: 300, align: 'center' })
|
|
52
|
+
* .map(p => ({ type: 'text', text: p.text, style, initial: { x: p.x, y: p.y, alpha: 0 }, at: 0.2 * p.index, ... }))
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
declare function splitText(text: string, style?: MeasureStyle, opts?: SplitOptions): TextPiece[];
|
|
56
|
+
|
|
8
57
|
type SequenceCtor = {
|
|
9
58
|
new (spec: SequenceSpec, parent: CompositionShape | null, root: CompositionShape): Sequence;
|
|
10
59
|
};
|
|
@@ -147,4 +196,4 @@ interface WithFadeOptions {
|
|
|
147
196
|
*/
|
|
148
197
|
declare function withFade<T extends SequenceCommon>(spec: T, opts: WithFadeOptions): T;
|
|
149
198
|
|
|
150
|
-
export { CameraSequenceSpec, ImageSequenceSpec, type KenBurnsOptions, type OrbitOptions, SequenceCommon, type SequenceCtor, SequenceSpec, type WithFadeOptions, kenBurns, orbit, registerSequenceType, withFade };
|
|
199
|
+
export { CameraSequenceSpec, ImageSequenceSpec, type KenBurnsOptions, type MeasureStyle, type OrbitOptions, SequenceCommon, type SequenceCtor, SequenceSpec, type SplitOptions, type TextPiece, type TextSize, type WithFadeOptions, kenBurns, measureText, orbit, registerSequenceType, splitText, withFade };
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
1
|
+
import { warnUnknownOptions, suggestName } from './chunk-DKGG7WD4.js';
|
|
2
|
+
import { CompositionSequence } from './chunk-5Q5XZG7Z.js';
|
|
3
|
+
import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-UBPZLGLU.js';
|
|
4
|
+
export { registerSequenceType } from './chunk-UBPZLGLU.js';
|
|
4
5
|
import * as PIXI from 'pixi.js';
|
|
5
|
-
import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
6
|
+
import { extensions, CullerPlugin, Application, Container, Rectangle, Culler, CanvasTextMetrics, TextStyle, Assets, ExtensionType, Filter, UniformGroup, GpuProgram, GlProgram, defaultFilterVert } from 'pixi.js';
|
|
6
7
|
import { gsap } from 'gsap';
|
|
7
8
|
import { PixiPlugin } from 'gsap/PixiPlugin';
|
|
8
9
|
import { QUALITY_VERY_HIGH, QUALITY_HIGH, QUALITY_MEDIUM, QUALITY_LOW, QUALITY_VERY_LOW, Output, BufferTarget, CanvasSource, AudioBufferSource, Input, ALL_FORMATS, BlobSource, WavOutputFormat, Conversion, VideoSampleSink, MkvOutputFormat, WebMOutputFormat, MovOutputFormat, Mp4OutputFormat } from 'mediabunny';
|
|
@@ -68,21 +69,213 @@ async function loadAssetBundle(assets, audioContext) {
|
|
|
68
69
|
async function mixdown(audios, totalDuration, sampleRate = 44100) {
|
|
69
70
|
if (audios.length === 0) return null;
|
|
70
71
|
const ctx = new OfflineAudioContext(2, Math.ceil(sampleRate * totalDuration), sampleRate);
|
|
72
|
+
const synthesised = /* @__PURE__ */ new Map();
|
|
73
|
+
const bufferOf = (a) => {
|
|
74
|
+
if (a.buffer) return a.buffer;
|
|
75
|
+
if (!a.synth) return null;
|
|
76
|
+
let buf = synthesised.get(a.synth.key);
|
|
77
|
+
if (!buf) {
|
|
78
|
+
const [left, right] = a.synth.render(sampleRate);
|
|
79
|
+
buf = ctx.createBuffer(2, left.length, sampleRate);
|
|
80
|
+
buf.getChannelData(0).set(left);
|
|
81
|
+
buf.getChannelData(1).set(right);
|
|
82
|
+
synthesised.set(a.synth.key, buf);
|
|
83
|
+
}
|
|
84
|
+
return buf;
|
|
85
|
+
};
|
|
71
86
|
for (const a of audios) {
|
|
87
|
+
const buffer = bufferOf(a);
|
|
88
|
+
if (!buffer) continue;
|
|
89
|
+
const begin = Math.max(0, a.start);
|
|
90
|
+
const skipped = begin - a.start;
|
|
91
|
+
if (skipped > 0) {
|
|
92
|
+
console.warn(
|
|
93
|
+
`pixi-effects: ${a.layer ?? "an audio layer"} starts at ${a.start.toFixed(2)}s, before the movie starts, so its first ${skipped.toFixed(2)}s are cut off. Start it at 0 or later.`
|
|
94
|
+
);
|
|
95
|
+
}
|
|
72
96
|
const src = ctx.createBufferSource();
|
|
73
|
-
src.buffer =
|
|
97
|
+
src.buffer = buffer;
|
|
74
98
|
src.loop = !!a.loop;
|
|
75
99
|
const gain = ctx.createGain();
|
|
76
|
-
gain.gain.setValueAtTime(a.initialVolume ?? 1,
|
|
100
|
+
gain.gain.setValueAtTime(a.initialVolume ?? 1, begin);
|
|
77
101
|
for (const kf of a.volumeKeyframes ?? []) {
|
|
78
|
-
gain.gain.linearRampToValueAtTime(kf.value, kf.time);
|
|
102
|
+
gain.gain.linearRampToValueAtTime(kf.value, Math.max(0, kf.time));
|
|
79
103
|
}
|
|
80
104
|
src.connect(gain).connect(ctx.destination);
|
|
81
|
-
src.start(
|
|
82
|
-
src.stop(a.end);
|
|
105
|
+
src.start(begin, skipped);
|
|
106
|
+
src.stop(Math.max(begin, a.end));
|
|
83
107
|
}
|
|
84
108
|
return await ctx.startRendering();
|
|
85
109
|
}
|
|
110
|
+
var KNEE = 0.9;
|
|
111
|
+
function limitMix(buffer, audios) {
|
|
112
|
+
let peak = 0;
|
|
113
|
+
let peakIndex = 0;
|
|
114
|
+
for (let ch = 0; ch < buffer.numberOfChannels; ch++) {
|
|
115
|
+
const d = buffer.getChannelData(ch);
|
|
116
|
+
for (let i = 0; i < d.length; i++) {
|
|
117
|
+
const x = Math.abs(d[i]);
|
|
118
|
+
if (x > peak) {
|
|
119
|
+
peak = x;
|
|
120
|
+
peakIndex = i;
|
|
121
|
+
}
|
|
122
|
+
if (x > KNEE) d[i] = Math.sign(d[i]) * (KNEE + (1 - KNEE) * Math.tanh((x - KNEE) / (1 - KNEE)));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const peakAt = peakIndex / buffer.sampleRate;
|
|
126
|
+
if (peak > 1) {
|
|
127
|
+
const playing = audios.filter((a) => a.start <= peakAt && peakAt < a.end).map((a) => a.layer ?? "an audio layer");
|
|
128
|
+
console.warn(
|
|
129
|
+
`pixi-effects: the audio mix peaks at ${peak.toFixed(2)} (${(20 * Math.log10(peak)).toFixed(1)} dBFS) at ${peakAt.toFixed(2)}s, so it was limited (it would distort). Playing there: ${[...new Set(playing)].join(", ")}. Lower their volume.`
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
return { peak, peakAt };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// src/core/spectrum.ts
|
|
136
|
+
function fft(re, im) {
|
|
137
|
+
const n = re.length;
|
|
138
|
+
for (let i = 1, j = 0; i < n; i++) {
|
|
139
|
+
let bit = n >> 1;
|
|
140
|
+
for (; j & bit; bit >>= 1) j ^= bit;
|
|
141
|
+
j ^= bit;
|
|
142
|
+
if (i < j) {
|
|
143
|
+
[re[i], re[j]] = [re[j], re[i]];
|
|
144
|
+
[im[i], im[j]] = [im[j], im[i]];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
for (let len = 2; len <= n; len <<= 1) {
|
|
148
|
+
const ang = -2 * Math.PI / len;
|
|
149
|
+
for (let i = 0; i < n; i += len) {
|
|
150
|
+
for (let k = 0; k < len / 2; k++) {
|
|
151
|
+
const wr = Math.cos(ang * k), wi = Math.sin(ang * k);
|
|
152
|
+
const a = i + k, b = a + len / 2;
|
|
153
|
+
const xr = re[b] * wr - im[b] * wi, xi = re[b] * wi + im[b] * wr;
|
|
154
|
+
re[b] = re[a] - xr;
|
|
155
|
+
im[b] = im[a] - xi;
|
|
156
|
+
re[a] = re[a] + xr;
|
|
157
|
+
im[a] = im[a] + xi;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
var FRAME = 2048;
|
|
163
|
+
function spectralCentroid(samples, sampleRate, from = 0, to = samples.length) {
|
|
164
|
+
let num = 0;
|
|
165
|
+
let den = 0;
|
|
166
|
+
const re = new Float64Array(FRAME);
|
|
167
|
+
const im = new Float64Array(FRAME);
|
|
168
|
+
const end = Math.min(to, samples.length);
|
|
169
|
+
for (let start = Math.max(0, from); start < end; start += FRAME / 2) {
|
|
170
|
+
for (let i = 0; i < FRAME; i++) {
|
|
171
|
+
const k = start + i;
|
|
172
|
+
const w = 0.5 - 0.5 * Math.cos(2 * Math.PI * i / (FRAME - 1));
|
|
173
|
+
re[i] = k < end ? samples[k] * w : 0;
|
|
174
|
+
im[i] = 0;
|
|
175
|
+
}
|
|
176
|
+
fft(re, im);
|
|
177
|
+
for (let b = 1; b < FRAME / 2; b++) {
|
|
178
|
+
const power = re[b] * re[b] + im[b] * im[b];
|
|
179
|
+
num += power * (b * sampleRate / FRAME);
|
|
180
|
+
den += power;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return den > 1e-9 ? Math.round(num / den / 10) * 10 : 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// src/core/inspectAudio.ts
|
|
187
|
+
var INAUDIBLE_DB = -50;
|
|
188
|
+
var db = (x) => Math.round(10 * Math.max(-120, 20 * Math.log10(x))) / 10;
|
|
189
|
+
var mono = (chans) => {
|
|
190
|
+
if (chans.length === 1) return chans[0];
|
|
191
|
+
const out = new Float32Array(chans[0].length);
|
|
192
|
+
for (const d of chans) for (let i = 0; i < out.length; i++) out[i] = out[i] + d[i] / chans.length;
|
|
193
|
+
return out;
|
|
194
|
+
};
|
|
195
|
+
function measure(chans, sampleRate) {
|
|
196
|
+
let peak = 0;
|
|
197
|
+
let at = 0;
|
|
198
|
+
for (const d of chans) for (let i = 0; i < d.length; i++) if (Math.abs(d[i]) > peak) {
|
|
199
|
+
peak = Math.abs(d[i]);
|
|
200
|
+
at = i;
|
|
201
|
+
}
|
|
202
|
+
return {
|
|
203
|
+
length: Math.round(chans[0].length / sampleRate * 1e3) / 1e3,
|
|
204
|
+
peakDb: db(peak),
|
|
205
|
+
loudestAt: Math.round(at / sampleRate * 1e3) / 1e3,
|
|
206
|
+
brightnessHz: spectralCentroid(mono(chans), sampleRate)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function analyzeAudio(mix, sources, stats, movieDuration, opts = {}) {
|
|
210
|
+
if (!mix) {
|
|
211
|
+
return { duration: movieDuration, sampleRate: 0, peakDb: -120, peakAt: 0, sources: [], windows: [], issues: ['no audio: the movie has no audio layers (sound effects need no files: { type: "audio", sfx: "pop", at: 1 })'] };
|
|
212
|
+
}
|
|
213
|
+
const sr = mix.sampleRate;
|
|
214
|
+
const chans = Array.from({ length: mix.numberOfChannels }, (_, c) => mix.getChannelData(c));
|
|
215
|
+
const mixMono = mono(chans);
|
|
216
|
+
const peakIn = (t0, t1) => {
|
|
217
|
+
let p = 0;
|
|
218
|
+
const i1 = Math.min(mix.length, Math.ceil(t1 * sr));
|
|
219
|
+
for (const d of chans) for (let i = Math.max(0, Math.floor(t0 * sr)); i < i1; i++) p = Math.max(p, Math.abs(d[i]));
|
|
220
|
+
return p;
|
|
221
|
+
};
|
|
222
|
+
const win = opts.window && opts.window > 0 ? Math.max(0.01, opts.window) : 0.1;
|
|
223
|
+
const windows2 = [];
|
|
224
|
+
for (let t = 0; t < movieDuration - 1e-9; t += win) {
|
|
225
|
+
const i0 = Math.floor(t * sr);
|
|
226
|
+
const i1 = Math.min(mix.length, Math.floor((t + win) * sr));
|
|
227
|
+
let sum = 0;
|
|
228
|
+
let peak = 0;
|
|
229
|
+
for (const d of chans) {
|
|
230
|
+
for (let i = i0; i < i1; i++) {
|
|
231
|
+
sum += d[i] * d[i];
|
|
232
|
+
peak = Math.max(peak, Math.abs(d[i]));
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const n = Math.max(1, (i1 - i0) * chans.length);
|
|
236
|
+
windows2.push({ t: Math.round(t * 1e3) / 1e3, rmsDb: db(Math.sqrt(sum / n)), peakDb: db(peak), brightnessHz: spectralCentroid(mixMono, sr, i0, i1) });
|
|
237
|
+
}
|
|
238
|
+
const issues = [];
|
|
239
|
+
if (stats && stats.peak > 1) {
|
|
240
|
+
issues.push(`the mix peaks at ${db(stats.peak)} dBFS at ${stats.peakAt.toFixed(2)}s and was limited \u2014 lower the volume of the layers playing there`);
|
|
241
|
+
}
|
|
242
|
+
const rendered = /* @__PURE__ */ new Map();
|
|
243
|
+
const soundOf = (s) => {
|
|
244
|
+
if (s.synth) {
|
|
245
|
+
let m = rendered.get(s.synth.key);
|
|
246
|
+
if (!m) rendered.set(s.synth.key, m = measure(s.synth.render(sr), sr));
|
|
247
|
+
return m;
|
|
248
|
+
}
|
|
249
|
+
const b = s.buffer;
|
|
250
|
+
return measure(Array.from({ length: b.numberOfChannels }, (_, c) => b.getChannelData(c)), b.sampleRate);
|
|
251
|
+
};
|
|
252
|
+
const list = [...sources].sort((a, b) => a.start - b.start).map((s) => {
|
|
253
|
+
const layer = s.layer ?? "an audio layer";
|
|
254
|
+
const source = s.source ?? "audio";
|
|
255
|
+
const peakDb = db(peakIn(s.start, Math.min(s.end, movieDuration)));
|
|
256
|
+
const sound = soundOf(s);
|
|
257
|
+
const topVolume = Math.max(s.initialVolume ?? 1, ...(s.volumeKeyframes ?? []).map((k) => k.value));
|
|
258
|
+
const ownDb = db(Math.pow(10, sound.peakDb / 20) * Math.max(0, topVolume));
|
|
259
|
+
if (s.start >= movieDuration) {
|
|
260
|
+
issues.push(`${layer} (${source}) starts at ${s.start.toFixed(2)}s, after the movie ends (${movieDuration}s)`);
|
|
261
|
+
} else if (ownDb < INAUDIBLE_DB || peakDb < INAUDIBLE_DB) {
|
|
262
|
+
issues.push(`${layer} (${source}) is inaudible from ${s.start.toFixed(2)}s to ${s.end.toFixed(2)}s (its own peak ${ownDb} dBFS at volume ${topVolume}; mix peak there ${peakDb} dBFS) \u2014 check its volume`);
|
|
263
|
+
}
|
|
264
|
+
if (s.synth && s.end > movieDuration + 1e-6 && s.start < movieDuration) {
|
|
265
|
+
issues.push(`${layer} (${source}) is cut off by the end of the movie at ${movieDuration}s (it runs to ${s.end.toFixed(2)}s)`);
|
|
266
|
+
}
|
|
267
|
+
return { layer, source, start: s.start, end: s.end, peakDb, sound };
|
|
268
|
+
});
|
|
269
|
+
return {
|
|
270
|
+
duration: movieDuration,
|
|
271
|
+
sampleRate: sr,
|
|
272
|
+
peakDb: db(stats?.peak ?? peakIn(0, movieDuration)),
|
|
273
|
+
peakAt: stats?.peakAt ?? 0,
|
|
274
|
+
sources: list,
|
|
275
|
+
windows: windows2,
|
|
276
|
+
issues
|
|
277
|
+
};
|
|
278
|
+
}
|
|
86
279
|
var VIDEO_CODEC_BY_FORMAT = {
|
|
87
280
|
mp4: "avc",
|
|
88
281
|
mov: "avc",
|
|
@@ -140,7 +333,7 @@ async function exportFrames(movie, options = {}) {
|
|
|
140
333
|
const audioSource = new AudioBufferSource({
|
|
141
334
|
codec: opts.audio.codec,
|
|
142
335
|
bitrate: opts.audio.bitrate
|
|
143
|
-
});
|
|
336
|
+
}, { startTimestamp: opts.audio.codec === "aac" ? -2112 / movie.audioBuffer.sampleRate : 0 });
|
|
144
337
|
output.addAudioTrack(audioSource);
|
|
145
338
|
await output.start();
|
|
146
339
|
await audioSource.add(movie.audioBuffer);
|
|
@@ -926,7 +1119,8 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
926
1119
|
const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
|
|
927
1120
|
const seen = /* @__PURE__ */ new Map();
|
|
928
1121
|
const scene = /* @__PURE__ */ new Map();
|
|
929
|
-
const
|
|
1122
|
+
const scaledToNothing = /* @__PURE__ */ new Set();
|
|
1123
|
+
const walk = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
|
|
930
1124
|
const blending = /* @__PURE__ */ new Set();
|
|
931
1125
|
for (const w of transitionWindowsOf(comp.spec)) {
|
|
932
1126
|
const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
|
|
@@ -955,7 +1149,9 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
955
1149
|
if (area === 0) onCanvas = "none";
|
|
956
1150
|
}
|
|
957
1151
|
const keys = collectPropKeys(seq.spec);
|
|
958
|
-
const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
1152
|
+
const moving = parentMoving || (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
1153
|
+
const scale = t?.scale;
|
|
1154
|
+
const zero = parentZero || !!scale && (scale.x === 0 || scale.y === 0);
|
|
959
1155
|
const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
|
|
960
1156
|
layers.push(info);
|
|
961
1157
|
let shown = bounds ? clip(bounds, view) : null;
|
|
@@ -967,7 +1163,8 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
967
1163
|
seen.set(info, shown);
|
|
968
1164
|
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
969
1165
|
if (group) scene.set(info, group);
|
|
970
|
-
if (
|
|
1166
|
+
if (zero) scaledToNothing.add(info);
|
|
1167
|
+
if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
|
|
971
1168
|
});
|
|
972
1169
|
};
|
|
973
1170
|
walk(root, "", false, true);
|
|
@@ -977,6 +1174,7 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
977
1174
|
const b = l.bounds;
|
|
978
1175
|
const who = `text layer "${l.path}"`;
|
|
979
1176
|
if (b.width < 1 || b.height < 1) {
|
|
1177
|
+
if (scaledToNothing.has(l)) continue;
|
|
980
1178
|
issues.push(`${who} has no size (empty text, or not drawn yet)`);
|
|
981
1179
|
} else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
|
|
982
1180
|
issues.push(`${who} is entirely outside the canvas (at ${Math.round(b.x)},${Math.round(b.y)}, ${Math.round(b.width)}\xD7${Math.round(b.height)})`);
|
|
@@ -1026,64 +1224,6 @@ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
|
|
|
1026
1224
|
return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
|
|
1027
1225
|
}
|
|
1028
1226
|
|
|
1029
|
-
// src/core/options.ts
|
|
1030
|
-
function distance(a, b) {
|
|
1031
|
-
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
1032
|
-
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
1033
|
-
for (let i = 1; i <= a.length; i++) {
|
|
1034
|
-
for (let j = 1; j <= b.length; j++) {
|
|
1035
|
-
dp[i][j] = Math.min(dp[i - 1][j] + 1, dp[i][j - 1] + 1, dp[i - 1][j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
1036
|
-
if (i > 1 && j > 1 && a[i - 1] === b[j - 2] && a[i - 2] === b[j - 1]) dp[i][j] = Math.min(dp[i][j], dp[i - 2][j - 2] + 1);
|
|
1037
|
-
}
|
|
1038
|
-
}
|
|
1039
|
-
return dp[a.length][b.length];
|
|
1040
|
-
}
|
|
1041
|
-
var isSubsequence = (small, big) => {
|
|
1042
|
-
let i = 0;
|
|
1043
|
-
for (const ch of big) if (i < small.length && ch === small[i]) i++;
|
|
1044
|
-
return i === small.length;
|
|
1045
|
-
};
|
|
1046
|
-
var COMMON_ALIASES = {
|
|
1047
|
-
fps: "frameRate",
|
|
1048
|
-
framerate: "frameRate",
|
|
1049
|
-
frame_rate: "frameRate",
|
|
1050
|
-
bg: "background",
|
|
1051
|
-
backgroundcolor: "background",
|
|
1052
|
-
bgcolor: "background",
|
|
1053
|
-
seconds: "duration",
|
|
1054
|
-
length: "duration",
|
|
1055
|
-
time: "duration",
|
|
1056
|
-
w: "width",
|
|
1057
|
-
h: "height",
|
|
1058
|
-
cols: "columns",
|
|
1059
|
-
sweep: "degrees",
|
|
1060
|
-
angle: "degrees"
|
|
1061
|
-
};
|
|
1062
|
-
function suggestName(name, valid) {
|
|
1063
|
-
const n = name.toLowerCase();
|
|
1064
|
-
const alias = COMMON_ALIASES[n];
|
|
1065
|
-
if (alias && valid.includes(alias)) return alias;
|
|
1066
|
-
let best = null;
|
|
1067
|
-
for (const v of valid) {
|
|
1068
|
-
const lv = v.toLowerCase();
|
|
1069
|
-
const d = distance(n, lv);
|
|
1070
|
-
const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
|
|
1071
|
-
const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
|
|
1072
|
-
if (!close && !abbreviation) continue;
|
|
1073
|
-
const score = close ? d : 3 + (lv.length - n.length) / 100;
|
|
1074
|
-
if (!best || score < best.score) best = { v, score };
|
|
1075
|
-
}
|
|
1076
|
-
return best ? best.v : null;
|
|
1077
|
-
}
|
|
1078
|
-
function warnUnknownOptions(where, opts, valid) {
|
|
1079
|
-
if (!opts || typeof opts !== "object") return;
|
|
1080
|
-
for (const key of Object.keys(opts)) {
|
|
1081
|
-
if (valid.includes(key)) continue;
|
|
1082
|
-
const guess = suggestName(key, valid);
|
|
1083
|
-
console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1086
|
-
|
|
1087
1227
|
// src/core/Movie.ts
|
|
1088
1228
|
extensions.add(CullerPlugin);
|
|
1089
1229
|
gsap.registerPlugin(PixiPlugin);
|
|
@@ -1110,6 +1250,9 @@ var Movie = class {
|
|
|
1110
1250
|
_rootSequence = null;
|
|
1111
1251
|
_rootContainer = null;
|
|
1112
1252
|
_raf = null;
|
|
1253
|
+
/** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
|
|
1254
|
+
_audioSources = [];
|
|
1255
|
+
_mixStats = null;
|
|
1113
1256
|
on(event, fn) {
|
|
1114
1257
|
(this._events[event] ??= []).push(fn);
|
|
1115
1258
|
if (event === "ready" && this._initState === "ready") {
|
|
@@ -1204,7 +1347,9 @@ var Movie = class {
|
|
|
1204
1347
|
composition.collectAudio(audios, 0);
|
|
1205
1348
|
if (audios.length > 0) {
|
|
1206
1349
|
this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
|
|
1350
|
+
if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
|
|
1207
1351
|
}
|
|
1352
|
+
this._audioSources = audios;
|
|
1208
1353
|
this.timeline.progress(1).progress(0);
|
|
1209
1354
|
await this._awaitVideoFrames();
|
|
1210
1355
|
this._updateSpace();
|
|
@@ -1292,6 +1437,15 @@ var Movie = class {
|
|
|
1292
1437
|
await this.gotoFrame(f, true);
|
|
1293
1438
|
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1294
1439
|
}
|
|
1440
|
+
/**
|
|
1441
|
+
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
1442
|
+
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
1443
|
+
*/
|
|
1444
|
+
inspectAudio(opts = {}) {
|
|
1445
|
+
warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
|
|
1446
|
+
this._requireReady("inspectAudio");
|
|
1447
|
+
return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, opts);
|
|
1448
|
+
}
|
|
1295
1449
|
/**
|
|
1296
1450
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
1297
1451
|
* by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
|
|
@@ -1408,6 +1562,8 @@ var Movie = class {
|
|
|
1408
1562
|
safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
|
|
1409
1563
|
this.app = null;
|
|
1410
1564
|
this.audioBuffer = null;
|
|
1565
|
+
this._audioSources = [];
|
|
1566
|
+
this._mixStats = null;
|
|
1411
1567
|
safeRun(() => this._audioContext?.close().catch(() => {
|
|
1412
1568
|
}));
|
|
1413
1569
|
this._audioContext = null;
|
|
@@ -1435,6 +1591,53 @@ function collectVideoSequences(seq, out) {
|
|
|
1435
1591
|
collectVideoSequences(child, out);
|
|
1436
1592
|
}
|
|
1437
1593
|
}
|
|
1594
|
+
var BY = ["chars", "words", "lines"];
|
|
1595
|
+
var ALIGN = ["left", "center", "right"];
|
|
1596
|
+
var widthOf = (text, style) => CanvasTextMetrics.measureText(text, style).width;
|
|
1597
|
+
function measureText(text, style = {}) {
|
|
1598
|
+
const m = CanvasTextMetrics.measureText(text, new TextStyle(style));
|
|
1599
|
+
return { width: m.width, height: m.height, lines: text.split("\n").length };
|
|
1600
|
+
}
|
|
1601
|
+
function splitText(text, style = {}, opts = {}) {
|
|
1602
|
+
warnUnknownOptions("splitText()", opts, ["by", "x", "y", "align"]);
|
|
1603
|
+
let by = opts.by ?? "chars";
|
|
1604
|
+
if (!BY.includes(by)) {
|
|
1605
|
+
const g = suggestName(String(by), BY);
|
|
1606
|
+
console.warn(`pixi-effects: splitText(): by "${by}" is not one of ${BY.map((b) => `"${b}"`).join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "chars"`);
|
|
1607
|
+
by = "chars";
|
|
1608
|
+
}
|
|
1609
|
+
let align = opts.align ?? "left";
|
|
1610
|
+
if (!ALIGN.includes(align)) {
|
|
1611
|
+
const g = suggestName(String(align), ALIGN);
|
|
1612
|
+
console.warn(`pixi-effects: splitText(): align "${align}" is not one of ${ALIGN.join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "left"`);
|
|
1613
|
+
align = "left";
|
|
1614
|
+
}
|
|
1615
|
+
const x0 = opts.x ?? 0, y0 = opts.y ?? 0;
|
|
1616
|
+
if (text === "") return [];
|
|
1617
|
+
const ts = new TextStyle(style);
|
|
1618
|
+
const lineHeight = CanvasTextMetrics.measureText("M", ts).lineHeight ?? CanvasTextMetrics.measureText("M", ts).height;
|
|
1619
|
+
const sentinel = widthOf("|", ts);
|
|
1620
|
+
const out = [];
|
|
1621
|
+
text.split("\n").forEach((line, lineNo) => {
|
|
1622
|
+
const lineWidth = widthOf(line, ts);
|
|
1623
|
+
const left = align === "center" ? x0 - lineWidth / 2 : align === "right" ? x0 - lineWidth : x0;
|
|
1624
|
+
const y = y0 + lineNo * lineHeight;
|
|
1625
|
+
const advance = (upTo) => upTo === 0 ? 0 : widthOf(line.slice(0, upTo) + "|", ts) - sentinel;
|
|
1626
|
+
const spans = [];
|
|
1627
|
+
if (by === "lines") {
|
|
1628
|
+
if (line.trim() !== "") spans.push([0, line.length]);
|
|
1629
|
+
} else if (by === "words") {
|
|
1630
|
+
for (const m of line.matchAll(/\S+/g)) spans.push([m.index, m.index + m[0].length]);
|
|
1631
|
+
} else {
|
|
1632
|
+
for (let i = 0; i < line.length; i++) if (!/\s/.test(line[i])) spans.push([i, i + 1]);
|
|
1633
|
+
}
|
|
1634
|
+
for (const [a, b] of spans) {
|
|
1635
|
+
const piece = line.slice(a, b);
|
|
1636
|
+
out.push({ text: piece, x: left + advance(a), y, width: widthOf(piece, ts), index: out.length, line: lineNo });
|
|
1637
|
+
}
|
|
1638
|
+
});
|
|
1639
|
+
return out;
|
|
1640
|
+
}
|
|
1438
1641
|
|
|
1439
1642
|
// src/presets/kenBurns.ts
|
|
1440
1643
|
var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
|
|
@@ -1628,6 +1831,6 @@ function withFade(spec, opts) {
|
|
|
1628
1831
|
return spec;
|
|
1629
1832
|
}
|
|
1630
1833
|
|
|
1631
|
-
export { Movie, kenBurns, orbit, withFade };
|
|
1834
|
+
export { Movie, kenBurns, measureText, orbit, splitText, withFade };
|
|
1632
1835
|
//# sourceMappingURL=index.js.map
|
|
1633
1836
|
//# sourceMappingURL=index.js.map
|