From 2d0f753c401605f50b05e3021ccbe27a2db566d7 Mon Sep 17 00:00:00 2001 From: ImmortalRabbit <29354535+ImmortalRabbit@users.noreply.github.com> Date: Mon, 4 Aug 2025 16:59:11 +0100 Subject: [PATCH 1/4] Remove memoery alloction for InterpolateColors following FIXME comment --- .../src/internal/data-grid/color-parser.ts | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/packages/core/src/internal/data-grid/color-parser.ts b/packages/core/src/internal/data-grid/color-parser.ts index e944efc00..cba31a5aa 100644 --- a/packages/core/src/internal/data-grid/color-parser.ts +++ b/packages/core/src/internal/data-grid/color-parser.ts @@ -98,7 +98,17 @@ export function interpolateColors(leftColor: string, rightColor: string, val: nu if (val >= 1) return rightColor; // Parse to rgba returns straight alpha colors, for interpolation we want pre-multiplied alpha - // FIXME: This can be faster if instead of makign an array we just use variables. No memory allocation. + const [lr, lg, lb, la] = parseToRgba(leftColor); + const [rr, rg, rb, ra] = parseToRgba(rightColor); + + const leftR = lr * la; + const leftG = lg * la; + const leftB = lb * la; + + const rightR = rr * ra; + const rightG = rg * ra; + const rightB = rb * ra; + const left = [...parseToRgba(leftColor)]; left[0] = left[0] * left[3]; left[1] = left[1] * left[3]; @@ -111,11 +121,11 @@ export function interpolateColors(leftColor: string, rightColor: string, val: nu const hScaler = val; const nScaler = 1 - val; - const a = left[3] * nScaler + right[3] * hScaler; + const a = la * nScaler + ra * hScaler; // now we need to divide the alpha back out to get linear alpha back for the final result - const r = Math.floor((left[0] * nScaler + right[0] * hScaler) / a); - const g = Math.floor((left[1] * nScaler + right[1] * hScaler) / a); - const b = Math.floor((left[2] * nScaler + right[2] * hScaler) / a); + const r = Math.floor((leftR * nScaler + rightR * hScaler) / a); + const g = Math.floor((leftG * nScaler + rightG * hScaler) / a); + const b = Math.floor((leftB * nScaler + rightB * hScaler) / a); return `rgba(${r}, ${g}, ${b}, ${a})`; } From 51ff01be02480ee27ca274f0a490cdde680f3d45 Mon Sep 17 00:00:00 2001 From: ImmortalRabbit <29354535+ImmortalRabbit@users.noreply.github.com> Date: Sat, 9 Aug 2025 00:09:43 +0100 Subject: [PATCH 2/4] Remove old code --- packages/core/src/internal/data-grid/color-parser.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/packages/core/src/internal/data-grid/color-parser.ts b/packages/core/src/internal/data-grid/color-parser.ts index cba31a5aa..9cbb768e6 100644 --- a/packages/core/src/internal/data-grid/color-parser.ts +++ b/packages/core/src/internal/data-grid/color-parser.ts @@ -109,15 +109,6 @@ export function interpolateColors(leftColor: string, rightColor: string, val: nu const rightG = rg * ra; const rightB = rb * ra; - const left = [...parseToRgba(leftColor)]; - left[0] = left[0] * left[3]; - left[1] = left[1] * left[3]; - left[2] = left[2] * left[3]; - const right = [...parseToRgba(rightColor)]; - right[0] = right[0] * right[3]; - right[1] = right[1] * right[3]; - right[2] = right[2] * right[3]; - const hScaler = val; const nScaler = 1 - val; From 99a43312e1a6ab63d2ee9019c76dbb3012eeb5e5 Mon Sep 17 00:00:00 2001 From: lukasmasuch Date: Thu, 14 Aug 2025 13:44:19 +0200 Subject: [PATCH 3/4] Prevent divison by zero --- packages/core/src/internal/data-grid/color-parser.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/core/src/internal/data-grid/color-parser.ts b/packages/core/src/internal/data-grid/color-parser.ts index 9cbb768e6..f5741a534 100644 --- a/packages/core/src/internal/data-grid/color-parser.ts +++ b/packages/core/src/internal/data-grid/color-parser.ts @@ -113,6 +113,8 @@ export function interpolateColors(leftColor: string, rightColor: string, val: nu const nScaler = 1 - val; const a = la * nScaler + ra * hScaler; + // If both colors are fully transparent the resulting alpha can be 0, avoid dividing by 0 + if (a === 0) return "rgba(0, 0, 0, 0)"; // now we need to divide the alpha back out to get linear alpha back for the final result const r = Math.floor((leftR * nScaler + rightR * hScaler) / a); const g = Math.floor((leftG * nScaler + rightG * hScaler) / a); From adf1e3295a9d88caa60a1468ff15e1e4634ecb5f Mon Sep 17 00:00:00 2001 From: lukasmasuch Date: Thu, 14 Aug 2025 13:44:26 +0200 Subject: [PATCH 4/4] Extend unit tests --- packages/core/test/color-parser.test.ts | 33 +++++++++++++++++++++++-- 1 file changed, 31 insertions(+), 2 deletions(-) diff --git a/packages/core/test/color-parser.test.ts b/packages/core/test/color-parser.test.ts index a7400cea8..56381c9f9 100644 --- a/packages/core/test/color-parser.test.ts +++ b/packages/core/test/color-parser.test.ts @@ -1,17 +1,41 @@ /* eslint-disable sonarjs/no-duplicate-string */ +import { describe, expect, test } from "vitest"; import { blend, + getLuminance, interpolateColors, parseToRgba, withAlpha, - getLuminance, } from "../src/internal/data-grid/color-parser.js"; -import { expect, describe, test } from "vitest"; describe("interpolateColors", () => { test("Smoke test", () => { expect(interpolateColors("rgba(0, 0, 0, 1)", "rgba(255, 255, 255, 1)", 0.5)).toEqual("rgba(127, 127, 127, 1)"); }); + + test("Fully transparent inputs do not produce NaN (alpha 0)", () => { + expect(interpolateColors("rgba(0, 0, 0, 0)", "rgba(0, 0, 0, 0)", 0.5)).toEqual("rgba(0, 0, 0, 0)"); + }); + + test("Pre-multiplied alpha behavior with one side transparent", () => { + // Left transparent, right opaque white + expect(interpolateColors("rgba(0, 0, 0, 0)", "rgba(255, 255, 255, 1)", 0.5)).toEqual( + "rgba(255, 255, 255, 0.5)" + ); + // Right transparent, left opaque white (symmetry check) + expect(interpolateColors("rgba(255, 255, 255, 1)", "rgba(0, 0, 0, 0)", 0.5)).toEqual( + "rgba(255, 255, 255, 0.5)" + ); + }); + + test("Clamp outside range: val <= 0 returns left, val >= 1 returns right", () => { + expect(interpolateColors("rgba(10, 20, 30, 1)", "rgba(200, 210, 220, 1)", -0.25)).toEqual( + "rgba(10, 20, 30, 1)" + ); + expect(interpolateColors("rgba(10, 20, 30, 1)", "rgba(200, 210, 220, 1)", 1.25)).toEqual( + "rgba(200, 210, 220, 1)" + ); + }); }); describe("parseToRgba", () => { @@ -36,4 +60,9 @@ describe("getLuminance", () => { test("Smoke test", () => { expect(getLuminance("rgba(255, 255, 255, 1)")).toEqual(1); }); + + test("Transparent has zero luminance", () => { + expect(getLuminance("rgba(0, 0, 0, 0)")).toEqual(0); + expect(getLuminance("transparent")).toEqual(0); + }); });