Files
GroundedHelper/frontend/lib/Grounded/designs/DotDigits.dart
alvocool fe268cb8aa Redesign the UI after the fitness reference and replace the logo
Light sage-to-cream ambient backdrop, frosted glass controls and painted
SceneArt landscapes in place of photography. Home becomes a dashboard with a
two-tone greeting, a frosted summary ring, category portraits, an up-next rail
of image-led cards and a floating glass tab bar. The routine library gets an
image header and painted cards; the player gets a scene stage, steps on an
arc, a dotted clock and a night pill with a lime spark. Goals become painted
cards in their own colour.

New logo: a lime sun resting on a ground line under two arcs. The splash
raises the sun and lights the arcs; launcher icons and native splash are
regenerated to match.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01KhQh614DMeYoQEH9SoMRUw
2026-11-02 12:33:13 +03:00

176 lines
4.9 KiB
Dart
Raw Permalink 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;
}
}