From 0d3455bf72b216c7700eed83bf0d7abdb4f195f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 22 Aug 2024 13:27:12 +0200 Subject: [PATCH 1/2] Use context2D to convert CSS4 colors to display-p3 --- src/marks/raster.js | 21 +++++++++++-- test/output/rasterVaporP3.svg | 59 +++++++++++++++++++++++++++++++++++ test/plots/raster-vapor.ts | 18 +++++++++++ 3 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 test/output/rasterVaporP3.svg diff --git a/src/marks/raster.js b/src/marks/raster.js index 2e6c4369aa..83461205ae 100644 --- a/src/marks/raster.js +++ b/src/marks/raster.js @@ -127,6 +127,9 @@ export class Raster extends AbstractRaster { // function, offset into the dense grid based on the current facet index. else if (this.data == null && index) offset = index.fi * n; + // Color space conversion + const colorBytes = this.colorSpace === "srgb" ? rgb : converter(this.colorSpace); + // Render the raster grid to the canvas, blurring if needed. const canvas = document.createElement("canvas"); canvas.width = w; @@ -134,7 +137,7 @@ export class Raster extends AbstractRaster { const context2d = canvas.getContext("2d", {colorSpace: this.colorSpace}); const image = context2d.createImageData(w, h); const imageData = image.data; - let {r, g, b} = rgb(this.fill) ?? {r: 0, g: 0, b: 0}; + let {r, g, b} = colorBytes(this.fill) ?? {r: 0, g: 0, b: 0}; let a = (this.fillOpacity ?? 1) * 255; for (let i = 0; i < n; ++i) { const j = i << 2; @@ -144,7 +147,7 @@ export class Raster extends AbstractRaster { imageData[j + 3] = 0; continue; } - ({r, g, b} = rgb(fi)); + ({r, g, b} = colorBytes(fi)); } if (FO) a = FO[i + offset] * 255; imageData[j + 0] = r; @@ -504,3 +507,17 @@ function denseY(y1, y2, width, height) { } }; } + +// Color space conversion +export function converter(colorSpace) { + const canvas = document.createElement("canvas"); + canvas.width = 1; + canvas.height = 1; + const context = canvas.getContext("2d", {colorSpace, willReadFrequently: true}); + return function (c) { + context.fillStyle = c; + context.fillRect(0, 0, 1, 1); + const [r, g, b] = context.getImageData(0, 0, 1, 1).data; + return {r, g, b}; + }; +} diff --git a/test/output/rasterVaporP3.svg b/test/output/rasterVaporP3.svg new file mode 100644 index 0000000000..d64ac87214 --- /dev/null +++ b/test/output/rasterVaporP3.svg @@ -0,0 +1,59 @@ + + + + + −80 + −60 + −40 + −20 + 0 + 20 + 40 + 60 + 80 + + + + −150 + −100 + −50 + 0 + 50 + 100 + 150 + + + + + \ No newline at end of file diff --git a/test/plots/raster-vapor.ts b/test/plots/raster-vapor.ts index abfe1d7674..f3dcb251c7 100644 --- a/test/plots/raster-vapor.ts +++ b/test/plots/raster-vapor.ts @@ -61,6 +61,24 @@ export async function contourVapor() { }); } +export async function rasterVaporP3() { + return Plot.plot({ + x: {transform: (x) => x - 180}, + y: {transform: (y) => 90 - y}, + color: { + type: "sqrt", + interpolate: (t: number) => `oklch(50% 0.25 ${220 + t * 140}deg)` + }, + marks: [ + Plot.raster(await vapor(), { + width: 360, + height: 180, + colorSpace: "display-p3" + }) + ] + }); +} + export async function rasterVaporPeters() { const radians = Math.PI / 180; const sin = (y) => Math.sin(y * radians); From 0e0ea85ee737f84f253f2820901a7366ffd6de83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Philippe=20Rivi=C3=A8re?= Date: Thu, 22 Aug 2024 15:23:36 +0200 Subject: [PATCH 2/2] * use the canvas converter for colors that d3.rgb can't parse (CSS4) * memoize (with some decent limit) for faster renders * clear rect to guard against semi-transparent color leaks --- src/marks/raster.js | 14 +- test/output/rasterPenguinsCSS4.svg | 415 +++++++++++++++++++++++++++++ test/plots/raster-penguins.ts | 6 + 3 files changed, 431 insertions(+), 4 deletions(-) create mode 100644 test/output/rasterPenguinsCSS4.svg diff --git a/src/marks/raster.js b/src/marks/raster.js index 83461205ae..1ec2bce7f7 100644 --- a/src/marks/raster.js +++ b/src/marks/raster.js @@ -127,8 +127,8 @@ export class Raster extends AbstractRaster { // function, offset into the dense grid based on the current facet index. else if (this.data == null && index) offset = index.fi * n; - // Color space conversion - const colorBytes = this.colorSpace === "srgb" ? rgb : converter(this.colorSpace); + // Color space and CSS4 color conversion + const colorBytes = converter(this.colorSpace); // Render the raster grid to the canvas, blurring if needed. const canvas = document.createElement("canvas"); @@ -508,16 +508,22 @@ function denseY(y1, y2, width, height) { }; } -// Color space conversion +// Color space and CSS4 conversions export function converter(colorSpace) { const canvas = document.createElement("canvas"); canvas.width = 1; canvas.height = 1; const context = canvas.getContext("2d", {colorSpace, willReadFrequently: true}); - return function (c) { + const mem = new Map(); + const canvasConverter = (c) => { + if (mem.has((c = String(c)))) return mem.get(c); context.fillStyle = c; + context.clearRect(0, 0, 1, 1); context.fillRect(0, 0, 1, 1); const [r, g, b] = context.getImageData(0, 0, 1, 1).data; + if (mem.size < 256) mem.set(c, {r, g, b}); return {r, g, b}; }; + let p; + return colorSpace === "srgb" ? (c) => (isNaN((p = rgb(c)).opacity) ? canvasConverter(c) : p) : canvasConverter; } diff --git a/test/output/rasterPenguinsCSS4.svg b/test/output/rasterPenguinsCSS4.svg new file mode 100644 index 0000000000..b4c7751835 --- /dev/null +++ b/test/output/rasterPenguinsCSS4.svg @@ -0,0 +1,415 @@ + + + + + 175 + 180 + 185 + 190 + 195 + 200 + 205 + 210 + 215 + 220 + 225 + 230 + + + ↑ flipper_length_mm + + + + 3,000 + 3,500 + 4,000 + 4,500 + 5,000 + 5,500 + 6,000 + + + body_mass_g → + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/test/plots/raster-penguins.ts b/test/plots/raster-penguins.ts index f428865e6c..8bebc53361 100644 --- a/test/plots/raster-penguins.ts +++ b/test/plots/raster-penguins.ts @@ -26,3 +26,9 @@ export async function rasterPenguinsRandomWalk() { export async function rasterPenguinsBlur() { return rasterPenguins({interpolate: "random-walk", blur: 7}); } + +export async function rasterPenguinsCSS4() { + // observable10 converted to oklch + const scale = d3.scaleOrdinal(["oklch(71.83% 0.176 30.86)", "oklch(54.8% 0.165 265.62)", "oklch(79.71% 0.16 82.35)"]); + return rasterPenguins({interpolate: "random-walk", fill: (d: string) => scale(d["island"])}); +}