From cb99feea2f139839ad92cdb1af672bb89dcc2abf Mon Sep 17 00:00:00 2001 From: Jake Date: Sat, 27 Sep 2025 22:57:34 +0800 Subject: [PATCH] refactored from single to multiple pages --- script.js | 176 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ style.css | 25 ++++++++ 2 files changed, 201 insertions(+) create mode 100644 script.js create mode 100644 style.css diff --git a/script.js b/script.js new file mode 100644 index 0000000..2be6991 --- /dev/null +++ b/script.js @@ -0,0 +1,176 @@ +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; + + frameSlider.oninput = () => { + currentFrame = parseInt(frameSlider.value); + frameDisplay.textContent = currentFrame; + isInterpolating = true; + + 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; + } + + 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 new file mode 100644 index 0000000..a32de86 --- /dev/null +++ b/style.css @@ -0,0 +1,25 @@ +body { + font-family: sans-serif; + text-align: center; +} + +.dial-container { + display: flex; + justify-content: center; + gap: 30px; + margin: 20px 0; +} + +.dial { + display: flex; + flex-direction: column; + align-items: center; +} + +textarea { + margin-top: 20px; +} + +canvas { + margin-bottom: 5px; +}