Time circuits, after Back to the Future: three LED rows (the car as the destination, the present time, the last departure from P), the speed in red digits and a flux capacitor that flows with the power; at 88 km/h it all flashes. Letters on fourteen segments (Segments14, tested). Quest, after World of Warcraft's interface: player frame, minimap, gold speed, cast bar, quest tracker, chat, action bar and the odometer as experience. Both in their own colours, the app with them. In THEMES.md with screenshots. Emulator only; not tried on the car. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LhzAVaUyqcw5PkGjz4mWW6
431 lines
18 KiB
Java
431 lines
18 KiB
Java
package com.evsuite.abrp;
|
||
|
||
import android.content.Context;
|
||
import android.graphics.Canvas;
|
||
import android.graphics.LinearGradient;
|
||
import android.graphics.Paint;
|
||
import android.graphics.Path;
|
||
import android.graphics.RectF;
|
||
import android.graphics.Shader;
|
||
import android.graphics.Typeface;
|
||
import android.os.SystemClock;
|
||
|
||
import java.util.Locale;
|
||
|
||
/**
|
||
* The Quest Overview style, after the interface of a fantasy online game like World of Warcraft:
|
||
* gold-trimmed frames over a dusk landscape, the car as the player character.
|
||
*
|
||
* Top left, the player frame: a round portrait, the gear in the level badge, the battery as the
|
||
* health bar and power as the energy bar. Top right, the minimap with the clock. In the middle
|
||
* the speed in large gold numbers, and under it the cast bar — "Accelerating" while drawing
|
||
* power, "Regenerating" while recovering it, "Coasting" in between and "Resting" in P. Right,
|
||
* the quest tracker: a road trip whose objectives are the trip, the usage and the climate.
|
||
* Bottom left, a chat window with system lines; bottom, P R N D as action buttons and the
|
||
* odometer as the experience bar (a level per 1000 km). Below 20 % a raid warning flashes.
|
||
* Unread values show "—" and fill nothing. Drawn per reading; only the raid warning blinks, by
|
||
* the readings that redraw it.
|
||
*/
|
||
final class QuestView extends ClusterCanvasView {
|
||
|
||
private static final int GOLD = 0xFFC9A24A, GOLD_DARK = 0xFF7A5C24, YELLOW = 0xFFFFD100,
|
||
PANEL = 0xE01A130D, HEALTH = 0xFF26B33A, ENERGY = 0xFFF2D21B, REGEN = 0xFF3FBF6F,
|
||
XP = 0xFF8E44C9, RED = 0xFFFF3B30, SYSTEM = 0xFFFFE96A;
|
||
|
||
private final Paint line = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||
private final Paint fill = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||
private final Paint text = new Paint(Paint.ANTI_ALIAS_FLAG);
|
||
private final Paint skyPaint = new Paint();
|
||
private final Path path = new Path();
|
||
private final RectF r = new RectF();
|
||
private final Typeface serif = Typeface.create(Typeface.SERIF, Typeface.BOLD);
|
||
private final Typeface sans = Typeface.create("sans-serif-condensed", Typeface.BOLD);
|
||
private int skyFor = -1;
|
||
|
||
QuestView(Context context) {
|
||
super(context);
|
||
line.setStyle(Paint.Style.STROKE);
|
||
}
|
||
|
||
@Override
|
||
protected void onDraw(Canvas c) {
|
||
super.onDraw(c);
|
||
float w = getWidth(), h = getHeight();
|
||
if (w <= 0 || h <= 0) return;
|
||
float u = Math.min(h / 640f, w / 1100f);
|
||
drawLandscape(c, w, h);
|
||
drawPlayer(c, u);
|
||
drawMinimap(c, w, u);
|
||
drawSpeed(c, w, u);
|
||
drawCastBar(c, w, u);
|
||
drawQuests(c, w, u);
|
||
drawChat(c, u);
|
||
drawActionBar(c, w, h, u);
|
||
drawXp(c, w, h, u);
|
||
}
|
||
|
||
// ---------- World ----------
|
||
|
||
private void drawLandscape(Canvas c, float w, float h) {
|
||
if (skyFor != (int) h) {
|
||
skyPaint.setShader(new LinearGradient(0, 0, 0, h, new int[] { 0xFF0B1424, 0xFF1C2A3A, 0xFF2A2418 },
|
||
new float[] { 0f, 0.55f, 1f }, Shader.TileMode.CLAMP));
|
||
skyFor = (int) h;
|
||
}
|
||
c.drawRect(0, 0, w, h, skyPaint);
|
||
// Two ranges of hills, the near one darker.
|
||
hills(c, w, h, h * 0.62f, 0xFF16202A, 0.9f);
|
||
hills(c, w, h, h * 0.72f, 0xFF10171D, 1.6f);
|
||
}
|
||
|
||
private void hills(Canvas c, float w, float h, float base, int colour, float wave) {
|
||
path.reset();
|
||
path.moveTo(0, h);
|
||
for (int i = 0; i <= 24; i++) {
|
||
float x = w * i / 24f;
|
||
float y = base - (float) (Math.sin(i * wave) * 0.5 + Math.sin(i * wave * 0.37 + 1) * 0.5) * h * 0.06f;
|
||
path.lineTo(x, y);
|
||
}
|
||
path.lineTo(w, h);
|
||
path.close();
|
||
fill.setColor(colour);
|
||
c.drawPath(path, fill);
|
||
}
|
||
|
||
// ---------- Frames ----------
|
||
|
||
/** A dark panel with a gold rim. */
|
||
private void panel(Canvas c, float l, float t, float rr, float b, float u) {
|
||
r.set(l, t, rr, b);
|
||
fill.setColor(PANEL);
|
||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||
line.setColor(GOLD_DARK);
|
||
line.setStrokeWidth(4f * u);
|
||
c.drawRoundRect(r, 6f * u, 6f * u, line);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(1.5f * u);
|
||
c.drawRoundRect(r, 6f * u, 6f * u, line);
|
||
}
|
||
|
||
/** Text with a dark outline, as game interfaces write over the world. */
|
||
private void outlined(Canvas c, String s, float x, float y, float size, int colour, Typeface face,
|
||
Paint.Align align, float u) {
|
||
text.setTypeface(face);
|
||
text.setTextSize(size);
|
||
text.setTextAlign(align);
|
||
text.setStyle(Paint.Style.STROKE);
|
||
text.setStrokeWidth(Math.max(2f * u, size * 0.08f));
|
||
text.setStrokeJoin(Paint.Join.ROUND);
|
||
text.setColor(0xFF000000);
|
||
c.drawText(s, x, y, text);
|
||
text.setStyle(Paint.Style.FILL);
|
||
text.setColor(colour);
|
||
c.drawText(s, x, y, text);
|
||
}
|
||
|
||
/** A bar with a dark track, a fill of [frac] and centred text. */
|
||
private void bar(Canvas c, float l, float t, float rr, float b, float frac, int colour, String label, float u) {
|
||
r.set(l, t, rr, b);
|
||
fill.setColor(0xFF0A0A0A);
|
||
c.drawRect(r, fill);
|
||
if (frac > 0f) {
|
||
r.set(l, t, l + (rr - l) * Math.min(1f, frac), b);
|
||
fill.setColor(colour);
|
||
c.drawRect(r, fill);
|
||
fill.setColor(0x33FFFFFF);
|
||
c.drawRect(r.left, t, r.right, t + (b - t) * 0.35f, fill);
|
||
}
|
||
line.setColor(GOLD_DARK);
|
||
line.setStrokeWidth(2f * u);
|
||
c.drawRect(l, t, rr, b, line);
|
||
outlined(c, label, (l + rr) / 2f, b - (b - t) * 0.22f, (b - t) * 0.72f, 0xFFFFFFFF, sans,
|
||
Paint.Align.CENTER, u);
|
||
}
|
||
|
||
// ---------- Player ----------
|
||
|
||
private void drawPlayer(Canvas c, float u) {
|
||
float px = 86f * u, py = 86f * u, pr = 60f * u;
|
||
// Name plate and bars, behind the portrait.
|
||
float l = px + 20f * u, rr = px + 380f * u;
|
||
panel(c, l, py - 56f * u, rr, py + 46f * u, u);
|
||
outlined(c, "MG4", l + 60f * u, py - 28f * u, 24f * u, YELLOW, serif, Paint.Align.LEFT, u);
|
||
float soc = v.socPercent == null ? 0f : v.socPercent / 100f;
|
||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||
bar(c, l + 56f * u, py - 18f * u, rr - 12f * u, py + 10f * u, soc, low ? RED : HEALTH,
|
||
fmt(v.socPercent, "%.0f %%", "—") + " · " + fmt(v.rangeKm, "%.0f km", "—"), u);
|
||
boolean regen = v.powerKw != null && v.powerKw < -0.5f;
|
||
float energy = v.powerKw == null ? 0f : Math.abs(v.powerKw) / 150f;
|
||
bar(c, l + 56f * u, py + 14f * u, rr - 12f * u, py + 36f * u, energy, regen ? REGEN : ENERGY,
|
||
v.powerKw == null ? "—" : String.format(Locale.US, regen ? "+%.1f kW" : "%.1f kW", Math.abs(v.powerKw)), u);
|
||
|
||
// The portrait: a gold ring round the car's name.
|
||
fill.setColor(0xFF140F0A);
|
||
c.drawCircle(px, py, pr, fill);
|
||
line.setColor(GOLD_DARK);
|
||
line.setStrokeWidth(10f * u);
|
||
c.drawCircle(px, py, pr, line);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(3f * u);
|
||
c.drawCircle(px, py, pr + 3f * u, line);
|
||
drawCarGlyph(c, px, py + 6f * u, pr * 0.62f, u);
|
||
// The level badge: the gear.
|
||
float bx = px - pr * 0.72f, by = py + pr * 0.72f, br = 20f * u;
|
||
fill.setColor(0xFF140F0A);
|
||
c.drawCircle(bx, by, br, fill);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(3f * u);
|
||
c.drawCircle(bx, by, br, line);
|
||
String gear = Gear.label(v.gear);
|
||
outlined(c, gear.isEmpty() ? "?" : gear, bx, by + 8f * u, 22f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||
}
|
||
|
||
/** A small car in profile for the portrait. */
|
||
private void drawCarGlyph(Canvas c, float cx, float cy, float s, float u) {
|
||
path.reset();
|
||
path.moveTo(cx - s, cy + s * 0.15f);
|
||
path.lineTo(cx - s * 0.92f, cy - s * 0.12f);
|
||
path.quadTo(cx - s * 0.55f, cy - s * 0.2f, cx - s * 0.35f, cy - s * 0.48f);
|
||
path.lineTo(cx + s * 0.3f, cy - s * 0.5f);
|
||
path.quadTo(cx + s * 0.62f, cy - s * 0.22f, cx + s * 0.95f, cy - s * 0.1f);
|
||
path.lineTo(cx + s, cy + s * 0.15f);
|
||
path.close();
|
||
fill.setColor(GOLD);
|
||
c.drawPath(path, fill);
|
||
fill.setColor(0xFF140F0A);
|
||
c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.2f, fill);
|
||
c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.2f, fill);
|
||
fill.setColor(GOLD);
|
||
c.drawCircle(cx - s * 0.55f, cy + s * 0.18f, s * 0.11f, fill);
|
||
c.drawCircle(cx + s * 0.55f, cy + s * 0.18f, s * 0.11f, fill);
|
||
}
|
||
|
||
// ---------- Minimap ----------
|
||
|
||
private void drawMinimap(Canvas c, float w, float u) {
|
||
float cx = w - 120f * u, cy = 128f * u, mr = 84f * u;
|
||
outlined(c, "The Open Road", cx, 30f * u, 20f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||
fill.setColor(0xFF1E3524);
|
||
c.drawCircle(cx, cy, mr, fill);
|
||
// Roads and a river, clipped to the map.
|
||
c.save();
|
||
path.reset();
|
||
path.addCircle(cx, cy, mr, Path.Direction.CW);
|
||
c.clipPath(path);
|
||
line.setColor(0xFF8A7650);
|
||
line.setStrokeWidth(5f * u);
|
||
c.drawLine(cx, cy + mr, cx, cy - mr, line);
|
||
c.drawLine(cx - mr, cy - mr * 0.35f, cx + mr, cy + mr * 0.1f, line);
|
||
line.setColor(0xFF2E5C8A);
|
||
line.setStrokeWidth(7f * u);
|
||
path.reset();
|
||
path.moveTo(cx - mr, cy + mr * 0.5f);
|
||
path.quadTo(cx - mr * 0.2f, cy + mr * 0.2f, cx + mr, cy + mr * 0.7f);
|
||
c.drawPath(path, line);
|
||
c.restore();
|
||
// The player arrow.
|
||
path.reset();
|
||
path.moveTo(cx, cy - 13f * u);
|
||
path.lineTo(cx + 9f * u, cy + 10f * u);
|
||
path.lineTo(cx, cy + 5f * u);
|
||
path.lineTo(cx - 9f * u, cy + 10f * u);
|
||
path.close();
|
||
fill.setColor(YELLOW);
|
||
c.drawPath(path, fill);
|
||
line.setColor(0xFF000000);
|
||
line.setStrokeWidth(1.5f * u);
|
||
c.drawPath(path, line);
|
||
// The stone rim.
|
||
line.setColor(0xFF4A4036);
|
||
line.setStrokeWidth(12f * u);
|
||
c.drawCircle(cx, cy, mr + 5f * u, line);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(2f * u);
|
||
c.drawCircle(cx, cy, mr + 11f * u, line);
|
||
c.drawCircle(cx, cy, mr - 1f * u, line);
|
||
// The clock under it.
|
||
panel(c, cx - 50f * u, cy + mr + 16f * u, cx + 50f * u, cy + mr + 48f * u, u);
|
||
outlined(c, v.clock.isEmpty() ? "—" : v.clock, cx, cy + mr + 40f * u, 22f * u, 0xFFFFFFFF, sans,
|
||
Paint.Align.CENTER, u);
|
||
}
|
||
|
||
// ---------- Speed and cast bar ----------
|
||
|
||
private void drawSpeed(Canvas c, float w, float u) {
|
||
boolean low = v.socPercent != null && v.socPercent < 20f;
|
||
if (low && (SystemClock.uptimeMillis() / 600) % 2 == 0) {
|
||
outlined(c, "Low battery!", w / 2f, 64f * u, 34f * u, RED, serif, Paint.Align.CENTER, u);
|
||
}
|
||
outlined(c, fmt(v.speedKmh, "%.0f", "—"), w / 2f, 236f * u, 150f * u, YELLOW, serif, Paint.Align.CENTER, u);
|
||
outlined(c, "km/h", w / 2f, 270f * u, 24f * u, GOLD, serif, Paint.Align.CENTER, u);
|
||
}
|
||
|
||
private void drawCastBar(Canvas c, float w, float u) {
|
||
boolean parked = v.gear != null && v.gear == 1;
|
||
Float p = v.powerKw;
|
||
String spell;
|
||
int colour;
|
||
float frac;
|
||
if (parked) {
|
||
spell = "Resting";
|
||
colour = 0xFF5B7BD9;
|
||
frac = 1f;
|
||
} else if (p == null) {
|
||
spell = "—";
|
||
colour = ENERGY;
|
||
frac = 0f;
|
||
} else if (p > 0.5f) {
|
||
spell = "Accelerating";
|
||
colour = 0xFFFFB300;
|
||
frac = p / 150f;
|
||
} else if (p < -0.5f) {
|
||
spell = "Regenerating";
|
||
colour = REGEN;
|
||
frac = -p / 50f;
|
||
} else {
|
||
spell = "Coasting";
|
||
colour = 0xFF8F8F8F;
|
||
frac = 0.02f;
|
||
}
|
||
float bw = Math.min(520f * u, w * 0.36f), l = (w - bw) / 2f + 18f * u, rr = l + bw, t = 296f * u, b = t + 30f * u;
|
||
// The spell icon.
|
||
float il = l - 40f * u;
|
||
r.set(il, t - 3f * u, il + 36f * u, b + 3f * u);
|
||
fill.setColor(0xFF140F0A);
|
||
c.drawRect(r, fill);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(2f * u);
|
||
c.drawRect(r, line);
|
||
drawSpellIcon(c, r.centerX(), r.centerY(), 13f * u, spell, colour, u);
|
||
r.set(l, t, rr, b);
|
||
fill.setColor(0xFF0A0A0A);
|
||
c.drawRect(r, fill);
|
||
r.set(l, t, l + bw * Math.max(0f, Math.min(1f, frac)), b);
|
||
fill.setColor(colour);
|
||
c.drawRect(r, fill);
|
||
fill.setColor(0x33FFFFFF);
|
||
c.drawRect(r.left, t, r.right, t + 10f * u, fill);
|
||
line.setColor(GOLD);
|
||
line.setStrokeWidth(2.5f * u);
|
||
c.drawRect(l, t, rr, b, line);
|
||
outlined(c, spell, l + 10f * u, b - 8f * u, 20f * u, 0xFFFFFFFF, sans, Paint.Align.LEFT, u);
|
||
outlined(c, p == null ? "—" : String.format(Locale.US, "%.1f kW", Math.abs(p)), rr - 10f * u, b - 8f * u,
|
||
20f * u, 0xFFFFFFFF, sans, Paint.Align.RIGHT, u);
|
||
}
|
||
|
||
private void drawSpellIcon(Canvas c, float cx, float cy, float s, String spell, int colour, float u) {
|
||
fill.setColor(colour);
|
||
path.reset();
|
||
switch (spell) {
|
||
case "Accelerating": // a lightning bolt
|
||
path.moveTo(cx + s * 0.2f, cy - s);
|
||
path.lineTo(cx - s * 0.6f, cy + s * 0.1f);
|
||
path.lineTo(cx, cy + s * 0.1f);
|
||
path.lineTo(cx - s * 0.2f, cy + s);
|
||
path.lineTo(cx + s * 0.6f, cy - s * 0.15f);
|
||
path.lineTo(cx, cy - s * 0.15f);
|
||
path.close();
|
||
c.drawPath(path, fill);
|
||
break;
|
||
case "Regenerating": // a leaf
|
||
path.moveTo(cx - s * 0.8f, cy + s * 0.8f);
|
||
path.quadTo(cx - s, cy - s, cx + s * 0.8f, cy - s * 0.8f);
|
||
path.quadTo(cx + s, cy + s * 0.9f, cx - s * 0.8f, cy + s * 0.8f);
|
||
c.drawPath(path, fill);
|
||
break;
|
||
case "Resting":
|
||
outlined(c, "Zz", cx, cy + s * 0.5f, s * 1.5f, colour, serif, Paint.Align.CENTER, u);
|
||
break;
|
||
default:
|
||
c.drawCircle(cx, cy, s * 0.5f, fill);
|
||
}
|
||
}
|
||
|
||
// ---------- Quest tracker and chat ----------
|
||
|
||
private void drawQuests(Canvas c, float w, float u) {
|
||
float x = w - 400f * u, y = 310f * u;
|
||
fill.setColor(YELLOW);
|
||
c.drawCircle(x - 18f * u, y - 9f * u, 11f * u, fill);
|
||
outlined(c, "!", x - 18f * u, y - 1f * u, 18f * u, 0xFF000000, serif, Paint.Align.CENTER, u);
|
||
outlined(c, "Road trip", x, y, 26f * u, YELLOW, serif, Paint.Align.LEFT, u);
|
||
String[] objectives = {
|
||
"Distance: " + fmt(v.tripKm, "%.1f km", "—"),
|
||
"Usage, 5 min: " + fmt(v.usage5Min, "%.1f kWh/100 km", "—"),
|
||
"Since start: " + fmt(v.usageTrip, "%.1f kWh/100 km", "—"),
|
||
"Climate: " + fmt(v.climateCelsius, "%.0f °C", "—"),
|
||
};
|
||
for (int i = 0; i < objectives.length; i++) {
|
||
outlined(c, "– " + objectives[i], x + 8f * u, y + (34f + i * 30f) * u, 21f * u, 0xFFE8E8E8, sans,
|
||
Paint.Align.LEFT, u);
|
||
}
|
||
}
|
||
|
||
private void drawChat(Canvas c, float u) {
|
||
float l = 22f * u, t = 368f * u, rr = l + 470f * u, b = 520f * u;
|
||
r.set(l, t, rr, b);
|
||
fill.setColor(0x66000000);
|
||
c.drawRoundRect(r, 6f * u, 6f * u, fill);
|
||
String state = v.gear == null ? "The car is not answering."
|
||
: v.gear == 1 ? "You feel rested."
|
||
: v.gear == 2 ? "You are moving backwards."
|
||
: "You are on the road.";
|
||
String[] lines = {
|
||
"Range remaining: " + fmt(v.rangeKm, "%.0f km", "—") + ".",
|
||
"Odometer: " + fmt(v.odometerKm, "%,.0f km", "—") + ".",
|
||
state,
|
||
};
|
||
for (int i = 0; i < lines.length; i++) {
|
||
outlined(c, lines[i], l + 14f * u, t + (38f + i * 40f) * u, 21f * u, SYSTEM, sans, Paint.Align.LEFT, u);
|
||
}
|
||
}
|
||
|
||
// ---------- Action bar and experience ----------
|
||
|
||
private void drawActionBar(Canvas c, float w, float h, float u) {
|
||
String[] gears = { "P", "R", "N", "D" };
|
||
String now = Gear.label(v.gear);
|
||
float s = 64f * u, gap = 8f * u, total = gears.length * s + (gears.length - 1) * gap;
|
||
float x = (w - total) / 2f, top = h - 104f * u;
|
||
panel(c, x - 12f * u, top - 10f * u, x + total + 12f * u, top + s + 10f * u, u);
|
||
for (int i = 0; i < gears.length; i++) {
|
||
boolean on = gears[i].equals(now);
|
||
r.set(x, top, x + s, top + s);
|
||
fill.setColor(on ? 0xFF3A2A12 : 0xFF141414);
|
||
c.drawRect(r, fill);
|
||
line.setColor(on ? YELLOW : GOLD_DARK);
|
||
line.setStrokeWidth((on ? 4f : 2f) * u);
|
||
c.drawRect(r, line);
|
||
outlined(c, gears[i], x + s / 2f, top + s * 0.72f, 36f * u, on ? YELLOW : 0xFF9A9A9A, serif,
|
||
Paint.Align.CENTER, u);
|
||
outlined(c, String.valueOf(i + 1), x + s - 6f * u, top + 16f * u, 13f * u, 0xFFD0D0D0, sans,
|
||
Paint.Align.RIGHT, u);
|
||
x += s + gap;
|
||
}
|
||
}
|
||
|
||
/** The odometer as experience: a level per 1000 km, the bar the way through this one. */
|
||
private void drawXp(Canvas c, float w, float h, float u) {
|
||
float l = 22f * u, rr = w - 22f * u, t = h - 26f * u, b = h - 10f * u;
|
||
r.set(l, t, rr, b);
|
||
fill.setColor(0xFF0A0A0A);
|
||
c.drawRect(r, fill);
|
||
String label = "—";
|
||
if (v.odometerKm != null) {
|
||
float into = v.odometerKm % 1000f;
|
||
r.set(l, t, l + (rr - l) * into / 1000f, b);
|
||
fill.setColor(XP);
|
||
c.drawRect(r, fill);
|
||
label = String.format(Locale.US, "Level %d · %.0f / 1000 km", (int) (v.odometerKm / 1000f) + 1, into);
|
||
}
|
||
line.setColor(GOLD_DARK);
|
||
line.setStrokeWidth(1.5f * u);
|
||
for (int i = 1; i < 20; i++) {
|
||
float x = l + (rr - l) * i / 20f;
|
||
c.drawLine(x, t, x, b, line);
|
||
}
|
||
c.drawRect(l, t, rr, b, line);
|
||
outlined(c, label, w / 2f, b - 3f * u, 13f * u, 0xFFFFFFFF, sans, Paint.Align.CENTER, u);
|
||
}
|
||
}
|