@johnmorrisdotca/kazoku-ui 0.2.1 → 0.7.2

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +72 -0
  2. package/README.md +100 -6
  3. package/dist/address-state/address.d.ts +76 -0
  4. package/dist/address-state/address.js +171 -0
  5. package/dist/address-state/index.d.ts +1 -0
  6. package/dist/address-state/index.js +2 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.js +1 -1
  9. package/dist/language-picker/define.d.ts +1 -0
  10. package/dist/language-picker/define.js +9 -0
  11. package/dist/language-picker/element.d.ts +39 -0
  12. package/dist/language-picker/element.js +194 -0
  13. package/dist/language-picker/index.d.ts +4 -0
  14. package/dist/language-picker/index.js +5 -0
  15. package/dist/language-picker/languages.d.ts +38 -0
  16. package/dist/language-picker/languages.js +100 -0
  17. package/dist/language-picker/styles.d.ts +1 -0
  18. package/dist/language-picker/styles.js +3 -0
  19. package/dist/language-picker/words.d.ts +8 -0
  20. package/dist/language-picker/words.js +7 -0
  21. package/dist/save-image/define.d.ts +1 -0
  22. package/dist/save-image/define.js +9 -0
  23. package/dist/save-image/element.d.ts +39 -0
  24. package/dist/save-image/element.js +186 -0
  25. package/dist/save-image/format.d.ts +20 -0
  26. package/dist/save-image/format.js +37 -0
  27. package/dist/save-image/index.d.ts +6 -0
  28. package/dist/save-image/index.js +8 -0
  29. package/dist/save-image/render.d.ts +55 -0
  30. package/dist/save-image/render.js +188 -0
  31. package/dist/save-image/styles.d.ts +1 -0
  32. package/dist/save-image/styles.js +9 -0
  33. package/dist/save-image/words.d.ts +12 -0
  34. package/dist/save-image/words.js +7 -0
  35. package/dist/shared/adopt.d.ts +4 -0
  36. package/dist/shared/adopt.js +34 -0
  37. package/dist/shared/base.d.ts +1 -0
  38. package/dist/shared/base.js +40 -0
  39. package/dist/shared/scheme.d.ts +1 -0
  40. package/dist/shared/scheme.js +19 -0
  41. package/dist/shared/store.d.ts +2 -0
  42. package/dist/shared/store.js +17 -0
  43. package/dist/shared/target.d.ts +2 -0
  44. package/dist/shared/target.js +11 -0
  45. package/dist/theme-switch/define.d.ts +1 -0
  46. package/dist/theme-switch/define.js +9 -0
  47. package/dist/theme-switch/element.d.ts +39 -0
  48. package/dist/theme-switch/element.js +203 -0
  49. package/dist/theme-switch/index.d.ts +4 -0
  50. package/dist/theme-switch/index.js +5 -0
  51. package/dist/theme-switch/styles.d.ts +1 -0
  52. package/dist/theme-switch/styles.js +8 -0
  53. package/dist/theme-switch/theme.d.ts +29 -0
  54. package/dist/theme-switch/theme.js +43 -0
  55. package/dist/theme-switch/words.d.ts +11 -0
  56. package/dist/theme-switch/words.js +7 -0
  57. package/dist/view-controls/controller.d.ts +22 -2
  58. package/dist/view-controls/controller.js +50 -9
  59. package/dist/view-controls/element.d.ts +7 -2
  60. package/dist/view-controls/element.js +84 -26
  61. package/dist/view-controls/math.d.ts +2 -2
  62. package/dist/view-controls/math.js +4 -3
  63. package/package.json +67 -4
@@ -1,5 +1,7 @@
1
1
  // <kazoku-view-controls>: the view controls as a framework-free custom element. It never draws the scene. It binds the pointer, pinch, wheel and keys
2
2
  // to the element named by `for`, and tells the page what the view is now with a `kazoku-viewchange` event; the page applies it to its canvas, WebGL, SVG or CSS 3D.
