@scenography/overlay-presets 0.2.0 → 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/dist/index.cjs +581 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +173 -4
- package/dist/index.d.mts +173 -4
- package/dist/index.mjs +577 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
package/dist/index.mjs
CHANGED
|
@@ -132,6 +132,7 @@ function cardboardButton(options = {}) {
|
|
|
132
132
|
ctx.gallery.setRender({ cardboard: enabled });
|
|
133
133
|
button.classList.toggle("btn-active", enabled);
|
|
134
134
|
button.setAttribute("aria-pressed", String(enabled));
|
|
135
|
+
ctx.gallery.emit("cardboard:change", { enabled });
|
|
135
136
|
};
|
|
136
137
|
button.addEventListener("click", onClick);
|
|
137
138
|
return {
|
|
@@ -141,6 +142,161 @@ function cardboardButton(options = {}) {
|
|
|
141
142
|
};
|
|
142
143
|
}
|
|
143
144
|
//#endregion
|
|
145
|
+
//#region src/components/reset-view/reset-view.ts
|
|
146
|
+
/**
|
|
147
|
+
* A "home" button that returns the camera to the scene's start position and
|
|
148
|
+
* orientation (`ctx.camera.reset()`) — for visitors who wander off and want to
|
|
149
|
+
* get back to the entrance. Defaults to a plain glyph; pass `lucideIcon("House")`.
|
|
150
|
+
*/
|
|
151
|
+
function resetViewButton(options = {}) {
|
|
152
|
+
const icon = options.icon ?? "⌂";
|
|
153
|
+
return (ctx) => {
|
|
154
|
+
const button = document.createElement("button");
|
|
155
|
+
button.type = "button";
|
|
156
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
157
|
+
setIcon(button, icon);
|
|
158
|
+
const onClick = () => ctx.gallery.ctx.camera.reset();
|
|
159
|
+
button.addEventListener("click", onClick);
|
|
160
|
+
const unbind = bindLocale(ctx, () => button.setAttribute("aria-label", resolveText(options.label ?? "Reset view")));
|
|
161
|
+
return {
|
|
162
|
+
element: button,
|
|
163
|
+
destroy: () => {
|
|
164
|
+
button.removeEventListener("click", onClick);
|
|
165
|
+
unbind();
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/components/view-mode/view-mode.ts
|
|
172
|
+
/** Lucide icon per mode, shown beside its label in the picker. */
|
|
173
|
+
const MODE_ICON = {
|
|
174
|
+
touch: "Hand",
|
|
175
|
+
vr: "Orbit",
|
|
176
|
+
cardboard: "RectangleGoggles"
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* A camera-control mode picker: a `ScanEye` button opening a small modal with one
|
|
180
|
+
* entry per mode (Touch / VR / Cardboard). Picking one calls
|
|
181
|
+
* `ctx.viewControls.setMode` — which, inside this click gesture, flips the
|
|
182
|
+
* gyroscope and stereo split (so iOS grants motion here). The active mode is
|
|
183
|
+
* marked and kept in sync via the `controls:change` event, so the settings panel
|
|
184
|
+
* and this picker never disagree.
|
|
185
|
+
*/
|
|
186
|
+
function viewModeButton(options) {
|
|
187
|
+
return (ctx) => {
|
|
188
|
+
const viewControls = ctx.gallery.ctx.viewControls;
|
|
189
|
+
const wrap = document.createElement("div");
|
|
190
|
+
const button = document.createElement("button");
|
|
191
|
+
button.type = "button";
|
|
192
|
+
button.className = "btn btn-circle btn-sm";
|
|
193
|
+
setIcon(button, lucideIcon("ScanEye"));
|
|
194
|
+
const dialog = document.createElement("dialog");
|
|
195
|
+
dialog.className = "modal";
|
|
196
|
+
const box = document.createElement("div");
|
|
197
|
+
box.className = "modal-box max-w-xs";
|
|
198
|
+
const close = document.createElement("button");
|
|
199
|
+
close.type = "button";
|
|
200
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
201
|
+
close.textContent = "✕";
|
|
202
|
+
box.appendChild(close);
|
|
203
|
+
const heading = document.createElement("h3");
|
|
204
|
+
heading.className = "text-lg font-bold mb-4";
|
|
205
|
+
box.appendChild(heading);
|
|
206
|
+
const list = document.createElement("div");
|
|
207
|
+
list.className = "flex flex-col gap-2";
|
|
208
|
+
box.appendChild(list);
|
|
209
|
+
const cleanups = [];
|
|
210
|
+
const relabels = [];
|
|
211
|
+
const optionButtons = [];
|
|
212
|
+
for (const item of options.modes) {
|
|
213
|
+
const optBtn = document.createElement("button");
|
|
214
|
+
optBtn.type = "button";
|
|
215
|
+
optBtn.className = "btn btn-block justify-start gap-3";
|
|
216
|
+
const iconEl = document.createElement("span");
|
|
217
|
+
iconEl.style.display = "inline-flex";
|
|
218
|
+
setIcon(iconEl, lucideIcon(MODE_ICON[item.mode]));
|
|
219
|
+
const label = document.createElement("span");
|
|
220
|
+
optBtn.append(iconEl, label);
|
|
221
|
+
const onPick = () => {
|
|
222
|
+
viewControls.setMode(item.mode);
|
|
223
|
+
syncActive();
|
|
224
|
+
dialog.close();
|
|
225
|
+
};
|
|
226
|
+
optBtn.addEventListener("click", onPick);
|
|
227
|
+
cleanups.push(() => optBtn.removeEventListener("click", onPick));
|
|
228
|
+
list.appendChild(optBtn);
|
|
229
|
+
optionButtons.push({
|
|
230
|
+
el: optBtn,
|
|
231
|
+
mode: item.mode
|
|
232
|
+
});
|
|
233
|
+
relabels.push(() => label.textContent = resolveText(item.label));
|
|
234
|
+
}
|
|
235
|
+
const syncActive = () => {
|
|
236
|
+
const current = viewControls.getMode();
|
|
237
|
+
for (const { el, mode } of optionButtons) el.classList.toggle("btn-active", mode === current);
|
|
238
|
+
};
|
|
239
|
+
const backdrop = document.createElement("form");
|
|
240
|
+
backdrop.method = "dialog";
|
|
241
|
+
backdrop.className = "modal-backdrop";
|
|
242
|
+
const backdropBtn = document.createElement("button");
|
|
243
|
+
backdropBtn.textContent = "close";
|
|
244
|
+
backdrop.appendChild(backdropBtn);
|
|
245
|
+
dialog.append(box, backdrop);
|
|
246
|
+
wrap.append(button, dialog);
|
|
247
|
+
const offChange = ctx.gallery.on("controls:change", syncActive);
|
|
248
|
+
const unbind = bindLocale(ctx, () => {
|
|
249
|
+
const title = resolveText(options.title);
|
|
250
|
+
button.setAttribute("aria-label", title);
|
|
251
|
+
heading.textContent = title;
|
|
252
|
+
close.setAttribute("aria-label", resolveText(options.closeLabel ?? "Close"));
|
|
253
|
+
for (const relabel of relabels) relabel();
|
|
254
|
+
});
|
|
255
|
+
const onOpen = () => {
|
|
256
|
+
syncActive();
|
|
257
|
+
dialog.showModal();
|
|
258
|
+
};
|
|
259
|
+
const onClose = () => dialog.close();
|
|
260
|
+
button.addEventListener("click", onOpen);
|
|
261
|
+
close.addEventListener("click", onClose);
|
|
262
|
+
return {
|
|
263
|
+
element: wrap,
|
|
264
|
+
destroy: () => {
|
|
265
|
+
button.removeEventListener("click", onOpen);
|
|
266
|
+
close.removeEventListener("click", onClose);
|
|
267
|
+
for (const fn of cleanups) fn();
|
|
268
|
+
offChange();
|
|
269
|
+
unbind();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
//#endregion
|
|
275
|
+
//#region src/components/view-mode/capabilities.ts
|
|
276
|
+
/**
|
|
277
|
+
* Sniff the device's control capabilities. Deliberately conservative: gyro is
|
|
278
|
+
* gated behind a touch (mobile) device too, since desktops expose the
|
|
279
|
+
* `DeviceOrientationEvent` interface but never fire it — pairing it with a coarse
|
|
280
|
+
* pointer avoids offering VR/Cardboard where they can't work.
|
|
281
|
+
*/
|
|
282
|
+
function detectControlCapabilities() {
|
|
283
|
+
return {
|
|
284
|
+
touch: typeof matchMedia !== "undefined" ? matchMedia("(pointer: coarse)").matches : typeof navigator !== "undefined" && (navigator.maxTouchPoints ?? 0) > 0,
|
|
285
|
+
gyro: typeof window !== "undefined" && "DeviceOrientationEvent" in window
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* The modes worth offering on this device, in display order. Touch needs a touch
|
|
290
|
+
* screen; VR and Cardboard additionally need the gyro. Desktop (no touch) gets an
|
|
291
|
+
* empty list — it uses keyboard/mouse and shows no mode picker at all.
|
|
292
|
+
*/
|
|
293
|
+
function availableModes(caps) {
|
|
294
|
+
const modes = [];
|
|
295
|
+
if (caps.touch) modes.push("touch");
|
|
296
|
+
if (caps.touch && caps.gyro) modes.push("vr", "cardboard");
|
|
297
|
+
return modes;
|
|
298
|
+
}
|
|
299
|
+
//#endregion
|
|
144
300
|
//#region src/components/environment/environment.ts
|
|
145
301
|
/**
|
|
146
302
|
* An environment panel: a button (e.g. a sun/cloud icon) opening a daisyUI modal
|
|
@@ -462,6 +618,286 @@ function infoPanel(options) {
|
|
|
462
618
|
};
|
|
463
619
|
}
|
|
464
620
|
//#endregion
|
|
621
|
+
//#region src/components/help/diagrams.ts
|
|
622
|
+
const CLOSE = `</svg>`;
|
|
623
|
+
const DIAGRAMS = { cardboard: "<svg viewBox=\"0 0 24 24\" width=\"100%\" height=\"100%\" fill=\"currentColor\" role=\"img\" aria-hidden=\"true\"><path d=\"M9.26402604,18 C8.79719433,18.6226198 8.05922695,19 7.26393202,19 L4.5,19 C3.11928813,19 2,17.8807119 2,16.5 L2,9.5 C2,8.27576856 2.8799598,7.25706275 4.04188098,7.04188098 C4.25706275,5.8799598 5.27576856,5 6.5,5 L17.5,5 C18.7242314,5 19.7429373,5.8799598 19.958119,7.04188098 C21.1200402,7.25706275 22,8.27576856 22,9.5 L22,16.5 C22,17.8807119 20.8807119,19 19.5,19 L16.736068,19 C15.940773,19 15.2028057,18.6226198 14.735974,18 L9.26402604,18 Z M9.82576985,17 L14.1742301,17 C13.7685119,16.3807773 13.0746523,16 12.322949,16 L11.677051,16 C10.9253477,16 10.2314881,16.3807773 9.82576985,17 Z M5.08535285,7 L18.9146471,7 C18.7087289,6.41740381 18.1531094,6 17.5,6 L6.5,6 C5.84689059,6 5.29127106,6.41740381 5.08535285,7 Z M8.8027864,16.7763932 C9.34713196,15.6877021 10.4598573,15 11.677051,15 L12.322949,15 C13.5401427,15 14.652868,15.6877021 15.1972136,16.7763932 L15.3944272,17.1708204 C15.6485152,17.6789964 16.1679099,18 16.736068,18 L19.5,18 C20.3284271,18 21,17.3284271 21,16.5 L21,9.5 C21,8.67157288 20.3284271,8 19.5,8 L4.5,8 C3.67157288,8 3,8.67157288 3,9.5 L3,16.5 C3,17.3284271 3.67157288,18 4.5,18 L7.26393202,18 C7.83209011,18 8.35148479,17.6789964 8.60557281,17.1708204 L8.8027864,16.7763932 Z M7.5,15 C6.11928813,15 5,13.8807119 5,12.5 C5,11.1192881 6.11928813,10 7.5,10 C8.88071187,10 10,11.1192881 10,12.5 C10,13.8807119 8.88071187,15 7.5,15 Z M7.5,14 C8.32842712,14 9,13.3284271 9,12.5 C9,11.6715729 8.32842712,11 7.5,11 C6.67157288,11 6,11.6715729 6,12.5 C6,13.3284271 6.67157288,14 7.5,14 Z M16.5,15 C15.1192881,15 14,13.8807119 14,12.5 C14,11.1192881 15.1192881,10 16.5,10 C17.8807119,10 19,11.1192881 19,12.5 C19,13.8807119 17.8807119,15 16.5,15 Z M16.5,14 C17.3284271,14 18,13.3284271 18,12.5 C18,11.6715729 17.3284271,11 16.5,11 C15.6715729,11 15,11.6715729 15,12.5 C15,13.3284271 15.6715729,14 16.5,14 Z\"/>" + CLOSE };
|
|
624
|
+
/** Build the diagram figure node for a simple tab kind (a div wrapping the SVG). */
|
|
625
|
+
function diagramNode(kind) {
|
|
626
|
+
const figure = document.createElement("div");
|
|
627
|
+
figure.className = "text-base-content/70 mx-auto mb-3 h-28 w-full max-w-xs";
|
|
628
|
+
figure.innerHTML = DIAGRAMS[kind];
|
|
629
|
+
return figure;
|
|
630
|
+
}
|
|
631
|
+
const GP_LEFT_STICK = "<path d=\"M182.4,251.5c-28.9,0-52.5,23.5-52.5,52.5s23.5,52.5,52.5,52.5c28.9,0,52.5-23.5,52.5-52.5S211.3,251.5,182.4,251.5z M182.4,331.9c-15.4,0-28-12.5-28-28s12.5-28,28-28c15.4,0,28,12.5,28,28S197.8,331.9,182.4,331.9z\"/>";
|
|
632
|
+
const GP_RIGHT_STICK = "<path d=\"M382.1,303.9c0-28.9-23.5-52.5-52.5-52.5c-28.9,0-52.5,23.5-52.5,52.5s23.5,52.5,52.5,52.5C358.6,356.4,382.1,332.8,382.1,303.9z M329.6,331.9c-15.4,0-28-12.5-28-28s12.5-28,28-28c15.4,0,28,12.5,28,28S345.1,331.9,329.6,331.9z\"/>";
|
|
633
|
+
const GP_DPAD = "<path d=\"M159.7,132.6c-6.8,0-12.3,5.5-12.3,12.3V167h-22.1c-6.8,0-12.3,5.5-12.3,12.3s5.5,12.3,12.3,12.3h22.1v22.1c0,6.8,5.5,12.3,12.3,12.3s12.3-5.5,12.3-12.3v-22.1h22.1c6.8,0,12.3-5.5,12.3-12.3s-5.5-12.3-12.3-12.3H172v-22.1C172,138.1,166.5,132.6,159.7,132.6z\"/>";
|
|
634
|
+
const GP_BTN_TOP = `<circle cx="353.4" cy="147.8" r="16.6"/>`;
|
|
635
|
+
const GP_BTN_LEFT = `<circle cx="319.5" cy="181.8" r="16.6"/>`;
|
|
636
|
+
const GP_BTN_RIGHT = `<circle cx="387.4" cy="181.8" r="16.6"/>`;
|
|
637
|
+
const GP_BTN_BOTTOM = `<circle cx="353.4" cy="215.7" r="16.6"/>`;
|
|
638
|
+
const lit = (part) => `<g class="text-primary" fill="currentColor">${part}</g>`;
|
|
639
|
+
/** The gamepad icon with only the parts used by `highlight` lit in the accent. */
|
|
640
|
+
function gamepadSvg(highlight) {
|
|
641
|
+
const on = (part, when) => highlight === when ? lit(part) : part;
|
|
642
|
+
return "<svg viewBox=\"0 0 512 512\" width=\"100%\" height=\"100%\" fill=\"currentColor\" role=\"img\" aria-hidden=\"true\"><path d=\"M467.8,173.6c-8.4-50-49.7-87-100.6-90.1l-2.1-0.4c-0.7-0.1-1.4-0.2-2.1-0.2H151.5c-53.3,0-98.4,38.1-107.2,90.4L12,346.4c-1,4.7-1.5,9.4-1.5,14.1c0,37.8,30.7,68.5,68.5,68.5c22.8,0,44.1-11.3,56.6-29.9l15.6-21.6c4-5.5,2.7-13.1-2.8-17.1c-5.5-4-13.1-2.7-17.1,2.8l-15.8,21.9c-8.2,12.2-21.9,19.5-36.5,19.5c-24.3,0-44-19.8-44-44c0-3,0.3-6.1,1-9.4l32.3-173.5c6.8-40.7,41.8-70.2,83.1-70.2H362l1.7,0.3c0.5,0.1,0.9,0.1,1.4,0.2c39,2.1,72,31.5,78.5,69.9L476,351.4c0.6,3,0.9,6.1,0.9,9.1c0,24.3-19.8,44-44,44c-14.7,0-28.3-7.3-36.8-19.8l-16.8-23.2c-4-5.5-11.6-6.7-17.1-2.8c-5.5,4-6.7,11.6-2.8,17.1l16.5,22.9c12.8,18.9,34,30.2,56.8,30.2c37.8,0,68.5-30.7,68.5-68.5c0-4.7-0.5-9.5-1.4-13.9L467.8,173.6z\"/>" + on(GP_LEFT_STICK, "move") + on(GP_RIGHT_STICK, "look") + on(GP_DPAD, "move") + GP_BTN_TOP + GP_BTN_LEFT + GP_BTN_RIGHT + on(GP_BTN_BOTTOM, "select") + CLOSE;
|
|
643
|
+
}
|
|
644
|
+
const PHONE_ROTATE = "<svg viewBox=\"-8.04 0 36.945037 32.134997\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><g fill=\"currentColor\" transform=\"translate(-614.78856,-194.333)\"><path d=\"m 630.278,196.333 a 0.855,0.855 0 0 1 0.854,0.854 v 26.427 a 0.855,0.855 0 0 1 -0.854,0.854 h -10.345 a 0.855,0.855 0 0 1 -0.854,-0.854 v -26.427 a 0.855,0.855 0 0 1 0.854,-0.854 h 10.345 m 0,-2 h -10.345 a 2.854,2.854 0 0 0 -2.854,2.854 v 26.427 a 2.854,2.854 0 0 0 2.854,2.854 h 10.345 a 2.854,2.854 0 0 0 2.854,-2.854 v -26.427 a 2.854,2.854 0 0 0 -2.854,-2.854 z\"/><path d=\"m 625.106,220.375 a 1.144,1.144 0 1 0 1.143,1.143 1.144,1.144 0 0 0 -1.143,-1.143 z\"/></g><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"2.1371\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m 21.517396,7.653044 c 0.34046,0.09237 0.678922,0.2068044 1.0138,0.3440646 4.368435,1.7905494 6.458225,6.7833844 4.667677,11.1518194 -1.013391,2.472391 -3.052499,4.214862 -5.423632,4.937107 M -0.65235214,23.84002 C -0.99282974,23.74769 -1.3312767,23.633223 -1.6661557,23.495962 -6.0346112,21.705405 -8.1243897,16.712597 -6.3338324,12.344142 -5.3655121,9.981708 -3.4606611,8.2856913 -1.2244333,7.5094329 M -2.1519003,10.593762 -0.93612324,7.6276059 -1.2244333,7.5094329 M 24.686186,25.279184 21.77524,24.086036 m 0,0 -0.05514,-0.02261 1.215708,-2.966184 M -3.9022805,6.41183 -1.2244349,7.5094331\"/></svg>";
|
|
645
|
+
const thumbDot = (cx, cy) => `<g class="text-primary" fill="currentColor"><circle cx="${cx}" cy="${cy}" r="3"/></g>`;
|
|
646
|
+
const grabPhone = (light) => "<svg viewBox=\"-8.04 0 49.591388 22.890837\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><g fill=\"currentColor\" transform=\"rotate(-90,219.77734,413.35469)\"><path d=\"m 633.13203,197.18696 a 2.854,2.854 0 0 0 -2.85402,-2.85394 h -8.64185 l 1.67971,1.99997 h 6.96214 a 0.855,0.855 0 0 1 0.85398,0.85397 V 223.614 a 0.855,0.855 0 0 1 -0.85398,0.85398 h -7.1771 l -1.67979,2.00004 h 8.85689 a 2.854,2.854 0 0 0 2.85402,-2.85402 z m -14.05302,1.1933 -1.82611,-2.17421 a 2.854,2.854 0 0 0 -0.17393,0.98091 V 223.614 a 2.854,2.854 0 0 0 0.11352,0.79686 l 1.88652,-2.24615 z\"/></g><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.73127\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m -7.1743772,20.405591 6.27449192,1.61962 c 0,0 2.28297208,-1.746 3.01734048,-3.303819 0.6250483,-1.325918 1.2501011,-3.541343 1.2501011,-3.541343 0,0 4.9303549,-3.331849 4.990109,-4.64706 C 8.4038939,9.51548 7.4523448,8.44669 6.489279,8.115078 5.4553799,7.759076 -0.61569418,12.732235 -0.61569418,12.732235 L 1.7109732,10.62309 1.7633602,4.158494 c 0,0 -2.9599019,-0.1661 -5.3375374,2.108198 -1.407702,1.34652 -3.6002,14.138899 -3.6002,14.138899 z\"/><path fill=\"none\" stroke=\"currentColor\" stroke-width=\"1.73127\" stroke-linecap=\"round\" stroke-linejoin=\"round\" d=\"m 40.685765,20.076393 -6.274492,1.61962 c 0,0 -2.282972,-1.746 -3.01734,-3.303819 -0.625049,-1.325918 -1.250101,-3.541343 -1.250101,-3.541343 0,0 -4.930355,-3.331849 -4.990109,-4.64706 -0.04623,-1.017509 0.90532,-2.086299 1.868386,-2.417911 1.033899,-0.356002 7.104973,4.617157 7.104973,4.617157 l -2.326667,-2.109145 -0.05239,-6.464596 c 0,0 2.959902,-0.1661 5.337537,2.108198 1.407702,1.34652 3.6002,14.138899 3.6002,14.138899 z\"/>" + (light === "move" ? thumbDot(8, 8) : "") + (light === "look" ? thumbDot(26, 8) : "") + `</svg>`;
|
|
647
|
+
const PHONE = {
|
|
648
|
+
rotate: PHONE_ROTATE,
|
|
649
|
+
hold: grabPhone(),
|
|
650
|
+
move: grabPhone("move"),
|
|
651
|
+
look: grabPhone("look")
|
|
652
|
+
};
|
|
653
|
+
const MOVE_STICK = "<svg viewBox=\"0 0 100 100\" width=\"100%\" height=\"100%\" role=\"img\" aria-hidden=\"true\"><circle cx=\"50\" cy=\"50\" r=\"30\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"6\"/><g class=\"text-primary\" fill=\"currentColor\"><circle cx=\"50\" cy=\"50\" r=\"12\"/></g><g fill=\"currentColor\"><path d=\"M50 12 l-7 10 h14 z\"/><path d=\"M50 88 l-7 -10 h14 z\"/><path d=\"M12 50 l10 -7 v14 z\"/><path d=\"M88 50 l-10 -7 v14 z\"/></g>" + CLOSE;
|
|
654
|
+
const KEY = 152;
|
|
655
|
+
const KEY_HALF = KEY / 2;
|
|
656
|
+
const KEY_UP = [262, 82];
|
|
657
|
+
const KEY_LEFT = [82, 259];
|
|
658
|
+
const KEY_DOWN = [262, 259];
|
|
659
|
+
const KEY_RIGHT = [442, 259];
|
|
660
|
+
const keyRect = ([cx, cy]) => `<rect x="${cx - KEY_HALF}" y="${cy - KEY_HALF}" width="${KEY}" height="${KEY}" rx="24" fill="none" stroke="currentColor" stroke-width="11"/>`;
|
|
661
|
+
const keyLetter = ([cx, cy], ch) => `<text x="${cx}" y="${cy}" font-family="system-ui, sans-serif" font-weight="800" font-size="96" text-anchor="middle" dominant-baseline="central" fill="currentColor" stroke="none">${ch}</text>`;
|
|
662
|
+
const keyArrow = ([cx, cy], dir) => {
|
|
663
|
+
const L = 42;
|
|
664
|
+
const H = 20;
|
|
665
|
+
const line = (d) => `<path d="${d}" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
666
|
+
switch (dir) {
|
|
667
|
+
case "up": return line(`M${cx} ${cy + L} V${cy - L}`) + line(`M${cx - H} ${cy - L + H} L${cx} ${cy - L} L${cx + H} ${cy - L + H}`);
|
|
668
|
+
case "down": return line(`M${cx} ${cy - L} V${cy + L}`) + line(`M${cx - H} ${cy + L - H} L${cx} ${cy + L} L${cx + H} ${cy + L - H}`);
|
|
669
|
+
case "left": return line(`M${cx + L} ${cy} H${cx - L}`) + line(`M${cx - L + H} ${cy - H} L${cx - L} ${cy} L${cx - L + H} ${cy + H}`);
|
|
670
|
+
default: return line(`M${cx - L} ${cy} H${cx + L}`) + line(`M${cx + L - H} ${cy - H} L${cx + L} ${cy} L${cx + L - H} ${cy + H}`);
|
|
671
|
+
}
|
|
672
|
+
};
|
|
673
|
+
const AWSD_LAYER = keyRect(KEY_UP) + keyLetter(KEY_UP, "W") + keyRect(KEY_LEFT) + keyLetter(KEY_LEFT, "A") + keyRect(KEY_DOWN) + keyLetter(KEY_DOWN, "S") + keyRect(KEY_RIGHT) + keyLetter(KEY_RIGHT, "D");
|
|
674
|
+
const KEYS = `<svg viewBox="0 0 524 741" width="100%" height="100%" role="img" aria-hidden="true"><g transform="translate(0 400)">${keyRect(KEY_UP) + keyArrow(KEY_UP, "up") + keyRect(KEY_LEFT) + keyArrow(KEY_LEFT, "left") + keyRect(KEY_DOWN) + keyArrow(KEY_DOWN, "down") + keyRect(KEY_RIGHT) + keyArrow(KEY_RIGHT, "right")}</g><g>${AWSD_LAYER}</g>` + CLOSE;
|
|
675
|
+
const MOUSE_BODY = "M8.5 0c-3.033 0-5.5 2.467-5.5 5.5v6c0 3.033 2.467 5.5 5.5 5.5s5.5-2.467 5.5-5.5v-6c0-3.033-2.467-5.5-5.5-5.5zM13 5.5v0.5h-4v-4.949c2.244 0.252 4 2.139 4 4.449zM8 1.051v4.949h-4v-0.5c0-2.31 1.756-4.197 4-4.449zM8.5 16c-2.481 0-4.5-2.019-4.5-4.5v-4.5h9v4.5c0 2.481-2.019 4.5-4.5 4.5z";
|
|
676
|
+
const MOUSE_LEFT_BTN = `M8 1.051v4.949h-4v-0.5c0-2.31 1.756-4.197 4-4.449z`;
|
|
677
|
+
const MOUSE_RIGHT_BTN = `M13 5.5v0.5h-4v-4.949c2.244 0.252 4 2.139 4 4.449z`;
|
|
678
|
+
const mouse = (light) => `<svg viewBox="0 0 17 17" width="100%" height="100%" role="img" aria-hidden="true">` + (light === "left" ? `<g class="text-primary" fill="currentColor"><path d="${MOUSE_LEFT_BTN}"/></g>` : "") + (light === "right" ? `<g class="text-primary" fill="currentColor"><path d="${MOUSE_RIGHT_BTN}"/></g>` : "") + `<path fill="currentColor" fill-rule="evenodd" d="${MOUSE_BODY}"/>` + CLOSE;
|
|
679
|
+
function figureFrom(svg) {
|
|
680
|
+
const figure = document.createElement("div");
|
|
681
|
+
figure.className = "text-base-content/70 h-16 w-16 shrink-0";
|
|
682
|
+
figure.innerHTML = svg;
|
|
683
|
+
return figure;
|
|
684
|
+
}
|
|
685
|
+
/**
|
|
686
|
+
* Icon factories for a tab whose pane is split into per-control sections, in the
|
|
687
|
+
* order its lines document them; `null` for single-diagram tabs. Each factory
|
|
688
|
+
* makes a fresh node (a DOM node can only live in one place).
|
|
689
|
+
*/
|
|
690
|
+
function sectionFigures(kind) {
|
|
691
|
+
switch (kind) {
|
|
692
|
+
case "touch": return [
|
|
693
|
+
"rotate",
|
|
694
|
+
"hold",
|
|
695
|
+
"move",
|
|
696
|
+
"look"
|
|
697
|
+
].map((step) => () => figureFrom(PHONE[step]));
|
|
698
|
+
case "vr": return [() => figureFrom(PHONE.rotate), () => figureFrom(MOVE_STICK)];
|
|
699
|
+
case "keyboard-mouse": return [
|
|
700
|
+
() => figureFrom(KEYS),
|
|
701
|
+
() => figureFrom(mouse("left")),
|
|
702
|
+
() => figureFrom(mouse("right"))
|
|
703
|
+
];
|
|
704
|
+
case "gamepad": return [
|
|
705
|
+
"move",
|
|
706
|
+
"look",
|
|
707
|
+
"select"
|
|
708
|
+
].map((h) => () => figureFrom(gamepadSvg(h)));
|
|
709
|
+
default: return null;
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
//#endregion
|
|
713
|
+
//#region src/components/help/modality.ts
|
|
714
|
+
/** Map a modality to the help tab that documents it. */
|
|
715
|
+
function modalityTab(modality) {
|
|
716
|
+
return modality;
|
|
717
|
+
}
|
|
718
|
+
function createModalityTracker() {
|
|
719
|
+
let last = typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches ? "touch" : "keyboard-mouse";
|
|
720
|
+
const onTouch = () => {
|
|
721
|
+
last = "touch";
|
|
722
|
+
};
|
|
723
|
+
const onPointer = (e) => {
|
|
724
|
+
if (e.pointerType === "touch") return;
|
|
725
|
+
last = "keyboard-mouse";
|
|
726
|
+
};
|
|
727
|
+
const onKey = () => {
|
|
728
|
+
last = "keyboard-mouse";
|
|
729
|
+
};
|
|
730
|
+
const onPad = () => {
|
|
731
|
+
last = "gamepad";
|
|
732
|
+
};
|
|
733
|
+
window.addEventListener("touchstart", onTouch, { passive: true });
|
|
734
|
+
window.addEventListener("pointerdown", onPointer, { passive: true });
|
|
735
|
+
window.addEventListener("keydown", onKey);
|
|
736
|
+
window.addEventListener("gamepadconnected", onPad);
|
|
737
|
+
return {
|
|
738
|
+
get: () => last,
|
|
739
|
+
destroy: () => {
|
|
740
|
+
window.removeEventListener("touchstart", onTouch);
|
|
741
|
+
window.removeEventListener("pointerdown", onPointer);
|
|
742
|
+
window.removeEventListener("keydown", onKey);
|
|
743
|
+
window.removeEventListener("gamepadconnected", onPad);
|
|
744
|
+
}
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
//#endregion
|
|
748
|
+
//#region src/components/help/help.ts
|
|
749
|
+
/**
|
|
750
|
+
* Build a help tab's pane body and return the text nodes to relocalize. Most tabs
|
|
751
|
+
* are one diagram above a bullet list. Touch and gamepad are split into sections:
|
|
752
|
+
* one icon per control (with just that part lit) next to its instruction, so the
|
|
753
|
+
* visitor sees which part does what; any extra lines (e.g. "connect a controller")
|
|
754
|
+
* stay as notes below.
|
|
755
|
+
*/
|
|
756
|
+
function buildPane(pane, tab) {
|
|
757
|
+
const figures = sectionFigures(tab.kind);
|
|
758
|
+
if (figures) return buildSectionedPane(pane, tab.lines, figures);
|
|
759
|
+
pane.appendChild(diagramNode(tab.kind));
|
|
760
|
+
const list = document.createElement(tab.ordered ? "ol" : "ul");
|
|
761
|
+
list.className = `${tab.ordered ? "list-decimal" : "list-disc"} list-outside pl-5 space-y-1.5 text-sm leading-relaxed`;
|
|
762
|
+
const lineEls = tab.lines.map((line) => {
|
|
763
|
+
const el = document.createElement("li");
|
|
764
|
+
list.appendChild(el);
|
|
765
|
+
return {
|
|
766
|
+
el,
|
|
767
|
+
line
|
|
768
|
+
};
|
|
769
|
+
});
|
|
770
|
+
pane.appendChild(list);
|
|
771
|
+
return lineEls;
|
|
772
|
+
}
|
|
773
|
+
function buildSectionedPane(pane, lines, figures) {
|
|
774
|
+
const lineEls = [];
|
|
775
|
+
figures.forEach((makeFigure, i) => {
|
|
776
|
+
const line = lines[i];
|
|
777
|
+
if (!line) return;
|
|
778
|
+
const row = document.createElement("div");
|
|
779
|
+
row.className = "flex items-center gap-4 mb-3";
|
|
780
|
+
row.appendChild(makeFigure());
|
|
781
|
+
const p = document.createElement("p");
|
|
782
|
+
p.className = "text-sm leading-relaxed";
|
|
783
|
+
row.appendChild(p);
|
|
784
|
+
pane.appendChild(row);
|
|
785
|
+
lineEls.push({
|
|
786
|
+
el: p,
|
|
787
|
+
line
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
const notes = lines.slice(figures.length);
|
|
791
|
+
if (notes.length > 0) {
|
|
792
|
+
const list = document.createElement("ul");
|
|
793
|
+
list.className = "list-disc list-outside pl-5 space-y-1.5 text-sm leading-relaxed text-base-content/70";
|
|
794
|
+
for (const line of notes) {
|
|
795
|
+
const el = document.createElement("li");
|
|
796
|
+
list.appendChild(el);
|
|
797
|
+
lineEls.push({
|
|
798
|
+
el,
|
|
799
|
+
line
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
pane.appendChild(list);
|
|
803
|
+
}
|
|
804
|
+
return lineEls;
|
|
805
|
+
}
|
|
806
|
+
let instanceCount = 0;
|
|
807
|
+
/**
|
|
808
|
+
* A visual controls-help panel: an ⓘ button opening a centered daisyUI modal
|
|
809
|
+
* with one tab per input mode, each showing an SVG diagram and a few localized
|
|
810
|
+
* lines. On open it pre-selects the tab for the mode you're using. Diagrams are
|
|
811
|
+
* built in (standard schemes); the text is app-supplied `Text`, relocalized live.
|
|
812
|
+
*/
|
|
813
|
+
function helpPanel(options) {
|
|
814
|
+
const autoSelect = options.autoSelect ?? true;
|
|
815
|
+
const groupName = `sg-help-tabs-${instanceCount += 1}`;
|
|
816
|
+
return (ctx) => {
|
|
817
|
+
const wrap = document.createElement("div");
|
|
818
|
+
const button = document.createElement("button");
|
|
819
|
+
button.type = "button";
|
|
820
|
+
button.className = "btn btn-circle btn-sm";
|
|
821
|
+
setIcon(button, lucideIcon("Info"));
|
|
822
|
+
const dialog = document.createElement("dialog");
|
|
823
|
+
dialog.className = "modal";
|
|
824
|
+
const box = document.createElement("div");
|
|
825
|
+
box.className = "modal-box max-w-md";
|
|
826
|
+
const close = document.createElement("button");
|
|
827
|
+
close.type = "button";
|
|
828
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
829
|
+
close.textContent = "✕";
|
|
830
|
+
box.appendChild(close);
|
|
831
|
+
const heading = document.createElement("h3");
|
|
832
|
+
heading.className = "text-lg font-bold mb-4";
|
|
833
|
+
box.appendChild(heading);
|
|
834
|
+
const tablist = document.createElement("div");
|
|
835
|
+
tablist.setAttribute("role", "tablist");
|
|
836
|
+
tablist.className = "tabs tabs-border";
|
|
837
|
+
box.appendChild(tablist);
|
|
838
|
+
const relabels = [];
|
|
839
|
+
const tabInputs = /* @__PURE__ */ new Map();
|
|
840
|
+
let firstInput = null;
|
|
841
|
+
for (const tab of options.tabs) {
|
|
842
|
+
const input = document.createElement("input");
|
|
843
|
+
input.type = "radio";
|
|
844
|
+
input.name = groupName;
|
|
845
|
+
input.role = "tab";
|
|
846
|
+
input.className = "tab";
|
|
847
|
+
if (!firstInput) {
|
|
848
|
+
input.checked = true;
|
|
849
|
+
firstInput = input;
|
|
850
|
+
}
|
|
851
|
+
const pane = document.createElement("div");
|
|
852
|
+
pane.className = "tab-content pt-4";
|
|
853
|
+
const lineEls = buildPane(pane, tab);
|
|
854
|
+
tablist.append(input, pane);
|
|
855
|
+
tabInputs.set(tab.kind, input);
|
|
856
|
+
relabels.push(() => {
|
|
857
|
+
input.setAttribute("aria-label", resolveText(tab.title));
|
|
858
|
+
for (const { el, line } of lineEls) el.textContent = resolveText(line);
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
const backdrop = document.createElement("form");
|
|
862
|
+
backdrop.method = "dialog";
|
|
863
|
+
backdrop.className = "modal-backdrop";
|
|
864
|
+
const backdropBtn = document.createElement("button");
|
|
865
|
+
backdropBtn.textContent = "close";
|
|
866
|
+
backdrop.appendChild(backdropBtn);
|
|
867
|
+
dialog.append(box, backdrop);
|
|
868
|
+
wrap.append(button, dialog);
|
|
869
|
+
const tracker = createModalityTracker();
|
|
870
|
+
const unbind = bindLocale(ctx, () => {
|
|
871
|
+
const title = resolveText(options.title);
|
|
872
|
+
button.setAttribute("aria-label", title);
|
|
873
|
+
heading.textContent = title;
|
|
874
|
+
close.setAttribute("aria-label", resolveText(options.closeLabel ?? "Close"));
|
|
875
|
+
for (const relabel of relabels) relabel();
|
|
876
|
+
});
|
|
877
|
+
const onOpen = () => {
|
|
878
|
+
if (autoSelect) {
|
|
879
|
+
const mode = ctx.gallery.ctx.viewControls.getMode();
|
|
880
|
+
const kind = mode === "cardboard" ? "cardboard" : mode === "vr" ? "vr" : modalityTab(tracker.get());
|
|
881
|
+
const target = tabInputs.get(kind) ?? firstInput;
|
|
882
|
+
if (target) target.checked = true;
|
|
883
|
+
}
|
|
884
|
+
dialog.showModal();
|
|
885
|
+
};
|
|
886
|
+
const onClose = () => dialog.close();
|
|
887
|
+
button.addEventListener("click", onOpen);
|
|
888
|
+
close.addEventListener("click", onClose);
|
|
889
|
+
return {
|
|
890
|
+
element: wrap,
|
|
891
|
+
destroy: () => {
|
|
892
|
+
button.removeEventListener("click", onOpen);
|
|
893
|
+
close.removeEventListener("click", onClose);
|
|
894
|
+
tracker.destroy();
|
|
895
|
+
unbind();
|
|
896
|
+
}
|
|
897
|
+
};
|
|
898
|
+
};
|
|
899
|
+
}
|
|
900
|
+
//#endregion
|
|
465
901
|
//#region src/components/artwork-info/artwork-info.ts
|
|
466
902
|
const AVAILABILITY_COLOR = {
|
|
467
903
|
available: "#22c55e",
|
|
@@ -480,7 +916,7 @@ function ensureStyles() {
|
|
|
480
916
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
481
917
|
const style = document.createElement("style");
|
|
482
918
|
style.id = STYLE_ID;
|
|
483
|
-
style.textContent = `.sg-artwork-panel{position:absolute;left:0;top:0;height:100%;width:${PANEL_FRACTION * 100}%;box-sizing:border-box;background:rgba(0,0,0,0.85);color:#fff;padding:2.5rem;z-index:10;overflow-y:auto;display:none;flex-direction:column;gap:0.75rem;pointer-events:auto;font-family:system-ui,sans-serif;}.sg-artwork-panel--open{display:flex;}.sg-artwork-panel__close{align-self:flex-end;background:none;border:none;color:#fff;font-size:1.5rem;line-height:1;cursor:pointer;opacity:0.7;padding:0;}.sg-artwork-panel__close:hover{opacity:1;}.sg-artwork-panel__title{font-size:1.75rem;font-weight:700;margin:0;}.sg-artwork-panel__meta{display:flex;flex-direction:column;gap:0.15rem;font-size:0.9rem;opacity:0.8;}.sg-artwork-panel__status{display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;font-weight:600;}.sg-artwork-panel__dot{width:0.7rem;height:0.7rem;border-radius:50%;flex:none;}.sg-artwork-panel__desc{font-size:1rem;line-height:1.6;margin-top:0.5rem;white-space:pre-line;}.sg-artwork-panel__link{align-self:flex-start;margin-top:0.5rem;display:inline-flex;align-items:center;gap:0.4rem;color:#fff;text-decoration:underline;font-size:0.95rem;}.sg-artwork-panel__link:hover{opacity:0.8;}`;
|
|
919
|
+
style.textContent = `.sg-artwork-panel{position:absolute;left:0;top:0;height:100%;width:${PANEL_FRACTION * 100}%;box-sizing:border-box;background:rgba(0,0,0,0.85);color:#fff;padding:2.5rem;z-index:10;overflow-y:auto;display:none;flex-direction:column;gap:0.75rem;pointer-events:auto;font-family:system-ui,sans-serif;}.sg-artwork-panel--open{display:flex;}.sg-artwork-panel__close{align-self:flex-end;background:none;border:none;color:#fff;font-size:1.5rem;line-height:1;cursor:pointer;opacity:0.7;padding:0;}.sg-artwork-panel__close:hover{opacity:1;}.sg-artwork-panel__title{font-size:1.75rem;font-weight:700;margin:0;}.sg-artwork-panel__meta{display:flex;flex-direction:column;gap:0.15rem;font-size:0.9rem;opacity:0.8;}.sg-artwork-panel__status{display:flex;align-items:center;gap:0.5rem;font-size:0.9rem;font-weight:600;}.sg-artwork-panel__dot{width:0.7rem;height:0.7rem;border-radius:50%;flex:none;}.sg-artwork-panel__desc{font-size:1rem;line-height:1.6;margin-top:0.5rem;white-space:pre-line;}.sg-artwork-panel__link{align-self:flex-start;margin-top:0.5rem;display:inline-flex;align-items:center;gap:0.4rem;color:#fff;text-decoration:underline;font-size:0.95rem;}.sg-artwork-panel__link:hover{opacity:0.8;}.sg-artwork-panel__nav{display:flex;gap:0.75rem;margin-top:auto;padding-top:1.5rem;}.sg-artwork-panel__navbtn{flex:1;display:inline-flex;align-items:center;justify-content:center;gap:0.4rem;background:rgba(255,255,255,0.1);border:1px solid rgba(255,255,255,0.25);color:#fff;border-radius:0.5rem;padding:0.6rem 0.8rem;font-size:0.95rem;cursor:pointer;font-family:inherit;}.sg-artwork-panel__navbtn:hover{background:rgba(255,255,255,0.2);}`;
|
|
484
920
|
document.head.appendChild(style);
|
|
485
921
|
}
|
|
486
922
|
const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
|
|
@@ -488,15 +924,19 @@ const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
|
|
|
488
924
|
* The artwork info panel. The engine emits `artwork:select { id }` when you click
|
|
489
925
|
* a piece's cartela (or the piece itself); this opens a left-hand panel (40%
|
|
490
926
|
* width, full height, black at 85% alpha) with the full caption and asks the
|
|
491
|
-
* camera to frame the piece in the free right 60%.
|
|
492
|
-
*
|
|
493
|
-
*
|
|
927
|
+
* camera to frame the piece in the free right 60%. Prev/next buttons at the
|
|
928
|
+
* bottom cycle through the pieces (wrapping) without leaving the panel. Closing
|
|
929
|
+
* with ✕ re-centers the piece full-width, pulling back to `minCloseDistance` when
|
|
930
|
+
* the framing would otherwise be too close; if the user walks/turns away the
|
|
931
|
+
* engine emits `artwork:released` and the panel just closes (no camera move).
|
|
494
932
|
*
|
|
495
933
|
* Content is data (`artworks`), unlocalized for now. The panel is mounted on the
|
|
496
934
|
* surface (escaping the overlay grid); the slot element is an inert placeholder.
|
|
497
935
|
*/
|
|
498
936
|
function artworkInfo(options) {
|
|
499
|
-
const
|
|
937
|
+
const list = options.artworks;
|
|
938
|
+
const byId = new Map(list.map((a) => [a.id, a]));
|
|
939
|
+
const indexOf = new Map(list.map((a, i) => [a.id, i]));
|
|
500
940
|
return (ctx) => {
|
|
501
941
|
ensureStyles();
|
|
502
942
|
const camera = ctx.gallery.ctx.camera;
|
|
@@ -524,7 +964,23 @@ function artworkInfo(options) {
|
|
|
524
964
|
link.target = "_blank";
|
|
525
965
|
link.rel = "noopener noreferrer";
|
|
526
966
|
link.textContent = `${options.linkText ?? "View artwork"} ↗`;
|
|
527
|
-
|
|
967
|
+
const nav = document.createElement("div");
|
|
968
|
+
nav.className = "sg-artwork-panel__nav";
|
|
969
|
+
const prevBtn = document.createElement("button");
|
|
970
|
+
prevBtn.type = "button";
|
|
971
|
+
prevBtn.className = "sg-artwork-panel__navbtn";
|
|
972
|
+
const prevText = options.prevLabel ?? "Previous";
|
|
973
|
+
prevBtn.textContent = `‹ ${prevText}`;
|
|
974
|
+
prevBtn.setAttribute("aria-label", prevText);
|
|
975
|
+
const nextBtn = document.createElement("button");
|
|
976
|
+
nextBtn.type = "button";
|
|
977
|
+
nextBtn.className = "sg-artwork-panel__navbtn";
|
|
978
|
+
const nextText = options.nextLabel ?? "Next";
|
|
979
|
+
nextBtn.textContent = `${nextText} ›`;
|
|
980
|
+
nextBtn.setAttribute("aria-label", nextText);
|
|
981
|
+
nav.append(prevBtn, nextBtn);
|
|
982
|
+
if (list.length <= 1) nav.style.display = "none";
|
|
983
|
+
panel.append(close, title, meta, status, desc, link, nav);
|
|
528
984
|
ctx.surface.appendChild(panel);
|
|
529
985
|
let currentId = null;
|
|
530
986
|
const render = (artwork) => {
|
|
@@ -553,18 +1009,29 @@ function artworkInfo(options) {
|
|
|
553
1009
|
panel.classList.remove("sg-artwork-panel--open");
|
|
554
1010
|
currentId = null;
|
|
555
1011
|
};
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
|
|
559
|
-
if (!artwork || !id) return;
|
|
1012
|
+
const show = (id) => {
|
|
1013
|
+
const artwork = byId.get(id);
|
|
1014
|
+
if (!artwork) return;
|
|
560
1015
|
currentId = id;
|
|
561
1016
|
render(artwork);
|
|
1017
|
+
panel.scrollTop = 0;
|
|
562
1018
|
panel.classList.add("sg-artwork-panel--open");
|
|
563
1019
|
camera.focusArtwork(id, {
|
|
564
1020
|
centerX: CENTER_X,
|
|
565
1021
|
widthFraction: FREE_FRACTION
|
|
566
1022
|
});
|
|
567
1023
|
};
|
|
1024
|
+
const step = (delta) => {
|
|
1025
|
+
if (currentId === null || list.length <= 1) return;
|
|
1026
|
+
const i = indexOf.get(currentId);
|
|
1027
|
+
if (i === void 0) return;
|
|
1028
|
+
const next = list[(i + delta + list.length) % list.length];
|
|
1029
|
+
if (next) show(next.id);
|
|
1030
|
+
};
|
|
1031
|
+
const onSelect = (payload) => {
|
|
1032
|
+
const id = payload?.id;
|
|
1033
|
+
if (id) show(id);
|
|
1034
|
+
};
|
|
568
1035
|
const onReleased = (payload) => {
|
|
569
1036
|
const id = payload?.id;
|
|
570
1037
|
if (id && id === currentId) hide();
|
|
@@ -572,11 +1039,15 @@ function artworkInfo(options) {
|
|
|
572
1039
|
const onClose = () => {
|
|
573
1040
|
const id = currentId;
|
|
574
1041
|
hide();
|
|
575
|
-
if (id) camera.centerArtwork(id);
|
|
1042
|
+
if (id) camera.centerArtwork(id, { minDistance: options.minCloseDistance ?? 0 });
|
|
576
1043
|
};
|
|
1044
|
+
const onPrev = () => step(-1);
|
|
1045
|
+
const onNext = () => step(1);
|
|
577
1046
|
const offSelect = ctx.gallery.on("artwork:select", onSelect);
|
|
578
1047
|
const offReleased = ctx.gallery.on("artwork:released", onReleased);
|
|
579
1048
|
close.addEventListener("click", onClose);
|
|
1049
|
+
prevBtn.addEventListener("click", onPrev);
|
|
1050
|
+
nextBtn.addEventListener("click", onNext);
|
|
580
1051
|
const placeholder = document.createElement("div");
|
|
581
1052
|
placeholder.style.display = "none";
|
|
582
1053
|
return {
|
|
@@ -585,6 +1056,8 @@ function artworkInfo(options) {
|
|
|
585
1056
|
offSelect();
|
|
586
1057
|
offReleased();
|
|
587
1058
|
close.removeEventListener("click", onClose);
|
|
1059
|
+
prevBtn.removeEventListener("click", onPrev);
|
|
1060
|
+
nextBtn.removeEventListener("click", onNext);
|
|
588
1061
|
panel.remove();
|
|
589
1062
|
}
|
|
590
1063
|
};
|
|
@@ -684,39 +1157,83 @@ function settingsPanel(options) {
|
|
|
684
1157
|
field.append(label, select);
|
|
685
1158
|
addTab(lang.tab, lang.label, field);
|
|
686
1159
|
}
|
|
687
|
-
if (options.handedness) {
|
|
688
|
-
const hand = options.handedness;
|
|
1160
|
+
if (options.handedness || options.controls) {
|
|
689
1161
|
const pane = document.createElement("div");
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
const
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
1162
|
+
if (options.handedness) {
|
|
1163
|
+
const hand = options.handedness;
|
|
1164
|
+
const legend = document.createElement("div");
|
|
1165
|
+
legend.className = "label-text mb-1 block";
|
|
1166
|
+
pane.appendChild(legend);
|
|
1167
|
+
const onChange = (e) => hand.onChange(e.target.value);
|
|
1168
|
+
const texts = [];
|
|
1169
|
+
for (const opt of hand.options) {
|
|
1170
|
+
const row = document.createElement("label");
|
|
1171
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
1172
|
+
const input = document.createElement("input");
|
|
1173
|
+
input.type = "radio";
|
|
1174
|
+
input.name = "sg-settings-handedness";
|
|
1175
|
+
input.value = opt.value;
|
|
1176
|
+
input.className = "radio radio-sm";
|
|
1177
|
+
input.checked = opt.value === hand.value;
|
|
1178
|
+
input.addEventListener("change", onChange);
|
|
1179
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
1180
|
+
const text = document.createElement("span");
|
|
1181
|
+
text.className = "label-text text-sm";
|
|
1182
|
+
row.append(input, text);
|
|
1183
|
+
pane.appendChild(row);
|
|
1184
|
+
texts.push({
|
|
1185
|
+
el: text,
|
|
1186
|
+
label: opt.label
|
|
1187
|
+
});
|
|
1188
|
+
}
|
|
1189
|
+
relabels.push(() => {
|
|
1190
|
+
legend.textContent = resolveText(hand.label);
|
|
1191
|
+
for (const { el, label } of texts) el.textContent = resolveText(label);
|
|
713
1192
|
});
|
|
714
1193
|
}
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
1194
|
+
if (options.controls) {
|
|
1195
|
+
const cfg = options.controls;
|
|
1196
|
+
const viewControls = ctx.gallery.ctx.viewControls;
|
|
1197
|
+
const legend = document.createElement("div");
|
|
1198
|
+
legend.className = `label-text mb-1 block${options.handedness ? " mt-3" : ""}`;
|
|
1199
|
+
pane.appendChild(legend);
|
|
1200
|
+
const list = document.createElement("div");
|
|
1201
|
+
list.className = "flex flex-col gap-1";
|
|
1202
|
+
pane.appendChild(list);
|
|
1203
|
+
const rows = [];
|
|
1204
|
+
for (const item of cfg.items) {
|
|
1205
|
+
const row = document.createElement("label");
|
|
1206
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
1207
|
+
const input = document.createElement("input");
|
|
1208
|
+
input.type = "checkbox";
|
|
1209
|
+
input.className = "toggle toggle-sm";
|
|
1210
|
+
input.checked = viewControls.get()[item.key];
|
|
1211
|
+
const onChange = () => viewControls.setFlag(item.key, input.checked);
|
|
1212
|
+
input.addEventListener("change", onChange);
|
|
1213
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
1214
|
+
const text = document.createElement("span");
|
|
1215
|
+
text.className = "label-text text-sm";
|
|
1216
|
+
row.append(input, text);
|
|
1217
|
+
list.appendChild(row);
|
|
1218
|
+
rows.push({
|
|
1219
|
+
input,
|
|
1220
|
+
key: item.key,
|
|
1221
|
+
el: text,
|
|
1222
|
+
label: item.label
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
const sync = () => {
|
|
1226
|
+
const flags = viewControls.get();
|
|
1227
|
+
for (const { input, key } of rows) input.checked = flags[key];
|
|
1228
|
+
};
|
|
1229
|
+
cleanups.push(ctx.gallery.on("controls:change", sync));
|
|
1230
|
+
relabels.push(() => {
|
|
1231
|
+
legend.textContent = resolveText(cfg.label);
|
|
1232
|
+
for (const { el, label } of rows) el.textContent = resolveText(label);
|
|
1233
|
+
});
|
|
1234
|
+
}
|
|
1235
|
+
const section = options.handedness ?? options.controls;
|
|
1236
|
+
addTab(section.tab, section.label, pane);
|
|
720
1237
|
}
|
|
721
1238
|
if (options.graphics) {
|
|
722
1239
|
const gfx = options.graphics;
|
|
@@ -869,14 +1386,30 @@ function defaultPreset(overlay, options = {}) {
|
|
|
869
1386
|
enter: lucideIcon("Maximize"),
|
|
870
1387
|
exit: lucideIcon("Minimize")
|
|
871
1388
|
} }));
|
|
1389
|
+
if (options.resetView) overlay.add("top-right", resetViewButton({
|
|
1390
|
+
icon: lucideIcon("House"),
|
|
1391
|
+
...typeof options.resetView === "object" ? options.resetView : {}
|
|
1392
|
+
}));
|
|
1393
|
+
if (options.viewMode) {
|
|
1394
|
+
const supported = availableModes(detectControlCapabilities());
|
|
1395
|
+
const modes = options.viewMode.modes.filter((m) => supported.includes(m.mode));
|
|
1396
|
+
if (modes.length >= 2) overlay.add("top-right", viewModeButton({
|
|
1397
|
+
...options.viewMode,
|
|
1398
|
+
modes
|
|
1399
|
+
}));
|
|
1400
|
+
}
|
|
872
1401
|
if (options.settings) overlay.add("top-right", settingsPanel(options.settings));
|
|
873
1402
|
if (options.environment) overlay.add("top-right", environmentPanel({
|
|
874
1403
|
icon: lucideIcon("CloudSun"),
|
|
875
1404
|
...options.environment
|
|
876
1405
|
}));
|
|
877
|
-
overlay.add("top-right", cardboardButton({
|
|
1406
|
+
if (options.cardboard) overlay.add("top-right", cardboardButton({
|
|
1407
|
+
icon: lucideIcon("RectangleGoggles"),
|
|
1408
|
+
...typeof options.cardboard === "object" ? options.cardboard : {}
|
|
1409
|
+
}));
|
|
878
1410
|
if (options.artworks) overlay.add("middle-left", artworkInfo(options.artworks));
|
|
879
|
-
if (options.
|
|
1411
|
+
if (options.help) overlay.add("bottom-right", helpPanel(options.help));
|
|
1412
|
+
else if (options.info) overlay.add("bottom-right", infoPanel(options.info));
|
|
880
1413
|
if (options.credits) overlay.add("bottom-right", creditsPanel({
|
|
881
1414
|
items: options.credits,
|
|
882
1415
|
title: options.creditsTitle,
|
|
@@ -885,6 +1418,6 @@ function defaultPreset(overlay, options = {}) {
|
|
|
885
1418
|
if (options.watermark !== false) overlay.add("bottom-left", watermark(options.watermark ?? {}));
|
|
886
1419
|
}
|
|
887
1420
|
//#endregion
|
|
888
|
-
export { actionButton, artworkInfo, cardboardButton, creditsPanel, defaultPreset, environmentPanel, fullscreenButton, infoPanel, lucideIcon, muteButton, settingsPanel, watermark };
|
|
1421
|
+
export { actionButton, artworkInfo, availableModes, cardboardButton, creditsPanel, defaultPreset, detectControlCapabilities, environmentPanel, fullscreenButton, helpPanel, infoPanel, lucideIcon, muteButton, resetViewButton, settingsPanel, viewModeButton, watermark };
|
|
889
1422
|
|
|
890
1423
|
//# sourceMappingURL=index.mjs.map
|