Skip to content

Kotlin / Jetpack Compose port — would you take it under ports/android? #14

Description

@andresain123

I have a working native Android port of thinking-orbs — Kotlin + Jetpack
Compose, no Skia, no React Native — built against 0.3.1 / spec 1.0.0 and
running in production in a shipped app. Before I package it up as a PR I'd
rather ask whether you want it in the repo at all, and where.

PORT_PLAN.md covers Android through ports/react-native on Skia, which is
the right call for RN apps. This is for the other case: a native Compose app,
where pulling in RN + Reanimated + Skia to draw a 64pt loading indicator is
not an option. It sits alongside the RN port rather than replacing it.

Image

Parity

Geometry-exact against spec/orbs-golden.json, unmodified and byte-identical
to the copy on main:

golden: 72 cases, 70115 values within 1.0E-4 (spec 1.0.0)

All 9 states x 2 sizes x 4 timestamps. The engine is hand-transcribed from
src/engine/*.ts, which is only defensible because the fixture catches a
mistyped constant or a sign error by exact case and field. 39 tests total,
including per-mode golden tests (lattice, orbits, strands, morph,
web), JS rounding semantics including negative zero, preset resolution, and
a coarse frame-budget floor.

The tunings are not transcribed. OrbSpec.kt is generated from
spec/orbs-spec.json by a codegen-kotlin.ts written to sit next to your
scripts/codegen-swift.ts, so a retune upstream flows into Kotlin by
re-running a script rather than retyping forty literals.

Behaviours

Filling in the column your table in PORT_PLAN.md leaves open:

Web Jetpack Compose
matchMedia + ancestor data-theme observer isSystemInDarkTheme() (theme param overrides)
prefers-reduced-motion -> static frame Settings.Global.ANIMATOR_DURATION_SCALE == 0 -> static frame
IntersectionObserver / visibilitychange pause Compose stops recomposing a stopped host (plus a paused param)
aria-label semantics { contentDescription; role = Role.Image }
shared performance.now clock one shared withFrameNanos clock, so co-mounted orbs stay in phase

Android has no direct prefers-reduced-motion; ANIMATOR_DURATION_SCALE == 0
is the closest system signal and is what the platform's own animation APIs
honour.

API

Deliberately shaped like the SwiftUI one:

ThinkingOrb(
    state = OrbState.Searching,
    size = OrbSize.Px64,
    theme = OrbTheme.Auto,      // Auto | Dark | Light
    speed = 1.0,
    paused = false,
    displaySize = 96.dp,        // arbitrary size, tuned preset geometry
    tint = Color(0xFFD64040),   // depth as opacity over one hue
)

displaySize scales inside the Canvas rather than through a
graphicsLayer, so it stays vector-crisp instead of rasterising and
upscaling.

Size and shape

~2,600 lines including tests. Zero third-party dependencies — Compose
foundation/ui/runtime only; the JSON parse is test-scope. Ships a demo
gallery module mirroring ThinkingOrbsDemo.

Layout I'd propose, mirroring ports/ios:

packages/thinking-orbs/
  scripts/codegen-kotlin.ts        # beside codegen-swift.ts
  ports/android/
    thinking-orbs-compose/         # the library
    ThinkingOrbsDemo/              # the gallery

Tests would read spec/orbs-golden.json in place rather than vendoring a
second copy.

Two things the port turned up in your spec pipeline

Neither blocks this; both change what a port should contain, so I would
rather ask than guess.

spec/orbs-golden.json cannot cover the 32px size. src/types.ts has
OrbSize = 64 | 32 | 20 and src/presets.ts carries 32px tunings for every
mode, but scripts/extract-golden.ts:24 hardcodes const SIZES: OrbSize[] = [64, 20]. Re-running npm run spec today reproduces the golden file
byte-for-byte, so there are no reference vectors for 32px and no port can be
verified at that size — ThinkingOrbsKit's OrbSize declares only px64 and
px20 for the same reason. This port matches that coverage. Adding 32 to
SIZES in both extractors would fix it for every port at once.

spec/orbs-spec.json is stale. extract-spec.ts does emit the 32px
presets, so regenerating changes the checked-in file — it predates the 32px
addition. That is the one file where re-running the pipeline is not a no-op.

On tint

This port exposes a tint parameter; ThinkingOrbsKit has none, so there was
no SwiftUI baseline to follow. It now follows inkColor() in
src/engine/core.ts instead — the ramp runs on the tint's own channels, toward
white on a light substrate and toward black on a dark one, with alpha passed
through untouched. An earlier revision held one hue and folded depth into
alpha; that predated inkColor and has been dropped.

There is no golden vector for the tinted path (the fixture carries greys only),
so FrozenTimeTest pins the formula directly, per theme and per channel.

What is not done

  • Not published to Maven Central. Consumable as a Gradle module today; happy
    to wire publishing if you want it on a coordinate.
  • No PNG cross-platform diff against the web canvas — the numeric golden pass
    covers geometry, but your Phase 3 pixel layer has no Android arm yet.
  • The performance floor runs on a desktop JVM, so it catches an
    order-of-magnitude regression and nothing finer. No Pixel-6a-class numbers
    against your Phase 3 target.
  • It currently builds inside another repo's Gradle setup; making it standalone
    (own settings/wrapper/version catalog) is part of preparing the PR.

Worth noting one thing the port turned up: the SwiftUI static path scales the
reduced-motion instant by preset speed where the web does not. Filed
separately.

If you want it, tell me where it should live and I'll open the PR against
that layout. If native Android is out of scope for the repo, no problem — I'd
just rather ask than drop 2,600 lines on you unannounced.

The branch is ready either way, rebased on current main:
https://github.com/andresain123/Libraries/tree/feat/android-compose-port

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions