Skip to main content

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 Page objects, each being an independent canvas. Ydesign currently supports one page per store โ€” one canvas, one design. Multi-page support is planned as an extension of SceneHandler, 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 toDataURL to generate a thumbnail
  • Restored later with loadJSON

Restore from JSONโ€‹

await store.loadJSON(json);

Internally, loadJSON:

  1. Clears the canvas
  2. Calls sceneHandler.importFromJSON(json) to reload objects
  3. Restores workarea size / background / fonts
  4. Fits to screen (workareaHandler.auto())
  5. Resets the history stack (historyHandler.init())
  6. 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:

StepPurpose
Clear the canvasAvoid leftover objects
Record objects with originX/Y === 'center'loadFromJSON positions by left/top; we reapply center after
formatObjects pre-processingNormalize origin, stringify ids, add crossOrigin to images, lock workarea
Call canvas.loadFromJSONFabric native load
Reapply centered positionssetPositionByOrigin('center', 'center')
Restore canvas dimensionsloadFromJSON overwrites width/height; we restore it
Re-fetch workarea referenceAll objects are re-created; old refs are stale
auto() + historyHandler.init()Fit to screen, reset history stack
restoreStrokesFromCanvasRestore 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:

  1. Call sceneHandler.exportToJSON() on the current page and save it into scenes[oldIndex].json
  2. Call sceneHandler.importFromJSON(scenes[newIndex].json) on the target page
  3. Emit a scene:changed event 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 activeSceneId page

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 โ€” SceneHandler gains exportToJSON + multi-scene state โœ… partially done
  • M2 โ€” store.scenes / store.setActiveScene APIs
  • 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โ€‹