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; } }