diff --git a/curveEditor.js b/curveEditor.js index 65ea8fc..84b9203 100644 --- a/curveEditor.js +++ b/curveEditor.js @@ -6,6 +6,7 @@ export class CurveEditor { this.ctx = canvas.getContext('2d'); this.timelineLength = timelineLength; + this.scale = 1; this.scaleX = 1; this.scaleY = 1; @@ -16,6 +17,14 @@ export class CurveEditor { 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, @@ -25,17 +34,17 @@ export class CurveEditor { // COMMENT OUT AND READ FROM EXISTING TIMELINE LATER - const slider = _slider;//document.getElementById('timeSlider'); - slider.max = this.timelineLength * this.pixelsPerSecond; + this.slider = _slider;//document.getElementById('timeSlider'); + this.slider.max = this.timelineLength * this.pixelsPerSecond; - console.log(slider.max); - slider.value = 0; - slider.addEventListener('input', () => { - this.currentTime = parseFloat(slider.value); + 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 @@ -45,39 +54,6 @@ export class CurveEditor { 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(4); this.initEvents(); this.draw(); @@ -150,8 +126,15 @@ export class CurveEditor { setLength(endTime) { this.timelineLength = endTime / this.pixelsPerSecond; - console.log("new endtime: " + endTime); + 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(); } loadCurveSets(curveSets) { @@ -171,6 +154,8 @@ export class CurveEditor { // Optional: update motor selector UI or redraw timeline //this.refreshMotorSelector?.(); // if you have a method for that //this.drawTimelineMarkers?.(); + + } @@ -245,17 +230,48 @@ export class CurveEditor { 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); + // 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; @@ -353,7 +369,9 @@ export class CurveEditor { 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); @@ -548,6 +566,15 @@ export class CurveEditor { 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 && @@ -566,10 +593,6 @@ export class CurveEditor { } } - - - - const mouse = this.inverseTransform({ x: e.offsetX, y: e.offsetY }); if (e.button === 1) { @@ -580,7 +603,7 @@ export class CurveEditor { } for (let curve of this.curves) { - for (let key of ['startPoint', 'startPointHandle', 'endPointHandle', 'endPoint']) { + 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 }; @@ -588,6 +611,7 @@ export class CurveEditor { } } } + }); @@ -608,6 +632,19 @@ export class CurveEditor { 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); } @@ -707,6 +744,45 @@ export class CurveEditor { }); } + 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 curve’s 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; diff --git a/index.html b/index.html index fd1188c..dd693cd 100644 --- a/index.html +++ b/index.html @@ -182,7 +182,7 @@