Skip to content
Welcome to vote for your favorite component! You can also tell me anything you want to say! (*´∀`)~♥

Orbital Radio radio

Grab the ball, drag it and fling it — it will be pulled by the gravity of each option and eventually get captured by the nearest black hole.

You can also click an option directly, and the ball will fly over automatically.

Usage Examples

Basic Usage

Can't decide what to drink? Let physics decide for you (ノ>_<)ノ ⌒*

Bubble Tea
Black Tea
Cocoa
Orange Juice
View example source code
vue
<template>
  <div class="w-full flex flex-col gap-4 border border-gray-200 rounded-xl p-6 py-10">
    <div class="flex justify-center">
      <radio-orbital
        v-model="selected"
        :options="options"
      />
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import RadioOrbital from '../radio-orbital.vue'

const { t } = useI18n()

const options = computed(() => [
  { label: t('bubbleTea'), value: 'bubble-tea' },
  { label: t('blackTea'), value: 'black-tea' },
  { label: t('cocoa'), value: 'cocoa' },
  { label: t('orangeJuice'), value: 'orange-juice' },
])

const selected = ref('bubble-tea')
</script>

How It Works

Each option acts like a black hole in space, exerting gravitational pull on the ball. After being flung, the ball's acceleration is calculated using Newton's law of universal gravitation, while a damping coefficient simulates air resistance to gradually slow it down.

When the ball gets close enough to a black hole and its speed drops below a threshold, it gets captured, triggering the selection event.

Here's how it works step by step:

  1. Pointer Events track the drag trajectory, and a sliding time window computes the velocity at the moment of release.
  2. After release, a physics simulation kicks in — each frame calculates the gravitational acceleration from all black holes.
  3. Damping gradually slows the ball down; hitting the viewport boundary causes a bounce.
  4. When the ball enters capture range with low enough speed, it gets sucked into the black hole and the selection is made.

Gravitational Softening

In real N-body simulations, when two objects get extremely close, the denominator in the gravity formula approaches zero, causing acceleration to skyrocket toward infinity.

The softening parameter adds a small value to the denominator to prevent this numerical explosion. (. ❛ ᴗ ❛.)

More Accurate Simulation

After sharing this component, someone suggested using Runge-Kutta instead of the Euler method for solving the equations.

After looking into it, I learned a lot! ヾ(◍'౪`◍)ノ゙

With the Euler method, the ball would exhibit unnatural bouncing when moving between black holes. After switching to RK4, the trajectory became noticeably smoother — no need to artificially lower gravity or increase damping to mask numerical errors.

