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/Types.ts ADDED
@@ -0,0 +1,301 @@
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 Pt, type Group, type Bound } from "./Pt";
4
+ import { type Space } from "./Space";
5
+ import { type UI, type UIPointerAction } from "./UI";
6
+
7
+ /**
8
+ * Typescript interface: IPt is an interface that represents an object with x, y, z, w properties.
9
+ */
10
+ export interface IPt {
11
+ x?: number;
12
+ y?: number;
13
+ z?: number;
14
+ w?: number;
15
+ }
16
+
17
+ /**
18
+ * Typescript type: PtLike represents the data of a point. It can be either a Pt instance or an array of numbers.
19
+ */
20
+ export type PtLike = Pt | Float32Array | number[];
21
+
22
+ /**
23
+ * Typescript type: GroupLike represents an array of Pt instances. It be a Group instance or an array of Pt. Unlike `PtIterable`, this type only allows arrays but not iterables.
24
+ */
25
+ export type GroupLike = Group | Pt[];
26
+
27
+ /**
28
+ * Typescript type: PtIterable represents an iterable list of Pt instances. Unlike `PtLikeIterable`, this type only allows Pt instances but not numbers' arrays.
29
+ * If you aren't sure what this type means, treat this as a [`Group`](#link) instance.
30
+ */
31
+ export type PtIterable = GroupLike | Pt[] | Iterable<Pt>;
32
+
33
+ /**
34
+ * Typescript type: PtLikeIterable is the most flexible way to represent an iterable list of point data. For example, it can be a Group, an iterable of Pt instances, or an array of numbers' arrays.
35
+ * If you aren't sure what this type means, treat this as a [`Group`](#link) instance.
36
+ */
37
+ export type PtLikeIterable = GroupLike | PtLike[] | Iterable<PtLike>;
38
+
39
+ /**
40
+ * Typescript type: TextMeasure represents a function that returns the rendered width of a string of text, such as canvas context's `measureText` or an estimator created via [`Typography.textWidthEstimator`](#link).
41
+ */
42
+ export type TextMeasure = (text: string) => number;
43
+
44
+ /**
45
+ * Typescript type: TextVerticalAlign represents the vertical alignment options accepted by [`CanvasForm.textBox`](#link) and [`CanvasForm.paragraphBox`](#link).
46
+ */
47
+ export type TextVerticalAlign =
48
+ "top" | "start" | "middle" | "center" | "bottom" | "end";
49
+
50
+ /**
51
+ * Typescript type: AnimateCallbackFn represents a callback function for animation. It accepts parameters to keep track of current time, current frame-time, and current space instance.
52
+ */
53
+ export type AnimateCallbackFn = (
54
+ time: number,
55
+ frameTime: number,
56
+ currentSpace: Space,
57
+ ) => void;
58
+
59
+ /**
60
+ * Typescript type: UIActionEvent represents the DOM events a Space dispatches to players and UI handlers — pointer, mouse, touch, and keyboard.
61
+ */
62
+ export type UIActionEvent =
63
+ MouseEvent | TouchEvent | PointerEvent | KeyboardEvent;
64
+
65
+ /**
66
+ * Typescript interface: IPlayer is an interface that represents a "player" object that can be added into a Space.
67
+ */
68
+ export interface IPlayer {
69
+ animateID?: string;
70
+ animate?: AnimateCallbackFn;
71
+ resize?(bound: Bound, evt?: Event | null): void;
72
+ action?(type: string, px: number, py: number, evt: UIActionEvent): void;
73
+ start?(bound: Bound, space: Space): void;
74
+ }
75
+
76
+ /**
77
+ * Typescript interface: ISpacePlayers represents a map of IPlayer instances.
78
+ */
79
+ export interface ISpacePlayers {
80
+ [key: string]: IPlayer;
81
+ }
82
+
83
+ /**
84
+ *Typescript interface: ITimer represents a time-recording object.
85
+ */
86
+ export interface ITimer {
87
+ prev: number;
88
+ diff: number;
89
+ end: number;
90
+ min: number;
91
+ }
92
+
93
+ /**
94
+ * Typescript type: TouchPointsKey represents a set of acceptable string keys for defining touch action.
95
+ */
96
+ export type TouchPointsKey = "touches" | "changedTouches" | "targetTouches";
97
+
98
+ /**
99
+ * Typescript interface: MultiTouchElement represents an element that can handle touch events.
100
+ */
101
+ export interface MultiTouchElement {
102
+ addEventListener(
103
+ evt: string,
104
+ callback: EventListenerOrEventListenerObject,
105
+ ): void;
106
+ removeEventListener(
107
+ evt: string,
108
+ callback: EventListenerOrEventListenerObject,
109
+ ): void;
110
+ }
111
+
112
+ /**
113
+ * Typescript type: Setup options for CanvasSpace. See [`CanvasSpace.setup()`](#link) function.
114
+ */
115
+ export type CanvasSpaceOptions = {
116
+ bgcolor?: string;
117
+ resize?: boolean;
118
+ retina?: boolean;
119
+ offscreen?: boolean;
120
+ pixelDensity?: number;
121
+ };
122
+
123
+ /**
124
+ * Typescript type: ColorType represents a defined set of string values such as "rgb" and "lab".
125
+ */
126
+ export type ColorType =
127
+ "rgb" | "hsl" | "hsb" | "lab" | "lch" | "luv" | "xyz" | "oklab" | "oklch";
128
+
129
+ /**
130
+ * Typescript type: DelaunayShape represents an object type that can store a Delaunay element. It has 3 indices (i, j, k) and two groups that represent a triangle and a circle.
131
+ */
132
+ export type DelaunayShape = {
133
+ i: number;
134
+ j: number;
135
+ k: number;
136
+ triangle: GroupLike;
137
+ circle: Group;
138
+ };
139
+
140
+ /**
141
+ * Typescript type: DelaunayMesh represents an object type that has an array of {key: shape} items, where each shape represents a DelaunayShape.
142
+ * Note the unusual shape: it is an array indexed by point index, where each entry is a dictionary keyed by `"min-max"` neighbor-pair strings. This mirrors the mesh cache built by [`Delaunay.mesh`](#link) and is kept as-is for compatibility.
143
+ */
144
+ export type DelaunayMesh = { [key: string]: DelaunayShape }[];
145
+
146
+ /**
147
+ * Typescript type: FlockBoundary is how a [`Flock`](#link) treats the edges of its bound:
148
+ * `"steer"` turns agents back within a margin, `"wrap"` moves them to the opposite edge,
149
+ * `"bounce"` reflects them, and `"none"` lets them leave.
150
+ */
151
+ export type FlockBoundary = "steer" | "wrap" | "bounce" | "none";
152
+
153
+ /**
154
+ * Typescript type: FlockOptions are the settings accepted by [`Create.flock`](#link) and
155
+ * [`Flock.setup`](#link). Every field is optional; see the matching [`Flock`](#link) accessor
156
+ * for its meaning and default.
157
+ */
158
+ export type FlockOptions = {
159
+ /** Radius within which an agent sees its neighbors. Default is 40. */
160
+ perception?: number;
161
+ /** Radius within which an agent steers away from its neighbors. Default is 20. */
162
+ separation?: number;
163
+ /** Weight of steering toward the neighbors' center. Default is 1. */
164
+ cohesionWeight?: number;
165
+ /** Weight of matching the neighbors' heading. Default is 1. */
166
+ alignWeight?: number;
167
+ /** Weight of steering away from close neighbors. Default is 1.5. */
168
+ separateWeight?: number;
169
+ /** Maximum speed, in units per second. Default is 100. */
170
+ maxSpeed?: number;
171
+ /** Minimum speed, in units per second. Default is 0. */
172
+ minSpeed?: number;
173
+ /** Maximum steering force, in units per second squared. Default is 200. */
174
+ maxForce?: number;
175
+ /** A [`Bound`](#link) or a Group of 2 Pts that keeps the flock in view. Default is none. */
176
+ bound?: GroupLike;
177
+ /** How the bound's edges are treated. Default is `"steer"`. */
178
+ boundary?: FlockBoundary;
179
+ /** Distance from an edge at which `"steer"` starts turning agents back. Default is 50. */
180
+ margin?: number;
181
+ /** Maximum simulated time in milliseconds per step. Default is 50. */
182
+ maxTimeStep?: number;
183
+ /** Speed given to agents added without a velocity. Default is half of `maxSpeed`. */
184
+ initialSpeed?: number;
185
+ };
186
+
187
+ /**
188
+ * Typescript type: DOMFormContext represents the current context for an DOMForm.
189
+ */
190
+ export type DOMFormContext = {
191
+ group: Element | null | undefined;
192
+ groupID: string;
193
+ groupCount: number;
194
+ currentID: string;
195
+ currentClass?: string;
196
+ style: Record<string, string | number | boolean>;
197
+ };
198
+
199
+ /**
200
+ * Typescript type: IntersectContext represents a type of an object that store the intersection info.
201
+ */
202
+ export type IntersectContext = {
203
+ which: number;
204
+ dist: number;
205
+ normal: Pt;
206
+ vertex: Pt;
207
+ edge: Group;
208
+ other?: unknown;
209
+ };
210
+
211
+ /**
212
+ * Typescript type: UIHandler represents a callback function to handle UI actions.
213
+ */
214
+ export type UIHandler = (
215
+ target: UI,
216
+ pt: PtLike,
217
+ type: UIPointerAction | (string & {}),
218
+ evt: UIActionEvent,
219
+ ) => void;
220
+
221
+ /**
222
+ * Typescript type: WarningType specifies a level of warning for [`Util.warnLevel`](#link).
223
+ */
224
+ export type WarningType = "error" | "warn" | "mute";
225
+
226
+ /**
227
+ * Typescript type: a callback function type used in `tempo.every(...).start( fn )`
228
+ */
229
+ export type ITempoStartFn = (count: number) => void | boolean;
230
+
231
+ /**
232
+ * Typescript type: a callback function type used in `tempo.every(...).progress( fn )`
233
+ */
234
+ export type ITempoProgressFn = (
235
+ count: number,
236
+ t: number,
237
+ ms: number,
238
+ start: boolean,
239
+ ) => void | boolean;
240
+
241
+ /**
242
+ * Typescript type: ITempoListener represents a listener created by Tempo class
243
+ */
244
+ export type ITempoListener = {
245
+ name?: string; // reference id
246
+ beats?: number | number[]; // rhythm in beats
247
+ period?: number; // current number of beats per period
248
+ duration?: number; // current duration in ms per period
249
+ offset?: number; // time offset
250
+ continuous?: boolean; // track progress is true, otherwise track only triggers
251
+ index?: number; // if beats is an array, this is the current index
252
+ count?: number; // number of periods started so far
253
+ fn: ITempoStartFn | ITempoProgressFn; // callback function
254
+ };
255
+
256
+ /**
257
+ * Typescript type: the return type of `tempo.every(...)`
258
+ */
259
+ export type ITempoResponses = {
260
+ start: (fn: ITempoStartFn, offset?: number, name?: string) => ITempoResponses;
261
+ progress: (
262
+ fn: ITempoProgressFn,
263
+ offset?: number,
264
+ name?: string,
265
+ ) => ITempoResponses;
266
+ };
267
+
268
+ /**
269
+ * Typescript type: ISoundAnalyzer represents an object that stores the AnalyzerNode properties
270
+ */
271
+ export type ISoundAnalyzer = {
272
+ node: AnalyserNode;
273
+ size: number;
274
+ data: Uint8Array;
275
+ };
276
+
277
+ /**
278
+ * Typescript type: SoundType represents a type of sound input. It corresponds to Sound.type property.
279
+ */
280
+ export type SoundType = "file" | "gen" | "input";
281
+
282
+ /**
283
+ * Typescript type: DefaultFormStyle represents a default object for visual styles such as fill, stroke, line width, and others.
284
+ */
285
+ export type DefaultFormStyle = {
286
+ fillStyle?: string | CanvasGradient | CanvasPattern;
287
+ strokeStyle?: string | CanvasGradient | CanvasPattern;
288
+ lineWidth?: number;
289
+ lineJoin?: CanvasLineJoin;
290
+ lineCap?: CanvasLineCap;
291
+ globalAlpha?: number;
292
+ };
293
+
294
+ /**
295
+ * Typescript type: CanvasPatternRepetition represents the string options to specify pattern repetition
296
+ */
297
+ export type CanvasPatternRepetition =
298
+ "repeat" | "repeat-x" | "repeat-y" | "no-repeat";
299
+
300
+ export type RenderingContext2D =
301
+ CanvasRenderingContext2D | OffscreenCanvasRenderingContext2D;
@@ -0,0 +1,228 @@
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 { Pt, Bound } from "./Pt";
4
+ import { Util } from "./Util";
5
+ import { type PtLikeIterable, type TextMeasure } from "./Types";
6
+
7
+ /**
8
+ * Typography provides helper functions to support typographic layouts. For a concrete example, see [a demo here](https://ptsjs.org/demo/?name=canvasform.textBox) that uses the [`CanvasForm.textBox`](#link) function.
9
+ */
10
+ export class Typography {
11
+ /**
12
+ * Create a heuristic text width estimate function. It will be less accurate but faster.
13
+ * @param fn a reference function that can measure text width accurately
14
+ * @param samples a list of string samples. Default is ["M", "n", "."]
15
+ * @param distribution a list of the samples' probability distribution, which should have the same length as `samples` and sum to 1. Default is [0.06, 0.8, 0.14]. (A distribution that sums to more or less than 1 will proportionally inflate or deflate every estimate.)
16
+ * @return a function that can estimate text width
17
+ */
18
+ static textWidthEstimator(
19
+ fn: TextMeasure,
20
+ samples: string[] = ["M", "n", "."],
21
+ distribution: number[] = [0.06, 0.8, 0.14],
22
+ ): TextMeasure {
23
+ if (samples.length !== distribution.length) {
24
+ throw new Error(
25
+ `textWidthEstimator: samples (${samples.length}) and distribution (${distribution.length}) must have the same length`,
26
+ );
27
+ }
28
+ const m = samples.map(fn);
29
+ const avg = new Pt(distribution).dot(m);
30
+ return (text: string): number => text.length * avg;
31
+ }
32
+
33
+ /**
34
+ * Create a memoizing text width function that measures each distinct character once and sums the cached widths. Nearly as accurate as the reference function for most texts (kerning and ligatures excepted) at close to estimator speed after warmup. The cache is keyed by character, so create a new instance whenever the font changes.
35
+ * @param fn a reference function that can measure text width accurately
36
+ * @return a function that measures text width using per-character caching
37
+ */
38
+ static charWidthCache(fn: TextMeasure): TextMeasure {
39
+ // Latin-1 goes through a typed array — a Map lookup per character would
40
+ // cost as much as the measurement it replaces. NaN marks "not yet measured".
41
+ const latin = new Float64Array(256).fill(NaN);
42
+ const cache = new Map<string, number>();
43
+ return (text: string): number => {
44
+ let sum = 0;
45
+ for (let i = 0, len = text.length; i < len; i++) {
46
+ const code = text.charCodeAt(i);
47
+ if (code < 256) {
48
+ let w = latin[code];
49
+ if (w !== w) {
50
+ w = fn(text[i]);
51
+ latin[code] = w;
52
+ }
53
+ sum += w;
54
+ } else {
55
+ let ch = text[i];
56
+ if (code >= 0xd800 && code <= 0xdbff && i + 1 < len) {
57
+ ch += text[i + 1]; // measure a surrogate pair as one character
58
+ i++;
59
+ }
60
+ let w = cache.get(ch);
61
+ if (w === undefined) {
62
+ w = fn(ch);
63
+ cache.set(ch, w);
64
+ }
65
+ sum += w;
66
+ }
67
+ }
68
+ return sum;
69
+ };
70
+ }
71
+
72
+ /**
73
+ * Truncate text to fit width. The result is guaranteed to fit: the largest prefix (possibly empty) is kept such that the prefix plus the tail measures within `width`. The cut never splits a surrogate pair. If even the tail alone cannot fit, `["", 0]` is returned.
74
+ * @param fn a function that can measure text width
75
+ * @param str text to truncate
76
+ * @param width width to fit
77
+ * @param tail text to indicate overflow such as "...". Default is empty "".
78
+ * @param hint optional expected number of characters to keep — a pure performance hint (any value yields the same result) that seeds the search, such as the previous line's length when wrapping. With an empty `tail`, a hint also avoids measuring the entire string.
79
+ * @return a tuple of the truncated text (tail included) and the number of characters kept from `str`
80
+ */
81
+ static truncate(
82
+ fn: TextMeasure,
83
+ str: string,
84
+ width: number,
85
+ tail: string = "",
86
+ hint?: number,
87
+ ): [string, number] {
88
+ const len = str.length;
89
+ let budget: number;
90
+ let max: number;
91
+ let seed: number;
92
+
93
+ if (hint !== undefined && !tail) {
94
+ // Hinted search with no tail: skip the full-string measure. The domain
95
+ // includes `len` itself, so "everything fits" is discovered by the
96
+ // search rather than by a separate upfront measurement.
97
+ budget = width;
98
+ max = len;
99
+ seed = Math.min(max, Math.max(0, Math.floor(hint)));
100
+ } else {
101
+ const full = fn(str);
102
+ if (full <= width) return [str, len];
103
+ budget = width - (tail ? fn(tail) : 0);
104
+ max = len - 1;
105
+ seed =
106
+ hint !== undefined
107
+ ? Math.min(max, Math.max(0, Math.floor(hint)))
108
+ : Math.min(max, Math.max(0, Math.floor((len * budget) / full)));
109
+ }
110
+
111
+ const fits = (k: number): boolean => fn(str.slice(0, k)) <= budget;
112
+
113
+ // Find the largest k in [0, max] where the prefix fits, starting from the
114
+ // seed — exact for linear measures, so the gallop below usually settles
115
+ // the boundary in two probes.
116
+ let best = -1;
117
+ let lo = 0;
118
+ let hi = max;
119
+
120
+ if (budget >= 0) {
121
+ const k = seed;
122
+ if (fits(k)) {
123
+ best = k;
124
+ lo = k + 1;
125
+ for (let inc = 1; lo <= hi; inc *= 2) {
126
+ const p = Math.min(hi, k + inc);
127
+ if (!fits(p)) {
128
+ hi = p - 1;
129
+ break;
130
+ }
131
+ best = p;
132
+ lo = p + 1;
133
+ }
134
+ } else {
135
+ hi = k - 1;
136
+ for (let inc = 1; lo <= hi; inc *= 2) {
137
+ const p = Math.max(lo, k - inc);
138
+ if (fits(p)) {
139
+ best = p;
140
+ lo = p + 1;
141
+ break;
142
+ }
143
+ hi = p - 1;
144
+ }
145
+ }
146
+ while (lo <= hi) {
147
+ const p = (lo + hi) >> 1;
148
+ if (fits(p)) {
149
+ best = p;
150
+ lo = p + 1;
151
+ } else {
152
+ hi = p - 1;
153
+ }
154
+ }
155
+ }
156
+
157
+ if (best < 0) return ["", 0];
158
+ if (best === len) return [str, len]; // hinted search found the whole string fits
159
+
160
+ // Don't cut between a surrogate pair's halves; a shorter prefix still fits.
161
+ let cut = best;
162
+ if (cut > 0) {
163
+ const code = str.charCodeAt(cut - 1);
164
+ if (code >= 0xd800 && code <= 0xdbff) cut--;
165
+ }
166
+
167
+ return [str.slice(0, cut) + tail, cut];
168
+ }
169
+
170
+ /**
171
+ * Get a function to scale font size proportionally to a box's size. (Deprecated form: passing an initial box as the first parameter is deprecated — it never affected the result — and will be removed in a future version.)
172
+ * @param ratio font-size to box-size ratio. Default is 1.
173
+ * @param byHeight `true` to scale by the box's height, `false` to scale by its width. Default is `true`.
174
+ * @returns a function where input parameter is a box, and returns a font size value (`ratio` multiplied by the box's height or width)
175
+ */
176
+ static fontSizeToBox(
177
+ ratio?: number,
178
+ byHeight?: boolean,
179
+ ): (box: PtLikeIterable) => number;
180
+ /**
181
+ * @deprecated The initial box never affected the result. Use `fontSizeToBox(ratio, byHeight)` instead.
182
+ */
183
+ static fontSizeToBox(
184
+ box: PtLikeIterable,
185
+ ratio?: number,
186
+ byHeight?: boolean,
187
+ ): (box: PtLikeIterable) => number;
188
+ static fontSizeToBox(
189
+ ratio: number | PtLikeIterable = 1,
190
+ byHeight: boolean | number = true,
191
+ legacyByHeight?: boolean,
192
+ ): (box: PtLikeIterable) => number {
193
+ if (typeof ratio !== "number") {
194
+ Util.warn(
195
+ "Typography.fontSizeToBox(box, ratio, byHeight) is deprecated: the initial box never affected the result. Use fontSizeToBox(ratio, byHeight) instead.",
196
+ );
197
+ ratio = typeof byHeight === "number" ? byHeight : 1;
198
+ byHeight = legacyByHeight === undefined ? true : legacyByHeight;
199
+ }
200
+ const r = ratio as number;
201
+ const by = byHeight as boolean;
202
+ return (box: PtLikeIterable): number => {
203
+ const bound = Bound.fromGroup(box);
204
+ return r * (by ? bound.height : bound.width);
205
+ };
206
+ }
207
+
208
+ /**
209
+ * Get a function to scale font size based on a threshold value.
210
+ * @param threshold threshold value. Cannot be 0.
211
+ * @param direction if negative, get a font size <= defaultSize; if positive, get a font size >= defaultSize; Default is 0 which will scale font without min or max limits.
212
+ * @returns a function whose input parameters are a default font size and a value to compare with threshold, and which returns a new font size value
213
+ */
214
+ static fontSizeToThreshold(
215
+ threshold: number,
216
+ direction: number = 0,
217
+ ): (defaultSize: number, val: number) => number {
218
+ if (threshold === 0) {
219
+ throw new Error("fontSizeToThreshold: threshold cannot be 0");
220
+ }
221
+ return function (defaultSize: number, val: number): number {
222
+ const d = (defaultSize * val) / threshold;
223
+ if (direction < 0) return Math.min(d, defaultSize);
224
+ if (direction > 0) return Math.max(d, defaultSize);
225
+ return d;
226
+ };
227
+ }
228
+ }