Scenes & Import/Export
A Scene is Ydesign's abstraction for "a complete design". A scene contains:
- Workarea info (size, background, bleed)
- All canvas elements (text, images, shapes, groups)
- The list of fonts in use
Every import, export, and design-switch flows through a scene, implemented by SceneHandler in @ydesign/core.
๐ก A note about "pages": SDKs like Polotno split a design into multiple
Pageobjects, each being an independent canvas. Ydesign currently supports one page per store โ one canvas, one design. Multi-page support is planned as an extension ofSceneHandler, see Multi-page (planned) at the end of this page.
Import / exportโ
Export the current design (JSON)โ
const json = store.toJSON();
/* => {
version: '6.x',
objects: [ ... ], // all canvas objects
background: '#ffffff',
// ... other fabric metadata
}
*/
The JSON can be:
- Stored in your database or object storage
- Combined with
toDataURLto generate a thumbnail - Restored later with
loadJSON
Restore from JSONโ
await store.loadJSON(json);
Internally, loadJSON:
- Clears the canvas
- Calls
sceneHandler.importFromJSON(json)to reload objects - Restores workarea size / background / fonts
- Fits to screen (
workareaHandler.auto()) - Resets the history stack (
historyHandler.init()) - Restores any custom image strokes
This is a full hot-swap โ no need to recreate the store or remount the editor.
Export as image (PNG / JPEG / WebP)โ
// base64
const dataUrl = await store.toDataURL({
multiplier: 2, // scale factor for high-DPI output
format: 'png',
quality: 0.9,
});
// Blob (easy to upload / download)
const blob = await store.toBlob({ multiplier: 2, format: 'jpeg' });
// Download directly
await store.saveAsImage({
multiplier: 2,
format: 'png',
fileName: 'my-design.png',
});
Full ExportOptionsโ
interface ExportOptions {
multiplier: number; // scale factor (required)
format?: 'jpeg' | 'png' | 'webp';
quality?: number; // 0-1, for jpeg / webp
enableRetinaScaling?: boolean; // add device pixel ratio
left?: number; // crop start x
top?: number; // crop start y
width?: number; // crop width
height?: number; // crop height
filter?: (object: any) => boolean; // skip objects returning false
}
Handy patterns:
// Export without watermark elements
await store.toBlob({
multiplier: 2,
filter: obj => obj.name !== 'watermark',
});
// Export only the 1000ร1000 region centered on the canvas
await store.toDataURL({
multiplier: 1,
left: (store.width - 1000) / 2,
top: (store.height - 1000) / 2,
width: 1000,
height: 1000,
});
Template center / backend integrationโ
A typical production flow:
import { reaction } from 'mobx';
// 1) Open a template
async function openTemplate(templateId: string) {
const res = await fetch(`/api/templates/${templateId}`);
const { json } = await res.json();
await store.loadJSON(json);
}
// 2) Auto-save (1s debounce)
reaction(
() => store.toJSON(),
json => {
fetch('/api/designs/current', {
method: 'PUT',
body: JSON.stringify(json),
});
},
{ delay: 1000 },
);
// 3) On publish, submit JSON + preview PNG
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 }),
});
}
The built-in Templates panel uses this same mechanism. Point it at your own backend with setAPI('templateList', ...).
Scenes & fontsโ
During loadJSON, Ydesign handles fonts automatically:
await store.loadJSON(json);
// Internally:
// 1. Scans json.objects for every fontFamily
// 2. Matches against the global font registry (via addGlobalFont)
// 3. Adds matched fonts into store.fonts, triggering on-demand load
That means:
- User fonts (
store.fonts) are serialized with the JSON โ different users opening the same design get the same rendering - Global fonts (
addGlobalFont) stay out of JSON but are matched at runtime
See Editor Configuration ยท Fonts.
SceneHandler core APIโ
For lower-level control, go directly to @ydesign/core's SceneHandler:
import type { ITemplate } from '@ydesign/core';
const scene: ITemplate = {
version: '6.0.0',
objects: [ /* ... */ ],
background: '#fff',
};
// Import (normalizes origin, stringifies ids, locks workarea, โฆ)
const workarea = await store.editor!.sceneHandler.importFromJSON(scene);
SceneHandler.importFromJSON does far more than Fabric's native canvas.loadFromJSON:
| Step | Purpose |
|---|---|
| Clear the canvas | Avoid leftover objects |
Record objects with originX/Y === 'center' | loadFromJSON positions by left/top; we reapply center after |
formatObjects pre-processing | Normalize origin, stringify ids, add crossOrigin to images, lock workarea |
Call canvas.loadFromJSON | Fabric native load |
| Reapply centered positions | setPositionByOrigin('center', 'center') |
| Restore canvas dimensions | loadFromJSON overwrites width/height; we restore it |
Re-fetch workarea reference | All objects are re-created; old refs are stale |
auto() + historyHandler.init() | Fit to screen, reset history stack |
restoreStrokesFromCanvas | Restore custom image strokes |
That glue is all centralized in SceneHandler. In practice, store.loadJSON() is enough for almost every case.
Multi-page (planned)โ
At the moment, one store owns one canvas. For business cards, social covers, or single-page posters that's perfect.
But scenarios like multi-page PDFs, slide decks, booklets need page switching. Our plan: extend SceneHandler so a store can hold multiple scene JSONs and switch between them via hot-reload.
Expected API (draft)โ
// All scenes
store.scenes; // => Array<{ id: string; thumbnail: string; json: ITemplate }>
// Add a page
store.addScene({ width: 1080, height: 1080 });
// Switch to a page
await store.setActiveScene(sceneId);
// Delete
store.deleteScene(sceneId);
// Duplicate the current page
store.cloneScene();
// Reorder
store.moveScene(fromIndex, toIndex);
Each setActiveScene call will:
- Call
sceneHandler.exportToJSON()on the current page and save it intoscenes[oldIndex].json - Call
sceneHandler.importFromJSON(scenes[newIndex].json)on the target page - Emit a
scene:changedevent so the UI updates
Expected JSON (draft)โ
{
"version": "1.0",
"activeSceneId": "scene-1",
"scenes": [
{ "id": "scene-1", "name": "Cover", "width": 1080, "height": 1080, "data": { "objects": [...] } },
{ "id": "scene-2", "name": "Page-1", "width": 1080, "height": 1080, "data": { "objects": [...] } }
],
"fonts": [ /* ... */ ]
}
Single-page and multi-page JSONs will be mutually compatible:
- Feed a single-page JSON to the multi-page editor โ equivalent to a single scene
- Feed a multi-page JSON to a single-page editor (future) โ loads only the
activeSceneIdpage
Why not just reuse Fabric's JSON?โ
Fabric's canvas.toJSON() knows about one canvas. Multi-page essentials include:
- Scene-level metadata (size / name / thumbnail / background per page)
- Cross-scene resource reuse (don't serialize the same font / image twice)
- Switch animations (cross-scene element continuity)
None of that is Fabric's job. Placing it on SceneHandler fits the "canvas = Fabric, business = handlers" layering (see Overview).
Roadmapโ
- M1 โ
SceneHandlergainsexportToJSON+ multi-scene state โ partially done - M2 โ
store.scenes/store.setActiveSceneAPIs - M3 โ Bottom "page timeline" UI (like slide thumbnails)
- M4 โ Cross-scene element clipboard
- M5 โ Multi-page PDF / long-image export
If you have a concrete multi-page use case, please open a GitHub Discussion or sign up for the beta.
Nextโ
- ๐ Store overview
- ๐ Elements
- ๐ Editor Configuration ยท Fonts