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