Files
GroundedHelper/frontend/lib/Grounded/designs/DotDigits.dart
alvocool 1b5d105400 Show real photos for people instead of scene art
Your avatar is your photo, picked from the device by tapping it on Home or
from a new Profile photo row in Settings, shrunk to 256 px and kept on the
phone (there is no upload endpoint yet). Group members show the photo the
server sends in a new `photo` field, everywhere avatars appear. Without a
photo, an initial on a neutral disc. Mock mode bundles free Unsplash
portraits for the sample people.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-09 08:22:41 +03:00

175 lines
4.9 KiB
Dart
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.

import 'dart:ui';
import 'package:flutter/material.dart';
import '../utils/Colors.dart';
/// A clock face drawn as a string of dots traced along each digit's outline —
/// the runner's signature readout. Only digits and ':' are drawn.
class DotDigits extends StatelessWidget {
final String value;
/// Glyph height in logical pixels.
final double height;
final Color? color;
const DotDigits({
super.key,
required this.value,
this.height = 64,
this.color,
});
@override
Widget build(BuildContext context) {
final _DotDigitsPainter painter =
_DotDigitsPainter(value, height, color ?? colorInk);
return CustomPaint(size: painter.measure(), painter: painter);
}
}
class _DotDigitsPainter extends CustomPainter {
final String value;
final double height;
final Color color;
_DotDigitsPainter(this.value, this.height, this.color);
/// Glyphs live in a 1 × 1.6 box.
double get _scale => height / 1.6;
double get _digitWidth => _scale * 1.0;
double get _colonWidth => _scale * 0.5;
double get _tracking => _scale * 0.34;
Size measure() {
double width = 0;
for (int i = 0; i < value.length; i++) {
width = width + (value[i] == ':' ? _colonWidth : _digitWidth);
if (i != value.length - 1) {
width = width + _tracking;
}
}
return Size(width + height * 0.08, height + height * 0.08);
}
@override
void paint(Canvas canvas, Size size) {
final double dot = height * 0.036;
final double gap = height * 0.082;
final Paint paint = Paint()..color = color;
canvas.translate(height * 0.04, height * 0.04);
double x = 0;
for (int i = 0; i < value.length; i++) {
final String glyph = value[i];
if (glyph == ':') {
canvas.drawCircle(
Offset(x + _colonWidth / 2, _scale * 0.52), dot * 1.25, paint);
canvas.drawCircle(
Offset(x + _colonWidth / 2, _scale * 1.12), dot * 1.25, paint);
x = x + _colonWidth + _tracking;
continue;
}
final Path? outline = _glyph(glyph);
if (outline != null) {
final Path placed = outline.transform((Matrix4.identity()
..translateByDouble(x, 0, 0, 1)
..scaleByDouble(_scale, _scale, 1, 1))
.storage);
for (PathMetric metric in placed.computeMetrics()) {
final int count = (metric.length / gap).floor().clamp(1, 400);
final double step = metric.length / count;
final int last = metric.isClosed ? count - 1 : count;
for (int n = 0; n <= last; n++) {
final Tangent? tangent = metric.getTangentForOffset(n * step);
if (tangent != null) {
canvas.drawCircle(tangent.position, dot, paint);
}
}
}
}
x = x + _digitWidth + _tracking;
}
}
Path? _glyph(String glyph) {
switch (glyph) {
case '0':
return Path()
..addRRect(RRect.fromRectAndRadius(
const Rect.fromLTWH(0, 0, 1, 1.6), const Radius.circular(0.5)));
case '1':
return Path()
..moveTo(0.22, 0.32)
..lineTo(0.6, 0)
..lineTo(0.6, 1.6);
case '2':
return Path()
..moveTo(0.03, 0.4)
..cubicTo(0.06, 0.02, 0.97, 0.0, 0.97, 0.44)
..cubicTo(0.97, 0.8, 0.05, 1.08, 0.0, 1.6)
..lineTo(1.0, 1.6);
case '3':
return Path()
..moveTo(0.04, 0.26)
..cubicTo(0.2, -0.06, 0.95, 0.0, 0.95, 0.4)
..cubicTo(0.95, 0.68, 0.66, 0.78, 0.4, 0.78)
..cubicTo(0.76, 0.78, 1.0, 0.92, 1.0, 1.2)
..cubicTo(1.0, 1.66, 0.18, 1.7, 0.0, 1.34);
case '4':
return Path()
..moveTo(0.76, 1.6)
..lineTo(0.76, 0.0)
..lineTo(0.0, 1.12)
..lineTo(1.0, 1.12);
case '5':
return Path()
..moveTo(0.92, 0.0)
..lineTo(0.14, 0.0)
..lineTo(0.06, 0.72)
..cubicTo(0.36, 0.54, 1.0, 0.6, 1.0, 1.08)
..cubicTo(1.0, 1.66, 0.2, 1.7, 0.0, 1.36);
case '6':
return Path()
..moveTo(0.86, 0.14)
..cubicTo(0.44, -0.12, 0.0, 0.24, 0.0, 1.1)
..addOval(const Rect.fromLTWH(0.0, 0.66, 1.0, 0.94));
case '7':
return Path()
..moveTo(0.0, 0.0)
..lineTo(1.0, 0.0)
..lineTo(0.34, 1.6);
case '8':
return Path()
..addOval(const Rect.fromLTWH(0.08, 0.0, 0.84, 0.74))
..addOval(const Rect.fromLTWH(0.0, 0.74, 1.0, 0.86));
case '9':
return Path()
..addOval(const Rect.fromLTWH(0.0, 0.0, 1.0, 0.94))
..moveTo(1.0, 0.5)
..cubicTo(1.0, 1.36, 0.56, 1.72, 0.14, 1.46);
default:
return null;
}
}
@override
bool shouldRepaint(covariant _DotDigitsPainter oldDelegate) {
return oldDelegate.value != value ||
oldDelegate.height != height ||
oldDelegate.color != color;
}
}