Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,13 @@ private val COOL = Color(0xFFFFE0C8) // resting (warm-default identity), matches
fun FieldView(
modifier: Modifier = Modifier,
accent: Color = Color(0xFF4DA3FF),
/**
* Multi-hue palette. Each particle takes a stable colour from this list (by its
* index in the pool), so the field renders in several hues at once — matching the
* Swift/JS engines, whose FieldOptions carry a `palette`. Defaults to `[accent]`,
* i.e. the previous single-hue behaviour, so existing callers are unaffected.
*/
palette: List<Color> = listOf(accent),
particleCount: Int = 300,
renderMode: RenderMode = RenderMode.DOTS,
content: @Composable () -> Unit = {},
Expand Down Expand Up @@ -160,15 +167,18 @@ fun FieldView(
frame // observe the frame tick so the canvas redraws each display frame
val c = controller ?: return@Canvas
val particles = c.particles
// Stable per-particle hue: the pool is updated in place, so a particle keeps
// its array index across frames — no per-frame colour flicker.
val hues = palette.ifEmpty { listOf(accent) }
when (renderMode) {
RenderMode.DOTS -> for (p in particles) {
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 👍 / 👎.

}

RenderMode.GLOW -> for (p in particles) {
RenderMode.GLOW -> particles.forEachIndexed { i, p ->
val heat = p.heat.coerceIn(0f, 1f)
val c0 = lerp(COOL, accent, heat)
val c0 = lerp(COOL, hues[i % hues.size], heat)
val glowR = 5f + p.size * 2f + heat * 16f
drawCircle(
brush = Brush.radialGradient(
Expand All @@ -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 👍 / 👎.

}
}
for (p in particles) {
particles.forEachIndexed { i, p ->
val heat = p.heat.coerceIn(0f, 1f)
drawCircle(lerp(COOL, accent, heat), 1.5f + heat * 2f, Offset(p.position.x, p.position.y), 0.9f)
drawCircle(lerp(COOL, hues[i % hues.size], heat), 1.5f + heat * 2f, Offset(p.position.x, p.position.y), 0.9f)
}
}

RenderMode.TRAILS -> if (trail != null) {
// fade the previous frame toward black, then stamp the particles.
trail.canvas.drawColor(android.graphics.Color.argb(38, 0, 0, 0), android.graphics.PorterDuff.Mode.SRC_OVER)
for (p in particles) {
particles.forEachIndexed { i, p ->
val heat = p.heat.coerceIn(0f, 1f)
trail.paint.color = lerp(COOL, accent, heat).toArgb()
trail.paint.color = lerp(COOL, hues[i % hues.size], heat).toArgb()
trail.canvas.drawCircle(p.position.x, p.position.y, 1.5f + p.size * 1.5f + heat * 3f, trail.paint)
}
drawImage(trail.bitmap.asImageBitmap())
} else {
for (p in particles) {
drawCircle(lerp(COOL, accent, p.heat.coerceIn(0f, 1f)), 2f, Offset(p.position.x, p.position.y), 0.85f)
particles.forEachIndexed { i, p ->
drawCircle(lerp(COOL, hues[i % hues.size], p.heat.coerceIn(0f, 1f)), 2f, Offset(p.position.x, p.position.y), 0.85f)
}
}
}
Expand Down
Loading