@joycostudio/xyz
v1.0.0
Published
joyco.studio 3d toolkit
Readme
Xyz
JOYCO's 3D toolkit. Keyed state ownership, typed definitions, CDN asset routing, ordered teardown, 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;defineAssetSourcefor Chopper-aware CDN paths;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. |
| Asset routing (defineAssetSource) | Keeps source names at call sites while selecting one output from a Chopper-compatible optimization plan. |
| 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.
defineAssetSource accepts Chopper's resolve function structurally, so Xyz does not require Chopper as a peer. Install @joycostudio/chopper in the consuming app when its config supplies the optimization plan.
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)
)Asset routing
import chopper from './chopper.config'
import { defineAssetSource } from '@joycostudio/xyz'
export const asset = defineAssetSource({
url: new URL('https://r2.joyco.studio/jam/'),
planner: chopper.resolve,
optimize: () =>
typeof window === 'undefined' || new URLSearchParams(window.location.search).get('compressed') !== 'false',
segments: {
common: 'common',
escapeFromHell: 'escape-from-hell',
},
})
asset.common('textures/noise.png')
// https://r2.joyco.studio/jam/common/textures/compressed/noise.webpThe planner sees the stable source key without its query or hash. Exactly one planned output is selected; unmatched, failed, or multi-output plans fall back to the source path.
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. |
| Definitions and assets | Literal-preserving configuration and pure source-to-CDN URL planning. |
| 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. |
| 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. |
| defineAssetSource(definition) | Create a callable CDN resolver with typed builders for every declared segment. |
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 / 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(). |
| Warmable | Interface for entities whose render state isn't already in the scene — implement a synchronous or asynchronous warmup() hook. |
| WarmupDriver | Renderer-owned hooks the caller supplies: pause, resume, initTexture, compile, render, nextFrame. |
Package exports
| Import path | Contents |
| ---------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| @joycostudio/xyz | Disposer, Derived, maxOf, latestOf, edge, defineAssetSource, and their types. |
| @joycostudio/xyz/debug | DebugManager, bindDebugSchema, ref, property, slider, sliders, bool, toggles, select, color, colorString, hexColor, enumField, folder, section, button, schemaValues, and their types. |
| @joycostudio/xyz/three | Everything from /track, plus Warmup, ThreeDOM, ThreeProjector, projection/sizing helpers, and their types. |
| @joycostudio/xyz/track | DynamicCanvas, TrackedElement, TrackingContext, createMetriCanvas, findStickyAncestor, and their types. |
DOM tracking
Three.js projects can import the DOM primitives and renderer adapters together
from @joycostudio/xyz/three. Use @joycostudio/xyz/track for renderer-independent
tracking. Both entry points expose the same classes and functions.
This ports gl-sync's shared tracking, sticky positioning, WebGPU multi-canvas
views, and shared WebGL2 fallback. Renderers, scenes, assets, and frame scheduling
remain application-owned.
import { ThreeDOM } from '@joycostudio/xyz/three'
await renderer.init()
const dom = new ThreeDOM({ renderer, metri, container })
dom.addView(element, { scene, camera })
const world = dom.addScene({ scene: worldScene, camera: worldCamera })
world.track(objectElement, objectGroup)
renderer.setAnimationLoop((time) => {
metri.update()
dom.update(time)
dom.render()
})
// On teardown: stop the loop, dom.destroy(), then dispose application resources.To prepare independent view targets before starting the loop, call
await dom.prepare(). Optional clip elements and per-view clipPadding
restrict drawing without changing camera framing or target dimensions.
Views and shared scene objects can coexist on one renderer/device. ThreeDOM
owns their placement and render passes. Pass an optional caller-owned TSL
pipeline to addView() or addScene() to include post-processing in
dom.render() and dom.prepare(), with per-registration sizing and final clipping.
Use alpha: true for the native WebGPU overlay, a distinct camera for each
registration, and a ClippingGroup for each clipped object binding.
See the mixed rendering strategy for the
architecture, Trazo diagram source, composition order, and ownership.
Requires @joycostudio/[email protected] and Three.js ^0.184.0 || ^0.185.1.
See the tracking API guide
for setup, ownership, and rendering constraints.
gl-sync is the canonical ThreeDOM tracking adoption project.
The guide includes a complete view example and server-rendered Trazo diagrams, with
focused pages for TSL pipelines
and tracking primitives.
The browser suite exercises built package exports: pnpm test:track
(install Chromium first with pnpm exec playwright install chromium).
Docs
The Fumadocs Core-backed site in apps/docs keeps its authored pages in apps/docs/content/docs as Markdown/MDX. The site shell uses the JOYCO UI kit, while the content embeds React examples from apps/docs/demos, including raw Three.js examples with no React Three Fiber dependency.
pnpm docsOpen the printed local URL and choose a concept from the catalog. Each page starts with a live demo and its actual source files in tabs directly underneath, followed by the API reference. Source is read from the same files that run the demo, including supporting files for multi-file examples. Register examples in apps/docs/lib/demos.ts and embed them with <DemoPreview name="…" />. The debug runtime page demonstrates DebugManager, URL semantics, and Option+D handling, while the declarative schemas page collects the live Tweakpane helpers. Use pnpm docs:build to verify the production integration.
Authored flow and seq code fences render as server-rendered SVG diagrams using
Trazo. Add title="Description" after the fence
language for an accessible title. Trazo validates syntax during the docs build;
Markdown endpoints retain the original fences. Readers can expand and copy the
source, and wide graphs scroll without shrinking their labels. Diagram colors
follow the site theme through CSS, without a client-side diagram renderer.
Development
pnpm install
pnpm test # Vitest suite (run); pnpm test:watch for watch mode
pnpm typecheck # tsc --noEmit, plus the docs app
pnpm build # tsup bundles, including tracking
pnpm lint # eslint --fixReleases go through Changesets:
pnpm changeset # describe the change and bump type in a feature PRAfter the feature PR merges, the Release workflow creates or updates Version Packages; merging that generated PR triggers publication. Beta releases use the same flow with Changesets prerelease mode. Follow the release guide; versioning and publishing commands are reserved for GitHub Actions.