Never thought making silly little components could be so educational. Thanks to everyone for the feedback! (*´∀`)~♥

Before diving into the scary-looking formulas, let's compare with an intuitive trajectory visualization.

Using identical initial conditions and the same black hole configuration, observe the difference in trajectories.

Euler
Euler Sub-step
RK4

The conclusions are clear:

  • Euler's ball exhibits abnormal acceleration due to numerical errors and oscillates wildly near zero points — the trajectory is extremely unstable.
  • Euler Sub-step splits each frame into 4 sub-steps, reducing local error and producing a noticeably more stable trajectory than plain Euler — but it is still first-order.
  • RK4 maintains a smooth and stable orbit.

Now let's break down the differences between Runge-Kutta and Euler.

From Euler to Runge-Kutta

At its core, a physics simulation is an ordinary differential equation (ODE) initial value problem: given the ball's position and velocity at a certain moment, derive the next state using the laws of mechanics.

Computers can't handle continuous time, so we slice time into small intervals dt (each frame's duration) and use numerical integration to approximate the real trajectory.

The core equations can be written as:

drdt=v,dvdt=a(r)

Where r is position, v is velocity, and a(r) is the position-dependent acceleration (gravity).

Euler Method (First Order)

The most intuitive approach — use the current slope and step forward directly:

vn+1=vn+a(rn)Δtrn+1=rn+vnΔt

Translated to code:

ts
velocity += acceleration * dt
position += velocity * dt

The Euler method's local truncation error is O(Δt2) and global error is O(Δt) , meaning errors accumulate rapidly unless dt is very small.

Why Euler Fails Here

The gravity formula is:

a=Gr2+ϵ2

As the ball approaches a black hole, r shrinks and acceleration spikes dramatically. Euler only samples once at the starting point, producing an enormous acceleration that gets directly applied to velocity.

It looks as if the ball gains extra acceleration out of nowhere, appearing to be shot to the other side of the black hole.

The result is a cycle of being pulled back, shot out, pulled back, shot out again — repeated bouncing.

This is known as the Slingshot Effect, a classic symptom of numerical instability.

Imagine drawing a sharp curve, but you're only allowed to glance at the direction once at the starting point, then walk a big step with your eyes closed — you'll definitely overshoot the curve. =͟͟͞͞( •̀д•́)

Euler Sub-stepping

The source of Euler's error is intuitive: the larger the dt, the more each step drifts.

The most direct fix is to split each frame's dt into N sub-steps and run one standard Euler integration per sub-step:

Δtsub=ΔtNvn+1=vn+a(rn)Δtsubrn+1=rn+vnΔtsub

Translated to code:

ts
const subStepCount = 4
const subDt = dt / subStepCount

for (let i = 0; i < subStepCount; i++) {
  const { ax, ay } = computeAcceleration(position.x, position.y)
  velocity.x += ax * subDt
  velocity.y += ay * subDt
  position.x += velocity.x * subDt
  position.y += velocity.y * subDt
}

The force field is resampled at each sub-step, so errors shrink significantly near strongly curved gravity wells.

The global error is still O(Δt) , but the constant factor shrinks by N — when N=4 , the error is roughly 14 of the original.

Sub-stepping is not the same as a higher-order method

Even with N sub-steps, Euler's error order remains first-order O(Δt) — only the effective step size shrinks.

RK4 achieves O(Δt4) with the same number of force evaluations (4 per frame) — the accuracy gap is a difference of magnitude, not just a multiplier.

Runge-Kutta 4th Order (RK4)

RK4's strategy goes beyond just looking at the starting point — it takes 4 samples within a single dt, considering how the force field changes across the entire interval.

The steps:

k1: Compute acceleration at the starting point (rn,vn)

k1=a(rn)

k2: Take a half step using k1, compute acceleration at the midpoint

k2=a(rn+vnΔt2)

k3: Take another half step using k2 (a different estimate), compute again

k3=a(rn+(vn+k2Δt2)Δt2)

k4: Take a full step using k3, compute acceleration at the endpoint

k4=a(rn+(vn+k3Δt)Δt)

Finally, the weighted average:

vn+1=vn+k1+2k2+2k3+k46Δtrn+1=rn+v(1)+2v(2)+2v(3)+v(4)6Δt

The weights 1:2:2:1 come from Simpson's rule — sampling once at each endpoint and twice at the midpoint, which can exactly integrate up to cubic polynomials.

RK4's local truncation error is O(Δt5) and global error is O(Δt4) . Compared to Euler's O(Δt) , the precision improvement is several orders of magnitude.

Intuitive Comparison

EulerEuler Sub-stepRK4
AnalogyGlance at the direction once, walk a big step blindSplit the big step into four small ones, look each timeWalk a bit, look around, repeat four times, then average out
Samples per frame1N4
Global errorO(Δt)O(Δt) (constant shrinks by N )O(Δt4)
Sharp turn behaviorEasily overshoots the curveMore stable than Euler, but still first-orderStays close to the curve

The trade-off is computing acceleration 4 times per frame (iterating through all black holes 4 times), but the number of black holes equals the number of options — usually no more than 10 — which is absolutely no problem for modern browsers. ( •̀ ω •́ )✧

Source Code

API

Props

interface RadioOption {
  label: string;
  value: string;
}

interface Props {
  /** 目前選取的值 */
  modelValue?: string;
  /** 選項列表 */
  options?: RadioOption[];
  /**
   * 球的直徑(px)
   * @default 12
   */
  ballSize?: number;
  /**
   * 重力強度
   * @default 5_000_000
   */
  gravity?: number;
  /**
   * 阻尼係數(0-1)。數值越小球減速越快,會很快停下來;數值越大球滑行距離越長,減速越慢
   * @default 0.9
   */
  damping?: number;
  /**
   * 捕捉距離(px)。球進入此範圍且速度夠低時會被吸入。數值越大越容易被捕捉;數值越小需要更精確地靠近黑洞
   * @default 14
   */
  captureDistance?: number;
  /**
   * 捕捉速度閾值(px/s)。球速度低於此值才會被捕捉。數值越大球在較快速度時也能被吸入;數值越小球必須幾乎靜止才會被捕捉
   * @default 100
   */
  captureSpeed?: number;
  /**
   * 重力軟化參數,防止球靠近黑洞時加速度爆炸。數值越大重力變化越平滑,球不會突然加速;數值越小球靠近黑洞時會急劇加速
   * @default 12
   */
  softening?: number;
  /**
   * 點選黑洞時球移動過去的動畫時長(ms)
   * @default 350
   */
  moveDuration?: number;
  /**
   * 主色調,用於球顏色與選中時的邊框色。接受任何 CSS 合法顏色值
   * @default '#34c6eb'
   */
  color?: string;
}

Emits

interface Emits {
  'update:modelValue': [value: string];
}

v0.60.0