Build your own designer
The designer’s main job is to let you build your own. Pixonaut.mount()
with its generated form is the quickest way to put one on a page, but that
form is only one consumer of the methods and events below: every control it
draws — its fields, the size picker, the step navigation — is written against
the same calls your own UI has. Anything the generated form can do, yours can.
Your UI holds no state of its own about the design. Read it with
getState() and the events, change it with the commands, and draw it however
your storefront looks: a canvas in a dialog, a form with your own option
groups beside it, thumbnails in a corner.
Pixonaut still checks the design. Your UI may leave out a field the template
offers, or call the commands in any order; approve() and commit() check
the design against the template as they always do, so no UI can produce a
print the template forbids.
Mount the canvas alone
Section titled “Mount the canvas alone”A page that loads only the core gets the canvas
alone from createDesigner, with no ui, and loads none of the generated
form. With the whole SDK, mount it with showForm: false:
const designer = await Pixonaut.mount('#canvas', { publishableKey: 'pk_…', templateId: 'classic-tshirt@2', showForm: false,});| Option | What it does |
|---|---|
showForm |
false draws no form, and with it no pickers: the canvas alone. |
showPickers |
false draws no print size picker, and none of the blank’s options, above the generated form, for a page that draws its own from getPrintSizes(), getOptions() and getVariants(). { size: false } or { variant: false } leaves out that one alone. |
showVariantPicker |
false draws none of the blank’s options but keeps the print size picker, for a page whose own variation form calls setVariant(). |
showViews |
false draws no switcher between the product’s views, for a page that draws its own from getViews() and renderView(). |
onChange |
A shortcut for designer.on('change', …). |
The canvas takes its element’s width and follows it when the element is resized.
Methods
Section titled “Methods”The design
Section titled “The design”| Method | What it does |
|---|---|
getFormSchema() |
The template’s steps, each with the fields the customer may fill and the layers they may place. See Headless mode. |
getState() |
The design now: values (with keyed values under $fields, and other templates’ under $templates), the chosen printSize, and the step a form in steps shows. |
setValue(layerId, prop, value) |
Set one value a field offers. |
getValue(layerId, prop) |
The value a field shows now: the customer’s, also one carried by its field key from another template or side, or else the template’s own. For a form of your own drawn again after setTemplate() or setSide(). |
setTransform(layerId, patch) |
Place a layer: any of xMm, yMm, rotationDeg, scale, wMm and hMm, and flipX and flipY to mirror it. |
scaleLayer(layerId, scale) |
Size a layer as its scale handle does: held to the sizes the template allows it, rounded to its step, and kept on the print area. setTransform writes a scale as given. |
resetLayer(layerId) |
Put a layer back as the template has it: every value the customer gave it, and its place. |
uploadAsset(file, layerId) |
Upload the customer’s picture for an image field. It emits progress while the file is sent, and resolves with its assetId and the file’s name, fileName. |
getFieldTexts() |
Each field’s layerId, prop, label and its value in words, for a cart or order summary: the text typed, the option’s word, a palette colour’s name, any other colour’s hex as the form heads it (#1A1A18), an uploaded picture’s file name. A crop has no line. |
settled() |
Resolves once every upload and improvement under way has written its value, however it ended. Await it before you read getState() to keep what the customer made. |
getResolution(layerId) |
How the picture uploaded for a layer measures at the chosen print size: effectiveDpi, the pixels it has (actualPx) and needs (minPx) along the edge that decides, the floorDpi it is judged against, and low when it falls short. The same measure the upload’s own warning takes: the picture on the layer as the template places it, before any crop. Under a repeat it is measured at the tile’s size, as the committed design is, so it changes with setRepeat({ size }). undefined until there is an upload, and without a print size. |
improveResolution(assetId, layerId) |
Ask for a sharper copy of an upload that offered one. |
The canvas
Section titled “The canvas”| Method | What it does |
|---|---|
selectLayer(layerId) |
Select a layer and show its handles. |
clearSelection() |
Select nothing. |
getSelectedLayer() |
The selected layer, or null. |
setInteractive(layerId, on) |
Turn a layer’s handles off (false), or on again: off, the customer cannot drag, turn or size it on the canvas. Its fields stay yours to offer. |
setCropMode(target) |
Let a drag on the canvas move a picture inside its frame: a layer’s ({ kind: 'layer', layerId }) or the image background’s ({ kind: 'background' }). Entering it selects the layer (none, for the background), which selection reports. null ends it, as Escape or a click away does. Returns false, and changes nothing, for a site with no picture to move, and for the background while the design’s repeat is a scatter. Under any other repeat the background’s frame is the tile at the centre of the print area. |
setMode(mode) |
'preview' shows the product alone, with no handles or guides, and takes no gesture; 'edit' brings them back. getMode() reads it. |
setLocked(locked) |
Lock the designer while you make a proof of it: no gesture, and no field of the generated form. |
attach(element) |
Draw the designer into another element, keeping the design, the selection and the step. |
detach() |
Take the designer out of its element, keeping the design, until attach() draws it again. |
A canvas in a dialog attaches when the dialog opens:
dialog.showModal();designer.attach(dialog.querySelector('.canvas'));dialog.addEventListener('close', () => designer.detach(), { once: true });A crop control of your own, with a Move button that turns into Done:
const target = { kind: 'layer', layerId: 'photo' };move.addEventListener('click', () => designer.setCropMode(move.dataset.cropping ? null : target));designer.on('crop', ({ target }) => { move.dataset.cropping = target ? 'yes' : ''; move.textContent = target ? 'Done' : 'Move';});A storefront that offers handles only behind a checkbox:
const adjust = document.querySelector('#adjust');const layout = () => { for (const { layerId } of designer.getFormSchema().steps.flatMap((s) => s.transforms)) { designer.setInteractive(layerId, adjust.checked); }};adjust.addEventListener('change', layout);layout();Steps, sizes and the product
Section titled “Steps, sizes and the product”| Method | What it does |
|---|---|
getStep(), setStep(index) |
Where a form drawn as steps stands, and which step it shows. A template’s steps are a hint for the generated form: getFormSchema() lists them, and your own form may show every field on one page. Nothing else depends on the step shown. |
getPrintSizes(), setPrintSize(index) |
Your size menu with the template’s verdict on each entry, and the customer’s choice. See Print sizes. |
getViews(), getView() |
The views the product is shown in, each with its label, its placement, whether it has a photo of the variant shown and its kind ('design' for the Design view, the artwork alone), and the view shown. See Views and variants. |
getPreviewAspect() |
The preview’s shape now, width over height, for a page that holds it in a frame of that shape. Read it once shown() has resolved, and follow on("previewShape"). |
setView(viewId) |
Show a view; one of another placement switches the design to that placement and the template the product prints there. |
getOptions() |
The blank’s options the product is chosen by, in its order, each with its values and how each value looks: a colour fill, a picture, or none. See Switching to another variant. |
getVariants() |
Each variant the product is offered in, with its value on each of the blank’s options, whether a view has a photo of it, and whether the design can be printed at its size (printable). Its colour and size are deprecated. |
setVariant(ref), getVariant() |
Show every view in the photo of another variant of the blank (another colour), and the variant shown. A variant with no photo in any view rejects with a VariantUnavailableError, unless the product has a photo of no variant. |
renderView(viewId, { variant, maxEdgePx }) |
A PNG of a view with the customer’s design on it, for a thumbnail of your own. |
preload(target, { mode, concurrency, budgetBytes }), cancelPreload() |
Load other variants’, views’ and sides’ photos and templates ahead, so switching to them is instant, or stop. Preloading spends the customer’s memory, bandwidth and CPU, and may slow the page on a low-end device. See Make switching instant. |
preloadPlan() |
What preload: 'auto' would do and why, from an estimate of what it would hold (a page cannot measure its own memory) and the browser’s signals. Loads nothing. |
setTemplate(templateId) |
Show another template on the side shown, carrying what the customer entered in settings with a field key. See Switching to another template. |
renderTemplate(templateId, { maxEdgePx, values, viewId, variant }) |
A PNG of any template, drawn in the designer’s worker: with its own sample, or with the values of getState() carried by field key. For a gallery of your own. See A gallery of templates. |
| Method | What it does |
|---|---|
getSides() |
Each side of the item, one per entry of placements, or per group of linked placements: its placementId, the templateId it shows, whether it is enabled, whether its design is valid, each side judged against its own template, and the placements it drives (linked, null for one designed on its own). See Sides. |
setSide(placementId) |
Show a side to edit, with its own template and values, in its first view. |
setSideEnabled(placementId, on) |
Have approve() and commit() take a side, or leave it out, for a product where the back is an option. The side keeps its values. |
setSideLink(placementId, { linked, mirrored }) |
Take a placement of a linked side out of its group to design it on its own, or put it back, and mirror its print or not. See Linking sides. |
enableRepeat(options), disableRepeat() |
Lay the template as a repeating pattern across every side’s print, in the layouts you offer, or place it once again. See Repeating the design. |
getRepeat(), setRepeat({ mode, size, seed }) |
The repeat offered and chosen, or null; and the shopper’s layout, size and arrangement. |
renderSidePrint({ maxEdgePx, placementId, templateId, values, linked, repeat }), setSidePrint(placementId, print) |
For a page that mounts a designer per side: the side shown’s print as a view of several placements draws it, and the print another designer’s side shows in this one’s views of several placements (null shows its fill colour). With placementId and templateId, a side whose designer is not mounted yet is drawn from its template, carrying the keyed values of values (a state’s), so the product’s photo shows every side from the start. linked names the group the side is designed in, which its repeat is sized across, and repeat the repeat it is laid with (null placed once); without it, the side is laid with this designer’s own repeat. |
Finishing
Section titled “Finishing”| Method | What it does |
|---|---|
validateLocal() |
Check the side shown in the browser, for instant feedback. |
approve() |
Ask Pixonaut for the proofs the customer approves: one per enabled side, under sides, with the first side’s on top. |
commit() |
Save the design without a proof: one design per enabled side, under sides, for one order. |
shown() |
Resolves once the designer is shown, as the shown event says. |
pixel.ask(prompt, { signal }), pixel.undo() |
Ask Pixel to change the design as one step, cancel the request through signal, and take that step back, for a template with Pixel on. See Pixel for your customers. |
destroy() |
Remove the designer from the page. |
Events
Section titled “Events”on(event, handler) subscribes, and returns the function that ends the
subscription:
const stop = designer.on('selection', ({ layerId }) => highlight(layerId));// laterstop();| Event | The handler is handed |
|---|---|
shown |
{}, once: the first preview is drawn and, in a mount split across your elements, has replaced what you printed in them. shown() resolves then too, for a subscriber that comes after it. |
change |
The design’s state, as getState() returns it, after any change. |
printSize |
{ index, size }: another print size is chosen, in the size picker or by setPrintSize(); its index in printSizes and the entry as getPrintSizes() gives it. |
selection |
{ layerId }: the selected layer, or null. |
step |
The step a form in steps now shows, as getStep() returns it. |
view |
{ viewId }: the product view now shown, by setView(), the strip, or a variant without a photo of the view shown. |
previewShape |
{ aspect }: the preview took another shape, width over height: a photo of another shape was placed, or a print size of another shape was drawn. |
variant |
{ ref }: the product variant now shown. |
side |
{ placementId }: the side now shown, by setSide() or by a view of another side. |
repeat |
{ repeat }: the repeat as getRepeat() answers it, once offered, chosen or withdrawn (null), and again when another side, variant or size moves its minSize. |
link |
{ placementId, linked, mirrored }: a placement of a linked side taken out of its group, put back or mirrored, by setSideLink(). |
print |
{ placementId }: a print a view of several placements draws may have changed: the side shown’s, as its preview draws again, or another placement’s, by setSidePrint(). |
mode |
{ mode }: 'edit' or 'preview'. |
crop |
{ target }: the picture a drag on the canvas now moves, or null once that ended. |
progress |
{ layerId, prop, fileName, sentBytes, totalBytes } while uploadAsset sends a file. |
error |
{ action, error } when approve, commit, upload, improve-resolution, cut-out or pixel fails. The promise you called rejects with the same error. A mount that fails is heard only by the onError option, with action: 'mount': there is no designer yet to subscribe to. |
approved |
What approve() resolves with. |
committed |
What commit() resolves with. |
pixel |
{ status }: a request to Pixel sent ('asking'), its changes applied ('done', with reply, changed and cutOuts), its failure ('failed'), its cancel ('cancelled'), or its step taken back ('undone'). See Pixel for your customers. |
preload |
{ loaded, total, estimatedBytes, failed, done } after each preloaded item settles, and once more, done: true, when the preload ends or is cancelled. |
A handler that throws is reported in the console; the other handlers still hear the event. A change a handler makes, such as ending crop mode, is reported once every handler has heard the event that caused it, so each handler hears the changes in the order they happened.
Listing the customer’s choices
Section titled “Listing the customer’s choices”getFieldTexts() gives each field of the form with its label and its value
in words, for your cart or order summary to list: the text typed, the
option’s word, a palette colour’s name, any other colour’s hex as the
form’s colour field heads it (#1A1A18, a CMYK colour as it previews), an
uploaded picture’s file name. A crop has no line.
for (const { label, value } of designer.getFieldTexts()) { if (value) lines.push(`${label}: ${value}`);}An upload’s result, and each progress event it sends, carry the file’s
name as fileName too.
Reporting errors
Section titled “Reporting errors”The SDK sends no error reports anywhere by itself. A failure reaches only the
promise you called, on('error') and the onError option. To report it to
your own error tracker, forward it from there; onError hears a mount that
fails as well:
designer.on('error', ({ action, error }) => { myTracker.captureException(error, { tags: { action } });});What you send, and to whom, is yours to decide.
A designer of your own
Section titled “A designer of your own”A complete designer from the calls above, and nothing else: the product page shows the design, a Customise button opens the canvas in a dialog, every field of the template is on one page even when the template is in steps, your own thumbnails switch between your product photos, and Approve shows the proof before Add to cart saves the design. The showcase has more storefronts built the same way.
<div id="product"></div><button id="customise">Customise</button><img id="proof" alt="Your proof" hidden><button id="add-to-cart" hidden>Add to cart</button>
<dialog id="designer"> <div class="canvas"></div> <div id="views"></div> <form id="fields"></form> <progress id="upload" max="1" value="0" hidden></progress> <p id="quality"></p> <p id="error" role="alert"></p> <button id="approve" type="button">Approve</button> <button id="close" type="button">Close</button></dialog>const designer = await Pixonaut.mount('#product', { publishableKey, templateId, mockups: productPhotos, printSizes: [{ widthMm: 400, heightMm: 500, label: '40×50 cm' }], showForm: false, showViews: false,});designer.setMode('preview');
// The canvas moves into the dialog to be designed, and back onto the// product page when the dialog closes, keeping the design.const dialog = document.querySelector('#designer');const product = document.querySelector('#product');document.querySelector('#customise').addEventListener('click', () => { dialog.showModal(); designer.attach(dialog.querySelector('.canvas')); designer.setMode('edit');});dialog.addEventListener('close', () => { designer.setMode('preview'); designer.attach(product);});document.querySelector('#close').addEventListener('click', () => dialog.close());
// Every field of every step on one page: a template's steps are a hint.const form = document.querySelector('#fields');const bar = document.querySelector('#upload');const quality = document.querySelector('#quality');const error = document.querySelector('#error');for (const step of designer.getFormSchema().steps) { const fieldset = document.createElement('fieldset'); const legend = document.createElement('legend'); legend.textContent = step.label; fieldset.append(legend); for (const field of step.fields) { const input = document.createElement('input'); input.name = `${field.layerId}.${field.prop}`; if (field.type === 'image') { input.type = 'file'; input.accept = 'image/*'; input.addEventListener('change', async () => { quality.textContent = ''; error.textContent = ''; bar.value = 0; bar.hidden = false; try { await designer.uploadAsset(input.files[0], field.layerId, field.prop); const resolution = designer.getResolution(field.layerId); quality.textContent = resolution?.low ? 'This picture is small for the print and may look blurred.' : 'This picture is sharp enough to print.'; } catch { error.textContent = 'This picture could not be uploaded. Try another one.'; } finally { bar.hidden = true; } }); } else if (field.type === 'text') { input.value = field.default ?? ''; input.addEventListener('input', () => designer.setValue(field.layerId, field.prop, input.value)); } else { continue; } const label = document.createElement('label'); label.append(field.label, input); fieldset.append(label); } form.append(fieldset);}designer.on('progress', ({ sentBytes, totalBytes }) => { bar.value = sentBytes / totalBytes;});
// A thumbnail of each photo with the design on it, drawn again once the// customer's edits settle.const strip = document.querySelector('#views');const drawViews = () => { const views = designer.getViews().filter((view) => view.hasPhoto); strip.replaceChildren( ...views.map((view) => { const button = document.createElement('button'); const picture = document.createElement('img'); button.type = 'button'; picture.alt = view.label; button.append(picture); button.setAttribute('aria-pressed', String(view.viewId === designer.getView())); button.addEventListener('click', () => designer.setView(view.viewId)); designer.renderView(view.viewId, { maxEdgePx: 96 }).then((png) => { picture.src = URL.createObjectURL(png); picture.addEventListener('load', () => URL.revokeObjectURL(picture.src), { once: true }); }); return button; }), );};let redraw;designer.on('change', () => { clearTimeout(redraw); redraw = setTimeout(drawViews, 400);});designer.on('view', drawViews);drawViews();
// Approve shows the proof the customer agrees to, and Add to cart saves// that design; a change after it takes both away until the next approval.const proof = document.querySelector('#proof');const addButton = document.querySelector('#add-to-cart');const unapprove = () => { proof.hidden = true; addButton.hidden = true;};designer.on('change', unapprove);document.querySelector('#approve').addEventListener('click', async () => { error.textContent = ''; try { const { proofUrl } = await designer.approve(); proof.src = proofUrl; proof.hidden = false; addButton.hidden = false; dialog.close(); } catch { error.textContent = 'This design cannot be printed as it is. Check the fields above.'; }});addButton.addEventListener('click', async () => { const { designId } = await designer.commit(); await addToCart(designId);});productPhotos are your photos of the product, as in
Show your own product photos, and addToCart is your
storefront’s: your server orders the design with its id.
Questions? Write to [email protected].