From f9813c2afd17a3978af5cdf4442f4ba59d95d70d Mon Sep 17 00:00:00 2001 From: Nils Stinnesbeck Date: Mon, 2 Nov 2026 14:45:35 +0100 Subject: [PATCH] App 2.45.2: Halloween's song banner waves Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6 --- SPECS.md | 1 + THEMES.md | 2 +- app/build.gradle | 4 +- .../java/com/evsuite/abrp/HalloweenView.java | 41 +++++++++++++++---- 4 files changed, 37 insertions(+), 11 deletions(-) diff --git a/SPECS.md b/SPECS.md index 9decef8..ef9228d 100644 --- a/SPECS.md +++ b/SPECS.md @@ -140,6 +140,7 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does * confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). +- 2026-11-02 — App 2.45.2: Halloween's song banner **waves** (owner: "Can it also be a bit wavy?"): waves run from the bat to the tail, still where the bat holds it and up to 7 units at the tail, and the text follows them (`drawTextOnPath`). Tested: emulator with the throwaway fake session, unit tests, lint. Not tried on the car. - 2026-11-02 — App 2.45.1 published (`car-2.45.1`). - 2026-11-02 — App 2.45.1: **Halloween's song flies through the sky** (owner: "Instead of taking up screen space on the bottom, can we somehow include the title in the theme itself? ... have text fly through the air"). A bat tows a parchment banner with "♪ title — artist" from left to right across the strip of sky above the sign, behind the moon, one pass about every half minute, again and again while something plays (the title is cut first so the artist stays); the line at the foot of the hill is gone. While a banner flies the view draws about 30 frames a second, as it does while driving. Tested: emulator with the throwaway fake session (banner clear of the sign along the whole pass), unit tests, lint. Not tried on the car. - 2026-11-02 — App 2.45.0 published (`car-2.45.0`). diff --git a/THEMES.md b/THEMES.md index 2bdb32c..a4f3dd9 100644 --- a/THEMES.md +++ b/THEMES.md @@ -267,7 +267,7 @@ you drive the bats fly, the potion bubbles and the ghost floats (where there is this screen). With "Pumpkin light follows the music" on (Settings, shown with this style; off by default) and Bluetooth or CarPlay music playing, the pumpkin's glow flares and its carving brightens on each kick drum; the radio cannot be heard, so it burns as usual then. What plays -(Bluetooth, CarPlay or the radio) flies across the top of the sky on a parchment banner that a +(Bluetooth, CarPlay or the radio) flies across the top of the sky on a waving parchment banner that a bat tows, behind the moon, again and again while it plays. The app turns the pumpkin's orange with it. diff --git a/app/build.gradle b/app/build.gradle index 037e240..230bffb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -34,8 +34,8 @@ android { applicationId "com.stinnesbeck.nilsmg4" minSdk 27 targetSdk 36 - versionCode 161 - versionName "2.45.1" + versionCode 162 + versionName "2.45.2" // Nothing about the server ships in the APK: its URL and ingest token are entered in // the app's Server tab (ServerConfig), and the database is only reachable by the server. diff --git a/app/src/main/java/com/evsuite/abrp/HalloweenView.java b/app/src/main/java/com/evsuite/abrp/HalloweenView.java index 7b5a13b..635ead2 100644 --- a/app/src/main/java/com/evsuite/abrp/HalloweenView.java +++ b/app/src/main/java/com/evsuite/abrp/HalloweenView.java @@ -6,6 +6,7 @@ import android.graphics.Canvas; import android.graphics.LinearGradient; import android.graphics.Paint; import android.graphics.Path; +import android.graphics.PathMeasure; import android.graphics.RadialGradient; import android.graphics.RectF; import android.graphics.Shader; @@ -50,6 +51,8 @@ final class HalloweenView extends ClusterCanvasView { private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG); private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG); private final Path path = new Path(); + private final Path baseline = new Path(); + private final PathMeasure measure = new PathMeasure(); private final RectF r = new RectF(); private final Typeface serif = Typeface.create("serif", Typeface.BOLD); private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD); @@ -280,7 +283,7 @@ final class HalloweenView extends ClusterCanvasView { // ---------- What plays: a bat tows it across the sky on a banner ---------- /** - * A bat pulls a parchment banner with the song from left to right, high in the sky, behind the + * A bat pulls a waving parchment banner with the song from left to right, high in the sky, behind the * sign and the moon, again and again while something plays. Returns whether it is drawn, so * the view keeps animating for it. */ @@ -300,20 +303,33 @@ final class HalloweenView extends ClusterCanvasView { // In the strip of sky above the sign, clear of it. float yc = 34f * u + (float) Math.sin(t * 1.3f) * 4f * u; float top = yc - bh / 2f, bottom = yc + bh / 2f, right = bx + bw, notch = 12f * u; - // The ribbon, its trailing end cut as a swallowtail. + // The ribbon, waving in the wind: still where the bat holds it, rippling more towards its + // tail, which is cut as a swallowtail. The text follows the waves. + int steps = Math.max(8, Math.round(bw / (12f * u))); path.reset(); - path.moveTo(bx, top); - path.lineTo(right, top); - path.lineTo(right, bottom); - path.lineTo(bx, bottom); - path.lineTo(bx + notch, yc); + for (int i = 0; i <= steps; i++) { + float x = right - bw * i / steps; + if (i == 0) path.moveTo(x, top + wave(x, right, bw, t, u)); + else path.lineTo(x, top + wave(x, right, bw, t, u)); + } + path.lineTo(bx + notch, yc + wave(bx + notch, right, bw, t, u)); + for (int i = steps; i >= 0; i--) path.lineTo(right - bw * i / steps, bottom + wave(right - bw * i / steps, right, bw, t, u)); path.close(); fill.setColor(BONE); c.drawPath(path, fill); line.setColor(0xFF8A6A3A); line.setStrokeWidth(2f * u); c.drawPath(path, line); - label(c, s, bx + notch + (bw - notch) / 2f, yc + 7f * u, 19f * u, INK, face, Paint.Align.CENTER); + baseline.reset(); + for (int i = 0; i <= steps; i++) { + float x = bx + notch + (bw - notch) * i / steps; + if (i == 0) baseline.moveTo(x, yc + wave(x, right, bw, t, u)); + else baseline.lineTo(x, yc + wave(x, right, bw, t, u)); + } + measure.setPath(baseline, false); + text.setColor(INK); + text.setTextAlign(Paint.Align.LEFT); + c.drawTextOnPath(s, baseline, Math.max(0f, (measure.getLength() - text.measureText(s)) / 2f), 7f * u, text); // The tow lines, and the bat at their end. float batX = right + tow, batY = yc - 10f * u; line.setColor(BAT_EDGE); @@ -324,6 +340,15 @@ final class HalloweenView extends ClusterCanvasView { return true; } + /** + * How far the banner bends at {@code x}: nothing at the bat's end ({@code right}), up to 7 units + * at the tail, in waves that run from the bat to the tail. + */ + private static float wave(float x, float right, float bw, float t, float u) { + float along = right - x; + return 7f * u * (along / bw) * (float) Math.sin(along / (170f * u) * 2f * Math.PI - t * 5f); + } + /** {@code s} cut with "…" to fit {@code room} in the text paint as set. */ private String cut(String s, float room) { if (text.measureText(s) <= room) return s;