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 {
|
export class CurveEditor {
|
||||||
constructor(canvas, timelineLength = 10) {
|
constructor(canvas, timelineLength = 10, _slider) {
|
||||||
this.canvas = canvas;
|
this.canvas = canvas;
|
||||||
this.ctx = canvas.getContext('2d');
|
this.ctx = canvas.getContext('2d');
|
||||||
this.timelineLength = timelineLength;
|
this.timelineLength = timelineLength;
|
||||||
|
|
@ -21,7 +21,7 @@ export class CurveEditor {
|
||||||
|
|
||||||
|
|
||||||
// COMMENT OUT AND READ FROM EXISTING TIMELINE LATER
|
// 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;
|
slider.max = this.timelineLength * this.pixelsPerSecond;
|
||||||
|
|
||||||
console.log(slider.max);
|
console.log(slider.max);
|
||||||
|
|
@ -80,7 +80,7 @@ export class CurveEditor {
|
||||||
console.log(this.curveSets);
|
console.log(this.curveSets);
|
||||||
}
|
}
|
||||||
|
|
||||||
addChannel(motorID){
|
addChannel(motorID) {
|
||||||
this.setCurves([
|
this.setCurves([
|
||||||
{
|
{
|
||||||
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
startPoint: { x: this.valueToX(0), y: this.valueToY(0) },
|
||||||
|
|
@ -259,20 +259,50 @@ export class CurveEditor {
|
||||||
return 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);
|
// getPositionAtTime(timeInSeconds, curve, valueToX, transform) {
|
||||||
return cubicBezier(t, p0, h0, h1, p1);
|
// // 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) {
|
drawCurves(ctx, curves, opacity = 1.0, showControlPoints = true) {
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ export class ServoMotor {
|
||||||
// Simplified constructor
|
// Simplified constructor
|
||||||
this.CHANNEL = arg1;
|
this.CHANNEL = arg1;
|
||||||
this.ID = arg2;
|
this.ID = arg2;
|
||||||
|
|
||||||
this.MODEL = arg3 || 'Unknown Model';
|
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">
|
<div class="tab-pane fade show active" id="animation" role="tabpanel" aria-labelledby="animation-tab">
|
||||||
<canvas id="curveCanvas" width="900" height="600"></canvas>
|
<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%;">
|
<input type="range" id="timeSlider" min="0" step="1" style="width: 80%;">
|
||||||
</div>
|
</div> -->
|
||||||
|
|
||||||
|
|
||||||
<div class="dial-container">
|
<div id="dialArea" class="dial-container"></div>
|
||||||
<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>
|
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" id="syncCheckbox"> Sync
|
<input type="checkbox" id="syncCheckbox"> Sync
|
||||||
</label>
|
</label>
|
||||||
|
|
@ -185,7 +170,7 @@
|
||||||
<input type="checkbox" id="feebackCheckbox"> Feedback
|
<input type="checkbox" id="feebackCheckbox"> Feedback
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<canvas id="timelineCanvas" width="800" height="30"></canvas>
|
<!-- <canvas id="timelineCanvas" width="800" height="30"></canvas> -->
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<label>Frame: <span id="frameDisplay">0</span></label><br>
|
<label>Frame: <span id="frameDisplay">0</span></label><br>
|
||||||
|
|
|
||||||
247
script.js
247
script.js
|
|
@ -37,13 +37,12 @@ window.onload = () => {
|
||||||
const dials = [];
|
const dials = [];
|
||||||
const frameSlider = document.getElementById('frameSlider');
|
const frameSlider = document.getElementById('frameSlider');
|
||||||
const frameDisplay = document.getElementById('frameDisplay');
|
const frameDisplay = document.getElementById('frameDisplay');
|
||||||
const canvas = document.getElementById('timelineCanvas');
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const totalFrames = 500;
|
|
||||||
|
|
||||||
|
|
||||||
const curveCanvas = document.getElementById('curveCanvas');
|
const curveCanvas = document.getElementById('curveCanvas');
|
||||||
const curveEditor = new CurveEditor(curveCanvas, 10);
|
const curveEditor = new CurveEditor(curveCanvas, 10, frameSlider);
|
||||||
|
|
||||||
|
|
||||||
// Animation File List
|
// Animation File List
|
||||||
|
|
@ -74,8 +73,10 @@ window.onload = () => {
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
let motor = new ServoMotor(0, 10 + i, "SCS009")
|
let motor = new ServoMotor(0, 10 + i, "SCS009")
|
||||||
robot.assignMotor(positions[i], motor);
|
robot.assignMotor(positions[i], motor);
|
||||||
}
|
|
||||||
|
|
||||||
|
addDial(motor.ID);
|
||||||
|
}
|
||||||
|
// addDial(123);
|
||||||
// Retrieve motor by position
|
// Retrieve motor by position
|
||||||
//console.log(robot.getMotor('eyelids'));
|
//console.log(robot.getMotor('eyelids'));
|
||||||
|
|
||||||
|
|
@ -176,78 +177,80 @@ window.onload = () => {
|
||||||
frameSlider.oninput = () => {
|
frameSlider.oninput = () => {
|
||||||
currentFrame = parseInt(frameSlider.value);
|
currentFrame = parseInt(frameSlider.value);
|
||||||
frameDisplay.textContent = currentFrame;
|
frameDisplay.textContent = currentFrame;
|
||||||
isInterpolating = true;
|
//console.log(currentFrame);
|
||||||
|
|
||||||
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;
|
|
||||||
|
|
||||||
|
|
||||||
|
syncDialsWithCurveEditor();
|
||||||
syncMotorsWithTimeline();
|
syncMotorsWithTimeline();
|
||||||
|
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
for (let i = 0; i < 5; i++) {
|
function addDial(motorID) {
|
||||||
dials[i] = new Nexus.Dial(`#dial${i}`, {
|
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],
|
size: [80, 80],
|
||||||
min: 0,
|
min: 0,
|
||||||
max: 1023,
|
max: 4095,
|
||||||
value: 512
|
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;
|
if (isInterpolating) return;
|
||||||
const val = Math.round(v);
|
const val = Math.round(v);
|
||||||
document.getElementById(`value${i}`).textContent = val;
|
document.getElementById(`value${index}`).textContent = val;
|
||||||
dialKeyframes[i][currentFrame] = val;
|
//dialKeyframes[index][currentFrame] = val;
|
||||||
drawTimelineMarkers();
|
|
||||||
|
|
||||||
|
|
||||||
syncMotorsWithTimeline();
|
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() {
|
function syncMotorsWithTimeline() {
|
||||||
|
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
if (syncCheckbox.checked && now - lastSyncTime >= syncIntervalMs) {
|
if (syncCheckbox.checked && now - lastSyncTime >= syncIntervalMs) {
|
||||||
lastSyncTime = now;
|
lastSyncTime = now;
|
||||||
|
|
@ -281,7 +284,6 @@ window.onload = () => {
|
||||||
document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected'));
|
document.querySelectorAll('.dial').forEach(d => d.classList.remove('selected'));
|
||||||
el.classList.add('selected');
|
el.classList.add('selected');
|
||||||
|
|
||||||
drawTimelineMarkers();
|
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -517,7 +519,7 @@ window.onload = () => {
|
||||||
|
|
||||||
document.getElementById("filenameInput").value = selectedFile.replace(/\.anim$/i, "");
|
document.getElementById("filenameInput").value = selectedFile.replace(/\.anim$/i, "");
|
||||||
|
|
||||||
drawTimelineMarkers();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -660,7 +662,6 @@ window.onload = () => {
|
||||||
clearBtn.addEventListener('click', () => {
|
clearBtn.addEventListener('click', () => {
|
||||||
currentFrame = 0;
|
currentFrame = 0;
|
||||||
dialKeyframes = Array.from({ length: 5 }, () => ({}));
|
dialKeyframes = Array.from({ length: 5 }, () => ({}));
|
||||||
drawTimelineMarkers();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -671,73 +672,6 @@ window.onload = () => {
|
||||||
document.getElementById('input').value = '';
|
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) => {
|
document.addEventListener('click', (e) => {
|
||||||
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
|
const ignoredTags = ['BUTTON', 'INPUT', 'TEXTAREA', 'CANVAS'];
|
||||||
|
|
@ -747,66 +681,11 @@ window.onload = () => {
|
||||||
if (!clickedInsideDial && !clickedControl && selectedDial !== null) {
|
if (!clickedInsideDial && !clickedControl && selectedDial !== null) {
|
||||||
selectedDial = null;
|
selectedDial = null;
|
||||||
document.querySelectorAll('.dial').forEach(el => el.classList.remove('selected'));
|
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