App 2.51.0: the Caterpillar after the book's newer pictures

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-11-07 11:01:09 +01:00
co-authored by Claude Sonnet 5.5
parent 3cad9499fb
commit 142f016174
6 changed files with 124 additions and 67 deletions
@@ -6,6 +6,7 @@ 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;
@@ -477,25 +478,58 @@ final class CaterpillarView extends ClusterCanvasView {
// ---------- 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 } };
/** Paper for a segment: three base colours, a light and a teal for the strokes; by state. */
private static final int[][] DRAWING = { { 0xFF5FBF52, 0xFF2E9E6A, 0xFF8ACD4A }, { 0xFFC9F06A, 0xFF1F8A7A } };
private static final int[][] REGEN = { { 0xFF2A8FC9, 0xFF1F78B4, 0xFF4AA8DA }, { 0xFF9BDCFF, 0xFF3AC8D0 } };
private static final int[][] OFF = { { 0xFF2B7A40, 0xFF256B38, 0xFF328C4B }, { 0xFF3A9060, 0xFF2A8078 } };
private static final float SEG_R = 47f;
/** [state][base colour]: a round, lit-from-the-upper-left shading for one segment, in its own units. */
private final Paint[][] shades = new Paint[3][3];
private Paint headShade;
private float shadesFor;
private static int mix(int a, int b, float t) {
int ar = (a >> 16) & 0xFF, ag = (a >> 8) & 0xFF, ab = a & 0xFF;
int br = (b >> 16) & 0xFF, bg = (b >> 8) & 0xFF, bb = b & 0xFF;
return 0xFF000000 | (ar + (int) ((br - ar) * t)) << 16 | (ag + (int) ((bg - ag) * t)) << 8 | (ab + (int) ((bb - ab) * t));
}
private void buildShades(float u) {
int[][][] states = { DRAWING, REGEN, OFF };
float r = SEG_R * u;
for (int st = 0; st < 3; st++) {
for (int k = 0; k < 3; k++) {
int base = states[st][0][k];
Paint p = new Paint(Paint.ANTI_ALIAS_FLAG);
p.setShader(new RadialGradient(-0.3f * r, -0.35f * r, 1.5f * r,
new int[] { mix(base, 0xFFFFFFFF, 0.3f), base, mix(base, 0xFF000000, 0.18f) },
new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
shades[st][k] = p;
}
}
headShade = new Paint(Paint.ANTI_ALIAS_FLAG);
headShade.setShader(new RadialGradient(-25f * u, -30f * u, 130f * u,
new int[] { 0xFFFF6A45, 0xFFE8452B, 0xFFA82414 }, new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
shadesFor = u;
}
/**
* 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.
* The caterpillar, after the picture book's newer pictures: plump round segments like balls of
* wound paper, each with brush strokes in light green and teal, bigger towards the head, a fringe
* of yellow and orange bristles along the back, stubby brown feet, and a red apple-shaped head with
* big teal eyes and two lilac ears. At rest it arches high in the middle. Power lights the segments
* (bright green, or blue when regenerating, dull when dark); the speed is on the head under the
* eyes. 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.
*/
private void drawCaterpillar(Canvas c, int w, float u, float pulse) {
float hx = w / 2f + 250f * u, hy = 410f * u, hr = 76f * u;
if (shadesFor != u) buildShades(u);
float hx = w / 2f + 250f * u, hy = 410f * 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;
int state = lit == 0 ? 2 : regen() ? 1 : 0;
float arch = (80f * (1f - crawl) + 26f * 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;
@@ -504,73 +538,94 @@ final class CaterpillarView extends ClusterCanvasView {
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;
xs[i] = hx - 104f * u - i * 46f * u * stretch;
ys[i] = hy + 22f * u - arch * (float) Math.sin(Math.PI * (i + 0.7) / (SEGMENTS + 0.4)) - lift[i] * 46f * u;
}
float r = SEG_R * 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[][] pal = on ? (regen() ? REGEN : DRAWING) : OFF;
Paint shade = shades[on ? (regen() ? 1 : 0) : 2][i % 3];
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);
// Bigger towards the head, and swelling a little with the music.
float grow = (0.82f + 0.2f * (1f - i / (SEGMENTS - 1f))) * (1f + (on ? pulse * (i % 2 == 0 ? 0.14f : 0.09f) : 0f));
c.save();
c.rotate(angle, x, y);
c.scale(grow, grow, x, y);
// The foot, under every other segment.
c.translate(xs[i], ys[i]);
c.rotate(angle);
c.scale(grow, grow);
// A stubby brown foot under every other one: a lifted foot swings forward and up.
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);
float up = lift[i] * 12f * u, fwd = lift[i] * 12f * u;
this.r.set(-0.2f * r + fwd, 0.85f * r - up, 0.4f * r + fwd, 1.4f * r - up);
fill.setColor(0xFF9A6A3A);
c.drawOval(this.r, fill);
}
// 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);
this.r.set(-r, -1.05f * r, r, 1.05f * r);
c.drawOval(this.r, shade);
// Brush strokes down the ball, light and teal in turn.
line.setStrokeWidth(3f * 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);
float x0 = k * 0.32f * r;
line.setColor(AccentColor.withAlpha(pal[1][(k + 2) % 2], 0x70));
path.reset();
path.moveTo(x0 * 0.7f, -0.8f * r);
path.quadTo(x0 * 1.5f, 0f, x0 * 0.7f, 0.8f * r);
c.drawPath(path, line);
}
// The fringe along the back, in yellow and orange.
line.setStrokeWidth(3f * u);
for (int k = -3; k <= 3; k++) {
line.setColor(on ? (k % 2 == 0 ? 0xFFFFC21F : 0xFFFF8A1F) : 0x80FFC21F);
c.drawLine(k * 0.28f * r, (-0.98f + Math.abs(k) * 0.05f) * r, k * 0.34f * r, (-1.22f + Math.abs(k) * 0.06f) * r, 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);
drawHead(c, hx, hy, u, pulse);
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);
outlined(c, power, hx - 104f * u - 4.5f * 46f * u, 556f * u, 34f * u, regen ? ICE : BONE, face, Paint.Align.CENTER, u);
}
/** A red apple-shaped head, big teal eyes with yellow rims and two lilac ears, the speed under the eyes. */
private void drawHead(Canvas c, float hx, float hy, float u, float pulse) {
float hs = 1f + pulse * 0.04f;
c.save();
c.translate(hx, hy);
c.scale(hs, hs);
// The ears, stubby and rounded, leaning out.
for (int side = -1; side <= 1; side += 2) {
c.save();
c.rotate(side * 12f, side * 36f * u, -64f * u);
r.set(side * 36f * u - 15f * u, -122f * u, side * 36f * u + 15f * u, -58f * u);
fill.setColor(0xFFB59AE0);
c.drawOval(r, fill);
r.set(side * 36f * u - 15f * u, -122f * u, side * 36f * u - 2f * u, -58f * u);
fill.setColor(0x40FFFFFF);
c.drawOval(r, fill);
c.restore();
}
r.set(-88f * u, -76f * u, 88f * u, 82f * u);
c.drawOval(r, headShade);
fill.setColor(0x55000000);
r.set(-12f * u, -78f * u, 12f * u, -68f * u);
c.drawOval(r, fill);
for (int side = -1; side <= 1; side += 2) {
float ex = side * 34f * u;
r.set(ex - 19f * u, -50f * u, ex + 19f * u, 6f * u);
fill.setColor(0xFF2FA89A);
c.drawOval(r, fill);
line.setColor(0xFFFFD84A);
line.setStrokeWidth(4f * u);
c.drawOval(r, line);
fill.setColor(0x99FFFFFF);
c.drawCircle(ex - 6f * u, -34f * u, 4.5f * u, fill);
}
outlined(c, fmt(v.speedKmh, "%.0f", "—"), 0f, 50f * u, 40f * u, 0xFFFFF3D0, serif, Paint.Align.CENTER, u);
label(c, "KM/H", 0f, 70f * u, 12f * u, 0xFFFFE0B0, face, Paint.Align.CENTER);
c.restore();
}
// ---------- What plays: a butterfly or a ladybird tows it on tissue paper ----------