feat: add initial velocity to spring transitions - #53
Merged
Conversation
`SpringTransition` gains a `velocity` field, matching `Animated.spring`'s
`velocity`: the spring starts with the property already in motion instead of
at rest. This is what a spring that continues a gesture needs — without it,
porting `Animated.spring({ velocity })` or `withSpring(v, { velocity })` to
EaseView drops the initial impulse and the animation reads as sluggish.
Units follow the JS side — DIPs/s for translateX/translateY, degrees/s for
rotate, unitless for scale and opacity — and the value is signed in value
space, so positive means the property is already increasing.
Platform notes:
- iOS: `CASpringAnimation.initialVelocity` is normalized against the
from->to distance rather than being in value units, so the value is divided
by the delta. That division also produces the correct sign for decreasing
animations. `settlingDuration` accounts for the initial velocity, so
`duration` is read after it is set.
- Android: `SpringAnimation.setStartVelocity` works in the property's own
units, which for TRANSLATION_X/Y is pixels — the DIP value is converted the
same way `EaseViewManager` converts the target.
- Web: not applied. A spring compiles to one normalized `linear()` easing
curve shared by every property in the category, and an initial velocity only
has meaning relative to each property's own from->to distance.
Adds a Spring Velocity demo to the example app under Timing, plus README,
usage, api-reference and refactor-skill updates. The skill also gains a note
on `Animated.spring`'s bounciness/speed pair, which resolves through
`SpringConfig.fromBouncinessAndSpeed` rather than mapping directly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
SpringTransitiongains avelocityfield, matchingAnimated.spring'svelocity: the spring starts with the property already in motion instead of at rest.I hit this porting
TouchableBouncetoEaseViewin an app.TouchableBounceusesAnimated.spring({ velocity: 0.4, bounciness }), and there was no way to express that0.4.It can't be folded into the other constants. A spring released from rest has
x'(0) = 0by construction, so no choice ofstiffness/damping/massproduces a curve that starts with slope. Fitting theTouchableBouncepress-in (scale 1 → 0.93,bounciness: 0) with a zero-velocity spring, searchingstiffnessover [20, 2000] anddampingover [1, 200], the best fit still misses by 5.2% of the travel and can't reproduce the shape: becausevelocityis signed in value space and the target is below the start,+0.4pushes away from the target first, peaking at scale 1.0022 around 13ms before springing down. Against the same spring withvelocity: 0the curves differ by up to 11% of the travel.To be clear about scale: on a ~170pt button that press-in overshoot is a fraction of a point, so for
TouchableBouncespecifically this is a fidelity detail, not a visible bug. The parameter earns its place on gestures — a fling handed off at 1200 DIP/s is dominated by the initial velocity, and there is currently no way to express it.Units follow the JS side — DIPs/s for
translateX/translateY, degrees/s forrotate, unitless forscaleandopacity— and the value is signed in value space, so positive means the property is already increasing.The two native platforms want this quantity in different terms, which is where the only real subtlety is:
CASpringAnimation.initialVelocityis normalized against the from→to distance (1means "the whole distance in one second"), not in value units, so the value is divided by the delta. That division also produces the correct sign when the animation is decreasing.settlingDurationaccounts for the initial velocity, sodurationis read after it's set.SpringAnimation.setStartVelocityworks in the property's own units, which forTRANSLATION_X/Yis pixels, so the DIP value is converted the same wayEaseViewManagerconverts the target. Every other property already shares its unit with the JS side.One consequence of
velocityliving on the transition rather than the property: atransformtransition covering both translate and scale can only carry one meaningful velocity. The refactor skill notes to split the categories when both need one.Why web isn't approximated
Worth spelling out, since the machinery looks like it's already there. Web emits a per-CSS-property transition list (
opacity 300ms linear(…), transform 300ms linear(…)), so a velocity-shaped curve per property is expressible in principle —linear()stops can fall outside [0, 1], so even the pull-back-past-the-start shape survives. Two things stop it being worth doing now:velocity / delta, so it needs each property's from→to distance. That means tracking previousanimatevalues and keying the easing cache on the ratio rather than justdamping-stiffness-mass.transformis one CSS property carrying translateX/Y, scale and rotate together, so it gets exactly one timing function — but each of those has its own delta and therefore its own normalization. Native animates them separately and normalizes each correctly; web can't, short of splitting into per-axis elements.So the properties web could support (opacity, border-radius) are the ones velocity is least useful for, and the one that motivates the parameter — transform, where gestures live — is the one it can't. I'd rather have it honestly documented as native-only than working on web in the cases nobody asks for. Happy to revisit if someone has a real opacity-with-velocity case.
Test Plan
yarn test— added cases covering thatvelocityreaches the native config, that negative values survive, that it defaults to0, and that it stays0on atimingtransition.-600/0/600, so the negative case (pulls back before travelling) and the positive case (launches ahead) are visible side by side. I have not run the example app on a simulator or device yet — the native paths below are reasoned from the platform APIs, and that demo is the thing that should confirm them before this is marked ready.yarn patchof 0.7.3 carrying the sameEaseView.mmedit: the prop parses, the animation is created, and aTouchableBounceport driven by it behaves correctly through press/release. That exercises the plumbing end to end, but I did not isolate the visual effect ofvelocityitself there.format:clangcould not run locally — the bundledclang-formatbinary is x86_64 and fails to spawn on this machine (EBADARCH). I formattedios/EaseView.mmwith a systemclang-formatagainst the repo config instead; CI's check is the real signal.