@preference-sl/pref-viewer 2.11.0-beta.13 → 2.11.0-beta.14

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.11.0-beta.13",
3
+ "version": "2.11.0-beta.14",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -35,12 +35,11 @@
35
35
  "index.d.ts"
36
36
  ],
37
37
  "dependencies": {
38
- "@babylonjs/core": "^8.39.0",
39
- "@babylonjs/gui": "^8.39.0",
40
- "@babylonjs/loaders": "^8.39.0",
41
- "@babylonjs/serializers": "^8.39.0",
38
+ "@babylonjs/core": "^8.39.2",
39
+ "@babylonjs/loaders": "^8.39.2",
40
+ "@babylonjs/serializers": "^8.39.2",
42
41
  "@panzoom/panzoom": "^4.6.0",
43
- "babylonjs-gltf2interface": "^8.39.0",
42
+ "babylonjs-gltf2interface": "^8.39.2",
44
43
  "buffer": "^6.0.3",
45
44
  "idb": "^8.0.3",
46
45
  "is-svg": "^6.1.0",
@@ -1,5 +1,4 @@
1
1
  import { Color3, HighlightLayer, Mesh, PickingInfo, Scene } from "@babylonjs/core";
2
- import { AdvancedDynamicTexture } from "@babylonjs/gui";
3
2
  import { PrefViewerColors } from "./styles.js";
4
3
  import OpeningAnimation from "./babylonjs-animation-opening.js";
5
4
 
@@ -7,30 +6,30 @@ import OpeningAnimation from "./babylonjs-animation-opening.js";
7
6
  * BabylonJSAnimationController - Manages animation playback, highlighting, and interactive controls for animated nodes in Babylon.js scenes.
8
7
  *
9
8
  * Summary:
10
- * This class detects, groups, and manages opening/closing animations for scene nodes, provides interactive highlighting of animated nodes and their meshes, and displays a GUI menu for animation control. It is designed for integration with product configurators and interactive 3D applications using Babylon.js.
9
+ * This class detects, groups, and manages opening/closing animations for scene nodes, provides interactive highlighting of animated nodes and their meshes, and displays a menu for animation control. It is designed for integration with product configurators and interactive 3D applications using Babylon.js.
11
10
  *
12
11
  * Key features:
13
12
  * - Detects and groups opening/closing animations in the scene.
14
13
  * - Tracks animated transformation nodes and their relationships to meshes.
15
14
  * - Highlights animated nodes and their child meshes on pointer hover.
16
- * - Displays and disposes the animation control menu (Babylon.js GUI) for animated nodes.
15
+ * - Displays and disposes the animation control menu for animated nodes.
17
16
  * - Provides public API for highlighting, showing the animation menu, and disposing resources.
18
17
  * - Cleans up all resources and observers to prevent memory leaks.
19
18
  *
20
19
  * Public Methods:
21
20
  * - dispose(): Disposes all resources managed by the animation controller.
22
21
  * - hightlightMeshes(pickingInfo): Highlights meshes that are children of an animated node when hovered.
23
- * - hideMenu(): Hides and disposes the animation control menu (Babylon.js GUI) if it exists.
22
+ * - hideMenu(): Hides and disposes the animation control menu if it exists.
24
23
  * - showMenu(pickingInfo): Displays the animation control menu for the animated node under the pointer.
25
24
  *
26
25
  * @class
27
26
  */
28
27
  export default class BabylonJSAnimationController {
29
28
  #scene = null;
29
+ #canvas = null;
30
30
  #animatedNodes = [];
31
31
  #highlightLayer = null;
32
32
  #highlightColor = Color3.FromHexString(PrefViewerColors.primary);
33
- #advancedDynamicTexture = null;
34
33
  #openingAnimations = [];
35
34
 
36
35
  /**
@@ -39,6 +38,7 @@ export default class BabylonJSAnimationController {
39
38
  */
40
39
  constructor(scene) {
41
40
  this.#scene = scene;
41
+ this.#canvas = this.#scene._engine._renderingCanvas;
42
42
  this.#initializeAnimations();
43
43
  }
44
44
 
@@ -47,11 +47,12 @@ export default class BabylonJSAnimationController {
47
47
  * @private
48
48
  */
49
49
  #initializeAnimations() {
50
+ this.hideMenu(); // Clean up any existing menus
50
51
  if (!this.#scene.animationGroups.length) {
51
52
  return;
52
53
  }
53
54
  this.#getAnimatedNodes();
54
- this.#getOpeneingAnimations();
55
+ this.#getOpeningAnimations();
55
56
  }
56
57
 
57
58
  /**
@@ -78,7 +79,7 @@ export default class BabylonJSAnimationController {
78
79
  * @description
79
80
  * Uses animation group names with the pattern "animation_open_<name>" and "animation_close_<name>".
80
81
  */
81
- #getOpeneingAnimations() {
82
+ #getOpeningAnimations() {
82
83
  const openings = {};
83
84
  this.#scene.animationGroups.forEach((animationGroup) => {
84
85
  const match = animationGroup.name.match(/^animation_(open|close)_(.+)$/);
@@ -137,7 +138,7 @@ export default class BabylonJSAnimationController {
137
138
 
138
139
  /**
139
140
  * Disposes all resources managed by the animation controller.
140
- * Cleans up the highlight layer, GUI texture, and internal animation/node lists.
141
+ * Cleans up the highlight layer, animation menu, and internal animation/node lists.
141
142
  * Should be called when the controller is no longer needed to prevent memory leaks.
142
143
  * @public
143
144
  */
@@ -183,24 +184,19 @@ export default class BabylonJSAnimationController {
183
184
  }
184
185
 
185
186
  /**
186
- * Hides and disposes the animation control menu (Babylon.js GUI) if it exists.
187
+ * Hides and disposes the animation control menu if it exists.
187
188
  * @public
188
189
  * @returns {void}
189
190
  */
190
191
  hideMenu() {
191
- // Remove any previously created Babylon GUI
192
- if (this.#advancedDynamicTexture) {
193
- this.#advancedDynamicTexture.dispose();
194
- this.#advancedDynamicTexture = null;
195
- }
192
+ this.#openingAnimations.forEach((openingAnimation) => openingAnimation.hideControls());
193
+ this.#canvas.parentElement.querySelectorAll("div.pref-viewer-3d.animation-menu").forEach((menu) => menu.remove());
196
194
  }
197
195
 
198
196
  /**
199
197
  * Displays the animation control menu for the animated node under the pointer.
200
198
  * @public
201
199
  * @param {PickingInfo} pickingInfo - Raycast info from pointer position.
202
- * @description
203
- * Creates the GUI if needed and invokes OpeningAnimation.showControls.
204
200
  */
205
201
  showMenu(pickingInfo) {
206
202
  if (!pickingInfo?.hit && !pickingInfo?.pickedMesh) {
@@ -217,9 +213,7 @@ export default class BabylonJSAnimationController {
217
213
  if (!openingAnimation) {
218
214
  return;
219
215
  }
220
- if (!this.#advancedDynamicTexture) {
221
- this.#advancedDynamicTexture = AdvancedDynamicTexture.CreateFullscreenUI("UI_Animation", true, this.#scene);
222
- }
223
- openingAnimation.showControls(this.#advancedDynamicTexture);
216
+
217
+ openingAnimation.showControls(this.#canvas);
224
218
  }
225
219
  }
@@ -1,6 +1,5 @@
1
- import { AdvancedDynamicTexture, Button, Control, Image, Slider, StackPanel } from "@babylonjs/gui";
2
1
  import OpeningAnimation from "./babylonjs-animation-opening.js";
3
- import { PrefViewerColors } from "./styles.js";
2
+ import { PrefViewer3DAnimationMenuStyles } from "./styles.js";
4
3
 
5
4
  /**
6
5
  * OpeningAnimationMenu - Manages and renders the animation control menu for opening/closing animations in a Babylon.js scene.
@@ -10,15 +9,23 @@ import { PrefViewerColors } from "./styles.js";
10
9
  * - Updates button states and slider position based on animation state, progress, and loop mode.
11
10
  * - Handles user interactions and invokes provided callbacks for animation actions.
12
11
  * - Synchronizes the slider value with animation progress, avoiding callback loops.
12
+ * - Provides setters to update animation state, progress, and loop mode from external controllers.
13
+ * - Disposes and cleans up all DOM resources when no longer needed.
13
14
  *
14
15
  * Public Setters:
15
16
  * - set animationState(state): Updates the animation state and button states.
16
17
  * - set animationProgress(progress): Updates the animation progress and slider value.
17
18
  * - set animationLoop(loop): Updates the loop mode and loop button states.
18
19
  *
20
+ * Public Methods:
21
+ * - dispose(): Disposes the animation menu and releases all associated DOM resources.
22
+ *
23
+ * Public Getters:
24
+ * - isVisible: Returns whether the animation menu is currently visible in the DOM.
25
+ *
19
26
  * Private Methods:
20
27
  * - #createMenu(): Initializes and renders the menu UI.
21
- * - #addButton(name, imageURL, enabled, active, visible, callback): Adds a button to the menu with specified properties.
28
+ * - #addButton(name, imageSVG, enabled, active, visible, callback): Adds a button to the menu with specified properties.
22
29
  * - #createButtons(): Creates all control buttons and sets their initial states.
23
30
  * - #getButtonByName(name): Retrieves a button control by its name.
24
31
  * - #setButtonState(name, enabled, active, visible): Updates the visual state of a button.
@@ -30,7 +37,7 @@ import { PrefViewerColors } from "./styles.js";
30
37
  * - #createSlider(): Creates and configures the animation progress slider.
31
38
  *
32
39
  * Usage Example:
33
- * const menu = new OpeningAnimationMenu(adt, state, progress, loop, {
40
+ * const menu = new OpeningAnimationMenu(name, canvas, state, progress, loop, {
34
41
  * onOpen: () => { ... },
35
42
  * onClose: () => { ... },
36
43
  * onPause: () => { ... },
@@ -49,45 +56,37 @@ export default class OpeningAnimationMenu {
49
56
  #animationLoop = false;
50
57
  #callbacks = null;
51
58
 
52
- // GUI Elements
53
- #advancedDynamicTexture = null;
54
- #mainPanel = null;
55
- #secondaryPanel = null;
59
+ #name = "";
60
+ #canvas = null;
61
+ #containerMain = null;
62
+ #containerButtons = null;
56
63
  #slider = null;
57
64
 
58
- // Style properties
59
- #buttonSize = 28;
60
- #buttonLoopPaddingLeft = 3;
61
- #colorActive = PrefViewerColors.primary;
62
- #colorEnabled = "#333333";
63
- #colorDisabled = "#777777";
64
- #colorIcon = "#FFFFFF";
65
- #colorBorder = "#FFFFFF";
66
- #sliderThumbWidth = 20;
67
- #sliderBarOffset = 10;
68
65
  #icon = {
69
- 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 fill="${this.#colorIcon}" d="M16,18.86V4.86l-11,7,11,7Z"/></svg>`,
70
- 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 fill="${this.#colorIcon}" d="M20,5V19L13,12M6,5V19H4V5M13,5V19L6,12"/></svg>`,
71
- 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 fill="${this.#colorIcon}" d="M8,5.14v14l11-7-11-7Z"/></svg>`,
72
- 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 fill="${this.#colorIcon}" d="M4,5V19L11,12M18,5V19H20V5M11,5V19L18,12"/></svg>`,
73
- 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 fill="${this.#colorIcon}" d="M14,19H18V5H14M6,19H10V5H6V19Z"/></svg>`,
74
- 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 fill="${this.#colorIcon}" d="M17,17H7V14L3,18L7,22V19H19V13H17M7,7H17V10L21,6L17,2V5H5V11H7V7Z"/></svg>`,
75
- 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 fill="${this.#colorIcon}" 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>`,
66
+ 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>`,
67
+ 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>`,
68
+ 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>`,
69
+ 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>`,
70
+ 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>`,
71
+ 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>`,
72
+ 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>`,
76
73
  };
77
74
 
78
75
  #isSettingSliderValue = false;
79
76
 
80
77
  /**
81
78
  * Constructs the OpeningAnimationMenu and initializes the menu UI.
82
- * @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture for rendering controls.
79
+ * @param {string} name - The name of the animation.
80
+ * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
83
81
  * @param {number} animationState - Current animation state (enum).
84
82
  * @param {number} animationProgress - Current animation progress (0 to 1).
85
83
  * @param {boolean} animationLoop - Whether the animation is set to loop.
86
84
  * @param {object} callbacks - Callback functions for menu actions (play, pause, open, close, etc.).
87
85
  * @public
88
86
  */
89
- constructor(advancedDynamicTexture, animationState, animationProgress, animationLoop, callbacks) {
90
- this.#advancedDynamicTexture = advancedDynamicTexture;
87
+ constructor(name, canvas, animationState, animationProgress, animationLoop, callbacks) {
88
+ this.#name = name;
89
+ this.#canvas = canvas;
91
90
  this.#animationState = animationState;
92
91
  this.#animationProgress = animationProgress;
93
92
  this.#animationLoop = animationLoop;
@@ -101,19 +100,19 @@ export default class OpeningAnimationMenu {
101
100
  * @private
102
101
  */
103
102
  #createMenu() {
104
- if (!this.#advancedDynamicTexture) {
105
- return;
106
- }
107
- this.#mainPanel = new StackPanel();
108
- this.#mainPanel.isVertical = true;
109
- this.#secondaryPanel = new StackPanel();
110
- this.#secondaryPanel.isVertical = false;
111
- this.#secondaryPanel.height = `${this.#buttonSize}px`;
112
- this.#mainPanel.addControl(this.#secondaryPanel);
103
+ this.#containerMain = document.createElement("div");
104
+ this.#containerMain.classList.add("pref-viewer-3d");
105
+ this.#containerMain.classList.add("animation-menu");
106
+ this.#containerMain.setAttribute("data-animation-name", this.#name);
107
+ this.#canvas.parentElement.prepend(this.#containerMain);
113
108
 
114
- this.#advancedDynamicTexture.addControl(this.#mainPanel);
115
- this.#mainPanel.horizontalAlignment = Control.HORIZONTAL_ALIGNMENT_CENTER;
116
- this.#mainPanel.verticalAlignment = Control.VERTICAL_ALIGNMENT_BOTTOM;
109
+ const style = document.createElement("style");
110
+ style.textContent = PrefViewer3DAnimationMenuStyles;
111
+ this.#containerMain.appendChild(style);
112
+
113
+ this.#containerButtons = document.createElement("div");
114
+ this.#containerButtons.classList.add("animation-menu-buttons");
115
+ this.#containerMain.appendChild(this.#containerButtons);
117
116
 
118
117
  this.#createButtons();
119
118
  this.#createSlider();
@@ -130,25 +129,21 @@ export default class OpeningAnimationMenu {
130
129
  * @param {boolean} visible - Whether the button is visible.
131
130
  * @param {function} callback - Callback to invoke on button click.
132
131
  */
133
- #addButton(name, imageURL, enabled = true, active = false, visible = true, callback) {
134
- const buttonProps = {
135
- background: active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled,
136
- color: this.#colorBorder,
137
- cornerRadius: 0,
138
- height: `${this.#buttonSize}px`,
139
- hoverCursor: "pointer",
140
- width: `${this.#buttonSize}px`,
141
- isVisible: visible,
142
- };
143
- const button = Button.CreateImageOnlyButton(`button_animation_${name}`, imageURL);
144
- Object.assign(button, buttonProps);
145
- button.image.stretch = Image.STRETCH_UNIFORM;
146
- button.onPointerUpObservable.add(() => {
132
+ #addButton(name, imageSVG, enabled = true, active = false, visible = true, callback) {
133
+ const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
134
+ const visibilityClass = visible ? "visible" : "hidden";
135
+
136
+ const button = document.createElement("button");
137
+ button.classList.add(stateClass, visibilityClass);
138
+ button.setAttribute("name", `button_animation_${name}`);
139
+ button.innerHTML = imageSVG;
140
+ button.addEventListener("click", (event) => {
147
141
  if (callback && typeof callback === "function") {
148
142
  callback();
149
143
  }
150
144
  });
151
- this.#secondaryPanel.addControl(button);
145
+
146
+ this.#containerButtons.appendChild(button);
152
147
  }
153
148
 
154
149
  /**
@@ -157,19 +152,13 @@ export default class OpeningAnimationMenu {
157
152
  */
158
153
  #createButtons() {
159
154
  const buttonsState = this.#getButtonsState();
160
- this.#addButton("closed", `data:image/svg+xml,${encodeURIComponent(this.#icon.closed)}`, buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible, this.#callbacks.onGoToClosed);
161
- this.#addButton("close", `data:image/svg+xml,${encodeURIComponent(this.#icon.close)}`, buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible, this.#callbacks.onClose);
162
- this.#addButton("pause", `data:image/svg+xml,${encodeURIComponent(this.#icon.pause)}`, buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible, this.#callbacks.onPause);
163
- this.#addButton("open", `data:image/svg+xml,${encodeURIComponent(this.#icon.open)}`, buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible, this.#callbacks.onOpen);
164
- this.#addButton("opened", `data:image/svg+xml,${encodeURIComponent(this.#icon.opened)}`, buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible, this.#callbacks.onGoToOpened);
165
- this.#addButton("repeat", `data:image/svg+xml,${encodeURIComponent(this.#icon.repeat)}`, buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible, this.#callbacks.onToggleLoop);
166
- this.#addButton("repeatOff", `data:image/svg+xml,${encodeURIComponent(this.#icon.repeatOff)}`, buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible, this.#callbacks.onToggleLoop);
167
-
168
- // Adjust padding for loop buttons
169
- this.#getButtonByName("repeat").paddingLeft = `${this.#buttonLoopPaddingLeft}px`;
170
- this.#getButtonByName("repeat").width = `${this.#buttonSize + this.#buttonLoopPaddingLeft}px`;
171
- this.#getButtonByName("repeatOff").paddingLeft = `${this.#buttonLoopPaddingLeft}px`;
172
- this.#getButtonByName("repeatOff").width = `${this.#buttonSize + this.#buttonLoopPaddingLeft}px`;
155
+ this.#addButton("closed", this.#icon.closed, buttonsState.closed.enabled, buttonsState.closed.active, buttonsState.closed.visible, this.#callbacks.onGoToClosed);
156
+ this.#addButton("close", this.#icon.close, buttonsState.close.enabled, buttonsState.close.active, buttonsState.close.visible, this.#callbacks.onClose);
157
+ this.#addButton("pause", this.#icon.pause, buttonsState.pause.enabled, buttonsState.pause.active, buttonsState.pause.visible, this.#callbacks.onPause);
158
+ this.#addButton("open", this.#icon.open, buttonsState.open.enabled, buttonsState.open.active, buttonsState.open.visible, this.#callbacks.onOpen);
159
+ this.#addButton("opened", this.#icon.opened, buttonsState.opened.enabled, buttonsState.opened.active, buttonsState.opened.visible, this.#callbacks.onGoToOpened);
160
+ this.#addButton("repeat", this.#icon.repeat, buttonsState.repeat.enabled, buttonsState.repeat.active, buttonsState.repeat.visible, this.#callbacks.onToggleLoop);
161
+ this.#addButton("repeatOff", this.#icon.repeatOff, buttonsState.repeatOff.enabled, buttonsState.repeatOff.active, buttonsState.repeatOff.visible, this.#callbacks.onToggleLoop);
173
162
  }
174
163
 
175
164
  /**
@@ -179,7 +168,7 @@ export default class OpeningAnimationMenu {
179
168
  * @returns {Button|null} The button control or null if not found.
180
169
  */
181
170
  #getButtonByName(name) {
182
- return this.#advancedDynamicTexture.getControlByName(`button_animation_${name}`);
171
+ return this.#containerButtons.querySelector(`button[name="button_animation_${name}"]`);
183
172
  }
184
173
 
185
174
  /**
@@ -195,8 +184,11 @@ export default class OpeningAnimationMenu {
195
184
  if (!button) {
196
185
  return;
197
186
  }
198
- button.background = active ? this.#colorActive : enabled ? this.#colorEnabled : this.#colorDisabled;
199
- button.isVisible = visible;
187
+ const classToRemove = ["active", "enabled", "disabled", "hidden", "visible"];
188
+ button.classList.remove(...classToRemove);
189
+ const stateClass = active ? "active" : enabled ? "enabled" : "disabled";
190
+ const visibilityClass = visible ? "visible" : "hidden";
191
+ button.classList.add(stateClass, visibilityClass);
200
192
  }
201
193
 
202
194
  /**
@@ -293,32 +285,45 @@ export default class OpeningAnimationMenu {
293
285
  * @private
294
286
  */
295
287
  #createSlider() {
296
- const sliderProps = {
297
- minimum: 0,
298
- maximum: 1,
299
- value: this.#animationProgress,
300
- height: `${this.#buttonSize}px`,
301
- width: `${this.#buttonSize * 7 + this.#buttonLoopPaddingLeft}px`, // Width based on number of buttons visible
302
- barOffset: `${this.#sliderBarOffset}px`,
303
- isThumbCircle: true,
304
- thumbWidth: `${this.#sliderThumbWidth}px`,
305
- background: this.#colorDisabled,
306
- color: this.#colorEnabled,
307
- borderColor: this.#colorBorder,
308
- thumbColor: this.#colorEnabled,
309
- };
310
- this.#slider = new Slider("slider_animation_progress");
311
- Object.assign(this.#slider, sliderProps);
312
- this.#slider.onValueChangedObservable.add((value) => {
288
+ this.#slider = document.createElement("input");
289
+ this.#slider.setAttribute("type", "range");
290
+ this.#slider.setAttribute("min", "0");
291
+ this.#slider.setAttribute("max", "1");
292
+ this.#slider.setAttribute("step", "0.01");
293
+ this.#slider.setAttribute("value", this.#animationProgress.toString());
294
+ this.#slider.classList.add("animation-menu-slider");
295
+
296
+ this.#slider.addEventListener("input", (event) => {
313
297
  if (this.#isSettingSliderValue) {
314
298
  this.#isSettingSliderValue = false;
315
299
  return;
316
300
  }
317
301
  if (this.#callbacks && typeof this.#callbacks.onSetAnimationProgress === "function") {
318
- this.#callbacks.onSetAnimationProgress(value);
302
+ this.#callbacks.onSetAnimationProgress(event.target.value);
319
303
  }
320
304
  });
321
- this.#mainPanel.addControl(this.#slider);
305
+
306
+ this.#containerMain.appendChild(this.#slider);
307
+ }
308
+
309
+ /**
310
+ * ---------------------------
311
+ * Public methods
312
+ * ---------------------------
313
+ */
314
+
315
+ /**
316
+ * Disposes the animation menu and releases all associated DOM resources.
317
+ * @public
318
+ * @returns {void}
319
+ */
320
+ dispose() {
321
+ if (this.isVisible) {
322
+ this.#containerMain.remove();
323
+ }
324
+ this.#containerMain = null;
325
+ this.#containerButtons = null;
326
+ this.#slider = null;
322
327
  }
323
328
 
324
329
  /**
@@ -358,4 +363,20 @@ export default class OpeningAnimationMenu {
358
363
  this.#animationState = state;
359
364
  this.#setPlayerButtonsState();
360
365
  }
366
+
367
+ /**
368
+ * ---------------------------
369
+ * Public getters
370
+ * ---------------------------
371
+ */
372
+
373
+ /**
374
+ * Returns whether the animation menu is currently visible in the DOM.
375
+ * @public
376
+ * @returns {boolean} True if the menu is visible, false otherwise.
377
+ */
378
+ get isVisible() {
379
+ const menuElement = this.#canvas.parentElement.querySelector(`div.animation-menu[data-animation-name="${this.#name}"]`);
380
+ return !!menuElement;
381
+ }
361
382
  }
@@ -1,4 +1,3 @@
1
- import { AdvancedDynamicTexture } from "@babylonjs/gui";
2
1
  import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
3
2
 
4
3
  /**
@@ -19,7 +18,7 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
19
18
  * - pause(): Pauses the current animation.
20
19
  * - goToOpened(): Moves animation to the fully opened state.
21
20
  * - goToClosed(): Moves animation to the fully closed state.
22
- * - showControls(advancedDynamicTexture): Displays the animation control menu.
21
+ * - showControls(canvas): Displays the animation control menu.
23
22
  * - hideControls(): Hides the animation control menu.
24
23
  * - isControlsVisible(): Returns true if the control menu is visible for this animation.
25
24
  *
@@ -39,14 +38,6 @@ import OpeningAnimationMenu from "./babylonjs-animation-opening-menu.js";
39
38
  * - #checkProgress(progress): Applies threshold logic to progress.
40
39
  * - #updateControlsSlider(): Updates the slider in the control menu.
41
40
  * - #updateControls(): Updates all controls in the menu.
42
- *
43
- * Usage Example:
44
- * const anim = new OpeningAnimation("door", openGroup, closeGroup);
45
- * anim.playOpen();
46
- * anim.pause();
47
- * anim.goToClosed();
48
- * anim.showControls(adt);
49
- * anim.hideControls();
50
41
  */
51
42
  export default class OpeningAnimation {
52
43
  static states = {
@@ -68,7 +59,6 @@ export default class OpeningAnimation {
68
59
  #speedRatio = 1.0;
69
60
  #loop = false;
70
61
 
71
- #advancedDynamicTexture = null;
72
62
  #menu = null;
73
63
  #progressThreshold = 0.025;
74
64
 
@@ -138,17 +128,18 @@ export default class OpeningAnimation {
138
128
  #goToOpened(useLoop = false) {
139
129
  this.#lastPausedFrame = this.#endFrame;
140
130
 
141
- if (!this.#openAnimation._isStarted) {
142
- this.#openAnimation.start();
131
+ if (this.#openAnimation._isStarted && !this.#openAnimation._isPaused){
132
+ this.#openAnimation.pause();
143
133
  }
144
- this.#openAnimation.pause();
145
- this.#openAnimation.goToFrame(this.#endFrame);
134
+ this.#openAnimation.goToFrame(this.#endFrame);
146
135
 
147
- if (!this.#closeAnimation._isStarted) {
136
+ if (this.#closeAnimation._isStarted && this.#closeAnimation._isPaused) {
137
+ this.#closeAnimation.goToFrame(this.#startFrame);
138
+ } else {
148
139
  this.#closeAnimation.start();
140
+ this.#closeAnimation.pause();
141
+ this.#closeAnimation.goToFrame(this.#startFrame);
149
142
  }
150
- this.#closeAnimation.pause();
151
- this.#closeAnimation.goToFrame(this.#startFrame);
152
143
 
153
144
  this.#state = OpeningAnimation.states.opened;
154
145
  this.#updateControls();
@@ -166,17 +157,18 @@ export default class OpeningAnimation {
166
157
  #goToClosed(useLoop = false) {
167
158
  this.#lastPausedFrame = this.#startFrame;
168
159
 
169
- if (!this.#closeAnimation._isStarted) {
170
- this.#closeAnimation.start();
160
+ if (this.#closeAnimation._isStarted && !this.#closeAnimation._isPaused) {
161
+ this.#closeAnimation.pause();
171
162
  }
172
- this.#closeAnimation.pause();
173
163
  this.#closeAnimation.goToFrame(this.#endFrame - this.#startFrame);
174
164
 
175
- if (!this.#openAnimation._isStarted) {
165
+ if (this.#openAnimation._isStarted && this.#openAnimation._isPaused) {
166
+ this.#openAnimation.goToFrame(this.#startFrame);
167
+ } else {
176
168
  this.#openAnimation.start();
169
+ this.#openAnimation.pause();
170
+ this.#openAnimation.goToFrame(this.#startFrame);
177
171
  }
178
- this.#openAnimation.pause();
179
- this.#openAnimation.goToFrame(this.#startFrame);
180
172
 
181
173
  this.#state = OpeningAnimation.states.closed;
182
174
  this.#updateControls();
@@ -328,6 +320,7 @@ export default class OpeningAnimation {
328
320
  if (this.#state === OpeningAnimation.states.opening || this.#state === OpeningAnimation.states.opened) {
329
321
  return;
330
322
  }
323
+
331
324
  if (this.#state === OpeningAnimation.states.closing) {
332
325
  this.#lastPausedFrame = this.#endFrame - this.#closeAnimation.getCurrentFrame();
333
326
  this.#closeAnimation.pause();
@@ -352,6 +345,7 @@ export default class OpeningAnimation {
352
345
  if (this.#state === OpeningAnimation.states.closing || this.#state === OpeningAnimation.states.closed) {
353
346
  return;
354
347
  }
348
+
355
349
  if (this.#state === OpeningAnimation.states.opening) {
356
350
  this.#lastPausedFrame = this.#openAnimation.getCurrentFrame();
357
351
  this.#openAnimation.pause();
@@ -405,11 +399,9 @@ export default class OpeningAnimation {
405
399
  * Displays the animation control menu and sets up callbacks.
406
400
  * Synchronizes slider and button states with animation.
407
401
  * @public
408
- * @param {AdvancedDynamicTexture} advancedDynamicTexture - Babylon.js GUI texture.
402
+ * @param {HTMLCanvasElement} canvas - The canvas element for rendering.
409
403
  */
410
- showControls(advancedDynamicTexture) {
411
- this.#advancedDynamicTexture = advancedDynamicTexture;
412
- this.#advancedDynamicTexture.metadata = { animationName: this.name };
404
+ showControls(canvas) {
413
405
  const controlCallbacks = {
414
406
  onGoToOpened: () => {
415
407
  if (this.#state === OpeningAnimation.states.opened) {
@@ -457,7 +449,7 @@ export default class OpeningAnimation {
457
449
  this.#menu.animationLoop = this.#loop;
458
450
  },
459
451
  };
460
- this.#menu = new OpeningAnimationMenu(this.#advancedDynamicTexture, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
452
+ this.#menu = new OpeningAnimationMenu(this.name, canvas, this.#state, this.#getProgress(), this.#loop, controlCallbacks);
461
453
 
462
454
  // Attach to Babylon.js scene render loop for real-time updates
463
455
  this.#openAnimation._scene.onBeforeRenderObservable.add(this.#updateControlsSlider.bind(this));
@@ -471,10 +463,9 @@ export default class OpeningAnimation {
471
463
  if (!this.isControlsVisible()) {
472
464
  return;
473
465
  }
474
- // Remove the observer when controls are hidden
475
466
  this.#openAnimation._scene.onBeforeRenderObservable.removeCallback(this.#updateControlsSlider.bind(this));
476
- this.#advancedDynamicTexture.dispose();
477
- this.#advancedDynamicTexture = null;
467
+ this.#menu.dispose();
468
+ this.#menu = null;
478
469
  }
479
470
 
480
471
  /**
@@ -483,7 +474,7 @@ export default class OpeningAnimation {
483
474
  * @returns {boolean} True if controls are visible for this animation; otherwise, false.
484
475
  */
485
476
  isControlsVisible() {
486
- return !!(this.#advancedDynamicTexture && this.#advancedDynamicTexture.metadata?.animationName === this.name && this.#menu);
477
+ return !!(this.#menu?.isVisible);
487
478
  }
488
479
 
489
480
  /**
@@ -497,7 +488,6 @@ export default class OpeningAnimation {
497
488
  * @public
498
489
  * @returns {number}
499
490
  */
500
-
501
491
  get state() {
502
492
  return this.#state;
503
493
  }
package/src/styles.js CHANGED
@@ -88,6 +88,144 @@ export const PrefViewer3DStyles = `
88
88
  }
89
89
  `;
90
90
 
91
+ export const PrefViewer3DAnimationMenuStyles = `
92
+ div.pref-viewer-3d.animation-menu, div.pref-viewer-3d.animation-menu * {
93
+ box-sizing: border-box;
94
+ padding: 0;
95
+ margin: 0;
96
+ }
97
+
98
+ div.pref-viewer-3d.animation-menu {
99
+ --color-primary: ${PrefViewerColors.primary};
100
+ --color-active: var(--color-primary);
101
+ --color-active-hover: color-mix(in oklab, var(--color-active), black 10%);
102
+ --color-enabled: #7a7a7a;
103
+ --color-enabled-hover: color-mix(in oklab, var(--color-enabled), black 15%);
104
+ --color-disabled: #bdbdbd;
105
+ --color-icon: #FFFFFF;
106
+ --color-border: #FFFFFF;
107
+ --button-size: 32px;
108
+ --icon-size: 24px;
109
+ --button-loop-margin-left: 3px;
110
+ --button-spacing: 2px;
111
+ --slider-thumb-width: 20px;
112
+ --slider-bar-height: 8px;
113
+ --slider-bar-offset: 10px;
114
+
115
+ display: block;
116
+ position: absolute;
117
+ bottom: 10px;
118
+
119
+ right: calc(50% - ((6 * var(--button-size) + 5 * var(--button-spacing) + var(--button-loop-margin-left)) / 2));
120
+ z-index: 1000;
121
+ }
122
+
123
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons {
124
+ padding: 0;
125
+ margin: 0;
126
+ display: flex;
127
+ gap: var(--button-spacing);
128
+ }
129
+
130
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button {
131
+ display: flex;
132
+ align-items: center;
133
+ justify-content: center;
134
+ padding: 0;
135
+ margin: 0;
136
+ cursor: pointer;
137
+ border: 1px solid var(--color-border);
138
+ width: var(--button-size);
139
+ height: var(--button-size);
140
+ background: var(--color-enabled);
141
+ }
142
+
143
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button>svg {
144
+ fill: var(--color-icon);
145
+ width: 100%;
146
+ height: 100%;
147
+ }
148
+
149
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.active {
150
+ background: var(--color-active);
151
+ }
152
+
153
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.active:hover {
154
+ background: var(--color-active-hover);
155
+ }
156
+
157
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.enabled {
158
+ background: var(--color-enabled);
159
+ }
160
+
161
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.enabled:hover {
162
+ background: var(--color-enabled-hover);
163
+ }
164
+
165
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.disabled {
166
+ background: var(--color-disabled);
167
+ }
168
+
169
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.visible {
170
+ display: block-flex;
171
+ }
172
+
173
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button.hidden {
174
+ display: none;
175
+ }
176
+
177
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button[name="button_animation_repeat"],
178
+ div.pref-viewer-3d.animation-menu>div.animation-menu-buttons>button[name="button_animation_repeatOff"] {
179
+ margin-left: var(--button-loop-margin-left);
180
+ }
181
+
182
+ div.pref-viewer-3d.animation-menu>input[type="range"] {
183
+ -webkit-appearance: none;
184
+ width: 100%;
185
+ height: var(--slider-bar-height);
186
+ border: 1px solid var(--color-border);
187
+ outline: none;
188
+ accent-color: var(--color-active);
189
+ background: var(--color-enabled);
190
+ border-radius: var(--slider-bar-height);
191
+ -webkit-transition: .2s;
192
+ transition: background .2s;
193
+ margin: var(--slider-bar-offset) 0px;
194
+ }
195
+
196
+ div.pref-viewer-3d.animation-menu>input[type="range"]::-webkit-slider-thumb {
197
+ -webkit-appearance: none;
198
+ appearance: none;
199
+ height: var(--slider-thumb-width);
200
+ width: var(--slider-thumb-width);
201
+ border-radius: 50%;
202
+ border: 1px solid var(--color-border);
203
+ background: var(--color-active);
204
+ cursor: pointer;
205
+ -webkit-transition: .2s;
206
+ transition: background .2s;
207
+ }
208
+
209
+ div.pref-viewer-3d.animation-menu>input[type="range"]::-webkit-slider-thumb:hover {
210
+ background: var(--color-active-hover);
211
+ }
212
+
213
+ div.pref-viewer-3d.animation-menu>input[type="range"]::-moz-range-track {
214
+ height: var(--slider-thumb-width);
215
+ width: var(--slider-thumb-width);
216
+ border-radius: 50%;
217
+ border: 1px solid var(--color-border);
218
+ background: var(--color-active);
219
+ cursor: pointer;
220
+ -webkit-transition: .2s;
221
+ transition: background .2s;
222
+ }
223
+
224
+ div.pref-viewer-3d.animation-menu>input[type="range"]::-moz-range-track:hover {
225
+ background: var(--color-active-hover);
226
+ }
227
+ `;
228
+
91
229
  export const PrefViewerDialogStyles = `
92
230
  pref-viewer-dialog {
93
231
  --color-primary: ${PrefViewerColors.primary};
@@ -98,9 +236,9 @@ export const PrefViewerDialogStyles = `
98
236
  --dialog-border-radius: 8px;
99
237
  --dialog-box-shadow: 0 4px 24px rgba(0, 0, 0, 0.25);
100
238
  --button-default-bg-color: #bbbbbb;
101
- --button-default-bg-color-hover: #a1a1a1;
102
- --button-primary-bg-color: color-mix(in oklab, var(--color-primary), white 25%);
103
- --button-primary-bg-color-hover: var(--color-primary);
239
+ --button-default-bg-color-hover: color-mix(in oklab, var(--button-default-bg-color), black 10%);
240
+ --button-primary-bg-color: var(--color-primary);
241
+ --button-primary-bg-color-hover: color-mix(in oklab, var(--button-primary-bg-color), black 10%);
104
242
  --button-border-radius: 4px;
105
243
  --button-padding-horizontal: 16px;
106
244
  --button-padding-vertical: 8px;