@aelionsdk/render-ir 0.1.0-beta.1
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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/captions.d.ts +16 -0
- package/dist/captions.d.ts.map +1 -0
- package/dist/captions.js +131 -0
- package/dist/color.d.ts +18 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +55 -0
- package/dist/compiler.d.ts +15 -0
- package/dist/compiler.d.ts.map +1 -0
- package/dist/compiler.js +562 -0
- package/dist/evaluate.d.ts +8 -0
- package/dist/evaluate.d.ts.map +1 -0
- package/dist/evaluate.js +260 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/text-layout.d.ts +46 -0
- package/dist/text-layout.d.ts.map +1 -0
- package/dist/text-layout.js +188 -0
- package/dist/time-map.d.ts +26 -0
- package/dist/time-map.d.ts.map +1 -0
- package/dist/time-map.js +208 -0
- package/dist/types.d.ts +242 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/package.json +45 -0
package/dist/evaluate.js
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import { mapIrSourceTime } from './time-map.js';
|
|
2
|
+
function isAnimation(value) {
|
|
3
|
+
if (value === null || Array.isArray(value) || typeof value !== 'object')
|
|
4
|
+
return false;
|
|
5
|
+
const animation = Reflect.get(value, 'animation');
|
|
6
|
+
return (animation !== null &&
|
|
7
|
+
!Array.isArray(animation) &&
|
|
8
|
+
typeof animation === 'object' &&
|
|
9
|
+
Array.isArray(Reflect.get(animation, 'keyframes')));
|
|
10
|
+
}
|
|
11
|
+
function cubicBezierCoordinate(t, first, second) {
|
|
12
|
+
const inverse = 1 - t;
|
|
13
|
+
return 3 * inverse * inverse * t * first + 3 * inverse * t * t * second + t * t * t;
|
|
14
|
+
}
|
|
15
|
+
function cubicBezierProgress(progress, easing) {
|
|
16
|
+
const x1 = numberProperty(easing.x1, 0);
|
|
17
|
+
const y1 = numberProperty(easing.y1, 0);
|
|
18
|
+
const x2 = numberProperty(easing.x2, 1);
|
|
19
|
+
const y2 = numberProperty(easing.y2, 1);
|
|
20
|
+
let low = 0;
|
|
21
|
+
let high = 1;
|
|
22
|
+
for (let iteration = 0; iteration < 20; iteration += 1) {
|
|
23
|
+
const middle = (low + high) / 2;
|
|
24
|
+
if (cubicBezierCoordinate(middle, x1, x2) < progress)
|
|
25
|
+
low = middle;
|
|
26
|
+
else
|
|
27
|
+
high = middle;
|
|
28
|
+
}
|
|
29
|
+
return cubicBezierCoordinate((low + high) / 2, y1, y2);
|
|
30
|
+
}
|
|
31
|
+
function animationTime(animation, sequenceTimeUs, ownerStartUs) {
|
|
32
|
+
return animation.timeSpace === 'sequence' ? sequenceTimeUs : sequenceTimeUs - ownerStartUs;
|
|
33
|
+
}
|
|
34
|
+
function positiveModulo(value, divisor) {
|
|
35
|
+
return ((value % divisor) + divisor) % divisor;
|
|
36
|
+
}
|
|
37
|
+
function infinityTime(timeUs, firstUs, lastUs, mode) {
|
|
38
|
+
const durationUs = lastUs - firstUs;
|
|
39
|
+
if (durationUs <= 0 || (mode !== 'cycle' && mode !== 'ping-pong'))
|
|
40
|
+
return timeUs;
|
|
41
|
+
const progress = positiveModulo(timeUs - firstUs, durationUs);
|
|
42
|
+
if (mode === 'cycle')
|
|
43
|
+
return firstUs + progress;
|
|
44
|
+
const cycle = Math.floor((timeUs - firstUs) / durationUs);
|
|
45
|
+
return Math.abs(cycle) % 2 === 0 ? firstUs + progress : lastUs - progress;
|
|
46
|
+
}
|
|
47
|
+
function interpolateJson(from, to, progress) {
|
|
48
|
+
if (typeof from === 'number' && typeof to === 'number')
|
|
49
|
+
return from + (to - from) * progress;
|
|
50
|
+
if (Array.isArray(from) && Array.isArray(to) && from.length === to.length) {
|
|
51
|
+
return from.map((value, index) => interpolateJson(value, to[index] ?? value, progress));
|
|
52
|
+
}
|
|
53
|
+
if (from !== null &&
|
|
54
|
+
to !== null &&
|
|
55
|
+
typeof from === 'object' &&
|
|
56
|
+
typeof to === 'object' &&
|
|
57
|
+
!Array.isArray(from) &&
|
|
58
|
+
!Array.isArray(to)) {
|
|
59
|
+
const keys = Object.keys(from);
|
|
60
|
+
if (keys.length === Object.keys(to).length && keys.every(key => Object.hasOwn(to, key))) {
|
|
61
|
+
return Object.fromEntries(keys.map(key => [key, interpolateJson(from[key] ?? null, to[key] ?? null, progress)]));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
return from;
|
|
65
|
+
}
|
|
66
|
+
function easingProgress(progress, from) {
|
|
67
|
+
const easing = objectProperty(Reflect.get(from, 'easing'));
|
|
68
|
+
if (easing.type === 'steps') {
|
|
69
|
+
const count = Math.max(1, Math.floor(numberProperty(easing.count, 1)));
|
|
70
|
+
return easing.position === 'start'
|
|
71
|
+
? Math.min(1, Math.ceil(progress * count) / count)
|
|
72
|
+
: Math.floor(progress * count) / count;
|
|
73
|
+
}
|
|
74
|
+
return from.interpolation === 'cubic-bezier' ? cubicBezierProgress(progress, easing) : progress;
|
|
75
|
+
}
|
|
76
|
+
export function evaluateAnimatedValue(value, sequenceTimeUs, ownerStartUs = 0) {
|
|
77
|
+
if (!isAnimation(value))
|
|
78
|
+
return value;
|
|
79
|
+
const animation = value.animation;
|
|
80
|
+
let timeUs = animationTime(animation, sequenceTimeUs, ownerStartUs);
|
|
81
|
+
const keyframes = value.animation.keyframes;
|
|
82
|
+
if (keyframes.length === 0)
|
|
83
|
+
return null;
|
|
84
|
+
const firstKeyframe = keyframes[0];
|
|
85
|
+
const lastKeyframe = keyframes.at(-1);
|
|
86
|
+
if (firstKeyframe === undefined || lastKeyframe === undefined)
|
|
87
|
+
return null;
|
|
88
|
+
if (timeUs < firstKeyframe.timeUs) {
|
|
89
|
+
timeUs = infinityTime(timeUs, firstKeyframe.timeUs, lastKeyframe.timeUs, animation.preInfinity);
|
|
90
|
+
}
|
|
91
|
+
else if (timeUs > lastKeyframe.timeUs) {
|
|
92
|
+
timeUs = infinityTime(timeUs, firstKeyframe.timeUs, lastKeyframe.timeUs, animation.postInfinity);
|
|
93
|
+
}
|
|
94
|
+
const right = keyframes.findIndex(keyframe => keyframe.timeUs > timeUs);
|
|
95
|
+
if (right === 0) {
|
|
96
|
+
if (animation.preInfinity === 'none')
|
|
97
|
+
return null;
|
|
98
|
+
if (animation.preInfinity !== 'linear' || keyframes.length < 2)
|
|
99
|
+
return keyframes[0]?.value ?? null;
|
|
100
|
+
}
|
|
101
|
+
if (right < 0) {
|
|
102
|
+
if (animation.postInfinity === 'none')
|
|
103
|
+
return null;
|
|
104
|
+
if (animation.postInfinity !== 'linear' || keyframes.length < 2) {
|
|
105
|
+
return keyframes.at(-1)?.value ?? null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const toIndex = right === 0 ? 1 : right < 0 ? keyframes.length - 1 : right;
|
|
109
|
+
const from = keyframes[toIndex - 1];
|
|
110
|
+
const to = keyframes[toIndex];
|
|
111
|
+
if (from === undefined || to === undefined)
|
|
112
|
+
return null;
|
|
113
|
+
if ((from.interpolation !== 'linear' && from.interpolation !== 'cubic-bezier') ||
|
|
114
|
+
to.timeUs === from.timeUs) {
|
|
115
|
+
return from.value;
|
|
116
|
+
}
|
|
117
|
+
let progress = (timeUs - from.timeUs) / (to.timeUs - from.timeUs);
|
|
118
|
+
progress = easingProgress(progress, from);
|
|
119
|
+
return interpolateJson(from.value, to.value, progress);
|
|
120
|
+
}
|
|
121
|
+
export function evaluateAnimatableNumber(value, sequenceTimeUs, ownerStartUs, fallback) {
|
|
122
|
+
const evaluated = value === undefined ? undefined : evaluateAnimatedValue(value, sequenceTimeUs, ownerStartUs);
|
|
123
|
+
return numberProperty(evaluated, fallback);
|
|
124
|
+
}
|
|
125
|
+
export function evaluateMaterialInstance(material, sequenceTimeUs, ownerStartUs = 0) {
|
|
126
|
+
return {
|
|
127
|
+
id: material.id,
|
|
128
|
+
parameters: Object.fromEntries(Object.entries(material.parameters).map(([id, value]) => [
|
|
129
|
+
id,
|
|
130
|
+
evaluateAnimatedValue(value, sequenceTimeUs, ownerStartUs),
|
|
131
|
+
])),
|
|
132
|
+
resourceBindings: material.resourceBindings,
|
|
133
|
+
inputBindings: material.inputBindings,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
function contains(startUs, durationUs, timeUs) {
|
|
137
|
+
return timeUs >= startUs && timeUs < startUs + durationUs;
|
|
138
|
+
}
|
|
139
|
+
function mapBaseClipSourceTime(clip, sequenceTimeUs, allowOutsideItem = false) {
|
|
140
|
+
const localUs = sequenceTimeUs - clip.range.startUs;
|
|
141
|
+
return mapIrSourceTime(allowOutsideItem ? { ...clip.source, boundary: 'hold' } : clip.source, clip.range.durationUs, localUs, {
|
|
142
|
+
allowOutsideItem,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
export function mapClipSourceTime(clip, sequenceTimeUs) {
|
|
146
|
+
return mapBaseClipSourceTime(clip, sequenceTimeUs);
|
|
147
|
+
}
|
|
148
|
+
function isVisualRenderClip(clip) {
|
|
149
|
+
return (clip.kind === 'visual-clip' ||
|
|
150
|
+
clip.kind === 'text-clip' ||
|
|
151
|
+
clip.kind === 'nested-sequence-clip' ||
|
|
152
|
+
clip.kind === 'generator-clip' ||
|
|
153
|
+
clip.kind === 'shape-clip' ||
|
|
154
|
+
clip.kind === 'material-content-clip' ||
|
|
155
|
+
clip.kind === 'adjustment-clip');
|
|
156
|
+
}
|
|
157
|
+
function mapNestedSourceTime(clip, sequenceTimeUs, allowOutsideItem = false) {
|
|
158
|
+
return mapIrSourceTime({
|
|
159
|
+
assetId: clip.source.sequenceId,
|
|
160
|
+
streamType: 'video',
|
|
161
|
+
streamIndex: 0,
|
|
162
|
+
sourceRange: clip.source.sourceRange,
|
|
163
|
+
timeMapping: clip.source.timeMapping,
|
|
164
|
+
boundary: allowOutsideItem ? 'hold' : clip.source.boundary,
|
|
165
|
+
}, clip.range.durationUs, sequenceTimeUs - clip.range.startUs, { allowOutsideItem });
|
|
166
|
+
}
|
|
167
|
+
function numberProperty(value, fallback) {
|
|
168
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
169
|
+
}
|
|
170
|
+
function objectProperty(value) {
|
|
171
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
172
|
+
? value
|
|
173
|
+
: {};
|
|
174
|
+
}
|
|
175
|
+
export function evaluateAudioState(ir, startUs, durationUs) {
|
|
176
|
+
if (!Number.isSafeInteger(startUs) ||
|
|
177
|
+
!Number.isSafeInteger(durationUs) ||
|
|
178
|
+
startUs < 0 ||
|
|
179
|
+
durationUs <= 0 ||
|
|
180
|
+
startUs + durationUs > ir.durationUs) {
|
|
181
|
+
throw new RangeError('Audio evaluation range is outside the Render IR duration');
|
|
182
|
+
}
|
|
183
|
+
const hasSoloTrack = ir.tracks.some(track => track.kind === 'audio' && track.enabled && objectProperty(track.audio).solo === true);
|
|
184
|
+
return {
|
|
185
|
+
startUs,
|
|
186
|
+
durationUs,
|
|
187
|
+
clips: ir.tracks
|
|
188
|
+
.filter(track => {
|
|
189
|
+
if (track.kind !== 'audio' || !track.enabled)
|
|
190
|
+
return false;
|
|
191
|
+
const audio = objectProperty(track.audio);
|
|
192
|
+
return audio.muted !== true && (!hasSoloTrack || audio.solo === true);
|
|
193
|
+
})
|
|
194
|
+
.flatMap(track => track.clips.flatMap(clip => {
|
|
195
|
+
if (clip.kind !== 'audio-clip' || !clip.enabled)
|
|
196
|
+
return [];
|
|
197
|
+
const overlapStart = Math.max(startUs, clip.range.startUs);
|
|
198
|
+
const overlapEnd = Math.min(startUs + durationUs, clip.range.startUs + clip.range.durationUs);
|
|
199
|
+
if (overlapStart >= overlapEnd)
|
|
200
|
+
return [];
|
|
201
|
+
const sourceStartUs = mapBaseClipSourceTime(clip, overlapStart);
|
|
202
|
+
if (sourceStartUs === null)
|
|
203
|
+
return [];
|
|
204
|
+
const trackAudio = objectProperty(track.audio);
|
|
205
|
+
const gainDb = evaluateAnimatableNumber(trackAudio.gainDb, overlapStart, 0, 0) + evaluateAnimatableNumber(clip.audio.gainDb, overlapStart, clip.range.startUs, 0);
|
|
206
|
+
const pan = evaluateAnimatableNumber(trackAudio.pan, overlapStart, 0, 0) + evaluateAnimatableNumber(clip.audio.pan, overlapStart, clip.range.startUs, 0);
|
|
207
|
+
return [
|
|
208
|
+
{
|
|
209
|
+
clip,
|
|
210
|
+
sourceStartUs,
|
|
211
|
+
sequenceStartUs: overlapStart,
|
|
212
|
+
durationUs: overlapEnd - overlapStart,
|
|
213
|
+
gain: 10 ** (gainDb / 20),
|
|
214
|
+
pan: Math.max(-1, Math.min(1, pan)),
|
|
215
|
+
trackAudio: track.audio ?? {},
|
|
216
|
+
},
|
|
217
|
+
];
|
|
218
|
+
})),
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
export function evaluateVisualState(ir, timeUs) {
|
|
222
|
+
if (!Number.isSafeInteger(timeUs) || timeUs < 0 || timeUs >= ir.durationUs) {
|
|
223
|
+
throw new RangeError('timeUs is outside the Render IR duration');
|
|
224
|
+
}
|
|
225
|
+
const transition = ir.transitions.find(value => contains(value.range.startUs, value.range.durationUs, timeUs));
|
|
226
|
+
const transitionInputIds = transition === undefined ? undefined : new Set([transition.fromItemId, transition.toItemId]);
|
|
227
|
+
const clips = ir.tracks
|
|
228
|
+
.filter(track => (track.kind === 'visual' || track.kind === 'caption') && track.enabled)
|
|
229
|
+
.flatMap(track => track.clips
|
|
230
|
+
.filter((clip) => isVisualRenderClip(clip) &&
|
|
231
|
+
clip.enabled &&
|
|
232
|
+
(contains(clip.range.startUs, clip.range.durationUs, timeUs) ||
|
|
233
|
+
transitionInputIds?.has(clip.id) === true))
|
|
234
|
+
.map(clip => ({
|
|
235
|
+
clip,
|
|
236
|
+
sourceTimeUs: clip.kind === 'visual-clip'
|
|
237
|
+
? mapBaseClipSourceTime(clip, timeUs, transitionInputIds?.has(clip.id) === true)
|
|
238
|
+
: clip.kind === 'nested-sequence-clip'
|
|
239
|
+
? mapNestedSourceTime(clip, timeUs, transitionInputIds?.has(clip.id) === true)
|
|
240
|
+
: null,
|
|
241
|
+
materials: clip.materialInstanceIds.flatMap(id => {
|
|
242
|
+
const value = ir.materials[id];
|
|
243
|
+
return value?.enabled === true ? [value] : [];
|
|
244
|
+
}),
|
|
245
|
+
})));
|
|
246
|
+
const transitionMaterial = transition === undefined ? undefined : ir.materials[transition.materialInstanceId];
|
|
247
|
+
return {
|
|
248
|
+
timeUs,
|
|
249
|
+
clips,
|
|
250
|
+
...(transition === undefined || transitionMaterial?.enabled !== true
|
|
251
|
+
? {}
|
|
252
|
+
: {
|
|
253
|
+
transition: {
|
|
254
|
+
transition,
|
|
255
|
+
progress: (timeUs - transition.range.startUs) / transition.range.durationUs,
|
|
256
|
+
material: transitionMaterial,
|
|
257
|
+
},
|
|
258
|
+
}),
|
|
259
|
+
};
|
|
260
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,YAAY,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { JsonObject } from '@aelionsdk/core';
|
|
2
|
+
import type { IrTextClip } from './types.js';
|
|
3
|
+
export interface IrLaidOutTextSpan {
|
|
4
|
+
readonly text: string;
|
|
5
|
+
readonly x: number;
|
|
6
|
+
readonly y: number;
|
|
7
|
+
readonly advancePx: number;
|
|
8
|
+
readonly glyphs: readonly IrLaidOutTextGlyph[];
|
|
9
|
+
readonly style: PortableTextStyle;
|
|
10
|
+
}
|
|
11
|
+
export interface IrLaidOutTextGlyph {
|
|
12
|
+
readonly text: string;
|
|
13
|
+
readonly x: number;
|
|
14
|
+
readonly advancePx: number;
|
|
15
|
+
}
|
|
16
|
+
export interface IrLaidOutTextLine {
|
|
17
|
+
readonly x: number;
|
|
18
|
+
readonly y: number;
|
|
19
|
+
readonly width: number;
|
|
20
|
+
readonly height: number;
|
|
21
|
+
readonly spans: readonly IrLaidOutTextSpan[];
|
|
22
|
+
}
|
|
23
|
+
export interface IrTextLayout {
|
|
24
|
+
readonly metricsId: 'aelion-portable-text-metrics/1';
|
|
25
|
+
readonly fontSizePx: number;
|
|
26
|
+
readonly overflowed: boolean;
|
|
27
|
+
readonly lines: readonly IrLaidOutTextLine[];
|
|
28
|
+
}
|
|
29
|
+
export interface PortableTextStyle {
|
|
30
|
+
readonly fontFamilies: readonly string[];
|
|
31
|
+
readonly fontSizePx: number;
|
|
32
|
+
readonly fontWeight: number;
|
|
33
|
+
readonly fontStyle: 'normal' | 'italic' | 'oblique';
|
|
34
|
+
readonly lineHeightPx: number;
|
|
35
|
+
readonly letterSpacingPx: number;
|
|
36
|
+
readonly fill: string;
|
|
37
|
+
readonly stroke: string | undefined;
|
|
38
|
+
readonly strokeWidthPx: number;
|
|
39
|
+
readonly align: 'start' | 'center' | 'end';
|
|
40
|
+
readonly direction: 'ltr' | 'rtl';
|
|
41
|
+
}
|
|
42
|
+
export declare function portableTextStyle(runStyle: JsonObject, paragraphStyle?: JsonObject, scale?: number): PortableTextStyle;
|
|
43
|
+
export declare function portableGlyphAdvance(character: string, style: PortableTextStyle): number;
|
|
44
|
+
/** Deterministic, host-independent line breaking used by Preview and Export. */
|
|
45
|
+
export declare function layoutIrText(clip: IrTextClip): IrTextLayout;
|
|
46
|
+
//# sourceMappingURL=text-layout.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"text-layout.d.ts","sourceRoot":"","sources":["../src/text-layout.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,YAAY,CAAC;AAQxD,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,SAAS,kBAAkB,EAAE,CAAC;IAC/C,QAAQ,CAAC,KAAK,EAAE,iBAAiB,CAAC;CACnC;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,SAAS,EAAE,gCAAgC,CAAC;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,OAAO,CAAC;IAC7B,QAAQ,CAAC,KAAK,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC9C;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,CAAC,YAAY,EAAE,SAAS,MAAM,EAAE,CAAC;IACzC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,QAAQ,GAAG,QAAQ,GAAG,SAAS,CAAC;IACpD,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,CAAC;IACpC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,KAAK,EAAE,OAAO,GAAG,QAAQ,GAAG,KAAK,CAAC;IAC3C,QAAQ,CAAC,SAAS,EAAE,KAAK,GAAG,KAAK,CAAC;CACnC;AAUD,wBAAgB,iBAAiB,CAC/B,QAAQ,EAAE,UAAU,EACpB,cAAc,GAAE,UAAe,EAC/B,KAAK,SAAI,GACR,iBAAiB,CAsBnB;AAED,wBAAgB,oBAAoB,CAAC,SAAS,EAAE,MAAM,EAAE,KAAK,EAAE,iBAAiB,GAAG,MAAM,CAaxF;AA8HD,gFAAgF;AAChF,wBAAgB,YAAY,CAAC,IAAI,EAAE,UAAU,GAAG,YAAY,CA2B3D"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
const graphemeSegmenter = new Intl.Segmenter('und', { granularity: 'grapheme' });
|
|
2
|
+
function graphemes(value) {
|
|
3
|
+
return Array.from(graphemeSegmenter.segment(value), segment => segment.segment);
|
|
4
|
+
}
|
|
5
|
+
function finite(value, fallback) {
|
|
6
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
7
|
+
}
|
|
8
|
+
function text(value, fallback) {
|
|
9
|
+
return typeof value === 'string' ? value : fallback;
|
|
10
|
+
}
|
|
11
|
+
export function portableTextStyle(runStyle, paragraphStyle = {}, scale = 1) {
|
|
12
|
+
const combined = { ...paragraphStyle, ...runStyle };
|
|
13
|
+
const families = Array.isArray(combined.fontFamilies)
|
|
14
|
+
? combined.fontFamilies.filter((value) => typeof value === 'string')
|
|
15
|
+
: [text(combined.fontFamily, 'sans-serif')];
|
|
16
|
+
const fontSizePx = Math.max(1, finite(combined.fontSizePx, 32) * scale);
|
|
17
|
+
const fontStyle = text(combined.fontStyle, 'normal');
|
|
18
|
+
const align = text(combined.align, 'start');
|
|
19
|
+
const direction = text(combined.direction, 'ltr');
|
|
20
|
+
return {
|
|
21
|
+
fontFamilies: families.length === 0 ? ['sans-serif'] : families,
|
|
22
|
+
fontSizePx,
|
|
23
|
+
fontWeight: Math.max(1, Math.min(1_000, finite(combined.fontWeight, 400))),
|
|
24
|
+
fontStyle: fontStyle === 'italic' || fontStyle === 'oblique' ? fontStyle : 'normal',
|
|
25
|
+
lineHeightPx: Math.max(fontSizePx, finite(combined.lineHeightPx, fontSizePx * 1.2) * scale),
|
|
26
|
+
letterSpacingPx: finite(combined.letterSpacingPx, 0) * scale,
|
|
27
|
+
fill: text(combined.fill, '#ffffff'),
|
|
28
|
+
stroke: typeof combined.stroke === 'string' ? combined.stroke : undefined,
|
|
29
|
+
strokeWidthPx: Math.max(0, finite(combined.strokeWidthPx, 0) * scale),
|
|
30
|
+
align: align === 'center' || align === 'end' ? align : 'start',
|
|
31
|
+
direction: direction === 'rtl' ? 'rtl' : 'ltr',
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
export function portableGlyphAdvance(character, style) {
|
|
35
|
+
const codePoint = character.codePointAt(0) ?? 0;
|
|
36
|
+
const em = character === ' '
|
|
37
|
+
? 0.33
|
|
38
|
+
: codePoint >= 0x2e80 || codePoint > 0xffff
|
|
39
|
+
? 1
|
|
40
|
+
: /[ilI1.,'`]/u.test(character)
|
|
41
|
+
? 0.32
|
|
42
|
+
: /[MW@#%]/u.test(character)
|
|
43
|
+
? 0.9
|
|
44
|
+
: 0.6;
|
|
45
|
+
return style.fontSizePx * em + style.letterSpacingPx;
|
|
46
|
+
}
|
|
47
|
+
function runTokens(run) {
|
|
48
|
+
return run.text
|
|
49
|
+
.split(/(\r?\n|[\t ]+|(?=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}]))/gu)
|
|
50
|
+
.filter(Boolean);
|
|
51
|
+
}
|
|
52
|
+
function tokenAdvance(token, style) {
|
|
53
|
+
return graphemes(token).reduce((total, character) => total + portableGlyphAdvance(character, style), 0);
|
|
54
|
+
}
|
|
55
|
+
function lineOffset(width, boxWidth, align, direction) {
|
|
56
|
+
if (align === 'center')
|
|
57
|
+
return (boxWidth - width) / 2;
|
|
58
|
+
if (align === 'end')
|
|
59
|
+
return direction === 'rtl' ? 0 : boxWidth - width;
|
|
60
|
+
return direction === 'rtl' ? boxWidth - width : 0;
|
|
61
|
+
}
|
|
62
|
+
function laidOutSpan(value, x, y, style) {
|
|
63
|
+
let cursor = x;
|
|
64
|
+
const glyphs = graphemes(value).map(character => {
|
|
65
|
+
const advancePx = portableGlyphAdvance(character, style);
|
|
66
|
+
const glyph = { text: character, x: cursor, advancePx };
|
|
67
|
+
cursor += advancePx;
|
|
68
|
+
return glyph;
|
|
69
|
+
});
|
|
70
|
+
return { text: value, x, y, advancePx: cursor - x, glyphs, style };
|
|
71
|
+
}
|
|
72
|
+
function layoutAtScale(clip, scale) {
|
|
73
|
+
const lines = [];
|
|
74
|
+
let cursorY = clip.box.y;
|
|
75
|
+
let current = [];
|
|
76
|
+
let width = 0;
|
|
77
|
+
let height = 0;
|
|
78
|
+
let align = 'start';
|
|
79
|
+
let direction = 'ltr';
|
|
80
|
+
let overflowed = false;
|
|
81
|
+
const flush = () => {
|
|
82
|
+
if (current.length === 0 && height === 0)
|
|
83
|
+
return;
|
|
84
|
+
const offset = lineOffset(width, clip.box.width, align, direction);
|
|
85
|
+
let rtlCursor = clip.box.x + width;
|
|
86
|
+
const positioned = current.map(span => {
|
|
87
|
+
if (direction !== 'rtl')
|
|
88
|
+
return span;
|
|
89
|
+
rtlCursor -= span.advancePx;
|
|
90
|
+
const shift = rtlCursor - span.x;
|
|
91
|
+
return {
|
|
92
|
+
...span,
|
|
93
|
+
x: rtlCursor,
|
|
94
|
+
glyphs: span.glyphs.map(glyph => ({ ...glyph, x: glyph.x + shift })),
|
|
95
|
+
};
|
|
96
|
+
});
|
|
97
|
+
lines.push({
|
|
98
|
+
x: clip.box.x + offset,
|
|
99
|
+
y: cursorY,
|
|
100
|
+
width,
|
|
101
|
+
height,
|
|
102
|
+
spans: positioned.map(span => ({
|
|
103
|
+
...span,
|
|
104
|
+
x: span.x + offset,
|
|
105
|
+
glyphs: span.glyphs.map(glyph => ({ ...glyph, x: glyph.x + offset })),
|
|
106
|
+
})),
|
|
107
|
+
});
|
|
108
|
+
cursorY += height;
|
|
109
|
+
current = [];
|
|
110
|
+
width = 0;
|
|
111
|
+
height = 0;
|
|
112
|
+
};
|
|
113
|
+
for (const paragraph of clip.paragraphs) {
|
|
114
|
+
for (const run of paragraph.runs) {
|
|
115
|
+
const style = portableTextStyle(run.style, paragraph.style, scale);
|
|
116
|
+
for (const token of runTokens(run)) {
|
|
117
|
+
if (token === '\n' || token === '\r\n') {
|
|
118
|
+
flush();
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (current.length === 0) {
|
|
122
|
+
align = style.align;
|
|
123
|
+
direction = style.direction;
|
|
124
|
+
}
|
|
125
|
+
const advance = tokenAdvance(token, style);
|
|
126
|
+
if (width > 0 && width + advance > clip.box.width && token.trim().length > 0)
|
|
127
|
+
flush();
|
|
128
|
+
if (cursorY + Math.max(height, style.lineHeightPx) > clip.box.y + clip.box.height) {
|
|
129
|
+
overflowed = true;
|
|
130
|
+
}
|
|
131
|
+
current.push(laidOutSpan(token, clip.box.x + width, cursorY, style));
|
|
132
|
+
width += advance;
|
|
133
|
+
height = Math.max(height, style.lineHeightPx);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
flush();
|
|
137
|
+
}
|
|
138
|
+
if (clip.overflow === 'ellipsis' && overflowed) {
|
|
139
|
+
const last = lines.findLast(line => line.y + line.height <= clip.box.y + clip.box.height);
|
|
140
|
+
if (last !== undefined && last.spans.length > 0) {
|
|
141
|
+
const spans = last.spans.slice();
|
|
142
|
+
const tail = spans.at(-1);
|
|
143
|
+
if (tail !== undefined) {
|
|
144
|
+
const replacement = laidOutSpan(`${tail.text.trimEnd()}…`, tail.x, tail.y, tail.style);
|
|
145
|
+
spans[spans.length - 1] = replacement;
|
|
146
|
+
}
|
|
147
|
+
const index = lines.indexOf(last);
|
|
148
|
+
lines.splice(index, lines.length - index, { ...last, spans });
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return {
|
|
152
|
+
metricsId: 'aelion-portable-text-metrics/1',
|
|
153
|
+
fontSizePx: 32 * scale,
|
|
154
|
+
overflowed,
|
|
155
|
+
lines,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** Deterministic, host-independent line breaking used by Preview and Export. */
|
|
159
|
+
export function layoutIrText(clip) {
|
|
160
|
+
if (clip.writingMode !== 'horizontal-tb') {
|
|
161
|
+
// Portable vertical fallback treats each grapheme as a line. It remains
|
|
162
|
+
// deterministic and preserves content when native vertical shaping is unavailable.
|
|
163
|
+
const vertical = {
|
|
164
|
+
...clip,
|
|
165
|
+
paragraphs: clip.paragraphs.map(paragraph => ({
|
|
166
|
+
...paragraph,
|
|
167
|
+
runs: paragraph.runs.map(run => ({ ...run, text: graphemes(run.text).join('\n') })),
|
|
168
|
+
})),
|
|
169
|
+
};
|
|
170
|
+
return layoutAtScale(vertical, 1);
|
|
171
|
+
}
|
|
172
|
+
let result = layoutAtScale(clip, 1);
|
|
173
|
+
if (clip.overflow !== 'auto-fit' || !result.overflowed)
|
|
174
|
+
return result;
|
|
175
|
+
let low = 0.25;
|
|
176
|
+
let high = 1;
|
|
177
|
+
for (let iteration = 0; iteration < 12; iteration++) {
|
|
178
|
+
const middle = (low + high) / 2;
|
|
179
|
+
const candidate = layoutAtScale(clip, middle);
|
|
180
|
+
if (candidate.overflowed)
|
|
181
|
+
high = middle;
|
|
182
|
+
else {
|
|
183
|
+
low = middle;
|
|
184
|
+
result = candidate;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { IrMediaSource, IrTimeMapping } from './types.js';
|
|
2
|
+
export type IrTimeMapDirection = 'forward' | 'reverse' | 'hold';
|
|
3
|
+
export interface IrTimeMapSegment {
|
|
4
|
+
readonly itemStartUs: number;
|
|
5
|
+
readonly itemEndUs: number;
|
|
6
|
+
readonly sourceStartUs: number;
|
|
7
|
+
readonly sourceEndUs: number;
|
|
8
|
+
readonly direction: IrTimeMapDirection;
|
|
9
|
+
readonly interpolation: 'linear' | 'hold' | 'cubic';
|
|
10
|
+
}
|
|
11
|
+
export interface IrTimeMapInverse {
|
|
12
|
+
readonly kind: 'point' | 'range';
|
|
13
|
+
readonly itemStartUs: number;
|
|
14
|
+
readonly itemEndUs: number;
|
|
15
|
+
}
|
|
16
|
+
/** Returns the canonical mapping, including compatibility for legacy hand-authored IR. */
|
|
17
|
+
export declare function irTimeMapping(source: IrMediaSource): IrTimeMapping;
|
|
18
|
+
/** Validates and exposes independently monotonic portions of a TimeMap. */
|
|
19
|
+
export declare function analyzeIrTimeMap(source: IrMediaSource, itemDurationUs: number): readonly IrTimeMapSegment[];
|
|
20
|
+
/** Inverts all monotonic TimeMap segments. Hold regions return an Item-local range. */
|
|
21
|
+
export declare function invertIrSourceTime(source: IrMediaSource, itemDurationUs: number, sourceTimeUs: number): readonly IrTimeMapInverse[];
|
|
22
|
+
/** Maps Item-local time to normalized absolute source presentation time. */
|
|
23
|
+
export declare function mapIrSourceTime(source: IrMediaSource, itemDurationUs: number, localUs: number, options?: {
|
|
24
|
+
readonly allowOutsideItem?: boolean;
|
|
25
|
+
}): number | null;
|
|
26
|
+
//# sourceMappingURL=time-map.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"time-map.d.ts","sourceRoot":"","sources":["../src/time-map.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,aAAa,EACb,aAAa,EACd,MAAM,YAAY,CAAC;AAEpB,MAAM,MAAM,kBAAkB,GAAG,SAAS,GAAG,SAAS,GAAG,MAAM,CAAC;AAEhE,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,kBAAkB,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,QAAQ,GAAG,MAAM,GAAG,OAAO,CAAC;CACrD;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,IAAI,EAAE,OAAO,GAAG,OAAO,CAAC;IACjC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAuBD,0FAA0F;AAC1F,wBAAgB,aAAa,CAAC,MAAM,EAAE,aAAa,GAAG,aAAa,CAElE;AA4CD,2EAA2E;AAC3E,wBAAgB,gBAAgB,CAC9B,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,MAAM,GACrB,SAAS,gBAAgB,EAAE,CAqD7B;AAcD,uFAAuF;AACvF,wBAAgB,kBAAkB,CAChC,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,MAAM,EACtB,YAAY,EAAE,MAAM,GACnB,SAAS,gBAAgB,EAAE,CA0C7B;AAqBD,4EAA4E;AAC5E,wBAAgB,eAAe,CAC7B,MAAM,EAAE,aAAa,EACrB,cAAc,EAAE,MAAM,EACtB,OAAO,EAAE,MAAM,EACf,OAAO,GAAE;IAAE,QAAQ,CAAC,gBAAgB,CAAC,EAAE,OAAO,CAAA;CAAO,GACpD,MAAM,GAAG,IAAI,CAkBf"}
|