mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-07 18:26:17 +00:00
fix(studio): retime the dragged element's own keyframe
Drag-to-retime resolved the dragged diamond against the selected element's animations and committed through the selected element's DOM selection, so dragging a diamond on a non-selected clip retimed the wrong tween. It now resolves against the clicked element's animations and commits through that element's selection, matching the delete path. The three diamond callbacks also take the TimelineKeyframeTarget they already had instead of five positional fields, and the two copies of the sourceFile#domId split share splitTimelineElementKey.
This commit is contained in:
@@ -48,12 +48,13 @@ interface TimelineClipDiamondsProps {
|
||||
onShiftClickKeyframe?: (elementId: string, percentage: number) => void;
|
||||
onContextMenuKeyframe?: (e: React.MouseEvent, elementId: string, percentage: number) => void;
|
||||
/** Drag-to-retime: move a keyframe to a new time, preserving its value + ease.
|
||||
* Both percentages are clip-relative: `fromClipPercentage` identifies the
|
||||
* dragged keyframe, `toClipPercentage` is the neighbour-clamped drop position.
|
||||
* The handler decides move (within the tween) vs resize (past its boundary). */
|
||||
* `keyframe` identifies the dragged keyframe (clip-relative percentage plus
|
||||
* whatever animation identity the row carries); `toClipPercentage` is the
|
||||
* neighbour-clamped drop position, also clip-relative. The handler decides
|
||||
* move (within the tween) vs resize (past its boundary). */
|
||||
onMoveKeyframe?: (
|
||||
elementId: string,
|
||||
fromClipPercentage: number,
|
||||
keyframe: TimelineKeyframeTarget,
|
||||
toClipPercentage: number,
|
||||
) => Promise<boolean>;
|
||||
/** Open the segment ease editor for the hovered mid-point button — available on
|
||||
@@ -549,7 +550,7 @@ export const TimelineClipDiamonds = memo(function TimelineClipDiamonds(
|
||||
onMoveKeyframe={
|
||||
props.onMoveKeyframe
|
||||
? (target, toClipPercentage) =>
|
||||
props.onMoveKeyframe?.(props.elementId, target.percentage, toClipPercentage) ??
|
||||
props.onMoveKeyframe?.(props.elementId, target, toClipPercentage) ??
|
||||
Promise.resolve(false)
|
||||
: undefined
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Eye, EyeSlash } from "@phosphor-icons/react";
|
||||
import { BeatStrip, BeatBackgroundLines } from "./BeatStrip";
|
||||
import { TimelineClip } from "./TimelineClip";
|
||||
import { TimelineClipDiamonds } from "./TimelineClipDiamonds";
|
||||
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
||||
import type { MusicBeatAnalysis } from "@hyperframes/core/beats";
|
||||
import { getTimelineEditCapabilities, resolveBlockedTimelineEditIntent } from "./timelineEditing";
|
||||
import type { TimelineTheme } from "./timelineTheme";
|
||||
@@ -76,7 +77,7 @@ export interface TimelineLaneBaseProps {
|
||||
onContextMenuKeyframe?: (e: React.MouseEvent, elementId: string, percentage: number) => void;
|
||||
onMoveKeyframe?: (
|
||||
elementId: string,
|
||||
fromClipPercentage: number,
|
||||
keyframe: TimelineKeyframeTarget,
|
||||
toClipPercentage: number,
|
||||
) => Promise<boolean>;
|
||||
onContextMenuClip?: (e: React.MouseEvent, element: TimelineElement) => void;
|
||||
|
||||
@@ -106,12 +106,12 @@ export function TimelineOverlays({
|
||||
<KeyframeDiamondContextMenu
|
||||
state={kfContextMenu}
|
||||
onClose={() => setKfContextMenu(null)}
|
||||
onDelete={(elId, pct) => onDeleteKeyframe?.(elId, pct)}
|
||||
onDelete={(elId, pct) => onDeleteKeyframe?.(elId, { percentage: pct })}
|
||||
onDeleteAll={(elId) => onDeleteAllKeyframes?.(elId)}
|
||||
onChangeEase={(elId, pct, ease) => onChangeKeyframeEase?.(elId, pct, ease)}
|
||||
onMoveToPlayhead={
|
||||
onMoveKeyframeToPlayhead
|
||||
? (elId, pct) => onMoveKeyframeToPlayhead(elId, pct)
|
||||
? (elId, pct) => onMoveKeyframeToPlayhead(elId, { percentage: pct })
|
||||
: undefined
|
||||
}
|
||||
onCopyProperties={(elId, pct) => {
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { TimelineElement } from "../store/playerStore";
|
||||
import type { TimelineMoveOperation } from "../../hooks/timelineMoveAdapter";
|
||||
import type { BlockedTimelineEditIntent } from "./timelineEditing";
|
||||
import type { PropertyGroupName } from "@hyperframes/core/gsap-parser";
|
||||
import type { TimelineKeyframeTarget } from "./timelineKeyframeIdentity";
|
||||
|
||||
export interface TimelinePropertyGroupKeyframeToggle {
|
||||
animationId: string;
|
||||
@@ -71,29 +72,16 @@ export interface TimelineEditCallbacks {
|
||||
onSplitElement?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
||||
onRazorSplit?: (element: TimelineElement, splitTime: number) => Promise<void> | void;
|
||||
onRazorSplitAll?: (splitTime: number) => Promise<void> | void;
|
||||
onDeleteKeyframe?: (
|
||||
elementId: string,
|
||||
percentage: number,
|
||||
propertyGroup?: string,
|
||||
tweenPercentage?: number,
|
||||
animationId?: string,
|
||||
) => void;
|
||||
onDeleteKeyframe?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||
onDeleteAllKeyframes?: (elementId: string) => void;
|
||||
onChangeKeyframeEase?: (elementId: string, percentage: number, ease: string) => void;
|
||||
onMoveKeyframeToPlayhead?: (
|
||||
elementId: string,
|
||||
percentage: number,
|
||||
propertyGroup?: string,
|
||||
tweenPercentage?: number,
|
||||
animationId?: string,
|
||||
) => void;
|
||||
onMoveKeyframeToPlayhead?: (elementId: string, keyframe: TimelineKeyframeTarget) => void;
|
||||
/** Drag-to-retime: `keyframe` identifies the dragged keyframe (its percentage
|
||||
* is clip-relative), `toClipPercentage` is the neighbour-clamped drop. */
|
||||
onMoveKeyframe?: (
|
||||
elementId: string,
|
||||
fromClipPercentage: number,
|
||||
keyframe: TimelineKeyframeTarget,
|
||||
toClipPercentage: number,
|
||||
propertyGroup?: string,
|
||||
tweenPercentage?: number,
|
||||
animationId?: string,
|
||||
) => Promise<boolean>;
|
||||
onToggleKeyframeAtPlayhead?: (element: TimelineElement) => void;
|
||||
onTogglePropertyGroupKeyframe?: (
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from "react";
|
||||
import { usePlayerStore, type TimelineElement, type DomClipChild } from "../store/playerStore";
|
||||
import type { ClipManifestClip } from "../lib/playbackTypes";
|
||||
import { createTimelineElementFromManifestClip } from "../lib/timelineDOM";
|
||||
import { buildTimelineElementKey } from "../lib/timelineElementHelpers";
|
||||
import { buildTimelineElementKey, splitTimelineElementKey } from "../lib/timelineElementHelpers";
|
||||
|
||||
function findTopLevelAncestor(id: string, parentMap: Map<string, string>): string | null {
|
||||
let current = parentMap.get(id);
|
||||
@@ -19,18 +19,13 @@ function findTopLevelAncestor(id: string, parentMap: Map<string, string>): strin
|
||||
return current;
|
||||
}
|
||||
|
||||
function extractDomId(key: string): string {
|
||||
const hashIdx = key.lastIndexOf("#");
|
||||
return hashIdx >= 0 ? key.slice(hashIdx + 1) : key;
|
||||
}
|
||||
|
||||
function resolveRawId(
|
||||
selectedId: string | null,
|
||||
manifest: ClipManifestClip[],
|
||||
parentMap: Map<string, string>,
|
||||
): string | null {
|
||||
if (!selectedId) return null;
|
||||
const rawId = extractDomId(selectedId);
|
||||
const rawId = splitTimelineElementKey(selectedId).domId;
|
||||
if (parentMap.has(rawId)) return rawId;
|
||||
if (parentMap.has(selectedId)) return selectedId;
|
||||
const clip = manifest.find((c) => c.label === selectedId || c.label === rawId);
|
||||
|
||||
@@ -298,6 +298,20 @@ export function buildTimelineElementKey(params: {
|
||||
return `${scope}:${params.id}:${params.fallbackIndex}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Inverse of {@link buildTimelineElementKey} for the `sourceFile#domId` form.
|
||||
* A key with no `#` is a bare dom id and carries no source file of its own, so
|
||||
* the caller supplies the scope it wants to look that id up in.
|
||||
*/
|
||||
export function splitTimelineElementKey(key: string): {
|
||||
sourceFile: string | null;
|
||||
domId: string;
|
||||
} {
|
||||
const hashIndex = key.lastIndexOf("#");
|
||||
if (hashIndex < 0) return { sourceFile: null, domId: key };
|
||||
return { sourceFile: key.slice(0, hashIndex), domId: key.slice(hashIndex + 1) };
|
||||
}
|
||||
|
||||
export function buildTimelineElementIdentity(params: {
|
||||
preferredId?: string | null;
|
||||
label: string;
|
||||
|
||||
Reference in New Issue
Block a user