kaplay 3001.0.0-alpha.1

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