summaryrefslogtreecommitdiff
path: root/apps/web/src/hooks/timeline
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/hooks/timeline')
-rw-r--r--apps/web/src/hooks/timeline/element/use-element-interaction.ts5
-rw-r--r--apps/web/src/hooks/timeline/element/use-keyframe-selection.ts229
-rw-r--r--apps/web/src/hooks/timeline/use-snap-indicator-position.ts8
3 files changed, 238 insertions, 4 deletions
diff --git a/apps/web/src/hooks/timeline/element/use-element-interaction.ts b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
index e7b93813..3831a86f 100644
--- a/apps/web/src/hooks/timeline/element/use-element-interaction.ts
+++ b/apps/web/src/hooks/timeline/element/use-element-interaction.ts
@@ -592,9 +592,12 @@ export function useElementInteraction({
});
if (!alreadySelected) {
selectElement({ trackId: track.id, elementId: element.id });
+ return;
}
+
+ editor.selection.clearKeyframeSelection();
},
- [isElementSelected, selectElement],
+ [editor.selection, isElementSelected, selectElement],
);
return {
diff --git a/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
new file mode 100644
index 00000000..50def392
--- /dev/null
+++ b/apps/web/src/hooks/timeline/element/use-keyframe-selection.ts
@@ -0,0 +1,229 @@
+import { useCallback, useSyncExternalStore } from "react";
+import { useEditor } from "@/hooks/use-editor";
+import type { SelectedKeyframeRef } from "@/types/animation";
+
+function getSelectedKeyframeId({
+ keyframe,
+}: {
+ keyframe: SelectedKeyframeRef;
+}): string {
+ return `${keyframe.trackId}:${keyframe.elementId}:${keyframe.propertyPath}:${keyframe.keyframeId}`;
+}
+
+function mergeUniqueKeyframes({
+ keyframes,
+}: {
+ keyframes: SelectedKeyframeRef[];
+}): SelectedKeyframeRef[] {
+ const keyframesById = new Map<string, SelectedKeyframeRef>();
+ for (const keyframe of keyframes) {
+ keyframesById.set(getSelectedKeyframeId({ keyframe }), keyframe);
+ }
+ return [...keyframesById.values()];
+}
+
+export function useKeyframeSelection() {
+ const editor = useEditor();
+ const selectedKeyframes = useSyncExternalStore(
+ (listener) => editor.selection.subscribe(listener),
+ () => editor.selection.getSelectedKeyframes(),
+ );
+ const keyframeSelectionAnchor = useSyncExternalStore(
+ (listener) => editor.selection.subscribe(listener),
+ () => editor.selection.getKeyframeSelectionAnchor(),
+ );
+
+ const isKeyframeSelected = useCallback(
+ ({ keyframe }: { keyframe: SelectedKeyframeRef }) => {
+ const keyframeId = getSelectedKeyframeId({ keyframe });
+ return selectedKeyframes.some(
+ (selectedKeyframe) =>
+ getSelectedKeyframeId({ keyframe: selectedKeyframe }) === keyframeId,
+ );
+ },
+ [selectedKeyframes],
+ );
+
+ const setKeyframeSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const uniqueKeyframes = mergeUniqueKeyframes({ keyframes });
+ editor.selection.setSelectedKeyframes({
+ keyframes: uniqueKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? uniqueKeyframes[uniqueKeyframes.length - 1] ?? null,
+ });
+ },
+ [editor],
+ );
+
+ const addKeyframesToSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const mergedKeyframes = mergeUniqueKeyframes({
+ keyframes: [...selectedKeyframes, ...keyframes],
+ });
+ editor.selection.setSelectedKeyframes({
+ keyframes: mergedKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? mergedKeyframes[mergedKeyframes.length - 1] ?? null,
+ });
+ },
+ [selectedKeyframes, editor],
+ );
+
+ const removeKeyframesFromSelection = useCallback(
+ ({
+ keyframes,
+ anchorKeyframe,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ anchorKeyframe?: SelectedKeyframeRef;
+ }) => {
+ const keyframeIdsToRemove = new Set(
+ keyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
+ );
+ const nextKeyframes = selectedKeyframes.filter(
+ (selectedKeyframe) =>
+ !keyframeIdsToRemove.has(
+ getSelectedKeyframeId({ keyframe: selectedKeyframe }),
+ ),
+ );
+ editor.selection.setSelectedKeyframes({
+ keyframes: nextKeyframes,
+ anchorKeyframe:
+ anchorKeyframe ?? nextKeyframes[nextKeyframes.length - 1] ?? null,
+ });
+ },
+ [selectedKeyframes, editor],
+ );
+
+ const clearKeyframeSelection = useCallback(() => {
+ editor.selection.clearKeyframeSelection();
+ }, [editor]);
+
+ const toggleKeyframeSelection = useCallback(
+ ({
+ keyframes,
+ isMultiKey,
+ }: {
+ keyframes: SelectedKeyframeRef[];
+ isMultiKey: boolean;
+ }) => {
+ const anchorKeyframe = keyframes[0];
+ if (!isMultiKey) {
+ setKeyframeSelection({ keyframes, anchorKeyframe });
+ return;
+ }
+
+ const areAllKeyframesSelected = keyframes.every((keyframe) =>
+ isKeyframeSelected({ keyframe }),
+ );
+ if (areAllKeyframesSelected) {
+ removeKeyframesFromSelection({ keyframes, anchorKeyframe });
+ return;
+ }
+
+ addKeyframesToSelection({ keyframes, anchorKeyframe });
+ },
+ [
+ setKeyframeSelection,
+ isKeyframeSelected,
+ removeKeyframesFromSelection,
+ addKeyframesToSelection,
+ ],
+ );
+
+ const selectKeyframeRange = useCallback(
+ ({
+ orderedKeyframes,
+ targetKeyframes,
+ isAdditive,
+ }: {
+ orderedKeyframes: SelectedKeyframeRef[];
+ targetKeyframes: SelectedKeyframeRef[];
+ isAdditive: boolean;
+ }) => {
+ if (orderedKeyframes.length === 0 || targetKeyframes.length === 0) {
+ return;
+ }
+
+ const anchorKeyframe =
+ keyframeSelectionAnchor ??
+ selectedKeyframes[selectedKeyframes.length - 1] ??
+ targetKeyframes[0];
+ if (!anchorKeyframe) {
+ return;
+ }
+
+ const targetKeyframeIds = new Set(
+ targetKeyframes.map((keyframe) => getSelectedKeyframeId({ keyframe })),
+ );
+ const anchorId = getSelectedKeyframeId({ keyframe: anchorKeyframe });
+ const anchorIndex = orderedKeyframes.findIndex(
+ (keyframe) => getSelectedKeyframeId({ keyframe }) === anchorId,
+ );
+ if (anchorIndex === -1) {
+ if (isAdditive) {
+ addKeyframesToSelection({
+ keyframes: targetKeyframes,
+ anchorKeyframe,
+ });
+ return;
+ }
+ setKeyframeSelection({ keyframes: targetKeyframes, anchorKeyframe });
+ return;
+ }
+
+ const targetIndexes = orderedKeyframes
+ .map((keyframe, index) => ({
+ keyframeId: getSelectedKeyframeId({ keyframe }),
+ index,
+ }))
+ .filter(({ keyframeId }) => targetKeyframeIds.has(keyframeId))
+ .map(({ index }) => index);
+ if (targetIndexes.length === 0) {
+ return;
+ }
+
+ const rangeStart = Math.min(anchorIndex, ...targetIndexes);
+ const rangeEnd = Math.max(anchorIndex, ...targetIndexes);
+ const rangeKeyframes = orderedKeyframes.slice(rangeStart, rangeEnd + 1);
+
+ if (isAdditive) {
+ addKeyframesToSelection({ keyframes: rangeKeyframes, anchorKeyframe });
+ return;
+ }
+
+ setKeyframeSelection({ keyframes: rangeKeyframes, anchorKeyframe });
+ },
+ [
+ keyframeSelectionAnchor,
+ selectedKeyframes,
+ addKeyframesToSelection,
+ setKeyframeSelection,
+ ],
+ );
+
+ return {
+ selectedKeyframes,
+ keyframeSelectionAnchor,
+ isKeyframeSelected,
+ setKeyframeSelection,
+ addKeyframesToSelection,
+ removeKeyframesFromSelection,
+ clearKeyframeSelection,
+ toggleKeyframeSelection,
+ selectKeyframeRange,
+ };
+}
diff --git a/apps/web/src/hooks/timeline/use-snap-indicator-position.ts b/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
index d78eb04d..f3e63894 100644
--- a/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
+++ b/apps/web/src/hooks/timeline/use-snap-indicator-position.ts
@@ -1,5 +1,5 @@
import { useEffect, useState } from "react";
-import { TIMELINE_CONSTANTS } from "@/constants/timeline-constants";
+import { timelineTimeToSnappedPixels } from "@/lib/timeline";
import type { TimelineTrack } from "@/types/timeline";
interface UseSnapIndicatorPositionParams {
@@ -50,8 +50,10 @@ export function useSnapIndicatorPosition({
? trackLabelsRef.current.offsetWidth
: 0;
- const timelinePosition =
- (snapPoint?.time || 0) * TIMELINE_CONSTANTS.PIXELS_PER_SECOND * zoomLevel;
+ const timelinePosition = timelineTimeToSnappedPixels({
+ time: snapPoint?.time ?? 0,
+ zoomLevel,
+ });
const leftPosition = trackLabelsWidth + timelinePosition - scrollLeft;
return {