@clypra/engine 1.0.1 → 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 +386 -0
- package/package.json +1 -1
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)
|