97 lines
2.5 KiB
Dart
97 lines
2.5 KiB
Dart
import 'package:flutter/material.dart';
|
|
|
|
/// The bar screen title. Ten quick taps light it up as a flickering neon
|
|
/// café sign for a few seconds.
|
|
class VenueTitle extends StatefulWidget {
|
|
final String text;
|
|
|
|
const VenueTitle({super.key, required this.text});
|
|
|
|
@override
|
|
State<VenueTitle> createState() => _VenueTitleState();
|
|
}
|
|
|
|
class _VenueTitleState extends State<VenueTitle>
|
|
with SingleTickerProviderStateMixin {
|
|
static const _tapsNeeded = 10;
|
|
static const _tapWindow = Duration(milliseconds: 600);
|
|
static const _neonPink = Color(0xFFFF2BD6);
|
|
|
|
/// Stretches of the animation (as fractions) where the tube is dark,
|
|
/// like a sign that sputters on and then dies out.
|
|
static const _darkSpans = [
|
|
(0.02, 0.05),
|
|
(0.07, 0.08),
|
|
(0.10, 0.14),
|
|
(0.55, 0.56),
|
|
(0.88, 0.89),
|
|
(0.91, 0.93),
|
|
];
|
|
|
|
late final AnimationController _neon = AnimationController(
|
|
vsync: this,
|
|
duration: const Duration(seconds: 5),
|
|
);
|
|
|
|
int _tapCount = 0;
|
|
DateTime? _lastTap;
|
|
|
|
@override
|
|
void dispose() {
|
|
_neon.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
void _onTap() {
|
|
final now = DateTime.now();
|
|
final last = _lastTap;
|
|
_tapCount = last != null && now.difference(last) < _tapWindow
|
|
? _tapCount + 1
|
|
: 1;
|
|
_lastTap = now;
|
|
|
|
if (_tapCount < _tapsNeeded) return;
|
|
_tapCount = 0;
|
|
_neon.forward(from: 0);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final baseStyle = DefaultTextStyle.of(context).style;
|
|
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: _onTap,
|
|
child: AnimatedBuilder(
|
|
animation: _neon,
|
|
builder: (context, _) {
|
|
final t = _neon.value;
|
|
final isLit =
|
|
_neon.isAnimating &&
|
|
!_darkSpans.any((span) => t >= span.$1 && t < span.$2);
|
|
|
|
return Text(
|
|
widget.text,
|
|
style: isLit
|
|
? baseStyle.copyWith(
|
|
color: const Color(0xFFFFD6F6),
|
|
fontWeight: FontWeight.w800,
|
|
shadows: const [
|
|
Shadow(color: _neonPink, blurRadius: 4),
|
|
Shadow(color: _neonPink, blurRadius: 12),
|
|
Shadow(color: _neonPink, blurRadius: 24),
|
|
],
|
|
)
|
|
: _neon.isAnimating
|
|
? baseStyle.copyWith(
|
|
color: baseStyle.color?.withValues(alpha: 0.35),
|
|
fontWeight: FontWeight.w800,
|
|
)
|
|
: baseStyle,
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
}
|