Compare commits
2
Commits
car-2.45.2
...
car-2.46.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cdf78c6d32 | ||
|
|
a445fef407 |
@@ -77,7 +77,7 @@ Repository: `ssh://git@git.stinnesbeck.com:2222/nils/mg4.git` (remote `origin`),
|
|||||||
|
|
||||||
| Tab | Contents |
|
| Tab | Contents |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles and Halloween, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`, the pumpkin's light flaring on the kick drum while on screen, when "Pumpkin light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
| Overview | value tiles (battery, range, range/usage/energy since start, 5-min averages of range, usage, power, speed, and under the battery what plays: `NowPlaying` picks from the media sessions `NowPlayingWatcher` follows — listened to only, never connected to, nothing logged or sent; Tiles, Día de Muertos and Halloween, Settings → "Show what's playing", `PREF_NOW_PLAYING`, on by default), vehicle info (odometer, lease allowance and the odometer against it), tyre pressures; read every 0.2 s on a background thread. Styles (Settings → Overview style): Tiles, Gauges (`GaugeView` needles), Cockpit (`RingView` rings, `TraceView` power trace), Retro (`RetroView`, seven-segment VFD), Futuristic (`FlowView`, animated energy flow), Sci-fi (`SciFiView`, starship console), Cartoon (`CartoonView`), MG (`OctagonView`, octagonal dials after the classic MG dashboards), and on `ClusterCanvasView`: Minimal, Terminal, Nixie tubes, Chronograph, Grid (`GridView`, neon lines over a moving grid floor, power as a light-cycle trail from `ClusterValues.powerTrail`), Grid with map (the same `GridView` over the roads round the car, top-down: GPS while it is shown, tiles from the server's `/v1/map/roads` through `MapRoads`, cached in `files/map`; `RoadTile`, `MapGeometry`; the map is `MapLayerView`, a hardware layer drawn once and moved and turned by the GPU every display frame), Time circuits (`TimeCircuitsView`, the time machine's LED rows with `Segments14` letters and a flux capacitor), Quest (`QuestView`, a fantasy game interface), Visor (`VisorView`, a helmet display with a motion tracker of the power trail), Empire (`EmpireView`, a strategy game with an isometric world and ages by odometer, `EmpireAge`), Día de Muertos (`MuertosView`, an ofrenda: papel picado, candles, a calavera in a ring of marigolds), Halloween (`HalloweenView`, a jack-o'-lantern, a potion flask, a tombstone on a graveyard hill), both counting with `Tally`, the pumpkin's light flaring on the kick drum while on screen, when "Pumpkin light follows the music" is on (off by default, `PREF_MUSIC_LIGHT`; `MusicTap`, `MusicPulse`: Android's audio analyser, 40–120 Hz, jumps not levels; Bluetooth and CarPlay, not the radio); the drawn styles share `ClusterValues`. A style shown in its own palette (Sci-fi, Cartoon, Nixie, Time circuits, Quest, Empire, Día de Muertos, Halloween; Retro, Terminal, Grid and Visor with "Original") gives the whole app its colours (`StylePalette`, `res/values/style_palettes.xml`) instead of the accent colour. A button in the navigation's header hides the navigation for a full-width Overview (`PREF_OVERVIEW_FULLSCREEN`); a swipe in from the left edge (`EdgeSwipe`) brings it back; without the app's own full screen the car's sidebar takes that edge, so a tab at the left edge (`edge_tab`) is tapped or swiped instead. Pages change only by the navigation (the pager's own swiping is off) |
|
||||||
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
| Consumption / Charging | period totals, daily log, charging sessions and curve |
|
||||||
| Live data | on/off, ABRP API key and user token, upload frequency |
|
| Live data | on/off, ABRP API key and user token, upload frequency |
|
||||||
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
| Server | URL, pairing by QR, ingest token, installation ID (read-only), config import |
|
||||||
|
|||||||
@@ -140,6 +140,8 @@ 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
|
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`
|
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).
|
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
|
||||||
|
- 2026-11-02 — App 2.46.0: **Día de Muertos shows what plays** (owner: "Can we also put that into the dia de muertos theme?", then "switch between monarch and alebrije", "one color per song"). A strip of papel picado (zigzag-cut edges) with "♪ title — artist" waves across the sky between the flags and the ring, behind the ring and the clock; a monarch butterfly and an alebrije (a painted horned creature with feathered wings and a curled tail) tow it on alternate passes. The paper's colour is one of the flags', fixed by the song (hash of title and artist, never logged). The banner moved out of `HalloweenView` into the shared `SongBanner`; Halloween looks as before. Same switch as Tiles and Halloween (Settings → "Show what's playing"). Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
|
||||||
|
- 2026-11-02 — App 2.45.2 published (`car-2.45.2`).
|
||||||
- 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.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 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.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.
|
||||||
|
|||||||
@@ -249,7 +249,10 @@ drawing, turquoise and anticlockwise while regenerating. The altar's candles are
|
|||||||
per 50 km. The path of marigold petals along the bottom, which in the tradition leads the visitors
|
per 50 km. The path of marigold petals along the bottom, which in the tradition leads the visitors
|
||||||
home, is the last two minutes of power. On the right the clock in a tin frame, P R N D as small
|
home, is the last two minutes of power. On the right the clock in a tin frame, P R N D as small
|
||||||
marigolds, and trip, usage, climate and odometer on the altar cloth. While you drive the flags
|
marigolds, and trip, usage, climate and odometer on the altar cloth. While you drive the flags
|
||||||
sway and the flames flicker. The app turns the papel picado's pink with it.
|
sway and the flames flicker. What plays (Bluetooth, CarPlay or the radio) crosses the sky between
|
||||||
|
the flags and the ring on a waving strip of cut paper, towed by a monarch butterfly and, on the next
|
||||||
|
pass, an alebrije; the paper takes one of the flags' colours, fixed by the song. The app turns the
|
||||||
|
papel picado's pink with it.
|
||||||
|
|
||||||

