App 2.43.0: Overview styles Día de Muertos and Halloween

Día de Muertos is an ofrenda: papel picado for the battery, a calavera
with the speed in a ring of marigolds for the power, candles for the
range, a path of petals for the last two minutes of power. Halloween is
a graveyard hill: a jack-o'-lantern with the speed in its grin, embers
for the power, a potion flask for the battery, a tombstone and bats for
the range. Each has its own palette; Tally counts what a value lights.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-31 09:24:56 +01:00
co-authored by Claude Opus 5.5
parent 1f49cdc410
commit adc2c86303
18 changed files with 1185 additions and 9 deletions
@@ -0,0 +1,532 @@
package com.evsuite.abrp;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.RectF;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.os.SystemClock;
import java.util.Locale;
/**
* The Halloween Overview style: a graveyard hill under a full moon.
*
* In the middle a jack-o'-lantern with the speed cut into its grin; its carving burns candle yellow
* while drawing and witch-fire green while regenerating, and the arch of embers over it is the
* power, left to right while drawing, green and right to left while regenerating. On the left a
* potion flask: the battery, red with "BATTERY LOW" blinking below 20 %, under a sign hanging from
* a cobweb with trip, usage, climate and odometer. On the right the moon holds the clock, and a
* tombstone reads the range ("here lies … km"); P R N D are four small gravestones, the one engaged
* lit. The bats are the range too, one per 50 km. The hill along the bottom is the last two minutes
* of power, rising with it, in orange or, where it regenerated, green. Unread values show "—" and
* light nothing.
*
* 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.
*/
final class HalloweenView extends ClusterCanvasView {
private static final int EMBERS = 25, BATS = 8;
private static final int SKY_TOP = 0xFF0B0620, SKY_LOW = 0xFF2A1140, HILL = 0xFF0A0612;
private static final int PUMPKIN = 0xFFF2760F, PUMPKIN_DARK = 0xFFB84A06, PUMPKIN_LIT = 0xFFFF9A2E;
private static final int CANDLE = 0xFFFFD84A, FIRE = 0xFF6BFF5A, RED = 0xFFFF3B30, STONE = 0xFF6C6A78;
private static final int STONE_DARK = 0xFF3E3C4A, MOON = 0xFFF4E9B8, MOON_SHADE = 0xFFD8C98C;
private static final int POTION = 0xFF7BF03A, GLASS = 0xFFB9D8D0, BONE = 0xFFF2EAD8, ORANGE = 0xFFFF8C1A;
private static final int STEM = 0xFF4E7A2B, WOOD = 0xFF2E1B10, INK = 0xFF140A08;
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
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 RectF r = new RectF();
private final Typeface serif = Typeface.create("serif", Typeface.BOLD);
private final Typeface face = Typeface.create("sans-serif-condensed", Typeface.BOLD);
private final Typeface thin = Typeface.create("sans-serif-condensed", Typeface.NORMAL);
private Bitmap sky;
private int skyW, skyH;
private float time;
private long lastFrame;
HalloweenView(Context context) {
super(context);
line.setStyle(Paint.Style.STROKE);
line.setStrokeCap(Paint.Cap.ROUND);
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
sky = null;
skyW = 0;
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
int w = getWidth(), h = getHeight();
if (w <= 0 || h <= 0) return;
float u = Math.min(h / 640f, w / 1100f);
boolean moving = v.speedKmh != null && v.speedKmh > 0.5f;
if (moving) {
long now = SystemClock.uptimeMillis();
time += lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
lastFrame = now;
} else {
lastFrame = 0;
}
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
c.drawBitmap(sky, 0, 0, image);
drawBats(c, w, u);
drawMoon(c, w, u);
drawGhost(c, w, u);
drawHill(c, w, h, u);
drawEmbers(c, w, u);
drawPumpkin(c, w, u);
drawSign(c, u);
drawFlask(c, u);
drawTombstone(c, w, u);
drawGears(c, w, u);
keepAnimating(moving, 33);
}
private void label(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a) {
text.setTypeface(tf);
text.setTextSize(size);
text.setTextAlign(a);
text.setColor(colour);
c.drawText(s, x, y, text);
}
private boolean regen() {
return v.powerKw != null && v.powerKw < -0.05f;
}
// ---------- Sky ----------
private void buildSky(int w, int h, float u) {
sky = Bitmap.createBitmap(w, h, Bitmap.Config.ARGB_8888);
skyW = w;
skyH = h;
Canvas c = new Canvas(sky);
Paint p = new Paint();
p.setShader(new LinearGradient(0, 0, 0, h, new int[] { SKY_TOP, SKY_LOW, 0xFF4A1A3A },
new float[] { 0f, 0.7f, 1f }, Shader.TileMode.CLAMP));
c.drawRect(0, 0, w, h, p);
Paint star = new Paint(Paint.ANTI_ALIAS_FLAG);
star.setColor(0x80FFFFFF);
for (int i = 0; i < 90; i++) {
float x = ((i * 191 + 17) % 1009) / 1009f * w, y = ((i * 83 + 5) % 421) * u;
c.drawCircle(x, y, (i % 4 == 0 ? 1.7f : 0.9f) * u, star);
}
// Bare trees in the distance.
tree(c, w * 0.235f, 590f * u, 150f * u, u);
tree(c, w * 0.745f, 590f * u, 118f * u, u);
}
/** A bare tree: a trunk and branches that fork twice. */
private void tree(Canvas c, float x, float y, float tall, float u) {
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setStyle(Paint.Style.STROKE);
p.setStrokeCap(Paint.Cap.ROUND);
p.setColor(0xFF160B20);
branch(c, p, x, y, -90f, tall * 0.5f, 7f * u, 4);
}
private void branch(Canvas c, Paint p, float x, float y, float deg, float len, float width, int depth) {
double a = Math.toRadians(deg);
float nx = x + (float) Math.cos(a) * len, ny = y + (float) Math.sin(a) * len;
p.setStrokeWidth(width);
c.drawLine(x, y, nx, ny, p);
if (depth == 0) return;
branch(c, p, nx, ny, deg - 24f - depth * 3f, len * 0.68f, width * 0.65f, depth - 1);
branch(c, p, nx, ny, deg + 28f - depth * 2f, len * 0.62f, width * 0.65f, depth - 1);
}
// ---------- Moon and clock ----------
private void drawMoon(Canvas c, int w, float u) {
float rad = 74f * u, cx = w - 100f * u - rad * 0.2f, cy = 96f * u;
glow.setShader(new RadialGradient(cx, cy, rad * 2.2f, new int[] { 0x55F4E9B8, 0x00F4E9B8 }, null,
Shader.TileMode.CLAMP));
c.drawCircle(cx, cy, rad * 2.2f, glow);
fill.setColor(MOON);
c.drawCircle(cx, cy, rad, fill);
fill.setColor(MOON_SHADE);
c.drawCircle(cx - 42f * u, cy - 34f * u, 12f * u, fill);
c.drawCircle(cx + 40f * u, cy + 38f * u, 16f * u, fill);
c.drawCircle(cx + 46f * u, cy - 26f * u, 7f * u, fill);
c.drawCircle(cx - 30f * u, cy + 44f * u, 8f * u, fill);
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 17f * u, 50f * u, INK, serif, Paint.Align.CENTER);
}
// ---------- Bats: the range ----------
private void drawBats(Canvas c, int w, float u) {
int lit = Tally.lit(v.rangeKm, 50f, BATS);
float cx = w / 2f;
for (int i = 0; i < lit; i++) {
// Each its own lane high in the sky, clear of the pumpkin's middle.
float lane = (40f + (i * 37) % 70) * u;
float span = w * 0.92f, speed = (26f + i * 5f) * u;
float x = ((i * 0.13f * span + time * speed) % span) + w * 0.04f;
float y = lane + (float) Math.sin(time * 2f + i) * 8f * u;
if (Math.abs(x - cx) < 60f * u) y -= 10f * u;
bat(c, x, y, (i % 2 == 0 ? 15f : 12f) * u, (float) Math.sin(time * 14f + i * 2f));
}
}
private void bat(Canvas c, float x, float y, float s, float flap) {
float tip = -s * 0.5f * flap;
path.reset();
path.moveTo(x, y - s * 0.3f);
path.quadTo(x + s * 0.6f, y - s * 0.7f + tip, x + s * 1.5f, y - s * 0.3f + tip);
path.quadTo(x + s * 1.1f, y + s * 0.05f, x + s * 0.9f, y + s * 0.35f);
path.quadTo(x + s * 0.6f, y + s * 0.05f, x + s * 0.35f, y + s * 0.4f);
path.quadTo(x + s * 0.15f, y + s * 0.15f, x, y + s * 0.5f);
path.quadTo(x - s * 0.15f, y + s * 0.15f, x - s * 0.35f, y + s * 0.4f);
path.quadTo(x - s * 0.6f, y + s * 0.05f, x - s * 0.9f, y + s * 0.35f);
path.quadTo(x - s * 1.1f, y + s * 0.05f, x - s * 1.5f, y - s * 0.3f + tip);
path.quadTo(x - s * 0.6f, y - s * 0.7f + tip, x, y - s * 0.3f);
path.close();
fill.setColor(0xFF06030C);
c.drawPath(path, fill);
}
// ---------- Ghost: only where there is room ----------
private void drawGhost(Canvas c, int w, float u) {
if (w / u < 1400f) return;
float x = (w / 2f + 272f * u + w - 290f * u) / 2f, y = 330f * u + (float) Math.sin(time * 1.4f) * 12f * u, s = 34f * u;
path.reset();
path.moveTo(x - s, y + s * 1.5f);
path.lineTo(x - s, y);
path.cubicTo(x - s, y - s * 1.4f, x + s, y - s * 1.4f, x + s, y);
path.lineTo(x + s, y + s * 1.5f);
float wob = (float) Math.sin(time * 3f) * s * 0.12f;
path.quadTo(x + s * 0.66f, y + s * 1.1f + wob, x + s * 0.33f, y + s * 1.5f);
path.quadTo(x, y + s * 1.1f - wob, x - s * 0.33f, y + s * 1.5f);
path.quadTo(x - s * 0.66f, y + s * 1.1f + wob, x - s, y + s * 1.5f);
path.close();
fill.setColor(0xB8F0F4FF);
c.drawPath(path, fill);
fill.setColor(0xFF1A1030);
c.drawCircle(x - s * 0.34f, y - s * 0.2f, s * 0.14f, fill);
c.drawCircle(x + s * 0.34f, y - s * 0.2f, s * 0.14f, fill);
r.set(x - s * 0.14f, y + s * 0.08f, x + s * 0.14f, y + s * 0.44f);
c.drawOval(r, fill);
}
// ---------- The hill: the last two minutes of power ----------
private void drawHill(Canvas c, int w, int h, float u) {
float base = h - 44f * u, amp = 36f * u;
float[] trail = v.powerTrail;
path.reset();
path.moveTo(0, h);
int n = trail == null ? 0 : trail.length;
if (n == 0) {
path.lineTo(0, base);
path.lineTo(w, base);
} else {
for (int i = 0; i < n; i++) {
float p = trail[i];
float height = Float.isNaN(p) ? 0f : Math.min(1f, Math.abs(p) / (p < 0 ? 40f : 60f));
path.lineTo(i / (n - 1f) * w, base - height * amp);
}
}
path.lineTo(w, h);
path.close();
fill.setColor(HILL);
c.drawPath(path, fill);
// The ridge line, each stretch in the colour of what the car was doing then.
if (n > 1) {
line.setStrokeWidth(3f * u);
for (int i = 1; i < n; i++) {
float a = trail[i - 1], b = trail[i];
if (Float.isNaN(a) || Float.isNaN(b)) continue;
float ha = Math.min(1f, Math.abs(a) / (a < 0 ? 40f : 60f));
float hb = Math.min(1f, Math.abs(b) / (b < 0 ? 40f : 60f));
line.setColor(b < 0 ? FIRE : ORANGE);
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
}
}
}
// ---------- The arch of embers: the power ----------
private void drawEmbers(Canvas c, int w, float u) {
float cx = w / 2f, cy = 348f * u, radX = 272f * u, radY = 244f * u;
int lit = Tally.ofPower(v.powerKw, 150f, 50f, EMBERS);
boolean regen = v.powerKw != null && v.powerKw < 0f;
for (int i = 0; i < EMBERS; i++) {
// Left to right over the top; regeneration comes back from the right.
int k = regen ? EMBERS - 1 - i : i;
double a = Math.PI + Math.PI * k / (EMBERS - 1);
float x = cx + (float) Math.cos(a) * radX, y = cy + (float) Math.sin(a) * radY;
boolean on = i < lit;
if (on) {
fill.setColor(AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x44));
c.drawCircle(x, y, 15f * u, fill);
}
fill.setColor(on ? (regen ? FIRE : CANDLE) : 0x38FFFFFF);
c.drawCircle(x, y, 7.5f * u, fill);
}
}
// ---------- The jack-o'-lantern: the speed ----------
private void drawPumpkin(Canvas c, int w, float u) {
float cx = w / 2f, cy = 348f * u, rx = 200f * u, ry = 158f * u;
boolean regen = regen();
int burn = regen ? FIRE : CANDLE;
// The light it throws, stronger the more power.
float share = v.powerKw == null ? 0f : Math.min(1f, Math.abs(v.powerKw) / 80f);
glow.setShader(new RadialGradient(cx, cy, rx * 1.6f,
new int[] { AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x30 + (int) (share * 0x50)),
AccentColor.withAlpha(regen ? FIRE : ORANGE, 0) }, null, Shader.TileMode.CLAMP));
c.drawCircle(cx, cy, rx * 1.6f, glow);
// The stem.
path.reset();
path.moveTo(cx - 22f * u, cy - ry + 12f * u);
path.quadTo(cx - 14f * u, cy - ry - 26f * u, cx + 14f * u, cy - ry - 44f * u);
path.lineTo(cx + 34f * u, cy - ry - 30f * u);
path.quadTo(cx + 14f * u, cy - ry - 12f * u, cx + 22f * u, cy - ry + 12f * u);
path.close();
fill.setColor(STEM);
c.drawPath(path, fill);
// The ribs, outermost first, overlapping.
float[][] ribs = { { -0.70f, 0.42f }, { 0.70f, 0.42f }, { -0.40f, 0.48f }, { 0.40f, 0.48f }, { 0f, 0.50f } };
for (int i = 0; i < ribs.length; i++) {
float rcx = cx + ribs[i][0] * rx, half = ribs[i][1] * rx;
r.set(rcx - half, cy - ry * (i < 2 ? 0.86f : 0.98f), rcx + half, cy + ry * (i < 2 ? 0.86f : 0.98f));
fill.setColor(i % 2 == 0 ? PUMPKIN : PUMPKIN_LIT);
if (i == ribs.length - 1) fill.setColor(PUMPKIN);
c.drawOval(r, fill);
line.setColor(PUMPKIN_DARK);
line.setStrokeWidth(2.5f * u);
c.drawOval(r, line);
}
// Eyes and nose: triangles, burning.
triangle(c, cx - 88f * u, cy - 92f * u, cx - 34f * u, cy - 28f * u, cx - 128f * u, cy - 28f * u, burn, u);
triangle(c, cx + 88f * u, cy - 92f * u, cx + 128f * u, cy - 28f * u, cx + 34f * u, cy - 28f * u, burn, u);
triangle(c, cx, cy - 28f * u, cx + 19f * u, cy + 4f * u, cx - 19f * u, cy + 4f * u, burn, u);
// The grin, with the speed cut dark into its light.
float gt = cy + 8f * u, gb = cy + 122f * u, gw = 156f * u;
path.reset();
path.moveTo(cx - gw, gt);
path.quadTo(cx, gt + 26f * u, cx + gw, gt);
path.quadTo(cx + gw * 0.82f, gb, cx, gb);
path.quadTo(cx - gw * 0.82f, gb, cx - gw, gt);
path.close();
fill.setColor(0xFF4A1F04);
c.drawPath(path, fill);
c.save();
c.scale(0.965f, 0.93f, cx, (gt + gb) / 2f);
fill.setColor(burn);
c.drawPath(path, fill);
c.restore();
// Two teeth at the grin's corners, clear of the digits.
fill.setColor(PUMPKIN);
tooth(c, cx - 120f * u, gt + 12f * u, 15f * u, 28f * u);
tooth(c, cx + 120f * u, gt + 12f * u, 15f * u, 28f * u);
label(c, fmt(v.speedKmh, "%.0f", "—"), cx, gb - 18f * u, 92f * u, INK, serif, Paint.Align.CENTER);
label(c, "KM/H", cx + 80f * u, cy + 64f * u, 16f * u, INK, face, Paint.Align.LEFT);
// The power under the pumpkin, above the ridge.
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
Math.abs(v.powerKw));
label(c, power, cx, cy + ry + 44f * u, 34f * u, regen ? FIRE : BONE, face, Paint.Align.CENTER);
}
private void triangle(Canvas c, float x1, float y1, float x2, float y2, float x3, float y3, int burn, float u) {
path.reset();
path.moveTo(x1, y1);
path.lineTo(x2, y2);
path.lineTo(x3, y3);
path.close();
fill.setColor(burn);
c.drawPath(path, fill);
line.setColor(0xFF4A1F04);
line.setStrokeWidth(4f * u);
c.drawPath(path, line);
}
private void tooth(Canvas c, float x, float y, float halfW, float tall) {
path.reset();
path.moveTo(x - halfW, y - tall * 0.3f);
path.lineTo(x + halfW, y - tall * 0.3f);
path.lineTo(x + halfW, y + tall * 0.7f);
path.lineTo(x - halfW, y + tall * 0.7f);
path.close();
c.drawPath(path, fill);
}
// ---------- Left: the sign under the cobweb ----------
private void drawSign(Canvas c, float u) {
// The cobweb in the corner.
float cs = 150f * u;
line.setColor(0x70E8E8F4);
line.setStrokeWidth(1.2f * u);
for (int i = 0; i <= 5; i++) {
double a = Math.PI / 2 * i / 5;
c.drawLine(0, 0, (float) Math.cos(a) * cs, (float) Math.sin(a) * cs, line);
}
for (int ring = 1; ring <= 4; ring++) {
float d = cs * ring / 4f;
path.reset();
for (int i = 0; i <= 5; i++) {
double a = Math.PI / 2 * i / 5;
float x = (float) Math.cos(a) * d, y = (float) Math.sin(a) * d;
if (i == 0) {
path.moveTo(x, y);
} else {
double mid = Math.PI / 2 * (i - 0.5) / 5;
path.quadTo((float) Math.cos(mid) * d * 0.86f, (float) Math.sin(mid) * d * 0.86f, x, y);
}
}
c.drawPath(path, line);
}
// The sign, on two threads.
float l = 28f * u, rr = l + 300f * u, t = 50f * u, b = 244f * u;
line.setColor(0xA0E8E8F4);
c.drawLine(l + 60f * u, 0, l + 60f * u, t, line);
c.drawLine(rr - 60f * u, 0, rr - 60f * u, t, line);
r.set(l, t, rr, b);
fill.setColor(WOOD);
c.drawRoundRect(r, 8f * u, 8f * u, fill);
line.setColor(0xFF6B4226);
line.setStrokeWidth(2.5f * u);
c.drawRoundRect(r, 8f * u, 8f * u, line);
String[][] rows = {
{ "TRIP", fmt(v.tripKm, "%.1f km", "—") },
{ "5 MIN", fmt(v.usage5Min, "%.1f kWh/100", "—") },
{ "SINCE START", fmt(v.usageTrip, "%.1f kWh/100", "—") },
{ "CLIMATE", fmt(v.climateCelsius, "%.0f °C", "—") },
{ "ODOMETER", fmt(v.odometerKm, "%,.0f km", "—") },
};
for (int i = 0; i < rows.length; i++) {
float y = t + (36f + i * 34f) * u;
label(c, rows[i][0], l + 16f * u, y, 19f * u, ORANGE, thin, Paint.Align.LEFT);
label(c, rows[i][1], rr - 16f * u, y, 23f * u, BONE, face, Paint.Align.RIGHT);
}
}
// ---------- The potion flask: the battery ----------
private void drawFlask(Canvas c, float u) {
float rad = 104f * u, cx = 28f * u + 150f * u, cy = 452f * u;
boolean low = v.socPercent != null && v.socPercent < 20f;
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
int colour = low ? RED : POTION;
float neckW = 34f * u, neckTop = cy - rad - 42f * u;
// The glass shape: a round belly and a neck.
path.reset();
path.addCircle(cx, cy, rad, Path.Direction.CW);
r.set(cx - neckW, neckTop, cx + neckW, cy - rad + 16f * u);
path.addRect(r, Path.Direction.CW);
fill.setColor(0x38B9D8D0);
c.drawPath(path, fill);
// The potion: as high as the battery is full, across the belly only.
if (v.socPercent != null) {
float share = Math.max(0f, Math.min(1f, v.socPercent / 100f));
float level = cy + rad - share * 2f * rad;
c.save();
c.clipRect(cx - rad, level, cx + rad, cy + rad + 1f);
fill.setColor(colour);
c.drawCircle(cx, cy, rad - 5f * u, fill);
c.restore();
// The surface, a brighter line.
float half = (float) Math.sqrt(Math.max(0f, (rad - 5f * u) * (rad - 5f * u) - (level - cy) * (level - cy)));
line.setColor(0xCCFFFFFF);
line.setStrokeWidth(2.5f * u);
if (half > 0f) c.drawLine(cx - half, level, cx + half, level, line);
// Bubbles rise only while driving.
fill.setColor(0x99FFFFFF);
for (int i = 0; i < 6; i++) {
float span = Math.max(1f, cy + rad - level - 12f * u);
float y = cy + rad - 12f * u - ((time * (24f + i * 7f) * u + i * 31f * u) % span);
float x = cx + (float) Math.sin(i * 2.1f + time * 1.5f) * rad * 0.5f;
c.drawCircle(x, y, (3f + i % 3) * u, fill);
}
}
// The glass, and its cork.
line.setColor(GLASS);
line.setStrokeWidth(4f * u);
c.drawCircle(cx, cy, rad, line);
c.drawLine(cx - neckW, neckTop + 12f * u, cx - neckW, cy - rad + 14f * u, line);
c.drawLine(cx + neckW, neckTop + 12f * u, cx + neckW, cy - rad + 14f * u, line);
r.set(cx - neckW - 4f * u, neckTop - 8f * u, cx + neckW + 4f * u, neckTop + 16f * u);
fill.setColor(0xFF9B6B3A);
c.drawRoundRect(r, 5f * u, 5f * u, fill);
// Dark where the potion is behind the words; outlined where its surface cuts through them.
float level = v.socPercent == null ? cy + rad
: cy + rad - Math.max(0f, Math.min(1f, v.socPercent / 100f)) * 2f * rad;
boolean numberOn = level <= cy - 18f * u, wordOn = level <= cy + 38f * u;
String number = fmt(v.socPercent, "%.0f %%", "—");
if (!numberOn && level < cy + 30f * u) {
text.setStyle(Paint.Style.STROKE);
text.setStrokeWidth(7f * u);
label(c, number, cx, cy + 22f * u, 62f * u, INK, serif, Paint.Align.CENTER);
text.setStyle(Paint.Style.FILL);
}
label(c, number, cx, cy + 22f * u, 62f * u, numberOn ? INK : low ? RED : BONE, serif, Paint.Align.CENTER);
if (low && blinkOn) {
label(c, "BATTERY LOW", cx, cy + 56f * u, 21f * u, BONE, face, Paint.Align.CENTER);
} else if (!low) {
label(c, "BATTERY", cx, cy + 56f * u, 19f * u, wordOn ? INK : POTION, face, Paint.Align.CENTER);
}
}
// ---------- The tombstone: the range; small gravestones: the gears ----------
private void stone(Canvas c, float l, float t, float rr, float b, int colour, int edge, float u) {
float mid = (l + rr) / 2f, half = (rr - l) / 2f;
path.reset();
path.moveTo(l, b);
path.lineTo(l, t + half);
path.cubicTo(l, t + half * 0.1f, mid - half * 0.5f, t, mid, t);
path.cubicTo(mid + half * 0.5f, t, rr, t + half * 0.1f, rr, t + half);
path.lineTo(rr, b);
path.close();
fill.setColor(colour);
c.drawPath(path, fill);
line.setColor(edge);
line.setStrokeWidth(3f * u);
c.drawPath(path, line);
}
private void drawTombstone(Canvas c, int w, float u) {
float rr = w - 40f * u, l = rr - 250f * u, t = 196f * u, b = 484f * u, mid = (l + rr) / 2f;
stone(c, l, t, rr, b, STONE, STONE_DARK, u);
label(c, "R.I.P.", mid, t + 70f * u, 36f * u, STONE_DARK, serif, Paint.Align.CENTER);
label(c, "HERE LIES", mid, t + 106f * u, 19f * u, 0xFF242230, face, Paint.Align.CENTER);
label(c, fmt(v.rangeKm, "%.0f", "—"), mid, t + 186f * u, 86f * u, BONE, serif, Paint.Align.CENTER);
label(c, "KM OF RANGE", mid, t + 224f * u, 21f * u, 0xFF242230, face, Paint.Align.CENTER);
}
private void drawGears(Canvas c, int w, float u) {
float rr = w - 40f * u, l = rr - 250f * u, t = 496f * u, b = 566f * u;
String[] gears = { "P", "R", "N", "D" };
String now = Gear.label(v.gear);
float step = (rr - l) / 4f, half = 24f * u;
for (int i = 0; i < gears.length; i++) {
float mid = l + (i + 0.5f) * step;
boolean on = gears[i].equals(now);
if (on) {
fill.setColor(0x55FF8C1A);
c.drawCircle(mid, (t + b) / 2f, 42f * u, fill);
}
stone(c, mid - half, t, mid + half, b, on ? ORANGE : STONE_DARK, on ? CANDLE : 0xFF242230, u);
label(c, gears[i], mid, b - 18f * u, 32f * u, on ? INK : 0xB0FFFFFF, serif, Paint.Align.CENTER);
}
}
}