diff --git a/index.html b/index.html index ad1eba9..3838218 100644 --- a/index.html +++ b/index.html @@ -1,280 +1,37 @@ - - + - - ESP32 Animation Creator - - + + ESP32 Animation Creator + + - -

ESP32 Animation Creator

- - +

ESP32 Animation Creator

+ + -
-
- -
- 512 -
-
- -
- 512 -
-
- -
- 512 -
-
- -
- 512 -
-
- -
- 512 -
-
- - +
+
512
+
512
+
512
+
512
+
512
+
-
-
- -
+ - -
- - - +
+
+ +
- + - - \ No newline at end of file + diff --git a/script.js b/script.js index 2be6991..0df8b97 100644 --- a/script.js +++ b/script.js @@ -1,176 +1,308 @@ window.onload = () => { - let isInterpolating = false; - let currentFrame = 0; - let dialKeyframes = Array.from({ length: 5 }, () => ({})); - let port, reader, writer; - const dials = []; - const frameSlider = document.getElementById('frameSlider'); - const frameDisplay = document.getElementById('frameDisplay'); - const canvas = document.getElementById('timelineCanvas'); - const ctx = canvas.getContext('2d'); - const totalFrames = 400; + let isInterpolating = false; + let currentFrame = 0; + let dialKeyframes = Array.from({ length: 5 }, () => ({})); + let port, reader, writer; + let selectedDial = null; + let draggingKeyframe = null; // { dialIndex, originalFrame } + let isDragging = false; + const dialColors = ['red', 'green', 'blue', 'orange', 'purple']; - frameSlider.oninput = () => { - currentFrame = parseInt(frameSlider.value); - frameDisplay.textContent = currentFrame; - isInterpolating = true; + const dials = []; + const frameSlider = document.getElementById('frameSlider'); + const frameDisplay = document.getElementById('frameDisplay'); + const canvas = document.getElementById('timelineCanvas'); + const ctx = canvas.getContext('2d'); + const totalFrames = 400; - for (let ch = 0; ch < 5; ch++) { - const keyframes = dialKeyframes[ch]; - let prevFrame = null, nextFrame = null; + frameSlider.oninput = () => { + currentFrame = parseInt(frameSlider.value); + frameDisplay.textContent = currentFrame; + isInterpolating = true; - for (let f = currentFrame; f >= 0; f--) { - if (keyframes[f] !== undefined) { - prevFrame = f; - break; + for (let ch = 0; ch < 5; ch++) { + const keyframes = dialKeyframes[ch]; + let prevFrame = null, nextFrame = null; + + for (let f = currentFrame; f >= 0; f--) { + if (keyframes[f] !== undefined) { + prevFrame = f; + break; + } + } + for (let f = currentFrame; f <= 399; f++) { + if (keyframes[f] !== undefined) { + nextFrame = f; + break; + } + } + + let value; + if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) { + const prevVal = keyframes[prevFrame]; + const nextVal = keyframes[nextFrame]; + const t = (currentFrame - prevFrame) / (nextFrame - prevFrame); + value = Math.round(prevVal + (nextVal - prevVal) * t); + } else if (prevFrame !== null) { + value = keyframes[prevFrame]; + } else if (nextFrame !== null) { + value = keyframes[nextFrame]; + } else { + value = 512; + } + + dials[ch].value = value; + document.getElementById(`value${ch}`).textContent = value; } - } - for (let f = currentFrame; f <= 399; f++) { - if (keyframes[f] !== undefined) { - nextFrame = f; - break; - } - } - let value; - if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) { - const prevVal = keyframes[prevFrame]; - const nextVal = keyframes[nextFrame]; - const t = (currentFrame - prevFrame) / (nextFrame - prevFrame); - value = Math.round(prevVal + (nextVal - prevVal) * t); - } else if (prevFrame !== null) { - value = keyframes[prevFrame]; - } else if (nextFrame !== null) { - value = keyframes[nextFrame]; - } else { - value = 512; - } + drawTimelineMarkers(); + isInterpolating = false; + }; - dials[ch].value = value; - document.getElementById(`value${ch}`).textContent = value; + for (let i = 0; i < 5; i++) { + dials[i] = new Nexus.Dial(`#dial${i}`, { + size: [80, 80], + min: 0, + max: 1023, + value: 512 + }); + + dials[i].colorize("accent", dialColors[i]); + + dials[i].on('change', (v) => { + if (isInterpolating) return; + const val = Math.round(v); + document.getElementById(`value${i}`).textContent = val; + dialKeyframes[i][currentFrame] = val; + drawTimelineMarkers(); + }); } + document.querySelectorAll('.dial').forEach(el => { + el.onclick = () => { + selectedDial = parseInt(el.dataset.index); + + document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected')); + el.classList.add('selected'); + + drawTimelineMarkers(); + }; + }); + + + document.getElementById('connect').onclick = async () => { + port = await navigator.serial.requestPort(); + await port.open({ baudRate: 115200 }); + writer = port.writable.getWriter(); + reader = port.readable.getReader(); + + const decoder = new TextDecoder(); + while (true) { + const { value, done } = await reader.read(); + if (done) break; + document.getElementById('log').value += decoder.decode(value); + } + }; + + document.getElementById('send').onclick = async () => { + const text = document.getElementById('input').value + '\n'; + const encoder = new TextEncoder(); + await writer.write(encoder.encode(text)); + document.getElementById('input').value = ''; + }; + + document.getElementById('sendFrame').onclick = async () => { + const positions = dials.map(d => Math.round(d.value)); + const message = `FRAME ${positions.join(',')}\n`; + const encoder = new TextEncoder(); + await writer.write(encoder.encode(message)); + }; + + function drawTimelineMarkers() { + ctx.clearRect(0, 0, canvas.width, canvas.height); + const width = canvas.width; + const height = canvas.height; + + // Draw tick marks every 50 frames + ctx.strokeStyle = '#aaa'; + ctx.lineWidth = 1; + for (let f = 0; f <= totalFrames; f += 25) { + const x = (f / totalFrames) * width; + ctx.beginPath(); + ctx.moveTo(x, height * 0.75); // small tick at bottom + ctx.lineTo(x, height); + ctx.stroke(); + } + for (let f = 0; f <= totalFrames; f += 50) { + const x = (f / totalFrames) * width; + ctx.beginPath(); + ctx.moveTo(x, height * 0.65); // small tick at bottom + ctx.lineTo(x, height); + ctx.stroke(); + } + + // Draw label on tick marks + ctx.fillStyle = '#666'; + ctx.font = '10px sans-serif'; + ctx.textAlign = 'center'; + + for (let f = 0; f <= totalFrames; f += 50) { + const x = (f / totalFrames) * width; + const seconds = f / 50; + ctx.fillText(seconds.toString() + "s", x, height - 12); + } + + + if (selectedDial !== null) { + for (let frame in dialKeyframes[selectedDial]) { + const x = (frame / 400) * width; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.strokeStyle = dialColors[selectedDial]; + ctx.lineWidth = 2; + ctx.stroke(); + } + } else { + for (let ch = 0; ch < 5; ch++) { + for (let frame in dialKeyframes[ch]) { + const x = (frame / 400) * width; + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, height); + ctx.strokeStyle = dialColors[ch]; + ctx.lineWidth = 1; + ctx.stroke(); + } + } + } + + const currentX = (currentFrame / 400) * width; + ctx.beginPath(); + ctx.moveTo(currentX, 0); + ctx.lineTo(currentX, height); + ctx.strokeStyle = 'black'; + ctx.lineWidth = 2; + ctx.stroke(); + } + + document.addEventListener('click', (e) => { + const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS']; + const clickedInsideDial = e.target.closest('.dial'); + const clickedControl = ignoredTags.includes(e.target.tagName); + + if (!clickedInsideDial && !clickedControl && selectedDial !== null) { + selectedDial = null; + document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected')); + drawTimelineMarkers(); + } + }); + + canvas.addEventListener('mousedown', (e) => { + const x = e.offsetX; + const frame = Math.round((x / canvas.width) * totalFrames); + + const dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4]; + + for (let ch of dialIndices) { + for (let f in dialKeyframes[ch]) { + const fx = (f / totalFrames) * canvas.width; + if (Math.abs(fx - x) < 5) { + draggingKeyframe = { dialIndex: ch, originalFrame: parseInt(f) }; + isDragging = true; + return; + } + } + } + }); + + canvas.addEventListener('mousemove', (e) => { + if (!isDragging || !draggingKeyframe) return; + + const x = e.offsetX; + const newFrame = Math.max(0, Math.min(totalFrames - 1, Math.round((x / canvas.width) * totalFrames))); + + const { dialIndex, originalFrame } = draggingKeyframe; + const value = dialKeyframes[dialIndex][originalFrame]; + + if (newFrame !== originalFrame) { + delete dialKeyframes[dialIndex][originalFrame]; + dialKeyframes[dialIndex][newFrame] = value; + draggingKeyframe.originalFrame = newFrame; + drawTimelineMarkers(); + } + }); + + + canvas.addEventListener('mouseup', () => { + isDragging = false; + draggingKeyframe = null; + }); + + canvas.addEventListener('dblclick', (e) => { + const x = e.offsetX; + const frame = Math.round((x / canvas.width) * totalFrames); + const dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4]; + + for (let ch of dialIndices) { + if (dialKeyframes[ch][frame] !== undefined) { + delete dialKeyframes[ch][frame]; + drawTimelineMarkers(); + } + } + }); + + + + document.getElementById('saveAnimation').onclick = async () => { + if (!writer) { + alert("Serial not connected."); + return; + } + + const buffer = new ArrayBuffer(totalFrames * 5 * 2); + const view = new DataView(buffer); + + for (let frame = 0; frame < totalFrames; frame++) { + for (let ch = 0; ch < 5; ch++) { + const keyframes = dialKeyframes[ch]; + let prevFrame = null, nextFrame = null; + + for (let f = frame; f >= 0; f--) { + if (keyframes[f] !== undefined) { + prevFrame = f; + break; + } + } + for (let f = frame; f <= totalFrames - 1; f++) { + if (keyframes[f] !== undefined) { + nextFrame = f; + break; + } + } + + let value; + if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) { + const prevVal = keyframes[prevFrame]; + const nextVal = keyframes[nextFrame]; + const t = (frame - prevFrame) / (nextFrame - prevFrame); + value = Math.round(prevVal + (nextVal - prevVal) * t); + } else if (prevFrame !== null) { + value = keyframes[prevFrame]; + } else if (nextFrame !== null) { + value = keyframes[nextFrame]; + } else { + value = 512; + } + + view.setUint16((frame * 5 + ch) * 2, value, true); + } + } + + + + await writer.write(buffer); + alert("Animation sent over serial."); + }; drawTimelineMarkers(); - isInterpolating = false; - }; - - for (let i = 0; i < 5; i++) { - dials[i] = new Nexus.Dial(`#dial${i}`, { - size: [80, 80], - min: 0, - max: 1023, - value: 512 - }); - - dials[i].on('change', (v) => { - if (isInterpolating) return; - const val = Math.round(v); - document.getElementById(`value${i}`).textContent = val; - dialKeyframes[i][currentFrame] = val; - drawTimelineMarkers(); - }); - } - - document.getElementById('connect').onclick = async () => { - port = await navigator.serial.requestPort(); - await port.open({ baudRate: 115200 }); - writer = port.writable.getWriter(); - reader = port.readable.getReader(); - - const decoder = new TextDecoder(); - while (true) { - const { value, done } = await reader.read(); - if (done) break; - document.getElementById('log').value += decoder.decode(value); - } - }; - - document.getElementById('send').onclick = async () => { - const text = document.getElementById('input').value + '\n'; - const encoder = new TextEncoder(); - await writer.write(encoder.encode(text)); - document.getElementById('input').value = ''; - }; - - document.getElementById('sendFrame').onclick = async () => { - const positions = dials.map(d => Math.round(d.value)); - const message = `FRAME ${positions.join(',')}\n`; - const encoder = new TextEncoder(); - await writer.write(encoder.encode(message)); - }; - - function drawTimelineMarkers() { - ctx.clearRect(0, 0, canvas.width, canvas.height); - const width = canvas.width; - const height = canvas.height; - - for (let ch = 0; ch < 5; ch++) { - for (let frame in dialKeyframes[ch]) { - const x = (frame / 400) * width; - ctx.beginPath(); - ctx.moveTo(x, 0); - ctx.lineTo(x, height); - ctx.strokeStyle = ['red', 'green', 'blue', 'orange', 'purple'][ch]; - ctx.lineWidth = 1; - ctx.stroke(); - } - } - - const currentX = (currentFrame / 400) * width; - ctx.beginPath(); - ctx.moveTo(currentX, 0); - ctx.lineTo(currentX, height); - ctx.strokeStyle = 'black'; - ctx.lineWidth = 2; - ctx.stroke(); - } - - document.getElementById('saveAnimation').onclick = async () => { - if (!writer) { - alert("Serial not connected."); - return; - } - - const buffer = new ArrayBuffer(totalFrames * 5 * 2); - const view = new DataView(buffer); - - for (let frame = 0; frame < totalFrames; frame++) { - for (let ch = 0; ch < 5; ch++) { - const keyframes = dialKeyframes[ch]; - let prevFrame = null, nextFrame = null; - - for (let f = frame; f >= 0; f--) { - if (keyframes[f] !== undefined) { - prevFrame = f; - break; - } - } - for (let f = frame; f <= totalFrames - 1; f++) { - if (keyframes[f] !== undefined) { - nextFrame = f; - break; - } - } - - let value; - if (prevFrame !== null && nextFrame !== null && prevFrame !== nextFrame) { - const prevVal = keyframes[prevFrame]; - const nextVal = keyframes[nextFrame]; - const t = (frame - prevFrame) / (nextFrame - prevFrame); - value = Math.round(prevVal + (nextVal - prevVal) * t); - } else if (prevFrame !== null) { - value = keyframes[prevFrame]; - } else if (nextFrame !== null) { - value = keyframes[nextFrame]; - } else { - value = 512; - } - - view.setUint16((frame * 5 + ch) * 2, value, true); - } - } - - await writer.write(buffer); - alert("Animation sent over serial."); - }; }; diff --git a/style.css b/style.css index a32de86..6e1112a 100644 --- a/style.css +++ b/style.css @@ -16,6 +16,18 @@ body { align-items: center; } +.dial.selected { + background-color: #ddd; + border-radius: 10px; + padding: 10px; +} + +canvas { + background-color: #f0f0f0; /* light grey */ + border: 1px solid #ccc; /* optional subtle border */ +} + + textarea { margin-top: 20px; }