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 @@ + + + + + 0 + + + 1 + + + + + + + + + + + + −1 + 0 + 1 + + + + + + + + + + + + + 0 + + + 0 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 @@ + + + + + + + + + + + + + 350 + 300 + 250 + 200 + 150 + 100 + 50 + + + + + + + + + + + 100 + 200 + 300 + 400 + 500 + 600 + + + + + + + + + + + \ 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"}) + ] + }); +}