import 'dart:math' as math; import 'package:flutter/material.dart'; import '../../game/models/season.dart'; import '../theme/palette.dart'; /// Set true in tests (flutter_test_config.dart): looping ambience would make /// pumpAndSettle spin forever. bool debugDisableLoopingAnimations = false; /// Full-screen season ambience: vertical gradient plus drifting particles /// (petals for season 1). Pure procedural — no image assets required; an AI /// illustration layer can be added on top later without touching callers. class SeasonBackground extends StatefulWidget { const SeasonBackground({super.key, required this.theme}); final SeasonTheme theme; @override State createState() => _SeasonBackgroundState(); } class _SeasonBackgroundState extends State with SingleTickerProviderStateMixin { late final AnimationController _drift = AnimationController( vsync: this, duration: const Duration(seconds: 18), ); late ThemeColors _colors = ThemeColors(widget.theme); @override void initState() { super.initState(); if (!debugDisableLoopingAnimations) _drift.repeat(); } @override void didUpdateWidget(SeasonBackground old) { super.didUpdateWidget(old); if (old.theme != widget.theme) _colors = ThemeColors(widget.theme); } @override void dispose() { _drift.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return RepaintBoundary( child: AnimatedBuilder( animation: _drift, builder: (context, _) => CustomPaint( size: Size.infinite, painter: _AmbiencePainter(colors: _colors, t: _drift.value), ), ), ); } } class _AmbiencePainter extends CustomPainter { const _AmbiencePainter({required this.colors, required this.t}); final ThemeColors colors; final double t; static const _particles = 9; // Deterministic pseudo-random in [0, 1) from an index. static double _hash(int i, double salt) { final v = math.sin(i * 12.9898 + salt) * 43758.5453; return v - v.floorToDouble(); } @override void paint(Canvas canvas, Size size) { final rect = Offset.zero & size; canvas.drawRect( rect, Paint() ..shader = LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: colors.gradient, stops: colors.gradient.length == 3 ? const [0.0, 0.55, 1.0] : null, ).createShader(rect), ); if (colors.particleType == 'none') return; for (var i = 0; i < _particles; i++) { final speed = 0.5 + _hash(i, 1) * 0.6; final phase = _hash(i, 2); final fall = (t * speed + phase) % 1.15 - 0.075; final x = (_hash(i, 3) + 0.05 * math.sin(t * 2 * math.pi + i * 1.7)) * size.width; final y = fall * size.height; final scale = 7 + _hash(i, 4) * 9; final angle = t * 2 * math.pi * (0.4 + _hash(i, 5)) + i; _paintParticle(canvas, Offset(x, y), scale, angle); } } void _paintParticle(Canvas canvas, Offset c, double s, double angle) { canvas.save(); canvas.translate(c.dx, c.dy); canvas.rotate(angle); final paint = Paint(); switch (colors.particleType) { case 'snow': paint.color = Colors.white.withValues(alpha: 0.35); canvas.drawCircle(Offset.zero, s * 0.4, paint); case 'leaves': paint.color = colors.accent.withValues(alpha: 0.35); canvas.drawOval( Rect.fromCenter(center: Offset.zero, width: s, height: s * 0.55), paint); default: // petals paint.color = colors.accent.withValues(alpha: 0.30); canvas.drawOval( Rect.fromCenter( center: Offset(s * 0.18, 0), width: s, height: s * 0.62), paint); canvas.drawOval( Rect.fromCenter( center: Offset(-s * 0.18, 0), width: s, height: s * 0.62), paint); } canvas.restore(); } @override bool shouldRepaint(_AmbiencePainter old) => old.t != t || old.colors != colors; }