Documentation Index
Fetch the complete documentation index at: https://mintlify.com/pocket-stack/pocketjs/llms.txt
Use this file to discover all available pages before exploring further.
spec/spec.ts is the single source of truth for every constant that crosses a language boundary in PocketJS. It defines the op codes, node types, property IDs, style-table binary format, font atlas format, pak container constants, the PSP button bitmask, and the target screen dimensions. Every host — QuickJS on PSP hardware, the browser WASM module, and headless Bun — speaks this exact wire protocol. The Rust core (core/src/spec.rs) is fully code-generated from spec/spec.ts and must never be edited by hand.
All numeric values on this page are extracted directly from spec/spec.ts. That file is the canonical source. If anything on this page disagrees with the file, the file wins.
Code generation and the drift guard
Running bun spec/gen-rust.ts from the repository root deterministically regenerates core/src/spec.rs from the TypeScript constants. The CI contract test (test/contract.ts) regenerates the file in memory on every run and byte-compares the result against the committed core/src/spec.rs — so TypeScript and Rust can never drift. If you change any value in spec/spec.ts, you must run codegen and commit the updated Rust file:
// spec/spec.ts — change a value, then:
bun spec/gen-rust.ts // regenerates core/src/spec.rs
git add core/src/spec.rs
The generated file opens with a comment making its provenance clear:
//! GENERATED — do not edit; run `bun spec/gen-rust.ts` (from PocketJS/).
//!
//! Source of truth: PocketJS/spec/spec.ts — every constant here mirrors it.
//! test/contract.ts regenerates this file in-memory and byte-compares;
//! if that fails, run `bun spec/gen-rust.ts` and commit the result.
The append-only rule. Numeric codes are never renumbered and never reused. Removing a code from the wire protocol would break any host that has already shipped. To deprecate a code, stop emitting it; the slot remains reserved forever. 0 is reserved as invalid/nop across every table.
Screen dimensions
The logical (and physical PSP) screen is always 480 × 272 pixels. Every host renders at this exact resolution.
export const SCREEN_W = 480;
export const SCREEN_H = 272;
Node types
The type argument passed to createNode must be one of these values:
| Value | Name | Description |
|---|
0 | view | Flex container / box. The workhorse element. |
1 | text | Text run, rendered from a baked font atlas. |
2 | image | Texture quad; can also carry an animated sprite. |
export const NODE_TYPE = {
view: 0,
text: 1,
image: 2,
} as const;
OP codes
Every ui.* method has a numeric code that is its wasm/FFI ABI identity. The TypeScript HostOps interface uses method names; these codes appear on the wire and in core/src/spec.rs.
| Code | Name | Signature |
|---|
1 | createNode | (type: i32) → id |
2 | destroyNode | (id) → void |
3 | insertBefore | (parent, child, anchorOr0) → void |
4 | removeChild | (parent, child) → void |
5 | setStyle | (id, styleId) → void |
6 | setProp | (id, propId: i32, value: f64) → void |
7 | setText | (id, str) → void |
8 | replaceText | (id, str) → void |
9 | uploadTexture | (buf, w, h, psm) → handle |
10 | setImage | (id, texHandle) → void |
11 | animate | (id, propId, to: f64, durMs, easing, delayMs) → animId |
12 | cancelAnim | (animId) → void |
13 | setFocus | (idOr0) → void |
14 | loadStyles | (buf) → void |
15 | loadFontAtlas | (buf) → void |
16 | measureText | (str, fontSlot) → width: f32 |
17 | setSprite | (id, atlas, frames, cols, step) → void |
export const OP = {
createNode: 1,
destroyNode: 2,
insertBefore: 3,
removeChild: 4,
setStyle: 5,
setProp: 6,
setText: 7,
replaceText: 8,
uploadTexture: 9,
setImage: 10,
animate: 11,
cancelAnim: 12,
setFocus: 13,
loadStyles: 14,
loadFontAtlas: 15,
measureText: 16,
setSprite: 17,
} as const;
PROP ids
Property ids are stable u8 values grouped by category with numeric gaps left for future growth. 0 is reserved. Ids are append-only: never renumber a shipped id.
Layout group (1–63) — fed to taffy; changes trigger a relayout.
| ID | Name | Type | Description |
|---|
1 | width | f32 px | Element width. SIZE_FULL (-1) means 100% of parent. |
2 | height | f32 px | Element height. SIZE_FULL (-1) means 100% of parent. |
3 | minW | f32 px | Minimum width. |
4 | minH | f32 px | Minimum height. |
5 | maxW | f32 px | Maximum width. |
6 | maxH | f32 px | Maximum height. |
8 | paddingT | f32 px | Top padding. |
9 | paddingR | f32 px | Right padding. |
10 | paddingB | f32 px | Bottom padding. |
11 | paddingL | f32 px | Left padding. |
12 | marginT | f32 px | Top margin. |
13 | marginR | f32 px | Right margin. |
14 | marginB | f32 px | Bottom margin. |
15 | marginL | f32 px | Left margin. |
16 | gap | f32 px | Gap between flex children (both axes). |
17 | flexDir | enum FlexDir | 0 = Row, 1 = Col. |
18 | justify | enum Justify | justify-content. |
19 | align | enum Align | align-items. |
20 | grow | f32 | Flex grow factor. |
21 | shrink | f32 | Flex shrink factor. |
22 | basis | f32 px | Flex basis. |
23 | flexWrap | int | 0 = nowrap, 1 = wrap. |
24 | posType | enum PosType | 0 = Relative, 1 = Absolute. |
25 | insetT | f32 px | Absolute positioning top offset. |
26 | insetR | f32 px | Absolute positioning right offset. |
27 | insetB | f32 px | Absolute positioning bottom offset. |
28 | insetL | f32 px | Absolute positioning left offset. |
29 | display | enum Display | 0 = Flex, 1 = None (removes from layout and paint). |
30 | overflow | enum Overflow | 0 = Visible, 1 = Hidden (scissor in draw). |
31 | zIndex | i32 | Paint order among siblings. Paint-only despite its layout-group ID. |
Visual group (64–95) — paint-only; no relayout.
| ID | Name | Type | Description |
|---|
64 | bgColor | color u32 ABGR | Background fill color. |
65 | gradFrom | color u32 ABGR | Gradient start color (used when gradDir is set). |
66 | gradTo | color u32 ABGR | Gradient end color. |
67 | gradDir | enum GradDir | 0 ToTop, 1 ToBottom, 2 ToLeft, 3 ToRight. |
68 | radius | f32 px | Corner radius. |
69 | opacity | f32 0–1 | Multiplies subtree alpha per-vertex. |
70 | borderColor | color u32 ABGR | Border color. |
71 | borderWidth | f32 px | Border width, drawn inset. Does not affect layout. |
72 | shadow | i32 | Baked shadow index: 0 = none, 1 = shadow, 2 = md, 3 = lg. |
Text group (96–127) — text layout props invalidate text measurement; paint-only for color.
| ID | Name | Type | Description |
|---|
96 | textColor | color u32 ABGR | Text color. |
97 | fontSlot | i32 | Baked font atlas slot index. Changes trigger text re-measure. |
98 | textAlign | enum TextAlign | 0 Left, 1 Center, 2 Right. |
99 | lineHeight | f32 px | Overrides the atlas default line advance. |
100 | tracking | f32 px | Extra advance per glyph (letter-spacing). |
Transform group (128–159) — paint-only; never invalidate layout.
| ID | Name | Type | Description |
|---|
128 | translateX | f32 px | Horizontal translation (about the node’s position). |
129 | translateY | f32 px | Vertical translation. |
130 | scale | f32 | Uniform scale about the node center (1 = identity). |
131 | rotate | f32 degrees | Rotation about the node center. |
132 | scaleX | f32 | Horizontal scale about the node center. |
133 | scaleY | f32 | Vertical scale about the node center. |
Animatable props
Props listed in the ANIMATABLE array can be tweened by animate(), driven by springs, and included in style-table transition masks. Their position in the array is their anim bit index — the bit used in the transition mask field of a style record (see Style table format).
The 32 animatable props in order (bit 0 first):
export const ANIMATABLE: readonly PropName[] = [
"width", // bit 0
"height", // bit 1
"paddingT", // bit 2 ... paddingR, paddingB, paddingL
"paddingR", // bit 3
"paddingB", // bit 4
"paddingL", // bit 5
"marginT", // bit 6 ... marginR, marginB, marginL
"marginR", // bit 7
"marginB", // bit 8
"marginL", // bit 9
"gap", // bit 10
"basis", // bit 11
"insetT", // bit 12 ... insetR, insetB, insetL
"insetR", // bit 13
"insetB", // bit 14
"insetL", // bit 15
"bgColor", // bit 16 (colors lerp per ABGR channel)
"gradFrom", // bit 17
"gradTo", // bit 18
"radius", // bit 19
"opacity", // bit 20
"borderColor", // bit 21
"borderWidth", // bit 22
"textColor", // bit 23
"lineHeight", // bit 24
"tracking", // bit 25
"translateX", // bit 26
"translateY", // bit 27
"scale", // bit 28
"rotate", // bit 29
"scaleX", // bit 30
"scaleY", // bit 31
];
TRANSITION_MASK_ALL = 0xffffffff means “every animatable prop” (transition-all).
ENUMS
All enum values are ordinals on the wire. They are append-only.
Easing
Used as the easing argument to animate(). Spring and SpringBouncy ignore durMs — physics determines the duration. OutBack overshoots approximately 10%. All easings tick at a fixed dt = 1/60 s.
| Ordinal | Name | Notes |
|---|
0 | Linear | Constant rate. |
1 | EaseIn | Starts slow, ends fast. |
2 | EaseOut | Starts fast, ends slow. |
3 | EaseInOut | Slow start and end. |
4 | OutBack | Overshoots ~10% before settling. |
5 | Spring | Physics spring; durMs ignored. |
6 | SpringBouncy | Bouncier spring; durMs ignored. |
PosType
| Ordinal | Name | Description |
|---|
0 | Relative | Default; participates in flex flow. |
1 | Absolute | Positioned via insetT/R/B/L relative to nearest ancestor. |
Overflow
| Ordinal | Name | Description |
|---|
0 | Visible | Default; children render outside the box bounds. |
1 | Hidden | Emits a scissor rect in the DrawList; children clip to the box. |
Align (align-items)
| Ordinal | Name |
|---|
0 | Start |
1 | Center |
2 | End |
3 | Stretch |
Justify (justify-content)
| Ordinal | Name |
|---|
0 | Start |
1 | Center |
2 | End |
3 | Between |
4 | Around |
FontSlot
Font slots correspond to baked atlas files at specific pixel sizes. The compiler derives which slots are needed from the Tailwind text-size utilities found in the app source (text-xs through text-4xl, both regular and bold). Slot indices are assigned by the build and encoded in each atlas header.
| Tailwind utility | px size |
|---|
text-xs | 12 px |
text-sm | 14 px |
text-base | 16 px |
text-lg | 18 px |
text-xl | 20 px |
text-2xl | 24 px |
text-4xl | 36 px |
The system supports up to MAX_FONT_SLOTS = 16 atlas slots.
TextAlign
| Ordinal | Name |
|---|
0 | Left |
1 | Center |
2 | Right |
PSM (texture pixel format)
Values must equal the corresponding rust-psp TexturePixelFormat values (sceGuTexMode argument).
| Value | Name | Description |
|---|
2 | PSM_4444 | RGBA 4444, 16-bit. Used for baked corner and shadow alpha sprites. |
3 | PSM_8888 | RGBA 8888, 32-bit. Used for uploaded images. |
BTN bitmask
The PSP button bitmask is identical on every host. Web and Bun hosts remap keyboard keys to these bits. Pass a BTN value as the mask argument to onButtonPress(), or combine bits with | to watch multiple buttons at once.
| Constant | Value (hex) | PSP Button |
|---|
SELECT | 0x0001 | SELECT |
START | 0x0008 | START |
UP | 0x0010 | D-pad Up |
RIGHT | 0x0020 | D-pad Right |
DOWN | 0x0040 | D-pad Down |
LEFT | 0x0080 | D-pad Left |
LTRIGGER | 0x0100 | L trigger |
RTRIGGER | 0x0200 | R trigger |
TRIANGLE | 0x1000 | Triangle |
CIRCLE | 0x2000 | Circle |
CROSS | 0x4000 | Cross |
SQUARE | 0x8000 | Square |
export const BTN = {
SELECT: 0x0001,
START: 0x0008,
UP: 0x0010,
RIGHT: 0x0020,
DOWN: 0x0040,
LEFT: 0x0080,
LTRIGGER: 0x0100,
RTRIGGER: 0x0200,
TRIANGLE: 0x1000,
CIRCLE: 0x2000,
CROSS: 0x4000,
SQUARE: 0x8000,
} as const;
By convention, CIRCLE is the primary action button (fires onPress on a focused Focusable), and LTRIGGER/RTRIGGER are used for paging by Gallery.
PAK container constants
PocketJS packs styles, font atlases, and images into a .pak file that reuses the dreamcart container format byte-for-byte, so existing tooling can open them.
Header and entry layout
The header is 32 bytes; entries are 24 bytes each. All multibyte fields are little-endian.
Header (32 bytes):
off 0 u32 magic = 0x4b504344 ('DCPK' in LE)
off 4 u16 version = 1
off 6 u16 flags
off 8 u32 entryCount
off 12 u32 dirOffset
off 16 u32 namesOffset
off 20 u32 blobsOffset (16-byte aligned)
off 24 u32 fileSize
off 28 u32 reserved
Entry (24 bytes):
off 0 u32 keyHash (FNV-1a 32-bit of the key string)
off 4 u32 blobOff (from start of file)
off 8 u32 byteLen
off 12 u32 nameOff (from namesOffset)
off 16 u16 nameLen
off 18 u8 dtype (advisory element type)
off 19 u8 reserved
off 20 u32 reserved
Entries are sorted by keyHash; blobs are 16-byte aligned.
Constants
| Constant | Value | Description |
|---|
PAK_MAGIC | 0x4b504344 | Magic bytes 'DCPK' (little-endian). |
PAK_VERSION | 1 | Format version. |
PAK_HEADER_SIZE | 32 | Byte size of the header. |
PAK_ENTRY_SIZE | 24 | Byte size of one directory entry. |
PAK_ALIGN | 16 | Blob alignment in bytes. |
PAK_FNV1A_OFFSET_BASIS | 0x811c9dc5 | FNV-1a 32-bit offset basis. |
PAK_FNV1A_PRIME | 0x01000193 | FNV-1a 32-bit prime. |
Key strings
The runtime uses these well-known key conventions to locate assets in the pak. Entry lookup hashes the key with FNV-1a 32-bit (h ^= byte; h *= prime).
| Key pattern | Description |
|---|
'ui:styles' | The compiled styles.bin blob. |
'ui:font.<slot>' | One baked font atlas. <slot> is the numeric slot index encoded in the blob header. |
'ui:img.<name>' | An image texture. <name> is the image filename without its ui:img. prefix. |
'ui:sprite.<name>' | An animated sprite atlas. <name> is the sprite atlas name. |
On PSP, native/src/pak.rs reads these keys and feeds Ui::load_styles, Ui::load_font_atlas, and Ui::upload_texture directly from the embedded bytes before JavaScript evaluation starts. The texture and sprite handles are then exposed to JS as ui.__textures and ui.__sprites. On web and Bun test hosts, pak.ts reads the same pak and delivers assets through the HostOps methods.
The style table is compiled by compiler/tailwind.ts and parsed by core/src/style.rs. It is little-endian and has no inter-field padding.
Header (8 bytes):
off 0 u32 magic = 0x54534344 ('DCST' in LE)
off 4 u16 version = 1
off 6 u16 styleCount
Then styleCount records, back-to-back. styleId = record index (0-based).
Record:
off 0 u8 flags
bit 0 STYLE_VARIANT_BASE base variant present
bit 1 STYLE_VARIANT_FOCUS focus: variant present
bit 2 STYLE_VARIANT_ACTIVE active: variant present
bit 3 STYLE_HAS_TRANSITION transition block present
[if STYLE_HAS_TRANSITION] transition block (12 bytes):
+0 u32 mask anim-bit mask (ANIMATABLE order; 0xffffffff = all)
+4 u16 durMs
+6 u16 delayMs
+8 u8 easing (ENUMS.Easing ordinal)
+9 u8[3] reserved (0)
For each present variant in order (base, focus, active):
+0 u8 propCount
then propCount × 6-byte prop records:
+0 u8 propId (PROP value)
+1 u8 reserved (0)
+2 u32 value (f32 bits | ABGR color | int, per PROP_VALUE_KIND)
export const STYLE_MAGIC = 0x54534344; // 'DCST' LE
export const STYLE_VERSION = 1;
export const STYLE_HEADER_SIZE = 8;
export const STYLE_TRANSITION_SIZE = 12;
export const STYLE_PROP_RECORD_SIZE = 6;
STYLE_ID_NONE = -1 passed to setStyle clears a node back to default style.
Fixed timestep
The core animation engine always ticks at a fixed dt = 1/60 s, regardless of host. Frame content is a pure function of frame index, which is what makes byte-exact golden screenshots possible across PSP hardware, PPSSPP, WASM, and headless Bun.
export const FIXED_DT = 1 / 60; // seconds