pixi-effects 0.3.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 +53 -0
- package/README.md +10 -6
- package/ai/SKILL.md +9 -4
- package/ai/reference/cheatsheet.md +19 -7
- package/ai/reference/pitfalls.md +32 -7
- package/ai/reference/recipes.md +117 -0
- package/ai/template.html +6 -3
- package/ai/tools/check.mjs +385 -0
- package/dist/{Base-BDw6dKRB.d.cts → Base-CPSFBkPm.d.ts} +12 -1
- package/dist/{Base-Ba4Ta4ap.d.ts → Base-DnRAxroQ.d.cts} +12 -1
- package/dist/Composition-63EXHNJJ.cjs +13 -0
- package/dist/{Composition-6FDH5OPM.cjs.map → Composition-63EXHNJJ.cjs.map} +1 -1
- package/dist/Composition-TUCRSMJU.js +4 -0
- package/dist/{Composition-FEYAUMFI.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-W8ISiEBB.d.cts → Movie-CuU_aaCm.d.cts} +56 -2
- package/dist/{Movie-DMpaT67V.d.ts → Movie-VyDiUYOj.d.ts} +56 -2
- package/dist/{chunk-ZL262ZRM.cjs → chunk-23M5QTWW.cjs} +70 -20
- package/dist/chunk-23M5QTWW.cjs.map +1 -0
- package/dist/{chunk-SIRVULFF.cjs → chunk-2RR43OIN.cjs} +256 -60
- package/dist/chunk-2RR43OIN.cjs.map +1 -0
- package/dist/{chunk-DIJG2RSF.js → chunk-5Q5XZG7Z.js} +59 -9
- 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-PN5A6QA7.js → chunk-UBPZLGLU.js} +255 -61
- 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 +336 -89
- 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 +320 -75
- 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-3c8Vymgw.d.ts → types-7VtTEKUW.d.cts} +102 -6
- package/dist/{types-3c8Vymgw.d.cts → types-7VtTEKUW.d.ts} +102 -6
- package/docs/api.md +36 -0
- package/docs/dsl.md +56 -4
- package/llms-full.txt +268 -21
- package/llms.txt +1 -1
- package/package.json +3 -1
- package/dist/Composition-6FDH5OPM.cjs +0 -13
- package/dist/Composition-FEYAUMFI.js +0 -4
- package/dist/chunk-DIJG2RSF.js.map +0 -1
- package/dist/chunk-PN5A6QA7.js.map +0 -1
- package/dist/chunk-SIRVULFF.cjs.map +0 -1
- package/dist/chunk-ZL262ZRM.cjs.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);
|
|
@@ -501,6 +694,15 @@ var TransitionMaskFilter = class extends Filter {
|
|
|
501
694
|
};
|
|
502
695
|
|
|
503
696
|
// src/core/Transitions.ts
|
|
697
|
+
var windows = /* @__PURE__ */ new WeakMap();
|
|
698
|
+
function transitionWindowsOf(spec) {
|
|
699
|
+
return windows.get(spec) ?? [];
|
|
700
|
+
}
|
|
701
|
+
function carryTransitionWindows(from, to) {
|
|
702
|
+
const w = windows.get(from);
|
|
703
|
+
if (w) windows.set(to, w);
|
|
704
|
+
return to;
|
|
705
|
+
}
|
|
504
706
|
function expandTransitions(spec) {
|
|
505
707
|
const out = {
|
|
506
708
|
...spec,
|
|
@@ -611,6 +813,9 @@ function expandTransitions(spec) {
|
|
|
611
813
|
if (tStart < toStart || tEnd > toEnd) {
|
|
612
814
|
throw new Error(`pixi-effects: ${tag} window [${tStart}, ${tEnd}] is not covered by \`to\` "${t.to}" (lives [${toStart}, ${toEnd}], so it starts at ${toStart})`);
|
|
613
815
|
}
|
|
816
|
+
const record = windows.get(out) ?? [];
|
|
817
|
+
record.push({ from: t.from, to: t.to, start: tStart, end: tEnd });
|
|
818
|
+
windows.set(out, record);
|
|
614
819
|
const abs = { ...t, at: tStart };
|
|
615
820
|
const nFrom = keyframeCount(fromEntry.seq);
|
|
616
821
|
const nTo = keyframeCount(toEntry.seq);
|
|
@@ -904,10 +1109,26 @@ var intersection = (a, b) => {
|
|
|
904
1109
|
const h = Math.min(a.y + a.height, b.y + b.height) - Math.max(a.y, b.y);
|
|
905
1110
|
return w > 0 && h > 0 ? w * h : 0;
|
|
906
1111
|
};
|
|
1112
|
+
var clip = (a, b) => {
|
|
1113
|
+
const x = Math.max(a.x, b.x), y = Math.max(a.y, b.y);
|
|
1114
|
+
const w = Math.min(a.x + a.width, b.x + b.width) - x, h = Math.min(a.y + a.height, b.y + b.height) - y;
|
|
1115
|
+
return w > 0 && h > 0 ? { x, y, width: w, height: h } : null;
|
|
1116
|
+
};
|
|
907
1117
|
function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
908
1118
|
const layers = [];
|
|
909
1119
|
const view = { x: 0, y: 0, width: canvas.width, height: canvas.height };
|
|
910
|
-
const
|
|
1120
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1121
|
+
const scene = /* @__PURE__ */ new Map();
|
|
1122
|
+
const scaledToNothing = /* @__PURE__ */ new Set();
|
|
1123
|
+
const walk = (comp, prefix, insideThreeD, parentVisible, parentScene, parentMoving = false, parentZero = false) => {
|
|
1124
|
+
const blending = /* @__PURE__ */ new Set();
|
|
1125
|
+
for (const w of transitionWindowsOf(comp.spec)) {
|
|
1126
|
+
const start = (comp.absoluteStart ?? 0) + w.start, end = (comp.absoluteStart ?? 0) + w.end;
|
|
1127
|
+
if (time >= start - 1e-9 && time <= end + 1e-9) {
|
|
1128
|
+
blending.add(w.from);
|
|
1129
|
+
blending.add(w.to);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
911
1132
|
comp.layers().forEach(({ seq, display, threeD }, i) => {
|
|
912
1133
|
const t = seq.target;
|
|
913
1134
|
const name = seq.spec.name;
|
|
@@ -928,9 +1149,22 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
928
1149
|
if (area === 0) onCanvas = "none";
|
|
929
1150
|
}
|
|
930
1151
|
const keys = collectPropKeys(seq.spec);
|
|
931
|
-
const moving = (seq.spec.keyframes ?? []).length > 0 && (keys.has("x") || keys.has("y"));
|
|
932
|
-
|
|
933
|
-
|
|
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);
|
|
1155
|
+
const info = { path: prefix + label, name, type: seq.spec.type, threeD, visible, alpha, moving, bounds, onCanvas };
|
|
1156
|
+
layers.push(info);
|
|
1157
|
+
let shown = bounds ? clip(bounds, view) : null;
|
|
1158
|
+
const maskTarget = seq.maskSequence?.target;
|
|
1159
|
+
if (shown && maskTarget && !seq.spec.maskInverted) {
|
|
1160
|
+
const m = maskTarget.getBounds();
|
|
1161
|
+
shown = clip(shown, { x: m.x, y: m.y, width: m.width, height: m.height });
|
|
1162
|
+
}
|
|
1163
|
+
seen.set(info, shown);
|
|
1164
|
+
const group = parentScene ?? (name !== void 0 && blending.has(name) ? name : void 0);
|
|
1165
|
+
if (group) scene.set(info, group);
|
|
1166
|
+
if (zero) scaledToNothing.add(info);
|
|
1167
|
+
if (seq instanceof CompositionSequence) walk(seq, prefix + label + "/", insideThreeD || threeD, visible, group, moving, zero);
|
|
934
1168
|
});
|
|
935
1169
|
};
|
|
936
1170
|
walk(root, "", false, true);
|
|
@@ -940,8 +1174,9 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
940
1174
|
const b = l.bounds;
|
|
941
1175
|
const who = `text layer "${l.path}"`;
|
|
942
1176
|
if (b.width < 1 || b.height < 1) {
|
|
1177
|
+
if (scaledToNothing.has(l)) continue;
|
|
943
1178
|
issues.push(`${who} has no size (empty text, or not drawn yet)`);
|
|
944
|
-
} else if (l.moving) ; else if (l.onCanvas === "none") {
|
|
1179
|
+
} else if (l.moving || scene.has(l)) ; else if (l.onCanvas === "none") {
|
|
945
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)})`);
|
|
946
1181
|
} else if (l.onCanvas === "partial") {
|
|
947
1182
|
const over = [];
|
|
@@ -954,7 +1189,10 @@ function inspectScene(root, frame, time, canvas, opts = {}) {
|
|
|
954
1189
|
}
|
|
955
1190
|
for (let i = 0; i < texts.length; i++) {
|
|
956
1191
|
for (let j = i + 1; j < texts.length; j++) {
|
|
957
|
-
const a = texts[i]
|
|
1192
|
+
const a = seen.get(texts[i]), b = seen.get(texts[j]);
|
|
1193
|
+
if (!a || !b) continue;
|
|
1194
|
+
const sa = scene.get(texts[i]), sb = scene.get(texts[j]);
|
|
1195
|
+
if (sa && sb && sa !== sb) continue;
|
|
958
1196
|
const smaller = Math.min(a.width * a.height, b.width * b.height);
|
|
959
1197
|
if (smaller <= 0) continue;
|
|
960
1198
|
const share = intersection(a, b) / smaller;
|
|
@@ -986,64 +1224,6 @@ function sheetLayout(n, columns, cellW, srcW, srcH, labelH) {
|
|
|
986
1224
|
return { cols, rows, cellW, cellH, width: cols * cellW, height: rows * (cellH + labelH), positions };
|
|
987
1225
|
}
|
|
988
1226
|
|
|
989
|
-
// src/core/options.ts
|
|
990
|
-
function distance(a, b) {
|
|
991
|
-
const dp = Array.from({ length: a.length + 1 }, (_, i) => [i, ...Array(b.length).fill(0)]);
|
|
992
|
-
for (let j = 1; j <= b.length; j++) dp[0][j] = j;
|
|
993
|
-
for (let i = 1; i <= a.length; i++) {
|
|
994
|
-
for (let j = 1; j <= b.length; j++) {
|
|
995
|
-
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));
|
|
996
|
-
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);
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
return dp[a.length][b.length];
|
|
1000
|
-
}
|
|
1001
|
-
var isSubsequence = (small, big) => {
|
|
1002
|
-
let i = 0;
|
|
1003
|
-
for (const ch of big) if (i < small.length && ch === small[i]) i++;
|
|
1004
|
-
return i === small.length;
|
|
1005
|
-
};
|
|
1006
|
-
var COMMON_ALIASES = {
|
|
1007
|
-
fps: "frameRate",
|
|
1008
|
-
framerate: "frameRate",
|
|
1009
|
-
frame_rate: "frameRate",
|
|
1010
|
-
bg: "background",
|
|
1011
|
-
backgroundcolor: "background",
|
|
1012
|
-
bgcolor: "background",
|
|
1013
|
-
seconds: "duration",
|
|
1014
|
-
length: "duration",
|
|
1015
|
-
time: "duration",
|
|
1016
|
-
w: "width",
|
|
1017
|
-
h: "height",
|
|
1018
|
-
cols: "columns",
|
|
1019
|
-
sweep: "degrees",
|
|
1020
|
-
angle: "degrees"
|
|
1021
|
-
};
|
|
1022
|
-
function suggestName(name, valid) {
|
|
1023
|
-
const n = name.toLowerCase();
|
|
1024
|
-
const alias = COMMON_ALIASES[n];
|
|
1025
|
-
if (alias && valid.includes(alias)) return alias;
|
|
1026
|
-
let best = null;
|
|
1027
|
-
for (const v of valid) {
|
|
1028
|
-
const lv = v.toLowerCase();
|
|
1029
|
-
const d = distance(n, lv);
|
|
1030
|
-
const close = d <= Math.max(1, Math.floor(Math.min(n.length, lv.length) / 3));
|
|
1031
|
-
const abbreviation = n.length >= 3 && isSubsequence(n, lv) || lv.length >= 2 && n.length > lv.length && n.includes(lv);
|
|
1032
|
-
if (!close && !abbreviation) continue;
|
|
1033
|
-
const score = close ? d : 3 + (lv.length - n.length) / 100;
|
|
1034
|
-
if (!best || score < best.score) best = { v, score };
|
|
1035
|
-
}
|
|
1036
|
-
return best ? best.v : null;
|
|
1037
|
-
}
|
|
1038
|
-
function warnUnknownOptions(where, opts, valid) {
|
|
1039
|
-
if (!opts || typeof opts !== "object") return;
|
|
1040
|
-
for (const key of Object.keys(opts)) {
|
|
1041
|
-
if (valid.includes(key)) continue;
|
|
1042
|
-
const guess = suggestName(key, valid);
|
|
1043
|
-
console.warn(`pixi-effects: ${where}: unknown option "${key}"${guess ? ` \u2014 did you mean "${guess}"?` : ""} (valid: ${valid.join(", ")})`);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1046
|
-
|
|
1047
1227
|
// src/core/Movie.ts
|
|
1048
1228
|
extensions.add(CullerPlugin);
|
|
1049
1229
|
gsap.registerPlugin(PixiPlugin);
|
|
@@ -1070,6 +1250,9 @@ var Movie = class {
|
|
|
1070
1250
|
_rootSequence = null;
|
|
1071
1251
|
_rootContainer = null;
|
|
1072
1252
|
_raf = null;
|
|
1253
|
+
/** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
|
|
1254
|
+
_audioSources = [];
|
|
1255
|
+
_mixStats = null;
|
|
1073
1256
|
on(event, fn) {
|
|
1074
1257
|
(this._events[event] ??= []).push(fn);
|
|
1075
1258
|
if (event === "ready" && this._initState === "ready") {
|
|
@@ -1154,6 +1337,7 @@ var Movie = class {
|
|
|
1154
1337
|
duration: this.duration,
|
|
1155
1338
|
...userComposition
|
|
1156
1339
|
};
|
|
1340
|
+
if (userComposition) carryTransitionWindows(userComposition, rootSeqSpec);
|
|
1157
1341
|
const composition = new CompositionSequence(rootSeqSpec, null, rootShape);
|
|
1158
1342
|
await composition.build();
|
|
1159
1343
|
this._rootSequence = composition;
|
|
@@ -1163,7 +1347,9 @@ var Movie = class {
|
|
|
1163
1347
|
composition.collectAudio(audios, 0);
|
|
1164
1348
|
if (audios.length > 0) {
|
|
1165
1349
|
this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
|
|
1350
|
+
if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
|
|
1166
1351
|
}
|
|
1352
|
+
this._audioSources = audios;
|
|
1167
1353
|
this.timeline.progress(1).progress(0);
|
|
1168
1354
|
await this._awaitVideoFrames();
|
|
1169
1355
|
this._updateSpace();
|
|
@@ -1251,6 +1437,15 @@ var Movie = class {
|
|
|
1251
1437
|
await this.gotoFrame(f, true);
|
|
1252
1438
|
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1253
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
|
+
}
|
|
1254
1449
|
/**
|
|
1255
1450
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
1256
1451
|
* by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
|
|
@@ -1261,6 +1456,7 @@ var Movie = class {
|
|
|
1261
1456
|
_renderNow() {
|
|
1262
1457
|
const app = this.app;
|
|
1263
1458
|
if (!app?.renderer) return;
|
|
1459
|
+
this._rootSequence?.syncFrame();
|
|
1264
1460
|
Culler.shared.cull(app.stage, app.renderer.screen, false);
|
|
1265
1461
|
app.renderer.render({ container: app.stage });
|
|
1266
1462
|
}
|
|
@@ -1366,6 +1562,8 @@ var Movie = class {
|
|
|
1366
1562
|
safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
|
|
1367
1563
|
this.app = null;
|
|
1368
1564
|
this.audioBuffer = null;
|
|
1565
|
+
this._audioSources = [];
|
|
1566
|
+
this._mixStats = null;
|
|
1369
1567
|
safeRun(() => this._audioContext?.close().catch(() => {
|
|
1370
1568
|
}));
|
|
1371
1569
|
this._audioContext = null;
|
|
@@ -1393,6 +1591,53 @@ function collectVideoSequences(seq, out) {
|
|
|
1393
1591
|
collectVideoSequences(child, out);
|
|
1394
1592
|
}
|
|
1395
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
|
+
}
|
|
1396
1641
|
|
|
1397
1642
|
// src/presets/kenBurns.ts
|
|
1398
1643
|
var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
|
|
@@ -1586,6 +1831,6 @@ function withFade(spec, opts) {
|
|
|
1586
1831
|
return spec;
|
|
1587
1832
|
}
|
|
1588
1833
|
|
|
1589
|
-
export { Movie, kenBurns, orbit, withFade };
|
|
1834
|
+
export { Movie, kenBurns, measureText, orbit, splitText, withFade };
|
|
1590
1835
|
//# sourceMappingURL=index.js.map
|
|
1591
1836
|
//# sourceMappingURL=index.js.map
|