/** * @param {number} duration */ const Frame = function (duration) { this.layers = []; this.duration = duration; } const LED_COLOR = { WHITE: 0, RED: 1, GREEN: 2, BLUE: 3, } const ANIMATION_FRAME_QUEUE = []; const COLOR_CLASS = []; COLOR_CLASS[LED_COLOR.WHITE] = 'led-white'; COLOR_CLASS[LED_COLOR.RED] = 'led-red'; COLOR_CLASS[LED_COLOR.GREEN] = 'led-green'; COLOR_CLASS[LED_COLOR.BLUE] = 'led-blue'; /** * @param {string} id */ const Selector = function (id) { this.items = []; this.currentItem = 0; this.html = document.getElementById(id); this.htmlDisplay = this.html.querySelector('.selector-display'); this.htmlItems = this.html.querySelector('.selector-items'); this.htmlItems.hidden = true; this.htmlDisplay.addEventListener( 'click', () => { this.htmlItems.hidden = !this.htmlItems.hidden; } ); for (const i in this.htmlItems.children) { this.htmlItems.children.item(i).addEventListener( 'click', () => { this.selectItem(i); } ); } this.onSelect = function () {}; /** * @param {number} index */ this.selectItem = function (index) { this.htmlItems.hidden = true; if (index !== this.currentItem) { this.currentItem = index; this.htmlDisplay.innerHTML = this.htmlItems.children.item(index).innerHTML; this.onSelect(index); } }; } /** * @param {number} width * @param {number} depth * @param {number} height */ const FrameFactory = function (width, depth, height) { this.width = width; this.depth = depth; this.height = height; /** * @param {number} duration * @returns Frame */ this.getFrame = function (duration) { const frame = new Frame(duration); for (let z = 0; z < this.height; z++) { const layer = []; for (let led = 0; led < this.width * this.depth; led++) { layer.push(false); } frame.layers.push(layer); } return frame; } } /** * @param {number} width * @param {number} depth * @param {number} height */ const Cube = function (width, depth, height) { this.width = width; this.depth = depth; this.height = height; this.html = document.getElementById('cube'); this.frames = []; this.currentFrame = 0; this.setup = function () { this._setupHtml(); } /** * @param {number} frameIndex */ this.setCurrentFrame = function (frameIndex) { this.currentFrame = frameIndex; this._loadFrame(frameIndex); } /** * @param {Frame} frame */ this.addFrame = function (frame) { this.frames.push(frame); } /** * @param {number} frameIndex */ this.deleteFrame = function (frameIndex) { this.frames = this.frames.slice(0, frameIndex).concat( this.frames.slice(frameIndex + 1) ); } /** * @param {number} layerIndex * @param {number} ledIndex */ this.switchLed = function (layerIndex, ledIndex) { this.frames[this.currentFrame].layers[layerIndex][ledIndex] = !this.frames[this.currentFrame].layers[layerIndex][ledIndex]; } /** * @param {number} layerIndex * @param {boolean} state */ this.setLayer = function (layerIndex, state) { for (let index = 0; index < this.width * this.depth; index++) { this.frames[this.currentFrame].layers[layerIndex][index] = state; } for (const row of this.html.children.item(this.height - layerIndex - 1).children) { for (const led of row.children) { this._setLed(led, state); } } } /** * @param {LED_COLOR} color */ this.setLedColor = function (color) { for (const layer of this.html.children) { for (const row of layer.children) { for (const led of row.children) { for (const cssClass of COLOR_CLASS) { led.classList.remove(cssClass); } led.classList.add(COLOR_CLASS[color]); } } } } /** * @param {number} frameIndex */ this._loadFrame = function (frameIndex) { for (let layer = 0; layer < this.frames[frameIndex].layers.length; layer++) { for (let row = 0; row < this.depth; row++) { for (let led = 0; led < this.width; led++) { const state = this.frames[frameIndex].layers[layer][row * this.depth + led]; this._setLed(this.html.children.item(this.height - layer - 1).children.item(row).children.item(led), state); } } } } this._setupHtml = function () { for (let z = 0; z < this.height; z++) { const layerElement = document.createElement('div'); layerElement.classList.add('layer'); for (let y = 0; y < this.depth; y++) { const row = document.createElement('div'); row.classList.add('row'); for (let x = 0; x < this.width; x++) { const led = document.createElement('div'); led.classList.add('led', 'led-off'); led.addEventListener( 'mousedown', (event) => { const index = y * this.depth + x; const layer = this.height - z - 1; this.switchLed(layer, index); this._setLed(event.target, this.frames[this.currentFrame].layers[layer][index]); } ); led.addEventListener( 'mouseover', (event) => { if (event.buttons === 1) { const index = y * this.depth + x; const layer = this.height - z - 1; this.switchLed(layer, index); this._setLed(event.target, this.frames[this.currentFrame].layers[layer][index]); } } ); row.appendChild(led); } layerElement.appendChild(row); } this.html.appendChild(layerElement); } } /** * @param {HTMLElement} ledHtml * @param {boolean} state */ this._setLed = function (ledHtml, state) { if (state) { ledHtml.classList.remove('led-off'); } else { ledHtml.classList.add('led-off'); } } } /** * @param {string} id */ const FrameMenu = function (id) { this.html = document.getElementById(id); this.htmlSlider = this.html.querySelector('#frame-slider'); this.htmlDisplayCurrentFrame = this.html.querySelector('#current-frame'); this.htmlDisplayFrames = this.html.querySelector('#frame-number'); this.htmlButtonAddFrameBefore = this.html.querySelector('#button-add-frame-before'); this.htmlButtonAddFrameAfter = this.html.querySelector('#button-add-frame-after'); this.htmlButtonToggleAnimation = this.html.querySelector('#button-toggle-animation'); this.htmlButtonDeleteFrame = this.html.querySelector('#button-delete-frame'); this.htmlInputDuration = this.html.querySelector('#frame-duration'); this.htmlMenuGlobalDuration = this.html.querySelector('#global-duration-popup'); this.htmlButtonGlobalDuration = this.html.querySelector('#button-scale-duration'); this.htmlGlobalDurationSlider = this.html.querySelector('#global-duration-slider'); this.htmlGlobalDurationValue = this.html.querySelector('#global-duration-value'); this.htmlButtonChangeGlobalDuration = this.html.querySelector('#button-change-global-duration'); this.htmlButtonCloseGlobalDurationPopup = this.html.querySelector('#button-close-menu'); this.isAnimationPlaying = false; this.htmlSlider.addEventListener( 'input', () => { this._updateFramePosition(); } ); this.htmlButtonAddFrameBefore.addEventListener( 'click', () => { this._increaseSliderMax(); this.onFramePrepend(); } ); this.htmlButtonAddFrameAfter.addEventListener( 'click', () => { this._increaseSliderMax(); this.onFrameAppend(); } ); this.htmlInputDuration.addEventListener( 'input', () => { this.onInputDuration(this.htmlInputDuration.value); } ); this.htmlButtonToggleAnimation.addEventListener( 'click', () => { this.isAnimationPlaying = !this.isAnimationPlaying; this._updateToggleButton(); this.onToggleAnimation(this.isAnimationPlaying); } ); this.htmlButtonDeleteFrame.addEventListener( 'click', () => { this._decreaseSliderMax(); this.onFrameDelete(); } ); this.htmlButtonGlobalDuration.addEventListener( 'click', () => { this.htmlMenuGlobalDuration.style.display = 'inherit'; } ); this.htmlGlobalDurationSlider.addEventListener( 'input', () => { this.htmlGlobalDurationValue.innerText = this.htmlGlobalDurationSlider.value; } ); this.htmlButtonChangeGlobalDuration.addEventListener( 'click', () => { this.onChangeGlobalDuration(); } ); this.htmlButtonCloseGlobalDurationPopup.addEventListener( 'click', () => { this.htmlMenuGlobalDuration.style.display = 'none'; } ) /** * @param {number} frameIndex */ this.slideToFrame = function (frameIndex) { this.htmlSlider.value = frameIndex + 1; this._updateFramePosition(); } /** * @param {number} frameIndex */ this.setDuration = function (duration) { this.htmlInputDuration.value = duration; } this.startAnimation = function () { this.isAnimationPlaying = false; this._updateToggleButton(); } this.stopAnimation = function () { this.isAnimationPlaying = false; this._updateToggleButton(); } this.onFramePrepend = function () {} this.onFrameAppend = function () {} this.onFrameDelete = function () {} this.onSlide = function () {} this.onInputDuration = function () {} this.onToggleAnimation = function () {} this.onChangeGlobalDuration = function () {} this._increaseSliderMax = function () { const frameNumber = parseInt(this.htmlSlider.max) + 1; this.htmlSlider.max = frameNumber; this.htmlDisplayFrames.innerText = frameNumber; if (frameNumber > 1) { this.htmlButtonToggleAnimation.disabled = false; } this.htmlButtonDeleteFrame.disabled = false; } this._decreaseSliderMax = function () { const frameNumber = parseInt(this.htmlSlider.max) - 1; this.htmlSlider.max = frameNumber; this.htmlDisplayFrames.innerText = frameNumber; if (frameNumber < 2) { this.htmlButtonToggleAnimation.disabled = true; this.htmlButtonDeleteFrame.disabled = true; } } this._updateFramePosition = function () { this.htmlDisplayCurrentFrame.innerText = this.htmlSlider.value; this.onSlide(this.htmlSlider.value - 1); } this._updateToggleButton = function () { if (this.isAnimationPlaying) { this.htmlButtonToggleAnimation.classList.add('icon-button-stop'); this.htmlButtonToggleAnimation.classList.remove('icon-button-play'); } else { this.htmlButtonToggleAnimation.classList.add('icon-button-play'); this.htmlButtonToggleAnimation.classList.remove('icon-button-stop'); } const elements = [ this.htmlSlider, this.htmlButtonAddFrameAfter, this.htmlButtonAddFrameBefore, this.htmlInputDuration, this.htmlButtonDeleteFrame, ]; for (const element of elements) { element.disabled = this.isAnimationPlaying; } } } const WIDTH = 4; const DEPTH = 4; const HEIGHT = 4; const cube = new Cube(WIDTH, DEPTH, HEIGHT); cube.setup(); const frameFactory = new FrameFactory(WIDTH, DEPTH, HEIGHT); cube.frames.push(frameFactory.getFrame(500)); cube.setCurrentFrame(0); cube.setLedColor(LED_COLOR.RED); const colorSelector = new Selector('color-selector'); colorSelector.onSelect = (index) => { cube.setLedColor(index); }; colorSelector.selectItem(LED_COLOR.RED); const frameMenu = new FrameMenu('frame-menu'); frameMenu.onSlide = (frameIndex) => { cube.setCurrentFrame(frameIndex); frameMenu.setDuration(cube.frames[cube.currentFrame].duration); } frameMenu.onFramePrepend = () => { cube.frames = cube.frames.slice(0, cube.currentFrame).concat( [frameFactory.getFrame(cube.frames[cube.currentFrame].duration)].concat( cube.frames.slice(cube.currentFrame) ) ); frameMenu.slideToFrame(cube.currentFrame - 1); } frameMenu.onFrameAppend = () => { cube.frames = cube.frames.slice(0, cube.currentFrame + 1).concat( [frameFactory.getFrame(cube.frames[cube.currentFrame].duration)].concat( cube.frames.slice(cube.currentFrame + 1) ) ); frameMenu.slideToFrame(cube.currentFrame + 1); } frameMenu.onInputDuration = (duration) => { if (+duration <= 0) { return; } cube.frames[cube.currentFrame].duration = +duration; } frameMenu.onToggleAnimation = (isPlaying) => { if (isPlaying) { let duration = 0; for (let frame = cube.currentFrame; frame < cube.frames.length; frame++) { duration += cube.frames[frame].duration; if (frame === cube.frames.length - 1) { setTimeout( () => { frameMenu.stopAnimation(); }, duration ); } ANIMATION_FRAME_QUEUE.push( setTimeout( () => { frameMenu.slideToFrame(frame + 1); }, duration ) ) } } else { for (const frame of ANIMATION_FRAME_QUEUE) { clearTimeout(frame); } frameMenu.stopAnimation(); } } frameMenu.onFrameDelete = () => { if (cube.frames.length < 2) { return; } cube.deleteFrame(cube.currentFrame); cube.currentFrame--; cube.currentFrame = cube.currentFrame < 0 ? 0 : cube.currentFrame; frameMenu.slideToFrame(cube.currentFrame); } frameMenu.onChangeGlobalDuration = () => { const factor = frameMenu.htmlGlobalDurationSlider.value / 100; for (const frame of cube.frames) { frame.duration *= factor; } frameMenu.htmlInputDuration.value = cube.frames[cube.currentFrame].duration; frameMenu.htmlMenuGlobalDuration.style.display = 'none'; frameMenu,htmlGlobalDurationSlider.value = 100; }