sophia_controller/index.html

280 lines
9.0 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>ESP32 Animation Creator</title>
<script src="https://unpkg.com/nexusui"></script>
<style>
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;
}
</style>
</head>
<body>
<h2>ESP32 Animation Creator</h2>
<button id="connect">Connect</button>
<button id="sendFrame">Send Frame</button>
<div class="dial-container">
<div class="dial">
<label>Motor 1</label>
<div id="dial0"></div>
<span id="value0">512</span>
</div>
<div class="dial">
<label>Motor 2</label>
<div id="dial1"></div>
<span id="value1">512</span>
</div>
<div class="dial">
<label>Motor 3</label>
<div id="dial2"></div>
<span id="value2">512</span>
</div>
<div class="dial">
<label>Motor 4</label>
<div id="dial3"></div>
<span id="value3">512</span>
</div>
<div class="dial">
<label>Motor 5</label>
<div id="dial4"></div>
<span id="value4">512</span>
</div>
</div>
<canvas id="timelineCanvas" width="800" height="30" style="margin-bottom: 5px;"></canvas>
<div>
<label>Frame: <span id="frameDisplay">0</span></label><br>
<input type="range" id="frameSlider" min="0" max="399" value="0" style="width: 80%">
</div>
<textarea id="log" rows="10" cols="60" readonly></textarea>
<br>
<input type="text" id="input" placeholder="Type message here">
<button id="send">Send</button>
<button id="saveAnimation">Save Animation</button>
<script>
let isInterpolating = false;
let currentFrame = 0;
let dialKeyframes = Array.from({ length: 5 }, () => ({}));
const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay');
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;
};
let port, reader, writer;
const dials = [];
// Initialize rotary dials
for (let i = 0; i < 5; i++) {
dials[i] = new Nexus.Dial(`#dial${i}`, {
size: [80, 80],
min: 0,
max: 1023,
value: 512
});
// Update display when dial changes
dials[i].on('change', (v) => {
if (isInterpolating) return;
const val = Math.round(v);
document.getElementById(`value${i}`).textContent = val;
dialKeyframes[i][currentFrame] = val;
drawTimelineMarkers();
});
}
// Connect to ESP32
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);
}
};
// Send typed message
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 = '';
};
// Send current dial positions as a frame
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));
};
const canvas = document.getElementById('timelineCanvas');
const ctx = canvas.getContext('2d');
const totalFrames = 400;
function drawTimelineMarkers() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const width = canvas.width;
const height = canvas.height;
// Draw keyframe ticks for all motors
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();
}
}
// Draw current frame marker
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 totalFrames = 400;
const buffer = new ArrayBuffer(totalFrames * 5 * 2); // 5 motors × 2 bytes
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); // little-endian
}
}
console.log(buffer);
await writer.write(buffer);
alert("Animation sent over serial.");
};
</script>
</body>
</html>