App 2.45.1: Halloween's song flies across the sky on a bat's banner

Instead of a line at the foot of the hill, a bat tows a parchment
banner with the title and artist through the sky above the sign.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-11-02 14:33:14 +01:00
co-authored by Claude Opus 5.5
parent ed32fbbc1f
commit 06a61282a6
4 changed files with 50 additions and 14 deletions
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4"
minSdk 27
targetSdk 36
versionCode 160
versionName "2.45.0"
versionCode 161
versionName "2.45.1"
// 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.
@@ -27,7 +27,7 @@ import java.util.Locale;
* of power, rising with it, in orange or, where it regenerated, green. Unread values show "—" and
* light nothing. With music playing, the pumpkin's glow swells and its carving brightens on the
* beat ({@link MusicTap}); colours and the embers keep their meaning, and with no music it burns as
* before. What plays is carved into the foot of the hill ({@link NowPlaying}).
* before. What plays flies across the sky on a banner a bat tows ({@link NowPlaying}).
*
* The night sky is drawn once into a bitmap per size; only the rest goes over it. Bats, bubbles and
* the ghost move (about 30 frames a second) only while the car moves and the view is on screen;
@@ -96,10 +96,10 @@ final class HalloweenView extends ClusterCanvasView {
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
c.drawBitmap(sky, 0, 0, image);
drawBats(c, w, u);
boolean song = drawSong(c, w, u);
drawMoon(c, w, u);
drawGhost(c, w, u);
drawHill(c, w, h, u);
drawSong(c, w, h, u);
drawEmbers(c, w, u);
float pulse = music == null ? 0f : music.pulse();
drawPumpkin(c, w, u, pulse);
@@ -107,7 +107,7 @@ final class HalloweenView extends ClusterCanvasView {
drawFlask(c, u);
drawTombstone(c, w, u);
drawGears(c, w, u);
keepAnimating(moving || pulse > 0f, 33);
keepAnimating(moving || pulse > 0f || song, 33);
}
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
@@ -277,17 +277,51 @@ final class HalloweenView extends ClusterCanvasView {
}
}
// ---------- What plays, carved into the hill ----------
// ---------- What plays: a bat tows it across the sky on a banner ----------
private void drawSong(Canvas c, int w, int h, float u) {
if (v.songTitle == null) return;
text.setTypeface(thin);
text.setTextSize(20f * u);
float room = w * 0.44f;
String artist = v.songArtist == null ? "" : " — " + cut(v.songArtist, room / 2f);
/**
* A bat pulls a 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.
*/
private boolean drawSong(Canvas c, int w, float u) {
if (v.songTitle == null) return false;
text.setTypeface(face);
text.setTextSize(19f * u);
float room = Math.min(560f * u, w * 0.45f);
String artist = v.songArtist == null ? "" : " — " + cut(v.songArtist, room / 2.5f);
// A long title gives way, so the artist stays.
String s = "♪ " + cut(v.songTitle, room - text.measureText("♪ " + artist)) + artist;
label(c, s, w / 2f, h - 12f * u, 20f * u, AccentColor.withAlpha(BONE, 0xC0), thin, Paint.Align.CENTER);
float t = SystemClock.uptimeMillis() / 1000f;
float bw = text.measureText(s) + 40f * u, bh = 30f * u, tow = 56f * u;
// One pass takes about half a minute; then it comes round again from the left.
float travel = w + bw + tow + 80f * u;
float bx = (t * 70f * u) % travel - bw - tow - 40f * u;
// 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.
path.reset();
path.moveTo(bx, top);
path.lineTo(right, top);
path.lineTo(right, bottom);
path.lineTo(bx, bottom);
path.lineTo(bx + notch, yc);
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);
// The tow lines, and the bat at their end.
float batX = right + tow, batY = yc - 10f * u;
line.setColor(BAT_EDGE);
line.setStrokeWidth(1.5f * u);
c.drawLine(right, top + 3f * u, batX, batY + 6f * u, line);
c.drawLine(right, bottom - 3f * u, batX, batY + 6f * u, line);
bat(c, batX, batY, 26f * u, (float) Math.sin(t * 12f), u);
return true;
}
/** {@code s} cut with "…" to fit {@code room} in the text paint as set. */