Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add Spring class #11519

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft

feat: add Spring class #11519

wants to merge 1 commit into from

Conversation

Rich-Harris
Copy link
Member

This adds a Spring class as an alternative to the existing spring store factory. The behaviour is essentially identical, but the API is slightly different:

-const coords = spring({ x: 50, y: 50 }, opts);
+const coords = new Spring({ x: 50, y: 50 }, opts);

// set new value immediately
-coords.set(value, { hard: true });
+coords.set(value, { instant: true });

// set new value but preserve momentum for 0.5 seconds (useful for 'throwing' interactions)
-coords.set(value, { soft: true });
+coords.set(value, { preserveMomentum: 0.5 });

instant and preserveMomentum are clearer than hard and soft, which seem like they're somehow symmetrical but are in fact unrelated.

As an alternative to providing an initial value, you can provide a function, making it easy to keep a spring in sync with some other value:

let { number } = $props();

-const thing = spring();
-$: thing.set(number);
+const thing = new Spring(() => number);

In this case it's still possible to do thing.set(value, opts), since you might (e.g.) need to use { instant: true }, and making it 'readonly' in the function case could be overly restrictive.

Initially I envisaged that there'd be a spring.target property, and you'd update the spring by updating that value. But reading through #2627 made me realise that there are many cases where you need programmatic control (e.g. gesture-based interactions) so we need a spring.set(...) method, and having two ways to do the same thing could be confusing.

That said, it might be nice to be able to do this sort of thing...

<input type="range" bind:value={spring.target} />

...and so the possibility is left open.

A nice thing about using classes: you can do const spring = new Spring(...) instead of always having to come up with a more descriptive name.

TODO

  • add a Tween class to go with Spring
  • docs
  • tests? (though there are very few spring tests to adapt — not totally sure what tests would look like here)

Before submitting the PR, please make sure you do the following

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • Prefix your PR title with feat:, fix:, chore:, or docs:.
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests and linting

  • Run the tests with pnpm test and lint the project with pnpm lint

Copy link

changeset-bot bot commented May 9, 2024

⚠️ No Changeset found

Latest commit: 01e4e97

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@abdel-17
Copy link

abdel-17 commented May 9, 2024

As an alternative to providing an initial value, you can provide a function, making it easy to keep a spring in sync with some other value:

Correct me if I'm wrong, but the value of the spring would get out of sync with the external state if you try to set it directly, no?

let { number } = $props();

const thing = new Spring(() => number);

thing.set(5); // oops now we’re out of sync

I think a value change callback could be added to avoid such cases.

let { number = $bindable() } = $props();

const thing = new Spring(() => number, {
  onChange: (v) => (number = v)
});

thing.set(5); // still in sync

@Rich-Harris
Copy link
Member Author

the value of the spring would get out of sync with the external state if you try to set it directly, no?

Yes, and the solution is 'don't do that'. In general you wouldn't need to set a store with a function input, but it's a useful escape hatch if you need to do something like this:

let progress = $state(0);
let spring = new Spring(() => progress);

function increment() {
  if (progress === 10) {
    spring.set(0, { instant: true });
    progress = 0;
  } else {
    progress += 1;
  }
}

It would be strange to have an onChange callback that updated to the target value rather than the current value.

@abdel-17
Copy link

abdel-17 commented May 9, 2024

Yeah fair. I just worry this might cause weird bugs because you expect the values to always be in sync.

@PuruVJ
Copy link
Collaborator

PuruVJ commented May 9, 2024

Possible to sneak in this as well? #9141 (comment)

@jeremy-deutsch
Copy link
Contributor

jeremy-deutsch commented May 10, 2024

Because of the function case, it feels like it might be better to have some kind of skipAnimation()/cancelAnimation()/jump() method instead of the { immediate: true } option? That way you can avoid set() altogether.

Like:

<script>
  let value = $state(0);

  const spring = new Spring(() => value);

  function resetSpring() {
    value = 0;
    // switch directly to the current value without animating
    spring.skipAnimation();
  }
</script>

This works even if you're using set():

<script>
  const spring = new Spring(0);

  function resetSpring() {
    spring.set(0);
    // switch directly to the current value without animating
    spring.skipAnimation();
  }
</script>

I like that this way you no longer have to know what the final spring position is in order to skip the animation. That seems really nice for the function case.

I also think a preserveMomentum(seconds) method could work similarly.

@grischaerbe
Copy link

I'd love to have preserveMomentum in milliseconds. Apart from that, I like the new API 👍

@ottomated
Copy link
Contributor

I'd love to see this in 5.0 and would like to work on it, @Rich-Harris do you have any input on the proposed APIs or should I just work on merge conflicts and the Tween class?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants