import { config } from "../config.js"; import Scene from "./scene.js"; import AppState from "./state.js"; /** * @namespace UI */ const UI = {}; UI.domParser = new DOMParser; UI.contentMenuTabs = `
`; UI.audioExample = ` `; /** * * @param {String} triggerSelector - Usually, the close modal trigger element(s) selector */ UI.pauseAudio = function(triggerSelector) { // What if more than one audio element is playing? const audio = document.querySelector('audio'); if (audio) { document.querySelectorAll(triggerSelector).forEach(el => { el.addEventListener('click', () => audio.pause()); }); document.querySelector('.modal').addEventListener('blur', () => { audio.pause(); }); } } /** * Resets the UI state (essentially hides the clipper toolbar if visible...) * @todo Other elements to reset?? Restore inital lighting conditions and viewpoint... */ UI.reset = function() { document.querySelector('#clipper-bar')?.classList.add('d-none'); document.querySelector('#clipper')?.classList.remove('border', 'border-2', 'border-white'); } /** * * @param {HTMLElement} target * @param {NodeListOf} btns * @param {String} axis - One of 'x', 'y', 'z' */ UI.showClipping = function(target, btns, axis) { if (axis) { Scene.addClippingPlane(axis, -1); target.classList.add('border', 'border-2', 'border-warning'); btns.forEach(btn => { if (btn.id !== target.id) { btn.classList.remove('border', 'border-2', 'border-warning'); } }); } } /** * @todo Get clipping button from state? Review logic!! * @param {String} triggerSelector * @param {String} targetSelector The selector for the target toolbar to be displayed */ UI.toggleClipperBar = function(triggerSelector, targetSelector) { const trigger = document.querySelector(triggerSelector); const toolbar = document.querySelector(targetSelector); const btns = toolbar.querySelectorAll('button'); const clipTargets = { clipX: {axis: 'x'}, clipY: {axis: 'y'}, clipZ: {axis: 'z'}, }; if (!AppState.clipping.listeners.button) { trigger.addEventListener( 'click', () => { toolbar.classList.toggle('d-none'); const aoCurrentState = AppState.ambientOcclusion; if (!toolbar.classList.contains('d-none')) { AppState.clipping.enabled = true; Scene.toggleAmbientOcclusion(false); btns.forEach(btn => { btn.classList.remove('border', 'border-2', 'border-warning'); }); trigger.className += ' border border-2 border-white'; toolbar.addEventListener('click', event => { UI.showClipping(event.target, btns, clipTargets[event.target.id]?.axis); }); } else { Scene.resetClipping(); let noBorder = trigger.className.replace(/ border.*$/g, ''); trigger.className = noBorder; Scene.toggleAmbientOcclusion(aoCurrentState); } } ); AppState.clipping.listeners.button = true; } } /** * A left side settings panel * @param {String} triggerId - The settings button id */ UI.toggleSettingsPanel = function(triggerId) { const btn = document.querySelector(`#${triggerId}`); const lightHeading = document.createElement('h2'); lightHeading.className = 'fs-5 ms-2 mb-3 mt-3'; lightHeading.innerHTML = ' Direzione luce'; const envHeading = document.createElement('h2'); envHeading.className = 'fs-5 ms-2 mb-3 mt-3'; envHeading.innerHTML = ' Ambiente'; btn.addEventListener('click', () => { ATON.UI.setSidePanelLeft(); ATON.UI.showSidePanel({header: ' Impostazioni'}); ATON.UI.elSidePanel.appendChild(lightHeading); const lightSliderX = this.createLightSlider('x', 'Asse X', [-2, 2], 0.1); const lightSliderY = this.createLightSlider('y', 'Asse Y', [-2, 2], 0.1); const lightSliderZ = this.createLightSlider('z', 'Asse Z', [-2, 2], 0.1); ATON.UI.elSidePanel.appendChild(lightSliderX); ATON.UI.elSidePanel.appendChild(lightSliderY); ATON.UI.elSidePanel.appendChild(lightSliderZ); ATON.UI.elSidePanel.appendChild(envHeading); const ambientOcclSwitch = document.createElement('div'); ambientOcclSwitch.className = 'form-check form-switch ms-4 mt-2'; ambientOcclSwitch.innerHTML = ` `; const shadowsSwitch = document.createElement('div'); shadowsSwitch.className = 'form-check form-switch ms-4 mt-2'; shadowsSwitch.innerHTML = ` `; const lightProbeSwitch = document.createElement('div'); lightProbeSwitch.className = 'form-check form-switch ms-4 mt-2'; lightProbeSwitch.innerHTML = ` `; shadowsSwitch.querySelector('input[type="checkbox"').checked = AppState.shadows; ambientOcclSwitch.querySelector('input[type="checkbox"').checked = AppState.ambientOcclusion; lightProbeSwitch.querySelector('input[type="checkbox"').checked = AppState.lightProbe; ATON.UI.elSidePanel.appendChild(ambientOcclSwitch); ATON.UI.elSidePanel.appendChild(shadowsSwitch); ATON.UI.elSidePanel.appendChild(lightProbeSwitch); // TODO: move somewhere else... document.querySelector('#aoSwitch').addEventListener( 'change', event => { Scene.toggleAmbientOcclusion(event.target.checked); AppState.ambientOcclusion = event.target.checked; } ); document.querySelector('#shadowsSwitch').addEventListener( 'change', event => { const checked = event.target.checked; ATON.toggleShadows(checked); AppState.shadows = checked; } ); // Not working properly? document.querySelector('#lpSwitch').addEventListener( 'change', event => { const checked = event.target.checked; ATON.setAutoLP(checked); //if (!checked) ATON.clearLightProbes(); AppState.lightProbe = checked; if (checked) ATON.updateLightProbes(); console.log('Light probe: ', checked); } ); }); } /** * * @param {String} direction - The axis direction, one of 'x','y','z' * @param {String} label - The slider label * @param {Number[]} range - The slider's range * @param {Number} step - The slider's step */ UI.createLightSlider = function(direction, label, range, step) { const currentVal = ATON.getMainLightDirection()[direction]; const lightSlider = ATON.UI.createSlider({ range, label, value: Number.parseFloat(currentVal).toPrecision(1), oninput: val => { const lightDir = ATON.getMainLightDirection(); // Keep existing direction values for the other axes lightDir[direction] = Number.parseFloat(val); Scene.changeLightDirection(lightDir); }, }); lightSlider.classList.add('ms-4'); lightSlider.querySelector('input').step = step; return lightSlider; } /** * Right-side main menu panel * @param {String} triggerId - The menu button id */ UI.toggleContentMenu = function(triggerId) { const btn = document.querySelector(`#${triggerId}`); btn.addEventListener('click', () => { ATON.UI.setSidePanelRight(); ATON.UI.showSidePanel({header: 'Contenuti'}); // Append tabs, then tab panes const tabs = this.domParser.parseFromString(UI.contentMenuTabs, 'text/html'); ATON.UI.elSidePanel.appendChild(tabs.querySelector('#content-tabs')); ATON.UI.elSidePanel.appendChild(tabs.querySelector('.tab-content')); this.buildLayersMenu(AppState.nodes, ATON.UI.elSidePanel.querySelector('#layer')); // This is just an example, at the moment... to be revised with config -> markers (scenes) -> menu if (AppState.sceneHasAudio) { let btn = this.domParser.parseFromString(UI.audioExample, 'text/html'); const audio = btn.querySelector('#audio-example'); ATON.UI.elSidePanel.querySelector('#media').appendChild(audio); } }); } /** * @todo Don't rebuild it every time the side panel is shown... * @param {Array} nodes The scenes nodes (IDs and status) * @param {HTMLElement} sidePanel ATON's side panel element */ UI.buildLayersMenu = function(nodes, sidePanel) { for(let node of nodes) { const checkboxStr = `
`; let element = this.domParser.parseFromString(checkboxStr, 'text/html'); element = element.querySelector('div.form-check'); sidePanel.appendChild(element); // Will this ever work?? element.addEventListener('change', event => toggleNode(node.id, event.target.checked)); } /** * This is terrible... * @param {String} id * @param {Boolean} status */ const toggleNode = (id, status) => { ATON.getSceneNode(id).toggle(status); AppState.nodes.find(n => n.id === id).active = status; } } export default UI;