@johnmorrisdotca/kazoku-ui 0.16.0 → 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 +10 -0
- package/README.md +1 -1
- 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 +16 -1
- package/dist/view-controls/address.js +25 -4
- package/dist/view-controls/controller.d.ts +13 -2
- package/dist/view-controls/controller.js +72 -9
- package/dist/view-controls/element.js +154 -26
- package/dist/view-controls/index.d.ts +2 -2
- package/dist/view-controls/index.js +2 -2
- package/dist/view-controls/levels.d.ts +3 -0
- package/dist/view-controls/levels.js +1 -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 +16 -7
- package/dist/view-controls/words.d.ts +5 -0
- package/dist/view-controls/words.js +6 -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) {
|
|
@@ -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, parseDirection, 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,7 +35,14 @@ 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">
|
|
@@ -78,7 +85,7 @@ const HTML = `
|
|
|
78
85
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
79
86
|
*/
|
|
80
87
|
export class ViewControlsElement extends HTMLElement {
|
|
81
|
-
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "grab-radius", "object-size", "speed", "direction", "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"];
|
|
82
89
|
#root;
|
|
83
90
|
#controller;
|
|
84
91
|
#view;
|
|
@@ -100,6 +107,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
100
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. */
|
|
101
108
|
#direction = "right";
|
|
102
109
|
#start;
|
|
110
|
+
#axis = "angle";
|
|
111
|
+
#tiltWay = "right";
|
|
112
|
+
#rollWay = "right";
|
|
103
113
|
#writing = false;
|
|
104
114
|
#labels = wordsFor("en");
|
|
105
115
|
#said = "";
|
|
@@ -173,13 +183,28 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
173
183
|
this.#arrange();
|
|
174
184
|
this.#limits();
|
|
175
185
|
const start = {};
|
|
176
|
-
for (const key of ["yaw", "tilt"]) {
|
|
186
|
+
for (const key of ["yaw", "tilt", "roll"]) {
|
|
177
187
|
const value = Number(this.getAttribute(key));
|
|
178
188
|
if (this.hasAttribute(key) && Number.isFinite(value))
|
|
179
189
|
start[key] = value;
|
|
180
190
|
}
|
|
181
|
-
const { pace, direction, ...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) : {};
|
|
182
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
|
+
}
|
|
183
208
|
c.turnable = !this.hasAttribute("flat");
|
|
184
209
|
this.#level = parseLevel(this.#remembered()) ?? parseLevel(this.getAttribute("controls")) ?? "basic";
|
|
185
210
|
this.#pace = this.#nearestPace(pace ?? parseSpeed(this.getAttribute("pace"), this.#paces()) ?? 1);
|
|
@@ -238,10 +263,15 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
238
263
|
this.#resolveView();
|
|
239
264
|
else if (name === "lang")
|
|
240
265
|
this.#label();
|
|
241
|
-
else if (name === "yaw" || name === "tilt") {
|
|
266
|
+
else if (name === "yaw" || name === "tilt" || name === "roll") {
|
|
242
267
|
if (value !== null && Number.isFinite(Number(value)))
|
|
243
268
|
c.set({ [name]: Number(value) });
|
|
244
269
|
}
|
|
270
|
+
else if (name === "axis") {
|
|
271
|
+
this.#axis = parseAxis(value) ?? "angle";
|
|
272
|
+
this.#speed();
|
|
273
|
+
this.#show();
|
|
274
|
+
}
|
|
245
275
|
else if (name === "zoom")
|
|
246
276
|
c.setStartZoom(this.#startZoom());
|
|
247
277
|
else if (name === "fit-scale")
|
|
@@ -266,6 +296,17 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
266
296
|
this.#speed();
|
|
267
297
|
this.#show();
|
|
268
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
|
+
}
|
|
269
310
|
else if (name === "pace" || name === "paces") {
|
|
270
311
|
this.#pace = this.#nearestPace(parseSpeed(this.getAttribute("pace"), this.#paces()) ?? this.#pace);
|
|
271
312
|
this.#speed();
|
|
@@ -341,27 +382,30 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
341
382
|
const c = () => this.#ensure();
|
|
342
383
|
const slider = (id) => {
|
|
343
384
|
const input = q(`#${id}`);
|
|
344
|
-
// 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.
|
|
345
386
|
const hold = () => {
|
|
346
387
|
this.#sliderHeld += 1;
|
|
347
|
-
if (this.#sliderHeld === 1
|
|
388
|
+
if (this.#sliderHeld === 1)
|
|
348
389
|
c().holdTurn();
|
|
349
390
|
};
|
|
350
391
|
const free = () => {
|
|
351
392
|
if (this.#sliderHeld === 0)
|
|
352
393
|
return;
|
|
353
394
|
this.#sliderHeld = 0;
|
|
354
|
-
|
|
355
|
-
c().releaseTurn();
|
|
395
|
+
c().releaseTurn();
|
|
356
396
|
};
|
|
357
397
|
input.addEventListener("pointerdown", () => hold());
|
|
358
398
|
input.addEventListener("pointerup", free);
|
|
359
399
|
input.addEventListener("pointercancel", free);
|
|
360
|
-
input.addEventListener("input", () => c().set({ [id]: Number(input.value) }, false, "slider"));
|
|
361
|
-
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"));
|
|
362
402
|
};
|
|
363
403
|
slider("yaw");
|
|
364
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
|
+
}
|
|
365
409
|
this.#zoomSlider(q("#zoom"), c);
|
|
366
410
|
const on = (b, run) => q(`[data-b="${b}"]`).addEventListener("click", run);
|
|
367
411
|
on("play", () => {
|
|
@@ -507,22 +551,71 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
507
551
|
const seconds = Number(this.getAttribute("autoplay-for"));
|
|
508
552
|
return seconds > 0 && Number.isFinite(seconds) ? seconds : undefined;
|
|
509
553
|
}
|
|
510
|
-
/** A way was pressed. It
|
|
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. */
|
|
511
555
|
#chooseDirection(direction) {
|
|
512
|
-
this.#
|
|
556
|
+
const axes = this.#axis === "all" ? ["angle", "tilt", "roll"] : [this.#axis];
|
|
557
|
+
for (const axis of axes)
|
|
558
|
+
this.#putWay(axis, direction);
|
|
513
559
|
this.#speed();
|
|
514
|
-
|
|
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) {
|
|
515
583
|
this.#writing = true;
|
|
516
|
-
|
|
584
|
+
if (written === null)
|
|
585
|
+
this.removeAttribute("axis");
|
|
586
|
+
else
|
|
587
|
+
this.setAttribute("axis", written);
|
|
517
588
|
this.#writing = false;
|
|
518
589
|
}
|
|
590
|
+
this.#speed();
|
|
519
591
|
this.#show();
|
|
520
592
|
if (this.hasAttribute("address"))
|
|
521
593
|
this.#save();
|
|
522
594
|
}
|
|
523
|
-
/** The
|
|
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. */
|
|
524
612
|
#speed() {
|
|
525
|
-
|
|
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);
|
|
526
619
|
}
|
|
527
620
|
#limits() {
|
|
528
621
|
const c = this.#ensure();
|
|
@@ -531,6 +624,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
531
624
|
return this.hasAttribute(name) && Number.isFinite(value) && value > 0 ? value : fallback;
|
|
532
625
|
};
|
|
533
626
|
c.limits.tiltMax = read("tilt-max", 30);
|
|
627
|
+
c.limits.rollMax = read("roll-max", 180);
|
|
534
628
|
c.limits.zoomMin = Math.min(1, read("zoom-min", 1));
|
|
535
629
|
c.limits.zoomMax = Math.max(c.limits.zoomMin, read("zoom-max", 5));
|
|
536
630
|
const tilt = this.#root.querySelector("#tilt");
|
|
@@ -538,6 +632,11 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
538
632
|
tilt.min = String(-c.limits.tiltMax);
|
|
539
633
|
tilt.max = String(c.limits.tiltMax);
|
|
540
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
|
+
}
|
|
541
640
|
this.#drawMarks();
|
|
542
641
|
}
|
|
543
642
|
#resolveView() {
|
|
@@ -581,6 +680,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
581
680
|
set('[data-g="zoom"]', words.zoom);
|
|
582
681
|
set('[data-g="pan"]', words.pan);
|
|
583
682
|
set('[data-g="direction"]', words.direction);
|
|
683
|
+
set('[data-g="axes"]', words.axis);
|
|
584
684
|
set('[data-g="drag"]', words.drag);
|
|
585
685
|
const speeds = this.#root.querySelector("kazoku-speed-buttons");
|
|
586
686
|
speeds?.setAttribute("lang", language);
|
|
@@ -599,17 +699,37 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
599
699
|
const q = (selector) => this.#root.querySelector(selector);
|
|
600
700
|
const yaw = q("#yaw");
|
|
601
701
|
const tilt = q("#tilt");
|
|
602
|
-
|
|
702
|
+
const roll = q("#roll");
|
|
603
703
|
tilt.value = String(Math.round(s.tilt));
|
|
604
|
-
|
|
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}°`;
|
|
605
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
|
+
}
|
|
606
727
|
q('[data-b="play"]').setAttribute("aria-pressed", String(s.turning));
|
|
607
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.
|
|
608
729
|
q(".centre").hidden = s.zoom === 1 && s.panX === 0 && s.panY === 0;
|
|
609
730
|
q('[data-b="out"]').disabled = s.zoom <= c.limits.zoomMin + 1e-6;
|
|
610
731
|
q('[data-b="in"]').disabled = s.zoom >= c.limits.zoomMax - 1e-6;
|
|
611
732
|
const advanced = this.#level === "advanced";
|
|
612
|
-
const words = this.#labels;
|
|
613
733
|
const zoom = zoomText(s.zoom);
|
|
614
734
|
// A frame of the automatic turn comes through here: what has not changed is not written.
|
|
615
735
|
const write = (node, text) => {
|
|
@@ -645,8 +765,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
645
765
|
const pace = speedText(this.#pace);
|
|
646
766
|
write(q('[data-o="pace"]'), pace);
|
|
647
767
|
say(q('[data-b="pace-now"]'), `${words.speed}: ${pace}`);
|
|
648
|
-
|
|
649
|
-
q('[data-b="turn-
|
|
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"));
|
|
650
771
|
const more = q('[data-b="more"]');
|
|
651
772
|
more.setAttribute("aria-expanded", String(advanced));
|
|
652
773
|
this.dataset.level = this.#level;
|
|
@@ -657,9 +778,9 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
657
778
|
q('[data-b="turn"]').setAttribute("aria-pressed", String(s.drag === "turn"));
|
|
658
779
|
// A flat view cannot turn: the angle, tilt, auto-turn and front are not shown, and a drag only moves.
|
|
659
780
|
const flat = !c.turnable;
|
|
660
|
-
for (const selector of [".angle", ".play"])
|
|
781
|
+
for (const selector of [".angle", ".tilt", ".roll", ".play", '[data-g="axes"]'])
|
|
661
782
|
q(selector).hidden = flat;
|
|
662
|
-
for (const selector of [".
|
|
783
|
+
for (const selector of [".front", '[data-g="drag"]', '[data-g="pacing"]'])
|
|
663
784
|
q(selector).hidden = flat || !advanced;
|
|
664
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.
|
|
665
786
|
q(".pace-now").hidden = flat || advanced || !s.turning;
|
|
@@ -697,7 +818,14 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
697
818
|
#save() {
|
|
698
819
|
this.#writer.write((search) => {
|
|
699
820
|
const s = this.#ensure().state;
|
|
700
|
-
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);
|
|
701
829
|
});
|
|
702
830
|
}
|
|
703
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";
|
|
@@ -11,6 +11,9 @@ export declare const parsePace: (text: string | null | undefined) => Pace | unde
|
|
|
11
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
12
|
export type Direction = "left" | "right";
|
|
13
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;
|
|
14
17
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
15
18
|
export declare const zoomText: (zoom: number) => string;
|
|
16
19
|
/**
|
|
@@ -6,6 +6,7 @@ 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
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);
|
|
9
10
|
/** The zoom as the readout shows it: `1×`, `2.4×`; below 1, two figures, so a small zoom never reads `0×`. */
|
|
10
11
|
export const zoomText = (zoom) => `${Number(zoom < 1 ? zoom.toPrecision(2) : zoom.toFixed(1))}×`;
|
|
11
12
|
export const LAYOUTS = ["classic", "horizontal", "vertical", "compact"];
|
|
@@ -10,12 +10,16 @@ export interface Size {
|
|
|
10
10
|
export interface Camera {
|
|
11
11
|
yaw: number;
|
|
12
12
|
tilt: number;
|
|
13
|
+
/** Degrees about the view's forward axis. Left out, it is 0. */
|
|
14
|
+
roll?: number;
|
|
13
15
|
zoom: number;
|
|
14
16
|
panX: number;
|
|
15
17
|
panY: number;
|
|
16
18
|
}
|
|
17
19
|
export interface Limits {
|
|
18
20
|
tiltMax: number;
|
|
21
|
+
/** How far a hand may roll, in degrees. 180 (the default) is a full turn. */
|
|
22
|
+
rollMax: number;
|
|
19
23
|
zoomMin: number;
|
|
20
24
|
zoomMax: number;
|
|
21
25
|
}
|
|
@@ -64,3 +68,49 @@ export declare function between(from: Camera, to: Camera, fraction: number): Cam
|
|
|
64
68
|
export declare const wheelZoomFactor: (deltaY: number, deltaMode: number, kind?: "pinch" | "wheel") => number;
|
|
65
69
|
/** Wheel steps closer together than this, in milliseconds, belong to one turn of the wheel or one swipe (with its glide). */
|
|
66
70
|
export declare const WHEEL_GESTURE_MS = 300;
|
|
71
|
+
/** A rotation. `w` is the real part. The identity is `{ x: 0, y: 0, z: 0, w: 1 }`. */
|
|
72
|
+
export interface Quat {
|
|
73
|
+
x: number;
|
|
74
|
+
y: number;
|
|
75
|
+
z: number;
|
|
76
|
+
w: number;
|
|
77
|
+
}
|
|
78
|
+
export interface Vec3 {
|
|
79
|
+
x: number;
|
|
80
|
+
y: number;
|
|
81
|
+
z: number;
|
|
82
|
+
}
|
|
83
|
+
export declare const quatIdentity: () => Quat;
|
|
84
|
+
/** Which way a signed rate points: -1 or 1. Zero and anything that is not a number count as 1. */
|
|
85
|
+
export declare const rateSign: (value: number) => number;
|
|
86
|
+
export declare const normalizeQuat: (q: Quat) => Quat;
|
|
87
|
+
/**
|
|
88
|
+
* Yaw about Y, then tilt about X, then roll about Z, in degrees. Past a right angle the numbers fold, so a tumble that rolls over keeps its quaternion and only shows these for the sliders.
|
|
89
|
+
*/
|
|
90
|
+
export declare function quatFromEuler(yaw: number, tilt: number, roll: number): Quat;
|
|
91
|
+
/** The yaw, tilt and roll a quaternion shows on the sliders. Tilt stays within a right angle; the quaternion itself does not flip. */
|
|
92
|
+
export declare function eulerFromQuat(q: Quat): {
|
|
93
|
+
yaw: number;
|
|
94
|
+
tilt: number;
|
|
95
|
+
roll: number;
|
|
96
|
+
};
|
|
97
|
+
/**
|
|
98
|
+
* Turn `q` by `yaw`, `tilt` and `roll` degrees about its own axes (Y, X, then Z). Small steps stay continuous through the poles: the up vector never jumps.
|
|
99
|
+
*/
|
|
100
|
+
export declare function spinLocal(q: Quat, yaw: number, tilt: number, roll: number): Quat;
|
|
101
|
+
/** The view's up axis after `q`. */
|
|
102
|
+
export declare function upVector(q: Quat): Vec3;
|
|
103
|
+
/**
|
|
104
|
+
* Tilt swinging inside `amplitude` degrees. `phase` is radians of the swing. At the ends it comes back, so a limited tilt never rolls over.
|
|
105
|
+
*/
|
|
106
|
+
export declare const pendulumTilt: (phase: number, amplitude: number) => number;
|
|
107
|
+
export interface TumbleSigns {
|
|
108
|
+
yaw: number;
|
|
109
|
+
tilt: number;
|
|
110
|
+
roll: number;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Degrees a second for a tumble. The three rates differ, and each drifts on its own incommensurate wave, so the motion does not repeat.
|
|
114
|
+
* `speed` is the size of the angle's rate. A sign below 0 reverses that axis.
|
|
115
|
+
*/
|
|
116
|
+
export declare function tumbleRates(seconds: number, speed: number, signs: TumbleSigns): TumbleSigns;
|