implements separate x and y zoom on curve editor

node_mode
Jake 2025-10-28 00:12:27 +08:00
parent 06633fa7b5
commit 0fa8e04493
5 changed files with 64 additions and 49 deletions

View File

@ -1,10 +1,14 @@
export class CurveEditor {
constructor(canvas, timelineLength = 10, _slider) {
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.scale = 1;
this.scaleX = 1;
this.scaleY = 1;
this.offset = { x: 0, y: 0 };
this.pixelsPerSecond = 48;
@ -133,15 +137,17 @@ export class CurveEditor {
addChannel(motorID) {
this.setCurves([
{
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
startPointHandle: { x: this.valueToX(this.timelineLength * 0.25), y: this.valueToY(0) },
endPointHandle: { x: this.valueToX(this.timelineLength * 0.75), y: this.valueToY(0) },
endPoint: { x: this.valueToX(this.timelineLength), y: this.valueToY(0) }
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;
console.log("new endtime: " + endTime);
@ -178,12 +184,12 @@ export class CurveEditor {
valueToY(v) {
return (this.canvas.height / 2 - v * (this.canvas.height / 2)) * this.scale + this.offset.y;
return (this.canvas.height / 2 - v * (this.canvas.height / 2)) * this.scaleY + this.offset.y;
}
// Maps pixel value of y axis to -1 to 1 normalised value
yToValue(y) {
return ((this.canvas.height / 2 - (y - this.offset.y) / this.scale) / (this.canvas.height / 2));
return ((this.canvas.height / 2 - (y - this.offset.y) / this.scaleY) / (this.canvas.height / 2));
}
// Maps normalised -1 to 1 value to motor range (0, 4095)
@ -206,25 +212,27 @@ export class CurveEditor {
}
valueToX(t) {
return t * this.pixelsPerSecond * this.scale + this.offset.x;
valueToX(value) {
return value * this.pixelsPerSecond * this.scaleX + this.offset.x;
}
xToValue(x) {
return (x - this.offset.x) / (this.pixelsPerSecond * this.scale);
return (x - this.offset.x) / (this.pixelsPerSecond * this.scaleX);
}
transform(p) {
return {
x: p.x * this.scale + this.offset.x,
y: p.y * this.scale + this.offset.y
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.scale,
y: (p.y - this.offset.y) / this.scale
x: (p.x - this.offset.x) / this.scaleX,
y: (p.y - this.offset.y) / this.scaleY
};
}
@ -389,14 +397,6 @@ export class CurveEditor {
}
}
// Draw control points (optional)
if (showControlPoints) {
// Draw handles
@ -448,7 +448,7 @@ export class CurveEditor {
ctx.lineWidth = 1.5;
ctx.font = "12px sans-serif";
ctx.fillStyle = "#666";
ctx.fillText(`${time}s`, x + 2, 12);
ctx.fillText(`${time}s`, x + 0 / this.scaleX, 12 / this.scaleY);
} else if (s % 2 === 0) {
ctx.strokeStyle = "#bbb";
ctx.lineWidth = 1;
@ -475,7 +475,8 @@ export class CurveEditor {
ctx.font = "12px sans-serif";
ctx.fillStyle = "#666";
ctx.fillText(v.toFixed(2), 4, y - 4);
ctx.fillText(v.toFixed(2), 4 / this.scaleX + 10, y - 6 / this.scaleY);
}
}
@ -572,6 +573,7 @@ export class CurveEditor {
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;
@ -644,7 +646,7 @@ export class CurveEditor {
this.offset.y += (after.y - before.y) * this.scaleY;
this.draw();
});
});
this.canvas.addEventListener('dblclick', e => {

View File

@ -155,7 +155,7 @@
<div class="tab-pane fade show active" id="animation" role="tabpanel" aria-labelledby="animation-tab">
<canvas id="curveCanvas" width="1920" height="800"></canvas>
<canvas id="curveCanvas"></canvas>
<!-- <div style="margin-top: 10px; text-align: center;">
<input type="range" id="timeSlider" min="0" step="1" style="width: 80%;">
</div> -->
@ -199,7 +199,7 @@
<canvas id="nodeeditor" width="800" height="600"></canvas>
<canvas id="nodeeditor"></canvas>
<div id="contextMenu" style="
position: absolute;
display: none;

View File

@ -5,6 +5,8 @@ import { ServoNode, CurveNode, VariableNode, NoiseNode, MathNode, MapNode, NODE_
export class NodeEditor {
constructor(canvas, options = {}) {
this.canvas = canvas;
canvas.width = canvas.offsetWidth;
canvas.height = canvas.offsetHeight;
this.ctx = canvas.getContext("2d");
this.nodes = [];
this.connections = [];

View File

@ -44,7 +44,7 @@ window.onload = () => {
const curveCanvas = document.getElementById('curveCanvas');
const curveEditor = new CurveEditor(curveCanvas, 10, frameSlider);
const curveEditor = new CurveEditor(curveCanvas, 6, frameSlider);
// Animation File List

View File

@ -74,7 +74,17 @@ body {
}
#curveCanvas {
width: 80%;
height: 800px;
display: block;
}
#nodeeditor {
width: 80%;
height: 1200px;
display: block;
}
canvas {
@ -82,6 +92,8 @@ canvas {
/* light grey */
border: 1px solid #ccc;
/* optional subtle border */
display: block;
margin: 0 auto;
}
@ -97,8 +109,7 @@ canvas {
padding: 4px 8px;
cursor: pointer;
}
.menu-item:hover {
background: #ddd;
}