pts 0.12.8 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/Util.ts ADDED
@@ -0,0 +1,454 @@
1
+ /*! Pts.js is licensed under Apache License 2.0. Copyright © 2017-current William Ngan and contributors. (https://github.com/williamngan/pts) */
2
+
3
+ import type { CanvasSpace } from "./Canvas";
4
+ import { Num } from "./Num";
5
+ import { Group, Pt } from "./Pt";
6
+ import { type WarningType, type PtLike, type PtLikeIterable } from "./Types";
7
+
8
+ /**
9
+ * Various constant values for enumerations and calculations.
10
+ */
11
+ export const Const = {
12
+ /** A string to indicate xy plane. */
13
+ xy: "xy",
14
+
15
+ /** A string to indicate yz plane. */
16
+ yz: "yz",
17
+
18
+ /** A string to indicate xz plane. */
19
+ xz: "xz",
20
+
21
+ /** A string to indicate xyz space. */
22
+ xyz: "xyz",
23
+
24
+ /** Represents horizontal direction. */
25
+ horizontal: 0,
26
+
27
+ /** Represents vertical direction. */
28
+ vertical: 1,
29
+
30
+ /** Represents identical point or value */
31
+ identical: 0,
32
+
33
+ /** Represents right position or direction */
34
+ right: 4,
35
+
36
+ /** Represents bottom right position or direction */
37
+ bottom_right: 5,
38
+
39
+ /** Represents bottom position or direction */
40
+ bottom: 6,
41
+
42
+ /** Represents bottom left position or direction */
43
+ bottom_left: 7,
44
+
45
+ /** Represents left position or direction */
46
+ left: 8,
47
+
48
+ /** Represents top left position or direction */
49
+ top_left: 1,
50
+
51
+ /** Represents top position or direction */
52
+ top: 2,
53
+
54
+ /** Represents top right position or direction */
55
+ top_right: 3,
56
+
57
+ /** Represents an arbitrary very small number. It is set as 0.0001 here. */
58
+ epsilon: 0.0001,
59
+
60
+ /** Represents Number.MAX_VALUE. Note: as a Float32 Pt value this overflows to Infinity. */
61
+ max: Number.MAX_VALUE,
62
+
63
+ /** Represents Number.MIN_VALUE, the smallest *positive* number (5e-324) — not the most negative number. Do not use it to initialize a running maximum; use -Infinity instead. As a Float32 Pt value this flushes to 0. */
64
+ min: Number.MIN_VALUE,
65
+
66
+ /** π radian (180 deg) */
67
+ pi: Math.PI,
68
+
69
+ /** Two π radian (360deg) */
70
+ two_pi: 6.283185307179586,
71
+
72
+ /** Half π radian (90deg) */
73
+ half_pi: 1.5707963267948966,
74
+
75
+ /** π/4 radian (45deg) */
76
+ quarter_pi: 0.7853981633974483,
77
+
78
+ /** π/180 or 1 degree in radian */
79
+ one_degree: 0.017453292519943295,
80
+
81
+ /** Multiply this constant with a radian to get a degree */
82
+ rad_to_deg: 57.29577951308232,
83
+
84
+ /** Multiply this constant with a degree to get a radian */
85
+ deg_to_rad: 0.017453292519943295,
86
+
87
+ /** Gravity acceleration (unit: m/s^2) and gravity force (unit: Newton) on 1kg of mass. */
88
+ gravity: 9.81,
89
+
90
+ /** 1 Newton: 0.10197 Kilogram-force */
91
+ newton: 0.10197,
92
+
93
+ /** Gaussian constant (1 / Math.sqrt(2 * Math.PI)) */
94
+ gaussian: 0.3989422804014327,
95
+ };
96
+
97
+ /**
98
+ * Util class provides static helper functions.
99
+ */
100
+ export class Util {
101
+ static _warnLevel: WarningType = "mute";
102
+
103
+ /**
104
+ * Set a global warning level setting. If no parameter is passed, this will return the current warn-level. See [`Util.warn`](#link).
105
+ * @param lv a [`WarningType`](#link) option, where "error" will throw an error, "warn" will log in console, and "mute" will ignore the error.
106
+ */
107
+ static warnLevel(lv?: WarningType): WarningType {
108
+ if (lv) {
109
+ Util._warnLevel = lv;
110
+ }
111
+ return Util._warnLevel;
112
+ }
113
+
114
+ /**
115
+ * Convert different kinds of parameters (arguments, array, object) into an array of numbers.
116
+ * @param args can be either a list of numbers, an array, a Pt, or an object with {x,y,z,w} properties
117
+ */
118
+ static getArgs(args: any[]): Array<number> {
119
+ if (args.length < 1) return [];
120
+
121
+ let pos = [];
122
+
123
+ let isArray = Array.isArray(args[0]) || ArrayBuffer.isView(args[0]);
124
+
125
+ // positional arguments: x,y,z,w,...
126
+ if (typeof args[0] === "number") {
127
+ pos = Array.prototype.slice.call(args);
128
+
129
+ // as an object of {x, y?, z?, w?}
130
+ } else if (typeof args[0] === "object" && !isArray) {
131
+ let a = ["x", "y", "z", "w"];
132
+ let p = args[0];
133
+ for (let i = 0; i < a.length; i++) {
134
+ if ((p.length && i >= p.length) || !(a[i] in p)) break; // check for length and key exist
135
+ pos.push(p[a[i]]);
136
+ }
137
+
138
+ // as an array of values
139
+ } else if (isArray) {
140
+ pos = Util.toNumericArray(args[0]);
141
+ }
142
+
143
+ return pos;
144
+ }
145
+
146
+ /**
147
+ * Copy an array or typed array of numbers into a fresh plain array.
148
+ * @param a an array or typed array
149
+ */
150
+ static toNumericArray(a: ArrayLike<number>): number[] {
151
+ // native slice is a fast path for plain arrays, but `[].slice.call` on a
152
+ // typed array falls back to the generic per-element protocol
153
+ if (Array.isArray(a)) return a.slice();
154
+ const out = [];
155
+ for (let i = 0, len = a.length; i < len; i++) {
156
+ out.push(a[i]);
157
+ }
158
+ return out;
159
+ }
160
+
161
+ /**
162
+ * Like [`Util.getArgs`](#link), but avoids copying when the arguments are already
163
+ * a numeric array, a typed array (eg, a Pt), or a list of numbers. The result may be
164
+ * the caller's own object, so it must be treated as read-only.
165
+ * @param args can be either a list of numbers, an array, a Pt, or an object with {x,y,z,w} properties
166
+ */
167
+ static getPtLike(args: any[]): PtLike {
168
+ const a0 = args[0];
169
+ if (typeof a0 === "number") return args; // rest args are a fresh, private array
170
+ if (args.length === 1 && (Array.isArray(a0) || ArrayBuffer.isView(a0))) {
171
+ return a0 as PtLike;
172
+ }
173
+ return Util.getArgs(args);
174
+ }
175
+
176
+ /**
177
+ * Send a warning message based on [`Util.warnLevel`](#link) global setting. This allows you to dynamically set whether minor errors should be thrown or printed in console or muted.
178
+ * @param message any error or warning message
179
+ * @param defaultReturn optional return value
180
+ */
181
+ static warn(message: string = "error", defaultReturn: any = undefined): any {
182
+ if (Util.warnLevel() == "error") {
183
+ throw new Error(message);
184
+ } else if (Util.warnLevel() == "warn") {
185
+ console.warn(message);
186
+ }
187
+ return defaultReturn;
188
+ }
189
+
190
+ /**
191
+ * Get a random integer. This can be useful for selecting a random index in an array.
192
+ * @deprecated Use [`Num.randomRange`](#link) instead, for example `Math.floor( Num.randomRange( start, start + range ) )`.
193
+ * @param range value range
194
+ * @param start Optional starting value
195
+ */
196
+ static randomInt(range: number, start: number = 0) {
197
+ Util.warn("Util.randomInt is deprecated. Please use `Num.randomRange`");
198
+ return Math.floor(Num.random() * range) + start;
199
+ }
200
+
201
+ /**
202
+ * Split an array into chunks of sub-array.
203
+ * @param pts an array
204
+ * @param size chunk size, ie, number of items in a chunk
205
+ * @param stride optional parameter to "walk through" the array in steps
206
+ * @param loopBack if `true`, always go through the array till the end and loop back to the beginning to complete the segments if needed.
207
+ * @param matchSize if `true`, all chunks's length must match `size`.
208
+ */
209
+ static split(
210
+ pts: any[],
211
+ size: number,
212
+ stride?: number,
213
+ loopBack: boolean = false,
214
+ matchSize = true,
215
+ ): any[][] {
216
+ let chunks: any[] = [];
217
+ let part: any[] = [];
218
+ let st: number = stride || size;
219
+ let index: number = 0;
220
+ if (pts.length <= 0 || st <= 0) return [];
221
+
222
+ while (index < pts.length) {
223
+ part = [];
224
+ for (let k = 0; k < size; k++) {
225
+ if (loopBack) {
226
+ part.push(pts[(index + k) % pts.length]);
227
+ } else {
228
+ if (index + k >= pts.length) break;
229
+ part.push(pts[index + k]);
230
+ }
231
+ }
232
+ index += st;
233
+ if (!matchSize || (matchSize && part.length === size)) chunks.push(part);
234
+ }
235
+
236
+ return chunks;
237
+ }
238
+
239
+ /**
240
+ * Flatten an array of arrays such as Group[] to a flat Array or Group.
241
+ * @param pts an array, usually an array of Groups
242
+ * @param flattenAsGroup a boolean to specify whether the return type should be a Group or Array. Default is `true` which returns a Group.
243
+ */
244
+ static flatten(pts: any[], flattenAsGroup: boolean = true): any {
245
+ // loop instead of concat.apply: spreading `pts` as arguments overflows
246
+ // the JS argument-count limit for very large inputs
247
+ const arr: unknown[] = flattenAsGroup ? new Group() : [];
248
+ for (let i = 0, len = pts.length; i < len; i++) {
249
+ const p = pts[i];
250
+ if (Array.isArray(p)) {
251
+ for (let k = 0, lenP = p.length; k < lenP; k++) arr.push(p[k]);
252
+ } else {
253
+ arr.push(p);
254
+ }
255
+ }
256
+ return arr;
257
+ }
258
+
259
+ /**
260
+ * Given two arrays of objects, and a function that operate on two objects, return an array. Objects must be of same type.
261
+ * @param a an array of object, eg `[Group, Group, ...]`
262
+ * @param b another array of object
263
+ * @param op a function that takes two parameters (a, b) and returns an object.
264
+ */
265
+ static combine<T>(a: T[], b: T[], op: (a: T, b: T) => T): T[] {
266
+ let result = [];
267
+ for (let i = 0, len = a.length; i < len; i++) {
268
+ for (let k = 0, lenB = b.length; k < lenB; k++) {
269
+ result.push(op(a[i], b[k]));
270
+ }
271
+ }
272
+ return result;
273
+ }
274
+
275
+ /**
276
+ * Zip arrays. eg, `[[1,2],[3,4],[5,6]] => [[1,3,5],[2,4,6]]`.
277
+ * @param arrays an array of arrays
278
+ */
279
+ static zip(arrays: Array<any>[]) {
280
+ let z = [];
281
+ for (let i = 0, len = arrays[0].length; i < len; i++) {
282
+ let p = [];
283
+ for (let k = 0; k < arrays.length; k++) {
284
+ p.push(arrays[k][i]);
285
+ }
286
+ z.push(p);
287
+ }
288
+ return z;
289
+ }
290
+
291
+ /**
292
+ * Create a convenient stepper. This returns a function which you can call repeatedly to step a counter.
293
+ * @param max Maximum of the stepper range. The resulting stepper will return values within [min, max). Note that the first call returns `min + stride`, not `min`.
294
+ * @param min Minimum of the stepper range. Default is 0.
295
+ * @param stride Stride of the step. Default is 1.
296
+ * @param callback An optional callback function `fn( step )`, which will be called each time when stepper function is called.
297
+ * @example `let counter = stepper(100); let c = counter(); c = counter(); ...`
298
+ * @returns a function which will increment the stepper and return its value at each call.
299
+ */
300
+ static stepper(
301
+ max: number,
302
+ min: number = 0,
303
+ stride: number = 1,
304
+ callback?: (n: number) => void,
305
+ ): () => number {
306
+ let c = min;
307
+ return function () {
308
+ c += stride;
309
+ if (c >= max) {
310
+ // anchored modulo keeps c in [min, max) even when stride > max - min
311
+ c = min + ((c - min) % (max - min));
312
+ }
313
+ if (callback) callback(c);
314
+ return c;
315
+ };
316
+ }
317
+
318
+ /**
319
+ * A convenient way to step through a range. Same as `for (i=0; i<range; i++)`, except this also stores the resulting return values at each step and return them as an array.
320
+ * @param range a range to step through
321
+ * @param fn a callback function `fn(index)`. If this function returns a value, it will be stored at each step
322
+ * @returns an index-aligned array of returned values: entries sit at their step index, so with a non-zero `start` the positions below `start` are empty holes
323
+ */
324
+ static forRange(
325
+ fn: (index: number) => any,
326
+ range: number,
327
+ start: number = 0,
328
+ step: number = 1,
329
+ ): any[] {
330
+ let temp = [];
331
+ for (let i = start, len = range; i < len; i += step) {
332
+ temp[i] = fn(i);
333
+ }
334
+ return temp;
335
+ }
336
+
337
+ /**
338
+ * A helper function to load data from a url via XMLHttpRequest GET. Since the response passed into callback is a string, if you're loading json data, you may use standard `JSON.parse(response)` to get a JSON object. For csv, try using a javascript csv library like papaparse or vega/datalib.
339
+ * @param url the request url
340
+ * @param callback a function to capture the data. It receives two parameters: a `response` as string, and a `success` status as boolean.
341
+ */
342
+ static load(
343
+ url: string,
344
+ callback: (response: string, success: boolean) => void,
345
+ ) {
346
+ let request = new XMLHttpRequest();
347
+ request.open("GET", url, true);
348
+
349
+ request.onload = function () {
350
+ if (request.status >= 200 && request.status < 400) {
351
+ callback(request.responseText, true);
352
+ } else {
353
+ callback(
354
+ `Server error (${request.status}) when loading "${url}"`,
355
+ false,
356
+ );
357
+ }
358
+ };
359
+
360
+ request.onerror = function () {
361
+ callback(`Unknown network error`, false);
362
+ };
363
+
364
+ request.send();
365
+ }
366
+
367
+ /**
368
+ * Download the current `CanvasSpace` as an image (jpg/png/webp). Calling this function will automatically trigger a download.
369
+ * @param space an instance of `CanvasSpace`
370
+ * @param filename the name of the file, without the extension name.
371
+ * @param filetype the image type (jpg/png/webp)
372
+ * @param quality a value between 0 to 1, if filetype is either "jpg" or "png"
373
+ */
374
+ static download(
375
+ space: CanvasSpace,
376
+ filename: string = "pts_canvas_image",
377
+ filetype: "jpeg" | "jpg" | "png" | "webp" = "png",
378
+ quality: number = 1,
379
+ ) {
380
+ const ftype = filetype === "jpg" ? "jpeg" : filetype;
381
+ space.element.toBlob(
382
+ function (blob) {
383
+ const link = document.createElement("a");
384
+ // toBlob yields null on encoding failure; createObjectURL then throws
385
+ // a TypeError, same as it always has — kept, not silently swallowed
386
+ const url = URL.createObjectURL(blob!);
387
+ link.href = url;
388
+ link.download = `${filename}.${filetype}`;
389
+ document.body.appendChild(link);
390
+ link.click();
391
+ document.body.removeChild(link);
392
+ URL.revokeObjectURL(url);
393
+ },
394
+ `image/${ftype}`,
395
+ quality,
396
+ );
397
+ }
398
+
399
+ /**
400
+ * Estimate performance by checking how long it takes to render a frame
401
+ * @param avgFrames The number of frames used calculate to average
402
+ * @example `let perf = Util.performance(); perf();`
403
+ * @returns milliseconds per frame
404
+ */
405
+ static performance(avgFrames: number = 10): () => number {
406
+ let last = Date.now();
407
+ let avg = [];
408
+ return function () {
409
+ const now = Date.now();
410
+ avg.push(now - last);
411
+ if (avg.length > avgFrames) avg.shift();
412
+ last = now;
413
+ return Math.floor(avg.reduce((a, b) => a + b, 0) / avg.length);
414
+ };
415
+ }
416
+
417
+ /**
418
+ * Check number of items in a Group against a required number
419
+ * @param pts a Group or an Iterable<PtLike>
420
+ * @param minRequired minimum number of items required
421
+ */
422
+ static arrayCheck(pts: PtLikeIterable, minRequired: number = 2): boolean {
423
+ if (Array.isArray(pts) && pts.length < minRequired) {
424
+ Util.warn(`Requires ${minRequired} or more Pts in this Group.`);
425
+ return false;
426
+ }
427
+ return true;
428
+ }
429
+
430
+ /**
431
+ * Convert an iterable into an array
432
+ * @param it an iterable
433
+ */
434
+ static iterToArray(it: Iterable<any>): any[] {
435
+ return !Array.isArray(it) ? [...it] : it;
436
+ }
437
+
438
+ /**
439
+ * Check if accessing from a mobile device. Can be useful since some experimental features may not be availble in mobile browsers.
440
+ */
441
+ static isMobile() {
442
+ return /iPhone|iPad|Android/i.test(navigator.userAgent);
443
+ }
444
+
445
+ /**
446
+ * Generate a time-based unique ID or a crypto-based ID.
447
+ * @returns
448
+ */
449
+ static uniqueId(useCrypto = false) {
450
+ return useCrypto && typeof crypto !== "undefined" && crypto?.randomUUID
451
+ ? crypto.randomUUID()
452
+ : Date.now().toString(36) + Math.random().toString(36).substring(2);
453
+ }
454
+ }
package/src/_module.ts ADDED
@@ -0,0 +1,18 @@
1
+ export * from "./Canvas";
2
+ export * from "./Create";
3
+ export * from "./Form";
4
+ export * from "./LinearAlgebra";
5
+ export * from "./Num";
6
+ export * from "./Op";
7
+ export * from "./Pt";
8
+ export * from "./Space";
9
+ export * from "./Color";
10
+ export * from "./Util";
11
+ export * from "./Dom";
12
+ export * from "./Svg";
13
+ export * from "./Typography";
14
+ export * from "./Physics";
15
+ export * from "./Play";
16
+ export * from "./UI";
17
+ export * from "./Image";
18
+ export * from "./Types";
package/src/_script.ts ADDED
@@ -0,0 +1,94 @@
1
+ import * as Canvas from "./Canvas";
2
+ import * as Create from "./Create";
3
+ import * as Form from "./Form";
4
+ import * as LinearAlgebra from "./LinearAlgebra";
5
+ import * as Num from "./Num";
6
+ import * as Op from "./Op";
7
+ import * as Pt from "./Pt";
8
+ import * as Space from "./Space";
9
+ import * as Color from "./Color";
10
+ import * as Util from "./Util";
11
+ import * as Dom from "./Dom";
12
+ import * as Svg from "./Svg";
13
+ import * as Typography from "./Typography";
14
+ import * as Physics from "./Physics";
15
+ import * as UI from "./UI";
16
+ import * as Play from "./Play";
17
+ import * as Image from "./Image";
18
+ import * as Types from "./Types";
19
+
20
+ declare global {
21
+ // the browser bundle mounts the whole library on globalThis.Pts
22
+
23
+ var Pts: Record<string, any>;
24
+ }
25
+
26
+ globalThis.Pts = {
27
+ ...Canvas,
28
+ ...Create,
29
+ ...Form,
30
+ ...LinearAlgebra,
31
+ ...Num,
32
+ ...Op,
33
+ ...Pt,
34
+ ...Space,
35
+ ...Color,
36
+ ...Util,
37
+ ...Dom,
38
+ ...Svg,
39
+ ...Typography,
40
+ ...Physics,
41
+ ...UI,
42
+ ...Play,
43
+ ...Image,
44
+ };
45
+
46
+ // A function to switch scope for Pts library. eg, Pts.namespace( window );
47
+ globalThis.Pts.namespace = (scope: any) => {
48
+ let lib = globalThis.Pts;
49
+ for (let k in lib) {
50
+ if (k != "namespace") {
51
+ scope[k] = lib[k];
52
+ }
53
+ }
54
+ };
55
+
56
+ globalThis.Pts.quickStart = (id: string | Element, bg: string = "#9ab") => {
57
+ if (!window) return;
58
+
59
+ let s: any = globalThis;
60
+ globalThis.Pts.namespace(s);
61
+
62
+ // pick the rendering backend from the mount element: an <svg> element (or a
63
+ // container holding one) gets an SVGSpace, anything else a CanvasSpace —
64
+ // so a sketch can swap renderers by changing only its HTML
65
+ // Preserve CanvasSpace's bare-ID shorthand for both rendering backends.
66
+ const mount =
67
+ typeof id === "string" && id[0] !== "#" && id[0] !== "." ? `#${id}` : id;
68
+ const elem =
69
+ typeof mount === "string" ? document.querySelector(mount) : mount;
70
+ const isSVG =
71
+ elem &&
72
+ ((elem as Element).nodeName.toLowerCase() === "svg" ||
73
+ !!(elem as Element).querySelector(":scope > svg"));
74
+
75
+ s.space = isSVG
76
+ ? new Svg.SVGSpace(mount).setup({ bgcolor: bg, resize: true })
77
+ : new Canvas.CanvasSpace(mount).setup({
78
+ bgcolor: bg,
79
+ resize: true,
80
+ retina: true,
81
+ });
82
+ s.form = s.space.getForm();
83
+
84
+ return function (animate = null, start = null, action = null, resize = null) {
85
+ s.space.add({
86
+ start: start,
87
+ animate: animate,
88
+ resize: resize,
89
+ action: action,
90
+ });
91
+
92
+ s.space.bindMouse().bindTouch().play();
93
+ };
94
+ };