# PopCraft > PopCraft is a design app that runs in the browser and offline: design, draw, pixel art, comics, whiteboards, prototypes and a motion studio (keyframes, compositions, audio, 3D, shaders) in one file format, rendered on the GPU. It exports images, PDF, video, Lottie, code and interactive web pages, and is extensible with plugins, themes, an agent and a REST API. This file follows the llms.txt standard: the docs index, then the complete reference for PopCraft's file formats — the `.popcraft` document, templates, libraries, the plugin manifest and the export formats — generated from the source code. The text of every docs page is in llms-full.txt: https://popcraft.app/llms-full.txt ## Introduction - [PopCraft Docs](https://popcraft.app/docs): Design, draw, animate and ship — and build on top of it all. ## Getting Started - [Getting started](https://popcraft.app/docs/getting-started): Open the editor and make your first file. - [Templates](https://popcraft.app/docs/getting-started/templates): Start from a finished file — anime shorts, music videos, game UI kits, photo slideshows and hundreds more — and make it yours. - [The editor window](https://popcraft.app/docs/getting-started/the-editor): What every panel in the editor is for. - [Keyboard shortcuts](https://popcraft.app/docs/getting-started/keyboard-shortcuts): The bindings worth learning, and where to find the rest. - [Language](https://popcraft.app/docs/getting-started/language): Choose the language of menus, dialogs and messages, and how right-to-left works. ## Designing - [Designing](https://popcraft.app/docs/designing): The Design mode toolset — frames, shapes, text, components. - [Frames and pages](https://popcraft.app/docs/designing/frames-and-pages): Artboards, sections, auto layout and the things that keep a canvas tidy. - [Shapes and vectors](https://popcraft.app/docs/designing/shapes-and-vectors): Paths, booleans, the shape builder and masks. - [Text](https://popcraft.app/docs/designing/text): Type, OpenType features, text on a path, text in a shape, auto-size, and text effects. - [Fills and effects](https://popcraft.app/docs/designing/fills-and-effects): Gradients, images, patterns, shaders, shadows, blurs and halftones. - [Components, styles and variables](https://popcraft.app/docs/designing/components-and-styles): Make something once and use it everywhere. - [Generating video](https://popcraft.app/docs/designing/generating-video): Make a short clip from a prompt or a start frame with Seedance, Veo or Grok, and place it as a video layer or a scene. - [Photos](https://popcraft.app/docs/designing/photos): Openly licensed stock photos, searched in the app, saved into your file with their credit. - [Media library](https://popcraft.app/docs/designing/media-library): Every picture, clip and sound your team's files already hold — uploads and generated media — browsed by project and file. - [Migrating from Microsoft Publisher](https://popcraft.app/docs/designing/migrating-from-publisher) ## Drawing - [Drawing](https://popcraft.app/docs/drawing): Draw mode, the brush engine and the brush library. - [Procreate brushes](https://popcraft.app/docs/drawing/procreate-brushes): Import .brush and .brushset files. ## Pixel Art - [Pixel art](https://popcraft.app/docs/pixel-art): The sprite studio, inside your design file. ## Comics - [Comics](https://popcraft.app/docs/comics): Panels, balloons, gutters and halftones — comic pages as a first-class thing. ## Boards - [Boards](https://popcraft.app/docs/boards): PopCorn — whiteboards that live in the same file as your design work. ## Prototyping - [Prototyping](https://popcraft.app/docs/prototyping): Turn frames into something you can click through. - [Animation](https://popcraft.app/docs/prototyping/animation): Motion presets per element, and the keyframe timeline. - [Presenting](https://popcraft.app/docs/prototyping/presenting): Slideshows, speaker notes, screen sharing in a call and a second screen. - [Drags, scrolls and hand-offs](https://popcraft.app/docs/prototyping/journeys): Scrub a timeline with a drag or a scroll, play and pause it from a button, and hand layers from screen to screen. - [Variables and logic](https://popcraft.app/docs/prototyping/variables-and-logic): Expressions, conditionals and live values — prototypes that keep score. - [Web preview](https://popcraft.app/docs/prototyping/web-preview): See a page as a real website beside the canvas, and click through it as you design. - [Breakpoints and web pages](https://popcraft.app/docs/prototyping/responsive): Design one page for a desktop, a tablet and a phone, and say what each layer is on the web. - [Forms and collections](https://popcraft.app/docs/prototyping/forms-and-content): Make layers real form controls, keep a page's content as records, and build a list and a page per record from one design. - [Code components](https://popcraft.app/docs/prototyping/code-components): Draw a component with your own React code. It runs on the web page and shows as a picture on the canvas. ## Animating - [Animating](https://popcraft.app/docs/animating): Animate mode is a motion studio in your design file: keyframes, curves, behaviours, text, 3D, scenes, sound, video, and exports to MP4, GIF and Lottie. - [Your first animation in 2 minutes](https://popcraft.app/docs/animating/first-animation): From a still design to something that moves: switch to Animate, add a preset to two layers, play it. - [Keys and the timeline](https://popcraft.app/docs/animating/keyframes): Record keys, pick and move them, markers and the work area, layer time, onion skin, and every Animate shortcut. - [Curves](https://popcraft.app/docs/animating/curves): How a value travels from key to key: easing presets, springs, hold, the curve editor, and the graph editor's value and speed curves. - [Motion paths](https://popcraft.app/docs/animating/motion-paths): A layer's path across the canvas: its key dots, bezier handles to bend it, and the anchor point it turns around. - [Presets and stagger](https://popcraft.app/docs/animating/presets-and-stagger): One-click entrances, exits, emphasis, camera moves and text cascades, and the rhythm between layers. - [Behaviours](https://popcraft.app/docs/animating/behaviours): Motion that runs on its own: spin, drift, pulse, wiggle, bounce, loops, wrap, clamp, link and audio, with or without keys. - [Text animation](https://popcraft.app/docs/animating/text): Per-letter, per-word and per-line animators, typewriters, and numbers that count. - [Compositions and transitions](https://popcraft.app/docs/animating/compositions): Scenes in sequence: each frame plays its own timeline, with transitions between them, a stage and sound. - [3D](https://popcraft.app/docs/animating/3d): Real 3D in a group or frame: tilt, turn and depth, a camera to move, lights and materials. - [Shaders and effects](https://popcraft.app/docs/animating/shaders-and-effects): Live GPU shader fills whose params you can key, texture and noise fills, and effects that animate. - [Audio](https://popcraft.app/docs/animating/audio): Sound on a composition: import, generate, lanes, trims, fades, gain keys, pan, the mix, beats and cutting to the beat. - [Lottie templates](https://popcraft.app/docs/animating/lottie-templates): Sector packs of animated icons, loaders, states, explainers and hero loops, drawn to export to Lottie exactly as they play. - [Audio-reactive motion](https://popcraft.app/docs/animating/audio-reactive): Make anything follow the music: loudness, bass, mids, treble, beats and the spectrum driving layers, colours, effects and shaders, the MilkDrop-style visualizers, the Audio reactive library and the music-video templates. - [Video](https://popcraft.app/docs/animating/video): Video fills that play on the timeline, and short clips generated with AI, placed as a layer or as a scene. - [Motion components](https://popcraft.app/docs/animating/motion-components): Components that animate, their inputs, how each instance plays, variant states, and motion libraries. - [Shader graph](https://popcraft.app/docs/animating/shader-graph): Shader fills built from nodes wired together — every built-in preset — and the knobs every unwired input becomes. - [Playback and Stage view](https://popcraft.app/docs/animating/playback-and-stage): Playing, looping and scrubbing the timeline, solo, instance playback, and Stage view: the active frame alone, like a video player. - [Exporting](https://popcraft.app/docs/animating/exporting-video): MP4, WebM, GIF, PNG sequences and Lottie of a frame's timeline or a composition, with sound, up to 120 fps; and importing Lottie. - [Presenting and the web](https://popcraft.app/docs/animating/presenting): Builds that wait for a click, animation in present mode and prototypes, smart animate, and motion on exported web pages. - [State machines](https://popcraft.app/docs/animating/state-machines): Make a frame's timeline interactive: states that each play a stretch of it, inputs, transitions with conditions, and listeners on layers. Plays in Present, exports to dotLottie and the web. - [Animating with the agent](https://popcraft.app/docs/animating/agent): Every Animate action as an agent tool: ask for motion in words. - [Every feature](https://popcraft.app/docs/animating/features): The complete list of what Animate does, each linked to the page that explains it. Checked against the code by the docs tests. ## Exporting - [Exporting](https://popcraft.app/docs/exporting): Getting the work back out — images, vectors, documents and video. - [Code and web export](https://popcraft.app/docs/exporting/code-and-web): Turn a frame into CSS, HTML, React, Tailwind, SwiftUI or Compose. - [Print](https://popcraft.app/docs/exporting/print): Bleed, imposition and preflight for work that goes on paper. - [Websites: export and publish](https://popcraft.app/docs/exporting/websites): Get a whole page as a working website. Download it to host yourself, hand it to a developer, or put it online from PopCraft. - [Next.js projects](https://popcraft.app/docs/exporting/nextjs): Export a whole page as a Next.js project a developer owns, on current versions, that installs, lints and builds as it comes. - [Publish a website](https://popcraft.app/docs/exporting/publishing): Put a page online from PopCraft at its own address, publish new versions, go back to an earlier one, and take the site offline. From the editor, the command palette, the AI agent, the command line and the API. - [Storefronts](https://popcraft.app/docs/exporting/commerce): Design a shop in PopCraft and publish it as a store that sells — a Next.js Commerce store on Vercel with your Shopify products, cart and checkout — or export it for a Next.js Commerce store. The Ecommerce plugin adds the Shopify catalogue, store setup and a Shopify theme. ## Integrations - [Integrations](https://popcraft.app/docs/integrations): Connect your own GitHub, Vercel, Neon and Supabase accounts to a workspace, by authorizing PopCraft or pasting a token. What a connection is, who can make one, and how the credential is kept. - [GitHub](https://popcraft.app/docs/integrations/github): Send a published site to a repository of your own on GitHub, as a commit you can review, branch and build on. - [Vercel](https://popcraft.app/docs/integrations/vercel): Deploy a published site to a Vercel project of your own, live at that project's address and domains. - [Neon](https://popcraft.app/docs/integrations/neon): Give a published site a Postgres database of your own on Neon. Its forms are saved, its visitors can create accounts and sign in, and each visitor keeps their own data. - [Supabase](https://popcraft.app/docs/integrations/supabase): Give a published site a Supabase project of your own. Its forms are saved in the project's Postgres, and its visitors can create accounts and sign in with Supabase Auth. ## Collaborating - [Collaborating](https://popcraft.app/docs/collaborating): Multiplayer, comments, version history and sharing. ## Your Account - [Your account](https://popcraft.app/docs/account): Signing in and out, teams, plans and AI credits, offline behaviour and the desktop app. ## Plugins - [PopCraft Plugins](https://popcraft.app/docs/plugins) - [Plugin API reference](https://popcraft.app/docs/plugins/api-reference) - [Getting started](https://popcraft.app/docs/plugins/getting-started) - [Importers](https://popcraft.app/docs/plugins/importers) - [Manifest](https://popcraft.app/docs/plugins/manifest) - [Plugin UI](https://popcraft.app/docs/plugins/plugin-ui) - [Publishing](https://popcraft.app/docs/plugins/publishing) - [A plugin from outside PopCraft](https://popcraft.app/docs/plugins/third-party) - [Widgets](https://popcraft.app/docs/plugins/widgets) ## Themes - [UI Themes](https://popcraft.app/docs/themes) - [Theme editor](https://popcraft.app/docs/themes/theme-editor) - [Token reference](https://popcraft.app/docs/themes/token-reference) - [Creating a theme](https://popcraft.app/docs/themes/creating-a-theme) - [CSS hooks](https://popcraft.app/docs/themes/css-hooks) - [Publishing themes](https://popcraft.app/docs/themes/publishing-themes) ## Agent - [Skills and agent profiles](https://popcraft.app/docs/agent) - [File format](https://popcraft.app/docs/agent/file-format) - [Tool names](https://popcraft.app/docs/agent/tool-names) ## REST API - [REST API](https://popcraft.app/docs/api): Read, create and edit files, read nodes, images and versions, read and write comments, and browse the marketplace from scripts and integrations. - [Designing with an AI agent](https://popcraft.app/docs/api/agents): How a coding agent (Claude Code, Codex, Cursor and others) makes real designs in PopCraft from a terminal: write the design as a script, build it, check it, render it, look, fix, and hand it over. - [Command line](https://popcraft.app/docs/api/cli): The popcraft CLI edits .popcraft files headlessly, renders exports, calls the REST API, publishes to the marketplace and manages the desktop app's plugins, themes, fonts, skills and agents. - [Design reference](https://popcraft.app/docs/api/design-reference): How to make PopCraft designs in code, as the official templates are made: the kit by topic, editing existing designs, making the decisions, and worked examples that build with no check failing. - [Endpoints](https://popcraft.app/docs/api/endpoints): Every endpoint of the PopCraft REST API, generated from the OpenAPI spec. - [Packages](https://popcraft.app/docs/api/packages): PopCraft's npm packages: the popcraft command line, the design kit, the plugin SDK and the runtime exported code shares — what each is for and which to install. - [Making templates](https://popcraft.app/docs/api/templates): Make templates the way PopCraft's own are made: every official template's source to start from, the Kit and every style system, the build, every check the gallery's templates pass, the gallery's pictures, and publishing to your account. ## File formats Everything below is generated from the TypeScript declarations that read and write these files, with their doc comments. Optional fields (`?`) may be absent; readers must treat an absent field as its documented default. ### The .popcraft document A `.popcraft` file is one `Document` (below) as JSON, UTF-8, pretty-printed. Menu › **Save as .popcraft** writes it; **Import .popcraft…** (⌘I), drag-and-drop and the file picker read it (a `.json` or legacy `.vizpops` file with the same shape reads too). Inside the app a document is stored in IndexedDB (and optionally synced to the cloud) with the same shape, except that image, video, audio and font bytes are `media://` references into the media store; a saved `.popcraft` file re-embeds them as `data:` URLs so the file stands alone, and opening it interns them again. A minimal document: ```json { "id": "doc-1", "name": "Launch post", "pages": [ "page-1" ], "nodes": { "page-1": { "id": "page-1", "type": "PAGE", "name": "Page 1", "parentId": null, "childIds": [ "frame-1" ], "visible": true, "locked": false, "opacity": 1, "x": 0, "y": 0, "width": 0, "height": 0, "rotation": 0 }, "frame-1": { "id": "frame-1", "type": "FRAME", "name": "Post", "parentId": "page-1", "childIds": [ "text-1" ], "x": 0, "y": 0, "width": 1080, "height": 1080, "rotation": 0, "opacity": 1, "visible": true, "locked": false, "fills": [ { "type": "SOLID", "color": { "r": 1, "g": 0.93, "b": 0, "a": 1 }, "opacity": 1, "visible": true } ], "strokes": [], "effects": [], "cornerRadius": 0, "blendMode": "NORMAL", "clipsContent": true }, "text-1": { "id": "text-1", "type": "TEXT", "name": "Headline", "parentId": "frame-1", "childIds": [], "x": 80, "y": 80, "width": 920, "height": 120, "rotation": 0, "opacity": 1, "visible": true, "locked": false, "characters": "Doors open Friday", "fontSize": 96, "fontFamily": "Bangers", "fontWeight": 400, "fills": [ { "type": "SOLID", "color": { "r": 0.14, "g": 0.12, "b": 0.13, "a": 1 }, "opacity": 1, "visible": true } ], "strokes": [], "effects": [], "cornerRadius": 0, "blendMode": "NORMAL" } }, "settings": { "kind": "SOCIAL" } } ``` #### The document ```ts export interface Document { id: string name: string /** * This device's record of the cloud file this document holds (lib/client/cloud/sync.ts `linkActive`). Local only — * never synced. A document whose link is lost still says which file it is, so auto-sync re-links it instead of * uploading it as a new file: a copy of a cloud file must never become a second cloud file on its own. */ cloudKey?: string pages: string[] // page IDs nodes: Record styles?: Record variables?: Record variableCollections?: Record /** Per-collection preview mode (collectionId → modeId); falls back to the collection's defaultModeId */ activeVariableModes?: Record /** Node (usually a frame) the user picked to represent this file in the file browser. * When unset, the thumbnail is auto-generated (largest frame, else whole page). */ thumbnailNodeId?: string /** Libraries used by this file (libraryId → the version imported); their items carry `lib::` ids. */ libraries?: Record /** The last PopCorn vote, kept with the file so results survive a reload (and reach files with no live room). */ voteResults?: VoteResults /** Kind, units, page size, bleed… (see `DocumentSettings`). Absent on plain canvases. */ settings?: DocumentSettings /** * The drawing palette: this file's set of made-up instruments. Absent until someone edits it, and * `defaultDrawPalette()` stands in (lib/presets/draw-palette.ts). */ drawPalette?: DrawTool[] /** Scene sequences (lib/motion/composition.ts): frames played one after another with transitions between them. */ compositions?: Record /** The document as a website: its breakpoints (lib/document/breakpoints.ts). Absent until the file sets any. */ site?: SiteSettings /** The file's code modules, by id (lib/code): the React source its code components are drawn by. */ codeModules?: Record /** The file's content collections and their records (lib/document/collections.ts). */ collections?: Record /** What each plugin keeps with this file, by plugin id (`setFileData`): its settings for this design, JSON only. */ pluginData?: Record> /** Every collection's records, by record id (flat, so two people editing different records both keep their edits). */ records?: Record /** * The font files this file carries (lib/fonts): each face its own entry, the bytes in the media store. A text set in * one of these families draws in it wherever the file opens: the canvas, a headless export, PDF, SVG and web code. * Absent on files that carry none (every file saved before fonts could be carried). */ fonts?: DocumentFont[] } /** * What kind of thing this file is. CANVAS is the Figma-style infinite canvas every file had before. `KIT` is only * what files made from the retired Kit template still say (it opens like Web): files of components are official * libraries now (seed/libraries), and no template, command or dialog makes a KIT file. */ export type DocumentKind = 'CANVAS' | 'PRINT' | 'SOCIAL' | 'PRESENTATION' | 'WEB' | 'APP' | 'BOARD' | 'KIT' | 'EMAIL' export interface DocumentSettings { /** The show's default for slides that set no `advance` of their own (unset: on a click). */ showAdvance?: ShowAdvance /** At the last slide's end, go back to the first (a kiosk, a looping motion piece) instead of holding it. */ showLoop?: boolean kind?: DocumentKind units?: Unit /** Raster DPI for exports of print kinds (300 is the print default). */ dpi?: number /** Default page size (px) for fixed-page kinds; `presetId` refers to `lib/presets/page-presets.ts`. */ pageSize?: { width: number; height: number; presetId?: string } bleed?: Edges margins?: Edges slug?: number facingPages?: boolean pageNumbering?: { start: number; format: 'ARABIC' | 'ROMAN' | 'ALPHA'; sectionPrefix?: string } colorMode?: 'RGB' | 'CMYK' baselineGrid?: { step: number; offset: number } } /** Display unit for lengths. Geometry is always stored in CSS px; see `lib/units/units.ts`. */ export type Unit = 'px' | 'pt' | 'in' | 'mm' | 'cm' | 'pc' export interface Edges { top: number; right: number; bottom: number; left: number } /** * How a show moves on from a slide or a scene (lib/motion/present.ts `advanceOf`): * - `click`: it waits for a click (or a key) at each build and at its end — a deck's default; * - `end`: its builds play through without waiting, and the show moves on when its animation ends — a * composition scene's default, and a motion template's; * - `after`: the same, `seconds` after its animation ends (a still slide: after it arrives). */ export type ShowAdvance = { on: 'click' } | { on: 'end' } | { on: 'after'; seconds: number } /** * One instrument on the drawing palette: a brush, a color and a size, kept together. * * A palette entry is not a tool in the toolbar sense — "brush" is. It is a brush already loaded: the red * marker, the green pencil, the 2px inker you use for panel borders. That is why the palette lives in the * FILE rather than the app: the set of instruments a comic is drawn with belongs to that comic, travels * with it, and is not the set you want open in a wireframe. */ export interface DrawTool { id: string /** What the pointer does — the same tool ids the toolbar uses. */ tool: ToolType /** Which brush, when the tool paints. */ preset: BrushPreset /** A brush from the user's own library, which overrides `preset`'s dynamics. */ userBrushId?: string color: Color size: number /** 0–1. Absent means whatever the brush's own default is. */ opacity?: number /** 0–100 stabilisation, as the draw bar's Smooth slider sets it. */ smoothing?: number /** What the person called it. Unnamed instruments show their brush's name. */ name?: string } export interface VoteResults { endedAt: number votesPerPerson: number scope: 'STICKIES' | 'SELECTION' | 'ALL' /** node id → votes across everyone. */ tallies: Record } ``` #### Nodes Every node lives in the flat `Document.nodes` map, keyed by id. `parentId` and `childIds` make the tree: `Document.pages` lists the PAGE nodes in order, a page's `childIds` are its top-level layers (usually FRAMEs, the artboards), and so on down. `type` picks the interface. A type not in the list is a plugin's native extension node (`NativeExtensionNode`). ```ts export type NodeType = | 'PAGE' | 'FRAME' | 'SECTION' | 'SLICE' | 'RECTANGLE' | 'ELLIPSE' | 'POLYGON' | 'STAR' | 'LINE' | 'ARROW' | 'TEXT' | 'GROUP' | 'VECTOR' | 'BOOLEAN' | 'IMAGE' | 'COMPONENT' | 'COMPONENT_SET' | 'INSTANCE' | 'STICKY' | 'PIXEL_GRID' | 'RASTER' | (string & {}) export type AnyNode = | PageNode | FrameNode | SectionNode | SliceNode | RectangleNode | EllipseNode | PolygonNode | StarNode | LineNode | ArrowNode | TextNode | GroupNode | VectorNode | BooleanNode | ImageNode | ComponentNode | ComponentSetNode | InstanceNode | StickyNode | PixelGridNode | RasterNode | NativeExtensionNode /** * Rest transform beyond position and rotation (lib/document/transform.ts `localMatrix`). All optional; absent * means the default, so documents without them place exactly as before. `width`/`height` stay the unscaled * layout box: scale is a transform (children, text and strokes scale with it), as in CSS and After Effects. * The Design-mode Scale tool still bakes sizes (scale-geometry.ts); these are for motion. */ export interface TransformRest { /** Horizontal scale about the anchor (default 1). Negative mirrors, like `flipHorizontal`. */ scaleX?: number /** Vertical scale about the anchor (default 1). Negative mirrors, like `flipVertical`. */ scaleY?: number /** Rotation/scale pivot as a fraction of the box width (default 0.5, the centre); may lie outside 0..1. */ anchorX?: number /** Rotation/scale pivot as a fraction of the box height (default 0.5); `1.15` pivots below the box. */ anchorY?: number /** Offset added after layout, in the parent's space (default 0). Auto layout never reads it. */ translateX?: number /** Offset added after layout, in the parent's space (default 0). */ translateY?: number /** * Tilt about the box's horizontal axis through the anchor, in degrees (default 0). Only a layer inside a 3D * context (a group or frame with `scene3d`) is drawn tilted; anywhere else it is ignored, like CSS `rotateX` * without a perspective. `rotation` is the turn about the view axis (rotateZ). See lib/document/scene3d.ts. */ rotateX?: number /** Turn about the box's vertical axis through the anchor, in degrees (default 0). 3D contexts only. */ rotateY?: number /** Depth in px toward the viewer (default 0; negative is farther away). 3D contexts only. */ z?: number } export interface BaseNode extends TransformRest, MotionFields { id: string name: string type: NodeType /** Dev Mode: notes pinned to this layer (numbered on the canvas in Dev view). */ annotations?: Annotation[] /** Dev Mode: links developers need for this layer (Storybook, tickets, docs). */ devResources?: DevResource[] /** PopCorn private mode: the user id this node is private to. It stays on that device (never synced, exported * to the cloud or shown to others) until revealed. See lib/collab/private.ts. */ privateTo?: string /** Lighting response when drawn as a layer of a 3D context. */ material3d?: Material3D /** * Accessibility: the text alternative exports and presenting use for this layer as a picture (HTML `alt`, SVG * ``/`aria-label`, the slide's reading order). `''` marks it decorative (skipped); undefined means none was * written (an image then falls back to its layer name). Edited in the inspector's Alt text section. */ altText?: string x: number y: number width: number height: number rotation: number opacity: number visible: boolean locked: boolean parentId: string | null childIds: string[] constraints?: { horizontal: 'MIN' | 'MAX' | 'CENTER' | 'STRETCH' | 'SCALE' vertical: 'MIN' | 'MAX' | 'CENTER' | 'STRETCH' | 'SCALE' } flipHorizontal?: boolean flipVertical?: boolean /** Layer inside a pixel canvas: ids of the animation frames it appears in (undefined = every frame). */ pixelFrameIds?: string[] minWidth?: number maxWidth?: number minHeight?: number maxHeight?: number /** What the layer sets differently at each breakpoint (by breakpoint id): lib/document/breakpoints.ts. */ responsive?: Record<string, ResponsiveProps> /** What the layer is on a web page: its element, its accessible name, where it links. */ semantics?: NodeSemantics /** The layer as a form control on a web page (a field, a checkbox, a button, the form itself). */ control?: ControlMeta componentPropertyReferences?: Record<string, string> /** * Inside a component: registry paths (lib/motion/properties.ts) this layer takes from a NUMBER or COLOR * component property, `{ 'fills[@f1].color': 'Accent' }`. An instance that sets the property shows its value * there — and a path the component also keys takes the instance's value instead of the keys (an input). */ componentPropertyPaths?: Record<string, string> boundVariables?: Record<string, string> /** * Live values: a property that follows an expression over the file's variables (lib/document/expressions.ts), * `{ width: 'health / maxHealth * 320', characters: 'pad(score, 6)', visible: 'health < 30' }`. Resolved with the * bound variables (lib/document/variables.ts), so a prototype's Set variable moves every layer that reads it. */ boundExpressions?: Record<string, string> /** * The record this layer and what it holds are about (lib/document/collections.ts `shownRecordOf`): one product, one * post, picked from a collection. Its `{{item.…}}` words, bound pictures and an instance's bound properties * (`boundExpressions['prop:Title'] = 'item_title'`) show that record's values, on the canvas and in every export. */ record?: RecordPin /** * Prototype: a shared element. Smart animate hands a layer over to the layer with the same key on the other * frame wherever each sits in its tree (a mini player's cover becoming the now-playing cover), instead of only * matching layers by name at the same place in the hierarchy (lib/editor/smart-animate.ts). */ sharedElement?: string } export interface SceneNode extends BaseNode { fills: Paint[] fillStyleId?: string strokes: StrokeStyle[] strokeStyleId?: string /** * Which paints first (SVG `paint-order`): FILL_FIRST draws the stroke over the fill, STROKE_FIRST draws it behind * ("Behind fill"), so an outline never covers the letters it surrounds. Unset: STROKE_FIRST on text, FILL_FIRST on * every other layer (lib/document/paint-order.ts). */ paintOrder?: 'FILL_FIRST' | 'STROKE_FIRST' /** Trim paths on the stroke (vectors and shapes). */ trim?: StrokeTrim cornerRadius: number | [number, number, number, number] cornerSmoothing?: number // 0-1 iOS squircle smoothing /** @deprecated Folded into `effects` on load (`migrateDocumentEffects`); readers go through `lib/document/effects.ts`. */ shadows?: Shadow[] /** @deprecated See `shadows`. */ blurs?: Blur[] /** Every effect on the node, in application order. */ effects?: Effect[] effectStyleId?: string /** Text: the effect preset that produced the current shadows (for the picker). */ textEffectPreset?: string /** Text: the preset's parameters, in hundredths of the font size (lib/text/effect-presets.ts). */ textEffectParams?: { distance: number; angle: number; blur: number; thickness: number; intensity: number; color?: Color } /** Text: the font size the preset's effects were derived at; a new size re-derives them (they scale with the text). */ textEffectSize?: number blendMode: BlendMode isMask?: boolean /** * Clipping mask (Procreate): this layer shows only where the layer BELOW it has pixels, and that * layer still draws normally — the difference from an ordinary mask, whose mask child is invisible. * Several layers can clip to the same base by each setting this in turn. */ clipToBelow?: boolean /** * Alpha lock (Procreate): the layer's silhouette is fixed. Painting recolors what is already there * and cannot add a pixel outside it or take one away — shading inside a shape without cutting a mask. */ alphaLock?: boolean maskType?: 'ALPHA' | 'VECTOR' | 'LUMINANCE' // default ALPHA when isMask exportSettings?: ExportSetting[] /** * When set on an image/shape, overlapping text on the same page wraps around this node's AABB. * Contour wrap is out of scope; this is bounding-box only. */ textWrap?: { mode: 'NONE' | 'BOUNDING_BOX'; padding?: number; sides?: 'BOTH' | 'LEFT' | 'RIGHT' } // Component properties isComponent?: boolean componentId?: string // If this is an instance, points to the main component // Auto Layout specific sizing layoutSizingHorizontal?: 'FIXED' | 'HUG' | 'FILL' layoutSizingVertical?: 'FIXED' | 'HUG' | 'FILL' layoutPositioning?: 'AUTO' | 'ABSOLUTE' minWidth?: number maxWidth?: number minHeight?: number maxHeight?: number /** An AI image job is filling this layer (drawn with a loading overlay; cleared when it lands). */ aiPending?: AiPending /** How this layer's image was generated, kept so it can be regenerated (or varied) later with the same settings. */ aiGeneration?: AiGeneration } export interface PageNode extends BaseNode { type: 'PAGE' backgroundColor: Color guides?: Guide[] prototypeDevice?: 'NONE' | 'IPHONE_14' | 'IPHONE_SE' | 'IPAD_PRO' | 'MACBOOK_PRO' | 'SURFACE_PRO' } /** Marks a top-level frame as one ordered page of a fixed-page document (see `lib/document/print-pages.ts`). */ export interface PrintPageMeta { isPrintPage: true /** Reading order; missing indexes sort last, then by position. */ index?: number /** Overrides of the document defaults, px. */ pageSize?: { width: number; height: number } /** * The page preset (lib/presets/page-presets.ts) this sheet is, when its size alone would name another one: a * 1920×1080 slide in a file of square posts, which is also "Video 1080p". A template offers the size by it. */ presetId?: string bleed?: Edges margins?: Edges /** Speaker notes (presentations). */ notes?: string /** Presentations: leave this slide out of the show (still exports unless the exporter skips it). */ skip?: boolean /** Presentations: how the show moves onto this slide. */ transition?: PrototypeTransition /** Presentations: how the show moves on from this slide (unset: the document's `showAdvance`, else on a click). */ advance?: ShowAdvance /** A master: a page whose content is drawn behind every page that names it (`masterId`); not a sheet itself. */ isMaster?: boolean /** The master this page inherits; its children render behind this page's own, read-only. */ masterId?: string /** Optional master for the first sheet (overrides `masterId` on page index 0). */ masterIdFirst?: string /** Optional master for left-hand sheets when facing pages are on (overrides `masterId`). */ masterIdLeft?: string /** Optional master for right-hand sheets when facing pages are on (overrides `masterId`). */ masterIdRight?: string /** Reserved header band height in px (print chrome guide); body content is not auto-clipped. */ headerHeight?: number /** Reserved footer band height in px (print chrome guide). */ footerHeight?: number } export interface FrameNode extends SceneNode { type: 'FRAME' /** * Pixel art shown pixel-perfect (lib/document/pixel-art.ts): in Present mode the screen is scaled so each art pixel * is a whole number of device pixels, and a game on it moves its sprites on the art's pixel grid. */ pixelPerfect?: boolean /** The design script that built this frame and its sibling sizes (lib/agent/design-script): run again to rebuild. */ designScript?: DesignScriptSource /** A 3D context (see `Scene3D`). */ scene3d?: Scene3D /** Present on the page frames of PRINT / SOCIAL / PRESENTATION documents. */ print?: PrintPageMeta clipsContent: boolean backgroundColor: Color layoutMode: 'NONE' | 'HORIZONTAL' | 'VERTICAL' | 'GRID' primaryAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' | 'SPACE_BETWEEN' counterAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' padding: { top: number; right: number; bottom: number; left: number } itemSpacing: number counterAxisSpacing?: number // Used for GRID layout mode layoutGridColumns?: number | 'AUTO' // Used for GRID layout mode layoutGridRows?: number | 'AUTO' // Used for GRID layout mode layoutGridMinColumnWidth?: number layoutGridMinRowHeight?: number layoutGridColumnSizes?: { sizing: 'FILL' | 'HUG' | 'FIXED', value: string }[] layoutGridRowSizes?: { sizing: 'FILL' | 'HUG' | 'FIXED', value: string }[] // Prototype interactions: Interaction[] overflowScrolling: 'NONE' | 'HORIZONTAL' | 'VERTICAL' | 'BOTH' isFlowStartingPoint: boolean /** A top-level frame as a page of a website: its address and what a browser tab and a search result say of it. */ route?: RouteMeta /** Repeat what the frame holds once for each record of a collection (lib/document/collections.ts). */ repeat?: RepeatMeta /** Name shown in the flows list when this frame starts a flow. */ flowName?: string /** Auto layout: wrap children onto new lines when the primary axis is fixed. */ layoutWrap?: 'NO_WRAP' | 'WRAP' componentPropertyReferences?: { visible?: string // ID of the boolean property characters?: string // ID of the text property /** Nested INSTANCE: the INSTANCE_SWAP / VARIANT property that chooses its main component. */ mainComponent?: string } layoutGrids?: LayoutGrid[] gridStyleId?: string } export interface SectionNode extends BaseNode { type: 'SECTION' backgroundColor: Color /** Dev Mode hand-off status shown on the section label. */ devStatus?: DevStatus // Sections don't have fills/strokes, just a label background } export interface SliceNode extends BaseNode { type: 'SLICE' exportSettings: ExportSetting[] } export interface RectangleNode extends SceneNode { type: 'RECTANGLE' } export interface EllipseNode extends SceneNode { type: 'ELLIPSE' arcData?: { startAngle: number endAngle: number innerRadius: number } } export interface ConnectorAttachment { nodeId: string position: 'TOP' | 'RIGHT' | 'BOTTOM' | 'LEFT' | 'CENTER' } export interface LineNode extends SceneNode { type: 'LINE' startPoint: { x: number; y: number } endPoint: { x: number; y: number } startArrowhead?: 'NONE' | 'ARROW' | 'TRIANGLE' | 'DIAMOND' | 'CIRCLE' | 'REVERSE_TRIANGLE' endArrowhead?: 'NONE' | 'ARROW' | 'TRIANGLE' | 'DIAMOND' | 'CIRCLE' | 'REVERSE_TRIANGLE' connectorStart?: ConnectorAttachment connectorEnd?: ConnectorAttachment routingMode?: 'STRAIGHT' | 'ORTHOGONAL' | 'BEZIER' routingPoints?: { x: number; y: number }[] autoRoute?: boolean } export interface ArrowNode extends SceneNode { type: 'ARROW' startPoint: { x: number; y: number } endPoint: { x: number; y: number } startArrowhead: 'NONE' | 'ARROW' | 'TRIANGLE' | 'DIAMOND' | 'CIRCLE' | 'REVERSE_TRIANGLE' endArrowhead: 'NONE' | 'ARROW' | 'TRIANGLE' | 'DIAMOND' | 'CIRCLE' | 'REVERSE_TRIANGLE' connectorStart?: ConnectorAttachment connectorEnd?: ConnectorAttachment routingMode?: 'STRAIGHT' | 'ORTHOGONAL' | 'BEZIER' routingPoints?: { x: number; y: number }[] autoRoute?: boolean } export interface PolygonNode extends SceneNode { type: 'POLYGON' pointCount: number // number of sides (3 = triangle, 5 = pentagon, 6 = hexagon, etc.) } export interface StarNode extends SceneNode { type: 'STAR' pointCount: number // number of points innerRadius: number // 0-1 ratio of inner to outer radius } export interface GroupNode extends BaseNode { type: 'GROUP' isMaskGroup?: boolean // true when this group wraps a mask + its masked layers /** A 3D context (see `Scene3D`). */ scene3d?: Scene3D } export interface VectorNode extends SceneNode { type: 'VECTOR' paths: VectorPath[] fillRule: 'NONZERO' | 'EVENODD' } export interface VectorNode extends SceneNode { type: 'VECTOR' paths: VectorPath[] fillRule: 'NONZERO' | 'EVENODD' // For pencil/brush strokes with variable width strokePoints?: StrokePoint[] brushPreset?: BrushPreset // Smooth cubic-Bézier centerline fitted from the stabilized stroke (fit-curve). The dab-stamps in // `paths` are what render; this is the clean, editable representation (few anchors + handles) for // future vector editing / re-flow. Present on brush strokes committed in VECTOR mode. centerline?: VectorPath // The brush recipe at commit time — lets Edit-Vector regenerate the stroke from an edited centerline. brushSize?: number brushDynamics?: BrushDynamics // Transient: while Edit Vector is active on a brush stroke, `paths` holds the centerline SPINE // (drawn as a stroked curve) and the real fills are stashed here; exit regenerates the stroke. brushEditingSpine?: boolean brushFills?: Paint[] } export interface VectorPath { points: VectorPoint[] closed: boolean data?: (string | number)[] // Flat command array for canvas rendering: ['M',x,y,'L',x,y,'C',...] } export interface VectorPoint { x: number y: number handleIn?: { x: number; y: number } handleOut?: { x: number; y: number } cornerRadius?: number } export interface VectorEditMode { nodeId: string activePathIndex: number selectedPoints: number[] } export interface BooleanNode extends SceneNode { type: 'BOOLEAN' booleanOperation: BooleanOperation /** 'LOCAL' once the children are stored relative to this node (the rule everywhere else). Absent on * documents written before that change; the loader rebases those once and sets it. */ booleanChildSpace?: 'LOCAL' paths: VectorPath[] fillRule: 'NONZERO' | 'EVENODD' } export type BooleanOperation = 'UNION' | 'SUBTRACT' | 'INTERSECT' | 'EXCLUDE' export interface ImageNode extends SceneNode { type: 'IMAGE' imageUrl: string scaleMode: 'FILL' | 'FIT' | 'CROP' | 'TILE' /** A stock photo's licence and attribution, kept with the layer (lib/photos/stock.ts). */ credit?: MediaCredit /** Non-destructive adjustment stack; the original image is never modified. */ adjustments?: ImageAdjustment[] /** * Mockup warp: where the image's four corners (top-left, top-right, bottom-right, bottom-left) sit, as * fractions of the layer's box. The image is projected onto that quadrilateral; outside it is transparent. */ perspective?: PerspectiveCorners } /** Non-destructive image adjustment (applied on the GPU at the image's native resolution, in order). */ export type ImageAdjustmentType = | 'BRIGHTNESS' | 'CONTRAST' | 'EXPOSURE' | 'SATURATION' | 'VIBRANCE' | 'HUE' | 'TEMPERATURE' | 'CURVES' | 'BLUR' | 'SHARPEN' | 'NOISE' | 'PIXELATE' | 'EDGE' | 'MONOCHROME' | 'INVERT' | 'POSTERIZE' | 'VIGNETTE' | 'BULGE' | 'PINCH' | 'SWIRL' export interface ImageAdjustment { id: string type: ImageAdjustmentType enabled: boolean /** Primary amount (meaning per type — see lib/engine/image-adjust/adjustments.ts). */ amount: number /** Secondary value: tint (TEMPERATURE), radius (warps, px-relative 0..1), mono noise, posterize levels… */ amount2?: number /** Warp / vignette centre in image space (0..1). */ center?: { x: number; y: number } /** MONOCHROME tint color. */ color?: Color /** CURVES: control points per channel (0..1). */ curves?: { rgb: CurvePoint[]; r: CurvePoint[]; g: CurvePoint[]; b: CurvePoint[] } } export interface CurvePoint { x: number; y: number } /** * Where an openly licensed picture came from, stored with the layer or fill that shows it so the attribution the * licence asks for survives copies, exports and offline use (lib/photos/stock.ts `creditLine`). */ export interface MediaCredit { /** Openverse (the stock photo search), or Wikimedia Commons directly (the official templates' own photographs). */ provider: 'openverse' | 'wikimedia' /** The provider's id for the picture. */ id: string title: string creator: string creatorUrl?: string /** Licence code as the provider gives it: `by`, `by-sa`, `cc0`, `pdm`, `by-nc`… */ license: string licenseVersion?: string licenseUrl?: string /** The page the picture was published on. */ sourceUrl: string /** Where the provider found it (flickr, wikimedia…). */ source?: string } export type PerspectiveCorners = [[number, number], [number, number], [number, number], [number, number]] export interface StickyNode extends SceneNode { type: 'STICKY' text: string authorName: string fontFamily?: string fontWeight?: number fontSize?: number fontStyle?: 'normal' | 'italic' textAlign?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' verticalAlign?: 'TOP' | 'CENTER' | 'BOTTOM' textDecoration?: 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' } export interface NativeExtensionNode extends TransformRest, MotionFields { type: string id: string name: string parentId: string | null childIds: string[] x: number y: number width: number height: number rotation: number opacity: number visible: boolean locked: boolean fills: Paint[] strokes: StrokeStyle[] cornerRadius: number | [number, number, number, number] shadows?: Shadow[] blurs?: Blur[] blendMode: BlendMode [key: string]: any // Extension-specific properties } /** * Live paint layer: a fixed-resolution RGBA bitmap brushes paint into on the GPU. Pixels live in the * media store as PNG tiles; the node holds only their refs, so undo/sync swap a handful of strings. */ export interface RasterNode extends SceneNode { type: 'RASTER' pixelWidth: number pixelHeight: number /** "tx,ty" → media:// ref of a RASTER_TILE_SIZE² PNG tile (absent = transparent). */ rasterTiles: Record<string, string> } export interface PixelGridNode extends SceneNode { type: 'PIXEL_GRID' pixelWidth: number pixelHeight: number colorMode: PixelColorMode palette: string[] // '#rrggbbaa' transparentIndex: number frames: PixelFrame[] activeFrame: number tags: PixelTag[] onionSkin?: { enabled: boolean; prev: number; next: number; opacity: number } tiledMode?: 'NONE' | 'X' | 'Y' | 'BOTH' referenceImageUrl?: string referenceOpacity?: number referenceVisible?: boolean // Tilemap mode: the canvas is tilemapCols × tilemapRows tiles; each frame's cels hold tile indices // (0xffffffff = empty) and the tile pixels live in `tileset` (editing a tile updates every use). tileset?: PixelTileset } export type PixelColorMode = 'RGBA' | 'INDEXED' | 'GRAYSCALE' export interface PixelFrame { id: string cels: string duration: number // ms } export interface PixelTag { id: string name: string from: number // frame index, inclusive to: number // frame index, inclusive direction: 'FORWARD' | 'REVERSE' | 'PING_PONG' color: string } export interface PixelTileset { tileW: number tileH: number tiles: string[] // base64 cels per tile (same encoding as frames) } export interface StrokePoint { x: number y: number pressure: number // 0-1, for variable width timestamp: number tilt?: number // 0 = upright … 1 = flat; undefined for mouse / pens without tilt azimuth?: number // radians, stylus direction in screen space; undefined when unknown } export interface AiPending { /** The prompt (or action label) shown under the loading indicator. */ prompt: string model: string /** ms since epoch; older than AI_PENDING_TTL_MS without a result = interrupted. */ startedAt: number } export interface Guide { id: string axis: 'X' | 'Y' pos: number // World coordinate /** FOLD guides are print fold lines (drawn dashed, kept with the page). */ kind?: 'FOLD' } export interface ExportSetting { suffix: string // GIF and SHEET (sprite sheet PNG + Aseprite JSON) are pixel-canvas formats format: 'PNG' | 'JPG' | 'WEBP' | 'SVG' | 'PDF' | 'GIF' | 'SHEET' scale: number sheetLayout?: 'ROWS' | 'COLUMNS' | 'PACKED' gifTag?: string // animate only this frame tag (empty: all frames) gifRange?: { from: number; to: number } // or only these frames (0-based, inclusive); wins over gifTag constraint?: { type: 'SCALE' | 'WIDTH' | 'HEIGHT'; value: number } /** Raster formats: print resolution — renders at dpi/96 and tags the file (JFIF density, PNG pHYs). Wins over `scale`. */ dpi?: number /** PDF only: print options (see `lib/io/pdf`). Absent: a screen PDF at the export scale. */ pdf?: { dpi?: number; bleed?: boolean; marks?: boolean; colorBars?: boolean; colorMode?: 'RGB' | 'CMYK'; textAs?: 'FONT' | 'OUTLINE'; spreads?: boolean; ats?: boolean } } export type BlendMode = | 'NORMAL' | 'DISSOLVE' | 'MULTIPLY' | 'SCREEN' | 'OVERLAY' | 'DARKEN' | 'LIGHTEN' | 'COLOR_DODGE' | 'COLOR_BURN' | 'HARD_LIGHT' | 'SOFT_LIGHT' | 'DIFFERENCE' | 'EXCLUSION' | 'HUE' | 'SATURATION' | 'COLOR' | 'LUMINOSITY' /** The colour labels a timeline row can carry (theme hues, so they follow the theme). */ export type TimelineColor = 'accent' | 'success' | 'warning' | 'danger' | 'component' | 'prototype' | 'ai' ``` #### Text ```ts export interface TextNode extends SceneNode { type: 'TEXT' characters: string fontSize: number fontFamily: string fontWeight: number fontStyle: 'normal' | 'italic' fontFeatures?: FontFeatureSettings // OpenType features, e.g. { liga: 0, tnum: 1, ss01: 1 } fontVariations?: FontVariationSettings // variable-font axes, e.g. { wght: 437, wdth: 90 } /** How emoji draw: the OS color emoji font (default), bundled color Twemoji, or one-color outlines. */ emojiStyle?: 'SYSTEM' | 'COLOR' | 'OUTLINE' textAlign: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' verticalAlign: 'TOP' | 'CENTER' | 'BOTTOM' lineHeight: number | 'AUTO' letterSpacing: number paragraphSpacing: number paragraphIndent: number textStyleId?: string textDecoration: 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' textDecorationStyle?: 'SOLID' | 'DASHED' | 'WAVY' textDecorationThickness?: number | 'AUTO' textDecorationOffset?: number | 'AUTO' textDecorationSkipInk?: boolean textDecorationColor?: Paint[] | 'AUTO' textCase: 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' textVerticalTrim?: 'NONE' | 'CAP_HEIGHT' /** Legacy flat list flag; `list` and per-paragraph `paragraphs[].list` supersede it. */ textListStyle?: 'NONE' | 'UNORDERED' | 'ORDERED' /** Node-wide list formatting (paragraph formats override it). */ list?: ListStyle /** Node-wide tab stops (paragraph formats override them). Without any, tabs fall every 4 em. */ tabStops?: TabStop[] /** Space before each paragraph but the first. */ spaceBefore?: number /** Per-paragraph formatting: alignment, spacing, indent, list, tabs, or a named paragraph style. */ paragraphs?: ParagraphFormat[] /** Break words at the line end with a hyphen (en-US patterns). */ hyphenate?: boolean /** Flow the text through columns of equal width. */ columns?: { count: number; gap: number } /** Text on a path: the line bends onto an arc or another node's outline (single line, no wrapping). */ textPath?: TextPath /** Area type: flow inside another node's outline (ignored while the text is on a path). */ textArea?: TextArea /** * Auto-size the type to its container — the area shape, the fixed box, or the path between its brackets. The * stored sizes stay as set; the layout scales every size (segments, spacing, line height) by one factor. */ textFit?: TextFit /** Threading: this frame continues the text of `threadPrevId` and overflows into `threadNextId`. Only the head owns characters. */ threadPrevId?: string threadNextId?: string /** Hyperlink URL applied to the whole text node (per-run links live on styledSegments). */ link?: string textTruncation?: 'NONE' | 'ENDING' /** Line clamp: keep at most this many lines and end the last one with an ellipsis (CSS line-clamp). */ maxLines?: number textAutoResize: 'NONE' | 'WIDTH_AND_HEIGHT' | 'HEIGHT' styledSegments?: TextSegment[] interactions: Interaction[] /** Per-letter, per-word or per-line animation (lib/motion/text-animators.ts): drawn only while the text plays. */ textAnimators?: TextAnimator[] /** A number the text shows, counting: while it plays the characters are `text.counter`'s value, formatted. */ textCounter?: TextCounter } export interface TextSegment { start: number end: number fontFeatures?: FontFeatureSettings fontVariations?: FontVariationSettings fills?: Paint[] strokes?: StrokeStyle[] fontSize?: number fontFamily?: string fontWeight?: number fontStyle?: 'normal' | 'italic' textDecoration?: 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' textDecorationStyle?: 'SOLID' | 'DASHED' | 'WAVY' textDecorationThickness?: number | 'AUTO' // either a % of font size (e.g. 0.1 for 10%) or 'AUTO' textDecorationOffset?: number | 'AUTO' // either a % of font size or 'AUTO' textDecorationSkipInk?: boolean textDecorationColor?: Paint[] | 'AUTO' textCase?: 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' textVerticalTrim?: 'NONE' | 'CAP_HEIGHT' textListStyle?: 'NONE' | 'UNORDERED' | 'ORDERED' /** Hyperlink URL for this run (rendered underlined; opens in present mode; `<a>` in SVG export). */ link?: string lineHeight?: number | 'AUTO' letterSpacing?: number } /** OpenType feature toggles (tag → 0 off / 1 on / n alternate index) and variation axis values. */ export type FontFeatureSettings = Record<string, number> export type FontVariationSettings = Record<string, number> /** * How a TEXT layer follows a path. `ARC`, `CIRCLE` and `WAVE` are the layer's own geometry (built from its line); * `PATH` follows another node's outline, live: editing that node moves the text with it. */ export interface TextPath { mode: 'ARC' | 'CIRCLE' | 'WAVE' | 'PATH' /** ARC: −100…100; 100 is a full circle, positive bends the ends downward. */ bend?: number /** CIRCLE: the radius in px (default: the text wraps about 80% of the way round). */ radius?: number /** WAVE: peak height in px (default half the font size) and whole waves along the line (default 1.5). */ amplitude?: number frequency?: number /** PATH: the node whose outline the text follows. */ pathId?: string /** * The start bracket, as a fraction of the path's length. A closed path is measured clockwise from its top * centre, so 0 is 12 o'clock and 0.5 is 6 o'clock. */ offset?: number /** The end bracket (default: the end of an open path; on a closed path, one full turn after the start). */ endOffset?: number /** Run the text the other way along the path, on its other side (below / inside a closed shape). */ flip?: boolean /** Where the text sits between the brackets. JUSTIFY stretches it to fill them exactly. Default LEFT. */ align?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFY' /** Which line of the glyphs rides on the path. Default BASELINE. */ placement?: TextPathPlacement /** px away from the path, on the glyphs' up side (negative: toward their feet). */ baselineShift?: number /** Extra px between letters along the path (on top of the text's own letter spacing). */ spacing?: number /** Default RAINBOW (each glyph turns to the tangent). */ effect?: TextPathEffect /** Text past the end bracket: hidden with an overflow marker (CLIP, the default), drawn on, or shrunk to fit. */ overflow?: 'CLIP' | 'VISIBLE' | 'SHRINK' } /** Where a glyph sits against the path (Illustrator's "Align to Path"). */ export type TextPathPlacement = 'BASELINE' | 'ASCENDER' | 'DESCENDER' | 'CENTER' /** How each glyph is shaped onto the path (Illustrator's Type on a Path effects). */ export type TextPathEffect = 'RAINBOW' | 'SKEW' | 'RIBBON' | 'STAIR' | 'GRAVITY' /** * Area type: the text flows inside another node's closed outline, each line as wide as the shape is at that * height. The text's box follows the shape; editing the shape reflows the text. */ export interface TextArea { shapeId: string /** Distance kept from the outline, px. */ inset?: number } /** Font size chosen by the layout to fill the container: only ever smaller (SHRINK) or whatever fills it (FIT). */ export type TextFit = 'NONE' | 'SHRINK' | 'FIT' /** * A text animator (After Effects' animator + range selector). The text's units are laid out in `order` in equal * slots over [0, 1]; the range selects [start, end] (shifted by `offset`), and a unit's weight is how much of its * slot the range covers, shaped by `ease` (hold: all or nothing), times `amount`. With a `stagger`, unit k (in * order) plays the animator's keys `k · stagger` ms later, so one set of keys becomes a cascade. * `range.*`, `amount` and each prop are keyable: `text.animators[@id].range.start`, `text.animators[@id].props.opacity`. */ export interface TextAnimator { /** Stable: tracks address the animator by it. */ id: string name?: string unit: TextAnimatorUnit /** Fractions of the units, 0..1 (offset −1..1). */ range: { start: number; end: number; offset: number } /** 0..1, default 1. */ amount?: number order: TextAnimatorOrder /** For `random`: the same seed gives the same order. */ seed?: number /** ms between consecutive units' time (0: they share it). */ stagger?: number props: TextAnimatorProps /** The weight across a unit's slot edge (default linear). */ ease?: Interp } /** What a text animator moves: each letter, word or line of the text on its own. */ export type TextAnimatorUnit = 'letter' | 'word' | 'line' /** Which unit a range covers first (and, with a stagger, starts first). `center` goes out from the middle. */ export type TextAnimatorOrder = 'forward' | 'reverse' | 'center' | 'random' /** * Where a fully-selected unit is, relative to where it rests: offsets in the text's own px, a scale factor and * degrees about the unit's centre, an opacity factor, a tint mixed over the fill, a blur (px) softening the glyphs. * A unit partly selected gets that fraction of each. */ export interface TextAnimatorProps { translateX?: number translateY?: number scale?: number rotation?: number opacity?: number color?: Color blur?: number } /** A counting number: `text.counter` (keyable) runs from `from` to `to`, shown with this formatting. */ export interface TextCounter { from: number to: number /** Digits after the point, 0..6. */ decimals?: number prefix?: string suffix?: string /** Between thousands ('' for none, ',' by default). */ separator?: string /** The value the text rests at (the finished figure, `to`, when unset). */ value?: number /** * Show the figure in whole steps from `from` (1: a countdown's seconds): it changes only once the value has * travelled a full step, so one key pair ticks a clock instead of one hold key per tick. Unset: every figure. */ step?: number } /** A tab stop, measured from the paragraph's left edge (px). */ export interface TabStop { position: number align: 'LEFT' | 'CENTER' | 'RIGHT' | 'DECIMAL' leader?: 'NONE' | 'DOT' | 'UNDERSCORE' } /** List formatting for a paragraph. `level` nests (0 = outermost); `indent` is per level (default 1.5 em). */ export interface ListStyle { type: 'NONE' | 'UNORDERED' | 'ORDERED' level?: number format?: 'BULLET' | 'DASH' | 'DECIMAL' | 'LOWER_ALPHA' | 'UPPER_ALPHA' | 'LOWER_ROMAN' | 'UPPER_ROMAN' indent?: number } /** * Formatting of one paragraph (keyed by the index of its first character). Anything unset falls back to the * node; `styleId` names a text style whose paragraph properties apply under these overrides. */ export interface ParagraphFormat { start: number styleId?: string textAlign?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' /** Space after the paragraph. */ paragraphSpacing?: number spaceBefore?: number /** First-line indent. */ paragraphIndent?: number list?: ListStyle tabStops?: TabStop[] hyphenate?: boolean } ``` #### Paints, strokes and effects ```ts export interface Color { r: number // 0-1 g: number // 0-1 b: number // 0-1 a: number // 0-1 } export interface ColorStop { /** Stable address for motion tracks (`fills[@f1].gradientStops[@g1].color`); assigned when a track first needs it. */ id?: string position: number color: Color } export interface Paint { /** Stable address for motion tracks (`fills[@f1].color`); assigned lazily by `ensureItemId` (lib/motion/properties.ts). */ id?: string type: 'SOLID' | 'GRADIENT_LINEAR' | 'GRADIENT_RADIAL' | 'GRADIENT_ANGULAR' | 'GRADIENT_DIAMOND' | 'IMAGE' | 'SHADER' | 'PATTERN' | 'VIDEO' color?: Color opacity?: number visible?: boolean /** * `false` draws this paint on the canvas but NOT in exports — a frame's background you want to design * against and ship transparent. Absent means included, so nothing existing changes meaning. */ includeInExport?: boolean // For Gradients gradientStops?: ColorStop[] gradientTransform?: number[][] // 3x3 matrix for gradient bounds/rotation gradientHandlePositions?: { x: number, y: number }[] // e.g. [{x: 0, y: 0.5}, {x: 1, y: 0.5}] // For Images imageHash?: string // points to some blob store, or base64 data for now imageUrl?: string // standard URL fallback scaleMode?: 'FILL' | 'FIT' | 'TILE' | 'CROP' /** A stock photo's licence and attribution, kept with the picture (lib/photos/stock.ts). */ imageCredit?: MediaCredit boundVariables?: Record<string, string> // For Shader fills: user WGSL that defines `fn shade(uv, res, time) -> vec4<f32>`, // compiled at runtime and rendered to an offscreen texture (see lib/engine/shader-fill.ts) shaderCode?: string shaderName?: string /** * Values for the shader's `struct Params { … }` fields, by field name (lib/document/shader-params.ts reflects the * struct). A field without a value here draws with its `// @default`. These go to a uniform, never into the code, * so a drag or an animated value re-renders the fill without recompiling it. Keyable per field: * `fills[@f1].shaderParams.speed`, and per component for vectors (`.center.x`) and colours (`.tint.r`). */ shaderParams?: Record<string, ShaderParamValue> /** * The node graph this shader was built from (lib/document/shader-graph.ts): it compiles to `shaderCode` plus the * starting `shaderParams` through the same reflected `struct Params` contract, so the renderer only ever sees the * code. Edit the graph and compile it again; the knob values stay in `shaderParams`. */ shaderGraph?: ShaderGraph // Texture tab round-trip: the parameters a generated noise/texture shader was built from textureParams?: TextureFillParams // For Pattern fills: tile another layer across the shape (Figma Draw pattern fill) patternSourceId?: string patternScale?: number // source size multiplier (default 1) patternSpacing?: { x: number; y: number } // gap between tiles, world units patternOffset?: { x: number; y: number } // tile phase shift, world units patternAlignment?: 'GRID' | 'BRICK_H' | 'BRICK_V' // Stock pattern (lib/patterns/stock-patterns.ts) instead of a source layer: id + [background, ink, accent] patternPreset?: string patternColors?: Color[] // For Video fills: the clip lives in the media store (IndexedDB) under its content hash videoHash?: string videoName?: string videoLoop?: boolean /** Legacy: whether the video's sound was off. Upgraded on load to `videoSound.muted` (lib/motion/migrate.ts). */ videoMuted?: boolean videoPlayback?: 'AUTO' | 'PRESENT_ONLY' | 'PAUSED' videoPosterTime?: number // seconds — the frame shown when paused / exported // A video is a clip on its frame's timeline (lib/motion/video-clip.ts): the layer's bar is where it plays (its // time offset and span), and the source it shows at layer time `t` is `videoIn + t · videoSpeed`. /** Source ms shown at the layer's time 0 (slip; below 0 when the source starts after the layer does): 0 unset. */ videoIn?: number /** Source ms per layer ms (0.25…4): 1 unset. Its sound is stretched to match, pitch kept. */ videoSpeed?: number /** Its sound, linked to the picture (lib/motion/sound.ts): unset, off (as a video without sound settings always was). */ videoSound?: VideoSound /** How the clip comes in over what is under it, from its bar's start (a cut when unset). */ videoTransition?: VideoTransition /** Pose only, never stored: the source second the layer shows at the posed moment (lib/motion/pose.ts). */ videoTime?: number } /** * A video's own sound, linked to its picture: heard over the layer's bar, from the same source time as the picture * (lib/motion/sound.ts derives an AudioClip from it, so it mixes, plays and exports as every other clip). Levels, * fades and balance are the AudioClip's own fields; gain keys are clip ms (0 = the bar's start). */ export interface VideoSound extends Pick<AudioClip, 'gain' | 'fadeIn' | 'fadeOut' | 'fadeCurve' | 'pan' | 'muted'> { /** ms the sound is heard later than its picture (an Alt-drag of one without the other); 0 unset. */ offset?: number } /** A video clip's transition in (lib/motion/video-clip.ts): a cross-dissolve over what is under it, or a dip through a colour. */ export interface VideoTransition { type: 'DISSOLVE' | 'DIP_BLACK' | 'DIP_WHITE' /** ms, from the bar's start. */ duration: number } export type TextureNoiseType = 'VALUE' | 'PERLIN' | 'WORLEY' | 'FBM' | 'PAPER' | 'CANVAS' export interface TextureFillParams { noise: TextureNoiseType scale: number // features per 100 world px-ish (1..50) contrast: number // 0..2 colorA: Color colorB: Color seed: number animated: boolean } export interface StrokeStyle { /** Stable address for motion tracks (`strokes[@s1].color`); assigned when a track first needs it. */ id?: string color: Color width: number | [number, number, number, number] alignment: 'CENTER' | 'INSIDE' | 'OUTSIDE' cap: 'NONE' | 'ROUND' | 'SQUARE' join: 'MITER' | 'BEVEL' | 'ROUND' dashPattern: number[] boundVariables?: Record<string, string> } export type Effect = ShadowEffect | BlurEffect | ProgressiveBlurEffect | MotionBlurEffect | NoiseEffect | TextureEffect | GlassEffect | HalftoneEffect export interface Shadow { /** Stable address for motion tracks (`effects[@e1].blur`); assigned when a track first needs it. Every effect carries one. */ id?: string color: Color offset: { x: number; y: number } blur: number spread: number visible: boolean type: 'DROP_SHADOW' | 'INNER_SHADOW' /** Drop shadow: draw it behind the layer's semi-transparent fill (Figma's "show behind transparent areas"). Unset on older files means yes. */ showBehindTransparent?: boolean } export type ShadowEffect = Shadow /** Legacy blur record (documents before the unified effect list); read through `lib/document/effects.ts`. */ export interface Blur { type: 'LAYER' | 'BACKGROUND' radius: number visible: boolean } export interface BlurEffect { id?: string; type: 'LAYER_BLUR' | 'BACKGROUND_BLUR'; radius: number; visible: boolean } /** Blur that ramps along a direction: none before `start`, full `radius` from `end` (fractions of the box along `angle`°). */ export interface ProgressiveBlurEffect { id?: string; type: 'PROGRESSIVE_BLUR'; radius: number; angle: number; start: number; end: number; visible: boolean; /** Which blur ramps: the layer itself (default) or the backdrop behind it. */ target?: 'LAYER' | 'BACKGROUND' } /** * Motion blur: the layer smeared along one direction — `radius` px of streak at `angle`° (0 is left–right, 90 top–bottom, * in the page's axes). On a group, frame or instance every layer inside smears the same way, so a whole character or a * camera group can streak through a whip pan or a punch (lib/document/effects.ts `inheritedMotionBlur`). */ export interface MotionBlurEffect { id?: string; type: 'MOTION_BLUR'; radius: number; angle: number; visible: boolean } /** Grain over the layer: `amount` 0–1, `size` in px, `density` 0–1 (share of cells that get grain); mono, per-channel color, duotone (A→B) or multitone (A/B/original). */ export interface NoiseEffect { id?: string; type: 'NOISE'; amount: number; size: number; monochrome: boolean; seed: number; visible: boolean; mode?: 'MONO' | 'COLOR' | 'DUOTONE' | 'MULTITONE'; density?: number; colorA?: Color; colorB?: Color } /** Procedural texture laid over the layer (same generators as texture fills), between two colors at `opacity`. */ export interface TextureEffect { id?: string; type: 'TEXTURE'; noise: TextureNoiseType; scale: number; contrast: number; opacity: number; colorA: Color; colorB: Color; seed: number; visible: boolean } /** Glass: the backdrop refracted through the layer's edges (`refraction` px, chromatic `dispersion` 0–1), frosted by `radius`, with a light rim (0–1) and a depth shadow (0–2). */ export interface GlassEffect { id?: string; type: 'GLASS'; radius: number; refraction?: number; dispersion?: number; rim?: number; depth?: number; visible: boolean } export interface HalftoneEffect { id?: string type: 'HALFTONE' /** Screen angle in degrees. */ angle?: number /** Cell size in world units. */ dotSize?: number /** Tone contrast (1 = unchanged, 0 = flat, up to 3). */ contrast?: number /** Ink color (single-tone mode). */ color1?: Color /** Paper / gap color (transparent when unset). */ color2?: Color /** Legacy: true = classic color halftone (dots take the source color). Superseded by colorMode. */ invert?: boolean /** Dots use the ink/band colors, or the source color under each cell. */ colorMode?: 'INK' | 'SOURCE' shape?: HalftoneShape /** Dot arrangement: square grid, parallel line screen, or concentric rings around `center`. */ layout?: 'UNIFORM' | 'LINEAR' | 'RADIAL' /** Where tone comes from: the layer's luminance, or a generated linear / radial gradient. */ toneSource?: 'CONTENT' | 'LINEAR_GRADIENT' | 'RADIAL_GRADIENT' /** Linear gradient direction in degrees. */ gradientAngle?: number /** Radial layout / gradient centre in node space (0..1). */ center?: { x: number; y: number } /** Swap light and dark. */ invertTone?: boolean /** Separate shapes / colors / sizes for light, mid and dark tones. */ bands?: { enabled: boolean; midStart: number; darkStart: number; light: HalftoneBand; mid: HalftoneBand; dark: HalftoneBand } visible: boolean } export type HalftoneShape = 'CIRCLE' | 'SQUARE' | 'DIAMOND' | 'LINE' | 'CROSS' | 'TRIANGLE' /** One tone band of a halftone (light / mid / dark). */ export interface HalftoneBand { shape: HalftoneShape color: Color /** Dot size multiplier for this band (1 = coverage matches tone). */ scale: number } ``` #### Shaders and the shader graph ```ts /** A shader param's value: `f32` a number, `vec2f`/`vec3f` a vector, `vec4f` (or `vec3f` marked `@color`) a colour. */ export type ShaderParamValue = number | Color | { x: number; y: number } | { x: number; y: number; z: number } /** * A node shader graph (see `Paint.shaderGraph`; lib/document/shader-graph.ts compiles it). Nodes and edges are maps * keyed by id, but they do not merge per node in collab: a layer's `fills` is one value in the shared doc * (lib/collab/ydoc.ts), so two people editing one graph at once overwrite each other. A node's unconnected inputs * double as its knobs, and each knob is a `struct Params` field of the compiled shader (`<node>_<port>`), so it is * keyable under that name. */ export interface ShaderGraph { /** `options`: compile-time choices (a math operator, the signal an Audio node follows); they change the code. */ nodes: Record<string, { id: string; kind: string; x: number; y: number; inputs?: Record<string, ShaderParamValue>; options?: Record<string, string> }> edges: Record<string, { id: string; from: { node: string; port: string }; to: { node: string; port: string } }> } ``` #### Styles ```ts export type StyleType = 'PAINT' | 'TEXT' | 'EFFECT' | 'GRID' export interface BaseStyle { id: string name: string type: StyleType description?: string } export interface PaintStyle extends BaseStyle { type: 'PAINT' paints: Paint[] } export interface TextStyle extends BaseStyle { type: 'TEXT' /** CHARACTER styles apply to runs or nodes; PARAGRAPH styles also carry the paragraph properties below. */ scope?: 'CHARACTER' | 'PARAGRAPH' textAlign?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' spaceBefore?: number list?: ListStyle tabStops?: TabStop[] hyphenate?: boolean fontFeatures?: FontFeatureSettings fontVariations?: FontVariationSettings fontFamily: string fontWeight: number fontSize: number fontStyle: 'normal' | 'italic' letterSpacing: number lineHeight: number | 'AUTO' paragraphSpacing: number paragraphIndent: number textDecoration: 'NONE' | 'UNDERLINE' | 'STRIKETHROUGH' textCase: 'ORIGINAL' | 'UPPER' | 'LOWER' | 'TITLE' } export interface EffectStyle extends BaseStyle { type: 'EFFECT' effects: Effect[] } export interface LayoutGrid { type: 'GRID' | 'COLUMNS' | 'ROWS' visible: boolean color: Color count?: number // for columns/rows size?: number // section size for grid, or width/height for cols/rows gutter?: number // for columns/rows margin?: number // offset for MIN/MAX alignment } export interface GridStyle extends BaseStyle { type: 'GRID' layoutGrids: LayoutGrid[] } export type Style = PaintStyle | TextStyle | EffectStyle | GridStyle ``` #### 3D ```ts /** * How a 3D layer takes the scene's lights (lib/document/scene3d.ts). Every field is optional and keyable; with no * lights in the scene, or `acceptsLights: false`, the layer draws exactly as it would unlit. */ export interface Material3D { /** Default true. */ acceptsLights?: boolean /** How much ambient light reaches the layer (default 1). */ ambient?: number /** How much directional, point and spot light it scatters (Lambert; default 1). */ diffuse?: number /** Blinn-Phong highlight strength (default 0: none). */ specular?: number /** Highlight tightness: the Blinn-Phong exponent (default 24). */ shininess?: number } export type Light3DType = 'AMBIENT' | 'DIRECTIONAL' | 'POINT' | 'SPOT' /** * A light in a 3D context. Positions and targets are in the context's own space: px from its top-left, x right, * y down, z toward the viewer. A directional light shines from its position toward its target (only the * direction matters); a spot is a point light limited to a cone around the line to its target. */ export interface Light3D { /** Stable: tracks address the light as `scene3d.lights[@id]`. */ id: string type: Light3DType name?: string color: Color /** 1 is full strength. */ intensity: number x: number y: number z: number targetX: number targetY: number targetZ: number /** Spot: the whole cone, in degrees (default 60). */ coneAngle?: number /** Spot: how much of the cone's edge fades, 0 (hard) to 1 (all of it; default 0.5). */ coneFeather?: number /** Default true. */ visible?: boolean } /** * The camera of a 3D context. Its eye starts where CSS `perspective` puts the viewer: in front of the point of * interest (default: the perspective origin on the z = 0 plane) at the zoom distance, so an untouched camera * frames the context exactly as the flat layout. Every field is keyable. */ export interface Camera3D { /** * Focal length in mm, on a 36 mm film across the context's width: the keyable truth. The zoom distance (the * CSS `perspective`, in px) and the field of view are derived from it (lib/document/transform3d.ts `zoomPx`). */ focalLength: number /** Rig offsets in the context's space (px): truck, pedestal, dolly. The point of interest travels with them. */ x?: number y?: number z?: number /** Orbit about the point of interest, in degrees (around the vertical axis). */ orbit?: number /** Tilt (look up/down) and pan (look left/right), degrees, about the eye. */ rotateX?: number rotateY?: number /** Roll about the view axis, degrees. */ roll?: number /** Point of interest in the context's space. Unset: the perspective origin, at z 0. */ poiX?: number poiY?: number poiZ?: number /** Depth of field. Reserved: stored and synced, not drawn yet. */ dof?: { enabled?: boolean; focusDistance?: number; aperture?: number; blurLevel?: number } } /** * A 3D context on a group or frame (CSS preserve-3d): its layers — its children, and the children of plain groups * below it — are placed in 3D (rotateX/rotateY/z), each drawn as a flat card seen through the camera, sorted back * to front. To everything outside, the context is a 2D layer at its own place. lib/document/scene3d.ts. */ export interface Scene3D { camera: Camera3D lights?: Light3D[] /** The perspective origin as a fraction of the box (default 0.5, 0.5: the centre). */ originX?: number originY?: number } /** How an ORBIT_3D drag turns its layer: a horizontal drag turns it about its vertical axis (rotateY), a vertical one tilts it (rotateX) when `tilt` is on, within `tiltRange`. */ export interface Orbit3D { /** Degrees per px of drag (default 0.4). */ degreesPerPx?: number /** A vertical drag tilts it too (default off). */ tilt?: boolean /** How far from its own the tilt goes, degrees (default [-30, 30]). */ tiltRange?: [number, number] /** * The layer a vertical drag tilts (its rotateX), when not the orbited one: a group holding it, so the object tilts * toward the camera whichever way it has turned, as a trackball does. */ tiltLayerId?: string } ``` #### Motion: keys, tracks, modifiers and timelines A node's animation is `motion` (`NodeMotion`): tracks keyed by property path (`x`, `opacity`, `fills[@<paintId>].color`, `fills[@f].shaderParams.<knob>`), each a sorted list of keys with an interpolation out of each key. A FRAME's `timeline` gives the clip its length, fps, work area, markers and its own audio lanes. Times are milliseconds. ```ts /** What every node can carry for motion (lib/motion). */ export interface MotionFields { /** Keyed property tracks, behaviours and layer time. Documents with the older `keyframes` / `timelineDuration` * fields are upgraded on load (lib/motion/migrate.ts). */ motion?: NodeMotion /** A top-level frame's timeline, as set in Animate mode; unset: long enough for its last key. */ timeline?: FrameTimeline /** A top-level frame's state machine (lib/motion/state-machine.ts): which part of its timeline plays, and when. */ stateMachine?: StateMachine /** The layer's colour label in the Animate timeline (After Effects' label colours): a way to group rows by eye. */ timelineColor?: TimelineColor /** * The part the layer plays in its screen's platformer (@popcraft/runtime physics): the player, the ground it * stands on, what it collects, what kills it, where it finishes — or an on-screen button that moves it. A screen * with a player plays as a game in Present mode and on a published site. */ physics?: LayerPhysics } export interface NodeMotion { tracks: Partial<Record<PropPath, Track>> /** Local time shift (ms): this layer and its subtree run `timeOffset` later (a layer start, a baked stagger). */ timeOffset?: number /** Visible range in parent time (ms): the layer exists in [in, out). */ span?: { in: number; out?: number } /** Live stagger over this container's children (lib/motion/time.ts). */ stagger?: Stagger } /** * A property path: `x`, `opacity`, `fills[@f1].color`, `effects[@e2].offset.x`, `text.content`. Array items are * addressed by their stable `id` (`[@id]`), never by index, so reordering or deleting items never retargets a track. */ export type PropPath = string & { readonly __propPath: unique symbol } /** A key's value; an outline (a vector's `paths`) morphs, see lib/motion/morph.ts. */ export type MotionValue = number | Color | string | boolean | VectorPath[] export interface Track<V extends MotionValue = MotionValue> { /** Sorted by `t`. Empty with modifiers means "the rest value plus the modifiers" (a constant spin needs no keys). */ keys: Key<V>[] modifiers?: Modifier[] muted?: boolean } export interface Key<V extends MotionValue = MotionValue> { /** Stable within its track: selection, copy/paste and collab address keys by id rather than by time. */ id: string /** ms, in the layer's local time. */ t: number v: V /** The curve to the next key. */ out: Interp /** * Spatial tangents of a position key (lib/motion/spatial.ts): px offsets from the key's position to the motion * path's bezier handles, arriving (`in`) and leaving (`out`). Stored on the x-axis key (`x` or `translateX`) of a * pair whose y-axis track has a key at the same time; absent: the path runs straight. */ spatial?: { in?: [number, number]; out?: [number, number] } /** Inside a component: the value comes from this NUMBER / COLOR property (`· mul + add` for numbers), per instance; * `v` is what it shows where the property has no value. "Count to N", "slide this far". */ bind?: KeyBinding } /** How a value travels from a key to the next one. */ export type Interp = | { kind: 'hold' } | { kind: 'linear' } /** CSS cubic-bezier control points. */ | { kind: 'bezier'; cp: [number, number, number, number] } /** A named curve; `param` is back's overshoot `c` (default 1.70158) or elastic's period (default 0.3). */ | { kind: 'ease'; name: EaseName; param?: number } /** A damped spring settling over the segment (motion.dev-style parameters). */ | { kind: 'spring'; stiffness: number; damping: number; mass: number } export type EaseFamily = 'sine' | 'quad' | 'cubic' | 'quart' | 'quint' | 'expo' | 'circ' | 'back' | 'elastic' | 'bounce' /** Penner's easing set (lib/editor/easing.ts `EASINGS`). */ export type EaseName = `${EaseFamily}-${'in' | 'out' | 'in-out'}` /** A key value taken from a component property. */ export interface KeyBinding { prop: string mul?: number add?: number } /** * A behaviour on a track, evaluated when the track is sampled (lib/motion/modifiers.ts). Pure and deterministic. * `loop` remaps time before the keys are sampled; the others change the value afterwards, in array order. * Rates and frequencies are per second of layer time; `bounce.start`/`end` are ms. */ export type Modifier = ModifierBase & ( /** Adds `rate` units per second: constant spin, scroll, drift. */ | { kind: 'spin'; rate: number } /** Adds `amp · sin(2π(freq·s + phase))`; `phase` in cycles. */ | { kind: 'sine'; amp: number; freq: number; phase?: number } /** Adds smooth seeded noise in [-amp, amp] (wiggle). */ | { kind: 'noise'; amp: number; freq: number; octaves?: number; seed: number } /** Adds `amp · |sin(π·freq·s)| · max(0, 1 − decay·s)` from `start` (until `end`): a decaying bounce. */ | { kind: 'bounce'; amp: number; freq: number; decay: number; start: number; end?: number } /** Past `range` (default: the first to the last key) the keys repeat, mirror, carry on at the end speed, or repeat stacked. */ | { kind: 'loop'; mode: 'cycle' | 'pingpong' | 'continue' | 'offset'; range?: [number, number] } /** Wraps the value into [offset, offset + period): a scrolling grid. */ | { kind: 'mod'; period: number; offset?: number } /** Takes another layer's sampled value (`· mul + add`, `delay` ms behind): a pick-whip. */ | { kind: 'link'; source: { nodeId: string; path: PropPath }; mul?: number; add?: number; delay?: number } | { kind: 'clamp'; min?: number; max?: number } /** * Follows the audio the timeline plays (lib/motion/audio-signals.ts): the signal (0…1, `gain`× then past * `threshold`) maps onto `from`…`to`, which is added to the value (`add`, the default), replaces it (`set`) or scales * it (`multiply`). A colour track mixes toward `color` by the signal. `attack`/`release` smooth it (ms), `lag` reads it * that many ms back (a waveform's history), `bin` picks a spectrum band (0 low … 1 high), `clip` one clip (id or media * hash). With nothing heard (editing at rest) the signal is 0. */ | { kind: 'audio'; signal: AudioSignalName; gain: number; from: number; to: number; mode?: AudioBehaviourMode; bin?: number; attack?: number; release?: number; lag?: number; threshold?: number; clip?: string; color?: Color } ) /** What every behaviour carries besides its own parameters. */ export interface ModifierBase { /** Stable within its track: the behaviours list addresses a behaviour by id, so a reorder never retargets an edit. */ id?: string /** * Keyed parameters: a numeric parameter with keys here takes its value from them (layer time, the keys' own curves) * instead of the stored field — a wiggle whose amplitude dies down, a spin that speeds up. */ animated?: { [param: string]: Key<number>[] | undefined } } /** What an `audio` behaviour follows (lib/motion/audio-analysis.ts computes each from the decoded file). */ export type AudioSignalName = 'level' | 'bass' | 'mid' | 'treble' | 'beat' | 'onset' | 'spectrum' /** How an `audio` behaviour's mapped signal meets the value. */ export type AudioBehaviourMode = 'add' | 'set' | 'multiply' export interface Stagger { /** ms between consecutive children. */ delay: number order: StaggerOrder /** For `random`: the same seed gives the same order. */ seed?: number /** Spreads the offsets along a curve instead of evenly (the total stays `delay · (n − 1)`). */ distribution?: Interp } /** The order a stagger visits a container's children in. */ export type StaggerOrder = 'index' | 'reverse' | 'center' | 'random' | { ids: string[] } /** * Frame rates a timeline snaps and steps at, in menu order — the one list the timeline, export, templates and the * command schemas read (120 for high-frame-rate screens and slow-motion edits). */ export const TIMELINE_FPS_VALUES = [24, 25, 30, 50, 60, 120] as const export type TimelineFps = (typeof TIMELINE_FPS_VALUES)[number] /** A named moment on a timeline: a snap target, a cue. `pause` markers are present mode's click steps. */ export interface TimelineMarker { id: string /** ms, frame time. */ t: number label?: string kind?: 'marker' | 'pause' } /** An animated top-level frame's timeline. */ export interface FrameTimeline { /** ms. */ duration: number /** The frame grid keys, the playhead and nudges snap to (60 when unset). */ fps?: TimelineFps /** The range [in, out) (ms) that playback loops within and export renders. Unset: the whole duration. */ workArea?: [number, number] /** Sorted by `t`, unique ids. */ markers?: TimelineMarker[] /** Sound of its own (frame ms), on lanes like a composition's: detached video sound, imported files. */ audio?: AudioClip[] } ``` #### Compositions, scenes and audio clips ```ts /** A scene sequence: its clips, in the order they play. */ export interface Composition { id: string name: string /** px: the stage every scene is shown on, centred. */ width: number height: number fps: TimelineFps /** Behind the scenes (where no clip is playing, or a scene does not cover the stage). */ background?: Color /** A deck's soundtrack (lib/audio/presentation): the page it narrates, how it was made, each slide's narration. */ presentation?: import('../audio/presentation/types').PresentationSoundtrack /** Sorted by `start`; each clip's transition fits in its overlap with the clip before (lib/motion/invariants.ts). */ clips: SceneClip[] /** Its sound (lib/motion/audio-plan.ts): clips of audio from the media store on lanes, mixed; sorted by lane, then start. */ audio?: AudioClip[] /** Playback loops within, and export renders, [in, out) ms of composition time. Unset: all of it. */ workArea?: [number, number] /** Sorted by `t`, unique ids. */ markers?: TimelineMarker[] /** Present mode: at the end, start again from the first scene instead of holding the last frame. */ showLoop?: boolean } /** One scene of a composition: frame `frameId`'s timeline from `in` to `out` (frame ms), starting at `start` (composition ms). */ export interface SceneClip { id: string frameId: string /** Composition ms the clip starts at. */ start: number /** Frame ms the clip plays from. */ in: number /** Frame ms it plays to (exclusive): the clip lasts `out − in`. */ out: number /** How it replaces the clip before it: over the first `duration` ms of this clip, while the one before plays out. */ transitionIn?: SceneTransition /** Present mode: at this scene's end, carry on (unset, `end`), wait for a click, or hold `seconds` first. */ advance?: ShowAdvance } /** * A piece of an audio file on one of a composition's lanes: the file's `in` to `out` (file ms), heard from `start` * (composition ms). Times are ms in the document; they become samples only inside the mixer (48 kHz). Clips on one * lane never overlap (lib/motion/audio-clips.ts); lanes sum. */ export interface AudioClip { id: string /** The file in the media store (lib/storage/media-store.ts, `popcraft-media:<hash>`), content-hashed like `videoHash`. */ mediaHash: string /** What the strip labels it (the file's name when imported). */ name: string /** Composition ms the clip is heard from. */ start: number /** File ms it plays from. */ in: number /** File ms it plays to (exclusive): the clip lasts `out − in`. */ out: number /** Level over the clip as a linear factor (1 = as recorded), keyed in clip ms (0 = the clip's start); unset: 1. */ gain?: Track<number> /** ms the clip takes to rise from silence at its start (linear); unset: none. */ fadeIn?: number /** ms it takes to fall to silence at its end (linear); unset: none. */ fadeOut?: number /** The fades' shape: straight lines (unset), or equal power (sin/cos) — what a crossfade uses, so its level holds. */ fadeCurve?: 'linear' | 'equalPower' /** Plays again back to back from its start for as long as its timeline plays (background music); unset: once. */ loop?: boolean /** File ms per clip ms (0.25…4), pitch kept (lib/motion/time-stretch.ts): the clip lasts `(out − in) / speed`. 1 unset. */ speed?: number /** Derived, never stored: the video layer this is the linked sound of (lib/motion/sound.ts). */ link?: string muted?: boolean /** Which lane it sits on (0 is the top one). */ lane: number /** Stereo balance, −1 (left) … 1 (right); unset: centre. */ pan?: number } /** A transition between two scenes (the same types and evaluator as prototype transitions: lib/motion/transitions.ts). */ export interface SceneTransition { type: TransitionType /** ms. */ duration: number /** The curve its progress follows. */ interp: Interp params?: TransitionParams } export interface TransitionParams { /** Where circle wipes, irises and zooms centre, as fractions of the stage (0.5, 0.5 by default). */ center?: { x: number; y: number } } ``` #### Components and instances ```ts export interface ComponentNode extends SceneNode { type: 'COMPONENT' description?: string /** A local copy of a library component (inserted or unlinked): which one, so another insert reuses this copy. */ libraryOrigin?: LibraryOrigin codeConnect?: CodeConnect /** * The component is drawn by code on a web page (lib/code): a React component from one of the file's code modules, * given the component's properties. Its layers are what shows where the code cannot run or has not been drawn yet. */ code?: CodeRef componentPropertyDefinitions: Record<string, ComponentPropertyDefinition> variantProperties?: Record<string, string> // e.g. { "Size": "Small", "State": "Hover" } layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL' primaryAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' | 'SPACE_BETWEEN' counterAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' padding?: { top: number; right: number; bottom: number; left: number } itemSpacing?: number /** * In a 3D scene, its instances pass through like a plain group (CSS `transform-style: preserve-3d`): their * layers are 3D layers of the scene, placed through the instance, instead of one flattened card — so a part of a * 3D object can be an interactive component and stay solid. Honoured when the component draws nothing of its own * (no visible fill, stroke or effect, no clip). lib/document/scene3d.ts `passesThrough`. */ preserve3d?: boolean } export interface ComponentSetNode extends SceneNode { type: 'COMPONENT_SET' description?: string /** A local copy of a library's component set (see `ComponentNode.libraryOrigin`). */ libraryOrigin?: LibraryOrigin // componentPropertyDefinitions is here to govern the whole set componentPropertyDefinitions: Record<string, ComponentPropertyDefinition> layoutMode?: 'NONE' | 'HORIZONTAL' | 'VERTICAL' primaryAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' | 'SPACE_BETWEEN' counterAxisAlignItems?: 'MIN' | 'MAX' | 'CENTER' padding?: { top: number; right: number; bottom: number; left: number } itemSpacing?: number } export interface InstanceNode extends SceneNode { type: 'INSTANCE' mainComponentId: string overrides: Record<string, Partial<AnyNode>> componentProperties: Record<string, ComponentPropertyValue> /** Inside a component: this nested instance's properties show on every instance of that component. */ isExposedInstance?: boolean /** How this instance plays its component's timeline (a motion component). Unset fields: the component's MOTION * property defaults, then `DEFAULT_PLAYBACK` (lib/motion/time.ts). */ playback?: InstancePlayback } /** * NUMBER and COLOR feed layer paths (`componentPropertyPaths`) and key values (`Key.bind`): a counter's target, a * progress, an accent. IMAGE is a picture's address, fed to an image layer's or an image fill's picture the same way. MOTION shows one of the instance's playback settings (`InstanceNode.playback`) as a property. */ export type ComponentPropertyType = 'BOOLEAN' | 'TEXT' | 'INSTANCE_SWAP' | 'VARIANT' | 'NUMBER' | 'COLOR' | 'MOTION' | 'IMAGE' /** A value an instance gives a component property. */ export type ComponentPropertyValue = string | boolean | number | Color export interface ComponentPropertyDefinition { type: ComponentPropertyType /** INSTANCE_SWAP / VARIANT: a component id. NUMBER: a number. COLOR: a color. IMAGE: a picture's address. MOTION: the bound playback field's default. */ defaultValue: ComponentPropertyValue /** INSTANCE_SWAP: components offered first in the picker (ids). VARIANT: the variants of one set (ids). */ preferredValues?: string[] /** NUMBER: the range and step the instance's control offers. */ min?: number max?: number step?: number /** MOTION: the playback field the property sets on each instance. */ binds?: PlaybackField } /** Where a local component was copied from: a library's component, made the file's own (lib/document/libraries.ts). */ export interface LibraryOrigin { libraryId: string; id: string; name?: string } /** * An instance's time mapping onto its component's timeline (lib/motion/time.ts `instanceTime`): the one mapping * nested instances compose and precomps reuse. */ export interface InstancePlayback { /** ms after its own zero (its layer time, or the trigger) that it starts. */ start?: number /** Component ms per ms (≥ 0; 0 holds the first frame). */ speed?: number loop?: PlaybackLoop /** What a finished, unlooped playback shows: its last frame (default) or its first. */ hold?: 'first' | 'last' /** Play only between two of the component timeline's markers (ids), in order. */ range?: [string, string] trigger?: PlaybackTrigger } /** What starts an instance's playback. `inView`: when it is on screen (present mode: the frame is shown; web export: scrolled into view). `manual`: only a state change (a variant switch) plays it. */ export type PlaybackTrigger = 'load' | 'click' | 'hover' | 'inView' | 'manual' export type PlaybackLoop = 'none' | 'loop' | 'pingpong' export type PlaybackField = 'start' | 'speed' | 'loop' | 'hold' | 'trigger' ``` #### Variables ```ts export interface VariableCollection { id: string name: string modes: { id: string, name: string }[] defaultModeId: string } export interface Variable { id: string name: string type: VariableType variableCollectionId: string valuesByMode: Record<string, any> description?: string /** Where the variable may be bound (empty / undefined: everywhere its type allows). */ scopes?: VariableScope[] /** How the variable is named in code per platform (Dev Mode shows it instead of the raw name). */ codeSyntax?: Partial<Record<CodeSyntaxPlatform, string>> } export type VariableType = 'COLOR' | 'FLOAT' | 'STRING' | 'BOOLEAN' export type VariableScope = 'ALL_FILLS' | 'FRAME_FILL' | 'SHAPE_FILL' | 'TEXT_FILL' | 'STROKE_COLOR' | 'EFFECT_COLOR' | 'WIDTH_HEIGHT' | 'GAP' | 'CORNER_RADIUS' | 'TEXT_CONTENT' | 'FONT_SIZE' | 'OPACITY' export type CodeSyntaxPlatform = 'WEB' | 'ANDROID' | 'iOS' ``` #### Prototyping: interactions, actions and conditionals ```ts export interface Interaction { id: string trigger: InteractionTrigger action: PrototypeAction triggerDelay?: number // ms, only for AFTER_TIMEOUT /** KEY_DOWN: the key that fires it (KeyboardEvent.key); any key when unset. */ triggerKey?: string /** ON_DRAG: only a drag this way fires it (a swipe down to close, a swipe right to go back); any way when unset. */ dragDirection?: DragDirection } export type InteractionTrigger = (typeof INTERACTION_TRIGGERS)[number] export interface PrototypeAction { navigationType: NavigationType destinationId: string | null // target frame / component id transition: PrototypeTransition // ── Variable actions ── variableId?: string // SET_VARIABLE: the variable to change variableValue?: any // SET_VARIABLE: the literal value to set (ignored when variableToggle) variableToggle?: boolean // SET_VARIABLE: flip a boolean variable instead of setting a literal /** * SET_VARIABLE: the value as an expression over the variables (`health - 15`, `min(maxHealth, health + 25)`, * `$key` for the key that fired): worked out when the action runs, in place of `variableValue`. A number set with * a transition duration glides there in present mode (bound layers ease over it) while the variable holds the * new value at once. */ variableExpression?: string /** CHANGE_TO: the instance that changes (unset: the one the interaction fired on, or the instance it sits in). */ targetId?: string /** CONDITIONAL: the branches, tried in order; the first that holds runs. */ branches?: ConditionalBranch[] collectionId?: string // SET_MODE: the collection whose active mode changes modeId?: string // SET_MODE: target mode id, or '__NEXT__' to cycle to the next mode // ── Overlay actions (OVERLAY / SWAP) ── overlayPosition?: OverlayPosition /** MANUAL position: offset of the overlay's top-left from the active frame's top-left. */ overlayOffset?: { x: number; y: number } /** Dim the frame behind the overlay (null / undefined: no dim). */ overlayBackground?: { color: Color; opacity: number } | null closeOnOutsideClick?: boolean // ── Timeline actions (TIMELINE) ── timeline?: TimelineControl // ── ORBIT_3D ── orbit?: Orbit3D /** OPEN_URL: where it goes (a web address, an email address, a phone number). */ link?: LinkTarget } export type NavigationType = (typeof NAVIGATION_TYPES)[number] /** One branch of a CONDITIONAL action: its actions run, in order, when `condition` holds (no condition: else). */ export interface ConditionalBranch { /** An expression over the variables (lib/document/expressions.ts); unset on the else branch. */ condition?: string actions: PrototypeAction[] } export interface PrototypeTransition { type: TransitionType duration: number // ms easing: EasingType /** SPRING easing parameters (Figma-style gentle/quick/bouncy presets map onto these). */ spring?: { stiffness: number; damping: number; mass: number } /** * SMART_ANIMATE: ms between the hand-offs of the destination's top-level layers, in layer order (a chained * hand-off: the backdrop moves first, then the cover, then the words). The whole glide still takes `duration`. */ stagger?: number /** CIRCLE_WIPE / IRIS / ZOOM_THROUGH: centre on the layer the interaction fired on instead of the frame's centre. */ origin?: 'CENTER' | 'LAYER' /** Where it centres, as fractions of the source frame: what the player works out for `origin: 'LAYER'` (and keeps for Back). */ params?: TransitionParams } export type TransitionType = | 'INSTANT' | 'DISSOLVE' | 'SMART_ANIMATE' | 'SLIDE_FROM_LEFT' | 'SLIDE_FROM_RIGHT' | 'SLIDE_FROM_TOP' | 'SLIDE_FROM_BOTTOM' | 'PUSH_FROM_LEFT' | 'PUSH_FROM_RIGHT' | 'PUSH_FROM_TOP' | 'PUSH_FROM_BOTTOM' /** Move out: the source slides away and reveals the destination underneath. */ | 'MOVE_OUT_TO_LEFT' | 'MOVE_OUT_TO_RIGHT' | 'MOVE_OUT_TO_TOP' | 'MOVE_OUT_TO_BOTTOM' /** The destination grows out of a circle about the transition's centre (a circle `clip-path` wipe). */ | 'CIRCLE_WIPE' /** Iris out: the source closes into a circle about the centre over the destination. */ | 'IRIS' /** The source rushes toward the view and fades while the destination settles in behind it. */ | 'ZOOM_THROUGH' /** A straight cut: `INSTANT` under the name editors use for scenes. */ | 'CUT' /** A whip pan: the camera swings left (both scenes rush right, the next one in from the left), streaked with motion blur. */ | 'WHIP_LEFT' /** A whip pan to the right: both scenes rush left, the next one in from the right, streaked with motion blur. */ | 'WHIP_RIGHT' /** * 3D (lib/motion/transitions.ts `transition3dPose`): the two frames turn, swing or travel in depth through a * perspective. A cube turns about the shared edge, the destination's face coming in from the named side. */ | 'CUBE_FROM_LEFT' | 'CUBE_FROM_RIGHT' | 'CUBE_FROM_TOP' | 'CUBE_FROM_BOTTOM' /** A card turned over: the source is its front, the destination its back, about the vertical (or horizontal) axis. */ | 'FLIP_HORIZONTAL' | 'FLIP_VERTICAL' /** A door: the source swings away on its left hinge, opening onto the destination (close: the destination swings shut over the source). */ | 'SWING_OPEN' | 'SWING_CLOSE' /** A deck: the destination slides up over the source, which sinks back (out: the source slides down off the destination). */ | 'STACK_IN' | 'STACK_OUT' /** The source falls away into the distance as the destination arrives from in front (pull: the other way). */ | 'DEPTH_PUSH' | 'DEPTH_PULL' /** Panels on a ring turning one step, the destination's from the named side. */ | 'CAROUSEL_FROM_LEFT' | 'CAROUSEL_FROM_RIGHT' export type EasingType = 'EASE_IN' | 'EASE_OUT' | 'EASE_IN_OUT' | 'LINEAR' | 'SPRING' export type OverlayPosition = 'CENTER' | 'TOP_LEFT' | 'TOP_CENTER' | 'TOP_RIGHT' | 'BOTTOM_LEFT' | 'BOTTOM_CENTER' | 'BOTTOM_RIGHT' | 'MANUAL' /** Which way a drag goes (ON_DRAG's `dragDirection`): its larger axis, and the sign along it. */ export type DragDirection = 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' /** * A TIMELINE action (lib/motion/timeline-control.ts): what it does to which timeline. * - SCRUB (ON_DRAG / ON_SCROLL): the drag's (or scroll's) distance along `axis` maps onto the span's time, `distance` * px covering the whole span; on release it holds, settles on the nearest marker, or plays on. * - PLAY / PAUSE / TOGGLE: the timeline's clock (a paused one holds where it is). * - SEEK: glide to a time, or the next / previous marker in the span: held, it holds there; playing, it plays on. */ export interface TimelineControl { op: 'SCRUB' | 'PLAY' | 'PAUSE' | 'TOGGLE' | 'SEEK' /** The active frame's timeline (default), or the motion instance the interaction belongs to. */ target?: 'FRAME' | 'INSTANCE' /** SCRUB: the drag axis (default X). */ axis?: 'X' | 'Y' /** SCRUB: px of drag that cover the span (default: the layer's own width or height). Negative: left / up moves on. */ distance?: number /** The ms range scrubbed and sought within (default: the whole timeline). */ span?: [number, number] /** SCRUB: after the drag ends — stay put (default), settle on the nearest marker, or play on from there. */ release?: 'HOLD' | 'SNAP' | 'PLAY' /** SEEK: a time (ms), or the next / previous marker. */ to?: number | 'NEXT' | 'PREV' /** SNAP and SEEK: how long the glide takes (ms, default 350). */ glide?: number } ``` #### Comments, annotations and Dev Mode ```ts export interface Comment { id: string authorId: string authorName: string authorAvatarUrl?: string message: string createdAt: number // timestamp updatedAt?: number resolved: boolean // Canvas position (world coordinates) x: number y: number // Page the pin is on (pins show only on their page; undefined = legacy, shown everywhere) pageId?: string // Attached node: the pin follows it, sitting at (offsetX, offsetY) from the node's world top-left nodeId?: string offsetX?: number offsetY?: number // emoji → user ids reactions?: Record<string, string[]> // Replies (nested comments) replies: CommentReply[] } export interface CommentReply { id: string authorId: string authorName: string authorAvatarUrl?: string message: string createdAt: number reactions?: Record<string, string[]> } /** Dev Mode annotation pinned to a layer. */ export interface Annotation { id: string; label: string; category?: 'GENERAL' | 'ACCESSIBILITY' | 'INTERACTION' | 'CONTENT' | 'DEV' } /** A link developers need alongside a layer (Storybook, Jira, docs…). */ export interface DevResource { id: string; name: string; url: string } export type DevStatus = 'READY_FOR_DEV' | 'COMPLETED' /** * Code Connect: how a component is written in the codebase. `template` is the call site with `{{Prop}}` * placeholders for the component's properties (and `{{children}}`); the dev panel fills them from the * selected instance's values. */ export interface CodeConnect { path: string; importStatement?: string; template: string; language?: 'react' | 'swift' | 'compose' | 'vue' | 'other' } ``` #### Brushes and drawing ```ts export interface BrushSettings { size: number // brush diameter in pixels opacity: number // 0-1 hardness: number // 0-1, edge softness // Procreate splits input smoothing into three independent systems. `smoothing` is StreamLine — the // speed-adaptive low-pass that glasses out curves. The other two are optional add-ons: smoothing: number // 0-100, StreamLine amount (curve smoothing / de-wobble) stabilization?: number // 0-100, trailing "rope" — the line lags behind the pen and catches up (weighted drag) motionFiltering?: number // 0-100, tremor de-noise — a short moving average that flattens hand shake pressureSensitivity: boolean preset: BrushPreset color: Color dynamics?: BrushDynamics // editable per-brush params; when unset, falls back to the preset's defaults renderMode?: BrushRenderMode // per-brush; when unset, falls back to the preset's default (defaultRenderMode) // "Blend as you draw": 0 = dry (paint the pure brush color), 1 = fully takes on the color already on // the canvas beneath each dab. Anything >0 forces RASTER (blending needs to read backing pixels). wetMix?: number // Smudge tool: how hard the finger pulls the pixels beneath the stroke (0 = none, 1 = long drag). smudgeStrength?: number // Liquify tool: which way the brush moves pixels, and how far. liquifyMode?: LiquifyMode liquifyStrength?: number // Color-dynamics target (Procreate "secondary color"). Used by colorPressure/colorTilt/colorStroke. secondaryColor?: Color // Per-stroke blend mode applied to the committed node (Procreate per-brush blend). blendMode?: BlendMode // When set, the active brush is a saved user brush (see lib/storage/brush-library.ts). userBrushId?: string // Paint into a RASTER paint layer (under the pen, or a new one) instead of creating a node per stroke. paintIntoLayer?: boolean } export type BrushPreset = // Library (Sept 2026) | 'MECH_PENCIL' | 'SKETCH_PENCIL' | 'COLORED_PENCIL' | 'VINE_CHARCOAL' | 'CHARCOAL_BLOCK' | 'CHARCOAL_PENCIL' | 'CHARCOAL_DUST' | 'BALLPOINT' | 'DIP_PEN' | 'FELT_TIP' | 'CROW_QUILL' | 'HATCH_PEN' | 'SCRATCH_PEN' | 'CONTOUR' | 'SOFT_ROUND' | 'SABLE' | 'DRY_BRUSH' | 'PALETTE_KNIFE' | 'STUCCO' | 'SPONGE' | 'RAKE' | 'SCUMBLE' | 'NOISE_TEX' | 'CANVAS_TEX' | 'PAPER_TOOTH' | 'BRUSH_SCRIPT' | 'CHISEL_MARKER' | 'FINE_HAIR' | 'SOFT_TINT' | 'HARD_ROUND' // Stretch (original set) | 'BASIC' | 'HEIST' | 'BLOCKBUSTER' | 'GRINDHOUSE' | 'BIOPIC' | 'SPAGHETTI' | 'SLASHER' | 'MARKER' | 'HIGHLIGHTER' | 'AIRBRUSH' | 'WATERCOLOR' // Inking | 'INKER' | 'BRUSH_PEN' | 'TECHNICAL' // Calligraphy | 'CALLIG_NIB' | 'ITALIC' | 'SCRIPT' // Sketching | 'PENCIL_2B' | 'CHARCOAL' | 'CRAYON' // Painting | 'FLAT_BRUSH' | 'ROUND_BRUSH' | 'GOUACHE' | 'OIL' // Spray | 'SPRAYPAINT' | 'SPLATTER' // ── Pro library (Procreate-style sets) ── // Sketching | 'PENCIL_6B' | 'PENCIL_HB' | 'SOFT_PASTEL' | 'OIL_PASTEL' | 'CHALK' | 'WILLOW_CHARCOAL' | 'CONTE' // Inking | 'STUDIO_PEN' | 'SYRUP' | 'GEL_PEN' | 'DRY_INK' | 'FINE_TIP' | 'INK_BLEED' // Calligraphy | 'MONOLINE' // Drawing | 'GESINSKI' | 'EAGLEHAWK' | 'OBERON' // Painting | 'ACRYLIC' | 'WET_ACRYLIC' | 'WATER_WASH' | 'WATER_BRUSH' // Airbrushing | 'AIRBRUSH_SOFT' | 'AIRBRUSH_MEDIUM' | 'AIRBRUSH_HARD' | 'AIRBRUSH_ULTRAFINE' // Spraypaints | 'FAT_CAP' | 'MEDIUM_NOZZLE' | 'FLICKS' // Textures | 'GRUNGE' | 'STIPPLE' | 'HALFTONE_DOTS' | 'CONCRETE' // Shape-source (stamp) brushes | 'BRISTLE_BRUSH' | 'SPATTER_STAMP' // Luminance (additive glow blends) | 'NEON' | 'LIGHTPEN' | 'GLIMMER' export type BrushTipShape = 'ROUND' | 'OVAL' | 'NIB' | 'CHISEL' | 'SQUARE' export type BrushTipStamp = 'BRISTLE' | 'SPATTER' | 'CHALK_BLOCK' | 'GRUNGE' | 'STIPPLE' export interface BrushDynamics { spacing: number // stamp step as a fraction of brush size (small = dense) scatter: number // max off-path jitter as a fraction of brush size count: number // stamps emitted per step (airbrush = many) jitter: number // 0..1 random per-stamp size reduction stampSize: number // base stamp radius as a fraction of brush size pressureSize: boolean // pressure modulates stamp radius taper: number // 0..1 taper of stamp size toward the stroke ends (both ends, unless split below) // ── Taper (Procreate "Taper") ── // `taper` is the shared default; these split it per end, the way a brush that flicks out but starts // blunt needs. Undefined falls back to `taper`, so a preset that only sets `taper` is unchanged. taperStart?: number // 0..1 lead-in taper; defaults to `taper` taperEnd?: number // 0..1 lift-off taper; defaults to `taper` // 0..1 — the taper also fades the mark, not just narrows it. A dry pencil flicks out thin AND pale; // an inker keeps full density right to the point. RASTER only (per-dab alpha). taperOpacity?: number // 0..1 — how wide the very tip stays, as a fraction of full width. Procreate's "Tip": 0 tapers to a // point, higher values stop short and keep a blunt nib. taperTip?: number // ── Shape levers (Procreate "Shape") ── tip?: BrushTipShape // dab silhouette; default ROUND // Shape SOURCE (Procreate stamp brushes): when set, each dab imprints this grayscale/alpha stamp // (tinted to the brush color, scaled to the dab, rotated to the dab angle) instead of a filled // polygon — the basis for bristle/spatter/chalk/texture brushes. RASTER only (a vector node can't // draw an arbitrary image). `tipStampImage` (a user-uploaded data URL) wins over `tipStamp` (a // built-in procedural stamp) when both are set. tipStamp?: BrushTipStamp tipStampImage?: string // true = tip rotates to follow the stroke direction; false = fixed tipAngle; 'AZIMUTH' = tip // orients to the stylus azimuth (falls back to direction for mouse / pens without azimuth) tipFollow?: boolean | 'AZIMUTH' tipAngle?: number // fixed tip angle in DEGREES (calligraphy nib); used when !tipFollow roundness?: number // 0.1..1 squash of the tip across its axis (1 = full) rotJitter?: number // 0..1 random per-dab rotation (fraction of ±180°) countJitter?: number // 0..1 random dropout of the per-step count // ── Dynamics levers (Procreate "Dynamics") ── speedSize?: number // -1..1: stroke speed scales dab size (+ = faster→bigger, − = faster→thinner) fallOff?: number // 0..1 stroke fades out along its arc length (RASTER only — per-dab alpha) // ── Color dynamics (RASTER only — vector nodes have a single fill) ── hueJitter?: number // 0..1 random per-dab hue shift lightJitter?: number // 0..1 random per-dab lightness shift // ── Apple-Pencil-style response ── pressureCurve?: number // -1..1 remap of pressure response (+ = softer/boosts light touch, − = firmer) // ── StreamLine viscosity ── // 0..1: how "gooey" the pen feels — scales the pulled-string drag radius (with the Smooth slider). // High = the line hangs behind the pen like elastic and gets dragged (Procreate inking pens). goo?: number // 0..1: how much of the recent line stays MUTABLE and re-fits ("curve as you draw" settling). // 1 = full liquid settle (digital ink); 0 = marks are permanent the moment they're laid — // physical media (pencils, charcoal, sprays) must NOT retroactively reshape. Default 0.7. settle?: number // ── Wet mix (Procreate "Wet Mix") ── // `BrushSettings.wetMix` is the brush's charge and stays the single "how wet is this" number. These // are the rest of what wet paint does, and each is off at 0 — a brush that sets none of them behaves // exactly as it did when wet mix was one lever. // // dilution thins the paint with water: the mark goes translucent and spreads // attack how fast the loaded charge is spent along the stroke — a brush that runs dry // pull the dab drags the color behind it forward, the way a loaded brush pushes a bead // grade wet paint floods the paper's tooth, so grain fades as the mark gets wetter dilution?: number attack?: number pull?: number grade?: number // ── Rendering (Procreate "Rendering") ── // Unset keeps the old derivation: ribbon brushes without a stamp glaze, everything else builds up. rendering?: BrushRendering // ── Grain (PHYSICAL brushes only — paper tooth inside the stroke) ── // The texture is anchored to the CANVAS (world space), so overlapping strokes share continuous // grain — exactly like drawing on real toothy paper (Procreate's "Texturized" grain behaviour). grain?: BrushGrainType grainDepth?: number // 0..1 — how strongly the paper valleys eat into the stroke grainScale?: number // tile scale (1 = native tooth size) — Procreate's grain "zoom" grainBehaviour?: BrushGrainBehaviour // default TEXTURIZED (anchored to the canvas) grainRotation?: number // degrees the tile is turned by, so one texture gives several surfaces grainBlend?: BrushGrainBlend // default SUBTRACT // ── Glazing ── pressureOpacity?: boolean // pressure also drives per-dab opacity (PHYSICAL only — light touch = translucent) // ── Pressure → scatter ── pressureScatter?: number // -1..1: + = harder press scatters wider, − = harder press tightens // ── Apple-Pencil tilt (Procreate "Apple Pencil" tab) ── // tilt = 0 upright … 1 lying flat. Mouse / pens without tilt read as upright (neutral). tiltSize?: number // -1..1: + = tilting the pen widens the dab (shading side), − = thins it tiltOpacity?: number // 0..1: tilting the pen makes dabs more translucent (PHYSICAL only) /** * 0..1: shading with the side of the tip. Laying the pen down flattens the dab into an oval lying along the pen * (its azimuth) — with tiltSize widening it and tiltOpacity lightening it, a pencil shades like a real lead's side. */ tiltShape?: number /** 0..1 of the way to flat (0.25 ≈ 22°): tilt up to this still draws with the point, as Procreate's tilt angle. */ tiltThreshold?: number // ── Secondary color dynamics (PHYSICAL only) — mix brush color → BrushSettings.secondaryColor ── colorPressure?: number // 0..1: light pressure leans toward the secondary color colorTilt?: number // 0..1: tilting leans toward the secondary color colorStroke?: number // 0..1: the stroke drifts to the secondary color along its length // ── Image grain (Procreate Grain.png) — overrides the procedural `grain` when set ── grainImage?: string } /** * How a stroke's dabs accumulate (Procreate's "Rendering" tab). * * The two ends of this already existed as one unnamed boolean: a stroke either max-blends its dabs and * takes the brush opacity once at the end (a glaze), or over-blends dabs that each carry the opacity and * therefore darken where the stroke crosses itself (build-up). Naming it makes the middle reachable. * * LIGHT_GLAZE one flat pass at the brush opacity; overlaps inside the stroke never darken * UNIFORM_GLAZE the same, and per-dab alpha (pressure, taper, fall-off) is ignored — dead flat * INTENSE_GLAZE dabs build up, but each lays down less than the full opacity * HEAVY_GLAZE dabs build up at full opacity — the stroke darkens fast where it overlaps * BLENDING build-up that also picks up the color underneath (wet mix is forced on) */ export type BrushRendering = 'LIGHT_GLAZE' | 'UNIFORM_GLAZE' | 'INTENSE_GLAZE' | 'HEAVY_GLAZE' | 'BLENDING' export type BrushGrainType = 'PAPER' | 'CANVAS' | 'SPECKLE' | 'NOISE' /** * Where the grain lives (Procreate's grain "Behaviour"). * * TEXTURIZED anchored to the canvas, so overlapping strokes share one continuous tooth — drawing on * real toothy paper. The only behaviour this engine had. * MOVING anchored to the brush, so the tooth travels with the stroke and smears along it — dry * media dragged across a surface rather than rubbed into one. */ export type BrushGrainBehaviour = 'TEXTURIZED' | 'MOVING' /** * How the grain meets the mark. * * BITE the tooth's peaks take paint away — what this engine always did * HOLD the opposite: paint clings to the peaks and the valleys stay bare, the way a crayon dragged * over a raised surface catches the tops * SOFT bites, but gently — the tooth has to be pronounced before it removes much, which is how a * fine-tooth paper reads under a soft pencil */ export type BrushGrainBlend = 'BITE' | 'HOLD' | 'SOFT' export type BrushRenderMode = 'VECTOR' | 'RASTER' export type SymmetryMode = 'VERTICAL' | 'HORIZONTAL' | 'QUADRANT' | 'RADIAL' export interface SymmetrySettings { mode: SymmetryMode center: { x: number; y: number } segments?: number // RADIAL only (default 8) } /** * How the liquify brush moves pixels (Procreate "Liquify"). * * PUSH carry pixels along with the brush * TWIRL_CW / TWIRL_CCW rotate the neighbourhood under the brush * PINCH draw the contents in toward the centre * EXPAND push them out from it * RECONSTRUCT relax the warp back toward the layer as it was when liquify first touched it */ export type LiquifyMode = 'PUSH' | 'TWIRL_CW' | 'TWIRL_CCW' | 'PINCH' | 'EXPAND' | 'RECONSTRUCT' /** A saved user brush (Brush Studio "Save as brush" / imported Procreate brush). */ export interface UserBrush { id: string name: string group: string // picker section ("My Brushes" or the imported set's name) basePreset: BrushPreset // fallback identity for code paths keyed by preset dynamics: BrushDynamics size?: number opacity?: number hardness?: number smoothing?: number stabilization?: number motionFiltering?: number renderMode?: BrushRenderMode wetMix?: number blendMode?: BlendMode secondaryColor?: Color source?: 'USER' | 'PROCREATE' /** Set when the brush came from an installed marketplace brush pack (uninstalling the pack removes it). */ packId?: string createdAt: number } ``` #### Everything else in the document model ```ts /** * Trim paths: only the part of a layer's stroke between `start` and `end` (fractions of its length) draws, turned * along the outline by `offset` degrees (360 is one lap). The fill is never trimmed. See lib/document/trim.ts. */ export interface StrokeTrim { start: number end: number offset: number /** A vector with several paths: each path trimmed on its own (unset), or all of them as one line, one after another. */ mode?: 'SIMULTANEOUS' | 'SEQUENTIAL' } export interface LayerPhysics { /** * `win`: hidden while the game plays, shown when the player reaches the goal (a "Level clear" banner). * `pose`: a layer inside the player shown only while the player is in its `pose` (an idle blink, a run cycle, a jump). * `parallax`: scenery that slides the other way as the player runs, by `parallax` of the player's travel (depth). * `reveal`: hidden in the design, shown when the block that names it (`bump.reveal`) is bumped (a used ? block). */ role: 'player' | 'solid' | 'platform' | 'collectible' | 'hazard' | 'goal' | 'control' | 'win' | 'pose' | 'parallax' | 'reveal' /** For a control: what pressing it does. */ control?: 'left' | 'right' | 'jump' /** For a pose layer: when the player shows it. A player without a layer for its state shows its idle one. */ pose?: 'idle' | 'run' | 'jump' /** For parallax scenery: how far it slides for each px the player travels (0.1 far away, 0.5 near). */ parallax?: number /** For a hazard: landing on it from above defeats it (the player bounces off) instead of costing a life. */ stomp?: boolean /** For a solid: hit from below it bumps up, gives `reward` coins the first time, and shows the layer `reveal` names. */ bump?: { reward?: number; reveal?: string } /** For a hazard: it walks `distance` px to the right of where it is drawn and back, at `speed` px/s (default 80), facing the way it goes. */ patrol?: { distance: number; speed?: number } /** For the player: how it moves (px, seconds); unset values are the runtime's defaults. */ settings?: { gravity?: number; speed?: number; accel?: number; jump?: number; maxFall?: number; coyote?: number; buffer?: number } } export interface AiGeneration { prompt: string /** The image model's id. */ model: string /** Aspect ratio key (lib/ai/image-models ASPECT_SIZES), e.g. "1:1". */ aspect: string /** The style hint; empty when none was given. */ style: string format: 'image' | 'svg' /** ms since epoch. */ createdAt: number } /** * A design written as code: the script, the size it was built at, every root it made (the master, its other sizes, * its components) and a hash of what it built, so a rebuild can tell when someone has edited the result by hand. */ export interface DesignScriptSource { source: string size: string | { width: number; height: number } roots: string[] builtHash: string } /** React source the file carries (lib/code): what a code component is drawn by. */ export interface CodeModule { id: string name: string /** TSX. Its exports are the components; `react` is the only package it may import unless `packages` names others. */ source: string /** Packages this file lets the module import beside React, each at an exact version (`{ "clsx": "2.1.1" }`). */ packages?: Record<string, string> } /** Which export of which module draws a component (the default export when unnamed). */ export interface CodeRef { moduleId: string; exportName?: string } /** One face of a font file a document carries (`Document.fonts`). */ export interface DocumentFont { id: string /** The family name texts use (`fontFamily`). It may differ from the name inside the file. */ family: string /** The font file: `media://<hash>` (TrueType, OpenType, a collection, WOFF or WOFF2). */ src: string /** The weight this face stands for, 1–1000 (400 when unset). */ weight?: number style?: 'normal' | 'italic' /** In a collection (.ttc / .otc): which face, from 0. */ face?: number /** The file it came from, for people reading the list. */ fileName?: string /** The face's PostScript name, as the file names it. */ postscriptName?: string /** * The file's bytes as a data URL. Only in a portable `.popcraft` file, which has no media store beside it; opening * the file moves them into the media store and drops this (lib/fonts/document-fonts.ts `internDocumentFonts`). */ data?: string } /** * A window width a page is designed for. The widest has no `maxWidth` and is the design itself (the base); each * narrower one applies below its `maxWidth`, and is drawn on the canvas at `previewWidth`. */ export interface Breakpoint { id: string; name: string; maxWidth: number | null; previewWidth: number } export interface SiteSettings { breakpoints?: Breakpoint[] /** * Whether a published site draws its 3D transitions on the GPU where the visitor's browser has WebGPU (unset: yes). * False: always in CSS 3D, and the GPU renderer is never shipped for them (lib/io/web/site.ts `screenTransitions3d`). */ gpuTransitions?: boolean /** * The site's environment: settings a store or a server reads by name (SHOPIFY_STORE_DOMAIN…), sent to its host as * its environment when it is deployed (lib/client/cloud/publish-store.ts). Kept in the file and seen by everyone who * can open it: public values only, never a secret. */ env?: Record<string, string> /** * The picture a link to the published site shows where it is shared (a chat, a social post): its `og:image` * (lib/io/web/project/share.ts). Unset: the first screen, drawn at 1200×630. */ shareImage?: ShareImageChoice } /** Where a site's share image comes from: the first screen, a frame of the file, or a picture the owner uploaded (`media://`). */ export type ShareImageChoice = { kind: 'auto' } | { kind: 'frame'; frameId: string } | { kind: 'image'; src: string } /** What a layer is as a form control on a web page. */ export const CONTROL_KINDS = ['FORM', 'INPUT', 'TEXTAREA', 'SELECT', 'CHECKBOX', 'RADIO', 'BUTTON'] as const export type ControlKind = (typeof CONTROL_KINDS)[number] export const INPUT_TYPES = ['text', 'email', 'number', 'tel', 'url', 'search', 'date', 'password', 'hidden'] as const /** * A layer as a form control (lib/document/controls.ts). The layer is still an ordinary layer: it is drawn, styled, * laid out and overridden as any other, and on a web page a real control takes its place and its look. */ export interface ControlMeta { kind: ControlKind /** The name its value is sent under. */ name?: string /** INPUT: what it takes (default text). */ inputType?: (typeof INPUT_TYPES)[number] /** INPUT / TEXTAREA: the hint shown while it is empty; unset: the words of its text layer. */ placeholder?: string required?: boolean /** The value it starts with; CHECKBOX / RADIO: the value it sends when checked; BUTTON: what it sends, under its name, with the form it sends. */ value?: string /** CHECKBOX / RADIO: checked to begin with. */ checked?: boolean /** SELECT: its choices. */ options?: { value: string; label: string }[] /** BUTTON: what it does in a form (default: submit). */ buttonType?: 'submit' | 'reset' | 'button' /** The variable that holds its value: typing writes it, so layers bound to it follow. */ variableId?: string /** FORM: what happens once it is sent (go to a thank-you screen, open an overlay). */ onSubmit?: PrototypeAction /** * FORM: what sending it does on a published site that has sign-in (lib/sites/auth.ts). `signUp` makes an account * from its `name`, `email` and `password` fields, `signIn` signs in with `email` and `password`, `signOut` ends the * session; `updateAccount` changes the visitor's name (`name`), `changePassword` their password (`password` and * `new_password`), `deleteAccount` ends the account (`password`), `forgotPassword` emails a reset link (`email`) that * opens the site's `/reset-password` page, where `resetPassword` sets the new one (`new_password`); `deleteRecord` (a form inside a list's copy) removes that copy's record from the visitor's own, `updateRecord` (also inside a copy) changes that record: the * fields the form sends take their new values and the rest stay as they were (a button's own `name` and `value` * are sent with the form it sends, so "Mark done" is a button named `done` with the value `Yes`). Unset: * its values are saved. `onSubmit` runs only when it worked. * * The commerce actions (`addToCart`, `updateCartLine`, `removeCartLine`, `checkout`, `search`) are the storefront's: * a components export (lib/io/web/project/components.ts) makes each a prop the form sends to, for a Next.js * Commerce server action (packages/runtime/src/commerce.ts). */ formAction?: 'signUp' | 'signIn' | 'signOut' | 'updateAccount' | 'changePassword' | 'deleteAccount' | 'forgotPassword' | 'resetPassword' | 'deleteRecord' | 'updateRecord' | CommerceAction /** FORM: the collection its submissions are kept in. */ collectionId?: string } /** The kind of value a collection's field holds. */ export const FIELD_TYPES = ['TEXT', 'RICH_TEXT', 'NUMBER', 'BOOLEAN', 'DATE', 'COLOR', 'MEDIA', 'URL', 'SLUG', 'OPTION'] as const export type FieldType = (typeof FIELD_TYPES)[number] export interface CollectionField { id: string; name: string; type: FieldType; options?: string[] } /** A set of records with the same fields: posts, products, team members (lib/document/collections.ts). */ export interface Collection { id: string name: string fields: CollectionField[] /** The field a record's page address is made from. */ slugFieldId?: string /** * Each signed-in visitor of the published site has their own records (lib/sites/data.ts): the ones in the file are * what the design is drawn with, and on the site a visitor sees and adds only theirs, kept in the site's database. */ perVisitor?: boolean /** * With `perVisitor`: every signed-in visitor sees everyone's records (a guest book, a team's board), each made by * whoever added it — `item_by` is their name, `item_mine` whether it is the visitor's own, and only its maker * changes or deletes one. */ shared?: boolean /** * What the records stand for in a storefront (packages/runtime/src/commerce.ts): its fields are the schema's, its * records are samples to design with, and a components export takes the real ones as a typed prop. */ schema?: CommerceSchema /** Record ids, in order. */ order: string[] } /** One record: its collection, and a value for each field (by field id). */ export interface CollectionRecord { id: string; collectionId: string; values: Record<string, unknown> /** When it was made (ISO), for a record a visitor saved: what `countSince` and `streak` count days by. */ createdAt?: string /** A shared collection's record: who made it, and whether that is the visitor looking at it. */ by?: string; mine?: boolean } /** One record of one collection, picked for a layer (`SceneNode.record`). */ export interface RecordPin { collectionId: string; recordId: string } /** A frame that repeats what it holds once for each record of a collection. */ export interface RepeatMeta { collectionId: string limit?: number offset?: number sort?: { fieldId: string; dir: 'ASC' | 'DESC' } /** An expression over `item` (lib/document/expressions.ts): only records it holds for are shown. */ filter?: string } /** A screen as a page of a website (lib/io/web/site.ts `routesOf`). */ export interface RouteMeta { /** Its address: `/`, `/pricing`, `/blog/first-post`. Unset: `/` for the first screen, else made from its name. */ path?: string /** The page's title (the browser tab, a search result); unset: the frame's name. */ title?: string description?: string /** A frame drawn as this page's own share image (`og:image`), in place of the site's (`SiteSettings.shareImage`). */ shareImage?: string /** The page a visitor gets for an address the site does not have. */ notFound?: boolean /** * Who the page is for, on a site with sign-in. `signedIn`: a visitor who is not signed in is taken to the site's * first `signedOut` page instead (its sign-in screen). `signedOut`: one who is signed in is taken to the first * `signedIn` page (their home). Unset: anyone. */ access?: 'signedIn' | 'signedOut' /** One page per record of this collection, at `path/<slug>`: the frame is the record's page, `item` its record. */ collectionId?: string } /** Where a link goes: a web address, one of the file's screens, a layer on the page, an email address or a phone number. */ export type LinkTarget = | { kind: 'url'; url: string; newTab?: boolean } | { kind: 'screen'; frameId: string } | { kind: 'anchor'; nodeId: string } | { kind: 'email'; value: string } | { kind: 'tel'; value: string } /** The element a layer is on a web page: a heading, a landmark, a list, a button, a link. */ export const SEMANTIC_TAGS = ['div', 'section', 'header', 'footer', 'nav', 'main', 'aside', 'article', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'p', 'span', 'button', 'figure', 'label'] as const export type SemanticTag = (typeof SEMANTIC_TAGS)[number] /** * What a layer means on a web page, beside how it looks (lib/document/semantics.ts): the element it is, how a screen * reader names it, the id a link to it uses, and where it links. */ export interface NodeSemantics { tag?: SemanticTag /** Its accessible name, where its words do not say it (an icon button's "Close"). */ label?: string /** Nothing a screen reader should meet: decoration. */ hidden?: boolean /** The id it has on the page (`#pricing`): what a link to this layer goes to. */ anchor?: string /** The whole layer is a link. */ link?: LinkTarget } /** * What a layer may set differently at a breakpoint: its layout and its size, whether it shows, and the size of its * words. Everything else (its paint, what it says) is the same at every width. */ export interface ResponsiveProps { visible?: boolean x?: number; y?: number; width?: number; height?: number minWidth?: number; maxWidth?: number; minHeight?: number; maxHeight?: number layoutMode?: FrameNode['layoutMode']; layoutWrap?: FrameNode['layoutWrap'] itemSpacing?: number; counterAxisSpacing?: number; padding?: FrameNode['padding'] primaryAxisAlignItems?: FrameNode['primaryAxisAlignItems']; counterAxisAlignItems?: FrameNode['counterAxisAlignItems'] layoutGridColumns?: FrameNode['layoutGridColumns'] layoutSizingHorizontal?: 'FIXED' | 'HUG' | 'FILL'; layoutSizingVertical?: 'FIXED' | 'HUG' | 'FILL' layoutPositioning?: 'AUTO' | 'ABSOLUTE' fontSize?: number; lineHeight?: number | 'AUTO'; textAlign?: 'LEFT' | 'CENTER' | 'RIGHT' | 'JUSTIFIED' /** The text style it is set in at this breakpoint (a headline that steps down a size): its size, leading and spacing come with it. */ textStyleId?: string } /** * Every trigger, in the order the editor lists them: the one list the type, the command schema and the editability * registry read. ON_SCROLL fires as the frame it is on scrolls: a TIMELINE scrub reads the scroll offset instead of * a drag. */ export const INTERACTION_TRIGGERS = ['ON_CLICK', 'ON_HOVER', 'ON_PRESS', 'ON_DRAG', 'MOUSE_ENTER', 'MOUSE_LEAVE', 'AFTER_TIMEOUT', 'KEY_DOWN', 'ON_SCROLL'] as const export const NAVIGATION_TYPES = ['NAVIGATE', 'OVERLAY', 'SWAP', 'SCROLL_TO', 'BACK', 'CLOSE', 'SET_VARIABLE', 'SET_MODE', 'CHANGE_TO', 'TIMELINE', 'ORBIT_3D', 'CONDITIONAL', 'OPEN_URL'] as const export type StateMachineValue = boolean | number | string export type StateMachineInputType = 'boolean' | 'number' | 'string' | 'event' /** A value the machine's guards read: a boolean, number or string with its starting value, or an event (fired, never held). */ export interface StateMachineInput { id: string name: string type: StateMachineInputType /** The starting value (events have none). */ value?: StateMachineValue } export type StateMachineGuardOp = 'eq' | 'neq' | 'gt' | 'gte' | 'lt' | 'lte' /** A condition on one input: compared with `value` (an event input: whether it was just fired). */ export interface StateMachineGuard { input: string op?: StateMachineGuardOp value?: StateMachineValue } export type StateMachineActionType = 'set' | 'toggle' | 'increment' | 'decrement' | 'fire' | 'reset' /** A change to one input: set to `value`, toggle a boolean, step a number by `value` (default 1), fire an event, reset to its start. */ export interface StateMachineAction { type: StateMachineActionType input: string value?: StateMachineValue } /** One state: the stretch of the timeline it plays (marker ids; unset `from`: the start, unset `to`: the next marker or the end). */ export interface StateMachineState { id: string name: string from?: string to?: string /** Plays its stretch over and over (default: once, then holds its last frame). */ loop?: boolean /** Timeline ms per ms (default 1). */ speed?: number /** Actions run when the state is entered. */ entry?: StateMachineAction[] /** Where it sits in the graph (px). */ x: number y: number } /** A way from one state (or any: '*') to another, taken when every guard holds. Checked in id order. */ export interface StateMachineTransition { id: string from: string to: string guards?: StateMachineGuard[] } export type StateMachineListenerType = 'pointerDown' | 'pointerUp' | 'pointerEnter' | 'pointerExit' | 'click' | 'complete' | 'keyDown' | 'keyUp' /** * What sets the inputs: a pointer event on a layer (any layer of the frame when unset), a key pressed or let go, or a * state's stretch finishing. */ export interface StateMachineListener { id: string type: StateMachineListenerType layer?: string /** keyDown / keyUp: the key (KeyboardEvent.key, letters in either case); unset, any key. */ key?: string actions: StateMachineAction[] } export interface StateMachine { /** Stable: the dotLottie file names the machine by it. */ id: string name: string /** The state it starts in. */ initial: string states: Record<string, StateMachineState> transitions: Record<string, StateMachineTransition> inputs: Record<string, StateMachineInput> listeners: Record<string, StateMachineListener> } ``` Editor state declared beside the model but never written to a file: `Camera`, `SelectionBounds`, `Command`, `SnapIndicator`, `ToolType`, `Extension`, `ExtensionManifest`. ### Motion, compositions and sound: how they play From `lib/motion/composition.ts`: > Compositions: scenes played one after another. Each clip plays a frame's own timeline (the frame stays an ordinary > artboard on the canvas, edited in its own timeline), trimmed to [in, out), and a clip's `transitionIn` overlaps > the clip before it for the transition's duration. > > Time: a clip maps composition time to its frame's time through `instanceTime` (lib/motion/time.ts) — the same > mapping motion-component instances and precomps use, with the clip's trim as its range and holding its last frame. > > Clips are kept in sequence (`sequenceClips`): the first starts at 0 and each later one where the one before > ends, minus its transition. Every edit here (reorder, trim, transition) re-sequences, so there are never gaps and > a transition always fits its overlap. The invariants (`repairComposition`) hold the same rules for documents that > arrive some other way (an old file, a collaborator, the agent). From `lib/motion/sound.ts`: > What a timeline sounds like, as audio clips: the one list the planner (audio-plan.ts), the export mix > (lib/io/video/composition-audio.ts) and live playback (audio-playback.ts) read, whatever the sound came from. > > a frame its own lanes (`timeline.audio`: imported files, detached video sound) and the linked sound of every > video layer in it — derived from the layer's clip mapping (video-clip.ts), so moving, trimming, > slipping or speeding the picture moves its sound with it, by construction > a scene its frame's sound, cut to the scene's trim and moved to where the scene plays; across a transition > the outgoing scene's sound and the incoming one's cross over with equal-power fades > a composition its own lanes and every scene's > > Derived clips carry `link` (the video layer) and ids made from their owner's (`<layer>~sound`, `<scene>/<clip>`), > so they are stable from one call to the next; nothing here writes the document. ### media:// references From `lib/storage/media-store.ts`: > media:// references for images inside documents ──────────────────────────────────────────── > > Brush bakes, smudges, pasted/dropped images and image fills used to live in the document JSON as > base64 data URLs — megabytes per stroke, copied into every history snapshot, save and (later) > sync message. They are now interned into this store and referenced as `media://<hash>`: > • interning is SYNCHRONOUS (decode + non-cryptographic 128-bit content hash + in-memory object > URL), so the node can reference the image immediately; the IndexedDB write happens after. > • resolution is synchronous when the blob is in memory (this session, or already loaded) and > async otherwise (`loadMediaUrl`), which the GPU image cache handles transparently. > • exports that leave the browser (.popcraft download, share links) re-embed data URLs. From `lib/storage/media-hash.ts`: > Content hashes for `media://<hash>` refs, shared by the client media store and the server media API. Where they appear: image paints (`Paint.imageHash`), IMAGE and RASTER node payloads, video fills (`videoHash`), `AudioClip.mediaHash`, uploaded fonts, brush bakes. A reader that cannot resolve a hash should keep the reference unchanged, not drop the node. ### Variables and prototype expressions From `lib/document/variables.ts`: > Variable resolution — the single implementation shared by the GPU render > loop, the SVG exporter, and world-bounds computation so they always agree. From `lib/document/expressions.ts`: > Prototype expressions: the small, safe language a "Set variable" action writes a value with (Figma's variable > expressions), a Conditional action branches on, and a layer's live value is bound to (`boundExpressions`, > resolved in lib/document/variables.ts). `health - 15`, `max(0, shield - 25)`, `score >= 1000 && !paused`, > `hp < 30 ? "LOW" : "OK"`, `pad(score, 6)`. > > Variables are named as the Variables panel names them (`player/health`), or by their last segment when that is > unique in the file (`health`); a name with spaces or dashes goes in braces (`{player/max health}`). `$key` is the > key that fired a key trigger. Nothing is ever `eval`ed: the text is tokenised and parsed into a tree once (cached > by its source), and the tree is walked against a scope. > > The grammar, loosest first: > ternary := or ('?' ternary ':' ternary)? > or := and (('||' | 'or') and)* > and := equality (('&&' | 'and') equality)* > equality := compare (('==' | '!=') compare)* > compare := sum (('<' | '<=' | '>' | '>=') sum)* > sum := product (('+' | '-') product)* + joins text when either side is text > product := unary (('*' | '/' | '%') unary)* > unary := ('-' | '!' | 'not') unary | primary > primary := number | "text" | 'text' | true | false | name | {name} | $key | fn '(' args ')' | '(' ternary ')' ### Components and instances From `lib/document/instances.ts`: > Instance children as Figma edits them: a child of an instance is addressed by a virtual id > `host::…::child`; edits to it become overrides on the host, except text and visibility that the > component bound to a property, which edit the property value instead. A nested instance the component > marks `isExposedInstance` shows its own properties on every host instance ("nested instances"). ### The shader graph From `lib/document/shader-graph.ts`: > The node shader graph: `Paint.shaderGraph` compiled to the WGSL of an ordinary SHADER fill (`shaderCode`, drawn by > lib/engine/shader-fill.ts) and the values of its knobs (`shaderParams`). There is one pipeline. A graph is an > authoring form of a shader fill, never a renderer of its own. The compiled code follows the contract every hand- > written shader follows (lib/document/shader-params.ts): `fn shade(uv, res, time)` plus a `struct Params` read > through `p`. So everything a shader fill has, a graph has too: the params panel, keyable knobs, audio inputs, > export. > > The model (lib/document/types.ts `ShaderGraph`): nodes and edges are maps keyed by id (a collaborator's > concurrent edit merges). Each node has a kind from `SHADER_NODES`, compile-time `options` (an operator, a signal) > and `inputs`. An input with no edge into it is a knob, the struct Params field `<node>_<port>`. It is keyable at > `fills[@f].shaderParams.<node>_<port>` and its value lives in the fill's `shaderParams` (the node's `inputs` give > the value it starts at). Topology and options make the code; knob values never do. So dragging a knob, keying it > or feeding it from the music re-renders without recompiling (the shader-fill cache key is `code|size|params`). > > Nodes are fields. Each compiles to a WGSL function of the pixel's coordinate, `fn sg_<id>(uv) -> T`, and a node > reads an input by calling it. Most pass their `uv` straight on. The warp nodes (zoom-rotate, kaleidoscope, tunnel, > liquid, mirror) call their input at a transformed coordinate. The feedback node calls its input many times, each > echo zoomed and turned a step further and read a little earlier in time. That gives MilkDrop's trails without a > previous frame, so every frame is a pure function of its time and knobs: deterministic, seekable, the same in > export. > > Port types are f32, vec2f, vec3f and vec4f (a colour, straight RGBA 0…1). A connection between two types converts: > a number spreads to a grey colour, a colour reads as its luminance, and so on (`coerce`). ### Versioning and migration A document carries no version number. Instead every document, however it arrives (opened from IndexedDB, imported, received from a collaborator on an older build, written by the agent), passes the same load funnel, and each step upgrades an older shape in place and is idempotent: a document with nothing to migrate comes back unchanged, and ids it makes are deterministic, so two clients migrating the same file write the same result. Old fields are still read, never written. Writers should produce the current shape; readers of files in the wild should expect the older shapes these steps accept. The load funnel, in order (from `lib/store/editor-store.ts`): 1. `fixCorruptedVectors` 2. `migrateDocumentEffects` 3. `migrateDocumentMotion` 4. `repairDocumentPaints` From `lib/motion/migrate.ts`: > Upgrading documents to the motion model, once, on load (the store's load funnel, beside > `migrateDocumentEffects`, and the collab binding for documents a collaborator on an older version wrote). > > keyframes.{x,y,width,height,rotation,opacity,cornerRadius} → motion.tracks[path], keys with ids > keyframes.fill → fills[@id].color (the first solid fill gets an id) > easing / bezier / spring → the key's `out` Interp > timelineDuration → timeline.duration > animation (a present-mode build) → a motion preset's keys (lib/motion/presets.ts) on > the frame's timeline, click steps after `pause` markers > a video fill's videoMuted → videoSound.muted: the video's linked sound > (lib/motion/sound.ts), off as before unless it was on > > Builds: a frame's on-load builds keep their delay and duration from 0; its first click step waits at a pause > marker placed where everything on load has finished (the builds and any keyframed motion), each further step at > the end of the one before — in the order the show played them (`order`, then tree order). The keys are those of > the preset matching the build (fade, rise, drop, pop, wipe, slide; exits by the direction they moved), with the > build's curve and the distances it used, so the show looks as it did. > > Idempotent: a node without the old fields comes back as the same object, and a document with nothing to > migrate is returned as is. Ids are deterministic (`k1…`, `f1…`, `m1…`), so two clients migrating the same > document write the same result. From `lib/document/effects.ts`: > The one effect system. A node's `effects` array holds every effect — shadows, blurs, progressive blur, > noise, glass, halftone — and an effect style can supply the whole list. Older documents stored shadows > and blurs in their own fields; `normalizeNodeEffects` folds those in when a document loads, and the > accessors below still read them so nothing else has to care which shape a node arrived in. > > Readers (renderer, exporters, Dev Mode) call `shadowsOf` / `blursOf` / `halftoneOf`; writers replace > `effects` through `withEffects`. Glass is a compound: it expands to a background blur, a rim highlight > and a soft drop shadow, so the GPU draws it with the passes it already has. Libraries carry their own `version` (`LibraryUse.version`, below), and templates keep old ids working through `meta.aliases`. ### The template format From `lib/templates/types.ts`: > The one template format. A template is either a whole document (pages, styles, variables — "open as a new > file") or a fragment (a node forest — "insert here", what PopCorn boards always were). Both carry the same > metadata, so the gallery, storage, sync and the marketplace never special-case a kind. ```ts export type TemplateKind = DocumentKind export type TemplateSource = 'builtin' | 'user' | 'workspace' | 'marketplace' /** What a kind implies for the editor, so UI, export and creation agree (see `kinds.ts`). */ export interface KindProfile { pageModel: 'fixed' | 'canvas' mode: ModeId defaultExport: 'pdf-print' | 'png' | 'prototype' | 'html-email' | 'none' /** What Play does: present on the canvas (the default), or open the page as a live website beside it. */ play?: 'present' | 'preview' } /** One size of a template: the roots laid out for that page size (a deck's slides, a set's post). `sizeId` is a page preset id, or 'free'. */ export interface TemplateVariant { sizeId: string rootIds: string[] width: number height: number thumbnail?: string } /** A place a brand kit can restyle: bound to a document variable the template's nodes reference. */ export interface BrandSlot { id: string kind: 'color' | 'font' | 'logo' | 'text' label: string variableId: string } /** Facets (animated, duration, fps, platforms, formats, use cases) are described in `vocabulary.ts`. */ export interface TemplateMeta extends TemplateFacets { id: string name: string description?: string kind: TemplateKind category: string tags: string[] locale?: string author?: { name: string; url?: string } license?: 'cc0' | 'popcraft' | 'proprietary' /** The card's still: a data URL (lib/templates/thumbnail.ts). */ thumbnail?: string /** An animated template's hover loop, where the app serves it (a cloud row's; lib/templates/thumbnail.ts). */ preview?: TemplatePreviewMedia /** Google / system family names to load before the template is shown. */ fonts: string[] brandSlots: BrandSlot[] source: TemplateSource /** Ids this template was published under before, still honoured when a file or seed names one. */ aliases?: string[] schema: 1 createdAt: number updatedAt: number } export interface TemplatePayload { mode: 'document' | 'fragment' /** Ids are local to the template; every use remaps them. Roots have `parentId: null`, positioned from the origin. */ nodes: Record<string, AnyNode> variants: TemplateVariant[] styles?: Record<string, Style> variables?: Record<string, Variable> variableCollections?: Record<string, VariableCollection> /** The file's content collections and their records (lib/document/collections.ts): a blog's posts, an app's sample data. */ collections?: Record<string, import('../document/types').Collection> records?: Record<string, import('../document/types').CollectionRecord> libraries?: Record<string, LibraryUse> /** Content hashes (media://) the nodes reference, so a use can fetch what is missing first. */ media: string[] /** Document mode: the canvas pages in order with their roots (every root also belongs to exactly one variant). */ pages?: { id: string; name: string; rootIds: string[]; guides?: import('../document/types').Guide[] }[] /** Document-level presentation the sheets were laid out for (a comic issue reads as spreads). */ facingPages?: boolean /** How the file presents (DocumentSettings `showAdvance` / `showLoop`): a motion piece moves on and loops by itself. */ show?: { advance?: import('../document/types').ShowAdvance; loop?: boolean } /** The node that stands for the file in the browser — a comic's front cover, a deck's title slide. */ coverNodeId?: string /** Scene sequences over the template's frames (lib/motion/composition.ts), remapped with them on every use. */ compositions?: Record<string, import('../document/types').Composition> /** The font files the template's texts are set in (`Document.fonts`); their hashes are in `media` too. */ fonts?: import('../document/types').DocumentFont[] } export interface Template { meta: TemplateMeta payload: TemplatePayload } ``` Official templates ship as `seed/templates/<id>.json.gz` (a gzipped `Template`) with `seed/templates/index.json` listing id, name, kind, category and size. ### The library format From `lib/document/libraries.ts`: > Team libraries: publish styles, variables and components from one file; use them in others. > > Importing a library copies its items into the subscribing document under namespaced ids > (`lib:<libraryId>:<id>`): styles and variables sit beside local ones, and component subtrees live under a > hidden library page that isn't in `document.pages`. Every existing resolver (fills and text styles, bound > variables, instances, export, collaboration) therefore works unchanged, and imported items keep working > offline. `document.libraries` records which version of each library is in the file. ```ts export interface LibraryPayload { styles: Record<string, Style> variables: Record<string, Variable> variableCollections: Record<string, VariableCollection> /** Published component roots (COMPONENT or COMPONENT_SET) and every node in their subtrees. */ components: { rootIds: string[]; nodes: Record<string, AnyNode> } } export interface LibraryUse { name: string version: number fileKey?: string importedAt: number } export type LibraryItemKind = 'component' | 'style' | 'variable' export interface LibraryItemChange { kind: LibraryItemKind; id: string; name: string; change: 'added' | 'changed' | 'removed' } ``` Official libraries ship as `seed/libraries/<id>.json.gz`, each an `OfficialLibrary`: ```ts /** What seed/libraries holds for one official library. */ export interface OfficialLibrary { meta: { id: string; name: string; description: string; tags: string[]; category: string | null; version: number aliases?: string[]; counts: { components: number; styles: number; variables: number; motion: number } createdAt: number; updatedAt: number } payload: LibraryPayload motion: LibraryMotion /** The showcase, made of the library's instances: the listing's preview and docs page, and what "Open docs" opens. */ docs: Template } ``` ### The plugin manifest A plugin is a folder (or zip) with `manifest.json` at its root, a main script that runs in a sandbox and talks to the document through the `popcraft` API, and optionally a UI page. Data-only plugins (theme packs, importers declared in `contributes`) need no main script. Guide: https://popcraft.app/docs/plugins/manifest ```ts /** What a plugin may do. `notify`, `showUI` and per-plugin storage are always allowed. */ export type PluginPermission = | 'read' // read nodes, selection, pages, styles, variables; receive change events | 'write' // create / update / delete nodes, set selection, create styles, set variable values | 'export' // render nodes to PNG/JPG/WEBP/SVG/PDF bytes | 'network' // fetch() from the plugin sandbox (otherwise blocked by CSP — offline by default) | 'comments' export interface PluginCommand { /** Passed to the plugin as `popcraft.command`. */ id: string label: string } export interface PluginManifest { id: string name: string version: string description?: string author?: string /** Path of the main script inside the package. Optional for data-only plugins (e.g. theme packs). */ main?: string /** Optional path of the UI HTML inside the package. */ ui?: string permissions: PluginPermission[] /** Menu commands. Defaults to a single "Run" command. */ commands?: PluginCommand[] /** Static contributions that need no code: they load without running the plugin sandbox. */ contributes?: PluginContributions /** Marketplace listing (lib/marketplace/listing.ts): all optional, editable later by the publisher. */ icon?: string screenshots?: string[] category?: string tags?: string[] homepage?: string } export interface PluginThemeContribution { /** Theme id; must start with the plugin id (`my.plugin.dark`). */ id: string label: string /** Path of the theme JSON inside the package. */ path: string } /** * A sidebar panel: a tab on the left or right rail that shows the plugin's UI docked in the panel. Opening the tab * runs the plugin with `command` (default: the panel id), and its UI (manifest.ui) appears in the panel instead of a * floating window. */ export interface PluginPanelContribution { /** Unique within the plugin: letters, digits, ".", "_" and "-". */ id: string /** The tab's tooltip and the panel's header. */ title: string /** A small square image as a data: URL (a zip package may name an .svg/.png/.webp file in it instead). Defaults to the plugin's icon. */ icon?: string /** The side the tab starts on; people can drag it to the other. Default right. */ side?: 'left' | 'right' /** The command the plugin is started with when the panel opens. */ command?: string } /** * A File → Import / Home Import / drop-zone format this plugin handles. * The host merges `accept` into pickers; picking a matching file runs `command` (default: first command). */ export interface PluginImporterContribution { /** Unique within the plugin. */ id: string /** Shown in menus (“Publisher (.pub)”). */ label: string /** Extensions (`.pub`) and/or MIME types the host should accept. */ accept: string[] /** Command id passed to the plugin when this importer runs. Defaults to the first manifest command. */ command?: string } /** * A File → Export format this plugin writes. Picking it runs `command` (default: first command) with an export * request (`popcraft.getExportRequest()`): the frames to export and the file name. The plugin renders them with * `exportNode` (the host's own exporters) and hands the bytes back with `saveExport`; the host saves the file. */ export interface PluginExporterContribution { /** Unique within the plugin. */ id: string /** Shown in File → Export (“Adobe Illustrator (.ai)”). */ label: string /** The saved file's extension, with its dot (`.ai`). The host enforces it on the saved name. */ extension: string /** Command id passed to the plugin when this exporter runs. Defaults to the first manifest command. */ command?: string } export interface PluginContributions { themes?: PluginThemeContribution[] panels?: PluginPanelContribution[] importers?: PluginImporterContribution[] exporters?: PluginExporterContribution[] } /** An installed plugin as persisted in IndexedDB. */ export interface InstalledPlugin { manifest: PluginManifest /** Empty for data-only plugins (no `main`). */ mainCode: string uiHtml?: string /** Parsed, validated themes from `contributes.themes`. */ themes?: ThemeManifest[] enabled: boolean /** Permissions the user approved (a subset of manifest.permissions); null until first run. */ granted: PluginPermission[] | null installedAt: number /** * Installed from the marketplace: the version, whether you installed it yourself, and the workspaces that install it for you. * Local: loaded by the desktop app from a package in its assets folders (path relative to them, file mtime). */ source?: | { kind: 'marketplace'; version: string; personal: boolean; workspaceIds: string[] } | { kind: 'local'; path: string; modifiedMs: number } } ``` ### Export formats Per-layer export settings (the Design panel's Export section) are `ExportSetting`s on the node (`exportSettings`): ```ts export interface ExportSetting { suffix: string // GIF and SHEET (sprite sheet PNG + Aseprite JSON) are pixel-canvas formats format: 'PNG' | 'JPG' | 'WEBP' | 'SVG' | 'PDF' | 'GIF' | 'SHEET' scale: number sheetLayout?: 'ROWS' | 'COLUMNS' | 'PACKED' gifTag?: string // animate only this frame tag (empty: all frames) gifRange?: { from: number; to: number } // or only these frames (0-based, inclusive); wins over gifTag constraint?: { type: 'SCALE' | 'WIDTH' | 'HEIGHT'; value: number } /** Raster formats: print resolution — renders at dpi/96 and tags the file (JFIF density, PNG pHYs). Wins over `scale`. */ dpi?: number /** PDF only: print options (see `lib/io/pdf`). Absent: a screen PDF at the export scale. */ pdf?: { dpi?: number; bleed?: boolean; marks?: boolean; colorBars?: boolean; colorMode?: 'RGB' | 'CMYK'; textAs?: 'FONT' | 'OUTLINE'; spreads?: boolean; ats?: boolean } } ``` Every export the app can run, from the command registry (the command palette, the agent and plugins all go through these): - `export.board` — Export board (agent tool `export_board`): Export a whiteboard: the whole board (or the selection) as one PNG, or its stickies as Markdown or CSV with a column per section or group (kanban columns, retro lanes). - `format` (`PNG` | `TEXT` | `MARKDOWN` | `CSV`): PNG: one picture. TEXT: the words — a line per column title, a line per card. MARKDOWN: a heading per column, a bullet per card. CSV: the columns side by side, like a kanban in a spreadsheet. Columns are the boxes the cards sit in (frames, sections, groups or shapes), titled by the text at their top. - `copy`: TEXT / MARKDOWN / CSV: put it on the clipboard (the person's action only). - `scope` (`auto` | `board` | `selection`): What to export: the selection, the whole board, or auto (the selection when there is one). - `scale`: PNG pixel scale (lowered automatically to keep a huge board under 8192px). - `download`: Save the file to the user's computer. Only when the user asked for a file. - `export.copy` — Copy as image - `id`: Layer to copy. Defaults to the first selected one. - `format` (`PNG` | `SVG`): Default PNG. - `scale`: PNG only; default 2. - `time`: Moment (ms) of the frame's timeline to export animated layers at. Defaults to the playhead when the frame is open in Animate, else the layers at rest. - `export.gif` — Export animation as GIF (agent tool `export_gif`): Render a frame's (or motion component's) timeline, or a composition (its scenes, transitions and — for video — its sound) to an animated GIF (no sound): frame rate (50 at most), size, range, a shared or per-frame palette, dithering and looping. - `id`: The frame or motion component whose timeline to export; defaults to the selected frame. - `composition`: A composition (scene sequence) to export instead of a frame: its scenes and transitions. - `fps`: Frames per second: 24, 25, 30, 50, 60, 120 (default: the timeline's own rate; a GIF 30 at most by default, 50 at most). - `resolution` (`720p` | `1080p` | `4k` | `source`): Output size: a preset sets the short edge (720 / 1080 / 2160 px); source (default) is the frame's or stage's size times `scale`. - `scale`: Pixel scale of the source size (default 1). - `range` (`work` | `all`): The work area (default; the whole timeline when there is none) or all of it. - `filename`: File name (the extension is added). - `palette` (`global` | `per-frame`): One palette for every frame (default: no flicker) or one per frame. - `dither` (`none` | `ordered`): Ordered (Bayer) dithering against banding (default none). - `loop`: Loop for ever (default) or play once. - `watermark`: The PopCraft watermark in the corner (default on). Only Creator and Studio can turn it off: the Free plan always carries it. - `export.lottie` — Export animation as Lottie (agent tool `export_lottie`): Write a frame's (or motion component's) timeline, or a composition (its scenes, transitions) as a Lottie (bodymovin JSON, or a .lottie zip) the user downloads: vector shapes, fills, strokes, masks, text as outlines and images as embedded assets, with our keys and bezier / hold easing where Lottie can say them and every other curve, behaviour and text animator keyed frame by frame. - `id`: The frame or motion component whose timeline to export; defaults to the selected frame. - `composition`: A composition (scene sequence) to export instead of a frame: its scenes and transitions. - `fps`: Frames per second: 24, 25, 30, 50, 60, 120 (default: the timeline's own rate; a GIF 30 at most by default, 50 at most). - `range` (`work` | `all`): The work area (default; the whole timeline when there is none) or all of it. - `filename`: File name (the extension is added). - `container` (`json` | `lottie`): Bodymovin JSON (`.json`, default) or dotLottie (`.lottie`, a dotLottie 2.0 zip). - `stateMachine`: A `.lottie` carries the frame's state machine (its states as marker segments, inputs, guards and listeners) when it has one. Default true. - `export.mp4` — Export animation as MP4 (agent tool `export_mp4`): Render a frame's (or motion component's) timeline, or a composition (its scenes, transitions and — for video — its sound) to an MP4 (H.264, AAC sound) the user downloads: frame rate, resolution preset or scale, work area or whole range, quality or an exact bitrate, sound on or off. - `id`: The frame or motion component whose timeline to export; defaults to the selected frame. - `composition`: A composition (scene sequence) to export instead of a frame: its scenes and transitions. - `fps`: Frames per second: 24, 25, 30, 50, 60, 120 (default: the timeline's own rate; a GIF 30 at most by default, 50 at most). - `resolution` (`720p` | `1080p` | `4k` | `source`): Output size: a preset sets the short edge (720 / 1080 / 2160 px); source (default) is the frame's or stage's size times `scale`. - `scale`: Pixel scale of the source size (default 1). - `range` (`work` | `all`): The work area (default; the whole timeline when there is none) or all of it. - `filename`: File name (the extension is added). - `quality` (`low` | `medium` | `high` | `very-high`): Encoding quality (default high); sets the bitrate from the size and rate. - `bitrate`: Bits per second, instead of `quality`. - `audio`: The sound in the video: a composition's audio and its scenes', or a frame's audio and its videos' own sound (default: yes when any is heard). MP4 carries AAC, WebM Opus; a browser that cannot encode either fails the export rather than dropping the sound. - `watermark`: The PopCraft watermark in the corner (default on). Only Creator and Studio can turn it off: the Free plan always carries it. - `export.nodes` — Export layers (agent tool `export_nodes`): Render layers to PNG, JPG, WEBP, SVG or PDF at a scale factor (or, with no format, each layer with the export settings saved on it), and report the size of each file. - `ids`: Layers to export. Defaults to the selection. - `format` (`PNG` | `JPG` | `WEBP` | `SVG` | `PDF`): Export every layer in this format. Leave out to export each layer with its own export settings (a layer that has none: one PNG at 1x). - `scale`: With format: pixel scale factor (default 1; 2 for @2x). - `suffix`: With format: added to each file name ("@2x"). - `download`: Save the files to the user's downloads folder. Off by default — never turn it on unless the user asked for a file. - `singlePdf`: For PDF only: put every layer in one document, one page each (default true). - `time`: Moment (ms) of the frame's timeline to export animated layers at. Defaults to the playhead when the frame is open in Animate, else the layers at rest. - `export.pages` — Export pages as images (agent tool `export_pages`): Download every page of a fixed-page document (print pages, else the top-level frames, or the given ids) as one zip of PNG, JPG or WEBP images at a print resolution (dpi; print documents default to 300), each tagged with that resolution. - `ids`: Defaults to the selection, then to the page's print pages / top-level frames. - `format` (`PNG` | `JPG` | `WEBP`): Default PNG. - `dpi`: Resolution; 96 is 1:1 with the canvas. Print pages default to 300. - `time`: Moment (ms) of the frame's timeline to export animated layers at. Defaults to the playhead when the frame is open in Animate, else the layers at rest. - `export.pdf` — Export as PDF - `ids`: Defaults to the selection, then to the page's top-level frames. - `scale`: Raster scale for a screen PDF (dpi = 96 × scale). Ignored when dpi is given. - `dpi`: Resolution for GPU-rendered parts. Print pages default to 300. - `bleed`: Extend print pages by their bleed and set a BleedBox (default: on for print pages). - `marks`: Crop marks outside the bleed. - `colorBars`: Registration targets and color bars (with marks). - `colorMode` (`RGB` | `CMYK`): CMYK is an uncalibrated conversion. - `textAs` (`FONT` | `OUTLINE`): Embedded fonts (default) or outlines. - `spreads`: Facing pages on one sheet (2–3, 4–5 …). Same as layout: spreads. - `splitSpreads`: Cut an oversized sheet (a centrefold) into single pages so the file has ONE page size — what almost every printer requires. Off for the printers that take a spread as one image and cut it themselves. Default true. - `layout` (`pages` | `spreads` | `booklet` | `nup2` | `nup4`): How pages sit on each printed sheet: one each, reader spreads, a saddle-stitch booklet to fold and staple, or two/four to a sheet. - `ats`: ATS-friendly: all text embedded, reading order, real spaces; gradient text becomes solid. - `time`: Moment (ms) of the frame's timeline to export animated layers at. Defaults to the playhead when the frame is open in Animate, else the layers at rest. - `export.pngSequence` — Export animation as PNG sequence (agent tool `export_png_sequence`): Render a frame's (or motion component's) timeline, or a composition (its scenes, transitions and — for video — its sound) to a zip of numbered PNG frames (no sound): frame rate, size and range. - `id`: The frame or motion component whose timeline to export; defaults to the selected frame. - `composition`: A composition (scene sequence) to export instead of a frame: its scenes and transitions. - `fps`: Frames per second: 24, 25, 30, 50, 60, 120 (default: the timeline's own rate; a GIF 30 at most by default, 50 at most). - `resolution` (`720p` | `1080p` | `4k` | `source`): Output size: a preset sets the short edge (720 / 1080 / 2160 px); source (default) is the frame's or stage's size times `scale`. - `scale`: Pixel scale of the source size (default 1). - `range` (`work` | `all`): The work area (default; the whole timeline when there is none) or all of it. - `filename`: File name (the extension is added). - `export.pptx` — Export as PowerPoint (agent tool `export_pptx`): Download the presentation as a PowerPoint (.pptx) file: its slides (skipped ones left out unless includeSkipped), or the given frames, with speaker notes. - `ids`: Defaults to the presentation's slides (skipped ones left out), then to the page's top-level frames. - `includeSkipped`: Also export slides marked skip. - `export.storefront` — Export storefront (agent tool `export_storefront`): Download a storefront design (a page whose lists and pages read collections with a commerce schema) as a store's project, in a zip. - `target` (`next-commerce-store` | `next-commerce`): next-commerce-store: a whole Next.js Commerce store drawn by the design, to deploy with a Shopify store's domain and Storefront token (publish_store deploys it for you). next-commerce: React components a Next.js Commerce store renders with its own data and server actions. A Shopify theme is the Ecommerce plugin's. - `styling` (`inline` | `tailwind`): next-commerce: style objects (default) or Tailwind CSS v4 classes. - `dir`: next-commerce: where the components go in the store's project (default components/popcraft). - `export.svg` — Export SVG (agent tool `export_svg`): Export one layer and its children as SVG markup. - `id`: Node to export. - `maxChars`: Truncate the markup at this many characters (default 20000). - `time`: Moment (ms) of the frame's timeline to export animated layers at. Defaults to the playhead when the frame is open in Animate, else the layers at rest. - `export.web` — Export as web code (agent tool `export_web`): Download web code. - `id`: The layer to export; defaults to the selection, then the page's first frame. - `format` (`html` | `html-inline` | `email` | `react` | `react-tailwind` | `site` | `next` | `next-tailwind`): Markup flavour for one layer (default: html), or the whole page as a website: site (static files any host serves: a page of HTML per screen plus the runtime that plays it), next-tailwind (the Next.js project with Tailwind CSS v4 classes in place of style objects) or next (a Next.js project: a page of TSX per screen, the code components as source). - `images` (`data` | `files`): Embed pictures as data URLs (default) or write them beside the markup in a zip. - `siteUrl`: For a static site (format site): the address it will be served from (https://example.com). Its share tags (og:image, og:url, twitter:image) are then absolute; without it they are written from the site's root. - `export.webm` — Export animation as WebM (agent tool `export_webm`): Render a frame's (or motion component's) timeline, or a composition (its scenes, transitions and — for video — its sound) to a WebM (VP9/AV1, Opus sound) the user downloads, with the same options as export_mp4. - `id`: The frame or motion component whose timeline to export; defaults to the selected frame. - `composition`: A composition (scene sequence) to export instead of a frame: its scenes and transitions. - `fps`: Frames per second: 24, 25, 30, 50, 60, 120 (default: the timeline's own rate; a GIF 30 at most by default, 50 at most). - `resolution` (`720p` | `1080p` | `4k` | `source`): Output size: a preset sets the short edge (720 / 1080 / 2160 px); source (default) is the frame's or stage's size times `scale`. - `scale`: Pixel scale of the source size (default 1). - `range` (`work` | `all`): The work area (default; the whole timeline when there is none) or all of it. - `filename`: File name (the extension is added). - `quality` (`low` | `medium` | `high` | `very-high`): Encoding quality (default high); sets the bitrate from the size and rate. - `bitrate`: Bits per second, instead of `quality`. - `audio`: The sound in the video: a composition's audio and its scenes', or a frame's audio and its videos' own sound (default: yes when any is heard). MP4 carries AAC, WebM Opus; a browser that cannot encode either fails the export rather than dropping the sound. - `watermark`: The PopCraft watermark in the corner (default on). Only Creator and Studio can turn it off: the Free plan always carries it. Video exports (MP4, WebM, GIF, Lottie/dotLottie, PNG sequence) render a frame's timeline or a composition over its work area; web exports (`export.web`) serialise one element tree into each markup flavour above; HTML and React keep the frame's motion and a prototype's screens and states, inline HTML and email stay at their first state. The PopCraft file itself (`.popcraft`, above) is the lossless format. <!-- Generated by scripts/gen-llms.ts from content/docs and the source. Do not edit by hand. -->