Willow Changelog

Release history and version notes for Willow.

Visibility Decoupled from Logic

Added
  • `node.SetBodyEnabled(bool)` / `node.BodyEnabled()` pause a physics body without releasing it.
  • Removes the body and its shape from the cp space; the `*PhysicsBody` wrapper stays attached to the node.
  • Position, rotation, velocity, mass, moment, friction, elasticity, and shape geometry are preserved across the toggle.
  • Re-enabling adds the body back to the same space and it resumes from the preserved state.
  • Distinct from `RemoveBody`, which releases the body back to the pool and clears `node.Body` to nil.
  • `EmitterConfig.SimulateWhileHidden` opts a particle emitter out of the default visibility pause.
  • Default false: emitters tick only when their node (and every ancestor) is visible.
  • Set true for effects whose state must persist while briefly off-screen (long smoke trails, etc).
Changed
  • `SetVisible(false)` now only pauses rendering and hit-testing.
  • `OnUpdate` callbacks, tweens, physics simulation, and world-transform recomputation continue to run on hidden subtrees.
  • Gameplay logic on hidden nodes sees up-to-date world coordinates, and physics bodies keep simulating in their parent space.
  • To pause a single body, use `SetBodyEnabled(false)`. To pause an entire subtree, detach with `RemoveFromParent()` and re-attach later.
  • Particle emitters still pause by default when invisible; opt out via `SimulateWhileHidden`.
Fixed
  • World transforms now recompute on invisible subtrees.
  • Previously, the transform walk early-returned on invisible nodes, leaving stale `WorldTransform` matrices for the whole subtree.
  • Physics-driven bodies on hidden nodes could drift because the parent inverse used to map world space back to local space was stale.
  • `UpdateWorldTransform` now recurses through invisible subtrees; the render and hit-test paths apply their own visibility gate so hidden nodes still do not draw or receive input.
  • `PhysicsParent.RemoveBody` is now idempotent: removing an already-detached body is a safe no-op. This lets `DisablePhysics` tear down subtrees containing a mix of enabled and disabled bodies without panicking.
  • New `Node` instances initialize `WorldTransform` to the identity matrix instead of a degenerate zero matrix, so spatial queries on a never-walked node return sensible values.

Pointer Screen Coords

Fixed
  • Pointer state now tracks the last screen-space coordinates of the cursor in addition to world-space. Drag and hover handlers that read from screen space no longer see stale values when the camera moves between frames.

Fullscreen Logical Sizing & Multi-Camera

Added
  • New example: `examples/demos/multi-camera/` showing a split-screen setup with two cameras and an F11 fullscreen toggle.
  • New example: `examples/demos/fullscreen-camera/` rendering a logical-center sprite plus edge markers to prove letterbox alignment under fullscreen.
Changed
  • Consolidated `gameShell.Layout` and `managerShell.Layout` into a single `gameBase.Layout`.
  • The scene-manager path now resolves the current scene per-Layout call, so scene switches between layouts are picked up correctly.
  • `Scene.OnResize` fires exactly once on the first `Layout` call under the new contract.
Fixed
  • Fullscreen runs honor `RunConfig.Width` / `RunConfig.Height` for logical screen sizing.
  • Previously, `Layout` returned the OS-supplied `outsideWidth` / `outsideHeight` in fullscreen, ignoring the configured logical size.
  • Cameras whose viewports were defined in `cfg.Width` / `cfg.Height` coordinates rendered into the wrong region and appeared mis-aligned against the letterboxed offscreen image.
  • `gameBase.Layout` now returns the configured logical size unconditionally; the letterbox math in the runtime handles the physical-to-logical mapping.

Optional 2D Physics

Added
  • Optional 2D rigid-body physics integrated via Chipmunk (`jakecoffman/cp/v2`). Enable on a subtree with `node.EnablePhysics`, attach bodies with `node.SetBody`, and per-frame world transforms write back automatically inside `Scene.Update`.
  • Body kinds: `PhysicsDynamic`, `PhysicsStatic`, `PhysicsKinematic`.
  • Shape descriptors: `PhysicsCircle`, `PhysicsBox`, `PhysicsSegment`, `PhysicsPolygon`.
  • Per-subtree isolated `cp.Space`. Sibling roots run independently; nesting panics.
  • Manual stepping via `node.StepPhysics(dt)` (suppresses the auto-tick for that root for the rest of the frame).
  • Each `*PhysicsBody` embeds `*cp.Body`, so the full Chipmunk verb surface (`ApplyImpulseAtLocalPoint`, `SetVelocityVector`, ...) is available without a wrapper.
  • Body+shape pooling by kind, internal to `SetBody` / `RemoveBody`.
  • Velocities, forces, torque, user data, sensor flag, collision type, and filter cleared on release.
  • Mass, moment, friction, elasticity, and shape geometry reset on acquire from the new `BodyDef`.
  • Bench: 96 B / 2 allocs/op pooled vs 648 B / 6 allocs/op unpooled on a tight spawn loop.
  • `-tags nophysics` build tag strips the Chipmunk dependency from the binary entirely (~500 KB delta).
  • Stub replacements keep the public surface compiling. Calling any physics method under `nophysics` panics.
  • Re-parenting a bodied node into or out of a physics subtree is reconciled transparently on the next tick.
  • New example: `examples/demos/physics/` uses the `willow.Physics*` shim.
  • New example: `examples/demos/physics_handrolled/` drives `cp/v2` directly, kept as a reference and visual-parity regression target.
  • New tool: `cmd/physics-parity/` diffs labelled screenshots from the two demos with a configurable pixel threshold.
  • New example: `examples/demos/pivot-percent/` showcases pivot mechanics (top-left default, centered explicit, percent-anchored).
  • `node.SetPivotPercent(fx, fy)` fractional pivot helper.
  • Sets pivot to `(fx*Width(), fy*Height())`. Works uniformly for white-pixel and textured sprites.
  • `SetPivotPercent(0.5, 0.5)` always centers, regardless of how the sprite was sized.
  • New docs page: Physics under "Systems".
Fixed
  • White-pixel sprite pivot was silently broken when combined with `SetSize`.
  • Previously, white-pixel sprites stored their display size inside `Scale` (1×1 source × `Scale` = the visible quad).
  • `SetPivot` is multiplied by `Scale` internally, so `SetPivot(32, 48)` on a `SetSize(64, 96)` white-pixel sprite produced a pivot of `(2048, 4608)` instead of `(32, 48)`.
  • That junk pivot leaked into `WorldTransform`, breaking `Camera.Follow` and yanking the whole scene off-screen.
  • Fixed by moving white-pixel size into dedicated geometry fields (mirroring Starling-style quads). Pivot is now in display-pixel space and behaves identically to textured sprites.

Custom Render Hooks

Added
  • `Node.CustomPaint` is now a usable extension point. Install a typed handler with `willow.SetCustomPaint(node, fn)` and append render commands that participate in normal sort, cull, and batching.
  • New `willow.Painter` facade with `AppendCommand`, `AppendTriangles`, `PaintDefault`, `Node`, `ViewTransform`, `WorldTransform`, `CullBounds`, `IsBuildingCache`, and `Pipeline` accessors.
  • New `willow.TrianglesPaint` helper struct for submitting batched triangle commands without hand-constructing a `RenderCommand`.
  • Custom-paint handlers cooperate with `CacheAsTree`: appended commands are attributed to the host node during cache builds and invalidate correctly.
  • `willow.Pipeline` re-exported as an unstable escape hatch for cases the Painter facade does not cover.
  • Engine-internal: any field, method, or behavior may change between minor versions without notice.
  • Reach for it via `p.Pipeline()` only when the stable Painter surface is missing what you need.
  • New example: `examples/demos/custompaint/` renders a procedural starburst as a single batched mesh.
  • New docs pages: Custom Painter and Pipeline Internals under "Extending Willow".
