Fix curve issues
This commit is contained in:
parent
a8c81c8352
commit
97b9ff71b7
20
src/main.js
20
src/main.js
|
|
@ -4783,8 +4783,8 @@ class GraphicsObject extends Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find surrounding keyframes using AnimationCurve's built-in method
|
// Find surrounding keyframes using AnimationCurve's built-in method
|
||||||
const { prev: prevKf, next: nextKf } = shapeIndexCurve.getBracketingKeyframes(currentTime);
|
const { prev: prevKf, next: nextKf, t: interpolationT } = shapeIndexCurve.getBracketingKeyframes(currentTime);
|
||||||
console.log(`[Widget.draw] Keyframes: prevKf=${JSON.stringify(prevKf)}, nextKf=${JSON.stringify(nextKf)}`);
|
console.log(`[Widget.draw] Keyframes: prevKf=${JSON.stringify(prevKf)}, nextKf=${JSON.stringify(nextKf)}, t=${interpolationT}`);
|
||||||
|
|
||||||
// Get interpolated value
|
// Get interpolated value
|
||||||
let shapeIndexValue = shapeIndexCurve.interpolate(currentTime);
|
let shapeIndexValue = shapeIndexCurve.interpolate(currentTime);
|
||||||
|
|
@ -4817,8 +4817,10 @@ class GraphicsObject extends Widget {
|
||||||
const shape2 = shapes.find(s => s.shapeIndex === nextKf.value);
|
const shape2 = shapes.find(s => s.shapeIndex === nextKf.value);
|
||||||
|
|
||||||
if (shape1 && shape2) {
|
if (shape1 && shape2) {
|
||||||
// Calculate t based on time position between keyframes
|
// Use the interpolated shapeIndexValue to calculate blend factor
|
||||||
const t = (currentTime - prevKf.time) / (nextKf.time - prevKf.time);
|
// This respects the bezier easing curve
|
||||||
|
const t = (shapeIndexValue - prevKf.value) / (nextKf.value - prevKf.value);
|
||||||
|
console.log(`[Widget.draw] Morphing from shape ${prevKf.value} to ${nextKf.value}, shapeIndexValue=${shapeIndexValue}, t=${t}`);
|
||||||
const morphedShape = shape1.lerpShape(shape2, t);
|
const morphedShape = shape1.lerpShape(shape2, t);
|
||||||
visibleShapes.push({
|
visibleShapes.push({
|
||||||
shape: morphedShape,
|
shape: morphedShape,
|
||||||
|
|
@ -6285,7 +6287,15 @@ function addKeyframeAtPlayhead() {
|
||||||
}
|
}
|
||||||
|
|
||||||
const shapeIndexCurve = animationData.getOrCreateCurve(`shape.${obj.shapeId}.shapeIndex`);
|
const shapeIndexCurve = animationData.getOrCreateCurve(`shape.${obj.shapeId}.shapeIndex`);
|
||||||
const shapeIndexKeyframe = new Keyframe(currentTime, newShapeIndex, 'linear');
|
// Check if a keyframe already exists at this time to preserve its interpolation type
|
||||||
|
const framerate = context.config?.framerate || 24;
|
||||||
|
const timeResolution = (1 / framerate) / 2;
|
||||||
|
const existingShapeIndexKf = shapeIndexCurve.getKeyframeAtTime(currentTime, timeResolution);
|
||||||
|
const interpolationType = existingShapeIndexKf ? existingShapeIndexKf.interpolation : 'linear';
|
||||||
|
const shapeIndexKeyframe = new Keyframe(currentTime, newShapeIndex, interpolationType);
|
||||||
|
// Preserve easeIn/easeOut if they exist
|
||||||
|
if (existingShapeIndexKf && existingShapeIndexKf.easeIn) shapeIndexKeyframe.easeIn = existingShapeIndexKf.easeIn;
|
||||||
|
if (existingShapeIndexKf && existingShapeIndexKf.easeOut) shapeIndexKeyframe.easeOut = existingShapeIndexKf.easeOut;
|
||||||
shapeIndexCurve.addKeyframe(shapeIndexKeyframe);
|
shapeIndexCurve.addKeyframe(shapeIndexKeyframe);
|
||||||
|
|
||||||
console.log(`Created new shape version with shapeIndex ${newShapeIndex} at time ${currentTime}`);
|
console.log(`Created new shape version with shapeIndex ${newShapeIndex} at time ${currentTime}`);
|
||||||
|
|
|
||||||
303
src/widgets.js
303
src/widgets.js
|
|
@ -555,6 +555,9 @@ class TimelineWindowV2 extends Widget {
|
||||||
// Hover state for showing keyframe values
|
// Hover state for showing keyframe values
|
||||||
this.hoveredKeyframe = null // {keyframe, x, y} - keyframe being hovered over and its screen position
|
this.hoveredKeyframe = null // {keyframe, x, y} - keyframe being hovered over and its screen position
|
||||||
|
|
||||||
|
// Hidden curves (Phase 6) - Set of curve parameter names
|
||||||
|
this.hiddenCurves = new Set()
|
||||||
|
|
||||||
// Phase 6: Segment dragging state
|
// Phase 6: Segment dragging state
|
||||||
this.draggingSegment = null // {track, initialMouseTime, segmentStartTime, animationData}
|
this.draggingSegment = null // {track, initialMouseTime, segmentStartTime, animationData}
|
||||||
|
|
||||||
|
|
@ -764,6 +767,83 @@ class TimelineWindowV2 extends Widget {
|
||||||
ctx.fillStyle = foregroundColor
|
ctx.fillStyle = foregroundColor
|
||||||
ctx.fillRect(buttonX + 2, buttonY + 2, buttonSize - 4, buttonSize - 4)
|
ctx.fillRect(buttonX + 2, buttonY + 2, buttonSize - 4, buttonSize - 4)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw legend for expanded curves (Phase 6)
|
||||||
|
if (track.object.curvesMode === 'expanded') {
|
||||||
|
// Get curves for this track
|
||||||
|
const curves = []
|
||||||
|
const obj = track.object
|
||||||
|
let animationData = null
|
||||||
|
|
||||||
|
// Find the AnimationData for this track
|
||||||
|
if (track.type === 'object') {
|
||||||
|
for (let layer of this.context.activeObject.allLayers) {
|
||||||
|
if (layer.children && layer.children.includes(obj)) {
|
||||||
|
animationData = layer.animationData
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (track.type === 'shape') {
|
||||||
|
for (let layer of this.context.activeObject.allLayers) {
|
||||||
|
if (layer.shapes && layer.shapes.some(s => s.shapeId === obj.shapeId)) {
|
||||||
|
animationData = layer.animationData
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animationData) {
|
||||||
|
const prefix = track.type === 'object' ? `child.${obj.idx}.` : `shape.${obj.shapeId}.`
|
||||||
|
for (let curveName in animationData.curves) {
|
||||||
|
if (curveName.startsWith(prefix)) {
|
||||||
|
curves.push(animationData.curves[curveName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (curves.length > 0) {
|
||||||
|
ctx.save()
|
||||||
|
const legendPadding = 3
|
||||||
|
const legendLineHeight = 12
|
||||||
|
const legendHeight = curves.length * legendLineHeight + legendPadding * 2
|
||||||
|
const legendY = y + this.trackHierarchy.trackHeight + 5 // Below track name row
|
||||||
|
|
||||||
|
// Draw legend items (no background box)
|
||||||
|
ctx.font = '9px sans-serif'
|
||||||
|
ctx.textAlign = 'left'
|
||||||
|
ctx.textBaseline = 'top'
|
||||||
|
|
||||||
|
for (let i = 0; i < curves.length; i++) {
|
||||||
|
const curve = curves[i]
|
||||||
|
const itemY = legendY + legendPadding + i * legendLineHeight
|
||||||
|
const isHidden = this.hiddenCurves.has(curve.parameter)
|
||||||
|
|
||||||
|
// Draw color dot (grayed out if hidden)
|
||||||
|
ctx.fillStyle = isHidden ? foregroundColor : curve.displayColor
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(10, itemY + 5, 3, 0, 2 * Math.PI)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
// Draw parameter name (extract last part after last dot)
|
||||||
|
ctx.fillStyle = isHidden ? foregroundColor : labelColor
|
||||||
|
const paramName = curve.parameter.split('.').pop()
|
||||||
|
const truncatedName = paramName.length > 12 ? paramName.substring(0, 10) + '...' : paramName
|
||||||
|
ctx.fillText(truncatedName, 18, itemY)
|
||||||
|
|
||||||
|
// Draw strikethrough if hidden
|
||||||
|
if (isHidden) {
|
||||||
|
ctx.strokeStyle = foregroundColor
|
||||||
|
ctx.lineWidth = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
const textWidth = ctx.measureText(truncatedName).width
|
||||||
|
ctx.moveTo(18, itemY + 5)
|
||||||
|
ctx.lineTo(18 + textWidth, itemY + 5)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1202,48 +1282,10 @@ class TimelineWindowV2 extends Widget {
|
||||||
return startY + curveHeight - padding - (normalizedValue * (curveHeight - 2 * padding))
|
return startY + curveHeight - padding - (normalizedValue * (curveHeight - 2 * padding))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw legend showing which color is which parameter
|
|
||||||
// Position it below the track name area, top-right of the curve area
|
|
||||||
ctx.save()
|
|
||||||
ctx.fillStyle = backgroundColor
|
|
||||||
ctx.strokeStyle = shadow
|
|
||||||
ctx.lineWidth = 1
|
|
||||||
|
|
||||||
// Calculate legend size
|
|
||||||
const legendPadding = 4
|
|
||||||
const legendLineHeight = 14
|
|
||||||
const legendHeight = curves.length * legendLineHeight + legendPadding * 2
|
|
||||||
const legendWidth = 100
|
|
||||||
const legendX = 5 // Small left margin
|
|
||||||
const legendY = startY + 40 // Below track name area
|
|
||||||
|
|
||||||
// Draw legend background
|
|
||||||
ctx.fillRect(legendX, legendY, legendWidth, legendHeight)
|
|
||||||
ctx.strokeRect(legendX, legendY, legendWidth, legendHeight)
|
|
||||||
|
|
||||||
// Draw legend items
|
|
||||||
ctx.font = '10px sans-serif'
|
|
||||||
ctx.textBaseline = 'top'
|
|
||||||
for (let i = 0; i < curves.length; i++) {
|
|
||||||
const curve = curves[i]
|
|
||||||
const y = legendY + legendPadding + i * legendLineHeight
|
|
||||||
|
|
||||||
// Draw color dot
|
|
||||||
ctx.fillStyle = curve.displayColor
|
|
||||||
ctx.beginPath()
|
|
||||||
ctx.arc(legendX + legendPadding + 4, y + 6, 3, 0, 2 * Math.PI)
|
|
||||||
ctx.fill()
|
|
||||||
|
|
||||||
// Draw parameter name (extract last part after last dot)
|
|
||||||
ctx.fillStyle = labelColor
|
|
||||||
const paramName = curve.parameter.split('.').pop()
|
|
||||||
ctx.fillText(paramName, legendX + legendPadding + 12, y + 2)
|
|
||||||
}
|
|
||||||
ctx.restore()
|
|
||||||
|
|
||||||
// Draw each curve
|
// Draw each curve
|
||||||
for (let curve of curves) {
|
for (let curve of curves) {
|
||||||
if (curve.keyframes.length === 0) continue
|
if (curve.keyframes.length === 0) continue
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) continue // Skip hidden curves
|
||||||
|
|
||||||
ctx.strokeStyle = curve.displayColor
|
ctx.strokeStyle = curve.displayColor
|
||||||
ctx.fillStyle = curve.displayColor
|
ctx.fillStyle = curve.displayColor
|
||||||
|
|
@ -1556,6 +1598,65 @@ class TimelineWindowV2 extends Widget {
|
||||||
if (this.requestRedraw) this.requestRedraw()
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if clicking on legend items (Phase 6)
|
||||||
|
if (track.object.curvesMode === 'expanded') {
|
||||||
|
const trackIndex = this.trackHierarchy.tracks.indexOf(track)
|
||||||
|
const trackYPos = this.trackHierarchy.getTrackY(trackIndex)
|
||||||
|
const legendPadding = 3
|
||||||
|
const legendLineHeight = 12
|
||||||
|
const legendY = trackYPos + this.trackHierarchy.trackHeight + 5
|
||||||
|
|
||||||
|
// Get curves for this track
|
||||||
|
const curves = []
|
||||||
|
const obj = track.object
|
||||||
|
let animationData = null
|
||||||
|
|
||||||
|
if (track.type === 'object') {
|
||||||
|
for (let layer of this.context.activeObject.allLayers) {
|
||||||
|
if (layer.children && layer.children.includes(obj)) {
|
||||||
|
animationData = layer.animationData
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (track.type === 'shape') {
|
||||||
|
for (let layer of this.context.activeObject.allLayers) {
|
||||||
|
if (layer.shapes && layer.shapes.some(s => s.shapeId === obj.shapeId)) {
|
||||||
|
animationData = layer.animationData
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (animationData) {
|
||||||
|
const prefix = track.type === 'object' ? `child.${obj.idx}.` : `shape.${obj.shapeId}.`
|
||||||
|
for (let curveName in animationData.curves) {
|
||||||
|
if (curveName.startsWith(prefix)) {
|
||||||
|
curves.push(animationData.curves[curveName])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if clicking on any legend item
|
||||||
|
for (let i = 0; i < curves.length; i++) {
|
||||||
|
const curve = curves[i]
|
||||||
|
const itemY = legendY + legendPadding + i * legendLineHeight
|
||||||
|
|
||||||
|
// Legend items are from x=5 to x=145, height of 12px
|
||||||
|
if (x >= 5 && x <= 145 && adjustedY >= itemY && adjustedY <= itemY + legendLineHeight) {
|
||||||
|
// Toggle visibility of this curve
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) {
|
||||||
|
this.hiddenCurves.delete(curve.parameter)
|
||||||
|
console.log(`Showing curve: ${curve.parameter}`)
|
||||||
|
} else {
|
||||||
|
this.hiddenCurves.add(curve.parameter)
|
||||||
|
console.log(`Hiding curve: ${curve.parameter}`)
|
||||||
|
}
|
||||||
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clicking elsewhere on track header selects it
|
// Clicking elsewhere on track header selects it
|
||||||
|
|
@ -1575,6 +1676,7 @@ class TimelineWindowV2 extends Widget {
|
||||||
// Phase 6: Check if clicking on tangent handle (highest priority for curves)
|
// Phase 6: Check if clicking on tangent handle (highest priority for curves)
|
||||||
if ((track.type === 'object' || track.type === 'shape') && track.object.curvesMode === 'expanded') {
|
if ((track.type === 'object' || track.type === 'shape') && track.object.curvesMode === 'expanded') {
|
||||||
const tangentInfo = this.getTangentHandleAtPoint(track, adjustedX, adjustedY)
|
const tangentInfo = this.getTangentHandleAtPoint(track, adjustedX, adjustedY)
|
||||||
|
console.log(`Tangent handle check result:`, tangentInfo)
|
||||||
if (tangentInfo) {
|
if (tangentInfo) {
|
||||||
// Start tangent dragging
|
// Start tangent dragging
|
||||||
this.draggingTangent = {
|
this.draggingTangent = {
|
||||||
|
|
@ -1756,6 +1858,9 @@ class TimelineWindowV2 extends Widget {
|
||||||
// Check if clicking close to an existing keyframe on ANY curve (within 8px)
|
// Check if clicking close to an existing keyframe on ANY curve (within 8px)
|
||||||
// First pass: check all curves for keyframe hits
|
// First pass: check all curves for keyframe hits
|
||||||
for (let curve of curves) {
|
for (let curve of curves) {
|
||||||
|
// Skip hidden curves
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) continue
|
||||||
|
|
||||||
for (let keyframe of curve.keyframes) {
|
for (let keyframe of curve.keyframes) {
|
||||||
const kfX = this.timelineState.timeToPixel(keyframe.time)
|
const kfX = this.timelineState.timeToPixel(keyframe.time)
|
||||||
const kfY = startY + curveHeight - padding - ((keyframe.value - minValue) / (maxValue - minValue) * (curveHeight - 2 * padding))
|
const kfY = startY + curveHeight - padding - ((keyframe.value - minValue) / (maxValue - minValue) * (curveHeight - 2 * padding))
|
||||||
|
|
@ -1786,6 +1891,12 @@ class TimelineWindowV2 extends Widget {
|
||||||
console.log(`Selected single keyframe`)
|
console.log(`Selected single keyframe`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't start dragging if this was a right-click
|
||||||
|
if (this.lastClickEvent?.button === 2) {
|
||||||
|
console.log(`Skipping drag - right-click detected (button=${this.lastClickEvent.button})`)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// Start dragging this keyframe (and all selected keyframes)
|
// Start dragging this keyframe (and all selected keyframes)
|
||||||
this.draggingKeyframe = {
|
this.draggingKeyframe = {
|
||||||
curve: curve, // Use the actual curve we clicked on
|
curve: curve, // Use the actual curve we clicked on
|
||||||
|
|
@ -1813,11 +1924,14 @@ class TimelineWindowV2 extends Widget {
|
||||||
}
|
}
|
||||||
|
|
||||||
// No keyframe was clicked, so add a new one
|
// No keyframe was clicked, so add a new one
|
||||||
// Find the closest curve to the click position
|
// Find the closest curve to the click position (only visible curves)
|
||||||
let targetCurve = curves[0]
|
let targetCurve = null
|
||||||
let minDistance = Infinity
|
let minDistance = Infinity
|
||||||
|
|
||||||
for (let curve of curves) {
|
for (let curve of curves) {
|
||||||
|
// Skip hidden curves
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) continue
|
||||||
|
|
||||||
// For each curve, find the value at this time
|
// For each curve, find the value at this time
|
||||||
const curveValue = curve.interpolate(clickTime)
|
const curveValue = curve.interpolate(clickTime)
|
||||||
if (curveValue !== null) {
|
if (curveValue !== null) {
|
||||||
|
|
@ -1831,6 +1945,9 @@ class TimelineWindowV2 extends Widget {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If all curves are hidden, don't add a keyframe
|
||||||
|
if (!targetCurve) return false
|
||||||
|
|
||||||
console.log('Adding keyframe at time', clickTime, 'with value', clickValue, 'to curve', targetCurve.parameter)
|
console.log('Adding keyframe at time', clickTime, 'with value', clickValue, 'to curve', targetCurve.parameter)
|
||||||
|
|
||||||
// Create keyframe directly
|
// Create keyframe directly
|
||||||
|
|
@ -2252,6 +2369,9 @@ class TimelineWindowV2 extends Widget {
|
||||||
|
|
||||||
// Check each curve for tangent handles
|
// Check each curve for tangent handles
|
||||||
for (let curve of curves) {
|
for (let curve of curves) {
|
||||||
|
// Skip hidden curves
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) continue
|
||||||
|
|
||||||
// Only check bezier keyframes that are selected
|
// Only check bezier keyframes that are selected
|
||||||
for (let i = 0; i < curve.keyframes.length; i++) {
|
for (let i = 0; i < curve.keyframes.length; i++) {
|
||||||
const kf = curve.keyframes[i]
|
const kf = curve.keyframes[i]
|
||||||
|
|
@ -2552,7 +2672,58 @@ class TimelineWindowV2 extends Widget {
|
||||||
|
|
||||||
// Apply the delta
|
// Apply the delta
|
||||||
selectedKeyframe.time = Math.max(0, selectedKeyframe.initialDragTime + timeDelta)
|
selectedKeyframe.time = Math.max(0, selectedKeyframe.initialDragTime + timeDelta)
|
||||||
selectedKeyframe.value = selectedKeyframe.initialDragValue + valueDelta
|
let newValue = selectedKeyframe.initialDragValue + valueDelta
|
||||||
|
|
||||||
|
// Special validation for shapeIndex curves: only allow values that correspond to actual shapes
|
||||||
|
if (this.draggingKeyframe.curve.parameter.endsWith('.shapeIndex')) {
|
||||||
|
// Extract shapeId from parameter name: "shape.{shapeId}.shapeIndex"
|
||||||
|
const match = this.draggingKeyframe.curve.parameter.match(/^shape\.([^.]+)\.shapeIndex$/)
|
||||||
|
if (match) {
|
||||||
|
const shapeId = match[1]
|
||||||
|
|
||||||
|
// Find all shapes with this shapeId and get their shapeIndex values
|
||||||
|
const track = this.draggingKeyframe.track
|
||||||
|
let layer = null
|
||||||
|
|
||||||
|
if (track.type === 'shape') {
|
||||||
|
// Find the layer containing this shape
|
||||||
|
for (let l of this.context.activeObject.allLayers) {
|
||||||
|
if (l.shapes && l.shapes.some(s => s.shapeId === shapeId)) {
|
||||||
|
layer = l
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (layer) {
|
||||||
|
const validIndexes = layer.shapes
|
||||||
|
.filter(s => s.shapeId === shapeId)
|
||||||
|
.map(s => s.shapeIndex)
|
||||||
|
.sort((a, b) => a - b)
|
||||||
|
|
||||||
|
if (validIndexes.length > 0) {
|
||||||
|
// Round to nearest integer first
|
||||||
|
const roundedValue = Math.round(newValue)
|
||||||
|
|
||||||
|
// Find the closest valid index
|
||||||
|
let closestIndex = validIndexes[0]
|
||||||
|
let closestDist = Math.abs(roundedValue - closestIndex)
|
||||||
|
|
||||||
|
for (let validIndex of validIndexes) {
|
||||||
|
const dist = Math.abs(roundedValue - validIndex)
|
||||||
|
if (dist < closestDist) {
|
||||||
|
closestDist = dist
|
||||||
|
closestIndex = validIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
newValue = closestIndex
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedKeyframe.value = newValue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resort keyframes in all affected curves
|
// Resort keyframes in all affected curves
|
||||||
|
|
@ -3045,18 +3216,37 @@ class TimelineWindowV2 extends Widget {
|
||||||
maxValue += rangePadding
|
maxValue += rangePadding
|
||||||
|
|
||||||
// Check if right-clicking on a keyframe (within 8px)
|
// Check if right-clicking on a keyframe (within 8px)
|
||||||
|
// Find the CLOSEST keyframe, not just the first one (and skip hidden curves)
|
||||||
|
let closestKeyframe = null
|
||||||
|
let closestCurve = null
|
||||||
|
let closestDistance = 8 // Maximum hit distance
|
||||||
|
|
||||||
for (let curve of curves) {
|
for (let curve of curves) {
|
||||||
|
// Skip hidden curves
|
||||||
|
if (this.hiddenCurves.has(curve.parameter)) continue
|
||||||
|
|
||||||
for (let i = 0; i < curve.keyframes.length; i++) {
|
for (let i = 0; i < curve.keyframes.length; i++) {
|
||||||
const keyframe = curve.keyframes[i]
|
const keyframe = curve.keyframes[i]
|
||||||
const kfX = this.timelineState.timeToPixel(keyframe.time)
|
const kfX = this.timelineState.timeToPixel(keyframe.time)
|
||||||
const kfY = startY + curveHeight - padding - ((keyframe.value - minValue) / (maxValue - minValue) * (curveHeight - 2 * padding))
|
const kfY = startY + curveHeight - padding - ((keyframe.value - minValue) / (maxValue - minValue) * (curveHeight - 2 * padding))
|
||||||
const distance = Math.sqrt((adjustedX - kfX) ** 2 + (adjustedY - kfY) ** 2)
|
const distance = Math.sqrt((adjustedX - kfX) ** 2 + (adjustedY - kfY) ** 2)
|
||||||
|
|
||||||
if (distance < 8) {
|
if (distance < closestDistance) {
|
||||||
// Phase 6: Check if shift key is pressed for quick delete
|
closestDistance = distance
|
||||||
const shiftPressed = event && event.shiftKey
|
closestKeyframe = keyframe
|
||||||
|
closestCurve = curve
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (shiftPressed) {
|
if (closestKeyframe) {
|
||||||
|
const keyframe = closestKeyframe
|
||||||
|
const curve = closestCurve
|
||||||
|
|
||||||
|
// Phase 6: Check if shift key is pressed for quick delete
|
||||||
|
const shiftPressed = event && event.shiftKey
|
||||||
|
|
||||||
|
if (shiftPressed) {
|
||||||
// Shift+right-click: quick delete
|
// Shift+right-click: quick delete
|
||||||
if (this.selectedKeyframes.size > 1) {
|
if (this.selectedKeyframes.size > 1) {
|
||||||
// Delete all selected keyframes
|
// Delete all selected keyframes
|
||||||
|
|
@ -3084,15 +3274,24 @@ class TimelineWindowV2 extends Widget {
|
||||||
} else {
|
} else {
|
||||||
// Regular right-click: show context menu
|
// Regular right-click: show context menu
|
||||||
if (this.selectedKeyframes.size > 1) {
|
if (this.selectedKeyframes.size > 1) {
|
||||||
this.showKeyframeContextMenu(Array.from(this.selectedKeyframes), curves)
|
// If right-clicking on a selected keyframe, show menu for all selected
|
||||||
|
if (this.selectedKeyframes.has(keyframe)) {
|
||||||
|
this.showKeyframeContextMenu(Array.from(this.selectedKeyframes), curves)
|
||||||
|
} else {
|
||||||
|
// Right-clicking on unselected keyframe: select it and show menu
|
||||||
|
this.selectedKeyframes.clear()
|
||||||
|
this.selectedKeyframes.add(keyframe)
|
||||||
|
this.showKeyframeContextMenu([keyframe], curves, curve)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
// No multi-selection: select this keyframe and show menu
|
||||||
|
this.selectedKeyframes.clear()
|
||||||
|
this.selectedKeyframes.add(keyframe)
|
||||||
this.showKeyframeContextMenu([keyframe], curves, curve)
|
this.showKeyframeContextMenu([keyframe], curves, curve)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
} // end if (closestKeyframe)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -3124,6 +3323,7 @@ class TimelineWindowV2 extends Widget {
|
||||||
action: async () => {
|
action: async () => {
|
||||||
keyframe.interpolation = 'linear'
|
keyframe.interpolation = 'linear'
|
||||||
console.log('Changed interpolation to linear')
|
console.log('Changed interpolation to linear')
|
||||||
|
// Keep flag set until next mousedown processes it
|
||||||
if (this.context.updateUI) this.context.updateUI()
|
if (this.context.updateUI) this.context.updateUI()
|
||||||
if (this.requestRedraw) this.requestRedraw()
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
}
|
}
|
||||||
|
|
@ -3134,7 +3334,6 @@ class TimelineWindowV2 extends Widget {
|
||||||
keyframe.interpolation = 'bezier'
|
keyframe.interpolation = 'bezier'
|
||||||
if (!keyframe.easeIn) keyframe.easeIn = { x: 0.42, y: 0 }
|
if (!keyframe.easeIn) keyframe.easeIn = { x: 0.42, y: 0 }
|
||||||
if (!keyframe.easeOut) keyframe.easeOut = { x: 0.58, y: 1 }
|
if (!keyframe.easeOut) keyframe.easeOut = { x: 0.58, y: 1 }
|
||||||
console.log('Changed interpolation to bezier')
|
|
||||||
if (this.context.updateUI) this.context.updateUI()
|
if (this.context.updateUI) this.context.updateUI()
|
||||||
if (this.requestRedraw) this.requestRedraw()
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
}
|
}
|
||||||
|
|
@ -3144,6 +3343,7 @@ class TimelineWindowV2 extends Widget {
|
||||||
action: async () => {
|
action: async () => {
|
||||||
keyframe.interpolation = 'step'
|
keyframe.interpolation = 'step'
|
||||||
console.log('Changed interpolation to step')
|
console.log('Changed interpolation to step')
|
||||||
|
// Keep flag set until next mousedown processes it
|
||||||
if (this.context.updateUI) this.context.updateUI()
|
if (this.context.updateUI) this.context.updateUI()
|
||||||
if (this.requestRedraw) this.requestRedraw()
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
}
|
}
|
||||||
|
|
@ -3153,6 +3353,7 @@ class TimelineWindowV2 extends Widget {
|
||||||
action: async () => {
|
action: async () => {
|
||||||
keyframe.interpolation = 'zero'
|
keyframe.interpolation = 'zero'
|
||||||
console.log('Changed interpolation to zero')
|
console.log('Changed interpolation to zero')
|
||||||
|
// Keep flag set until next mousedown processes it
|
||||||
if (this.context.updateUI) this.context.updateUI()
|
if (this.context.updateUI) this.context.updateUI()
|
||||||
if (this.requestRedraw) this.requestRedraw()
|
if (this.requestRedraw) this.requestRedraw()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue