App 2.29.0: a chibi Mascot, chosen in Settings beside the tyre unit

ChibiFigure draws a small, full-figure co-pilot in a hoodie whose whole
body acts the mood (waving, arms up and hopping, fist, hands on cheeks,
nose bubble, pointing back, leaning into the wind). Settings → Mascot
(Classic / Chibi, MascotCharacter) shows only for the Mascot style; the
chibi has lines of her own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
This commit is contained in:
2026-10-24 23:17:21 +02:00
co-authored by Claude Opus 5.5
parent 57cabfa0e1
commit 7df1930edd
12 changed files with 542 additions and 15 deletions
@@ -24,14 +24,20 @@ import android.os.SystemClock;
* power, 5-minute usage and the trip; the clock top right, left of the full-screen button.
* Unread values show "—".
*
* Two characters ({@link MascotCharacter}): this classic co-pilot, or the chibi drawn by
* {@link ChibiFigure}, each with her own lines.
*
* Drawn on a 100 × 100 grid scaled to the pane. Nothing animates on its own: blinking, the Zs,
* sparkles and wind move because readings redraw the view every 0.2 s.
*/
final class MascotView extends ClusterCanvasView {
private static final int[] LINES = {
R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content };
/** Her lines per {@link MascotCharacter}, then per {@link MascotMood}, in their orders. */
private static final int[][] LINES = {
{ R.array.mascot_waiting, R.array.mascot_reverse, R.array.mascot_worried, R.array.mascot_sleeping,
R.array.mascot_cheering, R.array.mascot_determined, R.array.mascot_fast, R.array.mascot_content },
{ R.array.chibi_waiting, R.array.chibi_reverse, R.array.chibi_worried, R.array.chibi_sleeping,
R.array.chibi_cheering, R.array.chibi_determined, R.array.chibi_fast, R.array.chibi_content } };
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
private final Paint stroke = new Paint(Paint.ANTI_ALIAS_FLAG);
@@ -39,7 +45,9 @@ final class MascotView extends ClusterCanvasView {
private final Path p = new Path();
private final RectF r = new RectF();
private final MascotMood.Holder holder = new MascotMood.Holder();
private final String[][] lines = new String[LINES.length][];
private final String[][][] lines = new String[LINES.length][][];
private final ChibiFigure chibi;
private MascotCharacter character = MascotCharacter.CLASSIC;
private final int accent, accentDark, accentLight, regen, warn, white, grey, track;
private final int hair = 0xFF2E2440, hairLight = 0xFF5A4880, skin = 0xFFFBE2D0, skinShade = 0xFFE9BFA9,
iris = 0xFF2F8F9D, pupil = 0xFF123238, lash = 0xFF221A2E, lips = 0xFFC2606A, mouth = 0xFF8E3A48,
@@ -56,7 +64,11 @@ final class MascotView extends ClusterCanvasView {
white = context.getColor(R.color.ev_text_primary);
grey = context.getColor(R.color.ev_text_secondary);
track = context.getColor(R.color.ev_outline);
for (int i = 0; i < LINES.length; i++) lines[i] = context.getResources().getStringArray(LINES[i]);
for (int i = 0; i < LINES.length; i++) {
lines[i] = new String[LINES[i].length][];
for (int j = 0; j < LINES[i].length; j++) lines[i][j] = context.getResources().getStringArray(LINES[i][j]);
}
chibi = new ChibiFigure(accent, accentLight, regen);
stroke.setStyle(Paint.Style.STROKE);
stroke.setStrokeCap(Paint.Cap.ROUND);
stroke.setStrokeJoin(Paint.Join.ROUND);
@@ -64,6 +76,12 @@ final class MascotView extends ClusterCanvasView {
text.setFontFeatureSettings("tnum");
}
/** Who sits beside the values (Settings → Mascot, shown for this style only). */
void setCharacter(MascotCharacter character) {
this.character = character == null ? MascotCharacter.CLASSIC : character;
invalidate();
}
@Override
protected void onDraw(Canvas c) {
super.onDraw(c);
@@ -80,11 +98,14 @@ final class MascotView extends ClusterCanvasView {
c.save();
c.translate(ox, oy);
c.scale(k, k);
drawCharacter(c, mood, t);
boolean chibiShown = character == MascotCharacter.CHIBI;
if (chibiShown) chibi.draw(c, mood, t, t % 4200 < 260);
else drawCharacter(c, mood, t);
c.restore();
float x0 = ox + 78f * k;
drawBubble(c, mood, t, x0, h, ox + 70f * k, oy + 46f * k, w);
float tipX = chibiShown ? ChibiFigure.TIP_X : 70f, tipY = chibiShown ? ChibiFigure.TIP_Y : 46f;
drawBubble(c, mood, t, x0, h, ox + tipX * k, oy + tipY * k, w);
drawValues(c, x0, ox + 93f * k, w, h);
}
@@ -420,7 +441,7 @@ final class MascotView extends ClusterCanvasView {
// ---------- Speech bubble and values, in pixels ----------
private void drawBubble(Canvas c, MascotMood mood, long t, float x0, float h, float tipX, float tipY, float w) {
String[] options = lines[mood.ordinal()];
String[] options = lines[character.ordinal()][mood.ordinal()];
String line = options.length == 0 ? "" : options[(int) ((t / 30000 + mood.ordinal()) % options.length)];
text.setTextAlign(Paint.Align.LEFT);
float pad = h * 0.045f, room = w - 140f * dp - x0 - 2f * pad;