@johnmorrisdotca/kazoku-ui 0.0.0-stage → 0.2.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/LICENSE +21 -0
- package/README.md +178 -2
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/view-controls/address.d.ts +24 -0
- package/dist/view-controls/address.js +60 -0
- package/dist/view-controls/controller.d.ts +74 -0
- package/dist/view-controls/controller.js +385 -0
- package/dist/view-controls/define.d.ts +1 -0
- package/dist/view-controls/define.js +14 -0
- package/dist/view-controls/element.d.ts +41 -0
- package/dist/view-controls/element.js +341 -0
- package/dist/view-controls/index.d.ts +6 -0
- package/dist/view-controls/index.js +7 -0
- package/dist/view-controls/math.d.ts +50 -0
- package/dist/view-controls/math.js +48 -0
- package/dist/view-controls/styles.d.ts +1 -0
- package/dist/view-controls/styles.js +70 -0
- package/dist/view-controls/words.d.ts +26 -0
- package/dist/view-controls/words.js +47 -0
- package/package.json +92 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## [0.2.0] - 2026-11-01
|
|
4
|
+
|
|
5
|
+
- A button, link or form control inside the view still works: a pointer press on a child that matches the new `ignore` selector is not captured, so the click goes through and a drag from the empty view still turns it. Attribute `ignore` and `createViewController` option `ignore`; the default is `button, a, input, select, textarea, label, [data-kazoku-ignore]` (`DEFAULT_IGNORE`); `ignore=""` leaves nothing alone.
|
|
6
|
+
- Open at a zoom set against the page's fit scale: `zoom` is `fit` (the default, unchanged) or a number where 1 is the scene's natural size, and `fit-scale` / `setFitScale(n)` / the `fitScale` option tell the controls what zoom 1 means. The event's `zoom` stays 1 at Fit. New controller members `fitScale`, `startZoom`, `setStartZoom`; a new `fit-scale` reason in the event. `formatAddress` takes a fifth argument, `startZoom`, so the opening zoom keeps the address plain.
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2026-11-01
|
|
9
|
+
|
|
10
|
+
The first release, and its first part: view controls, at `@johnmorrisdotca/kazoku-ui/view-controls`.
|
|
11
|
+
|
|
12
|
+
- `<kazoku-view-controls>`, a framework-free custom element (`/view-controls/element`, registered by `/view-controls/define`, which leaves a tag already taken alone), and `createViewController`, the same hands without an element.
|
|
13
|
+
- Angle and tilt sliders, Auto-turn with play and pause, Front; zoom out, 2 times, in and Fit; zoom about the pointer and with Ctrl or Command and the wheel; the four-arrow pan pad and the Move or Turn choice for a drag.
|
|
14
|
+
- Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
|
|
15
|
+
- Choices kept in the address, with keys you can rename (`parseAddress`, `formatAddress`).
|
|
16
|
+
- It never draws: it reports `{ yaw, tilt, zoom, panX, panY, turning, drag }` in a `kazoku-viewchange` event or an `onChange` callback.
|
|
17
|
+
- Labels in English and Japanese; light and dark; 48 px touch targets; a compact bar on a phone; no automatic turn for a reader who prefers reduced motion.
|
|
18
|
+
- No runtime dependencies.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 John Morris
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,179 @@
|
|
|
1
|
-
#
|
|
1
|
+
# Kazoku UI 家族
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/johnmorrisdotca/kazoku-ui/main/docs/images/hero-dark.webp">
|
|
5
|
+
<img alt="The demo: a white card with a blue circle and a black triangle, turned a little and tipped, in a frame with a dotted floor. Under it the view controls: Angle and Tilt sliders, Auto-turn and Front buttons, zoom out, 2 times, zoom in and Fit, four arrows to move the zoomed view, and the Move and Turn choice for a drag." src="https://raw.githubusercontent.com/johnmorrisdotca/kazoku-ui/main/docs/images/hero-light.webp" width="720">
|
|
6
|
+
</picture>
|
|
7
|
+
|
|
8
|
+
The family's shared interface parts, as their own versioned package: one part per subpath, so a page loads only what it uses. No runtime dependencies, MIT.
|
|
9
|
+
The first part is the **view controls** for any 3D or zoomable view. They never draw the scene: they tell your page what the reader chose, and your canvas, WebGL,
|
|
10
|
+
SVG or CSS 3D applies it.
|
|
11
|
+
|
|
12
|
+
[Demo](https://johnmorrisdotca.github.io/kazoku-ui/) · [Labels, English and 日本語](docs/labels.md) · [Changelog](CHANGELOG.md)
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
npm install @johnmorrisdotca/kazoku-ui
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Parts (each its own subpath; later ones: picture-tile pickers, theme swatches with light and dark, the English and 日本語 switch, Help, GitHub and npm pills, Save SVG and PNG, the family's backgrounds):
|
|
19
|
+
|
|
20
|
+
| Subpath | What it is |
|
|
21
|
+
|---|---|
|
|
22
|
+
| `@johnmorrisdotca/kazoku-ui/view-controls` | The controller, the arithmetic, the address and the labels. Imports on a server. |
|
|
23
|
+
| `@johnmorrisdotca/kazoku-ui/view-controls/element` | The `ViewControlsElement` class (needs a browser). |
|
|
24
|
+
| `@johnmorrisdotca/kazoku-ui/view-controls/define` | Registers `<kazoku-view-controls>` (once, and only if the tag is free). |
|
|
25
|
+
|
|
26
|
+
## View controls
|
|
27
|
+
|
|
28
|
+
- Angle and tilt sliders, Auto-turn with play and pause, Front.
|
|
29
|
+
- Zoom out, 2 times, zoom in and Fit. Zoom is about the pointer (and Ctrl or Command with the wheel, or a trackpad's pinch).
|
|
30
|
+
- The four-arrow pan pad (shown once zoomed in) and the Move or Turn choice for what a drag does.
|
|
31
|
+
- Touch: one finger turns at 1x and moves the view when zoomed in; two fingers pinch.
|
|
32
|
+
- Keys on the view: arrows turn and tip it, `+` and `-` zoom, `0` or Home is Front and Fit.
|
|
33
|
+
- The choices are kept in the address, with keys you can rename.
|
|
34
|
+
- Labels in English and Japanese; light and dark; 48 px touch targets; a compact bar on a phone; no automatic turn for a reader who prefers reduced motion.
|
|
35
|
+
- **A control never stops or resets the turn.** Zoom, tilt, pan, the Move or Turn choice, Fit and Front all leave Auto-turn running (Front only sets the angle to 0 and the turn goes on from there). A hand on the angle slider, or turning the view by dragging it, holds the turn still while it is down and the turn carries on from where the hand left it. Only the Auto-turn button pauses it.
|
|
36
|
+
|
|
37
|
+
### In plain HTML
|
|
38
|
+
|
|
39
|
+
```html
|
|
40
|
+
<div id="scene" tabindex="0" style="height: 320px">...your scene...</div>
|
|
41
|
+
<kazoku-view-controls for="scene" address></kazoku-view-controls>
|
|
42
|
+
|
|
43
|
+
<script type="module">
|
|
44
|
+
import "@johnmorrisdotca/kazoku-ui/view-controls/define";
|
|
45
|
+
|
|
46
|
+
const scene = document.getElementById("scene");
|
|
47
|
+
document.querySelector("kazoku-view-controls").addEventListener("kazoku-viewchange", (event) => {
|
|
48
|
+
const { yaw, tilt, zoom, panX, panY } = event.detail;
|
|
49
|
+
scene.style.transform = `translate(${panX}px, ${panY}px) scale(${zoom}) rotateX(${tilt}deg) rotateY(${yaw}deg)`;
|
|
50
|
+
});
|
|
51
|
+
</script>
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Put the element on the view or right beside it, so it is on screen with the view. `for` names the element the hands bind to (the pointer, pinch, wheel and keys); give it a `tabindex` if you want the keys on it, and size it to the view, because the pan is held to the room the zoom gives. With `overlay` the bar sits over the bottom of its positioned parent instead.
|
|
55
|
+
|
|
56
|
+
### In a framework
|
|
57
|
+
|
|
58
|
+
The element is plain DOM, so any framework uses it with a ref and one listener. React:
|
|
59
|
+
|
|
60
|
+
```jsx
|
|
61
|
+
import "@johnmorrisdotca/kazoku-ui/view-controls/define";
|
|
62
|
+
import { useEffect, useRef } from "react";
|
|
63
|
+
|
|
64
|
+
export function Viewer({ draw }) {
|
|
65
|
+
const view = useRef(null);
|
|
66
|
+
const controls = useRef(null);
|
|
67
|
+
useEffect(() => {
|
|
68
|
+
const bar = controls.current;
|
|
69
|
+
bar.view = view.current;
|
|
70
|
+
const on = (event) => draw(event.detail);
|
|
71
|
+
bar.addEventListener("kazoku-viewchange", on);
|
|
72
|
+
return () => bar.removeEventListener("kazoku-viewchange", on);
|
|
73
|
+
}, [draw]);
|
|
74
|
+
return (
|
|
75
|
+
<>
|
|
76
|
+
<div ref={view} tabIndex={0} style={{ height: 320 }} />
|
|
77
|
+
<kazoku-view-controls ref={controls} lang="en" />
|
|
78
|
+
</>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Without the element
|
|
84
|
+
|
|
85
|
+
`createViewController` is the same hands and state with no markup of its own, for a page that draws its own buttons. It touches the DOM only in `bind`.
|
|
86
|
+
|
|
87
|
+
```js
|
|
88
|
+
import { createViewController } from "@johnmorrisdotca/kazoku-ui/view-controls";
|
|
89
|
+
|
|
90
|
+
const view = createViewController({
|
|
91
|
+
speed: 30, // degrees a second for Auto-turn
|
|
92
|
+
onChange(state, { final, reason }) {
|
|
93
|
+
draw(state); // { yaw, tilt, zoom, panX, panY, turning, drag }
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
const unbind = view.bind(document.getElementById("scene")); // pointer, pinch, wheel, keys
|
|
97
|
+
myPlayButton.onclick = () => view.toggle();
|
|
98
|
+
myZoomIn.onclick = () => view.zoomBy(1.4);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Options of `createViewController` that match: `zoom` (`"fit"` or a number), `fitScale`, `ignore`. The controller also has `ignore` (changeable), `fitScale`, `startZoom` (the `zoom` the view opens at) and `setFitScale(n)`, `setStartZoom(zoom)`; `DEFAULT_IGNORE` is the default selector.
|
|
102
|
+
|
|
103
|
+
### Buttons and links inside the view
|
|
104
|
+
|
|
105
|
+
The hands capture the pointer on the `for` element, so a press on a button or link placed inside it would be taken for a drag and its click lost. A press on anything that matches `ignore` is left alone (no capture, no drag, no double-tap), and the click goes through. Mark any other child with `data-kazoku-ignore`. Dragging from the empty part of the view turns it as before.
|
|
106
|
+
|
|
107
|
+
### Opening at the scene's natural size
|
|
108
|
+
|
|
109
|
+
Zoom 1 is Fit, whatever size that is on the page. A page that draws its scene at `fitScale` (for example 0.4 of natural size when it fits the view) and wants to open at natural size sets `zoom="1"` and tells the controls the scale:
|
|
110
|
+
|
|
111
|
+
```js
|
|
112
|
+
const controls = document.querySelector("kazoku-view-controls");
|
|
113
|
+
controls.setAttribute("zoom", "1"); // natural size
|
|
114
|
+
controls.setFitScale(measure()); // the scale the scene is drawn at when it fits, for example 0.4
|
|
115
|
+
// the event's zoom is now 2.5; draw at event.detail.zoom * controls.fitScale
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`setFitScale` moves the view to the opening zoom for the new scale, unless the reader has already zoomed. Fit and the Fit key still go to zoom 1, and the address leaves a zoom out when it is the opening one. `zoom="fit"`, or no `zoom`, changes nothing from before.
|
|
119
|
+
|
|
120
|
+
## The event API
|
|
121
|
+
|
|
122
|
+
`kazoku-viewchange` is dispatched on the element (it bubbles and crosses shadow roots). `event.detail`, and the first argument of `onChange`, is:
|
|
123
|
+
|
|
124
|
+
| Field | Meaning |
|
|
125
|
+
|---|---|
|
|
126
|
+
| `yaw` | Degrees, -180 up to 180: the turn about the vertical. Dragging right makes it larger. |
|
|
127
|
+
| `tilt` | Degrees, held to plus or minus `tilt-max` (30): the top tips away as it gets larger (dragging up). |
|
|
128
|
+
| `zoom` | 1 to 5 (`zoom-min`, `zoom-max`): 1 is Fit. The scene is drawn at `zoom * fitScale` (see below). |
|
|
129
|
+
| `panX`, `panY` | CSS pixels the zoomed picture is moved from the middle of the view; 0 at 1x. |
|
|
130
|
+
| `turning` | True while Auto-turn is running. |
|
|
131
|
+
| `drag` | `"move"` or `"turn"`: what a plain drag does while zoomed in. |
|
|
132
|
+
| `final` | False for the steps of a drag, a pinch, a slider and the automatic turn; true when something settled. Do the costly drawing on `final`, the cheap transform on every event. |
|
|
133
|
+
| `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `fit-scale`, `play`, `pause` or `set`. |
|
|
134
|
+
|
|
135
|
+
An `init` event is sent once, a moment after the element connects, with the starting view (from the attributes and the address). `element.state` is the current view at any time.
|
|
136
|
+
|
|
137
|
+
### Attributes
|
|
138
|
+
|
|
139
|
+
| Attribute | |
|
|
140
|
+
|---|---|
|
|
141
|
+
| `for` | The id of the view the hands bind to. Or set `element.view = someElement`. |
|
|
142
|
+
| `lang` | `en` or `ja`. Otherwise the nearest `lang` above it, else English. |
|
|
143
|
+
| `yaw`, `tilt` | The starting angle and tilt. |
|
|
144
|
+
| `zoom` | The starting zoom: `fit` (the default) or a number, a scale where 1 is the scene's natural size. It opens at that number divided by `fit-scale`; with `fit-scale` left at 1 it is the `zoom` of the event. |
|
|
145
|
+
| `fit-scale` | The scale your page draws the scene at when it is at Fit (event `zoom` 1): what zoom 1 means. 1 by default. Or call `element.setFitScale(n)` once the scene is measured. |
|
|
146
|
+
| `ignore` | A CSS selector for what inside the view a pointer press leaves alone, so its click goes through: `button, a, input, select, textarea, label, [data-kazoku-ignore]` by default. A bare `ignore=""` leaves nothing alone. |
|
|
147
|
+
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
148
|
+
| `speed` | Degrees a second (30). |
|
|
149
|
+
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
150
|
+
| `tilt-max`, `zoom-min`, `zoom-max` | The limits (30, 1, 5). |
|
|
151
|
+
| `address` | Keep the choices in the page's address (`?yaw=40&tilt=10&zoom=2&drag=turn`). |
|
|
152
|
+
| `address-keys` | Other keys: `yaw:a,zoom:z`. |
|
|
153
|
+
| `theme` | `light` or `dark`; otherwise the page's colour scheme. |
|
|
154
|
+
| `overlay` | Sit over the bottom of a positioned parent. |
|
|
155
|
+
|
|
156
|
+
Methods: `play()`, `pause()`, `front()`, `fit()`, `set({ yaw, tilt, zoom, panX, panY, drag })`, `setFitScale(n)`, and `controller` (the `createViewController` object behind it). `element.words = { front: "Home" }` replaces labels.
|
|
157
|
+
Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-line`, `--kazoku-accent`; the parts are `bar`, `field`, `slider` and `button`.
|
|
158
|
+
|
|
159
|
+
### The address
|
|
160
|
+
|
|
161
|
+
Only what a reader chose is kept: angle, tilt, zoom and the drag choice. The pan is in pixels of one screen and the turning is of a moment, so neither is; while the view turns by itself the angle is left out. A value at its start (Fit, or the `zoom` the page opens at) leaves the address, and other parameters are kept. The address is written at most four times a second (Safari stops a page that changes its address more than 100 times in 30 seconds). Without the element: `parseAddress(search, keys?)` and `formatAddress(search, choices, keys?, leaveYaw?, startZoom?)`.
|
|
162
|
+
|
|
163
|
+
### Why the tag is only defined when it is free
|
|
164
|
+
|
|
165
|
+
`customElements.define` throws when a name is taken, and a page can meet that without any fault: two copies of this package (an app and a widget that each bundle one, or two versions), the script loaded twice, or a page that registered the tag to supply its own element. A throw would stop the importing module and the page's script with it. So `define` calls `customElements.get` first and the first definition wins; `defineViewControls(registry?)` returns whether it defined the tag.
|
|
166
|
+
|
|
167
|
+
## Size
|
|
168
|
+
|
|
169
|
+
Gzipped, unbundled: the element and its definition about 5.0 KB, the controller 3.9 KB, the maths 1.4 KB, the address 1.2 KB, the labels 0.7 KB and the styles 1.6 KB (`pnpm test:package` prints them). Nothing is loaded from the network.
|
|
170
|
+
|
|
171
|
+
## Checks
|
|
172
|
+
|
|
173
|
+
`pnpm check` (lint, types, 29 unit tests of the maths, the address, the controller and the package, the build and the packed package), and `pnpm test:demo` (the demo in Chromium and WebKit at a phone's width and a desk's: the controls in the first screen, a toggle during Auto-turn leaves the angle advancing, a button or link inside the view still clicks while a drag turns it, the opening zoom and fit scale, zoom about the pointer, pan when zoomed, touch, the address, the labels, reduced motion).
|
|
174
|
+
|
|
175
|
+
## Not yet
|
|
176
|
+
|
|
177
|
+
Not published to npm yet. The Japanese labels have not yet been read by a native speaker. The demo's scene is a CSS 3D card; the package has no opinion about what draws.
|
|
178
|
+
|
|
179
|
+
Licence: MIT. No brand, league or sponsor mark is in this package.
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type Limits } from "./math.ts";
|
|
2
|
+
export type DragMode = "move" | "turn";
|
|
3
|
+
export interface AddressKeys {
|
|
4
|
+
yaw: string;
|
|
5
|
+
tilt: string;
|
|
6
|
+
zoom: string;
|
|
7
|
+
drag: string;
|
|
8
|
+
}
|
|
9
|
+
export declare const DEFAULT_KEYS: AddressKeys;
|
|
10
|
+
export interface AddressChoices {
|
|
11
|
+
yaw: number;
|
|
12
|
+
tilt: number;
|
|
13
|
+
zoom: number;
|
|
14
|
+
drag: DragMode;
|
|
15
|
+
}
|
|
16
|
+
/** 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
|
+
export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
|
|
18
|
+
/**
|
|
19
|
+
* The query string (with its `?`, or empty) that keeps `choices` and every other parameter of `search`. A choice at its starting value is left out, so
|
|
20
|
+
* the plain address stays plain. `leaveYaw` keeps the angle out while the view turns by itself (it would change every frame). `startZoom` is the zoom the view opens at (1, Fit, unless the page starts elsewhere).
|
|
21
|
+
*/
|
|
22
|
+
export declare function formatAddress(search: string, choices: AddressChoices, keys?: AddressKeys, leaveYaw?: boolean, startZoom?: number): string;
|
|
23
|
+
/** Read `address-keys`: `yaw:a,tilt:b` renames the keys it names and leaves the others. */
|
|
24
|
+
export declare function parseKeys(text: string | null): AddressKeys;
|
|
@@ -0,0 +1,60 @@
|
|
|
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
|
+
// (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 { clamp, DEFAULT_LIMITS, wrapYaw } from "./math.js";
|
|
4
|
+
export const DEFAULT_KEYS = { yaw: "yaw", tilt: "tilt", zoom: "zoom", drag: "drag" };
|
|
5
|
+
const rounded = (value, places) => Number(value.toFixed(places));
|
|
6
|
+
/** 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
|
+
export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMITS) {
|
|
8
|
+
const params = new URLSearchParams(search);
|
|
9
|
+
const out = {};
|
|
10
|
+
const number = (key) => {
|
|
11
|
+
const text = params.get(key);
|
|
12
|
+
if (text === null || text.trim() === "")
|
|
13
|
+
return undefined;
|
|
14
|
+
const value = Number(text);
|
|
15
|
+
return Number.isFinite(value) ? value : undefined;
|
|
16
|
+
};
|
|
17
|
+
const yaw = number(keys.yaw);
|
|
18
|
+
const tilt = number(keys.tilt);
|
|
19
|
+
const zoom = number(keys.zoom);
|
|
20
|
+
if (yaw !== undefined)
|
|
21
|
+
out.yaw = rounded(wrapYaw(yaw), 0);
|
|
22
|
+
if (tilt !== undefined)
|
|
23
|
+
out.tilt = rounded(clamp(tilt, -limits.tiltMax, limits.tiltMax), 0);
|
|
24
|
+
if (zoom !== undefined)
|
|
25
|
+
out.zoom = rounded(clamp(zoom, limits.zoomMin, limits.zoomMax), 2);
|
|
26
|
+
const drag = params.get(keys.drag);
|
|
27
|
+
if (drag === "turn" || drag === "move")
|
|
28
|
+
out.drag = drag;
|
|
29
|
+
return out;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The query string (with its `?`, or empty) that keeps `choices` and every other parameter of `search`. A choice at its starting value is left out, so
|
|
33
|
+
* the plain address stays plain. `leaveYaw` keeps the angle out while the view turns by itself (it would change every frame). `startZoom` is the zoom the view opens at (1, Fit, unless the page starts elsewhere).
|
|
34
|
+
*/
|
|
35
|
+
export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = false, startZoom = 1) {
|
|
36
|
+
const params = new URLSearchParams(search);
|
|
37
|
+
const set = (key, value) => {
|
|
38
|
+
if (value === undefined)
|
|
39
|
+
params.delete(key);
|
|
40
|
+
else
|
|
41
|
+
params.set(key, value);
|
|
42
|
+
};
|
|
43
|
+
if (!leaveYaw)
|
|
44
|
+
set(keys.yaw, Math.round(choices.yaw) === 0 ? undefined : String(rounded(wrapYaw(choices.yaw), 0)));
|
|
45
|
+
set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
|
|
46
|
+
set(keys.zoom, Math.abs(choices.zoom - startZoom) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
|
|
47
|
+
set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
|
|
48
|
+
const text = params.toString();
|
|
49
|
+
return text === "" ? "" : `?${text}`;
|
|
50
|
+
}
|
|
51
|
+
/** Read `address-keys`: `yaw:a,tilt:b` renames the keys it names and leaves the others. */
|
|
52
|
+
export function parseKeys(text) {
|
|
53
|
+
const keys = { ...DEFAULT_KEYS };
|
|
54
|
+
for (const part of (text ?? "").split(",")) {
|
|
55
|
+
const [name, key] = part.split(":").map((s) => s.trim());
|
|
56
|
+
if (name !== undefined && key !== undefined && key !== "" && name in keys)
|
|
57
|
+
keys[name] = key;
|
|
58
|
+
}
|
|
59
|
+
return keys;
|
|
60
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import type { DragMode } from "./address.ts";
|
|
2
|
+
import { type Camera, type Limits, type Point, type Size } from "./math.ts";
|
|
3
|
+
export interface ViewState extends Camera {
|
|
4
|
+
/** True while the view turns by itself. A control never changes this; only play, pause and a grab of the view's own do not either (see `holdTurn`). */
|
|
5
|
+
turning: boolean;
|
|
6
|
+
/** What a plain drag does while zoomed in: move the view, or turn the piece. */
|
|
7
|
+
drag: DragMode;
|
|
8
|
+
}
|
|
9
|
+
export interface ChangeInfo {
|
|
10
|
+
/** False for the steps of a drag, a pinch, a slider or the automatic turn; true when something settled (a button, a release, a key, a wheel step). */
|
|
11
|
+
final: boolean;
|
|
12
|
+
/** What changed it: "set", "auto", "drag", "pinch", "wheel", "key", "zoom", "pan", "front", "fit", "play", "pause", "tap". */
|
|
13
|
+
reason: string;
|
|
14
|
+
}
|
|
15
|
+
/** What `bind` leaves alone by default: the page's own controls inside the view. */
|
|
16
|
+
export declare const DEFAULT_IGNORE = "button, a, input, select, textarea, label, [data-kazoku-ignore]";
|
|
17
|
+
export interface Clock {
|
|
18
|
+
request(callback: (now: number) => void): number;
|
|
19
|
+
cancel(id: number): void;
|
|
20
|
+
}
|
|
21
|
+
export interface ViewControllerOptions {
|
|
22
|
+
initial?: Partial<ViewState>;
|
|
23
|
+
/** False for a flat view that cannot turn: a drag then always moves it and the angle and tilt never change. */
|
|
24
|
+
turnable?: boolean;
|
|
25
|
+
/** Degrees a second when it turns by itself. */
|
|
26
|
+
speed?: number;
|
|
27
|
+
limits?: Partial<Limits>;
|
|
28
|
+
/** The starting zoom: "fit" (the default) or a scale where 1 is the scene's natural size; it opens at that scale divided by `fitScale`. With `fitScale` left at 1 a number is a zoom as `ViewState.zoom` has it. `initial.zoom` is used only when this is left out. */
|
|
29
|
+
zoom?: "fit" | number;
|
|
30
|
+
/** The page's scale when the view is at Fit (`state.zoom` 1): what zoom 1 means. The scene is drawn at `state.zoom * fitScale`. 1 by default. */
|
|
31
|
+
fitScale?: number;
|
|
32
|
+
/** A CSS selector for what inside the view `bind` leaves alone: a press on a match is not captured, so its click goes through. An empty string leaves nothing alone. */
|
|
33
|
+
ignore?: string;
|
|
34
|
+
/** The view's size in CSS pixels, for holding the pan; `bind` gives it from the element when this is left out. */
|
|
35
|
+
size?: () => Size | undefined;
|
|
36
|
+
onChange?: (state: ViewState, info: ChangeInfo) => void;
|
|
37
|
+
/** Animation frames; the browser's requestAnimationFrame by default. A test passes its own. */
|
|
38
|
+
clock?: Clock;
|
|
39
|
+
}
|
|
40
|
+
export interface ViewController {
|
|
41
|
+
readonly state: ViewState;
|
|
42
|
+
turnable: boolean;
|
|
43
|
+
speed: number;
|
|
44
|
+
readonly limits: Limits;
|
|
45
|
+
/** What a pointer press leaves alone inside the view (see `ViewControllerOptions.ignore`). Can be changed at any time. */
|
|
46
|
+
ignore: string;
|
|
47
|
+
/** What zoom 1 means to the page (see `ViewControllerOptions.fitScale`). */
|
|
48
|
+
readonly fitScale: number;
|
|
49
|
+
/** The `state.zoom` the view opens at: 1 for "fit", else the starting scale divided by `fitScale`. The address leaves a zoom at this value out. */
|
|
50
|
+
readonly startZoom: number;
|
|
51
|
+
/** Tell the controller what zoom 1 means. If the reader has not zoomed since the start, the view moves to the starting zoom for the new scale. */
|
|
52
|
+
setFitScale(scale: number): void;
|
|
53
|
+
/** Start at "fit" or at a scale relative to natural size, and go there now. */
|
|
54
|
+
setStartZoom(zoom: "fit" | number): void;
|
|
55
|
+
set(next: Partial<ViewState>, final?: boolean, reason?: string): void;
|
|
56
|
+
play(): void;
|
|
57
|
+
pause(): void;
|
|
58
|
+
toggle(): void;
|
|
59
|
+
/** Hold the automatic turn still while a hand (a slider being dragged) has the angle; `releaseTurn` lets it carry on from where the hand left it. Counts, so holds nest. */
|
|
60
|
+
holdTurn(): void;
|
|
61
|
+
releaseTurn(): void;
|
|
62
|
+
front(): void;
|
|
63
|
+
fit(): void;
|
|
64
|
+
/** Zoom to `next` about the point `at` (client pixels; the view's middle when left out). */
|
|
65
|
+
zoomAt(next: number, at?: Point, final?: boolean): void;
|
|
66
|
+
zoomBy(factor: number, at?: Point): void;
|
|
67
|
+
/** Move the view by whole fractions of its size: `panBy(1, 0)` is a quarter of its width to the right. */
|
|
68
|
+
panBy(stepsX: number, stepsY: number): void;
|
|
69
|
+
setDrag(drag: DragMode): void;
|
|
70
|
+
/** Bind the pointer, pinch, wheel and key hands to `view`. Returns a function that unbinds them. */
|
|
71
|
+
bind(view: HTMLElement): () => void;
|
|
72
|
+
destroy(): void;
|
|
73
|
+
}
|
|
74
|
+
export declare function createViewController(options?: ViewControllerOptions): ViewController;
|