@clipkit/runtime 1.0.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/LICENSE +54 -0
- package/README.md +98 -0
- package/dist/animation/easings.d.ts +9 -0
- package/dist/animation/easings.d.ts.map +1 -0
- package/dist/animation/easings.js +230 -0
- package/dist/animation/easings.js.map +1 -0
- package/dist/animation/expr.d.ts +44 -0
- package/dist/animation/expr.d.ts.map +1 -0
- package/dist/animation/expr.js +236 -0
- package/dist/animation/expr.js.map +1 -0
- package/dist/animation/keyframes.d.ts +23 -0
- package/dist/animation/keyframes.d.ts.map +1 -0
- package/dist/animation/keyframes.js +117 -0
- package/dist/animation/keyframes.js.map +1 -0
- package/dist/animation/motion-path.d.ts +18 -0
- package/dist/animation/motion-path.d.ts.map +1 -0
- package/dist/animation/motion-path.js +269 -0
- package/dist/animation/motion-path.js.map +1 -0
- package/dist/animation/noise1d.d.ts +5 -0
- package/dist/animation/noise1d.d.ts.map +1 -0
- package/dist/animation/noise1d.js +27 -0
- package/dist/animation/noise1d.js.map +1 -0
- package/dist/animation/presets.d.ts +60 -0
- package/dist/animation/presets.d.ts.map +1 -0
- package/dist/animation/presets.js +221 -0
- package/dist/animation/presets.js.map +1 -0
- package/dist/assets/cache.d.ts +18 -0
- package/dist/assets/cache.d.ts.map +1 -0
- package/dist/assets/cache.js +56 -0
- package/dist/assets/cache.js.map +1 -0
- package/dist/assets/fonts.d.ts +20 -0
- package/dist/assets/fonts.d.ts.map +1 -0
- package/dist/assets/fonts.js +127 -0
- package/dist/assets/fonts.js.map +1 -0
- package/dist/assets/loader.d.ts +18 -0
- package/dist/assets/loader.d.ts.map +1 -0
- package/dist/assets/loader.js +87 -0
- package/dist/assets/loader.js.map +1 -0
- package/dist/assets/lut.d.ts +5 -0
- package/dist/assets/lut.d.ts.map +1 -0
- package/dist/assets/lut.js +77 -0
- package/dist/assets/lut.js.map +1 -0
- package/dist/assets/media-time.d.ts +31 -0
- package/dist/assets/media-time.d.ts.map +1 -0
- package/dist/assets/media-time.js +65 -0
- package/dist/assets/media-time.js.map +1 -0
- package/dist/assets/mp4-frame-source.d.ts +44 -0
- package/dist/assets/mp4-frame-source.d.ts.map +1 -0
- package/dist/assets/mp4-frame-source.js +387 -0
- package/dist/assets/mp4-frame-source.js.map +1 -0
- package/dist/audio/encoder.d.ts +31 -0
- package/dist/audio/encoder.d.ts.map +1 -0
- package/dist/audio/encoder.js +96 -0
- package/dist/audio/encoder.js.map +1 -0
- package/dist/audio/fades.d.ts +16 -0
- package/dist/audio/fades.d.ts.map +1 -0
- package/dist/audio/fades.js +43 -0
- package/dist/audio/fades.js.map +1 -0
- package/dist/audio/limiter.d.ts +8 -0
- package/dist/audio/limiter.d.ts.map +1 -0
- package/dist/audio/limiter.js +39 -0
- package/dist/audio/limiter.js.map +1 -0
- package/dist/audio/loader.d.ts +6 -0
- package/dist/audio/loader.d.ts.map +1 -0
- package/dist/audio/loader.js +42 -0
- package/dist/audio/loader.js.map +1 -0
- package/dist/audio/mixer.d.ts +17 -0
- package/dist/audio/mixer.d.ts.map +1 -0
- package/dist/audio/mixer.js +204 -0
- package/dist/audio/mixer.js.map +1 -0
- package/dist/audio/varispeed.d.ts +24 -0
- package/dist/audio/varispeed.d.ts.map +1 -0
- package/dist/audio/varispeed.js +114 -0
- package/dist/audio/varispeed.js.map +1 -0
- package/dist/audio/wav.d.ts +6 -0
- package/dist/audio/wav.d.ts.map +1 -0
- package/dist/audio/wav.js +62 -0
- package/dist/audio/wav.js.map +1 -0
- package/dist/backend/backend.d.ts +579 -0
- package/dist/backend/backend.d.ts.map +1 -0
- package/dist/backend/backend.js +17 -0
- package/dist/backend/backend.js.map +1 -0
- package/dist/backend/webgl-backend.d.ts +97 -0
- package/dist/backend/webgl-backend.d.ts.map +1 -0
- package/dist/backend/webgl-backend.js +2142 -0
- package/dist/backend/webgl-backend.js.map +1 -0
- package/dist/backend/webgpu-backend.d.ts +121 -0
- package/dist/backend/webgpu-backend.d.ts.map +1 -0
- package/dist/backend/webgpu-backend.js +2481 -0
- package/dist/backend/webgpu-backend.js.map +1 -0
- package/dist/compositor/bitfont.d.ts +8 -0
- package/dist/compositor/bitfont.d.ts.map +1 -0
- package/dist/compositor/bitfont.js +52 -0
- package/dist/compositor/bitfont.js.map +1 -0
- package/dist/compositor/camera.d.ts +5 -0
- package/dist/compositor/camera.d.ts.map +1 -0
- package/dist/compositor/camera.js +114 -0
- package/dist/compositor/camera.js.map +1 -0
- package/dist/compositor/color.d.ts +26 -0
- package/dist/compositor/color.d.ts.map +1 -0
- package/dist/compositor/color.js +189 -0
- package/dist/compositor/color.js.map +1 -0
- package/dist/compositor/element-renderers/caption.d.ts +4 -0
- package/dist/compositor/element-renderers/caption.d.ts.map +1 -0
- package/dist/compositor/element-renderers/caption.js +376 -0
- package/dist/compositor/element-renderers/caption.js.map +1 -0
- package/dist/compositor/element-renderers/group.d.ts +12 -0
- package/dist/compositor/element-renderers/group.d.ts.map +1 -0
- package/dist/compositor/element-renderers/group.js +259 -0
- package/dist/compositor/element-renderers/group.js.map +1 -0
- package/dist/compositor/element-renderers/image.d.ts +4 -0
- package/dist/compositor/element-renderers/image.d.ts.map +1 -0
- package/dist/compositor/element-renderers/image.js +97 -0
- package/dist/compositor/element-renderers/image.js.map +1 -0
- package/dist/compositor/element-renderers/lit.d.ts +6 -0
- package/dist/compositor/element-renderers/lit.d.ts.map +1 -0
- package/dist/compositor/element-renderers/lit.js +82 -0
- package/dist/compositor/element-renderers/lit.js.map +1 -0
- package/dist/compositor/element-renderers/particles.d.ts +4 -0
- package/dist/compositor/element-renderers/particles.d.ts.map +1 -0
- package/dist/compositor/element-renderers/particles.js +212 -0
- package/dist/compositor/element-renderers/particles.js.map +1 -0
- package/dist/compositor/element-renderers/shape.d.ts +4 -0
- package/dist/compositor/element-renderers/shape.d.ts.map +1 -0
- package/dist/compositor/element-renderers/shape.js +171 -0
- package/dist/compositor/element-renderers/shape.js.map +1 -0
- package/dist/compositor/element-renderers/svg.d.ts +4 -0
- package/dist/compositor/element-renderers/svg.d.ts.map +1 -0
- package/dist/compositor/element-renderers/svg.js +210 -0
- package/dist/compositor/element-renderers/svg.js.map +1 -0
- package/dist/compositor/element-renderers/text.d.ts +25 -0
- package/dist/compositor/element-renderers/text.d.ts.map +1 -0
- package/dist/compositor/element-renderers/text.js +1358 -0
- package/dist/compositor/element-renderers/text.js.map +1 -0
- package/dist/compositor/element-renderers/video.d.ts +12 -0
- package/dist/compositor/element-renderers/video.d.ts.map +1 -0
- package/dist/compositor/element-renderers/video.js +109 -0
- package/dist/compositor/element-renderers/video.js.map +1 -0
- package/dist/compositor/fit.d.ts +18 -0
- package/dist/compositor/fit.d.ts.map +1 -0
- package/dist/compositor/fit.js +106 -0
- package/dist/compositor/fit.js.map +1 -0
- package/dist/compositor/lighting.d.ts +63 -0
- package/dist/compositor/lighting.d.ts.map +1 -0
- package/dist/compositor/lighting.js +141 -0
- package/dist/compositor/lighting.js.map +1 -0
- package/dist/compositor/mat4.d.ts +88 -0
- package/dist/compositor/mat4.d.ts.map +1 -0
- package/dist/compositor/mat4.js +245 -0
- package/dist/compositor/mat4.js.map +1 -0
- package/dist/compositor/project.d.ts +24 -0
- package/dist/compositor/project.d.ts.map +1 -0
- package/dist/compositor/project.js +105 -0
- package/dist/compositor/project.js.map +1 -0
- package/dist/compositor/render-context.d.ts +194 -0
- package/dist/compositor/render-context.d.ts.map +1 -0
- package/dist/compositor/render-context.js +10 -0
- package/dist/compositor/render-context.js.map +1 -0
- package/dist/compositor/resolve.d.ts +80 -0
- package/dist/compositor/resolve.d.ts.map +1 -0
- package/dist/compositor/resolve.js +276 -0
- package/dist/compositor/resolve.js.map +1 -0
- package/dist/compositor/scene.d.ts +10 -0
- package/dist/compositor/scene.d.ts.map +1 -0
- package/dist/compositor/scene.js +658 -0
- package/dist/compositor/scene.js.map +1 -0
- package/dist/compositor/transform.d.ts +73 -0
- package/dist/compositor/transform.d.ts.map +1 -0
- package/dist/compositor/transform.js +229 -0
- package/dist/compositor/transform.js.map +1 -0
- package/dist/compositor/unit.d.ts +27 -0
- package/dist/compositor/unit.d.ts.map +1 -0
- package/dist/compositor/unit.js +74 -0
- package/dist/compositor/unit.js.map +1 -0
- package/dist/encoder/exporter.d.ts +95 -0
- package/dist/encoder/exporter.d.ts.map +1 -0
- package/dist/encoder/exporter.js +341 -0
- package/dist/encoder/exporter.js.map +1 -0
- package/dist/encoder/index.d.ts +3 -0
- package/dist/encoder/index.d.ts.map +1 -0
- package/dist/encoder/index.js +2 -0
- package/dist/encoder/index.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +27 -0
- package/dist/index.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +32 -0
- package/dist/logger.js.map +1 -0
- package/dist/runtime.d.ts +216 -0
- package/dist/runtime.d.ts.map +1 -0
- package/dist/runtime.js +1012 -0
- package/dist/runtime.js.map +1 -0
- package/dist/svg/morph.d.ts +6 -0
- package/dist/svg/morph.d.ts.map +1 -0
- package/dist/svg/morph.js +62 -0
- package/dist/svg/morph.js.map +1 -0
- package/dist/svg/svg-renderer.d.ts +18 -0
- package/dist/svg/svg-renderer.d.ts.map +1 -0
- package/dist/svg/svg-renderer.js +142 -0
- package/dist/svg/svg-renderer.js.map +1 -0
- package/dist/text/caption-chunk.d.ts +17 -0
- package/dist/text/caption-chunk.d.ts.map +1 -0
- package/dist/text/caption-chunk.js +76 -0
- package/dist/text/caption-chunk.js.map +1 -0
- package/dist/text/font-atlas.d.ts +63 -0
- package/dist/text/font-atlas.d.ts.map +1 -0
- package/dist/text/font-atlas.js +225 -0
- package/dist/text/font-atlas.js.map +1 -0
- package/dist/text/measure.d.ts +38 -0
- package/dist/text/measure.d.ts.map +1 -0
- package/dist/text/measure.js +164 -0
- package/dist/text/measure.js.map +1 -0
- package/dist/text/text-animation.d.ts +52 -0
- package/dist/text/text-animation.d.ts.map +1 -0
- package/dist/text/text-animation.js +133 -0
- package/dist/text/text-animation.js.map +1 -0
- package/package.json +47 -0
|
@@ -0,0 +1,658 @@
|
|
|
1
|
+
// Frame-level scene rendering.
|
|
2
|
+
//
|
|
3
|
+
// renderSourceFrame iterates the elements in a Source, filters by their
|
|
4
|
+
// active time window, and dispatches each to the appropriate element
|
|
5
|
+
// renderer. The element renderers call into the Backend.
|
|
6
|
+
//
|
|
7
|
+
// Animations (named + keyframe) are not yet applied here; that's a
|
|
8
|
+
// follow-up. For Phase 2a we render static schema values.
|
|
9
|
+
import { interpolateKeyframes } from '../animation/keyframes.js';
|
|
10
|
+
import { isProgrammableBlend } from '../backend/backend.js';
|
|
11
|
+
import { getLogger } from '../logger.js';
|
|
12
|
+
import { buildAsciiAtlasCanvas } from './bitfont.js';
|
|
13
|
+
import { parseColor } from './color.js';
|
|
14
|
+
import { renderCaptionElement } from './element-renderers/caption.js';
|
|
15
|
+
import { renderGroupElement } from './element-renderers/group.js';
|
|
16
|
+
import { renderImageElement } from './element-renderers/image.js';
|
|
17
|
+
import { renderParticlesElement } from './element-renderers/particles.js';
|
|
18
|
+
import { renderShapeElement } from './element-renderers/shape.js';
|
|
19
|
+
import { renderPathShape } from './element-renderers/svg.js';
|
|
20
|
+
import { renderTextElement } from './element-renderers/text.js';
|
|
21
|
+
import { renderVideoElement } from './element-renderers/video.js';
|
|
22
|
+
import { applyAnimation, applyAspectRatio, depthOrder, resolve3D, resolveScalePair } from './resolve.js';
|
|
23
|
+
import { applyModelTransform, mat4Multiply } from './mat4.js';
|
|
24
|
+
import { resolveAnchor, resolveLength } from './unit.js';
|
|
25
|
+
import { anchorToCenter, quadMatrix3D } from './transform.js';
|
|
26
|
+
/**
|
|
27
|
+
* Render one frame of the source at the given time.
|
|
28
|
+
* Caller is responsible for backend.beginFrame() / endFrame() — this lets
|
|
29
|
+
* the runtime use the same scene-render code for preview and export
|
|
30
|
+
* without worrying about who controls the frame lifecycle.
|
|
31
|
+
*/
|
|
32
|
+
export function renderSourceFrame(source, ctx) {
|
|
33
|
+
const sourceDuration = typeof source.duration === 'number' ? source.duration : Infinity;
|
|
34
|
+
// Attach the dispatch on the context so the group renderer (which
|
|
35
|
+
// can't import this file without a cycle) can recurse into children.
|
|
36
|
+
ctx._dispatch = dispatchElement;
|
|
37
|
+
// Draw order: descending `layer` so the HIGHEST layer draws first
|
|
38
|
+
// (farthest back) and layer 1 draws last (on top) — the After Effects
|
|
39
|
+
// model. Array.prototype.sort is stable, so any elements that share a
|
|
40
|
+
// layer keep definition order. Then (§4.4.3) the list is re-ordered
|
|
41
|
+
// back-to-front by depth (`z`); equal depths keep this layer order.
|
|
42
|
+
// With all z = 0 this is pure layer order.
|
|
43
|
+
let ordered = [...source.elements].sort((a, b) => layerOf(b) - layerOf(a));
|
|
44
|
+
if (ctx.depthSort)
|
|
45
|
+
ordered = depthOrder(ordered, ctx);
|
|
46
|
+
for (const element of ordered) {
|
|
47
|
+
if (element.visible === false)
|
|
48
|
+
continue;
|
|
49
|
+
if (!isActiveAt(element, ctx.time, sourceDuration))
|
|
50
|
+
continue;
|
|
51
|
+
try {
|
|
52
|
+
dispatchElement(element, ctx);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
// One bad element shouldn't break the rest of the frame.
|
|
56
|
+
// eslint-disable-next-line no-console
|
|
57
|
+
console.error('[clipkit] Element render failed:', element.type, element.id, err);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function layerOf(el) {
|
|
62
|
+
// `layer` is required + validated; this fallback only guards malformed
|
|
63
|
+
// live state. Missing → far back (drawn first) under the descending
|
|
64
|
+
// sort, matching the old "missing track = back" behavior.
|
|
65
|
+
if (typeof el.layer === 'number' && Number.isFinite(el.layer))
|
|
66
|
+
return el.layer;
|
|
67
|
+
return Number.MAX_SAFE_INTEGER;
|
|
68
|
+
}
|
|
69
|
+
function dispatchElement(element, ctx) {
|
|
70
|
+
// Filters (blur_radius / brightness / contrast / saturation) and
|
|
71
|
+
// stylize effects (§4.7) wrap the element type-agnostically: render
|
|
72
|
+
// the element — subtree included — into a transparent surface-sized
|
|
73
|
+
// layer, then run the pass chain (blur → color ops → effects in
|
|
74
|
+
// array order), compositing the last pass back onto the surface.
|
|
75
|
+
// Cheap fast path: most elements have neither.
|
|
76
|
+
// Piecewise blend modes (overlay/hard-light/soft-light) also need
|
|
77
|
+
// the element isolated to a layer and composited against a backdrop
|
|
78
|
+
// snapshot — same layer path as filters/effects.
|
|
79
|
+
const progBlend = isProgrammableBlend(element.blend_mode);
|
|
80
|
+
if (hasFilterFields(element) || (element.effects && element.effects.length > 0) || progBlend) {
|
|
81
|
+
const filter = resolveFilter(element, ctx);
|
|
82
|
+
const effects = resolveEffects(element, ctx);
|
|
83
|
+
if (filter || effects.length > 0 || progBlend) {
|
|
84
|
+
renderLayeredElement(element, ctx, filter, effects);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
renderElementByType(element, ctx);
|
|
89
|
+
}
|
|
90
|
+
const FILTER_PROPS = ['blur_radius', 'brightness', 'contrast', 'saturation', 'hue_rotate'];
|
|
91
|
+
function hasFilterFields(element) {
|
|
92
|
+
for (const prop of FILTER_PROPS) {
|
|
93
|
+
if (element[prop] !== undefined)
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
const kfs = element.keyframe_animations;
|
|
97
|
+
if (kfs) {
|
|
98
|
+
for (const kf of kfs) {
|
|
99
|
+
if (FILTER_PROPS.includes(kf.property))
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
function resolveFilter(element, ctx) {
|
|
106
|
+
const blur = Math.max(0, resolveFilterValue(element, 'blur_radius', 0, ctx));
|
|
107
|
+
const brightness = Math.max(0, resolveFilterValue(element, 'brightness', 1, ctx));
|
|
108
|
+
const contrast = Math.max(0, resolveFilterValue(element, 'contrast', 1, ctx));
|
|
109
|
+
const saturation = Math.max(0, resolveFilterValue(element, 'saturation', 1, ctx));
|
|
110
|
+
const hueRotate = resolveFilterValue(element, 'hue_rotate', 0, ctx);
|
|
111
|
+
if (blur === 0 && brightness === 1 && contrast === 1 && saturation === 1 && hueRotate === 0)
|
|
112
|
+
return null;
|
|
113
|
+
return { blur, brightness, contrast, saturation, hueRotate };
|
|
114
|
+
}
|
|
115
|
+
/** number | Keyframe[] field, overlaid by keyframe_animations / presets. */
|
|
116
|
+
function resolveFilterValue(element, property, fallback, ctx) {
|
|
117
|
+
const raw = element[property];
|
|
118
|
+
let staticValue = fallback;
|
|
119
|
+
if (typeof raw === 'number' && Number.isFinite(raw)) {
|
|
120
|
+
staticValue = raw;
|
|
121
|
+
}
|
|
122
|
+
else if (Array.isArray(raw)) {
|
|
123
|
+
const elementStart = ctx.timeOffset + numberOrZero(element.time);
|
|
124
|
+
staticValue = interpolateKeyframes(raw, ctx.time - elementStart);
|
|
125
|
+
}
|
|
126
|
+
return applyAnimation(element, property, staticValue, ctx);
|
|
127
|
+
}
|
|
128
|
+
function resolveEffects(element, ctx) {
|
|
129
|
+
const list = element.effects;
|
|
130
|
+
if (!list || list.length === 0)
|
|
131
|
+
return [];
|
|
132
|
+
const elementStart = ctx.timeOffset + numberOrZero(element.time);
|
|
133
|
+
const localTime = ctx.time - elementStart;
|
|
134
|
+
const param = (raw, fallback) => {
|
|
135
|
+
if (typeof raw === 'number' && Number.isFinite(raw))
|
|
136
|
+
return raw;
|
|
137
|
+
if (Array.isArray(raw))
|
|
138
|
+
return interpolateKeyframes(raw, localTime);
|
|
139
|
+
return fallback;
|
|
140
|
+
};
|
|
141
|
+
const out = [];
|
|
142
|
+
for (const fx of list) {
|
|
143
|
+
switch (fx.type) {
|
|
144
|
+
case 'pixelate':
|
|
145
|
+
out.push({ kind: 'stylize', mode: 'pixelate', p0: Math.max(1, param(fx.cell_size, 8)) });
|
|
146
|
+
break;
|
|
147
|
+
case 'dither':
|
|
148
|
+
out.push({
|
|
149
|
+
kind: 'stylize', mode: 'dither',
|
|
150
|
+
p0: Math.max(2, param(fx.levels, 4)),
|
|
151
|
+
p1: Math.max(1, param(fx.pixel_size, 2)), // Bayer cell, logical px
|
|
152
|
+
});
|
|
153
|
+
break;
|
|
154
|
+
case 'halftone':
|
|
155
|
+
out.push({ kind: 'stylize', mode: 'halftone', p0: Math.max(2, param(fx.cell_size, 8)), p1: param(fx.angle, 45) });
|
|
156
|
+
break;
|
|
157
|
+
case 'ascii':
|
|
158
|
+
out.push({ kind: 'stylize', mode: 'ascii', p0: Math.max(4, param(fx.cell_size, 12)) });
|
|
159
|
+
break;
|
|
160
|
+
case 'glow': {
|
|
161
|
+
const c = parseColor(typeof fx.color === 'string' ? fx.color : '#FFFFFF');
|
|
162
|
+
out.push({
|
|
163
|
+
kind: 'stylize', mode: 'glow',
|
|
164
|
+
p0: Math.max(0, param(fx.intensity, 1)),
|
|
165
|
+
auxBlur: Math.max(1, param(fx.radius, 20)),
|
|
166
|
+
tint: [c[0] * c[3], c[1] * c[3], c[2] * c[3], c[3]],
|
|
167
|
+
});
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
case 'drop_shadow': {
|
|
171
|
+
const c = parseColor(typeof fx.color === 'string' ? fx.color : '#000000');
|
|
172
|
+
const op = Math.max(0, Math.min(1, param(fx.opacity, 0.6))) * c[3];
|
|
173
|
+
out.push({
|
|
174
|
+
kind: 'stylize', mode: 'drop_shadow',
|
|
175
|
+
p0: param(fx.offset_x, 0),
|
|
176
|
+
p1: param(fx.offset_y, 12),
|
|
177
|
+
auxBlur: Math.max(0.5, param(fx.blur, 18)),
|
|
178
|
+
tint: [c[0] * op, c[1] * op, c[2] * op, op],
|
|
179
|
+
});
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
case 'stroke': {
|
|
183
|
+
const c = parseColor(typeof fx.color === 'string' ? fx.color : '#FFFFFF');
|
|
184
|
+
out.push({
|
|
185
|
+
kind: 'stylize', mode: 'stroke',
|
|
186
|
+
p0: Math.max(1, param(fx.width, 4)),
|
|
187
|
+
tint: [c[0] * c[3], c[1] * c[3], c[2] * c[3], c[3]],
|
|
188
|
+
});
|
|
189
|
+
break;
|
|
190
|
+
}
|
|
191
|
+
case 'chroma_key': {
|
|
192
|
+
// tint carries the STRAIGHT key color; alpha = spill strength.
|
|
193
|
+
const c = parseColor(typeof fx.color === 'string' ? fx.color : '#00FF00');
|
|
194
|
+
out.push({
|
|
195
|
+
kind: 'stylize', mode: 'chroma_key',
|
|
196
|
+
p0: Math.max(0, param(fx.tolerance, 0.18)),
|
|
197
|
+
p1: Math.max(0, param(fx.softness, 0.1)),
|
|
198
|
+
tint: [c[0], c[1], c[2], Math.max(0, Math.min(1, param(fx.spill, 0.5)))],
|
|
199
|
+
});
|
|
200
|
+
break;
|
|
201
|
+
}
|
|
202
|
+
case 'luma_key':
|
|
203
|
+
out.push({
|
|
204
|
+
kind: 'stylize', mode: 'luma_key',
|
|
205
|
+
p0: Math.max(0, Math.min(1, param(fx.threshold, 0.5))),
|
|
206
|
+
p1: Math.max(0, param(fx.softness, 0.1)),
|
|
207
|
+
tint: [fx.invert === true ? 1 : 0, 0, 0, 0],
|
|
208
|
+
});
|
|
209
|
+
break;
|
|
210
|
+
case 'levels': {
|
|
211
|
+
const clamp01 = (v) => Math.max(0, Math.min(1, v));
|
|
212
|
+
out.push({
|
|
213
|
+
kind: 'stylize', mode: 'levels',
|
|
214
|
+
p0: Math.max(0.01, param(fx.gamma, 1)),
|
|
215
|
+
tint: [
|
|
216
|
+
clamp01(param(fx.in_black, 0)), clamp01(param(fx.in_white, 1)),
|
|
217
|
+
clamp01(param(fx.out_black, 0)), clamp01(param(fx.out_white, 1)),
|
|
218
|
+
],
|
|
219
|
+
});
|
|
220
|
+
break;
|
|
221
|
+
}
|
|
222
|
+
case 'lut': {
|
|
223
|
+
const url = typeof fx.source === 'string' ? fx.source : '';
|
|
224
|
+
const asset = url ? ctx.luts.get(url) : undefined;
|
|
225
|
+
if (!asset) {
|
|
226
|
+
getLogger().warn(`lut not loaded — pass skipped: ${url}`);
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
out.push({
|
|
230
|
+
kind: 'stylize', mode: 'lut',
|
|
231
|
+
p0: asset.size,
|
|
232
|
+
p1: Math.max(0, Math.min(1, param(fx.intensity, 1))),
|
|
233
|
+
lutTex: asset.texture,
|
|
234
|
+
});
|
|
235
|
+
break;
|
|
236
|
+
}
|
|
237
|
+
case 'fractal_noise': {
|
|
238
|
+
// Offsets pass pre-divided by scale (noise units) so the shader
|
|
239
|
+
// needs no pixel-ratio knowledge — see backend.ts param table.
|
|
240
|
+
const scale = Math.max(0.001, param(fx.scale, 100));
|
|
241
|
+
out.push({
|
|
242
|
+
kind: 'stylize', mode: 'fractal_noise',
|
|
243
|
+
p0: scale,
|
|
244
|
+
p1: param(fx.evolution, 0),
|
|
245
|
+
tint: [
|
|
246
|
+
param(fx.offset_x, 0) / scale,
|
|
247
|
+
param(fx.offset_y, 0) / scale,
|
|
248
|
+
Math.max(1, Math.min(8, Math.round(typeof fx.octaves === 'number' ? fx.octaves : 4))),
|
|
249
|
+
Math.max(0, Math.round(typeof fx.seed === 'number' ? fx.seed : 0)),
|
|
250
|
+
],
|
|
251
|
+
});
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
case 'turbulent_displace':
|
|
255
|
+
out.push({
|
|
256
|
+
kind: 'stylize', mode: 'turbulent_displace',
|
|
257
|
+
p0: Math.max(0, param(fx.amount, 16)),
|
|
258
|
+
p1: Math.max(1, param(fx.scale, 120)),
|
|
259
|
+
tint: [
|
|
260
|
+
param(fx.evolution, 0),
|
|
261
|
+
Math.max(1, Math.min(8, Math.round(typeof fx.octaves === 'number' ? fx.octaves : 2))),
|
|
262
|
+
Math.max(0, Math.round(typeof fx.seed === 'number' ? fx.seed : 0)),
|
|
263
|
+
0,
|
|
264
|
+
],
|
|
265
|
+
});
|
|
266
|
+
break;
|
|
267
|
+
case 'glass': {
|
|
268
|
+
// Defaults track the liquidglass reference: CLEAR glass
|
|
269
|
+
// (blur 0), refraction 0.69, chroma 0.05, edge highlight 0.05
|
|
270
|
+
// + full fresnel, shadow 0.3. Our dials map onto theirs:
|
|
271
|
+
// refraction is ÷30 (their unit ≈ 30px of bend), and
|
|
272
|
+
// edge_highlight scales the whole stock light rig (0.35 ≡
|
|
273
|
+
// exactly the reference's default mix).
|
|
274
|
+
const eh = Math.max(0, param(fx.edge_highlight, 0.35));
|
|
275
|
+
out.push({
|
|
276
|
+
kind: 'glass',
|
|
277
|
+
blur: Math.max(0, param(fx.blur_radius, 0)),
|
|
278
|
+
refract: Math.abs(param(fx.refraction, 21)) / 30,
|
|
279
|
+
chroma: Math.max(0, param(fx.dispersion, 0.05)),
|
|
280
|
+
edgeHL: eh * (0.05 / 0.35),
|
|
281
|
+
specular: 0,
|
|
282
|
+
fresnel: eh * (1 / 0.35),
|
|
283
|
+
saturation: Math.max(0, param(fx.backdrop_saturation, 1)) - 1,
|
|
284
|
+
zRadius: Math.max(1, param(fx.edge_width, 40)),
|
|
285
|
+
bevelMode: fx.mode === 'dome' ? 1 : 0,
|
|
286
|
+
shadowAlpha: Math.max(0, param(fx.shadow, 0.3)),
|
|
287
|
+
tint: typeof fx.tint === 'string' ? parseColor(fx.tint) : [0, 0, 0, 0],
|
|
288
|
+
});
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
291
|
+
default:
|
|
292
|
+
// Future effect types: skip with a warning, keep the element.
|
|
293
|
+
getLogger().warn(`unknown effect type — skipped: ${String(fx.type)}`);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return out;
|
|
297
|
+
}
|
|
298
|
+
/** Per-backend ascii glyph atlas (built once from the embedded bitfont). */
|
|
299
|
+
const asciiAtlases = new WeakMap();
|
|
300
|
+
function asciiAtlasFor(backend) {
|
|
301
|
+
let atlas = asciiAtlases.get(backend);
|
|
302
|
+
if (!atlas) {
|
|
303
|
+
atlas = backend.createTexture(buildAsciiAtlasCanvas());
|
|
304
|
+
asciiAtlases.set(backend, atlas);
|
|
305
|
+
}
|
|
306
|
+
return atlas;
|
|
307
|
+
}
|
|
308
|
+
function renderLayeredElement(element, ctx, filter, effects) {
|
|
309
|
+
const { backend } = ctx;
|
|
310
|
+
const sw = ctx.surfaceWidth;
|
|
311
|
+
const sh = ctx.surfaceHeight;
|
|
312
|
+
const keyBase = element.id ?? '__fx__';
|
|
313
|
+
// Render the element with its normal transform into a transparent
|
|
314
|
+
// layer. Its own filter/effect fields are stripped from the inner
|
|
315
|
+
// pass (the chain applies them) and so is blend_mode — blending
|
|
316
|
+
// against an empty layer would lose the backdrop, so the element's
|
|
317
|
+
// blend applies at the final composite instead.
|
|
318
|
+
const inner = {
|
|
319
|
+
...element,
|
|
320
|
+
blur_radius: undefined,
|
|
321
|
+
brightness: undefined,
|
|
322
|
+
contrast: undefined,
|
|
323
|
+
saturation: undefined,
|
|
324
|
+
effects: undefined,
|
|
325
|
+
blend_mode: undefined,
|
|
326
|
+
};
|
|
327
|
+
// Glass (§4.7) AND piecewise blend modes (§4.5) read the BACKDROP —
|
|
328
|
+
// snapshot the surface now, while it holds exactly the pixels drawn
|
|
329
|
+
// before this element, and before any scratch targets are pushed.
|
|
330
|
+
const progMode = isProgrammableBlend(element.blend_mode) ? element.blend_mode : undefined;
|
|
331
|
+
let backdropSnap = null;
|
|
332
|
+
let backdropFlipY = false;
|
|
333
|
+
if (progMode || effects.some((fx) => fx.kind === 'glass')) {
|
|
334
|
+
backdropSnap = acquireFilterTarget(ctx, `${keyBase}::bd`, sw, sh);
|
|
335
|
+
backdropFlipY = backend.copySurfaceTo(backdropSnap.target).flippedY;
|
|
336
|
+
}
|
|
337
|
+
const layerA = acquireFilterTarget(ctx, `${keyBase}::fx`, sw, sh);
|
|
338
|
+
backend.pushTarget(layerA.target, [0, 0, 0, 0]);
|
|
339
|
+
// try/finally so a throw mid-effect-chain can't leave the surface stack
|
|
340
|
+
// unbalanced — otherwise every later element in the frame draws into an
|
|
341
|
+
// orphaned offscreen FBO and the whole frame blackens (EXPORT-FLOW-ISSUES §4A).
|
|
342
|
+
try {
|
|
343
|
+
renderElementByType(inner, ctx);
|
|
344
|
+
}
|
|
345
|
+
finally {
|
|
346
|
+
backend.popTarget();
|
|
347
|
+
}
|
|
348
|
+
const cx = sw / 2;
|
|
349
|
+
const cy = sh / 2;
|
|
350
|
+
const passes = [];
|
|
351
|
+
if (filter) {
|
|
352
|
+
passes.push((src, blend) => {
|
|
353
|
+
const blurred = filter.blur > 0
|
|
354
|
+
? blurLadder(ctx, `${keyBase}::blur`, src, filter.blur, sw, sh)
|
|
355
|
+
: src;
|
|
356
|
+
backend.drawFilteredQuad({
|
|
357
|
+
cx, cy, width: sw, height: sh, texture: blurred,
|
|
358
|
+
blurRadius: 0, blurDir: [0, 1],
|
|
359
|
+
brightness: filter.brightness, contrast: filter.contrast,
|
|
360
|
+
saturation: filter.saturation, hueRotate: filter.hueRotate, blend,
|
|
361
|
+
});
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
for (const fx of effects) {
|
|
365
|
+
if (fx.kind === 'stylize') {
|
|
366
|
+
passes.push((src, blend) => backend.drawStylizedQuad({
|
|
367
|
+
cx, cy, width: sw, height: sh, texture: src,
|
|
368
|
+
mode: fx.mode, p0: fx.p0, p1: fx.p1,
|
|
369
|
+
aux: fx.mode === 'ascii'
|
|
370
|
+
? asciiAtlasFor(backend)
|
|
371
|
+
: fx.lutTex
|
|
372
|
+
? fx.lutTex
|
|
373
|
+
: fx.auxBlur
|
|
374
|
+
? blurLadder(ctx, `${keyBase}::style`, src, fx.auxBlur, sw, sh)
|
|
375
|
+
: undefined,
|
|
376
|
+
tint: fx.tint,
|
|
377
|
+
blend,
|
|
378
|
+
}));
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
// Glass (§4.7) — analytic: the pane geometry comes from the shape
|
|
382
|
+
// element itself (rounded-rect SDF in-shader, the liquidglass
|
|
383
|
+
// reference model). Non-shape elements skip the effect — deriving
|
|
384
|
+
// lens geometry from rasterized alpha produces rim artifacts.
|
|
385
|
+
passes.push((src, blend) => {
|
|
386
|
+
if (element.type !== 'shape') {
|
|
387
|
+
getLogger().warn('glass applies to shape elements — effect skipped');
|
|
388
|
+
backend.drawTexturedQuad({
|
|
389
|
+
cx, cy, width: sw, height: sh, rotation: 0, texture: src, blend,
|
|
390
|
+
});
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
const snap = backdropSnap;
|
|
394
|
+
const backdropTex = fx.blur > 0
|
|
395
|
+
? blurLadder(ctx, `${keyBase}::gfrost`, snap.target.texture, fx.blur, sw, sh)
|
|
396
|
+
: snap.target.texture;
|
|
397
|
+
const pane = resolvePaneBox(element, ctx);
|
|
398
|
+
backend.drawGlassQuad({
|
|
399
|
+
cx, cy, width: sw, height: sh,
|
|
400
|
+
backdrop: backdropTex,
|
|
401
|
+
backdropSharp: snap.target.texture,
|
|
402
|
+
backdropFlipY,
|
|
403
|
+
paneCx: pane.cx, paneCy: pane.cy,
|
|
404
|
+
paneHalfW: pane.width / 2, paneHalfH: pane.height / 2,
|
|
405
|
+
cornerRadius: pane.radius, rotation: pane.rotation,
|
|
406
|
+
paneHomography: pane.paneH,
|
|
407
|
+
zRadius: fx.zRadius,
|
|
408
|
+
refract: fx.refract, chroma: fx.chroma,
|
|
409
|
+
edgeHighlight: fx.edgeHL, specular: fx.specular, fresnel: fx.fresnel,
|
|
410
|
+
saturation: fx.saturation, tint: fx.tint, alpha: pane.alpha,
|
|
411
|
+
bevelMode: fx.bevelMode,
|
|
412
|
+
shadowAlpha: fx.shadowAlpha, shadowSpread: 10, shadowOffY: 1,
|
|
413
|
+
blend,
|
|
414
|
+
});
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
let src = layerA.target.texture;
|
|
419
|
+
let useA = false; // layerA holds the content; first intermediate goes to B
|
|
420
|
+
// Piecewise blend: every pass renders into an offscreen target (never
|
|
421
|
+
// straight to the surface), then the final texture is composited
|
|
422
|
+
// against the backdrop snapshot via drawBackdropBlend. With no
|
|
423
|
+
// filter/effect passes, the isolated layer itself is the result.
|
|
424
|
+
if (progMode) {
|
|
425
|
+
for (let i = 0; i < passes.length; i++) {
|
|
426
|
+
const dst = useA ? layerA : acquireFilterTarget(ctx, `${keyBase}::fx-scratch`, sw, sh);
|
|
427
|
+
backend.pushTarget(dst.target, [0, 0, 0, 0]);
|
|
428
|
+
try {
|
|
429
|
+
passes[i](src, undefined);
|
|
430
|
+
}
|
|
431
|
+
finally {
|
|
432
|
+
backend.popTarget();
|
|
433
|
+
}
|
|
434
|
+
src = dst.target.texture;
|
|
435
|
+
useA = !useA;
|
|
436
|
+
}
|
|
437
|
+
backend.drawBackdropBlend({
|
|
438
|
+
src,
|
|
439
|
+
backdrop: backdropSnap.target.texture,
|
|
440
|
+
mode: progMode,
|
|
441
|
+
width: sw,
|
|
442
|
+
height: sh,
|
|
443
|
+
backdropFlipY,
|
|
444
|
+
});
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
for (let i = 0; i < passes.length; i++) {
|
|
448
|
+
const last = i === passes.length - 1;
|
|
449
|
+
if (last) {
|
|
450
|
+
passes[i](src, element.blend_mode);
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
const dst = useA ? layerA : acquireFilterTarget(ctx, `${keyBase}::fx-scratch`, sw, sh);
|
|
454
|
+
backend.pushTarget(dst.target, [0, 0, 0, 0]);
|
|
455
|
+
try {
|
|
456
|
+
passes[i](src, undefined);
|
|
457
|
+
}
|
|
458
|
+
finally {
|
|
459
|
+
backend.popTarget();
|
|
460
|
+
}
|
|
461
|
+
src = dst.target.texture;
|
|
462
|
+
useA = !useA;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* Gaussian blur via a downsample ladder (normative — PROTOCOL.md §4.6).
|
|
468
|
+
*
|
|
469
|
+
* The 25-tap kernel spaces its taps σ/4 apart; at full resolution a
|
|
470
|
+
* large σ means taps 4+ px apart, and blurring a hard edge with taps
|
|
471
|
+
* that sparse leaves a faint staircase — H × V staircases cross into a
|
|
472
|
+
* visible grid of σ/4-sized squares (Ian spotted it in the glass
|
|
473
|
+
* frost). So: halve the image (each halving is a clean bilinear 2×2
|
|
474
|
+
* average) until the residual σ/f ≤ 4 — taps ≤ 1px apart — blur there,
|
|
475
|
+
* and let the consuming draw's bilinear sampling upsample smoothly.
|
|
476
|
+
* Also much cheaper: the heavy taps run on 1/f² of the pixels.
|
|
477
|
+
*
|
|
478
|
+
* Returns a texture LOGICALLY sized sw/f × sh/f; consumers sample by
|
|
479
|
+
* normalized UV so the size difference is invisible to them.
|
|
480
|
+
*/
|
|
481
|
+
function blurLadder(ctx, keyPrefix, src, sigma, sw, sh) {
|
|
482
|
+
const { backend } = ctx;
|
|
483
|
+
if (sigma <= 0)
|
|
484
|
+
return src;
|
|
485
|
+
let f = 1;
|
|
486
|
+
while (sigma / f > 4 && f < 16)
|
|
487
|
+
f *= 2;
|
|
488
|
+
let cur = src;
|
|
489
|
+
let w = sw;
|
|
490
|
+
let h = sh;
|
|
491
|
+
for (let level = 1; level < f; level *= 2) {
|
|
492
|
+
const nw = Math.max(1, Math.round(w / 2));
|
|
493
|
+
const nh = Math.max(1, Math.round(h / 2));
|
|
494
|
+
const t = acquireFilterTarget(ctx, `${keyPrefix}::ds${level}`, nw, nh);
|
|
495
|
+
backend.pushTarget(t.target, [0, 0, 0, 0]);
|
|
496
|
+
try {
|
|
497
|
+
backend.drawTexturedQuad({
|
|
498
|
+
cx: nw / 2, cy: nh / 2, width: nw, height: nh, rotation: 0, texture: cur,
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
finally {
|
|
502
|
+
backend.popTarget();
|
|
503
|
+
}
|
|
504
|
+
cur = t.target.texture;
|
|
505
|
+
w = nw;
|
|
506
|
+
h = nh;
|
|
507
|
+
}
|
|
508
|
+
const s = sigma / f;
|
|
509
|
+
const th = acquireFilterTarget(ctx, `${keyPrefix}::bh`, w, h);
|
|
510
|
+
backend.pushTarget(th.target, [0, 0, 0, 0]);
|
|
511
|
+
try {
|
|
512
|
+
backend.drawFilteredQuad({
|
|
513
|
+
cx: w / 2, cy: h / 2, width: w, height: h, texture: cur,
|
|
514
|
+
blurRadius: s, blurDir: [1, 0], brightness: 1, contrast: 1, saturation: 1,
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
finally {
|
|
518
|
+
backend.popTarget();
|
|
519
|
+
}
|
|
520
|
+
const tv = acquireFilterTarget(ctx, `${keyPrefix}::bv`, w, h);
|
|
521
|
+
backend.pushTarget(tv.target, [0, 0, 0, 0]);
|
|
522
|
+
try {
|
|
523
|
+
backend.drawFilteredQuad({
|
|
524
|
+
cx: w / 2, cy: h / 2, width: w, height: h, texture: th.target.texture,
|
|
525
|
+
blurRadius: s, blurDir: [0, 1], brightness: 1, contrast: 1, saturation: 1,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
finally {
|
|
529
|
+
backend.popTarget();
|
|
530
|
+
}
|
|
531
|
+
return tv.target.texture;
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* The glass pane's box in SURFACE coordinates — the same resolution
|
|
535
|
+
* path the shape renderer uses (animations, anchors, aspect ratio,
|
|
536
|
+
* scale pair, group model transform), so the SDF in the glass shader
|
|
537
|
+
* lands exactly on the shape's footprint.
|
|
538
|
+
*/
|
|
539
|
+
function resolvePaneBox(element, ctx) {
|
|
540
|
+
const { canvas } = ctx;
|
|
541
|
+
const x = applyAnimation(element, 'x', resolveLength(element.x, canvas.width, canvas), ctx);
|
|
542
|
+
const y = applyAnimation(element, 'y', resolveLength(element.y, canvas.height, canvas), ctx);
|
|
543
|
+
const { sx, sy } = resolveScalePair(element, ctx);
|
|
544
|
+
const box = applyAspectRatio(element, applyAnimation(element, 'width', resolveLength(element.width, canvas.width, canvas, 100), ctx), applyAnimation(element, 'height', resolveLength(element.height, canvas.height, canvas, 100), ctx));
|
|
545
|
+
const width = sx * box.width;
|
|
546
|
+
const height = sy * box.height;
|
|
547
|
+
const rotation = applyAnimation(element, 'rotation', numberOrZero(element.rotation ?? element.z_rotation), ctx);
|
|
548
|
+
const opacity01 = applyAnimation(element, 'opacity', typeof element.opacity === 'number' ? element.opacity : 1, ctx);
|
|
549
|
+
const xA = resolveAnchor(element.x_anchor);
|
|
550
|
+
const yA = resolveAnchor(element.y_anchor);
|
|
551
|
+
const { cx, cy } = anchorToCenter(x, y, width, height, xA, yA);
|
|
552
|
+
const isEllipse = String((element.shape ?? 'rectangle')).toLowerCase() === 'ellipse';
|
|
553
|
+
// CKP/1.0 glass under 3D (§4.7): own 3D fields or a non-affine chain
|
|
554
|
+
// put the pane on the projective path — pane geometry stays LOCAL and
|
|
555
|
+
// the homography (the plane restriction of the full matrix chain)
|
|
556
|
+
// carries every projection. The shader inverts it per fragment.
|
|
557
|
+
const t3d = resolve3D(element, ctx);
|
|
558
|
+
if (t3d !== null || !ctx.modelMatrix.aff) {
|
|
559
|
+
// quadMatrix3D with w = h = 2 is T(cx,cy,z)·Rz·Ry·Rx·F (unit scale);
|
|
560
|
+
// dropping the local-z row/col and un-flipping Y (pane-local is
|
|
561
|
+
// y-down) leaves the plane's 3×3 homography.
|
|
562
|
+
const m = mat4Multiply(ctx.modelMatrix, {
|
|
563
|
+
e: quadMatrix3D(cx, cy, 2, 2, rotation, 0, 0, t3d?.xRot ?? 0, t3d?.yRot ?? 0, t3d?.z ?? 0),
|
|
564
|
+
aff: false,
|
|
565
|
+
}).e;
|
|
566
|
+
const paneH = [m[0], m[1], m[3], -m[4], -m[5], -m[7], m[12], m[13], m[15]];
|
|
567
|
+
const radius = isEllipse ? Math.min(width, height) / 2 : numberOrZero(element.border_radius);
|
|
568
|
+
return {
|
|
569
|
+
cx: 0, cy: 0, width, height, rotation: 0, radius, paneH,
|
|
570
|
+
alpha: Math.max(0, Math.min(1, opacity01 * ctx.opacityFactor)),
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
const w = applyModelTransform(ctx.modelMatrix, ctx.opacityFactor, cx, cy, rotation, opacity01, width, height);
|
|
574
|
+
// An ellipse renders via the rounded-rect SDF with r = min(half) —
|
|
575
|
+
// a circle when square, a stadium otherwise (documented in §4.7).
|
|
576
|
+
const radius = isEllipse
|
|
577
|
+
? Math.min(w.width, w.height) / 2
|
|
578
|
+
: numberOrZero(element.border_radius);
|
|
579
|
+
return {
|
|
580
|
+
cx: w.cx, cy: w.cy, width: w.width, height: w.height,
|
|
581
|
+
rotation: w.rotation, radius,
|
|
582
|
+
alpha: Math.max(0, Math.min(1, w.opacity01)),
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
function acquireFilterTarget(ctx, key, width, height) {
|
|
586
|
+
let entry = ctx.groupTargets.get(key);
|
|
587
|
+
if (entry && (entry.width !== width || entry.height !== height)) {
|
|
588
|
+
ctx.backend.destroyRenderTarget(entry.target);
|
|
589
|
+
entry = undefined;
|
|
590
|
+
}
|
|
591
|
+
if (!entry) {
|
|
592
|
+
entry = { target: ctx.backend.createRenderTarget(width, height), width, height };
|
|
593
|
+
ctx.groupTargets.set(key, entry);
|
|
594
|
+
}
|
|
595
|
+
// Stamp on EVERY acquire (incl. the reuse path above, where `set` is not
|
|
596
|
+
// called) so frame-boundary LRU eviction sees this entry as touched this frame.
|
|
597
|
+
entry.lastTouched = ctx.frameIndex;
|
|
598
|
+
return entry;
|
|
599
|
+
}
|
|
600
|
+
function renderElementByType(element, ctx) {
|
|
601
|
+
switch (element.type) {
|
|
602
|
+
case 'shape':
|
|
603
|
+
// A shape is a primitive (SDF) unless it carries `paths` (vector form).
|
|
604
|
+
if (element.paths)
|
|
605
|
+
renderPathShape(element, ctx);
|
|
606
|
+
else
|
|
607
|
+
renderShapeElement(element, ctx);
|
|
608
|
+
return;
|
|
609
|
+
case 'text':
|
|
610
|
+
renderTextElement(element, ctx);
|
|
611
|
+
return;
|
|
612
|
+
case 'image':
|
|
613
|
+
renderImageElement(element, ctx);
|
|
614
|
+
return;
|
|
615
|
+
case 'video':
|
|
616
|
+
renderVideoElement(element, ctx);
|
|
617
|
+
return;
|
|
618
|
+
case 'audio':
|
|
619
|
+
// Audio elements don't render to the visual frame. Phase 2b.
|
|
620
|
+
return;
|
|
621
|
+
case 'caption':
|
|
622
|
+
renderCaptionElement(element, ctx);
|
|
623
|
+
return;
|
|
624
|
+
case 'particles':
|
|
625
|
+
renderParticlesElement(element, ctx);
|
|
626
|
+
return;
|
|
627
|
+
case 'group':
|
|
628
|
+
renderGroupElement(element, ctx);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
function isActiveAt(element, time, sourceDuration) {
|
|
633
|
+
const start = numberOrZero(element.time);
|
|
634
|
+
const elDur = parseDuration(element.duration, sourceDuration - start);
|
|
635
|
+
const end = start + elDur;
|
|
636
|
+
return time >= start && time <= end;
|
|
637
|
+
}
|
|
638
|
+
function numberOrZero(v) {
|
|
639
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
640
|
+
return v;
|
|
641
|
+
if (typeof v === 'string') {
|
|
642
|
+
const n = parseFloat(v);
|
|
643
|
+
return Number.isFinite(n) ? n : 0;
|
|
644
|
+
}
|
|
645
|
+
return 0;
|
|
646
|
+
}
|
|
647
|
+
function parseDuration(v, fallback) {
|
|
648
|
+
if (v === 'auto' || v === 'end' || v == null)
|
|
649
|
+
return fallback;
|
|
650
|
+
if (typeof v === 'number' && Number.isFinite(v))
|
|
651
|
+
return v;
|
|
652
|
+
if (typeof v === 'string') {
|
|
653
|
+
const n = parseFloat(v);
|
|
654
|
+
return Number.isFinite(n) ? n : fallback;
|
|
655
|
+
}
|
|
656
|
+
return fallback;
|
|
657
|
+
}
|
|
658
|
+
//# sourceMappingURL=scene.js.map
|