3
+ import { createAddressWriter } from "../address-state/address.js";
4
+ import { adoptStyles } from "../shared/adopt.js";
3
5
  import { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
4
6
  import { createViewController, DEFAULT_IGNORE } from "./controller.js";
5
7
  import { EVENT, TAG } from "./index.js";
@@ -44,12 +46,13 @@ const HTML = `
44
46
  /**
45
47
  * Attributes: `for` (the id of the view to bind), `lang` (en or ja; else the page's), `yaw`, `tilt` (the starting view), `zoom` (`fit`, the default, or a scale where 1
46
48
  * is the scene's natural size), `fit-scale` (the page's scale at Fit: what zoom 1 means), `ignore` (a selector for what inside the view a pointer press leaves alone;
47
- * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `speed` (degrees a second),
48
- * `flat` (the view cannot turn), `tilt-max`, `zoom-min`, `zoom-max`, `address` (keep the choices in the page's address), `address-keys` (`yaw:a,tilt:b`), `theme` (light
49
+ * buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second),
50
+ * `anchor` (`x,y`: the point the zoom buttons and keys zoom about, in pixels from the view's middle at Fit), `key-tilt` (`drag`, the default, or `wheel`: which way Arrow Up tips),
51
+ * `stop-turn` (a press, the wheel or a key on the view stops the automatic turn), `tilt-step` (round a drag's tilt to a multiple of this many degrees), `flat` (the view cannot turn), `tilt-max`, `zoom-min`, `zoom-max`, `address` (keep the choices in the page's address), `address-keys` (`yaw:a,tilt:b`), `theme` (light
49
52
  * or dark), `overlay` (sit over the bottom of a positioned parent).
50
53
  */
51
54
  export class ViewControlsElement extends HTMLElement {
52
- static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
55
+ static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
53
56
  #root;
54
57
  #controller;
55
58
  #view;
@@ -57,8 +60,11 @@ export class ViewControlsElement extends HTMLElement {
57
60
  #words = {};
58
61
  #ready = false;
59
62
  #sliderHeld = 0;
60
- #saveTimer = 0;
63
+ #writer = createAddressWriter({ delay: 250 });
61
64
  #keys = DEFAULT_KEYS;
65
+ #seen;
66
+ #onScreen = true;
67
+ #suspended = false;
62
68
  constructor() {
63
69
  super();
64
70
  this.#root = this.attachShadow({ mode: "open" });
@@ -98,6 +104,13 @@ export class ViewControlsElement extends HTMLElement {
98
104
  set(next) {
99
105
  this.#ensure().set(next);
100
106
  }
107
+ /** The point the zoom buttons and keys zoom about: `{ x, y }` in CSS pixels from the view's middle when it is at Fit, moving with the picture. `undefined` zooms about the view's middle. The `anchor` attribute (`"x,y"`) sets the same. */
108
+ get anchor() {
109
+ return this.#ensure().anchor;
110
+ }
111
+ set anchor(point) {
112
+ this.#ensure().anchor = point ?? undefined;
113
+ }
101
114
  /** What zoom 1 means to the page: the scene is drawn at `state.zoom * fitScale`. */
102
115
  get fitScale() {
103
116
  return this.#ensure().fitScale;
@@ -122,6 +135,10 @@ export class ViewControlsElement extends HTMLElement {
122
135
  c.turnable = !this.hasAttribute("flat");
123
136
  c.speed = Number(this.getAttribute("speed")) || 30;
124
137
  c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
138
+ for (const name of ["key-tilt", "stop-turn", "tilt-step"])
139
+ this.#option(name);
140
+ if (this.hasAttribute("anchor"))
141
+ this.#option("anchor");
125
142
  const fitScale = Number(this.getAttribute("fit-scale"));
126
143
  if (this.hasAttribute("fit-scale"))
127
144
  c.setFitScale(fitScale);
@@ -133,25 +150,30 @@ export class ViewControlsElement extends HTMLElement {
133
150
  this.ownerDocument.addEventListener("DOMContentLoaded", () => this.#resolveView(), { once: true });
134
151
  this.#ready = true;
135
152
  this.#show();
153
+ this.ownerDocument.addEventListener("visibilitychange", this.#sight);
154
+ this.#watch();
136
155
  // Said once the page has had its turn to listen; a page can also read `state` at any time.
137
156
  queueMicrotask(() => {
138
157
  if (!this.isConnected)
139
158
  return;
140
159
  this.#dispatch(c.state, { final: true, reason: "init" });
141
160
  if (this.hasAttribute("autoplay") && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
142
- c.play();
161
+ c.play(Number(this.getAttribute("autoplay-for")));
143
162
  });
144
163
  }
145
164
  disconnectedCallback() {
165
+ this.ownerDocument.removeEventListener("visibilitychange", this.#sight);
166
+ this.#seen?.disconnect();
167
+ this.#seen = undefined;
168
+ this.#suspended = false;
169
+ this.#onScreen = true;
146
170
  this.#unbind?.();
147
171
  this.#unbind = undefined;
148
172
  this.#controller?.destroy();
149
173
  this.#controller = undefined;
150
174
  this.#view = undefined;
151
175
  this.#ready = false;
152
- if (this.#saveTimer !== 0)
153
- globalThis.clearTimeout(this.#saveTimer);
154
- this.#saveTimer = 0;
176
+ this.#writer.cancel();
155
177
  }
156
178
  attributeChangedCallback(name, _old, value) {
157
179
  if (!this.#ready)
@@ -171,6 +193,8 @@ export class ViewControlsElement extends HTMLElement {
171
193
  c.setFitScale(Number(value));
172
194
  else if (name === "ignore")
173
195
  c.ignore = value ?? DEFAULT_IGNORE;
196
+ else if (name === "anchor" || name === "key-tilt" || name === "stop-turn" || name === "tilt-step")
197
+ this.#option(name);
174
198
  else if (name === "speed")
175
199
  c.speed = Number(value) || 30;
176
200
  else if (name === "flat") {
@@ -182,6 +206,20 @@ export class ViewControlsElement extends HTMLElement {
182
206
  else
183
207
  this.#limits();
184
208
  }
209
+ #option(name) {
210
+ const c = this.#ensure();
211
+ const value = this.getAttribute(name);
212
+ if (name === "anchor") {
213
+ const [x, y] = (value ?? "").trim().split(/[\s,]+/).map(Number);
214
+ c.anchor = x !== undefined && y !== undefined && Number.isFinite(x) && Number.isFinite(y) ? { x, y } : undefined;
215
+ }
216
+ else if (name === "key-tilt")
217
+ c.keyTilt = value === "wheel" ? "wheel" : "drag";
218
+ else if (name === "stop-turn")
219
+ c.stopTurn = value !== null;
220
+ else if (name === "tilt-step")
221
+ c.tiltStep = Number(value) > 0 && Number.isFinite(Number(value)) ? Number(value) : 0;
222
+ }
185
223
  #startZoom() {
186
224
  const value = Number(this.getAttribute("zoom"));
187
225
  return this.hasAttribute("zoom") && Number.isFinite(value) && value > 0 ? value : "fit";
@@ -193,7 +231,8 @@ export class ViewControlsElement extends HTMLElement {
193
231
  return this.#controller;
194
232
  }
195
233
  #build() {
196
- this.#root.innerHTML = `<style>${STYLES}</style>${HTML}`;
234
+ this.#root.innerHTML = HTML;
235
+ adoptStyles(this.#root, STYLES, this.nonce);
197
236
  const q = (selector) => this.#root.querySelector(selector);
198
237
  const c = this.#ensure();
199
238
  const slider = (id) => {
@@ -263,11 +302,40 @@ export class ViewControlsElement extends HTMLElement {
263
302
  this.#unbind?.();
264
303
  this.#unbind = undefined;
265
304
  this.#view = element;
266
- if (element === undefined)
267
- return;
268
305
  // A view takes the keys only if it can be focused: a page that wants arrows and plus and minus on it gives it a tabindex.
269
- this.#unbind = this.#ensure().bind(element);
306
+ if (element !== undefined)
307
+ this.#unbind = this.#ensure().bind(element);
308
+ this.#watch();
309
+ }
310
+ /** The automatic turn runs only while the view (else this element) is on screen in a visible tab; otherwise its frame loop is stopped. */
311
+ #watch() {
312
+ this.#seen?.disconnect();
313
+ this.#seen = undefined;
314
+ this.#onScreen = true;
315
+ if (typeof IntersectionObserver === "function" && this.isConnected) {
316
+ this.#seen = new IntersectionObserver((entries) => {
317
+ const last = entries[entries.length - 1];
318
+ if (last === undefined)
319
+ return;
320
+ this.#onScreen = last.isIntersecting;
321
+ this.#sight();
322
+ });
323
+ this.#seen.observe(this.#view ?? this);
324
+ }
325
+ this.#sight();
270
326
  }
327
+ #sight = () => {
328
+ if (this.#controller === undefined)
329
+ return;
330
+ const idle = !this.#onScreen || this.ownerDocument.visibilityState === "hidden";
331
+ if (idle === this.#suspended)
332
+ return;
333
+ this.#suspended = idle;
334
+ if (idle)
335
+ this.#controller.suspendTurn();
336
+ else
337
+ this.#controller.resumeTurn();
338
+ };
271
339
  #label() {
272
340
  const language = languageOf(this.getAttribute("lang") ?? this.closest("[lang]")?.getAttribute("lang") ?? this.ownerDocument.documentElement.lang);
273
341
  const words = wordsFor(language, this.#words);
@@ -323,19 +391,9 @@ export class ViewControlsElement extends HTMLElement {
323
391
  }
324
392
  /** The address is written at most four times a second: Safari stops a page that changes its address more than a hundred times in thirty seconds. */
325
393
  #save() {
326
- if (this.#saveTimer !== 0)
327
- return;
328
- this.#saveTimer = globalThis.setTimeout(() => {
329
- this.#saveTimer = 0;
330
- try {
331
- const s = this.#ensure().state;
332
- const next = formatAddress(globalThis.location.search, s, this.#keys, s.turning, this.#ensure().startZoom);
333
- if (next !== globalThis.location.search)
334
- globalThis.history.replaceState(globalThis.history.state, "", `${globalThis.location.pathname}${next}${globalThis.location.hash}`);
335
- }
336
- catch {
337
- /* an address that cannot be changed (a sandboxed frame): the view still works */
338
- }
339
- }, 250);
394
+ this.#writer.write((search) => {
395
+ const s = this.#ensure().state;
396
+ return formatAddress(search, s, this.#keys, s.turning, this.#ensure().startZoom);
397
+ });
340
398
  }
341
399
  }
@@ -44,7 +44,7 @@ export interface PinchStart {
44
44
  }
45
45
  /** The camera for two fingers now at `a` and `b`: zoom by the change of distance about where the pinch began, and the middle of the fingers also drags the view. `centre` is the view's middle in client pixels. */
46
46
  export declare function pinchTo(start: PinchStart, a: Point, b: Point, centre: Point, limits?: Limits): Camera;
47
- /** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). */
48
- export declare function dragTurn(start: Camera, dx: number, dy: number, size: Size, tips: boolean, limits?: Limits): Pick<Camera, "yaw" | "tilt">;
47
+ /** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). A `step` above 0 rounds the tilt to a multiple of it. */
48
+ export declare function dragTurn(start: Camera, dx: number, dy: number, size: Size, tips: boolean, limits?: Limits, step?: number): Pick<Camera, "yaw" | "tilt">;
49
49
  /** The factor a Ctrl or Command wheel turn zooms by (a trackpad's pinch arrives that way). `deltaMode` 1 is lines. */
50
50
  export declare const wheelZoomFactor: (deltaY: number, deltaMode: number) => number;
@@ -38,10 +38,11 @@ export function pinchTo(start, a, b, centre, limits = DEFAULT_LIMITS) {
38
38
  return zoomed;
39
39
  return { ...zoomed, panX: zoomed.panX + (middle.x - start.middle.x), panY: zoomed.panY + (middle.y - start.middle.y) };
40
40
  }
41
- /** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). */
42
- export function dragTurn(start, dx, dy, size, tips, limits = DEFAULT_LIMITS) {
41
+ /** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). A `step` above 0 rounds the tilt to a multiple of it. */
42
+ export function dragTurn(start, dx, dy, size, tips, limits = DEFAULT_LIMITS, step = 0) {
43
43
  const yaw = wrapYaw(start.yaw + (dx / (size.width || 360)) * TURN_ACROSS);
44
- const tilt = tips ? clamp(start.tilt - (dy / (size.height || 360)) * TILT_ACROSS, -limits.tiltMax, limits.tiltMax) : start.tilt;
44
+ const raw = start.tilt - (dy / (size.height || 360)) * TILT_ACROSS;
45
+ const tilt = tips ? clamp(step > 0 ? Math.round(raw / step) * step : raw, -limits.tiltMax, limits.tiltMax) : start.tilt;
45
46
  return { yaw, tilt };
46
47
  }
47
48
  /** The factor a Ctrl or Command wheel turn zooms by (a trackpad's pinch arrives that way). `deltaMode` 1 is lines. */
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@johnmorrisdotca/kazoku-ui",
3
- "version": "0.2.1",
4
- "description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package. First part: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) as a framework-free custom element and a plain controller. Zero dependencies.",
3
+ "version": "0.7.2",
4
+ "description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) a light, dark and auto theme switch, a language picker, choices kept in the address and a save-as-image button, each a framework-free custom element and a plain controller. Zero dependencies.",
5
5
  "keywords": [
6
6
  "view-controls",
7
7
  "3d",
@@ -16,7 +16,17 @@
16
16
  "typescript",
17
17
  "javascript",
18
18
  "zero-dependencies",
19
- "browser"
19
+ "browser",
20
+ "theme-switch",
21
+ "dark-mode",
22
+ "language-picker",
23
+ "i18n",
24
+ "address-state",
25
+ "url-state",
26
+ "save-image",
27
+ "png",
28
+ "svg",
29
+ "download"
20
30
  ],
21
31
  "homepage": "https://johnmorrisdotca.github.io/kazoku-ui/",
22
32
  "repository": {
@@ -30,7 +40,10 @@
30
40
  "license": "MIT",
31
41
  "type": "module",
32
42
  "sideEffects": [
33
- "./dist/view-controls/define.js"
43
+ "./dist/view-controls/define.js",
44
+ "./dist/theme-switch/define.js",
45
+ "./dist/language-picker/define.js",
46
+ "./dist/save-image/define.js"
34
47
  ],
35
48
  "main": "./dist/index.js",
36
49
  "module": "./dist/index.js",
@@ -55,6 +68,56 @@
55
68
  "types": "./dist/view-controls/define.d.ts",
56
69
  "import": "./dist/view-controls/define.js",
57
70
  "default": "./dist/view-controls/define.js"
71
+ },
72
+ "./theme-switch": {
73
+ "types": "./dist/theme-switch/index.d.ts",
74
+ "import": "./dist/theme-switch/index.js",
75
+ "default": "./dist/theme-switch/index.js"
76
+ },
77
+ "./theme-switch/element": {
78
+ "types": "./dist/theme-switch/element.d.ts",
79
+ "import": "./dist/theme-switch/element.js",
80
+ "default": "./dist/theme-switch/element.js"
81
+ },
82
+ "./theme-switch/define": {
83
+ "types": "./dist/theme-switch/define.d.ts",
84
+ "import": "./dist/theme-switch/define.js",
85
+ "default": "./dist/theme-switch/define.js"
86
+ },
87
+ "./language-picker": {
88
+ "types": "./dist/language-picker/index.d.ts",
89
+ "import": "./dist/language-picker/index.js",
90
+ "default": "./dist/language-picker/index.js"
91
+ },
92
+ "./language-picker/element": {
93
+ "types": "./dist/language-picker/element.d.ts",
94
+ "import": "./dist/language-picker/element.js",
95
+ "default": "./dist/language-picker/element.js"
96
+ },
97
+ "./language-picker/define": {
98
+ "types": "./dist/language-picker/define.d.ts",
99
+ "import": "./dist/language-picker/define.js",
100
+ "default": "./dist/language-picker/define.js"
101
+ },
102
+ "./address-state": {
103
+ "types": "./dist/address-state/index.d.ts",
104
+ "import": "./dist/address-state/index.js",
105
+ "default": "./dist/address-state/index.js"
106
+ },
107
+ "./save-image": {
108
+ "types": "./dist/save-image/index.d.ts",
109
+ "import": "./dist/save-image/index.js",
110
+ "default": "./dist/save-image/index.js"
111
+ },
112
+ "./save-image/element": {
113
+ "types": "./dist/save-image/element.d.ts",
114
+ "import": "./dist/save-image/element.js",
115
+ "default": "./dist/save-image/element.js"
116
+ },
117
+ "./save-image/define": {
118
+ "types": "./dist/save-image/define.d.ts",
119
+ "import": "./dist/save-image/define.js",
120
+ "default": "./dist/save-image/define.js"
58
121
  }
59
122
  },
60
123
  "files": [