Skip to content

Commit 9246dae

Browse files
Filmbostock
andauthored
fix barycentric extrapolation (#1701)
Fix barycentric extrapolation Note: When points on the hull are collinear, the extrapolation fails in some regions. I've found a way to fix this by reprojecting the points (to make the hull slightly bulge out, which resolves the ties), but in the end it was too much code for a use case that is pretty bad anyway (the delaunay triangulation itself being unstable in that case). This seems to be quite enough. closes #1700 --------- Co-authored-by: Mike Bostock <[email protected]>
1 parent fe1116b commit 9246dae

10 files changed

+283
-33
lines changed

src/marks/raster.js

Lines changed: 90 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {blurImage, Delaunay, randomLcg, rgb} from "d3";
22
import {valueObject} from "../channel.js";
33
import {create} from "../context.js";
4-
import {map, first, second, third, isTuples, isNumeric, isTemporal, take, identity} from "../options.js";
4+
import {map, first, second, third, isTuples, isNumeric, isTemporal, identity} from "../options.js";
55
import {maybeColorChannel, maybeNumberChannel} from "../options.js";
66
import {Mark} from "../mark.js";
77
import {applyAttr, applyDirectStyles, applyIndirectStyles, applyTransform, impliedString} from "../style.js";
@@ -282,31 +282,16 @@ export function interpolateNone(index, width, height, X, Y, V) {
282282

283283
export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
284284
return (index, width, height, X, Y, V) => {
285-
// Flatten the input coordinates to prepare to insert extrapolated points
286-
// along the perimeter of the grid (so there’s no blank output).
287-
const n = index.length;
288-
const nw = width >> 2;
289-
const nh = (height >> 2) - 1;
290-
const m = n + nw * 2 + nh * 2;
291-
const XY = new Float64Array(m * 2);
292-
for (let i = 0; i < n; ++i) (XY[i * 2] = X[index[i]]), (XY[i * 2 + 1] = Y[index[i]]);
293-
294-
// Add points along each edge, making sure to include the four corners for
295-
// complete coverage (with no chamfered edges).
296-
let i = n;
297-
const addPoint = (x, y) => ((XY[i * 2] = x), (XY[i * 2 + 1] = y), i++);
298-
for (let j = 0; j <= nw; ++j) addPoint((j / nw) * width, 0), addPoint((j / nw) * width, height);
299-
for (let j = 0; j < nh; ++j) addPoint(width, (j / nh) * height), addPoint(0, (j / nh) * height);
300-
301-
// To each edge point, assign the closest (non-extrapolated) value.
302-
V = take(V, index);
303-
const delaunay = new Delaunay(XY.subarray(0, n * 2));
304-
for (let j = n, ij; j < m; ++j) V[j] = V[(ij = delaunay.find(XY[j * 2], XY[j * 2 + 1], ij))];
305-
306285
// Interpolate the interior of all triangles with barycentric coordinates
307-
const {points, triangles} = new Delaunay(XY);
308-
const W = new V.constructor(width * height);
286+
const {points, triangles, hull} = Delaunay.from(
287+
index,
288+
(i) => X[i],
289+
(i) => Y[i]
290+
);
291+
const W = new V.constructor(width * height).fill(NaN);
292+
const S = new Uint8Array(width * height); // 1 if pixel has been seen.
309293
const mix = mixer(V, random);
294+
310295
for (let i = 0; i < triangles.length; i += 3) {
311296
const ta = triangles[i];
312297
const tb = triangles[i + 1];
@@ -323,9 +308,9 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
323308
const y2 = Math.max(Ay, By, Cy);
324309
const z = (By - Cy) * (Ax - Cx) + (Ay - Cy) * (Cx - Bx);
325310
if (!z) continue;
326-
const va = V[ta];
327-
const vb = V[tb];
328-
const vc = V[tc];
311+
const va = V[index[ta]];
312+
const vb = V[index[tb]];
313+
const vc = V[index[tc]];
329314
for (let x = Math.floor(x1); x < x2; ++x) {
330315
for (let y = Math.floor(y1); y < y2; ++y) {
331316
if (x < 0 || x >= width || y < 0 || y >= height) continue;
@@ -337,14 +322,91 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
337322
if (gb < 0) continue;
338323
const gc = 1 - ga - gb;
339324
if (gc < 0) continue;
340-
W[x + width * y] = mix(va, ga, vb, gb, vc, gc, x, y);
325+
const i = x + width * y;
326+
W[i] = mix(va, ga, vb, gb, vc, gc, x, y);
327+
S[i] = 1;
341328
}
342329
}
343330
}
331+
extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix);
344332
return W;
345333
};
346334
}
347335

