@johnmorrisdotca/kazoku-ui 0.15.2 → 0.17.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/CHANGELOG.md +18 -0
- package/README.md +7 -6
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/replay-bar/define.d.ts +1 -0
- package/dist/replay-bar/define.js +9 -0
- package/dist/replay-bar/element.d.ts +34 -0
- package/dist/replay-bar/element.js +219 -0
- package/dist/replay-bar/index.d.ts +4 -0
- package/dist/replay-bar/index.js +5 -0
- package/dist/replay-bar/state.d.ts +19 -0
- package/dist/replay-bar/state.js +27 -0
- package/dist/replay-bar/styles.d.ts +1 -0
- package/dist/replay-bar/styles.js +8 -0
- package/dist/replay-bar/words.d.ts +17 -0
- package/dist/replay-bar/words.js +7 -0
- package/dist/view-controls/address.d.ts +20 -0
- package/dist/view-controls/address.js +30 -4
- package/dist/view-controls/controller.d.ts +13 -2
- package/dist/view-controls/controller.js +72 -9
- package/dist/view-controls/element.d.ts +3 -2
- package/dist/view-controls/element.js +216 -27
- package/dist/view-controls/index.d.ts +2 -2
- package/dist/view-controls/index.js +2 -2
- package/dist/view-controls/levels.d.ts +6 -0
- package/dist/view-controls/levels.js +2 -0
- package/dist/view-controls/math.d.ts +50 -0
- package/dist/view-controls/math.js +73 -2
- package/dist/view-controls/sight.js +23 -0
- package/dist/view-controls/styles.d.ts +1 -1
- package/dist/view-controls/styles.js +22 -10
- package/dist/view-controls/words.d.ts +9 -0
- package/dist/view-controls/words.js +12 -0
- package/package.json +18 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,24 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
- **The vertical demo sits beside the view.** The axis chips made that column tall enough that Centre's row scrolled the page and Auto-turn left the pointer. On a desk the column stands beside the scene, so Centre adds its row and Auto-turn stays where it was.
|
|
6
|
+
|
|
7
|
+
## [0.17.0] - 2026-11-05
|
|
8
|
+
|
|
9
|
+
The view turns on all three axes (KAZOKU-UI-55), and a shared replay bar (KAZOKU-UI-57).
|
|
10
|
+
|
|
11
|
+
- **Roll, beside Angle and Tilt.** One slider, and chips for Angle, Tilt, Roll and All. The slider sets the axis the chips name (All keeps the angle). `roll` and `roll-max` (180) sit next to `tilt` and `tilt-max`.
|
|
12
|
+
- **Auto-turn chooses an axis:** Angle, Tilt, Roll or All. All is a tumble: three rates that drift, so it does not repeat. Each axis has its own direction. Tilt swings inside `tilt-max` and, at 90 or more, rolls over on a quaternion (`orientation` on the event) so the up axis does not flip. Off under reduced motion, as before. The choice is the `axis` attribute and `?axis=all`.
|
|
13
|
+
- **`<kazoku-replay-bar>`** (KAZOKU-UI-57). Play, pause, back to the start and repeat, with the family's speed buttons on the same row. The bar reports the choice (`kazoku-replay` with `{ action, playing, repeat, reason }`) and the page moves the thing. `playing` and `repeat` are attributes; `address` keeps them (`play=on`, `repeat=on`), and `address-keys` can rename either. The speed buttons use `steps` and `speed`, and their own address key `replay-speed` (`speed-key` renames it), so they do not share a view's `pace`. Reduced motion leaves play off. New part: `@johnmorrisdotca/kazoku-ui/replay-bar`, `/element` and `/define`.
|
|
14
|
+
|
|
15
|
+
## [0.16.0] - 2026-11-04
|
|
16
|
+
|
|
17
|
+
Which way Auto-turn goes is the reader's to choose.
|
|
18
|
+
|
|
19
|
+
- **Left and Right buttons** (KAZOKU-UI-34 and KAZOKU-UI-22). Under More, beside the speed buttons, a pair of buttons chooses which way Auto-turn goes. The choice is kept in the new `direction` attribute (`left` or `right`) and in the address as `?direction=left`; the angle goes on from where it was, at the same speed. By the drag rule `yaw` goes up as the hand goes right, so right is the positive `speed`. With no `direction`, the sign of `speed` decides (a negative `speed` starts the turn to the left, as before), and the address keeps a way only when it is not the page's own start. `address-keys` can rename the key (`direction:dir`). New words in both languages: `direction` (Turn direction, 回る向き), `turnLeft` (Left, 左へ), `turnRight` (Right, 右へ). A package with its own Reverse button (senshu-jerseys, dougu-sports' old `?dir=cw`) can drop it for these.
|
|
20
|
+
- **`autoplay-for` limits every turn** (KAZOKU-UI-22). A turn the Auto-turn button or `element.play()` starts now stops after `autoplay-for` seconds of turning, as the one `autoplay` starts always did; `play(seconds)` takes a limit of its own. Without `autoplay-for` nothing changes. A page that sets `autoplay-for` and wants its Auto-turn button unlimited keeps its own timer instead.
|
|
21
|
+
- **Flaky tests fixed at the cause** (KAZOKU-UI-11, 31, 25, 36, 32; tests and a template only, no change to the package's code). `address-state` wrote the page's own choice a quarter of a second after the click and the theme and language at once, so the order of the parameters was the order of the writes, which a slow machine changes: the test compares the parts, not the order (13 of 160 runs failed before, 0 of 160 after). The renderer's "turning draws once a frame" counted the stills' draws as the turn's and a fixed 800 ms of a slow software frame as too few: it now waits for the stills to finish and for the turn to be drawing, and for the last frame after the pause. The autoplay tests' "it turns while the view is on screen" waited a fixed 700 ms: they wait for the changes to arrive. The docs site's Tab test (new family template `templates/docs-site.demo.mjs`, to replace the copy in chikyu-earth and wakusei-planets) presses Alt+Tab where WebKit on a Mac does not Tab to links.
|
|
22
|
+
|
|
5
23
|
## [0.15.2] - 2026-11-04
|
|
6
24
|
|
|
7
25
|
The embed panel no longer scrolls the page.
|
package/README.md
CHANGED
|
@@ -172,7 +172,7 @@ Each its own subpath; later ones: picture-tile pickers, Help, GitHub and npm pil
|
|
|
172
172
|
|
|
173
173
|
- **Two levels, so the bar stays simple.** The default, `controls="basic"`, shows the angle slider, Auto-turn, zoom out and in with a **zoom readout** (`2.4×`; tap it to go back to Fit), Fit, and a small More button; while the view turns, one small **speed** button beside Auto-turn (it shows the word Speed over `1×`, and a press steps to the next speed). More (or `controls="advanced"`) adds the tilt slider, the speed buttons (0.5×, 1×, 2×), Front, and, once zoomed in, the four-arrow pan pad and the Move or Turn choice for what a drag does. The reader's choice with More is remembered in `localStorage` and beats the attribute.
|
|
174
174
|
- The zoom readout is the view's `zoom` (1× is Fit, whatever `fit-scale` is), one decimal, the one zoom number on the bar: it follows the buttons, the wheel, a pinch, a double press, Fit, the keys, the address, `set` and a changed limit. A screen reader is told "Zoom level 2.4×" once the zoom has settled for a moment, not for every step of a pinch.
|
|
175
|
-
- Auto-turn with play and pause; its speed is chosen with the family's [speed buttons](docs/guides/speed-buttons.md): 0.5× (half the `speed` attribute), 1× (the `speed` attribute, 30 degrees a second) or 2× (twice it), kept in the `pace` attribute and in the address as `?pace=0.5` or `?pace=2`.
|
|
175
|
+
- Auto-turn with play and pause; its speed is chosen with the family's [speed buttons](docs/guides/speed-buttons.md): 0.5× (half the `speed` attribute), 1× (the `speed` attribute, 30 degrees a second) or 2× (twice it), kept in the `pace` attribute and in the address as `?pace=0.5` or `?pace=2`. **Left and Right** (under More, beside the speed buttons) choose which way it goes, kept in the `direction` attribute and in the address as `?direction=left`; the angle goes on from where it was, at the same speed.
|
|
176
176
|
- **The wheel zooms.** The wheel over the view zooms it about the pointer, like a map; a trackpad's pinch (and Ctrl or Command with the wheel) zooms too. Shift and the wheel, or a sideways swipe, turns it. Tipping is a drag, the arrow keys or the tilt slider. With the pointer anywhere else the wheel scrolls the page, and a wheel that starts while the zoom is already at its end that way (turned down at Fit) scrolls the page too, so the page is never trapped.
|
|
177
177
|
- Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
|
|
178
178
|
- Keys on the view: arrows turn and tip it, `+` and `-` zoom, `0` or Home is Front and Fit.
|
|
@@ -362,8 +362,9 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
362
362
|
| `object-size` | `w,h`: the object's size at Fit in CSS pixels, when it is smaller than the view. A pan then keeps at least a quarter of the object in view, however far it is dragged; left out, the object is the picture, which keeps covering the view. Or set `element.objectSize = { width, height }`. |
|
|
363
363
|
| `grab-radius` | Exact grab, for a globe or any turnable piece: its on-screen radius in CSS pixels at Fit, or `fit` for half the smaller side of the view. A drag then turns it by the angle that keeps the touched point under the pointer (a drag of *d* pixels moves the ground at the middle by *d* pixels), and the radius grows with the zoom. Left out, a drag turns by a fixed angle across the view, as before. See [docs/drag-rule.md](docs/drag-rule.md). |
|
|
364
364
|
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
365
|
-
| `autoplay-for` | Seconds of turning after which
|
|
366
|
-
| `speed` | Degrees a second at a pace of 1 (30). |
|
|
365
|
+
| `autoplay-for` | Seconds of turning after which a turn stops by itself (as Auto-turn pressed): the one `autoplay` starts, and one the Auto-turn button or `play()` starts. Left out, a turn never stops. Time spent paused off screen or in a hidden tab is not counted. `play(seconds)` takes its own limit. |
|
|
366
|
+
| `speed` | Degrees a second at a pace of 1 (30). A negative number starts the turn to the left. |
|
|
367
|
+
| `direction` | Which way Auto-turn goes, `left` or `right` (by the drag rule `yaw` goes up as the hand goes right, so right is the positive `speed`). The reader's choice of the Left and Right buttons (under More) is written here and kept in the address as `direction`; the angle goes on from where it was, at the same speed. Left out, the sign of `speed` decides, and the address keeps a way only when it is not that start. |
|
|
367
368
|
| `pace` | The Auto-turn speed as a multiple of `speed`: 1 by default, 0.5 and 2 on the default buttons. The reader's choice is written here, and kept in the address as `pace`. |
|
|
368
369
|
| `paces` | The speeds the buttons stand for, `0.5 1 2` by default (any numbers: `paces="1 4 8"`). |
|
|
369
370
|
| `controls` | `basic` (the default) or `advanced`: which controls are shown (see above; the zoom readout in both, the speed in both while the view turns). Written in the page's markup it is where the reader starts; what the reader chose with More is remembered under `kazoku-view-controls` in `localStorage` (or the key `storage` names) and wins. Setting the attribute later changes the level at once. The host carries `data-level` with the level now. |
|
|
@@ -371,7 +372,7 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
371
372
|
| `storage` | The `localStorage` key the level is remembered under (`kazoku-view-controls`). |
|
|
372
373
|
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
373
374
|
| `tilt-max`, `zoom-min`, `zoom-max` | The limits (30, 1, 5). |
|
|
374
|
-
| `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn&pace=2`). |
|
|
375
|
+
| `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn&pace=2&direction=left`). |
|
|
375
376
|
| `address-keys` | Other keys: `yaw:a,zoom:z`. |
|
|
376
377
|
| `theme` | `light` or `dark`; otherwise the page's colour scheme. |
|
|
377
378
|
| `overlay` | Sit over the bottom of a positioned parent. With `layout="compact"` the row floats 8 px in from the bottom start corner instead of spanning the bottom. |
|
|
@@ -381,7 +382,7 @@ Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-lin
|
|
|
381
382
|
|
|
382
383
|
### The address
|
|
383
384
|
|
|
384
|
-
Only what a reader chose is kept: angle, tilt, zoom, the drag choice
|
|
385
|
+
Only what a reader chose is kept: angle, tilt, zoom, the drag choice, the auto-turn speed (`pace=0.5` or `pace=2`; 1 leaves the address) and the way it turns (`direction=left` or `direction=right`; the page's own start leaves the address). A page's own `speed` parameter is never touched, and `?pace=slow` or `fast` of 0.12 and 0.13 still read as 0.5 and 2. The pan is in pixels of one screen and the turning is of a moment, so neither is; while the view turns by itself the angle is left out. A value at its start (Fit, or the `zoom` the page opens at) leaves the address, and other parameters are kept. The address is written at most four times a second (Safari stops a page that changes its address more than 100 times in 30 seconds). Without the element: `parseAddress(search, keys?)` and `formatAddress(search, choices, keys?, leaveYaw?, startZoom?)`.
|
|
385
386
|
|
|
386
387
|
### Why the tag is only defined when it is free
|
|
387
388
|
|
|
@@ -552,7 +553,7 @@ One part, one subpath; the pure code, the element and the registration are three
|
|
|
552
553
|
|
|
553
554
|
## Demos
|
|
554
555
|
|
|
555
|
-
[The demo](https://johnmorrisdotca.github.io/kazoku-ui/) has the view controls, the language picker and the theme switch in its header. Beside it: [speed buttons](https://johnmorrisdotca.github.io/kazoku-ui/speed.html), [layouts](https://johnmorrisdotca.github.io/kazoku-ui/layouts.html), [the builder](https://johnmorrisdotca.github.io/kazoku-ui/builder.html), [address state](https://johnmorrisdotca.github.io/kazoku-ui/address.html), [save image](https://johnmorrisdotca.github.io/kazoku-ui/save-image.html), [the shared renderer](https://johnmorrisdotca.github.io/kazoku-ui/renderer.html), [exact grab](https://johnmorrisdotca.github.io/kazoku-ui/grab.html) and [a strict Content-Security-Policy](https://johnmorrisdotca.github.io/kazoku-ui/csp.html).
|
|
556
|
+
[The demo](https://johnmorrisdotca.github.io/kazoku-ui/) has the view controls, the language picker and the theme switch in its header. Beside it: [speed buttons](https://johnmorrisdotca.github.io/kazoku-ui/speed.html), [the replay bar](https://johnmorrisdotca.github.io/kazoku-ui/replay.html), [layouts](https://johnmorrisdotca.github.io/kazoku-ui/layouts.html), [the builder](https://johnmorrisdotca.github.io/kazoku-ui/builder.html), [address state](https://johnmorrisdotca.github.io/kazoku-ui/address.html), [save image](https://johnmorrisdotca.github.io/kazoku-ui/save-image.html), [the shared renderer](https://johnmorrisdotca.github.io/kazoku-ui/renderer.html), [exact grab](https://johnmorrisdotca.github.io/kazoku-ui/grab.html) and [a strict Content-Security-Policy](https://johnmorrisdotca.github.io/kazoku-ui/csp.html).
|
|
556
557
|
|
|
557
558
|
<picture>
|
|
558
559
|
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kazoku-ui/main/docs/images/builder-dark.webp">
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
/** The subpaths of the parts, in the order they were added. */
|
|
2
|
-
export declare const PARTS: readonly ["view-controls", "theme-switch", "language-picker", "address-state", "save-image", "speed-buttons", "embed-panel", "renderer", "testing"];
|
|
2
|
+
export declare const PARTS: readonly ["view-controls", "theme-switch", "language-picker", "address-state", "save-image", "speed-buttons", "embed-panel", "renderer", "testing", "replay-bar"];
|
|
3
3
|
export type Part = (typeof PARTS)[number];
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
// Kazoku UI 家族: the family's shared interface parts, each its own subpath so a page loads only what it uses.
|
|
2
2
|
/** The subpaths of the parts, in the order they were added. */
|
|
3
|
-
export const PARTS = ["view-controls", "theme-switch", "language-picker", "address-state", "save-image", "speed-buttons", "embed-panel", "renderer", "testing"];
|
|
3
|
+
export const PARTS = ["view-controls", "theme-switch", "language-picker", "address-state", "save-image", "speed-buttons", "embed-panel", "renderer", "testing", "replay-bar"];
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function defineReplayBar(registry?: CustomElementRegistry): boolean;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Importing this registers <kazoku-replay-bar>, unless the tag is already taken (the first definition wins).
|
|
2
|
+
import { ReplayBarElement, TAG } from "./element.js";
|
|
3
|
+
export function defineReplayBar(registry = customElements) {
|
|
4
|
+
if (registry.get(TAG) !== undefined)
|
|
5
|
+
return false;
|
|
6
|
+
registry.define(TAG, ReplayBarElement);
|
|
7
|
+
return true;
|
|
8
|
+
}
|
|
9
|
+
defineReplayBar();
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import "../speed-buttons/define.ts";
|
|
2
|
+
import { EVENT, TAG } from "./index.ts";
|
|
3
|
+
import { type ReplayWords } from "./words.ts";
|
|
4
|
+
export { EVENT, TAG };
|
|
5
|
+
/** What the `kazoku-replay` event carries in `detail`. */
|
|
6
|
+
export interface ReplayDetail {
|
|
7
|
+
/** What was pressed. `play` and `pause` are the one button. */
|
|
8
|
+
action: "play" | "pause" | "start" | "repeat";
|
|
9
|
+
playing: boolean;
|
|
10
|
+
repeat: boolean;
|
|
11
|
+
/** `init` (once, when it comes up), `user` (a button) or `set` (a property or attribute). */
|
|
12
|
+
reason: "init" | "user" | "set";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Attributes: `playing` (present, or `on`, while it plays), `repeat` (the loop), `steps` and `speed` (handed to the speed buttons),
|
|
16
|
+
* `address` (keep play, repeat and the speed in the address), `address-keys` (`play:p,repeat:r`), `speed-key` (the speed's own key, `replay-speed` by default),
|
|
17
|
+
* `lang`, `theme`.
|
|
18
|
+
*/
|
|
19
|
+
export declare class ReplayBarElement extends HTMLElement {
|
|
20
|
+
#private;
|
|
21
|
+
static observedAttributes: string[];
|
|
22
|
+
constructor();
|
|
23
|
+
/** Whether playback is going. Reduced motion keeps this false. */
|
|
24
|
+
get playing(): boolean;
|
|
25
|
+
set playing(next: boolean);
|
|
26
|
+
/** Whether the page should loop when it reaches the end. */
|
|
27
|
+
get repeat(): boolean;
|
|
28
|
+
set repeat(next: boolean);
|
|
29
|
+
/** Your own labels over the language's. */
|
|
30
|
+
set words(words: Partial<ReplayWords>);
|
|
31
|
+
connectedCallback(): void;
|
|
32
|
+
disconnectedCallback(): void;
|
|
33
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
34
|
+
}
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
// <kazoku-replay-bar>: play, pause, back to the start, repeat, and the family's speed buttons, in one row. It moves nothing itself.
|
|
2
|
+
// The page hears `kazoku-replay` and applies it, and hears `kazoku-speedchange` from the speed buttons. Reduced motion leaves play off.
|
|
3
|
+
import { applyParams, createAddressWriter, readParam } from "../address-state/address.js";
|
|
4
|
+
import { adoptStyles } from "../shared/adopt.js";
|
|
5
|
+
import { watchScheme } from "../shared/scheme.js";
|
|
6
|
+
import "../speed-buttons/define.js";
|
|
7
|
+
import { EVENT, TAG } from "./index.js";
|
|
8
|
+
import { addressKeys, DEFAULT_STEPS, flagOn, SPEED_KEY } from "./state.js";
|
|
9
|
+
import { STYLES } from "./styles.js";
|
|
10
|
+
import { languageOf, wordsFor } from "./words.js";
|
|
11
|
+
export { EVENT, TAG };
|
|
12
|
+
/**
|
|
13
|
+
* Attributes: `playing` (present, or `on`, while it plays), `repeat` (the loop), `steps` and `speed` (handed to the speed buttons),
|
|
14
|
+
* `address` (keep play, repeat and the speed in the address), `address-keys` (`play:p,repeat:r`), `speed-key` (the speed's own key, `replay-speed` by default),
|
|
15
|
+
* `lang`, `theme`.
|
|
16
|
+
*/
|
|
17
|
+
export class ReplayBarElement extends HTMLElement {
|
|
18
|
+
static observedAttributes = ["playing", "repeat", "steps", "speed", "lang", "label"];
|
|
19
|
+
#root;
|
|
20
|
+
#playing = false;
|
|
21
|
+
#repeat = false;
|
|
22
|
+
#ready = false;
|
|
23
|
+
#styled = false;
|
|
24
|
+
#words = {};
|
|
25
|
+
#writer = createAddressWriter({ delay: 0 });
|
|
26
|
+
#stop;
|
|
27
|
+
#motion;
|
|
28
|
+
#reflecting = false;
|
|
29
|
+
constructor() {
|
|
30
|
+
super();
|
|
31
|
+
this.#root = this.attachShadow({ mode: "open" });
|
|
32
|
+
}
|
|
33
|
+
/** Whether playback is going. Reduced motion keeps this false. */
|
|
34
|
+
get playing() {
|
|
35
|
+
return this.#playing;
|
|
36
|
+
}
|
|
37
|
+
set playing(next) {
|
|
38
|
+
this.#play(next, "set");
|
|
39
|
+
}
|
|
40
|
+
/** Whether the page should loop when it reaches the end. */
|
|
41
|
+
get repeat() {
|
|
42
|
+
return this.#repeat;
|
|
43
|
+
}
|
|
44
|
+
set repeat(next) {
|
|
45
|
+
this.#loop(next, "set");
|
|
46
|
+
}
|
|
47
|
+
/** Your own labels over the language's. */
|
|
48
|
+
set words(words) {
|
|
49
|
+
this.#words = words;
|
|
50
|
+
this.#paint();
|
|
51
|
+
}
|
|
52
|
+
connectedCallback() {
|
|
53
|
+
if (!this.#styled) {
|
|
54
|
+
const outer = this.getRootNode();
|
|
55
|
+
adoptStyles(this.#root, STYLES, this.nonce || (outer instanceof ShadowRoot ? outer.host.nonce : "") || "");
|
|
56
|
+
this.#styled = true;
|
|
57
|
+
}
|
|
58
|
+
this.#stop = watchScheme(this);
|
|
59
|
+
this.#motion = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)");
|
|
60
|
+
this.#motion?.addEventListener?.("change", this.#onMotion);
|
|
61
|
+
const keys = addressKeys(this.getAttribute("address-keys"));
|
|
62
|
+
const askedPlay = this.hasAttribute("address") ? flagOn(readParam(keys.play)) : this.hasAttribute("playing") && this.getAttribute("playing") !== "off";
|
|
63
|
+
const askedRepeat = this.hasAttribute("address") ? flagOn(readParam(keys.repeat)) : this.hasAttribute("repeat") && this.getAttribute("repeat") !== "off";
|
|
64
|
+
this.#playing = askedPlay && !this.#reduced();
|
|
65
|
+
this.#repeat = askedRepeat;
|
|
66
|
+
this.#reflect("playing", this.#playing);
|
|
67
|
+
this.#reflect("repeat", this.#repeat);
|
|
68
|
+
this.#render();
|
|
69
|
+
this.#ready = true;
|
|
70
|
+
queueMicrotask(() => {
|
|
71
|
+
if (this.isConnected)
|
|
72
|
+
this.#dispatch(this.#playing ? "play" : "pause", "init");
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
disconnectedCallback() {
|
|
76
|
+
this.#stop?.();
|
|
77
|
+
this.#stop = undefined;
|
|
78
|
+
this.#motion?.removeEventListener?.("change", this.#onMotion);
|
|
79
|
+
this.#writer.cancel();
|
|
80
|
+
this.#ready = false;
|
|
81
|
+
}
|
|
82
|
+
attributeChangedCallback(name, _old, value) {
|
|
83
|
+
if (!this.#ready || this.#reflecting)
|
|
84
|
+
return;
|
|
85
|
+
if (name === "playing")
|
|
86
|
+
this.#play(value !== "off" && value !== null, "set");
|
|
87
|
+
else if (name === "repeat")
|
|
88
|
+
this.#loop(value !== "off" && value !== null, "set");
|
|
89
|
+
else if (name === "steps" || name === "speed")
|
|
90
|
+
this.#speeds();
|
|
91
|
+
else
|
|
92
|
+
this.#paint();
|
|
93
|
+
}
|
|
94
|
+
#reduced() {
|
|
95
|
+
return this.#motion?.matches === true;
|
|
96
|
+
}
|
|
97
|
+
#onMotion = () => {
|
|
98
|
+
if (this.#reduced() && this.#playing)
|
|
99
|
+
this.#play(false, "set");
|
|
100
|
+
else
|
|
101
|
+
this.#paint();
|
|
102
|
+
};
|
|
103
|
+
#play(next, reason) {
|
|
104
|
+
const on = next && !this.#reduced();
|
|
105
|
+
if (on === this.#playing) {
|
|
106
|
+
this.#paint();
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
this.#playing = on;
|
|
110
|
+
this.#reflect("playing", on);
|
|
111
|
+
this.#paint();
|
|
112
|
+
this.#write();
|
|
113
|
+
this.#dispatch(on ? "play" : "pause", reason);
|
|
114
|
+
}
|
|
115
|
+
#loop(next, reason) {
|
|
116
|
+
if (next === this.#repeat)
|
|
117
|
+
return;
|
|
118
|
+
this.#repeat = next;
|
|
119
|
+
this.#reflect("repeat", next);
|
|
120
|
+
this.#paint();
|
|
121
|
+
this.#write();
|
|
122
|
+
this.#dispatch("repeat", reason);
|
|
123
|
+
}
|
|
124
|
+
#reflect(name, on) {
|
|
125
|
+
const have = this.hasAttribute(name) && this.getAttribute(name) !== "off";
|
|
126
|
+
if (have === on)
|
|
127
|
+
return;
|
|
128
|
+
this.#reflecting = true;
|
|
129
|
+
if (on)
|
|
130
|
+
this.setAttribute(name, "");
|
|
131
|
+
else
|
|
132
|
+
this.removeAttribute(name);
|
|
133
|
+
this.#reflecting = false;
|
|
134
|
+
}
|
|
135
|
+
#write() {
|
|
136
|
+
if (!this.hasAttribute("address"))
|
|
137
|
+
return;
|
|
138
|
+
const keys = addressKeys(this.getAttribute("address-keys"));
|
|
139
|
+
this.#writer.write((search) => applyParams(search, { [keys.play]: this.#playing ? "on" : null, [keys.repeat]: this.#repeat ? "on" : null }));
|
|
140
|
+
}
|
|
141
|
+
#speeds() {
|
|
142
|
+
const speeds = this.#root.querySelector("kazoku-speed-buttons");
|
|
143
|
+
if (!speeds)
|
|
144
|
+
return;
|
|
145
|
+
speeds.setAttribute("steps", this.getAttribute("steps") || DEFAULT_STEPS);
|
|
146
|
+
const speed = this.getAttribute("speed");
|
|
147
|
+
if (speed)
|
|
148
|
+
speeds.setAttribute("value", speed);
|
|
149
|
+
}
|
|
150
|
+
#render() {
|
|
151
|
+
const doc = this.ownerDocument;
|
|
152
|
+
const row = doc.createElement("div");
|
|
153
|
+
row.className = "row";
|
|
154
|
+
row.setAttribute("role", "group");
|
|
155
|
+
for (const act of ["play", "start", "repeat"]) {
|
|
156
|
+
const button = doc.createElement("button");
|
|
157
|
+
button.type = "button";
|
|
158
|
+
button.dataset.act = act;
|
|
159
|
+
button.setAttribute("part", act);
|
|
160
|
+
button.addEventListener("click", () => this.#press(act));
|
|
161
|
+
row.append(button);
|
|
162
|
+
}
|
|
163
|
+
const speeds = doc.createElement("kazoku-speed-buttons");
|
|
164
|
+
speeds.setAttribute("no-label", "");
|
|
165
|
+
speeds.setAttribute("steps", this.getAttribute("steps") || DEFAULT_STEPS);
|
|
166
|
+
if (this.hasAttribute("address")) {
|
|
167
|
+
speeds.setAttribute("address", "");
|
|
168
|
+
speeds.setAttribute("address-key", this.getAttribute("speed-key") || SPEED_KEY);
|
|
169
|
+
}
|
|
170
|
+
const speed = this.getAttribute("speed");
|
|
171
|
+
if (speed)
|
|
172
|
+
speeds.setAttribute("value", speed);
|
|
173
|
+
row.append(speeds);
|
|
174
|
+
this.#root.replaceChildren(row);
|
|
175
|
+
this.#paint();
|
|
176
|
+
}
|
|
177
|
+
#press(act) {
|
|
178
|
+
if (act === "play")
|
|
179
|
+
this.#play(!this.#playing, "user");
|
|
180
|
+
else if (act === "repeat")
|
|
181
|
+
this.#loop(!this.#repeat, "user");
|
|
182
|
+
else {
|
|
183
|
+
if (this.#playing) {
|
|
184
|
+
this.#playing = false;
|
|
185
|
+
this.#reflect("playing", false);
|
|
186
|
+
this.#write();
|
|
187
|
+
}
|
|
188
|
+
this.#paint();
|
|
189
|
+
this.#dispatch("start", "user");
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
#paint() {
|
|
193
|
+
const language = languageOf(this.getAttribute("lang") ?? this.closest("[lang]")?.getAttribute("lang") ?? this.ownerDocument.documentElement.lang);
|
|
194
|
+
const words = wordsFor(language, this.#words);
|
|
195
|
+
const row = this.#root.querySelector(".row");
|
|
196
|
+
if (!row)
|
|
197
|
+
return;
|
|
198
|
+
row.setAttribute("aria-label", this.getAttribute("label") || words.group);
|
|
199
|
+
const play = row.querySelector('[data-act="play"]');
|
|
200
|
+
const start = row.querySelector('[data-act="start"]');
|
|
201
|
+
const repeat = row.querySelector('[data-act="repeat"]');
|
|
202
|
+
if (play) {
|
|
203
|
+
play.textContent = this.#playing ? words.pause : words.play;
|
|
204
|
+
play.setAttribute("aria-pressed", String(this.#playing));
|
|
205
|
+
play.disabled = this.#reduced();
|
|
206
|
+
}
|
|
207
|
+
if (start)
|
|
208
|
+
start.textContent = words.start;
|
|
209
|
+
if (repeat) {
|
|
210
|
+
repeat.textContent = words.repeat;
|
|
211
|
+
repeat.setAttribute("aria-pressed", String(this.#repeat));
|
|
212
|
+
}
|
|
213
|
+
const speeds = row.querySelector("kazoku-speed-buttons");
|
|
214
|
+
speeds?.setAttribute("lang", language);
|
|
215
|
+
}
|
|
216
|
+
#dispatch(action, reason) {
|
|
217
|
+
this.dispatchEvent(new CustomEvent(EVENT, { detail: { action, playing: this.#playing, repeat: this.#repeat, reason }, bubbles: true, composed: true }));
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { addressKeys, DEFAULT_STEPS, flagOn, PLAY_KEY, REPEAT_KEY, SPEED_KEY } from "./state.ts";
|
|
2
|
+
export { languageOf, WORDS, wordsFor, type Language, type ReplayWords } from "./words.ts";
|
|
3
|
+
export declare const EVENT = "kazoku-replay";
|
|
4
|
+
export declare const TAG = "kazoku-replay-bar";
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// The replay bar without the element: the address keys and the on/off reading. Nothing here touches the DOM, so it imports on a server.
|
|
2
|
+
export { addressKeys, DEFAULT_STEPS, flagOn, PLAY_KEY, REPEAT_KEY, SPEED_KEY } from "./state.js";
|
|
3
|
+
export { languageOf, WORDS, wordsFor } from "./words.js";
|
|
4
|
+
export const EVENT = "kazoku-replay";
|
|
5
|
+
export const TAG = "kazoku-replay-bar";
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** The address key for playing, unless `address-keys` names another. */
|
|
2
|
+
export declare const PLAY_KEY = "play";
|
|
3
|
+
/** The address key for the repeat loop, unless `address-keys` names another. */
|
|
4
|
+
export declare const REPEAT_KEY = "repeat";
|
|
5
|
+
/** The address key the bar's speed buttons use, so they do not share a view's `pace`. */
|
|
6
|
+
export declare const SPEED_KEY = "replay-speed";
|
|
7
|
+
/** A replay's usual speeds. A page sets its own with `steps`. */
|
|
8
|
+
export declare const DEFAULT_STEPS = "0.25 0.5 1 2 4";
|
|
9
|
+
/** Whether an address or attribute value means on. Anything else, including nothing, is off. */
|
|
10
|
+
export declare function flagOn(value: string | null | undefined): boolean;
|
|
11
|
+
/**
|
|
12
|
+
* The address keys for play and repeat. `play:p,repeat:r` renames either; an unknown name is ignored.
|
|
13
|
+
* @param spec The `address-keys` attribute.
|
|
14
|
+
* @returns The key for each choice.
|
|
15
|
+
*/
|
|
16
|
+
export declare function addressKeys(spec: string | null | undefined): {
|
|
17
|
+
play: string;
|
|
18
|
+
repeat: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// The replay bar's own choices, with no DOM, so a server can read an address the same way the element does.
|
|
2
|
+
/** The address key for playing, unless `address-keys` names another. */
|
|
3
|
+
export const PLAY_KEY = "play";
|
|
4
|
+
/** The address key for the repeat loop, unless `address-keys` names another. */
|
|
5
|
+
export const REPEAT_KEY = "repeat";
|
|
6
|
+
/** The address key the bar's speed buttons use, so they do not share a view's `pace`. */
|
|
7
|
+
export const SPEED_KEY = "replay-speed";
|
|
8
|
+
/** A replay's usual speeds. A page sets its own with `steps`. */
|
|
9
|
+
export const DEFAULT_STEPS = "0.25 0.5 1 2 4";
|
|
10
|
+
/** Whether an address or attribute value means on. Anything else, including nothing, is off. */
|
|
11
|
+
export function flagOn(value) {
|
|
12
|
+
return value === "on" || value === "true" || value === "1";
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* The address keys for play and repeat. `play:p,repeat:r` renames either; an unknown name is ignored.
|
|
16
|
+
* @param spec The `address-keys` attribute.
|
|
17
|
+
* @returns The key for each choice.
|
|
18
|
+
*/
|
|
19
|
+
export function addressKeys(spec) {
|
|
20
|
+
const out = { play: PLAY_KEY, repeat: REPEAT_KEY };
|
|
21
|
+
for (const part of (spec ?? "").split(",")) {
|
|
22
|
+
const [name, key] = part.split(":").map((piece) => piece.trim());
|
|
23
|
+
if ((name === "play" || name === "repeat") && key)
|
|
24
|
+
out[name] = key;
|
|
25
|
+
}
|
|
26
|
+
return out;
|
|
27
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const STYLES = "\n:host {\n --_ink: var(--kazoku-ink, #1b1b1f);\n --_surface: var(--kazoku-surface, #ffffff);\n --_line: var(--kazoku-line, #c9ccd3);\n --_accent: var(--kazoku-accent, #2457d6);\n display: inline-block;\n box-sizing: border-box;\n color: var(--_ink);\n font: 600 0.9rem/1.2 system-ui, -apple-system, \"Hiragino Sans\", \"Noto Sans JP\", \"Segoe UI\", sans-serif;\n color-scheme: light dark;\n}\n@media (prefers-color-scheme: dark) {\n :host(:not([theme=\"light\"]):not([data-scheme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n }\n}\n:host([theme=\"dark\"]), :host([data-scheme=\"dark\"]:not([theme=\"light\"])) {\n --_ink: var(--kazoku-ink, #f1f2f6);\n --_surface: var(--kazoku-surface, #1c1d22);\n --_line: var(--kazoku-line, #4a4d58);\n --_accent: var(--kazoku-accent, #8fb0ff);\n color-scheme: dark;\n}\n:host([theme=\"light\"]) { color-scheme: light; }\n[hidden] { display: none !important; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\nbutton { -webkit-appearance: none; appearance: none; box-sizing: border-box; display: inline-flex; align-items: center; justify-content: center; gap: 6px; min-width: 48px; min-height: 48px; margin: 0; padding: 0 14px; border: 1px solid var(--_line); border-radius: 24px; background: var(--_surface); color: var(--_ink); font: inherit; cursor: pointer; white-space: nowrap; }\nbutton:hover:not(:disabled) { border-color: var(--_ink); }\nbutton:focus-visible { outline: 2px solid var(--_ink); outline-offset: 2px; }\nbutton[aria-pressed=\"true\"] { background: var(--_ink); color: var(--_surface); border-color: var(--_ink); }\nbutton:disabled { opacity: 0.4; cursor: default; }\nsvg { width: 18px; height: 18px; fill: currentColor; flex: none; }\n\n:host([hidden]) { display: none; }\n:host { display: flex; vertical-align: middle; }\n.row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }\nbutton[aria-pressed=\"true\"] { background: var(--_ink); color: var(--_surface); }\n";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BASE_STYLES } from "../shared/base.js";
|
|
2
|
+
// One row: play, back to the start, repeat, then the speed buttons. Nothing folds away. A narrow page wraps the row; it does not scroll the page sideways.
|
|
3
|
+
export const STYLES = `${BASE_STYLES}
|
|
4
|
+
:host([hidden]) { display: none; }
|
|
5
|
+
:host { display: flex; vertical-align: middle; }
|
|
6
|
+
.row { display: flex; align-items: center; flex-wrap: wrap; gap: 6px; }
|
|
7
|
+
button[aria-pressed="true"] { background: var(--_ink); color: var(--_surface); }
|
|
8
|
+
`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface ReplayWords {
|
|
2
|
+
/** The name of the bar, said by a screen reader. */
|
|
3
|
+
group: string;
|
|
4
|
+
/** The button that starts playback. */
|
|
5
|
+
play: string;
|
|
6
|
+
/** The same button while playback is going. */
|
|
7
|
+
pause: string;
|
|
8
|
+
/** The button that goes back to the start. */
|
|
9
|
+
start: string;
|
|
10
|
+
/** The button that loops. */
|
|
11
|
+
repeat: string;
|
|
12
|
+
}
|
|
13
|
+
export type Language = "en" | "ja";
|
|
14
|
+
export declare const WORDS: Record<Language, ReplayWords>;
|
|
15
|
+
/** The language to use for a `lang` value such as "ja", "ja-JP" or "en-CA": Japanese for ja, English for anything else. */
|
|
16
|
+
export declare const languageOf: (lang: string | null | undefined) => Language;
|
|
17
|
+
export declare const wordsFor: (language: Language, overrides?: Partial<ReplayWords>) => ReplayWords;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export const WORDS = {
|
|
2
|
+
en: { group: "Playback", play: "Play", pause: "Pause", start: "Start", repeat: "Repeat" },
|
|
3
|
+
ja: { group: "再生の操作", play: "再生", pause: "一時停止", start: "最初へ", repeat: "繰り返し" },
|
|
4
|
+
};
|
|
5
|
+
/** The language to use for a `lang` value such as "ja", "ja-JP" or "en-CA": Japanese for ja, English for anything else. */
|
|
6
|
+
export const languageOf = (lang) => (/^ja\b/i.test(lang ?? "") ? "ja" : "en");
|
|
7
|
+
export const wordsFor = (language, overrides = {}) => ({ ...WORDS[language], ...overrides });
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Direction, type TurnAxis } from "./levels.ts";
|
|
1
2
|
import { type Limits } from "./math.ts";
|
|
2
3
|
export type DragMode = "move" | "turn";
|
|
3
4
|
export interface AddressKeys {
|
|
@@ -7,6 +8,16 @@ export interface AddressKeys {
|
|
|
7
8
|
drag: string;
|
|
8
9
|
/** The auto-turn speed, a number (`0.5`, `2`; 1 is the start and leaves the address). `pace` by default, because a page's own `speed` (a clock's, a replay's) is its own. */
|
|
9
10
|
speed?: string;
|
|
11
|
+
/** Which way Auto-turn goes, `left` or `right`; `direction` by default. */
|
|
12
|
+
direction?: string;
|
|
13
|
+
/** The roll, degrees. `roll` by default. */
|
|
14
|
+
roll?: string;
|
|
15
|
+
/** Which axis Auto-turn drives. `axis` by default. */
|
|
16
|
+
axis?: string;
|
|
17
|
+
/** Which way tilt's auto-turn goes. `tiltDirection` by default. */
|
|
18
|
+
tiltDirection?: string;
|
|
19
|
+
/** Which way roll's auto-turn goes. `rollDirection` by default. */
|
|
20
|
+
rollDirection?: string;
|
|
10
21
|
}
|
|
11
22
|
export declare const DEFAULT_KEYS: AddressKeys;
|
|
12
23
|
export interface AddressChoices {
|
|
@@ -16,6 +27,15 @@ export interface AddressChoices {
|
|
|
16
27
|
drag: DragMode;
|
|
17
28
|
/** The auto-turn speed as a multiple of the element's `speed`: the number of the button that is on. */
|
|
18
29
|
pace?: number;
|
|
30
|
+
/** The way Auto-turn goes. In `formatAddress`: a way is written; `null` leaves it off the address (it is the page's own start); left out, the key is not touched. */
|
|
31
|
+
direction?: Direction | null;
|
|
32
|
+
roll?: number;
|
|
33
|
+
/** Which axis Auto-turn drives. `angle` is the start and leaves the address. Left out, the key is not touched. */
|
|
34
|
+
axis?: TurnAxis | null;
|
|
35
|
+
/** Tilt's own way. `null` leaves it off the address; left out, the key is not touched. */
|
|
36
|
+
tiltDirection?: Direction | null;
|
|
37
|
+
/** Roll's own way. `null` leaves it off the address; left out, the key is not touched. */
|
|
38
|
+
rollDirection?: Direction | null;
|
|
19
39
|
}
|
|
20
40
|
/** Read the choices out of a query string (`?yaw=40`, with or without the `?`). Anything missing or not a number is left out of the result. */
|
|
21
41
|
export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
// The choices kept in the page's address: pure string functions, so a server or a test can use them. Only what a person chose is kept
|
|
2
2
|
// (angle, tilt, zoom, what a drag does); the pan is in pixels of one screen and the turning of a moment, so neither is.
|
|
3
|
-
import { PACE_FACTOR, parsePace } from "./levels.js";
|
|
3
|
+
import { PACE_FACTOR, parseAxis, parseDirection, parsePace } from "./levels.js";
|
|
4
4
|
import { clamp, DEFAULT_LIMITS, wrapYaw } from "./math.js";
|
|
5
|
-
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "pace" };
|
|
5
|
+
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "pace", direction: "direction", roll: "roll", axis: "axis", tiltDirection: "tiltDirection", rollDirection: "rollDirection" };
|
|
6
6
|
const rounded = (value, places) => Number(value.toFixed(places));
|
|
7
7
|
/** Read the choices out of a query string (`?yaw=40`, with or without the `?`). Anything missing or not a number is left out of the result. */
|
|
8
8
|
export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMITS) {
|
|
@@ -22,6 +22,12 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
|
|
|
22
22
|
out.yaw = rounded(wrapYaw(yaw), 0);
|
|
23
23
|
if (tilt !== undefined)
|
|
24
24
|
out.tilt = rounded(clamp(tilt, -limits.tiltMax, limits.tiltMax), 0);
|
|
25
|
+
const roll = number(keys.roll ?? "roll");
|
|
26
|
+
if (roll !== undefined)
|
|
27
|
+
out.roll = rounded(clamp(wrapYaw(roll), -limits.rollMax, limits.rollMax), 0);
|
|
28
|
+
const axis = parseAxis(params.get(keys.axis ?? "axis"));
|
|
29
|
+
if (axis !== undefined)
|
|
30
|
+
out.axis = axis;
|
|
25
31
|
if (zoom !== undefined)
|
|
26
32
|
out.zoom = rounded(clamp(zoom, limits.zoomMin, limits.zoomMax), 2);
|
|
27
33
|
const drag = params.get(keys.drag);
|
|
@@ -33,6 +39,15 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
|
|
|
33
39
|
const value = named !== undefined ? PACE_FACTOR[named] : said === null || said.trim() === "" ? Number.NaN : Number(said);
|
|
34
40
|
if (Number.isFinite(value))
|
|
35
41
|
out.pace = value;
|
|
42
|
+
const direction = parseDirection(params.get(keys.direction ?? "direction"));
|
|
43
|
+
if (direction !== undefined)
|
|
44
|
+
out.direction = direction;
|
|
45
|
+
const tiltDirection = parseDirection(params.get(keys.tiltDirection ?? "tiltDirection"));
|
|
46
|
+
if (tiltDirection !== undefined)
|
|
47
|
+
out.tiltDirection = tiltDirection;
|
|
48
|
+
const rollDirection = parseDirection(params.get(keys.rollDirection ?? "rollDirection"));
|
|
49
|
+
if (rollDirection !== undefined)
|
|
50
|
+
out.rollDirection = rollDirection;
|
|
36
51
|
return out;
|
|
37
52
|
}
|
|
38
53
|
/**
|
|
@@ -47,15 +62,26 @@ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = f
|
|
|
47
62
|
else
|
|
48
63
|
params.set(key, value);
|
|
49
64
|
};
|
|
50
|
-
if (!leaveYaw)
|
|
65
|
+
if (!leaveYaw) {
|
|
51
66
|
set(keys.yaw, Math.round(choices.yaw) === 0 ? undefined : String(rounded(wrapYaw(choices.yaw), 0)));
|
|
52
|
-
|
|
67
|
+
set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
|
|
68
|
+
if (choices.roll !== undefined)
|
|
69
|
+
set(keys.roll ?? "roll", Math.round(choices.roll) === 0 ? undefined : String(rounded(wrapYaw(choices.roll), 0)));
|
|
70
|
+
}
|
|
53
71
|
set(keys.zoom, Math.abs(choices.zoom - startZoom) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
|
|
54
72
|
set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
|
|
55
73
|
// The pace key is touched only when the caller has a pace; the start (1) leaves it off the address.
|
|
56
74
|
const paceKey = keys.speed ?? "pace";
|
|
57
75
|
if (choices.pace !== undefined)
|
|
58
76
|
set(paceKey, choices.pace === 1 ? undefined : String(rounded(choices.pace, 3)));
|
|
77
|
+
if (choices.direction !== undefined)
|
|
78
|
+
set(keys.direction ?? "direction", choices.direction === null ? undefined : choices.direction);
|
|
79
|
+
if (choices.axis !== undefined)
|
|
80
|
+
set(keys.axis ?? "axis", choices.axis === null || choices.axis === "angle" ? undefined : choices.axis);
|
|
81
|
+
if (choices.tiltDirection !== undefined)
|
|
82
|
+
set(keys.tiltDirection ?? "tiltDirection", choices.tiltDirection === null || choices.tiltDirection === "right" ? undefined : choices.tiltDirection);
|
|
83
|
+
if (choices.rollDirection !== undefined)
|
|
84
|
+
set(keys.rollDirection ?? "rollDirection", choices.rollDirection === null || choices.rollDirection === "right" ? undefined : choices.rollDirection);
|
|
59
85
|
const text = params.toString();
|
|
60
86
|
return text === "" ? "" : `?${text}`;
|
|
61
87
|
}
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { DragMode } from "./address.ts";
|
|
2
|
-
import
|
|
2
|
+
import type { TurnAxis } from "./levels.ts";
|
|
3
|
+
import { type Camera, type Limits, type Point, type Quat, type Size } from "./math.ts";
|
|
3
4
|
export interface ViewState extends Camera {
|
|
5
|
+
/** The pose the page should draw. A tumble keeps this continuous when the slider numbers fold at a pole. */
|
|
6
|
+
orientation: Quat;
|
|
4
7
|
/** True while the view turns by itself. A control never changes this; only play, pause and a grab of the view's own do not either (see `holdTurn`). */
|
|
5
8
|
turning: boolean;
|
|
6
9
|
/** What a plain drag does while zoomed in: move the view, or turn the piece. */
|
|
@@ -24,8 +27,10 @@ export interface ViewControllerOptions {
|
|
|
24
27
|
initial?: Partial<ViewState>;
|
|
25
28
|
/** False for a flat view that cannot turn: a drag then always moves it and the angle and tilt never change. */
|
|
26
29
|
turnable?: boolean;
|
|
27
|
-
/** Degrees a second when it turns by itself. */
|
|
30
|
+
/** Degrees a second when it turns by itself. The sign is the active axis's direction. */
|
|
28
31
|
speed?: number;
|
|
32
|
+
/** Which axis turns by itself. `angle` (yaw) by default. */
|
|
33
|
+
turnAxis?: TurnAxis;
|
|
29
34
|
limits?: Partial<Limits>;
|
|
30
35
|
/** The starting zoom: "fit" (the default) or a scale where 1 is the scene's natural size; it opens at that scale divided by `fitScale`. With `fitScale` left at 1 a number is a zoom as `ViewState.zoom` has it. `initial.zoom` is used only when this is left out. */
|
|
31
36
|
zoom?: "fit" | number;
|
|
@@ -55,6 +60,12 @@ export interface ViewController {
|
|
|
55
60
|
readonly state: ViewState;
|
|
56
61
|
turnable: boolean;
|
|
57
62
|
speed: number;
|
|
63
|
+
/** Which axis `play` turns. `angle` is yaw. `all` tumbles on three axes. */
|
|
64
|
+
turnAxis: TurnAxis;
|
|
65
|
+
/** Tilt's own direction while `turnAxis` is `all` (and the sign of `speed` while it is `tilt`). 1 or -1. */
|
|
66
|
+
tiltSign: number;
|
|
67
|
+
/** Roll's own direction. 1 or -1. */
|
|
68
|
+
rollSign: number;
|
|
58
69
|
readonly limits: Limits;
|
|
59
70
|
/** What a pointer press leaves alone inside the view (see `ViewControllerOptions.ignore`). Can be changed at any time. */
|
|
60
71
|
ignore: string;
|