场景(Scene)& 导入导出
Scene 是 Ydesign 对"一份完整设计稿"的抽象。一个 Scene 包含:
- 工作区信息(尺寸、背景、出血位)
- 所有画布元素(文字、图片、形状、组合)
- 使用到的字体列表
所有的导入 / 导出 / 切换设计稿都围绕 Scene 展开,由 @ydesign/core 的 SceneHandler 实现。
💡 关于"页"(Page)的说明:Ydesign 已支持多页 —— 仍是一个
store/ 一张 Fabric 画布;多页是多份页面 JSON 的编排与热切换。详见本页 多页场景。
导入 / 导出
导出当前设计稿(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() 就够了。
多页场景
多页 = 多份页面 JSON 的编排,不是多套 Fabric Canvas。
切换页:提交当前画布 → 缓存页内历史 → 深拷贝热加载目标页。UI:@ydesign/react-editor/pages 底部缩略图条。
协作必读(心智模型 + 数据流 + 加载落点 + 切页保存 + 统一时间线):
👉 multi-page-scenes.md · 逻辑关系总览
分层
| 层 | 包 | 职责 |
|---|---|---|
| 引擎 | @ydesign/core | SceneHandler 切页;HistoryHandler 页内栈(可按页缓存) |
| 状态 | @ydesign/editor-store | pages / loadJSON / 统一时间线 undo·redo |
| UI | @ydesign/react-editor/pages | <Pages store={store} /> |
几条硬规则
| 规则 | 说明 |
|---|---|
| 落盘永远是多页 | 上游可给单页 JSON;toDocumentJSON() 才是工程文件 |
| 模板 ≠ 工程导入 | 模板先抽单页再 replacePage / importPage;append 是工程合并 |
| 切页中不写盘,切完再写 | 内容靠 _commitActivePage;activePageId 靠结束后落盘 |
撤销只走 store.undo() | 页结构与页内编辑按时间顺序交错;不要直连 historyHandler.undo |
Store API
store.pages; // 页列表(含 thumbnail / name / data)
store.activePageId;
await store.addPage({ width: 1080, height: 1080 });
await store.setActivePage(pageId);
await store.clonePage(pageId);
await store.deletePage(pageId); // 至少保留 1 页;删当前页会先切到上一页
store.renamePage(id, name);
store.movePage(fromIndex, toIndex);
await store.undo();
await store.redo();
// 多页文档(推荐持久化)
const doc = store.toDocumentJSON();
store.saveAsJSON('design.json');
await store.loadJSON(doc); // 默认 mode: replace,整包替换
// 追加页 / 替换页(第二参数决定「数据落点」)
await store.importPage(singlePageTemplate); // 末尾加一页
await store.replacePage(singlePageTemplate); // 覆盖当前页
await store.loadJSON(multiPageDoc, { mode: 'append' }); // 追加另一文档的全部页
loadJSON 三种 mode:
| mode | 行为 |
|---|---|
replace(默认) | 整包替换;旧单页 JSON 自动包装成 Page 1 |
append | 工程合并:单页追加 1 页;多页追加全部 pages |
replace-page | 替换指定页;多页 JSON 只取其 active 页 |
语法糖:importPage ≡ 追加单页;replacePage ≡ replace-page。详见 multi-page-scenes.md §4.0。
模板卡 ≠ 工程导入。 点模板卡时:先抽成单页再
importPage/replacePage;只有「整体替换工程」才对原文档直接loadJSON(replace)。
撤销: 使用 store.undo() / store.redo()(不要直接 historyHandler.undo)。
按时间线倒序交错撤销页结构与页内编辑;页内栈按页缓存,故「改字 → 删页 → Undo 恢复页」后仍可继续撤改字。仅切页不进历史。详见 multi-page-scenes.md §9。
页内容 vs 缩略图 vs 保存: 三条独立管线(data / thumbnail / 草稿落盘),切页时序见 multi-page-scenes.md §J。
兼容别名(deprecated):scenes / setActiveScene / addScene 等仍可用。
挂载 UI
import { Pages } from '@ydesign/react-editor/pages';
<WorkspaceWrap>
<Toolbar store={store} />
<Workspace store={store} />
<ZoomButtons store={store} />
<Pages store={store} />
</WorkspaceWrap>
对外 JSON(toDocumentJSON)
{
"width": 1080,
"height": 1080,
"fonts": [],
"pages": [
{
"id": "...",
"objects": [],
"width": 1080,
"height": 1080,
"background": "#ffffff",
"bleed": 0,
"clipPath": {},
"version": "6.9.1"
}
],
"activePageId": "...",
"unit": "px",
"dpi": 72,
"custom": null
}
- 不含
schemaVersion/audios;toDocumentJSON()导出页不含thumbnail/name - 单页旧 JSON(仅
objects)仍可loadJSON,自动包成 1 页;根级thumb会写入第 1 页缩略图 - 旧信封
{ kind: 'ydesign-document', scenes }仍可读 - 缩略图:
thumbnail/thumb为 UI 缓存(dataURL 或远程 URL)。loadJSON可读入;加载后hydrateMissingThumbnails()可补齐缺失项;Demo 本地草稿用toDraftJSON()保留 thumbnail
切页内部步骤
_commitActivePage:sceneHandler.exportToJSON()→ 当前页datasceneHandler.switchScene(deepClone(目标页 data))- 发出
scene:changed;history 对本页init()(每页独立撤回栈)
路线图
- M0–M3:文档 + Core 切页 + Store
pages+<Pages />+ demo 草稿 / 下载 JSON - M4:跨页剪贴板、拖拽排序
- M5:多页 PDF / 逐页导出(可对接
@ydesign/core/node)
下一步
- 👉 Store 总览
- 👉 元素操作
- 👉 编辑器配置 · 字体管理