Launch animation: rocket-style play button#36
Open
Xitee1 wants to merge 17 commits into
Open
Conversation
…n seed Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Avoids per-frame floatArrayOf allocation inside drawImpactEffects during the ~260ms impact phase. Negligible real-world impact but cleaner.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ving from crouchProgress The launch animation has three distinct button-scale moments (1.0 at idle, 0.92 at crouch, 1.04 impact recoil). Deriving buttonScale from crouchProgress can only express the first two; the impact recoil needs a separate driver. Add an explicit buttonScale parameter so the LaunchAnimationController can drive the full 820ms scale trajectory directly. crouchProgress now drives only the icon rotation/scale during crouch.
…ct effect Three linked design fixes after initial testing feedback: 1. Play icon lives only in the LaunchOverlay now — removed from PlayButton. Overlay renders it at button center during Idle/Crouch and animates position/rotation/scale during Launch. No more icon handoff between button and overlay, so no color jump or scale discontinuity at takeoff. A soft accent-colored glow fades in as the icon leaves the button, keeping it readable against the dark background. 2. Impact effect now hits the whole dial instead of the draggable knob. drawImpactEffects fills the dial's inner area with a bright center flash, radiates three shockwave rings from the center outward past the ring edge, and keeps the progress-arc brightness boost. The knob no longer gets its own aura — the knob is purely an interaction affordance, not an animation target. 3. Icon-scale compression during crouch is now driven by the controller's iconScale Animatable (0.9 at end of crouch), so the overlay-rendered icon visibly "crouches" just like the prototype. crouchProgress has been dropped — it was only wired to the button-internal icon which no longer exists.
Launch is now two-stage: a 200ms windup where the icon hovers out of the button while scaling up (gathering energy), then a 340ms flight with aggressive ease-out to the dial. Was previously a single 420ms ease-in-out that felt too uniform. Impact extended from 260ms to 600ms so the three shockwave ripples have room to fully expand and fade (matching the prototype's 0/110/220ms stagger). Added a white "bang" flash that fires instantly at impact for a clearer moment of contact, with an accent-colored halo behind it as the longer-lasting glow. Ripples now stay contained within the dial's ring area (was overshooting past the edge), so the effect reads as a contained shockwave on the dial face. Icon fade-out window tightened from travel 0.85→1.0 to 0.95→1.0 so the icon disappears crisply at the moment the impact begins, removing the ~60ms visual gap between takeoff-fade and impact.
Matches the v2 prototype after re-examining the reference. Three key visual elements were missing or wrong: 1. **Rocket trail** — the biggest gap. Renders a glowing white-to-accent line from the button center to the current icon position during the launch phase. Alpha ramps up to peak around mid-flight, fades out at the end. Without it, the icon just slides; with it, the launch reads as an actual rocket with an exhaust tail. 2. **Shockwaves originate at the ring, not the dial center**. The prototype draws the three concentric waves starting at r=ringRadius and expanding outward to ringRadius+~30dp, stroke thinning from 8dp to 0.5dp. Visually this reads as the progress ring itself radiating energy, not a ripple propagating out from some interior point. 3. **Icon scale curve matches the prototype keyframes**: 0.9 (end of crouch) → 1.1 at 30% of launch → 0.9 at 80% → 0.5 at impact, in three chained animateTo calls. Previous two-stage windup was too exaggerated (1.18 → 0.25) and the extra 120ms didn't help readability. Also reverted launch duration to 420ms (was 540) to match prototype exactly, tightened icon fade-out from 0.95→1.0 back to 0.8→1.0, and replaced the accent-halo dial fill with a more prominent white impact flash (6dp → 80dp, alpha fading over ~380ms).
iconScale was already keyframed per the prototype, but iconTravel was a single 420ms tween — smooth continuous acceleration with no dramatic pacing. The prototype's rocketShoot CSS keyframe bakes in three segment boundaries (30% / 80% of duration) and applies the easing per-segment, which makes velocity drop to near zero at each boundary. Splitting iconTravel into the same three segments produces that effect: a short "hang" right after the icon leaves the button (~126ms mark), a fast middle flight, and a soft approach to the dial. The pause-then-burst rhythm is where the "power" in the launch actually comes from.
…launch Two tweaks after another test pass: 1. **Travel/scale use a single keyframes spec with linear interpolation** between waypoints instead of three chained animateTo calls. Chained tweens each decelerate to zero at their end, producing hard stops at the 30% and 80% segment boundaries. Linear-per-segment keeps a constant velocity inside each segment and only shifts speed at the boundary — still dynamic (ratios 1:1.7:1.1 for travel) but without the "stopping" feel. The waypoints themselves are unchanged so the overall keyframe shape still matches the prototype. 2. **Stop icon no longer appears mid-launch.** The PlayButton's crossfade was keyed on `isRunning`, which becomes true as soon as the service starts — typically during the launch phase, 50-100ms after tap. That made it look like a *second* arrow popped out of the button behind the flying one. Now the TimerScreen derives a separate `playButtonShowsRunning = isRunning && phase == Idle` and passes it to the PlayButton; the visual state only flips to "running" after the controller has fully finished the animation and reset to Idle.
Linear-only per-segment was too smooth — the pause after the button
washed out. Ease-in-out per-segment was too hard — hit a wall at each
boundary. Sweet spot: a short explicit hold.
The launch timeline now has four beats:
- 0-100ms: ease-in-out lift to 0.22 (decelerates into the hold)
- 100-135ms: 35ms hold at 0.22 / scale 1.1 — the visible "hang" right
after the icon leaves the button, where it reads as gathering energy
- 135-336ms: linear cruise at higher constant velocity (big jump from
zero gives the acceleration its "power" moment)
- 336-420ms: linear approach to impact at the dial
Scale follows the same beats so the icon stays at peak size (1.1) during
the hold, then shrinks perspectively during the cruise. Total duration
unchanged at 420ms.
Reference artifacts produced during the feature's brainstorming/planning phase: the design decisions (scope, UX, edge cases) and the task breakdown that guided implementation.
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
Tapping the play button now triggers a launch animation: the icon rotates toward the dial, crouches briefly in the button, shoots up leaving a glowing accent-colored trail, and impacts the dial with a white flash, accent halo, and three shockwave rings radiating from the progress ring outward. The stop icon only appears after the animation completes.
The animation is gated by a new user setting (default on, seeded from Android's
Settings.Global.ANIMATOR_DURATION_SCALE == 0at first install), and is also skipped at runtime when the system has animations disabled.Based on the prototype designed in Claude Design — see
docs/superpowers/specs/2026-04-20-launch-animation-design.mdfor the full spec anddocs/superpowers/plans/2026-04-20-launch-animation.mdfor the implementation plan.launchAnimationEnabled: BooleanonUserSettings, persisted via DataStore with a one-time seed from system reduce-motionLaunchAnimationController(Animatable-based state machine driving Crouch → Launch → Impact phases sequentially),LaunchOverlaycomposable renders the flying icon + trail in the root coordinate space,PlayButtonandCircularDialgained optional params for controller-driven scale/impact stateTest plan
./gradlew assembleDebug lintgreen🤖 Generated with Claude Code