Xyz
State ownership primitives, a declarative Tweakpane debug layer, and Three.js scene warmup to kill first-frame hitches.
JOYCO's 3D toolkit. Keyed state ownership and ordered teardown for real-time apps, a declarative debug-pane layer over Tweakpane, and Three.js scene warmup that forces every shader compile and texture upload to happen before the player ever sees a hitch.
Mental model
Xyz is three independent pillars, each its own subpath so you only ship what you use:
- Core (
@joycostudio/xyz) —Derivedandedgefor state that multiple owners write to and one policy resolves;Disposerfor teardown that unwinds in reverse. - Debug (
@joycostudio/xyz/debug) —DebugManagerowns URL/keyboard activation and lazy pane creation; a declarative schema (slider,toggles,select,color,section,button, …) binds to a live Tweakpane folder without hand-wired glue. - Three (
@joycostudio/xyz/three) —Warmupdiscovers a scene's full renderable graph, exposes it long enough to push every texture/shader through the driver once, then restores every mutation exactly as it found it.
Features
| Feature | Description |
|---|---|
Keyed ownership (Derived) | Independent writers claim a slot by key; value resolves all current intents through one explicit policy. |
Transition sync (edge) | Turns a selector into a callback that only fires when the selected value actually changes. |
Ordered teardown (Disposer) | Registrations unwind in reverse order; disposal is idempotent and safe to register into after teardown. |
| Declarative debug schema | slider, sliders, bool, toggles, select, color, colorString, section, button build a DebugSchema with no Tweakpane import required. |
| URL + keyboard activation | DebugManager lazy-mounts the pane on ?debug or Option+D, and stays in sync with popstate. |
| Survives enable/disable | Registrations remount from fresh definitions every cycle — no stale bindings after a toggle. |
| Scene warmup | Warmup inventories geometries, materials, and textures (including TSL node graphs), pauses the driver, and forces one representative render before restoring visibility, culling, and draw ranges. |
| Late-resource auditing | audit() warns once per resource introduced after warmup — a throttled dev-only diagnostic, never a runtime upload. |
| No R3F dependency | The Three.js utilities are raw Three.js — bring your own renderer/canvas ownership split. |
Install
pnpm add @joycostudio/xyzthree and @tweakpane/core are optional peer dependencies — install them only if you use the /three or /debug subpaths.
Quick start
import { Derived, edge, maxOf } from '@joycostudio/xyz'
const blur = new Derived(0, maxOf)
blur.set('loader', 0.8)
blur.set('inspect', 0.35)
blur.value // 0.8
const syncPhase = edge(
(state: { phase: string }) => state.phase,
(next, previous) => console.log(previous, '→', next)
)Quick start (debug)
import { DebugManager, property, slider, bool } from '@joycostudio/xyz/debug'
import { Pane } from 'tweakpane'
const state = { amount: 0.5, enabled: true }
const debug = new DebugManager({
createPane: () => new Pane({ title: 'Scene' }),
})
debug.bind({
title: 'scene',
schema: () => ({
amount: slider(property(state, 'amount'), { min: 0, max: 1 }),
enabled: bool(property(state, 'enabled')),
}),
})Open with ?debug=true in the URL, or press Option+D.
Quick start (three)
import { Warmup } from '@joycostudio/xyz/three'
const warmup = new Warmup({
pause: () => (clock.running = false),
resume: () => (clock.running = true),
initTexture: (texture) => renderer.initTexture(texture),
compile: (scene, camera) => renderer.compileAsync(scene, camera),
render: () => renderer.render(scene, camera),
nextFrame: () => new Promise((resolve) => requestAnimationFrame(() => resolve())),
})
await warmup.start(scene, camera) // one representative frame, then the scene is restored
warmup.audit(scene) // dev-only: warns about resources introduced after warmupArchitecture
| Layer | Responsibility |
|---|---|
Core state (Derived, edge) | State resolution primitives with no DOM or rendering dependency. |
| Disposer | An owner-local teardown stack shared by the debug and three layers (and your own code). |
Debug schema (schema.ts) | Pure DebugField/DebugDefinition builders — no Tweakpane import, fully testable in isolation. |
Debug binding (bind.ts) | Binds a DebugDefinition into a live FolderApi: tracks live-refresh fields, nested sections, and computed sources for copy. |
| DebugManager | Owns URL/keyboard activation and lazy pane creation; registrations survive disable/enable and remount fresh. |
Scene discovery (warmup.ts) | discoverScene walks the graph once, inventories every geometry/material/texture (materials, uniforms, TSL nodes), and temporarily exposes hidden/culled/zero-instance state. |
| Warmup orchestration | Warmup pauses the driver, runs registered Warmable.warmup() hooks, uploads textures, compiles, renders once, then restores everything in reverse — even on failure. |
Core API
State
| Export | Description |
|---|---|
Derived<In, Out> | .set(key, value) / .clear(key) claim or release a slot; .value resolves through combine. |
maxOf | Resolve to the highest active value, never lower than the fallback. |
latestOf | Resolve to the most recently claimed slot. |
edge(select, onChange, equals?) | Returns a sync function: runs select every call, onChange only when the result changes. |
Disposer | .add(teardown), .listen(target, type, fn, options?), .dispose() — reverse-order, idempotent. |
Debug
| Export | Description |
|---|---|
DebugManager | .register(source) / .bind(definition) to mount; .enable() / .disable() / .toggle() / .sync() control activation. |
bindDebugSchema(host, definition, opts?) | Binds a DebugDefinition into an existing Tweakpane folder; returns apply() / refresh() / values(). |
ref(read, write) / property(obj, key, changed?) | Adapt a getter/setter pair, or an object property, into a ValueRef. |
slider / sliders | One bounded numeric control, or several declared from an object's numeric keys at once. |
bool / toggles | One boolean control, or several declared from an object's boolean keys at once. |
select / enumField | A string/number option control (enumField is an alias). |
color / colorString / hexColor | Bind an RGB-compatible object, or a CSS color string (hexColor is an alias). |
folder / debugFolder / section | Nest a DebugDefinition as a field; section adds a collapsed, copyable group. |
button | Declare a sync or async debug action, with optional feedback text. |
schemaValues(schema) | Read every control's current value (recursing into folders, skipping buttons). |
Three
| Export | Description |
|---|---|
Warmup | .add(entity) registers a Warmable; .start(scene, camera) runs once; .audit(scene); .dispose(). |
discoverScene(scene, camera) | Inventories geometries/materials/textures and returns a restore() to undo the temporary exposure. |
Warmable | Interface for entities whose render state isn't already in the scene — implement warmup(). |
WarmupDriver | Renderer-owned hooks the caller supplies: pause, resume, initTexture, compile, render, nextFrame. |
Package exports
| Import path | Contents |
|---|---|
@joycostudio/xyz | VERSION, Disposer, Derived, maxOf, latestOf, edge, and their types. |
@joycostudio/xyz/debug | DebugManager, bindDebugSchema, ref, property, slider, sliders, bool, toggles, select, color, colorString, hexColor, enumField, folder, debugFolder, section, button, schemaValues, toFolders, editableTarget, matchesShortcut, parameterEnabled, and their types. |
@joycostudio/xyz/three | Warmup, discoverScene, and their types (Warmable, WarmupDriver, WarmupCleanup, SceneDiscovery). |
Demo
The JOYCO UI catalog in apps/demo gives every public runtime export its own interactive Next.js route: 8 core utilities, 22 debug utilities, and 2 Three.js utilities. The 3D routes use raw Three.js — there is no React Three Fiber dependency — and follow the same thin-canvas/renderer-owner split as Jam.
pnpm demoOpen the printed local URL and choose an export from the catalog. The DebugManager route demonstrates URL and Option+D activation, while each schema helper has a focused live Tweakpane. Use pnpm demo:build to verify the production integration.
Development
pnpm install
pnpm test # Vitest suite (run); pnpm test:watch for watch mode
pnpm typecheck # tsc --noEmit, plus the demo app
pnpm build # tsup bundle (index, debug, three)
pnpm lint # eslint --fixReleases go through Changesets:
pnpm changeset # describe the change and bump type
pnpm version:package # apply pending changesets, update CHANGELOG
pnpm release # build and publish to npm