summaryrefslogtreecommitdiff
path: root/apps/web/src/components/editor/panels/timeline/graph-editor
diff options
context:
space:
mode:
Diffstat (limited to 'apps/web/src/components/editor/panels/timeline/graph-editor')
-rw-r--r--apps/web/src/components/editor/panels/timeline/graph-editor/session.ts30
-rw-r--r--apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts34
2 files changed, 42 insertions, 22 deletions
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts
index 15657bfb..663b4b5a 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts
+++ b/apps/web/src/components/editor/panels/timeline/graph-editor/session.ts
@@ -1,6 +1,7 @@
import {
getCurveHandlesForNormalizedCubicBezier,
getEditableScalarChannels,
+ getEasingModeForKind,
getNormalizedCubicBezierForScalarSegment,
getScalarKeyframeContext,
updateScalarKeyframeCurve,
@@ -54,7 +55,14 @@ export interface GraphEditorReadyState extends GraphEditorBaseSelectionState {
propertyPath: SelectedKeyframeRef["propertyPath"];
keyframeId: string;
element: TimelineElement;
+ /** Primary channel context, used for displaying the curve. */
context: ScalarGraphKeyframeContext;
+ /**
+ * All channel contexts that share this curve. For independent-easing bindings
+ * this is [context]. For shared-easing bindings (e.g. color) this contains
+ * all component contexts so patches can be applied to every channel at once.
+ */
+ allContexts: ScalarGraphKeyframeContext[];
cubicBezier: NormalizedCubicBezier;
}
@@ -234,16 +242,17 @@ export function resolveGraphEditorSelectionState({
});
}
- const scalarChannels = getEditableScalarChannels({
+ const scalarResult = getEditableScalarChannels({
animations: selectedElement.element.animations,
propertyPath: primaryKeyframe.propertyPath,
});
- if (scalarChannels.length === 0) {
+ if (!scalarResult || scalarResult.channels.length === 0) {
return createUnavailableState({
reason: "selected-keyframe-has-no-scalar-channel",
message: "The selected keyframe has no editable graph channel.",
});
}
+ const { binding: resolvedBinding, channels: scalarChannels } = scalarResult;
// When 2 keyframes are selected, resolve the earlier one as the outgoing-segment
// anchor so the graph editor edits the curve between the two selected keyframes.
@@ -264,6 +273,8 @@ export function resolveGraphEditorSelectionState({
}
}
+ const easingMode = getEasingModeForKind(resolvedBinding.kind);
+
const contexts = scalarChannels.flatMap((channel) => {
const context = getScalarKeyframeContext({
animations: selectedElement.element.animations,
@@ -293,14 +304,20 @@ export function resolveGraphEditorSelectionState({
});
}
- const nextSegmentContexts = contexts.filter(
+ // For shared-easing bindings (e.g. color), all components always use the same
+ // curve. Collapse to a single option so no per-component tabs are shown.
+ const visibleContexts =
+ easingMode === "shared" ? [contexts[0]] : contexts;
+ const allContexts = contexts.map(({ context }) => context);
+
+ const nextSegmentContexts = visibleContexts.filter(
({ context }) => context.nextKey !== null,
);
const preferredContext =
- contexts.find(({ option }) => option.key === preferredComponentKey) ?? null;
+ visibleContexts.find(({ option }) => option.key === preferredComponentKey) ?? null;
const activeContext =
- preferredContext ?? nextSegmentContexts[0] ?? contexts[0];
- const componentOptions = contexts.map(({ option }) => option);
+ preferredContext ?? nextSegmentContexts[0] ?? visibleContexts[0];
+ const componentOptions = visibleContexts.map(({ option }) => option);
if (!activeContext.context.nextKey) {
return createUnavailableState({
@@ -356,6 +373,7 @@ export function resolveGraphEditorSelectionState({
keyframeId: resolvedKeyframeId,
element: selectedElement.element,
context: activeContext.context,
+ allContexts,
cubicBezier,
};
}
diff --git a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts b/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts
index d0303b6f..8baa3c43 100644
--- a/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts
+++ b/apps/web/src/components/editor/panels/timeline/graph-editor/use-controller.ts
@@ -96,19 +96,17 @@ export function useGraphEditorController() {
return;
}
- const nextAnimations = applyGraphEditorCurvePreview({
- animations: state.element.animations,
- context: state.context,
- cubicBezier: nextValue,
- });
+ const nextAnimations = state.allContexts.reduce(
+ (animations, context) =>
+ applyGraphEditorCurvePreview({ animations, context, cubicBezier: nextValue }),
+ state.element.animations,
+ );
editor.timeline.previewElements({
updates: [
{
trackId: state.trackId,
elementId: state.elementId,
- updates: {
- animations: nextAnimations,
- },
+ updates: { animations: nextAnimations },
},
],
});
@@ -123,6 +121,8 @@ export function useGraphEditorController() {
return;
}
+ // Build patches from the primary context (all shared-easing channels have
+ // the same keyframe IDs, so the same patches apply to each).
const patches = buildGraphEditorCurvePatches({
context: state.context,
cubicBezier: nextValue,
@@ -132,14 +132,16 @@ export function useGraphEditorController() {
}
editor.timeline.updateKeyframeCurves({
- keyframes: patches.map(({ keyframeId, patch }) => ({
- trackId: state.trackId,
- elementId: state.elementId,
- propertyPath: state.propertyPath,
- componentKey: state.context.componentKey,
- keyframeId,
- patch,
- })),
+ keyframes: state.allContexts.flatMap((context) =>
+ patches.map(({ keyframeId, patch }) => ({
+ trackId: state.trackId,
+ elementId: state.elementId,
+ propertyPath: state.propertyPath,
+ componentKey: context.componentKey,
+ keyframeId,
+ patch,
+ })),
+ ),
});
hasPreviewRef.current = false;
},