|

|
||||||

|

|
||||||
@@ -268,7 +271,8 @@ this screen). With "Pumpkin light follows the music" on (Settings, shown with th
|
|||||||
default) and Bluetooth or CarPlay music playing, the pumpkin's glow flares and its carving
|
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
|
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 waving 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
|
bat tows, behind the moon, again and again while it plays (`SongBanner` draws the banner for both
|
||||||
|
this style and Día de Muertos). The app turns the
|
||||||
pumpkin's orange with it.
|
pumpkin's orange with it.
|
||||||
|
|
||||||

|

|
||||||
|
|||||||
+2
-2
@@ -34,8 +34,8 @@ android {
|
|||||||
applicationId "com.stinnesbeck.nilsmg4"
|
applicationId "com.stinnesbeck.nilsmg4"
|
||||||
minSdk 27
|
minSdk 27
|
||||||
targetSdk 36
|
targetSdk 36
|
||||||
versionCode 162
|
versionCode 163
|
||||||
versionName "2.45.2"
|
versionName "2.46.0"
|
||||||
|
|
||||||
// Nothing about the server ships in the APK: its URL and ingest token are entered in
|
// 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.
|
// the app's Server tab (ServerConfig), and the database is only reachable by the server.
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import android.graphics.Canvas;
|
|||||||
import android.graphics.LinearGradient;
|
import android.graphics.LinearGradient;
|
||||||
import android.graphics.Paint;
|
import android.graphics.Paint;
|
||||||
import android.graphics.Path;
|
import android.graphics.Path;
|
||||||
import android.graphics.PathMeasure;
|
|
||||||
import android.graphics.RadialGradient;
|
import android.graphics.RadialGradient;
|
||||||
import android.graphics.RectF;
|
import android.graphics.RectF;
|
||||||
import android.graphics.Shader;
|
import android.graphics.Shader;
|
||||||
@@ -51,8 +50,7 @@ final class HalloweenView extends ClusterCanvasView {
|
|||||||
private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG);
|
private final Paint image = new Paint(Paint.FILTER_BITMAP_FLAG);
|
||||||
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
|
private final Paint glow = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||||||
private final Path path = new Path();
|
private final Path path = new Path();
|
||||||
private final Path baseline = new Path();
|
private final SongBanner banner = new SongBanner();
|
||||||
private final PathMeasure measure = new PathMeasure();
|
|
||||||
private final RectF r = new RectF();
|
private final RectF r = new RectF();
|
||||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||||
@@ -291,71 +289,23 @@ final class HalloweenView extends ClusterCanvasView {
|
|||||||
if (v.songTitle == null) return false;
|
if (v.songTitle == null) return false;
|
||||||
text.setTypeface(face);
|
text.setTypeface(face);
|
||||||
text.setTextSize(19f * u);
|
text.setTextSize(19f * u);
|
||||||
float room = Math.min(560f * u, w * 0.45f);
|
String s = SongBanner.line(text, v.songTitle, v.songArtist, 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;
|
|
||||||
float t = SystemClock.uptimeMillis() / 1000f;
|
float t = SystemClock.uptimeMillis() / 1000f;
|
||||||
float bw = text.measureText(s) + 40f * u, bh = 30f * u, tow = 56f * u;
|
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 bx = SongBanner.left(t, w, bw, tow, u);
|
||||||
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.
|
// In the strip of sky above the sign, clear of it.
|
||||||
float yc = 34f * u + (float) Math.sin(t * 1.3f) * 4f * u;
|
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;
|
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, BONE, 0xFF8A6A3A, INK);
|
||||||
// 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();
|
|
||||||
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);
|
|
||||||
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.
|
// The tow lines, and the bat at their end.
|
||||||
float batX = right + tow, batY = yc - 10f * u;
|
float right = bx + bw, batX = right + tow, batY = yc - 10f * u;
|
||||||
line.setColor(BAT_EDGE);
|
line.setColor(BAT_EDGE);
|
||||||
line.setStrokeWidth(1.5f * u);
|
line.setStrokeWidth(1.5f * u);
|
||||||
c.drawLine(right, top + 3f * u, batX, batY + 6f * u, line);
|
c.drawLine(right, yc - bh / 2f + 3f * u, batX, batY + 6f * u, line);
|
||||||
c.drawLine(right, bottom - 3f * u, batX, batY + 6f * u, line);
|
c.drawLine(right, yc + bh / 2f - 3f * u, batX, batY + 6f * u, line);
|
||||||
bat(c, batX, batY, 26f * u, (float) Math.sin(t * 12f), u);
|
bat(c, batX, batY, 26f * u, (float) Math.sin(t * 12f), u);
|
||||||
return true;
|
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;
|
|
||||||
int fits = text.breakText(s, true, Math.max(0f, room - text.measureText("…")), null);
|
|
||||||
return s.substring(0, Math.max(0, fits)).trim() + "…";
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- The arch of embers: the power ----------
|
// ---------- The arch of embers: the power ----------
|
||||||
|
|
||||||
private void drawEmbers(Canvas c, int w, float u) {
|
private void drawEmbers(Canvas c, int w, float u) {
|
||||||
|
|||||||
@@ -1785,7 +1785,8 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
/** The styles that show what plays. */
|
/** The styles that show what plays. */
|
||||||
private static boolean showsNowPlaying(OverviewStyle style) {
|
private static boolean showsNowPlaying(OverviewStyle style) {
|
||||||
return style == OverviewStyle.TILES || style == OverviewStyle.HALLOWEEN;
|
return style == OverviewStyle.TILES || style == OverviewStyle.HALLOWEEN
|
||||||
|
|| style == OverviewStyle.MUERTOS;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The media sessions are watched only while what plays can be seen, and the switch is on. */
|
/** The media sessions are watched only while what plays can be seen, and the switch is on. */
|
||||||
|
|||||||
@@ -44,6 +44,7 @@ final class MuertosView extends ClusterCanvasView {
|
|||||||
private final Paint image = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
private final Paint image = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
|
||||||
private final Path path = new Path();
|
private final Path path = new Path();
|
||||||
private final Path flag = new Path();
|
private final Path flag = new Path();
|
||||||
|
private final SongBanner banner = new SongBanner();
|
||||||
private final RectF r = new RectF();
|
private final RectF r = new RectF();
|
||||||
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
|
||||||
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||||||
@@ -87,6 +88,7 @@ final class MuertosView extends ClusterCanvasView {
|
|||||||
c.drawBitmap(scene, 0, 0, image);
|
c.drawBitmap(scene, 0, 0, image);
|
||||||
float cx = w / 2f, cy = 345f * u;
|
float cx = w / 2f, cy = 345f * u;
|
||||||
drawFlags(c, w, u);
|
drawFlags(c, w, u);
|
||||||
|
boolean song = drawSong(c, w, u);
|
||||||
drawRing(c, cx, cy, u);
|
drawRing(c, cx, cy, u);
|
||||||
drawSpeed(c, cx, cy, u);
|
drawSpeed(c, cx, cy, u);
|
||||||
drawBattery(c, u);
|
drawBattery(c, u);
|
||||||
@@ -95,7 +97,7 @@ final class MuertosView extends ClusterCanvasView {
|
|||||||
drawClock(c, w, u);
|
drawClock(c, w, u);
|
||||||
drawGears(c, w, u);
|
drawGears(c, w, u);
|
||||||
drawRows(c, w, h, u);
|
drawRows(c, w, h, u);
|
||||||
keepAnimating(moving, 33);
|
keepAnimating(moving || song, 33);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
|
||||||
@@ -330,6 +332,165 @@ final class MuertosView extends ClusterCanvasView {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---------- What plays: a monarch or an alebrije tows it on a strip of papel picado ----------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A monarch butterfly (the souls' messengers) and an alebrije (Oaxaca's painted spirit creature)
|
||||||
|
* take turns towing a waving strip of cut paper with the song across the sky between the flags
|
||||||
|
* and the ring, behind the ring and the clock, again and again while something plays. The paper's
|
||||||
|
* colour is one of the flags', fixed by the song. 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);
|
||||||
|
String s = SongBanner.line(text, v.songTitle, v.songArtist, Math.min(560f * u, w * 0.45f));
|
||||||
|
float t = SystemClock.uptimeMillis() / 1000f;
|
||||||
|
float bw = text.measureText(s) + 40f * u, bh = 28f * u, tow = 64f * u;
|
||||||
|
float bx = SongBanner.left(t, w, bw, tow, u);
|
||||||
|
float yc = 106f * u + (float) Math.sin(t * 1.3f) * 2f * u;
|
||||||
|
int paper = PAPER[Math.floorMod((v.songTitle + "\n" + v.songArtist).hashCode(), PAPER.length)];
|
||||||
|
fill.setStyle(Paint.Style.FILL);
|
||||||
|
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 14f * u, paper, 0xCC2A0E36,
|
||||||
|
paper == PAPER[4] ? BONE : INK);
|
||||||
|
boolean butterfly = SongBanner.pass(t, w, bw, tow, u) % 2 == 0;
|
||||||
|
float right = bx + bw, cx = right + tow, cy = yc - 6f * u, size = (butterfly ? 40f : 36f) * u;
|
||||||
|
// The tow lines run to the tail.
|
||||||
|
float ax = cx - (butterfly ? 0.4f : 0.6f) * size, ay = cy - (butterfly ? 0f : 0.35f * size);
|
||||||
|
line.setColor(0x99FFFFFF);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
|
||||||
|
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
|
||||||
|
float flap = (float) Math.sin(t * (butterfly ? 9f : 7f));
|
||||||
|
if (butterfly) monarch(c, cx, cy, size, flap, u);
|
||||||
|
else alebrije(c, cx, cy, size, flap, u);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A monarch seen from above, flying right: orange wings veined and edged in black, white dots. */
|
||||||
|
private void monarch(Canvas c, float x, float y, float s, float flap, float u) {
|
||||||
|
float k = 0.3f + 0.7f * Math.abs(flap);
|
||||||
|
c.save();
|
||||||
|
c.translate(x, y);
|
||||||
|
for (int side = 0; side < 2; side++) {
|
||||||
|
c.save();
|
||||||
|
if (side == 1) c.scale(1f, -1f);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(0.3f * s, 0);
|
||||||
|
path.quadTo(0.3f * s, -0.75f * s * k, -0.02f * s, -0.95f * s * k);
|
||||||
|
path.quadTo(-0.45f * s, -0.7f * s * k, -0.1f * s, 0);
|
||||||
|
path.close();
|
||||||
|
path.moveTo(-0.05f * s, 0);
|
||||||
|
path.quadTo(-0.4f * s, -0.55f * s * k, -0.7f * s, -0.4f * s * k);
|
||||||
|
path.quadTo(-0.7f * s, -0.1f * s * k, -0.3f * s, 0);
|
||||||
|
path.close();
|
||||||
|
fill.setColor(0xFFFF8A1F);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(0xFF1A0A10);
|
||||||
|
line.setStrokeWidth(2.2f * u);
|
||||||
|
line.setStrokeJoin(Paint.Join.ROUND);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
line.setStrokeWidth(1.2f * u);
|
||||||
|
c.drawLine(0.1f * s, 0, -0.02f * s, -0.88f * s * k, line);
|
||||||
|
c.drawLine(0.05f * s, 0, -0.3f * s, -0.62f * s * k, line);
|
||||||
|
c.drawLine(-0.1f * s, 0, -0.62f * s, -0.36f * s * k, line);
|
||||||
|
fill.setColor(0xFFFFFFFF);
|
||||||
|
for (float[] d : new float[][] { { 0.14f, -0.74f }, { -0.1f, -0.88f }, { -0.3f, -0.72f }, { -0.62f, -0.36f } }) {
|
||||||
|
c.drawCircle(d[0] * s, d[1] * s * k, 1.6f * u, fill);
|
||||||
|
}
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
fill.setColor(0xFF1A0A10);
|
||||||
|
r.set(-0.4f * s, -0.07f * s, 0.35f * s, 0.07f * s);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
c.drawCircle(0.38f * s, 0, 0.08f * s, fill);
|
||||||
|
line.setColor(0xFF1A0A10);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawLine(0.42f * s, -0.04f * s, 0.62f * s, -0.2f * s, line);
|
||||||
|
c.drawLine(0.42f * s, 0.04f * s, 0.62f * s, 0.2f * s, line);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
|
/** An alebrije flying right: a painted horned creature with feathered wings and a curled tail. */
|
||||||
|
private void alebrije(Canvas c, float x, float y, float s, float flap, float u) {
|
||||||
|
int[] feathers = { TURQUOISE, GOLD, PINK, MARIGOLD, PAPER[4] };
|
||||||
|
c.save();
|
||||||
|
c.translate(x, y);
|
||||||
|
// The wing, behind the body: feathers fanned from the shoulder, lifted and dropped by the flap.
|
||||||
|
for (int i = 0; i < feathers.length; i++) {
|
||||||
|
c.save();
|
||||||
|
c.translate(-0.05f * s, -0.2f * s);
|
||||||
|
c.rotate(-125f + flap * 30f + i * 15f);
|
||||||
|
r.set(0, -0.07f * s, 0.8f * s, 0.07f * s);
|
||||||
|
fill.setColor(feathers[i]);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
line.setColor(INK);
|
||||||
|
line.setStrokeWidth(1.5f * u);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
// The curled tail.
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(-0.4f * s, 0.05f * s);
|
||||||
|
path.cubicTo(-0.8f * s, 0.1f * s, -0.85f * s, -0.3f * s, -0.6f * s, -0.35f * s);
|
||||||
|
line.setColor(PINK);
|
||||||
|
line.setStrokeWidth(0.1f * s);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
c.drawCircle(-0.6f * s, -0.35f * s, 0.07f * s, fill);
|
||||||
|
// Legs.
|
||||||
|
line.setColor(INK);
|
||||||
|
line.setStrokeWidth(3f * u);
|
||||||
|
c.drawLine(-0.25f * s, 0.15f * s, -0.3f * s, 0.42f * s, line);
|
||||||
|
c.drawLine(0.15f * s, 0.15f * s, 0.2f * s, 0.42f * s, line);
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
c.drawCircle(-0.3f * s, 0.43f * s, 0.05f * s, fill);
|
||||||
|
c.drawCircle(0.2f * s, 0.43f * s, 0.05f * s, fill);
|
||||||
|
// The body, painted with stripes and dots.
|
||||||
|
r.set(-0.45f * s, -0.2f * s, 0.35f * s, 0.2f * s);
|
||||||
|
fill.setColor(PINK);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
line.setColor(INK);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
line.setColor(TURQUOISE);
|
||||||
|
line.setStrokeWidth(0.05f * s);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(-0.35f * s, 0f);
|
||||||
|
for (int i = 1; i <= 6; i++) path.lineTo((-0.35f + 0.1f * i) * s, (i % 2 == 0 ? 0f : -0.07f) * s);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
for (float[] d : new float[][] { { -0.3f, 0.1f }, { -0.1f, 0.12f }, { 0.1f, 0.1f }, { 0.25f, 0.05f } }) {
|
||||||
|
c.drawCircle(d[0] * s, d[1] * s, 0.035f * s, fill);
|
||||||
|
}
|
||||||
|
// Horns, the head, snout, eye.
|
||||||
|
line.setColor(GOLD);
|
||||||
|
line.setStrokeWidth(3.5f * u);
|
||||||
|
path.reset();
|
||||||
|
path.moveTo(0.35f * s, -0.38f * s);
|
||||||
|
path.quadTo(0.2f * s, -0.55f * s, 0.3f * s, -0.78f * s);
|
||||||
|
path.moveTo(0.5f * s, -0.38f * s);
|
||||||
|
path.quadTo(0.65f * s, -0.52f * s, 0.55f * s, -0.78f * s);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
fill.setColor(MARIGOLD);
|
||||||
|
c.drawCircle(0.42f * s, -0.22f * s, 0.2f * s, fill);
|
||||||
|
line.setColor(INK);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawCircle(0.42f * s, -0.22f * s, 0.2f * s, line);
|
||||||
|
r.set(0.5f * s, -0.22f * s, 0.85f * s, -0.06f * s);
|
||||||
|
fill.setColor(GOLD);
|
||||||
|
c.drawOval(r, fill);
|
||||||
|
c.drawOval(r, line);
|
||||||
|
fill.setColor(PINK);
|
||||||
|
c.drawCircle(0.8f * s, -0.15f * s, 0.035f * s, fill);
|
||||||
|
fill.setColor(BONE);
|
||||||
|
c.drawCircle(0.45f * s, -0.28f * s, 0.07f * s, fill);
|
||||||
|
fill.setColor(INK);
|
||||||
|
c.drawCircle(0.47f * s, -0.28f * s, 0.035f * s, fill);
|
||||||
|
c.restore();
|
||||||
|
}
|
||||||
|
|
||||||
private void drawBattery(Canvas c, float u) {
|
private void drawBattery(Canvas c, float u) {
|
||||||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||||||
float x = 36f * u;
|
float x = 36f * u;
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.evsuite.abrp;
|
||||||
|
|
||||||
|
import android.graphics.Canvas;
|
||||||
|
import android.graphics.Paint;
|
||||||
|
import android.graphics.Path;
|
||||||
|
import android.graphics.PathMeasure;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The strip of paper a creature tows across a style's sky with the song on it, shared by the styles
|
||||||
|
* that show what plays (Halloween's parchment, Día de Muertos's papel picado). The ribbon waves in
|
||||||
|
* the wind: still where the creature holds it, rippling more towards its tail, which is cut as a
|
||||||
|
* swallowtail, and the text follows the waves. The style draws the creature and the tow lines. Keeps
|
||||||
|
* only the paths it reuses between frames.
|
||||||
|
*/
|
||||||
|
final class SongBanner {
|
||||||
|
|
||||||
|
private static final float SPEED = 70f;
|
||||||
|
private final Path path = new Path();
|
||||||
|
private final Path baseline = new Path();
|
||||||
|
private final PathMeasure measure = new PathMeasure();
|
||||||
|
|
||||||
|
/** One pass takes about half a minute; then the creature comes round again from the left. */
|
||||||
|
static float travel(int w, float bw, float tow, float u) {
|
||||||
|
return w + bw + tow + 80f * u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Where the banner's left end is at {@code t} seconds. */
|
||||||
|
static float left(float t, int w, float bw, float tow, float u) {
|
||||||
|
return (t * SPEED * u) % travel(w, bw, tow, u) - bw - tow - 40f * u;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** How many times it has crossed by {@code t}, to let two creatures take turns. */
|
||||||
|
static int pass(float t, int w, float bw, float tow, float u) {
|
||||||
|
return (int) (t * SPEED * u / travel(w, bw, tow, u));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Draws the banner from {@code bx} to {@code bx + bw}, centred on {@code yc}. {@code tooth} above
|
||||||
|
* zero cuts both long edges in zigzags of that width (cut paper). The text paint is set up by the
|
||||||
|
* caller (typeface and size); its colour and alignment are set here.
|
||||||
|
*/
|
||||||
|
void draw(Canvas c, Paint fill, Paint line, Paint text, String s, float bx, float yc, float bw, float bh,
|
||||||
|
float t, float u, float tooth, int paper, int edge, int ink) {
|
||||||
|
float top = yc - bh / 2f, bottom = yc + bh / 2f, right = bx + bw, notch = 12f * u;
|
||||||
|
float cut = tooth > 0f ? 3.5f * u : 0f;
|
||||||
|
int steps = Math.max(8, Math.round(bw / (tooth > 0f ? tooth / 2f : 12f * u)));
|
||||||
|
path.reset();
|
||||||
|
for (int i = 0; i <= steps; i++) {
|
||||||
|
float x = right - bw * i / steps;
|
||||||
|
float y = top + wave(x, right, bw, t, u) - (i % 2 == 1 ? cut : 0f);
|
||||||
|
if (i == 0) path.moveTo(x, y);
|
||||||
|
else path.lineTo(x, y);
|
||||||
|
}
|
||||||
|
path.lineTo(bx + notch, yc + wave(bx + notch, right, bw, t, u));
|
||||||
|
for (int i = steps; i >= 0; i--) {
|
||||||
|
float x = right - bw * i / steps;
|
||||||
|
path.lineTo(x, bottom + wave(x, right, bw, t, u) + (i % 2 == 1 ? cut : 0f));
|
||||||
|
}
|
||||||
|
path.close();
|
||||||
|
fill.setColor(paper);
|
||||||
|
c.drawPath(path, fill);
|
||||||
|
line.setColor(edge);
|
||||||
|
line.setStrokeWidth(2f * u);
|
||||||
|
c.drawPath(path, line);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* How far the banner bends at {@code x}: nothing at the creature's end ({@code right}), up to 7
|
||||||
|
* units at the tail, in waves that run from the creature 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** "♪ title — artist" in the text paint as set, cut with "…" to fit {@code room}; the title gives way first. */
|
||||||
|
static String line(Paint text, String title, String artist, float room) {
|
||||||
|
String a = artist == null ? "" : " — " + cut(text, artist, room / 2.5f);
|
||||||
|
return "♪ " + cut(text, title, room - text.measureText("♪ " + a)) + a;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String cut(Paint text, String s, float room) {
|
||||||
|
if (text.measureText(s) <= room) return s;
|
||||||
|
int fits = text.breakText(s, true, Math.max(0f, room - text.measureText("…")), null);
|
||||||
|
return s.substring(0, Math.max(0, fits)).trim() + "…";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user