dials now sync with robot config, and adjust position to match curve editor as timeline moves

node_mode
realrobots 2025-10-13 21:10:22 +08:00
parent 94daeafb79
commit 218e412ed2
4 changed files with 112 additions and 219 deletions

View File

@ -1,5 +1,5 @@
export class CurveEditor {
constructor(canvas, timelineLength = 10) {
constructor(canvas, timelineLength = 10, _slider) {
this.canvas = canvas;
this.ctx = canvas.getContext('2d');
this.timelineLength = timelineLength;
@ -21,7 +21,7 @@ export class CurveEditor {
// COMMENT OUT AND READ FROM EXISTING TIMELINE LATER
const slider = document.getElementById('timeSlider');
const slider = _slider;//document.getElementById('timeSlider');
slider.max = this.timelineLength * this.pixelsPerSecond;
console.log(slider.max);
@ -80,7 +80,7 @@ export class CurveEditor {
console.log(this.curveSets);
}
addChannel(motorID){
addChannel(motorID) {
this.setCurves([
{
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
@ -259,20 +259,50 @@ export class CurveEditor {
return t;
}
getPositionAtTime(timeInSeconds, curve, valueToX, transform) {
// Convert time to pixel X position
const targetX = valueToX(timeInSeconds);
// Transform control points
const p0 = transform(curve.startPoint);
const h0 = transform(curve.startPointHandle);
const h1 = transform(curve.endPointHandle);
const p1 = transform(curve.endPoint);
const t = solveTForX(targetX, p0, h0, h1, p1);
return cubicBezier(t, p0, h0, h1, p1);
// getPositionAtTime(timeInSeconds, curve, valueToX, transform) {
// // Convert time to pixel X position
// const targetX = valueToX(timeInSeconds);
// // Transform control points
// const p0 = transform(curve.startPoint);
// const h0 = transform(curve.startPointHandle);
// const h1 = transform(curve.endPointHandle);
// const p1 = transform(curve.endPoint);
// const t = solveTForX(targetX, p0, h0, h1, p1);
// return cubicBezier(t, p0, h0, h1, p1);
// }
getMotorPositionAtTime(motorID, timeInFrames) {
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
return null;
}
const curves = this.curveSets[motorID];
const timeInSeconds = timeInFrames / this.framesPerSecond;
const currentTimeX = timeInFrames;
for (let curve of curves) {
const p0 = this.transform(curve.startPoint);
const h0 = this.transform(curve.startPointHandle);
const h1 = this.transform(curve.endPointHandle);
const p1 = this.transform(curve.endPoint);
const minX = Math.min(p0.x, p1.x);
const maxX = Math.max(p0.x, p1.x);
if (currentTimeX >= minX && currentTimeX <= maxX) {
const t = this.solveTForX(currentTimeX, p0, h0, h1, p1);
const pt = this.cubicBezier(t, p0, h0, h1, p1);
return this.yToExportRange(pt.y);
}
}
return null; // No curve segment matched the time
}
drawCurves(ctx, curves, opacity = 1.0, showControlPoints = true) {

View File

@ -34,6 +34,7 @@ export class ServoMotor {
// Simplified constructor
this.CHANNEL = arg1;
this.ID = arg2;
this.MODEL = arg3 || 'Unknown Model';
}
}

View File

@ -156,28 +156,13 @@
<div class="tab-pane fade show active" id="animation" role="tabpanel" aria-labelledby="animation-tab">
<canvas id="curveCanvas" width="900" height="600"></canvas>
<div style="margin-top: 10px; text-align: center;">
<!-- <div style="margin-top: 10px; text-align: center;">
<input type="range" id="timeSlider" min="0" step="1" style="width: 80%;">
</div>
</div> -->
<div class="dial-container">
<div class="dial" data-index="0"><label>Motor 1</label>
<div id="dial0"></div><span id="value0">512</span>
</div>
<div class="dial" data-index="1"><label>Motor 2</label>
<div id="dial1"></div><span id="value1">512</span>
</div>
<div class="dial" data-index="2"><label>Motor 3</label>
<div id="dial2"></div><span id="value2">512</span>
</div>
<div class="dial" data-index="3"><label>Motor 4</label>
<div id="dial3"></div><span id="value3">512</span>
</div>
<div class="dial" data-index="4"><label>Motor 5</label>
<div id="dial4"></div><span id="value4">512</span>
</div>
</div>
<div id="dialArea" class="dial-container"></div>
<label>
<input type="checkbox" id="syncCheckbox"> Sync
</label>
@ -185,7 +170,7 @@
<input type="checkbox" id="feebackCheckbox"> Feedback
</label>
<canvas id="timelineCanvas" width="800" height="30"></canvas>
<!-- <canvas id="timelineCanvas" width="800" height="30"></canvas> -->
<div>
<label>Frame: <span id="frameDisplay">0</span></label><br>

247
script.js
View File

@ -37,13 +37,12 @@ window.onload = () => {
const dials = [];
const frameSlider = document.getElementById('frameSlider');
const frameDisplay = document.getElementById('frameDisplay');
const canvas = document.getElementById('timelineCanvas');
const ctx = canvas.getContext('2d');
const totalFrames = 500;
const curveCanvas = document.getElementById('curveCanvas');
const curveEditor = new CurveEditor(curveCanvas, 10);
const curveEditor = new CurveEditor(curveCanvas, 10, frameSlider);
// Animation File List
@ -74,8 +73,10 @@ window.onload = () => {
for (let i = 0; i < 5; i++) {
let motor = new ServoMotor(0, 10 + i, "SCS009")
robot.assignMotor(positions[i], motor);
}
addDial(motor.ID);
}
// addDial(123);
// Retrieve motor by position
//console.log(robot.getMotor('eyelids'));
@ -176,78 +177,80 @@ window.onload = () => {
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 <= totalFrames; 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;
//console.log(currentFrame);
syncDialsWithCurveEditor();
syncMotorsWithTimeline();
};
for (let i = 0; i < 5; i++) {
dials[i] = new Nexus.Dial(`#dial${i}`, {
function addDial(motorID) {
const index = dials.length;
// Create dial wrapper
const dialWrapper = document.createElement('div');
dialWrapper.className = 'dial';
dialWrapper.dataset.index = index;
// Create label
const label = document.createElement('label');
label.textContent = "Motor " + motorID;
console.log(motorID);
// Create dial container
const dialDiv = document.createElement('div');
dialDiv.id = `dial${index}`;
// Create value display
const valueSpan = document.createElement('span');
valueSpan.id = `value${index}`;
valueSpan.textContent = '2048';
// Assemble and append
dialWrapper.appendChild(label);
dialWrapper.appendChild(dialDiv);
dialWrapper.appendChild(valueSpan);
document.getElementById('dialArea').appendChild(dialWrapper);
// Create Nexus dial
const dial = new Nexus.Dial(`#dial${index}`, {
size: [80, 80],
min: 0,
max: 1023,
value: 512
max: 4095,
value: 4095 / 2
});
dials[i].colorize("accent", dialColors[i]);
dial.motorID = motorID;
dial.colorize("accent", dialColors[index]);
dials[i].on('change', (v) => {
dial.on('change', (v) => {
if (isInterpolating) return;
const val = Math.round(v);
document.getElementById(`value${i}`).textContent = val;
dialKeyframes[i][currentFrame] = val;
drawTimelineMarkers();
document.getElementById(`value${index}`).textContent = val;
//dialKeyframes[index][currentFrame] = val;
syncMotorsWithTimeline();
});
dials.push(dial);
}
function syncDialsWithCurveEditor() {
//let pos = curveEditor.getMotorPositionAtTime(11, currentFrame);
for (let ch = 0; ch < dials.length; ch++) {
console.log(dials[ch].motorID);
dials[ch].value = curveEditor.getMotorPositionAtTime(dials[ch].motorID, currentFrame);
//const value = dials[ch].value;
//motorPayloads.push({ motorId: ch, position: value });
}
//console.log(pos);
}
function syncMotorsWithTimeline() {
const now = Date.now();
if (syncCheckbox.checked && now - lastSyncTime >= syncIntervalMs) {
lastSyncTime = now;
@ -281,7 +284,6 @@ window.onload = () => {
document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected'));
el.classList.add('selected');
drawTimelineMarkers();
};
});
@ -517,7 +519,7 @@ window.onload = () => {
document.getElementById("filenameInput").value = selectedFile.replace(/\.anim$/i, "");
drawTimelineMarkers();
}
@ -660,7 +662,6 @@ window.onload = () => {
clearBtn.addEventListener('click', () => {
currentFrame = 0;
dialKeyframes = Array.from({ length: 5 }, () => ({}));
drawTimelineMarkers();
});
@ -671,73 +672,6 @@ window.onload = () => {
document.getElementById('input').value = '';
};
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 / totalFrames) * 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 / totalFrames) * width;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, height);
ctx.strokeStyle = dialColors[ch];
ctx.lineWidth = 1;
ctx.stroke();
}
}
}
const currentX = (currentFrame / totalFrames) * 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'];
@ -747,66 +681,11 @@ window.onload = () => {
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 dialIndices = selectedDial !== null ? [selectedDial] : [0, 1, 2, 3, 4];
for (let ch of dialIndices) {
for (let f in dialKeyframes[ch]) {
const frameNum = parseInt(f);
const fx = (frameNum / totalFrames) * canvas.width;
if (Math.abs(fx - x) < 15) {
draggingKeyframe = { dialIndex: ch, originalFrame: frameNum };
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;
syncMotorsWithTimeline();
});
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();
}
}
});
@ -815,8 +694,6 @@ window.onload = () => {
};
drawTimelineMarkers();