@johnmorrisdotca/kazoku-ui 0.7.2 → 0.9.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 +30 -0
- package/README.md +33 -10
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/renderer/cache.d.ts +38 -0
- package/dist/renderer/cache.js +104 -0
- package/dist/renderer/index.d.ts +5 -0
- package/dist/renderer/index.js +7 -0
- package/dist/renderer/pixels.d.ts +16 -0
- package/dist/renderer/pixels.js +25 -0
- package/dist/renderer/renderer.d.ts +118 -0
- package/dist/renderer/renderer.js +472 -0
- package/dist/renderer/scheduler.d.ts +42 -0
- package/dist/renderer/scheduler.js +91 -0
- package/dist/renderer/stills.d.ts +27 -0
- package/dist/renderer/stills.js +50 -0
- package/dist/testing/index.d.ts +34 -0
- package/dist/testing/index.js +64 -0
- package/dist/view-controls/address.d.ts +4 -0
- package/dist/view-controls/address.js +9 -1
- package/dist/view-controls/element.d.ts +1 -0
- package/dist/view-controls/element.js +101 -12
- package/dist/view-controls/index.d.ts +1 -0
- package/dist/view-controls/index.js +1 -0
- package/dist/view-controls/levels.d.ts +12 -0
- package/dist/view-controls/levels.js +9 -0
- package/dist/view-controls/styles.d.ts +1 -1
- package/dist/view-controls/styles.js +13 -5
- package/dist/view-controls/words.d.ts +7 -0
- package/dist/view-controls/words.js +14 -0
- package/package.json +26 -3
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/** The GL object kinds counted. */
|
|
2
|
+
export declare const GPU_KINDS: readonly ["Buffer", "Texture", "VertexArray", "Program", "Framebuffer", "Renderbuffer"];
|
|
3
|
+
export type GpuKind = (typeof GPU_KINDS)[number];
|
|
4
|
+
export interface GlCounts {
|
|
5
|
+
/** WebGL contexts made. */
|
|
6
|
+
contexts: number;
|
|
7
|
+
/** Draw calls (drawArrays, drawElements and their instanced forms). */
|
|
8
|
+
draws: number;
|
|
9
|
+
/** `requestAnimationFrame` calls made, and the callbacks that ran. */
|
|
10
|
+
rafRequests: number;
|
|
11
|
+
rafCallbacks: number;
|
|
12
|
+
created: Record<GpuKind, number>;
|
|
13
|
+
deleted: Record<GpuKind, number>;
|
|
14
|
+
}
|
|
15
|
+
/** Install the counters in a page, before its own scripts run. A string, so `page.addInitScript(COUNT_SCRIPT)` takes it as it is. */
|
|
16
|
+
export declare const COUNT_SCRIPT: string;
|
|
17
|
+
/** What the page needs to be: Playwright's `Page` fits. */
|
|
18
|
+
export interface PageLike {
|
|
19
|
+
evaluate<R>(fn: () => R): Promise<R>;
|
|
20
|
+
}
|
|
21
|
+
/** The counters now, as a copy. */
|
|
22
|
+
export declare function readCounts(page: PageLike): Promise<GlCounts>;
|
|
23
|
+
/** GPU objects alive: made minus deleted, over every kind. Flat across a run of changes means nothing leaks. */
|
|
24
|
+
export declare function gpuObjects(counts: GlCounts): number;
|
|
25
|
+
export interface CanvasSize {
|
|
26
|
+
/** The most pixels any canvas on the page holds, and the longest side of any. */
|
|
27
|
+
pixels: number;
|
|
28
|
+
side: number;
|
|
29
|
+
count: number;
|
|
30
|
+
}
|
|
31
|
+
/** The biggest canvas on the page, in device pixels. */
|
|
32
|
+
export declare function canvasSize(page: PageLike): Promise<CanvasSize>;
|
|
33
|
+
/** True when a canvas is within the budget (the renderer's own: 6 million pixels, 4096 a side). */
|
|
34
|
+
export declare function withinPixelBudget(size: CanvasSize, maxPixels?: number, maxSide?: number): boolean;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// A test kit for a package's freeze checks, so every 3D package asserts the same things: how many WebGL contexts, how many GPU objects stay alive, how many
|
|
2
|
+
// draws and animation frames a page at rest costs, and how big its biggest canvas is. It names no test framework: hand it Playwright's `page` (or anything with
|
|
3
|
+
// `addInitScript` and `evaluate`). Nothing here touches the DOM until the script runs in a page, so it imports on a server.
|
|
4
|
+
//
|
|
5
|
+
// await page.addInitScript(COUNT_SCRIPT); // before page.goto
|
|
6
|
+
// await page.goto(url);
|
|
7
|
+
// const before = await readCounts(page);
|
|
8
|
+
// ...the changes under test...
|
|
9
|
+
// const after = await readCounts(page);
|
|
10
|
+
// expect(after.contexts).toBe(1);
|
|
11
|
+
// expect(after.draws - before.draws).toBe(0); // at rest
|
|
12
|
+
// expect(gpuObjects(after)).toBe(gpuObjects(before)); // nothing leaked
|
|
13
|
+
/** The GL object kinds counted. */
|
|
14
|
+
export const GPU_KINDS = ["Buffer", "Texture", "VertexArray", "Program", "Framebuffer", "Renderbuffer"];
|
|
15
|
+
/** Install the counters in a page, before its own scripts run. A string, so `page.addInitScript(COUNT_SCRIPT)` takes it as it is. */
|
|
16
|
+
export const COUNT_SCRIPT = `(() => {
|
|
17
|
+
const c = (window.__kazokuCount = { contexts: 0, draws: 0, rafRequests: 0, rafCallbacks: 0, created: {}, deleted: {} });
|
|
18
|
+
const kinds = ${JSON.stringify(GPU_KINDS)};
|
|
19
|
+
for (const k of kinds) { c.created[k] = 0; c.deleted[k] = 0; }
|
|
20
|
+
const seen = new WeakSet();
|
|
21
|
+
for (const proto of [window.HTMLCanvasElement && HTMLCanvasElement.prototype, window.OffscreenCanvas && OffscreenCanvas.prototype]) {
|
|
22
|
+
if (!proto) continue;
|
|
23
|
+
const getContext = proto.getContext;
|
|
24
|
+
proto.getContext = function (type, ...rest) {
|
|
25
|
+
const context = getContext.call(this, type, ...rest);
|
|
26
|
+
if (context && /^(experimental-)?webgl/.test(String(type)) && !seen.has(context)) { seen.add(context); c.contexts += 1; }
|
|
27
|
+
return context;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
for (const proto of [window.WebGLRenderingContext && WebGLRenderingContext.prototype, window.WebGL2RenderingContext && WebGL2RenderingContext.prototype]) {
|
|
31
|
+
if (!proto) continue;
|
|
32
|
+
for (const k of kinds) {
|
|
33
|
+
const make = proto["create" + k];
|
|
34
|
+
const remove = proto["delete" + k];
|
|
35
|
+
if (make) proto["create" + k] = function (...a) { c.created[k] += 1; return make.apply(this, a); };
|
|
36
|
+
if (remove) proto["delete" + k] = function (o) { if (o) c.deleted[k] += 1; return remove.call(this, o); };
|
|
37
|
+
}
|
|
38
|
+
for (const name of ["drawArrays", "drawElements", "drawArraysInstanced", "drawElementsInstanced", "drawRangeElements"]) {
|
|
39
|
+
const draw = proto[name];
|
|
40
|
+
if (draw) proto[name] = function (...a) { c.draws += 1; return draw.apply(this, a); };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const raf = window.requestAnimationFrame.bind(window);
|
|
44
|
+
window.requestAnimationFrame = (callback) => { c.rafRequests += 1; return raf((t) => { c.rafCallbacks += 1; callback(t); }); };
|
|
45
|
+
})();`;
|
|
46
|
+
/** The counters now, as a copy. */
|
|
47
|
+
export function readCounts(page) {
|
|
48
|
+
return page.evaluate(() => JSON.parse(JSON.stringify(window.__kazokuCount)));
|
|
49
|
+
}
|
|
50
|
+
/** GPU objects alive: made minus deleted, over every kind. Flat across a run of changes means nothing leaks. */
|
|
51
|
+
export function gpuObjects(counts) {
|
|
52
|
+
return GPU_KINDS.reduce((sum, kind) => sum + counts.created[kind] - counts.deleted[kind], 0);
|
|
53
|
+
}
|
|
54
|
+
/** The biggest canvas on the page, in device pixels. */
|
|
55
|
+
export function canvasSize(page) {
|
|
56
|
+
return page.evaluate(() => {
|
|
57
|
+
const all = [...document.querySelectorAll("canvas")];
|
|
58
|
+
return { pixels: Math.max(0, ...all.map((c) => c.width * c.height)), side: Math.max(0, ...all.map((c) => Math.max(c.width, c.height))), count: all.length };
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/** True when a canvas is within the budget (the renderer's own: 6 million pixels, 4096 a side). */
|
|
62
|
+
export function withinPixelBudget(size, maxPixels = 6_000_000, maxSide = 4096) {
|
|
63
|
+
return size.pixels <= maxPixels && size.side <= maxSide;
|
|
64
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type Pace } from "./levels.ts";
|
|
1
2
|
import { type Limits } from "./math.ts";
|
|
2
3
|
export type DragMode = "move" | "turn";
|
|
3
4
|
export interface AddressKeys {
|
|
@@ -5,6 +6,8 @@ export interface AddressKeys {
|
|
|
5
6
|
tilt: string;
|
|
6
7
|
zoom: string;
|
|
7
8
|
drag: string;
|
|
9
|
+
/** The auto-turn speed (`slow` or `fast`; Medium is the start and leaves the address). */
|
|
10
|
+
speed?: string;
|
|
8
11
|
}
|
|
9
12
|
export declare const DEFAULT_KEYS: AddressKeys;
|
|
10
13
|
export interface AddressChoices {
|
|
@@ -12,6 +15,7 @@ export interface AddressChoices {
|
|
|
12
15
|
tilt: number;
|
|
13
16
|
zoom: number;
|
|
14
17
|
drag: DragMode;
|
|
18
|
+
pace?: Pace;
|
|
15
19
|
}
|
|
16
20
|
/** 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. */
|
|
17
21
|
export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
|
|
@@ -1,7 +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 { parsePace } from "./levels.js";
|
|
3
4
|
import { clamp, DEFAULT_LIMITS, wrapYaw } from "./math.js";
|
|
4
|
-
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag" };
|
|
5
|
+
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag", speed: "speed" };
|
|
5
6
|
const rounded = (value, places) => Number(value.toFixed(places));
|
|
6
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. */
|
|
7
8
|
export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMITS) {
|
|
@@ -26,6 +27,9 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
|
|
|
26
27
|
const drag = params.get(keys.drag);
|
|
27
28
|
if (drag === "turn" || drag === "move")
|
|
28
29
|
out.drag = drag;
|
|
30
|
+
const pace = parsePace(params.get(keys.speed ?? "speed"));
|
|
31
|
+
if (pace !== undefined)
|
|
32
|
+
out.pace = pace;
|
|
29
33
|
return out;
|
|
30
34
|
}
|
|
31
35
|
/**
|
|
@@ -45,6 +49,10 @@ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = f
|
|
|
45
49
|
set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
|
|
46
50
|
set(keys.zoom, Math.abs(choices.zoom - startZoom) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
|
|
47
51
|
set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
|
|
52
|
+
// The speed key is touched only when the caller has a pace, and a value that is not a pace word (a page's own `speed`) is left alone.
|
|
53
|
+
const speedKey = keys.speed ?? "speed";
|
|
54
|
+
if (choices.pace !== undefined && (choices.pace !== "medium" || parsePace(params.get(speedKey)) !== undefined))
|
|
55
|
+
set(speedKey, choices.pace === "medium" ? undefined : choices.pace);
|
|
48
56
|
const text = params.toString();
|
|
49
57
|
return text === "" ? "" : `?${text}`;
|
|
50
58
|
}
|
|
@@ -13,6 +13,7 @@ export interface ViewChangeDetail extends ViewState {
|
|
|
13
13
|
* 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;
|
|
14
14
|
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second),
|
|
15
15
|
* `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),
|
|
16
|
+
* `controls` (`basic`, the default, or `advanced`: which controls are shown; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
|
|
16
17
|
* `stop-turn` (a press, the wheel or a key on the view stops the automatic turn), `tilt-step` (round a drag's tilt to a multiple of this many degrees), `flat` (the view cannot turn), `tilt-max`, `zoom-min`, `zoom-max`, `address` (keep the choices in the page's address), `address-keys` (`yaw:a,tilt:b`), `theme` (light
|
|
17
18
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
18
19
|
*/
|
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// to the element named by `for`, and tells the page what the view is now with a `kazoku-viewchange` event; the page applies it to its canvas, WebGL, SVG or CSS 3D.
|
|
3
3
|
import { createAddressWriter } from "../address-state/address.js";
|
|
4
4
|
import { adoptStyles } from "../shared/adopt.js";
|
|
5
|
+
import { localStore, remember } from "../shared/store.js";
|
|
5
6
|
import { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
|
|
6
7
|
import { createViewController, DEFAULT_IGNORE } from "./controller.js";
|
|
7
8
|
import { EVENT, TAG } from "./index.js";
|
|
9
|
+
import { LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLevel, zoomText } from "./levels.js";
|
|
8
10
|
import { ZOOM_STEP, ZOOMED } from "./math.js";
|
|
9
11
|
import { STYLES } from "./styles.js";
|
|
10
12
|
import { languageOf, wordsFor } from "./words.js";
|
|
@@ -19,20 +21,27 @@ const ICON = {
|
|
|
19
21
|
right: icon("M9 5l9 7-9 7z"),
|
|
20
22
|
up: icon("M5 15l7-9 7 9z"),
|
|
21
23
|
down: icon("M5 9l7 9 7-9z"),
|
|
24
|
+
more: icon("M3 7h18v2H3zM3 15h18v2H3zM8 4a3 3 0 1 0 0 6 3 3 0 1 0 0-6zM16 12a3 3 0 1 0 0 6 3 3 0 1 0 0-6z"),
|
|
22
25
|
};
|
|
23
26
|
const HTML = `
|
|
24
27
|
<div class="bar" part="bar" role="group">
|
|
25
28
|
<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>
|
|
26
|
-
<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>
|
|
29
|
+
<div class="field tilt adv" 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>
|
|
27
30
|
<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>
|
|
28
|
-
<
|
|
31
|
+
<div class="group pace adv" role="group" data-g="pace"><span class="lbl" data-w="speed" aria-hidden="true"></span>
|
|
32
|
+
<button type="button" data-b="slow" aria-pressed="false" part="button"><span data-w="slow"></span></button>
|
|
33
|
+
<button type="button" data-b="medium" aria-pressed="true" part="button"><span data-w="medium"></span></button>
|
|
34
|
+
<button type="button" data-b="fast" aria-pressed="false" part="button"><span data-w="fast"></span></button>
|
|
35
|
+
</div>
|
|
36
|
+
<button type="button" class="front adv" data-b="front" part="button"><span data-w="front"></span></button>
|
|
29
37
|
<div class="group zoom" role="group" data-g="zoom">
|
|
30
38
|
<button type="button" data-b="out" part="button">${ICON.minus}</button>
|
|
31
|
-
<button type="button" class="
|
|
39
|
+
<button type="button" class="level" data-b="level" part="button"><span data-o="zoom">1×</span></button>
|
|
32
40
|
<button type="button" data-b="in" part="button">${ICON.plus}</button>
|
|
41
|
+
<button type="button" class="two adv" data-b="two" part="button">2×</button>
|
|
33
42
|
<button type="button" data-b="fit" part="button"><span data-w="fit"></span></button>
|
|
34
43
|
</div>
|
|
35
|
-
<div class="group pan" role="group" data-g="pan" hidden>
|
|
44
|
+
<div class="group pan adv" role="group" data-g="pan" hidden>
|
|
36
45
|
<button type="button" data-b="left" part="button">${ICON.left}</button>
|
|
37
46
|
<button type="button" data-b="up" part="button">${ICON.up}</button>
|
|
38
47
|
<button type="button" data-b="down" part="button">${ICON.down}</button>
|
|
@@ -42,17 +51,20 @@ const HTML = `
|
|
|
42
51
|
<button type="button" data-b="turn" aria-pressed="false" part="button"><span data-w="dragTurn"></span></button>
|
|
43
52
|
</div>
|
|
44
53
|
</div>
|
|
54
|
+
<button type="button" class="more" data-b="more" aria-expanded="false" part="button">${ICON.more}<span class="text" data-w="more"></span></button>
|
|
55
|
+
<span class="vh" role="status" aria-live="polite" data-live></span>
|
|
45
56
|
</div>`;
|
|
46
57
|
/**
|
|
47
58
|
* 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
|
|
48
59
|
* 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;
|
|
49
60
|
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `autoplay-for` (stop that turn by itself after this many seconds of turning), `speed` (degrees a second),
|
|
50
61
|
* `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),
|
|
62
|
+
* `controls` (`basic`, the default, or `advanced`: which controls are shown; the reader's own choice with the More button is remembered in localStorage, under `kazoku-view-controls` or the key `storage` names, and wins),
|
|
51
63
|
* `stop-turn` (a press, the wheel or a key on the view stops the automatic turn), `tilt-step` (round a drag's tilt to a multiple of this many degrees), `flat` (the view cannot turn), `tilt-max`, `zoom-min`, `zoom-max`, `address` (keep the choices in the page's address), `address-keys` (`yaw:a,tilt:b`), `theme` (light
|
|
52
64
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
53
65
|
*/
|
|
54
66
|
export class ViewControlsElement extends HTMLElement {
|
|
55
|
-
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
67
|
+
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "anchor", "key-tilt", "stop-turn", "tilt-step", "speed", "controls", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
56
68
|
#root;
|
|
57
69
|
#controller;
|
|
58
70
|
#view;
|
|
@@ -65,6 +77,11 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
65
77
|
#seen;
|
|
66
78
|
#onScreen = true;
|
|
67
79
|
#suspended = false;
|
|
80
|
+
#level = "basic";
|
|
81
|
+
#pace = "medium";
|
|
82
|
+
#labels = wordsFor("en");
|
|
83
|
+
#said = "";
|
|
84
|
+
#sayTimer;
|
|
68
85
|
constructor() {
|
|
69
86
|
super();
|
|
70
87
|
this.#root = this.attachShadow({ mode: "open" });
|
|
@@ -130,10 +147,12 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
130
147
|
if (this.hasAttribute(key) && Number.isFinite(value))
|
|
131
148
|
start[key] = value;
|
|
132
149
|
}
|
|
133
|
-
|
|
134
|
-
|
|
150
|
+
const { pace, ...chosen } = this.hasAttribute("address") ? parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits) : {};
|
|
151
|
+
Object.assign(start, chosen);
|
|
135
152
|
c.turnable = !this.hasAttribute("flat");
|
|
136
|
-
|
|
153
|
+
this.#level = parseLevel(this.#remembered()) ?? parseLevel(this.getAttribute("controls")) ?? "basic";
|
|
154
|
+
this.#pace = pace ?? "medium";
|
|
155
|
+
this.#speed();
|
|
137
156
|
c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
|
|
138
157
|
for (const name of ["key-tilt", "stop-turn", "tilt-step"])
|
|
139
158
|
this.#option(name);
|
|
@@ -174,6 +193,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
174
193
|
this.#view = undefined;
|
|
175
194
|
this.#ready = false;
|
|
176
195
|
this.#writer.cancel();
|
|
196
|
+
clearTimeout(this.#sayTimer);
|
|
197
|
+
this.#said = "";
|
|
177
198
|
}
|
|
178
199
|
attributeChangedCallback(name, _old, value) {
|
|
179
200
|
if (!this.#ready)
|
|
@@ -196,7 +217,11 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
196
217
|
else if (name === "anchor" || name === "key-tilt" || name === "stop-turn" || name === "tilt-step")
|
|
197
218
|
this.#option(name);
|
|
198
219
|
else if (name === "speed")
|
|
199
|
-
|
|
220
|
+
this.#speed();
|
|
221
|
+
else if (name === "controls") {
|
|
222
|
+
this.#level = parseLevel(value) ?? "basic";
|
|
223
|
+
this.#show();
|
|
224
|
+
}
|
|
200
225
|
else if (name === "flat") {
|
|
201
226
|
c.turnable = value === null;
|
|
202
227
|
this.#show();
|
|
@@ -265,6 +290,21 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
265
290
|
on("in", () => c.zoomBy(ZOOM_STEP));
|
|
266
291
|
on("two", () => c.zoomAt(2));
|
|
267
292
|
on("fit", () => c.fit());
|
|
293
|
+
on("level", () => c.fit());
|
|
294
|
+
on("more", () => {
|
|
295
|
+
this.#level = this.#level === "basic" ? "advanced" : "basic";
|
|
296
|
+
remember(this.#storageKey(), this.#level);
|
|
297
|
+
this.#show();
|
|
298
|
+
});
|
|
299
|
+
for (const pace of PACES) {
|
|
300
|
+
on(pace, () => {
|
|
301
|
+
this.#pace = pace;
|
|
302
|
+
this.#speed();
|
|
303
|
+
this.#show();
|
|
304
|
+
if (this.hasAttribute("address"))
|
|
305
|
+
this.#save();
|
|
306
|
+
});
|
|
307
|
+
}
|
|
268
308
|
on("left", () => c.panBy(-1, 0));
|
|
269
309
|
on("right", () => c.panBy(1, 0));
|
|
270
310
|
on("up", () => c.panBy(0, -1));
|
|
@@ -272,6 +312,21 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
272
312
|
on("move", () => c.setDrag("move"));
|
|
273
313
|
on("turn", () => c.setDrag("turn"));
|
|
274
314
|
}
|
|
315
|
+
#storageKey() {
|
|
316
|
+
return this.getAttribute("storage") || LEVEL_STORAGE_KEY;
|
|
317
|
+
}
|
|
318
|
+
#remembered() {
|
|
319
|
+
try {
|
|
320
|
+
return localStore()?.getItem(this.#storageKey()) ?? null;
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
/** The turn's degrees a second: the `speed` attribute (30) times the pace. */
|
|
327
|
+
#speed() {
|
|
328
|
+
this.#ensure().speed = (Number(this.getAttribute("speed")) || 30) * PACE_FACTOR[this.#pace];
|
|
329
|
+
}
|
|
275
330
|
#limits() {
|
|
276
331
|
const c = this.#ensure();
|
|
277
332
|
const read = (name, fallback) => {
|
|
@@ -339,6 +394,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
339
394
|
#label() {
|
|
340
395
|
const language = languageOf(this.getAttribute("lang") ?? this.closest("[lang]")?.getAttribute("lang") ?? this.ownerDocument.documentElement.lang);
|
|
341
396
|
const words = wordsFor(language, this.#words);
|
|
397
|
+
this.#labels = words;
|
|
342
398
|
for (const node of this.#root.querySelectorAll("[data-w]"))
|
|
343
399
|
node.textContent = words[node.dataset.w];
|
|
344
400
|
const set = (selector, label) => this.#root.querySelector(selector)?.setAttribute("aria-label", label);
|
|
@@ -350,11 +406,14 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
350
406
|
set('[data-g="zoom"]', words.zoom);
|
|
351
407
|
set('[data-g="pan"]', words.pan);
|
|
352
408
|
set('[data-g="drag"]', words.drag);
|
|
409
|
+
set('[data-g="pace"]', words.speed);
|
|
353
410
|
set('[data-b="left"]', words.panLeft);
|
|
354
411
|
set('[data-b="right"]', words.panRight);
|
|
355
412
|
set('[data-b="up"]', words.panUp);
|
|
356
413
|
set('[data-b="down"]', words.panDown);
|
|
357
414
|
this.#root.querySelector(".bar")?.setAttribute("lang", language);
|
|
415
|
+
if (this.#ready)
|
|
416
|
+
this.#show();
|
|
358
417
|
}
|
|
359
418
|
#show() {
|
|
360
419
|
const c = this.#ensure();
|
|
@@ -369,15 +428,45 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
369
428
|
q('[data-b="play"]').setAttribute("aria-pressed", String(s.turning));
|
|
370
429
|
q('[data-b="out"]').disabled = s.zoom <= c.limits.zoomMin + 1e-6;
|
|
371
430
|
q('[data-b="in"]').disabled = s.zoom >= c.limits.zoomMax - 1e-6;
|
|
372
|
-
|
|
431
|
+
const advanced = this.#level === "advanced";
|
|
432
|
+
const words = this.#labels;
|
|
433
|
+
const zoom = zoomText(s.zoom);
|
|
434
|
+
q('[data-o="zoom"]').textContent = zoom;
|
|
435
|
+
q('[data-b="level"]').setAttribute("aria-label", `${words.zoomLevel} ${zoom}, ${words.zoomReset}`);
|
|
436
|
+
this.#sayZoom(`${words.zoomLevel} ${zoom}`);
|
|
437
|
+
for (const pace of PACES)
|
|
438
|
+
q(`[data-b="${pace}"]`).setAttribute("aria-pressed", String(this.#pace === pace));
|
|
439
|
+
const more = q('[data-b="more"]');
|
|
440
|
+
more.setAttribute("aria-expanded", String(advanced));
|
|
441
|
+
this.dataset.level = this.#level;
|
|
442
|
+
for (const node of this.#root.querySelectorAll(".adv"))
|
|
443
|
+
node.hidden = !advanced;
|
|
444
|
+
q('[data-g="pan"]').hidden = !advanced || s.zoom <= ZOOMED;
|
|
373
445
|
q('[data-b="move"]').setAttribute("aria-pressed", String(s.drag === "move"));
|
|
374
446
|
q('[data-b="turn"]').setAttribute("aria-pressed", String(s.drag === "turn"));
|
|
375
447
|
// A flat view cannot turn: the angle, tilt, auto-turn and front are not shown, and a drag only moves.
|
|
376
448
|
const flat = !c.turnable;
|
|
377
|
-
for (const selector of [".angle", ".
|
|
449
|
+
for (const selector of [".angle", ".play"])
|
|
378
450
|
q(selector).hidden = flat;
|
|
451
|
+
for (const selector of [".tilt", ".front", '[data-g="drag"]', '[data-g="pace"]'])
|
|
452
|
+
q(selector).hidden = flat || !advanced;
|
|
379
453
|
this.toggleAttribute("data-turning", s.turning);
|
|
380
454
|
}
|
|
455
|
+
/** Tells a screen reader the zoom once it has settled for a moment (a pinch or a wheel makes many steps). */
|
|
456
|
+
#sayZoom(text) {
|
|
457
|
+
if (text === this.#said)
|
|
458
|
+
return;
|
|
459
|
+
const first = this.#said === "";
|
|
460
|
+
this.#said = text;
|
|
461
|
+
if (first)
|
|
462
|
+
return;
|
|
463
|
+
clearTimeout(this.#sayTimer);
|
|
464
|
+
this.#sayTimer = setTimeout(() => {
|
|
465
|
+
const live = this.#root.querySelector('[data-live]');
|
|
466
|
+
if (live !== null)
|
|
467
|
+
live.textContent = text;
|
|
468
|
+
}, 400);
|
|
469
|
+
}
|
|
381
470
|
#changed(state, info) {
|
|
382
471
|
if (!this.#ready)
|
|
383
472
|
return;
|
|
@@ -393,7 +482,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
393
482
|
#save() {
|
|
394
483
|
this.#writer.write((search) => {
|
|
395
484
|
const s = this.#ensure().state;
|
|
396
|
-
return formatAddress(search, s, this.#keys, s.turning, this.#ensure().startZoom);
|
|
485
|
+
return formatAddress(search, { ...s, pace: this.#pace }, this.#keys, s.turning, this.#ensure().startZoom);
|
|
397
486
|
});
|
|
398
487
|
}
|
|
399
488
|
}
|
|
@@ -4,3 +4,4 @@ export { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, TILT_ACRO
|
|
|
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 { LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLevel, parsePace, zoomText, type Level, type Pace } from "./levels.ts";
|
|
@@ -5,3 +5,4 @@ export { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, TILT_ACRO
|
|
|
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 { LEVEL_STORAGE_KEY, PACE_FACTOR, PACES, parseLevel, parsePace, zoomText } from "./levels.js";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** `basic` is the default: turn, zoom, Fit and Auto-turn. `advanced` adds tilt, speed, Front, 2 times and the pan pad. */
|
|
2
|
+
export type Level = "basic" | "advanced";
|
|
3
|
+
export declare const parseLevel: (text: string | null | undefined) => Level | undefined;
|
|
4
|
+
/** The localStorage key the level is remembered under, unless `storage` names another. */
|
|
5
|
+
export declare const LEVEL_STORAGE_KEY = "kazoku-view-controls";
|
|
6
|
+
/** Auto-turn speed: Medium is the element's `speed`, Slow half of it and Fast twice it. */
|
|
7
|
+
export type Pace = "slow" | "medium" | "fast";
|
|
8
|
+
export declare const PACES: readonly Pace[];
|
|
9
|
+
export declare const PACE_FACTOR: Record<Pace, number>;
|
|
10
|
+
export declare const parsePace: (text: string | null | undefined) => Pace | undefined;
|
|
11
|
+
/** The zoom as the readout shows it: `1×`, `2.4×`. */
|
|
12
|
+
export declare const zoomText: (zoom: number) => string;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// The controls' two levels and the auto-turn pace: pure, so a server or a test can use them.
|
|
2
|
+
export const parseLevel = (text) => (text === "basic" || text === "advanced" ? text : undefined);
|
|
3
|
+
/** The localStorage key the level is remembered under, unless `storage` names another. */
|
|
4
|
+
export const LEVEL_STORAGE_KEY = "kazoku-view-controls";
|
|
5
|
+
export const PACES = ["slow", "medium", "fast"];
|
|
6
|
+
export const PACE_FACTOR = { slow: 0.5, medium: 1, fast: 2 };
|
|
7
|
+
export const parsePace = (text) => PACES.find((pace) => pace === text);
|
|
8
|
+
/** The zoom as the readout shows it: `1×`, `2.4×`. */
|
|
9
|
+
export const zoomText = (zoom) => `${Number(zoom.toFixed(1))}×`;
|
|
@@ -1 +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: 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\"])) {\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\"]) {\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:host([overlay]) { position: absolute; inset-inline: 0; bottom: 0; z-index: 1; }\n:host([overlay]) .bar { background: color-mix(in srgb, var(--_surface) 88%, transparent); }\n[hidden] { display: none !important; }\n.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 8px; box-sizing: border-box; }\n.field { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; min-width: 0; }\n.field.tilt { flex-basis: 150px; }\n.field label { flex: none; }\n.field output { flex: none; min-width: 3.2em; text-align: end; font-variant-numeric: tabular-nums; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\ninput[type=\"range\"] { -webkit-appearance: none; appearance: none; flex: 1 1 auto; width: 100%; min-width: 60px; height: 48px; margin: 0; padding: 0; background: transparent; cursor: pointer; }\ninput[type=\"range\"]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-moz-range-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--_ink); outline-offset: -4px; border-radius: 8px; }\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; }\nbutton[aria-pressed=\"true\"] .i-play, button[aria-pressed=\"false\"] .i-pause { display: none; }\n.zoom button.two { min-width: 56px; }\n.drag { margin-inline-start: 8px; }\n/* A compact bar on a phone:
|
|
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: 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\"])) {\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\"]) {\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:host([overlay]) { position: absolute; inset-inline: 0; bottom: 0; z-index: 1; }\n:host([overlay]) .bar { background: color-mix(in srgb, var(--_surface) 88%, transparent); }\n[hidden] { display: none !important; }\n.bar { display: flex; flex-wrap: wrap; align-items: center; gap: 4px 12px; padding: 4px 8px; box-sizing: border-box; }\n.field { display: flex; align-items: center; gap: 8px; flex: 1 1 200px; min-width: 0; }\n.field.tilt { flex-basis: 150px; }\n.field label { flex: none; }\n.field output { flex: none; min-width: 3.2em; text-align: end; font-variant-numeric: tabular-nums; }\n.group { display: inline-flex; align-items: center; flex-wrap: wrap; gap: 4px; }\n.vh { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }\ninput[type=\"range\"] { -webkit-appearance: none; appearance: none; flex: 1 1 auto; width: 100%; min-width: 60px; height: 48px; margin: 0; padding: 0; background: transparent; cursor: pointer; }\ninput[type=\"range\"]::-webkit-slider-runnable-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-moz-range-track { height: 6px; border-radius: 3px; background: var(--_line); }\ninput[type=\"range\"]::-webkit-slider-thumb { -webkit-appearance: none; appearance: none; width: 28px; height: 28px; margin-top: -11px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]::-moz-range-thumb { width: 22px; height: 22px; border-radius: 50%; background: var(--_accent); border: 3px solid var(--_surface); box-shadow: 0 0 0 1px var(--_ink); }\ninput[type=\"range\"]:focus-visible { outline: 2px solid var(--_ink); outline-offset: -4px; border-radius: 8px; }\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[aria-expanded=\"true\"] { border-color: var(--_ink); box-shadow: inset 0 0 0 1px var(--_ink); }\nbutton:disabled { opacity: 0.4; cursor: default; }\nsvg { width: 18px; height: 18px; fill: currentColor; flex: none; }\nbutton[aria-pressed=\"true\"] .i-play, button[aria-pressed=\"false\"] .i-pause { display: none; }\n.zoom button.two { min-width: 56px; }\n.zoom button.level { min-width: 64px; padding: 0 8px; font-variant-numeric: tabular-nums; }\n.pace .lbl { margin-inline-end: 4px; }\n.drag { margin-inline-start: 8px; }\n.more { margin-inline-start: auto; }\n/* A compact bar on a phone: two rows to start with (angle, Auto-turn and More; the zoom), more rows when the reader asks for more; labels read only by a screen reader. */\n@media (max-width: 640px) {\n .bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px 48px; grid-template-areas: \"angle play more\" \"tilt tilt front\" \"pace pace pace\" \"zoom zoom zoom\" \"pan pan pan\"; gap: 0 8px; padding: 2px 8px; }\n .field.angle { grid-area: angle; }\n .field.tilt { grid-area: tilt; }\n .field label, .field output, .play .text, .more .text, .pace .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }\n .play, .more { padding: 0; }\n .play { grid-area: play; }\n .more { grid-area: more; }\n .front { grid-area: front; padding: 0 12px; }\n .pace { grid-area: pace; justify-self: end; flex-wrap: nowrap; }\n .pace button { padding: 0 12px; }\n .zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }\n .zoom button { padding: 0 8px; }\n .zoom button.level { padding: 0 6px; }\n .pan { grid-area: pan; justify-content: center; }\n .drag { margin-inline-start: 4px; }\n}\n";
|
|
@@ -48,22 +48,30 @@ button { -webkit-appearance: none; appearance: none; box-sizing: border-box; dis
|
|
|
48
48
|
button:hover:not(:disabled) { border-color: var(--_ink); }
|
|
49
49
|
button:focus-visible { outline: 2px solid var(--_ink); outline-offset: 2px; }
|
|
50
50
|
button[aria-pressed="true"] { background: var(--_ink); color: var(--_surface); border-color: var(--_ink); }
|
|
51
|
+
button[aria-expanded="true"] { border-color: var(--_ink); box-shadow: inset 0 0 0 1px var(--_ink); }
|
|
51
52
|
button:disabled { opacity: 0.4; cursor: default; }
|
|
52
53
|
svg { width: 18px; height: 18px; fill: currentColor; flex: none; }
|
|
53
54
|
button[aria-pressed="true"] .i-play, button[aria-pressed="false"] .i-pause { display: none; }
|
|
54
55
|
.zoom button.two { min-width: 56px; }
|
|
56
|
+
.zoom button.level { min-width: 64px; padding: 0 8px; font-variant-numeric: tabular-nums; }
|
|
57
|
+
.pace .lbl { margin-inline-end: 4px; }
|
|
55
58
|
.drag { margin-inline-start: 8px; }
|
|
56
|
-
|
|
59
|
+
.more { margin-inline-start: auto; }
|
|
60
|
+
/* A compact bar on a phone: two rows to start with (angle, Auto-turn and More; the zoom), more rows when the reader asks for more; labels read only by a screen reader. */
|
|
57
61
|
@media (max-width: 640px) {
|
|
58
|
-
.bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px
|
|
62
|
+
.bar { display: grid; grid-template-columns: minmax(0, 1fr) 48px 48px; grid-template-areas: "angle play more" "tilt tilt front" "pace pace pace" "zoom zoom zoom" "pan pan pan"; gap: 0 8px; padding: 2px 8px; }
|
|
59
63
|
.field.angle { grid-area: angle; }
|
|
60
64
|
.field.tilt { grid-area: tilt; }
|
|
61
|
-
.field label, .field output { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
62
|
-
.play {
|
|
63
|
-
.play
|
|
65
|
+
.field label, .field output, .play .text, .more .text, .pace .lbl { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); }
|
|
66
|
+
.play, .more { padding: 0; }
|
|
67
|
+
.play { grid-area: play; }
|
|
68
|
+
.more { grid-area: more; }
|
|
64
69
|
.front { grid-area: front; padding: 0 12px; }
|
|
70
|
+
.pace { grid-area: pace; justify-self: end; flex-wrap: nowrap; }
|
|
71
|
+
.pace button { padding: 0 12px; }
|
|
65
72
|
.zoom { grid-area: zoom; justify-self: end; flex-wrap: nowrap; }
|
|
66
73
|
.zoom button { padding: 0 8px; }
|
|
74
|
+
.zoom button.level { padding: 0 6px; }
|
|
67
75
|
.pan { grid-area: pan; justify-content: center; }
|
|
68
76
|
.drag { margin-inline-start: 4px; }
|
|
69
77
|
}
|
|
@@ -17,6 +17,13 @@ export interface ViewWords {
|
|
|
17
17
|
drag: string;
|
|
18
18
|
dragMove: string;
|
|
19
19
|
dragTurn: string;
|
|
20
|
+
zoomLevel: string;
|
|
21
|
+
zoomReset: string;
|
|
22
|
+
more: string;
|
|
23
|
+
speed: string;
|
|
24
|
+
slow: string;
|
|
25
|
+
medium: string;
|
|
26
|
+
fast: string;
|
|
20
27
|
}
|
|
21
28
|
export type Language = "en" | "ja";
|
|
22
29
|
export declare const WORDS: Record<Language, ViewWords>;
|
|
@@ -19,6 +19,13 @@ export const WORDS = {
|
|
|
19
19
|
drag: "What a drag does",
|
|
20
20
|
dragMove: "Move",
|
|
21
21
|
dragTurn: "Turn",
|
|
22
|
+
zoomLevel: "Zoom level",
|
|
23
|
+
zoomReset: "back to Fit",
|
|
24
|
+
more: "More",
|
|
25
|
+
speed: "Speed",
|
|
26
|
+
slow: "Slow",
|
|
27
|
+
medium: "Medium",
|
|
28
|
+
fast: "Fast",
|
|
22
29
|
},
|
|
23
30
|
ja: {
|
|
24
31
|
group: "表示の操作",
|
|
@@ -39,6 +46,13 @@ export const WORDS = {
|
|
|
39
46
|
drag: "ドラッグの動作",
|
|
40
47
|
dragMove: "移動",
|
|
41
48
|
dragTurn: "回転",
|
|
49
|
+
zoomLevel: "ズーム倍率",
|
|
50
|
+
zoomReset: "全体に戻す",
|
|
51
|
+
more: "詳細",
|
|
52
|
+
speed: "速さ",
|
|
53
|
+
slow: "ゆっくり",
|
|
54
|
+
medium: "ふつう",
|
|
55
|
+
fast: "はやい",
|
|
42
56
|
},
|
|
43
57
|
};
|
|
44
58
|
/** The language to use for a `lang` value such as "ja", "ja-JP" or "en-CA": Japanese for ja, English for anything else. */
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazoku-ui",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) a light, dark and auto theme switch, a language picker, choices kept in the address and a save-as-image button, each a framework-free custom element and a plain controller. Zero dependencies.",
|
|
3
|
+
"version": "0.9.0",
|
|
4
|
+
"description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) a light, dark and auto theme switch, a language picker, choices kept in the address and a save-as-image button, each a framework-free custom element and a plain controller. Zero dependencies; the shared 3D renderer takes three.js as an optional peer.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"view-controls",
|
|
7
7
|
"3d",
|
|
@@ -26,7 +26,11 @@
|
|
|
26
26
|
"save-image",
|
|
27
27
|
"png",
|
|
28
28
|
"svg",
|
|
29
|
-
"download"
|
|
29
|
+
"download",
|
|
30
|
+
"webgl",
|
|
31
|
+
"three",
|
|
32
|
+
"three.js",
|
|
33
|
+
"renderer"
|
|
30
34
|
],
|
|
31
35
|
"homepage": "https://johnmorrisdotca.github.io/kazoku-ui/",
|
|
32
36
|
"repository": {
|
|
@@ -118,6 +122,16 @@
|
|
|
118
122
|
"types": "./dist/save-image/define.d.ts",
|
|
119
123
|
"import": "./dist/save-image/define.js",
|
|
120
124
|
"default": "./dist/save-image/define.js"
|
|
125
|
+
},
|
|
126
|
+
"./renderer": {
|
|
127
|
+
"types": "./dist/renderer/index.d.ts",
|
|
128
|
+
"import": "./dist/renderer/index.js",
|
|
129
|
+
"default": "./dist/renderer/index.js"
|
|
130
|
+
},
|
|
131
|
+
"./testing": {
|
|
132
|
+
"types": "./dist/testing/index.d.ts",
|
|
133
|
+
"import": "./dist/testing/index.js",
|
|
134
|
+
"default": "./dist/testing/index.js"
|
|
121
135
|
}
|
|
122
136
|
},
|
|
123
137
|
"files": [
|
|
@@ -129,6 +143,14 @@
|
|
|
129
143
|
"engines": {
|
|
130
144
|
"node": ">=22"
|
|
131
145
|
},
|
|
146
|
+
"peerDependencies": {
|
|
147
|
+
"three": ">=0.160.0"
|
|
148
|
+
},
|
|
149
|
+
"peerDependenciesMeta": {
|
|
150
|
+
"three": {
|
|
151
|
+
"optional": true
|
|
152
|
+
}
|
|
153
|
+
},
|
|
132
154
|
"scripts": {
|
|
133
155
|
"build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.json",
|
|
134
156
|
"lint": "eslint .",
|
|
@@ -147,6 +169,7 @@
|
|
|
147
169
|
"@playwright/test": "^1.63.0",
|
|
148
170
|
"@types/node": "^24.0.0",
|
|
149
171
|
"eslint": "^9.0.0",
|
|
172
|
+
"three": "0.186.1",
|
|
150
173
|
"typescript": "^5.9.0",
|
|
151
174
|
"typescript-eslint": "^8.0.0"
|
|
152
175
|
},
|