regl-scatterplot
Version:
A WebGL-Powered Scalable Interactive Scatter Plot Library
1,836 lines (1,580 loc) • 287 kB
JavaScript
import createPubSub from 'pub-sub-es';
import createOriginalRegl from 'regl';
// @flekschas/utils v0.32.2 Copyright 2023 Fritz Lekschas
/* eslint no-param-reassign:0 */
/**
* Cubic in easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const cubicIn = (t) => t * t * t;
/**
* Cubic in and out easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const cubicInOut = (t) =>
t < 0.5 ? 4 * t * t * t : (t - 1) * (2 * t - 2) * (2 * t - 2) + 1;
/**
* Cubic out easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const cubicOut = (t) => --t * t * t + 1;
/**
* Linear easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} Same as the input
*/
const linear = (t) => t;
/**
* Quadratic in easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const quadIn = (t) => t * t;
/**
* Quadratic in and out easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const quadInOut = (t) => (t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t);
/**
* Quadratic out easing function
* @param {number} t - The input time to be eased. Must be in [0, 1] where `0`
* refers to the start and `1` to the end
* @return {number} The eased time
*/
const quadOut = (t) => t * (2 - t);
/**
* Identity function
* @type {<T>(x: T) => T}
* @param {*} x - Any kind of value
* @return {*} `x`
*/
const identity = (x) => x;
/**
* Check if two arrays contain the same elements
* @type {<T>(a: T[], b: T[]) => Boolean}
* @param {array} a - First array
* @param {array} b - Second array
* @return {boolean} If `true` the two arrays contain the same elements
*/
const hasSameElements = (a, b) => {
if (a === b) return true;
if (a.length !== b.length) return false;
const aSet = new Set(a);
const bSet = new Set(b);
// Since the arrays could contain duplicates, we have to check the set length
// as well
if (aSet.size !== bSet.size) return false;
return b.every((element) => aSet.has(element));
};
/**
* Vector L2 norm
*
* @description
* This is identical but much faster than `Math.hypot(...v)`
*
* @param {number[]} v - Numerical vector
* @return {number} L2 norm
*/
const l2Norm = (v) => Math.sqrt(v.reduce((sum, x) => sum + x ** 2, 0));
/**
* Get the maximum number of a vector while ignoring NaNs
*
* @description
* This version is muuuch faster than `Math.max(...v)` and supports vectors
* longer than 256^2, which is a limitation of `Math.max.apply(null, v)`.
*
* @param {number[]} v - Numerical vector
* @return {number} The largest number
*/
const max$1 = (v) =>
v.reduce((_max, a) => (a > _max ? a : _max), -Infinity);
/**
* Initialize an array of a certain length using a mapping function
*
* @description
* This is equivalent to `Array.from({ length }, mapFn)` but about 60% faster
*
* @param {number} length - Size of the array
* @param {function} mapFn - Mapping function
* @return {array} Initialized array
* @type {<T = number>(length: number, mapFn: (i: number, length: number) => T) => T[]}
*/
const rangeMap = (length, mapFn = (x) => x) => {
const out = [];
for (let i = 0; i < length; i++) {
out.push(mapFn(i, length));
}
return out;
};
/**
* Get the unique union of two vectors of integers
* @param {number[]} v - First vector of integers
* @param {number[]} w - Second vector of integers
* @return {number[]} Unique union of `v` and `w`
*/
const unionIntegers = (v, w) => {
const a = [];
v.forEach((x) => {
a[x] = true;
});
w.forEach((x) => {
a[x] = true;
});
return a.reduce((union, value, i) => {
if (value) union.push(i);
return union;
}, []);
};
/**
* Assign properties, constructors, etc. to an object
*
* @param {object} target - The target object that gets `sources` assigned to it
* @param {}
* @return {object}
*/
const assign = (target, ...sources) => {
sources.forEach((source) => {
// eslint-disable-next-line no-shadow
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
// By default, Object.assign copies enumerable Symbols, too
Object.getOwnPropertySymbols(source).forEach((symbol) => {
const descriptor = Object.getOwnPropertyDescriptor(source, symbol);
if (descriptor.enumerable) {
descriptors[symbol] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
};
/**
* Convenience function to compose functions
* @param {...function} fns - Array of functions
* @return {function} The composed function
*/
const pipe =
(...fns) =>
/**
* @param {*} x - Some value
* @return {*} Output of the composed function
*/
(x) =>
fns.reduce((y, f) => f(y), x);
/**
* Assign a constructor to the object
* @param {function} constructor - Constructor functions
*/
const withConstructor = (constructor) => (self) =>
assign(
{
__proto__: {
constructor,
},
},
self
);
/**
* Assign a static property to an object
* @param {string} name - Name of the property
* @param {*} value - Static value
*/
const withStaticProperty = (name, value) => (self) =>
assign(self, {
get [name]() {
return value;
},
});
/**
* L2 distance between a pair of points
*
* @description
* Identical but much faster than `l2Dist([fromX, fromY], [toX, toY])`
*
* @param {number} fromX - X coordinate of the first point
* @param {number} fromY - Y coordinate of the first point
* @param {number} toX - X coordinate of the second point
* @param {number} toY - Y coordinate of the first point
* @return {number} L2 distance
*/
const l2PointDist = (fromX, fromY, toX, toY) =>
Math.sqrt((fromX - toX) ** 2 + (fromY - toY) ** 2);
/**
* Create a worker from a function
* @param {function} fn - Function to be turned into a worker
* @return {Worker} Worker function
*/
const createWorker$1 = (fn) =>
new Worker(
window.URL.createObjectURL(
new Blob([`(${fn.toString()})()`], { type: 'text/javascript' })
)
);
/**
* Get a promise that resolves after the next `n` animation frames
* @param {number} n - Number of animation frames to wait
* @return {Promise} A promise that resolves after the next `n` animation frames
*/
const nextAnimationFrame = (n = 1) =>
new Promise((resolve) => {
let i = 0;
const raf = () =>
requestAnimationFrame(() => {
i++;
if (i < n) raf();
else resolve();
});
raf();
});
/**
* Throttle and debounce a function call
*
* Throttling a function call means that the function is called at most every
* `interval` milliseconds no matter how frequently you trigger a call.
* Debouncing a function call means that the function is called the earliest
* after `finalWait` milliseconds wait time where the function was not called.
* Combining the two ensures that the function is called at most every
* `interval` milliseconds and is ensured to be called with the very latest
* arguments after after `finalWait` milliseconds wait time at the end.
*
* The following imaginary scenario describes the behavior:
*
* MS | throttleTime=3 and debounceTime=3
* 1. y(f, 3, 3)(args1) => f(args1) called
* 2. y(f, 3, 3)(args2) => call ignored due to throttling
* 3. y(f, 3, 3)(args3) => call ignored due to throttling
* 4. y(f, 3, 3)(args4) => f(args4) called
* 5. y(f, 3, 3)(args5) => all ignored due to throttling
* 6. No call => nothing
* 7. No call => f(args5) called due to debouncing
*
* @param {functon} fn - Function to be throttled and debounced
* @param {number} throttleTime - Throttle intevals in milliseconds
* @param {number} debounceTime - Debounce wait time in milliseconds. By default
* this is the same as `throttleTime`.
* @return {function} - Throttled and debounced function
*/
const throttleAndDebounce = (fn, throttleTime, debounceTime = null) => {
let timeout;
let blockedCalls = 0;
// eslint-disable-next-line no-param-reassign
debounceTime = debounceTime === null ? throttleTime : debounceTime;
const debounced = (...args) => {
const later = () => {
// Since we throttle and debounce we should check whether there were
// actually multiple attempts to call this function after the most recent
// throttled call. If there were no more calls we don't have to call
// the function again.
if (blockedCalls > 0) {
fn(...args);
blockedCalls = 0;
}
};
clearTimeout(timeout);
timeout = setTimeout(later, debounceTime);
};
let isWaiting = false;
const throttledAndDebounced = (...args) => {
if (!isWaiting) {
fn(...args);
debounced(...args);
isWaiting = true;
blockedCalls = 0;
setTimeout(() => {
isWaiting = false;
}, throttleTime);
} else {
blockedCalls++;
debounced(...args);
}
};
throttledAndDebounced.reset = () => {
isWaiting = false;
};
throttledAndDebounced.cancel = () => {
clearTimeout(timeout);
};
throttledAndDebounced.now = (...args) => fn(...args);
return throttledAndDebounced;
};
/**
* Common utilities
* @module glMatrix
*/
// Configuration Constants
var EPSILON = 0.000001;
var ARRAY_TYPE = typeof Float32Array !== 'undefined' ? Float32Array : Array;
if (!Math.hypot) Math.hypot = function () {
var y = 0,
i = arguments.length;
while (i--) {
y += arguments[i] * arguments[i];
}
return Math.sqrt(y);
};
/**
* 4x4 Matrix<br>Format: column-major, when typed out it looks like row-major<br>The matrices are being post multiplied.
* @module mat4
*/
/**
* Creates a new identity mat4
*
* @returns {mat4} a new 4x4 matrix
*/
function create$2() {
var out = new ARRAY_TYPE(16);
if (ARRAY_TYPE != Float32Array) {
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
}
out[0] = 1;
out[5] = 1;
out[10] = 1;
out[15] = 1;
return out;
}
/**
* Creates a new mat4 initialized with values from an existing matrix
*
* @param {ReadonlyMat4} a matrix to clone
* @returns {mat4} a new 4x4 matrix
*/
function clone(a) {
var out = new ARRAY_TYPE(16);
out[0] = a[0];
out[1] = a[1];
out[2] = a[2];
out[3] = a[3];
out[4] = a[4];
out[5] = a[5];
out[6] = a[6];
out[7] = a[7];
out[8] = a[8];
out[9] = a[9];
out[10] = a[10];
out[11] = a[11];
out[12] = a[12];
out[13] = a[13];
out[14] = a[14];
out[15] = a[15];
return out;
}
/**
* Inverts a mat4
*
* @param {mat4} out the receiving matrix
* @param {ReadonlyMat4} a the source matrix
* @returns {mat4} out
*/
function invert(out, a) {
var a00 = a[0],
a01 = a[1],
a02 = a[2],
a03 = a[3];
var a10 = a[4],
a11 = a[5],
a12 = a[6],
a13 = a[7];
var a20 = a[8],
a21 = a[9],
a22 = a[10],
a23 = a[11];
var a30 = a[12],
a31 = a[13],
a32 = a[14],
a33 = a[15];
var b00 = a00 * a11 - a01 * a10;
var b01 = a00 * a12 - a02 * a10;
var b02 = a00 * a13 - a03 * a10;
var b03 = a01 * a12 - a02 * a11;
var b04 = a01 * a13 - a03 * a11;
var b05 = a02 * a13 - a03 * a12;
var b06 = a20 * a31 - a21 * a30;
var b07 = a20 * a32 - a22 * a30;
var b08 = a20 * a33 - a23 * a30;
var b09 = a21 * a32 - a22 * a31;
var b10 = a21 * a33 - a23 * a31;
var b11 = a22 * a33 - a23 * a32; // Calculate the determinant
var det = b00 * b11 - b01 * b10 + b02 * b09 + b03 * b08 - b04 * b07 + b05 * b06;
if (!det) {
return null;
}
det = 1.0 / det;
out[0] = (a11 * b11 - a12 * b10 + a13 * b09) * det;
out[1] = (a02 * b10 - a01 * b11 - a03 * b09) * det;
out[2] = (a31 * b05 - a32 * b04 + a33 * b03) * det;
out[3] = (a22 * b04 - a21 * b05 - a23 * b03) * det;
out[4] = (a12 * b08 - a10 * b11 - a13 * b07) * det;
out[5] = (a00 * b11 - a02 * b08 + a03 * b07) * det;
out[6] = (a32 * b02 - a30 * b05 - a33 * b01) * det;
out[7] = (a20 * b05 - a22 * b02 + a23 * b01) * det;
out[8] = (a10 * b10 - a11 * b08 + a13 * b06) * det;
out[9] = (a01 * b08 - a00 * b10 - a03 * b06) * det;
out[10] = (a30 * b04 - a31 * b02 + a33 * b00) * det;
out[11] = (a21 * b02 - a20 * b04 - a23 * b00) * det;
out[12] = (a11 * b07 - a10 * b09 - a12 * b06) * det;
out[13] = (a00 * b09 - a01 * b07 + a02 * b06) * det;
out[14] = (a31 * b01 - a30 * b03 - a32 * b00) * det;
out[15] = (a20 * b03 - a21 * b01 + a22 * b00) * det;
return out;
}
/**
* Multiplies two mat4s
*
* @param {mat4} out the receiving matrix
* @param {ReadonlyMat4} a the first operand
* @param {ReadonlyMat4} b the second operand
* @returns {mat4} out
*/
function multiply(out, a, b) {
var a00 = a[0],
a01 = a[1],
a02 = a[2],
a03 = a[3];
var a10 = a[4],
a11 = a[5],
a12 = a[6],
a13 = a[7];
var a20 = a[8],
a21 = a[9],
a22 = a[10],
a23 = a[11];
var a30 = a[12],
a31 = a[13],
a32 = a[14],
a33 = a[15]; // Cache only the current line of the second matrix
var b0 = b[0],
b1 = b[1],
b2 = b[2],
b3 = b[3];
out[0] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[1] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[2] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[3] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
b0 = b[4];
b1 = b[5];
b2 = b[6];
b3 = b[7];
out[4] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[5] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[6] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[7] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
b0 = b[8];
b1 = b[9];
b2 = b[10];
b3 = b[11];
out[8] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[9] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[10] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[11] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
b0 = b[12];
b1 = b[13];
b2 = b[14];
b3 = b[15];
out[12] = b0 * a00 + b1 * a10 + b2 * a20 + b3 * a30;
out[13] = b0 * a01 + b1 * a11 + b2 * a21 + b3 * a31;
out[14] = b0 * a02 + b1 * a12 + b2 * a22 + b3 * a32;
out[15] = b0 * a03 + b1 * a13 + b2 * a23 + b3 * a33;
return out;
}
/**
* Creates a matrix from a vector translation
* This is equivalent to (but much faster than):
*
* mat4.identity(dest);
* mat4.translate(dest, dest, vec);
*
* @param {mat4} out mat4 receiving operation result
* @param {ReadonlyVec3} v Translation vector
* @returns {mat4} out
*/
function fromTranslation(out, v) {
out[0] = 1;
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = 1;
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = 1;
out[11] = 0;
out[12] = v[0];
out[13] = v[1];
out[14] = v[2];
out[15] = 1;
return out;
}
/**
* Creates a matrix from a vector scaling
* This is equivalent to (but much faster than):
*
* mat4.identity(dest);
* mat4.scale(dest, dest, vec);
*
* @param {mat4} out mat4 receiving operation result
* @param {ReadonlyVec3} v Scaling vector
* @returns {mat4} out
*/
function fromScaling(out, v) {
out[0] = v[0];
out[1] = 0;
out[2] = 0;
out[3] = 0;
out[4] = 0;
out[5] = v[1];
out[6] = 0;
out[7] = 0;
out[8] = 0;
out[9] = 0;
out[10] = v[2];
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Creates a matrix from a given angle around a given axis
* This is equivalent to (but much faster than):
*
* mat4.identity(dest);
* mat4.rotate(dest, dest, rad, axis);
*
* @param {mat4} out mat4 receiving operation result
* @param {Number} rad the angle to rotate the matrix by
* @param {ReadonlyVec3} axis the axis to rotate around
* @returns {mat4} out
*/
function fromRotation(out, rad, axis) {
var x = axis[0],
y = axis[1],
z = axis[2];
var len = Math.hypot(x, y, z);
var s, c, t;
if (len < EPSILON) {
return null;
}
len = 1 / len;
x *= len;
y *= len;
z *= len;
s = Math.sin(rad);
c = Math.cos(rad);
t = 1 - c; // Perform rotation-specific matrix multiplication
out[0] = x * x * t + c;
out[1] = y * x * t + z * s;
out[2] = z * x * t - y * s;
out[3] = 0;
out[4] = x * y * t - z * s;
out[5] = y * y * t + c;
out[6] = z * y * t + x * s;
out[7] = 0;
out[8] = x * z * t + y * s;
out[9] = y * z * t - x * s;
out[10] = z * z * t + c;
out[11] = 0;
out[12] = 0;
out[13] = 0;
out[14] = 0;
out[15] = 1;
return out;
}
/**
* Returns the translation vector component of a transformation
* matrix. If a matrix is built with fromRotationTranslation,
* the returned vector will be the same as the translation vector
* originally supplied.
* @param {vec3} out Vector to receive translation component
* @param {ReadonlyMat4} mat Matrix to be decomposed (input)
* @return {vec3} out
*/
function getTranslation(out, mat) {
out[0] = mat[12];
out[1] = mat[13];
out[2] = mat[14];
return out;
}
/**
* Returns the scaling factor component of a transformation
* matrix. If a matrix is built with fromRotationTranslationScale
* with a normalized Quaternion paramter, the returned vector will be
* the same as the scaling vector
* originally supplied.
* @param {vec3} out Vector to receive scaling factor component
* @param {ReadonlyMat4} mat Matrix to be decomposed (input)
* @return {vec3} out
*/
function getScaling(out, mat) {
var m11 = mat[0];
var m12 = mat[1];
var m13 = mat[2];
var m21 = mat[4];
var m22 = mat[5];
var m23 = mat[6];
var m31 = mat[8];
var m32 = mat[9];
var m33 = mat[10];
out[0] = Math.hypot(m11, m12, m13);
out[1] = Math.hypot(m21, m22, m23);
out[2] = Math.hypot(m31, m32, m33);
return out;
}
/**
* 4 Dimensional Vector
* @module vec4
*/
/**
* Creates a new, empty vec4
*
* @returns {vec4} a new 4D vector
*/
function create$1() {
var out = new ARRAY_TYPE(4);
if (ARRAY_TYPE != Float32Array) {
out[0] = 0;
out[1] = 0;
out[2] = 0;
out[3] = 0;
}
return out;
}
/**
* Transforms the vec4 with a mat4.
*
* @param {vec4} out the receiving vector
* @param {ReadonlyVec4} a the vector to transform
* @param {ReadonlyMat4} m matrix to transform with
* @returns {vec4} out
*/
function transformMat4(out, a, m) {
var x = a[0],
y = a[1],
z = a[2],
w = a[3];
out[0] = m[0] * x + m[4] * y + m[8] * z + m[12] * w;
out[1] = m[1] * x + m[5] * y + m[9] * z + m[13] * w;
out[2] = m[2] * x + m[6] * y + m[10] * z + m[14] * w;
out[3] = m[3] * x + m[7] * y + m[11] * z + m[15] * w;
return out;
}
/**
* Perform some operation over an array of vec4s.
*
* @param {Array} a the array of vectors to iterate over
* @param {Number} stride Number of elements between the start of each vec4. If 0 assumes tightly packed
* @param {Number} offset Number of elements to skip at the beginning of the array
* @param {Number} count Number of vec4s to iterate over. If 0 iterates over entire array
* @param {Function} fn Function to call for each vector in the array
* @param {Object} [arg] additional argument to pass to fn
* @returns {Array} a
* @function
*/
(function () {
var vec = create$1();
return function (a, stride, offset, count, fn, arg) {
var i, l;
if (!stride) {
stride = 4;
}
if (!offset) {
offset = 0;
}
if (count) {
l = Math.min(count * stride + offset, a.length);
} else {
l = a.length;
}
for (i = offset; i < l; i += stride) {
vec[0] = a[i];
vec[1] = a[i + 1];
vec[2] = a[i + 2];
vec[3] = a[i + 3];
fn(vec, vec, arg);
a[i] = vec[0];
a[i + 1] = vec[1];
a[i + 2] = vec[2];
a[i + 3] = vec[3];
}
return a;
};
})();
/**
* 2 Dimensional Vector
* @module vec2
*/
/**
* Creates a new, empty vec2
*
* @returns {vec2} a new 2D vector
*/
function create() {
var out = new ARRAY_TYPE(2);
if (ARRAY_TYPE != Float32Array) {
out[0] = 0;
out[1] = 0;
}
return out;
}
/**
* Get the angle between two 2D vectors
* @param {ReadonlyVec2} a The first operand
* @param {ReadonlyVec2} b The second operand
* @returns {Number} The angle in radians
*/
function angle(a, b) {
var x1 = a[0],
y1 = a[1],
x2 = b[0],
y2 = b[1],
// mag is the product of the magnitudes of a and b
mag = Math.sqrt(x1 * x1 + y1 * y1) * Math.sqrt(x2 * x2 + y2 * y2),
// mag &&.. short circuits if mag == 0
cosine = mag && (x1 * x2 + y1 * y2) / mag; // Math.min(Math.max(cosine, -1), 1) clamps the cosine between -1 and 1
return Math.acos(Math.min(Math.max(cosine, -1), 1));
}
/**
* Perform some operation over an array of vec2s.
*
* @param {Array} a the array of vectors to iterate over
* @param {Number} stride Number of elements between the start of each vec2. If 0 assumes tightly packed
* @param {Number} offset Number of elements to skip at the beginning of the array
* @param {Number} count Number of vec2s to iterate over. If 0 iterates over entire array
* @param {Function} fn Function to call for each vector in the array
* @param {Object} [arg] additional argument to pass to fn
* @returns {Array} a
* @function
*/
(function () {
var vec = create();
return function (a, stride, offset, count, fn, arg) {
var i, l;
if (!stride) {
stride = 2;
}
if (!offset) {
offset = 0;
}
if (count) {
l = Math.min(count * stride + offset, a.length);
} else {
l = a.length;
}
for (i = offset; i < l; i += stride) {
vec[0] = a[i];
vec[1] = a[i + 1];
fn(vec, vec, arg);
a[i] = vec[0];
a[i + 1] = vec[1];
}
return a;
};
})();
const createCamera = (
initTarget = [0, 0],
initDistance = 1,
initRotation = 0,
initViewCenter = [0, 0],
initScaleBounds = [
[0, Infinity],
[0, Infinity],
],
initTranslationBounds = [
[-Infinity, Infinity],
[-Infinity, Infinity],
]
) => {
// Scratch variables
const scratch0 = new Float32Array(16);
const scratch1 = new Float32Array(16);
const scratch2 = new Float32Array(16);
let view = create$2();
let viewCenter = [...initViewCenter.slice(0, 2), 0, 1];
const scaleXBounds = Array.isArray(initScaleBounds[0])
? [...initScaleBounds[0]]
: [...initScaleBounds];
const scaleYBounds = Array.isArray(initScaleBounds[0])
? [...initScaleBounds[1]]
: [...initScaleBounds];
const translationXBounds = Array.isArray(initTranslationBounds[0])
? [...initTranslationBounds[0]]
: [...initTranslationBounds];
const translationYBounds = Array.isArray(initTranslationBounds[0])
? [...initTranslationBounds[1]]
: [...initTranslationBounds];
const getScaling$1 = () => getScaling(scratch0, view).slice(0, 2);
const getMinScaling = () => {
const scaling = getScaling$1();
return Math.min(scaling[0], scaling[1]);
};
const getMaxScaling = () => {
const scaling = getScaling$1();
return Math.max(scaling[0], scaling[1]);
};
const getRotation = () => Math.acos(view[0] / getMaxScaling());
const getScaleBounds = () => [[...scaleXBounds], [...scaleYBounds]];
const getTranslationBounds = () => [
[...translationXBounds],
[...translationYBounds],
];
const getDistance = () => {
const scaling = getScaling$1();
return [1 / scaling[0], 1 / scaling[1]];
};
const getMinDistance = () => 1 / getMinScaling();
const getMaxDistance = () => 1 / getMaxScaling();
const getTranslation$1 = () => getTranslation(scratch0, view).slice(0, 2);
const getTarget = () =>
transformMat4(scratch0, viewCenter, invert(scratch2, view))
.slice(0, 2);
const getView = () => view;
const getViewCenter = () => viewCenter.slice(0, 2);
const lookAt = ([x = 0, y = 0] = [], newDistance = 1, newRotation = 0) => {
// Reset the view
view = create$2();
translate([-x, -y]);
rotate(newRotation);
scale(1 / newDistance);
};
const translate = ([x = 0, y = 0] = []) => {
scratch0[0] = x;
scratch0[1] = y;
scratch0[2] = 0;
const t = fromTranslation(scratch1, scratch0);
// Translate about the viewport center
// This is identical to `i * t * i * view` where `i` is the identity matrix
multiply(view, t, view);
};
const scale = (d, mousePos) => {
const isArray = Array.isArray(d);
let dx = isArray ? d[0] : d;
let dy = isArray ? d[1] : d;
if (dx <= 0 || dy <= 0 || (dx === 1 && dy === 1)) return;
const scaling = getScaling$1();
const newXScale = scaling[0] * dx;
const newYScale = scaling[1] * dy;
dx =
Math.max(scaleXBounds[0], Math.min(newXScale, scaleXBounds[1])) /
scaling[0];
dy =
Math.max(scaleYBounds[0], Math.min(newYScale, scaleYBounds[1])) /
scaling[1];
if (dx === 1 && dy === 1) return; // There is nothing to do
scratch0[0] = dx;
scratch0[1] = dy;
scratch0[2] = 1;
const s = fromScaling(scratch1, scratch0);
const scaleCenter = mousePos ? [...mousePos, 0] : viewCenter;
const a = fromTranslation(scratch0, scaleCenter);
// Translate about the scale center
// I.e., the mouse position or the view center
multiply(
view,
a,
multiply(
view,
s,
multiply(view, invert(scratch2, a), view)
)
);
};
const rotate = (rad) => {
const r = create$2();
fromRotation(r, rad, [0, 0, 1]);
// Rotate about the viewport center
// This is identical to `i * r * i * view` where `i` is the identity matrix
multiply(view, r, view);
};
const setScaleBounds = (newBounds) => {
const isArray = Array.isArray(newBounds[0]);
scaleXBounds[0] = isArray ? newBounds[0][0] : newBounds[0];
scaleXBounds[1] = isArray ? newBounds[0][1] : newBounds[1];
scaleYBounds[0] = isArray ? newBounds[1][0] : newBounds[0];
scaleYBounds[1] = isArray ? newBounds[1][1] : newBounds[1];
};
const setTranslationBounds = (newBounds) => {
const isArray = Array.isArray(newBounds[0]);
translationXBounds[0] = isArray ? newBounds[0][0] : newBounds[0];
translationXBounds[1] = isArray ? newBounds[0][1] : newBounds[1];
translationYBounds[0] = isArray ? newBounds[1][0] : newBounds[0];
translationYBounds[1] = isArray ? newBounds[1][1] : newBounds[1];
};
const setView = (newView) => {
if (!newView || newView.length < 16) return;
view = newView;
};
const setViewCenter = (newViewCenter) => {
viewCenter = [...newViewCenter.slice(0, 2), 0, 1];
};
const reset = () => {
lookAt(initTarget, initDistance, initRotation);
};
// Init
lookAt(initTarget, initDistance, initRotation);
return {
get translation() {
return getTranslation$1();
},
get target() {
return getTarget();
},
get scaling() {
return getScaling$1();
},
get minScaling() {
return getMinScaling();
},
get maxScaling() {
return getMaxScaling();
},
get scaleBounds() {
return getScaleBounds();
},
get translationBounds() {
return getTranslationBounds();
},
get distance() {
return getDistance();
},
get minDistance() {
return getMinDistance();
},
get maxDistance() {
return getMaxDistance();
},
get rotation() {
return getRotation();
},
get view() {
return getView();
},
get viewCenter() {
return getViewCenter();
},
lookAt,
translate,
pan: translate,
rotate,
scale,
zoom: scale,
reset,
set: (...args) => {
console.warn('`set()` is deprecated. Please use `setView()` instead.');
return setView(...args);
},
setScaleBounds,
setTranslationBounds,
setView,
setViewCenter,
};
};
const MOUSE_DOWN_MOVE_ACTIONS = ["pan", "rotate"];
const KEY_MAP = {
alt: "altKey",
cmd: "metaKey",
ctrl: "ctrlKey",
meta: "metaKey",
shift: "shiftKey"
};
const dom2dCamera = (
element,
{
distance = 1.0,
target = [0, 0],
rotation = 0,
isNdc = true,
isFixed = false,
isPan = true,
isPanInverted = [false, true],
panSpeed = 1,
isRotate = true,
rotateSpeed = 1,
defaultMouseDownMoveAction = "pan",
mouseDownMoveModKey = "alt",
isZoom = true,
zoomSpeed = 1,
viewCenter,
scaleBounds,
translationBounds,
onKeyDown = () => {},
onKeyUp = () => {},
onMouseDown = () => {},
onMouseUp = () => {},
onMouseMove = () => {},
onWheel = () => {}
} = {}
) => {
let camera = createCamera(
target,
distance,
rotation,
viewCenter,
scaleBounds,
translationBounds
);
let mouseX = 0;
let mouseY = 0;
let mouseRelX = 0;
let mouseRelY = 0;
let prevMouseX = 0;
let prevMouseY = 0;
let isLeftMousePressed = false;
let scrollDist = 0;
let width = 1;
let height = 1;
let aspectRatio = 1;
let isInteractivelyChanged = false;
let isProgrammaticallyChanged = false;
let isMouseDownMoveModActive = false;
let panOnMouseDownMove = defaultMouseDownMoveAction === "pan";
let isPanX = isPan;
let isPanY = isPan;
let isPanXInverted = isPanInverted;
let isPanYInverted = isPanInverted;
let isZoomX = isZoom;
let isZoomY = isZoom;
const spreadXYSettings = () => {
isPanX = Array.isArray(isPan) ? Boolean(isPan[0]) : isPan;
isPanY = Array.isArray(isPan) ? Boolean(isPan[1]) : isPan;
isPanXInverted = Array.isArray(isPanInverted)
? Boolean(isPanInverted[0])
: isPanInverted;
isPanYInverted = Array.isArray(isPanInverted)
? Boolean(isPanInverted[1])
: isPanInverted;
isZoomX = Array.isArray(isZoom) ? Boolean(isZoom[0]) : isZoom;
isZoomY = Array.isArray(isZoom) ? Boolean(isZoom[1]) : isZoom;
};
spreadXYSettings();
const transformPanX = isNdc
? dX => (dX / width) * 2 * aspectRatio // to normalized device coords
: dX => dX;
const transformPanY = isNdc
? dY => (dY / height) * 2 // to normalized device coords
: dY => -dY;
const transformScaleX = isNdc
? x => (-1 + (x / width) * 2) * aspectRatio // to normalized device coords
: x => x;
const transformScaleY = isNdc
? y => 1 - (y / height) * 2 // to normalized device coords
: y => y;
const tick = () => {
if (isFixed) {
const isChanged = isProgrammaticallyChanged;
isProgrammaticallyChanged = false;
return isChanged;
}
isInteractivelyChanged = false;
const currentMouseX = mouseX;
const currentMouseY = mouseY;
if (
(isPanX || isPanY) &&
isLeftMousePressed &&
((panOnMouseDownMove && !isMouseDownMoveModActive) ||
(!panOnMouseDownMove && isMouseDownMoveModActive))
) {
const dX = isPanXInverted
? prevMouseX - currentMouseX
: currentMouseX - prevMouseX;
const transformedPanX = isPanX ? transformPanX(panSpeed * dX) : 0;
const dY = isPanYInverted
? prevMouseY - currentMouseY
: currentMouseY - prevMouseY;
const transformedPanY = isPanY ? transformPanY(panSpeed * dY) : 0;
if (transformedPanX !== 0 || transformedPanY !== 0) {
camera.pan([transformedPanX, transformedPanY]);
isInteractivelyChanged = true;
}
}
if ((isZoomX || isZoomY) && scrollDist) {
const dZ = zoomSpeed * Math.exp(scrollDist / height);
const transformedX = transformScaleX(mouseRelX);
const transformedY = transformScaleY(mouseRelY);
camera.scale(
[isZoomX ? 1 / dZ : 1, isZoomY ? 1 / dZ : 1],
[transformedX, transformedY]
);
isInteractivelyChanged = true;
}
if (
isRotate &&
isLeftMousePressed &&
((panOnMouseDownMove && isMouseDownMoveModActive) ||
(!panOnMouseDownMove && !isMouseDownMoveModActive)) &&
Math.abs(prevMouseX - currentMouseX) +
Math.abs(prevMouseY - currentMouseY) >
0
) {
const wh = width / 2;
const hh = height / 2;
const x1 = prevMouseX - wh;
const y1 = hh - prevMouseY;
const x2 = currentMouseX - wh;
const y2 = hh - currentMouseY;
// Angle between the start and end mouse position with respect to the
// viewport center
const radians = angle([x1, y1], [x2, y2]);
// Determine the orientation
const cross = x1 * y2 - x2 * y1;
camera.rotate(rotateSpeed * radians * Math.sign(cross));
isInteractivelyChanged = true;
}
// Reset scroll delta and mouse position
scrollDist = 0;
prevMouseX = currentMouseX;
prevMouseY = currentMouseY;
const isChanged = isInteractivelyChanged || isProgrammaticallyChanged;
isProgrammaticallyChanged = false;
return isChanged;
};
const config = ({
defaultMouseDownMoveAction: newDefaultMouseDownMoveAction = null,
isFixed: newIsFixed = null,
isPan: newIsPan = null,
isPanInverted: newIsPanInverted = null,
isRotate: newIsRotate = null,
isZoom: newIsZoom = null,
panSpeed: newPanSpeed = null,
rotateSpeed: newRotateSpeed = null,
zoomSpeed: newZoomSpeed = null,
mouseDownMoveModKey: newMouseDownMoveModKey = null
} = {}) => {
defaultMouseDownMoveAction =
newDefaultMouseDownMoveAction !== null &&
MOUSE_DOWN_MOVE_ACTIONS.includes(newDefaultMouseDownMoveAction)
? newDefaultMouseDownMoveAction
: defaultMouseDownMoveAction;
panOnMouseDownMove = defaultMouseDownMoveAction === "pan";
isFixed = newIsFixed !== null ? newIsFixed : isFixed;
isPan = newIsPan !== null ? newIsPan : isPan;
isPanInverted =
newIsPanInverted !== null ? newIsPanInverted : isPanInverted;
isRotate = newIsRotate !== null ? newIsRotate : isRotate;
isZoom = newIsZoom !== null ? newIsZoom : isZoom;
panSpeed = +newPanSpeed > 0 ? newPanSpeed : panSpeed;
rotateSpeed = +newRotateSpeed > 0 ? newRotateSpeed : rotateSpeed;
zoomSpeed = +newZoomSpeed > 0 ? newZoomSpeed : zoomSpeed;
spreadXYSettings();
mouseDownMoveModKey =
newMouseDownMoveModKey !== null &&
Object.keys(KEY_MAP).includes(newMouseDownMoveModKey)
? newMouseDownMoveModKey
: mouseDownMoveModKey;
};
const refresh = () => {
const bBox = element.getBoundingClientRect();
width = bBox.width;
height = bBox.height;
aspectRatio = width / height;
};
const keyUpHandler = event => {
isMouseDownMoveModActive = false;
onKeyUp(event);
};
const keyDownHandler = event => {
isMouseDownMoveModActive = event[KEY_MAP[mouseDownMoveModKey]];
onKeyDown(event);
};
const mouseUpHandler = event => {
isLeftMousePressed = false;
onMouseUp(event);
};
const mouseDownHandler = event => {
isLeftMousePressed = event.buttons === 1;
onMouseDown(event);
};
const offsetXSupport =
document.createEvent("MouseEvent").offsetX !== undefined;
const updateMouseRelXY = offsetXSupport
? event => {
mouseRelX = event.offsetX;
mouseRelY = event.offsetY;
}
: event => {
const bBox = element.getBoundingClientRect();
mouseRelX = event.clientX - bBox.left;
mouseRelY = event.clientY - bBox.top;
};
const updateMouseXY = event => {
mouseX = event.clientX;
mouseY = event.clientY;
};
const mouseMoveHandler = event => {
updateMouseXY(event);
onMouseMove(event);
};
const wheelHandler = event => {
if ((isZoomX || isZoomY) && !isFixed) {
event.preventDefault();
updateMouseXY(event);
updateMouseRelXY(event);
const scale = event.deltaMode === 1 ? 12 : 1;
scrollDist += scale * (event.deltaY || event.deltaX || 0);
}
onWheel(event);
};
const dispose = () => {
camera = undefined;
window.removeEventListener("keydown", keyDownHandler);
window.removeEventListener("keyup", keyUpHandler);
element.removeEventListener("mousedown", mouseDownHandler);
window.removeEventListener("mouseup", mouseUpHandler);
window.removeEventListener("mousemove", mouseMoveHandler);
element.removeEventListener("wheel", wheelHandler);
};
window.addEventListener("keydown", keyDownHandler, { passive: true });
window.addEventListener("keyup", keyUpHandler, { passive: true });
element.addEventListener("mousedown", mouseDownHandler, { passive: true });
window.addEventListener("mouseup", mouseUpHandler, { passive: true });
window.addEventListener("mousemove", mouseMoveHandler, { passive: true });
element.addEventListener("wheel", wheelHandler, { passive: false });
camera.config = config;
camera.dispose = dispose;
camera.refresh = refresh;
camera.tick = tick;
const withProgrammaticChange = fn =>
function() {
fn.apply(null, arguments);
isProgrammaticallyChanged = true;
};
camera.lookAt = withProgrammaticChange(camera.lookAt);
camera.translate = withProgrammaticChange(camera.translate);
camera.pan = withProgrammaticChange(camera.pan);
camera.rotate = withProgrammaticChange(camera.rotate);
camera.scale = withProgrammaticChange(camera.scale);
camera.zoom = withProgrammaticChange(camera.zoom);
camera.reset = withProgrammaticChange(camera.reset);
camera.set = withProgrammaticChange(camera.set);
camera.setScaleBounds = withProgrammaticChange(camera.setScaleBounds);
camera.setTranslationBounds = withProgrammaticChange(
camera.setTranslationBounds
);
camera.setView = withProgrammaticChange(camera.setView);
camera.setViewCenter = withProgrammaticChange(camera.setViewCenter);
refresh();
return camera;
};
function earcut(data, holeIndices, dim = 2) {
const outerLen = data.length;
let outerNode = linkedList(data, 0, outerLen, dim, true);
const triangles = [];
if (!outerNode || outerNode.next === outerNode.prev) return triangles;
let minX, minY, invSize;
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
if (data.length > 80 * dim) {
minX = Infinity;
minY = Infinity;
let maxX = -Infinity;
let maxY = -Infinity;
for (let i = dim; i < outerLen; i += dim) {
const x = data[i];
const y = data[i + 1];
if (x < minX) minX = x;
if (y < minY) minY = y;
if (x > maxX) maxX = x;
if (y > maxY) maxY = y;
}
// minX, minY and invSize are later used to transform coords into integers for z-order calculation
invSize = Math.max(maxX - minX, maxY - minY);
invSize = invSize !== 0 ? 32767 / invSize : 0;
}
earcutLinked(outerNode, triangles, dim, minX, minY, invSize, 0);
return triangles;
}
// create a circular doubly linked list from polygon points in the specified winding order
function linkedList(data, start, end, dim, clockwise) {
let last;
if (clockwise === (signedArea(data, start, end, dim) > 0)) {
for (let i = start; i < end; i += dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
} else {
for (let i = end - dim; i >= start; i -= dim) last = insertNode(i / dim | 0, data[i], data[i + 1], last);
}
if (last && equals(last, last.next)) {
removeNode(last);
last = last.next;
}
return last;
}
// eliminate colinear or duplicate points
function filterPoints(start, end) {
if (!start) return start;
if (!end) end = start;
let p = start,
again;
do {
again = false;
if (!p.steiner && (equals(p, p.next) || area(p.prev, p, p.next) === 0)) {
removeNode(p);
p = end = p.prev;
if (p === p.next) break;
again = true;
} else {
p = p.next;
}
} while (again || p !== end);
return end;
}
// main ear slicing loop which triangulates a polygon (given as a linked list)
function earcutLinked(ear, triangles, dim, minX, minY, invSize, pass) {
if (!ear) return;
// interlink polygon nodes in z-order
if (!pass && invSize) indexCurve(ear, minX, minY, invSize);
let stop = ear;
// iterate through ears, slicing them one by one
while (ear.prev !== ear.next) {
const prev = ear.prev;
const next = ear.next;
if (invSize ? isEarHashed(ear, minX, minY, invSize) : isEar(ear)) {
triangles.push(prev.i, ear.i, next.i); // cut off the triangle
removeNode(ear);
// skipping the next vertex leads to less sliver triangles
ear = next.next;
stop = next.next;
continue;
}
ear = next;
// if we looped through the whole remaining polygon and can't find any more ears
if (ear === stop) {
// try filtering points and slicing again
if (!pass) {
earcutLinked(filterPoints(ear), triangles, dim, minX, minY, invSize, 1);
// if this didn't work, try curing all small self-intersections locally
} else if (pass === 1) {
ear = cureLocalIntersections(filterPoints(ear), triangles);
earcutLinked(ear, triangles, dim, minX, minY, invSize, 2);
// as a last resort, try splitting the remaining polygon into two
} else if (pass === 2) {
splitEarcut(ear, triangles, dim, minX, minY, invSize);
}
break;
}
}
}
// check whether a polygon node forms a valid ear with adjacent nodes
function isEar(ear) {
const a = ear.prev,
b = ear,
c = ear.next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// now make sure we don't have other points inside the potential ear
const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
// triangle bbox
const x0 = Math.min(ax, bx, cx),
y0 = Math.min(ay, by, cy),
x1 = Math.max(ax, bx, cx),
y1 = Math.max(ay, by, cy);
let p = c.next;
while (p !== a) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) &&
area(p.prev, p, p.next) >= 0) return false;
p = p.next;
}
return true;
}
function isEarHashed(ear, minX, minY, invSize) {
const a = ear.prev,
b = ear,
c = ear.next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
const ax = a.x, bx = b.x, cx = c.x, ay = a.y, by = b.y, cy = c.y;
// triangle bbox
const x0 = Math.min(ax, bx, cx),
y0 = Math.min(ay, by, cy),
x1 = Math.max(ax, bx, cx),
y1 = Math.max(ay, by, cy);
// z-order range for the current triangle bbox;
const minZ = zOrder(x0, y0, minX, minY, invSize),
maxZ = zOrder(x1, y1, minX, minY, invSize);
let p = ear.prevZ,
n = ear.nextZ;
// look for points inside the triangle in both directions
while (p && p.z >= minZ && n && n.z <= maxZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
// look for remaining points in decreasing z-order
while (p && p.z >= minZ) {
if (p.x >= x0 && p.x <= x1 && p.y >= y0 && p.y <= y1 && p !== a && p !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, p.x, p.y) && area(p.prev, p, p.next) >= 0) return false;
p = p.prevZ;
}
// look for remaining points in increasing z-order
while (n && n.z <= maxZ) {
if (n.x >= x0 && n.x <= x1 && n.y >= y0 && n.y <= y1 && n !== a && n !== c &&
pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, n.x, n.y) && area(n.prev, n, n.next) >= 0) return false;
n = n.nextZ;
}
return true;
}
// go through all polygon nodes and cure small local self-intersections
function cureLocalIntersections(start, triangles) {
let p = start;
do {
const a = p.prev,
b = p.next.next;
if (!equals(a, b) && intersects(a, p, p.next, b) && locallyInside(a, b) && locallyInside(b, a)) {
triangles.push(a.i, p.i, b.i);
// remove two nodes involved
removeNode(p);
removeNode(p.next);
p = start = b;
}
p = p.next;
} while (p !== start);
return filterPoints(p);
}
// try splitting polygon into two and triangulate them independently
function splitEarcut(start, triangles, dim, minX, minY, invSize) {
// look for a valid diagonal that divides the polygon into two
let a = start;
do {
let b = a.next.next;
while (b !== a.prev) {
if (a.i !== b.i && isValidDiagonal(a, b)) {
// split the polygon in two by the diagonal
let c = splitPolygon(a, b);
// filter colinear points around the cuts
a = filterPoints(a, a.next);
c = filterPoints(c, c.next);
// run earcut on each half
earcutLinked(a, triangles, dim, minX, minY, invSize, 0);
earcutLinked(c, triangles, dim, minX, minY, invSize, 0);
return;
}
b = b.next;
}
a = a.next;
} while (a !== start);
}
// interlink polygon nodes in z-order
function indexCurve(start, minX, minY, invSize) {
let p = start;
do {
if (p.z === 0) p.z = zOrder(p.x, p.y, minX, minY, invSize);
p.prevZ = p.prev;
p.nextZ = p.next;
p = p.next;
} while (p !== start);
p.prevZ.nextZ = null;
p.prevZ = null;
sortLinked(p);
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
function sortLinked(list) {
let numMerges;
let inSize = 1;
do {
let p = list;
let e;
list = null;
let tail = null;
numMerges = 0;
while (p) {
numMerges++;
let q = p;
let pSize = 0;
for (let i = 0; i < inSize; i++) {
pSize++;
q = q.nextZ;
if (!q) break;
}
let qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize !== 0 && (qSize === 0 || !q || p.z <= q.z)) {
e = p;
p = p.nextZ;
pSize--;
} else {
e = q;
q = q.nextZ;
qSize--;
}
if (tail) tail.nextZ = e;
else list = e;
e.prevZ = tail;
tail = e;
}
p = q;
}
tail.nextZ = null;
inSize *= 2;
} while (numMerges > 1);
return list;
}
// z-order of a point given coords and inverse of the longer side of data bbox
function zOrder(x, y, minX, minY, invSize) {
// coords are transformed into non-negative 15-bit integer range
x = (x - minX) * invSize | 0;
y = (y - minY) * invSize | 0;
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00FF00FF;
y = (y | (y << 4)) & 0x0F0F0F0F;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return x | (y << 1);
}
// check if a point lies within a convex triangle
function pointInTriangle(ax, ay, bx, by, cx, cy, px, py) {
return (cx - px) * (ay - py) >= (ax - px) * (cy - py) &&
(ax - px) * (by - py) >= (bx - px) * (ay - py) &&
(bx - px) * (cy - py) >= (cx - px) * (by - py);
}
// check if a point lies within a convex triangle but false if its equal to the first point of the triangle
function pointInTriangleExceptFirst(ax, ay, bx, by, cx, cy, px, py) {
return !(ax === px && ay === py) && pointInTriangle(ax, ay, bx, by, cx, cy, px, py);
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
function isValidDiagonal(a, b) {
return a.next.i !== b.i && a.prev.i !== b.i && !intersectsPolygon(a, b) && // dones't intersect other edges
(locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a.prev, a, b.prev) || area(a, b.prev, b)) || // does not create opposite-facing sectors
equals(a, b) && area(a.prev, a, a.next) > 0 && area(b.prev, b, b.next) > 0); // special zero-length case
}
// signed area of a triangle
function area(p, q, r) {
return (q.y - p.y) * (r.x - q.x) - (q.x - p.x) * (r.y - q.y);
}
// check if two points are equal
function equals(p1, p2) {
return p1.x === p2.x && p1.y === p2.y;
}
// check if two segments intersect
function intersects(p1, q1, p2, q2) {
const o1 = sign(area(p1, q1, p2));
const o2 = sign(area(p1, q1, q2));
const o3 = sign(area(p2, q2, p1));
const o4 = sign(area(p2, q2, q