Skip to content

Repository files navigation

Compose Theme Reveal

Kotlin Compose Author Apache-2.0 API Maven Central

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

Compose Theme Reveal

How it works

  • 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.
  • 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.

Installation

repositories {
    mavenCentral()
}

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("com.mofeejegi.themereveal:theme-reveal-compose:0.1.0-alpha01")
        }
    }
}

Usage

@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)),
                        )
                    }
                },
            )
        }
    }
}

Arrival styles

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),
)

Input

  • 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).

Things to know

  • State remembered inside the host's content lambda exists once per world while a reveal runs, so it can drift apart. Hoist anything animated or interactive above the host.
  • Calls to revealTo made 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.

Sample

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.xcodeproj in Xcode and run.

License

This project is licensed under the Apache License 2.0.