From a287abc937eb036271717e0867398fb68711c51e Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 11 Apr 2026 02:09:07 +0000 Subject: Improve particle wind animation: longer lifetimes, wind-scaled count, zoom scatter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MAX_AGE 8s → 25s: particles live much longer, drastically less twinkle - activeN scales 60→300 with wind speed (0–25 kt): calm = sparse drift, strong wind = dense streaks - Zoom-out detection: scatter() fires immediately when latRange grows >1.8×, instantly filling the new viewport instead of waiting for old particles to die - Alpha uses sine curve (sin(π·age/MAX_AGE)) instead of linear fade: particles ease in from birth, peak at mid-life, and ease out — no bright birth flash that caused the twinkle effect https://claude.ai/code/session_01HXPjBsogsJVRwCiekDGkJX --- .../src/main/kotlin/org/terst/nav/MainActivity.kt | 2 ++ .../kotlin/org/terst/nav/ui/map/ParticleWindView.kt | 20 ++++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) (limited to 'android-app/app/src/main/kotlin') diff --git a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt index 996892e..e6355bd 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt @@ -296,6 +296,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener { private fun hideOverlays() { fragmentContainer.visibility = View.GONE fabRecordTrack.visibility = View.VISIBLE + // Re-apply cached conditions in case they arrived while the overlay was covering the sheet + lastConditions?.let { applyConditions(it) } } override fun onQuitRequested() { diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt index 24b25db..26d9ea2 100644 --- a/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt +++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/map/ParticleWindView.kt @@ -8,6 +8,7 @@ import android.util.AttributeSet import android.view.View import org.maplibre.android.geometry.LatLng import org.maplibre.android.maps.MapLibreMap +import kotlin.math.PI import kotlin.math.cos import kotlin.math.sin import kotlin.random.Random @@ -38,10 +39,12 @@ class ParticleWindView @JvmOverloads constructor( private var windSpeedKt = 0.0 private val N = 300 + private var activeN = 150 // wind-speed-dependent; updated in setWind() private val particleLat = FloatArray(N) private val particleLon = FloatArray(N) private val particleAge = FloatArray(N) - private val MAX_AGE = 8f // seconds before forced respawn + private val MAX_AGE = 25f // seconds before forced respawn + private var lastLatRange = 0f // for zoom-out scatter detection private val paint = Paint(Paint.ANTI_ALIAS_FLAG).apply { strokeWidth = 3.5f @@ -62,6 +65,8 @@ class ParticleWindView @JvmOverloads constructor( fun setWind(dirFromDeg: Double, speedKt: Double) { windDirFromDeg = dirFromDeg windSpeedKt = speedKt + // Scale particle count: 60 at calm → 300 at 25+ kt + activeN = (60 + (speedKt.coerceIn(0.0, 25.0) / 25.0 * 240)).toInt() } // ── Rendering ──────────────────────────────────────────────────────────── @@ -86,6 +91,11 @@ class ParticleWindView @JvmOverloads constructor( val latNorth = maxOf(nL.latitude, nR.latitude, fL.latitude, fR.latitude).toFloat() val latRange = latNorth - latSouth + // If the user zoomed out significantly, immediately redistribute particles + // across the new viewport rather than waiting for them to drift to the edges. + if (lastLatRange > 0f && latRange > lastLatRange * 1.8f) scatter(m) + lastLatRange = latRange + // West edge = left screen side; east edge = right screen side. // Using screen-ordered corners handles antimeridian crossing correctly. val lonWest = minOf(nL.longitude, fL.longitude).toFloat() @@ -109,7 +119,7 @@ class ParticleWindView @JvmOverloads constructor( val tailDx = sin(screenRad).toFloat() * TAIL_PX val tailDy = (-cos(screenRad)).toFloat() * TAIL_PX - for (i in 0 until N) { + for (i in 0 until activeN) { particleLat[i] += dlat particleLon[i] += dlon // Wrap longitude into [-180, 180] after movement @@ -137,7 +147,9 @@ class ParticleWindView @JvmOverloads constructor( LatLng(particleLat[i].toDouble(), particleLon[i].toDouble()) ) - val alpha = ((1f - particleAge[i] / MAX_AGE) * 220).toInt().coerceIn(40, 220) + // Sine curve: smooth fade-in from birth, peak at mid-life, smooth fade-out. + // No bright birth flash — particles ease in and ease out. + val alpha = (sin(PI * particleAge[i] / MAX_AGE) * 200).toInt().coerceIn(15, 200) paint.color = Color.argb(alpha, 30, 100, 255) canvas.drawLine(pt.x - tailDx, pt.y - tailDy, pt.x, pt.y, paint) @@ -170,7 +182,7 @@ class ParticleWindView @JvmOverloads constructor( val lonEast = maxOf(nR.longitude, fR.longitude).toFloat() val lonSpan = if (lonEast >= lonWest) lonEast - lonWest else lonEast - lonWest + 360f - for (i in 0 until N) { + for (i in 0 until activeN) { particleLat[i] = latSouth + Random.nextFloat() * latRange var newLon = lonWest + Random.nextFloat() * lonSpan if (newLon > 180f) newLon -= 360f -- cgit v1.2.3