diff --git a/packages/examples/LICENSE.md b/packages/examples/LICENSE.md index 9b5329a0a..45d13c0f4 100644 --- a/packages/examples/LICENSE.md +++ b/packages/examples/LICENSE.md @@ -51,6 +51,12 @@ courtesy to the original creator. Spacecraft 3D models (`craft_speederA`, `craft_speederB`, `craft_racer`, `craft_miner`) in `public/assets/multiMaterialMesh/` are taken from +The Water Overworld example (`waterOverworld/`) uses the +**"Free Pixel Art Side Scroller Asset Pack (32x32) Overworld"** published by +**GandalfHardcore**, free to use: + + + **"Space Kit (2.0)"** published by Kenney: diff --git a/packages/examples/src/examples/waterOverworld/entities.ts b/packages/examples/src/examples/waterOverworld/entities.ts index b89d72a4d..366c82aaa 100644 --- a/packages/examples/src/examples/waterOverworld/entities.ts +++ b/packages/examples/src/examples/waterOverworld/entities.ts @@ -2,10 +2,6 @@ * melonJS — Water Overworld example (shader builtins showcase). * Copyright (C) 2011 - 2026 AltByte Pte Ltd — MIT License. * See `packages/examples/LICENSE.md` for full license + asset credits. - * - * Scene and water shader contributed by the melonJS editor team; pixel art - * from GandalfHardcore's free 32x32 side-scroller asset pack (itch.io, - * free to reuse). */ import * as me from "melonjs"; @@ -315,8 +311,6 @@ export class Male extends me.Sprite { * - `screen_uv` — this fragment's position in that capture * - `noise_uv` — 0..1 across the water sprite itself, so the seamless * noise textures tile independently of where the frame sits in the atlas - * - * Shader and tuning by the melonJS editor team (ported verbatim). */ export class WaterTextureObj extends me.Sprite { private water: me.ShaderEffect; diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 4033d46ea..909aedc59 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -1,66 +1,47 @@ # Changelog -## [19.9.0] (melonJS 2) - _unreleased_ +## [19.9.0] (melonJS 2) - _2026-07-14_ + +**Highlights:** shader effects made easy. Effects that used to demand WebGL expertise, like a pond rippling with the scene reflected in it, heat haze, or frosted glass, now take a few lines of shader code: the engine hands your effect the screen behind it and the right coordinates, animated noise textures come built-in, and shaders preload like any other asset. See the new **Water Overworld** example for all of it in action. Also in this release: named anchor presets (`"bottom"`, `"top-left"`, and friends) on every renderable, shapes with holes in `Path2D`/SVG fills, and a 40+ bug-fix sweep across the loader, audio, texture atlas, and WebGL rendering. ### Added -- **`renderer.toFrameTexture()` — GPU frame capture as a `Texture2d`** (closes [#1544](https://github.com/melonjs/melonJS/issues/1544)) — capture the current camera view / framebuffer into a `Texture2d` entirely on the GPU, for screen-space effects (water refraction, heat haze, frosted glass) that need to sample "what's on screen behind me". The fourth member of the `toDataURL` / `toBlob` / `toImageBitmap` family — "the current frame as X" — and the only one whose result never leaves the GPU: a `copyTexImage2D` of the bound framebuffer, ~10–100× cheaper than a `readPixels` round-trip and with zero CPU⇄GPU stall. The capture reflects the framebuffer **at the call site** (a `flush()` runs first — the classic back-buffer-copy placement), reads whichever framebuffer is bound (backbuffer or a camera post-effect FBO, so it works under `Camera2d` and `Camera3d`), and returns a `Texture2d` you feed straight to a custom effect: `effect.setTexture("uScene", renderer.toFrameTexture())`. `setTexture` now binds such a GPU-resident capture as a **live** handle — re-capturing each frame into the shared slot refreshes what the shader samples with no re-bind. Options: `target` (reuse/mint a caller-owned capture) and `region` (sub-region copy). Color only (RGB, opaque); the Canvas renderer ships an offscreen-canvas equivalent for family parity. See the new **Aquarium** example (fish rendered to the scene, refracted through a rippling water surface that samples the captured frame). -- **BMFont XML font support for `BitmapText`** — bitmap fonts can now be loaded from the AngelCode BMFont **XML** flavour in addition to the text `.fnt` format (the serialisation is auto-detected). Many bitmap-font tools and asset packs (itch.io, dafont, …) export PNG + XML; those now load directly through the standard preloader (`{ type: "binary" }`), with no offline conversion step. The XML is parsed with a dependency-free regex parser (no `DOMParser`), so it also works outside the browser (Node / SSR) — unlike the DOM-based TMX XML path. -- **Holes & compound paths in `Path2D` / SVG fills** (#1253) — a path may now contain multiple sub-paths (every `M` in an SVG string, or every `moveTo()` call, starts a new one). On `fill()` the first sub-path is the outer contour and each subsequent sub-path is treated as a hole, so donuts, rings, letter counters ("O", "A", "8") and other shapes with holes render correctly under both the WebGL renderer (earcut hole triangulation) and the Canvas renderer (native winding rule). Method semantics follow the standard [`Path2D`](https://developer.mozilla.org/en-US/docs/Web/API/Path2D) API; see the updated **SVG Shapes** example. -- **`Texture2d` base class for texture assets** — a new abstract base (`me.Texture2d`) for user-constructed texture objects that own a texture source, exposed via `getTexture()`. `TextureAtlas` now extends it (no behavior change), and the renderables (`Sprite`, `Sprite3d`, `Mesh`) recognize any `Texture2d` by type and resolve it to its backing source — so a texture asset can be passed directly (`{ image: myTexture }`) just like a raw canvas. Raw DOM images/canvases and the loader's decoded `CompressedImage` data remain valid sources outside the class hierarchy. The contract deliberately admits both CPU-backed sources (a drawable canvas/image) and **GPU-resident** ones (an opaque backing that never leaves the GPU) — the shape a future WebGPU backend and screen-capture textures follow. -- **Procedural noise: `Noise` + `NoiseTexture2d`** — `me.Noise` is a renderer-free coherent-noise generator (simplex / perlin / value / valueCubic / cellular, with fBm, ridged and ping-pong fractal layering, plus optional domain warp) that you can sample on the CPU via `getNoise2d` / `getNoise3d` for gameplay (heightmaps, terrain, spawn jitter) — fully deterministic per `seed`. `me.NoiseTexture2d` is a `Texture2d` that bakes a `Noise` field into a drawable canvas, with three output modes: grayscale, a `Gradient` **color ramp**, or a tangent-space **normal map** (`asNormalMap` + `bumpStrength`) for per-pixel lighting; optional `seamless` tiling (`seamlessBlendSkirt`); and **live animation** (`animated` + `speed` — sampled in 3D, advanced by `update(dt)`). Each re-bake bumps a content `version` stamped on the canvas; the WebGL lit pipeline re-uploads the normal-map texture only when that version changes (the three.js `Texture.needsUpdate` model — no renderer handle needed). Both work under the Canvas and WebGL backends. -- **`shared` flag on `ShaderEffect` / `GLShader`** — set `effect.shared = true` on a post-processing shader that is reused across several renderables, so one renderable's cleanup (the `shader` setter, `removePostEffect`, `clearPostEffects`, or `destroy()`) doesn't free the GL program the others still use. Defaults to `false` (unchanged auto-destroy behavior); when set, you own the shader's lifecycle and call `destroy()` yourself. -- **`ShaderEffect.setTime(seconds)`** — a convenience for the `uTime` shader-animation convention, now on the base class: call it once per frame to write a shader's `uniform float uTime` (e.g. to scroll a static texture's UVs, or pulse / wave), driven by whatever clock you choose. A no-op when the shader doesn't declare `uTime`, so it's safe on any effect. Manual by design — the engine never calls it, so animation stays opt-in. The built-in `Hologram` / `Shine` effects now inherit it (their identical `setTime` overrides were removed). -- **`ShaderEffect.setTexture(name, image[, repeat])`** (closes [#1532](https://github.com/melonjs/melonJS/issues/1532)) — bind an **extra** `sampler2D` to a custom post-effect, beyond the sprite/target it processes (`uSampler`) — a noise map, mask, gradient, or flow table. Declare `uniform sampler2D ;` in your fragment and pass any engine texture — a `Texture2d` asset (`NoiseTexture2d`, `TextureAtlas`, …) directly, or a raw drawable source; the engine uploads it once, caches it, and (re)binds it to a reserved texture unit each draw while pointing the sampler uniform at it — no raw WebGL texture-unit juggling. Works on both post-effect paths (single-effect sprite shader and the multi-effect / camera FBO blit). Enables UV-distortion water, dissolve masks, flow maps, palette lookups, etc. Purely additive — effects that never call it are unchanged. See the **Aquarium** example (a water surface refracting the live scene through a GPU-scrolled seamless `NoiseTexture2d`). -- **Anchor-point presets + `Sprite3d` anchor support** (#1514) — `settings.anchorPoint` now accepts the named presets `"center"`, `"top"`, `"bottom"`, `"left"`, `"right"`, `"top-left"`, `"top-right"`, `"bottom-left"`, `"bottom-right"` in addition to an `{x, y}` object, uniformly across `Sprite`, `Entity`, `Collectable`, `ImageLayer`, `Text`, `BitmapText` (and their subclasses), including as a plain-string Tiled property. `Sprite3d` gains anchoring through the same `settings.anchorPoint` key (centered default, same convention as `Sprite`): the anchor is baked into the quad's local vertices so it composes with billboarding, flips and trimmed/rotated atlas frames, never leaks into the renderer transform on either camera path, is mutable at runtime via `sprite3d.anchorPoint.set(...)` (automatic re-bake + cull-bounds update), and grows the frustum-cull bounds exactly so an anchored sprite can't pop out while still on screen. On the 2D classes invalid values (unknown preset, malformed object) keep their historical silent `(0, 0)` outcome for full backward compatibility, but now log a console warning; `Sprite3d` (a new surface) throws. See the updated billboard example (`anchorPoint: "bottom"` — feet on the ground plane). -- **Shader builtins: `screen_texture` / `screen_uv` / `noise_uv`** — three names now get special treatment inside a `ShaderEffect` fragment body, so screen-reading and noise-scrolling effects need zero JavaScript plumbing and no UV math (design contributed by the melonJS editor team). Annotate a sampler with `uniform sampler2D screenTex : screen_texture;` and the engine keeps it filled with a capture of **everything drawn so far** (a true back-buffer copy via the `toFrameTexture` shared slot, refreshed automatically when the effect draws — one screen copy per draw, only for effects that use it); an optional wrap is accepted (`: screen_texture(repeat)`, WebGL 2). Use `screen_uv` anywhere in the body for the fragment's 0..1 screen position, and `noise_uv` for a frame-local 0..1 coordinate across the drawn object — it undoes atlas packing (and is scaled to object pixels), so a seamless noise texture tiles correctly no matter where the sprite's frame sits in its atlas. Fully backward compatible: the annotation was never valid GLSL, the varyings only activate when referenced, shaders that declare their own `screen_uv`/`noise_uv` are left untouched, and a body using none of them compiles byte-identical to before. See the new **Water Overworld** example — a side-scroller pond that refracts the scene behind it in three lines of shader. -- **Shader preloading: `"shader"` asset type + `loader.getShader()` + `ShaderEffect.clone()`** — custom shaders can now be preloaded like any other asset and are **compiled at load time** (the GLSL compile cost lands in the loading screen, and a compile error fails the load — `loader.load()` rejects with an error carrying the asset's name). The source is a GLSL fragment body following the `ShaderEffect` convention (`uniform` declarations + `vec4 apply(vec4 color, vec2 uv)`), loaded from a `src` URL / data: URI or inline via the `data` field (the inline-TMX convention): `{ name: "waterRipple", type: "shader", src: "shaders/waterRipple.frag" }`. `loader.getShader(name)` returns the **shared, loader-owned instance** — the *same* object on every call, with `shared = true` so renderable cleanup never auto-destroys it; everything it is assigned to shares one set of uniform values, and it is freed only by `loader.unload()` / `loader.unloadAll()` (which destroy the GL program). For per-renderable uniform values, the new `ShaderEffect.clone()` compiles an independent, caller-owned copy: it copies the *recipe* (fragment source, precision, uniform values, `setTexture` bindings — with its own GL uploads) but never the *ownership* — the clone's `shared` flag is **always reset to `false`**, so it is auto-destroyed with the renderable it is assigned to unless explicitly re-shared. `GLShader.clone()` exists likewise for raw full-program (vertex + fragment) shaders, with the same recipe-not-ownership semantics. The **Aquarium** and **Heat Haze** examples ship their fragments as preloaded shader assets. A shader asset can also be a complete **`{vertex, fragment}` program pair** — `src: {vertex: url, fragment: url}` or inline via `data:` — which compiles into a shared raw **`GLShader`** instead (returned by the same `loader.getShader()`), for the paths that take one directly: a `Mesh` custom shader, `renderer.customShader`, a custom batcher. Same shared/clone/unload lifecycle; WebGL-only (`null` under Canvas — a raw program has no canvas analog). - -### Fixed -- **Asset loading failed over the `file:` protocol (Cordova/Capacitor APK on Android)** — `fetchData` uses `fetch()`, which several WebView contexts refuse for `file://` URLs (it rejects with a network error). The engine previously relied on `fetch` returning `status 0` for `file:` (added when the old `XMLHttpRequest` loader was removed), but that only helps where `fetch` *resolves* — a Cordova Android WebView rejects outright, so preloading broke. `fetchData` now falls back to `XMLHttpRequest` for `file:` URLs (detected via the URL scheme or `location.protocol`), which supports the scheme (a successful local read reports `status 0`); `http(s)` URLs are unchanged. Restores the pre-fetch behaviour for packaged builds. -- **`loader.unloadAll()` threw if any fontface was loaded, and never actually freed videos** — two copy-paste type strings: the video-cache sweep dispatched entries as `type: "json"` (the lookup missed, so video assets survived every `unloadAll`), and the font-cache sweep used `type: "font"` — an unknown type that hit the loader's `unload` error path and **aborted the whole `unloadAll` mid-way**. Both now route through their correct types (`"video"` / `"fontface"`). Also fixed an always-true `typeof typeof globalThis.document` guard in the fontface unload case. -- **Physics startup banner logged a minifier-mangled class name** — the `Application` console header derived the physics adapter label from `adapter.constructor.name`, which is renamed in minified builds, so the built-in adapter printed e.g. `physics: ry`. It now prefers the adapter's stable `physicLabel` before falling back to the class name, so the built-in adapter reads `physics: builtin` (external adapters that set `name` are unaffected). -- **`Text` generic CSS font families silently fell back to serif** — `setFont` quoted *every* family name, which turned a CSS generic keyword (`sans-serif`, `monospace`, `serif`, `system-ui`, …) into a quoted, nonexistent specific family, so the browser rendered its default serif instead. Generic families are now left unquoted; specific names (e.g. `"Arial"`, `"My Font"`) are still quoted so names with spaces keep working. -- **Animated `NineSliceSprite` collapsed to its frame size** (#1115) — a `NineSliceSprite` driven by an animation (or any per-frame texture change) had its user-specified "expanded" `width`/`height` overwritten by each frame's source dimensions, shrinking the 9-slice panel to a single frame after the first animation step. Static nine-slices were unaffected because the frame is applied only once (during construction, before the expanded size is set). `NineSliceSprite` now applies a new frame by swapping only the source sub-texture, leaving the expanded size and bounds intact. First-ever test coverage for `NineSliceSprite` (the class previously had none). -- **SVG `A`/`H`/`V` commands opening a new sub-path drew from the wrong origin** — the arc (`A`), horizontal-line (`H`) and vertical-line (`V`) parsers used the previous sub-path's last point as the current pen position instead of the point set by the preceding `M`. A circular hole authored with arcs (e.g. a donut) therefore degenerated into a spiral with a radial slit. They now track the pen position correctly. Latent since these commands were added; surfaced by the new multi-sub-path holes support (#1253). -- **2D sprites could silently vanish when using a large `pos.z` sort key** (regression since 19.7) — since the depth pipeline landed, a sprite's `depth`/`pos.z` is written into the vertex `z` and participates in clip-space. Under a 2D orthographic camera `pos.z` is only a painter's sort key (the world is CPU-sorted on it), so a large value — a `baseZ + pos.y` Y-sort, or `Container.autoDepth` on a big world — pushed the vertex past the camera far plane (`Camera2d.far`, 1e6) and the GPU clip-culled the sprite (it disappeared, or rendered incorrectly on some drivers). `Renderer.setDepth` now feeds depth into the vertex stream only under a perspective projection (`Camera3d`, which genuinely uses z for parallax/depth); orthographic cameras keep depth out of clip-space, so a sort key can never clip a sprite. `Camera3d` / mesh depth is unaffected. -- **`Mesh.textureRepeat` leaked its wrap mode to every consumer of the same image** (#1503) — the setting was applied by mutating the per-image `TextureAtlas` shared by everything drawing that image, so two meshes (or a mesh and a sprite/pattern) pointing at one image with different wrap needs were last-writer-wins: the loser silently sampled with the wrong wrap. The wrap now lives on the mesh and is threaded to the batcher at draw time — sampler state per use, the same separation modern GPU APIs enforce — and the texture-unit cache's `(source, repeat)` keying gives each wrap its own GL texture, so per-mesh wraps coexist on one image. Also fixed alongside: unloading an image now frees the texture units of **all** its wrap-mode variants — previously only the unit matching the atlas's current `repeat` field was freed, pinning the rest until a full cache reset. -- **`new Color("darkgray")` threw, and several CSS color keywords were wrong or missing** — the named-color table's `darkgray`/`darkgrey` keys were pasted from a spec table *with* their footnote marker (`"darkgray[*]"`), so looking either name up missed the table and threw `invalid parameter`; `cyan`, `magenta` and `rebeccapurple` were missing entirely (same throw); and `silver`, `aliceblue` and `burlywood` carried single-digit channel typos (e.g. `silver` was `rgb(192,192,129)` instead of `rgb(192,192,192)`). The full 148-keyword table is now validated against the CSS spec by a test, so a paste error can never hide again. -- **`Matrix2d` 6-argument (canvas convention) form dropped translation and transposed rotation** — `new Matrix2d(a, b, c, d, e, f)` / `setTransform(a, b, c, d, e, f)` stored its components row-major into the column-major layout: `e`/`f` landed in the unused projective row (so `new Matrix2d(1, 0, 0, 1, x, y)` produced the identity with **no translation at all**) and any rotation came out inverted. The 9-argument form was unaffected. -- **`moveTowards()` never reached its target — and returned the target instead of `this`** (all four vector classes) — the arrival test compared the remaining distance against `step * step` (wrong units: snapped way too early for steps above 1, dithered around the target forever for fractional steps), and on "arrival" it returned the **caller's target vector** without moving `this` at all — so the vector never landed, and chaining onto the result silently mutated the target. It now lands exactly on the target and always returns `this`, per its own documentation; negative-step flee semantics are unchanged. -- **Every preloaded video was fetched with forced anonymous CORS** — the video parser unconditionally stamped the `crossorigin` attribute with the loader's `crossOrigin` setting, which defaults to `undefined` — and per the HTML spec an *invalid* value (the string `"undefined"`) maps to `anonymous`, unlike a *missing* attribute (no CORS). Cross-origin videos served without CORS headers failed to load outright when they would have played fine untainted. The attribute is now only set when `crossOrigin` is actually configured (the empty string remains a valid, honored value). -- **Video preloading hung forever on autoplay-restricted browsers (e.g. iOS Safari) unless `autoplay: false` was spelled out** — the parser waited for `canplay`, which those browsers never fire for a video that isn't allowed to play, and only an *explicit* `autoplay: false` opted into the reliable `loadedmetadata` path — so the common manifest shape (no `autoplay` key) stalled the whole preloader and the game never started. Preloading now completes on `loadedmetadata` unless `autoplay: true` explicitly asks to wait for `canplay`. -- **Compressed-texture loading crashed on any device missing one compression family** — six `WEBKIT_`-prefixed extension fallbacks in `getSupportedCompressedTextureFormats()` referenced `this._gl`, a field that is never assigned (the context lives in `this.gl`). The fallback only evaluates when the primary extension is absent, so the whole feature *worked on ANGLE/Metal Macs* (which expose every family) *and threw a TypeError everywhere else* — Windows (no ASTC), iOS (no S3TC), software renderers — killing every `.dds`/`.pvr`/`.pkm`/`.ktx`/`.ktx2` load. -- **Nested multi-effect post-effects corrupted the render-target pool and crashed every frame** — a renderable with 2+ `postEffects` drawn inside a container with 2+ `postEffects` hit a half-guarded nesting path: the inner pass cleared the outer pass's capture target, its `end` popped the outer pass's pool slot, and the outer `endPostEffect` then dereferenced `undefined` (per-frame TypeError; with camera effects active it silently blitted the wrong texture instead). The pool now tracks a proper stack of passes — each nesting level gets its own lazily-allocated capture/ping-pong pair — and the saved effect projection is a per-pass stack too, so nested post-effect passes compose correctly. -- **Gradient shape fills (`fillEllipse`/`fillPolygon`/`fillArc`/`fillRoundRect`) broke inverted and nested masks** — the internal stencil pass gated its write phase on the *hidden* region of an inverted `setMask` (painting the gradient inside the cutout instead of the visible area) and restored a `NOTEQUAL` parity test instead of the mask's actual test — inverting the mask for every subsequent draw, and leaking outside-all-masks pixels for nested masks. The pass now tags the shape's visible pixels with a high stencil bit (collision-free with mask levels), clips the gradient to the tag, and re-installs the exact stencil test `setMask` had established. (`fillRect` gradients were unaffected — they render through a textured quad.) -- **`disableScissor()` didn't flush, so sprites batched inside the scissor escaped it** — GL scissor applies at draw time, not at batch time; every other scissor mutation (`enableScissor`, `clipRect`, `restore`) flushes pending vertices first, but `disableScissor` didn't — so the common `enableScissor → drawImage → disableScissor` pattern clipped nothing: the quads were still queued when the test was turned off. -- **WebGL `clearRect()` painted opaque black instead of erasing** — its JSDoc (and the Canvas renderer) promise transparent black, but the internal `clearColor()` default (`"#000000"`) parses with alpha 1. Visible on `transparent: true` canvases and inside post-effect render targets, where the "erased" region composited as a solid black box. -- **Canvas `clipRect()` skip-optimizations ignored the active transform** — the "full-viewport no-op" and "same-as-last-clip" early-outs compared the rect's raw *local* values, so a canvas-sized clipped container positioned off-origin got no clip at all, and a clipped container nested inside a same-sized clipped container skipped its own clip via the stale cache — both Canvas-only divergences from the transform-aware WebGL scissor path (the #1349 fix). The skip logic now reasons in device space (and always clips under rotation/skew, where an axis-aligned cache can't). -- **`Path2D.arc()`/`arcTo()`/`ellipse()` appended to an open path started a new sub-path instead of connecting** — the native Path2D spec joins an arc to the current point with a straight line (`arcTo`'s own documentation even promises it), but all three called `moveTo()` unconditionally. Under this release's new multi-sub-path fill semantics that stray sub-path is treated as a **hole**, so an arc appended to a filled outline punched a hole in the shape. They now connect per the spec; the canonical circle-hole idiom (explicit `moveTo` before `arc()`) still registers a hole, and a virgin-path `arc()` still starts cleanly. -- **`Rect.right`/`Rect.bottom` returned the width/height when the edge sat exactly at coordinate 0** — the getters computed `this.left + w || w`, so an edge landing precisely on 0 (falsy) fell back to the size. Bounds and collision math around the origin came out wrong for such rectangles. -- **`Rect.toPolygon()` handed out the rectangle's live vertices** — `Polygon.setVertices` stores a `Vector2d[]` by reference, so the "new" polygon shared the rect's actual points array: transforming or mutating the polygon silently corrupted the rectangle. The vertices are now cloned. -- **`loader.load()` mutated the caller's asset descriptor, so retries double-prepended `baseURL`** — the resolved URL (fontface `url()` unwrap + `baseURL` prefix) was written back into `asset.src`; `loader.reload()` (which re-loads the same stored object) — or simply `load()`ing the same manifest entry twice — then fetched `base + base + src`. The URL is now resolved into a copy handed to the parser; the caller's object is never touched. -- **`ShaderEffect.setUniform()` silently dropped values while the effect was disabled** — including during a context-loss window, where the auto-disable gate defeated the very suspend-cache replay built for it (a uniform set mid-loss reverted to its pre-loss value after restore); a user-disabled effect lost values the same way. `setUniform` now always forwards to the inner shader (which correctly defers while suspended); `setTexture` likewise stores its binding regardless of the enabled state for the lazy upload on the next enabled draw, and `setTime` skips only while genuinely suspended. Canvas-mode stubs keep no-oping. -- **`loader.unload()` of a never-loaded fontface threw a TypeError** — the fontface case was the only one without a membership guard, and `FontFaceSet.delete(undefined)` is a WebIDL type error rather than a `false` return. It now returns `false` like every other asset type. -- **Re-preloading an audio manifest silently replaced each Howl and leaked the old one** — the audio parser was the only asset parser without an already-loaded guard, so returning to a stage that preloads (a common pattern) churned every clip's decoded buffers / HTML5 nodes. An already-loaded clip is now reported as cached (like every other asset type); `me.audio.unload()` first to genuinely reload one. -- **Parallel audio loads shared a single retry counter** — three failures of one flaky file pushed *another* sound's first failure straight over the give-up threshold, throwing a spurious fatal load error (the default `stopOnAudioError`). Retries are now budgeted per sound. -- **`me.audio.stopOnAudioError` was documented as settable but couldn't be set** — it's a module export, and assigning through the namespace throws a TypeError, so the documented "disable audio instead of throwing" mode was unreachable. New **`me.audio.setStopOnAudioError(value)`** is the supported way to change it. -- **`me.audio.seek()` / `rate()` setter forms returned Howler's `Howl` object while typed as `number`** — the same get/set contract lie fixed for `stereo`/`position`/`orientation`/`panner` in 19.5.0; both now return nothing when setting and a number when reading. -- **`me.audio.position()` / `stereo()` getters returned `null` for sounds never positioned/panned** — Howler keeps the group state at `null` until first set, and the getters passed that through while typed as a tuple / number. They now return the neutral defaults (`[0, 0, 0]` / `0`). -- **`me.audio.resume()` without an id could spawn a new instance instead of resuming** — Howler's bare `play()` only auto-resumes when *exactly one* instance is paused; with two or more (e.g. after `pause()` without an id, which pauses the whole group) it started a brand-new playback from 0 and left the paused instances stuck forever. `resume()` now explicitly resumes every paused instance, as its documentation always promised. -- **`me.audio.unload()` left `getCurrentTrack()` pointing at the unloaded track** — the current-track pointer is now cleared when the unloaded clip is the active track, so `pauseTrack()`/`resumeTrack()` don't silently act on a ghost. -- **aseprite atlas JSON: trim, rotation and pivot silently ignored; animations played at arbitrary speeds** — the texture parser read `trimmed`/`spriteSourceSize`/`sourceSize`/`pivot`/`rotated` off the frame *rect* instead of the frame *entry* (they are siblings of `frame` in aseprite's export, same layout TexturePacker uses — and the TexturePacker parser reads them correctly), so every one of them came back `undefined`: frames exported with "Trim Sprite" drew without their trim offsets (sprites shifted/jittered per animation frame) and `sourceSize` was missing entirely. Animation tags fared no better: the parser synthesized `speed = 10 × (frameCount − 1)` ms/frame — a 2-frame tag ran at 100 fps, an 11-frame tag at 10 fps — while the authored per-frame `duration` values sat unread in the JSON. Tags now build frame objects carrying each frame's own `duration` as its delay. -- **The atlas coordinate-alias cache poisoned full-image draws and never hit for sub-regions** — the alias key recorded for every frame (the #1281 lookup workaround) was built from the *texture* dimensions instead of the *region* dimensions. Consequence one: every legitimate coordinate lookup (`drawImage` with source coordinates) missed and silently created a duplicate ad-hoc region per frame. Consequence two: any frame sitting at offset `(0, 0)` — nearly universal — registered the alias `"0,0,imgW,imgH"` pointing at *itself*, so a later full-image draw of the same image returned that frame's UVs and drew frame 0 stretched over the destination instead of the whole image. -- **No-argument `createAnimationFromName()` / `getAnimationSettings()` built corrupted animations** — the documented "add every entry in the atlas" form iterated the raw atlas dictionary, which also contains the coordinate-alias keys (every frame was added twice — visually a half-speed animation with doubled frames) and, for aseprite atlases, the `anims` bookkeeping entry (a pseudo-region with `NaN` frame dimensions that broke the sprite). Dictionary iteration now skips anything that isn't a real region. -- **`TextureAtlas.addRegion()` on a video source produced `Infinity`/`NaN` UVs** — an unsized `HTMLVideoElement` reports `width/height = 0`, and `addRegion` was the one code path without the `videoWidth`/`videoHeight` fallback the upload and cache paths already had, so a video sprite with `framewidth`/`frameheight` rendered an invisible/garbage quad under WebGL (Canvas was fine — renderer parity bug). -- **Padded (non-divisible) spritesheets sampled shifted frames under WebGL** — when a sheet isn't divisible by its cell size the parser "truncated" the effective size, but that truncated size fed the UV divisor while the GL texture is uploaded at the image's physical size, scaling and shifting every frame's UVs (Canvas was unaffected — another renderer parity bug). The frame *grid* is still truncated; UVs are now always computed against the physical texture size. -- **A malformed atlas object constructed an empty, broken `TextureAtlas` instead of throwing** — the "format not supported" guard checked `.length` on a `Map` (always `undefined`), so the error was unreachable and the failure surfaced later as confusing `undefined` errors far from the cause. -- **Lit rendering: normal maps and color textures collided on the same GL texture units** — the lit batcher binds each sprite's normal map at a fixed offset (`maxBatchTextures + slot`, units 8–15 on 16-unit hardware) by pure arithmetic, while the renderer-wide texture-unit allocator handed those same units to color textures (an unlit sprite's, a mesh's, a pattern's — units are sticky, so the 9th distinct texture ever drawn was enough). Each batcher tracks its bindings per instance, so whichever bound second silently clobbered the other: unlit sprites rendered *showing the normal map*, or lit sprites shaded with *color data as normals*. Activating the lit batcher now reserves its normal range in the unit allocator (lazily, on first lit draw — unlit games keep their full pool), and `ShaderEffect.setTexture` extra samplers skip units reserved by others when claiming theirs. -- **A texture re-upload could land on another batcher's texture** — each batcher tracked the globally-shared `gl.activeTexture` state in a per-instance field, so after another batcher (or an FBO pass) moved the active unit, the stale copy let `bindTexture2D` skip the `gl.activeTexture` call and the following `texImage2D` wrote into whatever texture was really active. Concretely: a video sprite's per-frame force-re-upload after a 3D-mesh pass overwrote the mesh's texture with video frames while the video froze. The active unit is now tracked once per renderer (shared by all batchers), and uploads force-activate their target unit outright. -- **Camera post-effects could blank textures owned by non-current batchers** — the post-effect FBO setup and the end-of-frame blit both null GL unit 0's binding, but only the *current* batcher's bookkeeping was invalidated; a texture drawn exclusively through another batcher (a normal-mapped-only atlas, a mesh texture) that had been assigned unit 0 skipped its re-bind and sampled an unbound texture — opaque black for as long as the effect was active. Unit-0 invalidation now reaches every batcher. -- **Filled shapes larger than the vertex buffer silently vanished** — `PrimitiveBatcher.drawVertices` had no chunking, and the vertex buffer's typed-array writes past capacity are silently dropped while the draw call still used the full count: a filled ellipse at radius ≳ 435 px (`fillEllipse`/`fillArc`, or a large triangulated `fill()`) raised GL errors and rendered nothing — including everything else batched in the same flush. Oversized shapes are now split into buffer-sized chunks on primitive boundaries (strip/fan/loop modes stitch chunks with overlapped vertices), and thick-line expansion checks capacity per line pair. -- **`NoiseTexture2d.destroy()` leaked its GPU normal-map texture** — the lit batcher caches one GL texture per normal-map source in a strong-keyed map that was only emptied on a full renderer reset, so a per-level noise normal map leaked its GL texture *and* its baked canvas on every level swap. `Texture2d.destroy()` now broadcasts the new `event.TEXTURE2D_DESTROYED` with the backing source, and the lit batcher evicts the cached texture (deleting the GL object) in response. -- **The mesh depth-clear flag was shared between renderer instances** — the lazy "clear depth once per target" state lived at module scope, so with two WebGL Applications on one page (or any interleaved rendering) one renderer's frame start could re-arm or steal the other's depth clear mid-frame, breaking inter-mesh occlusion. The flag now lives on the renderer, and `event.RENDER_TARGET_CHANGED` carries the emitting renderer so subscribers ignore foreign broadcasts. -- **GL buffers leaked on every reset — and mesh index/vertex buffers went dead after a context restore** — the quad batchers recreated their static index buffer on every `GAME_RESET` without deleting the old one (an orphaned ~12 KB GL buffer per reset per batcher); the mesh batchers had the opposite problem: their reset only cleared counters, keeping GL buffer objects that belong to the *lost* context after a `webglcontextrestored`, so every subsequent mesh upload silently failed. Both families now delete and recreate their GL buffers on reset. +- **`renderer.toFrameTexture()`** — GPU capture of everything drawn so far, as a live `Texture2d` for shader input (back-buffer-copy semantics; Canvas parity via an offscreen copy). Closes [#1544](https://github.com/melonjs/melonJS/issues/1544) (initial approach contributed by @Vareniel) +- **Shader builtins: `screen_texture` / `screen_uv` / `noise_uv`** — annotate a sampler with `: screen_texture` and the engine keeps it filled with the screen behind the object; `screen_uv` / `noise_uv` are free varyings for screen-space and frame-local (atlas-independent) coordinates. No JS plumbing, fully backward compatible. See the new **Water Overworld** example (design and demo scene contributed by @Vareniel) +- **Shader preloading: `"shader"` asset type + `loader.getShader()` + `clone()`** — GLSL compiles at load time; `getShader()` returns the shared, loader-owned instance (`clone()` for per-renderable uniform values). A `{vertex, fragment}` pair compiles into a shared raw `GLShader` instead, for `Mesh` / custom-batcher shaders +- **`ShaderEffect.setTexture(name, image[, repeat])`** — bind extra `sampler2D` textures (noise, masks, LUTs) to an effect with no GL plumbing; see the reworked **Water Refraction** example. Closes [#1532](https://github.com/melonjs/melonJS/issues/1532) (thanks @Vareniel) +- **`ShaderEffect.setTime(seconds)`** — one-call `uTime` convention for shader animation; a safe no-op when the shader doesn't declare it +- **`shared` flag on `ShaderEffect` / `GLShader`** — opt an effect out of auto-destroy when it is reused across several renderables +- **Procedural noise: `Noise` + `NoiseTexture2d`** — deterministic CPU-samplable coherent noise (simplex / perlin / value / cellular, fBm / ridged / ping-pong fractals, domain warp), bakeable to a drawable canvas as grayscale, a color ramp, or an animated normal map +- **`Texture2d` base class** — common base for texture assets (`TextureAtlas`, `NoiseTexture2d`, frame captures); renderables accept any `Texture2d` directly as `image` +- **Anchor-point presets + `Sprite3d` anchor support** (#1514) — `settings.anchorPoint` accepts `"center"`, `"bottom"`, `"top-left"`, … uniformly across `Sprite`, `Entity`, `Collectable`, `ImageLayer`, `Text`, `BitmapText`; `Sprite3d` gains the same `anchorPoint` key — vertex-baked (billboard/flip/trim-safe) and mutable at runtime (thanks @asadullahbro) +- **Holes & compound paths in `Path2D` / SVG fills** (#1253) — every `moveTo()` / SVG `M` starts a sub-path; on `fill()` inner sub-paths render as holes (donuts, letter counters), under both renderers +- **BMFont XML support for `BitmapText`** — the AngelCode XML flavour loads directly through the standard preloader, auto-detected alongside the text `.fnt` format + +### Fixed +- Asset loading failed over the `file:` protocol (Cordova/Capacitor APK) — `fetchData` now falls back to `XMLHttpRequest` where `fetch()` refuses `file://` (thanks @Vareniel) +- `loader.unloadAll()` threw if a fontface was loaded, and never actually freed videos +- `loader.load()` mutated the caller's asset descriptor, double-prepending `baseURL` on retries; `loader.unload()` of a never-loaded fontface threw +- Video preloading hung on autoplay-restricted browsers (e.g. iOS Safari) unless `autoplay: false` was spelled out, and every video was fetched with forced anonymous CORS +- Compressed-texture loading crashed on any device missing one compression family +- Audio batch: re-preloading a manifest leaked the previous `Howl` per clip; parallel loads shared one retry budget; `stopOnAudioError` couldn't actually be set (new `setStopOnAudioError()`); `seek()` / `rate()` setter forms returned a `Howl` typed as `number`; `position()` / `stereo()` returned `null` for untouched sounds; `resume()` could spawn a new instance instead of resuming; `unload()` left `getCurrentTrack()` stale +- Texture-atlas batch: the coordinate-alias cache poisoned full-image draws of atlas images; no-argument `createAnimationFromName()` / `getAnimationSettings()` built corrupted animations; `addRegion()` on a video source produced `NaN` UVs; padded (non-divisible) spritesheets sampled shifted frames under WebGL; a malformed atlas object constructed silently instead of throwing +- aseprite atlas JSON: trim, rotation and pivot were silently ignored, and tag animations played at arbitrary speeds — authored per-frame durations are now honored +- WebGL batcher batch: lit normal maps and color textures collided on the same texture units; a texture re-upload could land on another batcher's texture; camera post-effects could blank textures owned by non-current batchers; filled shapes larger than the vertex buffer silently vanished; `NoiseTexture2d.destroy()` leaked its GPU texture; the mesh depth-clear flag was shared between renderer instances; GL buffers leaked on every reset and went stale after a context restore +- Nested multi-effect post-effects corrupted the render-target pool and crashed every frame +- Gradient shape fills (`fillEllipse` / `fillPolygon` / `fillArc` / `fillRoundRect`) broke inverted and nested stencil masks +- `disableScissor()` didn't flush, so sprites batched inside the scissor escaped it +- WebGL `clearRect()` painted opaque black instead of erasing; Canvas `clipRect()` skip-optimizations ignored the active transform +- `ShaderEffect.setUniform()` silently dropped values while the effect was disabled (e.g. mid context-loss) +- 2D sprites could vanish when using a large `pos.z` sort key (clip-culled; regression since 19.7) +- `Mesh.textureRepeat` leaked its wrap mode to every consumer of the same image (#1503) +- Animated `NineSliceSprite` collapsed to its frame size on the first animation step (#1115, thanks @NemoStein) +- `Path2D.arc()` / `arcTo()` / `ellipse()` appended to an open path detached instead of connecting (per the native `Path2D` spec); SVG `A`/`H`/`V` commands opening a new sub-path drew from the wrong origin +- `Rect.right` / `bottom` returned the size when the edge sat exactly at 0; `Rect.toPolygon()` handed out the rectangle's live vertices instead of clones +- `moveTowards()` never landed exactly on its target and returned the target instead of `this` (all four vector classes) +- `new Color("darkgray")` threw; `cyan` / `magenta` / `rebeccapurple` were missing; `silver` / `aliceblue` / `burlywood` had channel typos — the full CSS keyword table is now spec-validated by a test +- `Matrix2d` 6-argument (canvas convention) form dropped translation and transposed rotation +- `Text`: generic CSS font families (`sans-serif`, `monospace`, …) were quoted and silently fell back to serif +- The physics startup banner logged a minifier-mangled adapter class name ## [19.8.0] (melonJS 2) - _2026-06-26_