sophia_controller/curveEditor.js

906 lines
30 KiB
JavaScript
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.

export class CurveEditor {
constructor(canvas, timelineLength = 6, _slider) {
this.canvas = canvas;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
this.ctx = canvas.getContext('2d');
this.timelineLength = timelineLength;
this.logicalHeight = 800;
this.scale = 1;
this.scaleX = 1;
this.scaleY = 1;
this.offset = { x: 0, y: 0 };
this.pixelsPerSecond = 48;
this.exportRange = [0, 4095];
this.currentTime = timelineLength * this.pixelsPerSecond / 2;
this.durationHandle = {
x: timelineLength, // or maxTimeCS
y: 30, // top of canvas
radius: 10,
type: "duration"
};
this.motorButtons = {
width: 30,
height: 30,
padding: 10,
buttons: [] // will be populated dynamically
};
// COMMENT OUT AND READ FROM EXISTING TIMELINE LATER
this.slider = _slider;//document.getElementById('timeSlider');
this.slider.max = this.timelineLength * this.pixelsPerSecond;
console.log(this.slider.max);
this.slider.value = 0;
this.slider.addEventListener('input', () => {
this.currentTime = parseFloat(this.slider.value);
//console.log(slider.value);
this.draw();
});
this.setLength(timelineLength * this.pixelsPerSecond);
this.curveSets = {}; // motorID → array of curves
this.selectedMotorID = 4;
this.curves = [];
this.dragging = null;
this.isPanning = false;
this.panStart = { x: 0, y: 0 };
this.initEvents();
this.draw();
console.log(this.curveSets);
}
encodeCurves() {
const bufferSize = 1024; // adjust based on expected graph size
const buffer = new ArrayBuffer(bufferSize);
const view = new DataView(buffer);
let offset = 0;
const curveSegments = [];
Object.entries(this.curveSets).forEach(([motorIDStr, segments]) => {
const motorID = parseInt(motorIDStr, 10);
segments.forEach(segment => {
curveSegments.push({
motorID,
startTime: segment.startPoint.x,
endTime: segment.endPoint.x,
startPointY: segment.startPoint.y,
startHandleX: segment.startPointHandle.x,
startHandleY: segment.startPointHandle.y,
endHandleX: segment.endPointHandle.x,
endHandleY: segment.endPointHandle.y,
endPointY: segment.endPoint.y
});
});
});
console.log(curveSegments);
const curveCount = curveSegments.length;
view.setUint16(offset, curveCount, true); offset += 2;
// 🔹 Curve segments
curveSegments.forEach(seg => {
console.log(offset, seg.motorID);
view.setUint8(offset++, seg.motorID);
view.setUint16(offset, seg.startTime, true); offset += 2;
view.setUint16(offset, seg.endTime, true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.startPointY), true); offset += 2;
view.setUint16(offset, seg.startHandleX, true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.startHandleY), true); offset += 2;
view.setUint16(offset, seg.endHandleX, true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.endHandleY), true); offset += 2;
view.setInt16(offset, this.yToExportRange(seg.endPointY), true); offset += 2;
console.log(seg.startPointY, seg.endPointY);
console.log(this.yToExportRange(seg.startPointY), this.yToExportRange(seg.endPointY));
});
//console.log("🧵 Curve segments packed:", curveSegments.length);
return new Uint8Array(buffer.slice(0, offset));
}
setCurrentTime(time) {
this.currentTime = parseFloat(time);
this.draw();
}
addChannel(motorID) {
this.setCurves([
{
startPoint: { x: 0, y: this.canvas.height / 2 },
startPointHandle: { x: this.timelineLength * this.pixelsPerSecond * 0.25, y: this.canvas.height / 2 },
endPointHandle: { x: this.timelineLength * this.pixelsPerSecond * 0.75, y: this.canvas.height / 2 },
endPoint: { x: this.timelineLength * this.pixelsPerSecond, y: this.canvas.height / 2 }
}
]);
//console.log("TL LENGTH: " + this.timelineLength);
this.curveSets[motorID] = this.curves;
}
setLength(endTime) {
this.timelineLength = endTime / this.pixelsPerSecond;
this.slider.max = this.timelineLength * this.pixelsPerSecond;
//console.log(this.slider.max, this.slider.value);
if (this.slider.value > this.slider.max) {
this.slider.value = this.slider.max;
}
this.durationHandle.x = this.timelineLength;
//console.log("new endtime: " + endTime);
//this.currentTime = this.timelineLength * this.pixelsPerSecond / 2;
this.draw();
}
getLength(){
return this.timelineLength * this.pixelsPerSecond;
}
loadCurveSets(curveSets) {
this.curveSets = []
this.curveSets = curveSets;
console.log(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");
console.log(this.curveSets);
console.log(Object.keys(curveSets)[0]);
setSelectedMotor(parseInt(Object.keys(curveSets)[0]));
// hello
//this.selectAdjacentMotor(1);
// Optional: update motor selector UI or redraw timeline
//this.refreshMotorSelector?.(); // if you have a method for that
//this.drawTimelineMarkers?.();
}
selectMotor(motorID) {
this.selectedMotorID = motorID;
this.curves = this.curveSets[motorID] || [];
this.draw();
}
// valueToY(v) {
// return (this.canvas.height / 2 - v * (this.canvas.height / 2)) * this.scaleY + this.offset.y;
// }
valueToYNoOffset(v) {
return (this.canvas.height / 2 - v * (this.canvas.height / 2));
}
valueToY(v) {
return (this.logicalHeight / 2 - v * (this.logicalHeight / 2)) * this.scaleY + this.offset.y;
}
yToValue(y) {
return ((this.logicalHeight / 2 - (y - this.offset.y) / this.scaleY) / (this.logicalHeight / 2));
}
// Maps normalised -1 to 1 value to motor range (0, 4095)
yToExportRange(y) {
const [minOut, maxOut] = this.exportRange;
const clampedY = Math.max(0, Math.min(y, this.logicalHeight)); // optional safety
const normalized = clampedY / this.logicalHeight; // maps to [0, 1]
return Math.round(normalized * (maxOut - minOut) + minOut); // maps to [minOut, maxOut]
}
exportRangeToY(value) {
const [minOut, maxOut] = this.exportRange;
const normalized = 1 - (value - minOut) / (maxOut - minOut); // flipped
return this.valueToYNoOffset(normalized * 2 - 1);
}
valueToX(value) {
return value * this.pixelsPerSecond * this.scaleX + this.offset.x;
}
xToValue(x) {
return (x - this.offset.x) / (this.pixelsPerSecond * this.scaleX);
}
transform(p) {
return {
x: p.x * this.scaleX + this.offset.x,
y: p.y * this.scaleY + this.offset.y
};
}
inverseTransform(p) {
return {
x: (p.x - this.offset.x) / this.scaleX,
y: (p.y - this.offset.y) / this.scaleY
};
}
draw() {
const ctx = this.ctx;
ctx.clearRect(0, 0, this.canvas.width, this.canvas.height);
this.drawGrid(ctx);
// Draw inactive curves dimmed
// Draw inactive curves dimmed only if there are curve sets
if (this.curveSets && Object.keys(this.curveSets).length > 0) {
for (const [motorID, curves] of Object.entries(this.curveSets)) {
if (!curves || curves.length === 0) continue; // skip empty sets
const isSelected = motorID == this.selectedMotorID;
this.drawCurves(this.ctx, curves, isSelected ? 1.0 : 0.3, isSelected);
}
}
this.drawCurves(ctx, this.curves);
this.drawMotorButtons();
this.drawDurationHandle(ctx);
}
drawDurationHandle() {
const ctx = this.ctx;
const x = this.valueToX(this.durationHandle.x); // e.g. derived from header.frameCount or maxTime
const y = 20; // top of canvas
ctx.save();
// vertical guide line
ctx.strokeStyle = "red";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(x, 0);
ctx.lineTo(x, this.canvas.height);
ctx.stroke();
// draggable knob at the top
ctx.fillStyle = "red";
ctx.beginPath();
ctx.arc(x, this.durationHandle.y, this.durationHandle.radius, 0, Math.PI * 2); // circle 10px down from top
ctx.fill();
ctx.restore();
}
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;
}
getMotorPositionAtTime(motorID, timeInFrames) {
if (this.curveSets[motorID] === undefined || this.curveSets[motorID].length === 0) {
console.log("THIS");
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) {
ctx.save();
ctx.globalAlpha = opacity;
if (!curves) {
return;
}
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 + 0 / this.scaleX, 12 / this.scaleY);
} 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 / this.scaleX + 10, y - 6 / this.scaleY);
}
}
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;
setSelectedMotor(ids[newIndex]); // Global defined in script.js
}
initEvents() {
this.canvas.addEventListener('mousedown', e => {
const { buttons, width, height } = this.motorButtons;
const mx = e.offsetX;
const my = e.offsetY;
// Check duration handle
const handleX = this.valueToX(this.durationHandle.x);
const handleY = this.durationHandle.y; // same as draw
if (Math.hypot(e.offsetX - handleX, e.offsetY - handleY) < this.durationHandle.radius) {
this.dragging = { type: "durationHandle" };
return;
}
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) {
e.preventDefault(); // Prevent page panning
this.isPanning = true;
this.panStart = { x: e.offsetX, y: e.offsetY };
return;
}
for (let curve of this.curves) {
for (let key of ['startPointHandle', 'endPointHandle', 'startPoint', '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 if (this.dragging.type === "durationHandle") {
// Update duration in timeline units
const newTime = Math.max(0, mouse.x); // timeline units
// Optionally update timeline length
this.setLength(newTime);
this.adjustAllCurvesToDuration(newTime); // truncate/extend curves
//console.log(this.durationHandle.x);
this.draw();
return;
} 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);
// Apply zoom based on modifier keys
if (e.ctrlKey && !e.altKey) {
this.scaleX *= zoomFactor;
} else if (e.altKey && !e.ctrlKey) {
this.scaleY *= zoomFactor;
} else {
// Default: uniform zoom
this.scaleX *= zoomFactor;
this.scaleY *= zoomFactor;
}
const after = this.inverseTransform(mouse);
this.offset.x += (after.x - before.x) * this.scaleX;
this.offset.y += (after.y - before.y) * this.scaleY;
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;
}
}
});
}
adjustAllCurvesToDuration(newTime) {
for (const [motorID, curves] of Object.entries(this.curveSets)) {
if (!curves || curves.length === 0) continue;
for (let i = 0; i < curves.length; i++) {
const curve = curves[i];
// If a curve starts beyond the new duration, drop it
if (curve.startPoint.x >= newTime) {
curves.splice(i);
break;
}
// If a curve ends beyond the new duration, truncate it
if (curve.endPoint.x > newTime) {
curve.endPoint.x = newTime;
curve.endPointHandle.x = newTime;
curves.splice(i + 1);
break;
}
}
// If extending, push the last curves end point out
const last = curves[curves.length - 1];
if (last.endPoint.x < newTime) {
const delta = newTime - last.endPoint.x;
last.endPoint.x = newTime;
last.endPointHandle.x += delta;
}
this.curveSets[motorID] = curves;
}
// Update the active reference too
this.curves = this.curveSets[this.selectedMotorID] || [];
}
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);
//console.log(this.yToExportRange(curve.startPoint.y));
} 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];
//console.log(curve.endPoint.y);
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
);
}
}