Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,11 @@
- **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`.

## [19.8.0] (melonJS 2) - _2026-06-26_

Expand Down
19 changes: 16 additions & 3 deletions packages/melonjs/src/loader/parsers/video.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,14 @@ export function preloadVideo(data, onload, onerror, settings) {
videoElement.setAttribute("playsinline", "true");
videoElement.setAttribute("disablePictureInPicture", "true");
videoElement.setAttribute("controls", "false");
videoElement.setAttribute("crossorigin", settings.crossOrigin);
// only stamp crossorigin when actually configured: per the HTML spec a
// MISSING attribute means no CORS, but an INVALID value (e.g. the string
// "undefined") maps to Anonymous — which force-enables CORS and breaks
// cross-origin sources served without CORS headers. Note "" is a valid
// value (anonymous), so guard on type, not truthiness.
if (typeof settings.crossOrigin === "string") {
videoElement.setAttribute("crossorigin", settings.crossOrigin);
}

if (data.autoplay === true) {
videoElement.setAttribute("autoplay", "true");
Expand All @@ -70,8 +77,14 @@ export function preloadVideo(data, onload, onerror, settings) {
}

if (typeof onload === "function") {
// some mobile browser (e.g. safari) won't emit the canplay event if autoplay is disabled
if (data.stream === true || data.autoplay === false) {
// some mobile browsers (e.g. safari) won't emit the canplay event
// unless the video actually plays — only wait for it when autoplay
// was explicitly requested; everywhere else (including the common
// case of autoplay simply omitted) loadedmetadata is the reliable
// "preloaded" signal, otherwise preload hangs forever on
// autoplay-restricted browsers. Streaming videos always use
// loadedmetadata — their preload="metadata" never buffers to canplay.
if (data.stream === true || data.autoplay !== true) {
videoElement.onloadedmetadata = () => {
if (typeof onload === "function") {
onload();
Expand Down
15 changes: 10 additions & 5 deletions packages/melonjs/src/math/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ const hex8Rx =
const CSS_COLORS = [
// CSS1
["black", [0, 0, 0]],
["silver", [192, 192, 129]],
["silver", [192, 192, 192]],
["gray", [128, 128, 128]],
["white", [255, 255, 255]],
["maroon", [128, 0, 0]],
Expand All @@ -63,7 +63,7 @@ const CSS_COLORS = [
["orange", [255, 165, 0]],

// CSS3
["aliceblue", [240, 248, 245]],
["aliceblue", [240, 248, 255]],
["antiquewhite", [250, 235, 215]],
["aquamarine", [127, 255, 212]],
["azure", [240, 255, 255]],
Expand All @@ -72,20 +72,21 @@ const CSS_COLORS = [
["blanchedalmond", [255, 235, 205]],
["blueviolet", [138, 43, 226]],
["brown", [165, 42, 42]],
["burlywood", [222, 184, 35]],
["burlywood", [222, 184, 135]],
["cadetblue", [95, 158, 160]],
["chartreuse", [127, 255, 0]],
["chocolate", [210, 105, 30]],
["coral", [255, 127, 80]],
["cornflowerblue", [100, 149, 237]],
["cornsilk", [255, 248, 220]],
["crimson", [220, 20, 60]],
["cyan", [0, 255, 255]],
["darkblue", [0, 0, 139]],
["darkcyan", [0, 139, 139]],
["darkgoldenrod", [184, 134, 11]],
["darkgray[*]", [169, 169, 169]],
["darkgray", [169, 169, 169]],
["darkgreen", [0, 100, 0]],
["darkgrey[*]", [169, 169, 169]],
["darkgrey", [169, 169, 169]],
["darkkhaki", [189, 183, 107]],
["darkmagenta", [139, 0, 139]],
["darkolivegreen", [85, 107, 47]],
Expand Down Expand Up @@ -140,6 +141,7 @@ const CSS_COLORS = [
["lightyellow", [255, 255, 224]],
["limegreen", [50, 205, 50]],
["linen", [250, 240, 230]],
["magenta", [255, 0, 255]],
["mediumaquamarine", [102, 205, 170]],
["mediumblue", [0, 0, 205]],
["mediumorchid", [186, 85, 211]],
Expand Down Expand Up @@ -191,6 +193,9 @@ const CSS_COLORS = [
["wheat", [245, 222, 179]],
["whitesmoke", [245, 245, 245]],
["yellowgreen", [154, 205, 50]],

// CSS4
["rebeccapurple", [102, 51, 153]],
] as const;

type ColorName = (typeof CSS_COLORS)[number][0];
Expand Down
21 changes: 12 additions & 9 deletions packages/melonjs/src/math/matrix2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,15 +93,18 @@ export class Matrix2d {
this.val[7] = values[7]; // h - m12
this.val[8] = values[8]; // i - m22
} else if (values.length === 6) {
this.val[0] = values[0]; // a
this.val[1] = values[2]; // c
this.val[2] = values[4]; // e
this.val[3] = values[1]; // b
this.val[4] = values[3]; // d
this.val[5] = values[5]; // f
this.val[6] = 0; // g
this.val[7] = 0; // h
this.val[8] = 1; // i
// canvas-style (a, b, c, d, e, f): (a, b) is the first column
// (m00/m10), (c, d) the second (m01/m11), (e, f) the translation
// (m02/m12 — the tx/ty getters read val[6]/val[7])
this.val[0] = values[0]; // a - m00
this.val[1] = values[1]; // b - m10
this.val[2] = 0; // m20
this.val[3] = values[2]; // c - m01
this.val[4] = values[3]; // d - m11
this.val[5] = 0; // m21
this.val[6] = values[4]; // e - m02 (tx)
this.val[7] = values[5]; // f - m12 (ty)
this.val[8] = 1; // m22
}

return this;
Expand Down
11 changes: 7 additions & 4 deletions packages/melonjs/src/math/observableVector2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -440,14 +440,17 @@ export class ObservableVector2d {
* @returns Reference to this object for method chaining
*/
moveTowards(target: Vector2d | ObservableVector2d, step: number) {
const angle = Math.atan2(target.y - this.y, target.x - this.x);

const distance = this.distance(target);

if (distance === 0 || (step >= 0 && distance <= step * step)) {
return target;
// within one step (and not fleeing): land exactly on the target —
// and always mutate/return `this` (via set(), so observers fire),
// never the caller's target
if (distance === 0 || (step >= 0 && distance <= step)) {
this.set(target.x, target.y);
return this;
}

const angle = Math.atan2(target.y - this.y, target.x - this.x);
this.set(this.x + Math.cos(angle) * step, this.y + Math.sin(angle) * step);

return this;
Expand Down
15 changes: 9 additions & 6 deletions packages/melonjs/src/math/observableVector3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -505,17 +505,20 @@ export class ObservableVector3d {
* @returns Reference to this object for method chaining
*/
moveTowards(target: Vector2d | Vector3d | ObservableVector3d, step: number) {
const angle = Math.atan2(target.y - this.y, target.x - this.x);

const dx = this.x - target.x;
const dy = this.y - target.y;
const dx = target.x - this.x;
const dy = target.y - this.y;

const distance = Math.sqrt(dx * dx + dy * dy);

if (distance === 0 || (step >= 0 && distance <= step * step)) {
return target;
// within one step (and not fleeing): land exactly on the target (x/y
// only, this is a 2D interpolation) — and always mutate/return `this`
// (via set(), so observers fire), never the caller's target
if (distance === 0 || (step >= 0 && distance <= step)) {
this.set(target.x, target.y, this.z);
return this;
}

const angle = Math.atan2(dy, dx);
this.set(
this.x + Math.cos(angle) * step,
this.y + Math.sin(angle) * step,
Expand Down
11 changes: 7 additions & 4 deletions packages/melonjs/src/math/vector2d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -364,14 +364,17 @@ export class Vector2d {
* @returns Reference to this object for method chaining
*/
moveTowards(target: Vector2d, step: number) {
const angle = Math.atan2(target.y - this.y, target.x - this.x);

const distance = this.distance(target);

if (distance === 0 || (step >= 0 && distance <= step * step)) {
return target;
// within one step (and not fleeing): land exactly on the target —
// and always mutate/return `this`, never the caller's target
if (distance === 0 || (step >= 0 && distance <= step)) {
this.x = target.x;
this.y = target.y;
return this;
}

const angle = Math.atan2(target.y - this.y, target.x - this.x);
this.x += Math.cos(angle) * step;
this.y += Math.sin(angle) * step;

Expand Down
16 changes: 10 additions & 6 deletions packages/melonjs/src/math/vector3d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -415,17 +415,21 @@ export class Vector3d {
* @returns Reference to this object for method chaining
*/
moveTowards(target: Vector2d | Vector3d, step: number) {
const angle = Math.atan2(target.y - this.y, target.x - this.x);

const dx = this.x - target.x;
const dy = this.y - target.y;
const dx = target.x - this.x;
const dy = target.y - this.y;

const distance = Math.sqrt(dx * dx + dy * dy);

if (distance === 0 || (step >= 0 && distance <= step * step)) {
return target;
// within one step (and not fleeing): land exactly on the target (x/y
// only, this is a 2D interpolation) — and always mutate/return `this`,
// never the caller's target
if (distance === 0 || (step >= 0 && distance <= step)) {
this.x = target.x;
this.y = target.y;
return this;
}

const angle = Math.atan2(dy, dx);
this.x += Math.cos(angle) * step;
this.y += Math.sin(angle) * step;

Expand Down
Loading
Loading