From 40642fbcad70efc377d9a8cb462002ef3b775f4c Mon Sep 17 00:00:00 2001 From: davotoula Date: Mon, 8 Jun 2026 09:19:54 +0200 Subject: [PATCH] Manual visual tuning fix(audio): make Waves & Aurora spectrum-shaped and bounded to the canvas --- .../commons/audio/renderers/AuroraRenderer.kt | 41 ++++++++++++++----- .../commons/audio/renderers/WavesRenderer.kt | 26 ++++++++---- 2 files changed, 49 insertions(+), 18 deletions(-) diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/AuroraRenderer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/AuroraRenderer.kt index a595ff5c26..95c4edd049 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/AuroraRenderer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/AuroraRenderer.kt @@ -38,21 +38,29 @@ import com.vitorpamplona.amethyst.commons.audio.wrapHue import kotlinx.coroutines.flow.Flow import kotlin.math.sin +/** + * Three overlapping glowing ribbons. Each ribbon is a stroked line whose vertical displacement from + * its centre is the live spectrum of its band (interpolated across the width), so the ribbon's shape + * tracks the audio; the glow thickness swells with the overall energy. A small time shimmer (±8%) + * adds life without driving the motion. The line is clamped inside the canvas (accounting for the + * stroke width) so the tops/bottoms never get cut. + */ object AuroraRenderer : VisualizerRenderer { override val style = VisualizerStyle.AURORA private class Ribbon( val hue: (VisualizerPalette) -> Float, val yo: Float, - val sp: Float, - val band: Float, + val lo: Float, + val hi: Float, + val dir: Float, ) private val ribbons = listOf( - Ribbon({ it.midHue - 40f }, 0.5f, 1.0f, 0.15f), - Ribbon({ it.lowHue }, 0.55f, 1.4f, 0.45f), - Ribbon({ it.highHue }, 0.45f, 0.7f, 0.75f), + Ribbon({ it.midHue - 40f }, 0.55f, 0f, 0.5f, -1f), + Ribbon({ it.lowHue }, 0.5f, 0.15f, 0.85f, 1f), + Ribbon({ it.highHue }, 0.45f, 0.5f, 1f, -1f), ) @Composable @@ -67,15 +75,28 @@ object AuroraRenderer : VisualizerRenderer { val n = bins.size val w = size.width val h = size.height + + // overall energy → glow thickness swells with loudness (bounded) + var energy = 0f + for (b in bins) energy += b + energy /= bins.size + val strokeW = 12f + energy * 30f + val half = strokeW / 2f + ribbons.forEachIndexed { index, r -> - val idx = (r.band * (n - 1)).toInt().coerceIn(0, n - 1) - val amp = (0.3f + bins[idx] * 1.2f) * h * 0.5f val path = paths[index] path.reset() var x = 0f while (x <= w) { val f = x / w - val y = h * r.yo + sin(f * 6f * r.sp + t * 1.5f * r.sp) * amp + sin(f * 13f + t) * amp * 0.25f + // interpolate this ribbon's band → the displacement IS the spectrum + val fb = (r.lo + (r.hi - r.lo) * f) * (n - 1) + val i0 = fb.toInt().coerceIn(0, n - 1) + val i1 = (i0 + 1).coerceAtMost(n - 1) + val v = bins[i0] + (bins[i1] - bins[i0]) * (fb - i0) + val shimmer = 1f + 0.08f * sin(f * 9f + t) + val disp = (v * shimmer).coerceIn(0f, 1f) * h * 0.40f + val y = (h * r.yo + r.dir * disp).coerceIn(half, h - half) if (x == 0f) path.moveTo(x, y) else path.lineTo(x, y) x += 5f } @@ -85,10 +106,10 @@ object AuroraRenderer : VisualizerRenderer { brush = Brush.horizontalGradient( 0f to Color.hsl(hue, pal.saturation, pal.lightness, 0f), - 0.5f to Color.hsl(hue, pal.saturation, pal.lightness, 0.55f), + 0.5f to Color.hsl(hue, pal.saturation, pal.lightness, 0.6f), 1f to Color.hsl(hue, pal.saturation, pal.lightness, 0f), ), - style = Stroke(width = 22f + bins[idx] * 26f, cap = StrokeCap.Round), + style = Stroke(width = strokeW, cap = StrokeCap.Round), blendMode = BlendMode.Plus, ) } diff --git a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/WavesRenderer.kt b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/WavesRenderer.kt index 07c7053f0c..b59c00f04d 100644 --- a/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/WavesRenderer.kt +++ b/commons/src/commonMain/kotlin/com/vitorpamplona/amethyst/commons/audio/renderers/WavesRenderer.kt @@ -36,6 +36,12 @@ import com.vitorpamplona.amethyst.commons.audio.wrapHue import kotlinx.coroutines.flow.Flow import kotlin.math.sin +/** + * Three translucent, overlapping filled waves. Each wave's OUTLINE is the live spectrum of its + * frequency band (low / mid / high), interpolated across the width, so the shape tracks the audio + * directly. A small time shimmer (±6%) adds life without driving the motion, and the height is + * clamped to 0.92·h so the fill never reaches or leaves the top edge. + */ object WavesRenderer : VisualizerRenderer { override val style = VisualizerStyle.WAVES @@ -49,9 +55,9 @@ object WavesRenderer : VisualizerRenderer { private val layers = listOf( - Layer(0f, 0.33f, { it.lowHue }, 0.95f, 0f), - Layer(0.2f, 0.7f, { it.midHue }, 0.8f, 1.5f), - Layer(0.5f, 1f, { it.highHue }, 0.7f, 3f), + Layer(0f, 0.4f, { it.lowHue }, 1.0f, 0f), + Layer(0.2f, 0.75f, { it.midHue }, 0.9f, 1.5f), + Layer(0.5f, 1f, { it.highHue }, 0.8f, 3f), ) @Composable @@ -73,11 +79,15 @@ object WavesRenderer : VisualizerRenderer { var x = 0f while (x <= w) { val f = x / w - val idx = ((layer.lo + (layer.hi - layer.lo) * f) * (n - 1)).toInt().coerceIn(0, n - 1) - val v = bins[idx] * layer.amp - val wob = 0.5f + 0.5f * sin(f * 8f + layer.phase + t * 2f) - path.lineTo(x, h - v * h * (0.45f + 0.55f * wob)) - x += 6f + // interpolate the spectrum within this layer's band → smooth, audio-driven outline + val fb = (layer.lo + (layer.hi - layer.lo) * f) * (n - 1) + val i0 = fb.toInt().coerceIn(0, n - 1) + val i1 = (i0 + 1).coerceAtMost(n - 1) + val v = (bins[i0] + (bins[i1] - bins[i0]) * (fb - i0)) * layer.amp + val shimmer = 1f + 0.06f * sin(f * 12f + layer.phase + t * 1.5f) + val height = (v * shimmer).coerceIn(0f, 1f) * h * 0.92f + path.lineTo(x, h - height) + x += 4f } path.lineTo(w, h) path.close()