Skip to content

Commit

Permalink
Add support for repeat in timeline sequence (#2945)
Browse files Browse the repository at this point in the history
* Adding tests

* Destructuring repeat options

* Latest

* UPdating tests
  • Loading branch information
mattgperry authored Dec 13, 2024
1 parent 1108bd5 commit 0e54f81
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -615,4 +615,103 @@ describe("createAnimationsFromSequence", () => {
expect(typeof (ease as Easing[])[2]).toEqual("function")
expect(times).toEqual([0, 0.45454545454545453, 0.45454545454545453, 1])
})

test("It correctly repeats keyframes once", () => {
const animations = createAnimationsFromSequence(
[[a, { x: [0, 100] }, { duration: 1, repeat: 1 }]],
undefined,
undefined,
{ spring }
)

expect(animations.get(a)!.keyframes.x).toEqual([0, 100, 0, 100])
const { duration, times } = animations.get(a)!.transition.x
expect(duration).toEqual(2)
expect(times).toEqual([0, 0.5, 0.5, 1])
})

test("Repeating a segment correctly places the next segment at the end", () => {
const animations = createAnimationsFromSequence(
[
[a, { x: [0, 100] }, { duration: 1, repeat: 1 }],
[a, { y: [0, 100] }, { duration: 2 }],
],
undefined,
undefined,
{ spring }
)

const { keyframes, transition } = animations.get(a)!
expect(keyframes.x).toEqual([0, 100, 0, 100, null])
expect(keyframes.y).toEqual([0, 0, 100])

expect(transition.x.duration).toEqual(4)
expect(transition.x.times).toEqual([0, 0.25, 0.25, 0.5, 1])
expect(transition.y.duration).toEqual(4)
expect(transition.y.times).toEqual([0, 0.5, 1])
})

test.skip("It correctly adds repeatDelay between repeated keyframes", () => {
const animations = createAnimationsFromSequence(
[
[
a,
{ x: [0, 100] },
{ duration: 1, repeat: 1, repeatDelay: 0.5 },
],
],
undefined,
undefined,
{ spring }
)

expect(animations.get(a)!.keyframes.x).toEqual([0, 100, 100, 0, 100])
const { duration, times } = animations.get(a)!.transition.x
expect(duration).toEqual(2.5)
expect(times).toEqual([0, 0.4, 0.6, 0.6, 1])
})

test.skip("It correctly mirrors repeated keyframes", () => {
const animations = createAnimationsFromSequence(
[
[
a,
{ x: [0, 100] },
{ duration: 1, repeat: 3, repeatType: "mirror" },
],
],
undefined,
undefined,
{ spring }
)

expect(animations.get(a)!.keyframes.x).toEqual([
0, 100, 100, 0, 0, 100, 100, 0,
])
const { duration, times } = animations.get(a)!.transition.x
expect(duration).toEqual(4)
expect(times).toEqual([0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1])
})

test.skip("It correctly reverses repeated keyframes", () => {
const animations = createAnimationsFromSequence(
[
[
a,
{ x: [0, 100] },
{ duration: 1, repeat: 3, repeatType: "reverse" },
],
],
undefined,
undefined,
{ spring }
)

expect(animations.get(a)!.keyframes.x).toEqual([
0, 100, 100, 0, 0, 100, 100, 0,
])
const { duration, times } = animations.get(a)!.transition.x
expect(duration).toEqual(4)
expect(times).toEqual([0, 0.25, 0.25, 0.5, 0.5, 0.75, 0.75, 1])
})
})
46 changes: 45 additions & 1 deletion packages/framer-motion/src/animation/sequence/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,9 +26,14 @@ import {
import { calcNextTime } from "./utils/calc-time"
import { addKeyframes } from "./utils/edit"
import { compareByTime } from "./utils/sort"
import { invariant } from "motion-utils"
import { normalizeTimes } from "./utils/normalize-times"
import { calculateRepeatDuration } from "./utils/calc-repeat-duration"

const defaultSegmentEasing = "easeInOut"

const MAX_REPEAT = 20

export function createAnimationsFromSequence(
sequence: AnimationSequence,
{ defaultTransition = {}, ...sequenceTransition }: SequenceOptions = {},
Expand Down Expand Up @@ -100,6 +105,9 @@ export function createAnimationsFromSequence(
delay = 0,
times = defaultOffset(valueKeyframesAsList),
type = "keyframes",
repeat,
repeatType,
repeatDelay = 0,
...remainingTransition
} = valueTransition
let { ease = defaultTransition.ease || "easeOut", duration } =
Expand Down Expand Up @@ -156,7 +164,6 @@ export function createAnimationsFromSequence(
duration ??= defaultDuration

const startTime = currentTime + calculatedDelay
const targetTime = startTime + duration

/**
* If there's only one time offset of 0, fill in a second with length 1
Expand All @@ -179,6 +186,43 @@ export function createAnimationsFromSequence(
valueKeyframesAsList.length === 1 &&
valueKeyframesAsList.unshift(null)

/**
* Handle repeat options
*/
if (repeat) {
invariant(
repeat < MAX_REPEAT,
"Repeat count too high, must be less than 20"
)

duration = calculateRepeatDuration(
duration,
repeat,
repeatDelay
)

const originalKeyframes = [...valueKeyframesAsList]
const originalTimes = [...times]

for (let repeatIndex = 0; repeatIndex < repeat; repeatIndex++) {
valueKeyframesAsList.push(...originalKeyframes)

for (
let keyframeIndex = 0;
keyframeIndex < originalTimes.length;
keyframeIndex++
) {
times.push(
originalTimes[keyframeIndex] + (repeatIndex + 1)
)
}
}

normalizeTimes(times, repeat)
}

const targetTime = startTime + duration

/**
* Add keyframes, mapping offsets to absolute time.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { calculateRepeatDuration } from "../calc-repeat-duration"

describe("calculateRepeatDuration", () => {
test("It correctly calculates the duration", () => {
expect(calculateRepeatDuration(1, 0, 0)).toEqual(1)
expect(calculateRepeatDuration(1, 1, 0)).toEqual(2)
expect(calculateRepeatDuration(1, 2, 0)).toEqual(3)
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { normalizeTimes } from "../normalize-times"

describe("normalizeTimes", () => {
test("It correctly scales times", () => {
const times = [0, 0.5, 1, 1, 1.5, 2]
const repeat = 1
normalizeTimes(times, repeat)
expect(times).toEqual([0, 0.25, 0.5, 0.5, 0.75, 1])
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export function calculateRepeatDuration(
duration: number,
repeat: number,
_repeatDelay: number
): number {
return duration * (repeat + 1)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Take an array of times that represent repeated keyframes. For instance
* if we have original times of [0, 0.5, 1] then our repeated times will
* be [0, 0.5, 1, 1, 1.5, 2]. Loop over the times and scale them back
* down to a 0-1 scale.
*/
export function normalizeTimes(times: number[], repeat: number): void {
for (let i = 0; i < times.length; i++) {
times[i] = times[i] / (repeat + 1)
}
}

0 comments on commit 0e54f81

Please sign in to comment.