creator-plugin-development
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseCreator Plugin Development
Creator Plugin 开发
Creator Plugins extend the LottieFiles Creator animation application. They have a two-part sandboxed architecture:
- Plugin Sandbox () — Runs in isolation with access to the
plugin/plugin.tsglobal API. Can manipulate scenes, layers, shapes, keyframes. Cannot make network requests.creator - UI () — Standard React application rendered in an iframe. Can make network requests via
src/. Cannot access thefetchAPI.creator
The two parts communicate exclusively via message passing.
Creator Plugin 用于扩展 LottieFiles Creator 动画应用。它采用双部分 sandbox 架构:
- Plugin Sandbox () — 在隔离环境中运行,可访问
plugin/plugin.ts全局 API。可以操作 scene、layer、shape 和 keyframe。无法发起网络请求。creator - UI () — 在 iframe 中渲染的标准 React 应用。可以通过
src/发起网络请求。无法访问fetchAPI。creator
这两部分仅通过消息传递进行通信。
Project Structure
项目结构
text
my-plugin/
├── plugin/
│ ├── manifest.json # Plugin metadata (id, name, apiVersion, entry, ui)
│ ├── plugin.ts # Sandbox code — has `creator` API access
│ └── [helpers].ts # Optional helper modules
├── src/
│ ├── main.tsx # React DOM entry point
│ ├── app.tsx # Main UI component
│ └── components/ # React components
├── vite.config.ts # Uses @lottiefiles/vite-plugin-creator
├── tsconfig.json # Root config with references
├── tsconfig.plugin.json # Plugin sandbox TypeScript config (no DOM)
├── tsconfig.app.json # UI TypeScript config (DOM + JSX)
├── index.html # Vite app template
└── package.jsontsconfig.plugin.jsontsconfig.app.jsonThe plugin manifest () defines the plugin's identity and entry points:
plugin/manifest.jsonjson
{
"id": "unique-uuid-v4",
"name": "My Plugin",
"apiVersion": "1",
"entry": "plugin.js",
"ui": "ui.html"
}text
my-plugin/
├── plugin/
│ ├── manifest.json # Plugin metadata (id, name, apiVersion, entry, ui)
│ ├── plugin.ts # Sandbox code — has `creator` API access
│ └── [helpers].ts # Optional helper modules
├── src/
│ ├── main.tsx # React DOM entry point
│ ├── app.tsx # Main UI component
│ └── components/ # React components
├── vite.config.ts # Uses @lottiefiles/vite-plugin-creator
├── tsconfig.json # Root config with references
├── tsconfig.plugin.json # Plugin sandbox TypeScript config (no DOM)
├── tsconfig.app.json # UI TypeScript config (DOM + JSX)
├── index.html # Vite app template
└── package.jsontsconfig.plugin.jsontsconfig.app.jsonPlugin manifest()定义 Plugin 的标识信息和入口点:
plugin/manifest.jsonjson
{
"id": "unique-uuid-v4",
"name": "My Plugin",
"apiVersion": "1",
"entry": "plugin.js",
"ui": "ui.html"
}Development Commands
开发命令
bash
npm create @lottiefiles/creator-plugin@latest my-plugin # Scaffold a new pluginbash
npm create @lottiefiles/creator-plugin@latest my-plugin # Scaffold a new pluginFrom the plugin directory (e.g., plugins/my-plugin/):
From the plugin directory (e.g., plugins/my-plugin/):
npm run dev # Start dev server with HTTPS hot-reload
npm run build # TypeScript check + Vite production build
npm exec tsc -- -b # Type check (run before completing any task)
To load in Creator: **Plugins > Develop > New plugin** > enter the localhost URL from `npm run dev`.npm run dev # Start dev server with HTTPS hot-reload
npm run build # TypeScript check + Vite production build
npm exec tsc -- -b # Type check (run before completing any task)
在 Creator 中加载:**Plugins > Develop > New plugin** > 输入 `npm run dev` 输出的 localhost URL。Communication Pattern (Critical)
通信模式(关键)
This is the most common source of bugs. The message wrapping is asymmetric:
这是最常见的 bug 来源。消息包装方式是不对称的:
UI to Plugin
UI 到 Plugin
typescript
// In UI code (src/app.tsx) — MUST wrap in pluginMessage object
parent.postMessage(
{ pluginMessage: { type: 'create-shape', color: '#ff0000' } },
'*'
);typescript
// In UI code (src/app.tsx) — MUST wrap in pluginMessage object
parent.postMessage(
{ pluginMessage: { type: 'create-shape', color: '#ff0000' } },
'*'
);Plugin Receives Message
Plugin 接收消息
typescript
// In plugin sandbox (plugin/plugin.ts) — messages arrive unwrapped
creator.ui.onMessage((msg) => {
if (msg.type === 'create-shape') {
// Use creator API here
}
});typescript
// In plugin sandbox (plugin/plugin.ts) — messages arrive unwrapped
creator.ui.onMessage((msg) => {
if (msg.type === 'create-shape') {
// Use creator API here
}
});Plugin to UI
Plugin 到 UI
typescript
// In plugin sandbox — no wrapping needed
creator.ui.postMessage({ type: 'shape-created', layerId: layer.id });typescript
// In plugin sandbox — no wrapping needed
creator.ui.postMessage({ type: 'shape-created', layerId: layer.id });UI Receives Message
UI 接收消息
typescript
// In UI code — messages arrive wrapped in pluginMessage
window.addEventListener('message', (event) => {
const message = event.data.pluginMessage;
if (message?.type === 'shape-created') {
// Handle response
}
});typescript
// In UI code — messages arrive wrapped in pluginMessage
window.addEventListener('message', (event) => {
const message = event.data.pluginMessage;
if (message?.type === 'shape-created') {
// Handle response
}
});Type-Safe Messages
类型安全的消息
Define shared message types to catch mismatches at compile time:
typescript
// shared/types.ts
export type PluginMessage =
| { type: 'create-shape'; color: string }
| { type: 'import-svg'; content: string }
| { type: 'delete-selection' };For request/response tracking, include a field.
messageId定义共享消息类型,以在编译时发现不匹配问题:
typescript
// shared/types.ts
export type PluginMessage =
| { type: 'create-shape'; color: string }
| { type: 'import-svg'; content: string }
| { type: 'delete-selection' };如需跟踪请求/响应,请加入 字段。
messageIdKey API Patterns
关键 API 模式
Initialize Plugin
初始化 Plugin
typescript
creator.ui.show({ width: 300, height: 500 });typescript
creator.ui.show({ width: 300, height: 500 });Scene Access
Scene 访问
typescript
const scene = creator.activeScene;
scene.size; // { width, height }
scene.duration; // seconds
scene.framerate; // FPS
scene.layers; // ReadonlyArray<Layer>typescript
const scene = creator.activeScene;
scene.size; // { width, height }
scene.duration; // seconds
scene.framerate; // FPS
scene.layers; // ReadonlyArray<Layer>Create Shapes
创建 Shape
typescript
const layer = creator.activeScene.createShapeLayer();
const rect = layer.createRectangle({ size: { width: 200, height: 150 } });
layer.createFill({ type: 'SOLID', color: { r: 66, g: 133, b: 244 } });typescript
const layer = creator.activeScene.createShapeLayer();
const rect = layer.createRectangle({ size: { width: 200, height: 150 } });
layer.createFill({ type: 'SOLID', color: { r: 66, g: 133, b: 244 } });Import Assets
导入资源
typescript
// From URL
const anim = await scene.import({ type: 'LOTTIE', url: 'https://...' });
const img = await scene.import({ type: 'IMAGE', url: 'https://...' });
const svg = await scene.import({ type: 'SVG', url: 'https://...' });
// From content string
const svgLayer = await scene.import({ type: 'SVG', content: svgString });LOTTIE and SVG imports return . IMAGE imports return .
SceneLayerImageLayertypescript
// From URL
const anim = await scene.import({ type: 'LOTTIE', url: 'https://...' });
const img = await scene.import({ type: 'IMAGE', url: 'https://...' });
const svg = await scene.import({ type: 'SVG', url: 'https://...' });
// From content string
const svgLayer = await scene.import({ type: 'SVG', content: svgString });LOTTIE 和 SVG 导入返回 。IMAGE 导入返回 。
SceneLayerImageLayerAnimate Properties
动画属性
typescript
layer.position.addKeyframes([
{ frame: 0, value: { x: 100, y: 100 } },
{ frame: 60, value: { x: 400, y: 100 } },
]);
// With easing
const easeInOut = { type: 'CUBIC_BEZIER', x1: 0.42, y1: 0, x2: 0.58, y2: 1 };
layer.position.addKeyframes([
{ frame: 0, value: { x: 50, y: 100 }, easing: easeInOut },
{ frame: 60, value: { x: 350, y: 100 } },
]);typescript
layer.position.addKeyframes([
{ frame: 0, value: { x: 100, y: 100 } },
{ frame: 60, value: { x: 400, y: 100 } },
]);
// With easing
const easeInOut = { type: 'CUBIC_BEZIER', x1: 0.42, y1: 0, x2: 0.58, y2: 1 };
layer.position.addKeyframes([
{ frame: 0, value: { x: 50, y: 100 }, easing: easeInOut },
{ frame: 60, value: { x: 350, y: 100 } },
]);Selection
选择
typescript
const selectedNodes = creator.selection.nodes;
creator.on('selection:nodes', (nodes) => {
creator.ui.postMessage({ type: 'selection-changed', count: nodes.length });
});typescript
const selectedNodes = creator.selection.nodes;
creator.on('selection:nodes', (nodes) => {
creator.ui.postMessage({ type: 'selection-changed', count: nodes.length });
});Node Type Checking
节点类型检查
Always verify node types before operations:
typescript
const layers = creator.selection.nodes;
layers.forEach((node) => {
if (node.type === 'SHAPE_LAYER') {
// Shape layer operations (has .shapes, .fills, .strokes, .trimPaths)
} else if (node.type === 'IMAGE_LAYER') {
// Image layer operations (has .image)
} else if (node.type === 'SCENE_LAYER') {
// Scene layer operations (has .scene, .break())
} else if (node.type === 'TEXT_LAYER') {
// Text layer operations (has .text, .fontFamily, .fontStyle, .fontSize,
// .alignment, .fill/.createFill, .stroke/.createStroke)
}
});操作前始终验证节点类型:
typescript
const layers = creator.selection.nodes;
layers.forEach((node) => {
if (node.type === 'SHAPE_LAYER') {
// Shape layer operations (has .shapes, .fills, .strokes, .trimPaths)
} else if (node.type === 'IMAGE_LAYER') {
// Image layer operations (has .image)
} else if (node.type === 'SCENE_LAYER') {
// Scene layer operations (has .scene, .break())
} else if (node.type === 'TEXT_LAYER') {
// Text layer operations (has .text, .fontFamily, .fontStyle, .fontSize,
// .alignment, .fill/.createFill, .stroke/.createStroke)
}
});Network Requests
网络请求
The plugin sandbox cannot make requests. Use this pattern:
fetch- UI fetches data from external API
- UI sends data to plugin via
parent.postMessage({ pluginMessage: ... }, '*') - Plugin processes data and applies to scene
For complete examples, see .
references/network-and-libraries.mdPlugin sandbox 无法发起 请求。请使用以下模式:
fetch- UI 从外部 API 获取数据
- UI 通过 将数据发送给 Plugin
parent.postMessage({ pluginMessage: ... }, '*') - Plugin 处理数据并将其应用到 scene
完整示例请参阅 。
references/network-and-libraries.mdCommon Pitfalls
常见陷阱
- Missing wrapper — UI-to-plugin messages MUST be wrapped:
pluginMessage. Plugin-to-UI messages do NOT need wrapping.{ pluginMessage: { ... } } - Fetching from plugin sandbox — Network requests only work in UI code. Move calls to
fetch.src/ - Using /
localStorage— The sandboxed iframe blocks browser storage APIs. UsesessionStoragefrom plugin code instead.creator.clientStorage - Not checking node types — Always verify before accessing type-specific properties.
node.type - Setting on animated properties — Setting
staticValuewhen keyframes exist will not affect the animation. Clear keyframes first or modify keyframe values directly.staticValue - Invisible shapes — Shapes need a fill or stroke to be visible. After , call
createRectangle().createFill() - Scale values are percentages — = 100% scale (not
100). Use1.0for normal size.{ x: 100, y: 100 } - Opacity is 0-100 — Not 0-1. Use for fully opaque.
100 - Color values are 0-255 — RGB channels use the range .
{ r: 0-255, g: 0-255, b: 0-255 } - Not calling early — Call it at the top of
creator.ui.show(), before setting up message handlers.plugin.ts - Sending messages before UI is ready — right after
creator.ui.postMessage()will be dropped because the iframe hasn't loaded. Use a "ui-ready" handshake: have the UI sendcreator.ui.show()on mount, then send data from the plugin only after receiving that message.{ type: 'ui-ready' }
- 缺少 包装 — 从 UI 发往 Plugin 的消息必须包装为:
pluginMessage。从 Plugin 发往 UI 的消息则不需要包装。{ pluginMessage: { ... } } - 在 Plugin sandbox 中发起 fetch — 网络请求只能在 UI 代码中执行。将 调用移到
fetch。src/ - 使用 /
localStorage— sandbox iframe 会阻止浏览器 storage API。请改用 Plugin 代码中的sessionStorage。creator.clientStorage - 未检查节点类型 — 访问类型专属属性前,务必验证 。
node.type - 在已动画化的属性上设置 — 存在 keyframe 时设置
staticValue不会影响动画。请先清除 keyframe,或直接修改 keyframe 的值。staticValue - 不可见的 Shape — Shape 必须包含 fill 或 stroke 才会可见。调用 后,调用
createRectangle()。createFill() - 缩放值是百分比 — = 100% 缩放(不是
100)。正常大小使用1.0。{ x: 100, y: 100 } - 不透明度范围为 0-100 — 不是 0-1。完全不透明使用 。
100 - 颜色值为 0-255 — RGB 通道范围为 。
{ r: 0-255, g: 0-255, b: 0-255 } - 未尽早调用 — 在设置 message handler 之前,先在
creator.ui.show()顶部调用它。plugin.ts - 在 UI 就绪前发送消息 — 调用 后立即调用
creator.ui.show(),消息会被丢弃,因为 iframe 尚未加载。请使用“ui-ready”握手:UI 在 mount 时发送creator.ui.postMessage(),Plugin 仅在收到该消息后才发送数据。{ type: 'ui-ready' }
Verification Checklist
验证清单
Before considering a task complete:
- Run — fix all type errors
npm exec tsc -- -b - Test message flow: UI sends > plugin receives > plugin responds > UI receives
- Confirm network requests are made from UI code, not plugin sandbox
- Verify wrapping is correct in both directions
pluginMessage
在将任务视为完成之前:
- 运行 — 修复所有 type error
npm exec tsc -- -b - 测试消息流:UI 发送 > Plugin 接收 > Plugin 响应 > UI 接收
- 确认网络请求由 UI 代码发起,而不是 Plugin sandbox
- 验证两个方向的 包装均正确
pluginMessage
Reference Guide
参考指南
For deeper information, consult these reference files as needed:
| Reference | When to Consult |
|---|---|
| Detailed architecture, complete message passing examples, UI API |
| Scene hierarchy, node types, traversal patterns |
| Creating shapes, fills/strokes/gradients, keyframes, easing |
| LOTTIE/SVG/IMAGE import formats and patterns |
| clientStorage, node data, selection events, timeline API |
| Fetch-from-UI pattern, using npm packages |
如需了解更深入的信息,请根据需要查阅以下参考文件:
| 参考资料 | 何时查阅 |
|---|---|
| 详细架构、完整消息传递示例、UI API |
| Scene 层级、节点类型、遍历模式 |
| 创建 shape、填充/描边/渐变、keyframe、缓动 |
| LOTTIE/SVG/IMAGE 导入格式和模式 |
| clientStorage、节点数据、选择事件、timeline API |
| 从 UI 发起 fetch 的模式、使用 npm 包 |