summaryrefslogtreecommitdiff
path: root/android-app/app/src/main
diff options
context:
space:
mode:
authorPeter Stone <thepeterstone@gmail.com>2026-04-06 08:20:12 +0000
committerPeter Stone <thepeterstone@gmail.com>2026-04-06 08:20:12 +0000
commit36af31c9bda660706c3271380b13cba8486c0604 (patch)
tree0f816c0a993de83a160032393c5152b97279394c /android-app/app/src/main
parent61c50f135e69643bf2f905b2241fdb8dcc08abd3 (diff)
feat(ui): wave height scales view, period drives speed, whitecaps gated on wind
WaveView: animation speed = 8/period so long swell animates slowly; amplitude ceiling raised to 42% of view height; whitecaps only when windSpeedKt >= 12 (Beaufort 4). InstrumentHandler.updateWaveState: sizes view height from swell height (1ft→56dp, 8ft→160dp) and forwards windSpeedKt to WaveView. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'android-app/app/src/main')
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/MainActivity.kt3
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt17
-rw-r--r--android-app/app/src/main/kotlin/org/terst/nav/ui/WaveView.kt60
3 files changed, 50 insertions, 30 deletions
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 e5d3080..022b748 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
@@ -345,7 +345,8 @@ class MainActivity : AppCompatActivity(), SafetyFragment.SafetyListener {
val swellHtFt = c.swellHeightM?.let { (it * 3.28084f).toFloat() } ?: 3f
val windHtFt = c.waveHeightM?.let { (it * 3.28084f).toFloat() } ?: 1.5f
val swellPeriod = c.swellPeriodS?.toFloat() ?: 10f
- instrumentHandler?.updateWaveState(swellHtFt, swellPeriod, windHtFt)
+ val windKt = c.windSpeedKt?.toFloat() ?: 0f
+ instrumentHandler?.updateWaveState(swellHtFt, swellPeriod, windHtFt, windKt)
}
}
lifecycleScope.launch {
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
index 84815ce..cb59a3a 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/InstrumentHandler.kt
@@ -135,16 +135,27 @@ class InstrumentHandler(
}
/**
- * Updates the WaveView with current sea state. Call whenever new forecast data arrives.
- * All inputs in feet.
+ * Updates the WaveView with current sea state. Call once when conditions load.
+ *
+ * View height is set here to reflect swell scale: 1ft → 56dp, 8ft → 160dp.
+ * [windSpeedKt] gates whitecap rendering (Beaufort 4 threshold = 12 kt).
*/
fun updateWaveState(
swellHeightFt: Float,
swellPeriodSec: Float,
- windWaveHeightFt: Float
+ windWaveHeightFt: Float,
+ windSpeedKt: Float = 0f
) {
waveView.swellHeightFt = swellHeightFt
waveView.swellPeriodSec = swellPeriodSec
waveView.windWaveHeightFt = windWaveHeightFt
+ waveView.windSpeedKt = windSpeedKt
+
+ // Size the view to the swell — bigger swell = taller window
+ val density = waveView.resources.displayMetrics.density
+ val heightDp = (32f + swellHeightFt * 16f).coerceIn(56f, 160f)
+ val lp = waveView.layoutParams
+ lp.height = (heightDp * density).toInt()
+ waveView.layoutParams = lp
}
}
diff --git a/android-app/app/src/main/kotlin/org/terst/nav/ui/WaveView.kt b/android-app/app/src/main/kotlin/org/terst/nav/ui/WaveView.kt
index 30aca06..d3f9a4d 100644
--- a/android-app/app/src/main/kotlin/org/terst/nav/ui/WaveView.kt
+++ b/android-app/app/src/main/kotlin/org/terst/nav/ui/WaveView.kt
@@ -16,12 +16,11 @@ import kotlin.math.sin
* Draws an animated ocean-horizon scene used as the divider between the
* instrument section and the forecast section.
*
- * The primary wave is driven by swell height and period; a secondary
- * high-frequency wave represents wind chop. Whitecaps appear at wind-wave
- * crests. The view self-animates via [postInvalidateOnAnimation].
+ * Animation speed is driven by [swellPeriodSec] — a 20s period swell animates
+ * at half the speed of a 10s period swell. View height should be set by the
+ * caller to reflect actual swell height (see [InstrumentHandler.updateWaveState]).
*
- * Set [swellHeightFt], [swellPeriodSec], and [windWaveHeightFt] to update
- * the wave state when new forecast data arrives.
+ * Whitecaps are only drawn when [windSpeedKt] >= 12 (Beaufort 4).
*/
class WaveView @JvmOverloads constructor(
context: Context,
@@ -38,12 +37,15 @@ class WaveView @JvmOverloads constructor(
var windWaveHeightFt: Float = 1.5f
set(value) { field = value.coerceAtLeast(0f); invalidate() }
+ /** Wind speed in knots. Whitecaps are suppressed below 12 kt (Beaufort 4). */
+ var windSpeedKt: Float = 0f
+ set(value) { field = value.coerceAtLeast(0f); invalidate() }
+
private var startTimeMs = -1L
private val wavePath = Path()
-
- private val skyPaint = Paint()
- private val seaPaint = Paint()
+ private val skyPaint = Paint()
+ private val seaPaint = Paint()
private val shimmerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply {
style = Paint.Style.STROKE
@@ -86,17 +88,21 @@ class WaveView @JvmOverloads constructor(
val w = width.toFloat()
val h = height.toFloat()
- // Amplitudes scale with wave data, capped relative to view height
- val swellAmp = (h * (swellHeightFt / 28f)).coerceIn(h * 0.04f, h * 0.22f)
- val windAmp = (h * (windWaveHeightFt / 28f)).coerceIn(h * 0.02f, h * 0.10f)
+ // Animation speed scales inversely with period: 10s → 0.8 rad/s, 20s → 0.4, 6s → 1.33
+ val swellSpeed = 8.0 / swellPeriodSec
+ val windSpeed = swellSpeed * 2.2
+
+ // Amplitude fills most of the view height — the view itself is sized for swell scale
+ val swellAmp = (h * (swellHeightFt / 28f)).coerceIn(h * 0.08f, h * 0.42f)
+ val windAmp = (h * (windWaveHeightFt / 28f)).coerceIn(h * 0.02f, h * 0.16f)
val swellWlen = w * (swellPeriodSec / 14f)
val windWlen = swellWlen * 0.35f
val midY = h * 0.52f
fun waveY(x: Float): Float =
(midY
- + sin((x / swellWlen) * TWO_PI - t * 0.8) * swellAmp
- + sin((x / windWlen) * TWO_PI - t * 1.8 + 1.2) * windAmp).toFloat()
+ + sin((x / swellWlen) * TWO_PI - t * swellSpeed) * swellAmp
+ + sin((x / windWlen) * TWO_PI - t * windSpeed + 1.2) * windAmp).toFloat()
// ── Sky fill ──────────────────────────────────────────────────
wavePath.reset()
@@ -125,20 +131,22 @@ class WaveView @JvmOverloads constructor(
while (x <= w) { wavePath.lineTo(x, waveY(x)); x += 2f }
canvas.drawPath(wavePath, shimmerPaint)
- // ── Whitecaps at wind-wave crests ─────────────────────────────
- val capPath = Path()
- x = windWlen * 0.5f
- while (x <= w) {
- val wv = sin((x / windWlen) * TWO_PI - t * 1.8 + 1.2) * windAmp
- val wn = sin(((x + 3) / windWlen) * TWO_PI - t * 1.8 + 1.2) * windAmp
- if (wv > windAmp * 0.55 && wv >= wn) {
- val y = waveY(x)
- capPath.reset()
- capPath.moveTo(x - 7f, y + 1f)
- capPath.quadTo(x, y - 2.5f, x + 8f, y + 1f)
- canvas.drawPath(capPath, whitecapPaint)
+ // ── Whitecaps — only at Beaufort 4+ (≥12 kt) ─────────────────
+ if (windSpeedKt >= 12f) {
+ val capPath = Path()
+ x = windWlen * 0.5f
+ while (x <= w) {
+ val wv = sin((x / windWlen) * TWO_PI - t * windSpeed + 1.2) * windAmp
+ val wn = sin(((x + 3) / windWlen) * TWO_PI - t * windSpeed + 1.2) * windAmp
+ if (wv > windAmp * 0.55 && wv >= wn) {
+ val y = waveY(x)
+ capPath.reset()
+ capPath.moveTo(x - 7f, y + 1f)
+ capPath.quadTo(x, y - 2.5f, x + 8f, y + 1f)
+ canvas.drawPath(capPath, whitecapPaint)
+ }
+ x += windWlen * 0.9f
}
- x += windWlen * 0.9f
}
postInvalidateOnAnimation()