6b796b6d1d
Fix 1: Stop allocating ThemeColors on every animation frame by caching it in _colors field and updating only when theme changes via didUpdateWidget. Fix 2: Leaf particles now follow the season accent color instead of hardcoded orange, maintaining visual consistency with theme changes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
142 lines
4.1 KiB
Dart
142 lines
4.1 KiB
Dart
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<SeasonBackground> createState() => _SeasonBackgroundState();
|
|
}
|
|
|
|
class _SeasonBackgroundState extends State<SeasonBackground>
|
|
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;
|
|
}
|