@ibodr/utils 0.0.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/dist/index.d.ts +3098 -0
- package/dist/index.mjs +2573 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +43 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,2573 @@
|
|
|
1
|
+
export { default as isEqual } from 'lodash.isequal';
|
|
2
|
+
import isEqualWith from 'lodash.isequalwith';
|
|
3
|
+
export { default as isEqualWith } from 'lodash.isequalwith';
|
|
4
|
+
export { default as throttle } from 'lodash.throttle';
|
|
5
|
+
export { default as uniq } from 'lodash.uniq';
|
|
6
|
+
import { generateKeyBetween, generateNKeysBetween } from 'jittered-fractional-indexing';
|
|
7
|
+
|
|
8
|
+
// src/lib/version.ts
|
|
9
|
+
var DRAW_LIBRARY_VERSION_KEY = "__TLDRAW_LIBRARY_VERSIONS__";
|
|
10
|
+
function getLibraryVersions() {
|
|
11
|
+
if (globalThis[DRAW_LIBRARY_VERSION_KEY]) {
|
|
12
|
+
return globalThis[DRAW_LIBRARY_VERSION_KEY];
|
|
13
|
+
}
|
|
14
|
+
const info = {
|
|
15
|
+
versions: [],
|
|
16
|
+
didWarn: false,
|
|
17
|
+
scheduledNotice: null
|
|
18
|
+
};
|
|
19
|
+
Object.defineProperty(globalThis, DRAW_LIBRARY_VERSION_KEY, {
|
|
20
|
+
value: info,
|
|
21
|
+
writable: false,
|
|
22
|
+
configurable: false,
|
|
23
|
+
enumerable: false
|
|
24
|
+
});
|
|
25
|
+
return info;
|
|
26
|
+
}
|
|
27
|
+
function registerDrawLibraryVersion(name, version, modules) {
|
|
28
|
+
if (!name || !version || !modules) {
|
|
29
|
+
{
|
|
30
|
+
throw new Error("Missing name/version/module system in built version of tldraw library");
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
const info = getLibraryVersions();
|
|
34
|
+
if (isNextjsDev()) {
|
|
35
|
+
const isDuplicate = info.versions.some(
|
|
36
|
+
(v) => v.name === name && v.version === version && v.modules === modules
|
|
37
|
+
);
|
|
38
|
+
if (isDuplicate) return;
|
|
39
|
+
}
|
|
40
|
+
info.versions.push({ name, version, modules });
|
|
41
|
+
if (!info.scheduledNotice) {
|
|
42
|
+
try {
|
|
43
|
+
info.scheduledNotice = setTimeout(() => {
|
|
44
|
+
info.scheduledNotice = null;
|
|
45
|
+
checkLibraryVersions(info);
|
|
46
|
+
}, 100);
|
|
47
|
+
} catch {
|
|
48
|
+
checkLibraryVersions(info);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function checkLibraryVersions(info) {
|
|
53
|
+
if (!info.versions.length) return;
|
|
54
|
+
if (info.didWarn) return;
|
|
55
|
+
const sorted = info.versions.sort((a, b) => compareVersions(a.version, b.version));
|
|
56
|
+
const latestVersion = sorted[sorted.length - 1].version;
|
|
57
|
+
const matchingVersions = /* @__PURE__ */ new Set();
|
|
58
|
+
const nonMatchingVersions = /* @__PURE__ */ new Map();
|
|
59
|
+
for (const lib of sorted) {
|
|
60
|
+
if (nonMatchingVersions.has(lib.name)) {
|
|
61
|
+
matchingVersions.delete(lib.name);
|
|
62
|
+
entry(nonMatchingVersions, lib.name, /* @__PURE__ */ new Set()).add(lib.version);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (lib.version === latestVersion) {
|
|
66
|
+
matchingVersions.add(lib.name);
|
|
67
|
+
} else {
|
|
68
|
+
matchingVersions.delete(lib.name);
|
|
69
|
+
entry(nonMatchingVersions, lib.name, /* @__PURE__ */ new Set()).add(lib.version);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (nonMatchingVersions.size > 0) {
|
|
73
|
+
const message = [
|
|
74
|
+
`${format("[tldraw]", ["bold", "bgRed", "textWhite"])} ${format("You have multiple versions of tldraw libraries installed. This can lead to bugs and unexpected behavior.", ["textRed", "bold"])}`,
|
|
75
|
+
"",
|
|
76
|
+
`The latest version you have installed is ${format(`v${latestVersion}`, ["bold", "textBlue"])}. The following libraries are on the latest version:`,
|
|
77
|
+
...Array.from(matchingVersions, (name) => ` \u2022 \u2705 ${format(name, ["bold"])}`),
|
|
78
|
+
"",
|
|
79
|
+
`The following libraries are not on the latest version, or have multiple versions installed:`,
|
|
80
|
+
...Array.from(nonMatchingVersions, ([name, versions]) => {
|
|
81
|
+
const sortedVersions = Array.from(versions).sort(compareVersions).map((v) => format(`v${v}`, v === latestVersion ? ["textGreen"] : ["textRed"]));
|
|
82
|
+
return ` \u2022 \u274C ${format(name, ["bold"])} (${sortedVersions.join(", ")})`;
|
|
83
|
+
})
|
|
84
|
+
];
|
|
85
|
+
console.log(message.join("\n"));
|
|
86
|
+
info.didWarn = true;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const potentialDuplicates = /* @__PURE__ */ new Map();
|
|
90
|
+
for (const lib of sorted) {
|
|
91
|
+
entry(potentialDuplicates, lib.name, { version: lib.version, modules: [] }).modules.push(
|
|
92
|
+
lib.modules
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const duplicates = /* @__PURE__ */ new Map();
|
|
96
|
+
for (const [name, lib] of potentialDuplicates) {
|
|
97
|
+
if (lib.modules.length > 1) duplicates.set(name, lib);
|
|
98
|
+
}
|
|
99
|
+
if (duplicates.size > 0) {
|
|
100
|
+
const message = [
|
|
101
|
+
`${format("[tldraw]", ["bold", "bgRed", "textWhite"])} ${format("You have multiple instances of some tldraw libraries active. This can lead to bugs and unexpected behavior. ", ["textRed", "bold"])}`,
|
|
102
|
+
"",
|
|
103
|
+
"This usually means that your bundler is misconfigured, and is importing the same library multiple times - usually once as an ES Module, and once as a CommonJS module.",
|
|
104
|
+
"",
|
|
105
|
+
"The following libraries have been imported multiple times:",
|
|
106
|
+
...Array.from(duplicates, ([name, lib]) => {
|
|
107
|
+
const modules = lib.modules.map((m, i) => m === "esm" ? ` ${i + 1}. ES Modules` : ` ${i + 1}. CommonJS`).join("\n");
|
|
108
|
+
return ` \u2022 \u274C ${format(name, ["bold"])} v${lib.version}:
|
|
109
|
+
${modules}`;
|
|
110
|
+
}),
|
|
111
|
+
"",
|
|
112
|
+
"You should configure your bundler to only import one version of each library."
|
|
113
|
+
];
|
|
114
|
+
console.log(message.join("\n"));
|
|
115
|
+
info.didWarn = true;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function compareVersions(a, b) {
|
|
120
|
+
const aMatch = a.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/);
|
|
121
|
+
const bMatch = b.match(/^(\d+)\.(\d+)\.(\d+)(?:-(\w+))?$/);
|
|
122
|
+
if (!aMatch || !bMatch) return a.localeCompare(b);
|
|
123
|
+
if (aMatch[1] !== bMatch[1]) return Number(aMatch[1]) - Number(bMatch[1]);
|
|
124
|
+
if (aMatch[2] !== bMatch[2]) return Number(aMatch[2]) - Number(bMatch[2]);
|
|
125
|
+
if (aMatch[3] !== bMatch[3]) return Number(aMatch[3]) - Number(bMatch[3]);
|
|
126
|
+
if (aMatch[4] && bMatch[4]) return aMatch[4].localeCompare(bMatch[4]);
|
|
127
|
+
if (aMatch[4]) return 1;
|
|
128
|
+
if (bMatch[4]) return -1;
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
var formats = {
|
|
132
|
+
bold: "1",
|
|
133
|
+
textBlue: "94",
|
|
134
|
+
textRed: "31",
|
|
135
|
+
textGreen: "32",
|
|
136
|
+
bgRed: "41",
|
|
137
|
+
textWhite: "97"
|
|
138
|
+
};
|
|
139
|
+
function format(value, formatters = []) {
|
|
140
|
+
return `\x1B[${formatters.map((f) => formats[f]).join(";")}m${value}\x1B[m`;
|
|
141
|
+
}
|
|
142
|
+
function isNextjsDev() {
|
|
143
|
+
try {
|
|
144
|
+
return "__NEXT_DATA__" in globalThis;
|
|
145
|
+
} catch {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
function entry(map, key, defaultValue) {
|
|
150
|
+
if (map.has(key)) {
|
|
151
|
+
return map.get(key);
|
|
152
|
+
}
|
|
153
|
+
map.set(key, defaultValue);
|
|
154
|
+
return defaultValue;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// src/lib/array.ts
|
|
158
|
+
function rotateArray(arr, offset) {
|
|
159
|
+
if (arr.length === 0) return [];
|
|
160
|
+
const normalizedOffset = (Math.abs(offset) % arr.length + arr.length) % arr.length;
|
|
161
|
+
return [...arr.slice(normalizedOffset), ...arr.slice(0, normalizedOffset)];
|
|
162
|
+
}
|
|
163
|
+
function dedupe(input, equals) {
|
|
164
|
+
const result = [];
|
|
165
|
+
mainLoop: for (const item of input) {
|
|
166
|
+
for (const existing of result) {
|
|
167
|
+
if (equals ? equals(item, existing) : item === existing) {
|
|
168
|
+
continue mainLoop;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
result.push(item);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
function compact(arr) {
|
|
176
|
+
return arr.filter((i) => i !== void 0 && i !== null);
|
|
177
|
+
}
|
|
178
|
+
function last(arr) {
|
|
179
|
+
return arr[arr.length - 1];
|
|
180
|
+
}
|
|
181
|
+
function minBy(arr, fn) {
|
|
182
|
+
let min;
|
|
183
|
+
let minVal = Infinity;
|
|
184
|
+
for (const item of arr) {
|
|
185
|
+
const val = fn(item);
|
|
186
|
+
if (val < minVal) {
|
|
187
|
+
min = item;
|
|
188
|
+
minVal = val;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return min;
|
|
192
|
+
}
|
|
193
|
+
function maxBy(arr, fn) {
|
|
194
|
+
let max;
|
|
195
|
+
let maxVal = -Infinity;
|
|
196
|
+
for (const item of arr) {
|
|
197
|
+
const val = fn(item);
|
|
198
|
+
if (val > maxVal) {
|
|
199
|
+
max = item;
|
|
200
|
+
maxVal = val;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return max;
|
|
204
|
+
}
|
|
205
|
+
function partition(arr, predicate) {
|
|
206
|
+
const satisfies = [];
|
|
207
|
+
const doesNotSatisfy = [];
|
|
208
|
+
for (const item of arr) {
|
|
209
|
+
if (predicate(item)) {
|
|
210
|
+
satisfies.push(item);
|
|
211
|
+
} else {
|
|
212
|
+
doesNotSatisfy.push(item);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
return [satisfies, doesNotSatisfy];
|
|
216
|
+
}
|
|
217
|
+
function areArraysShallowEqual(arr1, arr2) {
|
|
218
|
+
if (arr1 === arr2) return true;
|
|
219
|
+
if (arr1.length !== arr2.length) return false;
|
|
220
|
+
for (let i = 0; i < arr1.length; i++) {
|
|
221
|
+
if (!Object.is(arr1[i], arr2[i])) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return true;
|
|
226
|
+
}
|
|
227
|
+
function mergeArraysAndReplaceDefaults(key, customEntries, defaults) {
|
|
228
|
+
const overrideTypes = new Set(customEntries.map((entry2) => entry2[key]));
|
|
229
|
+
const result = [];
|
|
230
|
+
for (const defaultEntry of defaults) {
|
|
231
|
+
if (overrideTypes.has(defaultEntry[key])) continue;
|
|
232
|
+
result.push(defaultEntry);
|
|
233
|
+
}
|
|
234
|
+
for (const customEntry of customEntries) {
|
|
235
|
+
result.push(customEntry);
|
|
236
|
+
}
|
|
237
|
+
return result;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// src/lib/function.ts
|
|
241
|
+
function omitFromStackTrace(fn) {
|
|
242
|
+
const wrappedFn = (...args) => {
|
|
243
|
+
try {
|
|
244
|
+
return fn(...args);
|
|
245
|
+
} catch (error) {
|
|
246
|
+
if (error instanceof Error && Error.captureStackTrace) {
|
|
247
|
+
Error.captureStackTrace(error, wrappedFn);
|
|
248
|
+
}
|
|
249
|
+
throw error;
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
return wrappedFn;
|
|
253
|
+
}
|
|
254
|
+
var noop = () => {
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/lib/control.ts
|
|
258
|
+
var Result = {
|
|
259
|
+
/**
|
|
260
|
+
* Create a successful result containing a value.
|
|
261
|
+
*
|
|
262
|
+
* @param value - The success value to wrap
|
|
263
|
+
* @returns An OkResult containing the value
|
|
264
|
+
*/
|
|
265
|
+
ok(value) {
|
|
266
|
+
return { ok: true, value };
|
|
267
|
+
},
|
|
268
|
+
/**
|
|
269
|
+
* Create a failed result containing an error.
|
|
270
|
+
*
|
|
271
|
+
* @param error - The error value to wrap
|
|
272
|
+
* @returns An ErrorResult containing the error
|
|
273
|
+
*/
|
|
274
|
+
err(error) {
|
|
275
|
+
return { ok: false, error };
|
|
276
|
+
},
|
|
277
|
+
/**
|
|
278
|
+
* Create a successful result containing an array of values.
|
|
279
|
+
*
|
|
280
|
+
* If any of the results are errors, the returned result will be an error containing the first error.
|
|
281
|
+
*
|
|
282
|
+
* @param results - The array of results to wrap
|
|
283
|
+
* @returns An OkResult containing the array of values
|
|
284
|
+
*/
|
|
285
|
+
all(results) {
|
|
286
|
+
return results.every((result) => result.ok) ? Result.ok(results.map((result) => result.value)) : Result.err(results.find((result) => !result.ok)?.error);
|
|
287
|
+
}
|
|
288
|
+
};
|
|
289
|
+
function exhaustiveSwitchError(value, property) {
|
|
290
|
+
const debugValue = property && value && typeof value === "object" && property in value ? value[property] : value;
|
|
291
|
+
throw new Error(`Unknown switch case ${debugValue}`);
|
|
292
|
+
}
|
|
293
|
+
var assert = omitFromStackTrace(
|
|
294
|
+
(value, message) => {
|
|
295
|
+
if (!value) {
|
|
296
|
+
throw new Error(message || "Assertion Error");
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
);
|
|
300
|
+
var assertExists = omitFromStackTrace((value, message) => {
|
|
301
|
+
if (value == null) {
|
|
302
|
+
throw new Error(message ?? "value must be defined");
|
|
303
|
+
}
|
|
304
|
+
return value;
|
|
305
|
+
});
|
|
306
|
+
function promiseWithResolve() {
|
|
307
|
+
let resolve;
|
|
308
|
+
let reject;
|
|
309
|
+
const promise = new Promise((res, rej) => {
|
|
310
|
+
resolve = res;
|
|
311
|
+
reject = rej;
|
|
312
|
+
});
|
|
313
|
+
return Object.assign(promise, {
|
|
314
|
+
resolve,
|
|
315
|
+
reject
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function sleep(ms) {
|
|
319
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// src/lib/bind.ts
|
|
323
|
+
function bind(...args) {
|
|
324
|
+
if (args.length === 2) {
|
|
325
|
+
const [originalMethod, context] = args;
|
|
326
|
+
context.addInitializer(function initializeMethod() {
|
|
327
|
+
assert(Reflect.isExtensible(this), "Cannot bind to a non-extensible class.");
|
|
328
|
+
const value = originalMethod.bind(this);
|
|
329
|
+
const ok = Reflect.defineProperty(this, context.name, {
|
|
330
|
+
value,
|
|
331
|
+
writable: true,
|
|
332
|
+
configurable: true
|
|
333
|
+
});
|
|
334
|
+
assert(ok, "Cannot bind a non-configurable class method.");
|
|
335
|
+
});
|
|
336
|
+
} else {
|
|
337
|
+
const [_target, propertyKey, descriptor] = args;
|
|
338
|
+
if (!descriptor || typeof descriptor.value !== "function") {
|
|
339
|
+
throw new TypeError(
|
|
340
|
+
`Only methods can be decorated with @bind. <${propertyKey}> is not a method!`
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
configurable: true,
|
|
345
|
+
get() {
|
|
346
|
+
const bound = descriptor.value.bind(this);
|
|
347
|
+
Object.defineProperty(this, propertyKey, {
|
|
348
|
+
value: bound,
|
|
349
|
+
configurable: true,
|
|
350
|
+
writable: true
|
|
351
|
+
});
|
|
352
|
+
return bound;
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// src/lib/cache.ts
|
|
359
|
+
var WeakCache = class {
|
|
360
|
+
/**
|
|
361
|
+
* The internal WeakMap storage for cached key-value pairs.
|
|
362
|
+
*
|
|
363
|
+
* @public
|
|
364
|
+
*/
|
|
365
|
+
items = /* @__PURE__ */ new WeakMap();
|
|
366
|
+
/**
|
|
367
|
+
* Get the cached value for a given key, computing it if not already cached.
|
|
368
|
+
*
|
|
369
|
+
* Retrieves the cached value associated with the given key. If no cached
|
|
370
|
+
* value exists, calls the provided callback function to compute the value, stores it
|
|
371
|
+
* in the cache, and returns it. Subsequent calls with the same key will return the
|
|
372
|
+
* cached value without recomputation.
|
|
373
|
+
*
|
|
374
|
+
* @param item - The object key to retrieve the cached value for
|
|
375
|
+
* @param cb - Callback function that computes the value when not already cached
|
|
376
|
+
* @returns The cached value if it exists, otherwise the newly computed value from the callback
|
|
377
|
+
*
|
|
378
|
+
* @example
|
|
379
|
+
* ```ts
|
|
380
|
+
* const cache = new WeakCache<HTMLElement, DOMRect>()
|
|
381
|
+
* const element = document.getElementById('my-element')!
|
|
382
|
+
*
|
|
383
|
+
* // First call computes and caches the bounding rect
|
|
384
|
+
* const rect1 = cache.get(element, (el) => el.getBoundingClientRect())
|
|
385
|
+
*
|
|
386
|
+
* // Second call returns cached value
|
|
387
|
+
* const rect2 = cache.get(element, (el) => el.getBoundingClientRect())
|
|
388
|
+
* // rect1 and rect2 are the same object
|
|
389
|
+
* ```
|
|
390
|
+
*/
|
|
391
|
+
get(item, cb) {
|
|
392
|
+
if (!this.items.has(item)) {
|
|
393
|
+
this.items.set(item, cb(item));
|
|
394
|
+
}
|
|
395
|
+
return this.items.get(item);
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
// src/lib/debounce.ts
|
|
400
|
+
function debounce(callback, wait) {
|
|
401
|
+
let state = void 0;
|
|
402
|
+
const fn = (...args) => {
|
|
403
|
+
if (!state) {
|
|
404
|
+
state = {};
|
|
405
|
+
state.promise = new Promise((resolve, reject) => {
|
|
406
|
+
state.resolve = resolve;
|
|
407
|
+
state.reject = reject;
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
clearTimeout(state.timeout);
|
|
411
|
+
state.latestArgs = args;
|
|
412
|
+
state.timeout = setTimeout(() => {
|
|
413
|
+
const s = state;
|
|
414
|
+
state = void 0;
|
|
415
|
+
try {
|
|
416
|
+
s.resolve(callback(...s.latestArgs));
|
|
417
|
+
} catch (e) {
|
|
418
|
+
s.reject(e);
|
|
419
|
+
}
|
|
420
|
+
}, wait);
|
|
421
|
+
return state.promise;
|
|
422
|
+
};
|
|
423
|
+
fn.cancel = () => {
|
|
424
|
+
if (!state) return;
|
|
425
|
+
clearTimeout(state.timeout);
|
|
426
|
+
state = void 0;
|
|
427
|
+
};
|
|
428
|
+
return fn;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
// src/lib/error.ts
|
|
432
|
+
var annotationsByError = /* @__PURE__ */ new WeakMap();
|
|
433
|
+
function annotateError(error, annotations) {
|
|
434
|
+
if (typeof error !== "object" || error === null) return;
|
|
435
|
+
let currentAnnotations = annotationsByError.get(error);
|
|
436
|
+
if (!currentAnnotations) {
|
|
437
|
+
currentAnnotations = { tags: {}, extras: {} };
|
|
438
|
+
annotationsByError.set(error, currentAnnotations);
|
|
439
|
+
}
|
|
440
|
+
if (annotations.tags) {
|
|
441
|
+
currentAnnotations.tags = {
|
|
442
|
+
...currentAnnotations.tags,
|
|
443
|
+
...annotations.tags
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
if (annotations.extras) {
|
|
447
|
+
currentAnnotations.extras = {
|
|
448
|
+
...currentAnnotations.extras,
|
|
449
|
+
...annotations.extras
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
function getErrorAnnotations(error) {
|
|
454
|
+
return annotationsByError.get(error) ?? { tags: {}, extras: {} };
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// src/lib/ExecutionQueue.ts
|
|
458
|
+
var ExecutionQueue = class {
|
|
459
|
+
/**
|
|
460
|
+
* Creates a new ExecutionQueue.
|
|
461
|
+
*
|
|
462
|
+
* Creates a new execution queue that will process tasks sequentially.
|
|
463
|
+
* If a timeout is provided, there will be a delay between each task execution,
|
|
464
|
+
* which is useful for rate limiting or controlling execution flow.
|
|
465
|
+
*
|
|
466
|
+
* timeout - Optional delay in milliseconds between task executions
|
|
467
|
+
* @example
|
|
468
|
+
* ```ts
|
|
469
|
+
* // Create queue without delay
|
|
470
|
+
* const fastQueue = new ExecutionQueue()
|
|
471
|
+
*
|
|
472
|
+
* // Create queue with 500ms delay between tasks
|
|
473
|
+
* const slowQueue = new ExecutionQueue(500)
|
|
474
|
+
* ```
|
|
475
|
+
*/
|
|
476
|
+
constructor(timeout) {
|
|
477
|
+
this.timeout = timeout;
|
|
478
|
+
}
|
|
479
|
+
timeout;
|
|
480
|
+
queue = [];
|
|
481
|
+
running = false;
|
|
482
|
+
/**
|
|
483
|
+
* Checks if the queue is empty and not currently running a task.
|
|
484
|
+
*
|
|
485
|
+
* Determines whether the execution queue has completed all tasks and is idle.
|
|
486
|
+
* Returns true only when there are no pending tasks in the queue AND no task is currently being executed.
|
|
487
|
+
*
|
|
488
|
+
* @returns True if the queue has no pending tasks and is not currently executing
|
|
489
|
+
* @example
|
|
490
|
+
* ```ts
|
|
491
|
+
* const queue = new ExecutionQueue()
|
|
492
|
+
*
|
|
493
|
+
* console.log(queue.isEmpty()) // true - queue is empty
|
|
494
|
+
*
|
|
495
|
+
* queue.push(() => console.log('task'))
|
|
496
|
+
* console.log(queue.isEmpty()) // false - task is running/pending
|
|
497
|
+
* ```
|
|
498
|
+
*/
|
|
499
|
+
isEmpty() {
|
|
500
|
+
return this.queue.length === 0 && !this.running;
|
|
501
|
+
}
|
|
502
|
+
async run() {
|
|
503
|
+
if (this.running) return;
|
|
504
|
+
try {
|
|
505
|
+
this.running = true;
|
|
506
|
+
while (this.queue.length) {
|
|
507
|
+
const task = this.queue.shift();
|
|
508
|
+
await task();
|
|
509
|
+
if (this.timeout) {
|
|
510
|
+
await sleep(this.timeout);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
} finally {
|
|
514
|
+
this.running = false;
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Adds a task to the queue and returns a promise that resolves with the task's result.
|
|
519
|
+
*
|
|
520
|
+
* Enqueues a task for sequential execution. The task will be executed after all
|
|
521
|
+
* previously queued tasks have completed. If a timeout was specified in the constructor,
|
|
522
|
+
* there will be a delay between this task and the next one.
|
|
523
|
+
*
|
|
524
|
+
* @param task - The function to execute (can be sync or async)
|
|
525
|
+
* @returns Promise that resolves with the task's return value
|
|
526
|
+
* @example
|
|
527
|
+
* ```ts
|
|
528
|
+
* const queue = new ExecutionQueue(100)
|
|
529
|
+
*
|
|
530
|
+
* // Add async task
|
|
531
|
+
* const result = await queue.push(async () => {
|
|
532
|
+
* const response = await fetch('/api/data')
|
|
533
|
+
* return response.json()
|
|
534
|
+
* })
|
|
535
|
+
*
|
|
536
|
+
* // Add sync task
|
|
537
|
+
* const number = await queue.push(() => 42)
|
|
538
|
+
* ```
|
|
539
|
+
*/
|
|
540
|
+
async push(task) {
|
|
541
|
+
return new Promise((resolve, reject) => {
|
|
542
|
+
this.queue.push(() => Promise.resolve(task()).then(resolve).catch(reject));
|
|
543
|
+
this.run();
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Clears all pending tasks from the queue.
|
|
548
|
+
*
|
|
549
|
+
* Immediately removes all pending tasks from the queue. Any currently
|
|
550
|
+
* running task will complete normally, but no additional tasks will be executed.
|
|
551
|
+
* This method does not wait for the current task to finish.
|
|
552
|
+
*
|
|
553
|
+
* @returns void
|
|
554
|
+
* @example
|
|
555
|
+
* ```ts
|
|
556
|
+
* const queue = new ExecutionQueue()
|
|
557
|
+
*
|
|
558
|
+
* // Add several tasks
|
|
559
|
+
* queue.push(() => console.log('task 1'))
|
|
560
|
+
* queue.push(() => console.log('task 2'))
|
|
561
|
+
* queue.push(() => console.log('task 3'))
|
|
562
|
+
*
|
|
563
|
+
* // Clear all pending tasks
|
|
564
|
+
* queue.close()
|
|
565
|
+
* // Only 'task 1' will execute if it was already running
|
|
566
|
+
* ```
|
|
567
|
+
*/
|
|
568
|
+
close() {
|
|
569
|
+
this.queue = [];
|
|
570
|
+
}
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
// src/lib/network.ts
|
|
574
|
+
async function fetch(input, init) {
|
|
575
|
+
return window.fetch(input, {
|
|
576
|
+
// We want to make sure that the referrer is not sent to other domains.
|
|
577
|
+
referrerPolicy: "strict-origin-when-cross-origin",
|
|
578
|
+
...init
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
var Image = (width, height) => {
|
|
582
|
+
const img = new window.Image(width, height);
|
|
583
|
+
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
584
|
+
return img;
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
// src/lib/file.ts
|
|
588
|
+
var FileHelpers = class _FileHelpers {
|
|
589
|
+
/**
|
|
590
|
+
* Converts a URL to an ArrayBuffer by fetching the resource.
|
|
591
|
+
*
|
|
592
|
+
* Fetches the resource at the given URL and returns its content as an ArrayBuffer.
|
|
593
|
+
* This is useful for loading binary data like images, videos, or other file types.
|
|
594
|
+
*
|
|
595
|
+
* @param url - The URL of the file to fetch
|
|
596
|
+
* @returns Promise that resolves to the file content as an ArrayBuffer
|
|
597
|
+
* @example
|
|
598
|
+
* ```ts
|
|
599
|
+
* const buffer = await FileHelpers.urlToArrayBuffer('https://example.com/image.png')
|
|
600
|
+
* console.log(buffer.byteLength) // Size of the file in bytes
|
|
601
|
+
* ```
|
|
602
|
+
* @public
|
|
603
|
+
*/
|
|
604
|
+
static async urlToArrayBuffer(url) {
|
|
605
|
+
const response = await fetch(url);
|
|
606
|
+
return await response.arrayBuffer();
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* Converts a URL to a Blob by fetching the resource.
|
|
610
|
+
*
|
|
611
|
+
* Fetches the resource at the given URL and returns its content as a Blob object.
|
|
612
|
+
* Blobs are useful for handling file data in web applications.
|
|
613
|
+
*
|
|
614
|
+
* @param url - The URL of the file to fetch
|
|
615
|
+
* @returns Promise that resolves to the file content as a Blob
|
|
616
|
+
* @example
|
|
617
|
+
* ```ts
|
|
618
|
+
* const blob = await FileHelpers.urlToBlob('https://example.com/document.pdf')
|
|
619
|
+
* console.log(blob.type) // 'application/pdf'
|
|
620
|
+
* console.log(blob.size) // Size in bytes
|
|
621
|
+
* ```
|
|
622
|
+
* @public
|
|
623
|
+
*/
|
|
624
|
+
static async urlToBlob(url) {
|
|
625
|
+
const response = await fetch(url);
|
|
626
|
+
return await response.blob();
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Converts a URL to a data URL by fetching the resource.
|
|
630
|
+
*
|
|
631
|
+
* Fetches the resource at the given URL and converts it to a base64-encoded data URL.
|
|
632
|
+
* If the URL is already a data URL, it returns the URL unchanged. This is useful for embedding
|
|
633
|
+
* resources directly in HTML or CSS.
|
|
634
|
+
*
|
|
635
|
+
* @param url - The URL of the file to convert, or an existing data URL
|
|
636
|
+
* @returns Promise that resolves to a data URL string
|
|
637
|
+
* @example
|
|
638
|
+
* ```ts
|
|
639
|
+
* const dataUrl = await FileHelpers.urlToDataUrl('https://example.com/image.jpg')
|
|
640
|
+
* // Returns: 'data:image/jpeg;base64,/9j/4AAQSkZJRgABAQEA...'
|
|
641
|
+
*
|
|
642
|
+
* const existing = await FileHelpers.urlToDataUrl('data:text/plain;base64,SGVsbG8=')
|
|
643
|
+
* // Returns the same data URL unchanged
|
|
644
|
+
* ```
|
|
645
|
+
* @public
|
|
646
|
+
*/
|
|
647
|
+
static async urlToDataUrl(url) {
|
|
648
|
+
if (url.startsWith("data:")) return url;
|
|
649
|
+
const blob = await _FileHelpers.urlToBlob(url);
|
|
650
|
+
return await _FileHelpers.blobToDataUrl(blob);
|
|
651
|
+
}
|
|
652
|
+
/**
|
|
653
|
+
* Convert a Blob to a base64 encoded data URL.
|
|
654
|
+
*
|
|
655
|
+
* Converts a Blob object to a base64-encoded data URL using the FileReader API.
|
|
656
|
+
* This is useful for displaying images or embedding file content directly in HTML.
|
|
657
|
+
*
|
|
658
|
+
* @param file - The Blob object to convert
|
|
659
|
+
* @returns Promise that resolves to a base64-encoded data URL string
|
|
660
|
+
* @example
|
|
661
|
+
* ```ts
|
|
662
|
+
* const blob = new Blob(['Hello World'], { type: 'text/plain' })
|
|
663
|
+
* const dataUrl = await FileHelpers.blobToDataUrl(blob)
|
|
664
|
+
* // Returns: 'data:text/plain;base64,SGVsbG8gV29ybGQ='
|
|
665
|
+
*
|
|
666
|
+
* // With an image file
|
|
667
|
+
* const imageDataUrl = await FileHelpers.blobToDataUrl(myImageFile)
|
|
668
|
+
* // Can be used directly in img src attribute
|
|
669
|
+
* ```
|
|
670
|
+
* @public
|
|
671
|
+
*/
|
|
672
|
+
static async blobToDataUrl(file) {
|
|
673
|
+
return await new Promise((resolve, reject) => {
|
|
674
|
+
if (file) {
|
|
675
|
+
const reader = new FileReader();
|
|
676
|
+
reader.onload = () => resolve(reader.result);
|
|
677
|
+
reader.onerror = (error) => reject(error);
|
|
678
|
+
reader.onabort = (error) => reject(error);
|
|
679
|
+
reader.readAsDataURL(file);
|
|
680
|
+
}
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
/**
|
|
684
|
+
* Convert a Blob to a unicode text string.
|
|
685
|
+
*
|
|
686
|
+
* Reads the content of a Blob object as a UTF-8 text string using the FileReader API.
|
|
687
|
+
* This is useful for reading text files or extracting text content from blobs.
|
|
688
|
+
*
|
|
689
|
+
* @param file - The Blob object to convert to text
|
|
690
|
+
* @returns Promise that resolves to the text content as a string
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* const textBlob = new Blob(['Hello World'], { type: 'text/plain' })
|
|
694
|
+
* const text = await FileHelpers.blobToText(textBlob)
|
|
695
|
+
* console.log(text) // 'Hello World'
|
|
696
|
+
*
|
|
697
|
+
* // With a text file from user input
|
|
698
|
+
* const content = await FileHelpers.blobToText(myTextFile)
|
|
699
|
+
* console.log(content) // File content as string
|
|
700
|
+
* ```
|
|
701
|
+
* @public
|
|
702
|
+
*/
|
|
703
|
+
static async blobToText(file) {
|
|
704
|
+
return await new Promise((resolve, reject) => {
|
|
705
|
+
if (file) {
|
|
706
|
+
const reader = new FileReader();
|
|
707
|
+
reader.onload = () => resolve(reader.result);
|
|
708
|
+
reader.onerror = (error) => reject(error);
|
|
709
|
+
reader.onabort = (error) => reject(error);
|
|
710
|
+
reader.readAsText(file);
|
|
711
|
+
}
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
static rewriteMimeType(blob, newMimeType) {
|
|
715
|
+
if (blob.type === newMimeType) return blob;
|
|
716
|
+
if (blob instanceof File) {
|
|
717
|
+
return new File([blob], blob.name, { type: newMimeType });
|
|
718
|
+
}
|
|
719
|
+
return new Blob([blob], { type: newMimeType });
|
|
720
|
+
}
|
|
721
|
+
};
|
|
722
|
+
|
|
723
|
+
// src/lib/hash.ts
|
|
724
|
+
function getHashForString(string) {
|
|
725
|
+
let hash = 0;
|
|
726
|
+
for (let i = 0; i < string.length; i++) {
|
|
727
|
+
hash = (hash << 5) - hash + string.charCodeAt(i);
|
|
728
|
+
hash |= 0;
|
|
729
|
+
}
|
|
730
|
+
return hash + "";
|
|
731
|
+
}
|
|
732
|
+
function getHashForObject(obj) {
|
|
733
|
+
return getHashForString(JSON.stringify(obj));
|
|
734
|
+
}
|
|
735
|
+
function getHashForBuffer(buffer) {
|
|
736
|
+
const view = new DataView(buffer);
|
|
737
|
+
let hash = 0;
|
|
738
|
+
for (let i = 0; i < view.byteLength; i++) {
|
|
739
|
+
hash = (hash << 5) - hash + view.getUint8(i);
|
|
740
|
+
hash |= 0;
|
|
741
|
+
}
|
|
742
|
+
return hash + "";
|
|
743
|
+
}
|
|
744
|
+
function lns(str) {
|
|
745
|
+
const result = str.split("");
|
|
746
|
+
result.push(...result.splice(0, Math.round(result.length / 5)));
|
|
747
|
+
result.push(...result.splice(0, Math.round(result.length / 4)));
|
|
748
|
+
result.push(...result.splice(0, Math.round(result.length / 3)));
|
|
749
|
+
result.push(...result.splice(0, Math.round(result.length / 2)));
|
|
750
|
+
return result.reverse().map((n) => +n ? +n < 5 ? 5 + +n : +n > 5 ? +n - 5 : n : n).join("");
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/lib/id.ts
|
|
754
|
+
var crypto = globalThis.crypto;
|
|
755
|
+
var urlAlphabet = "useandom-26T198340PX75pxJACKVERYMINDBUSHWOLF_GQZbfghjklqvwyzrict";
|
|
756
|
+
var POOL_SIZE_MULTIPLIER = 128;
|
|
757
|
+
var pool;
|
|
758
|
+
var poolOffset;
|
|
759
|
+
function fillPool(bytes) {
|
|
760
|
+
if (!pool || pool.length < bytes) {
|
|
761
|
+
pool = new Uint8Array(bytes * POOL_SIZE_MULTIPLIER);
|
|
762
|
+
crypto.getRandomValues(pool);
|
|
763
|
+
poolOffset = 0;
|
|
764
|
+
} else if (poolOffset + bytes > pool.length) {
|
|
765
|
+
crypto.getRandomValues(pool);
|
|
766
|
+
poolOffset = 0;
|
|
767
|
+
}
|
|
768
|
+
poolOffset += bytes;
|
|
769
|
+
}
|
|
770
|
+
function nanoid(size = 21) {
|
|
771
|
+
fillPool(size -= 0);
|
|
772
|
+
let id = "";
|
|
773
|
+
for (let i = poolOffset - size; i < poolOffset; i++) {
|
|
774
|
+
id += urlAlphabet[pool[i] & 63];
|
|
775
|
+
}
|
|
776
|
+
return id;
|
|
777
|
+
}
|
|
778
|
+
var impl = nanoid;
|
|
779
|
+
function mockUniqueId(fn) {
|
|
780
|
+
impl = fn;
|
|
781
|
+
}
|
|
782
|
+
function restoreUniqueId() {
|
|
783
|
+
impl = nanoid;
|
|
784
|
+
}
|
|
785
|
+
function uniqueId(size) {
|
|
786
|
+
return impl(size);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// src/lib/iterable.ts
|
|
790
|
+
function getFirstFromIterable(set) {
|
|
791
|
+
return set.values().next().value;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
// src/lib/LruCache.ts
|
|
795
|
+
var LruCache = class {
|
|
796
|
+
constructor(maxSize) {
|
|
797
|
+
this.maxSize = maxSize;
|
|
798
|
+
}
|
|
799
|
+
maxSize;
|
|
800
|
+
map = /* @__PURE__ */ new Map();
|
|
801
|
+
get(key) {
|
|
802
|
+
if (!this.map.has(key)) return void 0;
|
|
803
|
+
const value = this.map.get(key);
|
|
804
|
+
this.map.delete(key);
|
|
805
|
+
this.map.set(key, value);
|
|
806
|
+
return value;
|
|
807
|
+
}
|
|
808
|
+
set(key, value) {
|
|
809
|
+
if (this.map.has(key)) this.map.delete(key);
|
|
810
|
+
this.map.set(key, value);
|
|
811
|
+
if (this.map.size > this.maxSize) {
|
|
812
|
+
this.map.delete(this.map.keys().next().value);
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
has(key) {
|
|
816
|
+
return this.map.has(key);
|
|
817
|
+
}
|
|
818
|
+
// eslint-disable-next-line tldraw/no-setter-getter
|
|
819
|
+
get size() {
|
|
820
|
+
return this.map.size;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
|
|
824
|
+
// src/lib/media/apng.ts
|
|
825
|
+
function isApngAnimated(buffer) {
|
|
826
|
+
const view = new Uint8Array(buffer);
|
|
827
|
+
if (!view || !(typeof Buffer !== "undefined" && Buffer.isBuffer(view) || view instanceof Uint8Array) || view.length < 16) {
|
|
828
|
+
return false;
|
|
829
|
+
}
|
|
830
|
+
const isPNG = view[0] === 137 && view[1] === 80 && view[2] === 78 && view[3] === 71 && view[4] === 13 && view[5] === 10 && view[6] === 26 && view[7] === 10;
|
|
831
|
+
if (!isPNG) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
function indexOfSubstring(haystack, needle, fromIndex, upToIndex, chunksize = 1024) {
|
|
835
|
+
if (!needle) {
|
|
836
|
+
return -1;
|
|
837
|
+
}
|
|
838
|
+
needle = new RegExp(needle, "g");
|
|
839
|
+
const needle_length = needle.source.length;
|
|
840
|
+
const decoder = new TextDecoder();
|
|
841
|
+
const full_haystack_length = haystack.length;
|
|
842
|
+
if (typeof upToIndex === "undefined") {
|
|
843
|
+
upToIndex = full_haystack_length;
|
|
844
|
+
}
|
|
845
|
+
if (fromIndex >= full_haystack_length || upToIndex <= 0 || fromIndex >= upToIndex) {
|
|
846
|
+
return -1;
|
|
847
|
+
}
|
|
848
|
+
haystack = haystack.subarray(fromIndex, upToIndex);
|
|
849
|
+
let position = -1;
|
|
850
|
+
let current_index = 0;
|
|
851
|
+
let full_length = 0;
|
|
852
|
+
let needle_buffer = "";
|
|
853
|
+
outer: while (current_index < haystack.length) {
|
|
854
|
+
const next_index = current_index + chunksize;
|
|
855
|
+
const chunk = haystack.subarray(current_index, next_index);
|
|
856
|
+
const decoded = decoder.decode(chunk, { stream: true });
|
|
857
|
+
const text = needle_buffer + decoded;
|
|
858
|
+
let match;
|
|
859
|
+
let last_index = -1;
|
|
860
|
+
while ((match = needle.exec(text)) !== null) {
|
|
861
|
+
last_index = match.index - needle_buffer.length;
|
|
862
|
+
position = full_length + last_index;
|
|
863
|
+
break outer;
|
|
864
|
+
}
|
|
865
|
+
current_index = next_index;
|
|
866
|
+
full_length += decoded.length;
|
|
867
|
+
const needle_index = last_index > -1 ? last_index + needle_length : decoded.length - needle_length;
|
|
868
|
+
needle_buffer = decoded.slice(needle_index);
|
|
869
|
+
}
|
|
870
|
+
if (position >= 0) {
|
|
871
|
+
position += fromIndex >= 0 ? fromIndex : full_haystack_length + fromIndex;
|
|
872
|
+
}
|
|
873
|
+
return position;
|
|
874
|
+
}
|
|
875
|
+
const idatIdx = indexOfSubstring(view, "IDAT", 12);
|
|
876
|
+
if (idatIdx >= 12) {
|
|
877
|
+
const actlIdx = indexOfSubstring(view, "acTL", 8, idatIdx);
|
|
878
|
+
return actlIdx >= 8;
|
|
879
|
+
}
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
// src/lib/media/avif.ts
|
|
884
|
+
var isAvifAnimated = (buffer) => {
|
|
885
|
+
const view = new Uint8Array(buffer);
|
|
886
|
+
return view[3] === 44;
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
// src/lib/media/gif.ts
|
|
890
|
+
function getDataBlocksLength(buffer, offset) {
|
|
891
|
+
let length = 0;
|
|
892
|
+
while (buffer[offset + length]) {
|
|
893
|
+
length += buffer[offset + length] + 1;
|
|
894
|
+
}
|
|
895
|
+
return length + 1;
|
|
896
|
+
}
|
|
897
|
+
function isGIF(buffer) {
|
|
898
|
+
const enc = new TextDecoder("ascii");
|
|
899
|
+
const header = enc.decode(buffer.slice(0, 3));
|
|
900
|
+
return header === "GIF";
|
|
901
|
+
}
|
|
902
|
+
function isGifAnimated(buffer) {
|
|
903
|
+
const view = new Uint8Array(buffer);
|
|
904
|
+
let hasColorTable, colorTableSize;
|
|
905
|
+
let offset = 0;
|
|
906
|
+
let imagesCount = 0;
|
|
907
|
+
if (!isGIF(buffer)) {
|
|
908
|
+
return false;
|
|
909
|
+
}
|
|
910
|
+
hasColorTable = view[10] & 128;
|
|
911
|
+
colorTableSize = view[10] & 7;
|
|
912
|
+
offset += 6;
|
|
913
|
+
offset += 7;
|
|
914
|
+
offset += hasColorTable ? 3 * Math.pow(2, colorTableSize + 1) : 0;
|
|
915
|
+
while (imagesCount < 2 && offset < view.length) {
|
|
916
|
+
switch (view[offset]) {
|
|
917
|
+
// Image descriptor block. According to specification there could be any
|
|
918
|
+
// number of these blocks (even zero). When there is more than one image
|
|
919
|
+
// descriptor browsers will display animation (they shouldn't when there
|
|
920
|
+
// is no delays defined, but they do it anyway).
|
|
921
|
+
case 44:
|
|
922
|
+
imagesCount += 1;
|
|
923
|
+
hasColorTable = view[offset + 9] & 128;
|
|
924
|
+
colorTableSize = view[offset + 9] & 7;
|
|
925
|
+
offset += 10;
|
|
926
|
+
offset += hasColorTable ? 3 * Math.pow(2, colorTableSize + 1) : 0;
|
|
927
|
+
offset += getDataBlocksLength(view, offset + 1) + 1;
|
|
928
|
+
break;
|
|
929
|
+
// Skip all extension blocks. In theory this "plain text extension" blocks
|
|
930
|
+
// could be frames of animation, but no browser renders them.
|
|
931
|
+
case 33:
|
|
932
|
+
offset += 2;
|
|
933
|
+
offset += getDataBlocksLength(view, offset);
|
|
934
|
+
break;
|
|
935
|
+
// Stop processing on trailer block,
|
|
936
|
+
// all data after this point will is ignored by decoders
|
|
937
|
+
case 59:
|
|
938
|
+
offset = view.length;
|
|
939
|
+
break;
|
|
940
|
+
// Oops! This GIF seems to be invalid
|
|
941
|
+
default:
|
|
942
|
+
offset = view.length;
|
|
943
|
+
break;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
return imagesCount > 1;
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
// src/lib/media/png.ts
|
|
950
|
+
var TABLE = [
|
|
951
|
+
0,
|
|
952
|
+
1996959894,
|
|
953
|
+
3993919788,
|
|
954
|
+
2567524794,
|
|
955
|
+
124634137,
|
|
956
|
+
1886057615,
|
|
957
|
+
3915621685,
|
|
958
|
+
2657392035,
|
|
959
|
+
249268274,
|
|
960
|
+
2044508324,
|
|
961
|
+
3772115230,
|
|
962
|
+
2547177864,
|
|
963
|
+
162941995,
|
|
964
|
+
2125561021,
|
|
965
|
+
3887607047,
|
|
966
|
+
2428444049,
|
|
967
|
+
498536548,
|
|
968
|
+
1789927666,
|
|
969
|
+
4089016648,
|
|
970
|
+
2227061214,
|
|
971
|
+
450548861,
|
|
972
|
+
1843258603,
|
|
973
|
+
4107580753,
|
|
974
|
+
2211677639,
|
|
975
|
+
325883990,
|
|
976
|
+
1684777152,
|
|
977
|
+
4251122042,
|
|
978
|
+
2321926636,
|
|
979
|
+
335633487,
|
|
980
|
+
1661365465,
|
|
981
|
+
4195302755,
|
|
982
|
+
2366115317,
|
|
983
|
+
997073096,
|
|
984
|
+
1281953886,
|
|
985
|
+
3579855332,
|
|
986
|
+
2724688242,
|
|
987
|
+
1006888145,
|
|
988
|
+
1258607687,
|
|
989
|
+
3524101629,
|
|
990
|
+
2768942443,
|
|
991
|
+
901097722,
|
|
992
|
+
1119000684,
|
|
993
|
+
3686517206,
|
|
994
|
+
2898065728,
|
|
995
|
+
853044451,
|
|
996
|
+
1172266101,
|
|
997
|
+
3705015759,
|
|
998
|
+
2882616665,
|
|
999
|
+
651767980,
|
|
1000
|
+
1373503546,
|
|
1001
|
+
3369554304,
|
|
1002
|
+
3218104598,
|
|
1003
|
+
565507253,
|
|
1004
|
+
1454621731,
|
|
1005
|
+
3485111705,
|
|
1006
|
+
3099436303,
|
|
1007
|
+
671266974,
|
|
1008
|
+
1594198024,
|
|
1009
|
+
3322730930,
|
|
1010
|
+
2970347812,
|
|
1011
|
+
795835527,
|
|
1012
|
+
1483230225,
|
|
1013
|
+
3244367275,
|
|
1014
|
+
3060149565,
|
|
1015
|
+
1994146192,
|
|
1016
|
+
31158534,
|
|
1017
|
+
2563907772,
|
|
1018
|
+
4023717930,
|
|
1019
|
+
1907459465,
|
|
1020
|
+
112637215,
|
|
1021
|
+
2680153253,
|
|
1022
|
+
3904427059,
|
|
1023
|
+
2013776290,
|
|
1024
|
+
251722036,
|
|
1025
|
+
2517215374,
|
|
1026
|
+
3775830040,
|
|
1027
|
+
2137656763,
|
|
1028
|
+
141376813,
|
|
1029
|
+
2439277719,
|
|
1030
|
+
3865271297,
|
|
1031
|
+
1802195444,
|
|
1032
|
+
476864866,
|
|
1033
|
+
2238001368,
|
|
1034
|
+
4066508878,
|
|
1035
|
+
1812370925,
|
|
1036
|
+
453092731,
|
|
1037
|
+
2181625025,
|
|
1038
|
+
4111451223,
|
|
1039
|
+
1706088902,
|
|
1040
|
+
314042704,
|
|
1041
|
+
2344532202,
|
|
1042
|
+
4240017532,
|
|
1043
|
+
1658658271,
|
|
1044
|
+
366619977,
|
|
1045
|
+
2362670323,
|
|
1046
|
+
4224994405,
|
|
1047
|
+
1303535960,
|
|
1048
|
+
984961486,
|
|
1049
|
+
2747007092,
|
|
1050
|
+
3569037538,
|
|
1051
|
+
1256170817,
|
|
1052
|
+
1037604311,
|
|
1053
|
+
2765210733,
|
|
1054
|
+
3554079995,
|
|
1055
|
+
1131014506,
|
|
1056
|
+
879679996,
|
|
1057
|
+
2909243462,
|
|
1058
|
+
3663771856,
|
|
1059
|
+
1141124467,
|
|
1060
|
+
855842277,
|
|
1061
|
+
2852801631,
|
|
1062
|
+
3708648649,
|
|
1063
|
+
1342533948,
|
|
1064
|
+
654459306,
|
|
1065
|
+
3188396048,
|
|
1066
|
+
3373015174,
|
|
1067
|
+
1466479909,
|
|
1068
|
+
544179635,
|
|
1069
|
+
3110523913,
|
|
1070
|
+
3462522015,
|
|
1071
|
+
1591671054,
|
|
1072
|
+
702138776,
|
|
1073
|
+
2966460450,
|
|
1074
|
+
3352799412,
|
|
1075
|
+
1504918807,
|
|
1076
|
+
783551873,
|
|
1077
|
+
3082640443,
|
|
1078
|
+
3233442989,
|
|
1079
|
+
3988292384,
|
|
1080
|
+
2596254646,
|
|
1081
|
+
62317068,
|
|
1082
|
+
1957810842,
|
|
1083
|
+
3939845945,
|
|
1084
|
+
2647816111,
|
|
1085
|
+
81470997,
|
|
1086
|
+
1943803523,
|
|
1087
|
+
3814918930,
|
|
1088
|
+
2489596804,
|
|
1089
|
+
225274430,
|
|
1090
|
+
2053790376,
|
|
1091
|
+
3826175755,
|
|
1092
|
+
2466906013,
|
|
1093
|
+
167816743,
|
|
1094
|
+
2097651377,
|
|
1095
|
+
4027552580,
|
|
1096
|
+
2265490386,
|
|
1097
|
+
503444072,
|
|
1098
|
+
1762050814,
|
|
1099
|
+
4150417245,
|
|
1100
|
+
2154129355,
|
|
1101
|
+
426522225,
|
|
1102
|
+
1852507879,
|
|
1103
|
+
4275313526,
|
|
1104
|
+
2312317920,
|
|
1105
|
+
282753626,
|
|
1106
|
+
1742555852,
|
|
1107
|
+
4189708143,
|
|
1108
|
+
2394877945,
|
|
1109
|
+
397917763,
|
|
1110
|
+
1622183637,
|
|
1111
|
+
3604390888,
|
|
1112
|
+
2714866558,
|
|
1113
|
+
953729732,
|
|
1114
|
+
1340076626,
|
|
1115
|
+
3518719985,
|
|
1116
|
+
2797360999,
|
|
1117
|
+
1068828381,
|
|
1118
|
+
1219638859,
|
|
1119
|
+
3624741850,
|
|
1120
|
+
2936675148,
|
|
1121
|
+
906185462,
|
|
1122
|
+
1090812512,
|
|
1123
|
+
3747672003,
|
|
1124
|
+
2825379669,
|
|
1125
|
+
829329135,
|
|
1126
|
+
1181335161,
|
|
1127
|
+
3412177804,
|
|
1128
|
+
3160834842,
|
|
1129
|
+
628085408,
|
|
1130
|
+
1382605366,
|
|
1131
|
+
3423369109,
|
|
1132
|
+
3138078467,
|
|
1133
|
+
570562233,
|
|
1134
|
+
1426400815,
|
|
1135
|
+
3317316542,
|
|
1136
|
+
2998733608,
|
|
1137
|
+
733239954,
|
|
1138
|
+
1555261956,
|
|
1139
|
+
3268935591,
|
|
1140
|
+
3050360625,
|
|
1141
|
+
752459403,
|
|
1142
|
+
1541320221,
|
|
1143
|
+
2607071920,
|
|
1144
|
+
3965973030,
|
|
1145
|
+
1969922972,
|
|
1146
|
+
40735498,
|
|
1147
|
+
2617837225,
|
|
1148
|
+
3943577151,
|
|
1149
|
+
1913087877,
|
|
1150
|
+
83908371,
|
|
1151
|
+
2512341634,
|
|
1152
|
+
3803740692,
|
|
1153
|
+
2075208622,
|
|
1154
|
+
213261112,
|
|
1155
|
+
2463272603,
|
|
1156
|
+
3855990285,
|
|
1157
|
+
2094854071,
|
|
1158
|
+
198958881,
|
|
1159
|
+
2262029012,
|
|
1160
|
+
4057260610,
|
|
1161
|
+
1759359992,
|
|
1162
|
+
534414190,
|
|
1163
|
+
2176718541,
|
|
1164
|
+
4139329115,
|
|
1165
|
+
1873836001,
|
|
1166
|
+
414664567,
|
|
1167
|
+
2282248934,
|
|
1168
|
+
4279200368,
|
|
1169
|
+
1711684554,
|
|
1170
|
+
285281116,
|
|
1171
|
+
2405801727,
|
|
1172
|
+
4167216745,
|
|
1173
|
+
1634467795,
|
|
1174
|
+
376229701,
|
|
1175
|
+
2685067896,
|
|
1176
|
+
3608007406,
|
|
1177
|
+
1308918612,
|
|
1178
|
+
956543938,
|
|
1179
|
+
2808555105,
|
|
1180
|
+
3495958263,
|
|
1181
|
+
1231636301,
|
|
1182
|
+
1047427035,
|
|
1183
|
+
2932959818,
|
|
1184
|
+
3654703836,
|
|
1185
|
+
1088359270,
|
|
1186
|
+
936918e3,
|
|
1187
|
+
2847714899,
|
|
1188
|
+
3736837829,
|
|
1189
|
+
1202900863,
|
|
1190
|
+
817233897,
|
|
1191
|
+
3183342108,
|
|
1192
|
+
3401237130,
|
|
1193
|
+
1404277552,
|
|
1194
|
+
615818150,
|
|
1195
|
+
3134207493,
|
|
1196
|
+
3453421203,
|
|
1197
|
+
1423857449,
|
|
1198
|
+
601450431,
|
|
1199
|
+
3009837614,
|
|
1200
|
+
3294710456,
|
|
1201
|
+
1567103746,
|
|
1202
|
+
711928724,
|
|
1203
|
+
3020668471,
|
|
1204
|
+
3272380065,
|
|
1205
|
+
1510334235,
|
|
1206
|
+
755167117
|
|
1207
|
+
];
|
|
1208
|
+
if (typeof Int32Array !== "undefined") {
|
|
1209
|
+
TABLE = new Int32Array(TABLE);
|
|
1210
|
+
}
|
|
1211
|
+
var crc = (current, previous) => {
|
|
1212
|
+
let crc2 = 0 ^ -1;
|
|
1213
|
+
for (let index = 0; index < current.length; index++) {
|
|
1214
|
+
crc2 = TABLE[(crc2 ^ current[index]) & 255] ^ crc2 >>> 8;
|
|
1215
|
+
}
|
|
1216
|
+
return crc2 ^ -1;
|
|
1217
|
+
};
|
|
1218
|
+
var LEN_SIZE = 4;
|
|
1219
|
+
var CRC_SIZE = 4;
|
|
1220
|
+
var PngHelpers = class _PngHelpers {
|
|
1221
|
+
/**
|
|
1222
|
+
* Checks if binary data at the specified offset contains a valid PNG file signature.
|
|
1223
|
+
* Validates the 8-byte PNG signature: 89 50 4E 47 0D 0A 1A 0A.
|
|
1224
|
+
*
|
|
1225
|
+
* @param view - DataView containing the binary data to check
|
|
1226
|
+
* @param offset - Byte offset where the PNG signature should start
|
|
1227
|
+
* @returns True if the data contains a valid PNG signature, false otherwise
|
|
1228
|
+
*
|
|
1229
|
+
* @example
|
|
1230
|
+
* ```ts
|
|
1231
|
+
* // Validate PNG from file upload
|
|
1232
|
+
* const file = event.target.files[0]
|
|
1233
|
+
* const buffer = await file.arrayBuffer()
|
|
1234
|
+
* const view = new DataView(buffer)
|
|
1235
|
+
*
|
|
1236
|
+
* if (PngHelpers.isPng(view, 0)) {
|
|
1237
|
+
* console.log('Valid PNG file detected')
|
|
1238
|
+
* // Process PNG file...
|
|
1239
|
+
* } else {
|
|
1240
|
+
* console.error('Not a valid PNG file')
|
|
1241
|
+
* }
|
|
1242
|
+
* ```
|
|
1243
|
+
*/
|
|
1244
|
+
static isPng(view, offset) {
|
|
1245
|
+
if (view.getUint8(offset + 0) === 137 && view.getUint8(offset + 1) === 80 && view.getUint8(offset + 2) === 78 && view.getUint8(offset + 3) === 71 && view.getUint8(offset + 4) === 13 && view.getUint8(offset + 5) === 10 && view.getUint8(offset + 6) === 26 && view.getUint8(offset + 7) === 10) {
|
|
1246
|
+
return true;
|
|
1247
|
+
}
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
/**
|
|
1251
|
+
* Reads the 4-character chunk type identifier from a PNG chunk header.
|
|
1252
|
+
*
|
|
1253
|
+
* @param view - DataView containing the PNG data
|
|
1254
|
+
* @param offset - Byte offset of the chunk type field (after length field)
|
|
1255
|
+
* @returns 4-character string representing the chunk type (e.g., 'IHDR', 'IDAT', 'IEND')
|
|
1256
|
+
*
|
|
1257
|
+
* @example
|
|
1258
|
+
* ```ts
|
|
1259
|
+
* // Read chunk type from PNG header (after 8-byte signature)
|
|
1260
|
+
* const chunkType = PngHelpers.getChunkType(dataView, 8)
|
|
1261
|
+
* console.log(chunkType) // 'IHDR' (Image Header)
|
|
1262
|
+
*
|
|
1263
|
+
* // Read chunk type at a specific position during parsing
|
|
1264
|
+
* let offset = 8 // Skip PNG signature
|
|
1265
|
+
* const chunkLength = dataView.getUint32(offset)
|
|
1266
|
+
* const type = PngHelpers.getChunkType(dataView, offset + 4)
|
|
1267
|
+
* ```
|
|
1268
|
+
*/
|
|
1269
|
+
static getChunkType(view, offset) {
|
|
1270
|
+
return [
|
|
1271
|
+
String.fromCharCode(view.getUint8(offset)),
|
|
1272
|
+
String.fromCharCode(view.getUint8(offset + 1)),
|
|
1273
|
+
String.fromCharCode(view.getUint8(offset + 2)),
|
|
1274
|
+
String.fromCharCode(view.getUint8(offset + 3))
|
|
1275
|
+
].join("");
|
|
1276
|
+
}
|
|
1277
|
+
/**
|
|
1278
|
+
* Parses all chunks in a PNG file and returns their metadata.
|
|
1279
|
+
* Skips duplicate IDAT chunks but includes all other chunk types.
|
|
1280
|
+
*
|
|
1281
|
+
* @param view - DataView containing the complete PNG file data
|
|
1282
|
+
* @param offset - Starting byte offset (defaults to 0)
|
|
1283
|
+
* @returns Record mapping chunk types to their metadata (start position, data offset, and size)
|
|
1284
|
+
* @throws Error if the data is not a valid PNG file
|
|
1285
|
+
*
|
|
1286
|
+
* @example
|
|
1287
|
+
* ```ts
|
|
1288
|
+
* // Parse PNG structure for metadata extraction
|
|
1289
|
+
* const view = new DataView(await blob.arrayBuffer())
|
|
1290
|
+
* const chunks = PngHelpers.readChunks(view)
|
|
1291
|
+
*
|
|
1292
|
+
* // Check for specific chunks
|
|
1293
|
+
* const ihdrChunk = chunks['IHDR']
|
|
1294
|
+
* const physChunk = chunks['pHYs']
|
|
1295
|
+
*
|
|
1296
|
+
* if (physChunk) {
|
|
1297
|
+
* console.log(`Found pixel density info at byte ${physChunk.start}`)
|
|
1298
|
+
* } else {
|
|
1299
|
+
* console.log('No pixel density information found')
|
|
1300
|
+
* }
|
|
1301
|
+
* ```
|
|
1302
|
+
*/
|
|
1303
|
+
static readChunks(view, offset = 0) {
|
|
1304
|
+
const chunks = {};
|
|
1305
|
+
if (!_PngHelpers.isPng(view, offset)) {
|
|
1306
|
+
throw new Error("Not a PNG");
|
|
1307
|
+
}
|
|
1308
|
+
offset += 8;
|
|
1309
|
+
while (offset <= view.buffer.byteLength) {
|
|
1310
|
+
const start = offset;
|
|
1311
|
+
const len = view.getInt32(offset);
|
|
1312
|
+
offset += 4;
|
|
1313
|
+
const chunkType = _PngHelpers.getChunkType(view, offset);
|
|
1314
|
+
if (chunkType === "IDAT" && chunks[chunkType]) {
|
|
1315
|
+
offset += len + LEN_SIZE + CRC_SIZE;
|
|
1316
|
+
continue;
|
|
1317
|
+
}
|
|
1318
|
+
if (chunkType === "IEND") {
|
|
1319
|
+
break;
|
|
1320
|
+
}
|
|
1321
|
+
chunks[chunkType] = {
|
|
1322
|
+
start,
|
|
1323
|
+
dataOffset: offset + 4,
|
|
1324
|
+
size: len
|
|
1325
|
+
};
|
|
1326
|
+
offset += len + LEN_SIZE + CRC_SIZE;
|
|
1327
|
+
}
|
|
1328
|
+
return chunks;
|
|
1329
|
+
}
|
|
1330
|
+
/**
|
|
1331
|
+
* Parses the pHYs (physical pixel dimensions) chunk data.
|
|
1332
|
+
* Reads pixels per unit for X and Y axes, and the unit specifier.
|
|
1333
|
+
*
|
|
1334
|
+
* @param view - DataView containing the PNG data
|
|
1335
|
+
* @param offset - Byte offset of the pHYs chunk data
|
|
1336
|
+
* @returns Object with ppux (pixels per unit X), ppuy (pixels per unit Y), and unit specifier
|
|
1337
|
+
*
|
|
1338
|
+
* @example
|
|
1339
|
+
* ```ts
|
|
1340
|
+
* // Extract pixel density information for DPI calculation
|
|
1341
|
+
* const physChunk = PngHelpers.findChunk(dataView, 'pHYs')
|
|
1342
|
+
* if (physChunk) {
|
|
1343
|
+
* const physData = PngHelpers.parsePhys(dataView, physChunk.dataOffset)
|
|
1344
|
+
*
|
|
1345
|
+
* if (physData.unit === 1) { // meters
|
|
1346
|
+
* const dpiX = Math.round(physData.ppux * 0.0254)
|
|
1347
|
+
* const dpiY = Math.round(physData.ppuy * 0.0254)
|
|
1348
|
+
* console.log(`DPI: ${dpiX} x ${dpiY}`)
|
|
1349
|
+
* }
|
|
1350
|
+
* }
|
|
1351
|
+
* ```
|
|
1352
|
+
*/
|
|
1353
|
+
static parsePhys(view, offset) {
|
|
1354
|
+
return {
|
|
1355
|
+
ppux: view.getUint32(offset),
|
|
1356
|
+
ppuy: view.getUint32(offset + 4),
|
|
1357
|
+
unit: view.getUint8(offset + 8)
|
|
1358
|
+
};
|
|
1359
|
+
}
|
|
1360
|
+
/**
|
|
1361
|
+
* Finds a specific chunk type in the PNG file and returns its metadata.
|
|
1362
|
+
*
|
|
1363
|
+
* @param view - DataView containing the PNG file data
|
|
1364
|
+
* @param type - 4-character chunk type to search for (e.g., 'pHYs', 'IDAT')
|
|
1365
|
+
* @returns Chunk metadata object if found, undefined otherwise
|
|
1366
|
+
*
|
|
1367
|
+
* @example
|
|
1368
|
+
* ```ts
|
|
1369
|
+
* // Look for pixel density information in PNG
|
|
1370
|
+
* const physChunk = PngHelpers.findChunk(dataView, 'pHYs')
|
|
1371
|
+
* if (physChunk) {
|
|
1372
|
+
* const physData = PngHelpers.parsePhys(dataView, physChunk.dataOffset)
|
|
1373
|
+
* console.log(`Found pHYs chunk with ${physData.ppux} x ${physData.ppuy} pixels per unit`)
|
|
1374
|
+
* }
|
|
1375
|
+
*
|
|
1376
|
+
* // Check for text metadata
|
|
1377
|
+
* const textChunk = PngHelpers.findChunk(dataView, 'tEXt')
|
|
1378
|
+
* if (textChunk) {
|
|
1379
|
+
* console.log(`Found text metadata at byte ${textChunk.start}`)
|
|
1380
|
+
* }
|
|
1381
|
+
* ```
|
|
1382
|
+
*/
|
|
1383
|
+
static findChunk(view, type) {
|
|
1384
|
+
const chunks = _PngHelpers.readChunks(view);
|
|
1385
|
+
return chunks[type];
|
|
1386
|
+
}
|
|
1387
|
+
/**
|
|
1388
|
+
* Adds or replaces a pHYs chunk in a PNG file to set pixel density for high-DPI displays.
|
|
1389
|
+
* The method determines insertion point by prioritizing IDAT chunk position over existing pHYs,
|
|
1390
|
+
* creates a properly formatted pHYs chunk with CRC validation, and returns a new Blob.
|
|
1391
|
+
*
|
|
1392
|
+
* @param view - DataView containing the original PNG file data
|
|
1393
|
+
* @param dpr - Device pixel ratio multiplier (defaults to 1)
|
|
1394
|
+
* @param options - Optional Blob constructor options for MIME type and other properties
|
|
1395
|
+
* @returns New Blob containing the PNG with updated pixel density information
|
|
1396
|
+
*
|
|
1397
|
+
* @example
|
|
1398
|
+
* ```ts
|
|
1399
|
+
* // Export PNG with proper pixel density for high-DPI displays
|
|
1400
|
+
* const canvas = document.createElement('canvas')
|
|
1401
|
+
* const ctx = canvas.getContext('2d')
|
|
1402
|
+
* // ... draw content to canvas ...
|
|
1403
|
+
*
|
|
1404
|
+
* canvas.toBlob(async (blob) => {
|
|
1405
|
+
* if (blob) {
|
|
1406
|
+
* const view = new DataView(await blob.arrayBuffer())
|
|
1407
|
+
* // Create 2x DPI version for Retina displays
|
|
1408
|
+
* const highDpiBlob = PngHelpers.setPhysChunk(view, 2, { type: 'image/png' })
|
|
1409
|
+
* // Download or use the blob...
|
|
1410
|
+
* }
|
|
1411
|
+
* }, 'image/png')
|
|
1412
|
+
* ```
|
|
1413
|
+
*/
|
|
1414
|
+
static setPhysChunk(view, dpr = 1, options) {
|
|
1415
|
+
let offset = 46;
|
|
1416
|
+
let size = 0;
|
|
1417
|
+
const res1 = _PngHelpers.findChunk(view, "pHYs");
|
|
1418
|
+
if (res1) {
|
|
1419
|
+
offset = res1.start;
|
|
1420
|
+
size = res1.size;
|
|
1421
|
+
}
|
|
1422
|
+
const res2 = _PngHelpers.findChunk(view, "IDAT");
|
|
1423
|
+
if (res2) {
|
|
1424
|
+
offset = res2.start;
|
|
1425
|
+
size = 0;
|
|
1426
|
+
}
|
|
1427
|
+
const pHYsData = new ArrayBuffer(21);
|
|
1428
|
+
const pHYsDataView = new DataView(pHYsData);
|
|
1429
|
+
pHYsDataView.setUint32(0, 9);
|
|
1430
|
+
pHYsDataView.setUint8(4, "p".charCodeAt(0));
|
|
1431
|
+
pHYsDataView.setUint8(5, "H".charCodeAt(0));
|
|
1432
|
+
pHYsDataView.setUint8(6, "Y".charCodeAt(0));
|
|
1433
|
+
pHYsDataView.setUint8(7, "s".charCodeAt(0));
|
|
1434
|
+
const DPI_72 = 2835.5;
|
|
1435
|
+
pHYsDataView.setInt32(8, DPI_72 * dpr);
|
|
1436
|
+
pHYsDataView.setInt32(12, DPI_72 * dpr);
|
|
1437
|
+
pHYsDataView.setInt8(16, 1);
|
|
1438
|
+
const crcBit = new Uint8Array(pHYsData.slice(4, 17));
|
|
1439
|
+
pHYsDataView.setInt32(17, crc(crcBit));
|
|
1440
|
+
const startBuf = view.buffer.slice(0, offset);
|
|
1441
|
+
const endBuf = view.buffer.slice(offset + size);
|
|
1442
|
+
return new Blob([startBuf, pHYsData, endBuf], options);
|
|
1443
|
+
}
|
|
1444
|
+
};
|
|
1445
|
+
|
|
1446
|
+
// src/lib/media/webp.ts
|
|
1447
|
+
function isWebp(view) {
|
|
1448
|
+
if (!view || view.length < 12) {
|
|
1449
|
+
return false;
|
|
1450
|
+
}
|
|
1451
|
+
return view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80;
|
|
1452
|
+
}
|
|
1453
|
+
function isWebpAnimated(buffer) {
|
|
1454
|
+
const view = new Uint8Array(buffer);
|
|
1455
|
+
if (!isWebp(view)) {
|
|
1456
|
+
return false;
|
|
1457
|
+
}
|
|
1458
|
+
if (!view || view.length < 21) {
|
|
1459
|
+
return false;
|
|
1460
|
+
}
|
|
1461
|
+
return (view[20] >> 1 & 1) === 1;
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
// src/lib/media/media.ts
|
|
1465
|
+
var DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES = Object.freeze(["image/svg+xml"]);
|
|
1466
|
+
var DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES = Object.freeze([
|
|
1467
|
+
"image/jpeg",
|
|
1468
|
+
"image/png",
|
|
1469
|
+
"image/webp"
|
|
1470
|
+
]);
|
|
1471
|
+
var DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES = Object.freeze([
|
|
1472
|
+
"image/gif",
|
|
1473
|
+
"image/apng",
|
|
1474
|
+
"image/avif"
|
|
1475
|
+
]);
|
|
1476
|
+
var DEFAULT_SUPPORTED_IMAGE_TYPES = Object.freeze([
|
|
1477
|
+
...DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES,
|
|
1478
|
+
...DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES,
|
|
1479
|
+
...DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES
|
|
1480
|
+
]);
|
|
1481
|
+
var DEFAULT_SUPPORT_VIDEO_TYPES = Object.freeze([
|
|
1482
|
+
"video/mp4",
|
|
1483
|
+
"video/webm",
|
|
1484
|
+
"video/quicktime"
|
|
1485
|
+
]);
|
|
1486
|
+
var DEFAULT_SUPPORTED_MEDIA_TYPES = Object.freeze([
|
|
1487
|
+
...DEFAULT_SUPPORTED_IMAGE_TYPES,
|
|
1488
|
+
...DEFAULT_SUPPORT_VIDEO_TYPES
|
|
1489
|
+
]);
|
|
1490
|
+
var DEFAULT_SUPPORTED_MEDIA_TYPE_LIST = DEFAULT_SUPPORTED_MEDIA_TYPES.join(",");
|
|
1491
|
+
var MediaHelpers = class _MediaHelpers {
|
|
1492
|
+
/**
|
|
1493
|
+
* Load a video element from a URL with cross-origin support.
|
|
1494
|
+
*
|
|
1495
|
+
* @param src - The URL of the video to load
|
|
1496
|
+
* @param doc - Optional document to create the video element in
|
|
1497
|
+
* @returns Promise that resolves to the loaded HTMLVideoElement
|
|
1498
|
+
* @example
|
|
1499
|
+
* ```ts
|
|
1500
|
+
* const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')
|
|
1501
|
+
* console.log(`Video dimensions: ${video.videoWidth}x${video.videoHeight}`)
|
|
1502
|
+
* ```
|
|
1503
|
+
* @public
|
|
1504
|
+
*/
|
|
1505
|
+
static loadVideo(src, doc) {
|
|
1506
|
+
return new Promise((resolve, reject) => {
|
|
1507
|
+
const video = (doc ?? document).createElement("video");
|
|
1508
|
+
video.onloadeddata = () => resolve(video);
|
|
1509
|
+
video.onerror = (e) => {
|
|
1510
|
+
console.error(e);
|
|
1511
|
+
reject(new Error("Could not load video"));
|
|
1512
|
+
};
|
|
1513
|
+
video.crossOrigin = "anonymous";
|
|
1514
|
+
video.src = src;
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
/**
|
|
1518
|
+
* Extract a frame from a video element as a data URL.
|
|
1519
|
+
*
|
|
1520
|
+
* @param video - The HTMLVideoElement to extract frame from
|
|
1521
|
+
* @param time - The time in seconds to extract the frame from (default: 0)
|
|
1522
|
+
* @returns Promise that resolves to a data URL of the video frame
|
|
1523
|
+
* @example
|
|
1524
|
+
* ```ts
|
|
1525
|
+
* const video = await MediaHelpers.loadVideo('https://example.com/video.mp4')
|
|
1526
|
+
* const frameDataUrl = await MediaHelpers.getVideoFrameAsDataUrl(video, 5.0)
|
|
1527
|
+
* // Use frameDataUrl as image thumbnail
|
|
1528
|
+
* const img = document.createElement('img')
|
|
1529
|
+
* img.src = frameDataUrl
|
|
1530
|
+
* ```
|
|
1531
|
+
* @public
|
|
1532
|
+
*/
|
|
1533
|
+
static async getVideoFrameAsDataUrl(video, time = 0) {
|
|
1534
|
+
const promise = promiseWithResolve();
|
|
1535
|
+
let didSetTime = false;
|
|
1536
|
+
const onReadyStateChanged = () => {
|
|
1537
|
+
if (!didSetTime) {
|
|
1538
|
+
if (video.readyState >= video.HAVE_METADATA) {
|
|
1539
|
+
didSetTime = true;
|
|
1540
|
+
video.currentTime = time;
|
|
1541
|
+
} else {
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
if (video.readyState >= video.HAVE_CURRENT_DATA) {
|
|
1546
|
+
const canvas = (video.ownerDocument ?? document).createElement("canvas");
|
|
1547
|
+
canvas.width = video.videoWidth;
|
|
1548
|
+
canvas.height = video.videoHeight;
|
|
1549
|
+
const ctx = canvas.getContext("2d");
|
|
1550
|
+
if (!ctx) {
|
|
1551
|
+
throw new Error("Could not get 2d context");
|
|
1552
|
+
}
|
|
1553
|
+
ctx.drawImage(video, 0, 0);
|
|
1554
|
+
promise.resolve(canvas.toDataURL());
|
|
1555
|
+
}
|
|
1556
|
+
};
|
|
1557
|
+
const onError = (e) => {
|
|
1558
|
+
console.error(e);
|
|
1559
|
+
promise.reject(new Error("Could not get video frame"));
|
|
1560
|
+
};
|
|
1561
|
+
video.addEventListener("loadedmetadata", onReadyStateChanged);
|
|
1562
|
+
video.addEventListener("loadeddata", onReadyStateChanged);
|
|
1563
|
+
video.addEventListener("canplay", onReadyStateChanged);
|
|
1564
|
+
video.addEventListener("seeked", onReadyStateChanged);
|
|
1565
|
+
video.addEventListener("error", onError);
|
|
1566
|
+
video.addEventListener("stalled", onError);
|
|
1567
|
+
onReadyStateChanged();
|
|
1568
|
+
try {
|
|
1569
|
+
return await promise;
|
|
1570
|
+
} finally {
|
|
1571
|
+
video.removeEventListener("loadedmetadata", onReadyStateChanged);
|
|
1572
|
+
video.removeEventListener("loadeddata", onReadyStateChanged);
|
|
1573
|
+
video.removeEventListener("canplay", onReadyStateChanged);
|
|
1574
|
+
video.removeEventListener("seeked", onReadyStateChanged);
|
|
1575
|
+
video.removeEventListener("error", onError);
|
|
1576
|
+
video.removeEventListener("stalled", onError);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Load an image from a URL and get its dimensions along with the image element.
|
|
1581
|
+
*
|
|
1582
|
+
* @param src - The URL of the image to load
|
|
1583
|
+
* @param doc - Optional document to use for DOM operations (e.g. measuring SVG dimensions)
|
|
1584
|
+
* @returns Promise that resolves to an object with width, height, and the image element
|
|
1585
|
+
* @example
|
|
1586
|
+
* ```ts
|
|
1587
|
+
* const { w, h, image } = await MediaHelpers.getImageAndDimensions('https://example.com/image.png')
|
|
1588
|
+
* console.log(`Image size: ${w}x${h}`)
|
|
1589
|
+
* // Image is ready to use
|
|
1590
|
+
* document.body.appendChild(image)
|
|
1591
|
+
* ```
|
|
1592
|
+
* @public
|
|
1593
|
+
*/
|
|
1594
|
+
static getImageAndDimensions(src, doc) {
|
|
1595
|
+
return new Promise((resolve, reject) => {
|
|
1596
|
+
const img = Image();
|
|
1597
|
+
img.onload = () => {
|
|
1598
|
+
let dimensions;
|
|
1599
|
+
if (img.naturalWidth) {
|
|
1600
|
+
dimensions = {
|
|
1601
|
+
w: img.naturalWidth,
|
|
1602
|
+
h: img.naturalHeight
|
|
1603
|
+
};
|
|
1604
|
+
} else {
|
|
1605
|
+
const body = (doc ?? document).body;
|
|
1606
|
+
body.appendChild(img);
|
|
1607
|
+
dimensions = {
|
|
1608
|
+
w: img.clientWidth,
|
|
1609
|
+
h: img.clientHeight
|
|
1610
|
+
};
|
|
1611
|
+
body.removeChild(img);
|
|
1612
|
+
}
|
|
1613
|
+
resolve({ ...dimensions, image: img });
|
|
1614
|
+
};
|
|
1615
|
+
img.onerror = (e) => {
|
|
1616
|
+
console.error(e);
|
|
1617
|
+
reject(new Error("Could not load image"));
|
|
1618
|
+
};
|
|
1619
|
+
img.crossOrigin = "anonymous";
|
|
1620
|
+
img.referrerPolicy = "strict-origin-when-cross-origin";
|
|
1621
|
+
img.style.visibility = "hidden";
|
|
1622
|
+
img.style.position = "absolute";
|
|
1623
|
+
img.style.opacity = "0";
|
|
1624
|
+
img.style.zIndex = "-9999";
|
|
1625
|
+
img.src = src;
|
|
1626
|
+
});
|
|
1627
|
+
}
|
|
1628
|
+
/**
|
|
1629
|
+
* Get the size of a video blob
|
|
1630
|
+
*
|
|
1631
|
+
* @param blob - A Blob containing the video
|
|
1632
|
+
* @param doc - Optional document to create elements in
|
|
1633
|
+
* @returns Promise that resolves to an object with width and height properties
|
|
1634
|
+
* @example
|
|
1635
|
+
* ```ts
|
|
1636
|
+
* const file = new File([...], 'video.mp4', { type: 'video/mp4' })
|
|
1637
|
+
* const { w, h } = await MediaHelpers.getVideoSize(file)
|
|
1638
|
+
* console.log(`Video dimensions: ${w}x${h}`)
|
|
1639
|
+
* ```
|
|
1640
|
+
* @public
|
|
1641
|
+
*/
|
|
1642
|
+
static async getVideoSize(blob, doc) {
|
|
1643
|
+
return _MediaHelpers.usingObjectURL(blob, async (url) => {
|
|
1644
|
+
const video = await _MediaHelpers.loadVideo(url, doc);
|
|
1645
|
+
return { w: video.videoWidth, h: video.videoHeight };
|
|
1646
|
+
});
|
|
1647
|
+
}
|
|
1648
|
+
/**
|
|
1649
|
+
* Get the size of an image blob
|
|
1650
|
+
*
|
|
1651
|
+
* @param blob - A Blob containing the image
|
|
1652
|
+
* @param doc - Optional document to use for DOM operations
|
|
1653
|
+
* @returns Promise that resolves to an object with width and height properties
|
|
1654
|
+
* @example
|
|
1655
|
+
* ```ts
|
|
1656
|
+
* const file = new File([...], 'image.png', { type: 'image/png' })
|
|
1657
|
+
* const { w, h } = await MediaHelpers.getImageSize(file)
|
|
1658
|
+
* console.log(`Image dimensions: ${w}x${h}`)
|
|
1659
|
+
* ```
|
|
1660
|
+
* @public
|
|
1661
|
+
*/
|
|
1662
|
+
static async getImageSize(blob, doc) {
|
|
1663
|
+
const { w, h } = await _MediaHelpers.usingObjectURL(
|
|
1664
|
+
blob,
|
|
1665
|
+
(url) => _MediaHelpers.getImageAndDimensions(url, doc)
|
|
1666
|
+
);
|
|
1667
|
+
try {
|
|
1668
|
+
if (blob.type === "image/png") {
|
|
1669
|
+
const view = new DataView(await blob.arrayBuffer());
|
|
1670
|
+
if (PngHelpers.isPng(view, 0)) {
|
|
1671
|
+
const physChunk = PngHelpers.findChunk(view, "pHYs");
|
|
1672
|
+
if (physChunk) {
|
|
1673
|
+
const physData = PngHelpers.parsePhys(view, physChunk.dataOffset);
|
|
1674
|
+
if (physData.unit === 1 && physData.ppux === physData.ppuy) {
|
|
1675
|
+
const dpi = Math.round(physData.ppux * 0.0254);
|
|
1676
|
+
const r96 = dpi / 96;
|
|
1677
|
+
const r72 = dpi / 72;
|
|
1678
|
+
let pixelRatio = 1;
|
|
1679
|
+
if (Number.isInteger(r96) && r96 > 1) {
|
|
1680
|
+
pixelRatio = r96;
|
|
1681
|
+
} else if (Number.isInteger(r72) && r72 > 1) {
|
|
1682
|
+
pixelRatio = r72;
|
|
1683
|
+
}
|
|
1684
|
+
if (pixelRatio > 1) {
|
|
1685
|
+
return {
|
|
1686
|
+
w: Math.ceil(w / pixelRatio),
|
|
1687
|
+
h: Math.ceil(h / pixelRatio),
|
|
1688
|
+
pixelRatio
|
|
1689
|
+
};
|
|
1690
|
+
}
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
}
|
|
1694
|
+
}
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
console.error(err);
|
|
1697
|
+
return { w, h, pixelRatio: 1 };
|
|
1698
|
+
}
|
|
1699
|
+
return { w, h, pixelRatio: 1 };
|
|
1700
|
+
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Check if a media file blob contains animation data.
|
|
1703
|
+
*
|
|
1704
|
+
* @param file - The Blob to check for animation
|
|
1705
|
+
* @returns Promise that resolves to true if the file is animated, false otherwise
|
|
1706
|
+
* @example
|
|
1707
|
+
* ```ts
|
|
1708
|
+
* const file = new File([...], 'animation.gif', { type: 'image/gif' })
|
|
1709
|
+
* const animated = await MediaHelpers.isAnimated(file)
|
|
1710
|
+
* console.log(animated ? 'Animated' : 'Static')
|
|
1711
|
+
* ```
|
|
1712
|
+
* @public
|
|
1713
|
+
*/
|
|
1714
|
+
static async isAnimated(file) {
|
|
1715
|
+
if (file.type === "image/gif") {
|
|
1716
|
+
return isGifAnimated(await file.arrayBuffer());
|
|
1717
|
+
}
|
|
1718
|
+
if (file.type === "image/avif") {
|
|
1719
|
+
return isAvifAnimated(await file.arrayBuffer());
|
|
1720
|
+
}
|
|
1721
|
+
if (file.type === "image/webp") {
|
|
1722
|
+
return isWebpAnimated(await file.arrayBuffer());
|
|
1723
|
+
}
|
|
1724
|
+
if (file.type === "image/apng") {
|
|
1725
|
+
return isApngAnimated(await file.arrayBuffer());
|
|
1726
|
+
}
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
/**
|
|
1730
|
+
* Check if a MIME type represents an animated image format.
|
|
1731
|
+
*
|
|
1732
|
+
* @param mimeType - The MIME type to check
|
|
1733
|
+
* @returns True if the MIME type is an animated image format, false otherwise
|
|
1734
|
+
* @example
|
|
1735
|
+
* ```ts
|
|
1736
|
+
* const isAnimated = MediaHelpers.isAnimatedImageType('image/gif')
|
|
1737
|
+
* console.log(isAnimated) // true
|
|
1738
|
+
* ```
|
|
1739
|
+
* @public
|
|
1740
|
+
*/
|
|
1741
|
+
static isAnimatedImageType(mimeType) {
|
|
1742
|
+
return DEFAULT_SUPPORTED_ANIMATED_IMAGE_TYPES.includes(mimeType || "");
|
|
1743
|
+
}
|
|
1744
|
+
/**
|
|
1745
|
+
* Check if a MIME type represents a static (non-animated) image format.
|
|
1746
|
+
*
|
|
1747
|
+
* @param mimeType - The MIME type to check
|
|
1748
|
+
* @returns True if the MIME type is a static image format, false otherwise
|
|
1749
|
+
* @example
|
|
1750
|
+
* ```ts
|
|
1751
|
+
* const isStatic = MediaHelpers.isStaticImageType('image/jpeg')
|
|
1752
|
+
* console.log(isStatic) // true
|
|
1753
|
+
* ```
|
|
1754
|
+
* @public
|
|
1755
|
+
*/
|
|
1756
|
+
static isStaticImageType(mimeType) {
|
|
1757
|
+
return DEFAULT_SUPPORTED_STATIC_IMAGE_TYPES.includes(mimeType || "");
|
|
1758
|
+
}
|
|
1759
|
+
/**
|
|
1760
|
+
* Check if a MIME type represents a vector image format.
|
|
1761
|
+
*
|
|
1762
|
+
* @param mimeType - The MIME type to check
|
|
1763
|
+
* @returns True if the MIME type is a vector image format, false otherwise
|
|
1764
|
+
* @example
|
|
1765
|
+
* ```ts
|
|
1766
|
+
* const isVector = MediaHelpers.isVectorImageType('image/svg+xml')
|
|
1767
|
+
* console.log(isVector) // true
|
|
1768
|
+
* ```
|
|
1769
|
+
* @public
|
|
1770
|
+
*/
|
|
1771
|
+
static isVectorImageType(mimeType) {
|
|
1772
|
+
return DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES.includes(mimeType || "");
|
|
1773
|
+
}
|
|
1774
|
+
/**
|
|
1775
|
+
* Check if a MIME type represents any supported image format (static, animated, or vector).
|
|
1776
|
+
*
|
|
1777
|
+
* @param mimeType - The MIME type to check
|
|
1778
|
+
* @returns True if the MIME type is a supported image format, false otherwise
|
|
1779
|
+
* @example
|
|
1780
|
+
* ```ts
|
|
1781
|
+
* const isImage = MediaHelpers.isImageType('image/png')
|
|
1782
|
+
* console.log(isImage) // true
|
|
1783
|
+
* ```
|
|
1784
|
+
* @public
|
|
1785
|
+
*/
|
|
1786
|
+
static isImageType(mimeType) {
|
|
1787
|
+
return DEFAULT_SUPPORTED_IMAGE_TYPES.includes(mimeType || "");
|
|
1788
|
+
}
|
|
1789
|
+
/**
|
|
1790
|
+
* Utility function to create an object URL from a blob, execute a function with it, and automatically clean it up.
|
|
1791
|
+
*
|
|
1792
|
+
* @param blob - The Blob to create an object URL for
|
|
1793
|
+
* @param fn - Function to execute with the object URL
|
|
1794
|
+
* @returns Promise that resolves to the result of the function
|
|
1795
|
+
* @example
|
|
1796
|
+
* ```ts
|
|
1797
|
+
* const result = await MediaHelpers.usingObjectURL(imageBlob, async (url) => {
|
|
1798
|
+
* const { w, h } = await MediaHelpers.getImageAndDimensions(url)
|
|
1799
|
+
* return { width: w, height: h }
|
|
1800
|
+
* })
|
|
1801
|
+
* // Object URL is automatically revoked after function completes
|
|
1802
|
+
* console.log(`Image dimensions: ${result.width}x${result.height}`)
|
|
1803
|
+
* ```
|
|
1804
|
+
* @public
|
|
1805
|
+
*/
|
|
1806
|
+
static async usingObjectURL(blob, fn) {
|
|
1807
|
+
const url = URL.createObjectURL(blob);
|
|
1808
|
+
try {
|
|
1809
|
+
return await fn(url);
|
|
1810
|
+
} finally {
|
|
1811
|
+
URL.revokeObjectURL(url);
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
};
|
|
1815
|
+
|
|
1816
|
+
// src/lib/number.ts
|
|
1817
|
+
function lerp(a, b, t) {
|
|
1818
|
+
return a + (b - a) * t;
|
|
1819
|
+
}
|
|
1820
|
+
function invLerp(a, b, t) {
|
|
1821
|
+
return (t - a) / (b - a);
|
|
1822
|
+
}
|
|
1823
|
+
function rng(seed = "") {
|
|
1824
|
+
let x = 0;
|
|
1825
|
+
let y = 0;
|
|
1826
|
+
let z = 0;
|
|
1827
|
+
let w = 0;
|
|
1828
|
+
function next() {
|
|
1829
|
+
const t = x ^ x << 11;
|
|
1830
|
+
x = y;
|
|
1831
|
+
y = z;
|
|
1832
|
+
z = w;
|
|
1833
|
+
w ^= (w >>> 19 ^ t ^ t >>> 8) >>> 0;
|
|
1834
|
+
return w / 4294967296 * 2;
|
|
1835
|
+
}
|
|
1836
|
+
for (let k = 0; k < seed.length + 64; k++) {
|
|
1837
|
+
x ^= seed.charCodeAt(k) | 0;
|
|
1838
|
+
next();
|
|
1839
|
+
}
|
|
1840
|
+
return next;
|
|
1841
|
+
}
|
|
1842
|
+
function modulate(value, rangeA, rangeB, clamp = false) {
|
|
1843
|
+
const [fromLow, fromHigh] = rangeA;
|
|
1844
|
+
const [v0, v1] = rangeB;
|
|
1845
|
+
const result = v0 + (value - fromLow) / (fromHigh - fromLow) * (v1 - v0);
|
|
1846
|
+
return clamp ? v0 < v1 ? Math.max(Math.min(result, v1), v0) : Math.max(Math.min(result, v0), v1) : result;
|
|
1847
|
+
}
|
|
1848
|
+
function hasOwnProperty(obj, key) {
|
|
1849
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
1850
|
+
}
|
|
1851
|
+
function getOwnProperty(obj, key) {
|
|
1852
|
+
if (!hasOwnProperty(obj, key)) {
|
|
1853
|
+
return void 0;
|
|
1854
|
+
}
|
|
1855
|
+
return obj[key];
|
|
1856
|
+
}
|
|
1857
|
+
function objectMapKeys(object) {
|
|
1858
|
+
return Object.keys(object);
|
|
1859
|
+
}
|
|
1860
|
+
function objectMapValues(object) {
|
|
1861
|
+
return Object.values(object);
|
|
1862
|
+
}
|
|
1863
|
+
function objectMapEntries(object) {
|
|
1864
|
+
return Object.entries(object);
|
|
1865
|
+
}
|
|
1866
|
+
function* objectMapEntriesIterable(object) {
|
|
1867
|
+
for (const key in object) {
|
|
1868
|
+
if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
|
|
1869
|
+
yield [key, object[key]];
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
function objectMapFromEntries(entries) {
|
|
1873
|
+
return Object.fromEntries(entries);
|
|
1874
|
+
}
|
|
1875
|
+
function filterEntries(object, predicate) {
|
|
1876
|
+
const result = {};
|
|
1877
|
+
let didChange = false;
|
|
1878
|
+
for (const key in object) {
|
|
1879
|
+
if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
|
|
1880
|
+
const value = object[key];
|
|
1881
|
+
if (predicate(key, value)) {
|
|
1882
|
+
result[key] = value;
|
|
1883
|
+
} else {
|
|
1884
|
+
didChange = true;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
return didChange ? result : object;
|
|
1888
|
+
}
|
|
1889
|
+
function mapObjectMapValues(object, mapper) {
|
|
1890
|
+
const result = {};
|
|
1891
|
+
for (const key in object) {
|
|
1892
|
+
if (!Object.prototype.hasOwnProperty.call(object, key)) continue;
|
|
1893
|
+
result[key] = mapper(key, object[key]);
|
|
1894
|
+
}
|
|
1895
|
+
return result;
|
|
1896
|
+
}
|
|
1897
|
+
function areObjectsShallowEqual(obj1, obj2) {
|
|
1898
|
+
if (obj1 === obj2) return true;
|
|
1899
|
+
const keys1 = Object.keys(obj1);
|
|
1900
|
+
if (keys1.length !== Object.keys(obj2).length) return false;
|
|
1901
|
+
for (const key of keys1) {
|
|
1902
|
+
if (!hasOwnProperty(obj2, key)) return false;
|
|
1903
|
+
if (!Object.is(obj1[key], obj2[key])) return false;
|
|
1904
|
+
}
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
function groupBy(array, keySelector) {
|
|
1908
|
+
const result = {};
|
|
1909
|
+
for (const value of array) {
|
|
1910
|
+
const key = keySelector(value);
|
|
1911
|
+
if (!result[key]) result[key] = [];
|
|
1912
|
+
result[key].push(value);
|
|
1913
|
+
}
|
|
1914
|
+
return result;
|
|
1915
|
+
}
|
|
1916
|
+
function omit(obj, keys) {
|
|
1917
|
+
const result = { ...obj };
|
|
1918
|
+
for (const key of keys) {
|
|
1919
|
+
delete result[key];
|
|
1920
|
+
}
|
|
1921
|
+
return result;
|
|
1922
|
+
}
|
|
1923
|
+
function getChangedKeys(obj1, obj2) {
|
|
1924
|
+
const result = [];
|
|
1925
|
+
for (const key in obj1) {
|
|
1926
|
+
if (!Object.is(obj1[key], obj2[key])) {
|
|
1927
|
+
result.push(key);
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
return result;
|
|
1931
|
+
}
|
|
1932
|
+
function isEqualAllowingForFloatingPointErrors(obj1, obj2, threshold = 1e-6) {
|
|
1933
|
+
return isEqualWith(obj1, obj2, (value1, value2) => {
|
|
1934
|
+
if (typeof value1 === "number" && typeof value2 === "number") {
|
|
1935
|
+
return Math.abs(value1 - value2) < threshold;
|
|
1936
|
+
}
|
|
1937
|
+
return void 0;
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
// src/lib/perf.ts
|
|
1942
|
+
var PERFORMANCE_COLORS = {
|
|
1943
|
+
Good: "#40C057",
|
|
1944
|
+
Mid: "#FFC078",
|
|
1945
|
+
Poor: "#E03131"
|
|
1946
|
+
};
|
|
1947
|
+
var PERFORMANCE_PREFIX_COLOR = PERFORMANCE_COLORS.Good;
|
|
1948
|
+
function measureCbDuration(name, cb) {
|
|
1949
|
+
const start = performance.now();
|
|
1950
|
+
const result = cb();
|
|
1951
|
+
console.debug(
|
|
1952
|
+
`%cPerf%c ${name} took ${performance.now() - start}ms`,
|
|
1953
|
+
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
|
|
1954
|
+
"font-weight: normal"
|
|
1955
|
+
);
|
|
1956
|
+
return result;
|
|
1957
|
+
}
|
|
1958
|
+
function measureDuration(_target, propertyKey, descriptor) {
|
|
1959
|
+
const originalMethod = descriptor.value;
|
|
1960
|
+
descriptor.value = function(...args) {
|
|
1961
|
+
const start = performance.now();
|
|
1962
|
+
const result = originalMethod.apply(this, args);
|
|
1963
|
+
console.debug(
|
|
1964
|
+
`%cPerf%c ${propertyKey} took: ${performance.now() - start}ms`,
|
|
1965
|
+
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
|
|
1966
|
+
"font-weight: normal"
|
|
1967
|
+
);
|
|
1968
|
+
return result;
|
|
1969
|
+
};
|
|
1970
|
+
return descriptor;
|
|
1971
|
+
}
|
|
1972
|
+
var averages = /* @__PURE__ */ new Map();
|
|
1973
|
+
function measureAverageDuration(_target, propertyKey, descriptor) {
|
|
1974
|
+
const originalMethod = descriptor.value;
|
|
1975
|
+
descriptor.value = function(...args) {
|
|
1976
|
+
const start = performance.now();
|
|
1977
|
+
const result = originalMethod.apply(this, args);
|
|
1978
|
+
const end = performance.now();
|
|
1979
|
+
const length = end - start;
|
|
1980
|
+
if (length !== 0) {
|
|
1981
|
+
const value = averages.get(descriptor.value);
|
|
1982
|
+
const total = value.total + length;
|
|
1983
|
+
const count = value.count + 1;
|
|
1984
|
+
averages.set(descriptor.value, { total, count });
|
|
1985
|
+
console.debug(
|
|
1986
|
+
`%cPerf%c ${propertyKey} took ${(end - start).toFixed(2)}ms | average ${(total / count).toFixed(2)}ms`,
|
|
1987
|
+
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
|
|
1988
|
+
"font-weight: normal"
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
return result;
|
|
1992
|
+
};
|
|
1993
|
+
averages.set(descriptor.value, { total: 0, count: 0 });
|
|
1994
|
+
return descriptor;
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
// src/lib/PerformanceTracker.ts
|
|
1998
|
+
var PerformanceTracker = class {
|
|
1999
|
+
startTime = 0;
|
|
2000
|
+
name = "";
|
|
2001
|
+
frames = 0;
|
|
2002
|
+
started = false;
|
|
2003
|
+
frame = null;
|
|
2004
|
+
/**
|
|
2005
|
+
* Records animation frames to calculate frame rate.
|
|
2006
|
+
* Called automatically during performance tracking.
|
|
2007
|
+
*/
|
|
2008
|
+
// eslint-disable-next-line tldraw/prefer-class-methods
|
|
2009
|
+
recordFrame = () => {
|
|
2010
|
+
this.frames++;
|
|
2011
|
+
if (!this.started) return;
|
|
2012
|
+
this.frame = requestAnimationFrame(this.recordFrame);
|
|
2013
|
+
};
|
|
2014
|
+
/**
|
|
2015
|
+
* Starts performance tracking for a named operation.
|
|
2016
|
+
*
|
|
2017
|
+
* @param name - A descriptive name for the operation being tracked
|
|
2018
|
+
*
|
|
2019
|
+
* @example
|
|
2020
|
+
* ```ts
|
|
2021
|
+
* tracker.start('canvas-render')
|
|
2022
|
+
* // ... perform rendering operations
|
|
2023
|
+
* tracker.stop()
|
|
2024
|
+
* ```
|
|
2025
|
+
*/
|
|
2026
|
+
start(name) {
|
|
2027
|
+
this.name = name;
|
|
2028
|
+
this.frames = 0;
|
|
2029
|
+
this.started = true;
|
|
2030
|
+
if (this.frame !== null) cancelAnimationFrame(this.frame);
|
|
2031
|
+
this.frame = requestAnimationFrame(this.recordFrame);
|
|
2032
|
+
this.startTime = performance.now();
|
|
2033
|
+
}
|
|
2034
|
+
/**
|
|
2035
|
+
* Stops performance tracking and logs results to the console.
|
|
2036
|
+
*
|
|
2037
|
+
* Displays the operation name, frame rate, and uses color coding:
|
|
2038
|
+
* - Green background: \> 55 FPS (good performance)
|
|
2039
|
+
* - Yellow background: 30-55 FPS (moderate performance)
|
|
2040
|
+
* - Red background: \< 30 FPS (poor performance)
|
|
2041
|
+
*
|
|
2042
|
+
* @example
|
|
2043
|
+
* ```ts
|
|
2044
|
+
* tracker.start('interaction')
|
|
2045
|
+
* handleUserInteraction()
|
|
2046
|
+
* tracker.stop() // Logs: "Perf Interaction 60 fps"
|
|
2047
|
+
* ```
|
|
2048
|
+
*/
|
|
2049
|
+
stop() {
|
|
2050
|
+
this.started = false;
|
|
2051
|
+
if (this.frame !== null) cancelAnimationFrame(this.frame);
|
|
2052
|
+
const duration = (performance.now() - this.startTime) / 1e3;
|
|
2053
|
+
const fps = duration === 0 ? 0 : Math.floor(this.frames / duration);
|
|
2054
|
+
const background = fps > 55 ? PERFORMANCE_COLORS.Good : fps > 30 ? PERFORMANCE_COLORS.Mid : PERFORMANCE_COLORS.Poor;
|
|
2055
|
+
const color = background === PERFORMANCE_COLORS.Mid ? "black" : "white";
|
|
2056
|
+
const capitalized = this.name[0].toUpperCase() + this.name.slice(1);
|
|
2057
|
+
console.debug(
|
|
2058
|
+
`%cPerf%c ${capitalized} %c${fps}%c fps`,
|
|
2059
|
+
`color: white; background: ${PERFORMANCE_PREFIX_COLOR};padding: 2px;border-radius: 3px;`,
|
|
2060
|
+
"font-weight: normal",
|
|
2061
|
+
`font-weight: bold; padding: 2px; background: ${background};color: ${color};`,
|
|
2062
|
+
"font-weight: normal"
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
/**
|
|
2066
|
+
* Checks whether performance tracking is currently active.
|
|
2067
|
+
*
|
|
2068
|
+
* @returns True if tracking is in progress, false otherwise
|
|
2069
|
+
*
|
|
2070
|
+
* @example
|
|
2071
|
+
* ```ts
|
|
2072
|
+
* if (!tracker.isStarted()) {
|
|
2073
|
+
* tracker.start('new-operation')
|
|
2074
|
+
* }
|
|
2075
|
+
* ```
|
|
2076
|
+
*/
|
|
2077
|
+
isStarted() {
|
|
2078
|
+
return this.started;
|
|
2079
|
+
}
|
|
2080
|
+
};
|
|
2081
|
+
var generateKeysFn = generateNKeysBetween;
|
|
2082
|
+
var ZERO_INDEX_KEY = "a0";
|
|
2083
|
+
function validateIndexKey(index) {
|
|
2084
|
+
try {
|
|
2085
|
+
generateKeyBetween(index, null);
|
|
2086
|
+
} catch {
|
|
2087
|
+
throw new Error("invalid index: " + index);
|
|
2088
|
+
}
|
|
2089
|
+
}
|
|
2090
|
+
function getIndicesBetween(below, above, n) {
|
|
2091
|
+
return generateKeysFn(below ?? null, above ?? null, n);
|
|
2092
|
+
}
|
|
2093
|
+
function getIndicesAbove(below, n) {
|
|
2094
|
+
return generateKeysFn(below ?? null, null, n);
|
|
2095
|
+
}
|
|
2096
|
+
function getIndicesBelow(above, n) {
|
|
2097
|
+
return generateKeysFn(null, above ?? null, n);
|
|
2098
|
+
}
|
|
2099
|
+
function getIndexBetween(below, above) {
|
|
2100
|
+
return generateKeysFn(below ?? null, above ?? null, 1)[0];
|
|
2101
|
+
}
|
|
2102
|
+
function getIndexAbove(below = null) {
|
|
2103
|
+
return generateKeysFn(below, null, 1)[0];
|
|
2104
|
+
}
|
|
2105
|
+
function getIndexBelow(above = null) {
|
|
2106
|
+
return generateKeysFn(null, above, 1)[0];
|
|
2107
|
+
}
|
|
2108
|
+
function getIndices(n, start = "a1") {
|
|
2109
|
+
return [start, ...generateKeysFn(start, null, n)];
|
|
2110
|
+
}
|
|
2111
|
+
function sortByIndex(a, b) {
|
|
2112
|
+
if (a.index < b.index) {
|
|
2113
|
+
return -1;
|
|
2114
|
+
} else if (a.index > b.index) {
|
|
2115
|
+
return 1;
|
|
2116
|
+
}
|
|
2117
|
+
return 0;
|
|
2118
|
+
}
|
|
2119
|
+
function sortByMaybeIndex(a, b) {
|
|
2120
|
+
if (a.index && b.index) {
|
|
2121
|
+
return a.index < b.index ? -1 : 1;
|
|
2122
|
+
}
|
|
2123
|
+
if (a.index && b.index == null) {
|
|
2124
|
+
return -1;
|
|
2125
|
+
}
|
|
2126
|
+
if (a.index == null && b.index == null) {
|
|
2127
|
+
return 0;
|
|
2128
|
+
}
|
|
2129
|
+
return 1;
|
|
2130
|
+
}
|
|
2131
|
+
|
|
2132
|
+
// src/lib/retry.ts
|
|
2133
|
+
async function retry(fn, {
|
|
2134
|
+
attempts = 3,
|
|
2135
|
+
waitDuration = 1e3,
|
|
2136
|
+
abortSignal,
|
|
2137
|
+
matchError
|
|
2138
|
+
} = {}) {
|
|
2139
|
+
let error = null;
|
|
2140
|
+
for (let i = 0; i < attempts; i++) {
|
|
2141
|
+
if (abortSignal?.aborted) throw new Error("aborted");
|
|
2142
|
+
try {
|
|
2143
|
+
return await fn({ attempt: i, remaining: attempts - i, total: attempts });
|
|
2144
|
+
} catch (e) {
|
|
2145
|
+
if (matchError && !matchError(e)) throw e;
|
|
2146
|
+
error = e;
|
|
2147
|
+
await sleep(waitDuration);
|
|
2148
|
+
}
|
|
2149
|
+
}
|
|
2150
|
+
throw error;
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
// src/lib/sort.ts
|
|
2154
|
+
function sortById(a, b) {
|
|
2155
|
+
return a.id > b.id ? 1 : -1;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// src/lib/storage.tsx
|
|
2159
|
+
function getFromLocalStorage(key) {
|
|
2160
|
+
try {
|
|
2161
|
+
return localStorage.getItem(key);
|
|
2162
|
+
} catch {
|
|
2163
|
+
return null;
|
|
2164
|
+
}
|
|
2165
|
+
}
|
|
2166
|
+
function setInLocalStorage(key, value) {
|
|
2167
|
+
try {
|
|
2168
|
+
localStorage.setItem(key, value);
|
|
2169
|
+
} catch {
|
|
2170
|
+
}
|
|
2171
|
+
}
|
|
2172
|
+
function deleteFromLocalStorage(key) {
|
|
2173
|
+
try {
|
|
2174
|
+
localStorage.removeItem(key);
|
|
2175
|
+
} catch {
|
|
2176
|
+
}
|
|
2177
|
+
}
|
|
2178
|
+
function clearLocalStorage() {
|
|
2179
|
+
try {
|
|
2180
|
+
localStorage.clear();
|
|
2181
|
+
} catch {
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
function getFromSessionStorage(key) {
|
|
2185
|
+
try {
|
|
2186
|
+
return sessionStorage.getItem(key);
|
|
2187
|
+
} catch {
|
|
2188
|
+
return null;
|
|
2189
|
+
}
|
|
2190
|
+
}
|
|
2191
|
+
function setInSessionStorage(key, value) {
|
|
2192
|
+
try {
|
|
2193
|
+
sessionStorage.setItem(key, value);
|
|
2194
|
+
} catch {
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
function deleteFromSessionStorage(key) {
|
|
2198
|
+
try {
|
|
2199
|
+
sessionStorage.removeItem(key);
|
|
2200
|
+
} catch {
|
|
2201
|
+
}
|
|
2202
|
+
}
|
|
2203
|
+
function clearSessionStorage() {
|
|
2204
|
+
try {
|
|
2205
|
+
sessionStorage.clear();
|
|
2206
|
+
} catch {
|
|
2207
|
+
}
|
|
2208
|
+
}
|
|
2209
|
+
|
|
2210
|
+
// src/lib/stringEnum.ts
|
|
2211
|
+
function stringEnum(...values) {
|
|
2212
|
+
const obj = {};
|
|
2213
|
+
for (const value of values) {
|
|
2214
|
+
obj[value] = value;
|
|
2215
|
+
}
|
|
2216
|
+
return obj;
|
|
2217
|
+
}
|
|
2218
|
+
var timingVarianceFactor = 0.9;
|
|
2219
|
+
var getTargetTimePerFrame = (targetFps) => Math.floor(1e3 / targetFps) * timingVarianceFactor;
|
|
2220
|
+
var FpsScheduler = class {
|
|
2221
|
+
targetFps;
|
|
2222
|
+
targetTimePerFrame;
|
|
2223
|
+
fpsQueue = [];
|
|
2224
|
+
frameRaf;
|
|
2225
|
+
flushRaf;
|
|
2226
|
+
lastFlushTime;
|
|
2227
|
+
constructor(targetFps = 120) {
|
|
2228
|
+
this.targetFps = targetFps;
|
|
2229
|
+
this.targetTimePerFrame = getTargetTimePerFrame(targetFps);
|
|
2230
|
+
this.lastFlushTime = -this.targetTimePerFrame;
|
|
2231
|
+
}
|
|
2232
|
+
updateTargetFps(targetFps) {
|
|
2233
|
+
if (targetFps === this.targetFps) return;
|
|
2234
|
+
this.targetFps = targetFps;
|
|
2235
|
+
this.targetTimePerFrame = getTargetTimePerFrame(targetFps);
|
|
2236
|
+
this.lastFlushTime = -this.targetTimePerFrame;
|
|
2237
|
+
}
|
|
2238
|
+
flush() {
|
|
2239
|
+
const queue = this.fpsQueue.splice(0, this.fpsQueue.length);
|
|
2240
|
+
for (const fn of queue) {
|
|
2241
|
+
fn();
|
|
2242
|
+
}
|
|
2243
|
+
}
|
|
2244
|
+
tick(isOnNextFrame = false) {
|
|
2245
|
+
if (this.frameRaf) return;
|
|
2246
|
+
const now = Date.now();
|
|
2247
|
+
const elapsed = now - this.lastFlushTime;
|
|
2248
|
+
if (elapsed < this.targetTimePerFrame) {
|
|
2249
|
+
this.frameRaf = requestAnimationFrame(() => {
|
|
2250
|
+
this.frameRaf = void 0;
|
|
2251
|
+
this.tick(true);
|
|
2252
|
+
});
|
|
2253
|
+
return;
|
|
2254
|
+
}
|
|
2255
|
+
if (isOnNextFrame) {
|
|
2256
|
+
if (this.flushRaf) return;
|
|
2257
|
+
this.lastFlushTime = now;
|
|
2258
|
+
this.flush();
|
|
2259
|
+
} else {
|
|
2260
|
+
if (this.flushRaf) return;
|
|
2261
|
+
this.flushRaf = requestAnimationFrame(() => {
|
|
2262
|
+
this.flushRaf = void 0;
|
|
2263
|
+
this.lastFlushTime = Date.now();
|
|
2264
|
+
this.flush();
|
|
2265
|
+
});
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
/**
|
|
2269
|
+
* Creates a throttled version of a function that executes at most once per frame.
|
|
2270
|
+
* The default target frame rate is set by the FpsScheduler instance.
|
|
2271
|
+
* Subsequent calls within the same frame are ignored, ensuring smooth performance
|
|
2272
|
+
* for high-frequency events like mouse movements or scroll events.
|
|
2273
|
+
*
|
|
2274
|
+
* @param fn - The function to throttle, optionally with a cancel method
|
|
2275
|
+
* @returns A throttled function with an optional cancel method to remove pending calls
|
|
2276
|
+
*
|
|
2277
|
+
* @public
|
|
2278
|
+
*/
|
|
2279
|
+
fpsThrottle(fn) {
|
|
2280
|
+
const throttledFn = () => {
|
|
2281
|
+
if (this.fpsQueue.includes(fn)) {
|
|
2282
|
+
return;
|
|
2283
|
+
}
|
|
2284
|
+
this.fpsQueue.push(fn);
|
|
2285
|
+
this.tick();
|
|
2286
|
+
};
|
|
2287
|
+
throttledFn.cancel = () => {
|
|
2288
|
+
const index = this.fpsQueue.indexOf(fn);
|
|
2289
|
+
if (index > -1) {
|
|
2290
|
+
this.fpsQueue.splice(index, 1);
|
|
2291
|
+
}
|
|
2292
|
+
};
|
|
2293
|
+
return throttledFn;
|
|
2294
|
+
}
|
|
2295
|
+
/**
|
|
2296
|
+
* Schedules a function to execute on the next animation frame.
|
|
2297
|
+
* If the same function is passed multiple times before the frame executes,
|
|
2298
|
+
* it will only be called once, effectively batching multiple calls.
|
|
2299
|
+
*
|
|
2300
|
+
* @param fn - The function to execute on the next frame
|
|
2301
|
+
* @returns A cancel function that can prevent execution if called before the next frame
|
|
2302
|
+
*
|
|
2303
|
+
* @public
|
|
2304
|
+
*/
|
|
2305
|
+
throttleToNextFrame(fn) {
|
|
2306
|
+
if (!this.fpsQueue.includes(fn)) {
|
|
2307
|
+
this.fpsQueue.push(fn);
|
|
2308
|
+
this.tick();
|
|
2309
|
+
}
|
|
2310
|
+
return () => {
|
|
2311
|
+
const index = this.fpsQueue.indexOf(fn);
|
|
2312
|
+
if (index > -1) {
|
|
2313
|
+
this.fpsQueue.splice(index, 1);
|
|
2314
|
+
}
|
|
2315
|
+
};
|
|
2316
|
+
}
|
|
2317
|
+
};
|
|
2318
|
+
var defaultScheduler = new FpsScheduler(120);
|
|
2319
|
+
function fpsThrottle(fn) {
|
|
2320
|
+
return defaultScheduler.fpsThrottle(fn);
|
|
2321
|
+
}
|
|
2322
|
+
function throttleToNextFrame(fn) {
|
|
2323
|
+
return defaultScheduler.throttleToNextFrame(fn);
|
|
2324
|
+
}
|
|
2325
|
+
|
|
2326
|
+
// src/lib/timers.ts
|
|
2327
|
+
var Timers = class {
|
|
2328
|
+
timeouts = /* @__PURE__ */ new Map();
|
|
2329
|
+
intervals = /* @__PURE__ */ new Map();
|
|
2330
|
+
rafs = /* @__PURE__ */ new Map();
|
|
2331
|
+
/**
|
|
2332
|
+
* Creates a new Timers instance with bound methods for safe callback usage.
|
|
2333
|
+
* @example
|
|
2334
|
+
* ```ts
|
|
2335
|
+
* const timers = new Timers()
|
|
2336
|
+
* // Methods are pre-bound, safe to use as callbacks
|
|
2337
|
+
* element.addEventListener('click', timers.dispose)
|
|
2338
|
+
* ```
|
|
2339
|
+
*/
|
|
2340
|
+
constructor() {
|
|
2341
|
+
this.setTimeout = this.setTimeout.bind(this);
|
|
2342
|
+
this.setInterval = this.setInterval.bind(this);
|
|
2343
|
+
this.requestAnimationFrame = this.requestAnimationFrame.bind(this);
|
|
2344
|
+
this.dispose = this.dispose.bind(this);
|
|
2345
|
+
}
|
|
2346
|
+
/**
|
|
2347
|
+
* Creates a timeout that will be tracked under the specified context.
|
|
2348
|
+
* @param contextId - The context identifier to group this timer under.
|
|
2349
|
+
* @param handler - The function to execute when the timeout expires.
|
|
2350
|
+
* @param timeout - The delay in milliseconds (default: 0).
|
|
2351
|
+
* @param args - Additional arguments to pass to the handler.
|
|
2352
|
+
* @returns The timer ID that can be used with clearTimeout.
|
|
2353
|
+
* @example
|
|
2354
|
+
* ```ts
|
|
2355
|
+
* const timers = new Timers()
|
|
2356
|
+
* const id = timers.setTimeout('autosave', () => save(), 5000)
|
|
2357
|
+
* // Timer will be automatically cleared when 'autosave' context is disposed
|
|
2358
|
+
* ```
|
|
2359
|
+
* @public
|
|
2360
|
+
*/
|
|
2361
|
+
setTimeout(contextId, handler, timeout, ...args) {
|
|
2362
|
+
const id = window.setTimeout(handler, timeout, args);
|
|
2363
|
+
const current = this.timeouts.get(contextId) ?? [];
|
|
2364
|
+
this.timeouts.set(contextId, [...current, id]);
|
|
2365
|
+
return id;
|
|
2366
|
+
}
|
|
2367
|
+
/**
|
|
2368
|
+
* Creates an interval that will be tracked under the specified context.
|
|
2369
|
+
* @param contextId - The context identifier to group this timer under.
|
|
2370
|
+
* @param handler - The function to execute repeatedly.
|
|
2371
|
+
* @param timeout - The delay in milliseconds between executions (default: 0).
|
|
2372
|
+
* @param args - Additional arguments to pass to the handler.
|
|
2373
|
+
* @returns The interval ID that can be used with clearInterval.
|
|
2374
|
+
* @example
|
|
2375
|
+
* ```ts
|
|
2376
|
+
* const timers = new Timers()
|
|
2377
|
+
* const id = timers.setInterval('refresh', () => updateData(), 1000)
|
|
2378
|
+
* // Interval will be automatically cleared when 'refresh' context is disposed
|
|
2379
|
+
* ```
|
|
2380
|
+
* @public
|
|
2381
|
+
*/
|
|
2382
|
+
setInterval(contextId, handler, timeout, ...args) {
|
|
2383
|
+
const id = window.setInterval(handler, timeout, args);
|
|
2384
|
+
const current = this.intervals.get(contextId) ?? [];
|
|
2385
|
+
this.intervals.set(contextId, [...current, id]);
|
|
2386
|
+
return id;
|
|
2387
|
+
}
|
|
2388
|
+
/**
|
|
2389
|
+
* Requests an animation frame that will be tracked under the specified context.
|
|
2390
|
+
* @param contextId - The context identifier to group this animation frame under.
|
|
2391
|
+
* @param callback - The function to execute on the next animation frame.
|
|
2392
|
+
* @returns The request ID that can be used with cancelAnimationFrame.
|
|
2393
|
+
* @example
|
|
2394
|
+
* ```ts
|
|
2395
|
+
* const timers = new Timers()
|
|
2396
|
+
* const id = timers.requestAnimationFrame('render', () => draw())
|
|
2397
|
+
* // Animation frame will be automatically cancelled when 'render' context is disposed
|
|
2398
|
+
* ```
|
|
2399
|
+
* @public
|
|
2400
|
+
*/
|
|
2401
|
+
requestAnimationFrame(contextId, callback) {
|
|
2402
|
+
const id = window.requestAnimationFrame(callback);
|
|
2403
|
+
const current = this.rafs.get(contextId) ?? [];
|
|
2404
|
+
this.rafs.set(contextId, [...current, id]);
|
|
2405
|
+
return id;
|
|
2406
|
+
}
|
|
2407
|
+
/**
|
|
2408
|
+
* Disposes of all timers associated with the specified context.
|
|
2409
|
+
* Clears all timeouts, intervals, and animation frames for the given context ID.
|
|
2410
|
+
* @param contextId - The context identifier whose timers should be cleared.
|
|
2411
|
+
* @returns void
|
|
2412
|
+
* @example
|
|
2413
|
+
* ```ts
|
|
2414
|
+
* const timers = new Timers()
|
|
2415
|
+
* timers.setTimeout('ui', () => console.log('timeout'), 1000)
|
|
2416
|
+
* timers.setInterval('ui', () => console.log('interval'), 500)
|
|
2417
|
+
*
|
|
2418
|
+
* // Clear all 'ui' context timers
|
|
2419
|
+
* timers.dispose('ui')
|
|
2420
|
+
* ```
|
|
2421
|
+
* @public
|
|
2422
|
+
*/
|
|
2423
|
+
dispose(contextId) {
|
|
2424
|
+
this.timeouts.get(contextId)?.forEach((id) => clearTimeout(id));
|
|
2425
|
+
this.intervals.get(contextId)?.forEach((id) => clearInterval(id));
|
|
2426
|
+
this.rafs.get(contextId)?.forEach((id) => cancelAnimationFrame(id));
|
|
2427
|
+
this.timeouts.delete(contextId);
|
|
2428
|
+
this.intervals.delete(contextId);
|
|
2429
|
+
this.rafs.delete(contextId);
|
|
2430
|
+
}
|
|
2431
|
+
/**
|
|
2432
|
+
* Disposes of all timers across all contexts.
|
|
2433
|
+
* Clears every timeout, interval, and animation frame managed by this instance.
|
|
2434
|
+
* @returns void
|
|
2435
|
+
* @example
|
|
2436
|
+
* ```ts
|
|
2437
|
+
* const timers = new Timers()
|
|
2438
|
+
* timers.setTimeout('ui', () => console.log('ui'), 1000)
|
|
2439
|
+
* timers.setTimeout('background', () => console.log('bg'), 2000)
|
|
2440
|
+
*
|
|
2441
|
+
* // Clear everything
|
|
2442
|
+
* timers.disposeAll()
|
|
2443
|
+
* ```
|
|
2444
|
+
* @public
|
|
2445
|
+
*/
|
|
2446
|
+
disposeAll() {
|
|
2447
|
+
for (const contextId of this.timeouts.keys()) {
|
|
2448
|
+
this.dispose(contextId);
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
/**
|
|
2452
|
+
* Returns an object with timer methods bound to a specific context.
|
|
2453
|
+
* Convenient for getting context-specific timer functions without repeatedly passing the contextId.
|
|
2454
|
+
* @param contextId - The context identifier to bind the returned methods to.
|
|
2455
|
+
* @returns An object with setTimeout, setInterval, requestAnimationFrame, and dispose methods bound to the context.
|
|
2456
|
+
* @example
|
|
2457
|
+
* ```ts
|
|
2458
|
+
* const timers = new Timers()
|
|
2459
|
+
* const uiTimers = timers.forContext('ui')
|
|
2460
|
+
*
|
|
2461
|
+
* // These are equivalent to calling timers.setTimeout('ui', ...)
|
|
2462
|
+
* uiTimers.setTimeout(() => console.log('timeout'), 1000)
|
|
2463
|
+
* uiTimers.setInterval(() => console.log('interval'), 500)
|
|
2464
|
+
* uiTimers.requestAnimationFrame(() => console.log('frame'))
|
|
2465
|
+
*
|
|
2466
|
+
* // Dispose only this context
|
|
2467
|
+
* uiTimers.dispose()
|
|
2468
|
+
* ```
|
|
2469
|
+
* @public
|
|
2470
|
+
*/
|
|
2471
|
+
forContext(contextId) {
|
|
2472
|
+
return {
|
|
2473
|
+
setTimeout: (handler, timeout, ...args) => this.setTimeout(contextId, handler, timeout, args),
|
|
2474
|
+
setInterval: (handler, timeout, ...args) => this.setInterval(contextId, handler, timeout, args),
|
|
2475
|
+
requestAnimationFrame: (callback) => this.requestAnimationFrame(contextId, callback),
|
|
2476
|
+
dispose: () => this.dispose(contextId)
|
|
2477
|
+
};
|
|
2478
|
+
}
|
|
2479
|
+
};
|
|
2480
|
+
|
|
2481
|
+
// src/lib/url.ts
|
|
2482
|
+
var safeParseUrl = (url, baseUrl) => {
|
|
2483
|
+
try {
|
|
2484
|
+
return new URL(url, baseUrl);
|
|
2485
|
+
} catch {
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
};
|
|
2489
|
+
|
|
2490
|
+
// src/lib/value.ts
|
|
2491
|
+
function isDefined(value) {
|
|
2492
|
+
return value !== void 0;
|
|
2493
|
+
}
|
|
2494
|
+
function isNonNull(value) {
|
|
2495
|
+
return value !== null;
|
|
2496
|
+
}
|
|
2497
|
+
function isNonNullish(value) {
|
|
2498
|
+
return value !== null && value !== void 0;
|
|
2499
|
+
}
|
|
2500
|
+
function getStructuredClone() {
|
|
2501
|
+
if (typeof globalThis !== "undefined" && globalThis.structuredClone) {
|
|
2502
|
+
return [globalThis.structuredClone, true];
|
|
2503
|
+
}
|
|
2504
|
+
if (typeof global !== "undefined" && global.structuredClone) {
|
|
2505
|
+
return [global.structuredClone, true];
|
|
2506
|
+
}
|
|
2507
|
+
if (typeof window !== "undefined" && window.structuredClone) {
|
|
2508
|
+
return [window.structuredClone, true];
|
|
2509
|
+
}
|
|
2510
|
+
return [(i) => i ? JSON.parse(JSON.stringify(i)) : i, false];
|
|
2511
|
+
}
|
|
2512
|
+
var _structuredClone = getStructuredClone();
|
|
2513
|
+
var structuredClone = _structuredClone[0];
|
|
2514
|
+
var isNativeStructuredClone = _structuredClone[1];
|
|
2515
|
+
var STRUCTURED_CLONE_OBJECT_PROTOTYPE = Object.getPrototypeOf(structuredClone({}));
|
|
2516
|
+
|
|
2517
|
+
// src/lib/warn.ts
|
|
2518
|
+
var usedWarnings = /* @__PURE__ */ new Set();
|
|
2519
|
+
function warnDeprecatedGetter(name) {
|
|
2520
|
+
warnOnce(
|
|
2521
|
+
`Using '${name}' is deprecated and will be removed in the near future. Please refactor to use 'get${name[0].toLocaleUpperCase()}${name.slice(
|
|
2522
|
+
1
|
|
2523
|
+
)}' instead.`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
function warnOnce(message) {
|
|
2527
|
+
if (usedWarnings.has(message)) return;
|
|
2528
|
+
usedWarnings.add(message);
|
|
2529
|
+
console.warn(`[tldraw] ${message}`);
|
|
2530
|
+
}
|
|
2531
|
+
|
|
2532
|
+
// src/index.ts
|
|
2533
|
+
registerDrawLibraryVersion(
|
|
2534
|
+
"@ibodr/utils",
|
|
2535
|
+
"0.0.1",
|
|
2536
|
+
"esm"
|
|
2537
|
+
);
|
|
2538
|
+
/*!
|
|
2539
|
+
* MIT License: https://github.com/NoHomey/bind-decorator/blob/master/License
|
|
2540
|
+
* Copyright (c) 2016 Ivo Stratev
|
|
2541
|
+
*/
|
|
2542
|
+
/*!
|
|
2543
|
+
* MIT License: https://github.com/ai/nanoid/blob/main/LICENSE
|
|
2544
|
+
* Modified code originally from <https://github.com/ai/nanoid>
|
|
2545
|
+
* Copyright 2017 Andrey Sitnik <andrey@sitnik.ru>
|
|
2546
|
+
*
|
|
2547
|
+
* `nanoid` is currently only distributed as an ES module. Some tools (jest, playwright) don't
|
|
2548
|
+
* properly support ESM-only code yet, and tldraw itself is distributed as both an ES module and a
|
|
2549
|
+
* CommonJS module. By including nanoid here, we can make sure it works well in every environment
|
|
2550
|
+
* where tldraw is used. We can also remove some unused features like custom alphabets.
|
|
2551
|
+
*/
|
|
2552
|
+
/*!
|
|
2553
|
+
* MIT License: https://github.com/vHeemstra/is-apng/blob/main/license
|
|
2554
|
+
* Copyright (c) Philip van Heemstra
|
|
2555
|
+
*/
|
|
2556
|
+
/*!
|
|
2557
|
+
* MIT License
|
|
2558
|
+
* Modified code originally from <https://github.com/qzb/is-animated>
|
|
2559
|
+
* Copyright (c) 2016 Józef Sokołowski <j.k.sokolowski@gmail.com>
|
|
2560
|
+
*/
|
|
2561
|
+
/*!
|
|
2562
|
+
* MIT License: https://github.com/alexgorbatchev/crc/blob/master/LICENSE
|
|
2563
|
+
* Copyright: 2014 Alex Gorbatchev
|
|
2564
|
+
* Code: crc32, https://github.com/alexgorbatchev/crc/blob/master/src/calculators/crc32.ts
|
|
2565
|
+
*/
|
|
2566
|
+
/*!
|
|
2567
|
+
* MIT License: https://github.com/sindresorhus/is-webp/blob/main/license
|
|
2568
|
+
* Copyright (c) Sindre Sorhus <sindresorhus@gmail.com> (https://sindresorhus.com)
|
|
2569
|
+
*/
|
|
2570
|
+
|
|
2571
|
+
export { DEFAULT_SUPPORTED_IMAGE_TYPES, DEFAULT_SUPPORTED_MEDIA_TYPES, DEFAULT_SUPPORTED_MEDIA_TYPE_LIST, DEFAULT_SUPPORT_VIDEO_TYPES, ExecutionQueue, FileHelpers, FpsScheduler, Image, LruCache, MediaHelpers, PerformanceTracker, PngHelpers, Result, STRUCTURED_CLONE_OBJECT_PROTOTYPE, Timers, WeakCache, ZERO_INDEX_KEY, annotateError, areArraysShallowEqual, areObjectsShallowEqual, assert, assertExists, bind, clearLocalStorage, clearSessionStorage, compact, debounce, dedupe, deleteFromLocalStorage, deleteFromSessionStorage, exhaustiveSwitchError, fetch, filterEntries, fpsThrottle, getChangedKeys, getErrorAnnotations, getFirstFromIterable, getFromLocalStorage, getFromSessionStorage, getHashForBuffer, getHashForObject, getHashForString, getIndexAbove, getIndexBelow, getIndexBetween, getIndices, getIndicesAbove, getIndicesBelow, getIndicesBetween, getOwnProperty, groupBy, hasOwnProperty, invLerp, isDefined, isEqualAllowingForFloatingPointErrors, isNativeStructuredClone, isNonNull, isNonNullish, last, lerp, lns, mapObjectMapValues, maxBy, measureAverageDuration, measureCbDuration, measureDuration, mergeArraysAndReplaceDefaults, minBy, mockUniqueId, modulate, noop, objectMapEntries, objectMapEntriesIterable, objectMapFromEntries, objectMapKeys, objectMapValues, omit, omitFromStackTrace, partition, promiseWithResolve, registerDrawLibraryVersion, restoreUniqueId, retry, rng, rotateArray, safeParseUrl, setInLocalStorage, setInSessionStorage, sleep, sortById, sortByIndex, sortByMaybeIndex, stringEnum, structuredClone, throttleToNextFrame, uniqueId, validateIndexKey, warnDeprecatedGetter, warnOnce };
|
|
2572
|
+
//# sourceMappingURL=index.mjs.map
|
|
2573
|
+
//# sourceMappingURL=index.mjs.map
|