跳到主要内容

场景(Scene)& 导入导出

Scene 是 Ydesign 对"一份完整设计稿"的抽象。一个 Scene 包含:

  • 工作区信息(尺寸、背景、出血位)
  • 所有画布元素(文字、图片、形状、组合)
  • 使用到的字体列表

所有的导入 / 导出 / 切换设计稿都围绕 Scene 展开,由 @ydesign/coreSceneHandler 实现。

💡 关于"页"(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 内部会:

  1. 清空画布
  2. 调用 sceneHandler.importFromJSON(json) 把对象加载回来
  3. 恢复工作区尺寸 / 背景色 / 字体列表
  4. 自动适配屏幕(workareaHandler.auto()
  5. 重置历史栈(historyHandler.init()
  6. 恢复图片的自定义描边(如果有)

是一个完整的"热切换"操作,不需要重建 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/coreSceneHandler

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.loadFromJSONFabric 原生加载
恢复居中对象的真实坐标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/coreSceneHandler 切页;HistoryHandler 页内栈(可按页缓存)
状态@ydesign/editor-storepages / loadJSON / 统一时间线 undo·redo
UI@ydesign/react-editor/pages<Pages store={store} />

几条硬规则

规则说明
落盘永远是多页上游可给单页 JSON;toDocumentJSON() 才是工程文件
模板 ≠ 工程导入模板先抽单页再 replacePage / importPageappend 是工程合并
切页中不写盘,切完再写内容靠 _commitActivePageactivePageId 靠结束后落盘
撤销只走 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 ≡ 追加单页;replacePagereplace-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 / audiostoDocumentJSON() 导出页不含 thumbnail / name
  • 单页旧 JSON(仅 objects)仍可 loadJSON,自动包成 1 页;根级 thumb 会写入第 1 页缩略图
  • 旧信封 { kind: 'ydesign-document', scenes } 仍可读
  • 缩略图thumbnail / thumb 为 UI 缓存(dataURL 或远程 URL)。loadJSON 可读入;加载后 hydrateMissingThumbnails() 可补齐缺失项;Demo 本地草稿用 toDraftJSON() 保留 thumbnail

切页内部步骤

  1. _commitActivePagesceneHandler.exportToJSON() → 当前页 data
  2. sceneHandler.switchScene(deepClone(目标页 data))
  3. 发出 scene:changed;history 对本页 init()(每页独立撤回栈)

路线图

  • M0–M3:文档 + Core 切页 + Store pages + <Pages /> + demo 草稿 / 下载 JSON
  • M4:跨页剪贴板、拖拽排序
  • M5:多页 PDF / 逐页导出(可对接 @ydesign/core/node

下一步