A spin-to-win roulette wheel for Compose Multiplatform. Pure Canvas. No images. No third-party libs. Just vibes and equal-odds randomness.
"You miss 100% of the spins you don't take." — Wayne Gretzky, probably
Left: RouletteWheel (with icons) — Right: RouletteWheelSimple (text only)
Each segment shows: icon (left) + name (bold, 1 line) + description (2 lines max). If you don't pass an icon, you get a default star. You're welcome.
Same Prize object, icon field ignored. Just centered bold name + description.
For when you want the wheel to feel classy.
Add JitPack to your repositories:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
maven("https://jitpack.io")
}
}Add the dependency:
// build.gradle.kts
commonMain.dependencies {
implementation("com.github.Artificialss.ComposeRoulette:roulette:1.0.4")
}git clone git@github.com:Artificialss/compose-roulette.git
cd ComposeRoulette
./gradlew :roulette:publishToMavenLocalThen in your project:
// settings.gradle.kts
dependencyResolutionManagement {
repositories {
mavenLocal()
// ...
}
}
// build.gradle.kts
commonMain.dependencies {
implementation("com.artificialss:roulette:1.0.4")
}// settings.gradle.kts
includeBuild("../ComposeRoulette") {
dependencySubstitution {
substitute(module("com.artificialss:roulette")).using(project(":roulette"))
}
}
// build.gradle.kts
commonMain.dependencies {
implementation("com.artificialss:roulette")
}// 1. Define your prizes
val prizes = listOf(
Prize("gold", "Premium Forever", "Lifetime access", Color.Gold, icon = { CrownIcon() }),
Prize("trial", "7-Day Trial", "Try premium free", Color.Green),
Prize("lose", "Try Again", "Better luck next time", Color.DarkGray, tryAgain = true),
)
// 2. Create state + render
val state = rememberRouletteState(prizes.size)
val scope = rememberCoroutineScope()
RouletteWheel(
prizes = prizes,
state = state,
onResult = { prize -> println("Won: ${prize.name}") }
)
// 3. Spin
Button(onClick = {
scope.launch {
val winnerIndex = state.spin()
val winner = prizes[winnerIndex]
if (winner.tryAgain) { /* let them spin again */ }
else { /* grant the prize */ }
}
}) { Text("SPIN") }data class Prize(
val id: String, // Unique ID for result handling
val name: String, // Bold title, 1 line, ellipsis if too long
val description: String, // Subtitle, 2 lines max, ellipsis
val color: Color, // Segment background
val textColor: Color, // Text color (default: White)
val icon: (@Composable () -> Unit)?, // Optional icon composable (null = default star)
val tryAgain: Boolean // If true, the consumer should allow re-spin
)| Field | Required | Default | Notes |
|---|---|---|---|
id |
Yes | — | Use it to identify the result |
name |
Yes | — | 1 line, bold, ellipsis overflow |
description |
No | "" |
2 lines max, 70% alpha |
color |
No | Dark gray | Segment background |
textColor |
No | White | — |
icon |
No | None | Ignored by RouletteWheelSimple |
tryAgain |
No | false |
Signal the consumer to allow re-spin |
All prizes have equal probability. Always. No weighting.
@Composable
fun RouletteWheel(
prizes: List<Prize>,
state: RouletteState,
onResult: (Prize) -> Unit = {},
style: RouletteStyle = RouletteStyle(),
modifier: Modifier = Modifier
)@Composable
fun RouletteWheelSimple(
prizes: List<Prize>,
state: RouletteState,
onResult: (Prize) -> Unit = {},
style: RouletteStyle = RouletteStyle(),
modifier: Modifier = Modifier
)Auto-adaptive:
- Fills available space with 15dp padding
- Text and icons scale with segment count (4/8/12+ breakpoints)
- Labels auto-flip so they're never upside down
- Pointer is on the left side
// Create state
val state = rememberRouletteState(prizes.size)
// Custom spin duration (default: 4000ms)
val state = rememberRouletteState(prizes.size, spinDurationMs = 6000)| Property / Method | Description |
|---|---|
state.isSpinning |
true while animating |
state.lastWinnerIndex |
Last winner index (-1 if never spun) |
state.spin() |
Suspend. Random equal-odds spin. Returns winner index. |
state.spin(winnerIndex) |
Suspend. Lands on a specific prize. Returns winner index. |
scope.launch {
val winnerIndex = state.spin()
val winner = prizes[winnerIndex]
}Call your API first, then pass the result to spin(winnerIndex). The wheel animates to land on exactly that prize.
scope.launch {
// Your backend picks the winner
val picked = api.pickWinner(userId)
// Wheel spins and lands on that prize
val winnerIndex = state.spin(winnerIndex = picked)
val winner = prizes[winnerIndex]
}This keeps the library simple — no async callbacks, no internal network calls. You own the API call, the wheel just animates to the result.
data class RouletteStyle(
val borderColor: Color = Color.White,
val pointerColor: Color = Color.White,
val backgroundColor: Color = Color(0xFF0A0A0A),
val centerColor: Color = Color(0xFF1A1A1A),
val spinDurationMs: Int = 4000
)| Scenario | Behavior |
|---|---|
| 1 prize | Whole wheel = one color. Still spins. Still "wins". |
| 0 prizes | Nothing renders. |
| 20+ prizes | Segments shrink, text scales down, ellipsis. Never cut. |
tryAgain = true |
Consumer decides to re-enable spin. Wheel doesn't auto-retry. |
git clone git@github.com:Artificialss/compose-roulette.git
cd ComposeRoulette
./gradlew :demo:wasmJsBrowserDevelopmentRunOpens at localhost:8080. Both wheel variants side-by-side (landscape) or stacked (portrait). Click SPIN. Winner shown below.
| Platform | Status |
|---|---|
| Android | Supported |
| iOS | Supported |
| Web (WasmJS) | Supported |
| Web (JS) | Supported |
| Desktop (JVM) | Supported |
MIT — use it, ship it, spin it.
Made by Artificialss
