@johnmorrisdotca/kazoku-ui 0.1.0 → 0.2.1
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 +9 -0
- package/README.md +31 -7
- package/dist/view-controls/address.d.ts +2 -2
- package/dist/view-controls/address.js +3 -3
- package/dist/view-controls/controller.d.ts +18 -0
- package/dist/view-controls/controller.js +62 -0
- package/dist/view-controls/element.d.ts +8 -3
- package/dist/view-controls/element.js +32 -8
- package/dist/view-controls/index.d.ts +1 -1
- package/dist/view-controls/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.1] - 2026-11-01
|
|
4
|
+
|
|
5
|
+
- `ignore` now sees through shadow DOM: it tests the elements on the event's `composedPath()`, from the innermost up to the view, so a button inside a web component's open shadow root is left alone. Before, only the event's target (the host) was tested. A closed shadow root stays a limit: mark its host with `data-kazoku-ignore`.
|
|
6
|
+
|
|
7
|
+
## [0.2.0] - 2026-11-01
|
|
8
|
+
|
|
9
|
+
- 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.
|
|
10
|
+
- 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.
|
|
11
|
+
|
|
3
12
|
## [0.1.0] - 2026-11-01
|
|
4
13
|
|
|
5
14
|
The first release, and its first part: view controls, at `@johnmorrisdotca/kazoku-ui/view-controls`.
|
package/README.md
CHANGED
|
@@ -98,6 +98,27 @@ myPlayButton.onclick = () => view.toggle();
|
|
|
98
98
|
myZoomIn.onclick = () => view.zoomBy(1.4);
|
|
99
99
|
```
|
|
100
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
|
+
`ignore` also sees through shadow DOM: it tests every element on the event's path from the innermost one up to the view, so a button inside a web component's open shadow root (a card's own Animate button, say) is left alone even though the event's target is the component. A closed shadow root hides its inside from the page, so only the component itself can be matched: mark the host with `data-kazoku-ignore`, or add its tag name to `ignore`.
|
|
108
|
+
|
|
109
|
+
### Opening at the scene's natural size
|
|
110
|
+
|
|
111
|
+
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:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
const controls = document.querySelector("kazoku-view-controls");
|
|
115
|
+
controls.setAttribute("zoom", "1"); // natural size
|
|
116
|
+
controls.setFitScale(measure()); // the scale the scene is drawn at when it fits, for example 0.4
|
|
117
|
+
// the event's zoom is now 2.5; draw at event.detail.zoom * controls.fitScale
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
`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.
|
|
121
|
+
|
|
101
122
|
## The event API
|
|
102
123
|
|
|
103
124
|
`kazoku-viewchange` is dispatched on the element (it bubbles and crosses shadow roots). `event.detail`, and the first argument of `onChange`, is:
|
|
@@ -106,12 +127,12 @@ myZoomIn.onclick = () => view.zoomBy(1.4);
|
|
|
106
127
|
|---|---|
|
|
107
128
|
| `yaw` | Degrees, -180 up to 180: the turn about the vertical. Dragging right makes it larger. |
|
|
108
129
|
| `tilt` | Degrees, held to plus or minus `tilt-max` (30): the top tips away as it gets larger (dragging up). |
|
|
109
|
-
| `zoom` | 1 to 5 (`zoom-min`, `zoom-max`). |
|
|
130
|
+
| `zoom` | 1 to 5 (`zoom-min`, `zoom-max`): 1 is Fit. The scene is drawn at `zoom * fitScale` (see below). |
|
|
110
131
|
| `panX`, `panY` | CSS pixels the zoomed picture is moved from the middle of the view; 0 at 1x. |
|
|
111
132
|
| `turning` | True while Auto-turn is running. |
|
|
112
133
|
| `drag` | `"move"` or `"turn"`: what a plain drag does while zoomed in. |
|
|
113
134
|
| `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. |
|
|
114
|
-
| `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `play`, `pause` or `set`. |
|
|
135
|
+
| `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `fit-scale`, `play`, `pause` or `set`. |
|
|
115
136
|
|
|
116
137
|
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.
|
|
117
138
|
|
|
@@ -121,7 +142,10 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
121
142
|
|---|---|
|
|
122
143
|
| `for` | The id of the view the hands bind to. Or set `element.view = someElement`. |
|
|
123
144
|
| `lang` | `en` or `ja`. Otherwise the nearest `lang` above it, else English. |
|
|
124
|
-
| `yaw`, `tilt
|
|
145
|
+
| `yaw`, `tilt` | The starting angle and tilt. |
|
|
146
|
+
| `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. |
|
|
147
|
+
| `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. |
|
|
148
|
+
| `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. |
|
|
125
149
|
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
126
150
|
| `speed` | Degrees a second (30). |
|
|
127
151
|
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
@@ -131,12 +155,12 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
131
155
|
| `theme` | `light` or `dark`; otherwise the page's colour scheme. |
|
|
132
156
|
| `overlay` | Sit over the bottom of a positioned parent. |
|
|
133
157
|
|
|
134
|
-
Methods: `play()`, `pause()`, `front()`, `fit()`, `set({ yaw, tilt, zoom, panX, panY, drag })`, and `controller` (the `createViewController` object behind it). `element.words = { front: "Home" }` replaces labels.
|
|
158
|
+
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.
|
|
135
159
|
Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-line`, `--kazoku-accent`; the parts are `bar`, `field`, `slider` and `button`.
|
|
136
160
|
|
|
137
161
|
### The address
|
|
138
162
|
|
|
139
|
-
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 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?)`.
|
|
163
|
+
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?)`.
|
|
140
164
|
|
|
141
165
|
### Why the tag is only defined when it is free
|
|
142
166
|
|
|
@@ -144,11 +168,11 @@ Only what a reader chose is kept: angle, tilt, zoom and the drag choice. The pan
|
|
|
144
168
|
|
|
145
169
|
## Size
|
|
146
170
|
|
|
147
|
-
Gzipped, unbundled: the element and its definition about
|
|
171
|
+
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.
|
|
148
172
|
|
|
149
173
|
## Checks
|
|
150
174
|
|
|
151
|
-
`pnpm check` (lint, types,
|
|
175
|
+
`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).
|
|
152
176
|
|
|
153
177
|
## Not yet
|
|
154
178
|
|
|
@@ -17,8 +17,8 @@ export interface AddressChoices {
|
|
|
17
17
|
export declare function parseAddress(search: string, keys?: AddressKeys, limits?: Limits): Partial<AddressChoices>;
|
|
18
18
|
/**
|
|
19
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).
|
|
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
21
|
*/
|
|
22
|
-
export declare function formatAddress(search: string, choices: AddressChoices, keys?: AddressKeys, leaveYaw?: boolean): string;
|
|
22
|
+
export declare function formatAddress(search: string, choices: AddressChoices, keys?: AddressKeys, leaveYaw?: boolean, startZoom?: number): string;
|
|
23
23
|
/** Read `address-keys`: `yaw:a,tilt:b` renames the keys it names and leaves the others. */
|
|
24
24
|
export declare function parseKeys(text: string | null): AddressKeys;
|
|
@@ -30,9 +30,9 @@ export function parseAddress(search, keys = DEFAULT_KEYS, limits = DEFAULT_LIMIT
|
|
|
30
30
|
}
|
|
31
31
|
/**
|
|
32
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).
|
|
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
34
|
*/
|
|
35
|
-
export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = false) {
|
|
35
|
+
export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = false, startZoom = 1) {
|
|
36
36
|
const params = new URLSearchParams(search);
|
|
37
37
|
const set = (key, value) => {
|
|
38
38
|
if (value === undefined)
|
|
@@ -43,7 +43,7 @@ export function formatAddress(search, choices, keys = DEFAULT_KEYS, leaveYaw = f
|
|
|
43
43
|
if (!leaveYaw)
|
|
44
44
|
set(keys.yaw, Math.round(choices.yaw) === 0 ? undefined : String(rounded(wrapYaw(choices.yaw), 0)));
|
|
45
45
|
set(keys.tilt, Math.round(choices.tilt) === 0 ? undefined : String(rounded(choices.tilt, 0)));
|
|
46
|
-
set(keys.zoom, Math.abs(choices.zoom -
|
|
46
|
+
set(keys.zoom, Math.abs(choices.zoom - startZoom) < 0.005 ? undefined : String(rounded(choices.zoom, 2)));
|
|
47
47
|
set(keys.drag, choices.drag === "turn" ? "turn" : undefined);
|
|
48
48
|
const text = params.toString();
|
|
49
49
|
return text === "" ? "" : `?${text}`;
|
|
@@ -12,6 +12,8 @@ export interface ChangeInfo {
|
|
|
12
12
|
/** What changed it: "set", "auto", "drag", "pinch", "wheel", "key", "zoom", "pan", "front", "fit", "play", "pause", "tap". */
|
|
13
13
|
reason: string;
|
|
14
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]";
|
|
15
17
|
export interface Clock {
|
|
16
18
|
request(callback: (now: number) => void): number;
|
|
17
19
|
cancel(id: number): void;
|
|
@@ -23,6 +25,12 @@ export interface ViewControllerOptions {
|
|
|
23
25
|
/** Degrees a second when it turns by itself. */
|
|
24
26
|
speed?: number;
|
|
25
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;
|
|
26
34
|
/** The view's size in CSS pixels, for holding the pan; `bind` gives it from the element when this is left out. */
|
|
27
35
|
size?: () => Size | undefined;
|
|
28
36
|
onChange?: (state: ViewState, info: ChangeInfo) => void;
|
|
@@ -34,6 +42,16 @@ export interface ViewController {
|
|
|
34
42
|
turnable: boolean;
|
|
35
43
|
speed: number;
|
|
36
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;
|
|
37
55
|
set(next: Partial<ViewState>, final?: boolean, reason?: string): void;
|
|
38
56
|
play(): void;
|
|
39
57
|
pause(): void;
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, wheelZoomFactor, wrapYaw, ZOOMED, zoomAbout } from "./math.js";
|
|
2
|
+
/** What `bind` leaves alone by default: the page's own controls inside the view. */
|
|
3
|
+
export const DEFAULT_IGNORE = "button, a, input, select, textarea, label, [data-kazoku-ignore]";
|
|
4
|
+
const positive = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined);
|
|
2
5
|
const browserClock = () => ({ request: (callback) => globalThis.requestAnimationFrame(callback), cancel: (id) => globalThis.cancelAnimationFrame(id) });
|
|
3
6
|
export function createViewController(options = {}) {
|
|
4
7
|
const limits = { ...DEFAULT_LIMITS, ...options.limits };
|
|
@@ -8,7 +11,14 @@ export function createViewController(options = {}) {
|
|
|
8
11
|
let frame = 0;
|
|
9
12
|
let last = 0;
|
|
10
13
|
let holds = 0;
|
|
14
|
+
let fitScale = positive(options.fitScale) ?? 1;
|
|
15
|
+
let start = positive(options.zoom) ?? "fit";
|
|
16
|
+
let atStart = true;
|
|
17
|
+
const startState = () => (start === "fit" ? 1 : start / fitScale);
|
|
11
18
|
const state = { yaw: 0, tilt: 0, zoom: 1, panX: 0, panY: 0, turning: false, drag: "move", ...options.initial };
|
|
19
|
+
if (start !== "fit")
|
|
20
|
+
state.zoom = clamp(startState(), limits.zoomMin, limits.zoomMax);
|
|
21
|
+
atStart = state.zoom === clamp(startState(), limits.zoomMin, limits.zoomMax);
|
|
12
22
|
const size = () => {
|
|
13
23
|
const own = sizeOf?.();
|
|
14
24
|
if (own !== undefined)
|
|
@@ -29,6 +39,8 @@ export function createViewController(options = {}) {
|
|
|
29
39
|
const held = clampPan({ ...next, zoom: clamp(next.zoom, limits.zoomMin, limits.zoomMax), tilt: clamp(next.tilt, -limits.tiltMax, limits.tiltMax) }, size());
|
|
30
40
|
state.yaw = controller.turnable ? wrapYaw(held.yaw) : state.yaw;
|
|
31
41
|
state.tilt = controller.turnable ? held.tilt : state.tilt;
|
|
42
|
+
if (held.zoom !== state.zoom)
|
|
43
|
+
atStart = false;
|
|
32
44
|
state.zoom = held.zoom;
|
|
33
45
|
state.panX = held.panX;
|
|
34
46
|
state.panY = held.panY;
|
|
@@ -66,6 +78,32 @@ export function createViewController(options = {}) {
|
|
|
66
78
|
turnable: options.turnable ?? true,
|
|
67
79
|
speed: options.speed ?? 30,
|
|
68
80
|
limits,
|
|
81
|
+
ignore: options.ignore ?? DEFAULT_IGNORE,
|
|
82
|
+
get fitScale() {
|
|
83
|
+
return fitScale;
|
|
84
|
+
},
|
|
85
|
+
get startZoom() {
|
|
86
|
+
return clamp(startState(), limits.zoomMin, limits.zoomMax);
|
|
87
|
+
},
|
|
88
|
+
setFitScale(scale) {
|
|
89
|
+
const next = positive(scale);
|
|
90
|
+
if (next === undefined || next === fitScale)
|
|
91
|
+
return;
|
|
92
|
+
fitScale = next;
|
|
93
|
+
if (!atStart)
|
|
94
|
+
return;
|
|
95
|
+
const before = state.zoom;
|
|
96
|
+
take({ ...camera(), zoom: startState() });
|
|
97
|
+
atStart = true;
|
|
98
|
+
if (state.zoom !== before)
|
|
99
|
+
emit(true, "fit-scale");
|
|
100
|
+
},
|
|
101
|
+
setStartZoom(zoom) {
|
|
102
|
+
start = positive(zoom) ?? "fit";
|
|
103
|
+
take({ ...camera(), zoom: startState() });
|
|
104
|
+
atStart = true;
|
|
105
|
+
emit(true, "set");
|
|
106
|
+
},
|
|
69
107
|
set(next, final = true, reason = "set") {
|
|
70
108
|
const { turning, drag, ...cam } = next;
|
|
71
109
|
take({ ...camera(), ...cam });
|
|
@@ -161,9 +199,33 @@ export function createViewController(options = {}) {
|
|
|
161
199
|
holding = false;
|
|
162
200
|
};
|
|
163
201
|
const canTurn = () => controller.turnable;
|
|
202
|
+
const ignored = (e) => {
|
|
203
|
+
if (controller.ignore === "")
|
|
204
|
+
return false;
|
|
205
|
+
const path = typeof e.composedPath === "function" ? e.composedPath() : [];
|
|
206
|
+
try {
|
|
207
|
+
if (path.length === 0) {
|
|
208
|
+
const target = e.target;
|
|
209
|
+
const hit = typeof target?.closest === "function" ? target.closest(controller.ignore) : null;
|
|
210
|
+
return hit !== null && hit !== view && view.contains(hit);
|
|
211
|
+
}
|
|
212
|
+
for (const node of path) {
|
|
213
|
+
if (node === view)
|
|
214
|
+
return false;
|
|
215
|
+
if (typeof node.matches === "function" && node.matches(controller.ignore))
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
return false;
|
|
221
|
+
}
|
|
222
|
+
return false;
|
|
223
|
+
};
|
|
164
224
|
const onDown = (e) => {
|
|
165
225
|
if (e.pointerType === "mouse" && e.button > 2)
|
|
166
226
|
return;
|
|
227
|
+
if (ignored(e))
|
|
228
|
+
return;
|
|
167
229
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
168
230
|
try {
|
|
169
231
|
view.setPointerCapture(e.pointerId);
|
|
@@ -8,9 +8,11 @@ export interface ViewChangeDetail extends ViewState {
|
|
|
8
8
|
reason: string;
|
|
9
9
|
}
|
|
10
10
|
/**
|
|
11
|
-
* Attributes: `for` (the id of the view to bind), `lang` (en or ja; else the page's), `yaw`, `tilt
|
|
12
|
-
*
|
|
13
|
-
*
|
|
11
|
+
* 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
|
|
12
|
+
* 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;
|
|
13
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `speed` (degrees a second),
|
|
14
|
+
* `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
|
|
15
|
+
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
14
16
|
*/
|
|
15
17
|
export declare class ViewControlsElement extends HTMLElement {
|
|
16
18
|
#private;
|
|
@@ -30,6 +32,9 @@ export declare class ViewControlsElement extends HTMLElement {
|
|
|
30
32
|
front(): void;
|
|
31
33
|
fit(): void;
|
|
32
34
|
set(next: Partial<ViewState>): void;
|
|
35
|
+
/** What zoom 1 means to the page: the scene is drawn at `state.zoom * fitScale`. */
|
|
36
|
+
get fitScale(): number;
|
|
37
|
+
setFitScale(scale: number): void;
|
|
33
38
|
connectedCallback(): void;
|
|
34
39
|
disconnectedCallback(): void;
|
|
35
40
|
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// <kazoku-view-controls>: the view controls as a framework-free custom element. It never draws the scene. It binds the pointer, pinch, wheel and keys
|
|
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 { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys } from "./address.js";
|
|
4
|
-
import { createViewController } from "./controller.js";
|
|
4
|
+
import { createViewController, DEFAULT_IGNORE } from "./controller.js";
|
|
5
5
|
import { EVENT, TAG } from "./index.js";
|
|
6
6
|
import { ZOOM_STEP, ZOOMED } from "./math.js";
|
|
7
7
|
import { STYLES } from "./styles.js";
|
|
@@ -42,12 +42,14 @@ const HTML = `
|
|
|
42
42
|
</div>
|
|
43
43
|
</div>`;
|
|
44
44
|
/**
|
|
45
|
-
* Attributes: `for` (the id of the view to bind), `lang` (en or ja; else the page's), `yaw`, `tilt
|
|
46
|
-
*
|
|
47
|
-
*
|
|
45
|
+
* 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
|
|
46
|
+
* 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;
|
|
47
|
+
* buttons, links and form controls by default), `autoplay` (turn by itself from the start, unless the reader prefers reduced motion), `speed` (degrees a second),
|
|
48
|
+
* `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
|
|
49
|
+
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
48
50
|
*/
|
|
49
51
|
export class ViewControlsElement extends HTMLElement {
|
|
50
|
-
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
52
|
+
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
51
53
|
#root;
|
|
52
54
|
#controller;
|
|
53
55
|
#view;
|
|
@@ -96,6 +98,13 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
96
98
|
set(next) {
|
|
97
99
|
this.#ensure().set(next);
|
|
98
100
|
}
|
|
101
|
+
/** What zoom 1 means to the page: the scene is drawn at `state.zoom * fitScale`. */
|
|
102
|
+
get fitScale() {
|
|
103
|
+
return this.#ensure().fitScale;
|
|
104
|
+
}
|
|
105
|
+
setFitScale(scale) {
|
|
106
|
+
this.#ensure().setFitScale(scale);
|
|
107
|
+
}
|
|
99
108
|
connectedCallback() {
|
|
100
109
|
const c = this.#ensure();
|
|
101
110
|
if (this.#root.querySelector(".bar") === null)
|
|
@@ -103,7 +112,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
103
112
|
this.#keys = parseKeys(this.getAttribute("address-keys"));
|
|
104
113
|
this.#limits();
|
|
105
114
|
const start = {};
|
|
106
|
-
for (const key of ["yaw", "tilt"
|
|
115
|
+
for (const key of ["yaw", "tilt"]) {
|
|
107
116
|
const value = Number(this.getAttribute(key));
|
|
108
117
|
if (this.hasAttribute(key) && Number.isFinite(value))
|
|
109
118
|
start[key] = value;
|
|
@@ -112,6 +121,11 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
112
121
|
Object.assign(start, parseAddress(globalThis.location?.search ?? "", this.#keys, c.limits));
|
|
113
122
|
c.turnable = !this.hasAttribute("flat");
|
|
114
123
|
c.speed = Number(this.getAttribute("speed")) || 30;
|
|
124
|
+
c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
|
|
125
|
+
const fitScale = Number(this.getAttribute("fit-scale"));
|
|
126
|
+
if (this.hasAttribute("fit-scale"))
|
|
127
|
+
c.setFitScale(fitScale);
|
|
128
|
+
c.setStartZoom(this.#startZoom());
|
|
115
129
|
c.set(start, true, "init");
|
|
116
130
|
this.#label();
|
|
117
131
|
this.#resolveView();
|
|
@@ -147,10 +161,16 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
147
161
|
this.#resolveView();
|
|
148
162
|
else if (name === "lang")
|
|
149
163
|
this.#label();
|
|
150
|
-
else if (name === "yaw" || name === "tilt"
|
|
164
|
+
else if (name === "yaw" || name === "tilt") {
|
|
151
165
|
if (value !== null && Number.isFinite(Number(value)))
|
|
152
166
|
c.set({ [name]: Number(value) });
|
|
153
167
|
}
|
|
168
|
+
else if (name === "zoom")
|
|
169
|
+
c.setStartZoom(this.#startZoom());
|
|
170
|
+
else if (name === "fit-scale")
|
|
171
|
+
c.setFitScale(Number(value));
|
|
172
|
+
else if (name === "ignore")
|
|
173
|
+
c.ignore = value ?? DEFAULT_IGNORE;
|
|
154
174
|
else if (name === "speed")
|
|
155
175
|
c.speed = Number(value) || 30;
|
|
156
176
|
else if (name === "flat") {
|
|
@@ -162,6 +182,10 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
162
182
|
else
|
|
163
183
|
this.#limits();
|
|
164
184
|
}
|
|
185
|
+
#startZoom() {
|
|
186
|
+
const value = Number(this.getAttribute("zoom"));
|
|
187
|
+
return this.hasAttribute("zoom") && Number.isFinite(value) && value > 0 ? value : "fit";
|
|
188
|
+
}
|
|
165
189
|
#ensure() {
|
|
166
190
|
if (this.#controller === undefined) {
|
|
167
191
|
this.#controller = createViewController({ onChange: (state, info) => this.#changed(state, info) });
|
|
@@ -305,7 +329,7 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
305
329
|
this.#saveTimer = 0;
|
|
306
330
|
try {
|
|
307
331
|
const s = this.#ensure().state;
|
|
308
|
-
const next = formatAddress(globalThis.location.search, s, this.#keys, s.turning);
|
|
332
|
+
const next = formatAddress(globalThis.location.search, s, this.#keys, s.turning, this.#ensure().startZoom);
|
|
309
333
|
if (next !== globalThis.location.search)
|
|
310
334
|
globalThis.history.replaceState(globalThis.history.state, "", `${globalThis.location.pathname}${next}${globalThis.location.hash}`);
|
|
311
335
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { DEFAULT_KEYS, formatAddress, parseAddress, parseKeys, type AddressChoices, type AddressKeys, type DragMode } from "./address.ts";
|
|
2
|
-
export { createViewController, type ChangeInfo, type Clock, type ViewController, type ViewControllerOptions, type ViewState } from "./controller.ts";
|
|
2
|
+
export { createViewController, DEFAULT_IGNORE, type ChangeInfo, type Clock, type ViewController, type ViewControllerOptions, type ViewState } from "./controller.ts";
|
|
3
3
|
export { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, TILT_ACROSS, TURN_ACROSS, wheelZoomFactor, wrapYaw, ZOOM_STEP, ZOOMED, zoomAbout, type Camera, type Limits, type PinchStart, type Point, type Size } from "./math.ts";
|
|
4
4
|
export { languageOf, WORDS, wordsFor, type Language, type ViewWords } from "./words.ts";
|
|
5
5
|
export declare const EVENT = "kazoku-viewchange";
|
|
@@ -1,6 +1,6 @@
|
|
|
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
|
-
export { createViewController } from "./controller.js";
|
|
3
|
+
export { createViewController, DEFAULT_IGNORE } from "./controller.js";
|
|
4
4
|
export { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, TILT_ACROSS, TURN_ACROSS, 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";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazoku-ui",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "Kazoku UI 家族, the family's shared interface parts as their own versioned package. First part: view controls for any 3D or zoomable view (angle, tilt, auto-turn, zoom about the pointer, pan, touch, address state) as a framework-free custom element and a plain controller. Zero dependencies.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"view-controls",
|