@preference-sl/pref-viewer 2.11.0-beta.2 → 2.11.0-beta.20

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.
@@ -0,0 +1,395 @@
1
+ import OpeningAnimation from "./babylonjs-animation-opening.js";
2
+ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
3
+
4
+ /**
5
+ * OpeningAnimationMenu - Manages and renders the interactive animation control menu for opening/closing animations in a Babylon.js scene.
6
+ *
7
+ * Summary:
8
+ * Provides a floating HTML-based GUI menu with playback and loop controls for animated nodes, including buttons and a progress slider.
9
+ * Handles user interaction, updates UI state based on animation state, and invokes external callbacks for animation actions.
10
+ * Designed for integration with Babylon.js product configurators and interactive 3D applications.
11
+ *
12
+ * Key Features:
13
+ * - Renders a menu with buttons for controlling animation playback (open, close, pause, go to opened/closed, loop).
14
+ * - Updates button states and slider position based on animation state, progress, and loop mode.
15
+ * - Handles user interactions and invokes provided callbacks for animation actions.
16
+ * - Synchronizes the slider value with animation progress, avoiding callback loops.
17
+ * - Provides setters to update animation state, progress, and loop mode from external controllers.
18
+ * - Disposes and cleans up all DOM resources when no longer needed.
19
+ *
20
+ * Public Setters:
21
+ * - set animationState(state): Updates the animation state and button states.
22
+ * - set animationProgress(progress): Updates the animation progress and slider value.
23
+ * - set animationLoop(loop): Updates the loop mode and loop button states.
24
+ *
25
+ * Public Methods:
26
+ * - dispose(): Disposes the animation menu and releases all associated DOM resources.
27
+ *
28
+ * Public Getters:
29
+ * - isVisible: Returns whether the animation menu is currently visible in the DOM.
30
+ *
31
+ * Private Methods:
32
+ * - #createMenu(): Initializes and renders the menu UI.
33
+ * - #addButton(name, imageSVG, enabled, active, visible, callback): Adds a button to the menu with specified properties.
34
+ * - #createButtons(): Creates all control buttons and sets their initial states.
35
+ * - #getButtonByName(name): Retrieves a button control by its name.
36
+ * - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
37
+ * - #getPlayerButtonsState(): Returns the state (enabled, active, visible) for playback control buttons.
38
+ * - #getLoopButtonsState(): Returns the state for loop control buttons.
39
+ * - #getButtonsState(): Combines player and loop button states.
40
+ * - #setPlayerButtonsState(): Updates all playback control buttons.
41
+ * - #setLoopButtonsState(): Updates all loop control buttons.
42
+ * - #createSlider(): Creates and configures the animation progress slider.
43
+ * - #onSliderInput(event): Handles the slider input event for animation progress.
44
+ *
45
+ * Usage Example:
46
+ * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, {
47
+ * onOpen: () => { ... },
48
+ * onClose: () => { ... },
49
+ * onPause: () => { ... },
50
+ * onGoToOpened: () => { ... },
51
+ * onGoToClosed: () => { ... },
52
+ * onToggleLoop: () => { ... },
53
+ * onSetAnimationProgress: (progress) => { ... }
54
+ * });
55
+ * menu.animationState = OpeningAnimation.states.opening;
56
+ * menu.animationProgress = 0.5;
57
+ * menu.animationLoop = true;
58
+ */
59
+ export default class OpeningAnimationMenu {
60
+ #animationState = OpeningAnimation.states.closed;
61
+ #animationProgress = 0;
62
+ #animationLoop = false;
63
+ #callbacks = null;
64
+
65
+ #name = "";
66
+ #canvas = null;
67
+ #containerMain = null;
68
+ #containerButtons = null;
69
+ #slider = null;
70
+
71
+ #icon = {
72
+ close: `<svg id="play-backwards" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M16,18.86V4.86l-11,7,11,7Z"/></svg>`,
73
+ closed: `<svg id="skip-backward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12"/></svg>`,
74
+ open: `<svg id="play" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M8,5.14v14l11-7-11-7Z"/></svg>`,
75
+ opened: `<svg id="skip-forward" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12"/></svg>`,
76
+ pause: `<svg id="pause" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M14,19H18V5H14M6,19H10V5H6V19Z"/></svg>`,
77
+ repeat: `<svg id="repeat" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z"/></svg>`,
78
+ repeatOff: `<svg id="repeat-off" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2,5.27L3.28,4L20,20.72L18.73,22L15.73,19H7V22L3,18L7,14V17H13.73L7,10.27V11H5V8.27L2,5.27M17,13H19V17.18L17,15.18V13M17,5V2L21,6L17,10V7H8.82L6.82,5H17Z"/></svg>`,
79
+ };
80
+
81
+ #handler = {
82
+ onSliderInput: null,
83
+ };
84
+
85
+ /**
86
+ * Constructs the OpeningAnimationMenu and initializes the menu UI.
87
+ * @param {string} name - The name of the animation.
88
+ * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
89
+ * @param {number} animationState - Current animation state (enum).
90
+ * @param {number} animationProgress - Current animation progress (0 to 1).
91
+ * @param {boolean} animationLoop - Whether the animation is set to loop.
92
+ * @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
93
+ * @public
94
+ */
95
+ constructor(name, canvas, animationState, animationProgress, animationLoop, callbacks) {
96
+ this.#name = name;
97
+ this.#canvas = canvas;
98
+ this.#animationState = animationState;
99
+ this.#animationProgress = animationProgress;
100
+ this.#animationLoop = animationLoop;
101
+ this.#callbacks = callbacks;
102
+
103
+ this.#createMenu(animationState);
104
+ }
105
+
106
+ /**
107
+ * Internal helper to add a button to the menu.
108
+ * Sets button appearance and attaches the callback for user interaction.
109
+ * @private
110
+ * @param {string} name - Button identifier.
111
+ * @param {string} imageURL - SVG image data URL for the button icon.
112
+ * @param {boolean} enabled - Whether the button is enabled.
113
+ * @param {boolean} active - Whether the button is visually active.
114
+ * @param {boolean} visible - Whether the button is visible.
115
+ * @param {function} callback - Callback to invoke on button click.
116
+ */
117
+ #addButton(name, imageSVG, enabled = true, active = false, visible = true, callback) {
118
+ const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
119
+ const visibilityClass = visible ? "visible" : "hidden";
120
+
121
+ const button = document.createElement("button");
122
+ button.classList.add(stateClass, visibilityClass);
123
+ button.setAttribute("name", `button_animation_${name}`);
124
+ button.innerHTML = imageSVG;
125
+ button.addEventListener("click", (event) => {
126
+ if (callback && typeof callback === "function") {
127
+ callback();
128
+ }
129
+ });
130
+
131
+ this.#containerButtons.appendChild(button);
132
+ }
133
+
134
+ /**
135
+ * Creates all control buttons and sets their initial states.
136
+ * @private
137
+ */
138
+ #createButtons() {
139
+ const buttonsState = this.#getButtonsState();
140
+ this.#addButton("closed", this.#icon.closed, buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible, this.#callbacks.onGoToClosed);
141
+ this.#addButton("close", this.#icon.close, buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible, this.#callbacks.onClose);
142
+ this.#addButton("pause", this.#icon.pause, buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible, this.#callbacks.onPause);
143
+ this.#addButton("open", this.#icon.open, buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible, this.#callbacks.onOpen);
144
+ this.#addButton("opened", this.#icon.opened, buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible, this.#callbacks.onGoToOpened);
145
+ this.#addButton("repeat", this.#icon.repeat, buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible, this.#callbacks.onToggleLoop);
146
+ this.#addButton("repeatOff", this.#icon.repeatOff, buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible, this.#callbacks.onToggleLoop);
147
+ }
148
+
149
+ /**
150
+ * Creates and configures the animation progress slider.
151
+ * @private
152
+ */
153
+ #createSlider() {
154
+ this.#slider = document.createElement("input");
155
+ this.#slider.setAttribute("type", "range");
156
+ this.#slider.setAttribute("min", "0");
157
+ this.#slider.setAttribute("max", "1");
158
+ this.#slider.setAttribute("step", "0.01");
159
+ this.#slider.setAttribute("value", this.#animationProgress.toString());
160
+ this.#slider.classList.add("animation-menu-slider");
161
+ this.#handler.onSliderInput = this.#onSliderInput.bind(this);
162
+ this.#slider.addEventListener("input", this.#handler.onSliderInput);
163
+
164
+ this.#containerMain.appendChild(this.#slider);
165
+ }
166
+
167
+ /**
168
+ * Initializes and renders the menu UI, including buttons and slider.
169
+ * @private
170
+ */
171
+ #createMenu() {
172
+ this.#containerMain = document.createElement("div");
173
+ this.#containerMain.classList.add("pref-viewer-3d");
174
+ this.#containerMain.classList.add("animation-menu");
175
+ this.#containerMain.setAttribute("data-animation-name", this.#name);
176
+ this.#canvas.parentElement.prepend(this.#containerMain);
177
+
178
+ const style = document.createElement("style");
179
+ style.textContent = PrefViewer3DAnimationMenuStyles;
180
+ this.#containerMain.appendChild(style);
181
+
182
+ this.#containerButtons = document.createElement("div");
183
+ this.#containerButtons.classList.add("animation-menu-buttons");
184
+ this.#containerMain.appendChild(this.#containerButtons);
185
+
186
+ this.#createButtons();
187
+ this.#createSlider();
188
+ }
189
+
190
+ /**
191
+ * Retrieves a button control by its name.
192
+ * @private
193
+ * @param {string} name - Button identifier.
194
+ * @returns {Button|null} The button control or null if not found.
195
+ */
196
+ #getButtonByName(name) {
197
+ return this.#containerButtons.querySelector(`button[name="button_animation_${name}"]`);
198
+ }
199
+
200
+ /**
201
+ * Combines player and loop button states.
202
+ * @private
203
+ * @returns {object}
204
+ */
205
+ #getButtonsState() {
206
+ return Object.assign(this.#getPlayerButtonsState(), this.#getLoopButtonsState());
207
+ }
208
+
209
+ /**
210
+ * Returns the state for loop control buttons.
211
+ * @private
212
+ * @returns {object}
213
+ */
214
+ #getLoopButtonsState() {
215
+ const buttonsState = {
216
+ repeat: {
217
+ enabled: this.#animationLoop,
218
+ active: false,
219
+ visible: this.#animationLoop,
220
+ },
221
+ repeatOff: {
222
+ enabled: !this.#animationLoop,
223
+ active: false,
224
+ visible: !this.#animationLoop,
225
+ },
226
+ };
227
+ return buttonsState;
228
+ }
229
+
230
+ /**
231
+ * Returns the state (enabled, active, visible) for playback control buttons.
232
+ * @private
233
+ * @returns {object}
234
+ */
235
+ #getPlayerButtonsState() {
236
+ const buttonsState = {
237
+ opened: {
238
+ enabled: this.#animationState !== OpeningAnimation.states.opened,
239
+ active: false,
240
+ visible: true,
241
+ },
242
+ open: {
243
+ enabled: this.#animationState !== OpeningAnimation.states.opened && this.#animationState !== OpeningAnimation.states.opening,
244
+ active: this.#animationState === OpeningAnimation.states.opening,
245
+ visible: true,
246
+ },
247
+ pause: {
248
+ enabled: this.#animationState !== OpeningAnimation.states.paused && this.#animationState !== OpeningAnimation.states.closed && this.#animationState !== OpeningAnimation.states.opened,
249
+ active: this.#animationState === OpeningAnimation.states.paused,
250
+ visible: true,
251
+ },
252
+ close: {
253
+ enabled: this.#animationState !== OpeningAnimation.states.closed && this.#animationState !== OpeningAnimation.states.closing,
254
+ active: this.#animationState === OpeningAnimation.states.closing,
255
+ visible: true,
256
+ },
257
+ closed: {
258
+ enabled: this.#animationState !== OpeningAnimation.states.closed,
259
+ active: false,
260
+ visible: true,
261
+ },
262
+ };
263
+ return buttonsState;
264
+ }
265
+
266
+ /**
267
+ * Updates the visual state of a button (enabled, active, visible).
268
+ * @private
269
+ * @param {string} name - Button identifier.
270
+ * @param {boolean} enabled
271
+ * @param {boolean} active
272
+ * @param {boolean} visible
273
+ */
274
+ #setButtonState(name, enabled, active, visible = true) {
275
+ const button = this.#getButtonByName(name);
276
+ if (!button) {
277
+ return;
278
+ }
279
+ const classToRemove = ["active", "enabled", "disabled", "hidden", "visible"];
280
+ button.classList.remove(...classToRemove);
281
+ const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
282
+ const visibilityClass = visible ? "visible" : "hidden";
283
+ button.classList.add(stateClass, visibilityClass);
284
+ }
285
+
286
+ /**
287
+ * Updates all loop control buttons according to current loop mode.
288
+ * @private
289
+ */
290
+ #setLoopButtonsState() {
291
+ const buttonsState = this.#getLoopButtonsState();
292
+ this.#setButtonState("repeat", buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible);
293
+ this.#setButtonState("repeatOff", buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible);
294
+ }
295
+
296
+ /**
297
+ * Updates all playback control buttons according to current animation state.
298
+ * @private
299
+ */
300
+ #setPlayerButtonsState() {
301
+ const buttonsState = this.#getPlayerButtonsState();
302
+ this.#setButtonState("opened", buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible);
303
+ this.#setButtonState("open", buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible);
304
+ this.#setButtonState("pause", buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible);
305
+ this.#setButtonState("close", buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible);
306
+ this.#setButtonState("closed", buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible);
307
+ }
308
+
309
+ /**
310
+ * Handles the slider input event for animation progress.
311
+ * @private
312
+ * @param {Event} event - The input event from the slider.
313
+ * @returns {void}
314
+ * @description
315
+ * If the slider value is being set programmatically, the event is ignored to prevent callback loops.
316
+ */
317
+ #onSliderInput(event) {
318
+ if (this.#callbacks && typeof this.#callbacks.onSetAnimationProgress === "function") {
319
+ this.#callbacks.onSetAnimationProgress(event.target.value);
320
+ }
321
+ }
322
+
323
+ /**
324
+ * ---------------------------
325
+ * Public methods
326
+ * ---------------------------
327
+ */
328
+
329
+ /**
330
+ * Disposes the animation menu and releases all associated DOM resources.
331
+ * @public
332
+ * @returns {void}
333
+ */
334
+ dispose() {
335
+ this.#slider.removeEventListener("input", this.#handler.onSliderInput);
336
+ if (this.isVisible) {
337
+ this.#containerMain.remove();
338
+ }
339
+ this.#containerMain = null;
340
+ this.#containerButtons = null;
341
+ this.#slider = null;
342
+ }
343
+
344
+ /**
345
+ * ---------------------------
346
+ * Public setters
347
+ * ---------------------------
348
+ */
349
+
350
+ /**
351
+ * Sets the animation loop mode and updates loop button states.
352
+ * @public
353
+ * @param {boolean} loop
354
+ */
355
+ set animationLoop(loop) {
356
+ this.#animationLoop = loop;
357
+ this.#setLoopButtonsState();
358
+ }
359
+
360
+ /**
361
+ * Sets the animation progress value and updates the slider position.
362
+ * @public
363
+ * @param {number} progress - The new animation progress value (between 0 and 1).
364
+ */
365
+ set animationProgress(progress) {
366
+ this.#animationProgress = progress;
367
+ this.#slider.value = progress.toString();
368
+ }
369
+
370
+ /**
371
+ * Sets the animation state and updates playback button states.
372
+ * @public
373
+ * @param {number} state - The new animation state (enum).
374
+ */
375
+ set animationState(state) {
376
+ this.#animationState = state;
377
+ this.#setPlayerButtonsState();
378
+ }
379
+
380
+ /**
381
+ * ---------------------------
382
+ * Public getters
383
+ * ---------------------------
384
+ */
385
+
386
+ /**
387
+ * Returns whether the animation menu is currently visible in the DOM.
388
+ * @public
389
+ * @returns {boolean} True if the menu is visible, false otherwise.
390
+ */
391
+ get isVisible() {
392
+ const menuElement = this.#canvas.parentElement.querySelector(`div.animation-menu[data-animation-name="${this.#name}"]`);
393
+ return !!menuElement;
394
+ }
395
+ }