File Format (.purl and .purlx)

Purl uses two file formats:

Both formats use the same CellObject schema for objects. This document specifies the schemas so valid files can be generated programmatically.

For scripting syntax, see the Scripting doc. For dynamics behavior, see the Dynamics doc. For object semantics, see the Objects doc.


.purlx Format

A .purlx file is a thin wrapper around an array of objects. The objects use the exact same CellObject format as .purl files (documented below).

Structure

{
  "version": "1.0",
  "type": "purlx",
  "metadata": {
    "name": "Component Name",
    "description": "Optional description",
    "createdAt": 1700000000000,
    "tags": ["optional", "tags"]
  },
  "objects": [
    // Same CellObject format as cell.interior.objects in .purl
    // See "Objects (CellObject)" section below
  ]
}

.purlx Root

Field Type Required Description
version "1.0" yes Format version
type "purlx" yes Must be "purlx"
metadata.name string yes Display name
metadata.description string no Description
metadata.author string no Author name
metadata.createdAt number yes Unix timestamp in milliseconds
metadata.tags string[] no Searchable tags
objects CellObject[] yes Array of objects — same format as .purl cell objects

Import Behavior

When a .purlx is imported:

  1. All objects get new unique IDs (prevents collisions)
  2. Objects are positioned at the import location
  3. If names conflict with existing objects, imported objects get numeric suffixes (e.g., Shape1Shape12)
  4. Scripts referencing other objects in the same .purlx work correctly (they're imported together)

.purl Format

IDs

All id fields use the format {timestamp}-{random7} where timestamp is Date.now() and random7 is 7 random alphanumeric characters (base-36). Example: "1768686621803-sazcegg". Every id in a file must be unique.

Minimal Valid File

{
  "id": "1700000000000-abc1234",
  "title": "My Project",
  "author": "",
  "version": "1.0.0",
  "createdAt": 1700000000000,
  "updatedAt": 1700000000000,
  "cells": [
    {
      "id": "1700000000001-def5678",
      "label": "Start",
      "position": { "col": 2, "row": 2 },
      "rotation": 0,
      "size": { "width": 1, "height": 1 },
      "visuals": {
        "background": {
          "fillLayers": [{ "id": "bg1", "type": "color", "color": "#4b5563", "opacity": 1 }],
          "pattern": "none",
          "patternColor": "rgba(0,0,0,0.2)",
          "patternScale": 1
        }
      },
      "interior": { "objects": [] },
      "initialCamera": { "x": 0, "y": 0, "zoom": 1 }
    }
  ],
  "markers": [
    { "type": "start", "cellId": "1700000000001-def5678", "showInPlayMode": true }
  ]
}

Project (root)

Field Type Required Description
id string yes Unique project ID
title string yes Project name
author string yes Author name (can be "")
version string yes Semver, use "1.0.0"
createdAt number yes Unix timestamp in milliseconds
updatedAt number yes Unix timestamp in milliseconds
cells Cell[] yes Array of cells (scenes/screens)
markers Marker[] yes Navigation markers
settings GameSettings no Game/export settings

Marker

Field Type Required Description
type "start" or "finish" yes Marker type
cellId string yes ID of the marked cell
showInPlayMode boolean yes Show marker indicator in play

Every project needs at least one "start" marker pointing to a valid cell.

GameSettings

All fields optional. Defaults shown.

Field Type Default Description
letterboxColor string "#000000" Letterbox bar color
scaleMode "fit" or "fixed" "fit" How canvas scales to window
fullscreenMode "button", "auto", "disabled" "button" Fullscreen behavior
cameraDeadZone number 0.4 0-1, camera follow dead zone size
cameraAspectRatio number 1.7778 Camera width/height ratio (16:9)

Cell

Field Type Required Default Description
id string yes Unique cell ID
label string yes Display name
position { col, row } yes Grid position on map (integers, can be negative)
rotation number yes 0 Cell rotation in degrees
size { width, height } yes { "width": 1, "height": 1 } Cell dimensions in screen units (1 = one viewport)
visuals CellVisuals yes Background styling
interior CellInterior yes Objects and scripts
initialCamera { x, y, zoom } yes { "x": 0, "y": 0, "zoom": 1 } Initial camera in play mode
tags string[] no Custom tags for scripting
gravity number no -10 to 10, vertical acceleration
wind number no -10 to 10, wind magnitude
windAngle number no 0 Wind direction in degrees (0=right, 90=down)
perspectiveX number no 1 Parallax X factor (0=fixed, 1=scrolls with camera)
perspectiveY number no 1 Parallax Y factor

CellVisuals

{
  "background": {
    "fillLayers": [{ "id": "bg1", "type": "color", "color": "#4b5563", "opacity": 1 }],
    "pattern": "none",
    "patternColor": "rgba(0,0,0,0.2)",
    "patternScale": 1
  }
}
Field Type Required Description
fillLayers FillLayer[] yes Background fill layers (see Fill Layers section)
pattern string yes Overlay pattern: "none", "grid", "dots", etc.
patternColor string yes Pattern color with alpha
patternScale number yes Pattern scale (0.5-2)

CellInterior

{
  "objects": [],
  "scripts": { "scripts": [{ "id": "...", "name": "Main", "code": "...", "enabled": true }] },
  "layers": []
}

All fields except objects are optional.

Coordinate System

All object positions and sizes use unified coordinates where 1.0 = one cell height unit:

Polygon vertices, line endpoints, and path segment coordinates are 0-1 relative to the object's own bounds.

Objects (CellObject)

A CellObject is either a Prime or a Component. Primes have a primeType field. Components have a children field.

Prime

Required fields:

Field Type Description
id string Unique object ID
name string Script-addressable name (e.g., "Shape1", "Player")
primeType string "text", "button", "shape", "line", "grid"
x number X position in cell coords
y number Y position in cell coords
visible boolean Runtime visibility (usually true)

Common optional fields:

Field Type Default Description
libraryId string Source library template ID
width number Object width (not used by lines)
height number Object height (not used by lines)
zIndex number Render order (higher = on top, starts at 1)
buildVisible boolean true Editor-only visibility
pointerEvents string "auto" "auto" or "none"
opacity number 1 0-1
rotation number 0 Degrees
flipX boolean false Horizontal flip
flipY boolean false Vertical flip
pivot { x, y } { "x": 0.5, "y": 0.5 } Rotation center, 0-1 normalized
tags string[] Script-addressable tags
layerId string yes Layer this object belongs to
scripts ObjectScripts Event scripts

Shape Properties (primeType: "shape")

Field Type Default Description
shapeType string "polygon" See shape types below
fillLayers FillLayer[] Multi-layer fill (see Fill Layers below)
strokeWidth number 0 Border width
strokeColor string "#000000" Border color
strokeStyle string "solid" "solid", "dashed", "dotted"
cornerRadius number 0 Corner rounding
cornerRadiusUnit string "px" "px" or "%"
polygonPoints array Vertex array for polygon shapes
pathSegments array Bezier path for path shapes
content string Optional text displayed inside the shape
textColor string "#ffffff" Color of content text
textAlign string "center" Alignment: "left", "center", "right"

Shape types: "polygon", "circle", "ellipse", "path"

Polygon shapes require polygonPoints — an array of { "x": 0-1, "y": 0-1 } vertices relative to object bounds. A rectangle is 4 points:

"polygonPoints": [
  { "x": 0, "y": 0 }, { "x": 1, "y": 0 },
  { "x": 1, "y": 1 }, { "x": 0, "y": 1 }
]

Path shapes require pathSegments — an array of bezier curve commands (M, L, C, Q, Z).

Common libraryId values for shapes:

Shadow Properties

Field Type Default
shadowX number 0
shadowY number 0
shadowBlur number 0
shadowSpread number 0
shadowColor string "rgba(0,0,0,0.3)"

Text Properties (primeType: "text" or "button")

How text sizing works:

Typical single-line sizes:

Field Type Default Description
content string "" Text content (newlines ignored)
textColor string "#ffffff" Text color
fontSize number 0.05 Reserved for future use
fontFamily string "inherit" CSS font family
fontWeight string "normal" "normal" or "bold"
fontStyle string "normal" "normal" or "italic"
textDecoration string "none" "none" or "underline"
textAlign string "center" "left", "center", "right"
label string Button label (primeType: "button")

Line Properties (primeType: "line")

Lines have no width/height. They use endpoint coordinates in cell space.

Field Type Default Description
lineX1 number Start X in cell coords
lineY1 number Start Y
lineX2 number End X
lineY2 number End Y
strokeWidth number 2 Line thickness
strokeColor string "#ffffff" Line color
strokeStyle string "solid" "solid", "dashed", "dotted"
lineCap string "round" "butt", "round", "square"
lineStartMarker string "none" "none", "arrow", "circle", "square", "diamond"
lineEndMarker string "none" Same as above

Grid Properties (primeType: "grid")

Field Type Default Description
columns number 8 Number of columns
rows number 8 Number of rows
gridColor string "#888888" Grid line color
gridData array 2D array `(string

Dynamics Properties

These make objects interactive in play mode. See Dynamics doc for behavior details.

Field Type Description
movable MovableConfig Enables physics movement
jumpable JumpableConfig Enables jumping
input string What controls movement (see InputType)
subject boolean Camera follows this object + viewport bounded
inputBinding InputBinding Key bindings
blocking boolean or string Blocks other objects (true or expression)
mass number Object mass (default: width * height)
friction number Surface friction 0-1 (0=ice, 1=sticky)
restitution number Bounciness 0-1 (0=dead stop, 1=perfect bounce)
oneWay boolean One-way platform (blocks from above only)
sensor boolean Overlap detection without blocking (fires onOverlap/onOverlapEnd)
snapToGrid string Grid object ID for grid-based movement
perspectiveX number Per-object parallax X (0-1)
perspectiveY number Per-object parallax Y (0-1)

Presets

Presets store named snapshots of children's visual properties. Used with set Object "presetName" in scripts.

"presets": [
  {
    "name": "default",
    "values": {
      "ChildName": { "fillLayers": [{ "id": "c1", "type": "color", "color": "#ff0000", "opacity": 1 }] }
    },
    "includesPosition": false,
    "includesTransform": false
  }
],
"state": "default"

Component

A component is an invisible container. Its children move, scale, and rotate together. Required fields:

Field Type Description
id string Unique object ID
name string Script-addressable name
x number X position in cell coords
y number Y position in cell coords
visible boolean Runtime visibility
children CellObject[] Array of child Primes or Components

Optional fields (computed automatically on load if omitted):

Field Type Description
width number Container width (computed from children bounding box)
height number Container height (computed from children bounding box)

Components support the same optional fields as Primes: zIndex, rotation, pivot, scripts, tags, layerId, movable, jumpable, input, subject, blocking, sensor, mass, friction, restitution, oneWay, snapToGrid, perspectiveX, perspectiveY, presets, state, buildVisible, pointerEvents, inputBinding, libraryId.

Additional component field:

Field Type Description
exposedProperties array { childName, propertyName, displayName? }[] — properties surfaced to parent

Sub-Types

FillLayer

Multi-layer fill. Each layer has a type and composites over previous layers.

Common fields on all layers:

Field Type Required
id string yes
type string yes
opacity number yes (0-1)
blendMode string no

Color layer:

{ "id": "...", "type": "color", "opacity": 1, "color": "#6366f1" }

Image layer:

{ "id": "...", "type": "image", "opacity": 1, "imageData": "data:image/png;base64,...", "mode": "fill" }

Mode: "fill", "fit", "stretch", "tile", "center". Optional: scale, offsetX, offsetY.

Gradient layer:

{ "id": "...", "type": "gradient", "opacity": 1,
  "gradientType": "linear", "angle": 180,
  "stops": [{ "color": "#ff0000", "position": 0 }, { "color": "#0000ff", "position": 1 }] }

Gradient types: "linear", "radial", "conic", "diamond".

Pattern layer:

{ "id": "...", "type": "pattern", "opacity": 1,
  "pattern": "dots", "color": "#000000", "scale": 1 }

Patterns: "dots", "grid", "lines", "diagonal", "cross". Optional: weight.

Noise layer:

{ "id": "...", "type": "noise", "opacity": 1,
  "noiseType": "fractalNoise", "scale": 1, "intensity": 0.5, "monochrome": false }

Noise types: "fractalNoise", "turbulence".

PathSegment

Bezier path segments for shapeType: "path". All coordinates 0-1 relative to object bounds.

"pathSegments": [
  { "type": "M", "x": 0, "y": 0.5 },
  { "type": "C", "cp1x": 0.25, "cp1y": 0, "cp2x": 0.75, "cp2y": 1, "x": 1, "y": 0.5 },
  { "type": "Z" }
]
Type Fields Description
"M" x, y Move to
"L" x, y Line to
"C" cp1x, cp1y, cp2x, cp2y, x, y Cubic bezier
"Q" cpx, cpy, x, y Quadratic bezier
"Z" Close path

MovableConfig

{ "speed": 0.3, "acceleration": 5, "deceleration": 5 }
Field Type Required Description
speed number yes Max velocity (units/sec)
acceleration number no How quickly reaches max speed
deceleration number no Friction/drag when no input
moveStyle string no Animation style: "teleport", "slide", "fade", "jump"

JumpableConfig

{ "height": 0.5, "keys": ["Space", " "], "multiJump": 1 }
Field Type Default Description
height number 0.5 Upward impulse velocity
keys string[] ["Space", " "] Trigger keys
multiJump number 1 Max jumps before landing (2 = double jump)

InputType

"keyboard" | "click"

InputBinding

{ "player": 1 }
Field Type Description
player 1 or 2 Player 1: WASD + arrows. Player 2: arrows only
keys object Custom: { "up": ["w"], "down": ["s"], "left": ["a"], "right": ["d"] }

Scripts

{
  "scripts": {
    "scripts": [
      {
        "id": "1700000000010-scr1234",
        "name": "Main",
        "code": "onClick:\n  go \"Level2\"",
        "enabled": true
      }
    ]
  }
}

The code field contains Purl DSL source. See the Scripting doc for syntax. Scripts can be on cells (in interior.scripts) or objects (in object scripts field).

Layer

Editor-only organization. Does not affect play mode or scripts. Every cell must have at least one layer.

{ "id": "...", "name": "Background", "zIndex": 1, "buildVisible": true, "locked": false }
Field Type Required Description
id string yes Unique layer ID
name string yes Display name
layerId string no Parent layer ID (for nesting)
zIndex number no Visual ordering (higher = on top)
buildVisible boolean no Editor-only visibility (default true)
locked boolean no If true, objects in layer cannot be selected or edited

Objects reference their layer via layerId. Layers can nest via layerId on the layer itself.

Example: Shape with Fill and Stroke

{
  "id": "1700000000002-shp1234",
  "name": "Platform1",
  "primeType": "shape",
  "libraryId": "prime:rectangle",
  "x": 0.3,
  "y": 0.8,
  "width": 0.4,
  "height": 0.05,
  "visible": true,
  "zIndex": 2,
  "shapeType": "polygon",
  "polygonPoints": [
    { "x": 0, "y": 0 }, { "x": 1, "y": 0 },
    { "x": 1, "y": 1 }, { "x": 0, "y": 1 }
  ],
  "fillLayers": [
    { "id": "fl1", "type": "color", "opacity": 1, "color": "#22c55e" }
  ],
  "strokeWidth": 2,
  "strokeColor": "#000000",
  "blocking": true
}

Example: Movable Player with Script

{
  "id": "1700000000003-plr1234",
  "name": "Player",
  "primeType": "shape",
  "libraryId": "prime:circle",
  "x": 0.5,
  "y": 0.3,
  "width": 0.08,
  "height": 0.08,
  "visible": true,
  "zIndex": 5,
  "shapeType": "ellipse",
  "fillLayers": [
    { "id": "fl2", "type": "color", "opacity": 1, "color": "#3b82f6" }
  ],
  "movable": { "speed": 0.3, "acceleration": 5, "deceleration": 5 },
  "jumpable": { "height": 0.5 },
  "input": "keyboard",
  "subject": true,
  "mass": 1,
  "scripts": {
    "scripts": [
      {
        "id": "1700000000011-sc5678",
        "name": "Main",
        "code": "onEnter:\n  set self blocking true",
        "enabled": true
      }
    ]
  }
}

Example: Component with Children

{
  "id": "1700000000004-cmp1234",
  "name": "Coin",
  "x": 0.7,
  "y": 0.5,
  "width": 0.06,
  "height": 0.06,
  "visible": true,
  "zIndex": 3,
  "children": [
    {
      "id": "1700000000005-bg5678",
      "name": "Background",
      "primeType": "shape",
      "x": 0.7,
      "y": 0.5,
      "width": 0.06,
      "height": 0.06,
      "visible": true,
      "shapeType": "ellipse",
      "fillLayers": [
        { "id": "fl3", "type": "color", "opacity": 1, "color": "#fbbf24" }
      ]
    }
  ],
  "scripts": {
    "scripts": [
      {
        "id": "1700000000012-sc9012",
        "name": "Main",
        "code": "onClick:\n  hide self",
        "enabled": true
      }
    ]
  }
}

Note: component children use global cell coordinates (same coordinate space as the parent), not coordinates relative to the component bounds.

Example: Line

{
  "id": "1700000000006-lin1234",
  "name": "Line1",
  "primeType": "line",
  "libraryId": "prime:line",
  "x": 0,
  "y": 0,
  "visible": true,
  "zIndex": 1,
  "lineX1": 0.2,
  "lineY1": 0.5,
  "lineX2": 0.8,
  "lineY2": 0.5,
  "strokeWidth": 3,
  "strokeColor": "#ffffff",
  "lineEndMarker": "arrow"
}

Example: Side-View Platformer Cell

{
  "id": "1700000000007-cel1234",
  "label": "Level 1",
  "position": { "col": 2, "row": 2 },
  "rotation": 0,
  "size": { "width": 2, "height": 1 },
  "visuals": {
    "background": {
      "fillLayers": [{
        "id": "bg1", "type": "gradient", "opacity": 1,
        "gradientType": "linear", "angle": 180,
        "stops": [
          { "color": "#1e3a5f", "position": 0 },
          { "color": "#0f172a", "position": 1 }
        ]
      }],
      "pattern": "none",
      "patternColor": "rgba(0,0,0,0.2)",
      "patternScale": 1
    }
  },
  "interior": {
    "objects": [
      {
        "id": "1700000000008-flr1234",
        "name": "Floor",
        "primeType": "shape",
        "libraryId": "prime:rectangle",
        "x": 0,
        "y": 0.92,
        "width": 2,
        "height": 0.08,
        "visible": true,
        "zIndex": 1,
        "shapeType": "polygon",
        "polygonPoints": [
          { "x": 0, "y": 0 }, { "x": 1, "y": 0 },
          { "x": 1, "y": 1 }, { "x": 0, "y": 1 }
        ],
        "fillLayers": [
          { "id": "fl4", "type": "color", "opacity": 1, "color": "#374151" }
        ],
        "blocking": true
      },
      {
        "id": "1700000000009-ply1234",
        "name": "Player",
        "primeType": "shape",
        "libraryId": "prime:rectangle",
        "x": 0.2,
        "y": 0.75,
        "width": 0.06,
        "height": 0.1,
        "visible": true,
        "zIndex": 5,
        "shapeType": "polygon",
        "polygonPoints": [
          { "x": 0, "y": 0 }, { "x": 1, "y": 0 },
          { "x": 1, "y": 1 }, { "x": 0, "y": 1 }
        ],
        "fillLayers": [
          { "id": "fl5", "type": "color", "opacity": 1, "color": "#3b82f6" }
        ],
        "movable": { "speed": 0.4, "acceleration": 8, "deceleration": 6 },
        "jumpable": { "height": 0.55 },
        "input": "keyboard",
        "subject": true,
        "blocking": true,
        "mass": 1
      }
    ]
  },
  "gravity": 1.5,
  "initialCamera": { "x": 0, "y": 0, "zoom": 1 }
}