跳到主要内容

场景(Scene)& 导入导出

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

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

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

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

  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() 就够了。


多页场景(规划中)

当前版本一个 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,内部会:

  1. 当前页调用 sceneHandler.exportToJSON() 把数据存入 scenes[oldIndex].json
  2. 目标页调用 sceneHandler.importFromJSON(scenes[newIndex].json) 热加载
  3. 触发 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" 的分层原则(详见 概览)。

路线图

  • M1SceneHandler 增加 exportToJSON / 多场景管理状态 ✅ 部分完成
  • M2store.scenes / store.setActiveScene 等 API
  • M3:底部"页面时间轴" UI(类似 PPT 缩略图条)
  • M4:跨页元素剪贴板
  • M5:多页 PDF / 长图导出

如果你有真实多页场景需求,欢迎到 GitHub Discussions 提需求、或参加多页 Beta 测试。


下一步