iykyka/mobile/lib/widgets/twin_hero_backdrop.dart

165 lines
4.5 KiB
Dart

import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../theme/app_theme.dart';
/// Full-bleed Twin Shadow atmosphere: mist→paper gradient + overlapping
/// silhouettes (the "second self"). No badges, chips, or promo overlays.
class TwinHeroBackdrop extends StatefulWidget {
const TwinHeroBackdrop({super.key, required this.child});
final Widget child;
@override
State<TwinHeroBackdrop> createState() => _TwinHeroBackdropState();
}
class _TwinHeroBackdropState extends State<TwinHeroBackdrop> with SingleTickerProviderStateMixin {
late final AnimationController _drift;
@override
void initState() {
super.initState();
_drift = AnimationController(vsync: this, duration: const Duration(seconds: 14))..repeat(reverse: true);
}
@override
void dispose() {
_drift.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: _drift,
builder: (context, _) {
final t = Curves.easeInOut.transform(_drift.value);
return Stack(
fit: StackFit.expand,
children: [
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment(-0.2 + t * 0.15, -1),
end: Alignment(0.3 - t * 0.1, 1.1),
colors: const [
TwinTokens.glow,
TwinTokens.mist,
TwinTokens.paper,
],
stops: const [0.0, 0.42, 1.0],
),
),
),
CustomPaint(
painter: _TwinSilhouettePainter(phase: t),
),
widget.child,
],
);
},
);
}
}
class _TwinSilhouettePainter extends CustomPainter {
_TwinSilhouettePainter({required this.phase});
final double phase;
@override
void paint(Canvas canvas, Size size) {
final w = size.width;
final h = size.height;
final paintA = Paint()..color = TwinTokens.forest.withValues(alpha: 0.07 + phase * 0.02);
final paintB = Paint()..color = TwinTokens.ink.withValues(alpha: 0.05 + (1 - phase) * 0.02);
// Soft overlapping ovals — a person and their shadow-self.
final a = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(w * 0.72 + phase * 8, h * 0.22),
width: w * 0.55,
height: h * 0.42,
),
const Radius.circular(120),
);
final b = RRect.fromRectAndRadius(
Rect.fromCenter(
center: Offset(w * 0.78 + phase * 4, h * 0.26),
width: w * 0.48,
height: h * 0.38,
),
const Radius.circular(120),
);
canvas.drawRRect(a, paintA);
canvas.drawRRect(b, paintB);
// Thin crescent arc suggesting a second outline.
final arcPaint = Paint()
..color = TwinTokens.forest.withValues(alpha: 0.12)
..style = PaintingStyle.stroke
..strokeWidth = 1.4;
canvas.drawArc(
Rect.fromCenter(center: Offset(w * 0.78, h * 0.24), width: w * 0.42, height: h * 0.34),
-math.pi * 0.2,
math.pi * 1.1,
false,
arcPaint,
);
}
@override
bool shouldRepaint(covariant _TwinSilhouettePainter oldDelegate) => oldDelegate.phase != phase;
}
/// Brand wordmark entrance — fade + slight rise.
class TwinFadeUp extends StatefulWidget {
const TwinFadeUp({
super.key,
required this.child,
this.delay = Duration.zero,
this.duration = const Duration(milliseconds: 700),
});
final Widget child;
final Duration delay;
final Duration duration;
@override
State<TwinFadeUp> createState() => _TwinFadeUpState();
}
class _TwinFadeUpState extends State<TwinFadeUp> with SingleTickerProviderStateMixin {
late final AnimationController _c;
late final Animation<double> _opacity;
late final Animation<Offset> _offset;
@override
void initState() {
super.initState();
_c = AnimationController(vsync: this, duration: widget.duration);
_opacity = CurvedAnimation(parent: _c, curve: Curves.easeOutCubic);
_offset = Tween(begin: const Offset(0, 0.06), end: Offset.zero)
.animate(CurvedAnimation(parent: _c, curve: Curves.easeOutCubic));
Future<void>.delayed(widget.delay, () {
if (mounted) _c.forward();
});
}
@override
void dispose() {
_c.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return FadeTransition(
opacity: _opacity,
child: SlideTransition(position: _offset, child: widget.child),
);
}
}