pixi-effects 0.4.0 → 0.6.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 +45 -1
- package/README.md +11 -6
- package/ai/SKILL.md +7 -4
- package/ai/reference/cheatsheet.md +18 -5
- package/ai/reference/pitfalls.md +16 -11
- package/ai/reference/recipes.md +153 -1
- package/ai/template.html +6 -3
- package/ai/tools/check.mjs +385 -0
- package/dist/{Base-DhY9oMxr.d.ts → Base-4vNLl4tH.d.cts} +6 -1
- package/dist/{Base-5R7C5pHR.d.cts → Base-CD4AoT5-.d.ts} +6 -1
- package/dist/Composition-7SO5WYHU.js +4 -0
- package/dist/{Composition-RGTRZ2JX.js.map → Composition-7SO5WYHU.js.map} +1 -1
- package/dist/Composition-Y5VZSDDP.cjs +13 -0
- package/dist/{Composition-D7RCAGEA.cjs.map → Composition-Y5VZSDDP.cjs.map} +1 -1
- package/dist/Controller.cjs +47 -15
- package/dist/Controller.cjs.map +1 -1
- package/dist/Controller.d.cts +5 -2
- package/dist/Controller.d.ts +5 -2
- package/dist/Controller.js +47 -15
- package/dist/Controller.js.map +1 -1
- package/dist/{Movie-DRZtIGU7.d.ts → Movie-CADDBfpl.d.cts} +58 -2
- package/dist/{Movie-CghFXprP.d.cts → Movie-Ch2SUI1h.d.ts} +58 -2
- package/dist/{chunk-QGBOC5YL.cjs → chunk-4AGJF5RK.cjs} +33 -21
- package/dist/chunk-4AGJF5RK.cjs.map +1 -0
- package/dist/{chunk-ZQX7WIYL.js → chunk-7IJHJXDI.js} +380 -64
- package/dist/chunk-7IJHJXDI.js.map +1 -0
- package/dist/{chunk-A574IA4F.js → chunk-B2FTJLNB.js} +18 -6
- package/dist/chunk-B2FTJLNB.js.map +1 -0
- package/dist/chunk-DKGG7WD4.js +61 -0
- package/dist/chunk-DKGG7WD4.js.map +1 -0
- package/dist/{chunk-6BQ6IEW2.cjs → chunk-KQM2ZZMK.cjs} +380 -63
- package/dist/chunk-KQM2ZZMK.cjs.map +1 -0
- package/dist/chunk-UQBGW5DO.cjs +64 -0
- package/dist/chunk-UQBGW5DO.cjs.map +1 -0
- package/dist/index.cjs +333 -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 +317 -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-CRE9WKd4.d.ts → types-DrW9QeKU.d.cts} +118 -12
- package/dist/{types-CRE9WKd4.d.cts → types-DrW9QeKU.d.ts} +118 -12
- package/docs/api.md +37 -0
- package/docs/dsl.md +70 -10
- package/llms-full.txt +300 -30
- 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-CADDBfpl.cjs';
|
|
2
|
+
import { S as Sequence } from './Base-4vNLl4tH.cjs';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-DrW9QeKU.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-DrW9QeKU.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-Ch2SUI1h.js';
|
|
2
|
+
import { S as Sequence } from './Base-CD4AoT5-.js';
|
|
3
|
+
import { S as SequenceSpec, C as CompositionShape, I as ImageSequenceSpec, a as CameraSequenceSpec, b as SequenceCommon } from './types-DrW9QeKU.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-DrW9QeKU.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-B2FTJLNB.js';
|
|
3
|
+
import { homeDistance, DEFAULT_FOV, DEG, resolveAt, collectPropKeys } from './chunk-7IJHJXDI.js';
|
|
4
|
+
export { registerSequenceType } from './chunk-7IJHJXDI.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 || seq.showsNothingYet) 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,63 +1224,34 @@ 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/
|
|
1030
|
-
function
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
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(", ")})`);
|
|
1227
|
+
// src/core/startWhenRunning.ts
|
|
1228
|
+
function startWhenRunning(ctx, start, gesture) {
|
|
1229
|
+
if (ctx.state === "running") {
|
|
1230
|
+
start();
|
|
1231
|
+
return () => {
|
|
1232
|
+
};
|
|
1084
1233
|
}
|
|
1234
|
+
let done = false;
|
|
1235
|
+
const resume = () => {
|
|
1236
|
+
ctx.resume?.()?.catch(() => {
|
|
1237
|
+
});
|
|
1238
|
+
};
|
|
1239
|
+
const stop = () => {
|
|
1240
|
+
done = true;
|
|
1241
|
+
ctx.removeEventListener("statechange", onState);
|
|
1242
|
+
for (const e of GESTURES) gesture?.removeEventListener(e, resume, true);
|
|
1243
|
+
};
|
|
1244
|
+
const onState = () => {
|
|
1245
|
+
if (done || ctx.state !== "running") return;
|
|
1246
|
+
stop();
|
|
1247
|
+
start();
|
|
1248
|
+
};
|
|
1249
|
+
ctx.addEventListener("statechange", onState);
|
|
1250
|
+
for (const e of GESTURES) gesture?.addEventListener(e, resume, true);
|
|
1251
|
+
resume();
|
|
1252
|
+
return stop;
|
|
1085
1253
|
}
|
|
1254
|
+
var GESTURES = ["pointerdown", "mousedown", "touchend", "keydown"];
|
|
1086
1255
|
|
|
1087
1256
|
// src/core/Movie.ts
|
|
1088
1257
|
extensions.add(CullerPlugin);
|
|
@@ -1096,6 +1265,7 @@ var Movie = class {
|
|
|
1096
1265
|
audioBuffer = null;
|
|
1097
1266
|
audioSource = null;
|
|
1098
1267
|
gainNode = null;
|
|
1268
|
+
_cancelAudioStart = null;
|
|
1099
1269
|
_volume = 1;
|
|
1100
1270
|
_muted = false;
|
|
1101
1271
|
isPlaying = false;
|
|
@@ -1110,6 +1280,9 @@ var Movie = class {
|
|
|
1110
1280
|
_rootSequence = null;
|
|
1111
1281
|
_rootContainer = null;
|
|
1112
1282
|
_raf = null;
|
|
1283
|
+
/** What went into the mix, and its peak before limiting — kept for inspectAudio(). */
|
|
1284
|
+
_audioSources = [];
|
|
1285
|
+
_mixStats = null;
|
|
1113
1286
|
on(event, fn) {
|
|
1114
1287
|
(this._events[event] ??= []).push(fn);
|
|
1115
1288
|
if (event === "ready" && this._initState === "ready") {
|
|
@@ -1204,7 +1377,9 @@ var Movie = class {
|
|
|
1204
1377
|
composition.collectAudio(audios, 0);
|
|
1205
1378
|
if (audios.length > 0) {
|
|
1206
1379
|
this.audioBuffer = await mixdown(audios, this.duration, audioContext.sampleRate);
|
|
1380
|
+
if (this.audioBuffer) this._mixStats = limitMix(this.audioBuffer, audios);
|
|
1207
1381
|
}
|
|
1382
|
+
this._audioSources = audios;
|
|
1208
1383
|
this.timeline.progress(1).progress(0);
|
|
1209
1384
|
await this._awaitVideoFrames();
|
|
1210
1385
|
this._updateSpace();
|
|
@@ -1292,6 +1467,15 @@ var Movie = class {
|
|
|
1292
1467
|
await this.gotoFrame(f, true);
|
|
1293
1468
|
return inspectScene(this._rootSequence, f, f / this.frameRate, { width: this.width, height: this.height }, opts);
|
|
1294
1469
|
}
|
|
1470
|
+
/**
|
|
1471
|
+
* The soundtrack, measured: when every audio layer plays, how loud the mix is over time, and `issues`
|
|
1472
|
+
* (limiting, inaudible layers, sounds cut off by the end). Use it to CHECK sound you cannot hear.
|
|
1473
|
+
*/
|
|
1474
|
+
inspectAudio(opts = {}) {
|
|
1475
|
+
warnUnknownOptions("movie.inspectAudio()", opts, ["window"]);
|
|
1476
|
+
this._requireReady("inspectAudio");
|
|
1477
|
+
return analyzeAudio(this.audioBuffer, this._audioSources, this._mixStats, this.duration, opts);
|
|
1478
|
+
}
|
|
1295
1479
|
/**
|
|
1296
1480
|
* Draw the stage now. PixiJS's CullerPlugin only culls inside `app.render()` (the ticker), and we render
|
|
1297
1481
|
* by hand — while seeking and especially during `render()`, which stops the ticker — so without this the
|
|
@@ -1319,6 +1503,7 @@ var Movie = class {
|
|
|
1319
1503
|
play() {
|
|
1320
1504
|
if (this._initState !== "ready") return;
|
|
1321
1505
|
if (this.currentFrame >= this.totalFrames) this.currentFrame = 0;
|
|
1506
|
+
const wasPlaying = this.isPlaying;
|
|
1322
1507
|
this.isPlaying = true;
|
|
1323
1508
|
const startTime = performance.now() - this.currentFrame / this.frameRate * 1e3;
|
|
1324
1509
|
let inFlight = false;
|
|
@@ -1348,17 +1533,25 @@ var Movie = class {
|
|
|
1348
1533
|
this._raf = requestAnimationFrame(tick);
|
|
1349
1534
|
if (this.audioBuffer) {
|
|
1350
1535
|
const ctx = this._ensureAudioContext();
|
|
1351
|
-
|
|
1352
|
-
this.
|
|
1353
|
-
this.
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1536
|
+
const buffer = this.audioBuffer;
|
|
1537
|
+
this._cancelAudioStart?.();
|
|
1538
|
+
this._cancelAudioStart = startWhenRunning(ctx, () => {
|
|
1539
|
+
if (!this.isPlaying) return;
|
|
1540
|
+
this.audioSource = ctx.createBufferSource();
|
|
1541
|
+
this.audioSource.buffer = buffer;
|
|
1542
|
+
this.gainNode = ctx.createGain();
|
|
1543
|
+
this.gainNode.gain.value = this._muted ? 0 : this._volume;
|
|
1544
|
+
this.audioSource.connect(this.gainNode).connect(ctx.destination);
|
|
1545
|
+
this.audioSource.start(0, Math.min(this.currentFrame / this.frameRate, buffer.duration));
|
|
1546
|
+
}, typeof window !== "undefined" ? window : void 0);
|
|
1357
1547
|
}
|
|
1548
|
+
if (!wasPlaying) this.emit("play");
|
|
1358
1549
|
}
|
|
1359
1550
|
pause() {
|
|
1360
1551
|
const wasPlaying = this.isPlaying;
|
|
1361
1552
|
this.isPlaying = false;
|
|
1553
|
+
this._cancelAudioStart?.();
|
|
1554
|
+
this._cancelAudioStart = null;
|
|
1362
1555
|
if (this._raf) {
|
|
1363
1556
|
cancelAnimationFrame(this._raf);
|
|
1364
1557
|
this._raf = null;
|
|
@@ -1408,6 +1601,8 @@ var Movie = class {
|
|
|
1408
1601
|
safeRun(() => this.app?.destroy(true, { children: true, texture: true }));
|
|
1409
1602
|
this.app = null;
|
|
1410
1603
|
this.audioBuffer = null;
|
|
1604
|
+
this._audioSources = [];
|
|
1605
|
+
this._mixStats = null;
|
|
1411
1606
|
safeRun(() => this._audioContext?.close().catch(() => {
|
|
1412
1607
|
}));
|
|
1413
1608
|
this._audioContext = null;
|
|
@@ -1435,6 +1630,53 @@ function collectVideoSequences(seq, out) {
|
|
|
1435
1630
|
collectVideoSequences(child, out);
|
|
1436
1631
|
}
|
|
1437
1632
|
}
|
|
1633
|
+
var BY = ["chars", "words", "lines"];
|
|
1634
|
+
var ALIGN = ["left", "center", "right"];
|
|
1635
|
+
var widthOf = (text, style) => CanvasTextMetrics.measureText(text, style).width;
|
|
1636
|
+
function measureText(text, style = {}) {
|
|
1637
|
+
const m = CanvasTextMetrics.measureText(text, new TextStyle(style));
|
|
1638
|
+
return { width: m.width, height: m.height, lines: text.split("\n").length };
|
|
1639
|
+
}
|
|
1640
|
+
function splitText(text, style = {}, opts = {}) {
|
|
1641
|
+
warnUnknownOptions("splitText()", opts, ["by", "x", "y", "align"]);
|
|
1642
|
+
let by = opts.by ?? "chars";
|
|
1643
|
+
if (!BY.includes(by)) {
|
|
1644
|
+
const g = suggestName(String(by), BY);
|
|
1645
|
+
console.warn(`pixi-effects: splitText(): by "${by}" is not one of ${BY.map((b) => `"${b}"`).join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "chars"`);
|
|
1646
|
+
by = "chars";
|
|
1647
|
+
}
|
|
1648
|
+
let align = opts.align ?? "left";
|
|
1649
|
+
if (!ALIGN.includes(align)) {
|
|
1650
|
+
const g = suggestName(String(align), ALIGN);
|
|
1651
|
+
console.warn(`pixi-effects: splitText(): align "${align}" is not one of ${ALIGN.join(", ")}${g ? ` \u2014 did you mean "${g}"?` : ""}; using "left"`);
|
|
1652
|
+
align = "left";
|
|
1653
|
+
}
|
|
1654
|
+
const x0 = opts.x ?? 0, y0 = opts.y ?? 0;
|
|
1655
|
+
if (text === "") return [];
|
|
1656
|
+
const ts = new TextStyle(style);
|
|
1657
|
+
const lineHeight = CanvasTextMetrics.measureText("M", ts).lineHeight ?? CanvasTextMetrics.measureText("M", ts).height;
|
|
1658
|
+
const sentinel = widthOf("|", ts);
|
|
1659
|
+
const out = [];
|
|
1660
|
+
text.split("\n").forEach((line, lineNo) => {
|
|
1661
|
+
const lineWidth = widthOf(line, ts);
|
|
1662
|
+
const left = align === "center" ? x0 - lineWidth / 2 : align === "right" ? x0 - lineWidth : x0;
|
|
1663
|
+
const y = y0 + lineNo * lineHeight;
|
|
1664
|
+
const advance = (upTo) => upTo === 0 ? 0 : widthOf(line.slice(0, upTo) + "|", ts) - sentinel;
|
|
1665
|
+
const spans = [];
|
|
1666
|
+
if (by === "lines") {
|
|
1667
|
+
if (line.trim() !== "") spans.push([0, line.length]);
|
|
1668
|
+
} else if (by === "words") {
|
|
1669
|
+
for (const m of line.matchAll(/\S+/g)) spans.push([m.index, m.index + m[0].length]);
|
|
1670
|
+
} else {
|
|
1671
|
+
for (let i = 0; i < line.length; i++) if (!/\s/.test(line[i])) spans.push([i, i + 1]);
|
|
1672
|
+
}
|
|
1673
|
+
for (const [a, b] of spans) {
|
|
1674
|
+
const piece = line.slice(a, b);
|
|
1675
|
+
out.push({ text: piece, x: left + advance(a), y, width: widthOf(piece, ts), index: out.length, line: lineNo });
|
|
1676
|
+
}
|
|
1677
|
+
});
|
|
1678
|
+
return out;
|
|
1679
|
+
}
|
|
1438
1680
|
|
|
1439
1681
|
// src/presets/kenBurns.ts
|
|
1440
1682
|
var KEN_BURNS_KEYS = ["asset", "name", "at", "duration", "fit", "ease", "motion", "origin", "zoom", "direction", "angle", "from", "to"];
|
|
@@ -1628,6 +1870,6 @@ function withFade(spec, opts) {
|
|
|
1628
1870
|
return spec;
|
|
1629
1871
|
}
|
|
1630
1872
|
|
|
1631
|
-
export { Movie, kenBurns, orbit, withFade };
|
|
1873
|
+
export { Movie, kenBurns, measureText, orbit, splitText, withFade };
|
|
1632
1874
|
//# sourceMappingURL=index.js.map
|
|
1633
1875
|
//# sourceMappingURL=index.js.map
|