@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
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { turnOnlyInSight } from "./sight.js";
|
|
2
|
-
import { between, clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOMED, zoomAbout } from "./math.js";
|
|
2
|
+
import { between, clamp, clampPan, DEFAULT_LIMITS, dragTurn, eulerFromQuat, PAN_STEP, pendulumTilt, pinchTo, quatFromEuler, quatIdentity, spinLocal, tumbleRates, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOMED, zoomAbout } from "./math.js";
|
|
3
3
|
/** What `bind` leaves alone by default: the page's own controls inside the view. */
|
|
4
4
|
export const DEFAULT_IGNORE = "button, a, input, select, textarea, label, [data-kazoku-ignore]";
|
|
5
5
|
/** How long a move to the middle takes. */
|
|
@@ -21,11 +21,15 @@ export function createViewController(options = {}) {
|
|
|
21
21
|
let destroyed = false;
|
|
22
22
|
let centring = 0;
|
|
23
23
|
let stepping = false;
|
|
24
|
+
let tiltPhase = 0;
|
|
24
25
|
let fitScale = positive(options.fitScale) ?? 1;
|
|
25
26
|
let start = positive(options.zoom) ?? "fit";
|
|
26
27
|
let atStart = true;
|
|
27
28
|
const startState = () => (start === "fit" ? 1 : start / fitScale);
|
|
28
|
-
const state = { yaw: 0, tilt: 0, zoom: 1, panX: 0, panY: 0, turning: false, drag: "move", ...options.initial };
|
|
29
|
+
const state = { yaw: 0, tilt: 0, roll: 0, orientation: quatIdentity(), zoom: 1, panX: 0, panY: 0, turning: false, drag: "move", ...options.initial };
|
|
30
|
+
if (state.orientation === undefined)
|
|
31
|
+
state.orientation = quatIdentity();
|
|
32
|
+
state.roll ??= 0;
|
|
29
33
|
if (start !== "fit")
|
|
30
34
|
state.zoom = clamp(startState(), limits.zoomMin, limits.zoomMax);
|
|
31
35
|
atStart = state.zoom === clamp(startState(), limits.zoomMin, limits.zoomMax);
|
|
@@ -51,17 +55,25 @@ export function createViewController(options = {}) {
|
|
|
51
55
|
};
|
|
52
56
|
const camera = () => ({ yaw: state.yaw, tilt: state.tilt, zoom: state.zoom, panX: state.panX, panY: state.panY });
|
|
53
57
|
/** Take a camera in, held to the limits and, once the view's size is known, to the room the zoom gives. */
|
|
58
|
+
const pose = () => {
|
|
59
|
+
state.orientation = quatFromEuler(state.yaw, state.tilt, state.roll ?? 0);
|
|
60
|
+
};
|
|
54
61
|
const take = (next) => {
|
|
55
62
|
if (!stepping)
|
|
56
63
|
stopCentre();
|
|
57
|
-
const
|
|
64
|
+
const rollMax = limits.rollMax;
|
|
65
|
+
const roll = clamp(wrapYaw(next.roll ?? state.roll ?? 0), -rollMax, rollMax);
|
|
66
|
+
const held = clampPan({ ...next, roll, zoom: clamp(next.zoom, limits.zoomMin, limits.zoomMax), tilt: clamp(next.tilt, -limits.tiltMax, limits.tiltMax) }, size(), controller.objectSize);
|
|
58
67
|
state.yaw = controller.turnable ? wrapYaw(held.yaw) : state.yaw;
|
|
59
68
|
state.tilt = controller.turnable ? held.tilt : state.tilt;
|
|
69
|
+
state.roll = controller.turnable ? roll : state.roll;
|
|
60
70
|
if (held.zoom !== state.zoom)
|
|
61
71
|
atStart = false;
|
|
62
72
|
state.zoom = held.zoom;
|
|
63
73
|
state.panX = held.panX;
|
|
64
74
|
state.panY = held.panY;
|
|
75
|
+
pose();
|
|
76
|
+
tiltPhase = Math.asin(clamp(state.tilt / (limits.tiltMax || 1), -1, 1));
|
|
65
77
|
};
|
|
66
78
|
const centre = () => {
|
|
67
79
|
if (bound === undefined)
|
|
@@ -86,20 +98,71 @@ export function createViewController(options = {}) {
|
|
|
86
98
|
last = 0;
|
|
87
99
|
return;
|
|
88
100
|
}
|
|
89
|
-
const
|
|
101
|
+
const gap = last === 0 ? 0 : (now - last) / 1000;
|
|
102
|
+
// The picture moves at most a tenth of a second in one frame, so a late frame does not jump. The limit counts the time that really passed, up to a second, so a turn that stops itself does so when that time has passed.
|
|
103
|
+
const dt = Math.min(0.1, gap);
|
|
90
104
|
last = now;
|
|
91
|
-
if (
|
|
105
|
+
if (gap === 0)
|
|
92
106
|
return;
|
|
93
|
-
|
|
94
|
-
|
|
107
|
+
const rate = controller.speed;
|
|
108
|
+
if (controller.turnAxis === "tilt")
|
|
109
|
+
stepTilt(dt, rate);
|
|
110
|
+
else if (controller.turnAxis === "roll")
|
|
111
|
+
stepRoll(dt, rate);
|
|
112
|
+
else if (controller.turnAxis === "all")
|
|
113
|
+
stepAll(dt);
|
|
114
|
+
else {
|
|
115
|
+
state.yaw = wrapYaw(state.yaw + rate * dt);
|
|
116
|
+
pose();
|
|
117
|
+
}
|
|
118
|
+
turned += Math.min(gap, 1);
|
|
95
119
|
emit(false, "auto");
|
|
96
120
|
if (limit > 0 && turned >= limit)
|
|
97
121
|
controller.pause();
|
|
98
122
|
};
|
|
123
|
+
/** Tilt within the limit swings back. At 90 degrees or more it rolls over on the quaternion, so the up axis never flips. */
|
|
124
|
+
const stepTilt = (dt, rate) => {
|
|
125
|
+
if (limits.tiltMax >= 90) {
|
|
126
|
+
state.orientation = spinLocal(state.orientation, 0, rate * dt, 0);
|
|
127
|
+
const euler = eulerFromQuat(state.orientation);
|
|
128
|
+
state.yaw = euler.yaw;
|
|
129
|
+
state.tilt = euler.tilt;
|
|
130
|
+
state.roll = euler.roll;
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
tiltPhase += (rate * dt * Math.PI) / 180;
|
|
134
|
+
state.tilt = pendulumTilt(tiltPhase, limits.tiltMax);
|
|
135
|
+
pose();
|
|
136
|
+
};
|
|
137
|
+
const stepRoll = (dt, rate) => {
|
|
138
|
+
const max = limits.rollMax;
|
|
139
|
+
state.roll = max >= 180 ? wrapYaw((state.roll ?? 0) + rate * dt) : clamp((state.roll ?? 0) + rate * dt, -max, max);
|
|
140
|
+
pose();
|
|
141
|
+
};
|
|
142
|
+
const stepAll = (dt) => {
|
|
143
|
+
const signs = { yaw: controller.speed, tilt: controller.tiltSign, roll: controller.rollSign };
|
|
144
|
+
const rates = tumbleRates(turned, Math.abs(controller.speed), signs);
|
|
145
|
+
if (limits.tiltMax >= 90) {
|
|
146
|
+
state.orientation = spinLocal(state.orientation, rates.yaw * dt, rates.tilt * dt, rates.roll * dt);
|
|
147
|
+
const euler = eulerFromQuat(state.orientation);
|
|
148
|
+
state.yaw = euler.yaw;
|
|
149
|
+
state.tilt = euler.tilt;
|
|
150
|
+
state.roll = euler.roll;
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
state.yaw = wrapYaw(state.yaw + rates.yaw * dt);
|
|
154
|
+
state.roll = wrapYaw((state.roll ?? 0) + rates.roll * dt);
|
|
155
|
+
tiltPhase += (rates.tilt * dt * Math.PI) / 180;
|
|
156
|
+
state.tilt = pendulumTilt(tiltPhase, limits.tiltMax);
|
|
157
|
+
pose();
|
|
158
|
+
};
|
|
99
159
|
const controller = {
|
|
100
160
|
state,
|
|
101
161
|
turnable: options.turnable ?? true,
|
|
102
162
|
speed: options.speed ?? 30,
|
|
163
|
+
turnAxis: options.turnAxis ?? "angle",
|
|
164
|
+
tiltSign: 1,
|
|
165
|
+
rollSign: 1,
|
|
103
166
|
limits,
|
|
104
167
|
ignore: options.ignore ?? DEFAULT_IGNORE,
|
|
105
168
|
anchor: options.anchor,
|
|
@@ -193,7 +256,7 @@ export function createViewController(options = {}) {
|
|
|
193
256
|
last = 0;
|
|
194
257
|
},
|
|
195
258
|
front() {
|
|
196
|
-
take({ ...camera(), yaw: 0, tilt: 0 });
|
|
259
|
+
take({ ...camera(), yaw: 0, tilt: 0, roll: 0 });
|
|
197
260
|
emit(true, "front");
|
|
198
261
|
},
|
|
199
262
|
fit() {
|
|
@@ -203,7 +266,7 @@ export function createViewController(options = {}) {
|
|
|
203
266
|
centre(animate = true) {
|
|
204
267
|
stopCentre();
|
|
205
268
|
const from = camera();
|
|
206
|
-
const to = { yaw: 0, tilt: 0, zoom: 1, panX: 0, panY: 0 };
|
|
269
|
+
const to = { yaw: 0, tilt: 0, roll: 0, zoom: 1, panX: 0, panY: 0 };
|
|
207
270
|
const reduced = globalThis.matchMedia?.("(prefers-reduced-motion: reduce)")?.matches === true;
|
|
208
271
|
const same = from.yaw === 0 && from.tilt === 0 && from.zoom === 1 && from.panX === 0 && from.panY === 0;
|
|
209
272
|
if (!animate || reduced || same || destroyed) {
|
|
@@ -12,7 +12,7 @@ export interface ViewChangeDetail extends ViewState {
|
|
|
12
12
|
/**
|
|
13
13
|
* 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
|
|
14
14
|
* 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;
|
|
15
|
-
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop
|
|
15
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop a turn by itself after this many seconds of turning: the one `autoplay` starts, and one the Play button or `play()` starts), `speed` (degrees a second at a pace of 1; a negative one starts the turn to the left), `direction` (`left` or `right`: which way Auto-turn goes; the reader's choice of the Left and Right buttons is written here and kept in the address as `direction`, and the angle goes on from where it was at the same speed), `pace` (the auto-turn speed as a multiple of `speed`, 1 by default; the reader's choice of the buttons is written here and kept in the address as `pace`), `paces` (the speeds the buttons stand for, `0.5 1 2` by default),
|
|
16
16
|
* `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; the wheel itself zooms),
|
|
17
17
|
* `controls` (`basic`, the default, or `advanced`: which controls are shown, the zoom readout in both, and the speed in both while the view turns; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
|
|
18
18
|
* `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
|
|
@@ -32,7 +32,8 @@ export declare class ViewControlsElement extends HTMLElement {
|
|
|
32
32
|
set view(element: HTMLElement | undefined);
|
|
33
33
|
/** Your own labels over the language's, for example `{ front: "Home" }`. */
|
|
34
34
|
set words(words: Partial<ViewWords>);
|
|
35
|
-
|
|
35
|
+
/** Turn by itself. With `seconds`, stop after that many seconds of turning; else the `autoplay-for` attribute's, if there is one. */
|
|
36
|
+
play(seconds?: number): void;
|
|
36
37
|
pause(): void;
|
|
37
38
|
front(): void;
|
|
38
39
|
/** Put the object back in the middle at Fit and the default angle, with a brief move (at once for a reader who prefers reduced motion, or with `false`). */
|
|
@@ -6,7 +6,7 @@ import { localStore, remember } from "../shared/store.js";
|
|
|
6
6
|
import { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
|
|
7
7
|
import { createViewController, DEFAULT_IGNORE } from "./controller.js";
|
|
8
8
|
import { EVENT, TAG } from "./index.js";
|
|
9
|
-
import { LEVEL_STORAGE_KEY, parseLayout, parseLevel, zoomText } from "./levels.js";
|
|
9
|
+
import { LEVEL_STORAGE_KEY, parseAxis, parseDirection, parseLayout, parseLevel, zoomText } from "./levels.js";
|
|
10
10
|
import { ZOOM_STEP, ZOOMED } from "./math.js";
|
|
11
11
|
import { turnOnlyInSight } from "./sight.js";
|
|
12
12
|
import "../speed-buttons/define.js";
|
|
@@ -35,10 +35,23 @@ const THUMB = 28;
|
|
|
35
35
|
const HTML = `
|
|
36
36
|
<div class="bar" part="bar" role="group">
|
|
37
37
|
<div class="field angle" part="field angle"><label data-w="angle" for="yaw"></label><input id="yaw" type="range" min="-180" max="180" step="1" value="0" part="slider" /><output for="yaw" data-o="yaw">0°</output></div>
|
|
38
|
-
<div class="field tilt
|
|
38
|
+
<div class="field tilt" part="field tilt"><label data-w="tilt" for="tilt"></label><input id="tilt" type="range" min="-30" max="30" step="1" value="0" part="slider" /><output for="tilt" data-o="tilt">0°</output></div>
|
|
39
|
+
<div class="field roll" part="field roll"><label data-w="roll" for="roll"></label><input id="roll" type="range" min="-180" max="180" step="1" value="0" part="slider" /><output for="roll" data-o="roll">0°</output></div>
|
|
40
|
+
<div class="group axes" role="group" data-g="axes">
|
|
41
|
+
<button type="button" data-b="axis-angle" aria-pressed="true" part="button"><span data-w="angle"></span></button>
|
|
42
|
+
<button type="button" data-b="axis-tilt" aria-pressed="false" part="button"><span data-w="tilt"></span></button>
|
|
43
|
+
<button type="button" data-b="axis-roll" aria-pressed="false" part="button"><span data-w="roll"></span></button>
|
|
44
|
+
<button type="button" data-b="axis-all" aria-pressed="false" part="button"><span data-w="tumble"></span></button>
|
|
45
|
+
</div>
|
|
39
46
|
<button type="button" class="pace-now" data-b="pace-now" part="button" hidden><span class="lbl" data-w="speed"></span><span data-o="pace"></span></button>
|
|
40
47
|
<button type="button" class="play" data-b="play" aria-pressed="false" part="button">${ICON.play}${ICON.pause}<span class="text" data-w="autoTurn"></span></button>
|
|
41
|
-
<
|
|
48
|
+
<div class="pacing adv" data-g="pacing">
|
|
49
|
+
<kazoku-speed-buttons class="pace" data-g="pace"></kazoku-speed-buttons>
|
|
50
|
+
<div class="group dir" role="group" data-g="direction">
|
|
51
|
+
<button type="button" data-b="turn-left" aria-pressed="false" part="button">${ICON.left}<span data-w="turnLeft"></span></button>
|
|
52
|
+
<button type="button" data-b="turn-right" aria-pressed="true" part="button">${ICON.right}<span data-w="turnRight"></span></button>
|
|
53
|
+
</div>
|
|
54
|
+
</div>
|
|
42
55
|
<button type="button" class="front adv" data-b="front" part="button"><span data-w="front"></span></button>
|
|
43
56
|
<button type="button" class="centre" data-b="centre" part="button" hidden>${ICON.centre}<span class="text" data-w="centre"></span></button>
|
|
44
57
|
<div class="group zoom" role="group" data-g="zoom">
|
|
@@ -64,7 +77,7 @@ const HTML = `
|
|
|
64
77
|
/**
|
|
65
78
|
* 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
|
|
66
79
|
* 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;
|
|
67
|
-
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop
|
|
80
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop a turn by itself after this many seconds of turning: the one `autoplay` starts, and one the Play button or `play()` starts), `speed` (degrees a second at a pace of 1; a negative one starts the turn to the left), `direction` (`left` or `right`: which way Auto-turn goes; the reader's choice of the Left and Right buttons is written here and kept in the address as `direction`, and the angle goes on from where it was at the same speed), `pace` (the auto-turn speed as a multiple of `speed`, 1 by default; the reader's choice of the buttons is written here and kept in the address as `pace`), `paces` (the speeds the buttons stand for, `0.5 1 2` by default),
|
|
68
81
|
* `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; the wheel itself zooms),
|
|
69
82
|
* `controls` (`basic`, the default, or `advanced`: which controls are shown, the zoom readout in both, and the speed in both while the view turns; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
|
|
70
83
|
* `layout` (`classic`, the default, `horizontal`, `vertical` or `compact`: where the controls sit; the horizontal, vertical and compact layouts have the zoom slider),
|
|
@@ -72,7 +85,7 @@ const HTML = `
|
|
|
72
85
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
73
86
|
*/
|
|
74
87
|
export class ViewControlsElement extends HTMLElement {
|
|
75
|
-
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "pace", "paces", "controls", "layout", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
88
|
+
static observedAttributes = ["for", "lang", "yaw", "tilt", "roll", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "direction", "tilt-direction", "roll-direction", "axis", "pace", "paces", "controls", "layout", "flat", "tilt-max", "roll-max", "zoom-min", "zoom-max", "address-keys"];
|
|
76
89
|
#root;
|
|
77
90
|
#controller;
|
|
78
91
|
#view;
|
|
@@ -91,6 +104,13 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
91
104
|
/** The zoom the slider shows now; NaN until it has shown one, and again when the marks are redrawn for new limits. */
|
|
92
105
|
#zoomShown = Number.NaN;
|
|
93
106
|
#pace = 1;
|
|
107
|
+
/** Which way the turn goes now, and the way the page itself starts it (its `direction`, else the sign of its `speed`): the address keeps a way only when it is not the start. */
|
|
108
|
+
#direction = "right";
|
|
109
|
+
#start;
|
|
110
|
+
#axis = "angle";
|
|
111
|
+
#tiltWay = "right";
|
|
112
|
+
#rollWay = "right";
|
|
113
|
+
#writing = false;
|
|
94
114
|
#labels = wordsFor("en");
|
|
95
115
|
#said = "";
|
|
96
116
|
#sayTimer;
|
|
@@ -118,8 +138,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
118
138
|
this.#words = words;
|
|
119
139
|
this.#label();
|
|
120
140
|
}
|
|
121
|
-
|
|
122
|
-
|
|
141
|
+
/** Turn by itself. With `seconds`, stop after that many seconds of turning; else the `autoplay-for` attribute's, if there is one. */
|
|
142
|
+
play(seconds) {
|
|
143
|
+
this.#ensure().play(seconds ?? this.#playLimit());
|
|
123
144
|
}
|
|
124
145
|
pause() {
|
|
125
146
|
this.#ensure().pause();
|
|
@@ -162,16 +183,35 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
162
183
|
this.#arrange();
|
|
163
184
|
this.#limits();
|
|
164
185
|
const start = {};
|
|
165
|
-
for (const key of ["yaw", "tilt"]) {
|
|
186
|
+
for (const key of ["yaw", "tilt", "roll"]) {
|
|
166
187
|
const value = Number(this.getAttribute(key));
|
|
167
188
|
if (this.hasAttribute(key) && Number.isFinite(value))
|
|
168
189
|
start[key] = value;
|
|
169
190
|
}
|
|
170
|
-
const { pace, ...chosen } = this.hasAttribute("address") ? parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits) : {};
|
|
191
|
+
const { pace, direction, axis, tiltDirection, rollDirection, ...chosen } = this.hasAttribute("address") ? parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits) : {};
|
|
171
192
|
Object.assign(start, chosen);
|
|
193
|
+
this.#axis = axis ?? parseAxis(this.getAttribute("axis")) ?? this.#axis;
|
|
194
|
+
if (tiltDirection)
|
|
195
|
+
this.#tiltWay = tiltDirection;
|
|
196
|
+
else {
|
|
197
|
+
const tiltWay = parseDirection(this.getAttribute("tilt-direction"));
|
|
198
|
+
if (tiltWay !== undefined)
|
|
199
|
+
this.#tiltWay = tiltWay;
|
|
200
|
+
}
|
|
201
|
+
if (rollDirection)
|
|
202
|
+
this.#rollWay = rollDirection;
|
|
203
|
+
else {
|
|
204
|
+
const rollWay = parseDirection(this.getAttribute("roll-direction"));
|
|
205
|
+
if (rollWay !== undefined)
|
|
206
|
+
this.#rollWay = rollWay;
|
|
207
|
+
}
|
|
172
208
|
c.turnable = !this.hasAttribute("flat");
|
|
173
209
|
this.#level = parseLevel(this.#remembered()) ?? parseLevel(this.getAttribute("controls")) ?? "basic";
|
|
174
210
|
this.#pace = this.#nearestPace(pace ?? parseSpeed(this.getAttribute("pace"), this.#paces()) ?? 1);
|
|
211
|
+
// Read once: put back on a page after a move, the element is still started by the page and not by the way the reader has since chosen (which is in the attribute now).
|
|
212
|
+
const first = this.#start === undefined;
|
|
213
|
+
this.#start ??= this.#startDirection();
|
|
214
|
+
this.#direction = direction ?? (first ? this.#start : this.#direction);
|
|
175
215
|
this.#speed();
|
|
176
216
|
c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
|
|
177
217
|
for (const name of ["key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size"])
|
|
@@ -197,7 +237,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
197
237
|
return;
|
|
198
238
|
this.#dispatch(c.state, { final: true, reason: "init" });
|
|
199
239
|
if (this.hasAttribute("autoplay") && !globalThis.matchMedia?.("(prefers-reduced-motion: reduce)").matches)
|
|
200
|
-
c.play(
|
|
240
|
+
c.play(this.#playLimit());
|
|
201
241
|
});
|
|
202
242
|
}
|
|
203
243
|
disconnectedCallback() {
|
|
@@ -223,10 +263,15 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
223
263
|
this.#resolveView();
|
|
224
264
|
else if (name === "lang")
|
|
225
265
|
this.#label();
|
|
226
|
-
else if (name === "yaw" || name === "tilt") {
|
|
266
|
+
else if (name === "yaw" || name === "tilt" || name === "roll") {
|
|
227
267
|
if (value !== null && Number.isFinite(Number(value)))
|
|
228
268
|
c.set({ [name]: Number(value) });
|
|
229
269
|
}
|
|
270
|
+
else if (name === "axis") {
|
|
271
|
+
this.#axis = parseAxis(value) ?? "angle";
|
|
272
|
+
this.#speed();
|
|
273
|
+
this.#show();
|
|
274
|
+
}
|
|
230
275
|
else if (name === "zoom")
|
|
231
276
|
c.setStartZoom(this.#startZoom());
|
|
232
277
|
else if (name === "fit-scale")
|
|
@@ -235,8 +280,33 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
235
280
|
c.ignore = value ?? DEFAULT_IGNORE;
|
|
236
281
|
else if (name === "anchor" || name === "key-tilt" || name === "stop-turn" || name === "tilt-step" || name === "grab-radius" || name === "object-size")
|
|
237
282
|
this.#option(name);
|
|
238
|
-
else if (name === "speed")
|
|
283
|
+
else if (name === "speed") {
|
|
284
|
+
// The page's own start follows its speed's sign, unless the reader has chosen the other way.
|
|
285
|
+
const start = this.#startDirection();
|
|
286
|
+
if (this.#direction === (this.#start ?? start))
|
|
287
|
+
this.#direction = start;
|
|
288
|
+
this.#start = start;
|
|
289
|
+
this.#speed();
|
|
290
|
+
this.#show();
|
|
291
|
+
}
|
|
292
|
+
else if (name === "direction") {
|
|
293
|
+
if (this.#writing)
|
|
294
|
+
return;
|
|
295
|
+
this.#start = this.#direction = this.#startDirection();
|
|
239
296
|
this.#speed();
|
|
297
|
+
this.#show();
|
|
298
|
+
}
|
|
299
|
+
else if (name === "tilt-direction" || name === "roll-direction") {
|
|
300
|
+
if (this.#writing)
|
|
301
|
+
return;
|
|
302
|
+
const way = parseDirection(value) ?? "right";
|
|
303
|
+
if (name === "tilt-direction")
|
|
304
|
+
this.#tiltWay = way;
|
|
305
|
+
else
|
|
306
|
+
this.#rollWay = way;
|
|
307
|
+
this.#speed();
|
|
308
|
+
this.#show();
|
|
309
|
+
}
|
|
240
310
|
else if (name === "pace" || name === "paces") {
|
|
241
311
|
this.#pace = this.#nearestPace(parseSpeed(this.getAttribute("pace"), this.#paces()) ?? this.#pace);
|
|
242
312
|
this.#speed();
|
|
@@ -312,30 +382,38 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
312
382
|
const c = () => this.#ensure();
|
|
313
383
|
const slider = (id) => {
|
|
314
384
|
const input = q(`#${id}`);
|
|
315
|
-
// A hand on
|
|
385
|
+
// A hand on a turn slider holds the automatic turn still while it is down; the turn then goes on from where the hand left it.
|
|
316
386
|
const hold = () => {
|
|
317
387
|
this.#sliderHeld += 1;
|
|
318
|
-
if (this.#sliderHeld === 1
|
|
388
|
+
if (this.#sliderHeld === 1)
|
|
319
389
|
c().holdTurn();
|
|
320
390
|
};
|
|
321
391
|
const free = () => {
|
|
322
392
|
if (this.#sliderHeld === 0)
|
|
323
393
|
return;
|
|
324
394
|
this.#sliderHeld = 0;
|
|
325
|
-
|
|
326
|
-
c().releaseTurn();
|
|
395
|
+
c().releaseTurn();
|
|
327
396
|
};
|
|
328
397
|
input.addEventListener("pointerdown", () => hold());
|
|
329
398
|
input.addEventListener("pointerup", free);
|
|
330
399
|
input.addEventListener("pointercancel", free);
|
|
331
|
-
input.addEventListener("input", () => c().set({ [id]: Number(input.value) }, false, "slider"));
|
|
332
|
-
input.addEventListener("change", () => c().set({ [id]: Number(input.value) }, true, "slider"));
|
|
400
|
+
input.addEventListener("input", () => c().set({ [id === "yaw" ? this.#hand() : id]: Number(input.value) }, false, "slider"));
|
|
401
|
+
input.addEventListener("change", () => c().set({ [id === "yaw" ? this.#hand() : id]: Number(input.value) }, true, "slider"));
|
|
333
402
|
};
|
|
334
403
|
slider("yaw");
|
|
335
404
|
slider("tilt");
|
|
405
|
+
slider("roll");
|
|
406
|
+
for (const axis of ["angle", "tilt", "roll", "all"]) {
|
|
407
|
+
q(`[data-b="axis-${axis}"]`).addEventListener("click", () => this.#chooseAxis(axis));
|
|
408
|
+
}
|
|
336
409
|
this.#zoomSlider(q("#zoom"), c);
|
|
337
410
|
const on = (b, run) => q(`[data-b="${b}"]`).addEventListener("click", run);
|
|
338
|
-
on("play", () =>
|
|
411
|
+
on("play", () => {
|
|
412
|
+
if (c().state.turning)
|
|
413
|
+
c().pause();
|
|
414
|
+
else
|
|
415
|
+
c().play(this.#playLimit());
|
|
416
|
+
});
|
|
339
417
|
on("front", () => c().front());
|
|
340
418
|
on("out", () => c().zoomBy(1 / ZOOM_STEP));
|
|
341
419
|
on("in", () => c().zoomBy(ZOOM_STEP));
|
|
@@ -357,6 +435,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
357
435
|
this.#choosePace(value);
|
|
358
436
|
});
|
|
359
437
|
on("pace-now", () => speeds.step(1));
|
|
438
|
+
on("turn-left", () => this.#chooseDirection("left"));
|
|
439
|
+
on("turn-right", () => this.#chooseDirection("right"));
|
|
360
440
|
on("left", () => c().panBy(-1, 0));
|
|
361
441
|
on("right", () => c().panBy(1, 0));
|
|
362
442
|
on("up", () => c().panBy(0, -1));
|
|
@@ -462,9 +542,80 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
462
542
|
if (this.hasAttribute("address"))
|
|
463
543
|
this.#save();
|
|
464
544
|
}
|
|
465
|
-
/** The
|
|
545
|
+
/** The way the page itself starts the turn: its `direction`, else left for a negative `speed` and right for the rest. */
|
|
546
|
+
#startDirection() {
|
|
547
|
+
return parseDirection(this.getAttribute("direction")) ?? (Number(this.getAttribute("speed")) < 0 ? "left" : "right");
|
|
548
|
+
}
|
|
549
|
+
/** The seconds `autoplay-for` limits a turn to, or `undefined` for none. */
|
|
550
|
+
#playLimit() {
|
|
551
|
+
const seconds = Number(this.getAttribute("autoplay-for"));
|
|
552
|
+
return seconds > 0 && Number.isFinite(seconds) ? seconds : undefined;
|
|
553
|
+
}
|
|
554
|
+
/** A way was pressed. It sets the active axis's direction (all three when All is on). The angle goes on from where it was. */
|
|
555
|
+
#chooseDirection(direction) {
|
|
556
|
+
const axes = this.#axis === "all" ? ["angle", "tilt", "roll"] : [this.#axis];
|
|
557
|
+
for (const axis of axes)
|
|
558
|
+
this.#putWay(axis, direction);
|
|
559
|
+
this.#speed();
|
|
560
|
+
this.#show();
|
|
561
|
+
if (this.hasAttribute("address"))
|
|
562
|
+
this.#save();
|
|
563
|
+
}
|
|
564
|
+
#putWay(axis, direction) {
|
|
565
|
+
const name = axis === "tilt" ? "tilt-direction" : axis === "roll" ? "roll-direction" : "direction";
|
|
566
|
+
if (axis === "tilt")
|
|
567
|
+
this.#tiltWay = direction;
|
|
568
|
+
else if (axis === "roll")
|
|
569
|
+
this.#rollWay = direction;
|
|
570
|
+
else
|
|
571
|
+
this.#direction = direction;
|
|
572
|
+
if (this.getAttribute(name) !== direction) {
|
|
573
|
+
this.#writing = true;
|
|
574
|
+
this.setAttribute(name, direction);
|
|
575
|
+
this.#writing = false;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
/** Which axis Auto-turn drives, and which slider a reader last named. Angle is the start, so it leaves the attribute. */
|
|
579
|
+
#chooseAxis(axis) {
|
|
580
|
+
this.#axis = axis;
|
|
581
|
+
const written = axis === "angle" ? null : axis;
|
|
582
|
+
if (this.getAttribute("axis") !== written) {
|
|
583
|
+
this.#writing = true;
|
|
584
|
+
if (written === null)
|
|
585
|
+
this.removeAttribute("axis");
|
|
586
|
+
else
|
|
587
|
+
this.setAttribute("axis", written);
|
|
588
|
+
this.#writing = false;
|
|
589
|
+
}
|
|
590
|
+
this.#speed();
|
|
591
|
+
this.#show();
|
|
592
|
+
if (this.hasAttribute("address"))
|
|
593
|
+
this.#save();
|
|
594
|
+
}
|
|
595
|
+
/** The slider shows this axis. All is a tumble, so the slider stays on the angle. */
|
|
596
|
+
#hand() {
|
|
597
|
+
if (this.#axis === "tilt")
|
|
598
|
+
return "tilt";
|
|
599
|
+
if (this.#axis === "roll")
|
|
600
|
+
return "roll";
|
|
601
|
+
return "yaw";
|
|
602
|
+
}
|
|
603
|
+
/** The way Left and Right show for the axis that is on. All shows the angle's way. */
|
|
604
|
+
#activeWay() {
|
|
605
|
+
if (this.#axis === "tilt")
|
|
606
|
+
return this.#tiltWay;
|
|
607
|
+
if (this.#axis === "roll")
|
|
608
|
+
return this.#rollWay;
|
|
609
|
+
return this.#direction;
|
|
610
|
+
}
|
|
611
|
+
/** The turn's degrees a second: the size of the `speed` attribute (30) times the pace, going left when the active axis says so. */
|
|
466
612
|
#speed() {
|
|
467
|
-
|
|
613
|
+
const c = this.#ensure();
|
|
614
|
+
const size = Math.abs(Number(this.getAttribute("speed")) || 30) * this.#pace;
|
|
615
|
+
c.turnAxis = this.#axis;
|
|
616
|
+
c.tiltSign = this.#tiltWay === "left" ? -1 : 1;
|
|
617
|
+
c.rollSign = this.#rollWay === "left" ? -1 : 1;
|
|
618
|
+
c.speed = size * (this.#activeWay() === "left" ? -1 : 1);
|
|
468
619
|
}
|
|
469
620
|
#limits() {
|
|
470
621
|
const c = this.#ensure();
|
|
@@ -473,6 +624,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
473
624
|
return this.hasAttribute(name) && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
474
625
|
};
|
|
475
626
|
c.limits.tiltMax = read("tilt-max", 30);
|
|
627
|
+
c.limits.rollMax = read("roll-max", 180);
|
|
476
628
|
c.limits.zoomMin = Math.min(1, read("zoom-min", 1));
|
|
477
629
|
c.limits.zoomMax = Math.max(c.limits.zoomMin, read("zoom-max", 5));
|
|
478
630
|
const tilt = this.#root.querySelector("#tilt");
|
|
@@ -480,6 +632,11 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
480
632
|
tilt.min = String(-c.limits.tiltMax);
|
|
481
633
|
tilt.max = String(c.limits.tiltMax);
|
|
482
634
|
}
|
|
635
|
+
const roll = this.#root.querySelector("#roll");
|
|
636
|
+
if (roll !== null) {
|
|
637
|
+
roll.min = String(-c.limits.rollMax);
|
|
638
|
+
roll.max = String(c.limits.rollMax);
|
|
639
|
+
}
|
|
483
640
|
this.#drawMarks();
|
|
484
641
|
}
|
|
485
642
|
#resolveView() {
|
|
@@ -522,6 +679,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
522
679
|
set('[data-b="in"]', words.zoomIn);
|
|
523
680
|
set('[data-g="zoom"]', words.zoom);
|
|
524
681
|
set('[data-g="pan"]', words.pan);
|
|
682
|
+
set('[data-g="direction"]', words.direction);
|
|
683
|
+
set('[data-g="axes"]', words.axis);
|
|
525
684
|
set('[data-g="drag"]', words.drag);
|
|
526
685
|
const speeds = this.#root.querySelector("kazoku-speed-buttons");
|
|
527
686
|
speeds?.setAttribute("lang", language);
|
|
@@ -540,17 +699,37 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
540
699
|
const q = (selector) => this.#root.querySelector(selector);
|
|
541
700
|
const yaw = q("#yaw");
|
|
542
701
|
const tilt = q("#tilt");
|
|
543
|
-
|
|
702
|
+
const roll = q("#roll");
|
|
544
703
|
tilt.value = String(Math.round(s.tilt));
|
|
545
|
-
|
|
704
|
+
roll.value = String(Math.round(s.roll ?? 0));
|
|
705
|
+
// The one slider shows the axis the chips have chosen. All keeps the angle, which is where a tumble is read from.
|
|
706
|
+
const hand = this.#hand();
|
|
707
|
+
const span = hand === "tilt" ? c.limits.tiltMax : hand === "roll" ? c.limits.rollMax : 180;
|
|
708
|
+
const value = hand === "tilt" ? s.tilt : hand === "roll" ? (s.roll ?? 0) : s.yaw;
|
|
709
|
+
const text = String(Math.round(value));
|
|
710
|
+
if (yaw.max !== String(span)) {
|
|
711
|
+
yaw.max = String(span);
|
|
712
|
+
yaw.min = String(-span);
|
|
713
|
+
}
|
|
714
|
+
if (yaw.value !== text)
|
|
715
|
+
yaw.value = text;
|
|
716
|
+
const words = this.#labels;
|
|
717
|
+
const name = hand === "tilt" ? words.tilt : hand === "roll" ? words.roll : words.angle;
|
|
718
|
+
const label = q('[data-w="angle"]');
|
|
719
|
+
if (label.textContent !== name)
|
|
720
|
+
label.textContent = name;
|
|
721
|
+
q('[data-o="yaw"]').value = `${text}°`;
|
|
546
722
|
q('[data-o="tilt"]').value = `${Math.round(s.tilt)}°`;
|
|
723
|
+
q('[data-o="roll"]').value = `${Math.round(s.roll ?? 0)}°`;
|
|
724
|
+
for (const axis of ["angle", "tilt", "roll", "all"]) {
|
|
725
|
+
q(`[data-b="axis-${axis}"]`).setAttribute("aria-pressed", String(this.#axis === axis));
|
|
726
|
+
}
|
|
547
727
|
q('[data-b="play"]').setAttribute("aria-pressed", String(s.turning));
|
|
548
728
|
// Centre comes in once the view has been zoomed or moved, which is when the object can be lost; it puts it back in the middle at Fit and the default angle.
|
|
549
729
|
q(".centre").hidden = s.zoom === 1 && s.panX === 0 && s.panY === 0;
|
|
550
730
|
q('[data-b="out"]').disabled = s.zoom <= c.limits.zoomMin + 1e-6;
|
|
551
731
|
q('[data-b="in"]').disabled = s.zoom >= c.limits.zoomMax - 1e-6;
|
|
552
732
|
const advanced = this.#level === "advanced";
|
|
553
|
-
const words = this.#labels;
|
|
554
733
|
const zoom = zoomText(s.zoom);
|
|
555
734
|
// A frame of the automatic turn comes through here: what has not changed is not written.
|
|
556
735
|
const write = (node, text) => {
|
|
@@ -586,6 +765,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
586
765
|
const pace = speedText(this.#pace);
|
|
587
766
|
write(q('[data-o="pace"]'), pace);
|
|
588
767
|
say(q('[data-b="pace-now"]'), `${words.speed}: ${pace}`);
|
|
768
|
+
const way = this.#activeWay();
|
|
769
|
+
q('[data-b="turn-left"]').setAttribute("aria-pressed", String(way === "left"));
|
|
770
|
+
q('[data-b="turn-right"]').setAttribute("aria-pressed", String(way === "right"));
|
|
589
771
|
const more = q('[data-b="more"]');
|
|
590
772
|
more.setAttribute("aria-expanded", String(advanced));
|
|
591
773
|
this.dataset.level = this.#level;
|
|
@@ -596,9 +778,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
596
778
|
q('[data-b="turn"]').setAttribute("aria-pressed", String(s.drag === "turn"));
|
|
597
779
|
// A flat view cannot turn: the angle, tilt, auto-turn and front are not shown, and a drag only moves.
|
|
598
780
|
const flat = !c.turnable;
|
|
599
|
-
for (const selector of [".angle", ".play"])
|
|
781
|
+
for (const selector of [".angle", ".tilt", ".roll", ".play", '[data-g="axes"]'])
|
|
600
782
|
q(selector).hidden = flat;
|
|
601
|
-
for (const selector of [".
|
|
783
|
+
for (const selector of [".front", '[data-g="drag"]', '[data-g="pacing"]'])
|
|
602
784
|
q(selector).hidden = flat || !advanced;
|
|
603
785
|
// The speed is always in sight while the view turns: one small button in basic, which steps through the three; advanced always has the three.
|
|
604
786
|
q(".pace-now").hidden = flat || advanced || !s.turning;
|
|
@@ -636,7 +818,14 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
636
818
|
#save() {
|
|
637
819
|
this.#writer.write((search) => {
|
|
638
820
|
const s = this.#ensure().state;
|
|
639
|
-
return formatAddress(search, {
|
|
821
|
+
return formatAddress(search, {
|
|
822
|
+
...s,
|
|
823
|
+
pace: this.#pace,
|
|
824
|
+
direction: this.#direction === (this.#start ?? this.#direction) ? null : this.#direction,
|
|
825
|
+
axis: this.#axis,
|
|
826
|
+
tiltDirection: this.#tiltWay === "right" ? null : this.#tiltWay,
|
|
827
|
+
rollDirection: this.#rollWay === "right" ? null : this.#rollWay,
|
|
828
|
+
}, this.#keys, s.turning, this.#ensure().startZoom);
|
|
640
829
|
});
|
|
641
830
|
}
|
|
642
831
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
export { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys, type AddressChoices, type AddressKeys, type DragMode } from "./address.ts";
|
|
2
2
|
export { createViewController, DEFAULT_IGNORE, type ChangeInfo, type Clock, type ViewController, type ViewControllerOptions, type ViewState } from "./controller.ts";
|
|
3
|
-
export { between, clamp, clampPan, DEFAULT_LIMITS, degreesPerPixel, dragTurn, PAN_STEP, pinchTo, TILT_ACROSS, TURN_ACROSS, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOM_STEP, ZOOMED, zoomAbout, type Camera, type Limits, type PinchStart, type Point, type Size } from "./math.ts";
|
|
3
|
+
export { between, clamp, clampPan, DEFAULT_LIMITS, degreesPerPixel, dragTurn, eulerFromQuat, PAN_STEP, pendulumTilt, pinchTo, quatFromEuler, quatIdentity, rateSign, spinLocal, TILT_ACROSS, tumbleRates, TURN_ACROSS, upVector, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOM_STEP, ZOOMED, zoomAbout, type Camera, type Limits, type PinchStart, type Point, type Quat, type Size, type TumbleSigns, type Vec3 } from "./math.ts";
|
|
4
4
|
export { languageOf, WORDS, wordsFor, type Language, type ViewWords } from "./words.ts";
|
|
5
5
|
export declare const EVENT = "kazoku-viewchange";
|
|
6
6
|
export declare const TAG = "kazoku-view-controls";
|
|
7
|
-
export { LAYOUTS, LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLayout, parseLevel, parsePace, zoomText, type Layout, type Level, type Pace } from "./levels.ts";
|
|
7
|
+
export { LAYOUTS, LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseAxis, parseLayout, parseLevel, parsePace, zoomText, type Layout, type Level, type Pace, type TurnAxis } from "./levels.ts";
|
|
8
8
|
export { COMMON_ZOOMS, MARK_GAP, SLIDER_STEP, SNAP_PX, zoomAtPosition, zoomMarks, zoomPosition, zoomSnap, zoomStep, zoomToMark, type ZoomMark } from "./zoomScale.ts";
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
// The view controls without the element: the controller, the arithmetic, the address and the labels. Nothing here touches the DOM until `bind` is called, so it imports on a server.
|
|
2
2
|
export { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
|
|
3
3
|
export { createViewController, DEFAULT_IGNORE } from "./controller.js";
|
|
4
|
-
export { between, clamp, clampPan, DEFAULT_LIMITS, degreesPerPixel, dragTurn, PAN_STEP, pinchTo, TILT_ACROSS, TURN_ACROSS, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOM_STEP, ZOOMED, zoomAbout } from "./math.js";
|
|
4
|
+
export { between, clamp, clampPan, DEFAULT_LIMITS, degreesPerPixel, dragTurn, eulerFromQuat, PAN_STEP, pendulumTilt, pinchTo, quatFromEuler, quatIdentity, rateSign, spinLocal, TILT_ACROSS, tumbleRates, TURN_ACROSS, upVector, WHEEL_GESTURE_MS, wheelZoomFactor, wrapYaw, ZOOM_STEP, ZOOMED, zoomAbout } from "./math.js";
|
|
5
5
|
export { languageOf, WORDS, wordsFor } from "./words.js";
|
|
6
6
|
export const EVENT = "kazoku-viewchange";
|
|
7
7
|
export const TAG = "kazoku-view-controls";
|
|
8
|
-
export { LAYOUTS, LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLayout, parseLevel, parsePace, zoomText } from "./levels.js";
|
|
8
|
+
export { LAYOUTS, LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseAxis, parseLayout, parseLevel, parsePace, zoomText } from "./levels.js";
|
|
9
9
|
export { COMMON_ZOOMS, MARK_GAP, SLIDER_STEP, SNAP_PX, zoomAtPosition, zoomMarks, zoomPosition, zoomSnap, zoomStep, zoomToMark } from "./zoomScale.js";
|
|
@@ -8,6 +8,12 @@ export type Pace = "slow" | "medium" | "fast";
|
|
|
8
8
|
export declare const PACES: readonly Pace[];
|
|
9
9
|
export declare const PACE_FACTOR: Record<Pace, number>;
|
|
10
10
|
export declare const parsePace: (text: string | null | undefined) => Pace | undefined;
|
|
11
|
+
/** Which way Auto-turn goes. By the drag rule `yaw` goes up as the hand goes right, so `right` is a positive speed and `left` a negative one; the page's drawing decides what that looks like. */
|
|
12
|
+
export type Direction = "left" | "right";
|
|
13
|
+
export declare const parseDirection: (text: string | null | undefined) => Direction | undefined;
|
|
14
|
+
/** Which axis Auto-turn drives. `angle` is yaw, the default. `all` is a tumble on yaw, tilt and roll at once. */
|
|
15
|
+
export type TurnAxis = "angle" | "tilt" | "roll" | "all";
|
|
16
|
+
export declare const parseAxis: (text: string | null | undefined) => TurnAxis | undefined;
|
|
11
17
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
12
18
|
export declare const zoomText: (zoom: number) => string;
|
|
13
19
|
/**
|
|
@@ -5,6 +5,8 @@ export const LEVEL_STORAGE_KEY = "kazoku-view-controls";
|
|
|
5
5
|
export const PACES = ["slow", "medium", "fast"];
|
|
6
6
|
export const PACE_FACTOR = { slow: 0.5, medium: 1, fast: 2 };
|
|
7
7
|
export const parsePace = (text) => PACES.find((pace) => pace === text);
|
|
8
|
+
export const parseDirection = (text) => (text === "left" || text === "right" ? text : undefined);
|
|
9
|
+
export const parseAxis = (text) => (text === "angle" || text === "tilt" || text === "roll" || text === "all" ? text : undefined);
|
|
8
10
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
9
11
|
export const zoomText = (zoom) => `${Number(zoom < 1 ? zoom.toPrecision(2) : zoom.toFixed(1))}×`;
|
|
10
12
|
export const LAYOUTS = ["classic", "horizontal", "vertical", "compact"];
|