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. 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}). * * 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; * the light's pulse keeps it drawing while there is one. */ 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 static final int BAT = 0xFF6F6390, BAT_EDGE = 0xFFD6CCEA; 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; private MusicTap music; HalloweenView(Context context) { super(context); line.setStyle(Paint.Style.STROKE); line.setStrokeCap(Paint.Cap.ROUND); } /** Where the pumpkin's light hears the music; without it, the light does not follow any. */ void setMusic(MusicTap tap) { music = tap; } @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); drawSong(c, w, h, u); drawEmbers(c, w, u); float pulse = music == null ? 0f : music.pulse(); drawPumpkin(c, w, u, pulse); drawSign(c, u); drawFlask(c, u); drawTombstone(c, w, u); drawGears(c, w, u); keepAnimating(moving || pulse > 0f, 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 ? 20f : 16f) * u, (float) Math.sin(time * 14f + i * 2f), u); } } private void bat(Canvas c, float x, float y, float s, float flap, float u) { 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(); // Lit by the moon: black would vanish in the night sky. fill.setColor(BAT); c.drawPath(path, fill); line.setColor(BAT_EDGE); line.setStrokeWidth(1.5f * u); c.drawPath(path, line); } // ---------- 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); } } } // ---------- What plays, carved into the hill ---------- 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 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); } /** {@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 ---------- 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 pulse) { float cx = w / 2f, cy = 348f * u, rx = 200f * u, ry = 158f * u; boolean regen = regen(); // The carving brightens on the beat, gently, and stays yellow or green. int burn = brighter(regen ? FIRE : CANDLE, pulse * 0.35f); // The light it throws, stronger the more power, swelling with the music. float share = v.powerKw == null ? 0f : Math.min(1f, Math.abs(v.powerKw) / 80f); float reach = rx * (1.6f + pulse * 0.2f); glow.setShader(new RadialGradient(cx, cy, reach, new int[] { AccentColor.withAlpha(regen ? FIRE : ORANGE, 0x30 + (int) (share * 0x50) + (int) (pulse * 0x60)), AccentColor.withAlpha(regen ? FIRE : ORANGE, 0) }, null, Shader.TileMode.CLAMP)); c.drawCircle(cx, cy, reach, 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, each the colour of the rib it hangs from. tooth(c, cx, -120f * u, gw, gt, 26f * u, 17f * u, gt + 40f * u, PUMPKIN, u); tooth(c, cx, 120f * u, gw, gt, 26f * u, 17f * u, gt + 40f * u, PUMPKIN_LIT, 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); } /** The colour moved {@code share} of the way to white. */ private static int brighter(int colour, float share) { int r = (colour >> 16) & 0xFF, g = (colour >> 8) & 0xFF, b = colour & 0xFF; return 0xFF000000 | (r + (int) ((255 - r) * share)) << 16 | (g + (int) ((255 - g) * share)) << 8 | (b + (int) ((255 - b) * share)); } 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); } /** A triangle hanging from the grin's upper lip (a curve sagging by {@code sag}) to {@code tipY}. */ private void tooth(Canvas c, float cx, float dx, float gw, float gt, float sag, float halfW, float tipY, int colour, float u) { float left = lip(dx - halfW, gw, gt, sag), right = lip(dx + halfW, gw, gt, sag); path.reset(); // A little above the lip, so none of its dark edge shows across the tooth's root. path.moveTo(cx + dx - halfW, left - 1.5f * u); path.lineTo(cx + dx + halfW, right - 1.5f * u); path.lineTo(cx + dx, tipY); path.close(); fill.setColor(colour); c.drawPath(path, fill); line.setColor(0xFF4A1F04); line.setStrokeWidth(3f * u); c.drawLine(cx + dx - halfW, left + 2f * u, cx + dx, tipY, line); c.drawLine(cx + dx + halfW, right + 2f * u, cx + dx, tipY, line); } /** The height of the grin's upper lip {@code dx} from the middle. */ private static float lip(float dx, float gw, float gt, float sag) { float t = (gw - Math.abs(dx)) / (2f * gw); return gt + 2f * t * (1f - t) * sag; } // ---------- 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); } } }