diff --git a/static/app/components/core/principles/motion/motion.mdx b/static/app/components/core/principles/motion/motion.mdx index bf048d30246314..5fa3b46da16558 100644 --- a/static/app/components/core/principles/motion/motion.mdx +++ b/static/app/components/core/principles/motion/motion.mdx @@ -83,12 +83,13 @@ function AnimatedComponent() { The easing curve of an animation drastically changes our perception of it. These easing tokens have been chosen to provide snappy, natural motion to interactions. -| **Name** | **Description** | **Value** | -| -------- | -------------------------------------------------------------------------- | --------------------------------- | -| `smooth` | similar to `ease-in-out`, natural acceleration and deceleration | `cubic-bezier(0.72, 0, 0.16, 1)` | -| `snap` | an expressive snap, with slight anticipation and overshoot before settling | `cubic-bezier(0.8, -0.4, 0.5, 1)` | -| `enter` | similar to `ease-out`, starts fast and slows into place smoothly | `cubic-bezier(0.24, 1, 0.32, 1)` | -| `exit` | similar to `ease-in`, starts slowly and accelerates away quickly | `cubic-bezier(0.64, 0, 0.8, 0)` | +| **Name** | **Description** | **Value** | +| -------- | -------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------- | +| `smooth` | similar to `ease-in-out`, natural acceleration and deceleration | `cubic-bezier(0.72, 0, 0.16, 1)` | +| `snap` | an expressive snap, with slight anticipation and overshoot before settling | `cubic-bezier(0.8, -0.4, 0.5, 1)` | +| `spring` | a bouncy spring with some overshoot | `linear(0, 0.4005, 0.8613, 1.0429, 1.0528, 1.0214, 1.0015, 0.9965, 0.9977, 0.9994, 1.0001, 1)` | +| `enter` | similar to `ease-out`, starts fast and slows into place smoothly | `cubic-bezier(0.24, 1, 0.32, 1)` | +| `exit` | similar to `ease-in`, starts slowly and accelerates away quickly | `cubic-bezier(0.64, 0, 0.8, 0)` | ## Duration diff --git a/static/app/components/slideOverPanel.tsx b/static/app/components/slideOverPanel.tsx index a146ab8c6e4655..6bb2e81a334001 100644 --- a/static/app/components/slideOverPanel.tsx +++ b/static/app/components/slideOverPanel.tsx @@ -1,6 +1,6 @@ import {useEffect} from 'react'; import isPropValid from '@emotion/is-prop-valid'; -import {css} from '@emotion/react'; +import {css, useTheme} from '@emotion/react'; import styled from '@emotion/styled'; import {motion, type Transition} from 'framer-motion'; @@ -49,6 +49,8 @@ function SlideOverPanel({ panelWidth, ref, }: SlideOverPanelProps) { + const theme = useTheme(); + useEffect(() => { if (!collapsed && onOpen) { onOpen(); @@ -69,9 +71,7 @@ function SlideOverPanel({ exit={collapsedStyle} slidePosition={slidePosition} transition={{ - type: 'spring', - stiffness: 1000, - damping: 50, + ...theme.motion.framer.spring.moderate, ...transitionProps, }} role="complementary" diff --git a/static/app/stories/playground/motion.tsx b/static/app/stories/playground/motion.tsx index 7303f5323f8d85..7111dc2f32086c 100644 --- a/static/app/stories/playground/motion.tsx +++ b/static/app/stories/playground/motion.tsx @@ -44,10 +44,12 @@ export function MotionPlayground() { ({ - value, - label: value, - }))} + options={(['smooth', 'snap', 'spring', 'enter', 'exit'] as const).map( + value => ({ + value, + label: value, + }) + )} value={easing} onChange={opt => setEasing(opt.value)} /> @@ -156,12 +158,14 @@ interface TargetStateOptions { const TARGET_OPACITY: TargetConfig = { smooth: {start: 1, end: 1}, snap: {start: 1, end: 1}, + spring: {start: 1, end: 1}, enter: {start: 0, end: 1}, exit: {start: 1, end: 0}, }; const TARGET_AXIS: TargetConfig = { smooth: {start: -16, end: 16}, snap: {start: -16, end: 16}, + spring: {start: -16, end: 16}, enter: {start: -16, end: 0}, exit: {start: 0, end: 16}, }; @@ -169,12 +173,14 @@ const TARGET_CONFIGS: Record = { rotate: { smooth: {start: 0, end: 90}, snap: {start: 0, end: 90}, + spring: {start: 0, end: 90}, enter: {start: -90, end: 0}, exit: {start: 0, end: 90}, }, scale: { smooth: {start: 1, end: 1.125}, snap: {start: 1, end: 1.125}, + spring: {start: 1, end: 1.125}, enter: {start: 1.125, end: 1}, exit: {start: 1, end: 0.8}, }, diff --git a/static/app/utils/theme/theme.tsx b/static/app/utils/theme/theme.tsx index 65ec7d6b86184b..4753e35ce24e52 100644 --- a/static/app/utils/theme/theme.tsx +++ b/static/app/utils/theme/theme.tsx @@ -10,7 +10,7 @@ import type {CSSProperties} from 'react'; import {css} from '@emotion/react'; import color from 'color'; -import type {Transition} from 'framer-motion'; +import {spring, type Transition} from 'framer-motion'; // palette generated via: https://gka.github.io/palettes/#colors=444674,69519A,E1567C,FB7D46,F2B712|steps=20|bez=1|coL=1 const CHART_PALETTE = [ @@ -253,7 +253,10 @@ const generateTokens = (colors: Colors) => ({ }, }); -type MotionName = 'smooth' | 'snap' | 'enter' | 'exit'; +type SimpleMotionName = 'smooth' | 'snap' | 'enter' | 'exit'; + +type PhysicsMotionName = 'spring'; + type MotionDuration = 'fast' | 'moderate' | 'slow'; type MotionDefinition = Record; @@ -264,14 +267,14 @@ const motionDurations: Record = { slow: 240, }; -const motionCurves: Record = { +const motionCurves: Record = { smooth: [0.72, 0, 0.16, 1], snap: [0.8, -0.4, 0.5, 1], enter: [0.24, 1, 0.32, 1], exit: [0.64, 0, 0.8, 0], }; -const withDuration = ( +const motionCurveWithDuration = ( durations: Record, easing: [number, number, number, number] ): [MotionDefinition, Record] => { @@ -299,22 +302,80 @@ const withDuration = ( return [motion, framerMotion]; }; +const motionTransitions: Record> = { + spring: { + fast: { + type: 'spring', + stiffness: 1400, + damping: 50, + }, + moderate: { + type: 'spring', + stiffness: 1000, + damping: 50, + }, + slow: { + type: 'spring', + stiffness: 600, + damping: 50, + }, + }, +}; + +const motionTransitionWithDuration = ( + transitionDefinitions: Record +): [MotionDefinition, Record] => { + const motion = { + fast: `${spring({ + keyframes: [0, 1], + ...transitionDefinitions.fast, + })}`, + moderate: `${spring({ + keyframes: [0, 1], + ...transitionDefinitions.moderate, + })}`, + slow: `${spring({ + keyframes: [0, 1], + ...transitionDefinitions.slow, + })}`, + }; + + return [motion, transitionDefinitions]; +}; + function generateMotion() { - const [smoothMotion, smoothFramer] = withDuration(motionDurations, motionCurves.smooth); - const [snapMotion, snapFramer] = withDuration(motionDurations, motionCurves.snap); - const [enterMotion, enterFramer] = withDuration(motionDurations, motionCurves.enter); - const [exitMotion, exitFramer] = withDuration(motionDurations, motionCurves.exit); + const [smoothMotion, smoothFramer] = motionCurveWithDuration( + motionDurations, + motionCurves.smooth + ); + const [snapMotion, snapFramer] = motionCurveWithDuration( + motionDurations, + motionCurves.snap + ); + const [enterMotion, enterFramer] = motionCurveWithDuration( + motionDurations, + motionCurves.enter + ); + const [exitMotion, exitFramer] = motionCurveWithDuration( + motionDurations, + motionCurves.exit + ); + const [springMotion, springFramer] = motionTransitionWithDuration( + motionTransitions.spring + ); return { smooth: smoothMotion, snap: snapMotion, enter: enterMotion, exit: exitMotion, + spring: springMotion, framer: { smooth: smoothFramer, snap: snapFramer, enter: enterFramer, exit: exitFramer, + spring: springFramer, }, }; }