diff --git a/src/marks/raster.js b/src/marks/raster.js
index 21e9598e81..504b4a8972 100644
--- a/src/marks/raster.js
+++ b/src/marks/raster.js
@@ -288,9 +288,10 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
(i) => X[i],
(i) => Y[i]
);
- const W = new V.constructor(width * height);
- const I = new Uint8Array(width * height);
+ const W = new V.constructor(width * height).fill(NaN);
+ const S = new Uint8Array(width * height); // 1 if pixel has been seen.
const mix = mixer(V, random);
+
for (let i = 0; i < triangles.length; i += 3) {
const ta = triangles[i];
const tb = triangles[i + 1];
@@ -323,51 +324,38 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
if (gc < 0) continue;
const i = x + width * y;
W[i] = mix(va, ga, vb, gb, vc, gc, x, y);
- I[i] = 1;
+ S[i] = 1;
}
}
}
-
- extrapolateBarycentric(W, I, X, Y, V, width, height, hull, index, mix);
-
+ extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix);
return W;
};
}
-// Extrapolate by finding the closest point on the hull. Optimized with a
-// Delaunay search on the interpolated hull.
+// Extrapolate by finding the closest point on the hull.
function extrapolateBarycentric(W, I, X, Y, V, width, height, hull, index, mix) {
X = Float64Array.from(hull, (i) => X[index[i]]);
Y = Float64Array.from(hull, (i) => Y[index[i]]);
V = Array.from(hull, (i) => V[index[i]]);
-
- const points = [];
- const refs = [];
- for (let j = 0; j < hull.length; ++j) {
- const xa = X.at(j - 1);
- const ya = Y.at(j - 1);
- const dx = X.at(j) - xa;
- const dy = Y.at(j) - ya;
- const s = 1 / (1 + (Math.hypot(dx, dy) << 1));
- for (let d = 0; d < 1; d += s) {
- points.push(xa + d * dx, ya + d * dy);
- refs.push(j);
- }
- }
- const delaunay = new Delaunay(points);
- let iy, ix;
+ const n = X.length;
+ const rays = Array.from({length: n}, (_, j) => ray(j, X, Y));
+ let k = 0;
for (let y = 0; y < height; ++y) {
const yp = y + 0.5;
- ix = iy;
for (let x = 0; x < width; ++x) {
const i = x + width * y;
- const xp = x + 0.5;
if (!I[i]) {
- ix = delaunay.find(xp, yp, ix);
- if (x === 0) iy = ix;
- const j = refs[ix];
- const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
- W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
+ const xp = x + 0.5;
+ for (let l = 0; l < n; ++l) {
+ const j = (n + k + (l % 2 ? (l + 1) / 2 : -l / 2)) % n;
+ if (rays[j](xp, yp)) {
+ const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
+ W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
+ k = j;
+ break;
+ }
+ }
}
}
}
@@ -383,6 +371,42 @@ function segmentProject(x1, y1, x2, y2, x, y) {
return a > 0 && b > 0 ? a / (a + b) : +(a > b);
}
+function cross(xa, ya, xb, yb) {
+ return xa * yb - xb * ya;
+}
+
+function ray(j, X, Y) {
+ const n = X.length;
+ const xc = X.at(j - 2);
+ const yc = Y.at(j - 2);
+ const xa = X.at(j - 1);
+ const ya = Y.at(j - 1);
+ const xb = X[j];
+ const yb = Y[j];
+ const xd = X.at(j + 1 - n);
+ const yd = Y.at(j + 1 - n);
+ const dxab = xa - xb;
+ const dyab = ya - yb;
+ const dxca = xc - xa;
+ const dyca = yc - ya;
+ const dxbd = xb - xd;
+ const dybd = yb - yd;
+ const hab = Math.hypot(dxab, dyab);
+ const hca = Math.hypot(dxca, dyca);
+ const hbd = Math.hypot(dxbd, dybd);
+ return (x, y) => {
+ const dxa = x - xa;
+ const dya = y - ya;
+ const dxb = x - xb;
+ const dyb = y - yb;
+ return (
+ cross(dxa, dya, dxb, dyb) > -1e-6 &&
+ cross(dxa, dya, dxab, dyab) * hca - cross(dxa, dya, dxca, dyca) * hab > -1e-6 &&
+ cross(dxb, dyb, dxbd, dybd) * hab - cross(dxb, dyb, dxab, dyab) * hbd <= 0
+ );
+ };
+}
+
export function interpolateNearest(index, width, height, X, Y, V) {
const W = new V.constructor(width * height);
const delaunay = Delaunay.from(
diff --git a/test/output/rasterCa55Barycentric.svg b/test/output/rasterCa55Barycentric.svg
index ae8c4ed3f2..a252b93c47 100644
--- a/test/output/rasterCa55Barycentric.svg
+++ b/test/output/rasterCa55Barycentric.svg
@@ -14,6 +14,6 @@
}
-
+
\ No newline at end of file
diff --git a/test/output/rasterFacet.svg b/test/output/rasterFacet.svg
new file mode 100644
index 0000000000..a339e632b3
--- /dev/null
+++ b/test/output/rasterFacet.svg
@@ -0,0 +1,88 @@
+
\ No newline at end of file
diff --git a/test/output/rasterPrecision.svg b/test/output/rasterPrecision.svg
new file mode 100644
index 0000000000..58317959bb
--- /dev/null
+++ b/test/output/rasterPrecision.svg
@@ -0,0 +1,59 @@
+
\ No newline at end of file
diff --git a/test/output/rasterVaporEqualEarthBarycentric.svg b/test/output/rasterVaporEqualEarthBarycentric.svg
index 8fb9001ab9..68d95c0ef5 100644
--- a/test/output/rasterVaporEqualEarthBarycentric.svg
+++ b/test/output/rasterVaporEqualEarthBarycentric.svg
@@ -17,7 +17,7 @@
-
+
diff --git a/test/output/rasterWalmartBarycentric.svg b/test/output/rasterWalmartBarycentric.svg
index 44c22fb2ff..7cf25a853b 100644
--- a/test/output/rasterWalmartBarycentric.svg
+++ b/test/output/rasterWalmartBarycentric.svg
@@ -14,7 +14,7 @@
}
-
+
diff --git a/test/output/rasterWalmartBarycentricOpacity.svg b/test/output/rasterWalmartBarycentricOpacity.svg
index f542d1b370..a4517c0413 100644
--- a/test/output/rasterWalmartBarycentricOpacity.svg
+++ b/test/output/rasterWalmartBarycentricOpacity.svg
@@ -14,7 +14,7 @@
}
-
+
diff --git a/test/plots/index.ts b/test/plots/index.ts
index 93bc831398..b15aa96a04 100644
--- a/test/plots/index.ts
+++ b/test/plots/index.ts
@@ -232,6 +232,7 @@ export * from "./random-quantile.js";
export * from "./random-walk.js";
export * from "./raster-ca55.js";
export * from "./raster-penguins.js";
+export * from "./raster-precision.js";
export * from "./raster-vapor.js";
export * from "./raster-walmart.js";
export * from "./rect-band.js";
diff --git a/test/plots/raster-precision.ts b/test/plots/raster-precision.ts
new file mode 100644
index 0000000000..b0ae134dd2
--- /dev/null
+++ b/test/plots/raster-precision.ts
@@ -0,0 +1,40 @@
+import * as Plot from "@observablehq/plot";
+import * as d3 from "d3";
+
+// Test for floating point precision issue in interpolateBarycentric.
+export async function rasterPrecision() {
+ const data = d3.range(4).map((i) => {
+ const x = i % 2;
+ const y = Math.floor(i / 2);
+ return [49.4 + 100 * (x + y), 150.4 + 100 * (x - y)];
+ });
+ return Plot.plot({
+ x: {type: "identity"},
+ y: {type: "identity"},
+ color: {scheme: "Sinebow"},
+ marks: [
+ Plot.raster(data, {
+ fill: (d, i) => i,
+ interpolate: "barycentric"
+ }),
+ Plot.dot(data, {fill: (d, i) => i, stroke: "white"})
+ ]
+ });
+}
+
+export async function rasterFacet() {
+ const points = d3.range(0, 2 * Math.PI, Math.PI / 10).map((d) => [Math.cos(d), Math.sin(d)]);
+ return Plot.plot({
+ aspectRatio: 1,
+ inset: 100,
+ color: {scheme: "Sinebow"},
+ marks: [
+ Plot.raster(points, {
+ fill: "0",
+ fx: (d, i) => i % 2,
+ interpolate: "barycentric"
+ }),
+ Plot.dot(points, {fx: (d, i) => i % 2, fill: "0", stroke: "white"})
+ ]
+ });
+}