framer / motion

Open source, production-ready animation and gesture library for React
https://framer.com/motion
MIT License
23.73k stars 807 forks source link

[QUESTION] Migrate Decay #330

Closed amcdnl closed 4 years ago

amcdnl commented 5 years ago

Problem Statement

When a user drags when zooming/panning, I have a decay. I'm migrating from pose to motion and I'm curious what the best way to migrate the decay from popmotion would be.

I realize there is pan code in motion but I need to handle this a bit more programmatically since its more about moving the values rather than the actual dom. https://www.framer.com/api/motion/component/#panhandlers.onpan

Example Code

import { value, decay, ValueReaction, ColdSubscription } from 'popmotion';
import { clamp } from '@popmotion/popcorn';

class Pan extends Component {
  observer?: ValueReaction;
  decay?: ColdSubscription;

  onPanStart() {
    const { x, y } = this.props;
    this.observer = value({ x, y });
  }

  onPanMove(x: number, y: number) {
    this.observer && this.observer.update({ x, y });
 }

  onPanEnd(nativeEvent, source: 'mouse' | 'touch') {
    const {
      width,
      height,
      matrix,
      constrain,
      onPanEnd,
      onPanMove
    } = this.props;

      // Calculate the end matrix
      const endX = width * matrix.a - width;
      const endY = height * matrix.a - height;

      this.decay = decay({
        from: this.observer.get(),
        velocity: this.observer.getVelocity()
      })
        .pipe(res => ({
          x: constrain ? clamp(-endX, 0)(res.x) : res.x,
          y: constrain ? clamp(-endY, 0)(res.y) : res.y
        }))
        .start({
          update: ({ x, y }) => {
            this.rqf = requestAnimationFrame(() => {
              onPanMove({
                source: 'touch',
                nativeEvent,
                x,
                y
              });
            });
          },
          complete: () => {
            onPanEnd({
              nativeEvent,
              source
            });
          }
        });
  }
}

Demo

mattgperry commented 5 years ago

You can use the same code as above more or less. There's a caveat here that the internal version of Popmotion that Motion uses doens't support multi-dimensional animations so you'd need to do one for each.

class Component {
  x: motionValue(0)
  y: motionValue(0)

  onPanEnd() {
    this.decayX = decay({
      velocity: this.x.getVelocity()
    }).start((v) => {
       this.x.set(v)
    })
  }
}

There is a type: "inertia" animation that you can use in Framer Motion which is essentially decay but that'd take a bigger refactor. But you can start it in a programmatic way using useAnimation controls

controls.start({
  x: 0, // ignored
  y: 0, // ignored
  transition: { type: "inertia" }
})

Adding the pipe logic would be more difficult though, we need to think about how to do this a bit better.

amcdnl commented 5 years ago

@InventingWithMonster - I gave it a stab (https://gist.github.com/amcdnl/17a3642be09acb74db56c39a0b1e61e7) but it didn't end up very (probably due to my code). Would love to get your feedback.

mattgperry commented 5 years ago

Yeah that looks fine to me - does it work? Do you have an example of it in action?