123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751 |
- import { Bounds } from 'types'
- import vec from 'utils/vec'
-
- /**
- * ## Utils
- */
- 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<T>(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<T>(arr: T[], offset: number): T[] {
- return arr.map((_, i) => arr[(i + offset) % arr.length])
- }
-
- /**
- * Deep compare two arrays.
- * @param a
- * @param b
- */
- static deepCompareArrays<T>(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<T>(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<T, K>(a: T[], b: K[], fn?: (item: K) => T): boolean
- static arrsIntersect<T>(a: T[], b: T[]): boolean
- static arrsIntersect<T>(
- 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<T extends string | number>(...items: T[]): T[] {
- return Array.from(new Set(items).values())
- }
-
- /**
- * Convert a set to an array.
- * @param set
- */
- static setToArray<T>(set: Set<T>): 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]
- }
- }
|