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/Dom.ts ADDED
@@ -0,0 +1,940 @@
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 { MultiTouchSpace } from "./Space";
4
+ import { type Form, VisualForm, Font } from "./Form";
5
+ import { Util } from "./Util";
6
+ import { Pt, Bound } from "./Pt";
7
+ import {
8
+ type PtLike,
9
+ type GroupLike,
10
+ type IPlayer,
11
+ type DOMFormContext,
12
+ type PtLikeIterable,
13
+ } from "./Types";
14
+
15
+ /**
16
+ * DOMSpace hosts a Space in a DOM element. It is the subclassing entry point for building
17
+ * custom element-based spaces; usually its subclass [`SVGSpace`](#link) should be used instead.
18
+ * Learn more about spaces in [this guide](../guide/Space-0500.html).
19
+ *
20
+ * When using a Space inside a component framework, create it on mount and call
21
+ * [`DOMSpace.dispose`](#link) on unmount so listeners and the animation loop are released.
22
+ * For example, in React:
23
+ * ```
24
+ * useEffect(() => {
25
+ * const space = new SVGSpace(ref.current).setup({ resize: true });
26
+ * space.add(...).play();
27
+ * return () => { space.dispose(); };
28
+ * }, []);
29
+ * ```
30
+ * Dispose is idempotent, and a new Space can be mounted on the same element afterwards
31
+ * (as happens under React's StrictMode).
32
+ */
33
+ export class DOMSpace extends MultiTouchSpace {
34
+ protected _canvas: HTMLElement | SVGElement;
35
+ protected _container: Element;
36
+
37
+ id: string = "domspace";
38
+ protected _autoResize = true;
39
+ protected _bgcolor = "#e1e9f0";
40
+ protected _css = {};
41
+ private _domDisposed = false;
42
+ private _readyTimer: ReturnType<typeof setTimeout>;
43
+ // elements this space created for a missing target, removed on dispose
44
+ private _ownsContainer = false;
45
+
46
+ // one stable bound reference, so removeEventListener actually removes the
47
+ // listener that addEventListener added (and double-adds dedupe)
48
+ private readonly _resizeHandlerBound = this._resizeHandler.bind(this);
49
+
50
+ /**
51
+ * Create a DOMSpace for HTML DOM elements
52
+ * @param elem Specify an element by its "id" attribute as string, or by the element object itself. If left empty, a `<div id="pt_container"><div id="pt" /></div>` will be added to DOM; a missing id is created the same way. Use css to customize its appearance if needed.
53
+ * @param callback an optional callback `function(boundingBox, spaceElement)` to be called when element is appended and ready. Alternatively, a "ready" event will also be fired from the element when it's appended, which can be traced with `spaceInstance.element.addEventListener("ready")`
54
+ * @example `new DOMSpace( "#myElementID" )`
55
+ */
56
+ constructor(
57
+ elem: string | Element | null = "pt",
58
+ callback?: (bound: Bound, elem: Element) => void,
59
+ ) {
60
+ super();
61
+ this.refresh(false); // DOM elements persist unless a renderer opts into refreshing.
62
+
63
+ let _selector: Element | null = null;
64
+ this.id = "pts";
65
+
66
+ // check element or element id string
67
+ if (elem instanceof Element) {
68
+ _selector = elem;
69
+ this.id = "pts_existing_space";
70
+ } else {
71
+ const target = elem || "pt";
72
+ const id = target[0] === "#" || target[0] === "." ? target : "#" + target;
73
+ _selector = document.querySelector(id);
74
+ this.id = id.substr(1);
75
+ }
76
+
77
+ // if selector is not defined, create the elements
78
+ if (!_selector) {
79
+ this._container = DOMSpace.createElement("div", this.id + "_container");
80
+ this._canvas = this._createDefaultElement(this._container, this.id);
81
+ document.body.appendChild(this._container);
82
+ this._ownsContainer = true;
83
+ } else {
84
+ this._canvas = _selector as HTMLElement;
85
+ this._container = _selector.parentElement!;
86
+ }
87
+
88
+ // no mutation observer, so we set a timeout for ready event
89
+ this._readyTimer = setTimeout(this._ready.bind(this, callback), 50);
90
+ }
91
+
92
+ /**
93
+ * Create the drawing element for a target that does not exist yet, inside the created container.
94
+ * Subclasses that draw into a specific element type override this.
95
+ * @param container the created container
96
+ * @param id the id for the new element
97
+ */
98
+ protected _createDefaultElement(
99
+ container: Element,
100
+ id: string,
101
+ ): HTMLElement | SVGElement {
102
+ return DOMSpace.createElement("div", id, container) as HTMLElement;
103
+ }
104
+
105
+ /**
106
+ * Helper function to create a DOM element.
107
+ * @param elem element tag name
108
+ * @param id element id attribute
109
+ * @param appendTo Optional, if specified, the created element will be appended to this element
110
+ */
111
+ static createElement(
112
+ elem: string = "div",
113
+ id: string,
114
+ appendTo?: Element,
115
+ ): Element {
116
+ let d = document.createElement(elem);
117
+ if (id) d.setAttribute("id", id);
118
+ if (appendTo && appendTo.appendChild) appendTo.appendChild(d);
119
+ return d;
120
+ }
121
+
122
+ /**
123
+ * Handle callbacks after element is mounted in DOM.
124
+ * @param callback
125
+ */
126
+ private _ready(callback?: (bound: Bound, elem: Element) => void) {
127
+ if (this._domDisposed) return;
128
+ if (!this._container)
129
+ throw new Error(`Cannot initiate #${this.id} element`);
130
+
131
+ this._isReady = true;
132
+
133
+ this._resizeHandler(null);
134
+ this.clear(this._bgcolor);
135
+ this._canvas.dispatchEvent(new Event("ready"));
136
+
137
+ for (let k in this.players) {
138
+ if (this.players.hasOwnProperty(k)) {
139
+ if (this.players[k].start)
140
+ this.players[k].start(this.bound.clone(), this);
141
+ }
142
+ }
143
+
144
+ this._pointer = this.center;
145
+
146
+ if (callback) callback(this.bound, this._canvas);
147
+ }
148
+
149
+ /**
150
+ * Set up various options for DOMSpace. This is usually set during instantiation, eg `new DOMSpace(...).setup( {opt} )`.
151
+ * @param opt an object with these optional properties: **bgcolor** is a hex or rgba string to set initial background color of the canvas, or use `false` or "transparent" to set a transparent background; **resize** a boolean to set whether `<canvas>` size should auto resize to match its container's size, which can also be set using `autoSize()`.
152
+ * @example `space.setup({ bgcolor: "#f00", resize: true })`
153
+ */
154
+ setup(opt: { bgcolor?: string; resize?: boolean }): this {
155
+ if (opt.bgcolor) {
156
+ this._bgcolor = opt.bgcolor;
157
+ }
158
+
159
+ this.autoResize = opt.resize != undefined ? opt.resize : false;
160
+
161
+ return this;
162
+ }
163
+
164
+ /**
165
+ * Not implemented. See SVGSpace and HTMLSpace for implementation.
166
+ */
167
+ getForm(): Form {
168
+ return null as unknown as Form;
169
+ }
170
+
171
+ /**
172
+ * Set whether the canvas element should resize when its container is resized.
173
+ * @param auto a boolean value indicating if auto size is set
174
+ */
175
+ set autoResize(auto: boolean) {
176
+ this._autoResize = auto;
177
+ if (auto) {
178
+ window.addEventListener("resize", this._resizeHandlerBound);
179
+ } else {
180
+ delete (this._css as any)["width"];
181
+ delete (this._css as any)["height"];
182
+ window.removeEventListener("resize", this._resizeHandlerBound);
183
+ }
184
+ }
185
+ get autoResize(): boolean {
186
+ return this._autoResize;
187
+ }
188
+
189
+ /**
190
+ * This overrides Space's `resize` function. It's used as a callback function for window's resize event and not usually called directly. You can keep track of resize events with `resize: (bound, evt)` callback in your player objects (See [`Space.add`](#link) function).
191
+ * @param b a Bound object to resize to
192
+ * @param evt Optionally pass a resize event
193
+ */
194
+ resize(b: Bound, evt?: Event | null): this {
195
+ this.bound = b;
196
+ this.styles({ width: `${b.width}px`, height: `${b.height}px` }, true);
197
+
198
+ for (let k in this.players) {
199
+ if (this.players.hasOwnProperty(k)) {
200
+ let p = this.players[k];
201
+ if (p.resize) p.resize(this.bound, evt);
202
+ }
203
+ }
204
+
205
+ return this;
206
+ }
207
+
208
+ /**
209
+ * Window resize handling.
210
+ * @param evt
211
+ */
212
+ protected _resizeHandler(evt: Event | null) {
213
+ let b = Bound.fromBoundingRect(this._container.getBoundingClientRect());
214
+
215
+ if (this._autoResize) {
216
+ this.styles({ width: "100%", height: "100%" }, true);
217
+ } else {
218
+ this.styles({ width: `${b.width}px`, height: `${b.height}px` }, true);
219
+ }
220
+
221
+ this.resize(b, evt);
222
+ }
223
+
224
+ /**
225
+ * Get this DOM element.
226
+ */
227
+ get element(): Element {
228
+ return this._canvas;
229
+ }
230
+
231
+ /**
232
+ * Get the parent DOM element that contains this DOM element.
233
+ */
234
+ get parent(): Element {
235
+ return this._container;
236
+ }
237
+
238
+ /**
239
+ * A property to indicate if the Space is ready.
240
+ */
241
+ get ready(): boolean {
242
+ return this._isReady;
243
+ }
244
+
245
+ /**
246
+ * Clear the element's contents, and optionally set a new background color. This overrides Space's `clear` function.
247
+ * @param bg Optionally specify a custom background color in hex or rgba string, or "transparent". If not defined, it will use its `bgcolor` property as background color to clear the canvas.
248
+ */
249
+ clear(bg?: string): this {
250
+ if (bg) this.background = bg;
251
+ this._canvas.innerHTML = "";
252
+ return this;
253
+ }
254
+
255
+ /**
256
+ * Set a background color on the container element.
257
+ @param bg background color as hex or rgba string
258
+ */
259
+ set background(bg: string) {
260
+ this._bgcolor = bg;
261
+ (this._container as HTMLElement).style.backgroundColor = this._bgcolor;
262
+ }
263
+ get background(): string {
264
+ return this._bgcolor;
265
+ }
266
+
267
+ /**
268
+ * Add or update a style definition, and optionally update that style in the Element.
269
+ * @param key style name
270
+ * @param val style value
271
+ * @param update a boolean to update the element's style immediately if set to `true`. Default is `false`.
272
+ */
273
+ style(key: string, val: string, update: boolean = false): this {
274
+ (this._css as any)[key] = val;
275
+ if (update) (this._canvas.style as any)[key] = val;
276
+ return this;
277
+ }
278
+
279
+ /**
280
+ * Add of update a list of style definitions, and optionally update those styles in the Element.
281
+ * @param styles a key-value objects of style definitions
282
+ * @param update a boolean to update the element's style immediately if set to `true`. Default is `false`.
283
+ * @return this
284
+ */
285
+ styles(styles: Record<string, string>, update: boolean = false): this {
286
+ for (let k in styles) {
287
+ if (styles.hasOwnProperty(k)) this.style(k, (styles as any)[k], update);
288
+ }
289
+ return this;
290
+ }
291
+
292
+ /**
293
+ * A static helper function to add or update Element attributes.
294
+ * @param elem Element to update
295
+ * @param data an object with key-value pairs
296
+ * @returns this DOM element
297
+ */
298
+ static setAttr(elem: Element, data: Record<string, any>): Element {
299
+ for (let k in data) {
300
+ if (data.hasOwnProperty(k)) {
301
+ elem.setAttribute(k, (data as any)[k]);
302
+ }
303
+ }
304
+ return elem;
305
+ }
306
+
307
+ /**
308
+ * A static helper function to compose an inline style string from a object of styles.
309
+ * @param data an object with key-value pairs
310
+ * @example `DOMSpace.getInlineStyles( {width: "100px", "font-size": "10px"} )`
311
+ */
312
+ static getInlineStyles(data: Record<string, any>): string {
313
+ let str = "";
314
+ for (let k in data) {
315
+ if (data.hasOwnProperty(k)) {
316
+ if ((data as any)[k]) str += `${k}: ${(data as any)[k]}; `;
317
+ }
318
+ }
319
+ return str;
320
+ }
321
+
322
+ /**
323
+ * Dispose of browser resources held by this space and remove all players. Call this before
324
+ * unmounting the DOM, eg in a framework component's unmount/cleanup callback. Dispose is
325
+ * idempotent, and a new Space can be created on the same element afterwards.
326
+ */
327
+ dispose(): this {
328
+ if (this._domDisposed) return this;
329
+ this._domDisposed = true;
330
+ clearTimeout(this._readyTimer);
331
+
332
+ this.autoResize = false; // removes the window resize listener
333
+ this._unbindAll(); // removes mouse/touch listeners on the element
334
+ this._cancelAnimation(); // cancels the animation frame immediately
335
+
336
+ // Remove the players without the subclass's DOM-clearing `removeAll`:
337
+ // disposing must release resources, never destroy a user-owned host
338
+ // element's contents (a re-mount on the same element must work).
339
+ MultiTouchSpace.prototype.removeAll.call(this);
340
+ this._isReady = false;
341
+ // elements created for a missing target are this space's own
342
+ if (this._ownsContainer) this._container.remove();
343
+
344
+ return this;
345
+ }
346
+ }
347
+
348
+ /**
349
+ * @deprecated HTML rendering is deprecated and will be removed in a future major version. Use [`SVGSpace`](#link) for DOM-based output instead — it shares the supported subset of the [`CanvasForm`](#link) drawing API.
350
+ * **[Experimental]** HTMLSpace is a subclass of DOMSpace that works with HTML elements. See [a demo here](https://ptsjs.org/demo/?name=htmlform.scope).
351
+ */
352
+ export class HTMLSpace extends DOMSpace {
353
+ /**
354
+ * Get a new `HTMLForm` which provides visualization functions in html elements.
355
+ * @see `HTMLForm`
356
+ */
357
+ getForm(): Form {
358
+ return new HTMLForm(this);
359
+ }
360
+
361
+ /**
362
+ * A static function to add a DOM element inside a node. Usually you don't need to use this directly. See methods in [`HTMLForm`](#link) instead.
363
+ * @param parent the parent element, or `null` to use current `<svg>` as parent.
364
+ * @param name a string of element name, such as `rect` or `circle`
365
+ * @param id id attribute of the new element
366
+ * @param autoClass add a class based on the id (from char 0 to index of "-"). Default is true.
367
+ */
368
+ static htmlElement(
369
+ parent: Element | null | undefined,
370
+ name: string,
371
+ id?: string,
372
+ autoClass: boolean = true,
373
+ ): HTMLElement {
374
+ if (!parent || !parent.appendChild)
375
+ throw new Error("parent is not a valid DOM element");
376
+
377
+ // O(1) id lookup, then verify it's inside the parent so a same-id
378
+ // element elsewhere in the document is never silently adopted
379
+ let elem: Element | null = document.getElementById(id!);
380
+ if (elem && !parent.contains(elem)) elem = null;
381
+ if (!elem) {
382
+ elem = document.createElement(name);
383
+ elem.setAttribute("id", id!);
384
+
385
+ if (autoClass)
386
+ elem.setAttribute("class", id!.substring(0, id!.indexOf("-")));
387
+ parent.appendChild(elem);
388
+ }
389
+ return elem as HTMLElement;
390
+ }
391
+
392
+ /**
393
+ * Remove an item from this space.
394
+ * @param player a player item with an auto-assigned `animateID` property
395
+ */
396
+ remove(player: IPlayer): this {
397
+ let temp = this._container.querySelectorAll("." + HTMLForm.scopeID(player));
398
+
399
+ temp.forEach((el: Element) => {
400
+ el.parentNode!.removeChild(el);
401
+ });
402
+
403
+ return super.remove(player);
404
+ }
405
+
406
+ /**
407
+ * Remove all items from this space. This clears the contents of the space's
408
+ * element but never touches its container.
409
+ */
410
+ removeAll(): this {
411
+ this._canvas.innerHTML = "";
412
+ return super.removeAll();
413
+ }
414
+ }
415
+
416
+ let _htmlFormGroupID = 0;
417
+ let _htmlFormDomID = 0;
418
+
419
+ /**
420
+ * @deprecated HTML rendering is deprecated and will be removed in a future major version. Use [`SVGForm`](#link) for DOM-based output instead — it shares the complete [`CanvasForm`](#link) drawing API.
421
+ * **[Experimental]** HTMLForm is an implementation of abstract class [`VisualForm`](#link). It provide methods to express Pts on [`HTMLSpace`](#link).
422
+ */
423
+ export class HTMLForm extends VisualForm {
424
+ /**
425
+ * store common styles so that they can be restored to canvas context when using multiple forms. See `reset()`.
426
+ */
427
+ protected _style = {
428
+ filled: true,
429
+ stroked: true,
430
+ background: "#f03",
431
+ "border-color": "#fff",
432
+ color: "#000",
433
+ "border-width": "1px",
434
+ "border-radius": "0",
435
+ "border-style": "solid",
436
+ opacity: 1,
437
+ position: "absolute",
438
+ top: 0,
439
+ left: 0,
440
+ width: 0,
441
+ height: 0,
442
+ };
443
+
444
+ protected _ctx: DOMFormContext = {
445
+ group: null,
446
+ groupID: "pts",
447
+ groupCount: 0,
448
+ currentID: "pts0",
449
+ currentClass: "",
450
+ style: {},
451
+ };
452
+
453
+ // mutable statics are stored at module level and exposed through accessors so
454
+ // no post-class assignment is emitted (which would defeat tree-shaking)
455
+ static get groupID(): number {
456
+ return _htmlFormGroupID;
457
+ }
458
+ static set groupID(n: number) {
459
+ _htmlFormGroupID = n;
460
+ }
461
+ static get domID(): number {
462
+ return _htmlFormDomID;
463
+ }
464
+ static set domID(n: number) {
465
+ _htmlFormDomID = n;
466
+ }
467
+
468
+ protected _space: HTMLSpace;
469
+ protected _ready: boolean = false;
470
+ protected _formID: number = 0;
471
+
472
+ /**
473
+ * Create a new `HTMLForm`. Alternatively, you can use [`HTMLSpace.getForm`](#link) function to get an instance of HTMLForm.
474
+ * @param space the space to use
475
+ */
476
+ constructor(space: HTMLSpace) {
477
+ super();
478
+ this._space = space;
479
+ this._formID = HTMLForm.groupID++;
480
+
481
+ const init = () => {
482
+ this._ctx.group = this._space.element;
483
+ this._ctx.groupID = `pts_dom_${this._formID}`;
484
+ this._ctx.style = Object.assign({}, this._style);
485
+ this._ready = true;
486
+ };
487
+ // a form created after the space is ready would otherwise never
488
+ // initialize, since start callbacks only run at readiness
489
+ if (this._space.ready) init();
490
+ else this._space.add({ start: init });
491
+ }
492
+
493
+ /**
494
+ * Get the corresponding space for this form
495
+ */
496
+ get space(): HTMLSpace {
497
+ return this._space;
498
+ }
499
+
500
+ /**
501
+ * Usually not used directly. This updates a style in `_ctx` context or throw an Error if the style doesn't exist.
502
+ * @param k style key
503
+ * @param v style value
504
+ * @param unit Optional unit like 'px' to append to value
505
+ */
506
+ protected styleTo(k: string, v: any, unit: string = "") {
507
+ if ((this._ctx.style as any)[k] === undefined)
508
+ throw new Error(`${k} style property doesn't exist`);
509
+ this._ctx.style[k] = `${v}${unit}`;
510
+ }
511
+
512
+ /**
513
+ * Set current alpha value.
514
+ * @example `form.alpha(0.6)`
515
+ * @param a alpha value between 0 and 1
516
+ */
517
+ alpha(a: number): this {
518
+ this.styleTo("opacity", a);
519
+ return this;
520
+ }
521
+
522
+ /**
523
+ * Set current fill style. Provide a valid color string or `false` to specify no fill color.
524
+ * @example `form.fill("#F90")`, `form.fill("rgba(0,0,0,.5")`, `form.fill(false)`
525
+ * @param c fill color
526
+ */
527
+ fill(c: string | boolean): this {
528
+ if (typeof c == "boolean") {
529
+ this.styleTo("filled", c);
530
+ if (!c) this.styleTo("background", "transparent");
531
+ } else {
532
+ this.styleTo("filled", true);
533
+ this.styleTo("background", c);
534
+ }
535
+ return this;
536
+ }
537
+
538
+ /**
539
+ * Set current stroke style. Provide a valid color string or `false` to specify no stroke color.
540
+ * @example `form.stroke("#F90")`, `form.stroke("rgba(0,0,0,.5")`, `form.stroke(false)`, `form.stroke("#000", 0.5, 'round', 'square')`
541
+ * @param c stroke color which can be as color, gradient, or pattern. (See [canvas documentation](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/strokeStyle))
542
+ * @param width Optional value (can be floating point) to set line width
543
+ * @param linejoin not implemented in HTMLForm
544
+ * @param linecap not implemented in HTMLForm
545
+ */
546
+ stroke(
547
+ c: string | boolean,
548
+ width?: number,
549
+ linejoin?: string,
550
+ linecap?: string,
551
+ ): this {
552
+ if (typeof c == "boolean") {
553
+ this.styleTo("stroked", c);
554
+ if (!c) this.styleTo("border-width", 0);
555
+ } else {
556
+ this.styleTo("stroked", true);
557
+ this.styleTo("border-color", c);
558
+ this.styleTo("border-width", (width || 1) + "px");
559
+ }
560
+ return this;
561
+ }
562
+
563
+ /**
564
+ * Set current text color style. Provide a valid color string.
565
+ * @example `form.fill("#F90")`, `form.fill("rgba(0,0,0,.5")`, `form.fill(false)`
566
+ * @param c fill color
567
+ */
568
+ fillText(c: string): this {
569
+ this.styleTo("color", c);
570
+ return this;
571
+ }
572
+
573
+ /**
574
+ * Add custom class to the created element.
575
+ * @param c custom class name or `false` to reset it
576
+ * @example `form.fill("#f00").cls("myClass").rects(r)` `form.cls(false).circles(c)`
577
+ */
578
+ cls(c: string | boolean) {
579
+ if (typeof c == "boolean") {
580
+ this._ctx.currentClass = "";
581
+ } else {
582
+ this._ctx.currentClass = c;
583
+ }
584
+ return this;
585
+ }
586
+
587
+ /**
588
+ * Set the current font.
589
+ * @param sizeOrFont either a number to specify font-size, or a `Font` object to specify all font properties
590
+ * @param weight Optional font-weight string such as "bold"
591
+ * @param style Optional font-style string such as "italic"
592
+ * @param lineHeight Optional line-height number suchas 1.5
593
+ * @param family Optional font-family such as "Helvetica, sans-serif"
594
+ * @example `form.font( myFont )`, `form.font(14, "bold")`
595
+ */
596
+ font(
597
+ sizeOrFont: number | Font,
598
+ weight?: string,
599
+ style?: string,
600
+ lineHeight?: number,
601
+ family?: string,
602
+ ): this {
603
+ if (typeof sizeOrFont == "number") {
604
+ this._font.size = sizeOrFont;
605
+ if (family) this._font.face = family;
606
+ if (weight) this._font.weight = weight;
607
+ if (style) this._font.style = style;
608
+ if (lineHeight) this._font.lineHeight = lineHeight;
609
+ } else {
610
+ this._font = sizeOrFont;
611
+ }
612
+
613
+ this._ctx.style["font"] = this._font.value;
614
+
615
+ return this;
616
+ }
617
+
618
+ /**
619
+ * Reset the context's common styles to this form's styles. This supports using multiple forms on the same canvas context.
620
+ */
621
+ reset(): this {
622
+ this._ctx.style = Object.assign({}, this._style);
623
+
624
+ this._font = new Font(10, "sans-serif");
625
+ this._ctx.style["font"] = this._font.value;
626
+
627
+ return this;
628
+ }
629
+
630
+ /**
631
+ * Set this form's group scope by an ID, and optionally define the group's parent element. A group scope keeps track of elements by their generated IDs, and updates their properties as needed. See also `scope()`.
632
+ * @param group_id a string to use as prefix for the group's id. For example, group_id "hello" will create elements with id like "hello-1", "hello-2", etc
633
+ * @param group Optional DOM element to define this group's parent element
634
+ * @returns this form's context
635
+ */
636
+ updateScope(group_id: string, group?: Element): DOMFormContext {
637
+ this._ctx.group = group;
638
+ this._ctx.groupID = group_id;
639
+ this._ctx.groupCount = 0;
640
+ this.nextID();
641
+ return this._ctx;
642
+ }
643
+
644
+ /**
645
+ * Set the current group scope to an item added into space, in order to keep track of any point, circle, etc created within it. The item must have an `animateID` property, so that elements created within the item will have generated IDs like "item-{animateID}-{count}".
646
+ * @param item a "player" item that's added to space (see `space.add(...)`) and has an `animateID` property
647
+ * @returns this form's context
648
+ */
649
+ scope(item: IPlayer) {
650
+ if (!item || item.animateID == null)
651
+ throw new Error("item not defined or not yet added to Space");
652
+ // two forms scoped to the same player must not generate the same ids
653
+ return this.updateScope(
654
+ `${HTMLForm.scopeID(item)}-f${this._formID}`,
655
+ this.space.element,
656
+ );
657
+ }
658
+
659
+ /**
660
+ * Get next available id in the current group.
661
+ * @returns an id string
662
+ */
663
+ nextID(): string {
664
+ this._ctx.groupCount++;
665
+ this._ctx.currentID = `${this._ctx.groupID}-${this._ctx.groupCount}`;
666
+ return this._ctx.currentID;
667
+ }
668
+
669
+ /**
670
+ * A static function to generate an ID string based on a context object.
671
+ * @param ctx a context object for an HTMLForm
672
+ */
673
+ static getID(ctx: DOMFormContext): string {
674
+ return ctx.currentID || `p-${HTMLForm.domID++}`;
675
+ }
676
+
677
+ /**
678
+ * A static function to generate an ID string for a scope, based on a "player" item in the Space.
679
+ * @param item a "player" item that's added to space (see `space.add(...)`) and has an `animateID` property
680
+ */
681
+ static scopeID(item: IPlayer): string {
682
+ return `item-${item.animateID}`;
683
+ }
684
+
685
+ /**
686
+ * A static function to help adding style object to an element. This put all styles into `style` attribute instead of individual attributes, so that the styles can be parsed by Adobe Illustrator.
687
+ * @param elem A DOM element to add to
688
+ * @param styles an object of style properties
689
+ * @example `HTMLForm.style(elem, {fill: "#f90", stroke: false})`
690
+ * @returns DOM element
691
+ */
692
+ static style(elem: Element, styles: Record<string, any>): Element {
693
+ let st = [];
694
+
695
+ if (!styles["filled"]) st.push("background: none");
696
+ if (!styles["stroked"]) st.push("border: none");
697
+
698
+ for (let k in styles) {
699
+ if (styles.hasOwnProperty(k) && k != "filled" && k != "stroked") {
700
+ let v = styles[k];
701
+ if (v) {
702
+ if (!styles["filled"] && k.indexOf("background") === 0) {
703
+ continue;
704
+ } else if (!styles["stroked"] && k.indexOf("border-width") === 0) {
705
+ continue;
706
+ } else {
707
+ st.push(`${k}: ${v}`);
708
+ }
709
+ }
710
+ }
711
+ }
712
+
713
+ return HTMLSpace.setAttr(elem, { style: st.join(";") });
714
+ }
715
+
716
+ /**
717
+ * A helper function to set top, left, width, height of DOM element.
718
+ * @param ctx the HTMLForm context whose style is updated
719
+ * @param pt left and top position
720
+ * @param size width and height
721
+ */
722
+ static rectStyle(
723
+ ctx: DOMFormContext,
724
+ pt: PtLike,
725
+ size: PtLike,
726
+ ): DOMFormContext {
727
+ ctx.style["left"] = pt[0] + "px";
728
+ ctx.style["top"] = pt[1] + "px";
729
+ ctx.style["width"] = size[0] + "px";
730
+ ctx.style["height"] = size[1] + "px";
731
+ return ctx;
732
+ }
733
+
734
+ /**
735
+ * A helper function to set the top and left position styling of text DOM context.
736
+ * @param ctx context to add style to
737
+ * @param pt a Pt object or numeric array determining the top-left position of the text
738
+ */
739
+ static textStyle(ctx: DOMFormContext, pt: PtLike): DOMFormContext {
740
+ ctx.style["left"] = pt[0] + "px";
741
+ ctx.style["top"] = pt[1] + "px";
742
+ return ctx;
743
+ }
744
+
745
+ /**
746
+ * A static function to draws a point.
747
+ * @param ctx a context object of HTMLForm
748
+ * @param pt a Pt object or numeric array
749
+ * @param radius radius of the point. Default is 5.
750
+ * @param shape The shape of the point. Defaults to "square", but it can be "circle" or a custom shape function in your own implementation.
751
+ * @example `HTMLForm.point( p )`, `HTMLForm.point( p, 10, "circle" )`
752
+ */
753
+ static point(
754
+ ctx: DOMFormContext,
755
+ pt: PtLike,
756
+ radius: number = 5,
757
+ shape: string = "square",
758
+ ): Element {
759
+ if (shape === "circle") {
760
+ return HTMLForm.circle(ctx, pt, radius);
761
+ } else {
762
+ return HTMLForm.square(ctx, pt, radius);
763
+ }
764
+ }
765
+
766
+ /**
767
+ * Draws a point.
768
+ * @param pt a Pt object
769
+ * @param radius radius of the point. Default is 5.
770
+ * @param shape The shape of the point. Defaults to "square", but it can be "circle" or a custom shape function in your own implementation.
771
+ * @example `form.point( p )`, `form.point( p, 10, "circle" )`
772
+ */
773
+ point(pt: PtLike, radius: number = 5, shape: string = "square"): this {
774
+ this.nextID();
775
+ // reset radius for squares, so a square after a circle isn't rounded
776
+ this.styleTo("border-radius", shape == "circle" ? "100%" : "0");
777
+ HTMLForm.point(this._ctx, pt, radius, shape);
778
+ return this;
779
+ }
780
+
781
+ /**
782
+ * A static function to draw a circle.
783
+ * @param ctx a context object of HTMLForm
784
+ * @param pt center position of the circle
785
+ * @param radius radius of the circle
786
+ */
787
+ static circle(ctx: DOMFormContext, pt: PtLike, radius: number = 10): Element {
788
+ let elem = HTMLSpace.htmlElement(ctx.group, "div", HTMLForm.getID(ctx));
789
+ HTMLSpace.setAttr(elem, {
790
+ class: `pts-form pts-circle ${ctx.currentClass}`,
791
+ });
792
+ HTMLForm.rectStyle(
793
+ ctx,
794
+ new Pt(pt).$subtract(radius),
795
+ new Pt(radius * 2, radius * 2),
796
+ );
797
+ HTMLForm.style(elem, ctx.style);
798
+ return elem;
799
+ }
800
+
801
+ /**
802
+ * Draw a circle.
803
+ * @param pts usually a Group of 2 Pts, but it can also take an array of two numeric arrays [ [position], [size] ]
804
+ * @see [`Circle.fromCenter`](#link)
805
+ */
806
+ circle(pts: GroupLike | number[][]): this {
807
+ this.nextID();
808
+ this.styleTo("border-radius", "100%");
809
+ HTMLForm.circle(this._ctx, pts[0], pts[1][0]);
810
+ return this;
811
+ }
812
+
813
+ /**
814
+ * A static function to draw a square.
815
+ * @param ctx a context object of HTMLForm
816
+ * @param pt center position of the square
817
+ * @param halfsize half size of the square
818
+ */
819
+ static square(ctx: DOMFormContext, pt: PtLike, halfsize: number) {
820
+ let elem = HTMLSpace.htmlElement(ctx.group, "div", HTMLForm.getID(ctx));
821
+ HTMLSpace.setAttr(elem, {
822
+ class: `pts-form pts-square ${ctx.currentClass}`,
823
+ });
824
+ HTMLForm.rectStyle(
825
+ ctx,
826
+ new Pt(pt).$subtract(halfsize),
827
+ new Pt(halfsize * 2, halfsize * 2),
828
+ );
829
+ HTMLForm.style(elem, ctx.style);
830
+ return elem;
831
+ }
832
+
833
+ /**
834
+ * Draw a square, given a center and its half-size.
835
+ * @param pt center Pt
836
+ * @param halfsize half-size
837
+ */
838
+ square(pt: PtLike, halfsize: number): this {
839
+ this.nextID();
840
+ this.styleTo("border-radius", "0");
841
+ HTMLForm.square(this._ctx, pt, halfsize);
842
+ return this;
843
+ }
844
+
845
+ /**
846
+ * A static function to draw a rectangle.
847
+ * @param ctx a context object of HTMLForm
848
+ * @param pts a Group or an Iterable<PtLike> with 2 Pt specifying the top-left and bottom-right positions.
849
+ */
850
+ static rect(ctx: DOMFormContext, pts: PtLikeIterable): Element | undefined {
851
+ let p = Util.iterToArray(pts);
852
+ if (!Util.arrayCheck(p)) return;
853
+
854
+ let elem = HTMLSpace.htmlElement(ctx.group, "div", HTMLForm.getID(ctx));
855
+ HTMLSpace.setAttr(elem, { class: `pts-form pts-rect ${ctx.currentClass}` });
856
+ HTMLForm.rectStyle(ctx, p[0], p[1]);
857
+ HTMLForm.style(elem, ctx.style);
858
+ return elem;
859
+ }
860
+
861
+ /**
862
+ * Draw a rectangle.
863
+ * @param pts a Group or an Iterable<PtLike> with 2 Pt specifying the top-left and bottom-right positions.
864
+ */
865
+ rect(pts: PtLikeIterable): this {
866
+ this.nextID();
867
+ this.styleTo("border-radius", "0");
868
+ HTMLForm.rect(this._ctx, pts);
869
+ return this;
870
+ }
871
+
872
+ /**
873
+ * A static function to draw text.
874
+ * @param ctx a context object of HTMLForm
875
+ * @param pt a Point object to specify the anchor point
876
+ * @param txt a string of text to draw
877
+ */
878
+ static text(ctx: DOMFormContext, pt: PtLike, txt: string): Element {
879
+ let elem = HTMLSpace.htmlElement(ctx.group, "div", HTMLForm.getID(ctx));
880
+
881
+ HTMLSpace.setAttr(elem, { class: `pts-form pts-text ${ctx.currentClass}` });
882
+
883
+ elem.textContent = txt;
884
+ HTMLForm.textStyle(ctx, pt);
885
+ HTMLForm.style(elem, ctx.style);
886
+
887
+ return elem;
888
+ }
889
+
890
+ /**
891
+ * Draw text in a DOM element.
892
+ * @param pt a Pt or numeric array to specify the anchor point
893
+ * @param txt text
894
+ */
895
+ text(pt: PtLike, txt: string): this {
896
+ this.nextID();
897
+ HTMLForm.text(this._ctx, pt, txt);
898
+ return this;
899
+ }
900
+
901
+ /**
902
+ * A convenient way to draw some text on canvas for logging or debugging. It'll be draw on the top-left of the canvas as an overlay.
903
+ * @param txt text
904
+ */
905
+ log(txt: any): this {
906
+ this.fill("#000").stroke("#fff", 0.5).text([10, 14], txt);
907
+ return this;
908
+ }
909
+
910
+ /**
911
+ * Arc is not implemented in HTMLForm.
912
+ */
913
+ arc(
914
+ pt: PtLike,
915
+ radius: number,
916
+ startAngle: number,
917
+ endAngle: number,
918
+ cc?: boolean,
919
+ ): this {
920
+ Util.warn("arc is not implemented in HTMLForm");
921
+ return this;
922
+ }
923
+
924
+ /**
925
+ * Line is not implemented in HTMLForm.
926
+ */
927
+ line(pts: GroupLike | number[][]): this {
928
+ Util.warn("line is not implemented in HTMLForm");
929
+ return this;
930
+ }
931
+
932
+ /**
933
+ * Polygon is not implemented in HTMLForm.
934
+ * @param pts
935
+ */
936
+ polygon(pts: GroupLike | number[][]): this {
937
+ Util.warn("polygon is not implemented in HTMLForm");
938
+ return this;
939
+ }
940
+ }