Skip to content

feat(compose): multi-hue palette in FieldView — Kotlin port parity#1095

Merged
zachshallbetter merged 1 commit into
mainfrom
feat/field-multihue
Jul 22, 2026
Merged

feat(compose): multi-hue palette in FieldView — Kotlin port parity#1095
zachshallbetter merged 1 commit into
mainfrom
feat/field-multihue

Conversation

@zachshallbetter

Copy link
Copy Markdown
Owner

Resolves #1090. Verified against the Ascent iOS 10-intro-star on-device: orbital matter now renders pink→purple→cyan arcs, matching iOS, where it was pale monochrome before.

The gap

The Kotlin FieldView took one accent: Color and drew every particle as lerp(COOL, accent, heat). JS/Swift FieldOptions carry a palette; 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.
  • Each particle takes a stable colour by pool index (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 dep 0.10.1-local0.10.1 and drops mavenLocal.

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.

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>
@vercel

vercel Bot commented Jul 22, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
fundamental-engine Ready Ready Preview Jul 22, 2026 4:43am

Request Review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@zachshallbetter
zachshallbetter merged commit ca8186a into main Jul 22, 2026
16 checks passed
@zachshallbetter
zachshallbetter deleted the feat/field-multihue branch July 22, 2026 04:49
@zachshallbetter zachshallbetter mentioned this pull request Jul 22, 2026
zachshallbetter added a commit that referenced this pull request Jul 22, 2026
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

kotlin: FieldView collapses a multi-hue palette to a single accent

1 participant