Plan Beam Selector
- Skia
- Reanimated
- Gesture Handler
- Haptics
A pricing selector where choosing a plan sends a comet around that card's border. The trail starts wherever your finger landed, which is a small thing that makes the selection feel like it came from you.
The comet starts where you touched
The border is sampled into a list of points, each carrying its position along the path. On tap, the nearest of those points to the touch becomes the trail's origin, so a tap at the bottom-left sends the comet away from the bottom-left. The same sampled list is reused to place the glowing head every frame.
originT.set(nearestT(cardPts[hit], e.x, e.y));phase.set(0);phase.set(withTiming(1, { duration: cometMs }));// …const q = pointOnPerimeter(pts, cometHeadT.get());return vec(q.x, q.y);A trail that can cross the seam
Starting anywhere means the trail often runs off the end of the path and back to the beginning, and a path trim cannot wrap. wrapTrim returns one range when it fits and two when it does not, so the comet is drawn as two segments across the seam and there is no point on the border where it visibly restarts.
if (len >= 1) return [[0, 1]];const end = start + len;if (end <= 1) return [[start, end]];return [[start, 1], [0, end - 1]];A tail that grows in two parts
The tail is not proportional to progress. 18% of it snaps out in the first 20% of the lap, and the rest follows a square curve until the trail covers the whole border, so the comet reads as something that leaps away and then stretches. Its opacity rises over the first tenth and fades across the last 15%, which hides both ends of the animation.
return 0.18 * Math.min(1, p * 5) + 0.82 * p * p;// …if (p <= 0 || p >= 1) return 0;return Math.min(1, p * 10) * Math.min(1, (1 - p) / COMET_FADE);Tuning
- #8B5CF6
- #22D3EE
- 500
- 2.5
- 0.35
<PlanBeamSelector params={{ mode: "selector", beamA: "#8B5CF6", beamB: "#22D3EE", cometMs: 500, borderW: 2.5, breathe: 0.35 }} fireSignal={0} />