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/Space.ts
ADDED
|
@@ -0,0 +1,898 @@
|
|
|
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 { type Form } from "./Form";
|
|
5
|
+
import { UI, UIPointerActions as UIA } from "./UI";
|
|
6
|
+
import {
|
|
7
|
+
type ITimer,
|
|
8
|
+
type ISpacePlayers,
|
|
9
|
+
type IPlayer,
|
|
10
|
+
type AnimateCallbackFn,
|
|
11
|
+
type TouchPointsKey,
|
|
12
|
+
type UIActionEvent,
|
|
13
|
+
} from "./Types";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Space is an abstract class that represents a general context for expressing Pts. It's extended through subclasses such as [`CanvasSpace`](#link) and [`SVGSpace`](#link). You can also create your own extension of Space.
|
|
17
|
+
* See [Space guide](../guide/Space-0500.html) for details.
|
|
18
|
+
*/
|
|
19
|
+
export abstract class Space {
|
|
20
|
+
id: string = "space";
|
|
21
|
+
protected bound: Bound = new Bound();
|
|
22
|
+
|
|
23
|
+
protected _time: ITimer = { prev: 0, diff: 0, end: -1, min: 0 };
|
|
24
|
+
private _stopAfter = -1; // a stop(t) period awaiting the next frame's clock
|
|
25
|
+
protected players: ISpacePlayers = {};
|
|
26
|
+
protected playerCount = 0;
|
|
27
|
+
protected _ctx: any;
|
|
28
|
+
|
|
29
|
+
private _animID: number = -1;
|
|
30
|
+
private _fromFrame = false;
|
|
31
|
+
|
|
32
|
+
private _pause: boolean = false;
|
|
33
|
+
private _refresh: boolean | undefined = undefined;
|
|
34
|
+
private _renderFunc!: (context: any, self: Space) => null;
|
|
35
|
+
|
|
36
|
+
protected _pointer: Pt = new Pt();
|
|
37
|
+
|
|
38
|
+
protected _isReady = false;
|
|
39
|
+
protected _playing = false;
|
|
40
|
+
private _firstFrame = true;
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Set whether the rendering should be repainted on each frame.
|
|
44
|
+
* @param b a boolean value to set whether to repaint each frame
|
|
45
|
+
*/
|
|
46
|
+
refresh(b: boolean): this {
|
|
47
|
+
this._refresh = b;
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Set a minimum frame time
|
|
53
|
+
* @param ms at least this amount of milliseconds must have elapsed before frame advances
|
|
54
|
+
*/
|
|
55
|
+
minFrameTime(ms: number = 0): this {
|
|
56
|
+
this._time.min = ms;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Add an [`IPlayer`](#link) object or a [`AnimateCallbackFn`](#link) callback function to handle events in this Space. An IPlayer is an object with the following callback functions:
|
|
62
|
+
* - required: `animate: fn( time, ftime, space )`
|
|
63
|
+
* - optional: `start: fn(bound, space)`
|
|
64
|
+
* - optional: `resize: fn( size, event )`
|
|
65
|
+
* - optional: `action: fn( type, x, y, event )`
|
|
66
|
+
* Subclasses of Space may define other callback functions.
|
|
67
|
+
* @param p an [`IPlayer`](#link) object with animate function, or a callback function `fn(time, ftime)`.
|
|
68
|
+
*/
|
|
69
|
+
add(p: IPlayer | AnimateCallbackFn): this {
|
|
70
|
+
const player: IPlayer = typeof p == "function" ? { animate: p } : p;
|
|
71
|
+
|
|
72
|
+
const k = this.playerCount++;
|
|
73
|
+
const pid = player.animateID || this.id + k;
|
|
74
|
+
|
|
75
|
+
this.players[pid] = player;
|
|
76
|
+
player.animateID = pid;
|
|
77
|
+
// resize callbacks receive the live bound (as they do on space resize);
|
|
78
|
+
// treat it as read-only
|
|
79
|
+
if (player.resize && this.bound.inited) player.resize(this.bound);
|
|
80
|
+
|
|
81
|
+
// if _refresh is not set, set it to true
|
|
82
|
+
if (this._refresh === undefined) this._refresh = true;
|
|
83
|
+
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Remove a player from this Space.
|
|
89
|
+
* @param player an IPlayer that has an `animateID` property
|
|
90
|
+
*/
|
|
91
|
+
remove(player: IPlayer): this {
|
|
92
|
+
delete this.players[player.animateID!];
|
|
93
|
+
return this;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Remove all players from this Space.
|
|
98
|
+
*/
|
|
99
|
+
removeAll(): this {
|
|
100
|
+
this.players = {};
|
|
101
|
+
return this;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Main play loop. This implements `window.requestAnimationFrame` and calls it recursively.
|
|
106
|
+
* You may override this `play()` function to implement your own animation loop.
|
|
107
|
+
* @param time current time
|
|
108
|
+
*/
|
|
109
|
+
play(time = 0): this {
|
|
110
|
+
// A real RAF can have timestamp 0 too. Consume its marker before invoking
|
|
111
|
+
// players so a nested manual play() still cannot start a second loop.
|
|
112
|
+
const fromFrame = this._fromFrame;
|
|
113
|
+
this._fromFrame = false;
|
|
114
|
+
// make sure only one play loop is active: an external play() while the
|
|
115
|
+
// loop runs is a no-op...
|
|
116
|
+
if (time === 0 && this._animID !== -1 && !fromFrame) {
|
|
117
|
+
return this;
|
|
118
|
+
}
|
|
119
|
+
// ...and any other call (a frame callback, or a manual play(t)) replaces
|
|
120
|
+
// the pending frame instead of stacking a parallel chain — cancelling an
|
|
121
|
+
// already-fired frame id is a spec-defined no-op
|
|
122
|
+
if (this._animID !== -1) cancelAnimationFrame(this._animID);
|
|
123
|
+
this._animID = requestAnimationFrame((frameTime) => {
|
|
124
|
+
this._fromFrame = true;
|
|
125
|
+
try {
|
|
126
|
+
this.play(frameTime);
|
|
127
|
+
} finally {
|
|
128
|
+
this._fromFrame = false;
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (this._pause) {
|
|
133
|
+
// track time while paused so resuming doesn't deliver the entire
|
|
134
|
+
// pause duration as one frame's ftime
|
|
135
|
+
this._time.prev = time;
|
|
136
|
+
return this;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (this._firstFrame) {
|
|
140
|
+
// the first frame after a fresh start renders immediately with no
|
|
141
|
+
// elapsed time — `time` is an arbitrary clock timestamp, not a delta
|
|
142
|
+
// play() draws synchronously at synthetic time 0. Keep initialization
|
|
143
|
+
// pending until RAF supplies its first real clock timestamp.
|
|
144
|
+
this._firstFrame = time === 0 && !fromFrame;
|
|
145
|
+
this._time.diff = 0;
|
|
146
|
+
this._time.prev = time;
|
|
147
|
+
} else {
|
|
148
|
+
const diff = time - this._time.prev;
|
|
149
|
+
// accumulate until the minimum frame time is reached
|
|
150
|
+
if (diff < this._time.min) return this;
|
|
151
|
+
this._time.diff = diff;
|
|
152
|
+
this._time.prev = time;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
try {
|
|
156
|
+
this.playItems(time);
|
|
157
|
+
} catch (err) {
|
|
158
|
+
cancelAnimationFrame(this._animID);
|
|
159
|
+
this._animID = -1;
|
|
160
|
+
this._playing = false;
|
|
161
|
+
this._firstFrame = true;
|
|
162
|
+
throw err;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Replay the animation after [`Space.stop`](#link). This resets the end-time counter.
|
|
170
|
+
* You may also use [`Space.pause`](#link) and [`resume`](#link) for temporary pause.
|
|
171
|
+
*/
|
|
172
|
+
replay() {
|
|
173
|
+
this._time.end = -1;
|
|
174
|
+
this.play();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Main animate function. This calls all the items to perform.
|
|
179
|
+
* @param time current time
|
|
180
|
+
*/
|
|
181
|
+
protected playItems(time: number) {
|
|
182
|
+
this._playing = true;
|
|
183
|
+
|
|
184
|
+
// clear before draw if refresh is true
|
|
185
|
+
if (this._refresh) this.clear();
|
|
186
|
+
|
|
187
|
+
// animate all players
|
|
188
|
+
if (this._isReady) {
|
|
189
|
+
for (const k in this.players) {
|
|
190
|
+
if (this.players[k].animate)
|
|
191
|
+
this.players[k].animate(time, this._time.diff, this);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// resolve a stop period against the frame clock
|
|
196
|
+
if (this._stopAfter >= 0) {
|
|
197
|
+
this._time.end = time + this._stopAfter;
|
|
198
|
+
this._stopAfter = -1;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// stop if time ended
|
|
202
|
+
if (this._time.end >= 0 && time > this._time.end) {
|
|
203
|
+
cancelAnimationFrame(this._animID);
|
|
204
|
+
this._animID = -1;
|
|
205
|
+
this._playing = false;
|
|
206
|
+
this._firstFrame = true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Pause the animation.
|
|
212
|
+
* @param toggle a boolean value to set if this function call should be a toggle (between pause and resume)
|
|
213
|
+
*/
|
|
214
|
+
pause(toggle = false): this {
|
|
215
|
+
this._pause = toggle ? !this._pause : true;
|
|
216
|
+
return this;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Resume the pause animation.
|
|
221
|
+
*/
|
|
222
|
+
resume(): this {
|
|
223
|
+
this._pause = false;
|
|
224
|
+
return this;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Specify when the animation should stop: immediately, after a time period, or never stops.
|
|
229
|
+
* After stopping, use [`Space.replay`](#link) to play again.
|
|
230
|
+
* @param t a value in millisecond to specify a time period to play before stopping, or `-1` to play forever, or `0` to end immediately. Default is 0 which will stop the animation immediately.
|
|
231
|
+
*/
|
|
232
|
+
stop(t = 0): this {
|
|
233
|
+
// a period is measured from the next frame's clock, since frame
|
|
234
|
+
// timestamps are absolute and not comparable with a duration
|
|
235
|
+
this._stopAfter = t > 0 ? t : -1;
|
|
236
|
+
this._time.end = t > 0 ? -1 : t;
|
|
237
|
+
return this;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Cancel the active animation frame immediately. Subclasses should call this
|
|
242
|
+
* when they dispose browser resources instead of waiting for `stop()` to be
|
|
243
|
+
* observed by the next frame.
|
|
244
|
+
*/
|
|
245
|
+
protected _cancelAnimation(): this {
|
|
246
|
+
if (this._animID !== -1) cancelAnimationFrame(this._animID);
|
|
247
|
+
this._animID = -1;
|
|
248
|
+
this._playing = false;
|
|
249
|
+
this._firstFrame = true;
|
|
250
|
+
return this;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Play animation loop once. Optionally set a `duration` time to play for that specific duration.
|
|
255
|
+
* @param duration a value in millisecond to specify a time period to play before stopping, or `-1` to play forever
|
|
256
|
+
*/
|
|
257
|
+
playOnce(duration = 0): this {
|
|
258
|
+
this.play();
|
|
259
|
+
this.stop(duration);
|
|
260
|
+
return this;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Custom rendering.
|
|
265
|
+
* @param context rendering context
|
|
266
|
+
*/
|
|
267
|
+
protected render(context: any): this {
|
|
268
|
+
if (this._renderFunc) this._renderFunc(context, this);
|
|
269
|
+
return this;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Set a custom rendering function `fn(graphics_context, canvas_space)` if needed.
|
|
274
|
+
*/
|
|
275
|
+
set customRendering(f: (context: any, self: Space) => null) {
|
|
276
|
+
this._renderFunc = f;
|
|
277
|
+
}
|
|
278
|
+
get customRendering(): (context: any, self: Space) => null {
|
|
279
|
+
return this._renderFunc;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Indicate whether the animation is playing.
|
|
284
|
+
*/
|
|
285
|
+
get isPlaying(): boolean {
|
|
286
|
+
return this._playing;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* The outer bounding box which includes its positions.
|
|
291
|
+
*/
|
|
292
|
+
get outerBound(): Bound {
|
|
293
|
+
return this.bound.clone();
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* The inner bounding box of the space, excluding its positions.
|
|
298
|
+
*/
|
|
299
|
+
public get innerBound(): Bound {
|
|
300
|
+
return new Bound(Pt.make(this.size.length, 0), this.size.clone());
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* The size of this space's bounding box.
|
|
305
|
+
*/
|
|
306
|
+
get size(): Pt {
|
|
307
|
+
return this.bound.size.clone();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The center of this space's bounding box.
|
|
312
|
+
*/
|
|
313
|
+
get center(): Pt {
|
|
314
|
+
return this.size.divide(2);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* The width of this space's bounding box.
|
|
319
|
+
*/
|
|
320
|
+
get width(): number {
|
|
321
|
+
return this.bound.width;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* The height of this space's bounding box.
|
|
326
|
+
*/
|
|
327
|
+
get height(): number {
|
|
328
|
+
return this.bound.height;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Resize the space. To be implemented in subclasses.
|
|
333
|
+
* @param b a Bound representing the position and size of the space
|
|
334
|
+
* @param evt event
|
|
335
|
+
*/
|
|
336
|
+
abstract resize(b: Bound, evt?: Event | null): this;
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* clear all contents in the space. To be implemented in subclasses.
|
|
340
|
+
*/
|
|
341
|
+
abstract clear(): this;
|
|
342
|
+
|
|
343
|
+
/**
|
|
344
|
+
* Get a default form for drawing in this space. To be implemented in subclasses.
|
|
345
|
+
*/
|
|
346
|
+
abstract getForm(): Form;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
/**
|
|
350
|
+
* MultiTouchSpace is an abstract class that extends [`Space`](#link) to support user interactions via touch events.
|
|
351
|
+
* It's extended through subclasses such as [`CanvasSpace`](#link) and [`SVGSpace`](#link).
|
|
352
|
+
*/
|
|
353
|
+
export abstract class MultiTouchSpace extends Space {
|
|
354
|
+
// track mouse dragging
|
|
355
|
+
protected _pressed = false;
|
|
356
|
+
protected _dragged = false;
|
|
357
|
+
|
|
358
|
+
protected _hasMouse = false;
|
|
359
|
+
protected _hasTouch = false;
|
|
360
|
+
protected _hasKeyboard = false;
|
|
361
|
+
|
|
362
|
+
private _mouseTarget: Element | undefined;
|
|
363
|
+
private _touchTarget: Element | undefined;
|
|
364
|
+
private _keyboardTarget: EventTarget | undefined;
|
|
365
|
+
private _touchPassive = false;
|
|
366
|
+
|
|
367
|
+
private readonly _mouseDownBind = this._mouseDown.bind(
|
|
368
|
+
this,
|
|
369
|
+
) as unknown as EventListener;
|
|
370
|
+
private readonly _mouseUpBind = this._mouseUp.bind(
|
|
371
|
+
this,
|
|
372
|
+
) as unknown as EventListener;
|
|
373
|
+
private readonly _mouseOverBind = this._mouseOver.bind(
|
|
374
|
+
this,
|
|
375
|
+
) as unknown as EventListener;
|
|
376
|
+
private readonly _mouseOutBind = this._mouseOut.bind(
|
|
377
|
+
this,
|
|
378
|
+
) as unknown as EventListener;
|
|
379
|
+
private readonly _mouseMoveBind = this._mouseMove.bind(
|
|
380
|
+
this,
|
|
381
|
+
) as unknown as EventListener;
|
|
382
|
+
private readonly _mouseClickBind = this._mouseClick.bind(
|
|
383
|
+
this,
|
|
384
|
+
) as unknown as EventListener;
|
|
385
|
+
private readonly _contextMenuBind = this._contextMenu.bind(
|
|
386
|
+
this,
|
|
387
|
+
) as unknown as EventListener;
|
|
388
|
+
private readonly _touchStartBind = this._touchStart.bind(
|
|
389
|
+
this,
|
|
390
|
+
) as unknown as EventListener;
|
|
391
|
+
private readonly _touchMoveBind = this._touchMove.bind(
|
|
392
|
+
this,
|
|
393
|
+
) as unknown as EventListener;
|
|
394
|
+
private readonly _keyDownBind = this._keyDown.bind(
|
|
395
|
+
this,
|
|
396
|
+
) as unknown as EventListener;
|
|
397
|
+
private readonly _keyUpBind = this._keyUp.bind(
|
|
398
|
+
this,
|
|
399
|
+
) as unknown as EventListener;
|
|
400
|
+
|
|
401
|
+
// accept subclasses that implements addEventListener, removeEventListener, dispatchEvent
|
|
402
|
+
protected _canvas!: EventTarget;
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* Get the mouse or touch pointer that stores the last action.
|
|
406
|
+
*/
|
|
407
|
+
public get pointer(): Pt {
|
|
408
|
+
const p = this._pointer.clone();
|
|
409
|
+
p.id = this._pointer.id;
|
|
410
|
+
return p;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Bind event listener in canvas element. You can also use [`MultiTouchSpace.bindMouse`](#link) or [`MultiTouchSpace.bindTouch`](#link) to bind mouse or touch events conveniently.
|
|
415
|
+
* @param evt an event string such as "mousedown"
|
|
416
|
+
* @param callback callback function for this event
|
|
417
|
+
* @param options options for [addEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener).
|
|
418
|
+
* @param customTarget an optional event target to use instead of the canvas element
|
|
419
|
+
*/
|
|
420
|
+
bindCanvas(
|
|
421
|
+
evt: string,
|
|
422
|
+
callback: EventListener,
|
|
423
|
+
options: any = {},
|
|
424
|
+
customTarget?: Element,
|
|
425
|
+
) {
|
|
426
|
+
const target = customTarget ? customTarget : this._canvas;
|
|
427
|
+
target.addEventListener(evt, callback, options);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Unbind a callback from the event listener.
|
|
432
|
+
* @param evt an event string such as "mousedown"
|
|
433
|
+
* @param callback callback function to unbind
|
|
434
|
+
* @param options options for [removeEventListener](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener). This should match the options set in bindCanvas.
|
|
435
|
+
* @param customTarget If customTarget is set in bindCanvas, you'll need to pass the same instance here to unbind
|
|
436
|
+
*/
|
|
437
|
+
unbindCanvas(
|
|
438
|
+
evt: string,
|
|
439
|
+
callback: EventListener,
|
|
440
|
+
options: any = {},
|
|
441
|
+
customTarget?: Element,
|
|
442
|
+
) {
|
|
443
|
+
const target = customTarget ? customTarget : this._canvas;
|
|
444
|
+
target.removeEventListener(evt, callback, options);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
bindDoc(evt: string, callback: EventListener, options: any = {}) {
|
|
448
|
+
if (typeof document !== "undefined") {
|
|
449
|
+
document.addEventListener(evt, callback, options);
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
unbindDoc(evt: string, callback: EventListener, options: any = {}) {
|
|
454
|
+
if (typeof document !== "undefined") {
|
|
455
|
+
document.removeEventListener(evt, callback, options);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/**
|
|
460
|
+
* A convenient method to bind (or unbind) all mouse events in canvas element.
|
|
461
|
+
* All [`IPlayer`](#link) objects added to this space that implement an `action` callback property will receive mouse event callbacks.
|
|
462
|
+
* Mouse action names are defined by [`UIPointerActions`](#link), including "up", "down", "move", "drag", "drop", "over", "out", "click", "pointerdown", "pointerup", and "contextmenu".
|
|
463
|
+
* @param bind a boolean value to bind mouse events if set to `true`. If `false`, all mouse events will be unbound. Default is true.
|
|
464
|
+
* @param customTarget an optional event target to use instead of the canvas element
|
|
465
|
+
* @see [`Space.add`](#link)
|
|
466
|
+
*/
|
|
467
|
+
bindMouse(bind: boolean = true, customTarget?: Element): this {
|
|
468
|
+
if (bind) {
|
|
469
|
+
if (this._hasMouse) {
|
|
470
|
+
if (this._mouseTarget === customTarget) return this;
|
|
471
|
+
this.bindMouse(false);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
this._mouseTarget = customTarget;
|
|
475
|
+
this.bindCanvas("pointerdown", this._mouseDownBind, {}, customTarget);
|
|
476
|
+
this.bindCanvas("pointerup", this._mouseUpBind, {}, customTarget);
|
|
477
|
+
this.bindCanvas("pointerover", this._mouseOverBind, {}, customTarget);
|
|
478
|
+
this.bindCanvas("pointerout", this._mouseOutBind, {}, customTarget);
|
|
479
|
+
this.bindCanvas("pointercancel", this._mouseOutBind, {}, customTarget);
|
|
480
|
+
this.bindCanvas("pointermove", this._mouseMoveBind, {}, customTarget);
|
|
481
|
+
this.bindCanvas("click", this._mouseClickBind, {}, customTarget);
|
|
482
|
+
this.bindCanvas("contextmenu", this._contextMenuBind, {}, customTarget);
|
|
483
|
+
this._hasMouse = true;
|
|
484
|
+
} else if (this._hasMouse) {
|
|
485
|
+
const target = this._mouseTarget;
|
|
486
|
+
this.unbindCanvas("pointerdown", this._mouseDownBind, {}, target);
|
|
487
|
+
this.unbindCanvas("pointerup", this._mouseUpBind, {}, target);
|
|
488
|
+
this.unbindCanvas("pointerover", this._mouseOverBind, {}, target);
|
|
489
|
+
this.unbindCanvas("pointerout", this._mouseOutBind, {}, target);
|
|
490
|
+
this.unbindCanvas("pointercancel", this._mouseOutBind, {}, target);
|
|
491
|
+
this.unbindCanvas("pointermove", this._mouseMoveBind, {}, target);
|
|
492
|
+
this.unbindCanvas("click", this._mouseClickBind, {}, target);
|
|
493
|
+
this.unbindCanvas("contextmenu", this._contextMenuBind, {}, target);
|
|
494
|
+
this._hasMouse = false;
|
|
495
|
+
this._mouseTarget = undefined;
|
|
496
|
+
}
|
|
497
|
+
return this;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* A convenient method to bind (or unbind) all touch events in canvas element.
|
|
502
|
+
* All [`IPlayer`](#link) objects added to this space that implement an `action` callback property will receive touch event callbacks.
|
|
503
|
+
* Touch action names are defined by [`UIPointerActions`](#link), including "up", "down", "move", "drag", "drop", "over", and "out".
|
|
504
|
+
* @param bind a boolean value to bind touch events if set to `true`. If `false`, all touch events will be unbound. Default is true.
|
|
505
|
+
* @param passive a boolean value to set passive mode, ie, it won't block scrolling. Default is false.
|
|
506
|
+
* @param customTarget an optional event target to use instead of the canvas element
|
|
507
|
+
* @see [`Space.add`](#link)
|
|
508
|
+
*/
|
|
509
|
+
bindTouch(
|
|
510
|
+
bind: boolean = true,
|
|
511
|
+
passive: boolean = false,
|
|
512
|
+
customTarget?: Element,
|
|
513
|
+
): this {
|
|
514
|
+
if (bind) {
|
|
515
|
+
if (this._hasTouch) {
|
|
516
|
+
if (
|
|
517
|
+
this._touchTarget === customTarget &&
|
|
518
|
+
this._touchPassive === passive
|
|
519
|
+
)
|
|
520
|
+
return this;
|
|
521
|
+
this.bindTouch(false);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
this._touchTarget = customTarget;
|
|
525
|
+
this._touchPassive = passive;
|
|
526
|
+
this.bindCanvas(
|
|
527
|
+
"touchstart",
|
|
528
|
+
this._touchStartBind,
|
|
529
|
+
{ passive: passive },
|
|
530
|
+
customTarget,
|
|
531
|
+
);
|
|
532
|
+
this.bindCanvas("touchend", this._mouseUpBind, {}, customTarget);
|
|
533
|
+
this.bindCanvas(
|
|
534
|
+
"touchmove",
|
|
535
|
+
this._touchMoveBind,
|
|
536
|
+
{ passive: passive },
|
|
537
|
+
customTarget,
|
|
538
|
+
);
|
|
539
|
+
this.bindCanvas("touchcancel", this._mouseOutBind, {}, customTarget);
|
|
540
|
+
this._hasTouch = true;
|
|
541
|
+
} else if (this._hasTouch) {
|
|
542
|
+
const target = this._touchTarget;
|
|
543
|
+
const options = { passive: this._touchPassive };
|
|
544
|
+
this.unbindCanvas("touchstart", this._touchStartBind, options, target);
|
|
545
|
+
this.unbindCanvas("touchend", this._mouseUpBind, {}, target);
|
|
546
|
+
this.unbindCanvas("touchmove", this._touchMoveBind, options, target);
|
|
547
|
+
this.unbindCanvas("touchcancel", this._mouseOutBind, {}, target);
|
|
548
|
+
this._hasTouch = false;
|
|
549
|
+
this._touchTarget = undefined;
|
|
550
|
+
}
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Bind or unbind keyboard events. Events are attached to `document` by
|
|
556
|
+
* default, or to `customTarget` when one is provided.
|
|
557
|
+
*/
|
|
558
|
+
bindKeyboard(bind: boolean = true, customTarget?: EventTarget): this {
|
|
559
|
+
if (bind) {
|
|
560
|
+
const target = customTarget || document;
|
|
561
|
+
if (this._hasKeyboard) {
|
|
562
|
+
if (this._keyboardTarget === target) return this;
|
|
563
|
+
this.bindKeyboard(false);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
target.addEventListener("keydown", this._keyDownBind, {});
|
|
567
|
+
target.addEventListener("keyup", this._keyUpBind, {});
|
|
568
|
+
this._keyboardTarget = target;
|
|
569
|
+
this._hasKeyboard = true;
|
|
570
|
+
} else if (this._hasKeyboard) {
|
|
571
|
+
this._keyboardTarget!.removeEventListener(
|
|
572
|
+
"keydown",
|
|
573
|
+
this._keyDownBind,
|
|
574
|
+
{},
|
|
575
|
+
);
|
|
576
|
+
this._keyboardTarget!.removeEventListener("keyup", this._keyUpBind, {});
|
|
577
|
+
this._keyboardTarget = undefined;
|
|
578
|
+
this._hasKeyboard = false;
|
|
579
|
+
}
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/** Unbind all pointer, touch, and keyboard listeners owned by this space. */
|
|
584
|
+
protected _unbindAll(): this {
|
|
585
|
+
this.bindMouse(false);
|
|
586
|
+
this.bindTouch(false);
|
|
587
|
+
this.bindKeyboard(false);
|
|
588
|
+
return this;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
private _trackedUIs: UI[] = [];
|
|
592
|
+
private _uiPlayer: IPlayer | null = null;
|
|
593
|
+
|
|
594
|
+
/** Remove all players and the UI registrations forwarded by them. */
|
|
595
|
+
removeAll(): this {
|
|
596
|
+
this.untrack();
|
|
597
|
+
this._uiPlayer = null;
|
|
598
|
+
return super.removeAll();
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Track one or more [`UI`](#link) elements: every pointer, touch, and keyboard
|
|
603
|
+
* action dispatched by this space is forwarded to them via [`UI.track`](#link),
|
|
604
|
+
* so no manual `action` wiring is needed. Remember to also bind the events,
|
|
605
|
+
* eg via [`MultiTouchSpace.bindMouse`](#link). Keyboard actions are forwarded
|
|
606
|
+
* too (their x/y carry the shift/alt flags, as the space dispatches them).
|
|
607
|
+
* @param uis a UI, or an array of UIs
|
|
608
|
+
*/
|
|
609
|
+
track(uis: UI | UI[]): this {
|
|
610
|
+
const list = Array.isArray(uis) ? uis : [uis];
|
|
611
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
612
|
+
if (this._trackedUIs.indexOf(list[i]) < 0) this._trackedUIs.push(list[i]);
|
|
613
|
+
}
|
|
614
|
+
if (!this._uiPlayer) {
|
|
615
|
+
this._uiPlayer = {
|
|
616
|
+
animate: () => {},
|
|
617
|
+
action: (type: string, px: number, py: number, evt: Event) => {
|
|
618
|
+
UI.track(this._trackedUIs, type, new Pt(px, py), evt as MouseEvent);
|
|
619
|
+
},
|
|
620
|
+
};
|
|
621
|
+
this.add(this._uiPlayer);
|
|
622
|
+
}
|
|
623
|
+
return this;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Stop tracking one or more [`UI`](#link) elements added via [`MultiTouchSpace.track`](#link).
|
|
628
|
+
* @param uis a UI or an array of UIs to remove from tracking, or omit to stop tracking all
|
|
629
|
+
*/
|
|
630
|
+
untrack(uis?: UI | UI[]): this {
|
|
631
|
+
const list =
|
|
632
|
+
uis === undefined
|
|
633
|
+
? this._trackedUIs.slice()
|
|
634
|
+
: Array.isArray(uis)
|
|
635
|
+
? uis
|
|
636
|
+
: [uis];
|
|
637
|
+
for (let i = 0, len = list.length; i < len; i++) {
|
|
638
|
+
const at = this._trackedUIs.indexOf(list[i]);
|
|
639
|
+
if (at < 0) continue;
|
|
640
|
+
this._trackedUIs.splice(at, 1);
|
|
641
|
+
// a dragger removed mid-drag would otherwise stay "dragging" and
|
|
642
|
+
// resume on its next move without a press; end the drag as a drop
|
|
643
|
+
if (list[i].state("dragging")) {
|
|
644
|
+
list[i].listen(
|
|
645
|
+
UIA.drop,
|
|
646
|
+
this._pointer,
|
|
647
|
+
new Event("pointercancel") as unknown as UIActionEvent,
|
|
648
|
+
);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
return this;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Input is relative to the rendered element, not a cached container position
|
|
655
|
+
// or the event target (which may be an overlay). Read once per event.
|
|
656
|
+
private _inputTransform(): [number, number, number, number] {
|
|
657
|
+
if (typeof Element !== "undefined" && this._canvas instanceof Element) {
|
|
658
|
+
const rect = this._canvas.getBoundingClientRect();
|
|
659
|
+
return [
|
|
660
|
+
rect.left,
|
|
661
|
+
rect.top,
|
|
662
|
+
rect.width ? this.width / rect.width : 1,
|
|
663
|
+
rect.height ? this.height / rect.height : 1,
|
|
664
|
+
];
|
|
665
|
+
}
|
|
666
|
+
// Non-DOM subclasses may supply their own EventTarget and explicit bounds.
|
|
667
|
+
return [
|
|
668
|
+
this.bound.topLeft.x -
|
|
669
|
+
(typeof window === "undefined" ? 0 : window.scrollX),
|
|
670
|
+
this.bound.topLeft.y -
|
|
671
|
+
(typeof window === "undefined" ? 0 : window.scrollY),
|
|
672
|
+
1,
|
|
673
|
+
1,
|
|
674
|
+
];
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
/**
|
|
678
|
+
* A convenient method to convert the touch points in a touch event to an array of Pts.
|
|
679
|
+
* @param evt a touch event which contains touches, changedTouches, and targetTouches list
|
|
680
|
+
* @param which a string to select a touches list: "touches", "changedTouches", or "targetTouches". Default is "touches"
|
|
681
|
+
* @return an array of Pt, whose origin position (0,0) is offset to the top-left of this space
|
|
682
|
+
*/
|
|
683
|
+
touchesToPoints(evt: TouchEvent, which: TouchPointsKey = "touches"): Pt[] {
|
|
684
|
+
if (!evt || !evt[which]) return [];
|
|
685
|
+
const ts = [];
|
|
686
|
+
const [left, top, scaleX, scaleY] = this._inputTransform();
|
|
687
|
+
for (let i = 0; i < evt[which].length; i++) {
|
|
688
|
+
const t = evt[which].item(i)!;
|
|
689
|
+
ts.push(new Pt((t.clientX - left) * scaleX, (t.clientY - top) * scaleY));
|
|
690
|
+
}
|
|
691
|
+
return ts;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
/**
|
|
695
|
+
* Go through all the added [`IPlayer`](#link) objects and call its `action` callback function.
|
|
696
|
+
* @param type a [`UIPointerActions`](#link) constant or custom action string
|
|
697
|
+
* @param evt mouse or touch event
|
|
698
|
+
* @see [`Space.add`](#link)
|
|
699
|
+
*/
|
|
700
|
+
protected _mouseAction(
|
|
701
|
+
type: string,
|
|
702
|
+
evt: MouseEvent | TouchEvent | PointerEvent,
|
|
703
|
+
) {
|
|
704
|
+
if (!this.isPlaying) return;
|
|
705
|
+
|
|
706
|
+
// compute the event position once — not per player, and independent of
|
|
707
|
+
// whether any player is registered (the pointer must track regardless)
|
|
708
|
+
const [left, top, scaleX, scaleY] = this._inputTransform();
|
|
709
|
+
let px = 0,
|
|
710
|
+
py = 0;
|
|
711
|
+
|
|
712
|
+
if (evt instanceof MouseEvent) {
|
|
713
|
+
px = (evt.clientX - left) * scaleX;
|
|
714
|
+
py = (evt.clientY - top) * scaleY;
|
|
715
|
+
} else {
|
|
716
|
+
const touch =
|
|
717
|
+
evt.changedTouches && evt.changedTouches.length > 0
|
|
718
|
+
? evt.changedTouches.item(0)
|
|
719
|
+
: null;
|
|
720
|
+
if (touch) {
|
|
721
|
+
px = (touch.clientX - left) * scaleX;
|
|
722
|
+
py = (touch.clientY - top) * scaleY;
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
if (type) {
|
|
727
|
+
this._pointer.to(px, py);
|
|
728
|
+
this._pointer.id = type;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
for (const k in this.players) {
|
|
732
|
+
if (this.players.hasOwnProperty(k)) {
|
|
733
|
+
const v = this.players[k];
|
|
734
|
+
if (v.action) v.action(type, px, py, evt);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
// Ignore the duplicate pointer stream only when this event also reaches our
|
|
740
|
+
// touch listener. Mouse and touch bindings may use separate custom targets.
|
|
741
|
+
private _isTouchHandled(evt: PointerEvent | TouchEvent): boolean {
|
|
742
|
+
return (
|
|
743
|
+
"pointerType" in evt &&
|
|
744
|
+
evt.pointerType === "touch" &&
|
|
745
|
+
this._hasTouch &&
|
|
746
|
+
evt.composedPath().includes(this._touchTarget || this._canvas)
|
|
747
|
+
);
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* MouseDown handler.
|
|
752
|
+
* @param evt
|
|
753
|
+
*/
|
|
754
|
+
protected _mouseDown(evt: PointerEvent) {
|
|
755
|
+
if (this._isTouchHandled(evt)) return false;
|
|
756
|
+
this._mouseAction(UIA.down, evt);
|
|
757
|
+
this._mouseAction(UIA.pointerdown, evt);
|
|
758
|
+
this._pressed = true;
|
|
759
|
+
if (evt.target instanceof Element) {
|
|
760
|
+
try {
|
|
761
|
+
evt.target.setPointerCapture(evt.pointerId);
|
|
762
|
+
} catch {
|
|
763
|
+
// a synthetic or re-dispatched event has no active pointer to capture;
|
|
764
|
+
// dragging still works through the element's own move/up listeners
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
return false;
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
/**
|
|
771
|
+
* MouseUp handler.
|
|
772
|
+
* @param evt
|
|
773
|
+
*/
|
|
774
|
+
protected _mouseUp(evt: PointerEvent | TouchEvent) {
|
|
775
|
+
if (this._isTouchHandled(evt)) return false;
|
|
776
|
+
this._mouseAction(UIA.pointerup, evt);
|
|
777
|
+
if (this._dragged) {
|
|
778
|
+
this._mouseAction(UIA.drop, evt);
|
|
779
|
+
} else {
|
|
780
|
+
this._mouseAction(UIA.up, evt);
|
|
781
|
+
}
|
|
782
|
+
this._pressed = false;
|
|
783
|
+
this._dragged = false;
|
|
784
|
+
if (
|
|
785
|
+
evt instanceof PointerEvent &&
|
|
786
|
+
evt.target instanceof Element &&
|
|
787
|
+
evt.target.hasPointerCapture(evt.pointerId)
|
|
788
|
+
) {
|
|
789
|
+
evt.target.releasePointerCapture(evt.pointerId);
|
|
790
|
+
}
|
|
791
|
+
return false;
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
/**
|
|
795
|
+
* MouseMove handler.
|
|
796
|
+
* @param evt
|
|
797
|
+
*/
|
|
798
|
+
protected _mouseMove(evt: PointerEvent) {
|
|
799
|
+
if (this._isTouchHandled(evt)) return false;
|
|
800
|
+
if (this._pressed) {
|
|
801
|
+
this._dragged = true;
|
|
802
|
+
this._mouseAction(UIA.drag, evt);
|
|
803
|
+
} else {
|
|
804
|
+
this._mouseAction(UIA.move, evt);
|
|
805
|
+
}
|
|
806
|
+
return false;
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
/**
|
|
810
|
+
* MouseOver handler.
|
|
811
|
+
* @param evt
|
|
812
|
+
*/
|
|
813
|
+
protected _mouseOver(evt: PointerEvent) {
|
|
814
|
+
if (this._isTouchHandled(evt)) return false;
|
|
815
|
+
this._mouseAction(UIA.over, evt);
|
|
816
|
+
return false;
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
/**
|
|
820
|
+
* MouseOut handler.
|
|
821
|
+
* @param evt
|
|
822
|
+
*/
|
|
823
|
+
protected _mouseOut(evt: PointerEvent | TouchEvent) {
|
|
824
|
+
if (this._isTouchHandled(evt)) return false;
|
|
825
|
+
this._mouseAction(UIA.out, evt);
|
|
826
|
+
if (this._dragged) this._mouseAction(UIA.drop, evt);
|
|
827
|
+
this._pressed = false;
|
|
828
|
+
this._dragged = false;
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/**
|
|
833
|
+
* MouseClick handler.
|
|
834
|
+
* @param evt
|
|
835
|
+
*/
|
|
836
|
+
protected _mouseClick(evt: MouseEvent | TouchEvent) {
|
|
837
|
+
this._mouseAction(UIA.click, evt);
|
|
838
|
+
this._pressed = false;
|
|
839
|
+
this._dragged = false;
|
|
840
|
+
return false;
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/**
|
|
844
|
+
* ContextMenu handler.
|
|
845
|
+
* @param evt
|
|
846
|
+
*/
|
|
847
|
+
protected _contextMenu(evt: MouseEvent) {
|
|
848
|
+
this._mouseAction(UIA.contextmenu, evt);
|
|
849
|
+
return false;
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* TouchMove handler.
|
|
854
|
+
* @param evt
|
|
855
|
+
*/
|
|
856
|
+
protected _touchMove(evt: TouchEvent) {
|
|
857
|
+
this._mouseAction(UIA.move, evt);
|
|
858
|
+
if (this._pressed) {
|
|
859
|
+
this._dragged = true;
|
|
860
|
+
this._mouseAction(UIA.drag, evt);
|
|
861
|
+
}
|
|
862
|
+
// preventDefault is ignored (and logs an error) inside passive listeners
|
|
863
|
+
if (!this._touchPassive) evt.preventDefault();
|
|
864
|
+
return false;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
/**
|
|
868
|
+
* TouchStart handler.
|
|
869
|
+
* @param evt
|
|
870
|
+
*/
|
|
871
|
+
protected _touchStart(evt: TouchEvent) {
|
|
872
|
+
this._mouseAction(UIA.down, evt);
|
|
873
|
+
this._pressed = true;
|
|
874
|
+
if (!this._touchPassive) evt.preventDefault();
|
|
875
|
+
return false;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
protected _keyDown(evt: KeyboardEvent) {
|
|
879
|
+
this._keyboardAction(UIA.keydown, evt);
|
|
880
|
+
return false;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
protected _keyUp(evt: KeyboardEvent) {
|
|
884
|
+
this._keyboardAction(UIA.keyup, evt);
|
|
885
|
+
return false;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
protected _keyboardAction(type: string, evt: KeyboardEvent) {
|
|
889
|
+
if (!this.isPlaying) return;
|
|
890
|
+
for (const k in this.players) {
|
|
891
|
+
if (this.players.hasOwnProperty(k)) {
|
|
892
|
+
const v = this.players[k];
|
|
893
|
+
if (v.action)
|
|
894
|
+
v.action(type, evt.shiftKey ? 1 : 0, evt.altKey ? 1 : 0, evt);
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
}
|