UNPKG

regl-scatterplot

Version:

A WebGL-Powered Scalable Interactive Scatter Plot Library

1,836 lines (1,580 loc) 287 kB
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