diff options
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() |