336+
// Extrapolate by finding the closest point on the hull.
337+
function extrapolateBarycentric(W, S, X, Y, V, width, height, hull, index, mix) {
338+
X = Float64Array.from(hull, (i) => X[index[i]]);
339+
Y = Float64Array.from(hull, (i) => Y[index[i]]);
340+
V = Array.from(hull, (i) => V[index[i]]);
341+
const n = X.length;
342+
const rays = Array.from({length: n}, (_, j) => ray(j, X, Y));
343+
let k = 0;
344+
for (let y = 0; y < height; ++y) {
345+
const yp = y + 0.5;
346+
for (let x = 0; x < width; ++x) {
347+
const i = x + width * y;
348+
if (!S[i]) {
349+
const xp = x + 0.5;
350+
for (let l = 0; l < n; ++l) {
351+
const j = (n + k + (l % 2 ? (l + 1) / 2 : -l / 2)) % n;
352+
if (rays[j](xp, yp)) {
353+
const t = segmentProject(X.at(j - 1), Y.at(j - 1), X[j], Y[j], xp, yp);
354+
W[i] = mix(V.at(j - 1), t, V[j], 1 - t, V[j], 0, x, y);
355+
k = j;
356+
break;
357+
}
358+
}
359+
}
360+
}
361+
}
362+
}
363+
364+
// Projects a point p = [x, y] onto the line segment [p1, p2], returning the
365+
// projected coordinates p’ as t in [0, 1] with p’ = t p1 + (1 - t) p2.
366+
function segmentProject(x1, y1, x2, y2, x, y) {
367+
const dx = x2 - x1;
368+
const dy = y2 - y1;
369+
const a = dx * (x2 - x) + dy * (y2 - y);
370+
const b = dx * (x - x1) + dy * (y - y1);
371+
return a > 0 && b > 0 ? a / (a + b) : +(a > b);
372+
}
373+
374+
function cross(xa, ya, xb, yb) {
375+
return xa * yb - xb * ya;
376+
}
377+
378+
function ray(j, X, Y) {
379+
const n = X.length;
380+
const xc = X.at(j - 2);
381+
const yc = Y.at(j - 2);
382+
const xa = X.at(j - 1);
383+
const ya = Y.at(j - 1);
384+
const xb = X[j];
385+
const yb = Y[j];
386+
const xd = X.at(j + 1 - n);
387+
const yd = Y.at(j + 1 - n);
388+
const dxab = xa - xb;
389+
const dyab = ya - yb;
390+
const dxca = xc - xa;
391+
const dyca = yc - ya;
392+
const dxbd = xb - xd;
393+
const dybd = yb - yd;
394+
const hab = Math.hypot(dxab, dyab);
395+
const hca = Math.hypot(dxca, dyca);
396+
const hbd = Math.hypot(dxbd, dybd);
397+
return (x, y) => {
398+
const dxa = x - xa;
399+
const dya = y - ya;
400+
const dxb = x - xb;
401+
const dyb = y - yb;
402+
return (
403+
cross(dxa, dya, dxb, dyb) > -1e-6 &&
404+
cross(dxa, dya, dxab, dyab) * hca - cross(dxa, dya, dxca, dyca) * hab > -1e-6 &&
405+
cross(dxb, dyb, dxbd, dybd) * hab - cross(dxb, dyb, dxab, dyab) * hbd <= 0
406+
);
407+
};
408+
}
409+
348410
export function interpolateNearest(index, width, height, X, Y, V) {
349411
const W = new V.constructor(width * height);
350412
const delaunay = Delaunay.from(

test/output/rasterCa55Barycentric.svg

Lines changed: 1 addition & 1 deletion
Loading

test/output/rasterFacet.svg

Lines changed: 88 additions & 0 deletions
Loading

test/output/rasterPenguinsBarycentric.svg

Lines changed: 1 addition & 1 deletion
Loading

0 commit comments

Comments
 (0)