2 Commits
Author SHA1 Message Date
nilsandClaude Sonnet 5.5 142f016174 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
2026-11-07 11:01:09 +01:00
nilsandClaude Sonnet 5.5 3cad9499fb SPECS: app 2.50.0 published
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
2026-11-07 10:22:51 +01:00
6 changed files with 125 additions and 67 deletions
+2
View File
@@ -140,6 +140,8 @@ tyre pressures, GPS (lat, lon, elevation, heading) and the is_* flags. It does *
confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces confirmed car facts, app structure, UI conventions, build/test/ship, documents. It replaces
upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator` upstream's EVABRPUploader text. The test emulator is now in the flake: `nix run .#car-emulator`
(Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked). (Android 9 AOSP at 1920×720, 160 dpi; the car build installs as the system user — checked).
- 2026-11-07 — App 2.51.0: **the Caterpillar after the book's newer pictures** (owner sent a 3D picture of it). Plump round segments shaded like balls of wound paper (radial light from the upper left) 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; a big round red head with teal eyes rimmed in yellow and two lilac ears, the speed under the eyes. Crawl, readings and colours (green lit, blue regenerating, dull dark) unchanged. Original drawing, not the artwork. Tested: compiles, emulator screenshots with sample values (`screenshots/themes/caterpillar*.png`). Not tried on the car.
- 2026-11-07 — App 2.50.0 published (`car-2.50.0`).
- 2026-11-07 — App 2.50.0: **the Caterpillar crawls** (owner: "make it so the caterpillar appears to be going when we drive? Using normal caterpillar movement?"). While the car moves it crawls as a caterpillar does: a hump runs from the tail to the head, the body bunches and stretches a little, feet that are lifted swing forward and the others stay planted on a branch whose knots and leaves slide backwards with the speed; the crawl is quicker the faster the car goes (at most 1.2 crawls a second). It does not change its place on screen, so the readings stay put. Parked, it eases back into its high arch (it keeps drawing for that moment only). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (frames 0.25 s apart show the hump travelling and the branch sliding). Not tried on the car. - 2026-11-07 — App 2.50.0: **the Caterpillar crawls** (owner: "make it so the caterpillar appears to be going when we drive? Using normal caterpillar movement?"). While the car moves it crawls as a caterpillar does: a hump runs from the tail to the head, the body bunches and stretches a little, feet that are lifted swing forward and the others stay planted on a branch whose knots and leaves slide backwards with the speed; the crawl is quicker the faster the car goes (at most 1.2 crawls a second). It does not change its place on screen, so the readings stay put. Parked, it eases back into its high arch (it keeps drawing for that moment only). Two screenshots redone. Tested: unit tests, lint, emulator with sample values (frames 0.25 s apart show the hump travelling and the branch sliding). Not tried on the car.
- 2026-11-07 — App 2.49.1 published (`car-2.49.1`). - 2026-11-07 — App 2.49.1 published (`car-2.49.1`).
- 2026-11-07 — App 2.49.1: **the Caterpillar looks more like the one in the book** (owner: "Make it more like the original caterpillar", with the German edition's cover as the reference). It arches over the middle like the cover's: overlapping chunks of mottled green and teal paper, a yellow fringe along the back, olive feet, and a red-orange head with a yellow face, green eyes and purple antennae; the speed is on the face. Power still lights the segments (blue when regenerating) and the back humps while driving. Still my own drawing of it, not the book's artwork. Two screenshots redone. Tested: unit tests, lint, emulator with sample values. Not tried on the car. - 2026-11-07 — App 2.49.1: **the Caterpillar looks more like the one in the book** (owner: "Make it more like the original caterpillar", with the German edition's cover as the reference). It arches over the middle like the cover's: overlapping chunks of mottled green and teal paper, a yellow fringe along the back, olive feet, and a red-orange head with a yellow face, green eyes and purple antennae; the speed is on the face. Power still lights the segments (blue when regenerating) and the back humps while driving. Still my own drawing of it, not the book's artwork. Two screenshots redone. Tested: unit tests, lint, emulator with sample values. Not tried on the car.
+3 -2
View File
@@ -308,8 +308,9 @@ trumpet; the paper's colour is fixed by the song. The app turns red with it.
After a picture book about a very hungry caterpillar, drawn anew in cut-paper shapes and bright After a picture book about a very hungry caterpillar, drawn anew in cut-paper shapes and bright
tissue colours on the book's night page. The caterpillar arches over the middle, after the one on tissue colours on the book's night page. The caterpillar arches over the middle, after the one on
the book's cover: overlapping chunks of mottled green paper with a yellow fringe along the back and the newer pictures of the book: plump, round segments like balls of wound paper with brush strokes
olive feet, and a red-orange head with a yellow face, green eyes and purple antennae. The face in light green and teal, a fringe of yellow and orange bristles along the back and stubby brown
feet, and a big round red head with teal eyes rimmed in yellow and two lilac ears. The face
carries the speed, and its ten segments are the power, lit from the head backwards while drawing and carries the speed, and its ten segments are the power, lit from the head backwards while drawing and
blue while regenerating. While you drive it crawls the way a caterpillar does: a hump runs from its blue while regenerating. While you drive it crawls the way a caterpillar does: a hump runs from its
tail to its head, the feet it lifts swing forward and the others stay planted, and the branch it tail to its head, the feet it lifts swing forward and the others stay planted, and the branch it
+2 -2
View File
@@ -34,8 +34,8 @@ android {
applicationId "com.stinnesbeck.nilsmg4" applicationId "com.stinnesbeck.nilsmg4"
minSdk 27 minSdk 27
targetSdk 36 targetSdk 36
versionCode 168 versionCode 169
versionName "2.50.0" versionName "2.51.0"
// Nothing about the server ships in the APK: its URL and ingest token are entered in // Nothing about the server ships in the APK: its URL and ingest token are entered in
// the app's Server tab (ServerConfig), and the database is only reachable by the server. // the app's Server tab (ServerConfig), and the database is only reachable by the server.
@@ -6,6 +6,7 @@ import android.graphics.Canvas;
import android.graphics.LinearGradient; import android.graphics.LinearGradient;
import android.graphics.Paint; import android.graphics.Paint;
import android.graphics.Path; import android.graphics.Path;
import android.graphics.RadialGradient;
import android.graphics.RectF; import android.graphics.RectF;
import android.graphics.Shader; import android.graphics.Shader;
import android.graphics.Typeface; import android.graphics.Typeface;
@@ -477,25 +478,58 @@ final class CaterpillarView extends ClusterCanvasView {
// ---------- The caterpillar: speed and power ---------- // ---------- The caterpillar: speed and power ----------
/** Tissue-paper greens for a segment: base colours, a light patch and a teal patch, by state. */ /** Paper for a segment: three base colours, a light and a teal for the strokes; by state. */
private static final int[][] DRAWING = { { 0xFF4CC15C, 0xFF38A94A, 0xFF66CC6A }, { 0xFF9BE08A, 0xFF2FA89A } }; private static final int[][] DRAWING = { { 0xFF5FBF52, 0xFF2E9E6A, 0xFF8ACD4A }, { 0xFFC9F06A, 0xFF1F8A7A } };
private static final int[][] REGEN = { { 0xFF2A8FC9, 0xFF1F78B4, 0xFF4AA8DA }, { 0xFF9BDCFF, 0xFF3AC8D0 } }; private static final int[][] REGEN = { { 0xFF2A8FC9, 0xFF1F78B4, 0xFF4AA8DA }, { 0xFF9BDCFF, 0xFF3AC8D0 } };
private static final int[][] OFF = { { 0xFF2B7A40, 0xFF256B38, 0xFF328C4B }, { 0xFF3A9060, 0xFF2A8078 } }; 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 * The caterpillar, after the picture book's newer pictures: plump round segments like balls of
* mottled tissue paper in greens and teals, a fringe of yellow hair along its back, olive feet, * wound paper, each with brush strokes in light green and teal, bigger towards the head, a fringe
* and a red-orange head with a yellow face, green eyes and purple antennae. Power lights the * of yellow and orange bristles along the back, stubby brown feet, and a red apple-shaped head with
* segments (bright green, or blue when regenerating, dull when dark); the speed is on its face. * 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) { 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 lit = Tally.ofPower(v.powerKw, 150f, 50f, SEGMENTS);
int[][] palette = regen() ? REGEN : DRAWING; int state = lit == 0 ? 2 : regen() ? 1 : 0;
// At rest it arches high in the middle. Crawling, a real caterpillar pulls its rear forward and float arch = (80f * (1f - crawl) + 26f * crawl) * u;
// 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 cycle = phase % 1f;
float stretch = 1f + 0.08f * crawl * (float) Math.sin(cycle * 2f * Math.PI); float stretch = 1f + 0.08f * crawl * (float) Math.sin(cycle * 2f * Math.PI);
float wave = 1.25f - cycle * 1.6f; float wave = 1.25f - cycle * 1.6f;
@@ -504,73 +538,94 @@ final class CaterpillarView extends ClusterCanvasView {
float along = (SEGMENTS - 1 - i) / (SEGMENTS - 1f); float along = (SEGMENTS - 1 - i) / (SEGMENTS - 1f);
float d = Math.max(0f, 1f - Math.abs(along - wave) / 0.4f); float d = Math.max(0f, 1f - Math.abs(along - wave) / 0.4f);
lift[i] = crawl * d * d * (3f - 2f * d); lift[i] = crawl * d * d * (3f - 2f * d);
xs[i] = hx - 92f * u - i * 48f * u * stretch; xs[i] = hx - 104f * u - i * 46f * u * stretch;
ys[i] = hy + 24f * u - arch * (float) Math.sin(Math.PI * (i + 0.7) / (SEGMENTS + 0.4)) - lift[i] * 46f * u; 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. // From the tail towards the head, each overlapping the one before.
for (int i = SEGMENTS - 1; i >= 0; i--) { for (int i = SEGMENTS - 1; i >= 0; i--) {
boolean on = i < lit; boolean on = i < lit;
int[][] pal = on ? palette : OFF; int[][] pal = on ? (regen() ? REGEN : DRAWING) : OFF;
float x = xs[i], y = ys[i]; Paint shade = shades[on ? (regen() ? 1 : 0) : 2][i % 3];
int prev = Math.max(0, i - 1), next = Math.min(SEGMENTS - 1, i + 1); 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 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.save();
c.rotate(angle, x, y); c.translate(xs[i], ys[i]);
c.scale(grow, grow, x, y); c.rotate(angle);
// The foot, under every other segment. c.scale(grow, grow);
// A stubby brown foot under every other one: a lifted foot swings forward and up.
if (i % 2 == 0) { if (i % 2 == 0) {
line.setColor(0xFF8A7B2E); float up = lift[i] * 12f * u, fwd = lift[i] * 12f * u;
line.setStrokeWidth(8f * u); this.r.set(-0.2f * r + fwd, 0.85f * r - up, 0.4f * r + fwd, 1.4f * r - up);
// A lifted foot swings forward and up; a planted one is on the branch. fill.setColor(0xFF9A6A3A);
float up = lift[i] * 12f * u; c.drawOval(this.r, fill);
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. this.r.set(-r, -1.05f * r, r, 1.05f * r);
r.set(x - 37f * u, y - 48f * u, x + 37f * u, y + 48f * u); c.drawOval(this.r, shade);
fill.setColor(pal[0][i % 3]); // Brush strokes down the ball, light and teal in turn.
c.drawOval(r, fill); line.setStrokeWidth(3f * u);
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++) { 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(); c.restore();
} }
// The head: purple antennae, a red-orange head with a yellow face, green eyes, the speed. drawHead(c, hx, hy, u, pulse);
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(); boolean regen = regen();
String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW", String power = v.powerKw == null ? "— kW" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW",
Math.abs(v.powerKw)); 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 ---------- // ---------- What plays: a butterfly or a ladybird tows it on tissue paper ----------
Binary file not shown.

Before

Width:  |  Height:  |  Size: 266 KiB

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 233 KiB

After

Width:  |  Height:  |  Size: 282 KiB