loads curve animation files, curveEditor implemented INCOMPLETE
parent
a27551ff9a
commit
8698f47536
|
|
@ -0,0 +1,3 @@
|
||||||
|
{
|
||||||
|
"liveServer.settings.port": 5501
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,692 @@
|
||||||
|
export class CurveEditor {
|
||||||
|
constructor(canvas, timelineLength = 10) {
|
||||||
|
this.canvas = canvas;
|
||||||
|
this.ctx = canvas.getContext('2d');
|
||||||
|
this.timelineLength = timelineLength;
|
||||||
|
|
||||||
|
this.scale = 1;
|
||||||
|
this.offset = { x: 0, y: 0 };
|
||||||
|
this.pixelsPerSecond = 48;
|
||||||
|
|
||||||
|
this.currentTime = timelineLength * this.pixelsPerSecond / 2;
|
||||||
|
|
||||||
|
this.motorButtons = {
|
||||||
|
width: 30,
|
||||||
|
height: 30,
|
||||||
|
padding: 10,
|
||||||
|
buttons: [] // will be populated dynamically
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
// COMMENT OUT AND READ FROM EXISTING TIMELINE LATER
|
||||||
|
const slider = document.getElementById('timeSlider');
|
||||||
|
slider.max = this.timelineLength * this.pixelsPerSecond;
|
||||||
|
|
||||||
|
console.log(slider.max);
|
||||||
|
slider.value = 0;
|
||||||
|
slider.addEventListener('input', () => {
|
||||||
|
this.currentTime = parseFloat(slider.value);
|
||||||
|
console.log(slider.value);
|
||||||
|
this.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.curveSets = {}; // motorID → array of curves
|
||||||
|
this.selectedMotorID = 4;
|
||||||
|
this.curves = [];
|
||||||
|
this.dragging = null;
|
||||||
|
this.isPanning = false;
|
||||||
|
this.panStart = { x: 0, y: 0 };
|
||||||
|
|
||||||
|
// Default curve
|
||||||
|
// this.setCurves([
|
||||||
|
// {
|
||||||
|
// startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
||||||
|
// startPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(0.5) },
|
||||||
|
// endPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(-0.5) },
|
||||||
|
// endPoint: { x: this.valueToX(timelineLength), y: this.valueToY(0) }
|
||||||
|
// }
|
||||||
|
// ]);
|
||||||
|
// this.curveSets[this.selectedMotorID] = this.curves;
|
||||||
|
|
||||||
|
// let othercurves = [
|
||||||
|
// {
|
||||||
|
// startPoint: { x: this.valueToX(0), y: this.valueToY(.3) },
|
||||||
|
// startPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(0.5) },
|
||||||
|
// endPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(-0.5) },
|
||||||
|
// endPoint: { x: this.valueToX(timelineLength), y: this.valueToY(0) }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// this.curveSets[5] = othercurves;
|
||||||
|
|
||||||
|
// othercurves = [
|
||||||
|
// {
|
||||||
|
// startPoint: { x: this.valueToX(0), y: this.valueToY(-.5) },
|
||||||
|
// startPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(0.5) },
|
||||||
|
// endPointHandle: { x: this.valueToX(timelineLength / 2), y: this.valueToY(-0.5) },
|
||||||
|
// endPoint: { x: this.valueToX(timelineLength), y: this.valueToY(0) }
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// this.curveSets[7] = othercurves;
|
||||||
|
|
||||||
|
|
||||||
|
// this.setSelectedMotor(5);
|
||||||
|
|
||||||
|
this.initEvents();
|
||||||
|
this.draw();
|
||||||
|
console.log(this.curveSets);
|
||||||
|
}
|
||||||
|
|
||||||
|
setLength(endTime){
|
||||||
|
this.timelineLength = endTime / this.pixelsPerSecond;
|
||||||
|
console.log("new endtime: " + endTime);
|
||||||
|
//this.currentTime = this.timelineLength * this.pixelsPerSecond / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
loadCurveSets(curveSets) {
|
||||||
|
this.curveSets = curveSets;
|
||||||
|
|
||||||
|
// If selectedMotorID is present in the new set, load its curves
|
||||||
|
if (curveSets[this.selectedMotorID]) {
|
||||||
|
this.setCurves(curveSets[this.selectedMotorID]);
|
||||||
|
} else {
|
||||||
|
this.setCurves([]); // fallback to empty
|
||||||
|
}
|
||||||
|
console.log("LOADED");
|
||||||
|
this.setSelectedMotor(10)
|
||||||
|
//this.selectAdjacentMotor(1);
|
||||||
|
// Optional: update motor selector UI or redraw timeline
|
||||||
|
//this.refreshMotorSelector?.(); // if you have a method for that
|
||||||
|
//this.drawTimelineMarkers?.();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
setSelectedMotor(motorID) {
|
||||||
|
this.selectedMotorID = motorID;
|
||||||
|
this.curves = this.curveSets[motorID] || [];
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
valueToY(v) {
|
||||||
|
return (this.canvas.height / 2 - v * (this.canvas.height / 2)) * this.scale + this.offset.y;
|
||||||
|
}
|
||||||
|
|
||||||
|
yToValue(y) {
|
||||||
|
return ((this.canvas.height / 2 - (y - this.offset.y) / this.scale) / (this.canvas.height / 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
valueToX(t) {
|
||||||
|
return t * this.pixelsPerSecond * this.scale + this.offset.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
xToValue(x) {
|
||||||
|
return (x - this.offset.x) / (this.pixelsPerSecond * this.scale);
|
||||||
|
}
|
||||||
|
|
||||||
|
transform(p) {
|
||||||
|
return {
|
||||||
|
x: p.x * this.scale + this.offset.x,
|
||||||
|
y: p.y * this.scale + this.offset.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
inverseTransform(p) {
|
||||||
|
return {
|
||||||
|
x: (p.x - this.offset.x) / this.scale,
|
||||||
|
y: (p.y - this.offset.y) / this.scale
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
draw() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
this.drawGrid(ctx);
|
||||||
|
|
||||||
|
// Draw inactive curves dimmed
|
||||||
|
for (const [motorID, curves] of Object.entries(this.curveSets)) {
|
||||||
|
const isSelected = motorID == this.selectedMotorID;
|
||||||
|
this.drawCurves(this.ctx, curves, isSelected ? 1.0 : 0.3, isSelected);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.drawCurves(ctx, this.curves);
|
||||||
|
|
||||||
|
this.drawMotorButtons();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
drawMotorButtons() {
|
||||||
|
const ctx = this.ctx;
|
||||||
|
const canvas = this.canvas;
|
||||||
|
const { width, height, padding } = this.motorButtons;
|
||||||
|
const y = canvas.height - height - padding;
|
||||||
|
|
||||||
|
// Start from the right edge and move left
|
||||||
|
const xRight = canvas.width - width - padding;
|
||||||
|
const xMid = xRight - (width + padding);
|
||||||
|
const xLeft = xMid - (width + padding);
|
||||||
|
|
||||||
|
this.motorButtons.buttons = [
|
||||||
|
{ label: "<", x: xLeft, y, motorOffset: -1 },
|
||||||
|
{ label: `${this.selectedMotorID}`, x: xMid, y, motorOffset: 0 },
|
||||||
|
{ label: ">", x: xRight, y, motorOffset: 1 }
|
||||||
|
];
|
||||||
|
|
||||||
|
|
||||||
|
for (let btn of this.motorButtons.buttons) {
|
||||||
|
ctx.fillStyle = "#eee";
|
||||||
|
ctx.fillRect(btn.x, btn.y, width, height);
|
||||||
|
ctx.strokeStyle = "#333";
|
||||||
|
ctx.strokeRect(btn.x, btn.y, width, height);
|
||||||
|
ctx.fillStyle = "#000";
|
||||||
|
ctx.font = "16px sans-serif";
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
ctx.fillText(btn.label, btn.x + width / 2, btn.y + height / 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cubicBezierX(t, p0, h0, h1, p1) {
|
||||||
|
return Math.pow(1 - t, 3) * p0.x +
|
||||||
|
3 * Math.pow(1 - t, 2) * t * h0.x +
|
||||||
|
3 * (1 - t) * Math.pow(t, 2) * h1.x +
|
||||||
|
Math.pow(t, 3) * p1.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
solveTForX(targetX, P0, P1, P2, P3, epsilon = 0.5) {
|
||||||
|
let lower = 0;
|
||||||
|
let upper = 1;
|
||||||
|
let t;
|
||||||
|
|
||||||
|
for (let i = 0; i < 20; i++) {
|
||||||
|
t = (lower + upper) / 2;
|
||||||
|
|
||||||
|
const u = 1 - t;
|
||||||
|
const x = u ** 3 * P0.x + 3 * u ** 2 * t * P1.x + 3 * u * t ** 2 * P2.x + t ** 3 * P3.x;
|
||||||
|
|
||||||
|
if (Math.abs(x - targetX) < epsilon) {
|
||||||
|
return t;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (x < targetX) {
|
||||||
|
lower = t;
|
||||||
|
} else {
|
||||||
|
upper = t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
drawCurves(ctx, curves, opacity = 1.0, showControlPoints = true) {
|
||||||
|
ctx.save();
|
||||||
|
ctx.globalAlpha = opacity;
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Draw curve
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(p0.x, p0.y);
|
||||||
|
ctx.bezierCurveTo(h0.x, h0.y, h1.x, h1.y, p1.x, p1.y);
|
||||||
|
ctx.strokeStyle = "#0077cc";
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
const seconds = this.currentTime / 48; // convert to seconds
|
||||||
|
const currentTimeX = this.valueToX(seconds);
|
||||||
|
|
||||||
|
if (this.currentTime !== null) {
|
||||||
|
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);
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(pt.x, pt.y, 5, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = "#00cc66";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "#003300";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Draw control points (optional)
|
||||||
|
if (showControlPoints) {
|
||||||
|
// Draw handles
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(p0.x, p0.y);
|
||||||
|
ctx.lineTo(h0.x, h0.y);
|
||||||
|
ctx.moveTo(h1.x, h1.y);
|
||||||
|
ctx.lineTo(p1.x, p1.y);
|
||||||
|
ctx.strokeStyle = "#aaa";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
for (let key of ['startPoint', 'startPointHandle', 'endPointHandle', 'endPoint']) {
|
||||||
|
const p = this.transform(curve[key]);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(p.x, p.y, 6, 0, Math.PI * 2);
|
||||||
|
ctx.fillStyle = key.includes('Handle') ? "#888" : "#ff6600";
|
||||||
|
ctx.fill();
|
||||||
|
ctx.strokeStyle = "#333";
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
drawGrid(ctx) {
|
||||||
|
ctx.fillStyle = "#f0f0f0";
|
||||||
|
ctx.fillRect(0, 0, this.canvas.width, this.canvas.height);
|
||||||
|
|
||||||
|
const totalTicks = this.timelineLength * 4;
|
||||||
|
for (let s = 0; s <= totalTicks; s++) {
|
||||||
|
const time = s / 4;
|
||||||
|
const x = this.valueToX(time);
|
||||||
|
|
||||||
|
if (x < 0 || x > this.canvas.width) continue;
|
||||||
|
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(x, 0);
|
||||||
|
ctx.lineTo(x, this.canvas.height);
|
||||||
|
|
||||||
|
if (s % 4 === 0) {
|
||||||
|
ctx.strokeStyle = "#999";
|
||||||
|
ctx.lineWidth = 1.5;
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
ctx.fillStyle = "#666";
|
||||||
|
ctx.fillText(`${time}s`, x + 2, 12);
|
||||||
|
} else if (s % 2 === 0) {
|
||||||
|
ctx.strokeStyle = "#bbb";
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
} else {
|
||||||
|
ctx.strokeStyle = "#ddd";
|
||||||
|
ctx.lineWidth = 0.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
const visibleYMin = Math.max(this.yToValue(this.canvas.height), -1);
|
||||||
|
const visibleYMax = Math.min(this.yToValue(0), 1);
|
||||||
|
|
||||||
|
for (let v = Math.ceil(visibleYMin * 4) / 4; v <= visibleYMax; v += 0.25) {
|
||||||
|
const y = this.valueToY(v);
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(0, y);
|
||||||
|
ctx.lineTo(this.canvas.width, y);
|
||||||
|
|
||||||
|
ctx.strokeStyle = (v === 1 || v === -1 || v === 0) ? "#999" : "#ddd";
|
||||||
|
ctx.lineWidth = (v === 1 || v === -1 || v === 0) ? 1.5 : 0.5;
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.font = "12px sans-serif";
|
||||||
|
ctx.fillStyle = "#666";
|
||||||
|
ctx.fillText(v.toFixed(2), 4, y - 4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cubicBezier(t, P0, P1, P2, P3) {
|
||||||
|
const u = 1 - t;
|
||||||
|
return {
|
||||||
|
x: u ** 3 * P0.x + 3 * u ** 2 * t * P1.x + 3 * u * t ** 2 * P2.x + t ** 3 * P3.x,
|
||||||
|
y: u ** 3 * P0.y + 3 * u ** 2 * t * P1.y + 3 * u * t ** 2 * P2.y + t ** 3 * P3.y
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
splitCurve(curve, t) {
|
||||||
|
const lerp = (a, b, t) => ({
|
||||||
|
x: a.x + (b.x - a.x) * t,
|
||||||
|
y: a.y + (b.y - a.y) * t
|
||||||
|
});
|
||||||
|
|
||||||
|
const p01 = lerp(curve.startPoint, curve.startPointHandle, t);
|
||||||
|
const p12 = lerp(curve.startPointHandle, curve.endPointHandle, t);
|
||||||
|
const p23 = lerp(curve.endPointHandle, curve.endPoint, t);
|
||||||
|
|
||||||
|
const p012 = lerp(p01, p12, t);
|
||||||
|
const p123 = lerp(p12, p23, t);
|
||||||
|
|
||||||
|
const split = lerp(p012, p123, t);
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
startPoint: curve.startPoint,
|
||||||
|
startPointHandle: p01,
|
||||||
|
endPointHandle: p012,
|
||||||
|
endPoint: split
|
||||||
|
},
|
||||||
|
{
|
||||||
|
startPoint: split,
|
||||||
|
startPointHandle: p123,
|
||||||
|
endPointHandle: p23,
|
||||||
|
endPoint: curve.endPoint
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurves(curves) {
|
||||||
|
this.curves = curves;
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurves() {
|
||||||
|
return this.curves;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectAdjacentMotor(direction) {
|
||||||
|
const ids = Object.keys(this.curveSets)
|
||||||
|
.map(id => parseInt(id))
|
||||||
|
.sort((a, b) => a - b);
|
||||||
|
|
||||||
|
const currentIndex = ids.indexOf(this.selectedMotorID);
|
||||||
|
if (currentIndex === -1) return;
|
||||||
|
|
||||||
|
const newIndex = (currentIndex + direction + ids.length) % ids.length;
|
||||||
|
this.setSelectedMotor(ids[newIndex]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
initEvents() {
|
||||||
|
this.canvas.addEventListener('mousedown', e => {
|
||||||
|
const { buttons, width, height } = this.motorButtons;
|
||||||
|
const mx = e.offsetX;
|
||||||
|
const my = e.offsetY;
|
||||||
|
|
||||||
|
for (let btn of buttons) {
|
||||||
|
if (
|
||||||
|
mx >= btn.x &&
|
||||||
|
mx <= btn.x + width &&
|
||||||
|
my >= btn.y &&
|
||||||
|
my <= btn.y + height
|
||||||
|
) {
|
||||||
|
if (btn.motorOffset === -1) {
|
||||||
|
this.selectAdjacentMotor(-1);
|
||||||
|
} else if (btn.motorOffset === 1) {
|
||||||
|
this.selectAdjacentMotor(1);
|
||||||
|
}
|
||||||
|
console.log(this.selectedMotorID);
|
||||||
|
// No action needed for label button (motorOffset === 0)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const mouse = this.inverseTransform({ x: e.offsetX, y: e.offsetY });
|
||||||
|
|
||||||
|
if (e.button === 1) {
|
||||||
|
this.isPanning = true;
|
||||||
|
this.panStart = { x: e.offsetX, y: e.offsetY };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let curve of this.curves) {
|
||||||
|
for (let key of ['startPoint', 'startPointHandle', 'endPointHandle', 'endPoint']) {
|
||||||
|
const p = curve[key];
|
||||||
|
if (Math.hypot(p.x - mouse.x, p.y - mouse.y) < 10 / this.scale) {
|
||||||
|
this.dragging = { curve, key };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mousemove', e => {
|
||||||
|
if (this.isPanning) {
|
||||||
|
this.offset.x += e.offsetX - this.panStart.x;
|
||||||
|
this.offset.y += e.offsetY - this.panStart.y;
|
||||||
|
this.panStart = { x: e.offsetX, y: e.offsetY };
|
||||||
|
this.draw();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.dragging) {
|
||||||
|
const mouse = this.inverseTransform({ x: e.offsetX, y: e.offsetY });
|
||||||
|
const { curve, key } = this.dragging;
|
||||||
|
const index = this.curves.indexOf(curve);
|
||||||
|
|
||||||
|
if (key === 'startPoint' || key === 'endPoint') {
|
||||||
|
this.dragEndpoint(curve, key, mouse.x, mouse.y, index);
|
||||||
|
} else {
|
||||||
|
this.dragControlPoint(curve, key, mouse.x, mouse.y, index);
|
||||||
|
}
|
||||||
|
this.curveSets[this.selectedMotorID] = this.curves;
|
||||||
|
|
||||||
|
this.draw();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.addEventListener('mouseup', () => {
|
||||||
|
this.dragging = null;
|
||||||
|
this.isPanning = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.addEventListener('wheel', e => {
|
||||||
|
e.preventDefault();
|
||||||
|
const mouse = { x: e.offsetX, y: e.offsetY };
|
||||||
|
const zoomFactor = e.deltaY < 0 ? 1.1 : 0.9;
|
||||||
|
|
||||||
|
const before = this.inverseTransform(mouse);
|
||||||
|
this.scale *= zoomFactor;
|
||||||
|
const after = this.inverseTransform(mouse);
|
||||||
|
|
||||||
|
this.offset.x += (after.x - before.x) * this.scale;
|
||||||
|
this.offset.y += (after.y - before.y) * this.scale;
|
||||||
|
|
||||||
|
this.draw();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.canvas.addEventListener('dblclick', e => {
|
||||||
|
const mouse = this.inverseTransform({ x: e.offsetX, y: e.offsetY });
|
||||||
|
|
||||||
|
for (let i = 0; i < this.curves.length; i++) {
|
||||||
|
const curve = this.curves[i];
|
||||||
|
|
||||||
|
if (i > 0 && this.isNearPoint(mouse, curve.startPoint)) {
|
||||||
|
const prev = this.curves[i - 1];
|
||||||
|
const merged = {
|
||||||
|
startPoint: prev.startPoint,
|
||||||
|
startPointHandle: prev.startPointHandle,
|
||||||
|
endPointHandle: curve.endPointHandle,
|
||||||
|
endPoint: curve.endPoint
|
||||||
|
};
|
||||||
|
this.curves.splice(i - 1, 2, merged);
|
||||||
|
this.draw();
|
||||||
|
this.curveSets[this.selectedMotorID] = this.curves;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (i < this.curves.length - 1 && this.isNearPoint(mouse, curve.endPoint)) {
|
||||||
|
const next = this.curves[i + 1];
|
||||||
|
const merged = {
|
||||||
|
startPoint: curve.startPoint,
|
||||||
|
startPointHandle: curve.startPointHandle,
|
||||||
|
endPointHandle: next.endPointHandle,
|
||||||
|
endPoint: next.endPoint
|
||||||
|
};
|
||||||
|
this.curves.splice(i, 2, merged);
|
||||||
|
this.draw();
|
||||||
|
this.curveSets[this.selectedMotorID] = this.curves;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const x0 = curve.startPoint.x;
|
||||||
|
const x3 = curve.endPoint.x;
|
||||||
|
if (mouse.x >= x0 && mouse.x <= x3) {
|
||||||
|
let t = 0.5, minT = 0, maxT = 1;
|
||||||
|
for (let j = 0; j < 10; j++) {
|
||||||
|
const pt = this.cubicBezier(t, curve.startPoint, curve.startPointHandle, curve.endPointHandle, curve.endPoint);
|
||||||
|
if (pt.x < mouse.x) minT = t;
|
||||||
|
else maxT = t;
|
||||||
|
t = (minT + maxT) / 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [left, right] = this.splitCurve(curve, t);
|
||||||
|
this.curves.splice(i, 1, left, right);
|
||||||
|
this.draw();
|
||||||
|
this.curveSets[this.selectedMotorID] = this.curves;
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dragControlPoint(curve, key, mouseX, mouseY, index) {
|
||||||
|
curve[key].y = mouseY;
|
||||||
|
|
||||||
|
const firstX = this.curves[0].startPoint.x;
|
||||||
|
const lastX = this.curves[this.curves.length - 1].endPoint.x;
|
||||||
|
|
||||||
|
if (key === 'startPointHandle') {
|
||||||
|
const minX = Math.max(curve.startPoint.x, this.curves[index - 1]?.endPoint?.x ?? firstX);
|
||||||
|
const maxX = this.curves[index + 1]?.startPoint?.x ?? lastX;
|
||||||
|
curve.startPointHandle.x = Math.max(minX, Math.min(mouseX, maxX - 0.01));
|
||||||
|
} else if (key === 'endPointHandle') {
|
||||||
|
const minX = this.curves[index - 1]?.endPoint?.x ?? firstX;
|
||||||
|
const maxX = Math.min(curve.endPoint.x, this.curves[index + 1]?.endPoint?.x ?? lastX);
|
||||||
|
curve.endPointHandle.x = Math.max(minX, Math.min(mouseX, maxX - 0.01));
|
||||||
|
} else {
|
||||||
|
curve[key].x = Math.max(firstX, Math.min(mouseX, lastX));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dragEndpoint(curve, key, mouseX, mouseY, index) {
|
||||||
|
let deltaX = 0;
|
||||||
|
let deltaY = 0;
|
||||||
|
|
||||||
|
if (key === 'startPoint') {
|
||||||
|
const oldX = curve.startPoint.x;
|
||||||
|
const oldY = curve.startPoint.y;
|
||||||
|
|
||||||
|
if (index === 0) {
|
||||||
|
curve.startPoint.x = 0;
|
||||||
|
} else {
|
||||||
|
const firstX = this.curves[0].startPoint.x;
|
||||||
|
const nextX = this.curves[index + 1]?.startPoint?.x ?? Infinity;
|
||||||
|
curve.startPoint.x = Math.max(firstX + 0.01, Math.min(mouseX, nextX - 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
curve.startPoint.y = mouseY;
|
||||||
|
|
||||||
|
deltaX = curve.startPoint.x - oldX;
|
||||||
|
deltaY = curve.startPoint.y - oldY;
|
||||||
|
|
||||||
|
curve.startPointHandle.x += deltaX;
|
||||||
|
curve.startPointHandle.y += deltaY;
|
||||||
|
|
||||||
|
this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, index);
|
||||||
|
} else if (key === 'endPoint') {
|
||||||
|
const oldX = curve.endPoint.x;
|
||||||
|
const oldY = curve.endPoint.y;
|
||||||
|
|
||||||
|
if (index === this.curves.length - 1) {
|
||||||
|
curve.endPoint.x = this.timelineLength * this.pixelsPerSecond; // logical time, not screen space
|
||||||
|
} else {
|
||||||
|
const firstX = this.curves[0].startPoint.x;
|
||||||
|
const prevX = this.curves[index - 1]?.endPoint?.x ?? firstX;
|
||||||
|
const nextX = this.curves[index + 1]?.endPoint?.x ?? Infinity;
|
||||||
|
curve.endPoint.x = Math.max(prevX + 0.01, Math.min(mouseX, nextX - 0.01));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
curve.endPoint.y = mouseY;
|
||||||
|
|
||||||
|
deltaX = curve.endPoint.x - oldX;
|
||||||
|
deltaY = curve.endPoint.y - oldY;
|
||||||
|
|
||||||
|
curve.endPointHandle.x += deltaX;
|
||||||
|
curve.endPointHandle.y += deltaY;
|
||||||
|
|
||||||
|
this.dragControlPoint(curve, 'endPointHandle', curve.endPointHandle.x, curve.endPointHandle.y, index);
|
||||||
|
|
||||||
|
const nextCurve = this.curves[index + 1];
|
||||||
|
if (nextCurve) {
|
||||||
|
nextCurve.startPoint.x = curve.endPoint.x;
|
||||||
|
nextCurve.startPoint.y = curve.endPoint.y;
|
||||||
|
nextCurve.startPointHandle.x += deltaX;
|
||||||
|
nextCurve.startPointHandle.y += deltaY;
|
||||||
|
|
||||||
|
this.dragControlPoint(nextCurve, 'startPointHandle', nextCurve.startPointHandle.x, nextCurve.startPointHandle.y, index + 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// After all endpoint logic is done
|
||||||
|
for (let i = 0; i < this.curves.length; i++) {
|
||||||
|
const curve = this.curves[i];
|
||||||
|
|
||||||
|
this.dragControlPoint(curve, 'startPointHandle', curve.startPointHandle.x, curve.startPointHandle.y, i);
|
||||||
|
this.dragControlPoint(curve, 'endPointHandle', curve.endPointHandle.x, curve.endPointHandle.y, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
isNearPoint(mouse, point, threshold = 5) {
|
||||||
|
return (
|
||||||
|
Math.abs(mouse.x - point.x) < threshold &&
|
||||||
|
Math.abs(mouse.y - point.y) < threshold
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -155,6 +155,12 @@
|
||||||
|
|
||||||
|
|
||||||
<div class="tab-pane fade" id="animation" role="tabpanel" aria-labelledby="animation-tab">
|
<div class="tab-pane fade" id="animation" role="tabpanel" aria-labelledby="animation-tab">
|
||||||
|
<canvas id="curveCanvas" width="900" height="300"></canvas>
|
||||||
|
<div style="margin-top: 10px; text-align: center;">
|
||||||
|
<input type="range" id="timeSlider" min="0" step="1" style="width: 80%;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
<div class="dial-container">
|
<div class="dial-container">
|
||||||
<div class="dial" data-index="0"><label>Motor 1</label>
|
<div class="dial" data-index="0"><label>Motor 1</label>
|
||||||
<div id="dial0"></div><span id="value0">512</span>
|
<div id="dial0"></div><span id="value0">512</span>
|
||||||
|
|
|
||||||
105
script.js
105
script.js
|
|
@ -1,5 +1,7 @@
|
||||||
import { SerialManager } from './serial.js';
|
import { SerialManager } from './serial.js';
|
||||||
import { ServoMotor, getModelType, writeData } from './feetechDefinitions.js';
|
import { ServoMotor, getModelType, writeData } from './feetechDefinitions.js';
|
||||||
|
import { CurveEditor } from './curveEditor.js';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
|
|
@ -33,7 +35,8 @@ window.onload = () => {
|
||||||
const totalFrames = 500;
|
const totalFrames = 500;
|
||||||
|
|
||||||
|
|
||||||
|
const curveCanvas = document.getElementById('curveCanvas');
|
||||||
|
const curveEditor = new CurveEditor(curveCanvas, 10);
|
||||||
|
|
||||||
|
|
||||||
// Animation File List
|
// Animation File List
|
||||||
|
|
@ -373,8 +376,6 @@ window.onload = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLoadedFile(data) {
|
function handleLoadedFile(data) {
|
||||||
// Ensure data is a Uint8Array
|
|
||||||
console.log(data.buffer);
|
|
||||||
const raw = new Uint8Array(data);
|
const raw = new Uint8Array(data);
|
||||||
const view = new DataView(raw.buffer);
|
const view = new DataView(raw.buffer);
|
||||||
let offset = 0;
|
let offset = 0;
|
||||||
|
|
@ -388,7 +389,7 @@ window.onload = () => {
|
||||||
);
|
);
|
||||||
offset += 4;
|
offset += 4;
|
||||||
|
|
||||||
const frameCount = view.getUint16(offset, true); offset += 2; // big-endian
|
const frameCount = view.getUint16(offset, true); offset += 2;
|
||||||
const version = view.getUint8(offset++);
|
const version = view.getUint8(offset++);
|
||||||
const frameRate = view.getUint8(offset++);
|
const frameRate = view.getUint8(offset++);
|
||||||
offset += 8; // reserved
|
offset += 8; // reserved
|
||||||
|
|
@ -403,46 +404,71 @@ window.onload = () => {
|
||||||
console.log("📦 Version:", version);
|
console.log("📦 Version:", version);
|
||||||
console.log("⏱️ Frame Rate:", frameRate);
|
console.log("⏱️ Frame Rate:", frameRate);
|
||||||
|
|
||||||
const NUM_CHANNELS = 5;
|
// 🔹 Read curve segment count
|
||||||
const HEADER_SIZE = 16;
|
if (offset + 2 > view.byteLength) {
|
||||||
const FRAME_SIZE = NUM_CHANNELS * 2;
|
console.error("File too short to contain curve count");
|
||||||
//const frameCount = view.getUint16(4, true); // already parsed earlier
|
return;
|
||||||
|
|
||||||
const frames = [];
|
|
||||||
//let offset = HEADER_SIZE;
|
|
||||||
|
|
||||||
for (let i = 0; i < frameCount; i++) {
|
|
||||||
const frame = [];
|
|
||||||
for (let c = 0; c < NUM_CHANNELS; c++) {
|
|
||||||
frame.push(view.getUint16(offset, true));
|
|
||||||
offset += 2;
|
|
||||||
}
|
|
||||||
frames.push(frame);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(frames);
|
const curveCount = view.getUint16(offset, true);
|
||||||
console.log(offset);
|
|
||||||
offset = 5016;
|
|
||||||
let keyFrameCount = view.getUint16(offset, true);
|
|
||||||
offset += 2;
|
offset += 2;
|
||||||
|
|
||||||
let keyframes = [];
|
|
||||||
for (var i = 0; i < keyFrameCount; i++) {
|
const SEGMENT_SIZE = 17;
|
||||||
let motorID = view.getUint8(offset); offset += 1;
|
const curveSets = {};
|
||||||
let frame = view.getUint16(offset, true); offset += 2;
|
let latestEndTime = 1;
|
||||||
let position = view.getUint16(offset, true); offset += 2;
|
|
||||||
keyframes.push({ motorID, frame, position });
|
for (let i = 0; i < curveCount; i++) {
|
||||||
|
const motorID = 10;
|
||||||
|
offset += 1;
|
||||||
|
const startTime = view.getUint16(offset, true); offset += 2;
|
||||||
|
const endTime = view.getUint16(offset, true); offset += 2;
|
||||||
|
const startPointY = view.getUint16(offset, true); offset += 2;
|
||||||
|
const startHandleX = view.getUint16(offset, true); offset += 2;
|
||||||
|
const startHandleY = view.getUint16(offset, true); offset += 2;
|
||||||
|
const endHandleX = view.getUint16(offset, true); offset += 2;
|
||||||
|
const endHandleY = view.getUint16(offset, true); offset += 2;
|
||||||
|
const endPointY = view.getUint16(offset, true); offset += 2;
|
||||||
|
|
||||||
|
const toFloat = v => (v / 65535) * 2 - 1;
|
||||||
|
|
||||||
|
const curve = {
|
||||||
|
startPoint: {
|
||||||
|
x: startTime,
|
||||||
|
y: curveEditor.valueToY(toFloat(startPointY))
|
||||||
|
},
|
||||||
|
startPointHandle: {
|
||||||
|
x: startHandleX,
|
||||||
|
y: curveEditor.valueToY(toFloat(startHandleY))
|
||||||
|
},
|
||||||
|
endPointHandle: {
|
||||||
|
x: endHandleX,
|
||||||
|
y: curveEditor.valueToY(toFloat(endHandleY))
|
||||||
|
},
|
||||||
|
endPoint: {
|
||||||
|
x: endTime,
|
||||||
|
y: curveEditor.valueToY(toFloat(endPointY))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if (endTime > latestEndTime) {
|
||||||
|
latestEndTime = endTime;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(keyFrameCount);
|
if (!curveSets[motorID]) {
|
||||||
console.log(keyframes);
|
curveSets[motorID] = [];
|
||||||
dialKeyframes = Array.from({ length: 5 }, () => ({}));
|
|
||||||
keyframes.forEach(({ motorID, frame, position }) => {
|
|
||||||
if (!dialKeyframes[motorID]) {
|
|
||||||
dialKeyframes[motorID] = []; // Initialize if missing
|
|
||||||
}
|
}
|
||||||
dialKeyframes[motorID][frame] = position;
|
curveSets[motorID].push(curve);
|
||||||
});
|
console.log(motorID);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("🎯 Loaded Curves:", curveSets);
|
||||||
|
|
||||||
|
// 🔁 Inject into your curve editor
|
||||||
|
//loadCurvesIntoEditor(curves); // Replace with your actual editor hook
|
||||||
|
curveEditor.loadCurveSets(curveSets);
|
||||||
|
curveEditor.setLength(latestEndTime);
|
||||||
|
|
||||||
|
|
||||||
// 🔓 Unlock buttons
|
// 🔓 Unlock buttons
|
||||||
loadButton.disabled = false;
|
loadButton.disabled = false;
|
||||||
|
|
@ -453,6 +479,9 @@ window.onload = () => {
|
||||||
drawTimelineMarkers();
|
drawTimelineMarkers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
function sanitizeFilename(input) {
|
function sanitizeFilename(input) {
|
||||||
// Remove non-alphanumeric characters
|
// Remove non-alphanumeric characters
|
||||||
const stripped = input.replace(/[^a-zA-Z0-9]/g, "");
|
const stripped = input.replace(/[^a-zA-Z0-9]/g, "");
|
||||||
|
|
@ -688,7 +717,7 @@ window.onload = () => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
canvas.addEventListener('mousemove', (e) => {
|
canvas.addEventListener('mousemove', (e) => {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue