Files
mg4/app/src/main/java/com/evsuite/abrp/CaterpillarView.java
T
nilsandClaude Sonnet 5.5 60a31574a3 App 2.50.0: the Caterpillar crawls while you drive
A hump runs from the tail to the head, lifted feet swing forward while
the others stay planted, and the branch it walks on slides by with the
speed. Parked, it settles into its high arch.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-11-07 10:22:43 +01:00

610 lines
29 KiB
Java

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.RectF;
import android.graphics.Shader;
import android.graphics.Typeface;
import android.os.SystemClock;
import java.util.Locale;
/**
* The Caterpillar Overview style, after the picture book about a very hungry caterpillar (drawn
* anew, not copied from it): cut-paper shapes in bright tissue colours on the book's night page.
*
* In the middle the caterpillar, arched like the one on the book's cover (overlapping chunks of
* mottled green tissue paper, a yellow fringe along the back, a red-orange head with a yellow face):
* the face carries the speed, and its ten segments are the power, lit from the head backwards while
* drawing, blue while regenerating; while the car moves it crawls the way a caterpillar does: a hump runs from tail to head, its feet
* lifting and planting, on a branch that slides by. On the left the sun is the battery, filling with the charge, and turning into a
* grey moon with "BATTERY LOW" blinking below 20 %; above it a big leaf holds trip, usage, climate
* and odometer on paper labels. On the right the clock in a paper circle, and the
* story's fruit: one apple, two pears, three plums, four strawberries and five oranges, a fruit
* per 50 km of range, those used up with a bite taken out. P R N D are the four stages of its life,
* an egg, the caterpillar, a cocoon and a butterfly, the one engaged lit. The hills along the bottom
* are the last two minutes of power, rising with it. Unread values show "—" and light nothing.
* With music playing, the sun's rays and the caterpillar's segments bounce on the beat
* ({@link MusicTap}). What plays crosses the sky on a banner of tissue paper towed by a butterfly
* and, on the next pass, a ladybird ({@link NowPlaying}).
*
* The night, the stars and the far hills are drawn once into a bitmap per size. The caterpillar
* wiggles (about 30 frames a second) only while the car moves and the view is on screen; the
* pulse and the song keep it drawing while there are any.
*/
final class CaterpillarView extends ClusterCanvasView {
private static final int SEGMENTS = 10, RAYS = 14, FRUIT = 15;
private static final int NIGHT_TOP = 0xFF050B22, NIGHT_LOW = 0xFF12305A, NIGHT_END = 0xFF1D4A5E;
private static final int GREEN = 0xFF7ED957, GREEN_DARK = 0xFF2E8B3A, LEAF = 0xFF3FA34D, HEAD = 0xFFE53935;
private static final int SUN = 0xFFFFA726, SUN_LIGHT = 0xFFFFD54F, MOON = 0xFFB8BCC8, ICE = 0xFF5CC8FF;
private static final int PAPER = 0xFFFBF1DA, INK = 0xFF1B1530, BONE = 0xFFF6F0E4, YELLOW = 0xFFFFE066;
private static final int[] TISSUE = { 0xFFE53935, 0xFF2B7BE0, 0xFF2EA84F, 0xFFFFC928, 0xFF8E4DD6, 0xFFFF8A1F };
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 Path path = new Path();
private final Path bite = new Path();
private final SongBanner banner = new SongBanner();
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;
/** Where the crawl is (whole numbers are one full crawl), how far the ground has gone by, and how much it crawls (0 to 1). */
private float phase, travel, crawl;
private long lastFrame;
private MusicTap music;
CaterpillarView(Context context) {
super(context);
line.setStyle(Paint.Style.STROKE);
line.setStrokeCap(Paint.Cap.ROUND);
}
/** Where the sun and the caterpillar hear the music; without it, they do 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;
long now = SystemClock.uptimeMillis();
float dt = lastFrame == 0 ? 0f : Math.min(0.2f, (now - lastFrame) / 1000f);
lastFrame = moving || crawl > 0.01f ? now : 0;
if (moving) {
float kmh = Math.min(v.speedKmh, 130f);
time += dt;
// One crawl takes a second or two; faster the faster the car goes, and the ground slides by.
phase = (phase + dt * Math.min(1.2f, 0.4f + kmh / 90f)) % 1000f;
travel = (travel + dt * kmh * 1.1f * u) % 100000f;
}
// It settles into its resting arch when the car stops, and sets off again when it moves.
crawl += ((moving ? 1f : 0f) - crawl) * Math.min(1f, dt * 4f);
if (sky == null || skyW != w || skyH != h) buildSky(w, h, u);
c.drawBitmap(sky, 0, 0, image);
boolean song = drawSong(c, w, u);
drawHills(c, w, h, u);
float pulse = music == null ? 0f : music.pulse();
drawSun(c, u, pulse);
drawLeaf(c, u);
drawClock(c, w, u);
drawFruit(c, w, u);
drawGears(c, w, u);
drawBranch(c, w, u);
drawCaterpillar(c, w, u, pulse);
keepAnimating(moving || crawl > 0.01f || pulse > 0f || song, 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);
}
/** Text with a dark outline, to read over whatever is behind it. */
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface tf, Paint.Align a, float u) {
text.setStyle(Paint.Style.STROKE);
text.setStrokeWidth(Math.max(3f * u, size * 0.12f));
text.setStrokeJoin(Paint.Join.ROUND);
label(c, s, x, y, size, 0xDD0A0608, tf, a);
text.setStyle(Paint.Style.FILL);
label(c, s, x, y, size, colour, tf, a);
}
private boolean regen() {
return v.powerKw != null && v.powerKw < -0.05f;
}
// ---------- The night page ----------
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[] { NIGHT_TOP, NIGHT_LOW, NIGHT_END },
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(0xB0FFF6C8);
for (int i = 0; i < 80; i++) {
float x = ((i * 191 + 17) % 1009) / 1009f * w, y = ((i * 83 + 5) % 421) * u;
c.drawCircle(x, y, (i % 4 == 0 ? 2.2f : 1.2f) * u, star);
}
// A crescent moon, as on the book's first night.
Path moon = new Path(), cut = new Path();
float mx = w * 0.3f, my = 150f * u;
moon.addCircle(mx, my, 30f * u, Path.Direction.CW);
cut.addCircle(mx + 13f * u, my - 6f * u, 27f * u, Path.Direction.CW);
moon.op(cut, Path.Op.DIFFERENCE);
Paint paper = new Paint(Paint.ANTI_ALIAS_FLAG);
paper.setColor(0xFFFFF3B0);
c.drawPath(moon, paper);
hills(c, w, h, h - 150f * u, 0xFF124A3A, 0.8f, u);
hills(c, w, h, h - 100f * u, 0xFF1A5E45, 1.5f, u);
}
private void hills(Canvas c, int w, int h, float base, int colour, float wave, float u) {
Path hill = new Path();
hill.moveTo(0, h);
for (int i = 0; i <= 24; i++) {
float x = w * i / 24f;
hill.lineTo(x, base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * 22f * u);
}
hill.lineTo(w, h);
hill.close();
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setColor(colour);
c.drawPath(hill, p);
}
// ---------- The hills in front: the last two minutes of power ----------
private void drawHills(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(0xFF2E9A4E);
c.drawPath(path, fill);
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 ? ICE : YELLOW);
c.drawLine((i - 1f) / (n - 1f) * w, base - ha * amp, i / (n - 1f) * w, base - hb * amp, line);
}
}
}
// ---------- The sun (or the moon): the battery ----------
private void drawSun(Canvas c, float u, float pulse) {
float cx = 178f * u, cy = 452f * u, rad = 100f * u;
boolean low = v.socPercent != null && v.socPercent < 20f;
boolean blinkOn = (SystemClock.uptimeMillis() / 500) % 2 == 0;
float share = v.socPercent == null ? 0f : Math.max(0f, Math.min(1f, v.socPercent / 100f));
// The rays, longer and stronger with the music; none for the moon.
if (!low) {
for (int i = 0; i < RAYS; i++) {
double a = Math.PI * 2 * i / RAYS;
float cos = (float) Math.cos(a), sin = (float) Math.sin(a);
float tip = rad + (22f + pulse * 16f + (i % 2) * 8f) * u;
float nx = -sin * 8f * u, ny = cos * 8f * u;
path.reset();
path.moveTo(cx + cos * (rad - 2f * u) + nx, cy + sin * (rad - 2f * u) + ny);
path.lineTo(cx + cos * tip, cy + sin * tip);
path.lineTo(cx + cos * (rad - 2f * u) - nx, cy + sin * (rad - 2f * u) - ny);
path.close();
fill.setColor(AccentColor.withAlpha(SUN_LIGHT, 0x60 + (int) (share * 0x9F)));
c.drawPath(path, fill);
}
}
// The disc: dark where the charge is gone, the sun's colour as high as it is full.
fill.setColor(0xFF1E2748);
c.drawCircle(cx, cy, rad, fill);
if (v.socPercent != null) {
float level = cy + rad - share * 2f * rad;
c.save();
c.clipRect(cx - rad, level, cx + rad, cy + rad + 1f);
fill.setColor(low ? MOON : SUN);
c.drawCircle(cx, cy, rad, fill);
c.restore();
}
if (low) {
fill.setColor(0x33000000);
c.drawCircle(cx - 36f * u, cy - 30f * u, 13f * u, fill);
c.drawCircle(cx + 34f * u, cy + 40f * u, 17f * u, fill);
c.drawCircle(cx + 40f * u, cy - 36f * u, 8f * u, fill);
}
line.setColor(low ? MOON : SUN_LIGHT);
line.setStrokeWidth(4f * u);
c.drawCircle(cx, cy, rad, line);
String number = fmt(v.socPercent, "%.0f %%", "—");
outlined(c, number, cx, cy + 20f * u, 58f * u, BONE, serif, Paint.Align.CENTER, u);
if (low && blinkOn) outlined(c, "BATTERY LOW", cx, cy + 56f * u, 21f * u, BONE, face, Paint.Align.CENTER, u);
else if (!low) outlined(c, "BATTERY", cx, cy + 56f * u, 19f * u, YELLOW, face, Paint.Align.CENTER, u);
}
// ---------- The leaf: trip, usage, climate, odometer ----------
private void drawLeaf(Canvas c, float u) {
float l = 20f * u, rr = 336f * u, t = 34f * u, b = 246f * u;
// A stem, then the leaf: a rounded sheet with a pointed tip and veins.
line.setColor(GREEN_DARK);
line.setStrokeWidth(7f * u);
c.drawLine(l + 6f * u, b - 4f * u, l - 12f * u, b + 20f * u, line);
path.reset();
path.moveTo(l + 30f * u, t);
path.lineTo(rr - 50f * u, t);
path.quadTo(rr, t + 4f * u, rr + 10f * u, (t + b) / 2f);
path.quadTo(rr, b - 4f * u, rr - 50f * u, b);
path.lineTo(l + 30f * u, b);
path.quadTo(l, b, l, b - 30f * u);
path.lineTo(l, t + 30f * u);
path.quadTo(l, t, l + 30f * u, t);
path.close();
fill.setColor(LEAF);
c.drawPath(path, fill);
line.setColor(0xFF2E8B3A);
line.setStrokeWidth(3f * u);
c.drawPath(path, line);
line.setColor(0x55B6F0A8);
line.setStrokeWidth(2.5f * u);
c.drawLine(l + 8f * u, (t + b) / 2f, rr + 6f * u, (t + b) / 2f, line);
for (int i = 1; i < 6; i++) {
float x = l + i * 50f * u;
c.drawLine(x, (t + b) / 2f, x + 26f * u, t + 8f * u, line);
c.drawLine(x, (t + b) / 2f, x + 26f * u, b - 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 top = t + 12f * u + i * 38f * u;
c.save();
c.rotate((i % 2 == 0 ? -0.6f : 0.6f), (l + rr) / 2f, top + 16f * u);
r.set(l + 14f * u, top, rr - 14f * u, top + 32f * u);
fill.setColor(PAPER);
c.drawRect(r, fill);
label(c, rows[i][0], l + 24f * u, top + 22f * u, 16f * u, 0xFF6B4A8A, thin, Paint.Align.LEFT);
label(c, rows[i][1], rr - 24f * u, top + 24f * u, 21f * u, INK, face, Paint.Align.RIGHT);
c.restore();
}
}
// ---------- The clock ----------
private void drawClock(Canvas c, int w, float u) {
float cx = w - 115f * u, cy = 96f * u, rad = 62f * u;
// Two leaves on top.
fill.setColor(LEAF);
for (int side = -1; side <= 1; side += 2) {
c.save();
c.rotate(side * 40f, cx, cy);
r.set(cx - 12f * u, cy - rad - 34f * u, cx + 12f * u, cy - rad + 8f * u);
c.drawOval(r, fill);
c.restore();
}
fill.setColor(PAPER);
c.drawCircle(cx, cy, rad, fill);
line.setColor(GREEN_DARK);
line.setStrokeWidth(6f * u);
c.drawCircle(cx, cy, rad, line);
label(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + 14f * u, 40f * u, INK, serif, Paint.Align.CENTER);
}
// ---------- The fruit: the range ----------
private void drawFruit(Canvas c, int w, float u) {
float rr = w - 40f * u, l = rr - 250f * u;
label(c, fmt(v.rangeKm, "%.0f km", "— km"), (l + rr) / 2f, 220f * u, 36f * u, BONE, serif, Paint.Align.CENTER);
int whole = Tally.lit(v.rangeKm, 50f, FRUIT);
int index = 0;
for (int row = 0; row < 5; row++) {
for (int k = 0; k <= row; k++, index++) {
fruit(c, row, l + 24f * u + k * 46f * u, 256f * u + row * 50f * u, 19f * u, index < whole, u);
}
}
}
/** One fruit of kind {@code kind} (apple, pear, plum, strawberry, orange); used up ones have a bite out. */
private void fruit(Canvas c, int kind, float x, float y, float s, boolean whole, float u) {
final int[] colours = { 0xFFE53935, 0xFFA8D83F, 0xFF7E3FBF, 0xFFE8263E, 0xFFFF8F1F };
path.reset();
switch (kind) {
case 1: r.set(x - s * 0.8f, y - s, x + s * 0.8f, y + s); path.addOval(r, Path.Direction.CW); break;
case 3:
path.moveTo(x - s, y - s * 0.6f);
path.lineTo(x + s, y - s * 0.6f);
path.lineTo(x, y + s);
path.close();
break;
default: path.addCircle(x, y, s, Path.Direction.CW);
}
if (!whole) {
bite.reset();
bite.addCircle(x + s * 0.85f, y - s * 0.55f, s * 0.5f, Path.Direction.CW);
path.op(bite, Path.Op.DIFFERENCE);
}
fill.setColor(whole ? colours[kind] : AccentColor.withAlpha(colours[kind], 0x78));
c.drawPath(path, fill);
if (!whole) return;
// A stem and a leaf, seeds on the strawberry.
line.setColor(kind == 3 ? GREEN_DARK : 0xFF6B4226);
line.setStrokeWidth(2.5f * u);
c.drawLine(x, y - s * (kind == 3 ? 0.6f : 0.95f), x + 3f * u, y - s * 1.3f, line);
fill.setColor(GREEN);
r.set(x + 1f * u, y - s * 1.35f, x + 11f * u, y - s * 1.05f);
c.drawOval(r, fill);
if (kind == 3) {
fill.setColor(YELLOW);
for (int i = 0; i < 3; i++) c.drawCircle(x + (i - 1) * 7f * u, y + (i % 2) * 7f * u - 2f * u, 1.6f * u, fill);
}
}
// ---------- The stages of its life: the gears ----------
private void drawGears(Canvas c, int w, float u) {
float rr = w - 40f * u, l = rr - 250f * u, cy = 520f * u, rad = 26f * u;
String[] gears = { "P", "R", "N", "D" };
String now = Gear.label(v.gear);
float step = (rr - l) / 4f;
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(0x55FFE066);
c.drawCircle(mid, cy, 40f * u, fill);
}
fill.setColor(0xFF1B2A4A);
c.drawCircle(mid, cy, rad, fill);
line.setColor(on ? YELLOW : 0xFF3A5080);
line.setStrokeWidth(2.5f * u);
c.drawCircle(mid, cy, rad, line);
stage(c, i, mid, cy, u, on);
label(c, gears[i], mid, cy + rad + 22f * u, 22f * u, on ? YELLOW : 0xB0FFFFFF, serif, Paint.Align.CENTER);
}
}
/** The life stage for gear {@code i}: an egg, the caterpillar, a cocoon, a butterfly. */
private void stage(Canvas c, int i, float x, float y, float u, boolean on) {
int alpha = on ? 0xFF : 0x90;
switch (i) {
case 0:
r.set(x - 9f * u, y - 13f * u, x + 9f * u, y + 13f * u);
fill.setColor(AccentColor.withAlpha(0xFFF6F0E4, alpha));
c.drawOval(r, fill);
break;
case 1:
for (int k = 0; k < 3; k++) {
fill.setColor(AccentColor.withAlpha(k == 2 ? HEAD : GREEN, alpha));
c.drawCircle(x + (k - 1) * 10f * u, y + 2f * u, 7f * u, fill);
}
break;
case 2:
r.set(x - 8f * u, y - 16f * u, x + 8f * u, y + 16f * u);
fill.setColor(AccentColor.withAlpha(0xFFB08A5A, alpha));
c.drawOval(r, fill);
line.setColor(AccentColor.withAlpha(0xFF6B4226, alpha));
line.setStrokeWidth(1.5f * u);
c.drawLine(x - 7f * u, y - 4f * u, x + 7f * u, y - 4f * u, line);
c.drawLine(x - 7f * u, y + 5f * u, x + 7f * u, y + 5f * u, line);
break;
default:
fill.setColor(AccentColor.withAlpha(0xFF7E57E0, alpha));
r.set(x - 16f * u, y - 14f * u, x, y + 2f * u);
c.drawOval(r, fill);
r.set(x, y - 14f * u, x + 16f * u, y + 2f * u);
c.drawOval(r, fill);
fill.setColor(AccentColor.withAlpha(0xFFFF5FA2, alpha));
r.set(x - 14f * u, y - 2f * u, x, y + 12f * u);
c.drawOval(r, fill);
r.set(x, y - 2f * u, x + 14f * u, y + 12f * u);
c.drawOval(r, fill);
}
}
// ---------- The branch it crawls on ----------
/** A branch under the caterpillar; its knots and leaves slide backwards as the car goes. */
private void drawBranch(Canvas c, int w, float u) {
float hx = w / 2f + 250f * u, y = 410f * u + 87f * u;
float l = hx - 640f * u, rr = hx + 110f * u, span = rr - l;
line.setColor(0xFF6B4A2B);
line.setStrokeWidth(20f * u);
c.drawLine(l, y, rr, y, line);
line.setColor(0x44FFD59A);
line.setStrokeWidth(4f * u);
c.drawLine(l + 6f * u, y - 6f * u, rr - 6f * u, y - 6f * u, line);
for (int k = 0; k < 8; k++) {
float x = l + (((k * span / 8f) - travel) % span + span) % span;
fill.setColor(0xFF4A3018);
r.set(x - 9f * u, y - 5f * u, x + 9f * u, y + 6f * u);
c.drawOval(r, fill);
if (k % 2 == 0) {
fill.setColor(0xFF3FA34D);
r.set(x - 4f * u, y + 6f * u, x + 20f * u, y + 18f * u);
c.drawOval(r, fill);
}
}
}
// ---------- The caterpillar: speed and power ----------
/** Tissue-paper greens for a segment: base colours, a light patch and a teal patch, by state. */
private static final int[][] DRAWING = { { 0xFF4CC15C, 0xFF38A94A, 0xFF66CC6A }, { 0xFF9BE08A, 0xFF2FA89A } };
private static final int[][] REGEN = { { 0xFF2A8FC9, 0xFF1F78B4, 0xFF4AA8DA }, { 0xFF9BDCFF, 0xFF3AC8D0 } };
private static final int[][] OFF = { { 0xFF2B7A40, 0xFF256B38, 0xFF328C4B }, { 0xFF3A9060, 0xFF2A8078 } };
/**
* The caterpillar arches over the middle like the one on the book's cover: overlapping chunks of
* mottled tissue paper in greens and teals, a fringe of yellow hair along its back, olive feet,
* and a red-orange head with a yellow face, green eyes and purple antennae. Power lights the
* segments (bright green, or blue when regenerating, dull when dark); the speed is on its face.
*/
private void drawCaterpillar(Canvas c, int w, float u, float pulse) {
float hx = w / 2f + 250f * u, hy = 410f * u, hr = 76f * u;
int lit = Tally.ofPower(v.powerKw, 150f, 50f, SEGMENTS);
int[][] palette = regen() ? REGEN : DRAWING;
// At rest it arches high in the middle. Crawling, a real caterpillar pulls its rear forward and
// humps the body up, and the hump runs from the tail to the head while the feet it lifts swing
// forward and the others stay planted; the body also bunches and stretches as it goes.
float arch = (92f * (1f - crawl) + 30f * crawl) * u;
float cycle = phase % 1f;
float stretch = 1f + 0.08f * crawl * (float) Math.sin(cycle * 2f * Math.PI);
float wave = 1.25f - cycle * 1.6f;
float[] xs = new float[SEGMENTS], ys = new float[SEGMENTS], lift = new float[SEGMENTS];
for (int i = 0; i < SEGMENTS; i++) {
float along = (SEGMENTS - 1 - i) / (SEGMENTS - 1f);
float d = Math.max(0f, 1f - Math.abs(along - wave) / 0.4f);
lift[i] = crawl * d * d * (3f - 2f * d);
xs[i] = hx - 92f * u - i * 48f * u * stretch;
ys[i] = hy + 24f * u - arch * (float) Math.sin(Math.PI * (i + 0.7) / (SEGMENTS + 0.4)) - lift[i] * 46f * u;
}
// From the tail towards the head, each overlapping the one before.
for (int i = SEGMENTS - 1; i >= 0; i--) {
boolean on = i < lit;
int[][] pal = on ? palette : OFF;
float x = xs[i], y = ys[i];
int prev = Math.max(0, i - 1), next = Math.min(SEGMENTS - 1, i + 1);
float angle = (float) Math.toDegrees(Math.atan2(ys[prev] - ys[next], xs[prev] - xs[next]));
float grow = 1f + (on ? pulse * (i % 2 == 0 ? 0.16f : 0.1f) : 0f);
c.save();
c.rotate(angle, x, y);
c.scale(grow, grow, x, y);
// The foot, under every other segment.
if (i % 2 == 0) {
line.setColor(0xFF8A7B2E);
line.setStrokeWidth(8f * u);
// A lifted foot swings forward and up; a planted one is on the branch.
float up = lift[i] * 12f * u;
c.drawLine(x, y + 46f * u, x + (3f + lift[i] * 12f) * u, y + 60f * u - up, line);
}
// The chunk of paper with two patches of other tissue on it.
r.set(x - 37f * u, y - 48f * u, x + 37f * u, y + 48f * u);
fill.setColor(pal[0][i % 3]);
c.drawOval(r, fill);
fill.setColor(AccentColor.withAlpha(pal[1][0], 0x80));
r.set(x - 28f * u, y - (i % 2 == 0 ? 38f : 10f) * u, x + 4f * u, y + (i % 2 == 0 ? -4f : 36f) * u);
c.drawOval(r, fill);
fill.setColor(AccentColor.withAlpha(pal[1][1], 0x70));
r.set(x - 2f * u, y - (i % 2 == 0 ? 8f : 40f) * u, x + 30f * u, y + (i % 2 == 0 ? 38f : 0f) * u);
c.drawOval(r, fill);
// The fringe of hair along the back.
line.setColor(on ? 0xFFD9C84A : 0x80D9C84A);
line.setStrokeWidth(2f * u);
for (int k = -2; k <= 2; k++) {
c.drawLine(x + k * 12f * u, y - (46f - Math.abs(k) * 4f) * u, x + k * 15f * u, y - (57f - Math.abs(k) * 4f) * u, line);
}
c.restore();
}
// The head: purple antennae, a red-orange head with a yellow face, green eyes, the speed.
line.setColor(0xFF7B68C8);
line.setStrokeWidth(8f * u);
c.drawLine(hx - 24f * u, hy - hr + 10f * u, hx - 46f * u, hy - hr - 40f * u, line);
c.drawLine(hx + 24f * u, hy - hr + 10f * u, hx + 46f * u, hy - hr - 40f * u, line);
float hs = 1f + pulse * 0.04f;
fill.setColor(0xFFE8452B);
c.drawCircle(hx, hy, hr * hs, fill);
r.set(hx - 52f * u, hy - 64f * u, hx + 52f * u, hy + 62f * u);
fill.setColor(0xFFFFC21F);
c.drawOval(r, fill);
for (int side = -1; side <= 1; side += 2) {
r.set(hx + side * 26f * u - 11f * u, hy - 50f * u, hx + side * 26f * u + 11f * u, hy - 20f * u);
fill.setColor(0xFF3F9A3A);
c.drawOval(r, fill);
line.setColor(INK);
line.setStrokeWidth(2f * u);
c.drawOval(r, line);
fill.setColor(INK);
c.drawCircle(hx + side * 26f * u, hy - 38f * u, 3.5f * u, fill);
}
label(c, fmt(v.speedKmh, "%.0f", "—"), hx, hy + 30f * u, 46f * u, INK, serif, Paint.Align.CENTER);
label(c, "KM/H", hx, hy + 48f * u, 13f * u, 0xFF7A3A10, face, Paint.Align.CENTER);
boolean regen = regen();
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
Math.abs(v.powerKw));
outlined(c, power, hx - 92f * u - 4.5f * 48f * u, 556f * u, 34f * u, regen ? ICE : BONE, face, Paint.Align.CENTER, u);
}
// ---------- What plays: a butterfly or a ladybird tows it on tissue paper ----------
/**
* A butterfly and a ladybird take turns towing a waving strip of tissue paper with the song across
* the sky, behind the clock and the leaf, again and again while something plays. The paper's
* colour is 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);
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, tow = 76f * u;
float bx = SongBanner.left(t, w, bw, tow, u);
float yc = 66f * u + (float) Math.sin(t * 1.3f) * 3f * u;
int paper = TISSUE[Math.floorMod((v.songTitle + "\n" + v.songArtist).hashCode(), TISSUE.length)];
boolean darkInk = paper == TISSUE[3] || paper == TISSUE[5];
banner.draw(c, fill, line, text, s, bx, yc, bw, bh, t, u, 9f * u, paper, 0x66FFFFFF, darkInk ? INK : 0xFFFFFFFF);
boolean butterfly = SongBanner.pass(t, w, bw, tow, u) % 2 == 0;
float right = bx + bw, cx = right + tow, cy = yc - 4f * u, size = (butterfly ? 60f : 44f) * u;
float[] tail = butterfly ? SongCarriers.butterflyTail : SongCarriers.ladybirdTail;
float ax = cx + tail[0] * size, ay = cy + tail[1] * size;
line.setColor(0xCCFFFFFF);
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 ? 7f : 18f));
if (butterfly) SongCarriers.butterfly(c, fill, line, path, r, cx, cy, size, flap, u);
else SongCarriers.ladybird(c, fill, line, path, r, cx, cy, size, flap, u);
return true;
}
}