- Skia
- Reanimated
- Gesture Handler
- Haptics
A switch whose thumb melts as it travels and sets back into a solid bead at either end. The whole look is one SKSL shader clipped to the track, fed every frame by a single sprung progress value between 0 and 1.
Blobs that merge
fieldAt adds up r² / d² for every blob, and smoothstep turns the total into alpha around a threshold of 0.9. Where two blobs sit close together, their fields add up past the threshold in the gap between them, so they join through a neck instead of overlapping as two circles.
float fieldAt(float2 p) { float f = 0.0; for (int i = 0; i < ${MAX_BLOBS}; i++) { if (float(i) >= u_count) { break; } float4 b = u_blobs[i]; float2 d = p - b.xy; float dist2 = max(dot(d, d), 1.0); f += (b.z * b.z) / dist2; } return f;}Liquid only in flight
thumbBlobs scales everything by 4 * p * (1 - p), which is 0 at both ends of the track and 1 halfway. Mid-slide, a trailing blob stretches up to 2.4 thumb radii behind the head while a leading one runs ahead, and both shrink as they pull away. The same curve splits red and blue edges by up to 7 px. The math is written out inline, because a worklet calling another module worklet crashes on the UI runtime.
// travelEnvelope(p), inlinedconst env = 4 * p * (1 - p);// …const reach = thumbR * (0.7 + 1.7 * g) * env;const satR = thumbR * (0.92 - 0.45 * g * env);// …const trailRaw = headX - dir * reach;const leadRaw = headX + dir * reach * 0.55;Taps, drags and one spring
The pan only activates after 10 px of sideways travel, so a quick tap still reaches the tap gesture. Tap, drag release, the value control and replay all finish on the same spring, damped close to critical so a bounce doesn't bring the mid-slide stretch back. The haptic fires only when the side actually changes, and a cancelled drag still settles on the nearer side.
const SNAP_SPRING = { damping: 17, stiffness: 90, mass: 1 } as const;// …const pan = Gesture.Pan() .activeOffsetX([-10, 10]) // … .onEnd(() => { const target = progress.get() >= 0.5 ? 1 : 0; const changed = target !== state.get(); state.set(target); progress.set(withSpring(target, SNAP_SPRING)); if (changed) scheduleOnRN(tapHaptic); })Tuning
- On Color#37D67A
- Off Color#444A57
- Goo Stretch0–1, default 0.7
- StateOff








