// FaiDeltaMark — the F∆I signature element. A precise triangle // (∆) drawn from primitives, not a font glyph. Three states: // // - idle: static, accent colour, soft glow // - live: gentle pulse, like a heartbeat // - busy: rotates slowly, used while a long operation runs // // Sized small. Lives in the navigation rail header so it's // always visible and identifies the brand without screaming. import 'dart:math'; import 'package:flutter/material.dart'; enum FaiDeltaMode { idle, live, busy } class FaiDeltaMark extends StatefulWidget { final FaiDeltaMode mode; final double size; final Color color; const FaiDeltaMark({ super.key, required this.color, this.mode = FaiDeltaMode.idle, this.size = 36, }); @override State createState() => _FaiDeltaMarkState(); } class _FaiDeltaMarkState extends State with SingleTickerProviderStateMixin { late final AnimationController _ctrl; @override void initState() { super.initState(); _ctrl = AnimationController( vsync: this, duration: _durationFor(widget.mode), ); _restart(); } Duration _durationFor(FaiDeltaMode m) { switch (m) { case FaiDeltaMode.idle: return const Duration(seconds: 1); case FaiDeltaMode.live: return const Duration(milliseconds: 1600); case FaiDeltaMode.busy: return const Duration(seconds: 4); } } void _restart() { _ctrl.duration = _durationFor(widget.mode); switch (widget.mode) { case FaiDeltaMode.idle: _ctrl.stop(); _ctrl.value = 0; break; case FaiDeltaMode.live: _ctrl.repeat(reverse: true); break; case FaiDeltaMode.busy: _ctrl.repeat(); break; } } @override void didUpdateWidget(covariant FaiDeltaMark old) { super.didUpdateWidget(old); if (old.mode != widget.mode) _restart(); } @override void dispose() { _ctrl.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AnimatedBuilder( animation: _ctrl, builder: (_, _) { return CustomPaint( size: Size.square(widget.size), painter: _DeltaPainter( color: widget.color, mode: widget.mode, t: _ctrl.value, ), ); }, ); } } class _DeltaPainter extends CustomPainter { final Color color; final FaiDeltaMode mode; final double t; _DeltaPainter({required this.color, required this.mode, required this.t}); @override void paint(Canvas canvas, Size size) { final cx = size.width / 2; final cy = size.height / 2; final r = size.width * 0.42; canvas.save(); canvas.translate(cx, cy); if (mode == FaiDeltaMode.busy) { canvas.rotate(t * 2 * pi); } // Outer triangle path. final path = Path() ..moveTo(0, -r) ..lineTo(r * cos(pi / 6), r * sin(pi / 6)) ..lineTo(-r * cos(pi / 6), r * sin(pi / 6)) ..close(); // Glow halo for live / busy mode. if (mode != FaiDeltaMode.idle) { final pulseStrength = mode == FaiDeltaMode.live ? t : 1.0; final glow = Paint() ..color = color.withValues(alpha: 0.25 + 0.25 * pulseStrength) ..maskFilter = MaskFilter.blur(BlurStyle.normal, 6 + 6 * pulseStrength) ..style = PaintingStyle.fill; canvas.drawPath(path, glow); } // Outline stroke. final stroke = Paint() ..color = color ..strokeWidth = 2 ..style = PaintingStyle.stroke ..strokeJoin = StrokeJoin.round; canvas.drawPath(path, stroke); // Inner accent dot — pulses opacity in live mode. final dotAlpha = mode == FaiDeltaMode.live ? 0.4 + 0.6 * t : 1.0; final dot = Paint() ..color = color.withValues(alpha: dotAlpha) ..style = PaintingStyle.fill; canvas.drawCircle(Offset(0, r * 0.15), r * 0.14, dot); canvas.restore(); } @override bool shouldRepaint(covariant _DeltaPainter old) => old.t != t || old.mode != mode || old.color != color; }