Skip to content

Commit

Permalink
Merge pull request #2045 from framer/feature/duration
Browse files Browse the repository at this point in the history
Adding `.duration` to `animate()`
  • Loading branch information
mergetron[bot] authored Mar 29, 2023
2 parents 55b2be9 + db20c0f commit 22501f9
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 36 deletions.
4 changes: 2 additions & 2 deletions packages/framer-motion-3d/src/render/__tests__/index.test.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { ResolvedValues } from "framer-motion"
import { ResolvedValues, sync } from "framer-motion"
import * as React from "react"
import { useEffect, useRef } from "react"
import { Euler, Vector3 } from "three"
Expand Down Expand Up @@ -240,7 +240,7 @@ describe("motion for three", () => {
}

ReactThreeTestRenderer.create(<Component />)
setTimeout(() => resolve([x.get(), y.get()]), 5)
sync.update(() => resolve([x.get(), y.get()]))
})

expect(result[0]).toEqual(100)
Expand Down
10 changes: 9 additions & 1 deletion packages/framer-motion/src/animation/GroupPlaybackControls.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { AnimationPlaybackControls } from "./types"

type PropNames = "time" | "speed"
type PropNames = "time" | "speed" | "duration"

export class GroupPlaybackControls implements AnimationPlaybackControls {
animations: AnimationPlaybackControls[]
Expand Down Expand Up @@ -44,6 +44,14 @@ export class GroupPlaybackControls implements AnimationPlaybackControls {
this.setAll("speed", speed)
}

get duration() {
let max = 0
for (let i = 0; i < this.animations.length; i++) {
max = Math.max(max, this.animations[i].duration)
}
return max
}

private runAll(
methodName: keyof Omit<AnimationPlaybackControls, PropNames | "then">
) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ function createTestAnimationControls(
return {
time: 1,
speed: 1,
duration: 10,
stop: () => {},
play: () => {},
pause: () => {},
Expand Down Expand Up @@ -193,4 +194,26 @@ describe("GroupPlaybackControls", () => {
expect(a.speed).toEqual(-1)
expect(b.speed).toEqual(-1)
})

test("Gets max duration", async () => {
const a = createTestAnimationControls({
duration: 3,
})
const b = createTestAnimationControls({
duration: 2,
})
const c = createTestAnimationControls({
duration: 1,
})

const controls = new GroupPlaybackControls([a, b, c])

expect(controls.duration).toEqual(3)
})

test("Returns zero for no animations", async () => {
const controls = new GroupPlaybackControls([])

expect(controls.duration).toEqual(0)
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,15 @@ describe("animate() with WAAPI", () => {
)
})

test("Returns duration correctly", async () => {
const animation = animate(
document.createElement("div"),
{ opacity: 1 },
{ duration: 2, opacity: { duration: 3 } }
)
expect(animation.duration).toEqual(3)
})

test("Can accept timeline sequences", async () => {
const a = document.createElement("div")
const b = document.createElement("div")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,18 @@ describe("instantAnimation", () => {
expect(onUpdate).toBeCalledWith(1)
expect(onComplete).toBeCalled()
})

test("Returns duration: 0", async () => {
const animation = createInstantAnimation({
delay: 0,
keyframes: [0, 1],
})
expect(animation.duration).toEqual(0)

const animationWithDelay = createInstantAnimation({
delay: 0.2,
keyframes: [0, 1],
})
expect(animationWithDelay.duration).toEqual(0)
})
})
8 changes: 5 additions & 3 deletions packages/framer-motion/src/animation/animators/instant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { noop } from "../../utils/noop"

export function createInstantAnimation<V>({
keyframes,
delay: delayBy,
delay,
onUpdate,
onComplete,
}: ValueAnimationOptions<V>): AnimationPlaybackControls {
Expand All @@ -22,6 +22,7 @@ export function createInstantAnimation<V>({
return {
time: 0,
speed: 1,
duration: 0,
play: noop<void>,
pause: noop<void>,
stop: noop<void>,
Expand All @@ -34,10 +35,11 @@ export function createInstantAnimation<V>({
}
}

return delayBy
return delay
? animateValue({
keyframes: [0, 1],
duration: delayBy,
duration: 0,
delay,
onComplete: setValue,
})
: setValue()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1242,4 +1242,56 @@ describe("animate", () => {

await animation
})

test("Correctly returns duration", async () => {
expect(
animateValue({
keyframes: [0, 100],
duration: 1000,
}).duration
).toEqual(1)
})

test("Correctly returns duration when delay is defined", () => {
expect(
animateValue({
keyframes: [0, 100],
delay: 1000,
duration: 1000,
}).duration
).toEqual(1)
})

test("Correctly returns duration when repeat is defined", () => {
expect(
animateValue({
keyframes: [0, 100],
delay: 1000,
duration: 1000,
repeat: Infinity,
}).duration
).toEqual(1)
})

test("Correctly returns duration when animation is spring", () => {
expect(
animateValue({
keyframes: [0, 100],
delay: 1000,
duration: 1000,
type: "spring",
}).duration
).toEqual(1)
})

test("Correctly returns duration when animation is dynamic spring", () => {
expect(
animateValue({
keyframes: [0, 100],
stiffness: 200,
damping: 10,
type: "spring",
}).duration
).toEqual(1.2)
})
})
52 changes: 35 additions & 17 deletions packages/framer-motion/src/animation/animators/js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,8 @@ function calculateDuration(generator: KeyframeGenerator<unknown>) {
duration += timeStep
state = generator.next(duration)
}
return duration

return duration >= maxDuration ? Infinity : duration
}

export interface MainThreadAnimationControls<V>
Expand Down Expand Up @@ -147,28 +148,30 @@ export function animateValue<V = number>({
totalDuration = resolvedDuration * (repeat + 1) - repeatDelay
}

let time = 0
let currentTime = 0
const tick = (timestamp: number) => {
if (startTime === null) return

if (holdTime !== null) {
time = holdTime
currentTime = holdTime
} else {
time = (timestamp - startTime) * speed
currentTime = (timestamp - startTime) * speed
}

// Rebase on delay
time = Math.max(time - delay, 0)
const timeWithoutDelay = currentTime - delay
const isInDelayPhase = timeWithoutDelay < 0
currentTime = Math.max(timeWithoutDelay, 0)

/**
* If this animation has finished, set the current time
* to the total duration.
*/
if (playState === "finished" && holdTime === null) {
time = totalDuration
currentTime = totalDuration
}

let elapsed = time
let elapsed = currentTime

let frameGenerator = generator

Expand All @@ -178,7 +181,7 @@ export function animateValue<V = number>({
* than duration we'll get values like 2.5 (midway through the
* third iteration)
*/
const progress = time / resolvedDuration
const progress = currentTime / resolvedDuration

/**
* Get the current iteration (0 indexed). For instance the floor of
Expand Down Expand Up @@ -222,30 +225,37 @@ export function animateValue<V = number>({

let p = clamp(0, 1, iterationProgress)

if (time > totalDuration) {
if (currentTime > totalDuration) {
p = repeatType === "reverse" && iterationIsOdd ? 1 : 0
}

elapsed = p * resolvedDuration
}

const state = frameGenerator.next(elapsed)
/**
* If we're in negative time, set state as the initial keyframe.
* This prevents delay: x, duration: 0 animations from finishing
* instantly.
*/
const state = isInDelayPhase
? { done: false, value: keyframes[0] }
: frameGenerator.next(elapsed)

if (mapNumbersToKeyframes) {
state.value = mapNumbersToKeyframes(state.value)
}

let { done } = state

if (calculatedDuration !== null) {
done = time >= totalDuration
if (!isInDelayPhase && calculatedDuration !== null) {
done = currentTime >= totalDuration
}

const isAnimationFinished =
holdTime === null &&
(playState === "finished" ||
(playState === "running" && done) ||
(speed < 0 && time <= 0))
(speed < 0 && currentTime <= 0))

if (onUpdate) {
onUpdate(state.value)
Expand Down Expand Up @@ -311,33 +321,41 @@ export function animateValue<V = number>({
return currentFinishedPromise.then(resolve, reject)
},
get time() {
return millisecondsToSeconds(time)
return millisecondsToSeconds(currentTime)
},
set time(newTime: number) {
newTime = secondsToMilliseconds(newTime)

time = newTime
currentTime = newTime
if (holdTime !== null || !animationDriver || speed === 0) {
holdTime = newTime
} else {
startTime = animationDriver.now() - newTime / speed
}
},
get duration() {
const duration =
generator.calculatedDuration === null
? calculateDuration(generator)
: generator.calculatedDuration

return millisecondsToSeconds(duration)
},
get speed() {
return speed
},
set speed(newSpeed: number) {
if (newSpeed === speed || !animationDriver) return
speed = newSpeed
controls.time = millisecondsToSeconds(time)
controls.time = millisecondsToSeconds(currentTime)
},
get state() {
return playState
},
play,
pause: () => {
playState = "paused"
holdTime = time
holdTime = currentTime
},
stop: () => {
hasStopped = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,9 @@ export function createAcceleratedAnimation(
set speed(newSpeed: number) {
animation.playbackRate = newSpeed
},
get duration() {
return millisecondsToSeconds(duration)
},
play: () => {
if (hasStopped) return
animation.play()
Expand Down
26 changes: 13 additions & 13 deletions packages/framer-motion/src/animation/interfaces/motion-value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,19 +73,6 @@ export const animateMotionValue = (
},
}

if (
!isOriginAnimatable ||
!isTargetAnimatable ||
instantAnimationState.current ||
valueTransition.type === false
) {
/**
* If we can't animate this value, or the global instant animation flag is set,
* or this is simply defined as an instant transition, return an instant transition.
*/
return createInstantAnimation(options)
}

/**
* If there's no transition defined for this value, we can generate
* unqiue transition settings for this value.
Expand All @@ -110,6 +97,19 @@ export const animateMotionValue = (
options.repeatDelay = secondsToMilliseconds(options.repeatDelay)
}

if (
!isOriginAnimatable ||
!isTargetAnimatable ||
instantAnimationState.current ||
valueTransition.type === false
) {
/**
* If we can't animate this value, or the global instant animation flag is set,
* or this is simply defined as an instant transition, return an instant transition.
*/
return createInstantAnimation(options)
}

/**
* Animate via WAAPI if possible.
*/
Expand Down
Loading

0 comments on commit 22501f9

Please sign in to comment.