Skip to content
Open
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
1 change: 1 addition & 0 deletions packages/melonjs/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
- UI: `UITextButton#bitmapText`, for `label`

### Fixed
- Color: `parseHex()` keeps all 8 bits of alpha instead of rounding to one decimal, and `toHex8()` rounds float alpha to the nearest byte instead of truncating it. Low alpha values no longer become fully transparent, and 4- or 8-digit hex colors round-trip without changing alpha (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
- 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
Expand Down
10 changes: 3 additions & 7 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 Expand Up @@ -791,7 +787,7 @@ export class Color {
*/
toHex8(alpha = this.normalizedRGBA[3]) {
const a = this.normalizedRGBA;
return `#${toHex((a[0] * 255) >> 0)}${toHex((a[1] * 255) >> 0)}${toHex((a[2] * 255) >> 0)}${toHex((alpha * 255) >> 0)}`;
return `#${toHex((a[0] * 255) >> 0)}${toHex((a[1] * 255) >> 0)}${toHex((a[2] * 255) >> 0)}${toHex(Math.round(alpha * 255))}`;
}

/**
Expand Down
55 changes: 34 additions & 21 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,8 +291,8 @@ describe("Color", () => {
expect(red_color.toHex()).toEqual("#FF0000");
});

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

it("red_color rgba value is rgba(255,0,0,0.5)", () => {
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 @@ -521,11 +521,11 @@ describe("Color", () => {
});

it("converts half alpha", () => {
expect(new Color(0, 255, 0, 0.5).toHex8()).toEqual("#00FF007F");
expect(new Color(0, 255, 0, 0.5).toHex8()).toEqual("#00FF0080");
});

it("converts quarter alpha", () => {
expect(new Color(0, 0, 255, 0.25).toHex8()).toEqual("#0000FF3F");
expect(new Color(0, 0, 255, 0.25).toHex8()).toEqual("#0000FF40");
});

it("converts black with full alpha", () => {
Expand All @@ -539,16 +539,29 @@ describe("Color", () => {
it("accepts alpha override parameter", () => {
expect(new Color(0, 0, 255, 1).toHex8(0)).toEqual("#0000FF00");
expect(new Color(0, 0, 255, 0).toHex8(1)).toEqual("#0000FFFF");
expect(new Color(0, 0, 255, 0).toHex8(0.5)).toEqual("#0000FF7F");
expect(new Color(0, 0, 255, 0).toHex8(0.5)).toEqual("#0000FF80");
});

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("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("uses the same rounding for rgba() alpha", () => {
expect(new Color().parseRGB("rgba(0, 0, 255, 0.5)").toHex8()).toEqual(
"#0000FF80",
);
});

it("alpha override does not modify instance alpha", () => {
Expand Down