@clypra/engine 1.0.0 → 1.0.2

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/README.md ADDED
@@ -0,0 +1,386 @@
1
+ # @clypra/engine
2
+
3
+ The rendering and animation engine powering [Clypra Studio](https://github.com/AIEraDev/clypra-studio) — a high-performance Canvas 2D text effects system with full Lottie JSON tooling, keyframe animation, and CapCut-style template support.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @clypra/engine
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```ts
14
+ import { textEffectConfigToScene, evaluateScene, defaultConfig } from "@clypra/engine";
15
+
16
+ // Build a scene from a config
17
+ const scene = textEffectConfigToScene({
18
+ ...defaultConfig,
19
+ text: "CLYPRA",
20
+ fontFamily: "Poppins",
21
+ fontWeight: 900,
22
+ fontSize: 80,
23
+ fillType: "linear",
24
+ fillGradientStops: [
25
+ { color: "#FF5500", offset: 0 },
26
+ { color: "#FF0080", offset: 100 },
27
+ ],
28
+ bevelEnabled: true,
29
+ bevelDepth: 16,
30
+ bevelShadow: "#880000",
31
+ bevelHighlight: "#FFFFFF",
32
+ glowLayers: [{ enabled: true, color: "#FF2200", blur: 30, opacity: 60, type: "outer", strength: 2 }],
33
+ });
34
+
35
+ // Size the canvas to match the config
36
+ const canvas = document.getElementById("canvas") as HTMLCanvasElement;
37
+ canvas.width = scene.canvas.width;
38
+ canvas.height = scene.canvas.height;
39
+ const ctx = canvas.getContext("2d")!;
40
+
41
+ // Wait for fonts, then draw
42
+ const fontSpec = `${scene.text.fontWeight} ${scene.text.fontSize}px "${scene.text.fontFamily}"`;
43
+ await document.fonts.load(fontSpec);
44
+ evaluateScene(scene, 0, ctx);
45
+ ```
46
+
47
+ ---
48
+
49
+ ## Core Concepts
50
+
51
+ ### TextEffectConfig
52
+
53
+ The flat config object that describes every visual property of a text effect. Pass it to `textEffectConfigToScene()` to convert it into a `SceneDocument` for rendering.
54
+
55
+ ```ts
56
+ import { type TextEffectConfig, defaultConfig } from "@clypra/engine";
57
+
58
+ const config: TextEffectConfig = {
59
+ ...defaultConfig,
60
+ text: "MY TEXT",
61
+ fontFamily: "Montserrat",
62
+ fontWeight: 900,
63
+ fontSize: 100,
64
+
65
+ // Fill
66
+ fillType: "solid", // "solid" | "linear" | "radial" | "pattern" | "none"
67
+ fillColor: "#FFFFFF",
68
+ fillGradientAngle: 90,
69
+ fillGradientStops: [
70
+ { color: "#FF5500", offset: 0 },
71
+ { color: "#FF0080", offset: 100 },
72
+ ],
73
+ patternType: "grunge", // "chalk" | "noise" | "grunge" | "carbon" | "stripes" | "film" | "brushed" | "marble" | "halftone" | "paper"
74
+
75
+ // Stroke
76
+ strokeEnabled: true,
77
+ strokeColor: "#FFFFFF",
78
+ strokeWidth: 3,
79
+ strokePosition: "outside", // "outside" | "center" | "inside"
80
+ strokeOpacity: 100,
81
+ strokeLineJoin: "round", // "round" | "miter" | "bevel"
82
+ strokeBlur: 4, // soft glow on stroke edge
83
+ strokeType: "single", // "single" | "double" | "neon"
84
+ strokeColorSecondary: "#000000",
85
+ strokeWidthSecondary: 6,
86
+ strokeFadeRange: 0, // 0-100, vertical fade
87
+
88
+ // Shadow
89
+ shadowEnabled: true,
90
+ shadowColor: "#000000",
91
+ shadowBlur: 12,
92
+ shadowOffsetX: 4,
93
+ shadowOffsetY: 6,
94
+ shadowOpacity: 80,
95
+ shadowType: "drop", // "drop" | "inner"
96
+
97
+ // Glow (up to 6 layers)
98
+ glowLayers: [{ enabled: true, color: "#FF2200", blur: 30, opacity: 60, type: "outer", strength: 2, spread: 0 }],
99
+
100
+ // 3D Bevel / Extrusion
101
+ bevelEnabled: true,
102
+ bevelDepth: 20,
103
+ bevelHighlight: "#FFFFFF",
104
+ bevelShadow: "#1A0000",
105
+ bevelCoreColor: "#880000",
106
+ bevelDirection: "bottom-right", // "bottom-right" | "bottom" | "right"
107
+ bevelEdgeColor: "#333333",
108
+ bevelEdgeWidth: 1,
109
+ bevelBlur: 8,
110
+ bevelBlurColor: "#000000",
111
+ bevelPerspectiveEnabled: false,
112
+ bevelVanishingPointX: 40,
113
+ bevelVanishingPointY: 80,
114
+ bevelFocalLength: 400,
115
+
116
+ // Multi-stack extrusion
117
+ stackEnabled: false,
118
+ stackCount: 4,
119
+ stackOffsetX: 10,
120
+ stackOffsetY: -10,
121
+ stackOpacityDecay: 20,
122
+ stackColor1: "#FF7C00",
123
+ stackColor2: "#00FFDD",
124
+ stackColor3: "#FF00AA",
125
+ stackColor4: "#AA00FF",
126
+
127
+ // Background panel
128
+ panelEnabled: false,
129
+ panelColor: "#1A1A2E",
130
+ panelOpacity: 90,
131
+ panelRadius: 12,
132
+ panelPaddingX: 40,
133
+ panelPaddingY: 20,
134
+ panelStrokeEnabled: false,
135
+ panelStrokeColor: "#333333",
136
+ panelStrokeWidth: 1,
137
+
138
+ // Canvas
139
+ canvasWidth: 800,
140
+ canvasHeight: 200,
141
+ textPosX: "center", // "left" | "center" | "right"
142
+ textPosY: "middle", // "top" | "middle" | "bottom"
143
+ wrapText: true,
144
+ autoFitText: false,
145
+
146
+ // Per-character fill
147
+ perCharFillEnabled: false,
148
+ charFillColors: [],
149
+ };
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Rendering
155
+
156
+ ### `evaluateScene(scene, time, ctx)`
157
+
158
+ The main render function. Applies timeline animation at time `t` and draws to a Canvas 2D context.
159
+
160
+ ```ts
161
+ import { evaluateScene, textEffectConfigToScene, defaultConfig } from "@clypra/engine";
162
+
163
+ const scene = textEffectConfigToScene({ ...defaultConfig, text: "HELLO" });
164
+ const ctx = canvas.getContext("2d")!;
165
+
166
+ // Static render at t=0
167
+ evaluateScene(scene, 0, ctx);
168
+
169
+ // Animated render loop
170
+ let t = 0;
171
+ const fps = 30;
172
+ setInterval(() => {
173
+ t = (t + 1 / fps) % scene.timeline.duration;
174
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
175
+ evaluateScene(scene, t, ctx);
176
+ }, 1000 / fps);
177
+ ```
178
+
179
+ ### Font Loading (Critical)
180
+
181
+ Always wait for fonts before drawing. Rendering before fonts are ready produces incorrect layout and missing effects like stroke blur.
182
+
183
+ ```ts
184
+ import { preloadGoogleFont } from "@clypra/engine";
185
+
186
+ // Preload specific font
187
+ preloadGoogleFont("Montserrat", [400, 700, 900]);
188
+
189
+ // Wait for a specific face before drawing
190
+ const fontSpec = `${config.fontWeight} ${config.fontSize}px "${config.fontFamily}"`;
191
+ await document.fonts.load(fontSpec);
192
+
193
+ // Now draw
194
+ evaluateScene(scene, 0, ctx);
195
+ ```
196
+
197
+ ### Canvas Sizing
198
+
199
+ Always set canvas dimensions to match the config **before** drawing:
200
+
201
+ ```ts
202
+ canvas.width = config.canvasWidth || 800;
203
+ canvas.height = config.canvasHeight || 200;
204
+ evaluateScene(scene, 0, ctx);
205
+ ```
206
+
207
+ ### ctx.filter Support
208
+
209
+ Stroke blur, bevel ambient blur, and bloom effects use `ctx.filter`. Verify support in your environment:
210
+
211
+ ```ts
212
+ const testCtx = document.createElement("canvas").getContext("2d")!;
213
+ testCtx.filter = "blur(4px)";
214
+ const filterSupported = testCtx.filter !== "none" && testCtx.filter !== "";
215
+ ```
216
+
217
+ If `ctx.filter` is not supported (some WebViews, React Native canvas), use the `WebGLCompositor` fallback:
218
+
219
+ ```ts
220
+ import { evaluateScene, WebGLCompositor } from "@clypra/engine";
221
+
222
+ const compositor = new WebGLCompositor();
223
+ if (compositor.isSupported) {
224
+ const off = new OffscreenCanvas(canvas.width, canvas.height);
225
+ const offCtx = off.getContext("2d")!;
226
+ evaluateScene(scene, 0, offCtx);
227
+ compositor.renderToContext(ctx, off, { blur: 2, bloom: 0, bloomThreshold: 0.6 });
228
+ } else {
229
+ evaluateScene(scene, 0, ctx);
230
+ }
231
+ ```
232
+
233
+ ---
234
+
235
+ ## Presets
236
+
237
+ ```ts
238
+ import { builtInPresets, getPresetScene, blendScenes } from "@clypra/engine";
239
+
240
+ // Apply a built-in preset
241
+ const preset = builtInPresets.find((p) => p.id === "neon-crimson")!;
242
+ const scene = getPresetScene(preset);
243
+
244
+ // Blend two presets (0.0 = all A, 1.0 = all B)
245
+ const blended = blendScenes(sceneA, sceneB, 0.6);
246
+ ```
247
+
248
+ ---
249
+
250
+ ## Timeline Animation
251
+
252
+ ```ts
253
+ import { addTrack, addKeyframeAtTime, updateTimeline, ensureDefaultTimeline } from "@clypra/engine";
254
+
255
+ // Add a shadow drift animation track
256
+ let scene = addTrack(scene, shadowLayerId, "shadowOffsetY", [
257
+ { time: 0, value: 4, easing: "easeInOut" },
258
+ { time: 1.5, value: 16, easing: "easeInOut" },
259
+ { time: 3, value: 4, easing: "easeInOut" },
260
+ ]);
261
+
262
+ // Change timeline duration / fps
263
+ scene = updateTimeline(scene, { duration: 3, fps: 30, loop: true });
264
+
265
+ // Apply built-in demo animation (shadow drift + mask reveal)
266
+ scene = ensureDefaultTimeline(scene);
267
+ ```
268
+
269
+ ---
270
+
271
+ ## Export
272
+
273
+ ```ts
274
+ import { downloadPngSequenceZip, downloadSceneWebM, isWebMExportSupported, buildDotLottie, downloadLottieJson } from "@clypra/engine";
275
+
276
+ // PNG sequence as ZIP
277
+ downloadPngSequenceZip(scene, "my-effect", { fps: 30, duration: 2 });
278
+
279
+ // WebM video
280
+ if (isWebMExportSupported()) {
281
+ await downloadSceneWebM(scene, "my-effect.webm", { fps: 30, duration: 2 });
282
+ }
283
+
284
+ // dotLottie (.lottie) file
285
+ await buildDotLottie(lottieJson, "animation-id", { loop: true, autoplay: true });
286
+ ```
287
+
288
+ ---
289
+
290
+ ## Lottie Tooling
291
+
292
+ ### Build Lottie from scratch
293
+
294
+ ```ts
295
+ import { createBlankLottie, addTextLayer, addSolidLayer, addOrUpdateKeyframe, enableKeyframing } from "@clypra/engine";
296
+
297
+ let lottie = createBlankLottie(1920, 1080, 30, 120);
298
+ lottie = addSolidLayer(lottie, "Background", "#0A0A0F", 1920, 1080);
299
+ lottie = addTextLayer(lottie, "Title", "HELLO WORLD");
300
+
301
+ // Animate position
302
+ lottie = enableKeyframing(lottie, 0, "ks.p");
303
+ lottie = addOrUpdateKeyframe(lottie, 0, "ks.p", 0, [960, 200, 0], "easeOut");
304
+ lottie = addOrUpdateKeyframe(lottie, 0, "ks.p", 30, [960, 540, 0], "easeInOut");
305
+ ```
306
+
307
+ ### Inject text and styles
308
+
309
+ ```ts
310
+ import { injectBatch } from "@clypra/engine";
311
+
312
+ const result = injectBatch(lottieJson, {
313
+ textCustomization: {
314
+ customization: { primary: "BREAKING NEWS", secondary: "Reporter", accent: "9:41 PM" },
315
+ layers: mappedLayers,
316
+ },
317
+ colorOverrides: [{ layerName: "Accent Bar", color: "#FF2200" }],
318
+ hiddenLayers: new Set([3]),
319
+ });
320
+ ```
321
+
322
+ ### CapCut-style animation presets (30+)
323
+
324
+ ```ts
325
+ import { getAnimPreset, bakeAnimationIntoLayer } from "@clypra/engine";
326
+
327
+ const preset = getAnimPreset("zoom-in-bounce")!;
328
+ const animated = bakeAnimationIntoLayer(lottieJson, 0, preset, {
329
+ startFrame: 0,
330
+ endFrame: preset.defaultDurationFrames,
331
+ totalFrames: 120,
332
+ compW: 1920,
333
+ compH: 1080,
334
+ });
335
+ ```
336
+
337
+ Available presets — **Entrance:** `fade-in`, `slide-up`, `slide-down`, `slide-left`, `slide-right`, `zoom-in`, `zoom-in-bounce`, `pop-in`, `flip-x`, `flip-y`, `rotate-in`, `blur-in`, `drop-in`, `typewriter`, `wipe-left`, `glitch-in` — **Exit:** `fade-out`, `slide-out-up`, `slide-out-down`, `zoom-out`, `zoom-blast`, `glitch-out` — **Loop:** `pulse`, `breathe`, `float`, `shake`, `wobble`, `neon-flicker`, `wave` — **Emphasis:** `attention`, `jello`, `swing`
338
+
339
+ ### Built-in templates (13)
340
+
341
+ ```ts
342
+ import { getTemplatePreset } from "@clypra/engine";
343
+
344
+ const template = getTemplatePreset("neon-title")!;
345
+ const lottieJson = template.build(); // ready-to-use Lottie JSON
346
+ ```
347
+
348
+ Available: `clean-lower-third`, `minimal-lower-third`, `neon-title`, `cinematic-title`, `minimal-caption`, `typewriter-caption`, `bold-callout`, `sports-score`, `social-quote`, `kinetic-text`, `glitch-title`, `vertical-story`, `drop-in-title`
349
+
350
+ ---
351
+
352
+ ## Per-Character Fill
353
+
354
+ ```ts
355
+ import { resizeCharFillColors, setCharFillColor, rainbowCharFillColors } from "@clypra/engine";
356
+
357
+ let colors = resizeCharFillColors("HELLO", [], "#FFFFFF");
358
+ colors = setCharFillColor(colors, 0, "#FF0000"); // H → red
359
+ colors = setCharFillColor(colors, 4, "#0080FF"); // O → blue
360
+
361
+ // Or rainbow fill
362
+ const rainbow = rainbowCharFillColors("HELLO");
363
+
364
+ const config = { ...myConfig, perCharFillEnabled: true, charFillColors: colors };
365
+ ```
366
+
367
+ ---
368
+
369
+ ## Undo / History
370
+
371
+ ```ts
372
+ import { snapshotScene, parseHistorySnapshot } from "@clypra/engine";
373
+
374
+ // Save
375
+ const snapshot = snapshotScene(scene);
376
+ undoStack.push(snapshot);
377
+
378
+ // Restore
379
+ const { scene: prevScene } = parseHistorySnapshot(undoStack.pop()!);
380
+ ```
381
+
382
+ ---
383
+
384
+ ## License
385
+
386
+ Proprietary — [Clypra](https://github.com/AIEraDev/clypra-studio)
package/dist/index.cjs CHANGED
@@ -27,6 +27,7 @@ __export(index_exports, {
27
27
  ENTRANCE_PRESETS: () => ENTRANCE_PRESETS,
28
28
  EXIT_PRESETS: () => EXIT_PRESETS,
29
29
  FONT_WEIGHT_OPTIONS: () => FONT_WEIGHT_OPTIONS,
30
+ InkBrushEngine: () => InkBrushEngine,
30
31
  LEGACY_RENDERER_MAP: () => LEGACY_RENDERER_MAP,
31
32
  LOOP_PRESETS: () => LOOP_PRESETS,
32
33
  LOTTIE_ANIM_PRESETS: () => LOTTIE_ANIM_PRESETS,
@@ -6118,6 +6119,7 @@ function duplicateTrackAtPlayhead(doc, trackIndex, previewTime) {
6118
6119
  ENTRANCE_PRESETS,
6119
6120
  EXIT_PRESETS,
6120
6121
  FONT_WEIGHT_OPTIONS,
6122
+ InkBrushEngine,
6121
6123
  LEGACY_RENDERER_MAP,
6122
6124
  LOOP_PRESETS,
6123
6125
  LOTTIE_ANIM_PRESETS,