App 2.47.0: now playing in Quest, Empire, Terminal and Sci-fi

Quest: a dragon tows a scroll. Empire: an eagle flies one. Terminal: a
PLAYING row. Sci-fi: the song in black on the top bar.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-11-02 22:20:11 +01:00
co-authored by Claude Sonnet 5.5
parent 83dc8b231d
commit d80221db15
10 changed files with 223 additions and 5 deletions
+1 -1
View File
@@ -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, 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) | | 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, Terminal, Sci-fi, Quest, Empire, 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 |
+1
View File
@@ -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 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.47.0: **now playing in four more styles** (owner: "let's implement Now playing in more styles"; Spanish labels declined). Quest: a red dragon tows a waving scroll across the top of the sky, behind the frames. Empire: an eagle flies a scroll across the top of the world, behind the speed. Terminal: a PLAYING row above the prompt. Sci-fi: the song in black on the top bar. The creatures are in the new `SongCarriers` (dragon, eagle) beside `SongBanner`. Same switch as before (Settings → "Show what's playing"). Not yet in the rest: Gauges, Cockpit, Retro, Futuristic, MG, Minimal, Nixie, Chronograph, Grid, Grid with map, Time circuits, Visor, Cartoon. Tested: unit tests, lint, emulator with the throwaway fake session. Not tried on the car.
- 2026-11-02 — App 2.46.0 published (`car-2.46.0`). - 2026-11-02 — App 2.46.0 published (`car-2.46.0`).
- 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.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 published (`car-2.45.2`).
+4
View File
@@ -94,6 +94,7 @@ A starship console, after the look of the computer screens in 90s science-fictio
"elbow" frames in orange and lilac, pill-shaped labels, condensed capitals and blocky bar graphs "elbow" frames in orange and lilac, pill-shaped labels, condensed capitals and blocky bar graphs
on black. Below 20 % charge the frames turn red: red alert. The app takes on the lilac of the on black. Below 20 % charge the frames turn red: red alert. The app takes on the lilac of the
lower frame. lower frame.
What plays (Bluetooth, CarPlay or the radio) is written in black on the top bar, cut to its length.
![Sci-fi with the navigation](screenshots/themes/scifi-panel.png) ![Sci-fi with the navigation](screenshots/themes/scifi-panel.png)
![Sci-fi, full screen](screenshots/themes/scifi.png) ![Sci-fi, full screen](screenshots/themes/scifi.png)
@@ -131,6 +132,7 @@ driving.
As if a shell printed the car's state: `watch -n 0.2 vehicle-status` on a green-phosphor screen As if a shell printed the car's state: `watch -n 0.2 vehicle-status` on a green-phosphor screen
with faint scan lines, each value on a row with an ASCII bar, the gear as `[D]`, and a prompt with with faint scan lines, each value on a row with an ASCII bar, the gear as `[D]`, and a prompt with
a blinking cursor. A low battery turns its row amber. In green (shown) or in the accent colour. a blinking cursor. A low battery turns its row amber. In green (shown) or in the accent colour.
What plays (Bluetooth, CarPlay or the radio) shows as a PLAYING row above the prompt, cut to the width.
![Terminal with the navigation](screenshots/themes/terminal-panel.png) ![Terminal with the navigation](screenshots/themes/terminal-panel.png)
![Terminal, full screen](screenshots/themes/terminal.png) ![Terminal, full screen](screenshots/themes/terminal.png)
@@ -206,6 +208,7 @@ energy bar; a minimap with the clock; the speed in large gold numbers over a cas
are the trip, the usage and the climate; a chat window with system lines; P R N D as action are the trip, the usage and the climate; a chat window with system lines; P R N D as action
buttons, and the odometer as the experience bar, a level per 1000 km. Below 20 % a raid warning buttons, and the odometer as the experience bar, a level per 1000 km. Below 20 % a raid warning
flashes "Low battery!". The app turns gold with it. flashes "Low battery!". The app turns gold with it.
What plays (Bluetooth, CarPlay or the radio) crosses the top of the sky on a waving scroll towed by a red dragon, behind the frames, again and again while it plays.
![Quest with the navigation](screenshots/themes/quest-panel.png) ![Quest with the navigation](screenshots/themes/quest-panel.png)
![Quest, full screen](screenshots/themes/quest.png) ![Quest, full screen](screenshots/themes/quest.png)
@@ -234,6 +237,7 @@ while the ground and trees move past as fast as you drive and the speed stands l
land. The carved panel at the bottom has P R N D as command buttons, the unit's portrait and land. The carved panel at the bottom has P R N D as command buttons, the unit's portrait and
stats, and a diamond minimap with the odometer and the way to the next age. Below 20 % it warns stats, and a diamond minimap with the odometer and the way to the next age. Below 20 % it warns
"Your battery is running low!". The app turns the player's blue with it. "Your battery is running low!". The app turns the player's blue with it.
What plays (Bluetooth, CarPlay or the radio) crosses the top of the world on a waving scroll flown by an eagle, behind the speed, again and again while it plays.
![Empire with the navigation](screenshots/themes/empire-panel.png) ![Empire with the navigation](screenshots/themes/empire-panel.png)
![Empire, full screen](screenshots/themes/empire.png) ![Empire, full screen](screenshots/themes/empire.png)
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 163 versionCode 164
versionName "2.46.0" versionName "2.47.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.
@@ -38,6 +38,7 @@ final class EmpireView extends ClusterCanvasView {
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Path path = new Path(); private final Path path = 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(Typeface.SERIF, Typeface.BOLD); private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD); private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
@@ -61,10 +62,38 @@ final class EmpireView extends ClusterCanvasView {
advance(moving); advance(moving);
float top = 46f * u, bottom = h - 186f * u; float top = 46f * u, bottom = h - 186f * u;
drawWorld(c, w, top, bottom, u); drawWorld(c, w, top, bottom, u);
boolean song = drawSong(c, w, top, u);
drawSpeed(c, w, top, u); drawSpeed(c, w, top, u);
drawResources(c, w, top, u); drawResources(c, w, top, u);
drawPanel(c, w, h, bottom, u); drawPanel(c, w, h, bottom, u);
keepAnimating(moving, 33); keepAnimating(moving || song, 33);
}
// ---------- What plays: an eagle brings it on a scroll ----------
/**
* An eagle flies a waving scroll with the song across the top of the world, behind the speed,
* again and again while something plays. Returns whether it is drawn, so the view keeps animating.
*/
private boolean drawSong(Canvas c, float w, float top, float u) {
if (v.songTitle == null) return false;
text.setTypeface(serif);
text.setTextSize(19f * u);
text.setStyle(Paint.Style.FILL);
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, size = 58f * u, tow = size * 1.0f;
float bx = SongBanner.left(t, Math.round(w), bw, tow, u);
float yc = top + 30f * u + (float) Math.sin(t * 1.3f) * 3f * u;
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, 0xFFEFE0B8, WOOD_DARK, 0xFF2A1A08);
float right = bx + bw, cx = right + tow, cy = yc - 10f * u;
line.setColor(0xCCFFFFFF);
line.setStrokeWidth(1.5f * u);
float ax = cx + SongCarriers.eagleTail[0] * size, ay = cy + SongCarriers.eagleTail[1] * size;
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
SongCarriers.eagle(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 7f), t, u);
return true;
} }
private void advance(boolean moving) { private void advance(boolean moving) {
@@ -1786,7 +1786,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; || style == OverviewStyle.MUERTOS || style == OverviewStyle.QUEST || style == OverviewStyle.EMPIRE
|| style == OverviewStyle.TERMINAL || style == OverviewStyle.SCIFI;
} }
/** 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. */
@@ -37,6 +37,7 @@ final class QuestView extends ClusterCanvasView {
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG); private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint skyPaint = new Paint(); private final Paint skyPaint = new Paint();
private final Path path = new Path(); private final Path path = 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(Typeface.SERIF, Typeface.BOLD); private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD); private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
@@ -54,6 +55,7 @@ final class QuestView extends ClusterCanvasView {
if (w <= 0 || h <= 0) return; if (w <= 0 || h <= 0) return;
float u = Math.min(h / 640f, w / 1100f); float u = Math.min(h / 640f, w / 1100f);
drawLandscape(c, w, h); drawLandscape(c, w, h);
boolean song = drawSong(c, w, u);
drawPlayer(c, u); drawPlayer(c, u);
drawMinimap(c, w, u); drawMinimap(c, w, u);
drawSpeed(c, w, u); drawSpeed(c, w, u);
@@ -62,6 +64,35 @@ final class QuestView extends ClusterCanvasView {
drawChat(c, u); drawChat(c, u);
drawActionBar(c, w, h, u); drawActionBar(c, w, h, u);
drawXp(c, w, h, u); drawXp(c, w, h, u);
keepAnimating(song, 33);
}
// ---------- What plays: a dragon tows it on a scroll ----------
/**
* A dragon tows a waving scroll with the song across the top of the sky, behind the frames, again
* and again while something plays. Returns whether it is drawn, so the view keeps animating.
*/
private boolean drawSong(Canvas c, float w, float u) {
if (v.songTitle == null) return false;
text.setTypeface(serif);
text.setTextSize(19f * u);
text.setStyle(Paint.Style.FILL);
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, size = 58f * u, tow = size * 1.3f;
int wi = Math.round(w);
float bx = SongBanner.left(t, wi, bw, tow, u);
float yc = 72f * u + (float) Math.sin(t * 1.3f) * 3f * u;
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 0f, 0xFFE8D5A8, GOLD_DARK, 0xFF2A1A08);
float right = bx + bw, cx = right + tow, cy = yc - 8f * u;
line.setColor(0xCCFFFFFF);
line.setStrokeWidth(1.5f * u);
float ax = cx + SongCarriers.dragonTail[0] * size, ay = cy + SongCarriers.dragonTail[1] * size;
c.drawLine(right, yc - bh / 2f + 3f * u, ax, ay, line);
c.drawLine(right, yc + bh / 2f - 3f * u, ax, ay, line);
SongCarriers.dragon(c, fill, line, path, r, cx, cy, size, (float) Math.sin(t * 6f), t, u);
return true;
} }
// ---------- World ---------- // ---------- World ----------
@@ -67,6 +67,13 @@ public final class SciFiView extends View {
label(c, redAlert ? "RED ALERT · LOW RESERVES" : "PROPULSION STATUS", w * 0.61f + 10f * d, label(c, redAlert ? "RED ALERT · LOW RESERVES" : "PROPULSION STATUS", w * 0.61f + 10f * d,
bar * 0.86f, bar * 1.05f, redAlert ? alert : orange, Paint.Align.LEFT); bar * 0.86f, bar * 1.05f, redAlert ? alert : orange, Paint.Align.LEFT);
pill(c, w * 0.94f, 0, w, bar, frame2); pill(c, w * 0.94f, 0, w, bar, frame2);
if (v.songTitle != null) {
// What plays, in black on the top bar, cut to its length.
float x = side + 16f * d, size = bar * 0.8f;
text.setTextSize(size);
String s = SongBanner.line(text, v.songTitle, v.songArtist, w * 0.60f - x - 16f * d);
label(c, s, x, bar * 0.78f, size, 0xFF000000, Paint.Align.LEFT);
}
// The bottom bar's end: odometer and time. // The bottom bar's end: odometer and time.
String odo = v.odometerKm == null ? "—" : String.format(Locale.US, "%06.0f", v.odometerKm); String odo = v.odometerKm == null ? "—" : String.format(Locale.US, "%06.0f", v.odometerKm);
label(c, "ODO " + odo + " · " + (v.clock.isEmpty() ? "—" : v.clock), label(c, "ODO " + odo + " · " + (v.clock.isEmpty() ? "—" : v.clock),
@@ -0,0 +1,137 @@
package com.evsuite.abrp;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
/**
* The creatures that tow a {@link SongBanner} in the styles with a sky for it, drawn flying right
* with their centre at (x, y) and about {@code s} wide: a dragon (Quest) and an eagle (Empire).
* {@code flap} runs from −1 to 1 with the wing beat, {@code t} is the time in seconds. The caller
* passes its own paints and scratch objects; the paints' colour and width are set here. Where the
* tow lines tie on is {@link #dragonTail} and {@link #eagleTail}, as offsets in units of {@code s}.
*/
final class SongCarriers {
static final float[] dragonTail = { -1.0f, -0.25f }, eagleTail = { -0.55f, 0.05f };
private SongCarriers() {}
static void dragon(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
float flap, float t, float u) {
final int red = 0xFFB3262E, dark = 0xFF6E1219, belly = 0xFFE8B84A;
c.save();
c.translate(x, y);
// The wing behind the body: a membrane between three fingers, lifted and dropped by the flap.
c.save();
c.translate(-0.05f * s, -0.2f * s);
c.rotate(-120f + flap * 30f);
path.reset();
path.moveTo(0, 0);
path.lineTo(0.95f * s, -0.05f * s);
path.quadTo(0.8f * s, 0.15f * s, 0.62f * s, 0.12f * s);
path.quadTo(0.5f * s, 0.3f * s, 0.3f * s, 0.22f * s);
path.quadTo(0.2f * s, 0.35f * s, 0, 0.15f * s);
path.close();
fill.setColor(dark);
c.drawPath(path, fill);
line.setColor(0xFF2A0508);
line.setStrokeWidth(2f * u);
c.drawPath(path, line);
c.drawLine(0, 0.05f * s, 0.62f * s, 0.12f * s, line);
c.restore();
// The tail, ending in a spade.
path.reset();
path.moveTo(-0.4f * s, 0f);
path.cubicTo(-0.9f * s, 0.1f * s, -0.8f * s, -0.3f * s, -1.0f * s, -0.25f * s);
line.setColor(red);
line.setStrokeWidth(0.13f * s);
line.setStrokeCap(Paint.Cap.ROUND);
c.drawPath(path, line);
line.setStrokeCap(Paint.Cap.BUTT);
// Legs.
line.setColor(dark);
line.setStrokeWidth(3f * u);
c.drawLine(-0.25f * s, 0.12f * s, -0.3f * s, 0.34f * s, line);
c.drawLine(0.1f * s, 0.12f * s, 0.18f * s, 0.34f * s, line);
// Body with a pale belly.
r.set(-0.45f * s, -0.17f * s, 0.3f * s, 0.17f * s);
fill.setColor(red);
c.drawOval(r, fill);
r.set(-0.4f * s, 0.0f, 0.25f * s, 0.15f * s);
fill.setColor(belly);
c.drawOval(r, fill);
// Neck and head, a horn, an eye, and a small flame at the mouth.
path.reset();
path.moveTo(0.2f * s, -0.05f * s);
path.quadTo(0.4f * s, -0.05f * s, 0.5f * s, -0.3f * s);
line.setColor(red);
line.setStrokeWidth(0.15f * s);
c.drawPath(path, line);
r.set(0.46f * s, -0.45f * s, 0.86f * s, -0.24f * s);
fill.setColor(red);
c.drawOval(r, fill);
line.setColor(belly);
line.setStrokeWidth(3f * u);
c.drawLine(0.5f * s, -0.44f * s, 0.38f * s, -0.62f * s, line);
fill.setColor(0xFFFFE04A);
c.drawCircle(0.62f * s, -0.37f * s, 0.035f * s, fill);
fill.setColor(0xFF2A0508);
c.drawCircle(0.63f * s, -0.37f * s, 0.016f * s, fill);
float flick = 0.8f + 0.2f * (float) Math.sin(t * 25f);
fill.setColor(0xFFFF8A1F);
c.drawCircle(0.93f * s, -0.33f * s, 0.07f * s * flick, fill);
fill.setColor(0xFFFFE04A);
c.drawCircle(0.95f * s, -0.33f * s, 0.035f * s * flick, fill);
c.restore();
}
static void eagle(Canvas c, Paint fill, Paint line, Path path, RectF r, float x, float y, float s,
float flap, float t, float u) {
final int brown = 0xFF6B4423, dark = 0xFF3E2610, white = 0xFFF4EAD2, beak = 0xFFF2B21B;
c.save();
c.translate(x, y);
// The tail, a short fan.
path.reset();
path.moveTo(-0.3f * s, 0f);
path.lineTo(-0.58f * s, -0.1f * s);
path.lineTo(-0.6f * s, 0.02f * s);
path.lineTo(-0.55f * s, 0.14f * s);
path.close();
fill.setColor(dark);
c.drawPath(path, fill);
// The far wing, then the near one: feathers fanned from the shoulder, lifted by the flap.
for (int wing = 0; wing < 2; wing++) {
for (int i = 0; i < 5; i++) {
c.save();
c.translate(0.02f * s, -0.08f * s);
c.rotate(-130f + flap * 32f + i * 13f + (wing == 0 ? 8f : 0f));
r.set(0, -0.05f * s, (0.95f - 0.08f * Math.abs(i - 2)) * s, 0.05f * s);
fill.setColor(wing == 0 ? dark : (i % 2 == 0 ? brown : 0xFF84552E));
c.drawOval(r, fill);
c.restore();
}
}
// Body, white head, hooked beak, eye, talons.
r.set(-0.35f * s, -0.14f * s, 0.3f * s, 0.14f * s);
fill.setColor(brown);
c.drawOval(r, fill);
fill.setColor(white);
c.drawCircle(0.34f * s, -0.12f * s, 0.12f * s, fill);
path.reset();
path.moveTo(0.42f * s, -0.16f * s);
path.quadTo(0.6f * s, -0.15f * s, 0.55f * s, -0.04f * s);
path.lineTo(0.43f * s, -0.08f * s);
path.close();
fill.setColor(beak);
c.drawPath(path, fill);
fill.setColor(0xFF1A1008);
c.drawCircle(0.38f * s, -0.15f * s, 0.022f * s, fill);
line.setColor(beak);
line.setStrokeWidth(2.5f * u);
c.drawLine(-0.1f * s, 0.13f * s, -0.14f * s, 0.26f * s, line);
c.drawLine(0.05f * s, 0.13f * s, 0.02f * s, 0.26f * s, line);
c.restore();
}
}
@@ -84,6 +84,14 @@ final class TerminalView extends ClusterCanvasView {
y += line; y += line;
print(c, row("TYRES", "FL " + v.tyreFl + " FR " + v.tyreFr + " RL " + v.tyreRl + " RR " + v.tyreRr), x, y, green); print(c, row("TYRES", "FL " + v.tyreFl + " FR " + v.tyreFr + " RL " + v.tyreRl + " RR " + v.tyreRr), x, y, green);
y += line * 1.4f; y += line * 1.4f;
if (v.songTitle != null) {
// What plays, cut to the row's width.
String label = String.format(Locale.US, "%-9s", "PLAYING");
String s = v.songTitle + (v.songArtist == null ? "" : " — " + v.songArtist);
int fits = text.breakText(s, true, Math.max(0f, getWidth() - x - text.measureText(label)), null);
print(c, label + (fits < s.length() ? s.substring(0, Math.max(0, fits - 1)).trim() + "…" : s), x, y, green);
y += line;
}
boolean cursor = (SystemClock.uptimeMillis() / 500) % 2 == 0; boolean cursor = (SystemClock.uptimeMillis() / 500) % 2 == 0;
print(c, "[" + (v.clock.isEmpty() ? "--:--" : v.clock) + "] nils@mg4:~$ " + (cursor ? "█" : ""), x, y, green); print(c, "[" + (v.clock.isEmpty() ? "--:--" : v.clock) + "] nils@mg4:~$ " + (cursor ? "█" : ""), x, y, green);