Some checks failed
Security / Security check (push) Failing after 2s
The Studio design system, widgets and helpers carried a Fai* / fai_ prefix (FaiSpace, FaiColors, FaiTheme, FaiLog, 17 fai_*.dart files, the faiBinary* l10n keys). Studio is the Ch∆In product, so rename them to Chain* / chain_ — carefully preserving English fail/failure/failed. Also fix stale references: the 'fai' binary in l10n strings -> 'chain', FAI_* env vars (FAI_BIN/DATA_DIR/MODULES_DIR/TODAY/BOOTSTRAP_TOKEN) -> CHAIN_*, fai_platform -> fai_chain, fai_hub -> chain_hub. Vendor security-hook tooling (FAI_BANNED_TERMS_FILE) + the .fai bundle ext left. flutter analyze + test: clean (20 passed). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
173 lines
4.6 KiB
Dart
173 lines
4.6 KiB
Dart
// ChainDeltaMark — the Ch∆In 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 ChainDeltaMode { idle, live, busy }
|
|
|
|
class ChainDeltaMark extends StatefulWidget {
|
|
final ChainDeltaMode mode;
|
|
final double size;
|
|
final Color color;
|
|
|
|
const ChainDeltaMark({
|
|
super.key,
|
|
required this.color,
|
|
this.mode = ChainDeltaMode.idle,
|
|
this.size = 36,
|
|
});
|
|
|
|
@override
|
|
State<ChainDeltaMark> createState() => _FaiDeltaMarkState();
|
|
}
|
|
|
|
class _FaiDeltaMarkState extends State<ChainDeltaMark>
|
|
with SingleTickerProviderStateMixin {
|
|
late final AnimationController _ctrl;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_ctrl = AnimationController(
|
|
vsync: this,
|
|
duration: _durationFor(widget.mode),
|
|
);
|
|
_restart();
|
|
}
|
|
|
|
Duration _durationFor(ChainDeltaMode m) {
|
|
switch (m) {
|
|
case ChainDeltaMode.idle:
|
|
return const Duration(seconds: 1);
|
|
case ChainDeltaMode.live:
|
|
return const Duration(milliseconds: 1600);
|
|
case ChainDeltaMode.busy:
|
|
return const Duration(seconds: 4);
|
|
}
|
|
}
|
|
|
|
void _restart() {
|
|
_ctrl.duration = _durationFor(widget.mode);
|
|
switch (widget.mode) {
|
|
case ChainDeltaMode.idle:
|
|
_ctrl.stop();
|
|
_ctrl.value = 0;
|
|
break;
|
|
case ChainDeltaMode.live:
|
|
_ctrl.repeat(reverse: true);
|
|
break;
|
|
case ChainDeltaMode.busy:
|
|
_ctrl.repeat();
|
|
break;
|
|
}
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant ChainDeltaMark 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 ChainDeltaMode 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;
|
|
|
|
// In live mode: scale the whole mark between 0.85 and 1.15
|
|
// so the pulse is visible without staring. Idle stays at 1.0.
|
|
final scale = mode == ChainDeltaMode.live ? 0.85 + 0.30 * t : 1.0;
|
|
final r = size.width * 0.36 * scale;
|
|
|
|
canvas.save();
|
|
canvas.translate(cx, cy);
|
|
if (mode == ChainDeltaMode.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. In live mode the glow ring grows with the pulse
|
|
// and the alpha breathes harder.
|
|
if (mode != ChainDeltaMode.idle) {
|
|
final pulseStrength = mode == ChainDeltaMode.live ? t : 1.0;
|
|
final glow = Paint()
|
|
..color = color.withValues(alpha: 0.30 + 0.40 * pulseStrength)
|
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, 8 + 14 * pulseStrength)
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawPath(path, glow);
|
|
}
|
|
|
|
// Filled triangle, brighter on the up-beat for live mode.
|
|
final fillAlpha = mode == ChainDeltaMode.live ? 0.15 + 0.20 * t : 0.18;
|
|
final fill = Paint()
|
|
..color = color.withValues(alpha: fillAlpha)
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawPath(path, fill);
|
|
|
|
// Outline stroke.
|
|
final stroke = Paint()
|
|
..color = color
|
|
..strokeWidth = 2.2
|
|
..style = PaintingStyle.stroke
|
|
..strokeJoin = StrokeJoin.round;
|
|
canvas.drawPath(path, stroke);
|
|
|
|
// Inner accent dot.
|
|
final dotAlpha = mode == ChainDeltaMode.live ? 0.55 + 0.45 * t : 1.0;
|
|
final dot = Paint()
|
|
..color = color.withValues(alpha: dotAlpha)
|
|
..style = PaintingStyle.fill;
|
|
canvas.drawCircle(Offset(0, r * 0.18), r * 0.16, dot);
|
|
|
|
canvas.restore();
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _DeltaPainter old) =>
|
|
old.t != t || old.mode != mode || old.color != color;
|
|
}
|