@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
|
@@ -0,0 +1,385 @@
|
|
|
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);
|
|
5
|
+
const browserClock = () => ({ request: (callback) => globalThis.requestAnimationFrame(callback), cancel: (id) => globalThis.cancelAnimationFrame(id) });
|
|
6
|
+
export function createViewController(options = {}) {
|
|
7
|
+
const limits = { ...DEFAULT_LIMITS, ...options.limits };
|
|
8
|
+
const clock = options.clock ?? browserClock();
|
|
9
|
+
let sizeOf = options.size;
|
|
10
|
+
let bound;
|
|
11
|
+
let frame = 0;
|
|
12
|
+
let last = 0;
|
|
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);
|
|
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);
|
|
22
|
+
const size = () => {
|
|
23
|
+
const own = sizeOf?.();
|
|
24
|
+
if (own !== undefined)
|
|
25
|
+
return own;
|
|
26
|
+
if (bound === undefined)
|
|
27
|
+
return undefined;
|
|
28
|
+
const r = bound.getBoundingClientRect();
|
|
29
|
+
return { width: r.width, height: r.height };
|
|
30
|
+
};
|
|
31
|
+
const emit = (final, reason) => {
|
|
32
|
+
if (bound !== undefined)
|
|
33
|
+
bound.style.touchAction = state.zoom > ZOOMED ? "none" : "pan-y";
|
|
34
|
+
options.onChange?.({ ...state }, { final, reason });
|
|
35
|
+
};
|
|
36
|
+
const camera = () => ({ yaw: state.yaw, tilt: state.tilt, zoom: state.zoom, panX: state.panX, panY: state.panY });
|
|
37
|
+
/** Take a camera in, held to the limits and, once the view's size is known, to the room the zoom gives. */
|
|
38
|
+
const take = (next) => {
|
|
39
|
+
const held = clampPan({ ...next, zoom: clamp(next.zoom, limits.zoomMin, limits.zoomMax), tilt: clamp(next.tilt, -limits.tiltMax, limits.tiltMax) }, size());
|
|
40
|
+
state.yaw = controller.turnable ? wrapYaw(held.yaw) : state.yaw;
|
|
41
|
+
state.tilt = controller.turnable ? held.tilt : state.tilt;
|
|
42
|
+
if (held.zoom !== state.zoom)
|
|
43
|
+
atStart = false;
|
|
44
|
+
state.zoom = held.zoom;
|
|
45
|
+
state.panX = held.panX;
|
|
46
|
+
state.panY = held.panY;
|
|
47
|
+
};
|
|
48
|
+
const centre = () => {
|
|
49
|
+
if (bound === undefined)
|
|
50
|
+
return { x: 0, y: 0 };
|
|
51
|
+
const r = bound.getBoundingClientRect();
|
|
52
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2 };
|
|
53
|
+
};
|
|
54
|
+
/** Client pixels to pixels from the view's middle. */
|
|
55
|
+
const fromMiddle = (at) => {
|
|
56
|
+
if (at === undefined)
|
|
57
|
+
return { x: 0, y: 0 };
|
|
58
|
+
const c = centre();
|
|
59
|
+
return { x: at.x - c.x, y: at.y - c.y };
|
|
60
|
+
};
|
|
61
|
+
const tick = (now) => {
|
|
62
|
+
if (!state.turning)
|
|
63
|
+
return;
|
|
64
|
+
frame = clock.request(tick);
|
|
65
|
+
if (holds > 0) {
|
|
66
|
+
last = 0;
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const dt = last === 0 ? 0 : Math.min(0.1, (now - last) / 1000);
|
|
70
|
+
last = now;
|
|
71
|
+
if (dt === 0)
|
|
72
|
+
return;
|
|
73
|
+
state.yaw = wrapYaw(state.yaw + controller.speed * dt);
|
|
74
|
+
emit(false, "auto");
|
|
75
|
+
};
|
|
76
|
+
const controller = {
|
|
77
|
+
state,
|
|
78
|
+
turnable: options.turnable ?? true,
|
|
79
|
+
speed: options.speed ?? 30,
|
|
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
|
+
},
|
|
107
|
+
set(next, final = true, reason = "set") {
|
|
108
|
+
const { turning, drag, ...cam } = next;
|
|
109
|
+
take({ ...camera(), ...cam });
|
|
110
|
+
if (drag !== undefined)
|
|
111
|
+
state.drag = drag;
|
|
112
|
+
if (turning !== undefined && turning !== state.turning) {
|
|
113
|
+
if (turning)
|
|
114
|
+
controller.play();
|
|
115
|
+
else
|
|
116
|
+
controller.pause();
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
emit(final, reason);
|
|
120
|
+
},
|
|
121
|
+
play() {
|
|
122
|
+
if (state.turning || !controller.turnable)
|
|
123
|
+
return;
|
|
124
|
+
state.turning = true;
|
|
125
|
+
last = 0;
|
|
126
|
+
frame = clock.request(tick);
|
|
127
|
+
emit(true, "play");
|
|
128
|
+
},
|
|
129
|
+
pause() {
|
|
130
|
+
if (!state.turning)
|
|
131
|
+
return;
|
|
132
|
+
state.turning = false;
|
|
133
|
+
clock.cancel(frame);
|
|
134
|
+
emit(true, "pause");
|
|
135
|
+
},
|
|
136
|
+
toggle() {
|
|
137
|
+
if (state.turning)
|
|
138
|
+
controller.pause();
|
|
139
|
+
else
|
|
140
|
+
controller.play();
|
|
141
|
+
},
|
|
142
|
+
holdTurn() {
|
|
143
|
+
holds += 1;
|
|
144
|
+
},
|
|
145
|
+
releaseTurn() {
|
|
146
|
+
holds = Math.max(0, holds - 1);
|
|
147
|
+
last = 0;
|
|
148
|
+
},
|
|
149
|
+
front() {
|
|
150
|
+
take({ ...camera(), yaw: 0, tilt: 0 });
|
|
151
|
+
emit(true, "front");
|
|
152
|
+
},
|
|
153
|
+
fit() {
|
|
154
|
+
take({ ...camera(), zoom: 1, panX: 0, panY: 0 });
|
|
155
|
+
emit(true, "fit");
|
|
156
|
+
},
|
|
157
|
+
zoomAt(next, at, final = true) {
|
|
158
|
+
take(zoomAbout(camera(), next, fromMiddle(at), limits));
|
|
159
|
+
emit(final, "zoom");
|
|
160
|
+
},
|
|
161
|
+
zoomBy(factor, at) {
|
|
162
|
+
controller.zoomAt(state.zoom * factor, at);
|
|
163
|
+
},
|
|
164
|
+
panBy(stepsX, stepsY) {
|
|
165
|
+
const s = size();
|
|
166
|
+
take({ ...camera(), panX: state.panX + stepsX * (s?.width ?? 0) * PAN_STEP, panY: state.panY + stepsY * (s?.height ?? 0) * PAN_STEP });
|
|
167
|
+
emit(true, "pan");
|
|
168
|
+
},
|
|
169
|
+
setDrag(drag) {
|
|
170
|
+
state.drag = drag;
|
|
171
|
+
emit(true, "set");
|
|
172
|
+
},
|
|
173
|
+
bind(view) {
|
|
174
|
+
bound = view;
|
|
175
|
+
sizeOf = options.size;
|
|
176
|
+
return bindHands(view);
|
|
177
|
+
},
|
|
178
|
+
destroy() {
|
|
179
|
+
clock.cancel(frame);
|
|
180
|
+
state.turning = false;
|
|
181
|
+
unbind?.();
|
|
182
|
+
},
|
|
183
|
+
};
|
|
184
|
+
let unbind;
|
|
185
|
+
function bindHands(view) {
|
|
186
|
+
const pointers = new Map();
|
|
187
|
+
let gesture;
|
|
188
|
+
let holding = false;
|
|
189
|
+
let lastTap = 0;
|
|
190
|
+
let lastTapAt = { x: 0, y: 0 };
|
|
191
|
+
const hold = () => {
|
|
192
|
+
if (!holding)
|
|
193
|
+
controller.holdTurn();
|
|
194
|
+
holding = true;
|
|
195
|
+
};
|
|
196
|
+
const release = () => {
|
|
197
|
+
if (holding)
|
|
198
|
+
controller.releaseTurn();
|
|
199
|
+
holding = false;
|
|
200
|
+
};
|
|
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
|
+
};
|
|
214
|
+
const onDown = (e) => {
|
|
215
|
+
if (e.pointerType === "mouse" && e.button > 2)
|
|
216
|
+
return;
|
|
217
|
+
if (ignored(e))
|
|
218
|
+
return;
|
|
219
|
+
pointers.set(e.pointerId, { x: e.clientX, y: e.clientY });
|
|
220
|
+
try {
|
|
221
|
+
view.setPointerCapture(e.pointerId);
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* a pointer already gone */
|
|
225
|
+
}
|
|
226
|
+
if (pointers.size === 2) {
|
|
227
|
+
const [a, b] = [...pointers.values()];
|
|
228
|
+
gesture = { kind: "pinch", start: { distance: Math.hypot(a.x - b.x, a.y - b.y) || 1, middle: { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 }, camera: camera() } };
|
|
229
|
+
release();
|
|
230
|
+
}
|
|
231
|
+
else if (pointers.size === 1) {
|
|
232
|
+
// At 1x a plain press and drag turns the piece; zoomed in it moves the view unless the Turn choice is made. Shift, the middle or the right button always move a zoomed view. A flat view cannot turn, so a drag moves it.
|
|
233
|
+
const pan = e.button === 1 || e.button === 2 || e.shiftKey || (state.zoom > ZOOMED && (!canTurn() || state.drag !== "turn"));
|
|
234
|
+
gesture = { kind: pan ? "pan" : "turn", x: e.clientX, y: e.clientY, camera: camera(), moved: false };
|
|
235
|
+
if (!pan)
|
|
236
|
+
hold();
|
|
237
|
+
}
|
|
238
|
+
view.dataset.grab = "1";
|
|
239
|
+
};
|
|
240
|
+
const onMove = (e) => {
|
|
241
|
+
const p = pointers.get(e.pointerId);
|
|
242
|
+
if (p === undefined || gesture === undefined)
|
|
243
|
+
return;
|
|
244
|
+
p.x = e.clientX;
|
|
245
|
+
p.y = e.clientY;
|
|
246
|
+
if (gesture.kind === "pinch" && pointers.size >= 2) {
|
|
247
|
+
const [a, b] = [...pointers.values()];
|
|
248
|
+
take(pinchTo(gesture.start, a, b, centre(), limits));
|
|
249
|
+
emit(false, "pinch");
|
|
250
|
+
}
|
|
251
|
+
else if (gesture.kind === "pan") {
|
|
252
|
+
if (state.zoom > 1) {
|
|
253
|
+
take({ ...camera(), panX: gesture.camera.panX + (e.clientX - gesture.x), panY: gesture.camera.panY + (e.clientY - gesture.y) });
|
|
254
|
+
emit(false, "drag");
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
else if (gesture.kind === "turn") {
|
|
258
|
+
const dx = e.clientX - gesture.x;
|
|
259
|
+
const dy = e.clientY - gesture.y;
|
|
260
|
+
if (Math.abs(dx) + Math.abs(dy) > 3)
|
|
261
|
+
gesture.moved = true;
|
|
262
|
+
if (!canTurn())
|
|
263
|
+
return;
|
|
264
|
+
const r = view.getBoundingClientRect();
|
|
265
|
+
// 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.
|
|
266
|
+
take({ ...camera(), ...dragTurn(gesture.camera, dx, dy, { width: r.width, height: r.height }, e.pointerType !== "touch", limits) });
|
|
267
|
+
emit(false, "drag");
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
const onUp = (e) => {
|
|
271
|
+
if (!pointers.delete(e.pointerId))
|
|
272
|
+
return;
|
|
273
|
+
const wasPinch = gesture?.kind === "pinch";
|
|
274
|
+
const moved = gesture?.kind !== "pinch" && gesture?.moved === true;
|
|
275
|
+
if (pointers.size === 1) {
|
|
276
|
+
// One finger left of a pinch carries on as a drag from where it is.
|
|
277
|
+
const [rest] = [...pointers.values()];
|
|
278
|
+
gesture = { kind: "pan", x: rest.x, y: rest.y, camera: camera(), moved: true };
|
|
279
|
+
emit(true, "pinch");
|
|
280
|
+
}
|
|
281
|
+
else if (pointers.size === 0) {
|
|
282
|
+
gesture = undefined;
|
|
283
|
+
release();
|
|
284
|
+
delete view.dataset.grab;
|
|
285
|
+
emit(true, "drag");
|
|
286
|
+
// A double press (two quick taps or clicks, hardly moved): in to 2x where it was pressed, and again back to 1x.
|
|
287
|
+
if (!wasPinch && !moved && e.type === "pointerup") {
|
|
288
|
+
if (e.timeStamp - lastTap < 350 && Math.hypot(e.clientX - lastTapAt.x, e.clientY - lastTapAt.y) < 24) {
|
|
289
|
+
lastTap = 0;
|
|
290
|
+
controller.zoomAt(state.zoom > ZOOMED ? 1 : 2, { x: e.clientX, y: e.clientY });
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
lastTap = e.timeStamp;
|
|
294
|
+
lastTapAt = { x: e.clientX, y: e.clientY };
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
};
|
|
299
|
+
const onWheel = (e) => {
|
|
300
|
+
if (e.ctrlKey || e.metaKey) {
|
|
301
|
+
// A trackpad's pinch arrives as Ctrl and the wheel; Command and the wheel does the same for a mouse.
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
controller.zoomAt(state.zoom * wheelZoomFactor(e.deltaY, e.deltaMode), { x: e.clientX, y: e.clientY });
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const horizontal = Math.abs(e.deltaX) > Math.abs(e.deltaY);
|
|
307
|
+
const focused = view.ownerDocument.activeElement === view;
|
|
308
|
+
// A sideways swipe turns it wherever the pointer is over it; an up and down wheel only when the view has focus, as the page's scroll comes first.
|
|
309
|
+
if (!horizontal && !focused)
|
|
310
|
+
return;
|
|
311
|
+
e.preventDefault();
|
|
312
|
+
if (!canTurn())
|
|
313
|
+
return;
|
|
314
|
+
if (horizontal || e.shiftKey)
|
|
315
|
+
controller.set({ yaw: state.yaw + (e.deltaX || e.deltaY) * 0.3 }, true, "wheel");
|
|
316
|
+
else
|
|
317
|
+
controller.set({ tilt: Math.round(state.tilt + e.deltaY * 0.15) }, true, "wheel");
|
|
318
|
+
};
|
|
319
|
+
const onKey = (e) => {
|
|
320
|
+
if (e.ctrlKey || e.metaKey || e.altKey)
|
|
321
|
+
return;
|
|
322
|
+
const big = e.shiftKey ? 45 : 15;
|
|
323
|
+
const turn = (next) => {
|
|
324
|
+
if (canTurn())
|
|
325
|
+
controller.set(next, true, "key");
|
|
326
|
+
};
|
|
327
|
+
switch (e.key) {
|
|
328
|
+
case "ArrowLeft":
|
|
329
|
+
turn({ yaw: state.yaw - big });
|
|
330
|
+
break;
|
|
331
|
+
case "ArrowRight":
|
|
332
|
+
turn({ yaw: state.yaw + big });
|
|
333
|
+
break;
|
|
334
|
+
case "ArrowUp":
|
|
335
|
+
turn({ tilt: state.tilt + 5 });
|
|
336
|
+
break;
|
|
337
|
+
case "ArrowDown":
|
|
338
|
+
turn({ tilt: state.tilt - 5 });
|
|
339
|
+
break;
|
|
340
|
+
case "+":
|
|
341
|
+
case "=":
|
|
342
|
+
controller.zoomBy(1.25);
|
|
343
|
+
break;
|
|
344
|
+
case "-":
|
|
345
|
+
case "_":
|
|
346
|
+
controller.zoomBy(1 / 1.25);
|
|
347
|
+
break;
|
|
348
|
+
case "0":
|
|
349
|
+
case "Home":
|
|
350
|
+
controller.front();
|
|
351
|
+
controller.fit();
|
|
352
|
+
break;
|
|
353
|
+
default: return;
|
|
354
|
+
}
|
|
355
|
+
e.preventDefault();
|
|
356
|
+
};
|
|
357
|
+
const onMenu = (e) => e.preventDefault();
|
|
358
|
+
view.addEventListener("pointerdown", onDown);
|
|
359
|
+
view.addEventListener("pointermove", onMove);
|
|
360
|
+
view.addEventListener("pointerup", onUp);
|
|
361
|
+
view.addEventListener("pointercancel", onUp);
|
|
362
|
+
view.addEventListener("wheel", onWheel, { passive: false });
|
|
363
|
+
view.addEventListener("keydown", onKey);
|
|
364
|
+
// A right press is a pan, not the browser's menu.
|
|
365
|
+
view.addEventListener("contextmenu", onMenu);
|
|
366
|
+
view.style.touchAction = state.zoom > ZOOMED ? "none" : "pan-y";
|
|
367
|
+
const off = () => {
|
|
368
|
+
view.removeEventListener("pointerdown", onDown);
|
|
369
|
+
view.removeEventListener("pointermove", onMove);
|
|
370
|
+
view.removeEventListener("pointerup", onUp);
|
|
371
|
+
view.removeEventListener("pointercancel", onUp);
|
|
372
|
+
view.removeEventListener("wheel", onWheel);
|
|
373
|
+
view.removeEventListener("keydown", onKey);
|
|
374
|
+
view.removeEventListener("contextmenu", onMenu);
|
|
375
|
+
view.style.touchAction = "";
|
|
376
|
+
release();
|
|
377
|
+
if (bound === view)
|
|
378
|
+
bound = undefined;
|
|
379
|
+
unbind = undefined;
|
|
380
|
+
};
|
|
381
|
+
unbind = off;
|
|
382
|
+
return off;
|
|
383
|
+
}
|
|
384
|
+
return controller;
|
|
385
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function defineViewControls(registry?: CustomElementRegistry): boolean;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Importing this registers <kazoku-view-controls>, unless the tag is already taken.
|
|
2
|
+
//
|
|
3
|
+
// `customElements.define` throws when a name is already registered, and a page can meet that without doing anything wrong: two copies of
|
|
4
|
+
// 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 itself
|
|
5
|
+
// to supply its own element. A throw here would stop the importing module, and with it the page's script. So the tag is defined only when
|
|
6
|
+
// nobody has, and the first definition wins; read `customElements.get("kazoku-view-controls")` to see which one that is.
|
|
7
|
+
import { TAG, ViewControlsElement } from "./element.js";
|
|
8
|
+
export function defineViewControls(registry = customElements) {
|
|
9
|
+
if (registry.get(TAG) !== undefined)
|
|
10
|
+
return false;
|
|
11
|
+
registry.define(TAG, ViewControlsElement);
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
defineViewControls();
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { type ViewController, type ViewState } from "./controller.ts";
|
|
2
|
+
import { EVENT, TAG } from "./index.ts";
|
|
3
|
+
import { type ViewWords } from "./words.ts";
|
|
4
|
+
export { EVENT, TAG };
|
|
5
|
+
/** What the `kazoku-viewchange` event carries in `detail`. */
|
|
6
|
+
export interface ViewChangeDetail extends ViewState {
|
|
7
|
+
final: boolean;
|
|
8
|
+
reason: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
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).
|
|
16
|
+
*/
|
|
17
|
+
export declare class ViewControlsElement extends HTMLElement {
|
|
18
|
+
#private;
|
|
19
|
+
static observedAttributes: string[];
|
|
20
|
+
constructor();
|
|
21
|
+
/** The controller behind the element: the same methods and state a page that wants no element would use. */
|
|
22
|
+
get controller(): ViewController;
|
|
23
|
+
/** The view now (a copy). */
|
|
24
|
+
get state(): ViewState;
|
|
25
|
+
get view(): HTMLElement | undefined;
|
|
26
|
+
/** The element the hands bind to, in place of `for`. */
|
|
27
|
+
set view(element: HTMLElement | undefined);
|
|
28
|
+
/** Your own labels over the language's, for example `{ front: "Home" }`. */
|
|
29
|
+
set words(words: Partial<ViewWords>);
|
|
30
|
+
play(): void;
|
|
31
|
+
pause(): void;
|
|
32
|
+
front(): void;
|
|
33
|
+
fit(): void;
|
|
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;
|
|
38
|
+
connectedCallback(): void;
|
|
39
|
+
disconnectedCallback(): void;
|
|
40
|
+
attributeChangedCallback(name: string, _old: string | null, value: string | null): void;
|
|
41
|
+
}
|