Embed the designer
The designer is a widget you mount on your own product page. It loads a published template, shows your customer a live preview, and lets them fill in what the template allows. When they are done, it asks Pixonaut for a proof — the image of what will actually be printed — for the customer to approve.
It runs in the customer’s browser with your publishable key. Create one under API Keys in the studio, and list your storefront in its Allowed origins.
Loading the SDK
Section titled “Loading the SDK”Pixonaut serves every release of the SDK from its CDN, at an address of its
own that never changes: https://cdn.pixonaut.app/sdk/<version>/. Load it
from there, at the version you have tested against, with the integrity the
releases page lists for it:
<script src="https://cdn.pixonaut.app/sdk/<version>/pixonaut.iife.js" integrity="sha384-…" crossorigin="anonymous"></script>The classic script sets window.Pixonaut, which carries every export of the
module: Pixonaut.mount, and Pixonaut.createDesigner with
Pixonaut.designerUi for a server-rendered mount. To
load it as an ES module instead, preload pixonaut.esm.js with its integrity,
then import it from the same address:
<link rel="modulepreload" href="https://cdn.pixonaut.app/sdk/<version>/pixonaut.esm.js" integrity="sha384-…" crossorigin="anonymous"/><script type="module"> import Pixonaut from 'https://cdn.pixonaut.app/sdk/<version>/pixonaut.esm.js';</script>An import takes no integrity of its own: the preload checks the file, and
the import then takes the module the preload checked. A page that reads its
data on a server loads only the part of the SDK it still needs: see Load only
what a page uses. Without the preload, nothing checks the module
before it runs; with it, a changed file is never run, and the import fails.
The CDN is the way to load the SDK: it is not published to npm. You may
serve a release’s files from your own site instead, or bundle the whole SDK,
the core or the UI into it. You may not copy them into a theme, plugin or
package you distribute: the SDK’s licence allows them only on
your own site. The SDK finds its renderer beside its own script and checks the
renderer is the one it was released with before it runs any of it, so a
changed renderer is refused with a WasmIntegrityError naming the file. A
script served under its own name starts fetching the renderer while it is
still being run, before you mount anything: see
Shop themes and load time.
Each release stays served at its address, and keeps working against the live api, for as long as the releases page states: a release is served for longer than it works, and works for a while after the next one is out, so a store updates on its own schedule.
Licence
Section titled “Licence”The SDK’s licence is split by part:
- The client (
pixonaut-client.js) and the TypeScript declarations are MIT-licensed (LICENSE-MIT). Keep a copy of the client from the CDN in your own code and run it anywhere, a server included; code of any licence can type-check against the declarations. - The core, the UI, the whole bundle and the WebAssembly renderer are
proprietary (
LICENSE): you may use them only together with a Pixonaut account, and may load them from the CDN or bundle them into your own site. You may not redistribute them beyond your own site, or modify, reverse engineer, decompile or disassemble them or the WebAssembly.
Both licences are served beside the SDK and are in the tarball attached to
each release, with the third-party notices (THIRD_PARTY_NOTICES.md) for the open-source code it
contains.
The declarations are served beside the SDK as pixonaut.d.ts, each part’s
beside it (pixonaut-client.d.ts, pixonaut-core.d.ts, pixonaut-ui.d.ts),
with schema.d.ts, and are in the tarball attached to each release.
Pixonaut.version is the release you loaded. The renderer must be from that
same release: mount rejects with a WasmVersionMismatchError naming both
versions and where the renderer came from, since a renderer from another
release disagrees with the script about what it can do.
try { await Pixonaut.mount('#pixonaut', { publishableKey: 'pk_…', templateId: 'classic-tshirt@2' });} catch (err) { if (err instanceof Pixonaut.WasmVersionMismatchError) { console.error(`designer ${err.expected}, renderer ${err.found ?? 'unknown'}`); }}Content Security Policy
Section titled “Content Security Policy”A page with a Content Security Policy needs these directives for the designer, besides whatever it allows already:
| Directive | Why |
|---|---|
script-src 'wasm-unsafe-eval' blob:, https://cdn.pixonaut.app, and the wasmUrl origin if you pass one |
The renderer is WebAssembly, compiled in the page. Its script is checked before it runs and then loaded from a blob: address holding exactly the checked bytes. |
worker-src blob: |
The preview is drawn in a worker the designer starts from a blob: URL. |
connect-src the apiBase origin, the fontBase origin, https://cdn.pixonaut.app, and the wasmUrl origin if you pass one |
The api’s calls and uploads, the template’s font files, and the renderer’s files, fetched to be checked. |
img-src blob: |
The preview and the picture thumbnails are blob: images. The designer shows no data: image. |
img-src and connect-src the origins of your mockups photos |
The photo is shown, and read to shade the design against it. |
The designer adds no <style> element and no style attribute in markup; it
sets its styles from script, and the one stylesheet it adds (see
Under your theme’s styles) is built from script
too, which a policy without 'unsafe-inline' allows.
Mounting
Section titled “Mounting”The designer is an ES module whose default export is Pixonaut, or the
classic script that sets window.Pixonaut. One element is all it needs: the designer draws the preview and a form generated from the
template’s groups.
const designer = await Pixonaut.mount('#pixonaut', { publishableKey: 'pk_…', templateId: 'classic-tshirt@2',});mount takes a CSS selector or an element, and resolves once the template is
loaded and drawn. If the designer cannot start — the template or the designer’s
renderer does not load, or the placement you name
is refused — mount rejects with the reason and leaves nothing of the designer
on the page, so mounting again is safe. Whatever the element held before stays
there until the template has loaded, so a loading message survives a template
that does not; once the template is in, the designer replaces that content, and
a renderer that then fails to load leaves the element empty.
The preview and the form apart
Section titled “The preview and the form apart”A product page that shows the preview where its photo is, and the form in its
summary, mounts into two elements it places itself. The designer replaces
each element’s content, so give it two elements of its own, added inside
your gallery and your summary, rather than the gallery and the summary
themselves. What you print in them stays until the designer’s form is built
(the form’s element) and its preview drawn (the preview’s), and for good if
the mount fails, so a page rendered on your server can hold the designer’s
place at its final size: see Pages rendered on your server.
The shown event, and designer.shown(), which resolves then, say when the
designer has replaced what you printed: hide a placeholder of your own there.
<div class="product-gallery"><div id="pixonaut-preview"></div></div><div class="product-summary">… <div id="pixonaut-form"></div> …</div>const designer = await Pixonaut.mount({ previewTarget: '#pixonaut-preview', formTarget: '#pixonaut-form', publishableKey: 'pk_…', templateId: 'classic-tshirt@2',});Both take a CSS selector or an element, and they must be two elements, neither
inside the other. Place the form’s element outside your add-to-cart <form>
where you can. If it must sit inside it, pressing Enter in one of the
designer’s fields does not submit your form.
It is one designer: an edit in the form redraws the preview, onChange,
approve() and commit() work as with one element, and destroy() empties
both (with showForm: false, the form’s element is left as it was). The
preview takes its element’s full width, at the template’s shape, and follows it
as your theme resizes the element. Give that element a height in your CSS and
the preview is drawn inside it, as large as fits and centred, so switching
side, view, variant or photo never resizes it. The form, with the size picker
and the view switcher above it, takes its element’s. Pass viewsTarget too and
the view switcher is drawn into that element instead, such as under your
preview, as a counted, sideways-scrolling row of square photos,
--pixonaut-views-gap (12px) above it; it must be apart from the other two.
You place and size both, so neither part switches to the phone layout, and
fill is not offered for a split mount.
Options
Section titled “Options”| Option | What it does |
|---|---|
publishableKey |
Your publishable key. A mount takes this, shareLink or transport, never two. |
shareLink |
A designer share link’s token, in place of a key: the designer reaches only the template the link was made for, and the views and variants of the blank it is sold on. |
correctionLink |
A correction link’s token, with design, to correct a design your customer made. See Correcting a design. |
design |
With correctionLink: the id of the design to open. |
transport |
In place of a key: a function every request the designer makes goes through, for your server to send on with its own key. See Sending its requests through your server. |
templateId |
The published template to load. |
apiBase |
The address of the Pixonaut API. Without it, requests go to your page’s own origin. |
fontBase |
Where the template’s font files are fetched (<fontBase>/f/<hash>.ttf), when that is not apiBase. Without it, they are fetched under apiBase. The first preview fetches each one cut to the template’s characters, with a text query a proxy in between must pass on. |
wasmUrl |
Where the designer’s renderer, pixonaut_wasm.js, is loaded from. Without it, the designer loads it from beside its own script on the CDN; wherever it comes from, it must be the renderer the SDK was released with. See Loading the SDK. |
showForm |
false hides the generated form, for headless mode. |
showPickers |
false draws no print size picker, and none of the blank’s options, above the form. { size: false } leaves out the print size picker alone, and { variant: false } the blank’s options alone; a picker it does not name is drawn. |
showVariantPicker |
false draws none of the blank’s options but keeps the print size picker, for a store whose own variation form chooses the item with setVariant(); the same as showPickers: { variant: false }. |
showViews |
false draws no switcher between the product’s views, for one of your own. See Views and variants. |
showPrintAreaFrame |
true draws a thin grey frame around the print area on the product photo. Off by default: the print sits on the photo as it will be printed. A preview that fills its element stands on its stage and has no frame either way. |
showPlacementFrame |
true draws the placement frame, the ring and corner handles around a picture placed on the product photo, at all times. Off by default: the frame shows only while the shopper drags or sizes the picture, which they still do directly on the preview. |
accent |
Your accent colour, #rrggbb: the view, colour or tile chosen is ringed in it, a slider is filled in it up to its thumb, and Next is drawn in it, with black or white text, whichever reads. The selection on the canvas is drawn in it too: the frame around the selected layer, its handles’ white knobs, which fill with it while pressed, and the snap guides. Your page’s own --primary or --accent never reaches the canvas. Any other value is refused when the designer is mounted. Without it, the designer draws in its own ink. |
layout |
"page" or "steps": how the form lays out. See Page or steps. |
optionsStep |
{ title, content }: a first step holding the product’s pickers and your own fields. See Steps on your product page. |
lastStepContent |
An element of your page, such as your price, quantity and Add to cart, shown on the last step only. See Steps on your product page. |
formGroups |
{ groups }: the template’s groups and fields in your order, renamed for this mount, with your own elements placed among them. See Steps on your product page. |
previewTarget |
With formTarget, in place of the element mount is given: the element the preview is drawn into. See The preview and the form apart. |
formTarget |
The element the form and its pickers are drawn into, apart from the preview. |
viewsTarget |
With previewTarget and formTarget: the element the view switcher is drawn into, as a gallery of square photos. Without it, the switcher is drawn above the form. |
markTarget |
The element the Powered by Pixonaut mark is drawn into, apart from the elements the designer is drawn in: an element right under the card you draw around the preview, as wide as the card. Placed far from the preview, the mark no longer counts as shown. Without it, the mark sits under the preview. |
fill |
true makes the designer fill the one element you mount it in; not offered for the preview and the form apart. See Filling a page. |
selectOnClick |
Clicking a layer on the canvas selects it. On by default. |
snapping |
false turns off the template’s snap targets: a customer’s drag then snaps to nothing. |
upscale |
false offers no customer an upscale of a photo too small for the size they chose, whatever the template’s pictures offer. { belowDpi: 150 } offers one only for a photo below 150 dpi at that size. It narrows what the template offers and never widens it: an upscale still reaches the density the picture upscales to (“Upscale to” in the builder) and no further. |
previewDpi |
Resolution of the live preview, in dots per inch of the print. Without it, the preview is as many pixels wide as it is drawn on screen, at the screen’s pixel density, whatever the print size. |
previewMaxEdgePx |
Cap on the preview’s longest edge, in pixels, to keep large products cheap to draw. Defaults to 1100. |
onChange |
Called with the designer’s state whenever the customer changes something. |
onPrintSizeChange |
Called with { index, size } when another print size is chosen, in the designer’s size picker or by setPrintSize(). See Print sizes. |
onVariantChange |
Called with { ref } when another product variant is shown, in the designer’s pickers or by setVariant(), from the start: a variant picked before mount resolves is heard too, which on('variant') misses. The variant the product opens on is not reported. |
onError |
Called with { action, error } for every error event, and with action: 'mount' when the mount fails, before mount rejects. |
onValidation |
Called with the rules a design breaks when Pixonaut checks it. |
messages |
Your own wording for any of the designer’s strings. |
debug |
true logs to the browser console each request the designer makes and its answer, each event, the failures it otherwise keeps to itself, and hints for setting the link up (more sides than recommended), which a local host such as localhost gets without it. For diagnosing an embed; leave it off on a live store, as the lines carry the customer’s values. |
locale |
The language of the designer’s own words, one of en, sv, de, fr, es, nl, da, nb and fi. A regional tag uses its language ("de-CH" is German, "nb-NO" Norwegian Bokmål). Any other tag, or none, is the workspace’s default language. messages overrides any string on top. The template’s dynamic values, such as {date.long}, are written in the same language, except one whose name fixes its own. |
blankId |
The blank this design prints on. Given with placementId, never alone. See The product it prints on. |
placementId |
The print area on that blank. Given with blankId, never alone. |
panelLayout |
A panel layout of blankId to draw one design across, in place of placementId. See Draw across a panel layout. |
placements |
The placements of the blank the product sells, the sides of the item, each with the template printed there and, with enabled: false, left out until the customer chooses it. See Views and variants and Sides. |
sideValues |
"own" keeps each side’s keyed values apart; left out, a keyed value is on every side. See Sides. |
linkedPlacements |
Which of placements one design drives, such as a sweater’s two sleeves: a preset (suggested, all, each) or groups of your own, and whether a placement may be designed on its own. See Repeat a design and link sides. |
views |
The ids of the blank’s views the product is shown in, and 'design' for the Design view. Empty or left out, every view and the Design view. |
initialView |
The view shown first, one of placementId’s or 'design'. Without it, or naming another, the first view of placementId with a photo. |
variants |
The refs of the blank’s variants the product is offered in. Empty or left out, every one not retired. See Switching to another variant. |
initialVariant |
The variant shown first, one of variants. Without it, or naming one that cannot be shown, the first offered variant with a photo on placementId when there are options to choose; each view’s default photo when there are none. |
printSizes |
The sizes you sell this design at. See Print sizes. |
selectedPrintSizeIndex |
Which size starts selected. Defaults to the first. |
drawAtTemplateRatio |
true or false overrides whether the canvas keeps the template’s own shape at every print size (Templates). Left out, the template decides. |
mockups |
Your own product photos, shown instead of the blank’s. See Show your own product photos. |
initialValues |
The values the design starts from, as getState().values holds them. See Restoring a design in progress. |
Powered by Pixonaut
Section titled “Powered by Pixonaut”A small “Powered by Pixonaut” mark, with Pixonaut’s logo and wordmark, sits under the preview, never over it, so it is not taken for part of the print, and links to https://pixonaut.app. Drawing a card around the preview? Give the mark an element of its own under the card with markTarget. It shows on the Free and Starter plans. On Pro and Business it
shows until a member of your workspace turns it off in the studio’s branding
settings. The api decides it and sends the answer with the template the
designer opens, so there is no mount option, class or attribute that hides
it, and its own styles take precedence over your stylesheet’s. It shows in headless mode too, because the preview is still drawn. A
template you mount from memory (template) has no answer from the api, so
it shows the mark.
The template previews, showcases, catalogue images, proof images and print files the api renders never carry it, on any plan: show them as your own product images.
Page or steps
Section titled “Page or steps”The generated form lays out in one of two ways. As a "page", every group of
the template shows at once, each under a numbered heading. As "steps", the
form shows one group at a time under “Step 1 of 4” and the group’s name, with
Back and a “Next: Message” button naming the step that follows. Back and Next
are the form’s footer: when your page gives the designer’s element a height,
the step scrolls inside the form and the footer stays on its bottom edge;
otherwise the footer ends the step. In a narrow column the stepper drops the
step names and the counter reads “1 of 4”.
The layout option chooses. Without it, the template’s own layout decides,
as its author set it, and a template with none is a page, so an embed that
never passes layout looks as it always has. A group that offers the
customer nothing to fill in or place is left out in both layouts, and the
steps are numbered as the page numbers its headings.
Next is never blocked: a broken rule shows on its field, as it does on a page,
and approve() still checks the whole design. When approve() or commit()
is refused, the form moves to the first step holding a broken field, so the
customer sees what to fix. The last step has no Next and no finishing button:
adding to the cart or approving is your page’s button, outside the designer.
While the form is shown as steps, getStep() says which step it shows and
whether it is the last, setStep(index) moves it, and optionsStep and
lastStepContent draw your page’s own option fields and cart row inside the
steps. See Steps on your product page.
The stepper’s words are messages like any other: form.step-counter
(“Step {index} of {count}”), form.step-back (“Back”) and form.step-next
(“Next: {label}”). On a form too narrow for the steps’ names the counter is
form.step-counter-short (“{index} of {count}”), or your own
form.step-counter when you override that one and not the short form.
On a phone
Section titled “On a phone”The designer lays out by the width of the element you mount it in, not the
screen’s. When that element is too narrow for the preview and the form side by
side, the preview takes the whole width with the form under it, and the
element gets data-device="phone" ("desktop" otherwise) for your own
styling to follow. In steps, Back and Next stay at the bottom of the screen
while the step scrolls, inside the designer, so they never cover your page’s
own bars. When your page bounds the element’s height, with a height or
only a max-height, the preview takes at most 45% of it, the mark under it
included, narrowing for a tall plate, so the step keeps room to be filled in.
How long the designer takes to load on a phone, and what of, is in
Shop themes and load time.
Filling a page
Section titled “Filling a page”By default the designer flows in your page like any other content. A page or
window that holds nothing but the designer can pass fill: true instead. The
element you mount into then needs a height. The designer fills it: the preview
is centred on a stage, as large as fits, with a caption under it, and the
form sits in a column beside it that scrolls by itself. On a phone the
preview stays at the top and the whole form scrolls under it, the steps’
header included: unlike the embedded phone layout, the header is not lifted
above the preview. The phone layout
starts where the stage and the form column no longer fit side by side: at the
same width as without fill, unless you make the column wider.
The stage and the form column are yours to style, through custom properties set on the element:
| Property | What it sets | Default |
|---|---|---|
--pixonaut-stage-background |
The stage behind the preview. | transparent |
--pixonaut-stage-padding |
Space kept clear around the preview on a desktop, as CSS padding. Reserve room here for anything of your own drawn over the stage. |
24px |
--pixonaut-form-width |
The form column’s width. | 260px |
--pixonaut-form-background |
The form column’s background. | transparent |
From a script tag, set the properties by the names in this table; module
imports can read them from the exported FILL_PROPERTIES.
The caption is the message preview.caption (“Live preview · updates as you
fill in the form”), shown on a desktop only.
A text field with a maximum length shows how much is used, as the message
form.char-count (“{count} / {max}”), and, when the template gives it no
help text, its limit, as form.char-limit (“Up to {max} characters”).
Approving and committing
Section titled “Approving and committing”approve() asks Pixonaut to check the design and render its proof:
const { proofUrl, proofToken } = await designer.approve();Show proofUrl to the customer: it is the picture they are agreeing to. A
design breaking a template rule rejects with a PixonautApiError (422,
code "constraint_violation") carrying the violations, which
onValidation also receives, and the built-in form tells each at its field
until it is changed. A required picture left out (form.violation.required)
or a default kept that must change (form.violation.unchanged) is refused so
before any request is sent. A text too long for its box is told sooner, as
the customer types or picks a font or size.
The proof also records the variant the customer saw, and the view of the side shown, for display: your order screens can show the design on the colour and in the photo it was approved in. Neither is sent before the designer has one, and neither is checked: an order names its own variant.
So that the proof shows the design as it stands, setLocked(true) keeps the
customer from changing it while the proof is made: the preview takes no
gesture, not even a drag already under way, and the form takes no input.
setLocked(false) hands it back, as when approve() rejects.
This is the customer approving what they made, not your shop approving the
design. Hand the proofToken to your server with the cart, and your server
commits the design with its secret key (a connected store, with its
integration key), attaching any order details your
printshop needs. The design to order guide
takes it from there.
commit() checks the design and commits it in one step, with no proof for
the customer to approve first, and resolves the new design’s id:
const { designId } = await designer.commit();It is what the hosted designer page calls when the customer is done and its
link has no store to return to. It rejects on a broken rule as approve()
does.
To list the customer’s choices in your cart, see Listing the customer’s choices.
When Pixonaut refuses a request the designer makes itself (loading the
template in mount, approve() or commit()), it rejects with a
PixonautApiError. Its status is the HTTP status, so you can tell a
credential that no longer works (401), a template that is gone (404) and too
many requests (429) apart, its code is Pixonaut’s error code when the
answer names one, and its requestId names the request when the answer
carries one. A broken rule carries violations as well as its code, so
tell it from any other refusal by violations, not by whether there is a
code. A designer share link at its design limit is refused with the code
link_design_limit, and reopensAt says when the link makes designs again
(epoch milliseconds), or is null for a limit in total.
Correcting a design
Section titled “Correcting a design”A customer who wants to change a design they have already made — from the cart, or after asking for changes — corrects that design rather than making a new one. Your server hands out a correction link for it with its secret key, or a connected store with its integration key:
POST /v1/designs/{id}/correction-links→ 201 { "token": "…", "expiresAt": 1791900000000 }and your page mounts the design with it:
const designer = await Pixonaut.mount('#pixonaut', { correctionLink: token, design: designId,});The link works for a couple of hours, for that design only, from the origins your publishable key allows. The design opens as it is now, at the size it was bought at, so there is no size menu, and the customer may swap in a picture of their own.
To have the page look as the product page did, pass the same product options
as there: blankId, placementId, placements, views, initialView,
variants, and printSizes with selectedPrintSizeIndex. The design is then
drawn on the blank’s photos with the view switcher, in the box it was bought
at:
const designer = await Pixonaut.mount('#pixonaut', { correctionLink: token, design: designId, blankId: 'blank_apron', placementId: 'front', views: ['flat', 'design'],});The blank and placement must be the ones the design was made on, which
GET /v1/designs/{id} answers as blankId and placementId; any other
rejects mount() with a TypeError. A correction changes that one design, so
of placements only its own side is shown.
Without these options the design is drawn on its own.
approve() renders the corrected proof and changes nothing:
const { proofUrl, proofToken } = await designer.approve();Your server makes it the design’s next revision with its secret or
integration key, sending
{ "proofToken": "…" } to PATCH /v1/designs/{id}. The design then waits to
be approved again, as after any correction. If the design was corrected
elsewhere since your page opened it, that call is refused
revision_changed, and the customer should start again from the design as it
is now. commit() is refused on a correction.
The hosted correction page does the same without your page: see Sending the customer back to the store.
Headless mode
Section titled “Headless mode”To draw your own form, mount with showForm: false and build your UI from the
same steps the generated form uses. The preview is still followed by the
Powered by Pixonaut mark where your workspace shows it. Build your own designer
lists every method and event your UI can use:
const designer = await Pixonaut.mount('#canvas', { publishableKey, templateId, showForm: false,});
const { steps } = designer.getFormSchema();for (const step of steps) { for (const field of step.fields) { // field.type: 'text' | 'number' | 'color' | 'select' | 'image' | 'crop' // fieldLabel(field, msg) is the input's heading (see below): // field.label is the prop's key when the template names none. // field.description, when the template has one, is help text to show // under it, as text rather than markup. // An image field: render an upload control and call // const uploaded = await designer.uploadAsset(file, field.layerId) // When uploaded.improveResolution is set, the picture is short of the // resolution its picture offers an upscale below (the `upscale` option // may withdraw the offer) and you may offer a sharper copy, paid from // your credits, at the size it names: the picture's "Upscale to" // resolution, no larger. It replaces the upload and keeps the crop: // await designer.improveResolution(uploaded.assetId, field.layerId) // A crop field: see "Cropping a picture" below. // Anything else: // designer.setValue(field.layerId, field.prop, value) // A colour is stored as the CMYK it prints as, { cmyk: [c, m, y, k] } // in percent; a screen colour, { rgb: [r, g, b] }, is stored as the // CMYK the press prints it as. } // Each layer the customer may move, rotate or resize, in step order. for (const { layerId, transform } of step.transforms) { // designer.selectLayer(layerId) shows its handles on the canvas. // Each axis (transform.translate, .rotate, .scale, .resize) may carry // the template's label and description for its control. }}The generated form has no number fields for a layer’s position, rotation or
size: the customer drags the layer on the canvas. A layer whose flip the
template offers gets a Horizontal and a Vertical button. To offer numbers anyway,
build your own inputs over setTransform:
designer.setTransform('name', { xMm: 120, rotationDeg: 10 });It takes any of xMm, yMm, rotationDeg, scale, wMm and hMm, and
flipX and flipY (true mirrors the layer left to right or top to bottom),
redraws the preview and calls onChange, whose state carries the layer’s
transform under values['<layerId>.transform']. That is also where to read
the current numbers from after a drag. Pixonaut checks the transform against
the template’s limits when the design is approved, as it does every value.
It writes the values as given, with none of the clamping a drag applies, so
your inputs should hold to the axes the step’s transforms in
getFormSchema() offer. A size control of your own can call
scaleLayer(layerId, scale) instead, which holds the scale to the layer’s
bounds as its handle does.
A text layer’s offered size is a slider in the generated form, labelled with
the form.font-size message unless the template names it. The transform.*
messages that labelled the old number fields are gone; drop them from your
messages.
A customer places a layer on the preview: a tap selects it and its handles
move, rotate and resize it. A step with layers the customer may move ends
with one hint saying so (form.drag-hint-one, form.drag-hint, or the
author’s name for the move when the step moves one layer) and a Reset
(form.drag-reset) that puts back every layer the step places, a turned or
sized one too. A step whose layers may only be turned or sized has no hint.
A layer whose scale is offered also gets a size slider
after its fields, labelled form.scale unless the template names it. The
“Place” buttons are gone, and with them form.place-step and
form.place-layer; drop them from your messages.
If you translate every message, these keys are new and show in English
until you add them: form.drag-hint-one, form.drag-hint,
form.drag-reset, form.scale, form.step-counter-short and
form.char-limit. upload.button now reads “Upload an image”.
A field the template leaves unlabelled is headed with the form’s word for
what it sets, never the attribute’s internal name. Each word’s key is
form.label. and the field’s prop: form.label.content,
form.label.fill, form.label.color, form.label.imageAssetId,
form.label.shape, form.label.src, form.label.crop,
form.label.fontFamily, form.label.fontWeight, form.label.align,
form.label.valign and form.label.opacity; these form.label. keys are
new too. A text size keeps form.font-size. A label the template gives is
shown instead. A form of your own over getFormSchema() gets the
same heading from fieldLabel(field, msg), where msg answers a message
key (FieldWordKey), read from your own messages. Both functions are
named exports of the designer’s module:
const m = resolveMessages(myMessages);const heading = fieldLabel(field, (key) => m[key]);A text’s outline fields are drawn together under the heading
form.group.outline (“Outline”), and its shadow fields under
form.group.shadow (“Shadow”). Each heading appears only when the
template offers one of its fields.
Cropping a picture
Section titled “Cropping a picture”A picture whose template offers its crop has a crop field: an image
layer’s, a shape layer’s picture fill’s, or the image background’s, whose
field’s layerId is the artwork step’s $artwork. Its value
is { xFrac, yFrac, zoom }: zoom scales the picture inside its frame,
within the field’s constraints.minZoom and constraints.maxZoom (1 is the
picture just covering the frame), and xFrac and yFrac move the picture’s centre right
and down, in fractions of the frame’s width and height. The frame is the
layer’s box, and for the background the print box without its bleed; a
print adds the bleed on every side. The field’s default
is the template’s crop.
designer.setValue('photo', 'crop', { xFrac: 0.1, yFrac: -0.05, zoom: 1.5 });designer.setValue('photo', 'crop', null); // back to the template's cropdesigner.setValue('$artwork', 'crop', { xFrac: 0, yFrac: 0.2, zoom: 1.2 });A shift past the picture’s edge is drawn at the edge, so the picture always
covers its frame. A zoom outside the offered range is refused when the design
is approved. A new picture starts from the template’s crop: choosing a preset,
uploading a picture, or setting an image’s src, a picture fill or the
background’s imageAssetId sets a crop the customer made back to null.
The generated form draws the crop as a zoom slider, a control that lets the
customer drag the picture inside its frame on the preview (for the
background, a drag anywhere on the preview), and a reset. Your own
control turns that drag on with setCropMode({ kind: 'layer', layerId })
or setCropMode({ kind: 'background' }), off with setCropMode(null), and
hears it change with on('crop', …). While
that control is on, the frame takes focus and the arrow keys move the picture
too (Shift for a larger step). When a zoom spreads an uploaded picture’s pixels
too thinly for the minimum print resolution at the chosen size (the template
read’s printFloorDpi: the template’s own floor, else the workspace’s), the
crop field repeats the warning.asset.low-resolution message. Its words are the form.crop-zoom,
form.crop-zoom-value, form.crop-move, form.crop-done, form.crop-reset
and form.crop-surface messages; the last names the frame for assistive
technology.
getFormSchema() returns the same schema the API serves for the template,
with the author’s layout when the template sets one.
getState() returns the template id, the customer’s values and the chosen
print size, so you can keep a design in progress. A picture still uploading
writes its value when the upload ends: await designer.settled() first when
you keep the values as the customer leaves. destroy() removes the designer
from the page.
Restoring a design in progress
Section titled “Restoring a design in progress”To mount the designer again with what the customer entered, after a page
reload or for a product you mount anew, pass the values you kept from
getState() as initialValues:
const kept = designer.getState().values;// …later, or on the next page…const again = await Pixonaut.mount('#designer', { ...options, initialValues: kept });Once mount() resolves, the values are on the form, in the preview and in
getState(), as they were when you kept them, a picture’s crop included;
onChange is not called for them. A value for a layer the template no
longer has is left out, with a line in the console naming it.
values holds the template’s values by layer id. A value the customer set on
a setting with a field key is under
values.$fields, by its key, instead; the values of other templates the
customer looked at with setTemplate() are under values.$templates, by
template id. Keep values whole, and both come back with it.
Handles can reach a little past the canvas edge. If the canvas sits inside a scrolling container, give that container enough padding for the rotate handle above the canvas, or the handles are cut off at its edge.
The product it prints on
Section titled “The product it prints on”Name the blank and the placement on it that the design is for:
blankId: 'blank_tee',placementId: 'front',The proof is then fitted to that placement, and is the print the customer
gets: its size, bleed, safe area and print method.
approve() and commit() both send the pair, and a design committed from the
proof token records it without your server naming it again.
The live preview is fitted to the same placement: it is drawn at the size the proof prints at, with the placement’s bleed and safe area, and the checks it runs as the customer types and drags use that box too. What the customer sees while designing is framed as the proof they approve.
Give both or neither. A placement that does not exist, or a blank you do not
hold, is refused when the designer mounts: mount() rejects with an
PixonautApiError whose code is unknown_placement, before the customer has
entered anything. Without either, the design is fitted to the template’s own
shape, with no product.
Views and variants
Section titled “Views and variants”A blank’s photos are grouped into views, each one way of photographing one placement (“Flat”, “On model”), with a photo per colour. The designer shows the design on them: it reads the blank’s views and variants when it mounts and draws the photo of the view shown behind the design.
Say what your product sells: the placements, each with the template printed there, and, if not all of them, the views:
const designer = await Pixonaut.mount('#designer', { publishableKey: 'pk_…', templateId: 'logo-front@3', blankId: 'blank_tee', placementId: 'front', placements: [ { placementId: 'front' }, { placementId: 'back', templateId: 'logo-back@1' }, ], views: ['front-flat', 'back-flat'], initialView: 'front-flat',});placementIdis the one the designer opens on, withtemplateId, and must be one ofplacements. A placement that names no template prints the mount’stemplateId. Withoutplacements, the product sellsplacementIdalone.viewslists the views shown, by id, asGET /v1/blanks/{id}/mockupsanswers them. Empty or left out, every view of the placements the product sells, and the Design view. A placement it does not sell gets no view.initialViewis the view shown first, one ofplacementId’s: the designer opens on that placement. Without it, or naming a view of another placement, the first view ofplacementIdwith a photo. To open on the back, name the back asplacementId.
The designer draws a strip of thumbnails, one per view, each showing the customer’s design on that view at thumbnail size, drawn again once their edits settle. Clicking one shows that view; the strip is keyboard-operable, and each thumbnail is labelled with its view’s label. With one view, no strip is drawn.
The Design view follows the photographed views: the customer’s artwork
alone, on a transparency checker, with no photo behind it. Its id is
'design' (DESIGN_VIEW_ID), its label the message views.design
(“Design”). It shows the side the designer is on, and stays as it is when
the customer picks another colour. To leave it out, name the views you want
in views without it; to keep it beside named views, name 'design' too.
When the blank has a view of its own with the id design, that view is shown
and there is no Design view.
Switching to another view
Section titled “Switching to another view”await designer.setView('back-flat');A view of another placement switches the designer to that side,
with its own template and its own values: what the customer entered in a
setting with a field key is on it too,
and the rest of what they entered stays on the side they entered it on. setView()
resolves once the view is shown, and on("view") reports it. A view the
product does not show, or one with no photo of the variant shown, rejects with
a TypeError.
An approve() still waiting for its proofs when the side shown switches
rejects with a PlacementChangedError, rather than resolving proofs of a
design the customer has since moved on from; ask for the proofs again, and
drop any proofToken you received before the switch. A designer opened on a design to
correct keeps the placement the design was made for.
The placements a product sells are the sides of one item: a tee’s front and back, a sign’s two faces. The designer holds a design for each side, with its own template and its own values, and shows one at a time:
await designer.setSide('back');setSide() shows the side in its first view, as a view of it does, and
on("side") reports { placementId } whichever switched it. setTemplate()
switches the template of the side shown; the other sides keep theirs.
A keyed value is the item’s. A name typed on the front is on the back
wherever the back’s template has a setting with the same
field key, as one shirt carries one name.
Pass sideValues: 'own' to keep each side’s keyed values apart instead. A
value in a setting without a key stays on its side either way, also when
both sides print the same template.
getSides() lists every side, in placements order:
[ { placementId: 'front', templateId: 'logo-front@3', enabled: true, valid: true, linked: null }, { placementId: 'back', templateId: 'logo-back@1', enabled: true, valid: false, linked: null },]valid judges each side’s design against its own template in the browser,
as the form judges the side shown, so a name too long for the back’s box
marks the back while the customer types on the front. Every side’s template
is read when the designer mounts, and mount() resolves once they are.
A side can be optional. A product sold single- or double-sided leaves the
back out with enabled: false on its placements entry, and turns it on and
off with the customer’s choice:
designer.setSideEnabled('back', doubleSided.checked);A side left out keeps its values, is not proofed and is not committed. The
last side enabled cannot be left out: setSideEnabled() throws a
TypeError, as does a mount whose placements enable none.
approve() and commit() take every enabled side. approve() resolves
one proof per side under sides, in placements order, with the first
side’s proof on top, so a product with one side reads its proof as it always
has:
const { proofUrl, sides } = await designer.approve();// sides: [{ placementId: 'front', proofUrl, proofToken, … }, { placementId: 'back', … }]commit() does the same with designs: one per side, for
one order of the item, the
first side’s first. When any side’s design breaks a rule, they reject with
that side’s PixonautApiError, whose placementId names it; with several
refused, the first in placements order. onValidation hears only the side
shown’s violations, which its form marks; show the side named to let the
customer fix the rest. commit() checks every side before it commits any.
The state names the side shown as placementId, and holds the other sides
under $sides in its values, each on the template it was last shown in. To
mount the designer again where the customer left it, pass both back:
const state = designer.getState();// laterawait Pixonaut.mount('#designer', { ...options, placementId: state.placementId, templateId: state.templateId, initialValues: state.values,});Switching to another variant
Section titled “Switching to another variant”When the customer picks another variation of your product (another colour),
switch the designer to the blank’s variant for it, by its ref from
GET /v1/blanks/{id}/variants:
await designer.setVariant('v_black_m');Every view then shows that colour’s photo, and on("variant") reports the
switch. A view with no photo of the variant is hidden while it is shown; if
it was the view shown, the first view with a photo is shown instead. A colour
is a variant of the one blank, never another blank, so the design, the
placement and the print stay as they were.
On a mount with a blank, the designer draws a row above the form for each of
the blank’s options with two values or more, in the blank’s order. How a row
looks follows its values’ appearances, which you set on the blank, never the
option’s name: swatches when any value has one (a flat colour, two colours
half and half, or the value’s picture; a value without one is a plain circle
with its initial), with “Colour · Navy” naming the one chosen; text chips
otherwise. Each row shows 20 values, then “+N more”. Choosing a value
switches the variant as setVariant() does, keeping the other options’
values when a variant has them; a value with no photo in any view is faded
and cannot be chosen, unless no variant has one (all are offered then). variants narrows the variants offered;
initialVariant names the one the designer opens on (without it, the first
offered with a photo), whose values show as chosen. For your own picker, pass
showPickers: false (or showVariantPicker: false, to keep the print size
menu) and read getOptions(), each option’s values with their
appearance ({ kind: "fill", colours }, { kind: "image", url } or null),
and getVariants(), each variant with its value on each option, hasPhoto, and printable, false at a size the design can’t be printed at
(a cut-and-sewn XL whose panel is another shape), which the default picker disables. A variant’s colour and size are deprecated.
setVariant() rejects with a VariantUnavailableError, and the variant shown
stays, for a variant the designer cannot show. Its reason says why:
reason |
Why |
|---|---|
no_photo |
No view the product shows has a photo of it, though some have a photo of another variant. Treat the variation as unavailable on your page. |
variant_retired |
The variant is retired: no new design or order may name it. |
unknown_variant |
The ref is not one of the blank’s variants. |
not_offered |
The mount’s variants do not name it. |
Preloading the other variants, views and sides
Section titled “Preloading the other variants, views and sides”The designer loads a photo, and a side’s template and pictures, when the
customer first switches to it. To make switching instant, it can load them
ahead: preload: 'background' one at a time in idle time once the first view
is shown, 'eager' all at once, 'off' nothing, and 'auto' (the default)
one of those chosen from an estimate and the device. Preloading spends the
customer’s memory, bandwidth and CPU, and may slow the page on a low-end
device; auto chooses from an estimate, since a page cannot measure its own
memory. See Make switching instant for the modes,
preload(), cancelPreload(), preloadPlan() and on("preload").
Your own view switcher
Section titled “Your own view switcher”With showViews: false the designer draws no strip, and your page draws its
own from the calls the strip is made of:
const views = designer.getViews();// [{ viewId: 'front-flat', label: 'Flat', placement: 'front', hasPhoto: true,// photoUrl: 'https://…/front-flat-white.jpg', dye: null, kind: 'photo' }, …,// { viewId: 'design', label: 'Design', placement: null, hasPhoto: true, photoUrl: null, dye: null, kind: 'design' }]
for (const view of views.filter((v) => v.hasPhoto)) { const png = await designer.renderView(view.viewId, { maxEdgePx: 200 }); thumbnail(view).src = URL.createObjectURL(png);}designer.on('view', ({ viewId }) => markShown(viewId));designer.on('variant', () => redrawThumbnails());getViews()returns each view the product shows, for the variant shown: its id, label and placement, whether it has a photo of the variant, that photo’s url (photoUrl, null without one), the colour a view drawn from a 3D render dyes its garment in (dye, null for none), and itskind:'photo', or'design'for the Design view, which needs no photo and is alwayshasPhoto: true. Hide one withhasPhoto: false. A variant leaving a view’sphotoUrlanddyethe same (another size) changes nothing it shows.getView()is the view shown, andgetVariant()the variantsetVariant()named last (undefined before).renderView(viewId, { variant, maxEdgePx })resolves with a PNG of that view with the customer’s design on it: invariant’s photo (the variant shown when left out), its longest edgemaxEdgePxpixels (240 when left out). The design is the preview as it is drawn at that moment, shaded for the photo shown, and is drawn flat onto the view’s photo; draw your thumbnails again once the customer’s edits settle, onchange. A picture asked for before the preview’s first frame waits for it. Avariantof another tone is drawn unshaded, with its colour’s values. A view of another side is drawn with that side’s design as last shown, and before it has been shown, with its photo alone. The Design view’s PNG is the design alone and unshaded on a transparent ground, the same for everyvariant: draw a checker behind it yourself. It rejects assetView()does, and when the photo cannot be read: your own photos must be served with CORS headers.
setMockup(), getMockup(), selectedMockupIndex and onMockupChange are
gone: a photo is named by its view and the variant, never by its place in a
list. So is setPlacement(): a colour is a variant of the one blank, and a
placement is reached through its view.
Other templates
Section titled “Other templates”When your store lets the customer try another template, setTemplate()
switches the designer to it, carrying what they entered by field key, and
renderTemplate() draws any template with their values for a gallery of your
own: see Offer several templates.
Print sizes
Section titled “Print sizes”Pass the sizes you sell, in your own order and with your own labels:
printSizes: [ { widthMm: 300, heightMm: 400, label: '30 × 40' }, { widthMm: 500, heightMm: 700, label: '50 × 70' },],selectedPrintSizeIndex: 1,The designer draws a picker, and marks the sizes the template’s aspect ratio and tolerance refuse. Labels are shown exactly as you give them. The chosen size goes with the proof, and is the size the design is printed at.
On the blank’s photos the design is drawn at the chosen size: a size larger
than the placement’s usual one reaches past the usual area, as far as it will
print. Your own photos (mockups) mark only where the print goes, so the
design fills that area at any size.
In headless mode, getPrintSizes() returns each size with legal and, when
refused, a reason; setPrintSize(index) chooses one. The browser checks a
size against the template only, not against the maximum print area of a
product. To check both before a customer starts, ask the API whether the
size is legal for the template and placement.
Omit printSizes, or pass a single size, and no picker is drawn; a single
size still goes with the proof.
A size picked in the designer’s picker is heard by onPrintSizeChange with
{ index, size }. A store whose prices differ by size drives the size from
its own variation form instead, with setPrintSize(index) and
showPickers: { size: false }.
Your own photos
Section titled “Your own photos”For photos of your own rather than the blank’s, see Show your own product photos.
Questions? Write to [email protected].