Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
12218f4
20.13.0: bump the version
claude Oct 31, 2026
acbb163
UI: checkbox, slider, skinned button, panel layout and scrolling (#1722)
claude Oct 31, 2026
1fbc5c9
UI: fixes from review
claude Oct 31, 2026
1bf6e4b
Examples: a taller level list in the UI example
claude Oct 31, 2026
4cacf94
Examples: the UI example's level list at the top right, beside the bars
claude Oct 31, 2026
32626f9
Examples: the UI example's level list shows the first 16 levels
claude Oct 31, 2026
2e3c8f1
UI: consistent widget API, and fixes from a second review
claude Oct 31, 2026
d4a577c
Examples: UI example lint
claude Oct 31, 2026
99c7c51
UI: drop shadows on UIPanel and ProgressBar, used in the UI example
claude Oct 31, 2026
12bedbd
Gamepad plugin: keep a touch's hover events on the controls from the …
claude Oct 31, 2026
59f6398
UI: keyboard and gamepad focus navigation, and a hover/press bounce
claude Oct 31, 2026
6f1acf6
UI: keep the focus navigation to the minimum
claude Oct 31, 2026
44ee453
UI: fixes from a review of the focus navigation and the bounce
claude Oct 31, 2026
5291863
UI: focus lives on the widgets, with no focus manager to set up
claude Oct 31, 2026
1440ba7
Remove a scratch test file committed by mistake
claude Oct 31, 2026
32a4edc
UI: fill the gaps in the widget tests, and fix what they found
claude Oct 31, 2026
596b23b
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
d706c94
gamepad-plugin 1.0.2
claude Oct 31, 2026
d345737
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Oct 31, 2026
b39dd70
Skill: UI widgets, with a widget map and a complete menu example
claude Oct 31, 2026
ec8e1a4
Docs: UICheckbox and UISlider, with examples
claude Oct 31, 2026
57109da
Docs: UIPanel and ProgressBar, with examples
claude Oct 31, 2026
139c957
Docs: UIBaseElement, UITextButton, UISpriteElement, with examples
claude Oct 31, 2026
0a785e6
Changelog: onCancel also covers a called-off bindKey press
claude Oct 31, 2026
c365da3
UI: UISpriteElement lets events through by default, like the others
claude Oct 31, 2026
1d86e82
UI: a press stops at a UI element by default
claude Oct 31, 2026
6d971f4
UI: UISpriteElement is a UIBaseElement holding a Sprite
claude Nov 1, 2026
515a59a
Tests: UISpriteElement's public behaviour, old and new
claude Nov 1, 2026
71c91de
Input: keep the hover after a click, and end it when the pointer leav…
claude Nov 1, 2026
63bfee5
UI: fix the widget bugs found while writing the docs
claude Nov 1, 2026
8a41b06
UI: a radio group holds one choice from when its checkboxes are made
claude Nov 1, 2026
8a47072
Fixes from review: canvas leave reaches every hovered region; UISprit…
claude Nov 1, 2026
6b9b22b
UISpriteElement: setCurrentAnimation passes preserve_dt on
claude Nov 1, 2026
582b27c
Input: a cancelled pointer, or a lifted legacy touch, ends the hover
claude Nov 1, 2026
c70fe49
Changelog: a cancelled pointer ends the hover too
claude Nov 1, 2026
316a7c2
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
6d4a96a
Rendering: a clip inside a clip is the overlap of the two on WebGL an…
claude Nov 1, 2026
4d1c82e
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
c0c2258
Camera: a fade always calls onComplete before it is removed (#1810)
claude Nov 1, 2026
927b0c9
Tests: fadeIn / fadeOut onComplete through the game loop (#1810)
claude Nov 1, 2026
3ebc32a
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
39f681b
Tiled: seed object GUIDs past the map's ids when it has no nextobject…
claude Nov 1, 2026
26494a3
Merge remote-tracking branch 'origin/master' into claude/modest-johns…
claude Nov 1, 2026
b311be0
fix(line): 선분 연장선의 점을 포함하지 않도록 수정
mmporong Oct 31, 2026
ec612a5
fix(color): hex 알파 정밀도를 보존
mmporong Oct 31, 2026
8c16d0c
fix(math): 2의 거듭제곱 입력을 검증
mmporong Oct 31, 2026
864eddd
Color: keep toHex8() output as it was; the hex alpha fix is in parseH…
claude Nov 1, 2026
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
3 changes: 3 additions & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,10 @@
- UI: a `UISlider` lays its texts out again when they change: a vertical one widens for a longer value text (a new `valueFormat`, `min`, `max` or `step`), and on a horizontal one the value moves below the track when it would run into the caption, instead of overlapping it. `labelText` sets the caption later
- Input: a region stays hovered after a click while the pointer is still on it. A release used to forget the pointer, so leaving straight after a click never called `onOut` (a button stayed lit), and moving on over it called `onOver` again
- Input: the pointer leaving the canvas ends the hover of whatever it was over, not only of a region at the point where it left; a cancelled pointer, or a finger lifted on the legacy touch-event path, ends it too
- Color: `parseHex()` keeps all 8 bits of alpha instead of rounding to one decimal. Low alpha values no longer become fully transparent, and 4- or 8-digit hex colors round-trip through `toHex8()` without changing alpha (thanks @mmporong)
- Math: `isPowerOfTwo()` rejects zero, negative and fractional values instead of accepting them after 32-bit coercion, and handles powers and non-powers beyond the 32-bit range (thanks @mmporong)
- Input: a child cropped out of view by a container with `clipping`, or scrolled out of a `UIPanel`, no longer gets pointer events where it is not drawn
- Geometry: `Line#contains()` checks the segment endpoints as well as collinearity; a point on the line beyond either endpoint was reported as contained (thanks @mmporong)
- UI: a `UITextButton` moved after it was built draws its background where it is, not where it was first put
- UI: a blocking `UIPanel` stays opaque to hover for as long as the pointer is on it, not only on the frame the pointer enters
- UI: a draggable `UIBaseElement` pressed with no hover first, as a touch tap is, no longer throws, and drags by touch; in the world, rather than floating, it no longer jumps by the camera position when dragged
Expand Down
7 changes: 6 additions & 1 deletion packages/melonjs/src/geometries/line.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,12 @@ export class Line extends Polygon {

//(Cy - Ay) * (Bx - Ax) = (By - Ay) * (Cx - Ax)
return (
(_y - start.y) * (end.x - start.x) === (end.y - start.y) * (_x - start.x)
(_y - start.y) * (end.x - start.x) ===
(end.y - start.y) * (_x - start.x) &&
_x >= Math.min(start.x, end.x) &&
_x <= Math.max(start.x, end.x) &&
_y >= Math.min(start.y, end.y) &&
_y <= Math.max(start.y, end.y)
);
}

Expand Down
8 changes: 2 additions & 6 deletions packages/melonjs/src/math/color.ts
Original file line number Diff line number Diff line change
Expand Up @@ -703,11 +703,7 @@ export class Color {
parseInt(match[!argb ? 1 : 2], 16), // r
parseInt(match[!argb ? 2 : 3], 16), // g
parseInt(match[!argb ? 3 : 4], 16), // b
Number(
(clamp(parseInt(match[!argb ? 4 : 1], 16), 0, 255) / 255.0).toFixed(
1,
),
), // a
clamp(parseInt(match[!argb ? 4 : 1], 16), 0, 255) / 255.0, // a
);
}

Expand All @@ -730,7 +726,7 @@ export class Color {
parseInt(r + r, 16), // r
parseInt(g + g, 16), // g
parseInt(b + b, 16), // b
Number((clamp(parseInt(a + a, 16), 0, 255) / 255.0).toFixed(1)), // a
clamp(parseInt(a + a, 16), 0, 255) / 255.0, // a
);
}

Expand Down
6 changes: 5 additions & 1 deletion packages/melonjs/src/math/math.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,11 @@ export const EPSILON = 0.000001;
* @returns true if number is a power of two
*/
export function isPowerOfTwo(val: number) {
return (val & (val - 1)) === 0;
if (!Number.isInteger(val) || val <= 0) {
return false;
}
const exponent = Math.log2(val);
return Number.isInteger(exponent) && val === 2 ** exponent;
}

/**
Expand Down
48 changes: 31 additions & 17 deletions packages/melonjs/tests/color.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,10 +78,8 @@ describe("Color", () => {
expect(blue_color.parseHex("#0000FF").toRGB()).toEqual("rgb(0,0,255)");
});
// #RGBA
it("#0F08 value is rgba(0, 255, 0, 0.5)", () => {
expect(blue_color.parseHex("#0F08").toRGBA()).toEqual(
"rgba(0,255,0,0.5)",
);
it("#0F08 preserves its 4-bit alpha", () => {
expect(blue_color.parseHex("#0F08").alpha).toBeCloseTo(8 / 15);
});
// #RRGGBB
it("#FF00FF value is rgba(255, 0, 255, 1)", () => {
Expand All @@ -91,9 +89,11 @@ describe("Color", () => {
});
// #RRGGBBAA (finish with the blue color so that the test below passes)
it("#0000FF80 value is rgba(0, 0, 255, 0.5)", () => {
expect(blue_color.parseHex("#0000FF80").toRGBA()).toEqual(
"rgba(0,0,255,0.5)",
);
expect(blue_color.parseHex("#0000FF80").alpha).toBeCloseTo(128 / 255);
});

it("preserves low alpha values", () => {
expect(blue_color.parseHex("#0000FF01").alpha).toBeCloseTo(1 / 255);
});

// override the alpha with a specific value
Expand Down Expand Up @@ -291,7 +291,7 @@ describe("Color", () => {
expect(red_color.toHex()).toEqual("#FF0000");
});

it("red_color RGBA hex value is #FF0000FF", () => {
it("red_color RGBA hex value is #FF00007F", () => {
expect(red_color.toHex8()).toEqual("#FF00007F");
});

Expand Down Expand Up @@ -344,8 +344,8 @@ describe("Color", () => {
expect(blue_color.toRGB()).toEqual("rgb(0,0,255)");
});

it("blue_color rgba value is rgba(0, 0, 255, 0.5)", () => {
expect(blue_color.toRGBA()).toEqual("rgba(0,0,255,0.5)");
it("blue_color preserves its 8-bit alpha", () => {
expect(blue_color.alpha).toBeCloseTo(128 / 255);
});

it("lighten blue_color hex by 0.5 value is #7F7FFF", () => {
Expand Down Expand Up @@ -542,13 +542,27 @@ describe("Color", () => {
expect(new Color(0, 0, 255, 0).toHex8(0.5)).toEqual("#0000FF7F");
});

it("is consistent with parseHex round-trip for 8-digit hex", () => {
// alpha 0xFF round-trips cleanly (1.0 float)
const c = new Color().parseHex("#3A7BCDFF");
expect(c.toHex8()).toEqual("#3A7BCDFF");
// alpha 0x00 round-trips cleanly (0.0 float)
const c2 = new Color().parseHex("#3A7BCD00");
expect(c2.toHex8()).toEqual("#3A7BCD00");
it("round-trips every 8-bit hex alpha", () => {
for (let alpha = 0; alpha <= 255; alpha++) {
const hexAlpha = alpha.toString(16).padStart(2, "0").toUpperCase();
const hexColor = `#3A7BCD${hexAlpha}` as const;
expect(new Color().parseHex(hexColor).toHex8()).toEqual(hexColor);
}
});

it("round-trips every 4-bit hex alpha", () => {
for (const hexAlpha of "0123456789ABCDEF") {
expect(new Color().parseHex(`#ABC${hexAlpha}`).toHex8()).toEqual(
`#AABBCC${hexAlpha}${hexAlpha}`,
);
}
});

it("keeps its output for an rgba() alpha that is not a whole byte", () => {
// 0.5 is 127.5 of 255: toHex8 truncates, as it always has
expect(new Color().parseRGB("rgba(0, 0, 255, 0.5)").toHex8()).toEqual(
"#0000FF7F",
);
});

it("alpha override does not modify instance alpha", () => {
Expand Down
5 changes: 5 additions & 0 deletions packages/melonjs/tests/line.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,11 @@ describe("Shape : Line", () => {
expect(line.contains(29, 61)).toEqual(false);
});

it("does not contain collinear points beyond its endpoints", () => {
expect(line.contains(56, 120)).toEqual(false);
expect(line.contains(-28, -60)).toEqual(false);
});

it("Line Bounding Rect width is 28", () => {
expect(bounds.width).toEqual(28);
});
Expand Down
21 changes: 21 additions & 0 deletions packages/melonjs/tests/math.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,27 @@ describe("Math", () => {
it("32 is a Power of 2", () => {
expect(math.isPowerOfTwo(32)).toEqual(true);
});
it("0 and negative values are not Powers of 2", () => {
expect(math.isPowerOfTwo(0)).toEqual(false);
expect(math.isPowerOfTwo(-2)).toEqual(false);
});
it("fractional values are not Powers of 2", () => {
expect(math.isPowerOfTwo(2.5)).toEqual(false);
});
it("supports Powers of 2 outside the 32-bit range", () => {
expect(math.isPowerOfTwo(2 ** 40)).toEqual(true);
expect(math.isPowerOfTwo(3 * 2 ** 32)).toEqual(false);
});
it("rejects large non-Powers whose logarithms round to integers", () => {
for (const value of [
Number.MAX_SAFE_INTEGER,
2 ** 53 + 2,
2 ** 53 + 4,
2 ** 54 - 2,
]) {
expect(math.isPowerOfTwo(value)).toEqual(false);
}
});
it("1027 is not a Power of 2", () => {
expect(math.isPowerOfTwo(1027)).toEqual(false);
});
Expand Down
Loading