@tldraw/utils 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +1350 -80
- package/dist-cjs/index.js +5 -5
- package/dist-cjs/lib/ExecutionQueue.js +79 -0
- package/dist-cjs/lib/ExecutionQueue.js.map +2 -2
- package/dist-cjs/lib/PerformanceTracker.js +43 -0
- package/dist-cjs/lib/PerformanceTracker.js.map +2 -2
- package/dist-cjs/lib/array.js +3 -1
- package/dist-cjs/lib/array.js.map +2 -2
- package/dist-cjs/lib/bind.js.map +2 -2
- package/dist-cjs/lib/cache.js +27 -5
- package/dist-cjs/lib/cache.js.map +2 -2
- package/dist-cjs/lib/control.js +12 -0
- package/dist-cjs/lib/control.js.map +2 -2
- package/dist-cjs/lib/debounce.js.map +2 -2
- package/dist-cjs/lib/error.js.map +2 -2
- package/dist-cjs/lib/file.js +76 -11
- package/dist-cjs/lib/file.js.map +2 -2
- package/dist-cjs/lib/function.js.map +2 -2
- package/dist-cjs/lib/hash.js.map +2 -2
- package/dist-cjs/lib/id.js.map +2 -2
- package/dist-cjs/lib/iterable.js.map +2 -2
- package/dist-cjs/lib/json-value.js.map +1 -1
- package/dist-cjs/lib/media/apng.js.map +2 -2
- package/dist-cjs/lib/media/avif.js.map +2 -2
- package/dist-cjs/lib/media/gif.js.map +2 -2
- package/dist-cjs/lib/media/media.js +130 -4
- package/dist-cjs/lib/media/media.js.map +2 -2
- package/dist-cjs/lib/media/png.js +141 -0
- package/dist-cjs/lib/media/png.js.map +2 -2
- package/dist-cjs/lib/media/webp.js +1 -0
- package/dist-cjs/lib/media/webp.js.map +2 -2
- package/dist-cjs/lib/network.js.map +2 -2
- package/dist-cjs/lib/number.js.map +2 -2
- package/dist-cjs/lib/object.js +1 -1
- package/dist-cjs/lib/object.js.map +2 -2
- package/dist-cjs/lib/perf.js.map +2 -2
- package/dist-cjs/lib/reordering.js.map +2 -2
- package/dist-cjs/lib/retry.js.map +2 -2
- package/dist-cjs/lib/sort.js.map +2 -2
- package/dist-cjs/lib/storage.js.map +2 -2
- package/dist-cjs/lib/stringEnum.js.map +2 -2
- package/dist-cjs/lib/throttle.js.map +2 -2
- package/dist-cjs/lib/timers.js +103 -4
- package/dist-cjs/lib/timers.js.map +2 -2
- package/dist-cjs/lib/types.js.map +1 -1
- package/dist-cjs/lib/url.js.map +2 -2
- package/dist-cjs/lib/value.js.map +2 -2
- package/dist-cjs/lib/version.js.map +2 -2
- package/dist-cjs/lib/warn.js.map +2 -2
- package/dist-esm/index.d.mts +1350 -80
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ExecutionQueue.mjs +79 -0
- package/dist-esm/lib/ExecutionQueue.mjs.map +2 -2
- package/dist-esm/lib/PerformanceTracker.mjs +43 -0
- package/dist-esm/lib/PerformanceTracker.mjs.map +2 -2
- package/dist-esm/lib/array.mjs +3 -1
- package/dist-esm/lib/array.mjs.map +2 -2
- package/dist-esm/lib/bind.mjs.map +2 -2
- package/dist-esm/lib/cache.mjs +27 -5
- package/dist-esm/lib/cache.mjs.map +2 -2
- package/dist-esm/lib/control.mjs +12 -0
- package/dist-esm/lib/control.mjs.map +2 -2
- package/dist-esm/lib/debounce.mjs.map +2 -2
- package/dist-esm/lib/error.mjs.map +2 -2
- package/dist-esm/lib/file.mjs +76 -11
- package/dist-esm/lib/file.mjs.map +2 -2
- package/dist-esm/lib/function.mjs.map +2 -2
- package/dist-esm/lib/hash.mjs.map +2 -2
- package/dist-esm/lib/id.mjs.map +2 -2
- package/dist-esm/lib/iterable.mjs.map +2 -2
- package/dist-esm/lib/media/apng.mjs.map +2 -2
- package/dist-esm/lib/media/avif.mjs.map +2 -2
- package/dist-esm/lib/media/gif.mjs.map +2 -2
- package/dist-esm/lib/media/media.mjs +130 -4
- package/dist-esm/lib/media/media.mjs.map +2 -2
- package/dist-esm/lib/media/png.mjs +141 -0
- package/dist-esm/lib/media/png.mjs.map +2 -2
- package/dist-esm/lib/media/webp.mjs +1 -0
- package/dist-esm/lib/media/webp.mjs.map +2 -2
- package/dist-esm/lib/network.mjs.map +2 -2
- package/dist-esm/lib/number.mjs.map +2 -2
- package/dist-esm/lib/object.mjs.map +2 -2
- package/dist-esm/lib/perf.mjs.map +2 -2
- package/dist-esm/lib/reordering.mjs.map +2 -2
- package/dist-esm/lib/retry.mjs.map +2 -2
- package/dist-esm/lib/sort.mjs.map +2 -2
- package/dist-esm/lib/storage.mjs.map +2 -2
- package/dist-esm/lib/stringEnum.mjs.map +2 -2
- package/dist-esm/lib/throttle.mjs.map +2 -2
- package/dist-esm/lib/timers.mjs +103 -4
- package/dist-esm/lib/timers.mjs.map +2 -2
- package/dist-esm/lib/url.mjs.map +2 -2
- package/dist-esm/lib/value.mjs.map +2 -2
- package/dist-esm/lib/version.mjs.map +2 -2
- package/dist-esm/lib/warn.mjs.map +2 -2
- package/package.json +1 -1
- package/src/lib/ExecutionQueue.test.ts +162 -20
- package/src/lib/ExecutionQueue.ts +110 -1
- package/src/lib/PerformanceTracker.test.ts +124 -0
- package/src/lib/PerformanceTracker.ts +63 -1
- package/src/lib/array.test.ts +263 -1
- package/src/lib/array.ts +183 -14
- package/src/lib/bind.test.ts +47 -0
- package/src/lib/bind.ts +69 -4
- package/src/lib/cache.test.ts +73 -0
- package/src/lib/cache.ts +47 -6
- package/src/lib/control.test.ts +50 -0
- package/src/lib/control.ts +198 -9
- package/src/lib/debounce.ts +28 -3
- package/src/lib/error.test.ts +60 -0
- package/src/lib/error.ts +27 -1
- package/src/lib/file.test.ts +49 -0
- package/src/lib/file.ts +117 -12
- package/src/lib/function.ts +11 -0
- package/src/lib/hash.test.ts +99 -0
- package/src/lib/hash.ts +69 -2
- package/src/lib/id.test.ts +32 -0
- package/src/lib/id.ts +53 -5
- package/src/lib/iterable.test.ts +25 -0
- package/src/lib/iterable.ts +4 -5
- package/src/lib/json-value.ts +71 -4
- package/src/lib/media/apng.test.ts +67 -0
- package/src/lib/media/apng.ts +38 -21
- package/src/lib/media/avif.test.ts +26 -0
- package/src/lib/media/avif.ts +34 -0
- package/src/lib/media/gif.test.ts +52 -0
- package/src/lib/media/gif.ts +25 -2
- package/src/lib/media/media.test.ts +58 -0
- package/src/lib/media/media.ts +220 -11
- package/src/lib/media/png.ts +162 -1
- package/src/lib/media/webp.test.ts +81 -0
- package/src/lib/media/webp.ts +33 -1
- package/src/lib/network.test.ts +38 -0
- package/src/lib/network.ts +6 -0
- package/src/lib/number.test.ts +74 -0
- package/src/lib/number.ts +29 -5
- package/src/lib/object.test.ts +236 -0
- package/src/lib/object.ts +194 -14
- package/src/lib/perf.ts +75 -3
- package/src/lib/reordering.test.ts +168 -0
- package/src/lib/reordering.ts +62 -4
- package/src/lib/retry.test.ts +77 -0
- package/src/lib/retry.ts +47 -1
- package/src/lib/sort.test.ts +36 -0
- package/src/lib/sort.ts +22 -1
- package/src/lib/storage.test.ts +130 -0
- package/src/lib/storage.tsx +54 -8
- package/src/lib/stringEnum.ts +20 -1
- package/src/lib/throttle.ts +46 -8
- package/src/lib/timers.test.ts +75 -0
- package/src/lib/timers.ts +124 -5
- package/src/lib/types.ts +126 -4
- package/src/lib/url.test.ts +44 -0
- package/src/lib/url.ts +40 -1
- package/src/lib/value.test.ts +102 -0
- package/src/lib/value.ts +67 -3
- package/src/lib/version.test.ts +494 -56
- package/src/lib/version.ts +36 -1
- package/src/lib/warn.test.ts +64 -0
- package/src/lib/warn.ts +43 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { isWebpAnimated } from './webp'
|
|
3
|
+
|
|
4
|
+
// Helper function to create a buffer with WebP signature
|
|
5
|
+
function createWebpBuffer(size: number, animated = false): ArrayBuffer {
|
|
6
|
+
const buffer = new ArrayBuffer(Math.max(size, 21))
|
|
7
|
+
const view = new Uint8Array(buffer)
|
|
8
|
+
|
|
9
|
+
// Set RIFF signature (bytes 0-3: 'RIFF')
|
|
10
|
+
view[0] = 0x52 // R
|
|
11
|
+
view[1] = 0x49 // I
|
|
12
|
+
view[2] = 0x46 // F
|
|
13
|
+
view[3] = 0x46 // F
|
|
14
|
+
|
|
15
|
+
// Set file size (bytes 4-7) - little endian
|
|
16
|
+
const fileSize = size - 8
|
|
17
|
+
view[4] = fileSize & 0xff
|
|
18
|
+
view[5] = (fileSize >> 8) & 0xff
|
|
19
|
+
view[6] = (fileSize >> 16) & 0xff
|
|
20
|
+
view[7] = (fileSize >> 24) & 0xff
|
|
21
|
+
|
|
22
|
+
// Set WebP signature (bytes 8-11: 'WEBP')
|
|
23
|
+
view[8] = 87 // W
|
|
24
|
+
view[9] = 69 // E
|
|
25
|
+
view[10] = 66 // B
|
|
26
|
+
view[11] = 80 // P
|
|
27
|
+
|
|
28
|
+
// Set VP8X chunk header for extended format (bytes 12-19)
|
|
29
|
+
view[12] = 86 // V
|
|
30
|
+
view[13] = 80 // P
|
|
31
|
+
view[14] = 56 // 8
|
|
32
|
+
view[15] = 88 // X
|
|
33
|
+
|
|
34
|
+
// Set VP8X chunk size (bytes 16-19) - 10 bytes
|
|
35
|
+
view[16] = 10
|
|
36
|
+
view[17] = 0
|
|
37
|
+
view[18] = 0
|
|
38
|
+
view[19] = 0
|
|
39
|
+
|
|
40
|
+
// Set VP8X flags (byte 20) - bit 1 is animation flag
|
|
41
|
+
if (animated) {
|
|
42
|
+
view[20] = 0b00000010 // Animation flag set
|
|
43
|
+
} else {
|
|
44
|
+
view[20] = 0b00000000 // No animation flag
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return buffer
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('isWebpAnimated', () => {
|
|
51
|
+
it('should return false for non-WebP data', () => {
|
|
52
|
+
const buffer = new ArrayBuffer(25)
|
|
53
|
+
const view = new Uint8Array(buffer)
|
|
54
|
+
view[8] = 80 // Not WebP signature
|
|
55
|
+
view[9] = 78
|
|
56
|
+
view[10] = 71
|
|
57
|
+
view[11] = 0
|
|
58
|
+
|
|
59
|
+
expect(isWebpAnimated(buffer)).toBe(false)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should return false for buffer too small for animation check', () => {
|
|
63
|
+
const buffer = new ArrayBuffer(10)
|
|
64
|
+
expect(isWebpAnimated(buffer)).toBe(false)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('should return false for static WebP', () => {
|
|
68
|
+
const buffer = createWebpBuffer(100, false)
|
|
69
|
+
expect(isWebpAnimated(buffer)).toBe(false)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('should return true for animated WebP', () => {
|
|
73
|
+
const buffer = createWebpBuffer(100, true)
|
|
74
|
+
expect(isWebpAnimated(buffer)).toBe(true)
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
it('should return false for empty buffer', () => {
|
|
78
|
+
const buffer = new ArrayBuffer(0)
|
|
79
|
+
expect(isWebpAnimated(buffer)).toBe(false)
|
|
80
|
+
})
|
|
81
|
+
})
|
package/src/lib/media/webp.ts
CHANGED
|
@@ -2,7 +2,24 @@
|
|
|
2
2
|
* MIT License: https://github.com/sindresorhus/is-webp/blob/main/license
|
|
3
3
|
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
4
4
|
*/
|
|
5
|
-
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Determines whether a byte array represents a WebP image by checking the WebP file signature.
|
|
8
|
+
*
|
|
9
|
+
* @param view - The Uint8Array containing the potential WebP image data
|
|
10
|
+
* @returns True if the byte array is a valid WebP image, false otherwise
|
|
11
|
+
* @example
|
|
12
|
+
* ```ts
|
|
13
|
+
* // Check if file data is WebP format
|
|
14
|
+
* const file = new File([...], 'image.webp', { type: 'image/webp' })
|
|
15
|
+
* const buffer = await file.arrayBuffer()
|
|
16
|
+
* const view = new Uint8Array(buffer)
|
|
17
|
+
* const isWebPImage = isWebp(view)
|
|
18
|
+
* console.log(isWebPImage ? 'Valid WebP' : 'Not WebP')
|
|
19
|
+
* ```
|
|
20
|
+
* @internal
|
|
21
|
+
*/
|
|
22
|
+
export function isWebp(view: Uint8Array) {
|
|
6
23
|
if (!view || view.length < 12) {
|
|
7
24
|
return false
|
|
8
25
|
}
|
|
@@ -10,6 +27,21 @@ function isWebp(view: Uint8Array) {
|
|
|
10
27
|
return view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80
|
|
11
28
|
}
|
|
12
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Determines whether a WebP image file contains animation data by checking the animation flag in the WebP VP8X chunk.
|
|
32
|
+
*
|
|
33
|
+
* @param buffer - The ArrayBuffer containing the WebP image data
|
|
34
|
+
* @returns True if the WebP image is animated, false otherwise
|
|
35
|
+
* @example
|
|
36
|
+
* ```ts
|
|
37
|
+
* // Check if a WebP file from user input is animated
|
|
38
|
+
* const file = new File([...], 'image.webp', { type: 'image/webp' })
|
|
39
|
+
* const buffer = await file.arrayBuffer()
|
|
40
|
+
* const animated = isWebpAnimated(buffer)
|
|
41
|
+
* console.log(animated ? 'Animated WebP' : 'Static WebP')
|
|
42
|
+
* ```
|
|
43
|
+
* @public
|
|
44
|
+
*/
|
|
13
45
|
export function isWebpAnimated(buffer: ArrayBuffer) {
|
|
14
46
|
const view = new Uint8Array(buffer)
|
|
15
47
|
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { fetch, Image } from './network'
|
|
3
|
+
|
|
4
|
+
describe('fetch', () => {
|
|
5
|
+
it('should call window.fetch with referrerPolicy set to strict-origin-when-cross-origin', async () => {
|
|
6
|
+
const mockFetch = vi.spyOn(window, 'fetch').mockResolvedValue(new Response('test'))
|
|
7
|
+
|
|
8
|
+
await fetch('https://example.com')
|
|
9
|
+
|
|
10
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com', {
|
|
11
|
+
referrerPolicy: 'strict-origin-when-cross-origin',
|
|
12
|
+
})
|
|
13
|
+
mockFetch.mockRestore()
|
|
14
|
+
})
|
|
15
|
+
|
|
16
|
+
it('should override referrerPolicy if provided in init options', async () => {
|
|
17
|
+
const mockFetch = vi.spyOn(window, 'fetch').mockResolvedValue(new Response('test'))
|
|
18
|
+
|
|
19
|
+
const initOptions: RequestInit = {
|
|
20
|
+
referrerPolicy: 'no-referrer',
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
await fetch('https://example.com', initOptions)
|
|
24
|
+
|
|
25
|
+
expect(mockFetch).toHaveBeenCalledWith('https://example.com', {
|
|
26
|
+
referrerPolicy: 'no-referrer',
|
|
27
|
+
})
|
|
28
|
+
mockFetch.mockRestore()
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
describe('Image', () => {
|
|
33
|
+
it('should create an image with referrerPolicy set to strict-origin-when-cross-origin', () => {
|
|
34
|
+
const img = Image()
|
|
35
|
+
|
|
36
|
+
expect(img.referrerPolicy).toBe('strict-origin-when-cross-origin')
|
|
37
|
+
})
|
|
38
|
+
})
|
package/src/lib/network.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Just a wrapper around `window.fetch` that sets the `referrerPolicy` to `strict-origin-when-cross-origin`.
|
|
3
3
|
*
|
|
4
|
+
* @param input - A Request object or string containing the URL to fetch
|
|
5
|
+
* @param init - Optional request initialization options
|
|
6
|
+
* @returns Promise that resolves to the Response object
|
|
4
7
|
* @internal
|
|
5
8
|
*/
|
|
6
9
|
export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response> {
|
|
@@ -16,6 +19,9 @@ export async function fetch(input: RequestInfo | URL, init?: RequestInit): Promi
|
|
|
16
19
|
* Just a wrapper around `new Image`, and yeah, it's a bit strange that it's in the network.ts file
|
|
17
20
|
* but the main concern here is the referrerPolicy and setting it correctly.
|
|
18
21
|
*
|
|
22
|
+
* @param width - Optional width for the image element
|
|
23
|
+
* @param height - Optional height for the image element
|
|
24
|
+
* @returns HTMLImageElement with referrerPolicy set to 'strict-origin-when-cross-origin'
|
|
19
25
|
* @internal
|
|
20
26
|
*/
|
|
21
27
|
export const Image = (width?: number, height?: number) => {
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { describe, expect, test } from 'vitest'
|
|
2
|
+
import { invLerp, lerp, modulate, rng } from './number'
|
|
3
|
+
|
|
4
|
+
describe('lerp', () => {
|
|
5
|
+
test('should interpolate between two values correctly', () => {
|
|
6
|
+
expect(lerp(0, 100, 0.5)).toBe(50)
|
|
7
|
+
expect(lerp(10, 20, 0.25)).toBe(12.5)
|
|
8
|
+
expect(lerp(-10, -5, 0.5)).toBe(-7.5)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
test('should extrapolate when t is outside 0-1 range', () => {
|
|
12
|
+
expect(lerp(0, 10, 1.5)).toBe(15)
|
|
13
|
+
expect(lerp(0, 10, -0.5)).toBe(-5)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
describe('invLerp', () => {
|
|
18
|
+
test('should find normalized position correctly', () => {
|
|
19
|
+
expect(invLerp(0, 100, 25)).toBe(0.25)
|
|
20
|
+
expect(invLerp(10, 20, 15)).toBe(0.5)
|
|
21
|
+
expect(invLerp(-10, -5, -7.5)).toBe(0.5)
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
test('should handle values outside the range', () => {
|
|
25
|
+
expect(invLerp(0, 10, 15)).toBe(1.5)
|
|
26
|
+
expect(invLerp(0, 10, -5)).toBe(-0.5)
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
describe('rng', () => {
|
|
31
|
+
test('should generate deterministic results for same seed', () => {
|
|
32
|
+
const random1 = rng('same-seed')
|
|
33
|
+
const random2 = rng('same-seed')
|
|
34
|
+
|
|
35
|
+
const values1 = [random1(), random1(), random1()]
|
|
36
|
+
const values2 = [random2(), random2(), random2()]
|
|
37
|
+
|
|
38
|
+
expect(values1).toEqual(values2)
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('should generate different results for different seeds', () => {
|
|
42
|
+
const random1 = rng('seed-1')
|
|
43
|
+
const random2 = rng('seed-2')
|
|
44
|
+
|
|
45
|
+
const values1 = [random1(), random1(), random1()]
|
|
46
|
+
const values2 = [random2(), random2(), random2()]
|
|
47
|
+
|
|
48
|
+
expect(values1).not.toEqual(values2)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
describe('modulate', () => {
|
|
53
|
+
test('should map value between ranges correctly', () => {
|
|
54
|
+
expect(modulate(0, [0, 1], [0, 100])).toBe(0)
|
|
55
|
+
expect(modulate(0.5, [0, 1], [0, 100])).toBe(50)
|
|
56
|
+
expect(modulate(1, [0, 1], [0, 100])).toBe(100)
|
|
57
|
+
expect(modulate(30, [10, 50], [100, 200])).toBe(150)
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
test('should extrapolate without clamping by default', () => {
|
|
61
|
+
expect(modulate(2, [0, 1], [0, 100])).toBe(200)
|
|
62
|
+
expect(modulate(-1, [0, 1], [0, 100])).toBe(-100)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
test('should clamp results when clamp is true', () => {
|
|
66
|
+
expect(modulate(2, [0, 1], [0, 100], true)).toBe(100)
|
|
67
|
+
expect(modulate(-1, [0, 1], [0, 100], true)).toBe(0)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
test('should handle clamping with reversed target range', () => {
|
|
71
|
+
expect(modulate(2, [0, 1], [100, 0], true)).toBe(0)
|
|
72
|
+
expect(modulate(-1, [0, 1], [100, 0], true)).toBe(100)
|
|
73
|
+
})
|
|
74
|
+
})
|
package/src/lib/number.ts
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linear interpolate between two values.
|
|
3
3
|
*
|
|
4
|
+
* @param a - The start value
|
|
5
|
+
* @param b - The end value
|
|
6
|
+
* @param t - The interpolation factor (0-1)
|
|
7
|
+
* @returns The interpolated value
|
|
4
8
|
* @example
|
|
5
|
-
*
|
|
6
9
|
* ```ts
|
|
7
|
-
* const
|
|
10
|
+
* const halfway = lerp(0, 100, 0.5) // 50
|
|
11
|
+
* const quarter = lerp(10, 20, 0.25) // 12.5
|
|
8
12
|
* ```
|
|
9
|
-
*
|
|
10
13
|
* @public
|
|
11
14
|
*/
|
|
12
15
|
export function lerp(a: number, b: number, t: number) {
|
|
@@ -14,9 +17,18 @@ export function lerp(a: number, b: number, t: number) {
|
|
|
14
17
|
}
|
|
15
18
|
|
|
16
19
|
/**
|
|
17
|
-
* Inverse lerp between two values. Given a value `
|
|
20
|
+
* Inverse lerp between two values. Given a value `t` in the range [a, b], returns a number between
|
|
18
21
|
* 0 and 1.
|
|
19
22
|
*
|
|
23
|
+
* @param a - The start value of the range
|
|
24
|
+
* @param b - The end value of the range
|
|
25
|
+
* @param t - The value within the range [a, b]
|
|
26
|
+
* @returns The normalized position (0-1) of t within the range [a, b]
|
|
27
|
+
* @example
|
|
28
|
+
* ```ts
|
|
29
|
+
* const position = invLerp(0, 100, 25) // 0.25
|
|
30
|
+
* const normalized = invLerp(10, 20, 15) // 0.5
|
|
31
|
+
* ```
|
|
20
32
|
* @public
|
|
21
33
|
*/
|
|
22
34
|
export function invLerp(a: number, b: number, t: number) {
|
|
@@ -25,10 +37,22 @@ export function invLerp(a: number, b: number, t: number) {
|
|
|
25
37
|
|
|
26
38
|
/**
|
|
27
39
|
* Seeded random number generator, using [xorshift](https://en.wikipedia.org/wiki/Xorshift). The
|
|
28
|
-
* result will always be
|
|
40
|
+
* result will always be between -1 and 1.
|
|
29
41
|
*
|
|
30
42
|
* Adapted from [seedrandom](https://github.com/davidbau/seedrandom).
|
|
31
43
|
*
|
|
44
|
+
* @param seed - The seed string for deterministic random generation (defaults to empty string)
|
|
45
|
+
* @returns A function that will return a random number between -1 and 1 each time it is called
|
|
46
|
+
* @example
|
|
47
|
+
* ```ts
|
|
48
|
+
* const random = rng('my-seed')
|
|
49
|
+
* const num1 = random() // Always the same for this seed
|
|
50
|
+
* const num2 = random() // Next number in sequence
|
|
51
|
+
*
|
|
52
|
+
* // Different seed produces different sequence
|
|
53
|
+
* const otherRandom = rng('other-seed')
|
|
54
|
+
* const different = otherRandom() // Different value
|
|
55
|
+
* ```
|
|
32
56
|
* @public
|
|
33
57
|
*/
|
|
34
58
|
export function rng(seed = '') {
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
areObjectsShallowEqual,
|
|
4
|
+
filterEntries,
|
|
5
|
+
getChangedKeys,
|
|
6
|
+
groupBy,
|
|
7
|
+
hasOwnProperty,
|
|
8
|
+
isEqualAllowingForFloatingPointErrors,
|
|
9
|
+
mapObjectMapValues,
|
|
10
|
+
objectMapEntries,
|
|
11
|
+
objectMapEntriesIterable,
|
|
12
|
+
objectMapFromEntries,
|
|
13
|
+
objectMapKeys,
|
|
14
|
+
objectMapValues,
|
|
15
|
+
omit,
|
|
16
|
+
} from './object'
|
|
17
|
+
|
|
18
|
+
describe('hasOwnProperty', () => {
|
|
19
|
+
it('should work with objects that override hasOwnProperty', () => {
|
|
20
|
+
const obj = {
|
|
21
|
+
name: 'Alice',
|
|
22
|
+
hasOwnProperty: () => false,
|
|
23
|
+
}
|
|
24
|
+
expect(hasOwnProperty(obj, 'name')).toBe(true)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
describe('objectMapKeys', () => {
|
|
29
|
+
it('should return typed keys preserving order', () => {
|
|
30
|
+
const obj = { z: 1, a: 2, m: 3 }
|
|
31
|
+
const keys = objectMapKeys(obj)
|
|
32
|
+
expect(keys).toEqual(['z', 'a', 'm'])
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
describe('objectMapValues', () => {
|
|
37
|
+
it('should return typed values preserving order', () => {
|
|
38
|
+
const obj = { first: 'a', second: 'b', third: 'c' }
|
|
39
|
+
const values = objectMapValues(obj)
|
|
40
|
+
expect(values).toEqual(['a', 'b', 'c'])
|
|
41
|
+
})
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
describe('objectMapEntries', () => {
|
|
45
|
+
it('should return typed entries preserving order', () => {
|
|
46
|
+
const obj = { z: 1, a: 2 }
|
|
47
|
+
const entries = objectMapEntries(obj)
|
|
48
|
+
expect(entries).toEqual([
|
|
49
|
+
['z', 1],
|
|
50
|
+
['a', 2],
|
|
51
|
+
])
|
|
52
|
+
})
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
describe('objectMapEntriesIterable', () => {
|
|
56
|
+
it('should skip inherited properties and work as iterator', () => {
|
|
57
|
+
const parent = { inherited: 'value' }
|
|
58
|
+
const child = Object.create(parent)
|
|
59
|
+
child.own = 'ownValue'
|
|
60
|
+
|
|
61
|
+
const entries = Array.from(objectMapEntriesIterable(child))
|
|
62
|
+
expect(entries).toEqual([['own', 'ownValue']])
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('should work in for...of loops', () => {
|
|
66
|
+
const obj = { x: 10, y: 20 }
|
|
67
|
+
const collected: Array<[string, number]> = []
|
|
68
|
+
|
|
69
|
+
for (const [key, value] of objectMapEntriesIterable(obj)) {
|
|
70
|
+
collected.push([key, value])
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
expect(collected).toEqual([
|
|
74
|
+
['x', 10],
|
|
75
|
+
['y', 20],
|
|
76
|
+
])
|
|
77
|
+
})
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
describe('objectMapFromEntries', () => {
|
|
81
|
+
it('should create typed object from entries', () => {
|
|
82
|
+
const entries: Array<['name' | 'age', string | number]> = [
|
|
83
|
+
['name', 'Alice'],
|
|
84
|
+
['age', 30],
|
|
85
|
+
]
|
|
86
|
+
const obj = objectMapFromEntries(entries)
|
|
87
|
+
expect(obj).toEqual({ name: 'Alice', age: 30 })
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should handle duplicate keys (last wins)', () => {
|
|
91
|
+
const entries: Array<['key', string]> = [
|
|
92
|
+
['key', 'first'],
|
|
93
|
+
['key', 'second'],
|
|
94
|
+
]
|
|
95
|
+
const obj = objectMapFromEntries(entries)
|
|
96
|
+
expect(obj).toEqual({ key: 'second' })
|
|
97
|
+
})
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
describe('filterEntries', () => {
|
|
101
|
+
it('should filter entries and optimize unchanged objects', () => {
|
|
102
|
+
const scores = { alice: 85, bob: 92, charlie: 78 }
|
|
103
|
+
const passing = filterEntries(scores, (name, score) => score >= 80)
|
|
104
|
+
expect(passing).toEqual({ alice: 85, bob: 92 })
|
|
105
|
+
|
|
106
|
+
// Optimization: return same reference when no changes
|
|
107
|
+
const unchanged = filterEntries(scores, () => true)
|
|
108
|
+
expect(unchanged).toBe(scores)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
it('should pass key and value to predicate', () => {
|
|
112
|
+
const obj = { prefix_a: 1, other_b: 2, prefix_c: 3 }
|
|
113
|
+
const result = filterEntries(obj, (key, value) => key.startsWith('prefix_') && value > 1)
|
|
114
|
+
expect(result).toEqual({ prefix_c: 3 })
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
describe('mapObjectMapValues', () => {
|
|
119
|
+
it('should map values with key and value access', () => {
|
|
120
|
+
const obj = { a: 1, b: 2, c: 3 }
|
|
121
|
+
const result = mapObjectMapValues(obj, (key, value) => `${key}:${value}`)
|
|
122
|
+
expect(result).toEqual({ a: 'a:1', b: 'b:2', c: 'c:3' })
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should skip inherited properties', () => {
|
|
126
|
+
const parent = { inherited: 'value' }
|
|
127
|
+
const child = Object.create(parent)
|
|
128
|
+
child.own = 'ownValue'
|
|
129
|
+
|
|
130
|
+
const result = mapObjectMapValues(child, (k, v) => (v as string).toUpperCase())
|
|
131
|
+
expect(result).toEqual({ own: 'OWNVALUE' })
|
|
132
|
+
})
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
describe('areObjectsShallowEqual', () => {
|
|
136
|
+
it('should compare shallow equality correctly', () => {
|
|
137
|
+
const a = { x: 1, y: 2 }
|
|
138
|
+
const b = { x: 1, y: 2 }
|
|
139
|
+
const c = { x: 1, y: 3 }
|
|
140
|
+
const d = { x: 1, z: 2 }
|
|
141
|
+
|
|
142
|
+
expect(areObjectsShallowEqual(a, a)).toBe(true) // Same reference
|
|
143
|
+
expect(areObjectsShallowEqual(a, b)).toBe(true) // Same values
|
|
144
|
+
expect(areObjectsShallowEqual(a, c)).toBe(false) // Different values
|
|
145
|
+
expect(areObjectsShallowEqual(a, d as any)).toBe(false) // Different keys
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should use Object.is for value comparison', () => {
|
|
149
|
+
expect(areObjectsShallowEqual({ x: NaN }, { x: NaN })).toBe(true)
|
|
150
|
+
expect(areObjectsShallowEqual({ x: -0 }, { x: +0 })).toBe(false)
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
describe('groupBy', () => {
|
|
155
|
+
it('should group items by key selector function', () => {
|
|
156
|
+
const people = [
|
|
157
|
+
{ name: 'Alice', age: 25 },
|
|
158
|
+
{ name: 'Bob', age: 30 },
|
|
159
|
+
{ name: 'Charlie', age: 25 },
|
|
160
|
+
]
|
|
161
|
+
const byAge = groupBy(people, (person) => `age-${person.age}`)
|
|
162
|
+
expect(byAge).toEqual({
|
|
163
|
+
'age-25': [
|
|
164
|
+
{ name: 'Alice', age: 25 },
|
|
165
|
+
{ name: 'Charlie', age: 25 },
|
|
166
|
+
],
|
|
167
|
+
'age-30': [{ name: 'Bob', age: 30 }],
|
|
168
|
+
})
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
it('should accumulate duplicate keys', () => {
|
|
172
|
+
const items = ['apple', 'apricot', 'banana']
|
|
173
|
+
const result = groupBy(items, (item) => item.charAt(0))
|
|
174
|
+
expect(result).toEqual({ a: ['apple', 'apricot'], b: ['banana'] })
|
|
175
|
+
})
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
describe('omit', () => {
|
|
179
|
+
it('should create new object without specified keys', () => {
|
|
180
|
+
const user = { id: '123', name: 'Alice', password: 'secret', email: 'alice@example.com' }
|
|
181
|
+
const publicUser = omit(user, ['password'])
|
|
182
|
+
expect(publicUser).toEqual({ id: '123', name: 'Alice', email: 'alice@example.com' })
|
|
183
|
+
expect(publicUser).not.toBe(user)
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should handle multiple keys and non-existent keys', () => {
|
|
187
|
+
const obj = { a: 1, b: 2, c: 3 }
|
|
188
|
+
const result = omit(obj, ['b', 'nonexistent'])
|
|
189
|
+
expect(result).toEqual({ a: 1, c: 3 })
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('getChangedKeys', () => {
|
|
194
|
+
it('should identify changed keys using Object.is', () => {
|
|
195
|
+
const before = { name: 'Alice', age: 25, city: 'NYC' }
|
|
196
|
+
const after = { name: 'Alice', age: 26, city: 'NYC' }
|
|
197
|
+
expect(getChangedKeys(before, after)).toEqual(['age'])
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('should use Object.is comparison (NaN and zero handling)', () => {
|
|
201
|
+
const before = { nan: NaN, zero: -0, normal: 1 }
|
|
202
|
+
const after = { nan: NaN, zero: +0, normal: 2 }
|
|
203
|
+
expect(getChangedKeys(before, after)).toEqual(['zero', 'normal'])
|
|
204
|
+
})
|
|
205
|
+
|
|
206
|
+
it('should only check keys from first object', () => {
|
|
207
|
+
const before = { a: 1, b: 2 }
|
|
208
|
+
const after = { a: 1, b: 2, c: 3 }
|
|
209
|
+
expect(getChangedKeys(before, after)).toEqual([])
|
|
210
|
+
})
|
|
211
|
+
})
|
|
212
|
+
|
|
213
|
+
describe('isEqualAllowingForFloatingPointErrors', () => {
|
|
214
|
+
it('should handle floating point precision errors', () => {
|
|
215
|
+
const a = { x: 0.1 + 0.2 } // 0.30000000000000004
|
|
216
|
+
const b = { x: 0.3 }
|
|
217
|
+
expect(isEqualAllowingForFloatingPointErrors(a, b)).toBe(true)
|
|
218
|
+
})
|
|
219
|
+
|
|
220
|
+
it('should work with custom threshold and reject large differences', () => {
|
|
221
|
+
const a = { x: 0.1 }
|
|
222
|
+
const b = { x: 0.15 }
|
|
223
|
+
expect(isEqualAllowingForFloatingPointErrors(a, b, 0.1)).toBe(true)
|
|
224
|
+
expect(isEqualAllowingForFloatingPointErrors(a, b, 0.01)).toBe(false)
|
|
225
|
+
expect(isEqualAllowingForFloatingPointErrors({ x: 0.1 }, { x: 0.2 })).toBe(false)
|
|
226
|
+
})
|
|
227
|
+
|
|
228
|
+
it('should handle deep objects and arrays with mixed types', () => {
|
|
229
|
+
const a = { user: 'Alice', score: 0.1 + 0.2, values: [1.0000001, 'test'] }
|
|
230
|
+
const b = { user: 'Alice', score: 0.3, values: [1.0000002, 'test'] }
|
|
231
|
+
expect(isEqualAllowingForFloatingPointErrors(a, b)).toBe(true)
|
|
232
|
+
|
|
233
|
+
// Different structure should fail
|
|
234
|
+
expect(isEqualAllowingForFloatingPointErrors({ x: 0.3 }, { y: 0.3 })).toBe(false)
|
|
235
|
+
})
|
|
236
|
+
})
|