feat(studio): scale GSAP positions on clip resize + shift on drag + diamond fixes (#1448)

Resize: proportionally scale all GSAP animation positions and durations
to fit the new clip duration via scalePositionsInScript. This preserves
clip-relative keyframe percentages — diamonds don't move during resize,
nothing disappears. Modeled after After Effects Time Stretch behavior.

Drag: shift all GSAP positions by the time delta (unchanged from before).

Diamond rendering:
- Clamp diamonds at 0%/100% so they stay fully visible at clip edges
- Filter out-of-range keyframes using predicted percentages during resize
- Clamp connection lines to clip boundaries
- PropertyRows: same edge clamping for SVG diamonds

Parser: scalePositionsInScript (proportional position + duration scaling),
shiftPositionsInScript (rigid shift), scale-positions + shift-positions
mutation types, 5 shift tests passing.
This commit is contained in:
Miguel Ángel
2026-06-15 02:31:35 -04:00
committed by GitHub
parent abaf67176c
commit 11b050de9a
8 changed files with 343 additions and 15 deletions
@@ -6,7 +6,7 @@ interface GestureTrailOverlayProps {
sampleCount?: number;
trail?: Array<{ x: number; y: number }>;
simplifiedPoints?: Map<number, Record<string, number>>;
canvasRect: { left: number; top: number; width: number; height: number };
canvasRect: { left: number; top: number; width: number; height: number } | null;
compositionSize?: { width: number; height: number };
mode: "recording" | "preview";
accentColor?: string;
@@ -23,6 +23,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
accentColor = "#3CE6AC",
}: GestureTrailOverlayProps) {
const trailPoints = useMemo(() => {
if (!canvasRect) return "";
if (trail && trail.length > 1) {
return trail.map((p) => `${p.x - canvasRect.left},${p.y - canvasRect.top}`).join(" ");
}
@@ -32,7 +33,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
.map((s) => `${s.properties.x},${s.properties.y}`)
.join(" ");
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [samples, trail, sampleCount, canvasRect.left, canvasRect.top]);
}, [samples, trail, sampleCount, canvasRect?.left, canvasRect?.top]);
const simplifiedPath = useMemo(() => {
if (!simplifiedPoints || simplifiedPoints.size === 0) return "";
@@ -58,7 +59,7 @@ export const GestureTrailOverlay = memo(function GestureTrailOverlay({
return pts.sort((a, b) => a.pct - b.pct);
}, [simplifiedPoints]);
if (samples.length < 2 && !simplifiedPoints) return null;
if (!canvasRect || (samples.length < 2 && !simplifiedPoints)) return null;
return (
<svg