diff --git a/AGENTS.md b/AGENTS.md index 0eb9ffe..c6523d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,3 +14,9 @@ - No Co-Authored-By in commits - Conventional commit format - Test before push + +## Skills + +| Skill | Description | Path | +| --- | --- | --- | +| `premium-game-ui` | Layered premium game UI workflow: transparent overlays, cinematic reward screens, Flutter animation/performance rules. | `skills/premium-game-ui/SKILL.md` | diff --git a/assets/rewards/fire_progress_burst.png b/assets/rewards/fire_progress_burst.png new file mode 100644 index 0000000..02ada96 Binary files /dev/null and b/assets/rewards/fire_progress_burst.png differ diff --git a/assets/rewards/medal_unlock_burst.png b/assets/rewards/medal_unlock_burst.png new file mode 100644 index 0000000..81a4848 Binary files /dev/null and b/assets/rewards/medal_unlock_burst.png differ diff --git a/assets/rewards/reward_header_glow.png b/assets/rewards/reward_header_glow.png new file mode 100644 index 0000000..716eaed Binary files /dev/null and b/assets/rewards/reward_header_glow.png differ diff --git a/docs/premium_game_ui_pipeline.md b/docs/premium_game_ui_pipeline.md new file mode 100644 index 0000000..172d54f --- /dev/null +++ b/docs/premium_game_ui_pipeline.md @@ -0,0 +1,82 @@ +# Pipeline para UI premium de juego + +Objetivo: construir pantallas vistosas sin convertir una maqueta en “un PNG gigante”. La UI debe seguir siendo mantenible, animable, responsive, localizable y performante. + +## Diagnóstico en Farolero + +Los assets de `assets/rewards/` mezclan dos usos distintos: + +| Asset | Estado | Uso correcto | +| --- | --- | --- | +| `reward_header_glow.png` | PNG RGBA, pero sus esquinas son opacas (`alpha=255`) | Sirve como fondo/placa completa. No sirve como overlay transparente encima de UI. | +| `medal_unlock_burst.png` | Esquinas transparentes (`alpha=0`) | Correcto como burst/glow overlay, pendiente de revisar si tiene alpha tenue ocupando demasiado lienzo. | +| `fire_progress_burst.png` | Canal alpha, pero hay alpha parcial en bordes | Casi correcto; conviene limpiar/fade de bordes para evitar manchas al componer. | + +Conclusión: la sospecha era buena. Para componer pantallas premium sin “machacar” capas, los elementos decorativos deben ir separados y con transparencia real. Los fondos sí pueden ser opacos. + +## Regla base + +No generar “la pantalla final” como imagen. Generar un **kit por capas**: + +1. **Fondo base**: puede ser opaco, normalmente WebP/JPG/PNG grande. +2. **Overlays atmosféricos**: glows, rays, humo, confetti, sparks, viñetas. PNG/WebP con alpha. +3. **Elementos hero**: insignias, medallas, placas, marcos. PNG con alpha o SVG si es vector limpio. +4. **Layout real**: textos, botones, barras de progreso y estado en Flutter. +5. **Motion**: + - `flutter_animate`: transiciones de widgets, shimmer, scale, blur, secuencias. + - `confetti`: celebraciones puntuales, controlando cantidad de partículas. + - `Rive`: animación interactiva compleja, state machines, assets vectoriales vivos. + - `Lottie`: animaciones After Effects exportadas a JSON. + +## Tamaños recomendados + +| Tipo | Tamaño razonable | Notas | +| --- | ---: | --- | +| Iconos/avatar/medalla pequeña | 128–256 px | En Farolero 256 px está bien como master. | +| Burst circular/hero | 512–1024 px | Transparente, recortado al contenido útil. | +| Banner/header overlay | 1024–1536 px ancho | 2048 solo si realmente ocupa pantalla grande o tablet. | +| Fondo full screen | 1080×1920 o variantes | Mejor optimizar por target móvil. | + +Flutter soporta variantes por densidad (`1.0x`, `2.0x`, `3.0x`), así que para iconos repetidos conviene usar assets resolution-aware en vez de un único PNG enorme. + +## Criterios de aceptación de un asset transparente + +Antes de integrarlo: + +- Las esquinas deben tener `alpha=0`, salvo que sea fondo opaco. +- El bounding box visible debe estar centrado. +- No debe haber “velo” semitransparente cubriendo todo el lienzo si el asset es overlay. +- El archivo debe estar recortado al área necesaria, dejando margen visual controlado. +- Si se anima con escala/blur, dejar margen suficiente para no cortar glow. + +## Pros y contras por técnica + +| Técnica | Pros | Contras | Usarla para | +| --- | --- | --- | --- | +| PNG/WebP alpha | Alta fidelidad artística, fácil de componer | Puede pesar mucho, cuidado con overdraw | Glows, bursts, placas, humo, partículas baked | +| SVG | Ligero, escalable | Menos pictórico; filtros complejos pueden costar | Iconos limpios, formas, marcas | +| CustomPainter | Performante para geometría simple | Difícil lograr arte premium complejo | Rayos, líneas, fondos geométricos | +| `flutter_animate` | Rápido, mantenible, ideal para UI real | No reemplaza arte base | Entrada de paneles, shimmer, contadores | +| `confetti` | Perfecto para celebración | Muchas partículas pueden costar | Victoria, medalla desbloqueada | +| Rive | Interactivo y profesional | Requiere `.riv` y pipeline propio | Mascotas, insignias vivas, state machines | +| Lottie | Muy bueno para AE JSON | No todo AE exporta perfecto; revisar performance | Animaciones cerradas no interactivas | + +## Flujo correcto de trabajo + +1. **Dirección visual**: definir moodboard/prototipo y separar qué es fondo, qué es overlay y qué es UI real. +2. **Lista de capas**: documentar asset, tamaño, transparencia, punto de anclaje y uso. +3. **Generación**: pedir assets individuales, no pantallas completas, con fondo transparente cuando sean overlays. +4. **Validación técnica**: verificar alpha/corners/bounding box/tamaño. +5. **Integración Flutter**: componer con `Stack`, `Positioned`, widgets reales y animaciones controladas. +6. **Performance**: evitar `Opacity` grande y `saveLayer` innecesario; precalcular transparencias estáticas cuando se pueda. +7. **Revisión visual**: comparar contra maqueta y ajustar por capas, no rehacer toda la pantalla. + +## Fuentes revisadas + +- Flutter assets e imágenes: `https://docs.flutter.dev/ui/assets/assets-and-images` +- Flutter performance: `https://docs.flutter.dev/perf/best-practices` +- Flutter animations: `https://docs.flutter.dev/ui/animations` +- Rive Flutter runtime: `https://rive.app/docs/runtimes/flutter/flutter` +- Lottie Flutter: `https://pub.dev/packages/lottie` +- flutter_animate: `https://pub.dev/packages/flutter_animate` +- confetti: `https://pub.dev/packages/confetti` diff --git a/lib/pantallas/pantalla_fin_partida.dart b/lib/pantallas/pantalla_fin_partida.dart index d4de3e7..b0278cd 100644 --- a/lib/pantallas/pantalla_fin_partida.dart +++ b/lib/pantallas/pantalla_fin_partida.dart @@ -56,12 +56,6 @@ class _PantallaFinPartidaState extends State { return Scaffold( extendBodyBehindAppBar: true, - appBar: AppBar( - title: Text(l10n.gameOver), - automaticallyImplyLeading: false, - backgroundColor: Colors.transparent, - elevation: 0, - ), body: FondoFarolero( intenso: true, child: Stack( @@ -69,7 +63,7 @@ class _PantallaFinPartidaState extends State { Positioned.fill( child: IgnorePointer( child: CustomPaint( - painter: _RecompensaFondoPainter( + painter: _EscenarioFinPartidaPainter( color: ganaronJugadores ? TemaApp.colorVerde : TemaApp.colorAcento, @@ -95,10 +89,11 @@ class _PantallaFinPartidaState extends State { ), SafeArea( child: SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 18, 20, 24), + padding: const EdgeInsets.fromLTRB(20, 22, 20, 28), child: Column( children: [ _HeroResultado( + encabezado: l10n.gameOver, titulo: ganaronJugadores ? l10n.playersWin : l10n.impostorsWin, icono: ganaronJugadores @@ -183,11 +178,13 @@ class _PantallaFinPartidaState extends State { } class _HeroResultado extends StatelessWidget { + final String encabezado; final String titulo; final IconData icono; final Color color; const _HeroResultado({ + required this.encabezado, required this.titulo, required this.icono, required this.color, @@ -195,16 +192,37 @@ class _HeroResultado extends StatelessWidget { @override Widget build(BuildContext context) { + final tituloLimpio = titulo + .replaceAll('¡', '') + .replaceAll('!', '') + .trim() + .toUpperCase(); return Stack( alignment: Alignment.center, children: [ SizedBox( - height: 230, + height: 330, width: double.infinity, - child: CustomPaint(painter: _RayosRecompensaPainter(color: color)), + child: CustomPaint(painter: _HeroCinematicoPainter(color: color)), ), Column( children: [ + Text( + encabezado, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.headlineMedium?.copyWith( + color: TemaApp.colorDorado, + fontWeight: FontWeight.w900, + letterSpacing: -0.5, + shadows: [ + Shadow( + color: TemaApp.colorNaranja.withValues(alpha: 0.55), + blurRadius: 18, + ), + ], + ), + ).animate().fadeIn(duration: 260.ms).slideY(begin: -0.18), + const SizedBox(height: 42), Text( 'RESULTADOS', style: Theme.of(context).textTheme.labelLarge?.copyWith( @@ -213,10 +231,10 @@ class _HeroResultado extends StatelessWidget { letterSpacing: 4, ), ).animate().fadeIn(duration: 350.ms).slideY(begin: -0.25), - const SizedBox(height: 12), + const SizedBox(height: 18), Container( - width: 116, - height: 116, + width: 132, + height: 132, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( @@ -240,7 +258,22 @@ class _HeroResultado extends StatelessWidget { ), ], ), - child: Icon(icono, size: 64, color: TemaApp.colorDorado), + child: Stack( + alignment: Alignment.center, + children: [ + Icon(icono, size: 72, color: TemaApp.colorDorado), + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + color: Colors.white.withValues(alpha: 0.08), + ), + ), + ), + ), + ], + ), ) .animate() .scale( @@ -251,10 +284,11 @@ class _HeroResultado extends StatelessWidget { .shimmer(delay: 700.ms, duration: 1500.ms), const SizedBox(height: 14), Text( - titulo.toUpperCase(), + '¡$tituloLimpio!', textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineMedium?.copyWith( color: color, + fontSize: 31, fontWeight: FontWeight.w900, letterSpacing: 1.2, shadows: [ @@ -306,9 +340,10 @@ class _TarjetaProgresoGamificacion extends StatelessWidget { const SizedBox(height: 16), if (nuevas.isEmpty) Text( - 'Sin medallas nuevas esta vez. Segu? acumulando fuego.', - style: Theme.of(context).textTheme.bodyMedium?.copyWith( + 'Sin medallas nuevas esta vez. Seguí acumulando fuego.', + style: Theme.of(context).textTheme.titleMedium?.copyWith( color: TemaApp.colorTextoSecundario, + height: 1.35, ), ) else ...[ @@ -372,34 +407,49 @@ class _PanelRecompensa extends StatelessWidget { @override Widget build(BuildContext context) { - return Container( - width: double.infinity, - padding: const EdgeInsets.all(18), - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - const Color(0xFF111B28).withValues(alpha: 0.96), - const Color(0xFF180D22).withValues(alpha: 0.94), + return ClipRRect( + borderRadius: BorderRadius.circular(28), + child: Container( + width: double.infinity, + padding: const EdgeInsets.all(20), + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + const Color(0xFF101A27).withValues(alpha: 0.96), + const Color(0xFF1B0E24).withValues(alpha: 0.94), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + borderRadius: BorderRadius.circular(28), + border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.58)), + boxShadow: [ + BoxShadow( + color: TemaApp.colorNaranja.withValues(alpha: 0.20), + blurRadius: 36, + offset: const Offset(0, 18), + ), + BoxShadow( + color: Colors.black.withValues(alpha: 0.50), + blurRadius: 22, + offset: const Offset(0, 10), + ), ], - begin: Alignment.topLeft, - end: Alignment.bottomRight, ), - borderRadius: BorderRadius.circular(22), - border: Border.all(color: TemaApp.colorDorado.withValues(alpha: 0.52)), - boxShadow: [ - BoxShadow( - color: TemaApp.colorNaranja.withValues(alpha: 0.18), - blurRadius: 32, - offset: const Offset(0, 16), + foregroundDecoration: BoxDecoration( + borderRadius: BorderRadius.circular(28), + gradient: LinearGradient( + colors: [ + Colors.white.withValues(alpha: 0.06), + Colors.transparent, + TemaApp.colorDorado.withValues(alpha: 0.04), + ], + begin: Alignment.topLeft, + end: Alignment.bottomRight, ), - BoxShadow( - color: Colors.black.withValues(alpha: 0.42), - blurRadius: 18, - offset: const Offset(0, 8), - ), - ], + ), + child: child, ), - child: child, ); } } @@ -480,33 +530,56 @@ class _BarraFuegoPremium extends StatelessWidget { ], ), const SizedBox(height: 8), - ClipRRect( - borderRadius: BorderRadius.circular(999), - child: Container( - height: 22, - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.42), - border: Border.all( - color: TemaApp.colorDorado.withValues(alpha: 0.34), - ), + Container( + height: 30, + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.72), + borderRadius: BorderRadius.circular(999), + border: Border.all( + color: TemaApp.colorDorado.withValues(alpha: 0.38), ), - child: Align( - alignment: Alignment.centerLeft, - child: FractionallySizedBox( - widthFactor: normalizado, - child: Container( - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xFFE53935), - TemaApp.colorNaranja, - TemaApp.colorDorado, - Color(0xFFFFECBE), - ], + boxShadow: [ + BoxShadow( + color: TemaApp.colorNaranja.withValues(alpha: 0.24), + blurRadius: 18, + ), + ], + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(999), + child: Stack( + children: [ + FractionallySizedBox( + widthFactor: normalizado, + child: Container( + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xFFE53935), + TemaApp.colorNaranja, + TemaApp.colorDorado, + Color(0xFFFFECBE), + ], + ), ), ), ), - ), + Positioned.fill( + child: DecoratedBox( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Colors.white.withValues(alpha: 0.32), + Colors.transparent, + Colors.black.withValues(alpha: 0.18), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + ), + ), + ], ), ), ), @@ -539,6 +612,12 @@ class _MedallaDesbloqueada extends StatelessWidget { Stack( alignment: Alignment.center, children: [ + Image.asset( + 'assets/rewards/medal_unlock_burst.png', + width: 82, + height: 82, + fit: BoxFit.cover, + ), SizedBox( width: 70, height: 70, @@ -588,15 +667,20 @@ class _TarjetaSecreto extends StatelessWidget { return _PanelRecompensa( child: Column( children: [ - Text(l10n.theSecretWordWas, - style: Theme.of(context).textTheme.titleMedium), - const SizedBox(height: 8), + Text( + l10n.theSecretWordWas, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), + const SizedBox(height: 14), Text( palabra.toUpperCase(), textAlign: TextAlign.center, style: Theme.of(context).textTheme.headlineLarge?.copyWith( color: TemaApp.colorNaranja, - fontSize: 36, + fontSize: 42, fontWeight: FontWeight.w900, shadows: [ Shadow( @@ -630,7 +714,13 @@ class _TarjetaImpostores extends StatelessWidget { return _PanelRecompensa( child: Column( children: [ - Text(titulo, style: Theme.of(context).textTheme.titleMedium), + Text( + '🎭 $titulo', + style: Theme.of(context).textTheme.titleLarge?.copyWith( + fontWeight: FontWeight.w900, + color: Colors.white, + ), + ), const SizedBox(height: 10), ...impostores.map( (j) => Padding( @@ -756,27 +846,76 @@ class _BotonesFinPartida extends StatelessWidget { } } -class _RecompensaFondoPainter extends CustomPainter { +class _EscenarioFinPartidaPainter extends CustomPainter { final Color color; - const _RecompensaFondoPainter({required this.color}); + const _EscenarioFinPartidaPainter({required this.color}); @override void paint(Canvas canvas, Size size) { final paint = Paint()..isAntiAlias = true; + + paint.shader = const LinearGradient( + colors: [ + Color(0xFF050B14), + Color(0xFF091322), + Color(0xFF16091F), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(Offset.zero & size); + canvas.drawRect(Offset.zero & size, paint); + paint.shader = RadialGradient( - colors: [color.withValues(alpha: 0.28), Colors.transparent], + colors: [ + color.withValues(alpha: 0.28), + TemaApp.colorNaranja.withValues(alpha: 0.12), + Colors.transparent, + ], ).createShader( Rect.fromCircle( - center: Offset(size.width * 0.5, size.height * 0.22), - radius: size.width * 0.72, + center: Offset(size.width * 0.5, size.height * 0.30), + radius: size.width * 0.86, ), ); canvas.drawCircle( - Offset(size.width * 0.5, size.height * 0.22), - size.width * 0.72, + Offset(size.width * 0.5, size.height * 0.30), + size.width * 0.86, paint, ); + + paint.shader = null; + _drawSkyline(canvas, size, paint); + + for (var i = 0; i < 52; i++) { + final x = (i * 71 % math.max(size.width, 1)).toDouble(); + final y = (i * 137 % math.max(size.height, 1)).toDouble(); + final palette = [ + TemaApp.colorDorado, + TemaApp.colorNaranja, + TemaApp.colorAcento, + const Color(0xFFFFF1C7), + ]; + final confettiPaint = Paint() + ..isAntiAlias = true + ..color = palette[i % palette.length].withValues(alpha: 0.72); + canvas.save(); + canvas.translate(x, y); + canvas.rotate((i % 9 - 4) * 0.22); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset.zero, + width: 8 + (i % 4) * 6, + height: 4 + (i % 3) * 3, + ), + const Radius.circular(1.5), + ), + confettiPaint, + ); + canvas.restore(); + } + paint.shader = null; paint.color = TemaApp.colorDorado.withValues(alpha: 0.10); for (var i = 0; i < 34; i++) { @@ -786,34 +925,75 @@ class _RecompensaFondoPainter extends CustomPainter { } } + void _drawSkyline(Canvas canvas, Size size, Paint paint) { + paint.color = Colors.black.withValues(alpha: 0.36); + final base = size.height * 0.78; + final path = Path() + ..moveTo(0, size.height) + ..lineTo(0, base) + ..lineTo(size.width * 0.16, base - 42) + ..lineTo(size.width * 0.30, base - 12) + ..lineTo(size.width * 0.48, base - 60) + ..lineTo(size.width * 0.66, base - 26) + ..lineTo(size.width * 0.82, base - 72) + ..lineTo(size.width, base - 34) + ..lineTo(size.width, size.height) + ..close(); + canvas.drawPath(path, paint); + + paint.color = TemaApp.colorNaranja.withValues(alpha: 0.18); + canvas.drawRRect( + RRect.fromRectAndRadius( + Rect.fromCenter( + center: Offset(size.width * 0.52, base - 90), + width: 28, + height: 130, + ), + const Radius.circular(12), + ), + paint, + ); + } + @override - bool shouldRepaint(covariant _RecompensaFondoPainter oldDelegate) { + bool shouldRepaint(covariant _EscenarioFinPartidaPainter oldDelegate) { return oldDelegate.color != color; } } -class _RayosRecompensaPainter extends CustomPainter { +class _HeroCinematicoPainter extends CustomPainter { final Color color; - const _RayosRecompensaPainter({required this.color}); + const _HeroCinematicoPainter({required this.color}); @override void paint(Canvas canvas, Size size) { - final center = Offset(size.width / 2, size.height * 0.5); + final center = Offset(size.width / 2, size.height * 0.54); final paint = Paint()..isAntiAlias = true; + + paint.shader = RadialGradient( + colors: [ + TemaApp.colorNaranja.withValues(alpha: 0.30), + color.withValues(alpha: 0.14), + Colors.transparent, + ], + ).createShader(Rect.fromCircle(center: center, radius: size.width * 0.54)); + canvas.drawCircle(center, size.width * 0.54, paint); + paint.shader = null; + for (var i = 0; i < 28; i++) { final angle = (math.pi * 2 / 28) * i; final inner = Offset( - center.dx + math.cos(angle - 0.035) * 34, - center.dy + math.sin(angle - 0.035) * 34, + center.dx + math.cos(angle - 0.035) * 52, + center.dy + math.sin(angle - 0.035) * 52, ); final outer = Offset( - center.dx + math.cos(angle) * size.width * 0.46, - center.dy + math.sin(angle) * size.width * 0.46, + center.dx + math.cos(angle) * size.width * 0.58, + center.dy + math.sin(angle) * size.width * 0.58, ); final inner2 = Offset( - center.dx + math.cos(angle + 0.035) * 34, - center.dy + math.sin(angle + 0.035) * 34, + center.dx + math.cos(angle + 0.035) * 52, + center.dy + math.sin(angle + 0.035) * 52, ); paint.color = (i.isEven ? TemaApp.colorDorado : color) .withValues(alpha: i.isEven ? 0.16 : 0.09); @@ -829,7 +1009,7 @@ class _RayosRecompensaPainter extends CustomPainter { } @override - bool shouldRepaint(covariant _RayosRecompensaPainter oldDelegate) { + bool shouldRepaint(covariant _HeroCinematicoPainter oldDelegate) { return oldDelegate.color != color; } } diff --git a/pubspec.yaml b/pubspec.yaml index 9f460bc..a96eba1 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -37,3 +37,4 @@ flutter: - assets/words/ - assets/avatars/ - assets/medals/ + - assets/rewards/ diff --git a/skills/premium-game-ui/SKILL.md b/skills/premium-game-ui/SKILL.md new file mode 100644 index 0000000..7ee2f15 --- /dev/null +++ b/skills/premium-game-ui/SKILL.md @@ -0,0 +1,59 @@ +--- +name: premium-game-ui +description: > + Create professional, layered, high-impact game screens in Flutter without flattening the UI into a single image. + Trigger: when designing, generating, reviewing, or implementing premium game UI screens, reward screens, medal screens, avatar screens, overlays, transparent assets, visual effects, or cinematic game feedback. +license: Apache-2.0 +metadata: + author: gentleman-programming + version: "1.0" +--- + +## When to Use + +- A screen must look premium, cinematic, or game-like. +- Assets are being generated for Flutter UI. +- The user mentions transparent backgrounds, overlays, rewards, medals, avatars, glows, bursts, particles, confetti, Lottie, Rive, or visual polish. +- A prototype/mockup must be converted into real app UI. + +## Critical Patterns + +1. **Never flatten a real screen into one PNG.** Use real Flutter layout for text, buttons, state, progress, localization, and accessibility. +2. **Generate a layered asset kit.** Separate opaque backgrounds from transparent overlays. +3. **Transparent means technically transparent.** For overlays, image corners should usually be `alpha=0`; reject white/black baked backgrounds. +4. **Right size, not maximum size.** Icons/avatars usually 128–256 px. Hero bursts 512–1024 px. Full backgrounds can be larger. +5. **Use animation by responsibility.** + - `flutter_animate`: widget entrances, shimmer, scale, blur, staggered UI. + - `confetti`: short celebrations; keep particles controlled. + - `Rive`: interactive/stateful vector animation. + - `Lottie`: After Effects JSON animations. +6. **Avoid expensive composition.** Do not wrap large areas in `Opacity` when a semitransparent color/image or pre-baked alpha asset works. +7. **Plan before generating.** First write the layer list: filename, role, size, alpha requirement, anchor, and Flutter usage. + +## Asset Acceptance Checklist + +- [ ] Correct role: background, overlay, hero element, icon, or animation. +- [ ] Correct format: PNG/WebP alpha for pictorial overlays, SVG for simple vector, Rive/Lottie for motion. +- [ ] Correct alpha: overlay corners transparent unless intentionally full-canvas. +- [ ] Centered visible content; no neighboring sprite fragments. +- [ ] No unnecessary 2048+ px icons. +- [ ] Registered in `pubspec.yaml` only at the needed directory level. +- [ ] Integrated through `Stack`/widgets, not as a full-screen screenshot. + +## Flutter Integration Pattern + +```dart +Stack( + children: [ + const PremiumBackground(), // opaque base + Positioned.fill(child: Image.asset('assets/fx/vignette.png', fit: BoxFit.cover)), + Positioned(top: 80, left: 0, right: 0, child: RewardHero()), + Positioned.fill(child: IgnorePointer(child: ConfettiWidget(confettiController: controller))), + SafeArea(child: RealScreenContent()), // text/buttons/progress remain widgets + ], +) +``` + +## Documentation + +- Main pipeline: `docs/premium_game_ui_pipeline.md`