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