kaplay 3000.1.17

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/src/utils.ts ADDED
@@ -0,0 +1,525 @@
1
+ export class Registry<T> extends Map<number, T> {
2
+ private lastID: number
3
+ constructor(...args) {
4
+ super(...args)
5
+ this.lastID = 0
6
+ }
7
+ push(v: T): number {
8
+ const id = this.lastID
9
+ this.set(id, v)
10
+ this.lastID++
11
+ return id
12
+ }
13
+ pushd(v: T): () => void {
14
+ const id = this.push(v)
15
+ return () => this.delete(id)
16
+ }
17
+ }
18
+
19
+ export class EventController {
20
+ paused: boolean = false
21
+ readonly cancel: () => void
22
+ constructor(cancel: () => void) {
23
+ this.cancel = cancel
24
+ }
25
+ static join(events: EventController[]): EventController {
26
+ const ev = new EventController(() => events.forEach((e) => e.cancel()))
27
+ Object.defineProperty(ev, "paused", {
28
+ get: () => events[0].paused,
29
+ set: (p: boolean) => events.forEach((e) => e.paused = p),
30
+ })
31
+ ev.paused = false
32
+ return ev
33
+ }
34
+ }
35
+
36
+ export class Event<Args extends any[] = any[]> {
37
+ private handlers: Registry<(...args: Args) => void> = new Registry()
38
+ add(action: (...args: Args) => void): EventController {
39
+ const cancel = this.handlers.pushd((...args: Args) => {
40
+ if (ev.paused) return
41
+ action(...args)
42
+ })
43
+ const ev = new EventController(cancel)
44
+ return ev
45
+ }
46
+ addOnce(action: (...args) => void): EventController {
47
+ const ev = this.add((...args) => {
48
+ ev.cancel()
49
+ action(...args)
50
+ })
51
+ return ev
52
+ }
53
+ next(): Promise<Args> {
54
+ return new Promise((res) => this.addOnce(res))
55
+ }
56
+ trigger(...args: Args) {
57
+ this.handlers.forEach((action) => action(...args))
58
+ }
59
+ numListeners(): number {
60
+ return this.handlers.size
61
+ }
62
+ clear() {
63
+ this.handlers.clear()
64
+ }
65
+ }
66
+
67
+ // TODO: only accept one argument?
68
+ export class EventHandler<EventMap extends Record<string, any[]>> {
69
+ private handlers: Partial<{
70
+ [Name in keyof EventMap]: Event<EventMap[Name]>
71
+ }> = {}
72
+ on<Name extends keyof EventMap>(
73
+ name: Name,
74
+ action: (...args: EventMap[Name]) => void,
75
+ ): EventController {
76
+ if (!this.handlers[name]) {
77
+ this.handlers[name] = new Event<EventMap[Name]>()
78
+ }
79
+ return this.handlers[name].add(action)
80
+ }
81
+ onOnce<Name extends keyof EventMap>(
82
+ name: Name,
83
+ action: (...args: EventMap[Name]) => void,
84
+ ): EventController {
85
+ const ev = this.on(name, (...args) => {
86
+ ev.cancel()
87
+ action(...args)
88
+ })
89
+ return ev
90
+ }
91
+ next<Name extends keyof EventMap>(name: Name): Promise<unknown> {
92
+ return new Promise((res) => {
93
+ // TODO: can only pass 1 val to resolve()
94
+ this.onOnce(name, (...args: EventMap[Name]) => res(args[0]))
95
+ })
96
+ }
97
+ trigger<Name extends keyof EventMap>(name: Name, ...args: EventMap[Name]) {
98
+ if (this.handlers[name]) {
99
+ this.handlers[name].trigger(...args)
100
+ }
101
+ }
102
+ remove<Name extends keyof EventMap>(name: Name) {
103
+ delete this.handlers[name]
104
+ }
105
+ clear() {
106
+ this.handlers = {}
107
+ }
108
+ numListeners<Name extends keyof EventMap>(name: Name): number {
109
+ return this.handlers[name]?.numListeners() ?? 0
110
+ }
111
+ }
112
+
113
+ export function deepEq(o1: any, o2: any): boolean {
114
+ if (o1 === o2) {
115
+ return true
116
+ }
117
+ const t1 = typeof o1
118
+ const t2 = typeof o2
119
+ if (t1 !== t2) {
120
+ return false
121
+ }
122
+ if (t1 === "object" && t2 === "object" && o1 !== null && o2 !== null) {
123
+ if (Array.isArray(o1) !== Array.isArray(o2)) {
124
+ return false
125
+ }
126
+ const k1 = Object.keys(o1)
127
+ const k2 = Object.keys(o2)
128
+ if (k1.length !== k2.length) {
129
+ return false
130
+ }
131
+ for (const k of k1) {
132
+ const v1 = o1[k]
133
+ const v2 = o2[k]
134
+ if (!deepEq(v1, v2)) {
135
+ return false
136
+ }
137
+ }
138
+ return true
139
+ }
140
+ return false
141
+ }
142
+
143
+ export function base64ToArrayBuffer(base64: string): ArrayBuffer {
144
+ const binstr = window.atob(base64)
145
+ const len = binstr.length
146
+ const bytes = new Uint8Array(len)
147
+ for (let i = 0; i < len; i++) {
148
+ bytes[i] = binstr.charCodeAt(i)
149
+ }
150
+ return bytes.buffer
151
+ }
152
+
153
+ export function dataURLToArrayBuffer(url: string): ArrayBuffer {
154
+ return base64ToArrayBuffer(url.split(",")[1])
155
+ }
156
+
157
+ export function download(filename: string, url: string) {
158
+ const a = document.createElement("a")
159
+ a.href = url
160
+ a.download = filename
161
+ a.click()
162
+ }
163
+
164
+ export function downloadText(filename: string, text: string) {
165
+ download(filename, "data:text/plain;charset=utf-8," + text)
166
+ }
167
+
168
+ export function downloadJSON(filename: string, data: any) {
169
+ downloadText(filename, JSON.stringify(data))
170
+ }
171
+
172
+ export function downloadBlob(filename: string, blob: Blob) {
173
+ const url = URL.createObjectURL(blob)
174
+ download(filename, url)
175
+ URL.revokeObjectURL(url)
176
+ }
177
+
178
+ export const isDataURL = (str: string) => str.match(/^data:\w+\/\w+;base64,.+/)
179
+ export const getFileExt = (p: string) => p.split(".").pop()
180
+ export const getFileName = (p: string) => p.split(".").slice(0, -1).join(".")
181
+
182
+ type Func = (...args: any[]) => any
183
+
184
+ export function overload2<A extends Func, B extends Func>(fn1: A, fn2: B): A & B {
185
+ return ((...args) => {
186
+ const al = args.length
187
+ if (al === fn1.length) return fn1(...args)
188
+ if (al === fn2.length) return fn2(...args)
189
+ }) as A & B
190
+ }
191
+
192
+ export function overload3<
193
+ A extends Func,
194
+ B extends Func,
195
+ C extends Func,
196
+ >(fn1: A, fn2: B, fn3: C): A & B & C {
197
+ return ((...args) => {
198
+ const al = args.length
199
+ if (al === fn1.length) return fn1(...args)
200
+ if (al === fn2.length) return fn2(...args)
201
+ if (al === fn3.length) return fn3(...args)
202
+ }) as A & B & C
203
+ }
204
+
205
+ export function overload4<
206
+ A extends Func,
207
+ B extends Func,
208
+ C extends Func,
209
+ D extends Func,
210
+ >(fn1: A, fn2: B, fn3: C, fn4: D): A & B & C & D {
211
+ return ((...args) => {
212
+ const al = args.length
213
+ if (al === fn1.length) return fn1(...args)
214
+ if (al === fn2.length) return fn2(...args)
215
+ if (al === fn3.length) return fn3(...args)
216
+ if (al === fn4.length) return fn4(...args)
217
+ }) as A & B & C & D
218
+ }
219
+
220
+ export const uid = (() => {
221
+ let id = 0
222
+ return () => id++
223
+ })()
224
+
225
+ export const getErrorMessage = (error: unknown) =>
226
+ (error instanceof Error) ? error.message : String(error)
227
+
228
+ const warned = new Set()
229
+
230
+ export function warn(msg: string) {
231
+ if (!warned.has(msg)) {
232
+ warned.add(msg)
233
+ console.warn(msg)
234
+ }
235
+ }
236
+
237
+ export function deprecateMsg(oldName: string, newName: string) {
238
+ warn(`${oldName} is deprecated. Use ${newName} instead.`)
239
+ }
240
+
241
+ export function deprecate(oldName: string, newName: string, newFunc: (...args) => any) {
242
+ return (...args) => {
243
+ deprecateMsg(oldName, newName)
244
+ return newFunc(...args)
245
+ }
246
+ }
247
+
248
+ export function benchmark(task: () => void, times: number = 1) {
249
+ const t1 = performance.now()
250
+ for (let i = 0; i < times; i++) {
251
+ task()
252
+ }
253
+ const t2 = performance.now()
254
+ return t2 - t1
255
+ }
256
+
257
+ export function comparePerf(t1: () => void, t2: () => void, times: number = 1) {
258
+ return benchmark(t2, times) / benchmark(t1, times)
259
+ }
260
+
261
+ export class BinaryHeap<T> {
262
+ _items: T[]
263
+ _compareFn: (a: T, b: T) => boolean
264
+
265
+ /**
266
+ * Creates a binary heap with the given compare function
267
+ * Not passing a compare function will give a min heap
268
+ */
269
+ constructor(compareFn = (a: T, b: T) => a < b) {
270
+ this._compareFn = compareFn
271
+ this._items = []
272
+ }
273
+
274
+ /**
275
+ * Insert an item into the binary heap
276
+ */
277
+ insert(item: T) {
278
+ this._items.push(item)
279
+ this.moveUp(this._items.length - 1)
280
+ }
281
+
282
+ /**
283
+ * Remove the smallest item from the binary heap in case of a min heap
284
+ * or the greatest item from the binary heap in case of a max heap
285
+ */
286
+ remove() {
287
+ if (this._items.length === 0)
288
+ return null
289
+ const item = this._items[0]
290
+ const lastItem = this._items.pop()
291
+ if (this._items.length !== 0) {
292
+ this._items[0] = lastItem as T
293
+ this.moveDown(0)
294
+ }
295
+ return item
296
+ }
297
+
298
+ /**
299
+ * Remove all items
300
+ */
301
+ clear() {
302
+ this._items.splice(0, this._items.length)
303
+ }
304
+
305
+ moveUp(pos: number) {
306
+ while (pos > 0) {
307
+ const parent = Math.floor((pos - 1) / 2)
308
+ if (!this._compareFn(this._items[pos], this._items[parent]))
309
+ if (this._items[pos] >= this._items[parent])
310
+ break
311
+ this.swap(pos, parent)
312
+ pos = parent
313
+ }
314
+ }
315
+
316
+ moveDown(pos: number) {
317
+ while (pos < Math.floor(this._items.length / 2)) {
318
+ let child = 2 * pos + 1
319
+ if (child < this._items.length - 1 && !this._compareFn(this._items[child], this._items[child + 1]))
320
+ ++child
321
+ if (this._compareFn(this._items[pos], this._items[child]))
322
+ break
323
+ this.swap(pos, child)
324
+ pos = child
325
+ }
326
+ }
327
+
328
+ swap(index1: number, index2: number) {
329
+ [this._items[index1], this._items[index2]] = [this._items[index2], this._items[index1]]
330
+ }
331
+
332
+ /**
333
+ * Returns the amount of items
334
+ */
335
+ get length() {
336
+ return this._items.length
337
+ }
338
+ }
339
+
340
+ const enum EnumRunesCode {
341
+ HIGH_SURROGATE_START = 0xd800,
342
+ HIGH_SURROGATE_END = 0xdbff,
343
+
344
+ LOW_SURROGATE_START = 0xdc00,
345
+
346
+ REGIONAL_INDICATOR_START = 0x1f1e6,
347
+ REGIONAL_INDICATOR_END = 0x1f1ff,
348
+
349
+ FITZPATRICK_MODIFIER_START = 0x1f3fb,
350
+ FITZPATRICK_MODIFIER_END = 0x1f3ff,
351
+
352
+ VARIATION_MODIFIER_START = 0xfe00,
353
+ VARIATION_MODIFIER_END = 0xfe0f,
354
+
355
+ DIACRITICAL_MARKS_START = 0x20d0,
356
+ DIACRITICAL_MARKS_END = 0x20ff,
357
+
358
+ SUBDIVISION_INDICATOR_START = 0x1f3f4,
359
+ TAGS_START = 0xe0000,
360
+ TAGS_END = 0xe007f,
361
+
362
+ ZWJ = 0x200d,
363
+ }
364
+
365
+ const GRAPHEMES = Object.freeze([
366
+ 0x0308, // ( ◌̈ ) COMBINING DIAERESIS
367
+ 0x0937, // ( ष ) DEVANAGARI LETTER SSA
368
+ 0x093F, // ( ि ) DEVANAGARI VOWEL SIGN I
369
+ 0x0BA8, // ( ந ) TAMIL LETTER NA
370
+ 0x0BBF, // ( ி ) TAMIL VOWEL SIGN I
371
+ 0x0BCD, // ( ◌்) TAMIL SIGN VIRAMA
372
+ 0x0E31, // ( ◌ั ) THAI CHARACTER MAI HAN-AKAT
373
+ 0x0E33, // ( ำ ) THAI CHARACTER SARA AM
374
+ 0x0E40, // ( เ ) THAI CHARACTER SARA E
375
+ 0x0E49, // ( เ ) THAI CHARACTER MAI THO
376
+ 0x1100, // ( ᄀ ) HANGUL CHOSEONG KIYEOK
377
+ 0x1161, // ( ᅡ ) HANGUL JUNGSEONG A
378
+ 0x11A8, // ( ᆨ ) HANGUL JONGSEONG KIYEOK
379
+ ])
380
+
381
+ enum EnumCodeUnits {
382
+ unit_1 = 1,
383
+ unit_2 = 2,
384
+ unit_4 = 4,
385
+ }
386
+
387
+ export function runes(string: string): string[] {
388
+ if (typeof string !== "string") {
389
+ throw new TypeError("string cannot be undefined or null")
390
+ }
391
+ const result: string[] = []
392
+ let i = 0
393
+ let increment = 0
394
+ while (i < string.length) {
395
+ increment += nextUnits(i + increment, string)
396
+ if (isGrapheme(string[i + increment])) {
397
+ increment++
398
+ }
399
+ if (isVariationSelector(string[i + increment])) {
400
+ increment++
401
+ }
402
+ if (isDiacriticalMark(string[i + increment])) {
403
+ increment++
404
+ }
405
+ if (isZeroWidthJoiner(string[i + increment])) {
406
+ increment++
407
+ continue
408
+ }
409
+ result.push(string.substring(i, i + increment))
410
+ i += increment
411
+ increment = 0
412
+ }
413
+ return result
414
+ }
415
+
416
+ // Decide how many code units make up the current character.
417
+ // BMP characters: 1 code unit
418
+ // Non-BMP characters (represented by surrogate pairs): 2 code units
419
+ // Emoji with skin-tone modifiers: 4 code units (2 code points)
420
+ // Country flags: 4 code units (2 code points)
421
+ // Variations: 2 code units
422
+ // Subdivision flags: 14 code units (7 code points)
423
+ function nextUnits(i: number, string: string) {
424
+ const current = string[i]
425
+ // If we don't have a value that is part of a surrogate pair, or we're at
426
+ // the end, only take the value at i
427
+ if (!isFirstOfSurrogatePair(current) || i === string.length - 1) {
428
+ return EnumCodeUnits.unit_1
429
+ }
430
+
431
+ const currentPair = current + string[i + 1]
432
+ const nextPair = string.substring(i + 2, i + 5)
433
+
434
+ // Country flags are comprised of two regional indicator symbols,
435
+ // each represented by a surrogate pair.
436
+ // See http://emojipedia.org/flags/
437
+ // If both pairs are regional indicator symbols, take 4
438
+ if (isRegionalIndicator(currentPair) && isRegionalIndicator(nextPair)) {
439
+ return EnumCodeUnits.unit_4
440
+ }
441
+
442
+ // https://unicode.org/emoji/charts/full-emoji-list.html#subdivision-flag
443
+ // See https://emojipedia.org/emoji-tag-sequence/
444
+ // If nextPair is in Tags(https://en.wikipedia.org/wiki/Tags_(Unicode_block)),
445
+ // then find next closest U+E007F(CANCEL TAG)
446
+ if (isSubdivisionFlag(currentPair) && isSupplementarySpecialpurposePlane(nextPair)) {
447
+ return string.slice(i).indexOf(String.fromCodePoint(EnumRunesCode.TAGS_END)) + 2
448
+ }
449
+
450
+ // If the next pair make a Fitzpatrick skin tone
451
+ // modifier, take 4
452
+ // See http://emojipedia.org/modifiers/
453
+ // Technically, only some code points are meant to be
454
+ // combined with the skin tone modifiers. This function
455
+ // does not check the current pair to see if it is
456
+ // one of them.
457
+ if (isFitzpatrickModifier(nextPair)) {
458
+ return EnumCodeUnits.unit_4
459
+ }
460
+ return EnumCodeUnits.unit_2
461
+ }
462
+
463
+ function isFirstOfSurrogatePair(string: string) {
464
+ return string && betweenInclusive(string[0].charCodeAt(0), EnumRunesCode.HIGH_SURROGATE_START, EnumRunesCode.HIGH_SURROGATE_END)
465
+ }
466
+
467
+ function isRegionalIndicator(string: string) {
468
+ return betweenInclusive(codePointFromSurrogatePair(string), EnumRunesCode.REGIONAL_INDICATOR_START, EnumRunesCode.REGIONAL_INDICATOR_END)
469
+ }
470
+
471
+ function isSubdivisionFlag(string: string) {
472
+ return betweenInclusive(codePointFromSurrogatePair(string), EnumRunesCode.SUBDIVISION_INDICATOR_START, EnumRunesCode.SUBDIVISION_INDICATOR_START)
473
+ }
474
+
475
+ function isFitzpatrickModifier(string: string) {
476
+ return betweenInclusive(codePointFromSurrogatePair(string), EnumRunesCode.FITZPATRICK_MODIFIER_START, EnumRunesCode.FITZPATRICK_MODIFIER_END)
477
+ }
478
+
479
+ function isVariationSelector(string: string) {
480
+ return typeof string === "string" && betweenInclusive(string.charCodeAt(0), EnumRunesCode.VARIATION_MODIFIER_START, EnumRunesCode.VARIATION_MODIFIER_END)
481
+ }
482
+
483
+ function isDiacriticalMark(string: string) {
484
+ return typeof string === "string" && betweenInclusive(string.charCodeAt(0), EnumRunesCode.DIACRITICAL_MARKS_START, EnumRunesCode.DIACRITICAL_MARKS_END)
485
+ }
486
+
487
+ function isSupplementarySpecialpurposePlane(string: string) {
488
+ const codePoint = string.codePointAt(0)
489
+ return (typeof string === "string" && typeof codePoint === "number" && betweenInclusive(codePoint, EnumRunesCode.TAGS_START, EnumRunesCode.TAGS_END))
490
+ }
491
+
492
+ function isGrapheme(string: string) {
493
+ return typeof string === "string" && GRAPHEMES.includes(string.charCodeAt(0))
494
+ }
495
+
496
+ function isZeroWidthJoiner(string: string) {
497
+ return typeof string === "string" && string.charCodeAt(0) === EnumRunesCode.ZWJ
498
+ }
499
+
500
+ function codePointFromSurrogatePair(pair: string) {
501
+ const highOffset = pair.charCodeAt(0) - EnumRunesCode.HIGH_SURROGATE_START
502
+ const lowOffset = pair.charCodeAt(1) - EnumRunesCode.LOW_SURROGATE_START
503
+ return (highOffset << 10) + lowOffset + 0x10000
504
+ }
505
+
506
+ function betweenInclusive(value: number, lower: number, upper: number) {
507
+ return value >= lower && value <= upper
508
+ }
509
+
510
+ export function substring(string: string, start?: number, width?: number) {
511
+ const chars = runes(string)
512
+ if (start === undefined) {
513
+ return string
514
+ }
515
+ if (start >= chars.length) {
516
+ return ""
517
+ }
518
+ const rest = chars.length - start
519
+ const stringWidth = width === undefined ? rest : width
520
+ let endIndex = start + stringWidth
521
+ if (endIndex > (start + rest)) {
522
+ endIndex = undefined
523
+ }
524
+ return chars.slice(start, endIndex).join("")
525
+ }