import { Bounds } from 'types' import vec from 'utils/vec' /* ----------------- Start Copy Here ---------------- */ export default class Utils { /** * Linear interpolation betwen two numbers. * @param y1 * @param y2 * @param mu */ static lerp(y1: number, y2: number, mu: number): number { mu = Utils.clamp(mu, 0, 1) return y1 * (1 - mu) + y2 * mu } /** * Modulate a value between two ranges. * @param value * @param rangeA from [low, high] * @param rangeB to [low, high] * @param clamp */ static modulate( value: number, rangeA: number[], rangeB: number[], clamp = false ): number { const [fromLow, fromHigh] = rangeA const [v0, v1] = rangeB const result = v0 + ((value - fromLow) / (fromHigh - fromLow)) * (v1 - v0) return clamp ? v0 < v1 ? Math.max(Math.min(result, v1), v0) : Math.max(Math.min(result, v0), v1) : result } /** * Clamp a value into a range. * @param n * @param min */ static clamp(n: number, min: number): number static clamp(n: number, min: number, max: number): number static clamp(n: number, min: number, max?: number): number { return Math.max(min, typeof max !== 'undefined' ? Math.min(n, max) : n) } // TODO: replace with a string compression algorithm static compress(s: string): string { return s } // TODO: replace with a string decompression algorithm static decompress(s: string): string { return s } /** * Recursively clone an object or array. * @param obj */ static deepClone(obj: T): T { if (obj === null) return null const clone: any = { ...obj } Object.keys(obj).forEach( (key) => (clone[key] = typeof obj[key] === 'object' ? Utils.deepClone(obj[key]) : obj[key]) ) if (Array.isArray(obj)) { clone.length = obj.length return Array.from(clone) as any as T } return clone as T } /** * Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift). * The result will always be betweeen -1 and 1. * * Adapted from [seedrandom](https://github.com/davidbau/seedrandom). */ static rng(seed = ''): () => number { let x = 0 let y = 0 let z = 0 let w = 0 function next() { const t = x ^ (x << 11) ;(x = y), (y = z), (z = w) w ^= ((w >>> 19) ^ t ^ (t >>> 8)) >>> 0 return w / 0x100000000 } for (let k = 0; k < seed.length + 64; k++) { ;(x ^= seed.charCodeAt(k) | 0), next() } return next } /** * Shuffle the contents of an array. * @param arr * @param offset */ static shuffleArr(arr: T[], offset: number): T[] { return arr.map((_, i) => arr[(i + offset) % arr.length]) } /** * Deep compare two arrays. * @param a * @param b */ static deepCompareArrays(a: T[], b: T[]): boolean { if (a?.length !== b?.length) return false return Utils.deepCompare(a, b) } /** * Deep compare any values. * @param a * @param b */ static deepCompare(a: T, b: T): boolean { return a === b || JSON.stringify(a) === JSON.stringify(b) } /** * Find whether two arrays intersect. * @param a * @param b * @param fn An optional function to apply to the items of a; will check if b includes the result. */ static arrsIntersect(a: T[], b: K[], fn?: (item: K) => T): boolean static arrsIntersect(a: T[], b: T[]): boolean static arrsIntersect( a: T[], b: unknown[], fn?: (item: unknown) => T ): boolean { return a.some((item) => b.includes(fn ? fn(item) : item)) } /** * Get the unique values from an array of strings or numbers. * @param items */ static uniqueArray(...items: T[]): T[] { return Array.from(new Set(items).values()) } /** * Convert a set to an array. * @param set */ static setToArray(set: Set): T[] { return Array.from(set.values()) } /** * Get the outer of between a circle and a point. * @param C The circle's center. * @param r The circle's radius. * @param P The point. * @param side */ static getCircleTangentToPoint( C: number[], r: number, P: number[], side: number ): number[] { const B = vec.lrp(C, P, 0.5), r1 = vec.dist(C, B), delta = vec.sub(B, C), d = vec.len(delta) if (!(d <= r + r1 && d >= Math.abs(r - r1))) { return } const a = (r * r - r1 * r1 + d * d) / (2.0 * d), n = 1 / d, p = vec.add(C, vec.mul(delta, a * n)), h = Math.sqrt(r * r - a * a), k = vec.mul(vec.per(delta), h * n) return side === 0 ? vec.add(p, k) : vec.sub(p, k) } /** * Get outer tangents of two circles. * @param x0 * @param y0 * @param r0 * @param x1 * @param y1 * @param r1 * @returns [lx0, ly0, lx1, ly1, rx0, ry0, rx1, ry1] */ static getOuterTangentsOfCircles( C0: number[], r0: number, C1: number[], r1: number ): number[][] { const a0 = vec.angle(C0, C1) const d = vec.dist(C0, C1) // Circles are overlapping, no tangents if (d < Math.abs(r1 - r0)) return const a1 = Math.acos((r0 - r1) / d), t0 = a0 + a1, t1 = a0 - a1 return [ [C0[0] + r0 * Math.cos(t1), C0[1] + r0 * Math.sin(t1)], [C1[0] + r1 * Math.cos(t1), C1[1] + r1 * Math.sin(t1)], [C0[0] + r0 * Math.cos(t0), C0[1] + r0 * Math.sin(t0)], [C1[0] + r1 * Math.cos(t0), C1[1] + r1 * Math.sin(t0)], ] } /** * Get the closest point on the perimeter of a circle to a given point. * @param C The circle's center. * @param r The circle's radius. * @param P The point. */ static getClosestPointOnCircle( C: number[], r: number, P: number[] ): number[] { const v = vec.sub(C, P) return vec.sub(C, vec.mul(vec.div(v, vec.len(v)), r)) } static det( a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: number ): number { return a * e * i + b * f * g + c * d * h - a * f * h - b * d * i - c * e * g } /** * Get a circle from three points. * @param A * @param B * @param C * @returns [x, y, r] */ static circleFromThreePoints( A: number[], B: number[], C: number[] ): number[] { const a = Utils.det(A[0], A[1], 1, B[0], B[1], 1, C[0], C[1], 1) const bx = -Utils.det( A[0] * A[0] + A[1] * A[1], A[1], 1, B[0] * B[0] + B[1] * B[1], B[1], 1, C[0] * C[0] + C[1] * C[1], C[1], 1 ) const by = Utils.det( A[0] * A[0] + A[1] * A[1], A[0], 1, B[0] * B[0] + B[1] * B[1], B[0], 1, C[0] * C[0] + C[1] * C[1], C[0], 1 ) const c = -Utils.det( A[0] * A[0] + A[1] * A[1], A[0], A[1], B[0] * B[0] + B[1] * B[1], B[0], B[1], C[0] * C[0] + C[1] * C[1], C[0], C[1] ) const x = -bx / (2 * a) const y = -by / (2 * a) const r = Math.sqrt(bx * bx + by * by - 4 * a * c) / (2 * Math.abs(a)) return [x, y, r] } /** * Find the approximate perimeter of an ellipse. * @param rx * @param ry */ static perimeterOfEllipse(rx: number, ry: number): number { const h = Math.pow(rx - ry, 2) / Math.pow(rx + ry, 2) const p = Math.PI * (rx + ry) * (1 + (3 * h) / (10 + Math.sqrt(4 - 3 * h))) return p } /** * Get the short angle distance between two angles. * @param a0 * @param a1 */ static shortAngleDist(a0: number, a1: number): number { const max = Math.PI * 2 const da = (a1 - a0) % max return ((2 * da) % max) - da } /** * Get the long angle distance between two angles. * @param a0 * @param a1 */ static longAngleDist(a0: number, a1: number): number { return Math.PI * 2 - Utils.shortAngleDist(a0, a1) } /** * Interpolate an angle between two angles. * @param a0 * @param a1 * @param t */ static lerpAngles(a0: number, a1: number, t: number): number { return a0 + Utils.shortAngleDist(a0, a1) * t } /** * Get the short distance between two angles. * @param a0 * @param a1 */ static angleDelta(a0: number, a1: number): number { return Utils.shortAngleDist(a0, a1) } /** * Get the "sweep" or short distance between two points on a circle's perimeter. * @param C * @param A * @param B */ static getSweep(C: number[], A: number[], B: number[]): number { return Utils.angleDelta(vec.angle(C, A), vec.angle(C, B)) } /** * Rotate a point around a center. * @param x The x-axis coordinate of the point. * @param y The y-axis coordinate of the point. * @param cx The x-axis coordinate of the point to rotate round. * @param cy The y-axis coordinate of the point to rotate round. * @param angle The distance (in radians) to rotate. */ static rotatePoint(A: number[], B: number[], angle: number): number[] { const s = Math.sin(angle) const c = Math.cos(angle) const px = A[0] - B[0] const py = A[1] - B[1] const nx = px * c - py * s const ny = px * s + py * c return [nx + B[0], ny + B[1]] } /** * Clamp radians within 0 and 2PI * @param r */ static clampRadians(r: number): number { return (Math.PI * 2 + r) % (Math.PI * 2) } /** * Clamp rotation to even segments. * @param r * @param segments */ static clampToRotationToSegments(r: number, segments: number): number { const seg = (Math.PI * 2) / segments return Math.floor((Utils.clampRadians(r) + seg / 2) / seg) * seg } /** * Is angle c between angles a and b? * @param a * @param b * @param c */ static isAngleBetween(a: number, b: number, c: number): boolean { if (c === a || c === b) return true const PI2 = Math.PI * 2 const AB = (b - a + PI2) % PI2 const AC = (c - a + PI2) % PI2 return AB <= Math.PI !== AC > AB } /** * Convert degrees to radians. * @param d */ static degreesToRadians(d: number): number { return (d * Math.PI) / 180 } /** * Convert radians to degrees. * @param r */ static radiansToDegrees(r: number): number { return (r * 180) / Math.PI } /** * Get the length of an arc between two points on a circle's perimeter. * @param C * @param r * @param A * @param B */ static getArcLength( C: number[], r: number, A: number[], B: number[] ): number { const sweep = Utils.getSweep(C, A, B) return r * (2 * Math.PI) * (sweep / (2 * Math.PI)) } /** * Get a dash offset for an arc, based on its length. * @param C * @param r * @param A * @param B * @param step */ static getArcDashOffset( C: number[], r: number, A: number[], B: number[], step: number ): number { const del0 = Utils.getSweep(C, A, B) const len0 = Utils.getArcLength(C, r, A, B) const off0 = del0 < 0 ? len0 : 2 * Math.PI * C[2] - len0 return -off0 / 2 + step } /** * Get a dash offset for an ellipse, based on its length. * @param A * @param step */ static getEllipseDashOffset(A: number[], step: number): number { const c = 2 * Math.PI * A[2] return -c / 2 + -step } /** * Get an array of points between two points. * @param a * @param b * @param options */ static getPointsBetween( a: number[], b: number[], options = {} as { steps?: number ease?: (t: number) => number } ): number[][] { const { steps = 6, ease = (t) => t * t * t } = options return Array.from(Array(steps)) .map((_, i) => ease(i / steps)) .map((t) => [...vec.lrp(a, b, t), (1 - t) / 2]) } static getRayRayIntersection( p0: number[], n0: number[], p1: number[], n1: number[] ): number[] { const p0e = vec.add(p0, n0), p1e = vec.add(p1, n1), m0 = (p0e[1] - p0[1]) / (p0e[0] - p0[0]), m1 = (p1e[1] - p1[1]) / (p1e[0] - p1[0]), b0 = p0[1] - m0 * p0[0], b1 = p1[1] - m1 * p1[0], x = (b1 - b0) / (m0 - m1), y = m0 * x + b0 return [x, y] } static bez1d(a: number, b: number, c: number, d: number, t: number): number { return ( a * (1 - t) * (1 - t) * (1 - t) + 3 * b * t * (1 - t) * (1 - t) + 3 * c * t * t * (1 - t) + d * t * t * t ) } static getCubicBezierBounds( p0: number[], c0: number[], c1: number[], p1: number[] ): Bounds { // solve for x let a = 3 * p1[0] - 9 * c1[0] + 9 * c0[0] - 3 * p0[0] let b = 6 * p0[0] - 12 * c0[0] + 6 * c1[0] let c = 3 * c0[0] - 3 * p0[0] let disc = b * b - 4 * a * c let xl = p0[0] let xh = p0[0] if (p1[0] < xl) xl = p1[0] if (p1[0] > xh) xh = p1[0] if (disc >= 0) { const t1 = (-b + Math.sqrt(disc)) / (2 * a) if (t1 > 0 && t1 < 1) { const x1 = Utils.bez1d(p0[0], c0[0], c1[0], p1[0], t1) if (x1 < xl) xl = x1 if (x1 > xh) xh = x1 } const t2 = (-b - Math.sqrt(disc)) / (2 * a) if (t2 > 0 && t2 < 1) { const x2 = Utils.bez1d(p0[0], c0[0], c1[0], p1[0], t2) if (x2 < xl) xl = x2 if (x2 > xh) xh = x2 } } // Solve for y a = 3 * p1[1] - 9 * c1[1] + 9 * c0[1] - 3 * p0[1] b = 6 * p0[1] - 12 * c0[1] + 6 * c1[1] c = 3 * c0[1] - 3 * p0[1] disc = b * b - 4 * a * c let yl = p0[1] let yh = p0[1] if (p1[1] < yl) yl = p1[1] if (p1[1] > yh) yh = p1[1] if (disc >= 0) { const t1 = (-b + Math.sqrt(disc)) / (2 * a) if (t1 > 0 && t1 < 1) { const y1 = Utils.bez1d(p0[1], c0[1], c1[1], p1[1], t1) if (y1 < yl) yl = y1 if (y1 > yh) yh = y1 } const t2 = (-b - Math.sqrt(disc)) / (2 * a) if (t2 > 0 && t2 < 1) { const y2 = Utils.bez1d(p0[1], c0[1], c1[1], p1[1], t2) if (y2 < yl) yl = y2 if (y2 > yh) yh = y2 } } return { minX: xl, minY: yl, maxX: xh, maxY: yh, width: Math.abs(xl - xh), height: Math.abs(yl - yh), } } static getExpandedBounds(a: Bounds, b: Bounds): Bounds { const minX = Math.min(a.minX, b.minX), minY = Math.min(a.minY, b.minY), maxX = Math.max(a.maxX, b.maxX), maxY = Math.max(a.maxY, b.maxY), width = Math.abs(maxX - minX), height = Math.abs(maxY - minY) return { minX, minY, maxX, maxY, width, height } } static getCommonBounds(...b: Bounds[]): Bounds { if (b.length < 2) return b[0] let bounds = b[0] for (let i = 1; i < b.length; i++) { bounds = Utils.getExpandedBounds(bounds, b[i]) } return bounds } /** * Get a bezier curve data for a spline that fits an array of points. * @param pts * @param tension * @param isClosed * @param numOfSegments */ static getCurvePoints( pts: number[][], tension = 0.5, isClosed = false, numOfSegments = 3 ): number[][] { const _pts = [...pts], len = pts.length, res: number[][] = [] // results let t1x: number, // tension vectors t2x: number, t1y: number, t2y: number, c1: number, // cardinal points c2: number, c3: number, c4: number, st: number, st2: number, st3: number // The algorithm require a previous and next point to the actual point array. // Check if we will draw closed or open curve. // If closed, copy end points to beginning and first points to end // If open, duplicate first points to befinning, end points to end if (isClosed) { _pts.unshift(_pts[len - 1]) _pts.push(_pts[0]) } else { //copy 1. point and insert at beginning _pts.unshift(_pts[0]) _pts.push(_pts[len - 1]) // _pts.push(_pts[len - 1]) } // For each point, calculate a segment for (let i = 1; i < _pts.length - 2; i++) { // Calculate points along segment and add to results for (let t = 0; t <= numOfSegments; t++) { // Step st = t / numOfSegments st2 = Math.pow(st, 2) st3 = Math.pow(st, 3) // Cardinals c1 = 2 * st3 - 3 * st2 + 1 c2 = -(2 * st3) + 3 * st2 c3 = st3 - 2 * st2 + st c4 = st3 - st2 // Tension t1x = (_pts[i + 1][0] - _pts[i - 1][0]) * tension t2x = (_pts[i + 2][0] - _pts[i][0]) * tension t1y = (_pts[i + 1][1] - _pts[i - 1][1]) * tension t2y = (_pts[i + 2][1] - _pts[i][1]) * tension // Control points res.push([ c1 * _pts[i][0] + c2 * _pts[i + 1][0] + c3 * t1x + c4 * t2x, c1 * _pts[i][1] + c2 * _pts[i + 1][1] + c3 * t1y + c4 * t2y, ]) } } res.push(pts[pts.length - 1]) return res } /** * Simplify a line (using Ramer-Douglas-Peucker algorithm). * @param points An array of points as [x, y, ...][] * @param tolerance The minimum line distance (also called epsilon). * @returns Simplified array as [x, y, ...][] */ static simplify(points: number[][], tolerance = 1): number[][] { const len = points.length, a = points[0], b = points[len - 1], [x1, y1] = a, [x2, y2] = b if (len > 2) { let distance = 0 let index = 0 const max = Math.hypot(y2 - y1, x2 - x1) for (let i = 1; i < len - 1; i++) { const [x0, y0] = points[i], d = Math.abs((y2 - y1) * x0 - (x2 - x1) * y0 + x2 * y1 - y2 * x1) / max if (distance > d) continue distance = d index = i } if (distance > tolerance) { const l0 = Utils.simplify(points.slice(0, index + 1), tolerance) const l1 = Utils.simplify(points.slice(index + 1), tolerance) return l0.concat(l1.slice(1)) } } return [a, b] } }