@johnmorrisdotca/kazoku-ui 0.1.0 → 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 +5 -0
- package/README.md +29 -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 +52 -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,10 @@
|
|
|
1
1
|
# Changelog
|
|
2
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
|
+
|
|
3
8
|
## [0.1.0] - 2026-11-01
|
|
4
9
|
|
|
5
10
|
The first release, and its first part: view controls, at `@johnmorrisdotca/kazoku-ui/view-controls`.
|
package/README.md
CHANGED
|
@@ -98,6 +98,25 @@ 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
|
+
### 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
|
+
|
|
101
120
|
## The event API
|
|
102
121
|
|
|
103
122
|
`kazoku-viewchange` is dispatched on the element (it bubbles and crosses shadow roots). `event.detail`, and the first argument of `onChange`, is:
|
|
@@ -106,12 +125,12 @@ myZoomIn.onclick = () => view.zoomBy(1.4);
|
|
|
106
125
|
|---|---|
|
|
107
126
|
| `yaw` | Degrees, -180 up to 180: the turn about the vertical. Dragging right makes it larger. |
|
|
108
127
|
| `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`). |
|
|
128
|
+
| `zoom` | 1 to 5 (`zoom-min`, `zoom-max`): 1 is Fit. The scene is drawn at `zoom * fitScale` (see below). |
|
|
110
129
|
| `panX`, `panY` | CSS pixels the zoomed picture is moved from the middle of the view; 0 at 1x. |
|
|
111
130
|
| `turning` | True while Auto-turn is running. |
|
|
112
131
|
| `drag` | `"move"` or `"turn"`: what a plain drag does while zoomed in. |
|
|
113
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. |
|
|
114
|
-
| `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `play`, `pause` or `set`. |
|
|
133
|
+
| `reason` | What changed it: `init`, `auto`, `drag`, `pinch`, `wheel`, `key`, `slider`, `zoom`, `pan`, `front`, `fit`, `fit-scale`, `play`, `pause` or `set`. |
|
|
115
134
|
|
|
116
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.
|
|
117
136
|
|
|
@@ -121,7 +140,10 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
121
140
|
|---|---|
|
|
122
141
|
| `for` | The id of the view the hands bind to. Or set `element.view = someElement`. |
|
|
123
142
|
| `lang` | `en` or `ja`. Otherwise the nearest `lang` above it, else English. |
|
|
124
|
-
| `yaw`, `tilt
|
|
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. |
|
|
125
147
|
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
126
148
|
| `speed` | Degrees a second (30). |
|
|
127
149
|
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
@@ -131,12 +153,12 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
131
153
|
| `theme` | `light` or `dark`; otherwise the page's colour scheme. |
|
|
132
154
|
| `overlay` | Sit over the bottom of a positioned parent. |
|
|
133
155
|
|
|
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.
|
|
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.
|
|
135
157
|
Colours are custom properties: `--kazoku-ink`, `--kazoku-surface`, `--kazoku-line`, `--kazoku-accent`; the parts are `bar`, `field`, `slider` and `button`.
|
|
136
158
|
|
|
137
159
|
### The address
|
|
138
160
|
|
|
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?)`.
|
|
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?)`.
|
|
140
162
|
|
|
141
163
|
### Why the tag is only defined when it is free
|
|
142
164
|
|
|
@@ -144,11 +166,11 @@ Only what a reader chose is kept: angle, tilt, zoom and the drag choice. The pan
|
|
|
144
166
|
|
|
145
167
|
## Size
|
|
146
168
|
|
|
147
|
-
Gzipped, unbundled: the element and its definition about
|
|
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.
|
|
148
170
|
|
|
149
171
|
## Checks
|
|
150
172
|
|
|
151
|
-
`pnpm check` (lint, types,
|
|
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).
|
|
152
174
|
|
|
153
175
|
## Not yet
|
|
154
176
|
|
|
@@ -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,23 @@ export function createViewController(options = {}) {
|
|
|
161
199
|
holding = false;
|
|
162
200
|
};
|
|
163
201
|
const canTurn = () => controller.turnable;
|
|
202
|
+
const ignored = (e) => {
|
|
203
|
+
const target = e.target;
|
|
204
|
+
if (controller.ignore === "" || typeof target?.closest !== "function")
|
|
205
|
+
return false;
|
|
206
|
+
try {
|
|
207
|
+
const hit = target.closest(controller.ignore);
|
|
208
|
+
return hit !== null && hit !== view && view.contains(hit);
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
164
214
|
const onDown = (e) => {
|
|
165
215
|
if (e.pointerType === "mouse" && e.button > 2)
|
|
166
216
|
return;
|
|
217
|
+
if (ignored(e))
|
|
218
|
+
return;
|
|
167
219
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
168
220
|
try {
|
|
169
221
|
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.
|
|
3
|
+
"version": "0.2.0",
|
|
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",
|