1
1
import { blurImage , Delaunay , randomLcg , rgb } from "d3" ;
2
2
import { valueObject } from "../channel.js" ;
3
3
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" ;
5
5
import { maybeColorChannel , maybeNumberChannel } from "../options.js" ;
6
6
import { Mark } from "../mark.js" ;
7
7
import { applyAttr , applyDirectStyles , applyIndirectStyles , applyTransform , impliedString } from "../style.js" ;
@@ -282,31 +282,16 @@ export function interpolateNone(index, width, height, X, Y, V) {
282
282
283
283
export function interpolatorBarycentric ( { random = randomLcg ( 42 ) } = { } ) {
284
284
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
-
306
285
// 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.
309
293
const mix = mixer ( V , random ) ;
294
+
310
295
for ( let i = 0 ; i < triangles . length ; i += 3 ) {
311
296
const ta = triangles [ i ] ;
312
297
const tb = triangles [ i + 1 ] ;
@@ -323,9 +308,9 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
323
308
const y2 = Math . max ( Ay , By , Cy ) ;
324
309
const z = ( By - Cy ) * ( Ax - Cx ) + ( Ay - Cy ) * ( Cx - Bx ) ;
325
310
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 ] ] ;
329
314
for ( let x = Math . floor ( x1 ) ; x < x2 ; ++ x ) {
330
315
for ( let y = Math . floor ( y1 ) ; y < y2 ; ++ y ) {
331
316
if ( x < 0 || x >= width || y < 0 || y >= height ) continue ;
@@ -337,14 +322,91 @@ export function interpolatorBarycentric({random = randomLcg(42)} = {}) {
337
322
if ( gb < 0 ) continue ;
338
323
const gc = 1 - ga - gb ;
339
324
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 ;
341
328
}
342
329
}
343
330
}
331
+ extrapolateBarycentric ( W , S , X , Y , V , width , height , hull , index , mix ) ;
344
332
return W ;
345
333
} ;
346
334
}
347
335
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
+
348
410
export function interpolateNearest ( index , width , height , X , Y , V ) {
349
411
const W = new V . constructor ( width * height ) ;
350
412
const delaunay = Delaunay . from (
0 commit comments