@dnyantra/dtly 0.0.0-stage → 0.2.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.
Files changed (76) hide show
  1. package/LICENSE +26 -0
  2. package/README.md +172 -2
  3. package/dist/cli.d.ts +2 -0
  4. package/dist/cli.js +95 -0
  5. package/dist/cli.js.map +1 -0
  6. package/dist/core/anim.d.ts +3 -0
  7. package/dist/core/anim.js +61 -0
  8. package/dist/core/anim.js.map +1 -0
  9. package/dist/core/color.d.ts +4 -0
  10. package/dist/core/color.js +28 -0
  11. package/dist/core/color.js.map +1 -0
  12. package/dist/core/ease.d.ts +8 -0
  13. package/dist/core/ease.js +90 -0
  14. package/dist/core/ease.js.map +1 -0
  15. package/dist/core/model.d.ts +229 -0
  16. package/dist/core/model.js +4 -0
  17. package/dist/core/model.js.map +1 -0
  18. package/dist/core/text.d.ts +41 -0
  19. package/dist/core/text.js +222 -0
  20. package/dist/core/text.js.map +1 -0
  21. package/dist/index.d.ts +10 -0
  22. package/dist/index.js +13 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/mcp/index.d.ts +1 -0
  25. package/dist/mcp/index.js +2 -0
  26. package/dist/mcp/index.js.map +1 -0
  27. package/dist/mcp/server.d.ts +3 -0
  28. package/dist/mcp/server.js +193 -0
  29. package/dist/mcp/server.js.map +1 -0
  30. package/dist/node.d.ts +4 -0
  31. package/dist/node.js +6 -0
  32. package/dist/node.js.map +1 -0
  33. package/dist/ops/ops.d.ts +39 -0
  34. package/dist/ops/ops.js +259 -0
  35. package/dist/ops/ops.js.map +1 -0
  36. package/dist/ops/store.d.ts +4 -0
  37. package/dist/ops/store.js +22 -0
  38. package/dist/ops/store.js.map +1 -0
  39. package/dist/presets/index.d.ts +14 -0
  40. package/dist/presets/index.js +235 -0
  41. package/dist/presets/index.js.map +1 -0
  42. package/dist/presets/text.d.ts +5 -0
  43. package/dist/presets/text.js +19 -0
  44. package/dist/presets/text.js.map +1 -0
  45. package/dist/render/export.d.ts +43 -0
  46. package/dist/render/export.js +215 -0
  47. package/dist/render/export.js.map +1 -0
  48. package/dist/render/ffmpeg.d.ts +1 -0
  49. package/dist/render/ffmpeg.js +16 -0
  50. package/dist/render/ffmpeg.js.map +1 -0
  51. package/dist/render/node-host.d.ts +18 -0
  52. package/dist/render/node-host.js +168 -0
  53. package/dist/render/node-host.js.map +1 -0
  54. package/dist/render/renderer.d.ts +36 -0
  55. package/dist/render/renderer.js +372 -0
  56. package/dist/render/renderer.js.map +1 -0
  57. package/dist/starter.d.ts +5 -0
  58. package/dist/starter.js +32 -0
  59. package/dist/starter.js.map +1 -0
  60. package/dist/studio/server.d.ts +12 -0
  61. package/dist/studio/server.js +190 -0
  62. package/dist/studio/server.js.map +1 -0
  63. package/dist/studio-app/app.css +2 -0
  64. package/dist/studio-app/app.css.map +7 -0
  65. package/dist/studio-app/app.js +5 -0
  66. package/dist/studio-app/app.js.map +7 -0
  67. package/dist/studio-app/index.html +15 -0
  68. package/dist/version.d.ts +2 -0
  69. package/dist/version.js +3 -0
  70. package/dist/version.js.map +1 -0
  71. package/fonts/Baloo2.ttf +0 -0
  72. package/fonts/Mukta-Bold.ttf +0 -0
  73. package/fonts/OFL-Baloo2.txt +93 -0
  74. package/fonts/OFL-Mukta.txt +93 -0
  75. package/package.json +84 -4
  76. package/plugin/skills/dtly/SKILL.md +94 -0
