Skip to content

[FEATURE] Supporting more than two keyframes in springs #3404

@fpapado

Description

@fpapado

Is your feature request related to a problem? Please describe.

I have an animation sequence with multiple keyframes (of individual scaleX and scaleY transforms) with spring easing. It is a sequence where each step starts a spring transition to a keyframe, and each step starts individually, a set time after the previous one starts (i.e. how at: "<+" works). Steps can thus overlap, and the animations are meant to be additive.

In case my descriptions do not make sense, I have a repository with runnable code for what I describe, and also on Codesandbox 😌

The intent is to create a bouncy box that gets stretched and squeezed, like this:

intent.mp4

My initial hunch was to write this as a single animate (eliding some details for brevity):

animate(
  [
    [scope.current, { scaleX: 0, scaleY: 0 }, { duration: 0 }],
    [scope.current, { scaleX: 1.12, scaleY: 0.84 }],
    [scope.current, { scaleX: 0.98, scaleY: 1.06 }, { at: "<+0.15" }],
    [scope.current, { scaleX: 1, scaleY: 1 }, { at: "<+0.35" }],
    [scope.current, { scaleX: 1, scaleY: 1 }, { at: "<+0.15" }],
  ],
  {
    defaultTransition: {
      type: "spring",
      stiffness: 72,
      damping: 10,
    },
  },
);

This leads to the "spring only supports two keyframes" error (I love that error message by the way 🤩)

I then followed the idea of using separate transition steps:

animate([
  [scope.current, { scaleX: 0, scaleY: 0 }, { duration: 0 }],
  [scope.current, { scaleX: 1.12, scaleY: 0.84 }, { ...spring }],
  [scope.current, { scaleX: 0.98, scaleY: 1.06 }, { ...spring, at: "<+0.15" }],
  [scope.current, { scaleX: 1, scaleY: 1 }, { ...spring, at: "<+0.35" }],
  [scope.current, { scaleX: 1, scaleY: 1 }, { ...spring, at: "<+0.15" }],
]);

This is where I run into some trouble, because visually the box seems to skip steps when animating (see the comparison at the end).

Finally, instead of using at, I split up into multiple animation calls, like this:

animate([
  [scope.current, { scaleX: 0, scaleY: 0 }, { duration: 0 }],
  [scope.current, { scaleX: 1.12, scaleY: 0.84 }, spring],
]);

timeout(150).then(() => {
  animate(scope.current, { scaleX: 0.98, scaleY: 1.06 }, spring);

  timeout(350).then(() => {
    animate(scope.current, { scaleX: 1, scaleY: 1 }, spring);

    timeout(150).then(() => {
      animate(scope.current, { scaleX: 1, scaleY: 1 }, spring);
    });
  });
});

This seems to have the desired effect, though a few times the springs behave unpredictably (scaling the box too much). timeout is just a Promise wrapper around setTimeout, which is not optimal in terms of scheduling.

Describe the solution you'd like

Does this sound like a case that would be solved by supporting multiple keyframes for spring animations? It would be great to be able to use at: "<+" for this scenario, and have the springs add up. However, I might misunderstand, and composing springs in this way might not be simple. Please let me know 🗒️

Describe alternatives you've considered
Manually scheduling the additional animation calls seems to work as an alternative, but the setTimeout calls are not optimal. It is also more tedious to write, compared to how expressive the at: "<+" approach is.

Additional context

A comparison of the two approaches above, on the left is a sequence with separate springs, on the right right is the manually scheduled sequence:

comparison.mp4

Metadata

Metadata

Assignees

No one assigned

    Labels

    featureNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions