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
+
+
-
-
-
+
-
-
-
-
+
-
-
-
-
-
+
+
+
+
-
+
-
-
\ 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;
}