feat(compose): multi-hue palette in FieldView — Kotlin port parity#1095
Conversation
The Kotlin FieldView only took a single `accent` and rendered every particle as lerp(COOL, accent, heat), so consumers passing a palette (Ascent passes [pink, cyan, green, amber]) collapsed to one hue — visibly monochrome next to the Swift/JS engines, whose FieldOptions carry a palette. Adds `palette: List<Color> = listOf(accent)` (backward-compatible). Each particle takes a stable colour by its pool index — the pool is updated in place, so indices persist across frames and there's no per-frame colour flicker. Applied across all four render modes (DOTS/GLOW/LINKS/TRAILS). Verified in Ascent (Android) against the iOS intro reference: the star's orbital matter now renders pink→purple→cyan like iOS, was pale monochrome. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bba59ea348
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| RenderMode.DOTS -> particles.forEachIndexed { i, p -> | ||
| val heat = p.heat.coerceIn(0f, 1f) | ||
| drawCircle(lerp(COOL, accent, heat), 1.5f + p.size * 1.5f + heat * 3f, Offset(p.position.x, p.position.y), 0.85f) | ||
| drawCircle(lerp(COOL, hues[i % hues.size], heat), 1.5f + p.size * 1.5f + heat * 3f, Offset(p.position.x, p.position.y), 0.85f) |
There was a problem hiding this comment.
Key palette hues by particle id
When the field uses source forces such as spawn, mortal particles expire and FieldStore.remove swap-removes them (FieldStore.kt:25-30), so the tail particle can move to a new array index on the next frame. Because the new hue selection is based on i % hues.size, that same existing particle changes color even though Particle.id is already stable (Types.kt:45-46), producing the flicker this change is trying to avoid; use the particle id for palette selection in each mode instead of the list index.
Useful? React with 👍 / 👎.
| @@ -192,24 +202,24 @@ fun FieldView( | |||
| drawLine(accent, po, Offset(q.position.x, q.position.y), strokeWidth = 1f, alpha = (1f - d / LINK_RADIUS) * 0.25f) | |||
There was a problem hiding this comment.
Derive the link accent from palette-only calls
When a caller supplies palette without also overriding accent and selects RenderMode.LINKS, the particles use the requested hues but the connector layer remains the default blue accent. The JS and Swift paths resolve/adopt the accent from the first palette stop (field.ts:400-401, FieldEngine.swift:790-793), so palette-only Kotlin calls still render an extra blue link layer instead of matching the selected palette; derive the single link accent from the resolved palette when no explicit accent is provided.
Useful? React with 👍 / 👎.
Kotlin port parity — the Compose `FieldView` renders a multi-hue palette. Resolves #1090; ships the fix merged in #1095. ## What & scope - `FieldView` gains `palette: List<Color>` (backward-compatible; defaults to `[accent]`). Each particle takes a stable colour by pool index across all four render modes. Verified against the iOS `10-intro-star`. - **Kotlin-only.** JS and Swift are byte-identical to 0.10.0 — they already had the palette. npm + Swift republish at 0.10.1 **only** to hold cross-plane lockstep; no JS/Swift behaviour change. The changelog states this so the bump isn't misread. ## Versions - 7 packages + `FIELD_VERSION` → `0.10.1` (lockstep, drift test green). - **The private-app trap did not recur** — the `./packages/*` filter from #1094 held: site/starter/observatory unchanged at 0.2.0 / 0.1.0 / 0.0.0. ## Release `git tag v0.10.1` → npm + Swift (SPM) + Kotlin (Maven, via the CI job) in lockstep. Ascent then flips `0.10.1-local` → `0.10.1`. ## Still open #1091 — the parity matrix doesn't track colour, which is why this reached an app before a gate. Closing it makes a future palette divergence fail CI instead of shipping. ## Gates typecheck · build · `check:api` (no removals) · `check:docs` · `check:links` · FIELD_VERSION lockstep — all green.
Resolves #1090. Verified against the Ascent iOS
10-intro-staron-device: orbital matter now renders pink→purple→cyan arcs, matching iOS, where it was pale monochrome before.The gap
The Kotlin
FieldViewtook oneaccent: Colorand drew every particle aslerp(COOL, accent, heat). JS/SwiftFieldOptionscarry apalette; the Kotlin port collapsed a multi-colour field to its first hue. A real port-parity gap (found by an app developer's eye), not an app bug.The fix (+20/−10, one file)
palette: List<Color> = listOf(accent)— backward-compatible: default is the old single-hue behaviour, existing callers unaffected.hues[i % hues.size]) across all four render modes (DOTS, GLOW, LINES, TRAILS + fallback). The pool updates in place, so a particle keeps its index frame-to-frame — no colour flicker.palette.ifEmpty { listOf(accent) }guards an empty list. The LINES connector stays single-accent by design (multi-colour link lines would be visual noise).Release
Additive → patch
0.10.1. Ships via the lockstep tag added in 0.10.0 (npm + Swift + Kotlin from one tag). Ascent then flips its dep0.10.1-local→0.10.1and dropsmavenLocal.Still open
#1091 (the parity matrix doesn't track colour) remains — it's why this gap went uncaught, and closing it would make a future single-accent-vs-palette divergence fail the gate rather than reach an app.