App 2.45.2: Halloween's song banner waves

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:45:35 +01:00
co-authored by Claude Opus 5.5
parent 3a532cd498
commit f9813c2afd
4 changed files with 37 additions and 11 deletions
+2 -2
View File
@@ -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.
@@ -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;