Skip to content

Commit

Permalink
Spring updates (#2912)
Browse files Browse the repository at this point in the history
* Latest

* Latest

* Latest

* Latest

* Latest
  • Loading branch information
mattgperry authored Nov 27, 2024
1 parent cc2ebee commit dabfdcb
Show file tree
Hide file tree
Showing 14 changed files with 344 additions and 45 deletions.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ Motion adheres to [Semantic Versioning](http://semver.org/).

Undocumented APIs should be considered internal and may change without warning.

## [11.12.0] 2024-11-27

### Added

- New `visualDuration` option for `spring` animations.
- New `spring(visualDuration, bounce)` syntax.

## [11.11.16] 2024-11-14

### Fixed
Expand Down
131 changes: 131 additions & 0 deletions dev/react/src/examples/Animation-spring-css.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { spring } from "framer-motion/dom"
import { motion } from "framer-motion"
import { useEffect, useState } from "react"

const height = 100
const width = 500
const margin = 10

export function SpringVisualiser({ transition }: any) {
const { duration, bounce } = {
duration: transition.duration * 1000,
bounce: transition.bounce,
}
const springResolver = spring({
bounce,
visualDuration: duration,
keyframes: [0, 1],
})

let curveLine = `M${margin} ${margin + height}`
let perceptualMarker = ""

const step = 10
for (let i = 0; i <= width; i++) {
const t = i * step

if (t > duration && perceptualMarker === "") {
perceptualMarker = `M${margin + i} ${margin} L${margin + i} ${
margin + height
}`
}

curveLine += `L${margin + i} ${
margin + (height - springResolver.next(t).value * (height / 2))
}`
}

return (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width + margin * 2}
height={height + margin * 2}
>
<path
d={curveLine}
fill="transparent"
strokeWidth="2"
stroke="#AAAAAA"
strokeLinecap="round"
strokeLinejoin="round"
></path>
<path
d={perceptualMarker}
fill="transparent"
strokeWidth="2"
stroke="#AAAAAA"
strokeLinecap="round"
strokeLinejoin="round"
></path>
</svg>
)
}

/**
* An example of the tween transition type
*/

const style = {
width: 100,
height: 100,
background: "white",
}
export const App = () => {
const [state, setState] = useState(false)

const [duration, setDuration] = useState(1)
const [bounce, setBounce] = useState(0.2)

useEffect(() => {
setTimeout(() => {
setState(true)
}, 300)
}, [state])

return (
<>
<div
style={{
...style,
transform: state ? "translateX(200px)" : "none",
transition: "transform " + spring(duration, bounce),
}}
/>
<motion.div
animate={{
transform: state ? "translateX(200px)" : "translateX(0)",
}}
transition={{
visualDuration: duration,
bounce,
type: "spring",
}}
style={style}
/>
<SpringVisualiser
transition={{
duration,
type: "spring",
bounce,
durationBasedSpring: true,
}}
/>
<input
type="range"
min="0"
max="1"
step="0.01"
value={bounce}
onChange={(e) => setBounce(Number(e.target.value))}
/>
<input
type="range"
min="0"
max="10"
step="0.1"
value={duration}
onChange={(e) => setDuration(Number(e.target.value))}
/>
</>
)
}
11 changes: 5 additions & 6 deletions packages/framer-motion/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,23 +108,23 @@
"bundlesize": [
{
"path": "./dist/size-rollup-motion.js",
"maxSize": "33.86 kB"
"maxSize": "34.15 kB"
},
{
"path": "./dist/size-rollup-m.js",
"maxSize": "5.71 kB"
},
{
"path": "./dist/size-rollup-dom-animation.js",
"maxSize": "17 kB"
"maxSize": "17.3 kB"
},
{
"path": "./dist/size-rollup-dom-max.js",
"maxSize": "29.1 kB"
"maxSize": "29.4 kB"
},
{
"path": "./dist/size-rollup-animate.js",
"maxSize": "17.92 kB"
"maxSize": "18.2 kB"
},
{
"path": "./dist/size-rollup-scroll.js",
Expand All @@ -134,6 +134,5 @@
"path": "./dist/size-rollup-waapi-animate.js",
"maxSize": "2.54 kB"
}
],
"gitHead": "eeb1cc452e2b468d838ec76fd501b131b383c5c9"
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class NativeAnimation implements AnimationPlaybackControls {

hydrateKeyframes(valueName, valueKeyframes, readInitialKeyframe)

// TODO: Replace this with toString()?
if (isGenerator(options.type)) {
const generatorOptions = createGeneratorEasing(
options,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
import { EasingFunction } from "../../../../easing/types"
import { progress } from "../../../../utils/progress"

// Create a linear easing point for every 10 ms
const resolution = 10

export const generateLinearEasing = (
easing: EasingFunction,
duration: number // as milliseconds
duration: number, // as milliseconds
resolution: number = 10 // as milliseconds
): string => {
let points = ""
const numPoints = Math.max(Math.round(duration / resolution), 2)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -166,3 +166,56 @@ describe("spring", () => {
expect(duration).toBe(600)
})
})

describe("visualDuration", () => {
test("returns correct duration", () => {
const generator = spring({ keyframes: [0, 1], visualDuration: 0.5 })

expect(calcGeneratorDuration(generator)).toBe(1100)
})

test("correctly resolves shorthand", () => {
expect(
spring({
keyframes: [0, 1],
visualDuration: 0.5,
bounce: 0.25,
}).toString()
).toEqual(spring(0.5, 0.25).toString())
})
})

describe("toString", () => {
test("returns correct string", () => {
const physicsSpring = spring({
keyframes: [0, 1],
stiffness: 100,
damping: 10,
mass: 1,
})

expect(physicsSpring.toString()).toBe(
"1100ms linear(0, 0.04194850778210579, 0.14932950126380995, 0.2963437796500159, 0.46082096429364294, 0.6250338421482647, 0.7759436260445078, 0.9050063854127511, 1.0076716369056948, 1.08269417017245, 1.1313632152119162, 1.1567322462156397, 1.162910735616452, 1.1544580838082632, 1.135901202975243, 1.1113817077446062, 1.0844267521809636, 1.0578292518205104, 1.0336182525290103, 1.0130980771054825, 0.9969350165150217, 0.9852721277431441, 0.9778555743946217, 0.9741593857917341, 0.9734990920012613, 0.9751280926296118, 0.9783136126561669, 0.9823915565398572, 0.9868014376321201, 0.9911038405316862, 0.9949836212722639, 0.9982423449158154, 1.000783397865038, 1.0025928919562468, 1.0037189926384433, 1.0042517362931451, 1)"
)

const durationSpring = spring({
keyframes: [0, 1],
duration: 800,
bounce: 0.25,
})

expect(durationSpring.toString()).toBe(
"800ms linear(0, 0.054177405016021196, 0.17972848064273883, 0.3343501584874773, 0.4905262924508025, 0.6320839235932971, 0.7510610411804188, 0.8450670152703789, 0.9151922015692906, 0.9644450242720048, 0.9966514779382684, 1.0157332114866835, 1.025277758976255, 1.0283215605911404, 1.02727842591652, 1.023959776584356, 1.0196463059772216, 1.015182448481659, 1.0110747373077722, 1.0075826495638103, 1.0047960480865514, 1.0026971231302306, 1.001207153877063, 1.0002197848978414, 0.999623144749416, 0.9993132705317652, 1)"
)

const visualDurationSpring = spring({
keyframes: [0, 1],
visualDuration: 0.5,
bounce: 0.25,
})

expect(visualDurationSpring.toString()).toBe(
"850ms linear(0, 0.04598659284844886, 0.1550978525021358, 0.293435416964683, 0.4378422541508299, 0.5736798672774968, 0.692748833850205, 0.7914734576133282, 0.8694017506567162, 0.9280251869268804, 0.9698937637734765, 0.9979863588386336, 1.0152902865362166, 1.024544165390638, 1.028102233067043, 1.027884258234232, 1.0253819114839424, 1.0216990275613216, 1.0176091156745415, 1.0136185020587367, 1.0100275420648654, 1.0069854535323504, 1.0045366006169996, 1.0026576306325918, 1.0012858767919226, 1.0003400208170226, 0.999734279313089, 1)"
)
})
})
28 changes: 28 additions & 0 deletions packages/framer-motion/src/animation/generators/spring/defaults.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
export const springDefaults = {
// Default spring physics
stiffness: 100,
damping: 10,
mass: 1.0,
velocity: 0.0,

// Default duration/bounce-based options
duration: 800, // in ms
bounce: 0.3,
visualDuration: 0.3, // in seconds

// Rest thresholds
restSpeed: {
granular: 0.01,
default: 2,
},
restDelta: {
granular: 0.005,
default: 0.5,
},

// Limits
minDuration: 0.01, // in seconds
maxDuration: 10.0, // in seconds
minDamping: 0.05,
maxDamping: 1,
}
31 changes: 18 additions & 13 deletions packages/framer-motion/src/animation/generators/spring/find.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
millisecondsToSeconds,
secondsToMilliseconds,
} from "../../../utils/time-conversion"
import { springDefaults } from "./defaults"

/**
* This is ported from the Framer implementation of duration-based spring resolution.
Expand All @@ -13,22 +14,18 @@ import {
type Resolver = (num: number) => number

const safeMin = 0.001
export const minDuration = 0.01
export const maxDuration = 10.0
export const minDamping = 0.05
export const maxDamping = 1

export function findSpring({
duration = 800,
bounce = 0.25,
velocity = 0,
mass = 1,
duration = springDefaults.duration,
bounce = springDefaults.bounce,
velocity = springDefaults.velocity,
mass = springDefaults.mass,
}: SpringOptions) {
let envelope: Resolver
let derivative: Resolver

warning(
duration <= secondsToMilliseconds(maxDuration),
duration <= secondsToMilliseconds(springDefaults.maxDuration),
"Spring duration must be 10 seconds or less"
)

Expand All @@ -37,8 +34,16 @@ export function findSpring({
/**
* Restrict dampingRatio and duration to within acceptable ranges.
*/
dampingRatio = clamp(minDamping, maxDamping, dampingRatio)
duration = clamp(minDuration, maxDuration, millisecondsToSeconds(duration))
dampingRatio = clamp(
springDefaults.minDamping,
springDefaults.maxDamping,
dampingRatio
)
duration = clamp(
springDefaults.minDuration,
springDefaults.maxDuration,
millisecondsToSeconds(duration)
)

if (dampingRatio < 1) {
/**
Expand Down Expand Up @@ -87,8 +92,8 @@ export function findSpring({
duration = secondsToMilliseconds(duration)
if (isNaN(undampedFreq)) {
return {
stiffness: 100,
damping: 10,
stiffness: springDefaults.stiffness,
damping: springDefaults.damping,
duration,
}
} else {
Expand Down
Loading

0 comments on commit dabfdcb

Please sign in to comment.