Files
BlockSeasons/lib/ui/widgets/season_background.dart
T
airkjw 6b796b6d1d perf: cache ThemeColors across frames; theme-tint leaf particles
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>
2026-06-11 21:24:08 +09:00

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;
}