场景(Scene)& 导入导出
Scene 是 Ydesign 对"一份完整设计稿"的抽象。一个 Scene 包含:
- 工作区信息(尺寸、背景、出血位)
- 所有画布元素(文字、图片、形状、组合)
- 使用到的字体列表
所有的导入 / 导出 / 切换设计稿都围绕 Scene 展开,由 @ydesign/core 的 SceneHandler 实现。
💡 关于"页"(Page)的说明:Polotno 等同类 SDK 把设计稿拆成多个
Page,每个 Page 是一个独立画布。Ydesign 当前版本只支持单页 —— 一个store对应一张画布。多页能力会在后续通过扩展SceneHandler实现,详见本页末尾的 多页场景(规划中)。
导入 / 导出
导出当前设计稿(JSON)
const json = store.toJSON();
/* => {
version: '6.x',
objects: [ ... ], // 所有画布对象
background: '#ffffff',
// ... 其他 fabric 元信息
}
*/
拿到的 JSON 可以:
- 存到数据库 / 对象存储
- 生成缩略图(配合
toDataURL) - 下次打开时通过
loadJSON恢复
从 JSON 恢复设计稿
await store.loadJSON(json);
loadJSON 内部会:
- 清空画布
- 调用
sceneHandler.importFromJSON(json)把对象加载回来 - 恢复工作区尺寸 / 背景色 / 字体列表
- 自动适配屏幕(
workareaHandler.auto()) - 重置历史栈(
historyHandler.init()) - 恢复图片的自定义描边(如果有)
是一个完整的"热切换"操作,不需要重建 store 或重挂编辑器。
导出图片(PNG / JPEG / WebP)
// base64
const dataUrl = await store.toDataURL({
multiplier: 2, // 放大倍数(用于高清导出)
format: 'png',
quality: 0.9,
});
// Blob(方便上传 / 下载)
const blob = await store.toBlob({ multiplier: 2, format: 'jpeg' });
// 直接下载到本地
await store.saveAsImage({
multiplier: 2,
format: 'png',
fileName: 'my-design.png',
});
ExportOptions 完整字段
interface ExportOptions {
multiplier: number; // 输出倍数(必填)
format?: 'jpeg' | 'png' | 'webp'; // 格式
quality?: number; // 0-1,对 jpeg / webp 生效
enableRetinaScaling?: boolean; // 是否叠加屏幕 DPR
left?: number; // 裁剪起点 x
top?: number; // 裁剪起点 y
width?: number; // 裁剪宽度
height?: number; // 裁剪高度
filter?: (object: any) => boolean; // 过滤对象(返回 false 跳过)
}
实用技巧:
// 只导出"非水印"元素
await store.toBlob({
multiplier: 2,
filter: obj => obj.name !== 'watermark',
});
// 只导出画布中央的 1000×1000 区域
await store.toDataURL({
multiplier: 1,
left: (store.width - 1000) / 2,
top: (store.height - 1000) / 2,
width: 1000,
height: 1000,
});
模板中心 / 服务端加载
实际项目中通常这样组织:
import { reaction } from 'mobx';
// 1) 用户打开某个模板
async function openTemplate(templateId: string) {
const res = await fetch(`/api/templates/${templateId}`);
const { json } = await res.json();
await store.loadJSON(json);
}
// 2) 自动保存(debounce 1s)
reaction(
() => store.toJSON(),
json => {
fetch('/api/designs/current', {
method: 'PUT',
body: JSON.stringify(json),
});
},
{ delay: 1000 },
);
// 3) 发布时导出 PNG + JSON 一起提交
async function publish() {
const [json, dataUrl] = await Promise.all([
store.toJSON(),
store.toDataURL({ multiplier: 2, format: 'png' }),
]);
await fetch('/api/publish', {
method: 'POST',
body: JSON.stringify({ json, preview: dataUrl }),
});
}
内置的"模板"面板也是通过这个机制加载远端模板的 —— 可以通过 setAPI('templateList', ...) 把它指向你自己的后端。
Scene 与字体
loadJSON 时,Ydesign 会自动完成字体处理:
await store.loadJSON(json);
// 内部会:
// 1. 扫描 json.objects 中所有 fontFamily
// 2. 从全局字体池里找匹配的(addGlobalFont 注册的)
// 3. 把匹配到的字体写入 store.fonts,触发按需加载
这意味着:
- 用户字体(
store.fonts)会跟随 JSON 序列化,不同用户打开同一份设计稿都能获得一致的字体效果 - 全局字体(
addGlobalFont)不会进入 JSON,但会在运行时补全匹配
详见 编辑器配置 · 字体管理。
SceneHandler 核心 API
如果你需要更底层的控制,可以直接用 @ydesign/core 的 SceneHandler:
import type { ITemplate } from '@ydesign/core';
// 一份 Scene 数据
const scene: ITemplate = {
version: '6.0.0',
objects: [ /* ... */ ],
background: '#fff',
};
// 导入(会做 origin 归一 / id 转字符串 / workarea 锁定等预处理)
const workarea = await store.editor!.sceneHandler.importFromJSON(scene);
SceneHandler.importFromJSON 做的事情远比 Fabric 原生的 canvas.loadFromJSON 多:
| 步骤 | 作用 |
|---|---|
| 清空当前画布 | 避免残留对象 |
记录 originX/Y === 'center' 的对象 | 因为 Fabric loadFromJSON 会按 left/top 布局,完事后再把它们重新居中 |
formatObjects 预处理 | 统一 origin、ID 转字符串、image 补 crossOrigin、workarea 补锁定属性 |
调用 canvas.loadFromJSON | Fabric 原生加载 |
| 恢复居中对象的真实坐标 | setPositionByOrigin('center', 'center') |
| 恢复画布尺寸 | loadFromJSON 会覆盖画布宽高,这里重新设置 |
重新获取 workarea 引用 | 因为所有对象是重新创建的,旧引用已失效 |
auto() + historyHandler.init() | 适配屏幕、重置历史栈 |
restoreStrokesFromCanvas | 恢复图片的自定义描边 |
这些琐碎但必须的处理由 SceneHandler 收敛掉了。你几乎总是用 store.loadJSON() 就够了。
多页场景(规划中)
当前版本一个 store 对应一张画布。对于名片 / 社媒封面 / 单页海报等场景,这已经完全够用。
但像 PDF 多页文档、演示稿、小册子 这类场景需要"多页切换"的能力。我们的规划方向是:扩展 SceneHandler,让一个 store 可以持有多份 Scene JSON,切换时通过热加载实现。
预期 API(设计稿)
// 拿到所有场景
store.scenes; // => Array<{ id: string; thumbnail: string; json: ITemplate }>
// 添加一页
store.addScene({ width: 1080, height: 1080 });
// 切换到指定页
await store.setActiveScene(sceneId);
// 删除页
store.deleteScene(sceneId);
// 复制当前页
store.cloneScene();
// 调整页顺序
store.moveScene(fromIndex, toIndex);
每次切换 activeScene,内部会:
- 对当前页调用
sceneHandler.exportToJSON()把数据存入scenes[oldIndex].json - 对目标页调用
sceneHandler.importFromJSON(scenes[newIndex].json)热加载 - 触发
scene:changed事件,UI 自动更新
JSON 序列化格式(预期)
{
"version": "1.0",
"activeSceneId": "scene-1",
"scenes": [
{ "id": "scene-1", "name": "封面", "width": 1080, "height": 1080, "data": { "objects": [...] } },
{ "id": "scene-2", "name": "内页-1", "width": 1080, "height": 1080, "data": { "objects": [...] } }
],
"fonts": [ /* ... */ ]
}
单页 JSON 与多页 JSON 会保持互相兼容:
- 把单页 JSON 喂给多页编辑器,等价于只有一个 scene
- 把多页 JSON 喂给单页编辑器(未来版本支持),只加载
activeSceneId对应那页
为什么不直接复用 Fabric 的 JSON?
Fabric 原生 canvas.toJSON() 只知道"一张画布"。多页能力的核心是:
- 场景级的元数据(每页的尺寸、名称、缩略图、背景)
- 场景之间的资源复用(同一张图片、同一个字体,不要序列化多份)
- 切换动画(跨场景元素的连续性)
这些不是 Fabric 的职责。我们把它们抽象到 SceneHandler 这一层,正好符合 "画布 = Fabric、业务 = Handler" 的分层原则(详见 概览)。
路线图
- M1:
SceneHandler增加exportToJSON/ 多场景管理状态 ✅ 部分完成 - M2:
store.scenes/store.setActiveScene等 API - M3:底部"页面时间轴" UI(类似 PPT 缩略图条)
- M4:跨页元素剪贴板
- M5:多页 PDF / 长图导出
如果你有真实多页场景需求,欢迎到 GitHub Discussions 提需求、或参加多页 Beta 测试。
下一步
- 👉 Store 总览
- 👉 元素操作
- 👉 编辑器配置 · 字体管理