Changed
  • The `any` parameter passed to `Node.CustomPaint` is a `*Painter`. The field signature is unchanged from previous internal use; external users go through `willow.SetCustomPaint`.

Filter Pipeline Overhaul

Changed
  • Replaced the downscale/upscale blur with an iterative multi-pass shader operating at full resolution.
  • Multi-pass via the new `MultiPass` interface — the blur filter owns zero temporary images.
  • The render pipeline drives all ping-pong externally using its existing pooled scratch buffers.
  • ~37% faster CPU-side, eliminates atlas thrash from varying-size temp images.
  • Replaced the 9-DrawImage outline stamp with shader-based outlines.
  • Thickness 1–6: single-pass circular distance shader with anti-aliased edges (5x faster).
  • Thickness 7+: two-pass separable expansion via `MultiPass`, scaling linearly instead of quadratically (2.3x faster).
  • True circular outline shape replaces the old octagonal approximation.
  • Per-pixel filters now skip the offscreen render target on leaf sprites.
  • `ColorMatrixFilter` and `CustomDrawShaderFilter` apply directly at draw time via `DrawTrianglesShader`.
  • Consecutive sprites sharing the same filter instance batch into a single draw call.
  • Activates automatically for untinted leaf sprites with a single per-pixel filter.
  • Simplified the palette shader to use `imageSrc1Size()` for coordinate mapping, removing the `PaletteSize` and `TexWidth` uniforms.
  • Uniform buffers in `PaletteFilter` now use persistent `float32` slices instead of boxing bare values into `any` each frame.
Added
  • Added `MultiPass` interface for filters that need multiple shader passes.
  • Filters declare pass count via `Passes()` and configure per-pass state via `SetPass(i)`.
  • The pipeline runs N iterations of the existing ping-pong loop — filters own zero images.
  • Added `DrawFilter` interface for per-pixel filters that can render at draw time.
  • `DrawShader()`, `DrawUniforms()`, `DrawImages()` provide the shader and state.
  • Pipeline detects eligible nodes and emits inline sprite commands with the filter shader.
  • Added `CustomDrawShaderFilter` for user-provided per-pixel Kage shaders that skip the offscreen RT.
  • Added shader sprite batching with dedicated `ShaderVerts`/`ShaderInds` buffers and `uint16` overflow guard at 65532 vertices.
  • Added benchmark tests for blur (old vs new) and outline (old vs new) at multiple sizes and radii.
  • Added visual regression autotest for the filter gallery demo covering all 9 built-in filters.
Fixed
  • Fixed a pool image leak in `ApplyFiltersAny` when even swap counts left the scratch image unreleased.
  • Fixed `DirectImage` sprites rendering as invisible through the draw-time filter path (empty `TextureRegion` produced a zero-area quad).
  • Fixed draw-time filter path losing node color tint (tinted sprites now fall back to the offscreen RT where tint is baked in first).

GIF Recorder, Autotest Keys & Tilemap Helpers

Added
  • Built a GIF recorder so you can capture animated demos straight from the engine canvas.
  • Median-cut palette generation (`MaxColors`), transparent diff frames, similar-frame deduplication.
  • Frame dropping (`DropEvery`), resolution downscaling (`MaxWidth`/`MaxHeight`), region capture (`X`, `Y`, `Width`, `Height`, `Inset`).
  • Configurable FPS (default 30). All optimizations toggleable via `NoDedup`, `NoDiff`, `NoDownscale`.
  • Added `start_gif` / `stop_gif` test actions so GIF recording can be driven entirely from autotest scripts.
  • Added `scene.IsKeyPressed(key)` so autotests can check keyboard state the same way gameplay code does.
  • Works with real keyboard, held keys, and injected keys for fully data-driven test scripts.
  • Added keyboard actions to JSON test scripts for simulating held keys and quick presses.
  • `key_down` / `key_up` hold keys across frames. `key` does a quick press with auto-release after 2 frames.
  • Added tilemap helpers to reduce boilerplate when building tile layers from grid-based tilesets.
  • `RegionsFromGrid` builds a `[]TextureRegion` from tileset grid geometry.
  • `EncodeGID` combines a tile ID with flip flags. `TileFlipH`, `TileFlipV`, `TileFlipD`, `TileFlagMask` constants exported.

