v0.15 — physics dynamics, fixed-timestep, React/Aseprite/LDtk, docs-site polish#212
Merged
Conversation
added 30 commits
June 26, 2026 03:14
Allows specifying a custom Rectangle for viewport cull intersection instead of the computed node bounds. Useful for nodes with off-screen effects or custom cull regions. Serialized in commonFields.
Adds dist/exo.iife.js and dist/exo.iife.min.js as global IIFE bundles exposing ExoJS on globalThis. Enables script-tag CDN usage. Adds size-limit CI gate: ESM bundle ≤700KB gzip, IIFE min ≤250KB gzip.
Adds parallaxX/parallaxY to TileLayer (default 1.0). TiledMap.toTileMap() now forwards parallax factors from TiledLayer. TileLayerNode applies camera-relative parallax offset during render plan collection, enabling depth parallax effects from Tiled editor layer settings.
Adds TweenSequencer with sequential and parallel stage composition, wait() delays, repeat/yoyo, and lifecycle callbacks. Integrates with TweenManager via addTicker(). Factory: manager.createSequencer().
Tooltip: hover-triggered text label with configurable delay and position. ScrollContainer: clipped widget with vertical/horizontal scroll via mouse wheel. Content added to .content child container.
Integrates the advanced-blend-spike compositor code into the render plan. Modes Darken(5)..Luminosity(17) now use backdrop-aware compositing (GPU copy + blend shader) instead of broken fixed-function MIN/MAX. Modes 0-4 unchanged (fixed-function path). Adds isAdvancedBlendMode predicate and needsBackdropBlend barrier flag. Both WebGL2 and WebGPU backends implement composeWithBackdropBlend().
Adds Logger with LogSeverity/LogChannel/LogEntry, prod-stripped for non-error logs. Exports logger singleton with auto-attached console handler in dev mode. Adds new Signals: Application.onError, Scene.onLoad/onUnload, SystemRegistry.onAdd/onRemove for external system integration.
…plicate __DEV__ decl, prettier)
…tic factories
DX improvement — instead of manually constructing both ShaderSource and
MeshMaterial/SpriteMaterial, a single call suffices:
MeshMaterial.from(vertSrc, fragSrc, { uniforms, blendMode })
MeshMaterial.from(existingShaderSource, { uniforms })
Two TS overloads per class:
1. from(source: ShaderSource, options?: Omit<MaterialOptions, 'shader'>)
2. from(glslVertex, glslFragment, options?: { wgsl?, uniforms?, blendMode?, sampler? })
Implementation uses exactOptionalPropertyTypes-safe conditional spreads
(...(x !== undefined ? { x } : {})) and instanceof dispatch for the
ShaderSource overload. SpriteMaterial.from() is a structural mirror.
…tic factories
DX improvement — instead of manually constructing both ShaderSource and
MeshMaterial/SpriteMaterial, a single call suffices:
MeshMaterial.from(vertSrc, fragSrc, { uniforms, blendMode })
MeshMaterial.from(existingShaderSource, { uniforms })
Two TS overloads per class:
1. from(source: ShaderSource, options?: Omit<MaterialOptions, 'shader'>)
2. from(glslVertex, glslFragment, options?: { wgsl?, uniforms?, blendMode?, sampler? })
Implementation uses exactOptionalPropertyTypes-safe conditional spreads
(...(x !== undefined ? { x } : {})) and instanceof dispatch for the
ShaderSource overload. SpriteMaterial.from() is a structural mirror.
…oader Adds four new public signals to Loader for tracking foreground load batches without polling: - onLoadStart [key, url] fires when the loader transitions idle to active - onLoadProgress [loaded, total, key] fires after each item settles - onLoadComplete [] fires when all batch items have settled - onLoadError [key, error] fires on failure, does not suppress onLoadComplete Batch lifecycle: the first load() call that finds the loader idle starts a new batch and resets counters; concurrent calls within the same tick join the current batch so total grows dynamically. When active count returns to zero, onLoadComplete fires and totals reset. All four code paths in load() (extension-based, single string, array, Record) plus _createLoadingQueue (Asset/Assets/config-map) are instrumented. All four signals are destroyed in destroy().
Adds blob (8-neighbor) and edge (4-neighbor) Wang autotiling support to @codexo/exojs-tilemap. The corner-dependency rule ensures diagonal bits are only set when both adjacent cardinal directions are present, collapsing the 256 raw blob combinations to the standard 47 valid states. - WangSet: maps a bitmask to a local tile ID within a named tileset. Accepts both ReadonlyMap<number,number> and a plain Record. - autoTile(layer, wangSet, options?): two-pass (snapshot -> write) so reads always reflect pre-call state; supports matchFn to restrict the Wang group and wrapBorder to control border tile fill behaviour. - 7 unit tests covering corner dependency, matchFn scope, and edge mode.
Adds the new @codexo/exojs-ldtk extension package that parses the LDtk level-editor JSON format and converts each level to a runtime TileMap. - LdtkData.ts: TypeScript types for the LDtk JSON format (v1.5.x) - LdtkMap.ts: parsed source model with levels: TileMap[] and getLevelByName() - ldtkToTileMap.ts: pure converter (LdtkData + optional TileSet map → LdtkMap); Tiles/AutoLayer → TileLayer; IntGrid → TileLayer (auto-tiles when present); Entities → ObjectLayer with scalar field properties; no __DEV__ usage - loadLdtkMap.ts: async loader — fetches JSON, loads tileset textures, builds TileSets, then calls ldtkToTileMap - ldtkBinding.ts: AssetBinding claiming the .ldtk extension - ldtkExtension.ts: Extension descriptor depending on tilemapExtension - register.ts: side-effectful auto-registration entry - public.ts / index.ts: barrel + ExtensionTypeMap / AssetDefinitions augmentation - test/ldtkToTileMap.test.ts: 19 unit tests (no loader/texture required) - pnpm-workspace.yaml + vitest.config.ts: register the package
New package that integrates Aseprite JSON sprite sheet exports with the
ExoJS AnimatedSprite system. Supports both array-mode and hash-mode
Aseprite JSON exports, derives animation clips from frameTags with
per-frame fps averaging, and exposes a simple one-liner API:
const sheet = await loader.load(AsepriteSheet, 'hero.aseprite.json');
const sprite = sheet.createAnimatedSprite();
sprite.play('run');
Public API:
- AsepriteSheet.parse(data, texture) — static factory
- AsepriteSheet.createAnimatedSprite() — returns AnimatedSprite with all clips
- AsepriteSheet.clips — ReadonlyMap<string, AnimatedSpriteClipDefinition>
- AsepriteSheet.spritesheet — underlying Spritesheet
- AsepriteFormatError — thrown on invalid Aseprite JSON
- isAsepriteArrayData() — discriminator for array/hash mode
- asepriteExtension / asepriteBinding — for manual extension registration
- /register entry for global auto-registration
Note: pnpm-workspace.yaml registration left to a follow-up commit
(per task constraints: shared config files not modified here).
…p, useScene Provides React integration for the ExoJS game engine: - ExoCanvas: mounts canvas, creates/destroys Application, provides context - useExoApp: returns the Application from the nearest ExoCanvas ancestor - useScene: starts the engine on first mount, switches scenes on re-renders - ExoContext / useExoContext exported for advanced provider use Package ships TypeScript source (no Rollup build step); consumers supply the bundler.
…errors across new packages
Softly pulls a single body's grab point toward a movable target point (the standard mouse-drag primitive: physics sandboxes, puzzle/grab mechanics). The grab point is fixed on the body at creation; reassigning `joint.target` each frame drags it and wakes the body so the drag tracks live. Bounded by `maxForce` so heavy bodies lag. Reuses the existing soft-constraint joint infrastructure (Box2D-v3 soft factors, sub-step solve, warm-start). Being single-body, it stands a private static ground sentinel in for bodyA so the island/solver machinery — which assumes two bodies — sees a static anchor it never integrates, unions or mutates; only the dragged body is touched. Tests: SG-J17 (drags to target + tracks a moved target), SG-J18 (maxForce caps the pull). Full physics suite 120/120 green; typecheck + lint + prettier clean. Co-authored-by: Exoridus <github@codexo.de>
…195) * fix(core): oriented bounding-box axes for rotated-node SAT collision SceneNode.getNormals()/project() delegated to getBounds() — the loose axis-aligned AABB — even for rotated nodes. The SAT collision path (node-vs-node / -rect / -polygon) therefore tested the AABB's axes, so a rotated node accepted collisions its true oriented box rejects. Now, for non-axis-aligned nodes, getNormals()/project() build the oriented bounding box (the four local-bounds corners under the global transform) as a Polygon and use its edge normals / projection; the isAlignedBox fast path keeps the exact, cheaper AABB. The oriented polygon is built lazily (only rotated nodes that are SAT-tested pay it) and refreshed in place via a shared corner scratch — allocation-free. Tests: getNormals() returns diagonal axes for a 45° node; intersectsWith() rejects an AABB false-positive (target in the AABB corner, outside the oriented diamond). Note: rotated-node vs circle/ellipse/line/point still uses the AABB (unchanged) — only the SAT axis path is corrected here. * chore(core): sync API doc + export snapshots for the OBB change - scene-node.mdx: source-line anchor shifted by the added module scratch. - root-index snapshots: capture the pre-existing `Codec` core export (the physics PRs never ran the core lane, so the drift went unexercised; this core-touching change runs it and must be current). --------- Co-authored-by: Exoridus <github@codexo.de>
…d rotation) (#196) * fix(core): use the world transform to pick AABB vs OBB collision path The OBB collision fix gated on isAlignedBox, which inspects only the node's OWN rotation. A node with no rotation of its own but a rotated ancestor has a rotated WORLD box yet isAlignedBox is true — so it took the AABB fast path and SAT silently tested the loose axis-aligned box. (Latent since the SAT routing was added; never surfaced because the only collision example uses non-rotated axis-aligned rectangles.) getNormals()/project()/intersectsWith()/collidesWith()/contains() now gate on the global transform mapping axes to axes — (b≈0 && c≈0) || (a≈0 && d≈0) — instead of the node-local isAlignedBox. The public isAlignedBox getter keeps its node-local meaning. The fast/SAT choice only affects which (both-correct) path runs, so the epsilon is perf-only, never a correctness risk. Test: a Drawable with no own rotation under a 45°-rotated Container — isAlignedBox is true, yet a target in the world-AABB corner (outside the inherited diamond) is correctly rejected. Core suite 2498/2498 green. * docs(api): regenerate scene-node.mdx for world-alignment wording --------- Co-authored-by: Exoridus <github@codexo.de>
…rameAlpha) (#197) * feat(core): fixed-timestep loop (Scene.fixedUpdate / onFixedFrame / frameAlpha) Adds an accumulator-driven fixed timestep (Gaffer's "Fix Your Timestep") so physics and deterministic gameplay advance at a frame-rate-independent rate, decoupled from the variable render rate — the Unity Update/FixedUpdate split. Each frame, after input, the loop runs 0..N fixed steps: scene.fixedUpdate(fixedDelta) + onFixedFrame, then the variable scene.update / draw as before. Always on (default 1/60s, configurable via `new Application({ fixedTimeStep })`); fixedUpdate is a no-op by default, so existing games are behaviour-identical and the empty loop is free. - `FixedTimestep` (@internal): pure accumulator → step count + interpolation alpha, with an epsilon so exact multiples don't lose a step to float error, and a maxSteps spiral-of-death guard that drops backlog rather than carrying it. Accumulator resets on start/resume so a paused gap is not caught up. - `Scene.fixedUpdate(delta)` + `SceneManager.fixedUpdate` (skips a paused scene). - `Application.onFixedFrame`, `Application.frameAlpha` (render interpolation factor; the scene graph is not auto-interpolated — expose alpha, lerp opt-in), `ApplicationOptions.fixedTimeStep`. Tests: FixedTimestep maths (steps/alpha/cap/reset), the loop runs the right number of fixed steps + alpha + spiral cap, SceneManager skips a paused scene. Core suite 2510/2510 green. * test(core): stub _fixed in bypass-constructor app tests + regenerate API docs The Object.create(Application.prototype) loop tests hand-build the app and stub the fields update() touches; add the new _fixed accumulator stub (a 0-step no-op) alongside _frameClock. Regenerate the Application/Scene/ SceneManager API docs for the new fixed-timestep members. --------- Co-authored-by: Exoridus <github@codexo.de>
… (F2) (#198) Adds the three remaining extension packages to the API-doc generator's EXTENSION_PACKAGES so their public classes/enums get MDX pages and appear in the API navigation (physics / aseprite / ldtk subsystems + nav meta). React stays hand-written (hooks/JSX over generated API) per the plan. Fixes a latent build-api bug surfaced by physics: TypeDoc infers a per-package source base for a package that pulls in no core *source* files (only type-only imports), yielding package-relative file names (`shapes/BoxShape.ts`) that miss the sourceMarker filter and break source links. Pinning `basePath` to the repo root makes every package report repo-relative paths (`packages/<pkg>/src/...`) consistently — existing packages' pages are byte-identical (verified: 0 changed), physics/ aseprite/ldtk now generate. Generated: 16 physics, 2 aseprite, 1 ldtk pages (269 total). Interfaces/ types/functions are intentionally not paged (classes + enums only), so aseprite/ldtk are thin by design. Co-authored-by: Exoridus <github@codexo.de>
Both packages were near-untested. These are characterization tests for the shipped behaviour (real checked-in fixtures, no behaviour mocks): aseprite (3 files, 51 tests): AsepriteSheet parse for both array- and hash-frames forms (frame rekeying, rects, fps from durations + 12fps fallback), isAsepriteArrayData guard, clips from frameTags (inclusive range, loop, pingpong not expanded), createAnimatedSprite, the binding load path (image resolution + sub-load) and all malformed-input AsepriteFormatError cases, and the extension registration contract (root side-effect-free vs /register). ldtk (4 new files, +59 tests → 78): LdtkMap accessors + destroy forwarding, flip-bit decoding (ldtkFlipNone/X/Y/Xy), ldtkToTileMap across layer types (Tiles/AutoLayer/IntGrid/Entities) + grid-size derivation + field projection + entity-id formula, the loadLdtkMap loader path, and the extension contract. Flagged for follow-up (asserted as current behaviour, not fixed here): - ldtk `resolveLdtkUrl` throws on a relative .ldtk base URL (Tiled's adapter handles relative bases — real inconsistency). - ldtk `loadLdtkMap` does no structural validation (raw TypeError vs a typed format error like TiledFormatError). - aseprite parses `slices` into the type model but AsepriteSheet never surfaces them (unimplemented, not a bug). Co-authored-by: Exoridus <github@codexo.de>
The @codexo/exojs-react package had no tests and no vitest project.
Adds the infrastructure and a meaningful suite (5 files, 24 tests) that
mocks the engine (a MockApplication) so the React glue is what's under
test, not WebGL.
Infra (additive — the other 8 jsdom projects are byte-for-byte unchanged):
- devDeps @testing-library/react + @testing-library/dom (root); pnpm
resolved their react/react-dom peers to the existing workspace 19.x.
- vitest.config: an `exojs-react` jsdom project reusing the shared
createJsdomTestProject helper unchanged, with esbuild automatic JSX set
at the project level only (the rendering-perf `plugins` pattern).
- eslint.config: widen the three package-test globs `.ts` → `.{ts,tsx}`
(a superset — no other package has .tsx tests; verified unchanged for
them). Without it `.tsx` tests fell through to the typed config and
crashed eslint.
Tests: useExoApplication (construct once, canvas binding, recreate +
destroy on backend change, live-sync size/sizingMode/clearColor with
value-keying, destroy on unmount), ExoCanvas (positioned wrapper + canvas,
div/canvas prop forwarding, children overlay, context), Scenes/Scene/
useActiveScene (activation + setScene with transition + null on unknown),
useScene, and the context error path (useExoApp throws outside a provider).
Verified: exojs-react 24/24; multi-project run (react + core + aseprite +
particles) 2639/2639 green — the shared-config change disturbs nothing.
Written by a subagent; the shared-infra changes were reviewed before commit.
Co-authored-by: Exoridus <github@codexo.de>
`pnpm create:package <name>` scaffolds a new lockstep extension package and auto-wires it into the single sources of truth, so adding the N-th package is one command instead of a ~10-file hand-edit. Generates `packages/exojs-<name>/` (package.json/tsconfig/rollup/src/test/ README/LICENSE) templated from the existing packages, and edits (idempotently): - `scripts/release/lockstep-packages.ts` — the LockstepPackage entry, - `scripts/ci/select-lanes.mjs` — RUNTIME_PACKAGES, - `pnpm-workspace.yaml` — the explicit member list (the workspace lists members explicitly, so the package is invisible to pnpm without it). Then prints a concrete, copy-pasteable checklist for what it deliberately does NOT auto-edit (enumerated YAML / other runtimes): the `_ci-checks.yml` + `release.yml` `--filter` lines (the latter enforced by verify:release-matrix), the `vitest.config.ts` project, the root `package.json` filter lines, and the npm placeholder-publish + Trusted-Publisher (OIDC) bootstrap from RELEASING.md. Flags: `--register` (ships `/register` + `sideEffects` array) vs `--library` (default, `sideEffects: false`); `--dep <pkg>` (workspace runtime dep, tiled→ tilemap style, repeatable/comma-list); `--description`; `--no-offline-smoke`. Version + peer range derive from Core's package.json. `--dep` also threads any `*-source` custom condition a dep needs (e.g. particles' `#` imports). Verified end-to-end: library + register+dep + multi-dep modes all generate a package that `tsc --noEmit` passes, wire all three SoT files, and the smoke artifacts revert cleanly. Written by a subagent; reviewed + re-smoke-tested before commit. Co-authored-by: Exoridus <github@codexo.de>
…#202) The live playground reported `Module '@codexo/exojs' has no exported member 'Application'` (and ~10 similar). Root cause: the vendor declaration tree ships Node `#`-subpath imports (`export * from '#core/index'`). Node and tsc resolve `#` natively via package.json `imports`, so npm consumers are fine — but Monaco's in-browser TypeScript worker does NOT apply the imports map, so every `#`-imported symbol (Application, Scene, …) resolves to nothing. This regressed silently when the `@/` → `#` migration deleted the old dts-alias rewrite (Node didn't need it; the browser does). sync-exo-vendor now rewrites each `#<path>` to a path relative to its declaration file (per the root `imports` map, `#<path>` → `dist/esm/<path>`, i.e. `<path>` within the copied esm tree), then **asserts no import-position `#` survives** — a stray one silently breaks the playground, so this guard fails the build loudly on every sync/deploy rather than shipping broken types. Verified: re-sync rewrites 169 declarations; `esm/index.d.ts` is now relative and `Application`/`ApplicationStatus` resolve through `./core/index → ./Application`. GLSL `"#version …"` strings, hex colours and sourcemap comments are correctly not touched (guard checks import position only). Co-authored-by: Exoridus <github@codexo.de>
) Adds the optimised ExoJS brand SVGs (9 marks + wordmarks) under site/public/brand/ and regenerates the full favicon/PWA icon set from the contained dark mark (mark-e-dot-dark.svg — the bare mark can vanish on light browser chrome): - favicon.svg (the dark mark), favicon.ico (48/32/16), favicon-96x96.png, apple-touch-icon.png (180), icon-192.png, icon-512.png. - SVGs optimised with a repo-root svgo.config.mjs that keeps viewBox / currentColor / a11y hooks and strips fixed dimensions; favicons rasterised via ImageMagick (RSVG) from a 1024 master. Recipe in brand/README.md. Fixes the PWA manifest: its icon `src`s were absolute (`/web-app-manifest-…`) which 404 under the `/ExoJS/` base — now relative (`icon-192.png` / `icon-512.png`), resolved against the manifest URL. Removes the two orphan web-app-manifest PNGs. The <link> tags in AppShell.astro already pointed at these filenames, so no head change is needed. Co-authored-by: Exoridus <github@codexo.de>
* fix(site): derive api subsystem enum from the single source of truth The `subsystem` Zod enum in content.config.ts had drifted: it never gained `physics` (v0.14), `aseprite`, or `ldtk` (#199), but the generated API pages for those packages carry exactly those values. That broke the whole Astro build (dev, build, and check) on the branch — `astro dev` failed validating aseprite-format-error.mdx, with every physics page queued behind it. api-reference.ts already lists all subsystems in API_SUBSYSTEM_ORDER (order + labels). Derive the validation enum from it so the schema can never fall out of sync with the package list again. * fix(site): stop prose styles bleeding into guide chrome boxes The chapter layout scoped its typographic rules (h2/h3/p/li/links) to .chapter-prose, which wraps the whole article — including the GuideMeta boxes ("What you'll learn" / "Before you start"). Those boxes' <h2> headings inherited the prose-h2 styling: a 56px top margin, top padding, and a "#" ::before glyph, leaving a large empty gap and a stray "#" in each box; <li> picked up an extra margin, loosening the bullets. Scope the prose rules to .chapter-content (the rendered MDX body) instead. GuideMeta sits outside it, so the bleed is gone — specificity-proof, without touching GuideMeta.astro. The MDX body, lead, related-api, and pager are unaffected. Also bump the TryIt list gap from 4px to 8px so links that wrap to two lines stay visually distinct. --------- Co-authored-by: Exoridus <github@codexo.de>
Adds the Pocket Relic Bot as a waving corner greeter in the top-right of the landing hero, peeking over the code panel. The source render (companion-hero.png, flat magenta key) is run through the project's magenta→alpha chroma-key + despill, resized to 600px, and shipped as a 36KB WebP in site/public/brand/. - Decorative (aria-hidden); the headline carries the meaning. - Gentle float/bob loop, disabled under prefers-reduced-motion. - Anchored to the centered hero content so it tracks the panel on wide screens. - Hidden below 1000px, where the hero stacks to one column. Co-authored-by: Exoridus <github@codexo.de>
Code blocks always render on the dark `--color-code-bg` surface (dark in both themes), but the syntax colour was bridged to the site's data-theme: `html[data-theme='light']` forced `--shiki-light`, the light-on-light variant — dark text on the dark panel, i.e. unreadable in the light theme. This hit every code surface: the home hero/quickstart panels, guide MDX blocks, and snippets. Pin the syntax colours to `--shiki-dark` regardless of site theme, matching the always-dark surface. Dual-theme `<Code>`/markdown output still defines the var. Co-authored-by: Exoridus <github@codexo.de>
#207) The "On this page" rail used an IntersectionObserver with a narrow `-20% 0px -65% 0px` band: a heading only went active while inside the top 20–35% of the viewport. Headings near the page bottom never scroll high enough to enter that band, so the final section(s) never activated — at the very bottom the rail stayed stuck on whichever heading was last in the band (often the first). Replace it with a scroll-position computation: the active heading is the last one whose top has crossed a line ~28% down the viewport, and the last heading is forced once scrolled to the page bottom. rAF-throttled scroll/resize listeners. Verified via Playwright at top / middle / bottom — correct heading active in each, exactly one active at a time. Co-authored-by: Exoridus <github@codexo.de>
`onChange={value => ...}` left `value` implicitly `any` — the only error
`astro check` reported on the site. Annotate it `string | undefined` (the
@monaco-editor/react OnChange signature). No behaviour change; `astro check`
is now clean.
Co-authored-by: Exoridus <github@codexo.de>
Lands five package guides into the guide IA, each with API-verified code blocks:
- assets/aseprite, assets/ldtk — new chapters in the Assets part
- physics/physics-basics, physics/joints-and-dynamics — new Physics part
- integrations/react — new Integrations part
guide-structure.ts gains the two new parts and the two Assets chapters with
levels, learning goals, prerequisites, and API links (all resolving to existing
API pages). tsconfig.guides.json gains source paths for @codexo/exojs-{physics,
aseprite,ldtk} (+/register for the two extensions) so the extracted snippets
type-check against engine source.
Verified: typecheck:guides green (82 snippets), guide-structure reconciliation
test 19/19, and the new pages render (HTTP 200, sidebar shows Physics +
Integrations, no console errors).
Co-authored-by: Exoridus <github@codexo.de>
Reworks the guide Callout into the eight Pocket Relic Bot states from the companion design brief: info, hint, warning, pitfall, success, debug, invalid, broken. Each callout now pairs the painterly companion sprite (a trailing-edge badge with a state-coloured screen-glow) with a state-coloured glyph + label — glyph and label carry the meaning, so the callout still reads via shape and text, not colour alone. - 8 transparent companion sprites keyed + optimised to ~6 KB WebP each in site/public/brand/companion/. - Colour exclusivity per the brief: amber=warning, mint=success, gray=invalid, red=broken; info/hint/pitfall/debug share the companion cyan and are told apart by glyph. State colours are fixed, so they stay stable across the user-selectable accent themes. - Migrates the existing usages to the new states: common-mistake→pitfall, tip→hint, and the two browser callouts → info / warning by meaning. Verified all eight states render cleanly in light and dark (Playwright), with no sprite halo on either theme; astro check 0 errors. The right-rail "Need a hand?" helper card is a deliberate follow-up. Co-authored-by: Exoridus <github@codexo.de>
…#211) Follow-up to the 8-state callouts (#210). The first pass used the wrong, stale sprite source and rendered the companion too small. - Source: slice the 8 states from the production 8-state sheet (exojs-companion-12), which carries the finished faces + state glyphs — not the raw faceless bodies. - Crop to the upper body (head + torso, where the expression lives) and seat it large on the callout's bottom edge, legs clipped — matching the mockup scale. - Flip the companion to face inward toward the text; pitfall and debug keep their native orientation so their ?/[ ] face glyphs don't mirror. - Swap the weak text-in-thin-circle glyph for proper Lucide-style SVG icons in a soft state-tinted chip; sentence-case titles. Verified all eight states light + dark and side-by-side against the canonical mockup (scale / upper-body crop / inward orientation now match); astro check 0. Co-authored-by: Exoridus <github@codexo.de>
Monaco only registered core @codexo/exojs typings, so every extension example reported ts2307 'Cannot find module'. Emit a per-extension typings manifest + monaco-registry (virtual package.json, subpath shims, '#'-import rewrite) in the vendor sync, and load them in EditorCode alongside core.
The iframe canvas lives in the iframe realm, so 'instanceof HTMLCanvasElement' (parent realm) was always false and the --canvas-w/h/zoom sizing never ran. Use a realm-safe null check, and scale the canvas to the real panel width instead of leaving it at native size.
Replace the CSS text placeholder ('e' tile + 'ExoJS' text) with the inline, theme-aware exo.js wordmark (currentColor + var(--accent)).
Clicking a TOC link jumps the heading to the top, which the scroll heuristic alone could attribute to the next section. Add a click handler that highlights the clicked link immediately.
…ompanion Animated example thumbnails (new ExampleThumb), hero grid-drift/glow + cap-strip pulse, example cards now link straight to the playground, a shorter tween-based hero snippet, and the companion mascot moves into the live-example section.
|
To use Codex here, create an environment for this repo. |
The particles package imports '@codexo/exojs/renderer-sdk' (RenderBackendType) at runtime, but the entry was missing from preview.html's import-map, so every particles example threw 'Failed to resolve module specifier'. Added the local + CDN entries.
…typecheck exojs-ldtk pulls in exojs-tilemap source (via the @codexo/exojs-tilemap path), which imports @codexo/exojs/renderer-sdk. ldtk's tsconfig lacked that mapping (tilemap's has it), so the source-condition typecheck fell back to node resolution: green locally with a built dist, TS2307 on CI (no dist), cascading into ~25 follow-on errors. Mirror tilemap's path mappings.
* fix(examples): batch performance-overlay sprites via a Container The overlay rendered 1600 sprites with one context.render() per sprite, emitting one draw call each. Adding them to a Container and rendering it once batches them into a single draw call (7.8ms -> 1.8ms on the spike). * test(rendering): add cross-call sprite batching regression test (red) * perf(rendering): frame-scoped draw-plan lifecycle for cross-call batching setView flushes only on real view change; the transform buffer resets once per frame; the plan builder bases node indices at the frame buffer count; nested plans isolate their rows. Per-call renders now batch (1000 -> 1 draw). Leaves the barrier-path allocation gate red — fixed in the next commit. * perf(rendering): delta-upload transform texture rows per flush A frame-scoped buffer made barrier flushes re-upload a growing buffer (O(N^2)). Uploading only [uploadedRows, count) per flush via commitRect makes it O(N) while keeping the cross-frame hash-guard skip. Fixes the effect-barrier gate. * test(rendering): per-call render output matches Container render * fix(rendering): upload exact dirty transform-row range, not a high-water mark Task 3's delta upload tracked only the highest uploaded row, so a slot reused below that mark (a filter composite reusing a row a nested plan had rewound) was never re-uploaded, leaving stale transform data — the filter-boundary browser test rendered the wrong color. Track the exact written-slot range [dirtyMin, dirtyMax] in TransformBuffer instead; the delta upload pushes precisely the changed rows regardless of reuse. Restores filter-boundary (browser 149/149), keeps effect-barrier under budget and cross-call batching. * perf(rendering): WebGPU parity for frame-scoped cross-call batching Mirror the WebGl2 backend's Tasks 2-4 lifecycle changes onto WebGPU: - TransformBuffer is now frame-scoped (reset in resetStats, not per plan) - Add transformBufferCount getter so RenderPlanBuilder offsets node indices correctly for WebGPU (previously fell back to 0 -> no cross-call batching) - _beginDrawPlan: push base/hash stacks instead of resetting; reserve is based on frame-global count + plan nodes to avoid mid-frame reallocations - _endDrawPlan: pop stacks; nested plans flush + rewindTo to free their rows - setView: conditional flush (only on real view change) to stop breaking batches on every render() call that re-applies the same camera view - WebGpuTransformStorage.getBuffer: delta upload via consumeDirtyRange instead of full-buffer writeBuffer on every flush boundary * fix(webgpu): full re-upload after storage-buffer grow; remove dead begin() After _growBuffer creates a new empty GPUBuffer, set _needsFullUpload=true. In getBuffer, always consumeDirtyRange first (clears stale range), then branch: full [0,count) upload when _needsFullUpload, else delta rowCount>0. Mirrors WebGl2's full-upload-on-grow so mid-frame reallocated slots are never read as uninitialized transforms by the shader. Also removes the dead begin(nodeCount) wrapper — callers use buffer.begin() directly. * fix(webgpu): restore begin() wrapper + fix RT-display test for frame-scoped slots The prior commit removed WebGpuTransformStorage.begin() as dead code, but it has ~25 test call sites (30 tests broke). Restore it. Also the webgpu-backend RenderTexture+Sprite test asserted the sprite transform in slot 0, but with frame-scoped batching the graphics-into-RT is slot 0 and the sprite lands in slot 1 — read slot 1 (transform verified: tx=24, ty=18 present after the full-upload-on-grow). Full exojs project green (2510); no other regressions. Process note: the exojs unit project (test/**) was not run during the earlier tasks — only rendering-perf + browser-webgl; this surfaced both issues. * chore(rendering): fix import order + prettier formatting Autofix: sort the playRenderTree import in the perf harness; prettier-format WebGpuTransformStorage after the begin() restore. verify:quick green. * test(particles): seed frame-scoped batching stacks in WebGpuBackend mock The particle GPU-injection test mocks the backend via Object.create(prototype), bypassing the constructor that initializes _planBaseStack/_planHashStack (used by _beginDrawPlan since the cross-call batching work). Seed them like the existing device mock. Full test suite green (3609). * docs(rendering): update stale comments + add TransformBuffer dirty-range tests - RenderingContext: setView flushes only on view change (not unconditionally); correctness rests on trailing flush() and renderer-switch flushes - RenderInstruction: nodeIndex is frame-global [frameBase, frameBase+nodeCount), not plan-local [0, nodeCount) - WebGpuTransformStorage: clarify consumeDirtyRange is inside the upload branch only; add upload-guard note explaining why a skipped flush is safe - WebGl2Backend: same upload-guard safety note as WebGpu counterpart - test: 6 new TransformBuffer dirty-range cases (consumeDirtyRange sentinel, coverage+self-clearing, below-HWM reuse, clamping, rewindTo, begin reset) --------- Co-authored-by: Exoridus <github@codexo.de>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v0.15 — feature release
Merges the full
feat/v0.15line intomain(77 commits). Highlights by area:Physics (
@codexo/exojs-physics)drawSleepingdebugCore & rendering
Scene.fixedUpdate/onFixedFrame/frameAlphaMeshMaterial.from()/SpriteMaterial.from()factories;SceneNode.cullAreaReact / Aseprite / LDtk
ExoCanvasbatteries-included wrapper; React Testing Library suite (jsdom)create:packagescaffolder; lockstep release wiringDocs site & playground
exo.jsheader wordmark#-subpath type fix, extension typings (ts2307) resolved, panel-fill + cross-realm canvas detection, light-theme code contrastNotes
main, so all the docs-site polish (fix(site): rewrite '#'-subpath imports in playground vendor types (P0) #202–fix(site): rework companion callout badges to match the mockup #211 + this branch tip) only goes live once this merges.verify:quick(typecheck core+guides+examples+packages,lint:all,format:check,docs:api:check) passed locally.