615 lines
17 KiB
JavaScript
615 lines
17 KiB
JavaScript
/**
|
|
* @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).querySelectorAll('.row')) {
|
|
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.querySelectorAll('.row')) {
|
|
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);
|
|
}
|
|
|
|
const layerIndex = this.height - z - 1;
|
|
|
|
const submenu = new LayerSubmenu(layerElement);
|
|
submenu.onLayerOn = () => {
|
|
this.setLayer(layerIndex, true);
|
|
}
|
|
submenu.onLayerOff = () => {
|
|
this.setLayer(layerIndex, false);
|
|
}
|
|
submenu.init();
|
|
|
|
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 {HTMLElement} layer
|
|
*/
|
|
const LayerSubmenu = function (layer) {
|
|
this.layer = layer;
|
|
this.html = document.createElement('div');
|
|
this.buttonLayerOn = document.createElement('button');
|
|
this.buttonLayerOff = document.createElement('button');
|
|
|
|
this,onLayerOn = function () {}
|
|
this.onLayerOff = function () {}
|
|
|
|
this.init = function () {
|
|
this.html.classList.add('layer-submenu');
|
|
|
|
const line = document.createElement('div');
|
|
line.classList.add('submenu-line');
|
|
this.html.appendChild(line);
|
|
|
|
const buttons = document.createElement('div');
|
|
buttons.classList.add('layer-buttons');
|
|
this.html.appendChild(buttons);
|
|
|
|
this.buttonLayerOn.classList.add('layer-button', 'layer-button-on');
|
|
this.buttonLayerOn.title = 'Alle LEDs an';
|
|
buttons.appendChild(this.buttonLayerOn);
|
|
|
|
this.buttonLayerOn.addEventListener(
|
|
'click',
|
|
() => {
|
|
this.onLayerOn();
|
|
}
|
|
);
|
|
|
|
this.buttonLayerOff.classList.add('layer-button', 'layer-button-off');
|
|
this.buttonLayerOff.title = 'Alle LEDs aus';
|
|
buttons.appendChild(this.buttonLayerOff);
|
|
|
|
this.buttonLayerOff.addEventListener(
|
|
'click',
|
|
() => {
|
|
this.onLayerOff();
|
|
}
|
|
);
|
|
|
|
this.layer.appendChild(this.html);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @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;
|
|
} |