Scene Management, Animation & Camera Shake

Added
  • Added a SceneManager for navigating between scenes without rebuilding everything from scratch.
  • Stack-based push/pop/replace with visual transitions (FadeTransition included).
  • Added AnimationPlayer for managing named sprite frame animation sequences.
  • Added camera shake with a trauma system that decays over time, so impacts feel physical.
  • Exposed gameplay data that was previously locked inside the engine.
  • Nodes: `WorldPosition`, `WorldBounds`, `GetWorldAlpha`.
  • Scenes: `PointerPosition`, `IsPointerDown`.
  • Cameras: `ViewMatrix`, `InverseViewMatrix`.
  • Spatial helpers: `DistanceBetween`, `DirectionBetween`.
  • Added FXAA post-processing for smoother edges, with improved SDF font rendering.
  • Added `Scene.OnResize` callback so UIs can respond to window resize events.
  • Added `InjectHover` for simulating pointer movement without a button press in tests.
  • Opened up the tilemap data access API so gameplay systems can query tile data without reaching into internals.
Changed
  • Replaced the legacy font system with a modular `fontgen` package that handles both SDF and bitmap fonts.
  • Built-in shaders now compile eagerly at init time, avoiding first-frame stalls.
  • Removed a redundant `UpdateWorldTransform` pass and repacked the Node struct for better cache locality.

Internal Architecture Refactor

Changed
  • Reorganized engine internals into `internal/` packages to keep the codebase maintainable as it grows.
  • Public API is unchanged; all types remain aliased at the root package.
  • Renamed `SpriteFont` to `DistanceFieldFont` to better reflect what it actually does.

Fonts, Atlas & Node Lookup

Added
  • Added two text rendering systems so you can pick the right one for your art style.
  • `DistanceFieldFont` renders SDF/MSDF fonts from TTF files for resolution-independent text.
  • `PixelFont` renders bitmap fonts with pixel-perfect crispness, word wrap, and alignment.
  • Added `NodeIndex` for fast node lookups by name or tag, with wildcard query support.
  • Added a dynamic texture atlas with shelf-based bin packing for runtime atlas construction.
  • `DynamicAtlas` supports a staging/flush workflow for batch updates.
  • Shipped `atlaspack` and `fontgen` CLI tools for generating sprite and font atlases offline.
  • Added `AntiAlias` option on `DrawTriangles` for smoother geometry edges.
  • Re-exported all easing functions at the package root so you do not need to import `gween/ease` separately.
Changed
  • Simplified `TweenConfig` to accept duration and easing together instead of as separate arguments.

TileMap Support

Added
  • Added tilemap rendering so large tile-based worlds can scroll smoothly with the camera.
  • `TileMapViewport` and `TileMapLayer` handle viewport culling and per-tile rendering.
Changed
  • Renamed `MarkDirty` to `Invalidate` for consistency with the rest of the API.

Render Pipeline Optimizations

Added
  • Added `CacheAsTree` so static subtrees can skip re-traversal entirely.
  • Manual and auto cache modes let you choose between explicit invalidation and automatic dirty tracking.
Changed
  • Coalesced sprite batching now merges draw calls for nodes sharing a texture atlas, reducing GPU overhead.
  • Switched color and transform fields to `float32`, cutting per-node memory by roughly 25%.
  • Invisible nodes now skip world-transform updates entirely instead of computing transforms that are never used.

Clipping & Masking

Added
  • Added clipping containers so panels can scissor their children without offscreen render passes.
  • Added erase mask compositing for cutout and reveal effects.
  • Light rendering now caches resolved images and transforms, reducing per-frame GPU work.

Initial Release

Added
  • First release of the retained-mode scene graph and display tree for Ebitengine.