diff --git a/packages/melonjs/CHANGELOG.md b/packages/melonjs/CHANGELOG.md index 6eef656a6..f688c0ac8 100644 --- a/packages/melonjs/CHANGELOG.md +++ b/packages/melonjs/CHANGELOG.md @@ -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 diff --git a/packages/melonjs/src/geometries/line.ts b/packages/melonjs/src/geometries/line.ts index 781cf7077..d69342996 100644 --- a/packages/melonjs/src/geometries/line.ts +++ b/packages/melonjs/src/geometries/line.ts @@ -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) ); } diff --git a/packages/melonjs/src/math/color.ts b/packages/melonjs/src/math/color.ts index d1e7ede91..fb0ead517 100644 --- a/packages/melonjs/src/math/color.ts +++ b/packages/melonjs/src/math/color.ts @@ -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 ); } @@ -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 ); } diff --git a/packages/melonjs/src/math/math.ts b/packages/melonjs/src/math/math.ts index 26dbdecde..fa0ff085f 100644 --- a/packages/melonjs/src/math/math.ts +++ b/packages/melonjs/src/math/math.ts @@ -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; } /** diff --git a/packages/melonjs/tests/color.spec.ts b/packages/melonjs/tests/color.spec.ts index 1a422ef0e..6c42384c3 100644 --- a/packages/melonjs/tests/color.spec.ts +++ b/packages/melonjs/tests/color.spec.ts @@ -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)", () => { @@ -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 @@ -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"); }); @@ -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", () => { @@ -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", () => { diff --git a/packages/melonjs/tests/line.spec.ts b/packages/melonjs/tests/line.spec.ts index 1996de187..32c22a1ff 100644 --- a/packages/melonjs/tests/line.spec.ts +++ b/packages/melonjs/tests/line.spec.ts @@ -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); }); diff --git a/packages/melonjs/tests/math.spec.js b/packages/melonjs/tests/math.spec.js index e54cef701..3eb362aba 100644 --- a/packages/melonjs/tests/math.spec.js +++ b/packages/melonjs/tests/math.spec.js @@ -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); });