@scenography/overlay-presets 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +584 -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 +580 -44
- package/dist/index.mjs.map +1 -1
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -133,6 +133,7 @@ function cardboardButton(options = {}) {
|
|
|
133
133
|
ctx.gallery.setRender({ cardboard: enabled });
|
|
134
134
|
button.classList.toggle("btn-active", enabled);
|
|
135
135
|
button.setAttribute("aria-pressed", String(enabled));
|
|
136
|
+
ctx.gallery.emit("cardboard:change", { enabled });
|
|
136
137
|
};
|
|
137
138
|
button.addEventListener("click", onClick);
|
|
138
139
|
return {
|
|
@@ -142,6 +143,161 @@ function cardboardButton(options = {}) {
|
|
|
142
143
|
};
|
|
143
144
|
}
|
|
144
145
|
//#endregion
|
|
146
|
+
//#region src/components/reset-view/reset-view.ts
|
|
147
|
+
/**
|
|
148
|
+
* A "home" button that returns the camera to the scene's start position and
|
|
149
|
+
* orientation (`ctx.camera.reset()`) — for visitors who wander off and want to
|
|
150
|
+
* get back to the entrance. Defaults to a plain glyph; pass `lucideIcon("House")`.
|
|
151
|
+
*/
|
|
152
|
+
function resetViewButton(options = {}) {
|
|
153
|
+
const icon = options.icon ?? "⌂";
|
|
154
|
+
return (ctx) => {
|
|
155
|
+
const button = document.createElement("button");
|
|
156
|
+
button.type = "button";
|
|
157
|
+
button.className = options.className ?? "btn btn-circle btn-sm";
|
|
158
|
+
setIcon(button, icon);
|
|
159
|
+
const onClick = () => ctx.gallery.ctx.camera.reset();
|
|
160
|
+
button.addEventListener("click", onClick);
|
|
161
|
+
const unbind = bindLocale(ctx, () => button.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.label ?? "Reset view")));
|
|
162
|
+
return {
|
|
163
|
+
element: button,
|
|
164
|
+
destroy: () => {
|
|
165
|
+
button.removeEventListener("click", onClick);
|
|
166
|
+
unbind();
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
//#endregion
|
|
172
|
+
//#region src/components/view-mode/view-mode.ts
|
|
173
|
+
/** Lucide icon per mode, shown beside its label in the picker. */
|
|
174
|
+
const MODE_ICON = {
|
|
175
|
+
touch: "Hand",
|
|
176
|
+
vr: "Orbit",
|
|
177
|
+
cardboard: "RectangleGoggles"
|
|
178
|
+
};
|
|
179
|
+
/**
|
|
180
|
+
* A camera-control mode picker: a `ScanEye` button opening a small modal with one
|
|
181
|
+
* entry per mode (Touch / VR / Cardboard). Picking one calls
|
|
182
|
+
* `ctx.viewControls.setMode` — which, inside this click gesture, flips the
|
|
183
|
+
* gyroscope and stereo split (so iOS grants motion here). The active mode is
|
|
184
|
+
* marked and kept in sync via the `controls:change` event, so the settings panel
|
|
185
|
+
* and this picker never disagree.
|
|
186
|
+
*/
|
|
187
|
+
function viewModeButton(options) {
|
|
188
|
+
return (ctx) => {
|
|
189
|
+
const viewControls = ctx.gallery.ctx.viewControls;
|
|
190
|
+
const wrap = document.createElement("div");
|
|
191
|
+
const button = document.createElement("button");
|
|
192
|
+
button.type = "button";
|
|
193
|
+
button.className = "btn btn-circle btn-sm";
|
|
194
|
+
setIcon(button, lucideIcon("ScanEye"));
|
|
195
|
+
const dialog = document.createElement("dialog");
|
|
196
|
+
dialog.className = "modal";
|
|
197
|
+
const box = document.createElement("div");
|
|
198
|
+
box.className = "modal-box max-w-xs";
|
|
199
|
+
const close = document.createElement("button");
|
|
200
|
+
close.type = "button";
|
|
201
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
202
|
+
close.textContent = "✕";
|
|
203
|
+
box.appendChild(close);
|
|
204
|
+
const heading = document.createElement("h3");
|
|
205
|
+
heading.className = "text-lg font-bold mb-4";
|
|
206
|
+
box.appendChild(heading);
|
|
207
|
+
const list = document.createElement("div");
|
|
208
|
+
list.className = "flex flex-col gap-2";
|
|
209
|
+
box.appendChild(list);
|
|
210
|
+
const cleanups = [];
|
|
211
|
+
const relabels = [];
|
|
212
|
+
const optionButtons = [];
|
|
213
|
+
for (const item of options.modes) {
|
|
214
|
+
const optBtn = document.createElement("button");
|
|
215
|
+
optBtn.type = "button";
|
|
216
|
+
optBtn.className = "btn btn-block justify-start gap-3";
|
|
217
|
+
const iconEl = document.createElement("span");
|
|
218
|
+
iconEl.style.display = "inline-flex";
|
|
219
|
+
setIcon(iconEl, lucideIcon(MODE_ICON[item.mode]));
|
|
220
|
+
const label = document.createElement("span");
|
|
221
|
+
optBtn.append(iconEl, label);
|
|
222
|
+
const onPick = () => {
|
|
223
|
+
viewControls.setMode(item.mode);
|
|
224
|
+
syncActive();
|
|
225
|
+
dialog.close();
|
|
226
|
+
};
|
|
227
|
+
optBtn.addEventListener("click", onPick);
|
|
228
|
+
cleanups.push(() => optBtn.removeEventListener("click", onPick));
|
|
229
|
+
list.appendChild(optBtn);
|
|
230
|
+
optionButtons.push({
|
|
231
|
+
el: optBtn,
|
|
232
|
+
mode: item.mode
|
|
233
|
+
});
|
|
234
|
+
relabels.push(() => label.textContent = (0, _scenography_overlay.resolveText)(item.label));
|
|
235
|
+
}
|
|
236
|
+
const syncActive = () => {
|
|
237
|
+
const current = viewControls.getMode();
|
|
238
|
+
for (const { el, mode } of optionButtons) el.classList.toggle("btn-active", mode === current);
|
|
239
|
+
};
|
|
240
|
+
const backdrop = document.createElement("form");
|
|
241
|
+
backdrop.method = "dialog";
|
|
242
|
+
backdrop.className = "modal-backdrop";
|
|
243
|
+
const backdropBtn = document.createElement("button");
|
|
244
|
+
backdropBtn.textContent = "close";
|
|
245
|
+
backdrop.appendChild(backdropBtn);
|
|
246
|
+
dialog.append(box, backdrop);
|
|
247
|
+
wrap.append(button, dialog);
|
|
248
|
+
const offChange = ctx.gallery.on("controls:change", syncActive);
|
|
249
|
+
const unbind = bindLocale(ctx, () => {
|
|
250
|
+
const title = (0, _scenography_overlay.resolveText)(options.title);
|
|
251
|
+
button.setAttribute("aria-label", title);
|
|
252
|
+
heading.textContent = title;
|
|
253
|
+
close.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.closeLabel ?? "Close"));
|
|
254
|
+
for (const relabel of relabels) relabel();
|
|
255
|
+
});
|
|
256
|
+
const onOpen = () => {
|
|
257
|
+
syncActive();
|
|
258
|
+
dialog.showModal();
|
|
259
|
+
};
|
|
260
|
+
const onClose = () => dialog.close();
|
|
261
|
+
button.addEventListener("click", onOpen);
|
|
262
|
+
close.addEventListener("click", onClose);
|
|
263
|
+
return {
|
|
264
|
+
element: wrap,
|
|
265
|
+
destroy: () => {
|
|
266
|
+
button.removeEventListener("click", onOpen);
|
|
267
|
+
close.removeEventListener("click", onClose);
|
|
268
|
+
for (const fn of cleanups) fn();
|
|
269
|
+
offChange();
|
|
270
|
+
unbind();
|
|
271
|
+
}
|
|
272
|
+
};
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
//#endregion
|
|
276
|
+
//#region src/components/view-mode/capabilities.ts
|
|
277
|
+
/**
|
|
278
|
+
* Sniff the device's control capabilities. Deliberately conservative: gyro is
|
|
279
|
+
* gated behind a touch (mobile) device too, since desktops expose the
|
|
280
|
+
* `DeviceOrientationEvent` interface but never fire it — pairing it with a coarse
|
|
281
|
+
* pointer avoids offering VR/Cardboard where they can't work.
|
|
282
|
+
*/
|
|
283
|
+
function detectControlCapabilities() {
|
|
284
|
+
return {
|
|
285
|
+
touch: typeof matchMedia !== "undefined" ? matchMedia("(pointer: coarse)").matches : typeof navigator !== "undefined" && (navigator.maxTouchPoints ?? 0) > 0,
|
|
286
|
+
gyro: typeof window !== "undefined" && "DeviceOrientationEvent" in window
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* The modes worth offering on this device, in display order. Touch needs a touch
|
|
291
|
+
* screen; VR and Cardboard additionally need the gyro. Desktop (no touch) gets an
|
|
292
|
+
* empty list — it uses keyboard/mouse and shows no mode picker at all.
|
|
293
|
+
*/
|
|
294
|
+
function availableModes(caps) {
|
|
295
|
+
const modes = [];
|
|
296
|
+
if (caps.touch) modes.push("touch");
|
|
297
|
+
if (caps.touch && caps.gyro) modes.push("vr", "cardboard");
|
|
298
|
+
return modes;
|
|
299
|
+
}
|
|
300
|
+
//#endregion
|
|
145
301
|
//#region src/components/environment/environment.ts
|
|
146
302
|
/**
|
|
147
303
|
* An environment panel: a button (e.g. a sun/cloud icon) opening a daisyUI modal
|
|
@@ -463,6 +619,286 @@ function infoPanel(options) {
|
|
|
463
619
|
};
|
|
464
620
|
}
|
|
465
621
|
//#endregion
|
|
622
|
+
//#region src/components/help/diagrams.ts
|
|
623
|
+
const CLOSE = `</svg>`;
|
|
624
|
+
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 };
|
|
625
|
+
/** Build the diagram figure node for a simple tab kind (a div wrapping the SVG). */
|
|
626
|
+
function diagramNode(kind) {
|
|
627
|
+
const figure = document.createElement("div");
|
|
628
|
+
figure.className = "text-base-content/70 mx-auto mb-3 h-28 w-full max-w-xs";
|
|
629
|
+
figure.innerHTML = DIAGRAMS[kind];
|
|
630
|
+
return figure;
|
|
631
|
+
}
|
|
632
|
+
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\"/>";
|
|
633
|
+
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\"/>";
|
|
634
|
+
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\"/>";
|
|
635
|
+
const GP_BTN_TOP = `<circle cx="353.4" cy="147.8" r="16.6"/>`;
|
|
636
|
+
const GP_BTN_LEFT = `<circle cx="319.5" cy="181.8" r="16.6"/>`;
|
|
637
|
+
const GP_BTN_RIGHT = `<circle cx="387.4" cy="181.8" r="16.6"/>`;
|
|
638
|
+
const GP_BTN_BOTTOM = `<circle cx="353.4" cy="215.7" r="16.6"/>`;
|
|
639
|
+
const lit = (part) => `<g class="text-primary" fill="currentColor">${part}</g>`;
|
|
640
|
+
/** The gamepad icon with only the parts used by `highlight` lit in the accent. */
|
|
641
|
+
function gamepadSvg(highlight) {
|
|
642
|
+
const on = (part, when) => highlight === when ? lit(part) : part;
|
|
643
|
+
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;
|
|
644
|
+
}
|
|
645
|
+
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>";
|
|
646
|
+
const thumbDot = (cx, cy) => `<g class="text-primary" fill="currentColor"><circle cx="${cx}" cy="${cy}" r="3"/></g>`;
|
|
647
|
+
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>`;
|
|
648
|
+
const PHONE = {
|
|
649
|
+
rotate: PHONE_ROTATE,
|
|
650
|
+
hold: grabPhone(),
|
|
651
|
+
move: grabPhone("move"),
|
|
652
|
+
look: grabPhone("look")
|
|
653
|
+
};
|
|
654
|
+
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;
|
|
655
|
+
const KEY = 152;
|
|
656
|
+
const KEY_HALF = KEY / 2;
|
|
657
|
+
const KEY_UP = [262, 82];
|
|
658
|
+
const KEY_LEFT = [82, 259];
|
|
659
|
+
const KEY_DOWN = [262, 259];
|
|
660
|
+
const KEY_RIGHT = [442, 259];
|
|
661
|
+
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"/>`;
|
|
662
|
+
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>`;
|
|
663
|
+
const keyArrow = ([cx, cy], dir) => {
|
|
664
|
+
const L = 42;
|
|
665
|
+
const H = 20;
|
|
666
|
+
const line = (d) => `<path d="${d}" fill="none" stroke="currentColor" stroke-width="11" stroke-linecap="round" stroke-linejoin="round"/>`;
|
|
667
|
+
switch (dir) {
|
|
668
|
+
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}`);
|
|
669
|
+
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}`);
|
|
670
|
+
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}`);
|
|
671
|
+
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}`);
|
|
672
|
+
}
|
|
673
|
+
};
|
|
674
|
+
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");
|
|
675
|
+
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;
|
|
676
|
+
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";
|
|
677
|
+
const MOUSE_LEFT_BTN = `M8 1.051v4.949h-4v-0.5c0-2.31 1.756-4.197 4-4.449z`;
|
|
678
|
+
const MOUSE_RIGHT_BTN = `M13 5.5v0.5h-4v-4.949c2.244 0.252 4 2.139 4 4.449z`;
|
|
679
|
+
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;
|
|
680
|
+
function figureFrom(svg) {
|
|
681
|
+
const figure = document.createElement("div");
|
|
682
|
+
figure.className = "text-base-content/70 h-16 w-16 shrink-0";
|
|
683
|
+
figure.innerHTML = svg;
|
|
684
|
+
return figure;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Icon factories for a tab whose pane is split into per-control sections, in the
|
|
688
|
+
* order its lines document them; `null` for single-diagram tabs. Each factory
|
|
689
|
+
* makes a fresh node (a DOM node can only live in one place).
|
|
690
|
+
*/
|
|
691
|
+
function sectionFigures(kind) {
|
|
692
|
+
switch (kind) {
|
|
693
|
+
case "touch": return [
|
|
694
|
+
"rotate",
|
|
695
|
+
"hold",
|
|
696
|
+
"move",
|
|
697
|
+
"look"
|
|
698
|
+
].map((step) => () => figureFrom(PHONE[step]));
|
|
699
|
+
case "vr": return [() => figureFrom(PHONE.rotate), () => figureFrom(MOVE_STICK)];
|
|
700
|
+
case "keyboard-mouse": return [
|
|
701
|
+
() => figureFrom(KEYS),
|
|
702
|
+
() => figureFrom(mouse("left")),
|
|
703
|
+
() => figureFrom(mouse("right"))
|
|
704
|
+
];
|
|
705
|
+
case "gamepad": return [
|
|
706
|
+
"move",
|
|
707
|
+
"look",
|
|
708
|
+
"select"
|
|
709
|
+
].map((h) => () => figureFrom(gamepadSvg(h)));
|
|
710
|
+
default: return null;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
//#endregion
|
|
714
|
+
//#region src/components/help/modality.ts
|
|
715
|
+
/** Map a modality to the help tab that documents it. */
|
|
716
|
+
function modalityTab(modality) {
|
|
717
|
+
return modality;
|
|
718
|
+
}
|
|
719
|
+
function createModalityTracker() {
|
|
720
|
+
let last = typeof matchMedia !== "undefined" && matchMedia("(pointer: coarse)").matches ? "touch" : "keyboard-mouse";
|
|
721
|
+
const onTouch = () => {
|
|
722
|
+
last = "touch";
|
|
723
|
+
};
|
|
724
|
+
const onPointer = (e) => {
|
|
725
|
+
if (e.pointerType === "touch") return;
|
|
726
|
+
last = "keyboard-mouse";
|
|
727
|
+
};
|
|
728
|
+
const onKey = () => {
|
|
729
|
+
last = "keyboard-mouse";
|
|
730
|
+
};
|
|
731
|
+
const onPad = () => {
|
|
732
|
+
last = "gamepad";
|
|
733
|
+
};
|
|
734
|
+
window.addEventListener("touchstart", onTouch, { passive: true });
|
|
735
|
+
window.addEventListener("pointerdown", onPointer, { passive: true });
|
|
736
|
+
window.addEventListener("keydown", onKey);
|
|
737
|
+
window.addEventListener("gamepadconnected", onPad);
|
|
738
|
+
return {
|
|
739
|
+
get: () => last,
|
|
740
|
+
destroy: () => {
|
|
741
|
+
window.removeEventListener("touchstart", onTouch);
|
|
742
|
+
window.removeEventListener("pointerdown", onPointer);
|
|
743
|
+
window.removeEventListener("keydown", onKey);
|
|
744
|
+
window.removeEventListener("gamepadconnected", onPad);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
//#endregion
|
|
749
|
+
//#region src/components/help/help.ts
|
|
750
|
+
/**
|
|
751
|
+
* Build a help tab's pane body and return the text nodes to relocalize. Most tabs
|
|
752
|
+
* are one diagram above a bullet list. Touch and gamepad are split into sections:
|
|
753
|
+
* one icon per control (with just that part lit) next to its instruction, so the
|
|
754
|
+
* visitor sees which part does what; any extra lines (e.g. "connect a controller")
|
|
755
|
+
* stay as notes below.
|
|
756
|
+
*/
|
|
757
|
+
function buildPane(pane, tab) {
|
|
758
|
+
const figures = sectionFigures(tab.kind);
|
|
759
|
+
if (figures) return buildSectionedPane(pane, tab.lines, figures);
|
|
760
|
+
pane.appendChild(diagramNode(tab.kind));
|
|
761
|
+
const list = document.createElement(tab.ordered ? "ol" : "ul");
|
|
762
|
+
list.className = `${tab.ordered ? "list-decimal" : "list-disc"} list-outside pl-5 space-y-1.5 text-sm leading-relaxed`;
|
|
763
|
+
const lineEls = tab.lines.map((line) => {
|
|
764
|
+
const el = document.createElement("li");
|
|
765
|
+
list.appendChild(el);
|
|
766
|
+
return {
|
|
767
|
+
el,
|
|
768
|
+
line
|
|
769
|
+
};
|
|
770
|
+
});
|
|
771
|
+
pane.appendChild(list);
|
|
772
|
+
return lineEls;
|
|
773
|
+
}
|
|
774
|
+
function buildSectionedPane(pane, lines, figures) {
|
|
775
|
+
const lineEls = [];
|
|
776
|
+
figures.forEach((makeFigure, i) => {
|
|
777
|
+
const line = lines[i];
|
|
778
|
+
if (!line) return;
|
|
779
|
+
const row = document.createElement("div");
|
|
780
|
+
row.className = "flex items-center gap-4 mb-3";
|
|
781
|
+
row.appendChild(makeFigure());
|
|
782
|
+
const p = document.createElement("p");
|
|
783
|
+
p.className = "text-sm leading-relaxed";
|
|
784
|
+
row.appendChild(p);
|
|
785
|
+
pane.appendChild(row);
|
|
786
|
+
lineEls.push({
|
|
787
|
+
el: p,
|
|
788
|
+
line
|
|
789
|
+
});
|
|
790
|
+
});
|
|
791
|
+
const notes = lines.slice(figures.length);
|
|
792
|
+
if (notes.length > 0) {
|
|
793
|
+
const list = document.createElement("ul");
|
|
794
|
+
list.className = "list-disc list-outside pl-5 space-y-1.5 text-sm leading-relaxed text-base-content/70";
|
|
795
|
+
for (const line of notes) {
|
|
796
|
+
const el = document.createElement("li");
|
|
797
|
+
list.appendChild(el);
|
|
798
|
+
lineEls.push({
|
|
799
|
+
el,
|
|
800
|
+
line
|
|
801
|
+
});
|
|
802
|
+
}
|
|
803
|
+
pane.appendChild(list);
|
|
804
|
+
}
|
|
805
|
+
return lineEls;
|
|
806
|
+
}
|
|
807
|
+
let instanceCount = 0;
|
|
808
|
+
/**
|
|
809
|
+
* A visual controls-help panel: an ⓘ button opening a centered daisyUI modal
|
|
810
|
+
* with one tab per input mode, each showing an SVG diagram and a few localized
|
|
811
|
+
* lines. On open it pre-selects the tab for the mode you're using. Diagrams are
|
|
812
|
+
* built in (standard schemes); the text is app-supplied `Text`, relocalized live.
|
|
813
|
+
*/
|
|
814
|
+
function helpPanel(options) {
|
|
815
|
+
const autoSelect = options.autoSelect ?? true;
|
|
816
|
+
const groupName = `sg-help-tabs-${instanceCount += 1}`;
|
|
817
|
+
return (ctx) => {
|
|
818
|
+
const wrap = document.createElement("div");
|
|
819
|
+
const button = document.createElement("button");
|
|
820
|
+
button.type = "button";
|
|
821
|
+
button.className = "btn btn-circle btn-sm";
|
|
822
|
+
setIcon(button, lucideIcon("Info"));
|
|
823
|
+
const dialog = document.createElement("dialog");
|
|
824
|
+
dialog.className = "modal";
|
|
825
|
+
const box = document.createElement("div");
|
|
826
|
+
box.className = "modal-box max-w-md";
|
|
827
|
+
const close = document.createElement("button");
|
|
828
|
+
close.type = "button";
|
|
829
|
+
close.className = "btn btn-sm btn-circle btn-ghost absolute right-2 top-2";
|
|
830
|
+
close.textContent = "✕";
|
|
831
|
+
box.appendChild(close);
|
|
832
|
+
const heading = document.createElement("h3");
|
|
833
|
+
heading.className = "text-lg font-bold mb-4";
|
|
834
|
+
box.appendChild(heading);
|
|
835
|
+
const tablist = document.createElement("div");
|
|
836
|
+
tablist.setAttribute("role", "tablist");
|
|
837
|
+
tablist.className = "tabs tabs-border";
|
|
838
|
+
box.appendChild(tablist);
|
|
839
|
+
const relabels = [];
|
|
840
|
+
const tabInputs = /* @__PURE__ */ new Map();
|
|
841
|
+
let firstInput = null;
|
|
842
|
+
for (const tab of options.tabs) {
|
|
843
|
+
const input = document.createElement("input");
|
|
844
|
+
input.type = "radio";
|
|
845
|
+
input.name = groupName;
|
|
846
|
+
input.role = "tab";
|
|
847
|
+
input.className = "tab";
|
|
848
|
+
if (!firstInput) {
|
|
849
|
+
input.checked = true;
|
|
850
|
+
firstInput = input;
|
|
851
|
+
}
|
|
852
|
+
const pane = document.createElement("div");
|
|
853
|
+
pane.className = "tab-content pt-4";
|
|
854
|
+
const lineEls = buildPane(pane, tab);
|
|
855
|
+
tablist.append(input, pane);
|
|
856
|
+
tabInputs.set(tab.kind, input);
|
|
857
|
+
relabels.push(() => {
|
|
858
|
+
input.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(tab.title));
|
|
859
|
+
for (const { el, line } of lineEls) el.textContent = (0, _scenography_overlay.resolveText)(line);
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
const backdrop = document.createElement("form");
|
|
863
|
+
backdrop.method = "dialog";
|
|
864
|
+
backdrop.className = "modal-backdrop";
|
|
865
|
+
const backdropBtn = document.createElement("button");
|
|
866
|
+
backdropBtn.textContent = "close";
|
|
867
|
+
backdrop.appendChild(backdropBtn);
|
|
868
|
+
dialog.append(box, backdrop);
|
|
869
|
+
wrap.append(button, dialog);
|
|
870
|
+
const tracker = createModalityTracker();
|
|
871
|
+
const unbind = bindLocale(ctx, () => {
|
|
872
|
+
const title = (0, _scenography_overlay.resolveText)(options.title);
|
|
873
|
+
button.setAttribute("aria-label", title);
|
|
874
|
+
heading.textContent = title;
|
|
875
|
+
close.setAttribute("aria-label", (0, _scenography_overlay.resolveText)(options.closeLabel ?? "Close"));
|
|
876
|
+
for (const relabel of relabels) relabel();
|
|
877
|
+
});
|
|
878
|
+
const onOpen = () => {
|
|
879
|
+
if (autoSelect) {
|
|
880
|
+
const mode = ctx.gallery.ctx.viewControls.getMode();
|
|
881
|
+
const kind = mode === "cardboard" ? "cardboard" : mode === "vr" ? "vr" : modalityTab(tracker.get());
|
|
882
|
+
const target = tabInputs.get(kind) ?? firstInput;
|
|
883
|
+
if (target) target.checked = true;
|
|
884
|
+
}
|
|
885
|
+
dialog.showModal();
|
|
886
|
+
};
|
|
887
|
+
const onClose = () => dialog.close();
|
|
888
|
+
button.addEventListener("click", onOpen);
|
|
889
|
+
close.addEventListener("click", onClose);
|
|
890
|
+
return {
|
|
891
|
+
element: wrap,
|
|
892
|
+
destroy: () => {
|
|
893
|
+
button.removeEventListener("click", onOpen);
|
|
894
|
+
close.removeEventListener("click", onClose);
|
|
895
|
+
tracker.destroy();
|
|
896
|
+
unbind();
|
|
897
|
+
}
|
|
898
|
+
};
|
|
899
|
+
};
|
|
900
|
+
}
|
|
901
|
+
//#endregion
|
|
466
902
|
//#region src/components/artwork-info/artwork-info.ts
|
|
467
903
|
const AVAILABILITY_COLOR = {
|
|
468
904
|
available: "#22c55e",
|
|
@@ -481,7 +917,7 @@ function ensureStyles() {
|
|
|
481
917
|
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
482
918
|
const style = document.createElement("style");
|
|
483
919
|
style.id = STYLE_ID;
|
|
484
|
-
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;}`;
|
|
920
|
+
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);}`;
|
|
485
921
|
document.head.appendChild(style);
|
|
486
922
|
}
|
|
487
923
|
const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
|
|
@@ -489,15 +925,19 @@ const join = (parts, sep) => parts.filter((p) => Boolean(p)).join(sep);
|
|
|
489
925
|
* The artwork info panel. The engine emits `artwork:select { id }` when you click
|
|
490
926
|
* a piece's cartela (or the piece itself); this opens a left-hand panel (40%
|
|
491
927
|
* width, full height, black at 85% alpha) with the full caption and asks the
|
|
492
|
-
* camera to frame the piece in the free right 60%.
|
|
493
|
-
*
|
|
494
|
-
*
|
|
928
|
+
* camera to frame the piece in the free right 60%. Prev/next buttons at the
|
|
929
|
+
* bottom cycle through the pieces (wrapping) without leaving the panel. Closing
|
|
930
|
+
* with ✕ re-centers the piece full-width, pulling back to `minCloseDistance` when
|
|
931
|
+
* the framing would otherwise be too close; if the user walks/turns away the
|
|
932
|
+
* engine emits `artwork:released` and the panel just closes (no camera move).
|
|
495
933
|
*
|
|
496
934
|
* Content is data (`artworks`), unlocalized for now. The panel is mounted on the
|
|
497
935
|
* surface (escaping the overlay grid); the slot element is an inert placeholder.
|
|
498
936
|
*/
|
|
499
937
|
function artworkInfo(options) {
|
|
500
|
-
const
|
|
938
|
+
const list = options.artworks;
|
|
939
|
+
const byId = new Map(list.map((a) => [a.id, a]));
|
|
940
|
+
const indexOf = new Map(list.map((a, i) => [a.id, i]));
|
|
501
941
|
return (ctx) => {
|
|
502
942
|
ensureStyles();
|
|
503
943
|
const camera = ctx.gallery.ctx.camera;
|
|
@@ -525,7 +965,23 @@ function artworkInfo(options) {
|
|
|
525
965
|
link.target = "_blank";
|
|
526
966
|
link.rel = "noopener noreferrer";
|
|
527
967
|
link.textContent = `${options.linkText ?? "View artwork"} ↗`;
|
|
528
|
-
|
|
968
|
+
const nav = document.createElement("div");
|
|
969
|
+
nav.className = "sg-artwork-panel__nav";
|
|
970
|
+
const prevBtn = document.createElement("button");
|
|
971
|
+
prevBtn.type = "button";
|
|
972
|
+
prevBtn.className = "sg-artwork-panel__navbtn";
|
|
973
|
+
const prevText = options.prevLabel ?? "Previous";
|
|
974
|
+
prevBtn.textContent = `‹ ${prevText}`;
|
|
975
|
+
prevBtn.setAttribute("aria-label", prevText);
|
|
976
|
+
const nextBtn = document.createElement("button");
|
|
977
|
+
nextBtn.type = "button";
|
|
978
|
+
nextBtn.className = "sg-artwork-panel__navbtn";
|
|
979
|
+
const nextText = options.nextLabel ?? "Next";
|
|
980
|
+
nextBtn.textContent = `${nextText} ›`;
|
|
981
|
+
nextBtn.setAttribute("aria-label", nextText);
|
|
982
|
+
nav.append(prevBtn, nextBtn);
|
|
983
|
+
if (list.length <= 1) nav.style.display = "none";
|
|
984
|
+
panel.append(close, title, meta, status, desc, link, nav);
|
|
529
985
|
ctx.surface.appendChild(panel);
|
|
530
986
|
let currentId = null;
|
|
531
987
|
const render = (artwork) => {
|
|
@@ -554,18 +1010,29 @@ function artworkInfo(options) {
|
|
|
554
1010
|
panel.classList.remove("sg-artwork-panel--open");
|
|
555
1011
|
currentId = null;
|
|
556
1012
|
};
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
|
|
560
|
-
if (!artwork || !id) return;
|
|
1013
|
+
const show = (id) => {
|
|
1014
|
+
const artwork = byId.get(id);
|
|
1015
|
+
if (!artwork) return;
|
|
561
1016
|
currentId = id;
|
|
562
1017
|
render(artwork);
|
|
1018
|
+
panel.scrollTop = 0;
|
|
563
1019
|
panel.classList.add("sg-artwork-panel--open");
|
|
564
1020
|
camera.focusArtwork(id, {
|
|
565
1021
|
centerX: CENTER_X,
|
|
566
1022
|
widthFraction: FREE_FRACTION
|
|
567
1023
|
});
|
|
568
1024
|
};
|
|
1025
|
+
const step = (delta) => {
|
|
1026
|
+
if (currentId === null || list.length <= 1) return;
|
|
1027
|
+
const i = indexOf.get(currentId);
|
|
1028
|
+
if (i === void 0) return;
|
|
1029
|
+
const next = list[(i + delta + list.length) % list.length];
|
|
1030
|
+
if (next) show(next.id);
|
|
1031
|
+
};
|
|
1032
|
+
const onSelect = (payload) => {
|
|
1033
|
+
const id = payload?.id;
|
|
1034
|
+
if (id) show(id);
|
|
1035
|
+
};
|
|
569
1036
|
const onReleased = (payload) => {
|
|
570
1037
|
const id = payload?.id;
|
|
571
1038
|
if (id && id === currentId) hide();
|
|
@@ -573,11 +1040,17 @@ function artworkInfo(options) {
|
|
|
573
1040
|
const onClose = () => {
|
|
574
1041
|
const id = currentId;
|
|
575
1042
|
hide();
|
|
576
|
-
if (id) camera.centerArtwork(id);
|
|
1043
|
+
if (id) camera.centerArtwork(id, { minDistance: options.minCloseDistance ?? 0 });
|
|
577
1044
|
};
|
|
1045
|
+
const onReset = () => hide();
|
|
1046
|
+
const onPrev = () => step(-1);
|
|
1047
|
+
const onNext = () => step(1);
|
|
578
1048
|
const offSelect = ctx.gallery.on("artwork:select", onSelect);
|
|
579
1049
|
const offReleased = ctx.gallery.on("artwork:released", onReleased);
|
|
1050
|
+
const offReset = ctx.gallery.on("camera:reset", onReset);
|
|
580
1051
|
close.addEventListener("click", onClose);
|
|
1052
|
+
prevBtn.addEventListener("click", onPrev);
|
|
1053
|
+
nextBtn.addEventListener("click", onNext);
|
|
581
1054
|
const placeholder = document.createElement("div");
|
|
582
1055
|
placeholder.style.display = "none";
|
|
583
1056
|
return {
|
|
@@ -585,7 +1058,10 @@ function artworkInfo(options) {
|
|
|
585
1058
|
destroy() {
|
|
586
1059
|
offSelect();
|
|
587
1060
|
offReleased();
|
|
1061
|
+
offReset();
|
|
588
1062
|
close.removeEventListener("click", onClose);
|
|
1063
|
+
prevBtn.removeEventListener("click", onPrev);
|
|
1064
|
+
nextBtn.removeEventListener("click", onNext);
|
|
589
1065
|
panel.remove();
|
|
590
1066
|
}
|
|
591
1067
|
};
|
|
@@ -685,39 +1161,83 @@ function settingsPanel(options) {
|
|
|
685
1161
|
field.append(label, select);
|
|
686
1162
|
addTab(lang.tab, lang.label, field);
|
|
687
1163
|
}
|
|
688
|
-
if (options.handedness) {
|
|
689
|
-
const hand = options.handedness;
|
|
1164
|
+
if (options.handedness || options.controls) {
|
|
690
1165
|
const pane = document.createElement("div");
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
const
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
1166
|
+
if (options.handedness) {
|
|
1167
|
+
const hand = options.handedness;
|
|
1168
|
+
const legend = document.createElement("div");
|
|
1169
|
+
legend.className = "label-text mb-1 block";
|
|
1170
|
+
pane.appendChild(legend);
|
|
1171
|
+
const onChange = (e) => hand.onChange(e.target.value);
|
|
1172
|
+
const texts = [];
|
|
1173
|
+
for (const opt of hand.options) {
|
|
1174
|
+
const row = document.createElement("label");
|
|
1175
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
1176
|
+
const input = document.createElement("input");
|
|
1177
|
+
input.type = "radio";
|
|
1178
|
+
input.name = "sg-settings-handedness";
|
|
1179
|
+
input.value = opt.value;
|
|
1180
|
+
input.className = "radio radio-sm";
|
|
1181
|
+
input.checked = opt.value === hand.value;
|
|
1182
|
+
input.addEventListener("change", onChange);
|
|
1183
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
1184
|
+
const text = document.createElement("span");
|
|
1185
|
+
text.className = "label-text text-sm";
|
|
1186
|
+
row.append(input, text);
|
|
1187
|
+
pane.appendChild(row);
|
|
1188
|
+
texts.push({
|
|
1189
|
+
el: text,
|
|
1190
|
+
label: opt.label
|
|
1191
|
+
});
|
|
1192
|
+
}
|
|
1193
|
+
relabels.push(() => {
|
|
1194
|
+
legend.textContent = (0, _scenography_overlay.resolveText)(hand.label);
|
|
1195
|
+
for (const { el, label } of texts) el.textContent = (0, _scenography_overlay.resolveText)(label);
|
|
714
1196
|
});
|
|
715
1197
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
1198
|
+
if (options.controls) {
|
|
1199
|
+
const cfg = options.controls;
|
|
1200
|
+
const viewControls = ctx.gallery.ctx.viewControls;
|
|
1201
|
+
const legend = document.createElement("div");
|
|
1202
|
+
legend.className = `label-text mb-1 block${options.handedness ? " mt-3" : ""}`;
|
|
1203
|
+
pane.appendChild(legend);
|
|
1204
|
+
const list = document.createElement("div");
|
|
1205
|
+
list.className = "flex flex-col gap-1";
|
|
1206
|
+
pane.appendChild(list);
|
|
1207
|
+
const rows = [];
|
|
1208
|
+
for (const item of cfg.items) {
|
|
1209
|
+
const row = document.createElement("label");
|
|
1210
|
+
row.className = "label cursor-pointer justify-start gap-3 py-1";
|
|
1211
|
+
const input = document.createElement("input");
|
|
1212
|
+
input.type = "checkbox";
|
|
1213
|
+
input.className = "toggle toggle-sm";
|
|
1214
|
+
input.checked = viewControls.get()[item.key];
|
|
1215
|
+
const onChange = () => viewControls.setFlag(item.key, input.checked);
|
|
1216
|
+
input.addEventListener("change", onChange);
|
|
1217
|
+
cleanups.push(() => input.removeEventListener("change", onChange));
|
|
1218
|
+
const text = document.createElement("span");
|
|
1219
|
+
text.className = "label-text text-sm";
|
|
1220
|
+
row.append(input, text);
|
|
1221
|
+
list.appendChild(row);
|
|
1222
|
+
rows.push({
|
|
1223
|
+
input,
|
|
1224
|
+
key: item.key,
|
|
1225
|
+
el: text,
|
|
1226
|
+
label: item.label
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
const sync = () => {
|
|
1230
|
+
const flags = viewControls.get();
|
|
1231
|
+
for (const { input, key } of rows) input.checked = flags[key];
|
|
1232
|
+
};
|
|
1233
|
+
cleanups.push(ctx.gallery.on("controls:change", sync));
|
|
1234
|
+
relabels.push(() => {
|
|
1235
|
+
legend.textContent = (0, _scenography_overlay.resolveText)(cfg.label);
|
|
1236
|
+
for (const { el, label } of rows) el.textContent = (0, _scenography_overlay.resolveText)(label);
|
|
1237
|
+
});
|
|
1238
|
+
}
|
|
1239
|
+
const section = options.handedness ?? options.controls;
|
|
1240
|
+
addTab(section.tab, section.label, pane);
|
|
721
1241
|
}
|
|
722
1242
|
if (options.graphics) {
|
|
723
1243
|
const gfx = options.graphics;
|
|
@@ -870,14 +1390,30 @@ function defaultPreset(overlay, options = {}) {
|
|
|
870
1390
|
enter: lucideIcon("Maximize"),
|
|
871
1391
|
exit: lucideIcon("Minimize")
|
|
872
1392
|
} }));
|
|
1393
|
+
if (options.resetView) overlay.add("top-right", resetViewButton({
|
|
1394
|
+
icon: lucideIcon("House"),
|
|
1395
|
+
...typeof options.resetView === "object" ? options.resetView : {}
|
|
1396
|
+
}));
|
|
1397
|
+
if (options.viewMode) {
|
|
1398
|
+
const supported = availableModes(detectControlCapabilities());
|
|
1399
|
+
const modes = options.viewMode.modes.filter((m) => supported.includes(m.mode));
|
|
1400
|
+
if (modes.length >= 2) overlay.add("top-right", viewModeButton({
|
|
1401
|
+
...options.viewMode,
|
|
1402
|
+
modes
|
|
1403
|
+
}));
|
|
1404
|
+
}
|
|
873
1405
|
if (options.settings) overlay.add("top-right", settingsPanel(options.settings));
|
|
874
1406
|
if (options.environment) overlay.add("top-right", environmentPanel({
|
|
875
1407
|
icon: lucideIcon("CloudSun"),
|
|
876
1408
|
...options.environment
|
|
877
1409
|
}));
|
|
878
|
-
overlay.add("top-right", cardboardButton({
|
|
1410
|
+
if (options.cardboard) overlay.add("top-right", cardboardButton({
|
|
1411
|
+
icon: lucideIcon("RectangleGoggles"),
|
|
1412
|
+
...typeof options.cardboard === "object" ? options.cardboard : {}
|
|
1413
|
+
}));
|
|
879
1414
|
if (options.artworks) overlay.add("middle-left", artworkInfo(options.artworks));
|
|
880
|
-
if (options.
|
|
1415
|
+
if (options.help) overlay.add("bottom-right", helpPanel(options.help));
|
|
1416
|
+
else if (options.info) overlay.add("bottom-right", infoPanel(options.info));
|
|
881
1417
|
if (options.credits) overlay.add("bottom-right", creditsPanel({
|
|
882
1418
|
items: options.credits,
|
|
883
1419
|
title: options.creditsTitle,
|
|
@@ -888,15 +1424,20 @@ function defaultPreset(overlay, options = {}) {
|
|
|
888
1424
|
//#endregion
|
|
889
1425
|
exports.actionButton = actionButton;
|
|
890
1426
|
exports.artworkInfo = artworkInfo;
|
|
1427
|
+
exports.availableModes = availableModes;
|
|
891
1428
|
exports.cardboardButton = cardboardButton;
|
|
892
1429
|
exports.creditsPanel = creditsPanel;
|
|
893
1430
|
exports.defaultPreset = defaultPreset;
|
|
1431
|
+
exports.detectControlCapabilities = detectControlCapabilities;
|
|
894
1432
|
exports.environmentPanel = environmentPanel;
|
|
895
1433
|
exports.fullscreenButton = fullscreenButton;
|
|
1434
|
+
exports.helpPanel = helpPanel;
|
|
896
1435
|
exports.infoPanel = infoPanel;
|
|
897
1436
|
exports.lucideIcon = lucideIcon;
|
|
898
1437
|
exports.muteButton = muteButton;
|
|
1438
|
+
exports.resetViewButton = resetViewButton;
|
|
899
1439
|
exports.settingsPanel = settingsPanel;
|
|
1440
|
+
exports.viewModeButton = viewModeButton;
|
|
900
1441
|
exports.watermark = watermark;
|
|
901
1442
|
|
|
902
1443
|
//# sourceMappingURL=index.cjs.map
|