
Reveal new UI themes through animated masks or card-by-card transitions, composing incoming and outgoing worlds live with customizable arrival styles, presets, and controllable reveal sequencing.
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
ThemeRevealHost composes 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.RevealController owns the theme value. revealTo suspends until the reveal reaches full coverage, and only then commits the new value.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.Allow leaves input alone, for hosts whose interaction lives outside the revealed content (a pager page revealing after it settles, say).content lambda exists once per world while a reveal runs, so it can drift apart. Hoist anything animated or interactive above the host.revealTo made while a reveal is running coalesce: the running reveal finishes, and only the newest request runs after it.revealTo.The sample shows all twelve presets, stepping through six palettes. The UI lives in sample/shared; each platform has its own entry point:
./gradlew :sample:androidApp:installDebug
./gradlew :sample:desktopApp:run
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun
sample/iosApp/iosApp.xcodeproj in Xcode and run.This project is licensed under the Apache License 2.0.
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
ThemeRevealHost composes 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.RevealController owns the theme value. revealTo suspends until the reveal reaches full coverage, and only then commits the new value.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.Allow leaves input alone, for hosts whose interaction lives outside the revealed content (a pager page revealing after it settles, say).content lambda exists once per world while a reveal runs, so it can drift apart. Hoist anything animated or interactive above the host.revealTo made while a reveal is running coalesce: the running reveal finishes, and only the newest request runs after it.revealTo.The sample shows all twelve presets, stepping through six palettes. The UI lives in sample/shared; each platform has its own entry point:
./gradlew :sample:androidApp:installDebug
./gradlew :sample:desktopApp:run
./gradlew :sample:webApp:wasmJsBrowserDevelopmentRun
sample/iosApp/iosApp.xcodeproj in Xcode and run.This project is licensed under the Apache License 2.0.