A Compose Multiplatform library that reveals a new theme through the old one. The incoming world is composed alongside the current one and uncovered behind an animated mask (an expanding front, a parting slit or a growing box, with its own edge, glow, wobble and shake), or card by card across a board that flips, sweeps or unfolds in perspective. Each theme can arrive with its own personality.
Runs on Android, iOS, Desktop (JVM) and Web (Wasm and JS).
Demo it here: https://mofeejegi-portfolio.web.app/libraries/themereveal
ThemeRevealHostcomposes your content once with the committed theme and, only while a reveal runs, a second time with the incoming theme, clipped to the animated mask or drawn as turning cards. Both worlds stay live; nothing is snapshotted.RevealControllerowns the theme value.revealTosuspends until the reveal reaches full coverage, and only then commits the new value.- The library is generic over the theme type: a Material
ColorScheme, your own tokens, an enum. It depends only on Compose runtime, foundation and ui.
repositories {
mavenCentral()
}
kotlin {
sourceSets {
commonMain.dependencies {
implementation("com.mofeejegi.themereveal:theme-reveal-compose:0.1.0-alpha01")
}
}
}@Composable
fun App() {
val controller = rememberRevealController(LightColors)
val scope = rememberCoroutineScope()
ThemeRevealHost(controller = controller, modifier = Modifier.fillMaxSize()) { colors ->
// Composed twice while a reveal runs: once per world.
MaterialTheme(colorScheme = colors) {
Screen(
onToggleTheme = { tapPosition ->
scope.launch {
controller.revealTo(
target = if (controller.current == LightColors) DarkColors else LightColors,
origin = tapPosition, // host coordinates; omit to reveal from the centre
style = ArrivalStyle.iris(accent = Color(0xFF6FA8FF)),
)
}
},
)
}
}
}Twelve presets ship with the library, each an extension on ArrivalStyle.Companion:
| Preset | Character |
|---|---|
ArrivalStyle.iris(accent) |
A clean luminous ring with a fine shimmer and a trace of shake |
ArrivalStyle.cut(accent) |
Two dead-straight fronts parting from the origin |
ArrivalStyle.bloom(accent) |
No front line: a soft halo over counter-rotating petal lobes |
ArrivalStyle.rift(accent) |
A broad living wobble, a glowing front and a slight world-shake |
ArrivalStyle.chaos(accent) |
Jagged shards, a thick blast line and the hardest shake in the set |
ArrivalStyle.paperBurn(ember, char) |
A crackling ember line with a charred rim behind it |
ArrivalStyle.curtain(accent) |
Two vertical fronts drawing apart like stage drapes, with a slow fabric swell |
ArrivalStyle.slash(accent) |
A fast diagonal cut: a razor-thin line and a jolt of shake |
ArrivalStyle.aperture(accent) |
A rectangular frame opening from the origin, with square corners |
ArrivalStyle.flipboard(accent) |
The old world as a board of cards that flip away in a shuffled order, in perspective |
ArrivalStyle.cascade(accent) |
The new world unfolds over the old, card by card, in a diagonal wave |
ArrivalStyle.shutters(accent) |
Full-width slats flip away from top to bottom, like a blind |
Or build your own:
val style = ArrivalStyle(
front = FrontShape.Radial, // or Slit(angle), Box(angle), Tiles(width, height, order, turn)
edge = EdgeCharacter(
wobble = listOf(WobbleBand(amplitude = 8.dp, frequency = 7f, speed = 3f)),
strokeWidth = 2.dp,
glowWidth = 16.dp,
),
accent = Color(0xFFFF6B3D),
shake = 2.dp,
duration = 900,
envelope = Envelope(rampIn = 0.3f, rampOut = 0.7f),
)InputPolicy.Block(the default) consumes all input inside the host until the reveal completes. A tap fast-forwards the reveal; it never cancels or rewinds it.InputPolicy.Allowleaves input alone, for hosts whose interaction lives outside the revealed content (a pager page revealing after it settles, say).
- State remembered inside the host's
contentlambda exists once per world while a reveal runs, so it can drift apart. Hoist anything animated or interactive above the host. - Calls to
revealTomade while a reveal is running coalesce: the running reveal finishes, and only the newest request runs after it. - A running reveal does not recompose anything per frame. Its clocks are read only in draw and placement, and the frame loop exists only inside
revealTo.
The sample shows all twelve presets, stepping through six palettes. The UI lives in sample/shared; each platform has its own entry point:
- Android:
./gradlew :sample:androidApp:installDebug - Desktop:
./gradlew :sample:desktopApp:run - Web:
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun - iOS: open
sample/iosApp/iosApp.xcodeprojin Xcode and run.
This project is licensed under the Apache License 2.0.
