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/README.md +92 -80
- package/dist/index.d.mts +6208 -1254
- package/dist/index.d.mts.map +1 -0
- package/dist/index.d.ts +6208 -1254
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9314 -10680
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +9266 -10611
- package/dist/index.mjs.map +1 -0
- package/dist/pts.js +9446 -10777
- package/dist/pts.js.map +1 -0
- package/dist/pts.min.js +3 -5
- package/dist/pts.min.js.map +1 -0
- package/package.json +91 -31
- package/src/Canvas.ts +1642 -0
- package/src/Color.ts +1109 -0
- package/src/Create.ts +1547 -0
- package/src/Dom.ts +940 -0
- package/src/Form.ts +312 -0
- package/src/Image.ts +722 -0
- package/src/LinearAlgebra.ts +530 -0
- package/src/Num.ts +1091 -0
- package/src/Op.ts +2127 -0
- package/src/Physics.ts +1233 -0
- package/src/Play.ts +861 -0
- package/src/Pt.ts +1303 -0
- package/src/Space.ts +898 -0
- package/src/Svg.ts +1573 -0
- package/src/Types.ts +301 -0
- package/src/Typography.ts +228 -0
- package/src/UI.ts +757 -0
- package/src/Util.ts +454 -0
- package/src/_module.ts +18 -0
- package/src/_script.ts +94 -0
- package/src/_triangulate.ts +884 -0
- package/src/uheprng.ts +153 -0
package/src/UI.ts
ADDED
|
@@ -0,0 +1,757 @@
|
|
|
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, Group } from "./Pt";
|
|
4
|
+
import { Rectangle, Circle, Polygon } from "./Op";
|
|
5
|
+
import {
|
|
6
|
+
type UIHandler,
|
|
7
|
+
type UIActionEvent,
|
|
8
|
+
type GroupLike,
|
|
9
|
+
type PtLike,
|
|
10
|
+
type PtLikeIterable,
|
|
11
|
+
} from "./Types";
|
|
12
|
+
|
|
13
|
+
/** A hit-test function for a UI shape: given the UI's group, a point, and the UI's states, return whether the point is within the shape. */
|
|
14
|
+
export type UIShapeTest = (
|
|
15
|
+
group: Group,
|
|
16
|
+
pt: PtLike,
|
|
17
|
+
states: { [key: string]: any },
|
|
18
|
+
) => boolean;
|
|
19
|
+
|
|
20
|
+
function _withinSegment(
|
|
21
|
+
a: PtLike,
|
|
22
|
+
b: PtLike,
|
|
23
|
+
pt: PtLike,
|
|
24
|
+
threshold: number,
|
|
25
|
+
): boolean {
|
|
26
|
+
if (threshold < 0) return false;
|
|
27
|
+
const dx = b[0] - a[0];
|
|
28
|
+
const dy = b[1] - a[1];
|
|
29
|
+
const lengthSq = dx * dx + dy * dy;
|
|
30
|
+
const t =
|
|
31
|
+
lengthSq === 0
|
|
32
|
+
? 0
|
|
33
|
+
: Math.max(
|
|
34
|
+
0,
|
|
35
|
+
Math.min(1, ((pt[0] - a[0]) * dx + (pt[1] - a[1]) * dy) / lengthSq),
|
|
36
|
+
);
|
|
37
|
+
const px = pt[0] - a[0] - t * dx;
|
|
38
|
+
const py = pt[1] - a[1] - t * dy;
|
|
39
|
+
return px * px + py * py <= threshold * threshold;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Shape hit tests, keyed by shape name. Extensible via `UI.registerShape`.
|
|
43
|
+
const _shapeTests: { [key: string]: UIShapeTest } = {
|
|
44
|
+
rectangle: (group, pt) => Rectangle.withinBound(group, pt),
|
|
45
|
+
circle: (group, pt) => Circle.withinBound(group, pt),
|
|
46
|
+
polygon: (group, pt) => Polygon.hasIntersectPoint(group, pt),
|
|
47
|
+
line: (group, pt, states) => {
|
|
48
|
+
const threshold = states.lineThreshold ?? 5;
|
|
49
|
+
return (
|
|
50
|
+
group.length >= 2 && _withinSegment(group[0], group[1], pt, threshold)
|
|
51
|
+
);
|
|
52
|
+
},
|
|
53
|
+
polyline: (group, pt, states) => {
|
|
54
|
+
const threshold = states.lineThreshold ?? 5;
|
|
55
|
+
for (let i = 0, len = group.length - 1; i < len; i++) {
|
|
56
|
+
if (_withinSegment(group[i], group[i + 1], pt, threshold)) return true;
|
|
57
|
+
}
|
|
58
|
+
return false;
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* **[Experimental]** A set of string constatns to represent different UI types, for use in [`UI`](#link) instances.
|
|
64
|
+
*/
|
|
65
|
+
export const UIShape = {
|
|
66
|
+
rectangle: "rectangle",
|
|
67
|
+
circle: "circle",
|
|
68
|
+
polygon: "polygon",
|
|
69
|
+
polyline: "polyline",
|
|
70
|
+
line: "line",
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* **[Experimental]** A set of string constants to represent different UI event types.
|
|
75
|
+
*/
|
|
76
|
+
export const UIPointerActions = {
|
|
77
|
+
up: "up",
|
|
78
|
+
down: "down",
|
|
79
|
+
move: "move",
|
|
80
|
+
drag: "drag",
|
|
81
|
+
uidrag: "uidrag",
|
|
82
|
+
drop: "drop",
|
|
83
|
+
uidrop: "uidrop",
|
|
84
|
+
over: "over",
|
|
85
|
+
out: "out",
|
|
86
|
+
enter: "enter",
|
|
87
|
+
leave: "leave",
|
|
88
|
+
click: "click",
|
|
89
|
+
keydown: "keydown",
|
|
90
|
+
keyup: "keyup",
|
|
91
|
+
pointerdown: "pointerdown",
|
|
92
|
+
pointerup: "pointerup",
|
|
93
|
+
contextmenu: "contextmenu",
|
|
94
|
+
all: "all",
|
|
95
|
+
} as const;
|
|
96
|
+
|
|
97
|
+
/** A known pointer, touch, or keyboard action dispatched by a Pts space. */
|
|
98
|
+
export type UIPointerAction =
|
|
99
|
+
(typeof UIPointerActions)[keyof typeof UIPointerActions];
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* **[Experimental]** An abstract class that represents an UI element. It wraps a [`Group`](#link) and supports UI event handling.
|
|
103
|
+
* Extend this class to create custom UI elements.
|
|
104
|
+
*/
|
|
105
|
+
export class UI {
|
|
106
|
+
private _abortCleanup: { [type: string]: Map<number, () => void> } = {};
|
|
107
|
+
_group: Group;
|
|
108
|
+
_shape: string;
|
|
109
|
+
|
|
110
|
+
protected static _counter: number = 0;
|
|
111
|
+
protected _id: string;
|
|
112
|
+
protected _actions: { [type: string]: (UIHandler | null)[] };
|
|
113
|
+
// built-in machinery (UIButton hover, UIDragger drag) registers here, so
|
|
114
|
+
// public `off(type)` cannot remove it along with user handlers
|
|
115
|
+
protected _sysActions: { [type: string]: (UIHandler | null)[] };
|
|
116
|
+
protected _states: { [key: string]: any };
|
|
117
|
+
|
|
118
|
+
protected _holds = new Map<number, string>();
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Create an UI element. You may also create a new UI using one of the static helper like [`UI.fromRectangle`](#link) or [`UI.fromCircle`](#link).
|
|
122
|
+
* @param group a Group or an Iterable<PtLike> that defines the UI's appearance
|
|
123
|
+
* @param shape specifies the shape of the Group
|
|
124
|
+
* @param states optional a state object keep track of custom states for this UI
|
|
125
|
+
* @param id optional id string
|
|
126
|
+
*/
|
|
127
|
+
constructor(
|
|
128
|
+
group: PtLikeIterable,
|
|
129
|
+
shape: string,
|
|
130
|
+
states: { [key: string]: any } = {},
|
|
131
|
+
id?: string,
|
|
132
|
+
) {
|
|
133
|
+
this._group = Group.fromArray(group);
|
|
134
|
+
this._shape = shape;
|
|
135
|
+
this._id = id === undefined ? `ui_${UI._counter++}` : id;
|
|
136
|
+
this._states = states;
|
|
137
|
+
this._actions = {};
|
|
138
|
+
this._sysActions = {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Register a custom shape hit test, or override a built-in one. The shape
|
|
143
|
+
* name can then be used when constructing a UI.
|
|
144
|
+
* @param shape shape name
|
|
145
|
+
* @param fn a function `(group, pt, states) => boolean` that returns whether the point hits the shape
|
|
146
|
+
*/
|
|
147
|
+
static registerShape(shape: string, fn: UIShapeTest): void {
|
|
148
|
+
_shapeTests[shape] = fn;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* A static helper function to create a Rectangle UI.
|
|
153
|
+
* @param group a Group or an Iterable<PtLike> with 2 Pt representing a rectangle
|
|
154
|
+
* @param states optional a state object keep track of custom states for this UI
|
|
155
|
+
* @param id optional id string
|
|
156
|
+
*/
|
|
157
|
+
static fromRectangle(group: PtLikeIterable, states: {}, id?: string): UI {
|
|
158
|
+
return new this(group, UIShape.rectangle, states, id);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* A static helper function to create a Circle UI.
|
|
163
|
+
* @param group a Group or an Iterable<PtLike> with 2 Pt representing a circle
|
|
164
|
+
* @param states optional a state object keep track of custom states for this UI
|
|
165
|
+
* @param id optional id string
|
|
166
|
+
*/
|
|
167
|
+
static fromCircle(group: PtLikeIterable, states: {}, id?: string): UI {
|
|
168
|
+
return new this(group, UIShape.circle, states, id);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* A static helper function to create a Polygon UI.
|
|
173
|
+
* @param group a Group or an Iterable<PtLike> representing a polygon
|
|
174
|
+
* @param states optional a state object keep track of custom states for this UI
|
|
175
|
+
* @param id optional id string
|
|
176
|
+
*/
|
|
177
|
+
static fromPolygon(group: PtLikeIterable, states: {}, id?: string): UI {
|
|
178
|
+
return new this(group, UIShape.polygon, states, id);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* A static helper function to create a new UI based on another UI.
|
|
183
|
+
* @param ui base UI
|
|
184
|
+
* @param states optional a state object keep track of custom states for this UI
|
|
185
|
+
*/
|
|
186
|
+
static fromUI(ui: UI, states?: object, id?: string): UI {
|
|
187
|
+
// copy the source states so the new UI doesn't share mutations
|
|
188
|
+
return new this(ui.group, ui.shape, states || { ...ui._states }, id);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* An unique id of the UI.
|
|
193
|
+
*/
|
|
194
|
+
get id(): string {
|
|
195
|
+
return this._id;
|
|
196
|
+
}
|
|
197
|
+
set id(d: string) {
|
|
198
|
+
this._id = d;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* A group of Pts that defines this UI's shape.
|
|
203
|
+
*/
|
|
204
|
+
get group(): Group {
|
|
205
|
+
return this._group;
|
|
206
|
+
}
|
|
207
|
+
set group(d: Group) {
|
|
208
|
+
this._group = d;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* A string that describes this UI's shape.
|
|
213
|
+
*/
|
|
214
|
+
get shape(): string {
|
|
215
|
+
return this._shape;
|
|
216
|
+
}
|
|
217
|
+
set shape(d: string) {
|
|
218
|
+
this._shape = d;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Get and/or set a specific UI state.
|
|
223
|
+
* @param key state's name
|
|
224
|
+
* @param value optionally set a new value for this state.key
|
|
225
|
+
* @returns If `value` is changed, return this instance. Otherwise, return the value of the specific key.
|
|
226
|
+
*/
|
|
227
|
+
state(key: string, value?: any): any {
|
|
228
|
+
if (!key) return null;
|
|
229
|
+
if (value !== undefined) {
|
|
230
|
+
this._states[key] = value;
|
|
231
|
+
return this;
|
|
232
|
+
}
|
|
233
|
+
return this._states[key];
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Get a specific UI state. Unlike [`UI.state`](#link), this is a plain typed getter.
|
|
238
|
+
* @param key state's name
|
|
239
|
+
*/
|
|
240
|
+
getState<T = any>(key: string): T {
|
|
241
|
+
return this._states[key];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Set a specific UI state. Unlike [`UI.state`](#link), this can also store `undefined`.
|
|
246
|
+
* @param key state's name
|
|
247
|
+
* @param value the value to set
|
|
248
|
+
*/
|
|
249
|
+
setState(key: string, value: any): this {
|
|
250
|
+
this._states[key] = value;
|
|
251
|
+
return this;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Add an event handler. Remember this UI will also need to be tracked for events, via `UI.track` or [`MultiTouchSpace.track`](#link).
|
|
256
|
+
* @param type event type, either one of [`UIPointerActions`](#link) or a custom type
|
|
257
|
+
* @param fn a [`UIHandler`](#link) callback function: `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
258
|
+
* @param options optionally `{ once }` to remove the handler after its first call, and/or `{ signal }` with an [`AbortSignal`](https://developer.mozilla.org/en-US/docs/Web/API/AbortSignal) that removes it on abort (an already-aborted signal registers nothing)
|
|
259
|
+
* @returns an id number that reference to this handler, for use in [`UI.off`](#link), or -1 if nothing was registered
|
|
260
|
+
*/
|
|
261
|
+
on(
|
|
262
|
+
type: UIPointerAction | (string & {}),
|
|
263
|
+
fn: UIHandler,
|
|
264
|
+
options?: { once?: boolean; signal?: AbortSignal },
|
|
265
|
+
): number {
|
|
266
|
+
if (!fn) return -1;
|
|
267
|
+
if (options?.signal?.aborted) return -1;
|
|
268
|
+
if (!this._actions[type]) this._actions[type] = [];
|
|
269
|
+
|
|
270
|
+
let handler = fn;
|
|
271
|
+
let id = -1;
|
|
272
|
+
if (options?.once) {
|
|
273
|
+
handler = (t, p, ty, e) => {
|
|
274
|
+
this.off(type, id);
|
|
275
|
+
fn(t, p, ty, e);
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
id = UI._addHandler(this._actions[type], handler);
|
|
279
|
+
if (options?.signal) {
|
|
280
|
+
const signal = options.signal;
|
|
281
|
+
const abort = () => this.off(type, id);
|
|
282
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
283
|
+
if (!this._abortCleanup[type]) this._abortCleanup[type] = new Map();
|
|
284
|
+
this._abortCleanup[type].set(id, () =>
|
|
285
|
+
signal.removeEventListener("abort", abort),
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
return id;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Remove an event handler.
|
|
293
|
+
* @param type event type
|
|
294
|
+
* @param which an ID number returned by [`UI.on`](#link). If this is not defined, all handlers in this type will be removed (built-in machinery like UIButton's click counting is unaffected). Note that after removal an id is stale — removing it twice may affect a handler that has since reused the slot.
|
|
295
|
+
*/
|
|
296
|
+
off(type: UIPointerAction | (string & {}), which?: number): boolean {
|
|
297
|
+
if (!this._actions[type]) return false;
|
|
298
|
+
if (which === undefined) {
|
|
299
|
+
this._abortCleanup[type]?.forEach((cleanup) => cleanup());
|
|
300
|
+
delete this._abortCleanup[type];
|
|
301
|
+
delete this._actions[type];
|
|
302
|
+
return true;
|
|
303
|
+
} else {
|
|
304
|
+
this._abortCleanup[type]?.get(which)?.();
|
|
305
|
+
this._abortCleanup[type]?.delete(which);
|
|
306
|
+
return UI._removeHandler(this._actions[type], which);
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Listen for UI events and trigger action handlers.
|
|
312
|
+
* @param type an action type. Can be one of UIPointerActions or a custom one.
|
|
313
|
+
* @param p a point to check
|
|
314
|
+
* @param evt a MouseEvent emitted by the browser (See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent))
|
|
315
|
+
*/
|
|
316
|
+
listen(
|
|
317
|
+
type: UIPointerAction | (string & {}),
|
|
318
|
+
p: PtLike,
|
|
319
|
+
evt: UIActionEvent,
|
|
320
|
+
): boolean {
|
|
321
|
+
let fired = false;
|
|
322
|
+
const userActions = this._actions[type];
|
|
323
|
+
const sysActions = this._sysActions[type];
|
|
324
|
+
|
|
325
|
+
if (userActions || sysActions) {
|
|
326
|
+
if (this._within(p) || this._holdsType(type)) {
|
|
327
|
+
if (sysActions) {
|
|
328
|
+
UI._trigger(sysActions, this, p, type, evt);
|
|
329
|
+
fired = true;
|
|
330
|
+
}
|
|
331
|
+
if (userActions) {
|
|
332
|
+
UI._trigger(userActions, this, p, type, evt);
|
|
333
|
+
fired = true;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// "all" handlers observe every event, regardless of position
|
|
339
|
+
if (this._actions["all"]) {
|
|
340
|
+
UI._trigger(this._actions["all"], this, p, type, evt);
|
|
341
|
+
fired = true;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
return fired;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
/** Check whether an action type is currently held, without allocating. */
|
|
348
|
+
private _holdsType(type: string): boolean {
|
|
349
|
+
for (const held of this._holds.values()) {
|
|
350
|
+
if (held === type) return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** Register a built-in handler unaffected by public `off`. */
|
|
356
|
+
protected _sysOn(type: string, fn: UIHandler): number {
|
|
357
|
+
if (!this._sysActions[type]) this._sysActions[type] = [];
|
|
358
|
+
return UI._addHandler(this._sysActions[type], fn);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/** Remove a built-in handler registered with `_sysOn`. */
|
|
362
|
+
protected _sysOff(type: string, which: number): boolean {
|
|
363
|
+
if (!this._sysActions[type]) return false;
|
|
364
|
+
return UI._removeHandler(this._sysActions[type], which);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Continue to keep track of an actions even if it's not within this UI. Useful for hover-leave and drag-outside.
|
|
369
|
+
* @param type a string defined in [`UIPointerActions`](#link)
|
|
370
|
+
*/
|
|
371
|
+
protected hold(type: string): number {
|
|
372
|
+
let newKey = Math.max(0, ...Array.from(this._holds.keys())) + 1;
|
|
373
|
+
this._holds.set(newKey, type);
|
|
374
|
+
return newKey;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Stop keeping track of this action
|
|
379
|
+
* @param key an id returned by the [`UI.hold`](#link) function
|
|
380
|
+
*/
|
|
381
|
+
protected unhold(key?: number): void {
|
|
382
|
+
if (key !== undefined) {
|
|
383
|
+
this._holds.delete(key);
|
|
384
|
+
} else {
|
|
385
|
+
this._holds.clear();
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* A static function to listen for a list of UIs. See also [`UI.listen`](#link).
|
|
391
|
+
* @param uis an array of UI
|
|
392
|
+
* @param type an action type. Can be one of `UIPointerActions` or a custom one.
|
|
393
|
+
* @param p a point to check
|
|
394
|
+
* @param evt a MouseEvent emitted by the browser (See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent))
|
|
395
|
+
*/
|
|
396
|
+
static track(uis: UI[], type: string, p: PtLike, evt: UIActionEvent): void {
|
|
397
|
+
for (let i = 0, len = uis.length; i < len; i++) {
|
|
398
|
+
uis[i].listen(type, p, evt);
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Take a custom render function to render this UI.
|
|
404
|
+
* @param fn a render function
|
|
405
|
+
*/
|
|
406
|
+
render(fn: (group: Group, states: { [key: string]: any }) => void): void {
|
|
407
|
+
fn(this._group, this._states);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Returns a string representation of this UI
|
|
412
|
+
*/
|
|
413
|
+
toString(): string {
|
|
414
|
+
return `UI ${this.group.toString()}`;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Check intersection using the hit test registered for this UI's shape.
|
|
419
|
+
* @param p a point to check
|
|
420
|
+
* @returns a boolean to indicate if the event should be triggered
|
|
421
|
+
*/
|
|
422
|
+
protected _within(p: PtLike): boolean {
|
|
423
|
+
const fn = _shapeTests[this._shape];
|
|
424
|
+
if (!fn) return false;
|
|
425
|
+
return fn(this._group, p, this._states);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Static function to trigger an array of UIHandlers
|
|
430
|
+
*/
|
|
431
|
+
protected static _trigger(
|
|
432
|
+
fns: (UIHandler | null)[],
|
|
433
|
+
target: UI,
|
|
434
|
+
pt: PtLike,
|
|
435
|
+
type: string,
|
|
436
|
+
evt: UIActionEvent,
|
|
437
|
+
) {
|
|
438
|
+
if (fns) {
|
|
439
|
+
for (let i = 0, len = fns.length; i < len; i++) {
|
|
440
|
+
if (fns[i]) fns[i]!(target, pt, type, evt);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
/**
|
|
446
|
+
* Static function to add a new handler to an array store of UIHandlers.
|
|
447
|
+
*/
|
|
448
|
+
protected static _addHandler(
|
|
449
|
+
fns: (UIHandler | null)[],
|
|
450
|
+
fn: UIHandler,
|
|
451
|
+
): number {
|
|
452
|
+
if (!fn) return -1;
|
|
453
|
+
// reuse a removed slot so ids stay stable and the array doesn't grow
|
|
454
|
+
// unboundedly when handlers are added and removed repeatedly
|
|
455
|
+
for (let i = 0, len = fns.length; i < len; i++) {
|
|
456
|
+
if (fns[i] === null) {
|
|
457
|
+
fns[i] = fn;
|
|
458
|
+
return i;
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
fns.push(fn);
|
|
462
|
+
return fns.length - 1;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Static function to remove an existing handler from an array store of UIHandlers.
|
|
467
|
+
* The slot is nulled (not spliced) so other handlers' ids remain valid.
|
|
468
|
+
*/
|
|
469
|
+
protected static _removeHandler(
|
|
470
|
+
fns: (UIHandler | null)[],
|
|
471
|
+
index: number,
|
|
472
|
+
): boolean {
|
|
473
|
+
if (index >= 0 && index < fns.length && fns[index]) {
|
|
474
|
+
fns[index] = null;
|
|
475
|
+
return true;
|
|
476
|
+
}
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* **[Experimental]** A simple button that extends [`UI`](#link) to track clicks and hovers.
|
|
483
|
+
*/
|
|
484
|
+
export class UIButton extends UI {
|
|
485
|
+
private _hoverID: number = -1;
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
* Create an UIButton. A button has 2 states, "clicks" (number) and "hover" (boolean), which you can access through [`UI.state`](#link) function. You may also create a new UIButton using one of the static helper like [`UI.fromRectangle`](#link) or [`UI.fromCircle`](#link).
|
|
489
|
+
* @param group a Group or an Iterable<PtLike> that defines the UI's appearance
|
|
490
|
+
* @param shape specifies the shape of the Group
|
|
491
|
+
* @param states Optional default state object
|
|
492
|
+
* @param id Optional id string
|
|
493
|
+
*/
|
|
494
|
+
constructor(
|
|
495
|
+
group: PtLikeIterable,
|
|
496
|
+
shape: string,
|
|
497
|
+
states: { [key: string]: any } = {},
|
|
498
|
+
id?: string,
|
|
499
|
+
) {
|
|
500
|
+
super(group, shape, states, id);
|
|
501
|
+
|
|
502
|
+
if (states.hover === undefined) this._states["hover"] = false;
|
|
503
|
+
if (states.clicks === undefined) this._states["clicks"] = 0;
|
|
504
|
+
|
|
505
|
+
const UA = UIPointerActions;
|
|
506
|
+
|
|
507
|
+
// listen for clicks when mouse up and increment clicks
|
|
508
|
+
this._sysOn(UA.up, () => {
|
|
509
|
+
this.state("clicks", this._states.clicks + 1);
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
// listen for move events and fire enter and leave events accordingly
|
|
513
|
+
this._sysOn(
|
|
514
|
+
UA.move,
|
|
515
|
+
(target: UI, pt: PtLike, type: string, evt: UIActionEvent) => {
|
|
516
|
+
let hover = this._within(pt);
|
|
517
|
+
|
|
518
|
+
// hover on
|
|
519
|
+
if (hover && !this._states.hover) {
|
|
520
|
+
this.state("hover", true);
|
|
521
|
+
|
|
522
|
+
// enter trigger
|
|
523
|
+
UI._trigger(this._actions[UA.enter], this, pt, UA.enter, evt);
|
|
524
|
+
|
|
525
|
+
// listen for hover off
|
|
526
|
+
let _capID = this.hold(UA.move); // keep hold of second move
|
|
527
|
+
this._hoverID = this._sysOn(
|
|
528
|
+
UA.move,
|
|
529
|
+
(t: UI, p: PtLike, ty: string, e: UIActionEvent) => {
|
|
530
|
+
if (!this._within(p) && !this.state("dragging")) {
|
|
531
|
+
this.state("hover", false);
|
|
532
|
+
// leave trigger, with the current position and event
|
|
533
|
+
UI._trigger(this._actions[UA.leave], this, p, UA.leave, e);
|
|
534
|
+
this._sysOff(UA.move, this._hoverID); // remove second move listener
|
|
535
|
+
this.unhold(_capID); // stop keeping hold of second move
|
|
536
|
+
}
|
|
537
|
+
},
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
},
|
|
541
|
+
);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
/**
|
|
545
|
+
* Add a new click handler. Remember this button will also need to be tracked for events via `UI.track`. If you want to track right clicks, you may also consider [`UIButton.onContextMenu`](#link).
|
|
546
|
+
* @param fn a [`UIHandler`](#link) callback function: `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
547
|
+
* @returns an id number that refers to this handler, for use in [`UIButton.offClick`](#link) or [`UI.off`](#link).
|
|
548
|
+
*/
|
|
549
|
+
onClick(fn: UIHandler): number {
|
|
550
|
+
return this.on(UIPointerActions.up, fn);
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
/**
|
|
554
|
+
* Remove an existing click handler
|
|
555
|
+
* @param id an ID number returned by [`UIButton.onClick`](#link). If this is not defined, all handlers in this type will be removed.
|
|
556
|
+
* @returns a boolean indicating whether the handler was removed successfully
|
|
557
|
+
*/
|
|
558
|
+
offClick(id: number): boolean {
|
|
559
|
+
return this.off(UIPointerActions.up, id);
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Add a new contextmenu handler. `contextmenu` is similar to right click, see the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/API/Element/contextmenu_event). Remember this button will also need to be tracked for events via `UI.track`. Also note that you may need to use `event.preventDefault()` in the callback function to prevent other events from triggering.
|
|
564
|
+
* @param fn a [`UIHandler`](#link) callback function: `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
565
|
+
* @returns an id number that refers to this handler, for use in [`UIButton.offContextMenu`](#link) or [`UI.off`](#link).
|
|
566
|
+
*/
|
|
567
|
+
onContextMenu(fn: UIHandler): number {
|
|
568
|
+
return this.on(UIPointerActions.contextmenu, fn);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Remove an existing contextmenu handler
|
|
573
|
+
* @param id an ID number returned by [`UIButton.onContextMenu`](#link). If this is not defined, all handlers in this type will be removed.
|
|
574
|
+
* @returns a boolean indicating whether the handler was removed successfully
|
|
575
|
+
*/
|
|
576
|
+
offContextMenu(id: number): boolean {
|
|
577
|
+
return this.off(UIPointerActions.contextmenu, id);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Add handlers for hover events. Remember this button will also need to be tracked for events via `UI.track`.
|
|
582
|
+
* @param enter an optional [`UIHandler`](#link) function to handle when pointer enters hover. Eg, `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
583
|
+
* @param leave an optional [`UIHandler`](#link) function to handle when pointer exits hover. Eg, `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
584
|
+
* @returns id numbers that refer to enter/leave handlers, for use in [`UIButton.offHover`](#link) or [`UI.off`](#link).
|
|
585
|
+
*/
|
|
586
|
+
onHover(enter?: UIHandler, leave?: UIHandler): (number | undefined)[] {
|
|
587
|
+
let ids: (number | undefined)[] = [undefined, undefined];
|
|
588
|
+
if (enter) ids[0] = this.on(UIPointerActions.enter, enter);
|
|
589
|
+
if (leave) ids[1] = this.on(UIPointerActions.leave, leave);
|
|
590
|
+
return ids;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Remove handlers for hover events.
|
|
595
|
+
* @param enterID an ID number returned by [`UI.onClick`](#link), or -1 to skip. If this is not defined, all handlers in this type will be removed.
|
|
596
|
+
* @param leaveID an ID number returned by [`UI.onClick`](#link), or -1 to skip. If this is not defined, all handlers in this type will be removed.
|
|
597
|
+
* @returns an array of booleans indicating whether the handlers were removed successfully
|
|
598
|
+
*/
|
|
599
|
+
offHover(enterID?: number, leaveID?: number): boolean[] {
|
|
600
|
+
let s = [false, false];
|
|
601
|
+
if (enterID === undefined || enterID >= 0)
|
|
602
|
+
s[0] = this.off(UIPointerActions.enter, enterID);
|
|
603
|
+
if (leaveID === undefined || leaveID >= 0)
|
|
604
|
+
s[1] = this.off(UIPointerActions.leave, leaveID);
|
|
605
|
+
return s;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
/**
|
|
610
|
+
* [Experimental] A draggable UI that provides handler such as [`UIDragger.onDrag`](#link) and [`UIDragger.onDrop`](#link).
|
|
611
|
+
*/
|
|
612
|
+
export class UIDragger extends UIButton {
|
|
613
|
+
private _draggingID: number = -1;
|
|
614
|
+
private _dragID: number = -1;
|
|
615
|
+
private _moveHoldID: number = -1;
|
|
616
|
+
private _dragHoldID: number = -1;
|
|
617
|
+
private _dropHoldID: number = -1;
|
|
618
|
+
private _upHoldID: number = -1;
|
|
619
|
+
private _outHoldID: number = -1;
|
|
620
|
+
private _lastMoveEvent: UIActionEvent | undefined;
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Create a dragger which has all the states in UIButton, with additional "dragging" (a boolean indicating whether it's currently being dragged) and "offset" (a Pt representing the offset between this UI's position and the pointer's position when dragged) states. (See [`UI.state`](#link)) You may also create a new UIDragger using one of the static helper like [`UI.fromRectangle`](#link) or [`UI.fromCircle`](#link).
|
|
624
|
+
* @param group a Group or an Iterable<PtLike> that defines the UI's appearance
|
|
625
|
+
* @param shape specifies the shape of the Group
|
|
626
|
+
* @param states Optional default state object
|
|
627
|
+
* @param id Optional id string
|
|
628
|
+
*/
|
|
629
|
+
constructor(
|
|
630
|
+
group: PtLikeIterable,
|
|
631
|
+
shape: string,
|
|
632
|
+
states: { [key: string]: any } = {},
|
|
633
|
+
id?: string,
|
|
634
|
+
) {
|
|
635
|
+
super(group, shape, states, id);
|
|
636
|
+
if (states.dragging === undefined) this._states["dragging"] = false;
|
|
637
|
+
if (states.moved === undefined) this._states["moved"] = false;
|
|
638
|
+
if (states.offset === undefined) this._states["offset"] = new Pt();
|
|
639
|
+
|
|
640
|
+
const UA = UIPointerActions;
|
|
641
|
+
|
|
642
|
+
/*
|
|
643
|
+
* Note: drag/drop is implemented in Space.ts, uidrag/uidrop is
|
|
644
|
+
* reimplemented here so that we can keep track of move events happening
|
|
645
|
+
* outside of the UI element. E.g. when the mouse moves faster than the
|
|
646
|
+
* UI refreshes.
|
|
647
|
+
*/
|
|
648
|
+
|
|
649
|
+
// Handle pointer down and begin dragging
|
|
650
|
+
this._sysOn(
|
|
651
|
+
UA.down,
|
|
652
|
+
(target: UI, pt: PtLike, type: string, evt: UIActionEvent) => {
|
|
653
|
+
// begin listening for all events after dragging starts
|
|
654
|
+
if (this._moveHoldID === -1) {
|
|
655
|
+
this.state("dragging", true);
|
|
656
|
+
this.state("offset", new Pt(pt).subtract(target.group[0]));
|
|
657
|
+
this._moveHoldID = this.hold(UA.move); // keep hold of move
|
|
658
|
+
this._dragHoldID = this.hold(UA.drag);
|
|
659
|
+
this._outHoldID = this.hold(UA.out);
|
|
660
|
+
}
|
|
661
|
+
if (this._dropHoldID === -1) {
|
|
662
|
+
this._dropHoldID = this.hold(UA.drop); // keep hold of drop (normal drag and drop)
|
|
663
|
+
}
|
|
664
|
+
if (this._upHoldID === -1) {
|
|
665
|
+
this._upHoldID = this.hold(UA.up); // keep hold of up (cancel dragging if simple click)
|
|
666
|
+
}
|
|
667
|
+
if (this._draggingID === -1) {
|
|
668
|
+
const drag = (t: UI, p: PtLike, ty: string, e: UIActionEvent) => {
|
|
669
|
+
// Touch movement forwards both move and drag for the same event.
|
|
670
|
+
const paired = ty === UA.drag && e === this._lastMoveEvent;
|
|
671
|
+
this._lastMoveEvent = ty === UA.move ? e : undefined;
|
|
672
|
+
if (paired) return;
|
|
673
|
+
if (this.state("dragging")) {
|
|
674
|
+
UI._trigger(this._actions[UA.uidrag], t, p, UA.uidrag, e);
|
|
675
|
+
this.state("moved", true);
|
|
676
|
+
}
|
|
677
|
+
};
|
|
678
|
+
this._draggingID = this._sysOn(UA.move, drag);
|
|
679
|
+
this._dragID = this._sysOn(UA.drag, drag);
|
|
680
|
+
}
|
|
681
|
+
},
|
|
682
|
+
);
|
|
683
|
+
|
|
684
|
+
// Handle pointer drop or up and end dragging
|
|
685
|
+
const endDrag = (
|
|
686
|
+
target: UI,
|
|
687
|
+
pt: PtLike,
|
|
688
|
+
type: string,
|
|
689
|
+
evt: UIActionEvent,
|
|
690
|
+
) => {
|
|
691
|
+
this.state("dragging", false);
|
|
692
|
+
// remove move listener
|
|
693
|
+
this._sysOff(UA.move, this._draggingID);
|
|
694
|
+
this._sysOff(UA.drag, this._dragID);
|
|
695
|
+
this._draggingID = -1;
|
|
696
|
+
this._dragID = -1;
|
|
697
|
+
this._lastMoveEvent = undefined;
|
|
698
|
+
// stop keeping hold of move
|
|
699
|
+
this.unhold(this._moveHoldID);
|
|
700
|
+
this._moveHoldID = -1;
|
|
701
|
+
this.unhold(this._dragHoldID);
|
|
702
|
+
this._dragHoldID = -1;
|
|
703
|
+
this.unhold(this._outHoldID);
|
|
704
|
+
this._outHoldID = -1;
|
|
705
|
+
// stop keeping hold of drop
|
|
706
|
+
this.unhold(this._dropHoldID);
|
|
707
|
+
this._dropHoldID = -1;
|
|
708
|
+
// stop keeping hold of up
|
|
709
|
+
this.unhold(this._upHoldID);
|
|
710
|
+
this._upHoldID = -1;
|
|
711
|
+
// trigger event
|
|
712
|
+
if (this.state("moved")) {
|
|
713
|
+
UI._trigger(this._actions[UA.uidrop], target, pt, UA.uidrop, evt);
|
|
714
|
+
this.state("moved", false);
|
|
715
|
+
}
|
|
716
|
+
};
|
|
717
|
+
this._sysOn(UA.drop, endDrag);
|
|
718
|
+
this._sysOn(UA.up, endDrag);
|
|
719
|
+
this._sysOn(UA.out, endDrag);
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
/**
|
|
723
|
+
* Add a new drag handler. Remember this button will also need to be tracked for events via `UI.track`.
|
|
724
|
+
* @param fn a [`UIHandler`](#link) callback function: `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`. You can access the states "dragging" and "offset" (See [`UI.state`](#link)) in the callback.
|
|
725
|
+
* @returns an id number that refers to this handler, for use in [`UIDragger.offDrag`](#link) or [`UI.off`](#link).
|
|
726
|
+
*/
|
|
727
|
+
onDrag(fn: UIHandler): number {
|
|
728
|
+
return this.on(UIPointerActions.uidrag, fn);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
/**
|
|
732
|
+
* Remove an existing drag handler
|
|
733
|
+
* @param id an ID number returned by [`UIDragger.onDrag`](#link). If this is not defined, all handlers in this type will be removed.
|
|
734
|
+
* @returns a boolean indicating whether the handler was removed successfully
|
|
735
|
+
*/
|
|
736
|
+
offDrag(id: number): boolean {
|
|
737
|
+
return this.off(UIPointerActions.uidrag, id);
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/**
|
|
741
|
+
* Add a new drop handler. Remember this button will also need to be tracked for events via `UI.track`.
|
|
742
|
+
* @param fn a [`UIHandler`](#link) callback function: `fn( target:UI, pt:Pt, type:string, evt:MouseEvent )`
|
|
743
|
+
* @returns an id number that refers to this handler, for use in [`UIDragger.offDrop`](#link) or [`UI.off`](#link).
|
|
744
|
+
*/
|
|
745
|
+
onDrop(fn: UIHandler): number {
|
|
746
|
+
return this.on(UIPointerActions.uidrop, fn);
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Remove an existing drop handler
|
|
751
|
+
* @param id an ID number returned by [`UIDragger.onDrag`](#link). If this is not defined, all handlers in this type will be removed.
|
|
752
|
+
* @returns a boolean indicating whether the handler was removed successfully
|
|
753
|
+
*/
|
|
754
|
+
offDrop(id: number): boolean {
|
|
755
|
+
return this.off(UIPointerActions.uidrop, id);
|
|
756
|
+
}
|
|
757
|
+
}
|