dials now sync with robot config, and adjust position to match curve editor as timeline moves
parent
94daeafb79
commit
218e412ed2
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -34,6 +34,7 @@ export class ServoMotor {
|
|||
// Simplified constructor
|
||||
this.CHANNEL = arg1;
|
||||
this.ID = arg2;
|
||||
|
||||
this.MODEL = arg3 || 'Unknown Model';
|
||||
}
|
||||
}
|
||||
|
|
|
|||
25
index.html
25
index.html
|
|
@ -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
247
script.js
|
|
@ -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();
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue