@johnmorrisdotca/kazoku-ui 0.2.1 → 0.3.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 +8 -0
- package/README.md +21 -3
- package/dist/view-controls/controller.d.ts +16 -1
- package/dist/view-controls/controller.js +24 -6
- package/dist/view-controls/element.d.ts +6 -1
- package/dist/view-controls/element.js +30 -2
- package/dist/view-controls/math.d.ts +2 -2
- package/dist/view-controls/math.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0] - 2026-11-01
|
|
4
|
+
|
|
5
|
+
- Zoom about a point of your own: the `anchor` attribute (`x,y`), the `element.anchor` setter and the controller's `anchor` member and option take a point in CSS pixels from the view's middle at Fit. The + and - buttons, the 2x button and the + and - keys zoom about it, and it stays fixed on screen within a pixel; wheel, pinch and double tap still zoom about the pointer. With no anchor they zoom about the middle, as before. `zoomAt` and `zoomBy` with no point use it.
|
|
6
|
+
- Arrow Up the way a wheel goes: `key-tilt="wheel"` (option `keyTilt: "wheel"`) makes Arrow Up tip the way a wheel turned up does. The default, `drag`, still tips like a drag up.
|
|
7
|
+
- `stop-turn` (option `stopTurn`) stops the automatic turn on a press, the wheel or a key on the view, with a `pause` event. Off by default; buttons and sliders never stop it.
|
|
8
|
+
- `tilt-step` (option `tiltStep`) rounds the tilt of a drag to a multiple of this many degrees; `1` gives whole degrees. `dragTurn` takes it as a seventh argument. 0 by default.
|
|
9
|
+
- The demo switches them on in its address: `?anchor=pin`, `?key-tilt=wheel`, `?stop-turn`, `?tilt-step=1`.
|
|
10
|
+
|
|
3
11
|
## [0.2.1] - 2026-11-01
|
|
4
12
|
|
|
5
13
|
- `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`.
|
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ 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.
|
|
101
|
+
Options of `createViewController` that match: `zoom` (`"fit"` or a number), `fitScale`, `ignore`, `anchor`, `keyTilt`, `stopTurn`, `tiltStep`. `anchor`, `keyTilt`, `stopTurn` and `tiltStep` are also members you can change at any time. 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
102
|
|
|
103
103
|
### Buttons and links inside the view
|
|
104
104
|
|
|
@@ -106,6 +106,20 @@ The hands capture the pointer on the `for` element, so a press on a button or li
|
|
|
106
106
|
|
|
107
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
108
|
|
|
109
|
+
### Zooming about a point of your own
|
|
110
|
+
|
|
111
|
+
The zoom buttons and keys have no pointer, so they zoom about the view's middle. If your scene's middle is not the view's (a piece standing to one side, say), tell the controls where it is:
|
|
112
|
+
|
|
113
|
+
```js
|
|
114
|
+
controls.anchor = { x: 60, y: -30 }; // pixels from the view's middle when it is at Fit
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Zoom is about that point of the picture, so it stays put on screen however far in the reader is and wherever the view has been moved to: `+` then `-` leaves it where it was within a pixel. Without the element, set `view.anchor` on the controller, or pass `anchor` to `createViewController`; `view.zoomBy(f)` and `view.zoomAt(z)` with no point use it.
|
|
118
|
+
|
|
119
|
+
### Keys, the wheel and the turn
|
|
120
|
+
|
|
121
|
+
Arrow Up tips like a drag up (`tilt` gets larger). A page whose wheel and keys should agree sets `key-tilt="wheel"`: Arrow Up then tips the way a wheel turned up does (`tilt` gets smaller), and Arrow Down the other way. `stop-turn` stops the automatic turn when the reader takes hold of the view with a press, the wheel or a key (a `pause` event is sent first); the defaults keep it turning, and a button or slider never stops it. `tilt-step="1"` makes a drag give whole degrees of tilt.
|
|
122
|
+
|
|
109
123
|
### Opening at the scene's natural size
|
|
110
124
|
|
|
111
125
|
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:
|
|
@@ -146,6 +160,10 @@ An `init` event is sent once, a moment after the element connects, with the star
|
|
|
146
160
|
| `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
161
|
| `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
162
|
| `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. |
|
|
163
|
+
| `anchor` | `x,y`: the point the + and - buttons, the 2x button and the + and - keys zoom about, in CSS pixels from the middle of the view when it is at Fit (zoom 1, no pan). It is a point on the picture, so it moves with it and stays fixed on screen while the buttons zoom. Left out, they zoom about the view's middle, as before. The wheel, a pinch and a double tap still zoom about the pointer. Or set `element.anchor = { x, y }` (`undefined` clears it). |
|
|
164
|
+
| `key-tilt` | Which way Arrow Up tips the view: `drag` (the default: like a drag up) or `wheel` (the way a wheel turned up tips it, the other way). Arrow Down goes the other way from Up. |
|
|
165
|
+
| `stop-turn` | A press on the view, the wheel over it or a key it handles stops the automatic turn. Off by default, so the turn carries on. Buttons and sliders never stop it. |
|
|
166
|
+
| `tilt-step` | Round the tilt of a drag to a multiple of this many degrees (`1` gives whole degrees). 0 by default: not rounded. |
|
|
149
167
|
| `autoplay` | Turn by itself from the start, unless the reader prefers reduced motion. |
|
|
150
168
|
| `speed` | Degrees a second (30). |
|
|
151
169
|
| `flat` | The view cannot turn: no angle, tilt, Auto-turn or Front, and a drag moves it. |
|
|
@@ -168,11 +186,11 @@ Only what a reader chose is kept: angle, tilt, zoom and the drag choice. The pan
|
|
|
168
186
|
|
|
169
187
|
## Size
|
|
170
188
|
|
|
171
|
-
Gzipped, unbundled: the element and its definition about 5.0 KB, the controller 3
|
|
189
|
+
Gzipped, unbundled: the element and its definition about 5.0 KB, the controller 4.3 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.
|
|
172
190
|
|
|
173
191
|
## Checks
|
|
174
192
|
|
|
175
|
-
`pnpm check` (lint, types,
|
|
193
|
+
`pnpm check` (lint, types, 36 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, zoom about an anchor (`?anchor=pin`), Arrow Up either way (`?key-tilt=wheel`), `?stop-turn`, `?tilt-step=1`, pan when zoomed, touch, the address, the labels, reduced motion).
|
|
176
194
|
|
|
177
195
|
## Not yet
|
|
178
196
|
|
|
@@ -14,6 +14,8 @@ export interface ChangeInfo {
|
|
|
14
14
|
}
|
|
15
15
|
/** What `bind` leaves alone by default: the page's own controls inside the view. */
|
|
16
16
|
export declare const DEFAULT_IGNORE = "button, a, input, select, textarea, label, [data-kazoku-ignore]";
|
|
17
|
+
/** Which way Arrow Up tips the view: like a drag up (`"drag"`, the default) or the way the wheel turned up does (`"wheel"`, the other way). */
|
|
18
|
+
export type KeyTilt = "drag" | "wheel";
|
|
17
19
|
export interface Clock {
|
|
18
20
|
request(callback: (now: number) => void): number;
|
|
19
21
|
cancel(id: number): void;
|
|
@@ -31,6 +33,14 @@ export interface ViewControllerOptions {
|
|
|
31
33
|
fitScale?: number;
|
|
32
34
|
/** 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
35
|
ignore?: string;
|
|
36
|
+
/** The point the zoom buttons and keys zoom about, in content coordinates: CSS pixels from the view's middle when it is at Fit (zoom 1, no pan). It moves with the picture, so the point stays where it is on screen while the view zooms. Left out, they zoom about the view's middle. */
|
|
37
|
+
anchor?: Point;
|
|
38
|
+
/** Which way Arrow Up tips the view: `"drag"` (the default, as a drag up) or `"wheel"` (as a wheel turned up). */
|
|
39
|
+
keyTilt?: KeyTilt;
|
|
40
|
+
/** True to stop the automatic turn when the reader presses the view, uses the wheel on it or presses a key it handles. False (the default) lets it carry on. Buttons and sliders never stop it. */
|
|
41
|
+
stopTurn?: boolean;
|
|
42
|
+
/** Round the tilt of a drag to a multiple of this many degrees; 0 (the default) leaves it unrounded, 1 gives whole degrees. */
|
|
43
|
+
tiltStep?: number;
|
|
34
44
|
/** The view's size in CSS pixels, for holding the pan; `bind` gives it from the element when this is left out. */
|
|
35
45
|
size?: () => Size | undefined;
|
|
36
46
|
onChange?: (state: ViewState, info: ChangeInfo) => void;
|
|
@@ -44,6 +54,11 @@ export interface ViewController {
|
|
|
44
54
|
readonly limits: Limits;
|
|
45
55
|
/** What a pointer press leaves alone inside the view (see `ViewControllerOptions.ignore`). Can be changed at any time. */
|
|
46
56
|
ignore: string;
|
|
57
|
+
/** The point the buttons and keys zoom about (see `ViewControllerOptions.anchor`); `undefined` for the view's middle. Can be changed at any time. */
|
|
58
|
+
anchor: Point | undefined;
|
|
59
|
+
keyTilt: KeyTilt;
|
|
60
|
+
stopTurn: boolean;
|
|
61
|
+
tiltStep: number;
|
|
47
62
|
/** What zoom 1 means to the page (see `ViewControllerOptions.fitScale`). */
|
|
48
63
|
readonly fitScale: number;
|
|
49
64
|
/** 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. */
|
|
@@ -61,7 +76,7 @@ export interface ViewController {
|
|
|
61
76
|
releaseTurn(): void;
|
|
62
77
|
front(): void;
|
|
63
78
|
fit(): void;
|
|
64
|
-
/** Zoom to `next` about the point `at` (client pixels; the view's middle
|
|
79
|
+
/** Zoom to `next` about the point `at` (client pixels); left out, about the `anchor`, else the view's middle. */
|
|
65
80
|
zoomAt(next: number, at?: Point, final?: boolean): void;
|
|
66
81
|
zoomBy(factor: number, at?: Point): void;
|
|
67
82
|
/** Move the view by whole fractions of its size: `panBy(1, 0)` is a quarter of its width to the right. */
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { clamp, clampPan, DEFAULT_LIMITS, dragTurn, PAN_STEP, pinchTo, wheelZoomFactor, wrapYaw, ZOOMED, zoomAbout } from "./math.js";
|
|
2
2
|
/** What `bind` leaves alone by default: the page's own controls inside the view. */
|
|
3
3
|
export const DEFAULT_IGNORE = "button, a, input, select, textarea, label, [data-kazoku-ignore]";
|
|
4
|
+
const HANDLED_KEYS = ["ArrowLeft", "ArrowRight", "ArrowUp", "ArrowDown", "+", "=", "-", "_", "0", "Home"];
|
|
4
5
|
const positive = (value) => (typeof value === "number" && Number.isFinite(value) && value > 0 ? value : undefined);
|
|
5
6
|
const browserClock = () => ({ request: (callback) => globalThis.requestAnimationFrame(callback), cancel: (id) => globalThis.cancelAnimationFrame(id) });
|
|
6
7
|
export function createViewController(options = {}) {
|
|
@@ -51,10 +52,12 @@ export function createViewController(options = {}) {
|
|
|
51
52
|
const r = bound.getBoundingClientRect();
|
|
52
53
|
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
53
54
|
};
|
|
54
|
-
/** Client pixels to pixels from the view's middle. */
|
|
55
|
+
/** Client pixels to pixels from the view's middle; with none, the anchor's place on screen now, else the middle. */
|
|
55
56
|
const fromMiddle = (at) => {
|
|
56
|
-
if (at === undefined)
|
|
57
|
-
|
|
57
|
+
if (at === undefined) {
|
|
58
|
+
const a = controller.anchor;
|
|
59
|
+
return a === undefined ? { x: 0, y: 0 } : { x: state.panX + state.zoom * a.x, y: state.panY + state.zoom * a.y };
|
|
60
|
+
}
|
|
58
61
|
const c = centre();
|
|
59
62
|
return { x: at.x - c.x, y: at.y - c.y };
|
|
60
63
|
};
|
|
@@ -79,6 +82,10 @@ export function createViewController(options = {}) {
|
|
|
79
82
|
speed: options.speed ?? 30,
|
|
80
83
|
limits,
|
|
81
84
|
ignore: options.ignore ?? DEFAULT_IGNORE,
|
|
85
|
+
anchor: options.anchor,
|
|
86
|
+
keyTilt: options.keyTilt ?? "drag",
|
|
87
|
+
stopTurn: options.stopTurn ?? false,
|
|
88
|
+
tiltStep: options.tiltStep ?? 0,
|
|
82
89
|
get fitScale() {
|
|
83
90
|
return fitScale;
|
|
84
91
|
},
|
|
@@ -199,6 +206,10 @@ export function createViewController(options = {}) {
|
|
|
199
206
|
holding = false;
|
|
200
207
|
};
|
|
201
208
|
const canTurn = () => controller.turnable;
|
|
209
|
+
const grab = () => {
|
|
210
|
+
if (controller.stopTurn)
|
|
211
|
+
controller.pause();
|
|
212
|
+
};
|
|
202
213
|
const ignored = (e) => {
|
|
203
214
|
if (controller.ignore === "")
|
|
204
215
|
return false;
|
|
@@ -226,6 +237,7 @@ export function createViewController(options = {}) {
|
|
|
226
237
|
return;
|
|
227
238
|
if (ignored(e))
|
|
228
239
|
return;
|
|
240
|
+
grab();
|
|
229
241
|
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
230
242
|
try {
|
|
231
243
|
view.setPointerCapture(e.pointerId);
|
|
@@ -273,7 +285,7 @@ export function createViewController(options = {}) {
|
|
|
273
285
|
return;
|
|
274
286
|
const r = view.getBoundingClientRect();
|
|
275
287
|
// Up and down tips it for a mouse; on a finger the browser keeps the vertical drag to scroll the page, and the tilt slider tips it.
|
|
276
|
-
take({ ...camera(), ...dragTurn(gesture.camera, dx, dy, { width: r.width, height: r.height }, e.pointerType !== "touch", limits) });
|
|
288
|
+
take({ ...camera(), ...dragTurn(gesture.camera, dx, dy, { width: r.width, height: r.height }, e.pointerType !== "touch", limits, controller.tiltStep) });
|
|
277
289
|
emit(false, "drag");
|
|
278
290
|
}
|
|
279
291
|
};
|
|
@@ -310,6 +322,7 @@ export function createViewController(options = {}) {
|
|
|
310
322
|
if (e.ctrlKey || e.metaKey) {
|
|
311
323
|
// A trackpad's pinch arrives as Ctrl and the wheel; Command and the wheel does the same for a mouse.
|
|
312
324
|
e.preventDefault();
|
|
325
|
+
grab();
|
|
313
326
|
controller.zoomAt(state.zoom * wheelZoomFactor(e.deltaY, e.deltaMode), { x: e.clientX, y: e.clientY });
|
|
314
327
|
return;
|
|
315
328
|
}
|
|
@@ -321,6 +334,7 @@ export function createViewController(options = {}) {
|
|
|
321
334
|
e.preventDefault();
|
|
322
335
|
if (!canTurn())
|
|
323
336
|
return;
|
|
337
|
+
grab();
|
|
324
338
|
if (horizontal || e.shiftKey)
|
|
325
339
|
controller.set({ yaw: state.yaw + (e.deltaX || e.deltaY) * 0.3 }, true, "wheel");
|
|
326
340
|
else
|
|
@@ -329,6 +343,10 @@ export function createViewController(options = {}) {
|
|
|
329
343
|
const onKey = (e) => {
|
|
330
344
|
if (e.ctrlKey || e.metaKey || e.altKey)
|
|
331
345
|
return;
|
|
346
|
+
if (!HANDLED_KEYS.includes(e.key))
|
|
347
|
+
return;
|
|
348
|
+
grab();
|
|
349
|
+
const up = controller.keyTilt === "wheel" ? -5 : 5;
|
|
332
350
|
const big = e.shiftKey ? 45 : 15;
|
|
333
351
|
const turn = (next) => {
|
|
334
352
|
if (canTurn())
|
|
@@ -342,10 +360,10 @@ export function createViewController(options = {}) {
|
|
|
342
360
|
turn({ yaw: state.yaw + big });
|
|
343
361
|
break;
|
|
344
362
|
case "ArrowUp":
|
|
345
|
-
turn({ tilt: state.tilt +
|
|
363
|
+
turn({ tilt: state.tilt + up });
|
|
346
364
|
break;
|
|
347
365
|
case "ArrowDown":
|
|
348
|
-
turn({ tilt: state.tilt -
|
|
366
|
+
turn({ tilt: state.tilt - up });
|
|
349
367
|
break;
|
|
350
368
|
case "+":
|
|
351
369
|
case "=":
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ViewController, type ViewState } from "./controller.ts";
|
|
2
2
|
import { EVENT, TAG } from "./index.ts";
|
|
3
|
+
import { type Point } from "./math.ts";
|
|
3
4
|
import { type ViewWords } from "./words.ts";
|
|
4
5
|
export { EVENT, TAG };
|
|
5
6
|
/** What the `kazoku-viewchange` event carries in `detail`. */
|
|
@@ -11,7 +12,8 @@ export interface ViewChangeDetail extends ViewState {
|
|
|
11
12
|
* 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
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;
|
|
13
14
|
* 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
|
-
* `
|
|
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
|
+
* `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
|
|
15
17
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
16
18
|
*/
|
|
17
19
|
export declare class ViewControlsElement extends HTMLElement {
|
|
@@ -32,6 +34,9 @@ export declare class ViewControlsElement extends HTMLElement {
|
|
|
32
34
|
front(): void;
|
|
33
35
|
fit(): void;
|
|
34
36
|
set(next: Partial<ViewState>): void;
|
|
37
|
+
/** The point the zoom buttons and keys zoom about: `{ x, y }` in CSS pixels from the view's middle when it is at Fit, moving with the picture. `undefined` zooms about the view's middle. The `anchor` attribute (`"x,y"`) sets the same. */
|
|
38
|
+
get anchor(): Point | undefined;
|
|
39
|
+
set anchor(point: Point | undefined | null);
|
|
35
40
|
/** What zoom 1 means to the page: the scene is drawn at `state.zoom * fitScale`. */
|
|
36
41
|
get fitScale(): number;
|
|
37
42
|
setFitScale(scale: number): void;
|
|
@@ -45,11 +45,12 @@ const HTML = `
|
|
|
45
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
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
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
|
-
* `
|
|
48
|
+
* `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),
|
|
49
|
+
* `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
|
|
49
50
|
* or dark), `overlay` (sit over the bottom of a positioned parent).
|
|
50
51
|
*/
|
|
51
52
|
export class ViewControlsElement extends HTMLElement {
|
|
52
|
-
static observedAttributes = ["for", "lang", "yaw", "tilt", "zoom", "fit-scale", "ignore", "speed", "flat", "tilt-max", "zoom-min", "zoom-max", "address-keys"];
|
|
53
|
+
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"];
|
|
53
54
|
#root;
|
|
54
55
|
#controller;
|
|
55
56
|
#view;
|
|
@@ -98,6 +99,13 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
98
99
|
set(next) {
|
|
99
100
|
this.#ensure().set(next);
|
|
100
101
|
}
|
|
102
|
+
/** The point the zoom buttons and keys zoom about: `{ x, y }` in CSS pixels from the view's middle when it is at Fit, moving with the picture. `undefined` zooms about the view's middle. The `anchor` attribute (`"x,y"`) sets the same. */
|
|
103
|
+
get anchor() {
|
|
104
|
+
return this.#ensure().anchor;
|
|
105
|
+
}
|
|
106
|
+
set anchor(point) {
|
|
107
|
+
this.#ensure().anchor = point ?? undefined;
|
|
108
|
+
}
|
|
101
109
|
/** What zoom 1 means to the page: the scene is drawn at `state.zoom * fitScale`. */
|
|
102
110
|
get fitScale() {
|
|
103
111
|
return this.#ensure().fitScale;
|
|
@@ -122,6 +130,10 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
122
130
|
c.turnable = !this.hasAttribute("flat");
|
|
123
131
|
c.speed = Number(this.getAttribute("speed")) || 30;
|
|
124
132
|
c.ignore = this.getAttribute("ignore") ?? DEFAULT_IGNORE;
|
|
133
|
+
for (const name of ["key-tilt", "stop-turn", "tilt-step"])
|
|
134
|
+
this.#option(name);
|
|
135
|
+
if (this.hasAttribute("anchor"))
|
|
136
|
+
this.#option("anchor");
|
|
125
137
|
const fitScale = Number(this.getAttribute("fit-scale"));
|
|
126
138
|
if (this.hasAttribute("fit-scale"))
|
|
127
139
|
c.setFitScale(fitScale);
|
|
@@ -171,6 +183,8 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
171
183
|
c.setFitScale(Number(value));
|
|
172
184
|
else if (name === "ignore")
|
|
173
185
|
c.ignore = value ?? DEFAULT_IGNORE;
|
|
186
|
+
else if (name === "anchor" || name === "key-tilt" || name === "stop-turn" || name === "tilt-step")
|
|
187
|
+
this.#option(name);
|
|
174
188
|
else if (name === "speed")
|
|
175
189
|
c.speed = Number(value) || 30;
|
|
176
190
|
else if (name === "flat") {
|
|
@@ -182,6 +196,20 @@ export class ViewControlsElement extends HTMLElement {
|
|
|
182
196
|
else
|
|
183
197
|
this.#limits();
|
|
184
198
|
}
|
|
199
|
+
#option(name) {
|
|
200
|
+
const c = this.#ensure();
|
|
201
|
+
const value = this.getAttribute(name);
|
|
202
|
+
if (name === "anchor") {
|
|
203
|
+
const [x, y] = (value ?? "").trim().split(/[\s,]+/).map(Number);
|
|
204
|
+
c.anchor = x !== undefined && y !== undefined && Number.isFinite(x) && Number.isFinite(y) ? { x, y } : undefined;
|
|
205
|
+
}
|
|
206
|
+
else if (name === "key-tilt")
|
|
207
|
+
c.keyTilt = value === "wheel" ? "wheel" : "drag";
|
|
208
|
+
else if (name === "stop-turn")
|
|
209
|
+
c.stopTurn = value !== null;
|
|
210
|
+
else if (name === "tilt-step")
|
|
211
|
+
c.tiltStep = Number(value) > 0 && Number.isFinite(Number(value)) ? Number(value) : 0;
|
|
212
|
+
}
|
|
185
213
|
#startZoom() {
|
|
186
214
|
const value = Number(this.getAttribute("zoom"));
|
|
187
215
|
return this.hasAttribute("zoom") && Number.isFinite(value) && value > 0 ? value : "fit";
|
|
@@ -44,7 +44,7 @@ export interface PinchStart {
|
|
|
44
44
|
}
|
|
45
45
|
/** The camera for two fingers now at `a` and `b`: zoom by the change of distance about where the pinch began, and the middle of the fingers also drags the view. `centre` is the view's middle in client pixels. */
|
|
46
46
|
export declare function pinchTo(start: PinchStart, a: Point, b: Point, centre: Point, limits?: Limits): Camera;
|
|
47
|
-
/** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). */
|
|
48
|
-
export declare function dragTurn(start: Camera, dx: number, dy: number, size: Size, tips: boolean, limits?: Limits): Pick<Camera, "yaw" | "tilt">;
|
|
47
|
+
/** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). A `step` above 0 rounds the tilt to a multiple of it. */
|
|
48
|
+
export declare function dragTurn(start: Camera, dx: number, dy: number, size: Size, tips: boolean, limits?: Limits, step?: number): Pick<Camera, "yaw" | "tilt">;
|
|
49
49
|
/** The factor a Ctrl or Command wheel turn zooms by (a trackpad's pinch arrives that way). `deltaMode` 1 is lines. */
|
|
50
50
|
export declare const wheelZoomFactor: (deltaY: number, deltaMode: number) => number;
|
|
@@ -38,10 +38,11 @@ export function pinchTo(start, a, b, centre, limits = DEFAULT_LIMITS) {
|
|
|
38
38
|
return zoomed;
|
|
39
39
|
return { ...zoomed, panX: zoomed.panX + (middle.x - start.middle.x), panY: zoomed.panY + (middle.y - start.middle.y) };
|
|
40
40
|
}
|
|
41
|
-
/** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). */
|
|
42
|
-
export function dragTurn(start, dx, dy, size, tips, limits = DEFAULT_LIMITS) {
|
|
41
|
+
/** A one-pointer drag that turns: `dx` and `dy` pixels from where it began, in a view of `size`. A finger does not tilt (the page's own scroll keeps the vertical drag). A `step` above 0 rounds the tilt to a multiple of it. */
|
|
42
|
+
export function dragTurn(start, dx, dy, size, tips, limits = DEFAULT_LIMITS, step = 0) {
|
|
43
43
|
const yaw = wrapYaw(start.yaw + (dx / (size.width || 360)) * TURN_ACROSS);
|
|
44
|
-
const
|
|
44
|
+
const raw = start.tilt - (dy / (size.height || 360)) * TILT_ACROSS;
|
|
45
|
+
const tilt = tips ? clamp(step > 0 ? Math.round(raw / step) * step : raw, -limits.tiltMax, limits.tiltMax) : start.tilt;
|
|
45
46
|
return { yaw, tilt };
|
|
46
47
|
}
|
|
47
48
|
/** The factor a Ctrl or Command wheel turn zooms by (a trackpad's pinch arrives that way). `deltaMode` 1 is lines. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@johnmorrisdotca/kazoku-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|