diff --git a/css/styles.css b/css/styles.css index c2cb9a3..85590bf 100644 --- a/css/styles.css +++ b/css/styles.css @@ -48,29 +48,131 @@ canvas { } /* Panels */ -#modesPanel, #aboutPanel, #controlPanel, #modeInfoPanel { +#modesPanel, #welcomePanel, #aboutPanel, #animationPanel, #colorsetPanel, #patternPanel, #modeInfoPanel, #colorPickerPanel { background-color: #181a1b; border: 1px solid #3e4446; border-radius: 5px; color: #d8d4cf; - padding: 10px; + padding: 5px; position: absolute; width: 370px; } +#welcomePanel { + top: 75px; + left: 50%; /* Center the div */ + width: 900px; + transform: translate(-25%, 0%); + text-align: left; + z-index: 50; +} + +#welcomePanel label { + font-size: 20px; +} +#welcomePanel input { + height: 16px; + width: 16px; +} + +#welcomePanel p { + font-size: 20px; +} + +#welcomePanel h2 { + margin-bottom: 0; +} + +/* public/css/components/welcome-panel.css */ +.wiki-button { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 12px 20px; + font-size: 18px; + font-weight: bold; + text-decoration: none; + color: white; + background-color: #28a745; /* Green color */ + border-radius: 8px; + transition: background-color 0.3s ease, transform 0.2s ease; + margin: 20px 0; +} + +.wiki-button .arrow { + margin-left: 10px; + font-size: 20px; +} + +.wiki-button:hover { + background-color: #218838; /* Darker green on hover */ + transform: translateY(-2px); +} + +.wiki-button:active { + background-color: #1e7e34; /* Even darker green on click */ + transform: translateY(1px); +} + + #aboutPanel { - left: 10px; + left: 5px; + top: 5px; text-align: center; - top: 10px; z-index: 3; } -#controlPanel { - left: 10px; - top: 60px; +#patternPanel { + left: 5px; + top: 103px; + z-index: 3; +} + +#colorsetPanel { + left: 5px; + top: 495px; z-index: 3; } +/* Color Picker Panel Specific Styles */ +#colorPickerPanel { + top: 417px; + left: 392px; + width: 480px; + z-index: 5; /* Ensure it stacks above other UI elements */ +} + +.color-picker-controls { + margin: 0px; + margin-top: 0px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.color-picker-panel .color-picker-top-section, +.color-picker-panel .color-picker-bottom-section { + width: 100%; +} + +.color-picker-panel .sv-box-container, +.color-picker-panel .sliders-section, +.color-picker-panel .radial-hue-cone-container, +.color-picker-panel .input-box-container { + margin-bottom: 10px; +} + +.color-picker-panel .sv-box-container { + display: flex; + justify-content: center; + align-items: center; +} + +.color-picker-panel .input-group { + display: flex; + justify-content: space-between; +} + #modeInfoPanel { left: 10px; top: 263px; @@ -78,8 +180,8 @@ canvas { } #modesPanel { - top: 10px; - right: 10px; + top: 5px; + right: 5px; z-index: 3; } @@ -169,7 +271,7 @@ canvas { fieldset { border: 1px solid #3e4446; border-radius: 5px; - margin-bottom: 15px; + margin-bottom: 5px; padding: 10px; } @@ -178,10 +280,15 @@ legend { padding: 0 5px; } -#patternDropdown { - margin-bottom: 5px; +#patternDivider { + margin-left: 10px; margin-right: 10px; - width: 230px; + border-color: rgba(0, 0, 0, 0.3); +} + +#patternDropdown { + width: 100%; + margin-right: 5px; background-color: #333; /* Dark background */ color: #fff; /* Light text color for contrast */ border: 1px solid #555; /* Dark border for distinction */ @@ -284,6 +391,7 @@ legend { font-size: 28px; font-weight: bold; user-select:none; + margin-right: 10px; } .delete-color:hover, @@ -596,6 +704,26 @@ legend { background-color: #218838; } + +.close-btn { + color: #aaa; + margin-bottom: -8px; + margin-top: -6px; + font-size: 28px; + font-weight: bold; + margin-right: 5px; + cursor: pointer; + user-select: none; +} + +.close-btn:hover, +.close-btn:focus { + color: red; + text-decoration: none; + cursor: pointer; + user-select:none; +} + .close { color: #aaa; float: right; @@ -680,6 +808,11 @@ legend { outline: none; } +#hexInputLabel { + /* make the word 'Hex' line up with 'H' and 'R' */ + margin-left: -10px; +} + #deviceImageContainer { position: relative; width: 100%; /* Take full width of the container */ @@ -696,6 +829,16 @@ legend { border: none; } +.led-overlay { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + pointer-events: none; /* Prevent blocking interactions with the image */ + z-index: 2; /* Ensure it appears above the image */ +} + .led-indicator { position: absolute; width: 13px; /* Adjust size */ @@ -704,6 +847,7 @@ legend { border-radius: 50%; /* Circular indicators */ cursor: pointer; /* Change cursor on hover */ transform: translate(-50%, -50%); + z-index: 3; /* Ensure indicators are above other UI elements */ } .led-indicator.selected { @@ -712,6 +856,7 @@ legend { #ledControls { display: flex; + justify-content: space-between; gap: 5px; /* Space between buttons */ margin-top: 10px; /* Space above the button group */ } @@ -720,7 +865,7 @@ legend { text-align: center; /* Center text */ text-decoration: none; /* Remove underline */ display: inline-block; /* Display as inline-block */ - font-size: 14px; /* Font size */ + font-size: 12px; /* Font size */ background-color: #202020; color: #d8d4cf; border: 1px solid #454545; @@ -795,7 +940,7 @@ legend { position: absolute; background-color: #121212; box-shadow: 0 8px 16px rgba(0, 0, 0, 0.2); - z-index: 1; + z-index: 5; width: 100%; } @@ -876,41 +1021,17 @@ legend { opacity: 0.6; } -.color-entry { - width: 50px; - height: 24px; - cursor: pointer; - display: inline-block; - border-radius: 4px; - margin-right: 10px; - margin-left: 5px; - border: 2px solid #090c0e; -} - .color-container { display: flex; align-items: center; margin-bottom: 5px; } -.color-picker-modal-content { - display: flex; - flex-direction: column; - align-items: center; - width: 100%; - max-width: 600px; - margin: auto; - position: relative; - z-index: 2; -} - - .color-picker-top-section { display: flex; width: 100%; - justify-content: left; /* Distribute space between SV box and sliders */ + justify-content: space-between; /* Distribute space between SV box and sliders */ flex-wrap: wrap; /* Ensure elements wrap to the next line if necessary */ - margin-bottom: 20px; position: relative; z-index: 1; } @@ -918,8 +1039,9 @@ legend { .color-picker-bottom-section { display: flex; width: 100%; + margin-top: 15px; justify-content: space-between; /* Align hue circle and input boxes */ - align-items: left; + align-items: center; flex-wrap: wrap; position: relative; z-index: 3; @@ -931,9 +1053,9 @@ legend { } .color-input { - width: 45px; + width: 34px; height: 24px; - padding: 5px; + padding: 3px; background-color: #292c2e; border: 1px solid #454545; border-radius: 4px; @@ -987,23 +1109,28 @@ legend { .sliders-section { padding: 0; - margin-top: -20px; - margin-left: 35px; + margin-left: 65px; } .sv-box-container { + display: flex; + flex-direction: column; /* Stack label and slider */ + align-items: left; width: 200px; /* Minimum width to prevent squishing */ - height: 200px; + height: auto; margin-right: 15px; position: relative; } +.sv-box-container label { + margin-bottom: 5px; +} + .sv-box { width: 100%; height: 100%; background: linear-gradient(to top, black, transparent), linear-gradient(to right, white, hsl(0, 100%, 50%)); border-radius: 4px; - margin-top: 5px; cursor: crosshair; position: relative; border: 1px solid #a3a3a3; @@ -1021,8 +1148,14 @@ legend { } .hue-slider-container { + display: flex; + flex-direction: column; /* Stack label and slider */ + align-items: center; width: 30px; - height: 200px; /* Match SV box height */ +} + +.hue-slider-container label { + margin-bottom: 5px; } .hue-slider { @@ -1032,7 +1165,6 @@ legend { border: 1px solid #454545; background: linear-gradient(to bottom, red, yellow, lime, cyan, blue, magenta, red); border-radius: 4px; - margin-top: 5px; cursor: pointer; } @@ -1162,6 +1294,7 @@ legend { display: flex; justify-content: flex-start; /* Align items to the left */ z-index: 1; /* Lower z-index to ensure it's behind inputs */ + margin-left: 10px; } .radial-hue-cone { @@ -1201,11 +1334,10 @@ legend { .hue-labels { position: absolute; - top: 50%; - left: 50%; + top: 10px; + left: 10px; width: 100%; /* Ensure the container covers the whole circle */ height: 100%; - transform: translate(-47%, -47%); pointer-events: none; z-index: 3; display: flex; @@ -1376,3 +1508,398 @@ i { background-color: #777; /* Even lighter when pressed */ } + + + + +/* Animation panel stuff */ +#animationPanel { + top: 54px; + left: 5px; + background-color: #181a1b; + border: 1px solid #3e4446; + border-radius: 5px; + color: #d8d4cf; + position: absolute; + z-index: 2; + transition: transform 0.4s ease; /* Sliding animation */ +} + +.animation-button { + width: 40px; + height: 40px; + margin: 5px; +} + +.animation-buttons-container { + display: flex; + justify-content: space-between; + padding: 10px; + padding-top: 0; + padding-bottom: 0; +} + +.animation-buttons-container { + display: flex; + gap: 10px; + margin-bottom: 10px; +} + +.animation-button { + width: 50px; + height: 50px; + display: flex; + align-items: center; + justify-content: center; + font-size: 24px; + border: 1px solid #ccc; + border-radius: 8px; + background: #202020; + cursor: pointer; + transition: background 0.2s, transform 0.1s; +} + +.animation-button:hover { + background: #e0e0e0; + transform: scale(1.1); +} + +.animation-button i { + pointer-events: none; /* Ensure clicks pass through to the button */ +} + + +/* Pattern panel stuff */ +.control-line { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 8px; + height: 30px; /* Consistent height for rows */ +} + +.control-slider-container { + position: relative; + flex: 5; + height: 100%; + display: flex; + align-items: center; /* Align slider and label vertically */ + overflow: hidden; /* Prevent overlapping visuals */ +} + +.control-label { + position: absolute; + z-index: 2; + font-weight: bold; + color: #d8d4cf; + left: 16px; + top: 50%; + transform: translateY(-50%); /* Center vertically */ + pointer-events: none; /* Ensure it doesn’t interfere with slider interaction */ +} + +.control-slider { + -webkit-appearance: none; + appearance: none; + width: 100%; + height: 100%; + background: linear-gradient(to right, #5e5e5e 50%, #3e3e3e 50%); /* Placeholder gradient */ + margin-right: 0; + outline: none; + position: relative; + border-top-left-radius: 4px; /* Ensure full rounding */ + border-bottom-left-radius: 4px; /* Ensure full rounding */ + position: relative; /* Needed for ::before */ +} + +.control-slider::-webkit-slider-runnable-track { + width: 100%; + height: 100%; + background: linear-gradient(to right, #5e5e5e var(--slider-fill), #3e3e3e var(--slider-fill)); /* Dynamic fill */ + border: none; +} + +.control-slider::-webkit-slider-thumb { + -webkit-appearance: none; + appearance: none; + width: 2px; + height: 100%; + background: #111; /* The draggable "line" */ + cursor: pointer; + z-index: 3; /* Ensure it's above the slider track */ +} + +.control-slider::-moz-range-track { + width: 100%; + height: 100%; + background: linear-gradient(to right, #5e5e5e var(--slider-fill), #3e3e3e var(--slider-fill)); + border: none; +} + +.control-slider::-moz-range-thumb { + width: 2px; + height: 100%; + background: #111; + cursor: pointer; + border: none; +} + +.control-input { + flex: 1; + height: 30px; + background: #292c2e; + color: #d8d4cf; + border: none; + text-align: center; + margin: 0; + padding: 0; + border: 1px solid black; + border-top-right-radius: 5px; + border-bottom-right-radius: 5px; +} + +.control-input:focus { + outline: none; +} + +.control-slider-container.disabled .control-slider.disabled { + background: #111111; + pointer-events: none; +} + +.control-slider-container.disabled .control-label { + color: #333; +} + +.control-input.disabled { + background: #1e1e1e; + pointer-events: none; +} + + + +#patternDropdownContainer { + padding: 0; + display: flex; + align-items: center; + justify-content: space-between; +} + +.pattern-buttons { + display: flex; + gap: 5px; +} + +#patternRandomizeButton { + font-size: 22px; + padding: 1px; + margin-left: 5px; + margin-right: 5px; +} + +.icon-button { + width: 32px; + height: 32px; + border: none; + border-radius: 4px; + background-color: #333; + color: white; + font-size: 14px; + align-items: center; + justify-content: center; + cursor: pointer; + transition: background-color 0.2s; + border: 1px solid black; +} + +.help-button { + width: 32px; + height: 32px; + border: none; + background: none; + color: white; + font-size: 18px; + align-items: center; + justify-content: center; + cursor: pointer; +} + +#githubLink { + font-size: 18px; + background: none; + padding: 0; + margin: 0; +} + +.icon-button:hover { + background-color: #444; +} + +.icon-button:active { + background-color: #555; +} + + + +/* about panel stuff */ +.about-panel-content { + display: flex; + align-items: center; + justify-content: space-between; +} + +.about-label { + font-size: 16px; + color: #d8d4cf; +} + +.action-buttons { + display: flex; + gap: 10px; +} + +.icon-button { + background: none; + border: none; + color: inherit; + font-size: 20px; + cursor: pointer; + padding: 5px; + transition: transform 0.2s, color 0.2s; +} + +.icon-button:hover { + color: #66ff66; + transform: scale(1.1); +} + +.icon-button:active { + transform: scale(1); +} + +.icon-button i { + pointer-events: none; /* Prevent accidental selection of the icon */ +} + + + +/* colorset panel stuff */ +#colorset { + display: column; + grid-template-columns: repeat(4, 1fr); /* 4 columns */ + gap: 10px; /* Space between slots */ +} + +.color-box { + display: flex; + align-items: center; + justify-content: space-between; + border: 2px solid #444; /* Border for all slots */ + border-bottom-right-radius: 5px; + border-top-right-radius: 5px; + border-radius: 5px; + padding: 0; + margin-left: 0; + margin-bottom: 3px; + height: 40px; /* Fixed height */ + box-sizing: border-box; /* Include padding in size */ + overflow: none; +} + +.color-box.empty { + background: #1a1a1a; /* Dark background for empty slots */ + border: 2px dashed #666; /* Dotted border for empty slots */ + color: #999; /* Placeholder text color */ + justify-content: center; /* Center content */ + overflow: none; +} + +.color-entry { + width: 50px; + display: inline-block; + flex: 0 0 30%; /* Take 30% of the box width */ + height: 100%; + cursor: pointer; + border: none; + border-right: 2px solid #333; + margin-left: 0; + border-top-left-radius: 3px; + border-bottom-left-radius: 3px; + transition: flex 0.3s ease; /* Smooth transition for width */ +} + +.color-hex-input { + flex: 1; /* Fill remaining space */ + border: none; + background: transparent; + color: #fff; + padding: 0 5px; + text-align: center; +} + +.color-box.empty:hover { + background: #222; /* Slightly darker on hover */ + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.color-box .color-hex-input:focus { + border-top-left-radius: 5px; + border-bottom-left-radius: 5px; +} + +.color-box .color-entry:hover { + flex: 0 0 50%; +} + + + +/* draggable panels */ + +.draggable-panel { + position: absolute; + border: 1px solid #3e4446; + border-radius: 5px; + background-color: #181a1b; + user-select: none; +} + +.panel-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 5px; + padding-left: 7px; + background-color: #292c2e; + border-bottom: 1px solid #3e4446; + cursor: move; /* Indicate draggable for header */ + border-radius: 5px; +} + +.panel-title { + font-size: 14px; + font-weight: bold; + color: #d8d4cf; +} + +.collapse-btn { + background: none; + border: none; + color: white; + font-size: 14px; + cursor: pointer; +} + +.collapse-btn:hover { + color: #66ff66; +} + +.panel-content { + display: flex; + flex-direction: column; + height: auto; + padding-top: 10px; + padding: 10px; +} + + diff --git a/index.html b/index.html index 5119cf4..c1ed6b5 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,5 @@ - diff --git a/js/AboutPanel.js b/js/AboutPanel.js index 1b21ef5..cfcb753 100644 --- a/js/AboutPanel.js +++ b/js/AboutPanel.js @@ -1,20 +1,56 @@ -/* AboutPanel.js */ -import Panel from './Panel.js' +import Panel from './Panel.js'; export default class AboutPanel extends Panel { - constructor(lightshow, vortexPort) { + constructor(editor) { const content = ` - - - - - `; - super('aboutPanel', content); - this.lightshow = lightshow - this.vortexPort = vortexPort; +
+ +
+ + +
+
+ `; + super('aboutPanel', content, 'Help & About'); + this.editor = editor; + this.lightshow = editor.lightshow; + this.vortexPort = editor.vortexPort; } initialize() { - // ... + this.addClickListener('patternHelpButton', this.showHelp); + this.addClickListener('githubLinkButton', this.gotoGithub); + // collapse the about panel by default + this.toggleCollapse(false); + } + + addClickListener(buttonId, callback) { + const button = document.getElementById(buttonId); + if (button) { + button.addEventListener('click', callback.bind(this)); + } else { + console.error(`Button with ID ${buttonId} not found.`); + } + } + + gotoGithub() { + try { + window.open("https://github.com/StoneOrbits/VortexEngine", '_blank').focus(); + } catch (error) { + console.error("Could not open GitHub link:", error); + } + } + + showHelp() { + try { + window.open("https://stoneorbits.github.io/VortexEngine/lightshow_lol.html", '_blank').focus(); + } catch (error) { + console.error("Could not open help link:", error); + } } } + diff --git a/js/AnimationPanel.js b/js/AnimationPanel.js new file mode 100644 index 0000000..9bbe2a8 --- /dev/null +++ b/js/AnimationPanel.js @@ -0,0 +1,143 @@ +import Panel from './Panel.js'; + +export default class AnimationPanel extends Panel { + constructor(editor) { + const controls = [ + { + id: 'tickRate', + type: 'range', + min: 1, + max: 30, + default: 3, + label: 'Speed', + update: value => editor.lightshow.tickRate = value, + }, + { + id: 'trailSize', + type: 'range', + min: 1, + max: 300, + default: 100, + label: 'Trail', + update: value => editor.lightshow.trailSize = value, + }, + { + id: 'dotSize', + type: 'range', + min: 5, + max: 50, + default: 25, + label: 'Size', + update: value => editor.lightshow.dotSize = value, + }, + { + id: 'blurFac', + type: 'range', + min: 1, + max: 10, + default: 5, + label: 'Blur', + update: value => editor.lightshow.blurFac = value, + }, + { + id: 'circleRadius', + type: 'range', + min: 0, + max: 600, + default: 400, + label: 'Radius', + update: value => editor.lightshow.circleRadius = value, + }, + { + id: 'spread', + type: 'range', + min: 0, + max: 100, + default: 15, + label: 'Spread', + update: value => editor.lightshow.spread = parseInt(value), + }, + ]; + + const content = ` +
+ + + + +
+
+ ${AnimationPanel.generateControlsContent(controls)} +
+ `; + + super('animationPanel', content, 'Animation'); + + this.editor = editor; + this.lightshow = editor.lightshow; + this.controls = controls; + this.isVisible = true; + } + + static generateControlsContent(controls) { + return controls.map(control => ` +
+ + +
+ `).join(''); + } + + initialize() { + const panelElement = document.getElementById('animationPanel'); + + // Attach event listeners to controls + this.controls.forEach(control => { + const element = this.panel.querySelector(`#${control.id}`); + element.addEventListener('input', event => { + control.update(event.target.value); + }); + }); + + // Attach event listeners to shape buttons + this.attachShapeButtonListeners(); + + // hide the spread slider + document.getElementById('spread_div').style.display = 'none'; + + // collapse the animation panel by default + this.toggleCollapse(false); + } + + attachShapeButtonListeners() { + const shapes = [ + { id: 'renderCircleButton', shape: 'circle', label: 'Circle' }, + { id: 'renderInfinityButton', shape: 'figure8', label: 'Infinity' }, + { id: 'renderHeartButton', shape: 'heart', label: 'Heart' }, + { id: 'renderBoxButton', shape: 'box', label: 'Box' }, + ]; + + shapes.forEach(({ id, shape, label }) => { + const button = this.panel.querySelector(`#${id}`); + button.addEventListener('click', () => { + this.lightshow.setShape(shape); + this.lightshow.angle = 0; // Reset angle + }); + }); + } +} + diff --git a/js/ColorPicker.js b/js/ColorPickerPanel.js similarity index 62% rename from js/ColorPicker.js rename to js/ColorPickerPanel.js index 258c60c..2717385 100644 --- a/js/ColorPicker.js +++ b/js/ColorPickerPanel.js @@ -1,117 +1,128 @@ -import Modal from './Modal.js'; +import Panel from './Panel.js'; -export default class ColorPicker { - constructor(lightshow) { - this.lightshow = lightshow; - this.modals = []; +export default class ColorPickerPanel extends Panel { + constructor(editor) { + super('colorPickerPanel', '', 'Color Picker', { showCloseButton: true }); // Pass id and title to Panel + this.editor = editor; + this.lightshow = editor.lightshow; this.selectedIndex = 0; this.colorState = { r: 0, g: 0, b: 0, h: 0, s: 0, v: 0 }; + this.preventPropagation = false; // Prevent infinite update loops } - rgbToHsv(r, g, b) { - const RGBCol = new this.lightshow.vortexLib.RGBColor(r, g, b); - const HSVCol = this.lightshow.vortexLib.rgb_to_hsv_generic(RGBCol); - return { h: HSVCol.hue, s: HSVCol.sat, v: HSVCol.val }; + initialize() { + this.initColorPickerContent(); + + // Automatically append to the document body + this.appendTo(document.body); + this.hide(); // Initially hide the panel } - hsvToRgb(h, s, v) { - const HSVCol = new this.lightshow.vortexLib.HSVColor(h, s, v); - const RGBCol = this.lightshow.vortexLib.hsv_to_rgb_generic(HSVCol); - return { r: RGBCol.red, g: RGBCol.green, b: RGBCol.blue }; + initColorPickerContent() { + // Create and structure the content container for the color picker + this.contentContainer.innerHTML = `
`; } - openColorPickerModal(index, colorSet, updateColorCallback) { + openColorPicker(index, colorSet, updateColorCallback) { const col = colorSet.get(index); - - // Create or reuse the modal for the current index - if (!this.modal) { - this.modal = new Modal('color_picker'); - } this.selectedIndex = index; const { h, s, v } = this.rgbToHsv(col.red, col.green, col.blue); this.colorState = { r: col.red, g: col.green, b: col.blue, h, s, v }; - // Show the modal with the current color - this.modal.show({ - title: 'Edit Color', - blurb: `
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- - -
-
- - -
-
- - -
-
-
-
-
-
-
-
-
-
-
-
R
-
Y
-
G
-
C
-
B
-
P
-
-
-
-
- - - - - - -
-
- - - - - - -
-
- - -
-
-
-
`, - }); - - // Initialize color picker controls - this.initColorPickerControls(updateColorCallback); + const controlsContainer = this.contentContainer.querySelector('.color-picker-controls'); + controlsContainer.innerHTML = this.createColorPickerHTML(h, s, v, col); - // Initialize the hue circle + this.show(); // Show the panel + this.initColorPickerControls(updateColorCallback); this.initHueCircle(h); } + rgbToHsv(r, g, b) { + const RGBCol = new this.lightshow.vortexLib.RGBColor(r, g, b); + const HSVCol = this.lightshow.vortexLib.rgb_to_hsv_generic(RGBCol); + return { h: HSVCol.hue, s: HSVCol.sat, v: HSVCol.val }; + } + + hsvToRgb(h, s, v) { + const HSVCol = new this.lightshow.vortexLib.HSVColor(h, s, v); + const RGBCol = this.lightshow.vortexLib.hsv_to_rgb_generic(HSVCol); + return { r: RGBCol.red, g: RGBCol.green, b: RGBCol.blue }; + } + + createColorPickerHTML(h, s, v, col) { + return ` +
+
+ +
+
+
+
+
+ +
+
+
+
+
+
+
+ + +
+
+ + +
+
+ + +
+
+
+
+
+
+
+
+
+
+
+
R
+
Y
+
G
+
C
+
B
+
P
+
+
+
+
+ + + + + + +
+
+ + + + + + +
+
+ + +
+
+
+ `; + } + initColorPickerControls(updateColorCallback) { const hueSlider = document.querySelector('.hue-slider'); const hueSelector = document.querySelector('.hue-selector'); @@ -147,7 +158,13 @@ export default class ColorPicker { // Update all color-related elements and call the external callback const updateColorUI = (isDragging = false) => { + if (this.preventPropagation) return; + + this.preventPropagation = true; + const { r, g, b, h, s, v } = this.colorState; + + // Update controls redSlider.value = r; greenSlider.value = g; blueSlider.value = b; @@ -157,45 +174,54 @@ export default class ColorPicker { hueInput.value = Math.round(h); satInput.value = Math.round(s); valInput.value = Math.round(v); - hexInput.value = `#${((1 << 24) + (r << 16) + (g << 8) + b) - .toString(16) - .slice(1)}`; + hexInput.value = `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`; this.updateSvBoxBackground(h); this.updateSvSelector(s, v); this.setHueSlider(h); this.setHueIndicator(h); + + // Trigger external callback updateColorCallback( this.selectedIndex, `#${((1 << 24) + (r << 16) + (g << 8) + b).toString(16).slice(1)}`, - isDragging // Pass isDragging to callback + isDragging ); + + this.preventPropagation = false; }; // Handler functions for different input changes const handleRgbSliderChange = (isDragging) => { + if (this.preventPropagation) return; + + // Get the current RGB values const r = parseInt(redSlider.value, 10) & 0xff; const g = parseInt(greenSlider.value, 10) & 0xff; const b = parseInt(blueSlider.value, 10) & 0xff; + + // Update color state and propagate changes const { h, s, v } = this.rgbToHsv(r, g, b); this.colorState = { r, g, b, h, s, v }; + + // Update UI immediately updateColorUI(isDragging); }; const handleRgbInputChange = (event) => { - let input = event.target; + if (this.preventPropagation) return; + + const input = event.target; let value = parseInt(input.value, 10); - if (isNaN(value)) { - value = 0; - } + if (isNaN(value)) value = 0; value = Math.max(0, Math.min(255, value)); - input.value = value; const r = parseInt(redInput.value, 10) & 0xff; const g = parseInt(greenInput.value, 10) & 0xff; const b = parseInt(blueInput.value, 10) & 0xff; + const { h, s, v } = this.rgbToHsv(r, g, b); this.colorState = { r, g, b, h, s, v }; updateColorUI(); @@ -247,29 +273,34 @@ export default class ColorPicker { }; const handleHueConeChange = (event, isDragging) => { - const hueCone = document.querySelector('.radial-hue-cone'); + if (this.preventPropagation) return; + const rect = hueCone.getBoundingClientRect(); const x = event.clientX - rect.left - hueCone.offsetWidth / 2; const y = event.clientY - rect.top - hueCone.offsetHeight / 2; const angle = Math.atan2(-y, x) * (180 / Math.PI); const correctedAngle = angle < 0 ? angle + 360 : angle; const hue = (correctedAngle / 360) * 255; + const { s, v } = this.colorState; const { r, g, b } = this.hsvToRgb(hue, s, v); + this.colorState = { r, g, b, h: hue, s, v }; updateColorUI(isDragging); }; const startMoveListener = (event, moveHandler) => { - let isDragging = true; // Set dragging flag to true when mouse starts moving + let isDragging = true; - const moveEventHandler = (moveEvent) => moveHandler(moveEvent, isDragging); + const moveEventHandler = (moveEvent) => { + moveHandler(moveEvent, isDragging); + }; const stopDragging = () => { - isDragging = false; // Reset dragging flag when mouse stops + isDragging = false; document.removeEventListener('mousemove', moveEventHandler); document.removeEventListener('mouseup', stopDragging); - updateColorUI(false); + updateColorUI(false); // Final update after dragging ends }; moveHandler(event, isDragging); @@ -288,20 +319,23 @@ export default class ColorPicker { startMoveListener(event, handleHueConeChange) ); - // Handle RGB sliders with mousedown for dragging behavior const handleRgbSliderMouseDown = (event, slider, handler) => { - let isDragging = true; // Set dragging flag to true when mouse starts moving + let isDragging = false; - const handleRgbDrag = () => handler(isDragging); + const handleRgbDrag = () => { + isDragging = true; // Now dragging + handler(true); // Call handler with dragging state + }; const stopRgbDragging = () => { - isDragging = false; document.removeEventListener('mousemove', handleRgbDrag); document.removeEventListener('mouseup', stopRgbDragging); - updateColorUI(false); + handler(false); // Final update when dragging stops }; - handler(isDragging); + // Immediate update on click + handler(false); + document.addEventListener('mousemove', handleRgbDrag); document.addEventListener('mouseup', stopRgbDragging, { once: true }); }; @@ -343,7 +377,7 @@ export default class ColorPicker { const svBox = document.querySelector('.sv-box'); svBox.style.background = `linear-gradient(to top, black, transparent), linear-gradient(to right, white, hsl(${(hue / 255) * 360}, 100%, 50%))`; } - + updateSvSelector(sat, val) { const svBox = document.querySelector('.sv-box'); const rect = svBox.getBoundingClientRect(); @@ -361,5 +395,6 @@ export default class ColorPicker { const topPosition = (hue / 255) * rect.height; hueSelector.style.top = `${topPosition}px`; } + } diff --git a/js/ColorsetPanel.js b/js/ColorsetPanel.js new file mode 100644 index 0000000..e96aedb --- /dev/null +++ b/js/ColorsetPanel.js @@ -0,0 +1,220 @@ +import Panel from './Panel.js'; +import Modal from './Modal.js'; +import Notification from './Notification.js'; +import ColorPickerPanel from './ColorPickerPanel.js'; + +export default class ColorsetPanel extends Panel { + constructor(editor) { + const content = ` +
+ `; + + super('colorsetPanel', content, 'Colorset'); + this.editor = editor + this.lightshow = editor.lightshow; + this.vortexPort = editor.vortexPort; + this.targetLed = 0; + this.targetLeds = [ this.targetLed ]; + this.isMulti = false; + this.multiEnabled = false; + } + + initialize() { + this.refresh(); + // Listen for the modeChange event + document.addEventListener('modeChange', (event) => { + //console.log("Mode change detected by control panel, refreshing"); + const selectedLeds = event.detail; + // if array is just multi do this: + if (selectedLeds.includes('multi')) { + this.setTargetMulti(); + } else { + this.setTargetSingles(selectedLeds); + } + //console.log('mode changed:', selectedLeds); + this.refresh(true); + this.editor.demoModeOnDevice(); + }); + document.addEventListener('ledsChange', (event) => { + const selectedLeds = event.detail; + // if array is just multi do this: + if (selectedLeds.includes('multi')) { + this.setTargetMulti(); + } else { + this.setTargetSingles(selectedLeds); + } + //console.log('LEDs changed:', this.targetLeds); + this.refresh(true); + }); + document.addEventListener('deviceConnected', (event) => { + //console.log("Control Panel detected device conneted"); + this.refresh(true); + this.vortexPort.startReading(); + this.editor.demoModeOnDevice(); + }); + } + + setTargetSingles(selectedLeds = null) { + if (!selectedLeds) { + const ledCount = this.lightshow.vortex.engine().leds().ledCount(); + selectedLeds = [] + for (let i = 0; i < ledCount; i++) { + selectedLeds.push(i.toString()); + } + } + this.targetLeds = selectedLeds.map(led => parseInt(led, 10));; + this.targetLed = this.targetLeds[0]; + this.isMulti = false; + } + + setTargetMulti() { + this.targetLed = this.lightshow.vortex.engine().leds().ledMulti(); + this.targetLeds = [ this.targetLed ]; + this.isMulti = true; + } + + refresh(fromEvent = false) { + this.refreshColorset(fromEvent); + } + + async refreshColorset(fromEvent = false) { + const colorsetElement = document.getElementById('colorset'); + const cur = this.lightshow.vortex.engine().modes().curMode(); + colorsetElement.innerHTML = ''; // Clear colorset + + if (!cur) { + return; + } + + const set = cur.getColorset(this.targetLed); + const numColors = set ? set.numColors() : 0; + + for (let i = 0; i < numColors; i++) { + const container = document.createElement('div'); + container.className = 'color-box'; + + const col = set.get(i); + const hexColor = `#${((1 << 24) + (col.red << 16) + (col.green << 8) + col.blue).toString(16).slice(1).toUpperCase()}`; + + // Create color entry + const colorEntry = document.createElement('div'); + colorEntry.style.backgroundColor = hexColor; + colorEntry.className = 'color-entry'; + colorEntry.dataset.index = i; + colorEntry.addEventListener('click', () => + this.editor.colorPicker.openColorPicker(i, set, this.updateColor.bind(this)) + ); + + // Create hex label + const hexLabel = document.createElement('label'); + hexLabel.textContent = hexColor; + + // Create delete button + const deleteButton = document.createElement('span'); + deleteButton.className = 'delete-color'; + deleteButton.dataset.index = i; + deleteButton.textContent = '×'; + deleteButton.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent triggering color picker + this.delColor(Number(deleteButton.dataset.index)); + document.dispatchEvent(new CustomEvent('patternChange')); + }); + + // Append elements to container + container.appendChild(colorEntry); + container.appendChild(hexLabel); + container.appendChild(deleteButton); + + colorsetElement.appendChild(container); + } + + // Add empty slots for adding colors + if (numColors < 8) { + const addColorContainer = document.createElement('div'); + addColorContainer.className = 'color-box empty'; + addColorContainer.textContent = '+'; + addColorContainer.addEventListener('click', () => { + this.addColor(); + document.dispatchEvent(new CustomEvent('patternChange')); + }); + colorsetElement.appendChild(addColorContainer); + } + } + + // Helper: Update Color from Hex Input + updateColorHex(index, hexValue) { + const cur = this.lightshow.vortex.engine().modes().curMode(); + const set = cur.getColorset(this.targetLed); + const color = this.hexToRGB(hexValue); + set.set(index, new this.lightshow.vortexLib.RGBColor(color.r, color.g, color.b)); + cur.init(); + this.refreshColorset(); + } + + // Helper: Convert Hex to RGB + hexToRGB(hex) { + let bigint = parseInt(hex.replace(/^#/, ''), 16); + return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 }; + } + + updateColor(index, hexValue, isDragging) { + let hex = hexValue ? hexValue.replace(/^#/, '') : 0; + let bigint = parseInt(hex, 16); + let r = (bigint >> 16) & 255; + let g = (bigint >> 8) & 255; + let b = bigint & 255; + const cur = this.lightshow.vortex.engine().modes().curMode(); + if (!cur) { + return; + } + let set = cur.getColorset(this.targetLed); + let col = new this.lightshow.vortexLib.RGBColor(r, g, b); + set.set(index, col); + this.targetLeds.forEach(led => { + cur.setColorset(set, led); + }); + // re-initialize the demo mode because num colors may have changed + cur.init(); + // save + this.lightshow.vortex.engine().modes().saveCurMode(); + // refresh + this.refreshColorset(); + if (isDragging) { + this.editor.demoColorOnDevice(col); + } else { + // demo on device + this.editor.demoModeOnDevice(); + } + } + + addColor() { + this.lightshow.addColor(255, 255, 255, this.targetLeds); + this.refreshColorset(); + // demo on device + this.editor.demoModeOnDevice(); + } + + delColor(index) { + const cur = this.lightshow.vortex.engine().modes().curMode(); + if (!cur) { + return; + } + let set = cur.getColorset(this.targetLed); + if (set.numColors() <= 0) { + return; + } + set.removeColor(index); + this.targetLeds.forEach(led => { + cur.setColorset(set, led); + }); + // re-initialize the demo mode because num colors may have changed + cur.init(); + // save + this.lightshow.vortex.engine().modes().saveCurMode(); + // refresh + this.refreshColorset(); + // demo on device + this.editor.demoModeOnDevice(); + } +} + diff --git a/js/ControlPanel.js b/js/ControlPanel.js index 4aa35de..5a3a768 100644 --- a/js/ControlPanel.js +++ b/js/ControlPanel.js @@ -5,87 +5,22 @@ import ColorPicker from './ColorPicker.js'; export default class ControlPanel extends Panel { constructor(lightshow, vortexPort) { - const controls = [ - { - id: 'tickRate', - type: 'range', - min: 1, - max: 30, - default: 3, - label: 'Speed', - update: value => lightshow.tickRate = value - }, - { - id: 'trailSize', - type: 'range', - min: 1, - max: 300, - default: 100, - label: 'Trail', - update: value => lightshow.trailSize = value - }, - { - id: 'dotSize', - type: 'range', - min: 5, - max: 50, - default: 25, - label: 'Size', - update: value => lightshow.dotSize = value - }, - { - id: 'blurFac', - type: 'range', - min: 1, - max: 10, - default: 5, - label: 'Blur', - update: value => lightshow.blurFac = value - }, - { - id: 'circleRadius', - type: 'range', - min: 0, - max: 600, - default: 400, - label: 'Radius', - update: value => lightshow.circleRadius = value - }, - { - id: 'spread', // New Slider ID - type: 'range', - min: 0, - max: 100, - default: 15, - label: 'Spread', - display: 'none', - update: value => lightshow.spread = parseInt(value) // Assume 'spread' is a new property in Lightshow - } - ]; - const content = ` -
- Animation -
- ${ControlPanel.generateControlsContent(controls)} -
-
-
- Pattern - - -
-
-
- Colorset - -
-
+
+ Pattern +
+ +
+
+
+
+ Colorset +
+
`; super('controlPanel', content); this.lightshow = lightshow; - this.controls = controls; this.vortexPort = vortexPort; this.targetLed = 0; this.targetLeds = [ this.targetLed ]; @@ -95,26 +30,12 @@ export default class ControlPanel extends Panel { this.clickCounts = {}; this.clickTimers = {}; this.sineWaveAnimations = {}; - this.controls.forEach(control => { - this.clickCounts[control.id] = 0; - this.sineWaveAnimations[control.id] = false; - }); // Instantiate the ColorPicker this.colorPicker = new ColorPicker(lightshow); } - static generateControlsContent(controls) { - return controls.map(control => ` -
- - -
- `).join(''); - } - initialize() { - this.attachEventListeners(); this.populatePatternDropdown(); this.attachPatternDropdownListener(); this.refresh(); @@ -223,38 +144,6 @@ export default class ControlPanel extends Panel { this.sineWaveAnimations[controlId] = false; } - attachEventListeners() { - this.controls.forEach(control => { - const element = this.panel.querySelector(`#${control.id}`); - element.addEventListener('input', (event) => { - control.update(event.target.value); - }); - element.addEventListener('click', this.handleControlClick.bind(this)); - }); - const randomizePatternButton = document.getElementById('randomizePattern'); - randomizePatternButton.addEventListener('click', () => { - this.lightshow.randomizePattern(this.targetLeds); - document.dispatchEvent(new CustomEvent('patternChange')); - // refresh - this.refreshPatternDropdown(); - this.refreshPatternArgs(); - this.refreshColorset(); - // demo on device - this.demoModeOnDevice(); - }); - const randomizeColorsetButton = document.getElementById('randomizeColorset'); - randomizeColorsetButton.addEventListener('click', () => { - this.lightshow.randomizeColorset(this.targetLeds); - document.dispatchEvent(new CustomEvent('patternChange')); - // refresh - this.refreshPatternDropdown(); - this.refreshPatternArgs(); - this.refreshColorset(); - // demo on device - this.demoModeOnDevice(); - }); - } - populatePatternDropdown() { const dropdown = document.getElementById('patternDropdown'); dropdown.innerHTML = ''; @@ -381,70 +270,86 @@ export default class ControlPanel extends Panel { } async refreshColorset(fromEvent = false) { - const colorsetElement = document.getElementById("colorset"); - let cur = this.lightshow.vortex.engine().modes().curMode(); + const colorsetElement = document.getElementById('colorset'); + const cur = this.lightshow.vortex.engine().modes().curMode(); + colorsetElement.innerHTML = ''; // Clear colorset + if (!cur) { - colorsetElement.textContent = ''; return; } - let colorsetHtml = ''; - let dropdown = document.getElementById('patternDropdown'); - const pat = cur.getPatternID(this.targetLed); - dropdown.value = pat.value; - const set = cur.getColorset(this.targetLed); - let numCol = set.numColors(); - if (numCol) { - for (var i = 0; i < numCol; ++i) { - let col = set.get(i); - const hexColor = `#${((1 << 24) + (col.red << 16) + (col.green << 8) + col.blue).toString(16).slice(1)}`.toUpperCase(); - colorsetHtml += `
- × -
- -
`; - } - } - if (!numCol || numCol < 8) { - colorsetHtml += ` -
- + -
`; - } - colorsetElement.innerHTML = colorsetHtml; - - // Attach event listeners for color entries - const colorEntries = colorsetElement.querySelectorAll('.color-entry'); - colorEntries.forEach((entry, idx) => { - entry.addEventListener('click', () => { - const cur = this.lightshow.vortex.engine().modes().curMode(); - if (!cur) { - return; - } - const set = cur.getColorset(this.targetLed); - this.colorPicker.openColorPickerModal(idx, set, (index, color, dragging) => this.updateColor(index, color, dragging)); - }); - }); + const set = cur.getColorset(this.targetLed); + const numColors = set ? set.numColors() : 0; - // Attach event listeners for del col buttons - const deleteButtons = colorsetElement.querySelectorAll('.delete-color'); - deleteButtons.forEach(button => { - button.addEventListener('click', () => { - this.delColor(Number(button.getAttribute('data-index'))); + for (let i = 0; i < numColors; i++) { + const container = document.createElement('div'); + container.className = 'color-container'; + + const col = set.get(i); + const hexColor = `#${((1 << 24) + (col.red << 16) + (col.green << 8) + col.blue).toString(16).slice(1).toUpperCase()}`; + + // Create color entry + const colorEntry = document.createElement('div'); + colorEntry.style.backgroundColor = hexColor; + colorEntry.className = 'color-entry'; + colorEntry.dataset.index = i; + colorEntry.addEventListener('click', () => + this.colorPicker.openColorPickerModal(i, set, this.updateColor.bind(this)) + ); + + // Create hex label + const hexLabel = document.createElement('label'); + hexLabel.textContent = hexColor; + + // Create delete button + const deleteButton = document.createElement('span'); + deleteButton.className = 'delete-color'; + deleteButton.dataset.index = i; + deleteButton.textContent = '×'; + deleteButton.addEventListener('click', (e) => { + e.stopPropagation(); // Prevent triggering color picker + this.delColor(Number(deleteButton.dataset.index)); document.dispatchEvent(new CustomEvent('patternChange')); }); - }); - // Attach event listeners for add col button - const addButton = colorsetElement.querySelector('.add-color'); - if (addButton) { - addButton.addEventListener('click', () => { + // Append elements to container + container.appendChild(deleteButton); + container.appendChild(colorEntry); + container.appendChild(hexLabel); + + colorsetElement.appendChild(container); + } + + // Add empty slots for adding colors + if (numColors < 8) { + const addColorContainer = document.createElement('div'); + addColorContainer.className = 'color-container add-color'; + addColorContainer.textContent = '+'; + addColorContainer.addEventListener('click', () => { this.addColor(); document.dispatchEvent(new CustomEvent('patternChange')); }); + colorsetElement.appendChild(addColorContainer); } } + // Helper: Update Color from Hex Input + updateColorHex(index, hexValue) { + const cur = this.lightshow.vortex.engine().modes().curMode(); + const set = cur.getColorset(this.targetLed); + const color = this.hexToRGB(hexValue); + set.set(index, new this.lightshow.vortexLib.RGBColor(color.r, color.g, color.b)); + cur.init(); + this.refreshColorset(); + } + + // Helper: Convert Hex to RGB + hexToRGB(hex) { + let bigint = parseInt(hex.replace(/^#/, ''), 16); + return { r: (bigint >> 16) & 255, g: (bigint >> 8) & 255, b: bigint & 255 }; + } + + updateColor(index, hexValue, isDragging) { let hex = hexValue.replace(/^#/, ''); let bigint = parseInt(hex, 16); @@ -475,26 +380,6 @@ export default class ControlPanel extends Panel { } } - async demoColorOnDevice(color) { - try { - if (!this.vortexPort.isTransmitting && this.vortexPort.isActive()) { - await this.vortexPort.demoColor(this.lightshow.vortexLib, this.lightshow.vortex, color); - } - } catch (error) { - Notification.failure("Failed to demo color (" + error + ")"); - } - } - - async demoModeOnDevice() { - try { - if (!this.vortexPort.isTransmitting && this.vortexPort.isActive()) { - await this.vortexPort.demoCurMode(this.lightshow.vortexLib, this.lightshow.vortex); - } - } catch (error) { - Notification.failure("Failed to demo mode (" + error + ")"); - } - } - getTooltipText(sliderName) { const descriptions = { "on duration": "This determines how long the LED light stays 'on' during each blink. Think of it like the length of time each color shows up when the LED is cycling through colors.", @@ -552,101 +437,172 @@ export default class ControlPanel extends Panel { return nicerNames[sliderName] || sliderName; // Default text if no description is found } + //async refreshPatternArgs(fromEvent = false) { + // const paramsDiv = document.getElementById('patternParams'); + // const patternID = this.lightshow.vortexLib.PatternID.values[document.getElementById('patternDropdown').value]; + // if (!patternID) { + // // Clear existing parameters + // paramsDiv.innerHTML = ''; + // return; + // } + // const numOfParams = this.lightshow.vortex.numCustomParams(patternID); + // let customParams = this.lightshow.vortex.getCustomParams(patternID); + // // Clear existing parameters + // paramsDiv.innerHTML = ''; + // let cur = this.lightshow.vortex.engine().modes().curMode(); + // if (!cur) { + // // Clear existing parameters + // paramsDiv.innerHTML = ''; + // return; + // } + + // for (let i = 0; i < numOfParams; i++) { + // const container = document.createElement('div'); + // container.className = 'param-container'; + // const label = document.createElement('label'); + // let sliderName = customParams.get(i).slice(2) + // .replace(/([a-z])([A-Z])/g, '$1 $2') + // .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') + // .toLowerCase(); + // label.textContent = this.getTooltipNiceName(sliderName); + // const slider = document.createElement('input'); + // slider.type = 'range'; + // slider.className = 'param-slider'; + // if (sliderName === 'on duration') { + // // on duration cannot be 0, it can but it kinda breaks stuff + // slider.min = '1'; + // } else { + // slider.min = '0'; + // } + // slider.max = '255'; + // slider.step = '1'; + // slider.value = cur.getArg(i, this.targetLed) || '0'; + + // // Display value + // const displayValue = document.createElement('span'); + // displayValue.className = 'slider-value'; + // displayValue.textContent = slider.value; + + // // Description of what the slider does + // const helpIcon = document.createElement('i'); + // helpIcon.className = 'fas fa-question-circle help-icon'; + // helpIcon.setAttribute('data-tooltip', this.getTooltipText(sliderName)); // Modify this line for each slider's specific tooltip content. + // helpIcon.onclick = () => { this.toggleTooltip(helpIcon); }; + + // helpIcon.addEventListener('click', function(event) { + // event.stopPropagation(); // Prevent the document click event from immediately hiding the tooltip + // }); + + // const labelContainer = document.createElement('div'); + // labelContainer.className = 'label-container'; + // labelContainer.appendChild(label); + // labelContainer.appendChild(helpIcon); + + // const sliderContainer = document.createElement('div'); + // sliderContainer.className = 'slider-container'; + // sliderContainer.appendChild(slider); + // sliderContainer.appendChild(displayValue); + + // container.appendChild(labelContainer); + // container.appendChild(sliderContainer); + // paramsDiv.appendChild(container); + + // // Event for slider + // slider.addEventListener('input', (event) => { + // const paramName = label.textContent.replace(/([a-z])([A-Z])/g, '$1 $2') + // .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') + // .toLowerCase(); + // displayValue.textContent = event.target.value; // Update the displayed value + // let cur = this.lightshow.vortex.engine().modes().curMode(); + // this.targetLeds.forEach((led) => { + // let pat = cur.getPattern(led); + // pat.setArg(i, event.target.value); + // }); + // }); + // slider.addEventListener('change', async () => { + // // init + // cur.init(); + // // save + // this.lightshow.vortex.engine().modes().saveCurMode(); + // // send to device + // document.dispatchEvent(new CustomEvent('patternChange')); + // // demo on device + // this.demoModeOnDevice(); + // }); + // } + //} async refreshPatternArgs(fromEvent = false) { const paramsDiv = document.getElementById('patternParams'); const patternID = this.lightshow.vortexLib.PatternID.values[document.getElementById('patternDropdown').value]; if (!patternID) { - // Clear existing parameters - paramsDiv.innerHTML = ''; + paramsDiv.innerHTML = this.generateEmptySlots(7); // Fill with placeholders up to max 7 return; } + const numOfParams = this.lightshow.vortex.numCustomParams(patternID); let customParams = this.lightshow.vortex.getCustomParams(patternID); - // Clear existing parameters - paramsDiv.innerHTML = ''; - let cur = this.lightshow.vortex.engine().modes().curMode(); - if (!cur) { - // Clear existing parameters - paramsDiv.innerHTML = ''; - return; - } + paramsDiv.innerHTML = ''; // Clear existing params - for (let i = 0; i < numOfParams; i++) { + for (let i = 0; i < 7; i++) { // Fixed 7 slots for layout consistency const container = document.createElement('div'); container.className = 'param-container'; - const label = document.createElement('label'); - let sliderName = customParams.get(i).slice(2) - .replace(/([a-z])([A-Z])/g, '$1 $2') - .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') - .toLowerCase(); - label.textContent = this.getTooltipNiceName(sliderName); - const slider = document.createElement('input'); - slider.type = 'range'; - slider.className = 'param-slider'; - if (sliderName === 'on duration') { - // on duration cannot be 0, it can but it kinda breaks stuff - slider.min = '1'; - } else { - slider.min = '0'; - } - slider.max = '255'; - slider.step = '1'; - slider.value = cur.getArg(i, this.targetLed) || '0'; - - // Display value - const displayValue = document.createElement('span'); - displayValue.className = 'slider-value'; - displayValue.textContent = slider.value; - - // Description of what the slider does - const helpIcon = document.createElement('i'); - helpIcon.className = 'fas fa-question-circle help-icon'; - helpIcon.setAttribute('data-tooltip', this.getTooltipText(sliderName)); // Modify this line for each slider's specific tooltip content. - helpIcon.onclick = () => { this.toggleTooltip(helpIcon); }; - - helpIcon.addEventListener('click', function(event) { - event.stopPropagation(); // Prevent the document click event from immediately hiding the tooltip - }); - const labelContainer = document.createElement('div'); - labelContainer.className = 'label-container'; - labelContainer.appendChild(label); - labelContainer.appendChild(helpIcon); + if (i < numOfParams) { + let sliderName = customParams.get(i) + .slice(2) + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') + .toLowerCase(); - const sliderContainer = document.createElement('div'); - sliderContainer.className = 'slider-container'; - sliderContainer.appendChild(slider); - sliderContainer.appendChild(displayValue); + const label = document.createElement('label'); + label.textContent = this.getTooltipNiceName(sliderName); - container.appendChild(labelContainer); - container.appendChild(sliderContainer); - paramsDiv.appendChild(container); - - // Event for slider - slider.addEventListener('input', (event) => { - const paramName = label.textContent.replace(/([a-z])([A-Z])/g, '$1 $2') - .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') - .toLowerCase(); - displayValue.textContent = event.target.value; // Update the displayed value - let cur = this.lightshow.vortex.engine().modes().curMode(); - this.targetLeds.forEach((led) => { - let pat = cur.getPattern(led); - pat.setArg(i, event.target.value); + const slider = document.createElement('input'); + slider.type = 'range'; + slider.min = '0'; + slider.max = '255'; + slider.step = '1'; + slider.value = this.lightshow.vortex.engine().modes().curMode().getArg(i, this.targetLed) || '0'; + slider.className = 'custom-slider'; + + const textbox = document.createElement('input'); + textbox.type = 'number'; + textbox.min = slider.min; + textbox.max = slider.max; + textbox.value = slider.value; + textbox.className = 'custom-textbox'; + + // Sync slider and textbox values + slider.addEventListener('input', (event) => { + textbox.value = event.target.value; + this.updatePatternArg(i, event.target.value); }); - }); - slider.addEventListener('change', async () => { - // init - cur.init(); - // save - this.lightshow.vortex.engine().modes().saveCurMode(); - // send to device - document.dispatchEvent(new CustomEvent('patternChange')); - // demo on device - this.demoModeOnDevice(); - }); + textbox.addEventListener('input', (event) => { + slider.value = event.target.value; + this.updatePatternArg(i, event.target.value); + }); + + container.appendChild(label); + container.appendChild(slider); + container.appendChild(textbox); + } + + paramsDiv.appendChild(container); } } + // Helper: Update Pattern Argument + updatePatternArg(index, value) { + const cur = this.lightshow.vortex.engine().modes().curMode(); + this.targetLeds.forEach((led) => { + cur.getPattern(led).setArg(index, value); + }); + cur.init(); + this.lightshow.vortex.engine().modes().saveCurMode(); + document.dispatchEvent(new CustomEvent('patternChange')); + this.demoModeOnDevice(); + } + toggleTooltip(element) { const tooltipText = element.getAttribute('data-tooltip'); if (!tooltipText) return; diff --git a/js/EspUpdater.js b/js/EspUpdater.js new file mode 100644 index 0000000..165f5a5 --- /dev/null +++ b/js/EspUpdater.js @@ -0,0 +1,129 @@ +class ESPUpdater { + constructor(port, logger) { + this.port = port; + this.logger = logger; + this.reader = null; + this.writer = null; + this.inputBuffer = []; + this.flashWriteSize = 4096; // Default for ESP + } + + async connect() { + this.reader = this.port.readable.getReader(); + this.writer = this.port.writable.getWriter(); + this.logger.log("Connected to ESP device."); + } + + async disconnect() { + this.reader.releaseLock(); + this.writer.releaseLock(); + await this.port.close(); + this.logger.log("Disconnected from ESP device."); + } + + async initialize() { + await this.hardReset(true); + await this.sync(); + } + + async sync() { + for (let i = 0; i < 5; i++) { + this.inputBuffer.length = 0; + await this.sendCommand(ESP_SYNC, SYNC_PACKET); + try { + let [_reply, data] = await this.getResponse(ESP_SYNC); + if (data[0] === 0 && data[1] === 0) { + this.logger.log("Device synchronized."); + return true; + } + } catch (e) { + this.logger.log("Sync attempt failed."); + } + } + throw new Error("Failed to synchronize with the ESP."); + } + + async flashFirmware(binaryData, updateProgress) { + await this.flashBegin(binaryData.byteLength); + let position = 0; + let seq = 0; + while (position < binaryData.byteLength) { + const chunk = binaryData.slice(position, position + this.flashWriteSize); + await this.flashBlock(chunk, seq); + position += chunk.byteLength; + seq++; + updateProgress(position, binaryData.byteLength); + } + await this.flashFinish(); + } + + async flashBegin(size) { + const numBlocks = Math.ceil(size / this.flashWriteSize); + const buffer = pack("If you found this website then you're likely a flow artist or glover, if you have no idea what that means then welcome to your first lightshow. - This website is an ongoing development and you can expect to find daily changes and fixes, below are some basic descriptions of the controls

- -

Animation

-

The four 'Animation' controls in the top left only effect the lightshow on this website, they do not effect Vortex Devices and will not be saved.

- -

Pattern & Colorset

-

Pick a pre-made pattern from the list, or adjust the parameters to fine-tune any pattern. Decide on a group of 1 to 8 colors to accompany the pattern. There are two types of patterns: strobes and blends, blends are the same as strobes but instead of blinking from color to color they smoothly blend. All patterns in the list can be made by adjusting the sliders (blend adds two sliders).

- -

Modes & Leds

-

Each mode allows for a different pattern and colorset combination, you can add more modes to the list, share a mode by URL, or export & import a mode in JSON format. - If a Vortex Device is connected then alternative leds can be selected to target, otherwise only one led is available

- -
- -
-`; - -// instantiate VortexLib webassembly module +// Instantiate VortexLib webassembly module VortexLib().then(vortexLib => { - // the lightshow needs the canvas id to operate on - const canvas = document.getElementById('lightshowCanvas'); - // instantiate the lightshow - let lightshow = new Lightshow(vortexLib, canvas); - // initialize the lightshow - lightshow.start(); - - // Add key event listener for changing shapes - window.addEventListener('keydown', (event) => { - switch (event.key) { - case '1': - lightshow.setShape('circle'); - break; - case '2': - lightshow.setShape('figure8'); - break; - case '3': - lightshow.setShape('heart'); - break; - case '4': - lightshow.setShape('box'); - break; - default: - // do nothing for all other keypresses - return; - } - lightshow.angle = 0; - }); - - // create panels for the lightshow - let aboutPanel = new AboutPanel(lightshow, vortexPort); - let controlPanel = new ControlPanel(lightshow, vortexPort); - let modesPanel = new ModesPanel(lightshow, vortexPort); - - // Append panels to the body - aboutPanel.appendTo(document.body); - controlPanel.appendTo(document.body); - modesPanel.appendTo(document.body); - - // initialize the modes panel - aboutPanel.initialize(); - controlPanel.initialize(); - modesPanel.initialize(); - - // finally import the modedata on the url if there is any - const urlParams = new URLSearchParams(window.location.search); - const encodedData = urlParams.get('data'); - if (encodedData) { - try { - // Decode the Base64 string and parse the JSON data - modesPanel.importPatternFromData(atob(encodedData), false); - } catch (error) { - console.error('Error parsing mode data:', error); - } - } - - // resize the lightshow when window drags - window.addEventListener('resize', () => { - lightshow.resetToCenter(); - }); - - window.randomize = controlPanel.randomize.bind(controlPanel); - - // Check if the welcome modal should be shown - const showWelcome = localStorage.getItem('showWelcome') !== 'false'; - - if (showWelcome) { - // Create a new instance of the Modal class - const welcomeModal = new Modal('welcome'); - - // Configuration for the welcome modal - const welcomeConfig = { - title: welcomeTitle, - blurb: welcomeBlurb, - }; - - // Show the welcome modal - welcomeModal.show(welcomeConfig); - - // Add event listener to the checkbox - document.getElementById('doNotShowAgain').addEventListener('change', (event) => { - localStorage.setItem('showWelcome', !event.target.checked); - }); - } + // Instantiate the VortexEditor + const vortexEditor = new VortexEditor(vortexLib); + // and initialize it + vortexEditor.initialize(); }); diff --git a/js/ModesPanel.js b/js/ModesPanel.js index 30d80d8..2eecb52 100644 --- a/js/ModesPanel.js +++ b/js/ModesPanel.js @@ -5,7 +5,7 @@ import Notification from './Notification.js'; import ChromalinkPanel from './ChromalinkPanel.js'; export default class ModesPanel extends Panel { - constructor(lightshow, vortexPort) { + constructor(editor) { const content = `
@@ -62,9 +62,10 @@ export default class ModesPanel extends Panel {
`; - super('modesPanel', content); - this.lightshow = lightshow; - this.vortexPort = vortexPort; + super('modesPanel', content, 'Modes and Device Controls'); + this.editor = editor; + this.lightshow = editor.lightshow; + this.vortexPort = editor.vortexPort; this.shareModal = new Modal('share'); this.exportModal = new Modal('export'); this.importModal = new Modal('import'); @@ -578,130 +579,16 @@ export default class ModesPanel extends Panel { this.handleLedSelectionChange(); } - getLedPositions(deviceName = 'Orbit') { - // Add logic to return LED positions based on the device name - const ledPositions = []; - - // Example logic for different devices - if (deviceName === 'Orbit') { - return this.getLedPositionsOrbit(); - } else if (deviceName === 'Gloves') { - return this.getLedPositionsGlove(); - } else if (deviceName === 'Handle') { - return this.getLedPositionsHandle(); - } else if (deviceName === 'Duo') { - return this.getLedPositionsDuo(); - } else if (deviceName === 'Chromadeck') { - return this.getLedPositionsChromadeck(); - } else if (deviceName === 'Spark') { - return this.getLedPositionsSpark(); + async getLedPositions(deviceName) { + try { + const cacheBuster = '?v=' + new Date().getTime(); + const response = await fetch(`public/data/${deviceName.toLowerCase()}-led-positions.json${cacheBuster}`); + const data = await response.json(); + return data; + } catch (error) { + console.error(`Error loading LED positions for ${deviceName}:`, error); + return { points: [], original_width: 1, original_height: 1 }; } - return ledPositions; - } - - getLedPositionsHandle() { - const ledPositions = [ - { x: 100, y: 125 }, // front - { x: 100, y: 160 }, // tip - { x: 233, y: 125 }, // back - ]; - return ledPositions; - - } - - getLedPositionsGlove() { - const ledPositions = [ - { x: 246, y: 45 }, // pinkie tip - { x: 246, y: 60 }, // pinkie top - { x: 207, y: 20 }, // ring tip - { x: 207, y: 35 }, // ring top - { x: 168, y: 9 }, // middle tip - { x: 168, y: 24 }, // middle top - { x: 128, y: 20 }, // index tip - { x: 128, y: 35 }, // index top - { x: 89, y: 86 }, // thumb tip - { x: 89, y: 101 }, // thumb top - ]; - return ledPositions; - } - - getLedPositionsOrbit() { - const ledPositions = []; - let size = 11; - // Quadrant 1 top - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 40 + (i * size) + 67, y: 108 + (i * size) }); - // Quadrant 1 edge - ledPositions.push({ x: ledPositions[2].x + 16, y: ledPositions[2].y + 16 }); - // Quadrant 1 bottom - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 140 + (i * size) + 67, y: 129 - (i * size) }); - // Quadrant 2 bottom - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 208 + (i * size) + 67, y: 108 + (i * size) }); - // Quadrant 2 edge - ledPositions.push({ x: 23, y: 146 }); - // Quadrant 2 top - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 40 + (i * size), y: 129 - (i * size) }); - // Quadrant 3 top - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 62 - (i * size), y: 62 - (i * size) }); - // Quadrant 3 edge - ledPositions.push({ x: ledPositions[16].x - 17, y: ledPositions[16].y - 18 }); - // Quadrant 3 bottom - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 297 - (i * size), y: 40 + (i * size) }); - // Quadrant 4 bottom - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 162 - (i * size) + 67, y: 62 - (i * size) }); - // Quadrant 4 edge - ledPositions.push({ x: 145, y: 24 }); - // Quadrant 4 top - for (let i = 0; i < 3; ++i) ledPositions.push({ x: 130 - (i * size), y: 40 + (i * size) }); - return ledPositions; - } - - getLedPositionsChromadeck() { - const ledPositions = [ - // lol poor mans scaling - {x: 250 * (334 / 500), y: 11 * (333 / 500)}, // Outer Ring 12 Oclock - {x: 318 * (334 / 500), y: 32 * (333 / 500)}, // Outer Ring 1 Oclock - {x: 360 * (334 / 500), y: 90 * (333 / 500)}, // Outer Ring 2 Oclock - {x: 360 * (334 / 500), y: 162 * (333 / 500) }, // Outer Ring 4 Oclock - {x: 318 * (334 / 500), y: 219 * (333 / 500) }, // Outer Ring 5 Oclock - {x: 250 * (334 / 500), y: 241 * (333 / 500) }, // Outer Ring 6 Oclock - {x: 183 * (334 / 500), y: 219 * (333 / 500) }, // Outer Ring 7 Oclock - {x: 141 * (334 / 500), y: 161 * (333 / 500) }, // Outer Ring 8 Oclock - {x: 141 * (334 / 500), y: 90 * (333 / 500) }, // Outer Ring 10 Oclock - {x: 183 * (334 / 500), y: 33 * (333 / 500) }, // Outer Ring 11 Oclock - {x: 250 * (334 / 500), y: 44 * (333 / 500) }, // Inner Ring 12 Oclock - {x: 298 * (334 / 500), y: 60 * (333 / 500) }, // Inner Ring 1 Oclock - {x: 328 * (334 / 500), y: 100 * (333 / 500) }, // Inner Ring 2 Oclock - {x: 328 * (334 / 500), y: 150 * (333 / 500) }, // Inner Ring 4 Oclock - {x: 298 * (334 / 500), y: 192 * (333 / 500) }, // Inner Ring 5 Oclock - {x: 250 * (334 / 500), y: 206 * (333 / 500) }, // Inner Ring 6 Oclock - {x: 203 * (334 / 500), y: 191 * (333 / 500) }, // Inner Ring 7 Oclock - {x: 173 * (334 / 500), y: 151 * (333 / 500) }, // Inner Ring 8 Oclock - {x: 173 * (334 / 500), y: 100 * (333 / 500) }, // Inner Ring 10 Oclock - {x: 203 * (334 / 500), y: 60 * (333 / 500) }, // Inner Ring 11 Oclock - ]; - return ledPositions; - } - - getLedPositionsSpark() { - const ledPositions = [ - // lol poor mans scaling - {x: 300 * (334 / 500), y: 40 * (333 / 500)}, // 1 oclock - {x: 350 * (334 / 500), y: 125 * (333 / 500)}, // 3 oclock - {x: 300 * (334 / 500), y: 212 * (333 / 500)}, // 5 oclock - {x: 200 * (334 / 500), y: 212 * (333 / 500)}, // 7 oclock - {x: 150 * (334 / 500), y: 125 * (333 / 500)}, // 9 oclock - {x: 200 * (334 / 500), y: 40 * (333 / 500)}, // 11 oclock - ]; - return ledPositions; - } - - getLedPositionsDuo() { - const ledPositions = [ - // lol poor mans scaling - {x: 250 * (334 / 500), y: 25 * (333 / 500)}, // tip led - {x: 250 * (334 / 500), y: 140 * (333 / 500)}, // top led - ]; - return ledPositions; } onDeviceDisconnect() { @@ -723,7 +610,6 @@ export default class ModesPanel extends Panel { refresh(fromEvent = false) { this.refreshModeList(fromEvent); - this.refreshLedList(fromEvent); this.updateLedIndicators(); // Ensure indicators are updated } @@ -780,52 +666,52 @@ export default class ModesPanel extends Panel { this.applyLedSelections(selectedLeds); } - renderLedIndicators(deviceName = null) { + async renderLedIndicators(deviceName = null) { const ledsFieldset = document.getElementById('ledsFieldset'); const deviceImageContainer = document.getElementById('deviceImageContainer'); - const ledList = document.getElementById('ledList'); const ledControls = document.getElementById('ledControls'); if (!deviceName || deviceName === 'None') { - ledsFieldset.style.display = 'none'; // Hide the entire fieldset + ledsFieldset.style.display = 'none'; return; } - ledsFieldset.style.display = 'block'; // Show the fieldset - deviceImageContainer.innerHTML = ''; // Clear any existing content - ledList.style.display = 'block'; // Show the LED list - ledControls.style.display = 'flex'; // Show the LED controls + ledsFieldset.style.display = 'block'; + deviceImageContainer.innerHTML = ''; + ledList.style.display = 'block'; + ledControls.style.display = 'flex'; + deviceImageContainer.innerHTML = ''; + const overlay = document.createElement('div'); + overlay.classList.add('led-overlay'); + deviceImageContainer.appendChild(overlay); + + const deviceData = await this.getLedPositions(deviceName); const deviceImageSrc = this.devices[deviceName].image; + if (deviceImageSrc) { const deviceImage = document.createElement('img'); - deviceImage.src = deviceImageSrc; - deviceImageContainer.appendChild(deviceImage); - } - - const ledPositions = this.getLedPositions(deviceName); - const cur = this.lightshow.vortex.engine().modes().curMode(); - const isMultiLed = cur && cur.isMultiLed(); // Check if the current mode uses a multi-LED pattern - - ledPositions.forEach((position, index) => { - const ledIndicator = document.createElement('div'); - ledIndicator.classList.add('led-indicator'); - ledIndicator.style.left = position.x + 'px'; - ledIndicator.style.top = position.y + 'px'; - ledIndicator.dataset.ledIndex = index; - - if (isMultiLed) { - ledIndicator.classList.add('selected'); - } - - deviceImageContainer.appendChild(ledIndicator); - }); + deviceImage.src = deviceImageSrc + '?v=' + new Date().getTime(); + deviceImage.style.display = 'block'; + deviceImage.style.width = '100%'; + deviceImage.style.height = 'auto'; + + deviceImage.onload = () => { + const scaleX = deviceImageContainer.clientWidth / deviceData.original_width; + const scaleY = deviceImageContainer.clientHeight / deviceData.original_height; + + deviceData.points.forEach((point, index) => { + const ledIndicator = document.createElement('div'); + ledIndicator.classList.add('led-indicator'); + ledIndicator.style.left = `${point.x * scaleX}px`; + ledIndicator.style.top = `${point.y * scaleY}px`; + ledIndicator.dataset.ledIndex = index; + + overlay.appendChild(ledIndicator); + }); + }; - // Disable LED controls if multi-LED pattern is applied - if (isMultiLed) { - ledControls.style.display = 'none'; - } else { - ledControls.style.display = 'flex'; + deviceImageContainer.appendChild(deviceImage); } } @@ -980,7 +866,7 @@ export default class ModesPanel extends Panel { } this.lightshow.vortex.setCurMode(curSel, false); this.attachModeEventListeners(); - this.refreshLedList(); + this.refreshLedList(fromEvent); } selectMode(index) { @@ -1094,7 +980,6 @@ export default class ModesPanel extends Panel { return; } this.refreshModeList(); - this.refreshLedList(); this.refreshPatternControlPanel(); Notification.success("Successfully Added Mode " + modeCount); } @@ -1209,7 +1094,6 @@ export default class ModesPanel extends Panel { this.lightshow.setLedCount(1); break; } - this.refreshLedList(); this.refreshModeList(); this.renderLedIndicators(initialDevice); this.handleLedSelectionChange(); @@ -1364,7 +1248,6 @@ export default class ModesPanel extends Panel { this.lightshow.vortex.setCurMode(cur, false); this.lightshow.vortex.engine().modes().saveCurMode(); this.refreshModeList(); - this.refreshLedList(); this.refreshPatternControlPanel(); Notification.success("Successfully Deleted Mode " + index); } @@ -1400,7 +1283,6 @@ export default class ModesPanel extends Panel { Notification.success("Successfully pulled save"); } this.refreshModeList(); - this.refreshLedList(); this.refreshPatternControlPanel(); } diff --git a/js/Panel.js b/js/Panel.js index 157e736..abb7f52 100644 --- a/js/Panel.js +++ b/js/Panel.js @@ -1,13 +1,306 @@ /* Panel.js */ export default class Panel { - constructor(id, content) { + static panels = []; // Static list to track all panels + + constructor(id, content, title = 'Panel', options = {}) { this.panel = document.createElement('div'); this.panel.id = id; - this.panel.innerHTML = content; + this.panel.className = 'draggable-panel'; + + const { showCloseButton = false } = options; + + const header = document.createElement('div'); + header.className = 'panel-header'; + header.innerHTML = `${title}`; + + if (showCloseButton) { + const closeBtn = document.createElement('span'); + closeBtn.className = 'close-btn'; + closeBtn.textContent = '×'; + closeBtn.addEventListener('click', () => this.hide()); + header.appendChild(closeBtn); + } else { + const collapseBtn = document.createElement('button'); + collapseBtn.className = 'collapse-btn'; + collapseBtn.textContent = '▼'; + collapseBtn.addEventListener('click', () => this.toggleCollapse()); + header.appendChild(collapseBtn); + } + + // Create content container + this.contentContainer = document.createElement('div'); + this.contentContainer.className = 'panel-content'; + this.contentContainer.innerHTML = content; + + // Append header and content to the panel + this.panel.appendChild(header); + this.panel.appendChild(this.contentContainer); + + this.isCollapsed = false; // Track collapse state + this.isVisible = true; // Track visibility state + this.originalPosition = { left: 0, top: 0 }; // Store the initial position + this.snapMargin = 5; // snap to this distance + this.snapRadius = 15; // snap within this distance + + this.panel.style.height = ''; // Full height + + this.initDraggable(); + + // Add this panel to the global list + Panel.panels.push(this); } appendTo(parent) { parent.appendChild(this.panel); + + const rect = this.panel.getBoundingClientRect(); + this.panel.style.left = `${rect.left}px`; + this.panel.style.top = `${rect.top}px`; + } + + initCollapse(collapseBtn) { + } + + show() { + if (!this.isVisible) { + this.isVisible = true; + this.panel.style.display = ''; + } + if (this.isCollapsed) { + this.toggleCollapse(); + } + } + + hide() { + if (this.isVisible) { + this.isVisible = false; + this.panel.style.display = 'none'; + } + } + + getSnappedPanels() { + const rect = this.panel.getBoundingClientRect(); + + return Panel.panels.filter((otherPanel) => { + if (otherPanel === this) return false; // Skip self + + const otherRect = otherPanel.panel.getBoundingClientRect(); + + // Check if the other panel is below this panel + const isBelow = otherRect.top - rect.bottom <= this.snapRadius && otherRect.top > rect.bottom; + + // Check if the horizontal ranges overlap + const isOverlappingHorizontally = + otherRect.left < rect.right + this.snapRadius && + otherRect.right > rect.left - this.snapRadius; + + return isBelow && isOverlappingHorizontally; + }); + } + + + toggleCollapse(propagate = true) { + const previousHeight = this.panel.offsetHeight; + + // Step 1: Identify snapped panels BEFORE resizing + const snappedPanels = this.getSnappedPanels(); + + // Step 2: Perform resizing + let newHeight = 44; // Default height after collapse + if (!this.isCollapsed) { + this.contentContainer.style.display = 'none'; + this.panel.style.height = '32px'; + } else { + this.contentContainer.style.display = 'flex'; + this.panel.style.height = ''; // Auto height + newHeight = this.panel.offsetHeight; + } + + if (propagate) { + const heightChange = newHeight - previousHeight; + // Step 3: Move snapped panels AFTER resizing + snappedPanels.forEach((otherPanel) => { + // Propagate movement recursively + otherPanel.moveSnappedPanels(heightChange); + otherPanel.panel.style.top = `${ + parseFloat(otherPanel.panel.style.top || otherPanel.panel.getBoundingClientRect().top) + heightChange + }px`; + }); + } + + // Step 4: Toggle the collapse state + this.isCollapsed = !this.isCollapsed; + } + + moveSnappedPanels(heightChange) { + const rect = this.panel.getBoundingClientRect(); + const snappedPanels = this.getSnappedPanels(); + + for (const otherPanel of snappedPanels) { + if (otherPanel === this) continue; // Skip self + + const otherRect = otherPanel.panel.getBoundingClientRect(); + + // Calculate the new position for the snapped panel + const currentTop = parseFloat(otherPanel.panel.style.top || otherRect.top); + const newTop = currentTop + heightChange; + + // Recursively move panels snapped to this one + otherPanel.moveSnappedPanels(heightChange); + + otherPanel.panel.style.top = `${newTop}px`; + + // Return immediately after finding a snapped panel + return; + } + } + + initDraggable() { + let isDragging = false; + let offsetX, offsetY; + + const onMouseDown = (e) => { + if (e.target === this.panel || e.target.closest('.panel-header')) { + isDragging = true; + offsetX = e.clientX - this.panel.offsetLeft; + offsetY = e.clientY - this.panel.offsetTop; + + this.panel.style.zIndex = 1000; // Bring the panel to the top + this.panel.style.position = 'absolute'; + } + }; + + const onMouseMove = (e) => { + if (isDragging) { + const newLeft = e.clientX - offsetX; + const newTop = e.clientY - offsetY; + + // Normal dragging behavior + this.panel.style.left = `${newLeft}px`; + this.panel.style.top = `${newTop}px`; + } + }; + + const onMouseUp = () => { + if (isDragging) { + isDragging = false; + + const rect = this.panel.getBoundingClientRect(); + const snapPoints = [ + ...this.getScreenSnapPoints(), + ...this.getOtherPanelSnapPoints(), + { x: this.originalPosition.left, y: this.originalPosition.top }, // Original position + ]; + + const { snappedX, snappedY } = this.findClosestSnap(rect, snapPoints); + + // Apply snapping if within range + if (snappedX !== null) { + this.panel.style.left = `${snappedX}px`; + } + if (snappedY !== null) { + this.panel.style.top = `${snappedY}px`; + } + } + }; + + this.panel.addEventListener('mousedown', onMouseDown); + document.addEventListener('mousemove', onMouseMove); + document.addEventListener('mouseup', onMouseUp); + } + + findClosestSnap(rect, snapPoints) { + let closestX = null; + let closestY = null; + let closestXDistance = this.snapRadius; + let closestYDistance = this.snapRadius; + + snapPoints.forEach((point) => { + const xDistance = Math.abs(rect.left - point.x); + const yDistance = Math.abs(rect.top - point.y); + + // Snap only within the snapRadius + if (xDistance < closestXDistance && xDistance <= this.snapRadius) { + closestX = point.x; + closestXDistance = xDistance; + } + if (yDistance < closestYDistance && yDistance <= this.snapRadius) { + closestY = point.y; + closestYDistance = yDistance; + } + }); + + // Return snapped positions (only for the axis within range) + return { snappedX: closestXDistance <= this.snapRadius ? closestX : null, snappedY: closestYDistance <= this.snapRadius ? closestY : null }; + } + + getScreenSnapPoints() { + const width = window.innerWidth; + const height = window.innerHeight; + + return [ + // Corners + { x: 0, y: 0 }, // Top-left + { x: width, y: 0 }, // Top-right + { x: 0, y: height }, // Bottom-left + { x: width, y: height }, // Bottom-right + + // Centerlines + { x: width / 2, y: 0 }, // Center-top + { x: width / 2, y: height / 2 }, // Absolute center + { x: width / 2, y: height }, // Center-bottom + { x: 0, y: height / 2 }, // Center-left + { x: width, y: height / 2 }, // Center-right + + // Edges (top, bottom, left, right) + { x: 0, y: 0 }, // Top-left corner + { x: width / 2, y: 0 }, // Middle of the top edge + { x: width, y: 0 }, // Top-right corner + { x: 0, y: height }, // Bottom-left corner + { x: width / 2, y: height }, // Middle of the bottom edge + { x: width, y: height }, // Bottom-right corner + ]; + } + + getOtherPanelSnapPoints() { + const snapPoints = []; + + Panel.panels.forEach((otherPanel) => { + if (otherPanel === this) return; // Skip self + + const otherRect = otherPanel.panel.getBoundingClientRect(); + + // Panel edges + const left = otherRect.left; + const right = otherRect.right; + const top = otherRect.top; + const bottom = otherRect.bottom; + + // Panel centerlines + const centerX = left + (right - left) / 2; + const centerY = top + (bottom - top) / 2; + + // Snap points (edges + centerlines) + snapPoints.push( + { x: left, y: top }, // Top-left + { x: right, y: top }, // Top-right + { x: left, y: bottom }, // Bottom-left + { x: right, y: bottom }, // Bottom-right + { x: centerX, y: top }, // Top-center + { x: centerX, y: bottom }, // Bottom-center + { x: left, y: centerY }, // Center-left + { x: right, y: centerY } // Center-right + ); + + // Offset snap points by snapMargin + snapPoints.push( + { x: left - this.snapMargin, y: top - this.snapMargin }, + { x: right + this.snapMargin, y: top - this.snapMargin }, + { x: left - this.snapMargin, y: bottom + this.snapMargin }, + { x: right + this.snapMargin, y: bottom + this.snapMargin } + ); + }); + return snapPoints; } } diff --git a/js/PatternPanel.js b/js/PatternPanel.js new file mode 100644 index 0000000..eff1728 --- /dev/null +++ b/js/PatternPanel.js @@ -0,0 +1,359 @@ +import Panel from './Panel.js'; + +export default class PatternPanel extends Panel { + constructor(editor) { + const content = ` +
+ +
+ +
+
+
+
+ `; + super('patternPanel', content, 'Pattern'); + this.editor = editor; + this.lightshow = editor.lightshow; + this.vortexPort = editor.vortexPort; + this.targetLed = 0; + this.targetLeds = [this.targetLed]; + this.multiEnabled = false; + this.isMulti = false; + } + + initialize() { + this.populatePatternDropdown(); + this.attachPatternDropdownListener(); + this.refresh(); + document.addEventListener('modeChange', this.handleModeChange.bind(this)); + document.addEventListener('ledsChange', this.handleLedsChange.bind(this)); + document.addEventListener('deviceConnected', this.handleDeviceConnected.bind(this)); + + // Attach event listeners for help and randomize buttons + document.getElementById('patternRandomizeButton').addEventListener('click', () => this.randomizePattern()); + } + + randomizePattern() { + const dropdown = document.getElementById('patternDropdown'); + const options = Array.from(dropdown.options); + const randomOption = options[Math.floor(Math.random() * options.length)]; + + if (randomOption) { + dropdown.value = randomOption.value; + this.handlePatternSelect(); // Apply the random pattern + } + } + + handleModeChange(event) { + const selectedLeds = event.detail; + if (selectedLeds.includes('multi')) { + this.setTargetMulti(); + } else { + this.setTargetSingles(selectedLeds); + } + this.populatePatternDropdown(); + this.refresh(true); + this.editor.demoModeOnDevice(); + } + + handleLedsChange(event) { + const selectedLeds = event.detail; + if (selectedLeds.includes('multi')) { + this.setTargetMulti(); + } else { + this.setTargetSingles(selectedLeds); + } + this.populatePatternDropdown(); + this.refresh(true); + } + + handleDeviceConnected() { + this.multiEnabled = true; + this.populatePatternDropdown(); + this.refresh(true); + this.vortexPort.startReading(); + this.editor.demoModeOnDevice(); + } + + setTargetSingles(selectedLeds = null) { + const ledCount = this.lightshow.vortex.engine().leds().ledCount(); + this.targetLeds = (selectedLeds || Array.from({ length: ledCount }, (_, i) => i.toString())) + .map(led => parseInt(led, 10)); + this.targetLed = this.targetLeds[0]; + this.isMulti = false; + } + + setTargetMulti() { + this.targetLed = this.lightshow.vortex.engine().leds().ledMulti(); + this.targetLeds = [this.targetLed]; + this.isMulti = true; + } + + refresh() { + this.refreshPatternDropdown(); + this.refreshPatternArgs(); + } + + populatePatternDropdown() { + const dropdown = document.getElementById('patternDropdown'); + dropdown.innerHTML = ''; + + // Create optgroups for each pattern type + const strobeGroup = document.createElement('optgroup'); + strobeGroup.label = "Strobe Patterns"; + const blendGroup = document.createElement('optgroup'); + blendGroup.label = "Blend Patterns"; + const solidGroup = document.createElement('optgroup'); + solidGroup.label = "Solid Patterns"; + const multiGroup = document.createElement('optgroup'); + multiGroup.label = "Special Patterns (Multi Led)"; + + // Get the PatternID enum values from your wasm module + const patternEnum = this.lightshow.vortexLib.PatternID; + + for (let pattern in patternEnum) { + if (patternEnum.hasOwnProperty(pattern)) { + if (pattern === 'values' || pattern === 'argCount' || + patternEnum[pattern] === patternEnum.PATTERN_NONE || + patternEnum[pattern] === patternEnum.PATTERN_COUNT) { + continue; + } + let option = document.createElement('option'); + let str = this.lightshow.vortex.patternToString(patternEnum[pattern]); + if (str.startsWith("complementary")) { + str = "comp. " + str.slice(14); + } + option.text = str; + option.value = patternEnum[pattern].value; + dropdown.appendChild(option); + + if (str.includes("blend")) { + blendGroup.appendChild(option); + } else if (str.includes("solid")) { + solidGroup.appendChild(option); + } else if (patternEnum[pattern].value > patternEnum.PATTERN_SOLID.value) { + multiGroup.appendChild(option); + } else { + strobeGroup.appendChild(option); + } + } + } + + // Append the optgroups to the dropdown + dropdown.appendChild(strobeGroup); + dropdown.appendChild(blendGroup); + dropdown.appendChild(solidGroup); + + if (this.editor.modesPanel.selectedDevice !== 'None') { + dropdown.appendChild(multiGroup); + } + } + + attachPatternDropdownListener() { + const dropdown = document.getElementById('patternDropdown'); + dropdown.addEventListener('change', this.handlePatternSelect.bind(this)); + } + + refreshPatternDropdown() { + const dropdown = document.getElementById('patternDropdown'); + const curMode = this.lightshow.vortex.engine().modes().curMode(); + if (!curMode) { + dropdown.value = -1; + dropdown.disabled = true; + return; + } + dropdown.disabled = false; + dropdown.value = curMode.getPatternID(this.targetLed).value; + } + + handlePatternSelect() { + const dropdown = document.getElementById('patternDropdown'); + const selectedPattern = dropdown.value; + const curMode = this.lightshow.vortex.engine().modes().curMode(); + const patID = this.lightshow.vortexLib.PatternID.values[selectedPattern]; + if (!curMode || !patID) return; + + const set = curMode.getColorset(this.targetLed); + + if (this.lightshow.vortexLib.isSingleLedPatternID(patID)) { + curMode.clearPattern(this.lightshow.vortex.engine().leds().ledMulti()); + if (this.isMulti) { + curMode.setPattern(patID, this.lightshow.vortex.engine().leds().ledCount(), null, null); + curMode.setColorset(set, this.lightshow.vortex.engine().leds().ledCount()); + this.setTargetSingles(); + } else { + this.targetLeds.forEach(led => { + curMode.setPattern(patID, led, null, null); + curMode.setColorset(set, led); + }); + } + } else { + curMode.setPattern(patID, this.lightshow.vortex.engine().leds().ledMulti(), null, null); + curMode.setColorset(set, this.lightshow.vortex.engine().leds().ledMulti()); + this.setTargetMulti(); + } + curMode.init(); + this.lightshow.vortex.engine().modes().saveCurMode(); + document.dispatchEvent(new CustomEvent('patternChange')); + this.refreshPatternArgs(); + this.editor.demoModeOnDevice(); + } + + refreshPatternArgs() { + const paramsDiv = document.getElementById('patternParams'); + const curMode = this.lightshow.vortex.engine().modes().curMode(); + const patternID = this.lightshow.vortexLib.PatternID.values[document.getElementById('patternDropdown').value]; + + if (!curMode || !patternID) { + paramsDiv.innerHTML = this.generateEmptySlots(7); + return; + } + + const numOfParams = this.lightshow.vortex.numCustomParams(patternID); + paramsDiv.innerHTML = ''; // Clear existing params + + for (let i = 0; i < 7; i++) { + const container = document.createElement('div'); + container.className = 'control-line'; + const isDisabled = i >= numOfParams; + + const sliderContainer = document.createElement('div'); + sliderContainer.className = 'control-slider-container'; + if (isDisabled) sliderContainer.classList.add('disabled'); + + const label = document.createElement('span'); + label.className = 'control-label'; + + let customParams = this.lightshow.vortex.getCustomParams(patternID); + const param = customParams.get(i) + if (param) { + // convert to all lowercase cleaned version + const sliderNameClean = param.slice(2) + .replace(/([a-z])([A-Z])/g, '$1 $2') + .replace(/([A-Z])([A-Z][a-z])/g, '$1 $2') + .toLowerCase(); + // lookup the friendly nice name with clean version + label.textContent = this.getTooltipNiceName(sliderNameClean); + } + + const slider = document.createElement('input'); + slider.type = 'range'; + slider.min = '0'; + slider.max = '255'; + slider.value = isDisabled ? 0 : curMode.getArg(i, this.targetLed) || '0'; + slider.className = 'control-slider'; + this.updateSliderFill(slider); // Set initial gradient + + const input = document.createElement('input'); + input.type = 'number'; + input.min = '0'; + input.max = '255'; + input.value = slider.value; + input.className = `control-input ${isDisabled ? 'disabled' : ''}`; + if (isDisabled) { + input.disabled = true; + slider.disabled = true; + } + + // Sync slider and input values + slider.addEventListener('input', (e) => { + input.value = e.target.value; + this.updatePatternArg(i, e.target.value); + this.updateSliderFill(slider); // Update gradient + }); + input.addEventListener('input', (e) => { + slider.value = e.target.value; + this.updatePatternArg(i, e.target.value); + this.updateSliderFill(slider); // Update gradient + }); + + sliderContainer.append(label, slider); + container.append(sliderContainer, input); + paramsDiv.appendChild(container); + } + } + + updateSliderFill(slider) { + const value = slider.value; + const max = slider.max || 255; // Default max to 255 + const percent = (value / max) * 100; // Calculate fill percentage + slider.style.setProperty('--slider-fill', `${percent}%`); + } + + updatePatternArg(index, value) { + const curMode = this.lightshow.vortex.engine().modes().curMode(); + if (!curMode) return; + this.targetLeds.forEach(led => { + curMode.getPattern(led).setArg(index, value); + }); + curMode.init(); + this.lightshow.vortex.engine().modes().saveCurMode(); + document.dispatchEvent(new CustomEvent('patternChange')); + this.editor.demoModeOnDevice(); + } + + generateEmptySlots(max) { + return Array.from({ length: max }, () => `
`).join(''); + } + + getTooltipText(sliderName) { + const descriptions = { + "on duration": "This determines how long the LED light stays 'on' during each blink. Think of it like the length of time each color shows up when the LED is cycling through colors.", + "off duration": "This is the amount of time the LED light stays 'off' between each blink. It's like the pause or gap between each color as the LED cycles.", + "gap duration": "After the LED completes one full cycle of colors, this sets the length of the pause before it starts the next cycle.", + "dash duration": "After the main gap, this adds an extra 'on' period. Imagine it as an additional burst of light after the cycle completes.", + "group size": "This is the amount of on-off blinks in a cycle. If this is 0, it will use the number of colors. This will do nothing if gap is 0", + "blend speed": "This controls the speed at which the LED transitions or blends from one color to the next. If it's set to 0, the LED will stay on a single color without moving to the next.", + "num flips": "Every other blink the LED will show a hue that's related to the current color. This setting controls how many times that happens.", + "col index": "If you're using a solid pattern, this decides which specific color from the colorset will be displayed. For example, if you set it to 0, it will pick the first color; if 1, the second, and so on.", + // Add more slider names and their descriptions as needed + } + return descriptions[sliderName] || "Description not available"; // Default text if no description is found + } + + getTooltipNiceName(sliderName) { + // these names come from vortexlib we give them nicer names for labaels + const nicerNames = { + "on duration": "'On' Duration", + "off duration": "'Off' Duration", + "gap duration": "'Gap' Duration", + "dash duration": "'Dash' Duration", + "group size": "Group Size", + "blend speed": "Blend Speed", + "num flips": "Blend Flip Count", + "col index": "Solid Color Index", + // hueshift + "blink on duration": "Blink On Duration", + "blink off duration": "Blink Off Duration", + "blend delay": "Blend Delay", + // theatre_case + "step duration": "Step Duration", + // zigzag + "snake size": "Snake Size", + "fade amount": "Fade Amount", + // dripmorph + "speed": "Speed", + // lighthouse + "fade rate": "Fade Rate", + // pulsish + "on duration1": "'On' Duration 1", + "off duration1": "'Off' Duration 1", + "on duration2": "'On' Duration 2", + "off duration2": "'Off' Duration 2", + // split strobie + "first pattern args.arg1": "First Pattern Arg 1", + "first pattern args.arg2": "First Pattern Arg 2", + "first pattern args.arg3": "First Pattern Arg 3", + "second pattern args.arg1": "Second Pattern Arg 1", + "second pattern args.arg2": "Second Pattern Arg 2", + "first pat": "First Pattern ID", + "sec pat": "Second Pattern ID", + // Add more slider names and their descriptions as needed + }; + return nicerNames[sliderName] || sliderName; // Default text if no description is found + } +} + diff --git a/js/VortexEditor.js b/js/VortexEditor.js new file mode 100644 index 0000000..92a0803 --- /dev/null +++ b/js/VortexEditor.js @@ -0,0 +1,101 @@ +/* VortexEditor.js */ +import Lightshow from './Lightshow.js'; +import AboutPanel from './AboutPanel.js'; +import AnimationPanel from './AnimationPanel.js'; +import ControlPanel from './ControlPanel.js'; +import PatternPanel from './PatternPanel.js'; +import ColorsetPanel from './ColorsetPanel.js'; +import ColorPickerPanel from './ColorPickerPanel.js'; +import ModesPanel from './ModesPanel.js'; +import Modal from './Modal.js'; +import VortexPort from './VortexPort.js'; +import WelcomePanel from './WelcomePanel.js'; // Add this import + +export default class VortexEditor { + constructor(vortexLib) { + this.vortexLib = vortexLib; + + // Create and append canvas dynamically + this.canvas = document.createElement('canvas'); + this.canvas.id = 'lightshowCanvas'; + this.canvas.width = 800; + this.canvas.height = 600; + document.body.appendChild(this.canvas); + + // Initialize VortexPort + this.vortexPort = new VortexPort(); + + // Instantiate Lightshow + this.lightshow = new Lightshow(vortexLib, this.canvas); + + // Instantiate Panels + this.welcomePanel = new WelcomePanel(this); // Add WelcomePanel + this.aboutPanel = new AboutPanel(this); + this.animationPanel = new AnimationPanel(this); + this.patternPanel = new PatternPanel(this); + this.colorsetPanel = new ColorsetPanel(this); + this.modesPanel = new ModesPanel(this); + this.colorPicker = new ColorPickerPanel(this); + + this.panels = [ + this.welcomePanel, // Add WelcomePanel to panels array + this.aboutPanel, + this.animationPanel, + this.patternPanel, + this.colorsetPanel, + this.modesPanel, + this.colorPicker, + ]; + } + + initialize() { + // Start the lightshow + this.lightshow.start(); + + // Append panels to the DOM + this.panels.forEach((panel) => panel.appendTo(document.body)); + + // Initialize Panels + this.panels.forEach((panel) => panel.initialize()); + + // Handle URL-imported mode data + this.importModeDataFromUrl(); + + // Listen for window resize to adjust lightshow + window.addEventListener('resize', () => this.lightshow.resetToCenter()); + } + + importModeDataFromUrl() { + const urlParams = new URLSearchParams(window.location.search); + const encodedData = urlParams.get('data'); + + if (encodedData) { + try { + this.modesPanel.importPatternFromData(atob(encodedData), false); + } catch (error) { + console.error('Error parsing mode data:', error); + } + } + } + + async demoColorOnDevice(color) { + try { + if (!this.vortexPort.isTransmitting && this.vortexPort.isActive()) { + await this.vortexPort.demoColor(this.lightshow.vortexLib, this.lightshow.vortex, color); + } + } catch (error) { + Notification.failure("Failed to demo color (" + error + ")"); + } + } + + async demoModeOnDevice() { + try { + if (!this.vortexPort.isTransmitting && this.vortexPort.isActive()) { + await this.vortexPort.demoCurMode(this.lightshow.vortexLib, this.lightshow.vortex); + } + } catch (error) { + Notification.failure("Failed to demo mode (" + error + ")"); + } + } +} + diff --git a/js/WelcomePanel.js b/js/WelcomePanel.js new file mode 100644 index 0000000..235ab0a --- /dev/null +++ b/js/WelcomePanel.js @@ -0,0 +1,43 @@ +/* WelcomePanel.js */ +import Panel from './Panel.js'; + +export default class WelcomePanel extends Panel { + constructor(editor) { + const content = ` +

Welcome to lightshow.lol

+

Hello! If you found this website then you're likely a flow artist or glover. If you have no idea what that means, then welcome to your first lightshow! + This website is an ongoing development designed for both enjoyment and as a tool to control Vortex Lightshow Devices.

+ +

New Updates

+

Lots of new updates are being deployed lately, explore the new UI and keep an eye out for new features. See the + Github to share suggestions or report any bugs.

+ +

The Wiki

+

If you're new or just want to dive deeper, check out the Vortex Engine Wiki for guides, tips, and instructions:

+ + Learn More + + +
+ +
+ `; + + super('welcomePanel', content, 'Welcome', { showCloseButton: true }); + this.welcomeToken = 'showNewWelcome'; + this.editor = editor; + } + + initialize() { + const doNotShowCheckbox = this.panel.querySelector('#doNotShowAgain'); + doNotShowCheckbox.addEventListener('change', (event) => { + localStorage.setItem(this.welcomeToken, !event.target.checked); + }); + + const showWelcome = localStorage.getItem(this.welcomeToken) !== 'false'; + if (!showWelcome) { + this.hide(); + } + } +} + diff --git a/public/data/chromadeck-led-positions.json b/public/data/chromadeck-led-positions.json new file mode 100644 index 0000000..3ada878 --- /dev/null +++ b/public/data/chromadeck-led-positions.json @@ -0,0 +1,27 @@ +{ + "device_name": "Chromadeck", + "original_width": 500, + "original_height": 250, + "points": [ + {"x": 250, "y": 11, "name": "Outer Ring 12 Oclock" }, + {"x": 318, "y": 32, "name": "Outer Ring 1 Oclock" }, + {"x": 360, "y": 90, "name": "Outer Ring 2 Oclock" }, + {"x": 360, "y": 162, "name": "Outer Ring 4 Oclock" }, + {"x": 318, "y": 219, "name": "Outer Ring 5 Oclock" }, + {"x": 250, "y": 241, "name": "Outer Ring 6 Oclock" }, + {"x": 183, "y": 219, "name": "Outer Ring 7 Oclock" }, + {"x": 141, "y": 161, "name": "Outer Ring 8 Oclock" }, + {"x": 141, "y": 90, "name": "Outer Ring 10 Oclock" }, + {"x": 183, "y": 33, "name": "Outer Ring 11 Oclock" }, + {"x": 250, "y": 44, "name": "Inner Ring 12 Oclock" }, + {"x": 298, "y": 60, "name": "Inner Ring 1 Oclock" }, + {"x": 328, "y": 100, "name": "Inner Ring 2 Oclock" }, + {"x": 328, "y": 150, "name": "Inner Ring 4 Oclock" }, + {"x": 298, "y": 192, "name": "Inner Ring 5 Oclock" }, + {"x": 250, "y": 206, "name": "Inner Ring 6 Oclock" }, + {"x": 203, "y": 191, "name": "Inner Ring 7 Oclock" }, + {"x": 173, "y": 151, "name": "Inner Ring 8 Oclock" }, + {"x": 173, "y": 100, "name": "Inner Ring 10 Oclock" }, + {"x": 203, "y": 60, "name": "Inner Ring 11 Oclock" } + ] +} diff --git a/public/data/duo-led-positions.json b/public/data/duo-led-positions.json new file mode 100644 index 0000000..2e104d3 --- /dev/null +++ b/public/data/duo-led-positions.json @@ -0,0 +1,9 @@ +{ + "device_name": "Duo", + "original_width": 500, + "original_height": 250, + "points": [ + {"x": 250, "y": 22, "name": "Tip Led" }, + {"x": 250, "y": 138, "name": "Top Led" } + ] +} diff --git a/public/data/gloves-led-positions.json b/public/data/gloves-led-positions.json new file mode 100644 index 0000000..a8ba00a --- /dev/null +++ b/public/data/gloves-led-positions.json @@ -0,0 +1,17 @@ +{ + "device_name": "Gloves", + "original_width": 500, + "original_height": 250, + "points": [ + { "x": 425, "y": 150, "name": "pinkie tip" }, + { "x": 425, "y": 172, "name": "pinkie top" }, + { "x": 337, "y": 77, "name": "ring tip" }, + { "x": 337, "y": 98, "name": "ring top" }, + { "x": 251, "y": 39, "name": "middle tip" }, + { "x": 251, "y": 61, "name": "middle top" }, + { "x": 163, "y": 77, "name": "index tip" }, + { "x": 163, "y": 99, "name": "index top" }, + { "x": 66, "y": 201, "name": "thumb tip" }, + { "x": 66, "y": 225, "name": "thumb top" } + ] +} diff --git a/public/data/handle-led-positions.json b/public/data/handle-led-positions.json new file mode 100644 index 0000000..25c6920 --- /dev/null +++ b/public/data/handle-led-positions.json @@ -0,0 +1,10 @@ +{ + "device_name": "Handle", + "original_width": 500, + "original_height": 250, + "points": [ + { "x": 150, "y": 191, "name": "front" }, + { "x": 150, "y": 222, "name": "tip" }, + { "x": 353, "y": 216, "name": "back" } + ] +} diff --git a/public/data/orbit-led-positions.json b/public/data/orbit-led-positions.json new file mode 100644 index 0000000..1e08612 --- /dev/null +++ b/public/data/orbit-led-positions.json @@ -0,0 +1,35 @@ +{ + "device_name": "Orbit", + "original_width": 500, + "original_height": 250, + "points": [ + {"x": 161, "y": 161, "name": "Quadrant 1 top inner" }, + {"x": 178, "y": 178, "name": "Quadrant 1 top middle" }, + {"x": 195, "y": 195, "name": "Quadrant 1 top outer" }, + {"x": 218, "y": 218, "name": "Quadrant 1 edge" }, + {"x": 309, "y": 194, "name": "Quadrant 1 bottom outer" }, + {"x": 325, "y": 178, "name": "Quadrant 1 bottom middle" }, + {"x": 341, "y": 162, "name": "Quadrant 1 bottom inner" }, + {"x": 410, "y": 161, "name": "Quadrant 2 bottom inner" }, + {"x": 427, "y": 178, "name": "Quadrant 2 bottom middle" }, + {"x": 443, "y": 194, "name": "Quadrant 2 bottom outer" }, + {"x": 36, "y": 218, "name": "Quadrant 2 edge" }, + {"x": 58, "y": 195, "name": "Quadrant 2 top outer" }, + {"x": 76, "y": 178, "name": "Quadrant 2 top middle" }, + {"x": 93, "y": 161, "name": "Quadrant 2 top inner" }, + {"x": 92, "y": 93, "name": "Quadrant 3 top inner" }, + {"x": 76, "y": 77, "name": "Quadrant 3 top middle" }, + {"x": 59, "y": 60, "name": "Quadrant 3 top outer" }, + {"x": 35, "y": 35, "name": "Quadrant 3 edge" }, + {"x": 442, "y": 60, "name": "Quadrant 3 bottom outer" }, + {"x": 426, "y": 76, "name": "Quadrant 3 bottom middle" }, + {"x": 410, "y": 93, "name": "Quadrant 3 bottom inner" }, + {"x": 342, "y": 93, "name": "Quadrant 4 bottom inner" }, + {"x": 325, "y": 76, "name": "Quadrant 4 bottom middle" }, + {"x": 309, "y": 60, "name": "Quadrant 4 bottom outer" }, + {"x": 217, "y": 36, "name": "Quadrant 4 edge" }, + {"x": 192, "y": 61, "name": "Quadrant 4 top outer" }, + {"x": 176, "y": 77, "name": "Quadrant 4 top middle" }, + {"x": 159, "y": 93, "name": "Quadrant 4 top inner" } + ] +} diff --git a/public/data/spark-led-positions.json b/public/data/spark-led-positions.json new file mode 100644 index 0000000..979c60c --- /dev/null +++ b/public/data/spark-led-positions.json @@ -0,0 +1,13 @@ +{ + "device_name": "Spark", + "original_width": 500, + "original_height": 250, + "points": [ + {"x": 300, "y": 40, "name": "1 oclock" }, + {"x": 350, "y": 125, "name": "3 oclock" }, + {"x": 300, "y": 212, "name": "5 oclock" }, + {"x": 200, "y": 212, "name": "7 oclock" }, + {"x": 150, "y": 125, "name": "9 oclock" }, + {"x": 200, "y": 40, "name": "11 oclock" } + ] +} diff --git a/public/data/transparent-glove-led-positions.json b/public/data/transparent-glove-led-positions.json new file mode 100644 index 0000000..4eecf45 --- /dev/null +++ b/public/data/transparent-glove-led-positions.json @@ -0,0 +1,18 @@ + +{ + "device_name": "Gloves", + "original_width": 1200, + "original_height": 1205, + "points": [ + { "x": 1011, "y": 638, "name": "pinkie tip" }, + { "x": 989, "y": 740, "name": "pinkie top" }, + { "x": 867, "y": 816, "name": "ring tip" }, + { "x": 760, "y": 836, "name": "ring top" }, + { "x": 446, "y": 727, "name": "middle tip" }, + { "x": 649, "y": 842, "name": "middle top" }, + { "x": 524, "y": 873, "name": "index tip" }, + { "x": 351, "y": 902, "name": "index top" }, + { "x": 224, "y": 844, "name": "thumb tip" }, + { "x": 165, "y": 671, "name": "thumb top" } + ] +} diff --git a/public/images/gloves.png b/public/images/gloves.png index 4c17340..ee8ee79 100644 Binary files a/public/images/gloves.png and b/public/images/gloves.png differ