Files
mg4/app/src/main/java/com/evsuite/abrp/ChibiFigure.java
T
nilsandClaude Opus 5.5 7df1930edd 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
2026-10-24 23:17:21 +02:00

398 lines
15 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package com.evsuite.abrp;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.RectF;
/**
* The chibi co-pilot for the Mascot Overview style ({@link MascotCharacter#CHIBI}): the small,
* full-figure version — a big round head with a coral bob and a star clip, a hoodie in the
* accent colour, short legs, white trainers — drawn with dark outlines like a sticker.
*
* Her whole body acts the mood: waves while content, throws both arms up and hops while
* cheering (star eyes), pumps a fist when determined, holds her cheeks with a tear when worried,
* sleeps with a nose bubble in P, glances and points back in R, leans into the wind when
* fast, and wonders "?" while waiting.
*
* Draws on the same 100 × 100 grid as {@link MascotView}'s classic figure; it holds no state.
*/
final class ChibiFigure {
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 Path p = new Path();
private final Path q = new Path();
private final RectF r = new RectF();
private final int accent, accentLight, regen;
private final int ink = 0xFF3A2A33, hair = 0xFFF0855C, hairLight = 0xFFFFB08A, skin = 0xFFFFE6D6,
iris = 0xFF6E45B8, irisLight = 0xFFA27BE6, blush = 0x66FF7A8A, trousers = 0xFF2D3142,
shoe = 0xFFF6F6F8, mouth = 0xFF8E3A48, tongue = 0xFFE88A95;
ChibiFigure(int accent, int accentLight, int regen) {
this.accent = accent;
this.accentLight = accentLight;
this.regen = regen;
line.setStyle(Paint.Style.STROKE);
line.setStrokeCap(Paint.Cap.ROUND);
line.setStrokeJoin(Paint.Join.ROUND);
line.setColor(ink);
text.setTextAlign(Paint.Align.CENTER);
text.setFakeBoldText(true);
}
/** Where the speech bubble's tail points, on the grid. */
static final float TIP_X = 70f, TIP_Y = 34f;
void draw(Canvas c, MascotMood mood, long t, boolean blink) {
// Smaller and further left than the grid, so her big head stays clear of the speed.
c.save();
c.translate(-6f, 0f);
c.scale(0.92f, 0.92f, 50f, 100f);
// A hop while cheering.
float hop = mood == MascotMood.CHEERING ? -3.5f * (float) Math.abs(Math.sin(t / 180.0)) : 0f;
fill.setColor(0x40000000);
float shadow = 18f + hop * 0.8f;
c.drawOval(oval(50f - shadow, 96.5f, 50f + shadow, 100f), fill);
c.save();
c.translate(0f, hop);
float lean = mood == MascotMood.FAST ? 5f : mood == MascotMood.SLEEPING ? -6f : 0f;
c.rotate(lean, 50f, 96f);
drawLegs(c);
drawBody(c);
// Hands on her cheeks and the fist at her chest are in front of the head.
boolean inFront = mood == MascotMood.WORRIED || mood == MascotMood.DETERMINED;
if (!inFront) drawArms(c, mood, t);
drawHead(c, mood, t, blink);
if (inFront) drawArms(c, mood, t);
c.restore();
switch (mood) {
case SLEEPING: zs(c, t); break;
case CHEERING: sparkles(c, t); break;
case FAST: wind(c, t); break;
case WAITING: question(c, t); break;
default: break;
}
c.restore();
}
private void drawLegs(Canvas c) {
fill.setColor(trousers);
line.setStrokeWidth(1.2f);
for (float x : new float[] { 42f, 52f }) {
oval(x, 84f, x + 6f, 95f);
c.drawRect(r, fill);
c.drawRect(r, line);
}
fill.setColor(shoe);
for (float x : new float[] { 39.5f, 50.5f }) {
oval(x, 92.5f, x + 10f, 98.5f);
c.drawOval(r, fill);
c.drawOval(r, line);
}
}
private void drawBody(Canvas c) {
p.reset();
p.moveTo(35.5f, 88f);
p.lineTo(38f, 67f);
p.quadTo(50f, 60f, 62f, 67f);
p.lineTo(64.5f, 88f);
p.quadTo(50f, 91f, 35.5f, 88f);
p.close();
fill.setColor(accent);
c.drawPath(p, fill);
line.setStrokeWidth(1.2f);
c.drawPath(p, line);
// The pocket and the drawstrings.
line.setStrokeWidth(0.9f);
p.reset();
p.moveTo(42.5f, 88.5f);
p.lineTo(43.5f, 79.5f);
p.quadTo(50f, 78f, 56.5f, 79.5f);
p.lineTo(57.5f, 88.5f);
c.drawPath(p, line);
line.setColor(Color.WHITE);
line.setStrokeWidth(1f);
c.drawLine(46.5f, 66f, 46f, 74f, line);
c.drawLine(53.5f, 66f, 54f, 74f, line);
line.setColor(ink);
}
/** Two arms from the shoulders to hands placed by the mood. */
private void drawArms(Canvas c, MascotMood mood, long t) {
float wave = (float) Math.sin(t / 220.0) * 3f;
float lx, ly, rx, ry;
switch (mood) {
case CONTENT: lx = 33f; ly = 82f; rx = 82f + wave * 0.5f; ry = 64f + wave; break;
case CHEERING: lx = 16f; ly = 56f; rx = 84f; ry = 56f; break;
case DETERMINED: lx = 33f; ly = 82f; rx = 57f; ry = 72f; break;
case WORRIED: lx = 34f; ly = 55f; rx = 66f; ry = 55f; break;
// Pointing back.
case REVERSE: lx = 20f; ly = 66f; rx = 67f; ry = 82f; break;
case FAST: lx = 30f; ly = 76f; rx = 70f; ry = 76f; break;
default: lx = 33f; ly = 82f; rx = 67f; ry = 82f;
}
arm(c, 39.5f, 69f, lx, ly);
arm(c, 60.5f, 69f, rx, ry);
}
private void arm(Canvas c, float sx, float sy, float hx, float hy) {
line.setStrokeWidth(7.4f);
c.drawLine(sx, sy, hx, hy, line);
line.setColor(accent);
line.setStrokeWidth(5f);
c.drawLine(sx, sy, hx, hy, line);
line.setColor(ink);
fill.setColor(skin);
line.setStrokeWidth(1.1f);
c.drawCircle(hx, hy, 3.2f, fill);
c.drawCircle(hx, hy, 3.2f, line);
}
private void drawHead(Canvas c, MascotMood mood, long t, boolean blink) {
float tilt = mood == MascotMood.REVERSE ? -10f : mood == MascotMood.SLEEPING ? 10f
: mood == MascotMood.WAITING ? 6f : 0f;
c.save();
c.rotate(tilt, 50f, 62f);
// The bob behind the face.
fill.setColor(hair);
line.setStrokeWidth(1.3f);
p.reset();
p.addOval(oval(20f, 5f, 80f, 62f), Path.Direction.CW);
q.reset();
q.addRoundRect(oval(19.5f, 30f, 80.5f, 64f), 7f, 7f, Path.Direction.CW);
p.op(q, Path.Op.UNION);
c.drawPath(p, fill);
c.drawPath(p, line);
// The face.
fill.setColor(skin);
oval(25f, 15f, 75f, 65f);
c.drawOval(r, fill);
c.drawOval(r, line);
// Bangs.
p.reset();
p.moveTo(22.5f, 40f);
p.cubicTo(19f, 7f, 81f, 7f, 77.5f, 40f);
p.lineTo(73f, 27f);
p.lineTo(66f, 34f);
p.lineTo(60f, 23f);
p.lineTo(52f, 32f);
p.lineTo(45f, 22f);
p.lineTo(38f, 32f);
p.lineTo(32f, 25f);
p.lineTo(27f, 35f);
p.close();
fill.setColor(hair);
c.drawPath(p, fill);
c.drawPath(p, line);
line.setColor(hairLight);
line.setStrokeWidth(1.6f);
c.drawArc(oval(30f, 10f, 56f, 30f), 200f, 60f, false, line);
line.setColor(ink);
// The star clip.
fill.setColor(accentLight);
star(c, 70f, 19f, 4.5f);
line.setStrokeWidth(0.8f);
c.drawPath(p, line);
if (mood == MascotMood.CHEERING || mood == MascotMood.FAST || mood == MascotMood.CONTENT
|| mood == MascotMood.SLEEPING || mood == MascotMood.WORRIED) {
fill.setColor(blush);
c.drawOval(oval(29.5f, 50f, 37f, 54f), fill);
c.drawOval(oval(63f, 50f, 70.5f, 54f), fill);
}
eye(c, 39f, 44f, mood, blink, t);
eye(c, 61f, 44f, mood, blink, t);
brows(c, mood);
mouth(c, mood, t);
if (mood == MascotMood.WORRIED) {
fill.setColor(0xFF8FD3FF);
drop(c, 72f, 30f, 3f);
drop(c, 34.5f, 52f, 1.8f);
}
c.restore();
}
private void eye(Canvas c, float cx, float cy, MascotMood mood, boolean blink, long t) {
line.setColor(ink);
line.setStrokeWidth(1.5f);
if (mood == MascotMood.SLEEPING || (blink && mood != MascotMood.CHEERING && mood != MascotMood.FAST)) {
c.drawArc(oval(cx - 4.5f, cy - 3f, cx + 4.5f, cy + 2f), 20f, 140f, false, line);
return;
}
if (mood == MascotMood.FAST) {
// ^ ^
c.drawArc(oval(cx - 4.5f, cy - 2f, cx + 4.5f, cy + 5f), 200f, 140f, false, line);
return;
}
float look = mood == MascotMood.REVERSE ? -2f : 0f;
oval(cx - 4.6f, cy - 5.6f, cx + 4.6f, cy + 5.6f);
if (mood == MascotMood.REVERSE) {
fill.setColor(Color.WHITE);
c.drawOval(r, fill);
fill.setColor(iris);
c.drawOval(oval(cx - 3f + look, cy - 4f, cx + 3f + look, cy + 4.5f), fill);
} else {
fill.setColor(iris);
c.drawOval(r, fill);
fill.setColor(irisLight);
c.drawOval(oval(cx - 3.6f, cy + 0.5f, cx + 3.6f, cy + 5f), fill);
}
if (mood == MascotMood.CHEERING) {
fill.setColor(Color.WHITE);
star(c, cx, cy - 0.5f, 3.6f);
} else {
fill.setColor(Color.WHITE);
c.drawCircle(cx - 1.6f + look, cy - 2.4f, 2f, fill);
c.drawCircle(cx + 1.8f + look, cy + 2.4f, 0.9f, fill);
}
if (mood == MascotMood.DETERMINED) {
// A flat lid over the top: focused.
fill.setColor(skin);
c.drawRect(cx - 5.2f, cy - 6.4f, cx + 5.2f, cy - 2f, fill);
line.setStrokeWidth(1.6f);
c.drawLine(cx - 5f, cy - 2f, cx + 5f, cy - 2f, line);
return;
}
line.setStrokeWidth(1.6f);
c.drawArc(oval(cx - 5f, cy - 5.9f, cx + 5f, cy + 5.6f), 200f, 140f, false, line);
}
private void brows(Canvas c, MascotMood mood) {
line.setColor(ink);
line.setStrokeWidth(1.4f);
if (mood == MascotMood.DETERMINED) {
c.drawLine(34f, 33.5f, 43.5f, 36.5f, line);
c.drawLine(56.5f, 36.5f, 66f, 33.5f, line);
} else if (mood == MascotMood.WORRIED) {
c.drawLine(34f, 36f, 43f, 33f, line);
c.drawLine(57f, 33f, 66f, 36f, line);
}
}
private void mouth(Canvas c, MascotMood mood, long t) {
line.setColor(ink);
line.setStrokeWidth(1.2f);
switch (mood) {
case CHEERING:
case FAST:
p.reset();
p.moveTo(45f, 53.5f);
p.quadTo(50f, 61.5f, 55f, 53.5f);
p.close();
fill.setColor(mouth);
c.drawPath(p, fill);
fill.setColor(tongue);
c.drawOval(oval(47.5f, 56.4f, 52.5f, 58.4f), fill);
c.drawPath(p, line);
break;
case DETERMINED:
fill.setColor(Color.WHITE);
oval(44.5f, 53f, 55.5f, 57f);
c.drawRoundRect(r, 1.5f, 1.5f, fill);
c.drawRoundRect(r, 1.5f, 1.5f, line);
c.drawLine(44.8f, 55f, 55.2f, 55f, line);
break;
case WORRIED:
p.reset();
p.moveTo(45.5f, 56f);
p.quadTo(47f, 54.3f, 48.5f, 56f);
p.quadTo(50f, 57.7f, 51.5f, 56f);
p.quadTo(53f, 54.3f, 54.5f, 56f);
c.drawPath(p, line);
break;
case SLEEPING: {
fill.setColor(mouth);
c.drawOval(oval(48.6f, 54.5f, 51.4f, 57f), fill);
// A nose bubble that swells and pops.
float grow = (t % 2400) / 2400f;
fill.setColor(0x7090D8FF);
c.drawCircle(53.5f, 51f, 1f + grow * 4f, fill);
break;
}
case REVERSE:
fill.setColor(mouth);
c.drawOval(oval(48.6f, 54f, 51.4f, 57f), fill);
break;
case WAITING:
c.drawLine(47.5f, 55.5f, 52.5f, 55.5f, line);
break;
default:
// A cat smile: w.
p.reset();
p.moveTo(45.5f, 54f);
p.quadTo(47.75f, 57.5f, 50f, 54.5f);
p.quadTo(52.25f, 57.5f, 54.5f, 54f);
c.drawPath(p, line);
}
}
private void zs(Canvas c, long t) {
float phase = (t % 3000) / 3000f;
float[][] zs = { { 20f, 24f, 6f }, { 13f, 14f, 7.5f }, { 5f, 4f, 9f } };
for (float[] z : zs) {
text.setTextSize(z[2]);
text.setColor(AccentColor.withAlpha(accentLight, Math.round(255 * (1f - phase * 0.5f))));
c.drawText("Z", z[0] - phase * 2f, z[1] + z[2] - phase * 3f, text);
}
}
private void sparkles(Canvas c, long t) {
float[][] at = { { 14f, 30f }, { 86f, 26f }, { 12f, 62f }, { 88f, 60f }, { 50f, 1.5f } };
for (int i = 0; i < at.length; i++) {
float s = 2.4f + 1.8f * (float) Math.abs(Math.sin(t / 260.0 + i * 1.3));
fill.setColor(i % 2 == 0 ? regen : Color.WHITE);
star(c, at[i][0], at[i][1], s);
}
}
private void wind(Canvas c, long t) {
line.setStrokeWidth(1f);
line.setColor(0x88FFFFFF);
for (int i = 0; i < 5; i++) {
float x = 18f - ((t / 6f + i * 37f) % 20f);
float y = 20f + i * 14f;
c.drawLine(x - 10f, y, x, y, line);
}
line.setColor(ink);
}
private void question(Canvas c, long t) {
text.setTextSize(14f);
text.setColor(accentLight);
float bob = (float) Math.sin(t / 400.0) * 1.2f;
c.drawText("?", 16f, 18f + bob, text);
}
private void drop(Canvas c, float x, float y, float s) {
p.reset();
p.moveTo(x, y - s * 1.6f);
p.quadTo(x + s * 1.2f, y + s * 0.2f, x, y + s);
p.quadTo(x - s * 1.2f, y + s * 0.2f, x, y - s * 1.6f);
c.drawPath(p, fill);
}
/** A four-pointed sparkle; leaves its outline in {@link #p}. */
private void star(Canvas c, float x, float y, float s) {
p.reset();
p.moveTo(x, y - s);
p.quadTo(x, y, x + s, y);
p.quadTo(x, y, x, y + s);
p.quadTo(x, y, x - s, y);
p.quadTo(x, y, x, y - s);
p.close();
c.drawPath(p, fill);
}
private RectF oval(float left, float top, float right, float bottom) {
r.set(left, top, right, bottom);
return r;
}
}