@@ -0,0 +1,16 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ let checked = false;
3
+ // FFmpeg is required but not bundled (common static builds are GPL); fail early with a clear fix.
4
+ export const checkFfmpeg = () => {
5
+ if (checked)
6
+ return;
7
+ const r = spawnSync('ffmpeg', ['-version'], { encoding: 'utf8' });
8
+ if (r.error || r.status !== 0) {
9
+ throw new Error('DT.ly needs FFmpeg on your PATH to read video and write MP4.\n' +
10
+ ' macOS: brew install ffmpeg\n' +
11
+ ' Ubuntu: sudo apt install ffmpeg\n' +
12
+ ' Windows: winget install ffmpeg');
13
+ }
14
+ checked = true;
15
+ };
16
+ //# sourceMappingURL=ffmpeg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/render/ffmpeg.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,SAAS,EAAC,MAAM,oBAAoB,CAAC;AAE7C,IAAI,OAAO,GAAG,KAAK,CAAC;AAEpB,kGAAkG;AAClG,MAAM,CAAC,MAAM,WAAW,GAAG,GAAG,EAAE;IAC9B,IAAI,OAAO;QAAE,OAAO;IACpB,MAAM,CAAC,GAAG,SAAS,CAAC,QAAQ,EAAE,CAAC,UAAU,CAAC,EAAE,EAAC,QAAQ,EAAE,MAAM,EAAC,CAAC,CAAC;IAChE,IAAI,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,MAAM,IAAI,KAAK,CACb,gEAAgE;YAC9D,kCAAkC;YAClC,sCAAsC;YACtC,kCAAkC,CACrC,CAAC;IACJ,CAAC;IACD,OAAO,GAAG,IAAI,CAAC;AACjB,CAAC,CAAC"}
@@ -0,0 +1,18 @@
1
+ import { Canvas } from 'skia-canvas';
2
+ import { type Effect, type Project, type VideoLayer } from '../core/model.js';
3
+ import type { CanvasLike, Drawable, Host } from './renderer.js';
4
+ export declare const newCanvas: (w: number, h: number) => Canvas;
5
+ export declare const registerFonts: (project: Project, baseDir: string) => void;
6
+ export declare class NodeHost implements Host {
7
+ private baseDir;
8
+ defaultFont: string;
9
+ private images;
10
+ private readers;
11
+ constructor(baseDir: string);
12
+ resolve(src: string): string;
13
+ createCanvas(w: number, h: number): CanvasLike;
14
+ image(src: string): Promise<Drawable>;
15
+ videoFrame(layer: VideoLayer, sourceTime: number, w: number, h: number, fps: number): Promise<Drawable>;
16
+ bakedVideoEffects(layer: VideoLayer): Set<Effect>;
17
+ close(): void;
18
+ }
@@ -0,0 +1,168 @@
1
+ // Node host: skia-canvas for drawing, FFmpeg for decoding video frames.
2
+ import { spawn } from 'node:child_process';
3
+ import path from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { Canvas, FontLibrary, ImageData, loadImage } from 'skia-canvas';
6
+ import { isAnimated } from '../core/model.js';
7
+ // CPU rendering by default: deterministic, and the GPU backend can drop draws when a shader
8
+ // compile times out (seen with JPEG/YUV images on Metal). DTLY_GPU=1 opts back in.
9
+ export const newCanvas = (w, h) => {
10
+ const c = new Canvas(w, h);
11
+ c.gpu = process.env.DTLY_GPU === '1';
12
+ return c;
13
+ };
14
+ const BUNDLED_FONTS = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '../../fonts');
15
+ let bundledLoaded = false;
16
+ export const registerFonts = (project, baseDir) => {
17
+ if (!bundledLoaded) {
18
+ FontLibrary.use('Baloo 2', [path.join(BUNDLED_FONTS, 'Baloo2.ttf')]);
19
+ FontLibrary.use('Mukta', [path.join(BUNDLED_FONTS, 'Mukta-Bold.ttf')]);
20
+ bundledLoaded = true;
21
+ }
22
+ for (const [family, files] of Object.entries(project.fonts ?? {})) {
23
+ FontLibrary.use(family, files.map((f) => path.resolve(baseDir, f)));
24
+ }
25
+ };
26
+ // Streams frames sequentially from one FFmpeg process; seeks by restarting when needed.
27
+ class VideoReader {
28
+ file;
29
+ w;
30
+ h;
31
+ fit;
32
+ rate;
33
+ extraFilters;
34
+ proc;
35
+ chunks = [];
36
+ pending = 0; // bytes held in `chunks`
37
+ frames = [];
38
+ nextIndex = 0; // index (relative to start) of frames[0]
39
+ start = 0;
40
+ ended = false;
41
+ waiters = [];
42
+ canvas;
43
+ lastIndex = -1;
44
+ frameBytes;
45
+ constructor(file, w, h, fit, rate, extraFilters = []) {
46
+ this.file = file;
47
+ this.w = w;
48
+ this.h = h;
49
+ this.fit = fit;
50
+ this.rate = rate;
51
+ this.extraFilters = extraFilters;
52
+ this.frameBytes = w * h * 4;
53
+ this.canvas = newCanvas(w, h);
54
+ }
55
+ open(startSeconds) {
56
+ this.proc?.kill('SIGKILL');
57
+ this.chunks = [];
58
+ this.pending = 0;
59
+ this.frames = [];
60
+ this.nextIndex = 0;
61
+ this.ended = false;
62
+ this.start = startSeconds;
63
+ const { w, h, fit } = this;
64
+ const scale = fit === 'fill' ? `scale=${w}:${h}`
65
+ : fit === 'contain' ? `scale=${w}:${h}:force_original_aspect_ratio=decrease,pad=${w}:${h}:(ow-iw)/2:(oh-ih)/2:color=black@0`
66
+ : `scale=${w}:${h}:force_original_aspect_ratio=increase,crop=${w}:${h}`;
67
+ this.proc = spawn('ffmpeg', ['-v', 'error', '-ss', String(Math.max(0, startSeconds)), '-i', this.file, '-vf', [`fps=${this.rate}`, scale, ...this.extraFilters, 'format=rgba'].join(','), '-f', 'rawvideo', '-pix_fmt', 'rgba', '-']);
68
+ const proc = this.proc;
69
+ proc.stdout.on('data', (chunk) => {
70
+ if (proc !== this.proc)
71
+ return;
72
+ // Gather chunks and copy once per frame (concatenating per chunk is quadratic at 8 MB/frame).
73
+ this.chunks.push(chunk);
74
+ this.pending += chunk.length;
75
+ while (this.pending >= this.frameBytes) {
76
+ const all = this.chunks.length === 1 ? this.chunks[0] : Buffer.concat(this.chunks, this.pending);
77
+ this.frames.push(all.subarray(0, this.frameBytes));
78
+ const rest = all.subarray(this.frameBytes);
79
+ this.chunks = rest.length ? [rest] : [];
80
+ this.pending = rest.length;
81
+ }
82
+ if (this.frames.length > 8)
83
+ proc.stdout.pause();
84
+ this.wake();
85
+ });
86
+ proc.stdout.on('end', () => { if (proc === this.proc) {
87
+ this.ended = true;
88
+ this.wake();
89
+ } });
90
+ proc.on('error', () => { this.ended = true; this.wake(); });
91
+ }
92
+ wake() { const w = this.waiters; this.waiters = []; w.forEach((f) => f()); }
93
+ async frame(sourceTime) {
94
+ const want = Math.max(0, Math.round(sourceTime * this.rate));
95
+ if (want === this.lastIndex)
96
+ return this.canvas;
97
+ const rel = want - Math.round(this.start * this.rate);
98
+ if (!this.proc || rel < this.nextIndex || rel - this.nextIndex > this.rate * 3)
99
+ this.open(want / this.rate);
100
+ const target = want - Math.round(this.start * this.rate);
101
+ while (true) {
102
+ while (this.frames.length && this.nextIndex < target) {
103
+ this.frames.shift();
104
+ this.nextIndex++;
105
+ }
106
+ if (this.frames.length && this.nextIndex === target)
107
+ break;
108
+ if (this.ended)
109
+ break; // past the end of the source: hold the last frame
110
+ this.proc.stdout.resume();
111
+ await new Promise((r) => this.waiters.push(r));
112
+ }
113
+ const data = this.frames[0];
114
+ if (data) {
115
+ const ctx = this.canvas.getContext('2d');
116
+ // skia-canvas records draw commands; clearing first stops full-frame puts piling up each frame.
117
+ ctx.clearRect(0, 0, this.w, this.h);
118
+ ctx.putImageData(new ImageData(new Uint8ClampedArray(data.buffer, data.byteOffset, data.length), this.w, this.h), 0, 0);
119
+ this.lastIndex = want;
120
+ }
121
+ return this.canvas;
122
+ }
123
+ close() { this.proc?.kill('SIGKILL'); }
124
+ }
125
+ export class NodeHost {
126
+ baseDir;
127
+ defaultFont = 'Baloo 2';
128
+ images = new Map();
129
+ readers = new Map();
130
+ constructor(baseDir) {
131
+ this.baseDir = baseDir;
132
+ }
133
+ resolve(src) { return path.resolve(this.baseDir, src); }
134
+ createCanvas(w, h) { return newCanvas(w, h); }
135
+ image(src) {
136
+ if (!this.images.has(src))
137
+ this.images.set(src, loadImage(this.resolve(src)));
138
+ return this.images.get(src);
139
+ }
140
+ videoFrame(layer, sourceTime, w, h, fps) {
141
+ const rate = fps / (layer.speed ?? 1);
142
+ const key = [layer.id, layer.src, w, h, layer.fit, rate].join('|');
143
+ let r = this.readers.get(key);
144
+ if (!r)
145
+ this.readers.set(key, (r = new VideoReader(this.resolve(layer.src), w, h, layer.fit ?? 'cover', rate, [...this.bakedVideoEffects(layer)].map(ffmpegFilter))));
146
+ return r.frame(sourceTime);
147
+ }
148
+ // Static colour/blur effects on video run inside FFmpeg, in parallel with drawing.
149
+ bakedVideoEffects(layer) {
150
+ return new Set((layer.effects ?? []).filter((e) => BAKEABLE.has(e.type) && !isAnimated(e.radius ?? e.amount)));
151
+ }
152
+ close() { this.readers.forEach((r) => r.close()); this.readers.clear(); }
153
+ }
154
+ const BAKEABLE = new Set(['blur', 'brightness', 'saturate', 'contrast']);
155
+ // FFmpeg equivalents of the CSS filters the renderer would otherwise apply.
156
+ const ffmpegFilter = (e) => {
157
+ switch (e.type) {
158
+ case 'blur': return `gblur=sigma=${Number(e.radius)}`;
159
+ case 'brightness': {
160
+ const k = Number(e.amount) / 100;
161
+ return `colorchannelmixer=rr=${k}:gg=${k}:bb=${k}`;
162
+ }
163
+ case 'saturate': return `eq=saturation=${Number(e.amount) / 100}`;
164
+ case 'contrast': return `eq=contrast=${Number(e.amount) / 100}`;
165
+ default: return 'null';
166
+ }
167
+ };
168
+ //# sourceMappingURL=node-host.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"node-host.js","sourceRoot":"","sources":["../../src/render/node-host.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,36 @@
1
+ import type { Composition, Effect, Layer, Project, Vec2, VideoLayer } from '../core/model.js';
2
+ type Ctx = CanvasRenderingContext2D;
3
+ export interface CanvasLike {
4
+ width: number;
5
+ height: number;
6
+ getContext(type: '2d'): Ctx;
7
+ }
8
+ export interface Drawable {
9
+ width: number;
10
+ height: number;
11
+ }
12
+ export interface Host {
13
+ createCanvas(w: number, h: number): CanvasLike;
14
+ image(src: string): Promise<Drawable>;
15
+ videoFrame(layer: VideoLayer, sourceTime: number, w: number, h: number, fps: number): Promise<Drawable>;
16
+ bakedVideoEffects?(layer: VideoLayer): Set<Effect>;
17
+ defaultFont: string;
18
+ }
19
+ type M = [number, number, number, number, number, number];
20
+ export declare class Renderer {
21
+ project: Project;
22
+ host: Host;
23
+ private pool;
24
+ constructor(project: Project, host: Host);
25
+ comp(id: string): Composition;
26
+ private acquire;
27
+ private release;
28
+ layerSize(layer: Layer, comp: Composition, t: number): Vec2;
29
+ matrix(layer: Layer, comp: Composition, t: number, depth?: number): M;
30
+ renderComp(ctx: Ctx, comp: Composition, t: number, withBackground?: boolean): Promise<void>;
31
+ isActive(layer: Layer, comp: Composition, t: number): boolean;
32
+ renderLayer(target: Ctx, layer: Layer, comp: Composition, t: number): Promise<void>;
33
+ private applyShadow;
34
+ private drawContent;
35
+ }
36
+ export {};
@@ -0,0 +1,372 @@
1
+ // Environment-agnostic compositor: draws a composition at time t onto any Canvas2D context.
2
+ // The same code runs in Node (skia-canvas) for export and in the browser for preview.
3
+ import { val } from '../core/anim.js';
4
+ import { parseColor, rgbaString } from '../core/color.js';
5
+ import { drawText } from '../core/text.js';
6
+ const I = [1, 0, 0, 1, 0, 0];
7
+ const mul = (m, n) => [
8
+ m[0] * n[0] + m[2] * n[1], m[1] * n[0] + m[3] * n[1],
9
+ m[0] * n[2] + m[2] * n[3], m[1] * n[2] + m[3] * n[3],
10
+ m[0] * n[4] + m[2] * n[5] + m[4], m[1] * n[4] + m[3] * n[5] + m[5],
11
+ ];
12
+ const BLEND = {
13
+ normal: 'source-over', multiply: 'multiply', screen: 'screen', overlay: 'overlay', add: 'lighter',
14
+ darken: 'darken', lighten: 'lighten', softLight: 'soft-light', difference: 'difference',
15
+ };
16
+ export class Renderer {
17
+ project;
18
+ host;
19
+ pool = new Map();
20
+ constructor(project, host) {
21
+ this.project = project;
22
+ this.host = host;
23
+ }
24
+ comp(id) {
25
+ const c = this.project.comps.find((c) => c.id === id);
26
+ if (!c)
27
+ throw new Error(`Unknown composition "${id}"`);
28
+ return c;
29
+ }
30
+ acquire(w, h) {
31
+ const list = this.pool.get(`${w}x${h}`);
32
+ const c = list?.pop() ?? this.host.createCanvas(w, h);
33
+ const ctx = c.getContext('2d');
34
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
35
+ ctx.clearRect(0, 0, w, h);
36
+ return c;
37
+ }
38
+ release(c) {
39
+ const k = `${c.width}x${c.height}`;
40
+ if (!this.pool.has(k))
41
+ this.pool.set(k, []);
42
+ this.pool.get(k).push(c);
43
+ }
44
+ // Natural size of a layer's content in layer space (used for default anchor points).
45
+ layerSize(layer, comp, t) {
46
+ switch (layer.type) {
47
+ case 'solid': return layer.size ?? [comp.width, comp.height];
48
+ case 'image':
49
+ case 'video': return layer.size ?? [comp.width, comp.height];
50
+ case 'precomp': {
51
+ const c = this.comp(layer.comp);
52
+ return [c.width, c.height];
53
+ }
54
+ default: return [0, 0];
55
+ }
56
+ }
57
+ // Layer-space → comp-space matrix, including parents (AE parenting: transform only).
58
+ matrix(layer, comp, t, depth = 0) {
59
+ const tr = layer.transform ?? {};
60
+ const size = this.layerSize(layer, comp, t);
61
+ const anchor = val(tr.anchor, t, [size[0] / 2, size[1] / 2]);
62
+ const pos = val(tr.position, t, [comp.width / 2, comp.height / 2]);
63
+ const [sx, sy] = val(tr.scale, t, [100, 100]);
64
+ const rot = (val(tr.rotation, t, 0) * Math.PI) / 180;
65
+ const skew = Math.tan((val(tr.skew, t, 0) * Math.PI) / 180);
66
+ const cos = Math.cos(rot), sin = Math.sin(rot);
67
+ let m = [1, 0, 0, 1, pos[0], pos[1]];
68
+ m = mul(m, [cos, sin, -sin, cos, 0, 0]);
69
+ m = mul(m, [1, 0, skew, 1, 0, 0]);
70
+ m = mul(m, [sx / 100, 0, 0, sy / 100, 0, 0]);
71
+ m = mul(m, [1, 0, 0, 1, -anchor[0], -anchor[1]]);
72
+ if (layer.parent && depth < 20) {
73
+ const p = comp.layers.find((l) => l.id === layer.parent);
74
+ if (p)
75
+ m = mul(this.matrix(p, comp, t, depth + 1), m);
76
+ }
77
+ return m;
78
+ }
79
+ async renderComp(ctx, comp, t, withBackground = true) {
80
+ if (withBackground && comp.background) {
81
+ ctx.save();
82
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
83
+ ctx.fillStyle = comp.background;
84
+ ctx.fillRect(0, 0, comp.width, comp.height);
85
+ ctx.restore();
86
+ }
87
+ const matteSources = new Set(comp.layers.map((l) => l.matte?.layer).filter(Boolean));
88
+ for (let i = comp.layers.length - 1; i >= 0; i--) {
89
+ const layer = comp.layers[i];
90
+ if (layer.hidden || layer.type === 'audio' || layer.type === 'null')
91
+ continue;
92
+ if (matteSources.has(layer.id))
93
+ continue; // mattes only show through the layer that uses them
94
+ await this.renderLayer(ctx, layer, comp, t);
95
+ }
96
+ }
97
+ isActive(layer, comp, t) {
98
+ return t >= (layer.inPoint ?? 0) && t < (layer.outPoint ?? comp.duration);
99
+ }
100
+ async renderLayer(target, layer, comp, t) {
101
+ if (!this.isActive(layer, comp, t))
102
+ return;
103
+ const tr = layer.transform ?? {};
104
+ const opacity = val(tr.opacity, t, 100) / 100;
105
+ if (opacity <= 0)
106
+ return;
107
+ const baked = layer.type === 'video' ? this.host.bakedVideoEffects?.(layer) : undefined;
108
+ const effects = (layer.effects ?? []).filter((e) => !baked?.has(e));
109
+ // Filters and drop shadows can be applied while drawing; only masks, mattes, motion blur,
110
+ // glow and fill need the layer composited in a full-frame buffer first (much slower).
111
+ const direct = !layer.masks?.length && !layer.matte && !layer.motionBlur && effects.every((e) => DIRECT_EFFECTS.has(e.type));
112
+ if (direct) {
113
+ target.save();
114
+ target.globalAlpha *= opacity;
115
+ target.globalCompositeOperation = BLEND[layer.blendMode ?? 'normal'];
116
+ const filters = filterString(effects, t);
117
+ if (filters)
118
+ target.filter = filters;
119
+ this.applyShadow(target, effects, t);
120
+ target.setTransform(...this.matrix(layer, comp, t));
121
+ await this.drawContent(target, layer, comp, t);
122
+ target.restore();
123
+ return;
124
+ }
125
+ const buf = this.acquire(comp.width, comp.height);
126
+ const b = buf.getContext('2d');
127
+ if (layer.motionBlur) {
128
+ // Average sub-frame samples across the shutter (AE: 180° shutter, ~8-16 samples).
129
+ const n = Math.max(2, layer.motionBlur.samples ?? 8);
130
+ const span = ((layer.motionBlur.shutter ?? 180) / 360) / comp.fps;
131
+ for (let k = 0; k < n; k++) {
132
+ const st = t - span / 2 + (span * k) / (n - 1);
133
+ b.save();
134
+ b.globalAlpha = 1 / (k + 1);
135
+ b.setTransform(...this.matrix(layer, comp, st));
136
+ await this.drawContent(b, layer, comp, st);
137
+ b.restore();
138
+ }
139
+ }
140
+ else {
141
+ b.save();
142
+ b.setTransform(...this.matrix(layer, comp, t));
143
+ await this.drawContent(b, layer, comp, t);
144
+ b.restore();
145
+ }
146
+ if (layer.masks?.length) {
147
+ const m = this.matrix(layer, comp, t);
148
+ for (const mask of layer.masks) {
149
+ b.save();
150
+ b.setTransform(...m);
151
+ b.globalCompositeOperation = mask.inverted ? 'destination-out' : 'destination-in';
152
+ b.beginPath();
153
+ if (mask.shape === 'path' && mask.points?.length) {
154
+ mask.points.forEach(([x, y], i) => (i ? b.lineTo(x, y) : b.moveTo(x, y)));
155
+ b.closePath();
156
+ }
157
+ else {
158
+ const [x, y, w, h] = val(mask.rect, t, [0, 0, 0, 0]);
159
+ if (mask.shape === 'ellipse')
160
+ b.ellipse(x + w / 2, y + h / 2, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
161
+ else
162
+ b.roundRect(x, y, w, h, mask.radius ?? 0);
163
+ }
164
+ b.fillStyle = '#000';
165
+ b.fill();
166
+ b.restore();
167
+ }
168
+ }
169
+ for (const e of effects) {
170
+ if (e.type === 'fill') {
171
+ b.save();
172
+ b.globalCompositeOperation = 'source-in';
173
+ b.globalAlpha = val(e.opacity, t, 100) / 100;
174
+ b.fillStyle = e.color;
175
+ b.fillRect(0, 0, comp.width, comp.height);
176
+ b.restore();
177
+ }
178
+ }
179
+ if (layer.matte) {
180
+ const src = comp.layers.find((l) => l.id === layer.matte.layer);
181
+ if (src) {
182
+ const mb = this.acquire(comp.width, comp.height);
183
+ await this.renderLayer(mb.getContext('2d'), { ...src, hidden: false, matte: undefined }, comp, t);
184
+ b.save();
185
+ b.globalCompositeOperation = layer.matte.mode === 'alphaInverted' ? 'destination-out' : 'destination-in';
186
+ b.drawImage(mb, 0, 0);
187
+ b.restore();
188
+ this.release(mb);
189
+ }
190
+ }
191
+ target.save();
192
+ target.setTransform(1, 0, 0, 1, 0, 0);
193
+ target.globalAlpha *= opacity;
194
+ target.globalCompositeOperation = BLEND[layer.blendMode ?? 'normal'];
195
+ const filters = filterString(effects, t);
196
+ if (filters)
197
+ target.filter = filters;
198
+ this.applyShadow(target, effects, t);
199
+ const glow = effects.find((e) => e.type === 'glow');
200
+ if (glow) {
201
+ const strength = val(glow.strength, t, 100) / 100;
202
+ target.save();
203
+ target.shadowColor = 'transparent';
204
+ target.filter = `blur(${val(glow.radius, t, 24)}px)`;
205
+ target.globalCompositeOperation = 'lighter';
206
+ target.globalAlpha *= strength;
207
+ if (glow.color) {
208
+ const g = this.acquire(comp.width, comp.height);
209
+ const gc = g.getContext('2d');
210
+ gc.drawImage(buf, 0, 0);
211
+ gc.globalCompositeOperation = 'source-in';
212
+ gc.fillStyle = glow.color;
213
+ gc.fillRect(0, 0, comp.width, comp.height);
214
+ target.drawImage(g, 0, 0);
215
+ this.release(g);
216
+ }
217
+ else
218
+ target.drawImage(buf, 0, 0);
219
+ target.restore();
220
+ }
221
+ target.drawImage(buf, 0, 0);
222
+ target.restore();
223
+ this.release(buf);
224
+ }
225
+ applyShadow(ctx, effects, t) {
226
+ const shadow = effects.find((e) => e.type === 'dropShadow');
227
+ if (!shadow)
228
+ return;
229
+ const dist = val(shadow.distance, t, 8), ang = ((shadow.angle ?? 135) * Math.PI) / 180;
230
+ const c = parseColor(shadow.color ?? '#000') ?? [0, 0, 0, 1];
231
+ ctx.shadowColor = rgbaString([c[0], c[1], c[2], c[3] * (val(shadow.opacity, t, 60) / 100)]);
232
+ ctx.shadowBlur = val(shadow.blur, t, 16);
233
+ ctx.shadowOffsetX = Math.sin(ang) * dist;
234
+ ctx.shadowOffsetY = -Math.cos(ang) * dist;
235
+ }
236
+ async drawContent(ctx, layer, comp, t) {
237
+ const inP = layer.inPoint ?? 0;
238
+ switch (layer.type) {
239
+ case 'solid': {
240
+ const [w, h] = layer.size ?? [comp.width, comp.height];
241
+ ctx.fillStyle = val(layer.color, t, '#000');
242
+ ctx.fillRect(0, 0, w, h);
243
+ return;
244
+ }
245
+ case 'shape': return drawShape(ctx, layer, t);
246
+ case 'text': return drawText(ctx, layer, t, inP, layer.outPoint ?? comp.duration, this.project.fonts ? Object.keys(this.project.fonts)[0] ?? this.host.defaultFont : this.host.defaultFont);
247
+ case 'image': {
248
+ const img = await this.host.image(layer.src);
249
+ const [w, h] = layer.size ?? [comp.width, comp.height];
250
+ drawFitted(ctx, img, w, h, layer.fit ?? 'cover');
251
+ return;
252
+ }
253
+ case 'video': {
254
+ const [w, h] = layer.size ?? [comp.width, comp.height];
255
+ const srcTime = (layer.trimIn ?? 0) + (t - inP) * (layer.speed ?? 1);
256
+ const frame = await this.host.videoFrame(layer, srcTime, w, h, comp.fps);
257
+ ctx.drawImage(frame, 0, 0, w, h);
258
+ return;
259
+ }
260
+ case 'precomp': {
261
+ const child = this.comp(layer.comp);
262
+ const ct = (t - inP) * (layer.speed ?? 1) + (layer.timeOffset ?? 0);
263
+ const c = this.acquire(child.width, child.height);
264
+ await this.renderComp(c.getContext('2d'), child, ct, true);
265
+ ctx.drawImage(c, 0, 0);
266
+ this.release(c);
267
+ return;
268
+ }
269
+ }
270
+ }
271
+ }
272
+ const DIRECT_EFFECTS = new Set(['blur', 'brightness', 'saturate', 'contrast', 'dropShadow']);
273
+ const filterString = (effects, t) => effects
274
+ .map((e) => {
275
+ if (e.type === 'blur') {
276
+ const r = val(e.radius, t, 0);
277
+ return r > 0.05 ? `blur(${r}px)` : '';
278
+ }
279
+ if (e.type === 'brightness')
280
+ return `brightness(${val(e.amount, t, 100)}%)`;
281
+ if (e.type === 'saturate')
282
+ return `saturate(${val(e.amount, t, 100)}%)`;
283
+ if (e.type === 'contrast')
284
+ return `contrast(${val(e.amount, t, 100)}%)`;
285
+ return '';
286
+ })
287
+ .filter(Boolean)
288
+ .join(' ');
289
+ const drawFitted = (ctx, img, w, h, fit) => {
290
+ if (fit === 'fill')
291
+ return ctx.drawImage(img, 0, 0, w, h);
292
+ const s = fit === 'cover' ? Math.max(w / img.width, h / img.height) : Math.min(w / img.width, h / img.height);
293
+ const dw = img.width * s, dh = img.height * s;
294
+ ctx.save();
295
+ ctx.beginPath();
296
+ ctx.rect(0, 0, w, h);
297
+ ctx.clip();
298
+ ctx.drawImage(img, (w - dw) / 2, (h - dh) / 2, dw, dh);
299
+ ctx.restore();
300
+ };
301
+ // Shape outline as a polyline (layer space, centred on the origin), used for trim paths.
302
+ const outline = (s, t) => {
303
+ const [w, h] = val(s.size, t, [200, 200]);
304
+ switch (s.shape) {
305
+ case 'rect': return { pts: [[-w / 2, -h / 2], [w / 2, -h / 2], [w / 2, h / 2], [-w / 2, h / 2]], closed: true };
306
+ case 'ellipse': return { pts: [...Array(96).keys()].map((i) => { const a = (i / 96) * Math.PI * 2 - Math.PI / 2; return [Math.cos(a) * w / 2, Math.sin(a) * h / 2]; }), closed: true };
307
+ case 'polygon': {
308
+ const r = val(s.radius, t, 100), n = s.sides ?? 6;
309
+ return { pts: [...Array(n).keys()].map((i) => { const a = (i / n) * Math.PI * 2 - Math.PI / 2; return [Math.cos(a) * r, Math.sin(a) * r]; }), closed: true };
310
+ }
311
+ default: return { pts: s.points ?? [], closed: s.closed ?? s.shape === 'path' };
312
+ }
313
+ };
314
+ const drawShape = (ctx, s, t) => {
315
+ const start = val(s.trim?.start, t, 0) / 100, end = val(s.trim?.end, t, 100) / 100;
316
+ ctx.beginPath();
317
+ if (s.trim && (start > 0 || end < 1)) {
318
+ const { pts, closed } = outline(s, t);
319
+ const all = closed ? [...pts, pts[0]] : pts;
320
+ const lens = all.slice(1).map((p, i) => Math.hypot(p[0] - all[i][0], p[1] - all[i][1]));
321
+ const total = lens.reduce((a, b) => a + b, 0);
322
+ const a = Math.min(start, end) * total, z = Math.max(start, end) * total;
323
+ let acc = 0, started = false;
324
+ for (let i = 0; i < lens.length; i++) {
325
+ const s0 = acc, s1 = acc + lens[i];
326
+ acc = s1;
327
+ if (s1 < a || s0 > z)
328
+ continue;
329
+ const p = (d) => { const k = lens[i] ? (d - s0) / lens[i] : 0; return [all[i][0] + (all[i + 1][0] - all[i][0]) * k, all[i][1] + (all[i + 1][1] - all[i][1]) * k]; };
330
+ const from = p(Math.max(a, s0)), to = p(Math.min(z, s1));
331
+ if (!started) {
332
+ ctx.moveTo(...from);
333
+ started = true;
334
+ }
335
+ ctx.lineTo(...to);
336
+ }
337
+ }
338
+ else if (s.shape === 'rect') {
339
+ const [w, h] = val(s.size, t, [200, 200]);
340
+ ctx.roundRect(-w / 2, -h / 2, w, h, val(s.radius, t, 0));
341
+ }
342
+ else if (s.shape === 'ellipse') {
343
+ const [w, h] = val(s.size, t, [200, 200]);
344
+ ctx.ellipse(0, 0, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
345
+ }
346
+ else {
347
+ const { pts, closed } = outline(s, t);
348
+ pts.forEach((p, i) => (i ? ctx.lineTo(...p) : ctx.moveTo(...p)));
349
+ if (closed)
350
+ ctx.closePath();
351
+ }
352
+ const trimmed = !!s.trim && (start > 0 || end < 1);
353
+ if (s.fill && !trimmed && s.shape !== 'line') {
354
+ if (typeof s.fill === 'object' && 'linear' in s.fill) {
355
+ const g = ctx.createLinearGradient(...s.fill.linear.from, ...s.fill.linear.to);
356
+ for (const [o, c] of s.fill.linear.stops)
357
+ g.addColorStop(o, c);
358
+ ctx.fillStyle = g;
359
+ }
360
+ else
361
+ ctx.fillStyle = val(s.fill, t, '#fff');
362
+ ctx.fill();
363
+ }
364
+ if (s.stroke) {
365
+ ctx.strokeStyle = val(s.stroke, t, '#fff');
366
+ ctx.lineWidth = val(s.strokeWidth, t, 4);
367
+ ctx.lineCap = s.lineCap ?? 'round';
368
+ ctx.lineJoin = 'round';
369
+ ctx.stroke();
370
+ }
371
+ };
372
+ //# sourceMappingURL=renderer.js.map