Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 25 additions & 1 deletion packages/studio/src/components/editor/AnimationCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { memo, useCallback, useMemo, useState } from "react";
import { memo, useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { SUPPORTED_EASES, SUPPORTED_PROPS } from "@hyperframes/core/gsap-constants";
import { trackStudioSegmentEaseEdit } from "../../telemetry/events";
Expand All @@ -23,13 +23,17 @@ interface AnimationCardProps extends GsapAnimationEditCallbacks {
animation: GsapAnimation;
defaultExpanded: boolean;
flat?: boolean;
focusedSegment?: { tweenPercentage: number } | null;
onFocusSegmentConsumed?: () => void;
}

// fallow-ignore-next-line complexity
export const AnimationCard = memo(function AnimationCard({
animation,
defaultExpanded,
flat,
focusedSegment,
onFocusSegmentConsumed,
onUpdateProperty,
onUpdateMeta,
onDeleteAnimation,
Expand All @@ -50,6 +54,25 @@ export const AnimationCard = memo(function AnimationCard({
const [addingProp, setAddingProp] = useState(false);
const [addingFromProp, setAddingFromProp] = useState(false);
const [expandedKfPct, setExpandedKfPct] = useState<number | null>(null);
const cardRef = useRef<HTMLDivElement>(null);
const pendingAutoScrollRef = useRef(false);

useEffect(() => {
if (!focusedSegment) return;
setExpanded(true);
pendingAutoScrollRef.current = true;
setExpandedKfPct(focusedSegment.tweenPercentage);
onFocusSegmentConsumed?.();
}, [focusedSegment, onFocusSegmentConsumed]);

useEffect(() => {
if (!pendingAutoScrollRef.current || expandedKfPct === null) return;
const segment = cardRef.current?.querySelector<HTMLElement>(
`[data-ease-segment-pct="${expandedKfPct}"]`,
);
segment?.scrollIntoView({ block: "nearest", behavior: "smooth" });
pendingAutoScrollRef.current = false;
}, [expandedKfPct]);

const usedProps = useMemo(
() => new Set(Object.keys(animation.properties)),
Expand Down Expand Up @@ -154,6 +177,7 @@ export const AnimationCard = memo(function AnimationCard({

return (
<div
ref={cardRef}
data-flat-effect-card={flat ? "true" : undefined}
className={
flat
Expand Down
68 changes: 68 additions & 0 deletions packages/studio/src/components/editor/GsapAddAnimationControl.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";

const STYLES = {
classic: {
method:
"rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white",
cancel: "px-1.5 text-[11px] text-neutral-500 hover:text-neutral-300",
trigger: "text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200",
},
flat: {
method:
"rounded-lg border border-panel-border-input bg-panel-input px-2.5 py-1.5 text-[11px] font-medium text-panel-text-2 transition-colors hover:border-panel-text-4 hover:text-panel-text-0",
cancel: "px-1.5 text-[11px] text-panel-text-3 hover:text-panel-text-1",
trigger: "text-[11px] font-medium text-panel-text-3 transition-colors hover:text-panel-text-1",
},
};

export function GsapAddAnimationControl({
open,
setOpen,
onAddAnimation,
track,
variant,
}: {
open: boolean;
setOpen: (open: boolean) => void;
onAddAnimation: (method: "to" | "from" | "set" | "fromTo") => void;
track: (control: string, name: string) => void;
variant: keyof typeof STYLES;
}) {
const styles = STYLES[variant];

return (
<div className="relative pt-1">
{open ? (
<div className="flex gap-1.5">
{ADD_METHODS.map((method) => (
<button
key={method}
type="button"
title={METHOD_TOOLTIPS[method]}
onClick={() => {
track("button", `Add ${method} animation`);
onAddAnimation(method);
setOpen(false);
}}
className={styles.method}
>
{ADD_METHOD_LABELS[method] ?? method}
</button>
))}
<button type="button" onClick={() => setOpen(false)} className={styles.cancel}>
Cancel
</button>
</div>
) : (
<button
type="button"
onClick={() => setOpen(true)}
className={styles.trigger}
title="Add a new animation effect to this element"
>
+ Add effect
</button>
)}
</div>
);
}
175 changes: 18 additions & 157 deletions packages/studio/src/components/editor/GsapAnimationSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import { memo, useState } from "react";
import type { GsapAnimation } from "@hyperframes/core/gsap-parser";
import { Film } from "../../icons/SystemIcons";
import { Section } from "./propertyPanelPrimitives";
import { ADD_METHODS, ADD_METHOD_LABELS, METHOD_TOOLTIPS } from "./gsapAnimationConstants";
import { AnimationCard } from "./AnimationCard";
import {
trackAnimationMetaUpdate,
type GsapAnimationEditCallbacks,
withTrackedGsapAnimationCallbacks,
} from "./gsapAnimationCallbacks";
import { useTrackDesignInput } from "../../contexts/DesignPanelInputContext";
import { usePlayerStore } from "../../player";
import { GsapAddAnimationControl } from "./GsapAddAnimationControl";

interface GsapAnimationSectionProps extends GsapAnimationEditCallbacks {
animations: GsapAnimation[];
Expand All @@ -21,41 +22,14 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
animations,
multipleTimelines,
unsupportedTimelinePattern,
onUpdateProperty,
onUpdateMeta,
onDeleteAnimation,
onAddProperty,
onRemoveProperty,
onUpdateFromProperty,
onAddFromProperty,
onRemoveFromProperty,
onAddAnimation,
onLivePreview,
onLivePreviewEnd,
onSetArcPath,
onUpdateArcSegment,
onUpdateKeyframeEase,
onSetAllKeyframeEases,
onUnroll,
...callbacks
}: GsapAnimationSectionProps) {
const track = useTrackDesignInput();
const [addMenuOpen, setAddMenuOpen] = useState(false);
const trackProperty = (property: string) => {
const control =
property === "visibility"
? "toggle"
: property === "filter" || property === "clipPath"
? "text"
: "metric";
track(control, property);
};
const updateMeta = (
animationId: string,
updates: { duration?: number; ease?: string; position?: number },
) => {
trackAnimationMetaUpdate(track, updates);
onUpdateMeta(animationId, updates);
};
const trackedCallbacks = withTrackedGsapAnimationCallbacks(callbacks, track);
const focusedEaseSegment = usePlayerStore((s) => s.focusedEaseSegment);
const setFocusedEaseSegment = usePlayerStore((s) => s.setFocusedEaseSegment);

return (
<Section title="Animation" icon={<Film size={15} />}>
Expand All @@ -76,137 +50,24 @@ export const GsapAnimationSection = memo(function GsapAnimationSection({
<div className="space-y-2">
{animations.map((anim, index) => (
<AnimationCard
{...trackedCallbacks}
key={anim.id}
animation={anim}
defaultExpanded={index === 0}
onUpdateProperty={(animationId, property, value) => {
trackProperty(property);
onUpdateProperty(animationId, property, value);
}}
onUpdateMeta={updateMeta}
onDeleteAnimation={(animationId) => {
track("button", "Remove animation");
onDeleteAnimation(animationId);
}}
onAddProperty={(animationId, property) => {
track("select", "Add effect property");
onAddProperty(animationId, property);
}}
onRemoveProperty={(animationId, property) => {
track("button", `Remove ${property}`);
onRemoveProperty(animationId, property);
}}
onUpdateFromProperty={
onUpdateFromProperty
? (animationId, property, value) => {
trackProperty(property);
onUpdateFromProperty(animationId, property, value);
}
: undefined
}
onAddFromProperty={
onAddFromProperty
? (animationId, property) => {
track("select", "Add from property");
onAddFromProperty(animationId, property);
}
: undefined
}
onRemoveFromProperty={
onRemoveFromProperty
? (animationId, property) => {
track("button", `Remove from ${property}`);
onRemoveFromProperty(animationId, property);
}
: undefined
}
onLivePreview={onLivePreview}
onLivePreviewEnd={onLivePreviewEnd}
onSetArcPath={
onSetArcPath
? (animationId, config) => {
track(
"toggle",
config.autoRotate !== undefined ? "Auto rotate" : "Arc motion",
);
onSetArcPath(animationId, config);
}
: undefined
}
onUpdateArcSegment={
onUpdateArcSegment
? (animationId, segmentIndex, update) => {
if (update.curviness === undefined) {
track("button", `Reset arc segment ${segmentIndex + 1}`);
}
onUpdateArcSegment(animationId, segmentIndex, update);
}
: undefined
}
onUpdateKeyframeEase={
onUpdateKeyframeEase
? (animationId, percentage, ease) => {
track("select", "Keyframe ease");
onUpdateKeyframeEase(animationId, percentage, ease);
}
: undefined
}
onSetAllKeyframeEases={
onSetAllKeyframeEases
? (animationId, ease) => {
track("select", "All keyframe eases");
onSetAllKeyframeEases(animationId, ease);
}
: undefined
}
onUnroll={
onUnroll
? (animationId) => {
track("button", "Unroll animation");
onUnroll(animationId);
}
: undefined
focusedSegment={
focusedEaseSegment?.animationId === anim.id ? focusedEaseSegment : null
}
onFocusSegmentConsumed={() => setFocusedEaseSegment(null)}
/>
))}

<div className="relative pt-1">
{addMenuOpen ? (
<div className="flex gap-1.5">
{ADD_METHODS.map((method) => (
<button
key={method}
type="button"
title={METHOD_TOOLTIPS[method]}
onClick={() => {
track("button", `Add ${method} animation`);
onAddAnimation(method);
setAddMenuOpen(false);
}}
className="rounded-lg border border-neutral-700 bg-neutral-900 px-2.5 py-1.5 text-[11px] font-medium text-neutral-300 transition-colors hover:border-neutral-600 hover:text-white"
>
{ADD_METHOD_LABELS[method] ?? method}
</button>
))}
<button
type="button"
onClick={() => setAddMenuOpen(false)}
className="px-1.5 text-[11px] text-neutral-500 hover:text-neutral-300"
>
Cancel
</button>
</div>
) : (
<button
type="button"
onClick={() => setAddMenuOpen(true)}
className="text-[11px] font-medium text-neutral-400 transition-colors hover:text-neutral-200"
title="Add a new animation effect to this element"
>
+ Add effect
</button>
)}
</div>
<GsapAddAnimationControl
open={addMenuOpen}
setOpen={setAddMenuOpen}
onAddAnimation={onAddAnimation}
track={track}
variant="classic"
/>
</div>
)}
</Section>
Expand Down
Loading
Loading