diff --git a/src/marks/raster.js b/src/marks/raster.js
index 2e6c4369aa..1ec2bce7f7 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 and CSS4 color conversion
+ const colorBytes = 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,23 @@ function denseY(y1, y2, width, height) {
}
};
}
+
+// 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});
+ 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 @@
+
\ No newline at end of file
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 @@
+
\ 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"])});
+}
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);