super assets
@@ -19,4 +19,4 @@
|
|||||||
|
|
||||||
| Skill | Description | Path |
|
| 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` |
|
| `premium-game-ui` | Layered premium game UI workflow: AI-generated high-quality transparent assets, cinematic screens, Flutter animation/performance rules. | `skills/premium-game-ui/SKILL.md` |
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.8 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 504 KiB |
|
After Width: | Height: | Size: 1.2 MiB |
|
After Width: | Height: | Size: 1.1 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.3 MiB |
|
After Width: | Height: | Size: 1.7 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.6 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 1.5 MiB |
@@ -2,6 +2,24 @@
|
|||||||
|
|
||||||
Objetivo: construir pantallas vistosas sin convertir una maqueta en “un PNG gigante”. La UI debe seguir siendo mantenible, animable, responsive, localizable y performante.
|
Objetivo: construir pantallas vistosas sin convertir una maqueta en “un PNG gigante”. La UI debe seguir siendo mantenible, animable, responsive, localizable y performante.
|
||||||
|
|
||||||
|
|
||||||
|
## Regla obligatoria: arte generado de alta calidad
|
||||||
|
|
||||||
|
Una pantalla premium NO se considera terminada si solo usa gradientes, `CustomPainter`, iconos Material y glows simples. Eso puede servir como scaffolding, pero no alcanza el nivel visual buscado.
|
||||||
|
|
||||||
|
Para cada pantalla o lote premium hay que definir y generar assets artísticos reales:
|
||||||
|
|
||||||
|
| Capa | Asset esperado | Transparencia |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| Fondo atmosférico | Imagen generada opaca, móvil vertical, 1080x1920 o similar | No necesaria |
|
||||||
|
| Hero/emblema/mascota | Imagen generada PNG/WebP de alta calidad | Obligatoria |
|
||||||
|
| Marco de panel/botón | Imagen generada con bordes/brillos premium | Obligatoria |
|
||||||
|
| Burst/glow/humo/confetti | Overlay generado pictórico | Obligatoria |
|
||||||
|
| Iconos/medallas/avatares | 128-256 px o 512 px si son hero | Obligatoria |
|
||||||
|
|
||||||
|
Si el asset va encima de Flutter, debe tener fondo transparente real. Si el generador no entrega alpha nativo, se genera con chroma-key plano, se elimina localmente y se valida que las esquinas tengan `alpha=0`.
|
||||||
|
|
||||||
|
La regla práctica es brutal pero necesaria: **si el resultado parece Flutter con filtros, todavía falta arte**.
|
||||||
## Diagnóstico en Farolero
|
## Diagnóstico en Farolero
|
||||||
|
|
||||||
Los assets de `assets/rewards/` mezclan dos usos distintos:
|
Los assets de `assets/rewards/` mezclan dos usos distintos:
|
||||||
|
|||||||
@@ -0,0 +1,127 @@
|
|||||||
|
# Professional Generated UI Art Plan
|
||||||
|
|
||||||
|
Este plan corrige el enfoque anterior: una pantalla premium no se da por terminada si solo usa Flutter, gradientes, `CustomPainter`, iconos Material y glows procedurales. Eso es estructura. La dirección visual profesional exige assets generados de alta calidad, integrados por capas.
|
||||||
|
|
||||||
|
## Reglas no negociables
|
||||||
|
|
||||||
|
- La UI real sigue en Flutter: textos, localización, botones, inputs, progreso, navegación y estado.
|
||||||
|
- El arte visual potente se genera como assets independientes.
|
||||||
|
- Todo asset que va encima de Flutter debe tener transparencia real: PNG/WebP con alpha.
|
||||||
|
- Si el generador no entrega alpha nativo, se usa chroma-key plano y eliminación local.
|
||||||
|
- Se valida tamaño, alpha en esquinas, centrado y peso antes de integrar.
|
||||||
|
- Se diseña para móvil, tablet, iPad y iPhone: fondos flexibles, contenido con ancho máximo, assets no estirados.
|
||||||
|
|
||||||
|
## Estructura de assets
|
||||||
|
|
||||||
|
```text
|
||||||
|
assets/ui/generated/
|
||||||
|
shared/
|
||||||
|
main/
|
||||||
|
mode/
|
||||||
|
create_game/
|
||||||
|
join_lobby/
|
||||||
|
final_rewards/
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contrato de tamaños
|
||||||
|
|
||||||
|
| Tipo | Tamaño master | Uso |
|
||||||
|
| --- | ---: | --- |
|
||||||
|
| Fondo vertical móvil | 1080×1920 | `BoxFit.cover`, puede ser opaco |
|
||||||
|
| Fondo tablet | 1440×1920 o 2048×1536 | `BoxFit.cover`, opcional por lote |
|
||||||
|
| Hero principal | 768–1024 px | PNG transparente |
|
||||||
|
| Emblema / mascota | 512–1024 px | PNG transparente |
|
||||||
|
| Marco de card/botón | 1024×256 / 1024×512 | PNG transparente |
|
||||||
|
| Icono/medalla | 128–256 px | PNG transparente |
|
||||||
|
| Burst/glow/humo | 512–1024 px | PNG transparente |
|
||||||
|
|
||||||
|
## Manifiesto de assets inicial
|
||||||
|
|
||||||
|
| ID | Ruta | Pantallas | Rol | Alpha | Estado |
|
||||||
|
| --- | --- | --- | --- | --- | --- |
|
||||||
|
| `main_atmosphere_bg` | `assets/ui/generated/main/main_atmosphere_bg.png` | Principal | Fondo vertical premium | No | Falta generar |
|
||||||
|
| `main_lantern_hero` | `assets/ui/generated/main/main_lantern_hero.png` | Principal | Hero/emblema de farol | Sí | Falta generar |
|
||||||
|
| `main_cta_frame` | `assets/ui/generated/main/main_cta_frame.png` | Principal | Marco/texture CTA | Sí | Falta generar |
|
||||||
|
| `shared_glass_panel_frame` | `assets/ui/generated/shared/glass_panel_frame.png` | Varias | Marco panel premium | Sí | Falta generar |
|
||||||
|
| `mode_duel_hero` | `assets/ui/generated/mode/mode_duel_hero.png` | Elegir modo | Hero de decisión | Sí | Falta generar |
|
||||||
|
| `create_game_header_art` | `assets/ui/generated/create_game/header_art.png` | Crear partida | Cabecera visual | Sí | Falta generar |
|
||||||
|
| `join_lobby_signal_art` | `assets/ui/generated/join_lobby/signal_art.png` | Unirse/lobby | Arte conexión/QR | Sí | Falta generar |
|
||||||
|
| `final_rewards_cinematic` | `assets/ui/generated/final_rewards/cinematic_burst.png` | Fin partida | Burst recompensa | Sí | Falta generar |
|
||||||
|
|
||||||
|
## Prompts base
|
||||||
|
|
||||||
|
### Fondo principal
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use case: stylized-concept
|
||||||
|
Asset type: mobile game background, opaque
|
||||||
|
Primary request: high-end cinematic night village background for a social deduction mobile game called Farolero, warm lantern light, mysterious alleys, premium game UI atmosphere, dark navy and amber palette, depth, subtle fog, no text, no UI, no characters, vertical composition.
|
||||||
|
Size intent: 1080x1920.
|
||||||
|
Avoid: logos, letters, readable signs, buttons, phone status bar, flat vector look.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Hero farol principal transparente
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use case: stylized-concept
|
||||||
|
Asset type: transparent hero emblem
|
||||||
|
Primary request: premium stylized lantern emblem for a mobile social deduction game, cinematic amber glow, polished 3D game icon quality, ornate but readable silhouette, centered, no text.
|
||||||
|
Transparency: must be isolated on flat chroma-key background if native transparency is unavailable; no shadows baked into the background.
|
||||||
|
Size intent: 1024x1024.
|
||||||
|
Avoid: white background, black background, letters, watermark.
|
||||||
|
```
|
||||||
|
|
||||||
|
### Marco CTA transparente
|
||||||
|
|
||||||
|
```text
|
||||||
|
Use case: stylized-concept
|
||||||
|
Asset type: transparent UI frame
|
||||||
|
Primary request: premium golden rounded mobile game CTA button frame, glossy amber material, subtle bevel, inner glow, transparent center area usable behind Flutter text, no text, no icons.
|
||||||
|
Transparency: transparent outside frame and transparent usable center.
|
||||||
|
Size intent: 1024x256.
|
||||||
|
Avoid: baked text, opaque rectangle background, excessive decorations.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Validación técnica
|
||||||
|
|
||||||
|
Para overlays:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
Add-Type -AssemblyName System.Drawing
|
||||||
|
$bmp=[Drawing.Bitmap]::FromFile("asset.png")
|
||||||
|
@($bmp.GetPixel(0,0).A,$bmp.GetPixel($bmp.Width-1,0).A,$bmp.GetPixel(0,$bmp.Height-1).A,$bmp.GetPixel($bmp.Width-1,$bmp.Height-1).A)
|
||||||
|
$bmp.Dispose()
|
||||||
|
```
|
||||||
|
|
||||||
|
Resultado esperado: `0,0,0,0` salvo fondos opacos documentados.
|
||||||
|
|
||||||
|
## Rollout
|
||||||
|
|
||||||
|
1. Pantalla principal: corregir dirección visual base.
|
||||||
|
2. Elegir modo + crear partida: adaptar estilo sin perder usabilidad.
|
||||||
|
3. Unirse + lobby: QR y conexión como assets reales.
|
||||||
|
4. Final de partida: cinematográfica de recompensas.
|
||||||
|
5. Resto de pantallas: aplicar assets compartidos y ajustar responsive.
|
||||||
|
|
||||||
|
## Phase 2 generated assets
|
||||||
|
|
||||||
|
- `assets/ui/generated/mode/mode_duel_hero.png`: hero transparente 1024?1024 para selecci?n de modo.
|
||||||
|
- `assets/ui/generated/mode/mode_single_card_frame.png`: marco transparente 1400?520 para tarjeta de un m?vil.
|
||||||
|
- `assets/ui/generated/mode/mode_multi_card_frame.png`: marco transparente 1400?520 para tarjeta multidispositivo.
|
||||||
|
- `assets/ui/generated/create_game/create_game_header_art.png`: cabecera transparente 1400?620 para crear partida.
|
||||||
|
- `assets/ui/generated/shared/glass_panel_frame.png`: marco transparente compartido 1400?520 para paneles premium.
|
||||||
|
|
||||||
|
Validaci?n: todos los assets de Phase 2 son PNG RGBA y tienen alpha 0 en las cuatro esquinas.
|
||||||
|
## Phase 2 correction ? image generation, not procedural assets
|
||||||
|
|
||||||
|
La primera iteraci?n de Phase 2 fue descartada porque los PNG se hab?an producido con Python/Pillow. Eso NO cumple el est?ndar premium: eran composici?n program?tica, no arte generado de alta calidad.
|
||||||
|
|
||||||
|
Assets rehechos con el flujo correcto del skill `imagegen` + `premium-game-ui`:
|
||||||
|
|
||||||
|
- `assets/ui/generated/mode/mode_duel_hero.png` ? generado con `image_gen`, source chroma-key en `mode_duel_hero_source_chroma.png`, alpha removido localmente.
|
||||||
|
- `assets/ui/generated/mode/mode_single_card_frame.png` ? generado con `image_gen`, source chroma-key en `mode_single_card_frame_source_chroma.png`, alpha removido localmente.
|
||||||
|
- `assets/ui/generated/mode/mode_multi_card_frame.png` ? generado con `image_gen`, source chroma-key en `mode_multi_card_frame_source_chroma.png`, alpha removido localmente.
|
||||||
|
- `assets/ui/generated/create_game/create_game_header_art.png` ? generado con `image_gen`, source chroma-key en `create_game_header_art_source_chroma.png`, alpha removido localmente.
|
||||||
|
- `assets/ui/generated/shared/glass_panel_frame.png` ? generado con `image_gen`, source chroma-key en `glass_panel_frame_source_chroma.png`, alpha removido localmente.
|
||||||
|
|
||||||
|
Validaci?n: todos los PNG finales son RGBA y tienen alpha 0 en las cuatro esquinas.
|
||||||
@@ -323,9 +323,8 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
EncabezadoFarolero(
|
_CrearPartidaHeader(
|
||||||
icono: Icons.groups,
|
titulo: '?C?mo quieres jugar?',
|
||||||
titulo: '¿Cómo quieres jugar?',
|
|
||||||
subtitulo: l10n.playersRange,
|
subtitulo: l10n.playersRange,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
@@ -610,3 +609,63 @@ class _PantallaCrearPartidaState extends State<PantallaCrearPartida> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class _CrearPartidaHeader extends StatelessWidget {
|
||||||
|
final String titulo;
|
||||||
|
final String subtitulo;
|
||||||
|
|
||||||
|
const _CrearPartidaHeader({
|
||||||
|
required this.titulo,
|
||||||
|
required this.subtitulo,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
final ancho = constraints.maxWidth.clamp(320.0, 720.0).toDouble();
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(maxWidth: ancho),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
|
children: [
|
||||||
|
AspectRatio(
|
||||||
|
aspectRatio: 2,
|
||||||
|
child: Image.asset(
|
||||||
|
'assets/ui/generated/create_game/create_game_header_art.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
opacity: const AlwaysStoppedAnimation(0.96),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text(
|
||||||
|
titulo,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.headlineSmall?.copyWith(
|
||||||
|
color: TemaApp.colorDorado,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
shadows: [
|
||||||
|
Shadow(
|
||||||
|
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
||||||
|
blurRadius: 16,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitulo,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
|
color: TemaApp.colorTextoSecundario,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
import 'dart:math' as math;
|
|
||||||
|
|
||||||
import 'package:farolero/l10n/generated/app_localizations.dart';
|
import 'package:farolero/l10n/generated/app_localizations.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_animate/flutter_animate.dart';
|
import 'package:flutter_animate/flutter_animate.dart';
|
||||||
@@ -31,8 +29,12 @@ class PantallaPrincipal extends StatelessWidget {
|
|||||||
intenso: true,
|
intenso: true,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
const Positioned.fill(
|
Positioned.fill(
|
||||||
child: IgnorePointer(child: CustomPaint(painter: _InicioFondoPainter())),
|
child: Image.asset(
|
||||||
|
'assets/ui/generated/main/main_atmosphere_bg.png',
|
||||||
|
fit: BoxFit.cover,
|
||||||
|
alignment: Alignment.center,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: IgnorePointer(
|
child: IgnorePointer(
|
||||||
@@ -53,92 +55,108 @@ class PantallaPrincipal extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
SafeArea(
|
SafeArea(
|
||||||
child: Center(
|
child: LayoutBuilder(
|
||||||
child: SingleChildScrollView(
|
builder: (context, constraints) {
|
||||||
padding: const EdgeInsets.fromLTRB(20, 18, 20, 24),
|
final isWide = constraints.maxWidth >= 700;
|
||||||
child: ConstrainedBox(
|
final maxContentWidth = isWide ? 540.0 : 430.0;
|
||||||
constraints: const BoxConstraints(maxWidth: 430),
|
final horizontalPadding = isWide ? 32.0 : 20.0;
|
||||||
child: Column(
|
|
||||||
children: [
|
return Center(
|
||||||
_PerfilInicioPremium(
|
child: SingleChildScrollView(
|
||||||
nombre: perfil.nombre,
|
padding: EdgeInsets.fromLTRB(
|
||||||
nick: perfil.nick,
|
horizontalPadding,
|
||||||
avatarAsset: perfil.avatarAsset,
|
18,
|
||||||
fuego: gamificacion.fuego,
|
horizontalPadding,
|
||||||
medallas: gamificacion.medallas,
|
24,
|
||||||
onAjustes: () {
|
),
|
||||||
Navigator.push(
|
child: ConstrainedBox(
|
||||||
context,
|
constraints: BoxConstraints(maxWidth: maxContentWidth),
|
||||||
MaterialPageRoute(
|
child: Column(
|
||||||
builder: (_) => const PantallaAjustes(),
|
children: [
|
||||||
),
|
_PerfilInicioPremium(
|
||||||
);
|
nombre: perfil.nombre,
|
||||||
},
|
nick: perfil.nick,
|
||||||
ajustesTooltip: l10n.settings,
|
avatarAsset: perfil.avatarAsset,
|
||||||
).animate().fadeIn(duration: 280.ms).slideY(begin: -0.12),
|
fuego: gamificacion.fuego,
|
||||||
const SizedBox(height: 42),
|
medallas: gamificacion.medallas,
|
||||||
_HeroInicioPremium(subtitulo: l10n.subtitle)
|
onAjustes: () {
|
||||||
.animate()
|
Navigator.push(
|
||||||
.fadeIn(delay: 120.ms, duration: 420.ms)
|
context,
|
||||||
.scale(begin: const Offset(0.92, 0.92)),
|
MaterialPageRoute(
|
||||||
const SizedBox(height: 46),
|
builder: (_) => const PantallaAjustes(),
|
||||||
_BotonInicioPremium.primario(
|
),
|
||||||
texto: 'Jugar',
|
);
|
||||||
icono: Icons.play_arrow_rounded,
|
},
|
||||||
onPressed: () {
|
ajustesTooltip: l10n.settings,
|
||||||
Navigator.push(
|
).animate().fadeIn(duration: 280.ms).slideY(begin: -0.12),
|
||||||
context,
|
SizedBox(height: isWide ? 48 : 34),
|
||||||
MaterialPageRoute(
|
_HeroInicioPremium(
|
||||||
builder: (_) => const PantallaSeleccionModoJuego(),
|
subtitulo: l10n.subtitle,
|
||||||
),
|
compact: constraints.maxHeight < 760,
|
||||||
);
|
)
|
||||||
},
|
.animate()
|
||||||
).animate().fadeIn(delay: 240.ms).slideY(begin: 0.16),
|
.fadeIn(delay: 120.ms, duration: 420.ms)
|
||||||
const SizedBox(height: 14),
|
.scale(begin: const Offset(0.92, 0.92)),
|
||||||
_BotonInicioPremium.secundario(
|
SizedBox(height: isWide ? 46 : 34),
|
||||||
texto: l10n.joinGame,
|
_BotonInicioPremium.primario(
|
||||||
icono: Icons.bolt_rounded,
|
texto: 'Jugar',
|
||||||
onPressed: () {
|
icono: Icons.play_arrow_rounded,
|
||||||
Navigator.push(
|
onPressed: () {
|
||||||
context,
|
Navigator.push(
|
||||||
MaterialPageRoute(builder: (_) => const PantallaUnirse()),
|
context,
|
||||||
);
|
MaterialPageRoute(
|
||||||
},
|
builder: (_) => const PantallaSeleccionModoJuego(),
|
||||||
).animate().fadeIn(delay: 320.ms).slideY(begin: 0.16),
|
),
|
||||||
const SizedBox(height: 12),
|
);
|
||||||
_BotonInicioPremium.oscuro(
|
},
|
||||||
texto: l10n.howToPlay,
|
).animate().fadeIn(delay: 240.ms).slideY(begin: 0.16),
|
||||||
icono: Icons.question_mark_rounded,
|
const SizedBox(height: 14),
|
||||||
onPressed: () {
|
_BotonInicioPremium.secundario(
|
||||||
Navigator.push(
|
texto: l10n.joinGame,
|
||||||
context,
|
icono: Icons.bolt_rounded,
|
||||||
MaterialPageRoute(builder: (_) => const PantallaReglas()),
|
onPressed: () {
|
||||||
);
|
Navigator.push(
|
||||||
},
|
context,
|
||||||
).animate().fadeIn(delay: 390.ms).slideY(begin: 0.16),
|
MaterialPageRoute(builder: (_) => const PantallaUnirse()),
|
||||||
const SizedBox(height: 14),
|
);
|
||||||
_AccesoHistorialPremium(
|
},
|
||||||
etiqueta: 'Historial',
|
).animate().fadeIn(delay: 320.ms).slideY(begin: 0.16),
|
||||||
onPressed: () {
|
const SizedBox(height: 12),
|
||||||
Navigator.push(
|
_BotonInicioPremium.oscuro(
|
||||||
context,
|
texto: l10n.howToPlay,
|
||||||
MaterialPageRoute(builder: (_) => const PantallaHistorial()),
|
icono: Icons.question_mark_rounded,
|
||||||
);
|
onPressed: () {
|
||||||
},
|
Navigator.push(
|
||||||
).animate().fadeIn(delay: 470.ms).slideY(begin: 0.14),
|
context,
|
||||||
const SizedBox(height: 28),
|
MaterialPageRoute(builder: (_) => const PantallaReglas()),
|
||||||
Text(
|
);
|
||||||
l10n.playersRange,
|
},
|
||||||
textAlign: TextAlign.center,
|
).animate().fadeIn(delay: 390.ms).slideY(begin: 0.16),
|
||||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
const SizedBox(height: 14),
|
||||||
color: TemaApp.colorTextoSecundario.withValues(alpha: 0.82),
|
_AccesoHistorialPremium(
|
||||||
letterSpacing: 0.8,
|
etiqueta: 'Historial',
|
||||||
),
|
onPressed: () {
|
||||||
|
Navigator.push(
|
||||||
|
context,
|
||||||
|
MaterialPageRoute(builder: (_) => const PantallaHistorial()),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
).animate().fadeIn(delay: 470.ms).slideY(begin: 0.14),
|
||||||
|
const SizedBox(height: 28),
|
||||||
|
Text(
|
||||||
|
l10n.playersRange,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||||
|
color: TemaApp.colorTextoSecundario.withValues(alpha: 0.82),
|
||||||
|
letterSpacing: 0.8,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
);
|
||||||
),
|
},
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -252,56 +270,45 @@ class _PerfilInicioPremium extends StatelessWidget {
|
|||||||
|
|
||||||
class _HeroInicioPremium extends StatelessWidget {
|
class _HeroInicioPremium extends StatelessWidget {
|
||||||
final String subtitulo;
|
final String subtitulo;
|
||||||
|
final bool compact;
|
||||||
|
|
||||||
const _HeroInicioPremium({required this.subtitulo});
|
const _HeroInicioPremium({
|
||||||
|
required this.subtitulo,
|
||||||
|
required this.compact,
|
||||||
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final heroSize = compact ? 148.0 : 188.0;
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: 230,
|
height: compact ? 260 : 320,
|
||||||
child: Stack(
|
child: Stack(
|
||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Positioned.fill(
|
Positioned.fill(
|
||||||
child: IgnorePointer(child: CustomPaint(painter: _HeroInicioPainter())),
|
child: Image.asset(
|
||||||
|
'assets/ui/premium/lantern_radial_glow.png',
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
opacity: const AlwaysStoppedAnimation(0.48),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
Column(
|
Column(
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Image.asset(
|
||||||
width: 92,
|
'assets/ui/generated/main/main_lantern_hero.png',
|
||||||
height: 92,
|
width: heroSize,
|
||||||
decoration: BoxDecoration(
|
height: heroSize,
|
||||||
shape: BoxShape.circle,
|
fit: BoxFit.contain,
|
||||||
gradient: RadialGradient(
|
|
||||||
colors: [
|
|
||||||
TemaApp.colorDorado.withValues(alpha: 0.92),
|
|
||||||
TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
||||||
Colors.black.withValues(alpha: 0.78),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
border: Border.all(color: TemaApp.colorDorado, width: 3),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: TemaApp.colorNaranja.withValues(alpha: 0.65),
|
|
||||||
blurRadius: 42,
|
|
||||||
spreadRadius: 7,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: const Icon(
|
|
||||||
Icons.lightbulb_rounded,
|
|
||||||
color: Color(0xFF251304),
|
|
||||||
size: 54,
|
|
||||||
),
|
|
||||||
).animate(onPlay: (controller) => controller.repeat(reverse: true)).scale(
|
).animate(onPlay: (controller) => controller.repeat(reverse: true)).scale(
|
||||||
begin: const Offset(0.98, 0.98),
|
begin: const Offset(0.985, 0.985),
|
||||||
end: const Offset(1.04, 1.04),
|
end: const Offset(1.035, 1.035),
|
||||||
duration: 1400.ms,
|
duration: 1800.ms,
|
||||||
curve: Curves.easeInOut,
|
curve: Curves.easeInOut,
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
SizedBox(height: compact ? 8 : 12),
|
||||||
const LogoFarolero(size: 64),
|
LogoFarolero(size: compact ? 58 : 72),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
Text(
|
Text(
|
||||||
subtitulo,
|
subtitulo,
|
||||||
@@ -320,7 +327,7 @@ class _HeroInicioPremium extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
'Descubr� al impostor antes de que sea tarde',
|
'Descubrí al impostor antes de que sea tarde',
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||||
color: TemaApp.colorTextoSecundario,
|
color: TemaApp.colorTextoSecundario,
|
||||||
@@ -437,23 +444,44 @@ class _BotonInicioPremium extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: Row(
|
child: ClipRRect(
|
||||||
children: [
|
borderRadius: BorderRadius.circular(hero ? 26 : 22),
|
||||||
SizedBox(width: hero ? 70 : 62, child: Icon(icono, color: foreground, size: hero ? 38 : 27)),
|
child: Stack(
|
||||||
Expanded(
|
children: [
|
||||||
child: Text(
|
if (hero)
|
||||||
texto.toUpperCase(),
|
Positioned.fill(
|
||||||
textAlign: TextAlign.center,
|
child: Image.asset(
|
||||||
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
'assets/ui/generated/main/main_cta_frame.png',
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
SizedBox(
|
||||||
|
width: hero ? 70 : 62,
|
||||||
|
child: Icon(
|
||||||
|
icono,
|
||||||
color: foreground,
|
color: foreground,
|
||||||
fontSize: hero ? 28 : 18,
|
size: hero ? 38 : 27,
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
letterSpacing: hero ? 1.6 : 1.0,
|
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
child: Text(
|
||||||
|
texto.toUpperCase(),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: Theme.of(context).textTheme.titleLarge?.copyWith(
|
||||||
|
color: foreground,
|
||||||
|
fontSize: hero ? 28 : 18,
|
||||||
|
fontWeight: FontWeight.w900,
|
||||||
|
letterSpacing: hero ? 1.6 : 1.0,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
SizedBox(width: hero ? 70 : 62),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
SizedBox(width: hero ? 70 : 62),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -523,105 +551,3 @@ BoxDecoration _decoracionCristal({required double radius, required double alpha}
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
class _InicioFondoPainter extends CustomPainter {
|
|
||||||
const _InicioFondoPainter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final paint = Paint()..isAntiAlias = true;
|
|
||||||
paint.shader = const LinearGradient(
|
|
||||||
colors: [Color(0xFF050A12), Color(0xFF0A1524), Color(0xFF15091D)],
|
|
||||||
begin: Alignment.topCenter,
|
|
||||||
end: Alignment.bottomCenter,
|
|
||||||
).createShader(Offset.zero & size);
|
|
||||||
canvas.drawRect(Offset.zero & size, paint);
|
|
||||||
|
|
||||||
final farol = Offset(size.width * 0.5, size.height * 0.34);
|
|
||||||
paint.shader = RadialGradient(
|
|
||||||
colors: [
|
|
||||||
TemaApp.colorDorado.withValues(alpha: 0.28),
|
|
||||||
TemaApp.colorNaranja.withValues(alpha: 0.12),
|
|
||||||
Colors.transparent,
|
|
||||||
],
|
|
||||||
).createShader(Rect.fromCircle(center: farol, radius: size.width * 0.78));
|
|
||||||
canvas.drawCircle(farol, size.width * 0.78, paint);
|
|
||||||
paint.shader = null;
|
|
||||||
|
|
||||||
_drawSkyline(canvas, size, paint);
|
|
||||||
_drawSparks(canvas, size);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawSkyline(Canvas canvas, Size size, Paint paint) {
|
|
||||||
paint.color = Colors.black.withValues(alpha: 0.38);
|
|
||||||
final base = size.height * 0.80;
|
|
||||||
final path = Path()
|
|
||||||
..moveTo(0, size.height)
|
|
||||||
..lineTo(0, base - 20)
|
|
||||||
..lineTo(size.width * 0.14, base - 58)
|
|
||||||
..lineTo(size.width * 0.26, base - 24)
|
|
||||||
..lineTo(size.width * 0.42, base - 78)
|
|
||||||
..lineTo(size.width * 0.58, base - 34)
|
|
||||||
..lineTo(size.width * 0.74, base - 74)
|
|
||||||
..lineTo(size.width * 0.90, base - 28)
|
|
||||||
..lineTo(size.width, base - 48)
|
|
||||||
..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.5, base - 108), width: 24, height: 150),
|
|
||||||
const Radius.circular(12),
|
|
||||||
),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _drawSparks(Canvas canvas, Size size) {
|
|
||||||
final palette = [TemaApp.colorDorado, TemaApp.colorNaranja, const Color(0xFFFFF2C9)];
|
|
||||||
for (var i = 0; i < 64; i++) {
|
|
||||||
final x = (i * 67 % math.max(size.width, 1)).toDouble();
|
|
||||||
final y = (i * 113 % math.max(size.height, 1)).toDouble();
|
|
||||||
final paint = Paint()
|
|
||||||
..isAntiAlias = true
|
|
||||||
..color = palette[i % palette.length].withValues(alpha: 0.12 + (i % 4) * 0.06);
|
|
||||||
canvas.drawCircle(Offset(x, y), 1.2 + (i % 3), paint);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant _InicioFondoPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|
||||||
class _HeroInicioPainter extends CustomPainter {
|
|
||||||
const _HeroInicioPainter();
|
|
||||||
|
|
||||||
@override
|
|
||||||
void paint(Canvas canvas, Size size) {
|
|
||||||
final center = Offset(size.width / 2, size.height * 0.45);
|
|
||||||
final paint = Paint()..isAntiAlias = true;
|
|
||||||
paint.shader = RadialGradient(
|
|
||||||
colors: [TemaApp.colorNaranja.withValues(alpha: 0.30), 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 < 24; i++) {
|
|
||||||
final angle = math.pi * 2 * i / 24;
|
|
||||||
paint.color = (i.isEven ? TemaApp.colorDorado : TemaApp.colorNaranja).withValues(alpha: i.isEven ? 0.14 : 0.08);
|
|
||||||
canvas.drawPath(
|
|
||||||
Path()
|
|
||||||
..moveTo(center.dx + math.cos(angle - 0.035) * 45, center.dy + math.sin(angle - 0.035) * 45)
|
|
||||||
..lineTo(center.dx + math.cos(angle) * size.width * 0.44, center.dy + math.sin(angle) * size.width * 0.44)
|
|
||||||
..lineTo(center.dx + math.cos(angle + 0.035) * 45, center.dy + math.sin(angle + 0.035) * 45)
|
|
||||||
..close(),
|
|
||||||
paint,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
|
||||||
bool shouldRepaint(covariant _HeroInicioPainter oldDelegate) => false;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ class PantallaSeleccionModoJuego extends StatelessWidget {
|
|||||||
const _ModoHero().animate().fadeIn(duration: 320.ms).slideY(begin: -0.12),
|
const _ModoHero().animate().fadeIn(duration: 320.ms).slideY(begin: -0.12),
|
||||||
const SizedBox(height: 34),
|
const SizedBox(height: 34),
|
||||||
_ModoCard(
|
_ModoCard(
|
||||||
|
marcoAsset: 'assets/ui/generated/mode/mode_single_card_frame.png',
|
||||||
icono: Icons.phone_android_rounded,
|
icono: Icons.phone_android_rounded,
|
||||||
titulo: 'Un móvil',
|
titulo: 'Un móvil',
|
||||||
subtitulo: 'Partida en este dispositivo',
|
subtitulo: 'Partida en este dispositivo',
|
||||||
@@ -44,6 +45,7 @@ class PantallaSeleccionModoJuego extends StatelessWidget {
|
|||||||
).animate().fadeIn(delay: 120.ms).slideX(begin: -0.08),
|
).animate().fadeIn(delay: 120.ms).slideX(begin: -0.08),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
_ModoCard(
|
_ModoCard(
|
||||||
|
marcoAsset: 'assets/ui/generated/mode/mode_multi_card_frame.png',
|
||||||
icono: Icons.devices_rounded,
|
icono: Icons.devices_rounded,
|
||||||
titulo: 'Multidispositivo',
|
titulo: 'Multidispositivo',
|
||||||
subtitulo: 'Cada jugador en su móvil',
|
subtitulo: 'Cada jugador en su móvil',
|
||||||
@@ -75,74 +77,45 @@ class _ModoHero extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return SizedBox(
|
return Column(
|
||||||
height: 230,
|
children: [
|
||||||
child: Stack(
|
SizedBox(
|
||||||
alignment: Alignment.center,
|
height: 230,
|
||||||
children: [
|
child: Image.asset(
|
||||||
Positioned.fill(
|
'assets/ui/generated/mode/mode_duel_hero.png',
|
||||||
child: Image.asset(
|
fit: BoxFit.contain,
|
||||||
'assets/ui/premium/lantern_radial_glow.png',
|
opacity: const AlwaysStoppedAnimation(0.95),
|
||||||
fit: BoxFit.contain,
|
|
||||||
opacity: const AlwaysStoppedAnimation(0.58),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Column(
|
),
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
const SizedBox(height: 10),
|
||||||
children: [
|
Text(
|
||||||
Container(
|
'?C?mo quer?s jugar?',
|
||||||
width: 90,
|
textAlign: TextAlign.center,
|
||||||
height: 90,
|
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||||
decoration: BoxDecoration(
|
color: TemaApp.colorDorado,
|
||||||
shape: BoxShape.circle,
|
fontSize: 32,
|
||||||
gradient: RadialGradient(
|
fontWeight: FontWeight.w900,
|
||||||
colors: [
|
shadows: [
|
||||||
TemaApp.colorDorado.withValues(alpha: 0.95),
|
Shadow(color: TemaApp.colorNaranja.withValues(alpha: 0.45), blurRadius: 16),
|
||||||
TemaApp.colorNaranja.withValues(alpha: 0.58),
|
],
|
||||||
Colors.black.withValues(alpha: 0.76),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
border: Border.all(color: TemaApp.colorDorado, width: 3),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: TemaApp.colorNaranja.withValues(alpha: 0.55),
|
|
||||||
blurRadius: 42,
|
|
||||||
spreadRadius: 5,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: const Icon(Icons.sports_esports_rounded, size: 48, color: Color(0xFF241103)),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 18),
|
),
|
||||||
Text(
|
const SizedBox(height: 8),
|
||||||
'¿Cómo querés jugar?',
|
Text(
|
||||||
textAlign: TextAlign.center,
|
'Eleg? el tipo de partida y arranc? sin fricci?n.',
|
||||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
textAlign: TextAlign.center,
|
||||||
color: TemaApp.colorDorado,
|
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
||||||
fontSize: 32,
|
color: TemaApp.colorTextoSecundario,
|
||||||
fontWeight: FontWeight.w900,
|
|
||||||
shadows: [
|
|
||||||
Shadow(color: TemaApp.colorNaranja.withValues(alpha: 0.45), blurRadius: 16),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
),
|
||||||
Text(
|
],
|
||||||
'Elegí el tipo de partida y arrancá sin fricción.',
|
|
||||||
textAlign: TextAlign.center,
|
|
||||||
style: Theme.of(context).textTheme.titleMedium?.copyWith(
|
|
||||||
color: TemaApp.colorTextoSecundario,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class _ModoCard extends StatelessWidget {
|
class _ModoCard extends StatelessWidget {
|
||||||
|
final String marcoAsset;
|
||||||
final IconData icono;
|
final IconData icono;
|
||||||
final String titulo;
|
final String titulo;
|
||||||
final String subtitulo;
|
final String subtitulo;
|
||||||
@@ -151,6 +124,7 @@ class _ModoCard extends StatelessWidget {
|
|||||||
final VoidCallback onTap;
|
final VoidCallback onTap;
|
||||||
|
|
||||||
const _ModoCard({
|
const _ModoCard({
|
||||||
|
required this.marcoAsset,
|
||||||
required this.icono,
|
required this.icono,
|
||||||
required this.titulo,
|
required this.titulo,
|
||||||
required this.subtitulo,
|
required this.subtitulo,
|
||||||
@@ -189,6 +163,16 @@ class _ModoCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
|
Positioned.fill(
|
||||||
|
child: ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(28),
|
||||||
|
child: Image.asset(
|
||||||
|
marcoAsset,
|
||||||
|
fit: BoxFit.fill,
|
||||||
|
opacity: const AlwaysStoppedAnimation(0.86),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Image.asset(
|
child: Image.asset(
|
||||||
'assets/ui/premium/card_sheen_overlay.png',
|
'assets/ui/premium/card_sheen_overlay.png',
|
||||||
|
|||||||
@@ -39,3 +39,4 @@ flutter:
|
|||||||
- assets/medals/
|
- assets/medals/
|
||||||
- assets/rewards/
|
- assets/rewards/
|
||||||
- assets/ui/premium/
|
- assets/ui/premium/
|
||||||
|
- assets/ui/generated/
|
||||||
|
|||||||
@@ -1,53 +1,83 @@
|
|||||||
---
|
---
|
||||||
name: premium-game-ui
|
name: premium-game-ui
|
||||||
description: >
|
description: >
|
||||||
Create professional, layered, high-impact game screens in Flutter without flattening the UI into a single image.
|
Create professional, layered, high-impact game screens in Flutter using AI-generated, high-quality transparent art assets 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.
|
Trigger: when designing, generating, reviewing, or implementing premium game UI screens, reward screens, medal screens, avatar screens, overlays, transparent assets, visual effects, cinematic game feedback, or mockup-to-Flutter work.
|
||||||
license: Apache-2.0
|
license: Apache-2.0
|
||||||
metadata:
|
metadata:
|
||||||
author: gentleman-programming
|
author: gentleman-programming
|
||||||
version: "1.0"
|
version: "1.1"
|
||||||
---
|
---
|
||||||
|
|
||||||
## When to Use
|
## When to Use
|
||||||
|
|
||||||
- A screen must look premium, cinematic, or game-like.
|
- A screen must look premium, cinematic, addictive, or game-like.
|
||||||
- Assets are being generated for Flutter UI.
|
- 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.
|
- 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.
|
- A prototype/mockup must be converted into real app UI.
|
||||||
|
- A screen currently looks like plain Flutter plus gradients/glows and needs real art direction.
|
||||||
|
|
||||||
## Critical Patterns
|
## Critical Patterns
|
||||||
|
|
||||||
1. **Never flatten a real screen into one PNG.** Use real Flutter layout for text, buttons, state, progress, localization, and accessibility.
|
1. **Never flatten a real screen into one PNG.** Use real Flutter layout for text, buttons, state, progress, localization, accessibility, and responsive behavior.
|
||||||
2. **Generate a layered asset kit.** Separate opaque backgrounds from transparent overlays.
|
2. **Do not fake premium art with simple procedural glows.** If the target is cinematic/professional, generate real high-quality raster art assets with the image generation system and integrate them. CustomPainter, gradients, and stock Material icons are support layers, not hero art.
|
||||||
3. **Transparent means technically transparent.** For overlays, image corners should usually be `alpha=0`; reject white/black baked backgrounds.
|
3. **Generate a layered asset kit before coding.** Separate opaque backgrounds from transparent overlays, hero illustrations, frames, badges, buttons, bursts, particles, smoke, and decorative props.
|
||||||
4. **Right size, not maximum size.** Icons/avatars usually 128–256 px. Hero bursts 512–1024 px. Full backgrounds can be larger.
|
4. **Transparent means technically transparent.** Every overlay/hero/frame/prop that sits above Flutter UI must be PNG/WebP with alpha; image corners should usually be `alpha=0`. Reject white/black baked backgrounds.
|
||||||
5. **Use animation by responsibility.**
|
5. **High-quality generated assets are mandatory for premium screens.** For each premium screen or lote, create or reuse at least one production-grade generated art asset unless the screen is intentionally text-only.
|
||||||
|
6. **Right size, not maximum size.** Icons/avatars/medals usually 128-256 px. Hero/foreground illustrations 512-1024 px. Full mobile backgrounds can be 1080x1920 or equivalent.
|
||||||
|
7. **Use animation by responsibility.**
|
||||||
- `flutter_animate`: widget entrances, shimmer, scale, blur, staggered UI.
|
- `flutter_animate`: widget entrances, shimmer, scale, blur, staggered UI.
|
||||||
- `confetti`: short celebrations; keep particles controlled.
|
- `confetti`: short celebrations; keep particles controlled.
|
||||||
- `Rive`: interactive/stateful vector animation.
|
- `Rive`: interactive/stateful vector animation.
|
||||||
- `Lottie`: After Effects JSON animations.
|
- `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.
|
8. **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.
|
9. **Plan before generating.** First write the layer list: filename, role, size, alpha requirement, anchor, and Flutter usage.
|
||||||
|
10. **No placeholders in final premium work.** Generic icons, basic circles, simple radial glows, plain gradients, and procedural sparkles are acceptable only as temporary scaffolding. Replace them with generated art assets before claiming the screen is done.
|
||||||
|
|
||||||
|
## Mandatory Image Generation Rule
|
||||||
|
|
||||||
|
For premium/cinematic screens, the implementation MUST use generated bitmap assets where visual impact matters.
|
||||||
|
|
||||||
|
| Need | Required output |
|
||||||
|
| --- | --- |
|
||||||
|
| Full-screen atmosphere | Generated opaque background, usually 1080x1920 or 1440x2560. |
|
||||||
|
| Hero symbol / mascot / emblem | Generated PNG/WebP with transparent background, usually 512-1024 px. |
|
||||||
|
| Card/button frame art | Generated PNG/WebP with transparent background and alpha corners. |
|
||||||
|
| Reward burst / glow / confetti / smoke | Generated PNG/WebP with alpha, corners transparent. |
|
||||||
|
| Small icon/medal/avatar | Generated or hand-authored asset at 128-256 px, transparent background. |
|
||||||
|
|
||||||
|
If an asset needs transparency and the generator cannot emit native alpha, generate it on a flat chroma-key background, remove the key locally, and validate alpha before integration.
|
||||||
|
|
||||||
## Asset Acceptance Checklist
|
## Asset Acceptance Checklist
|
||||||
|
|
||||||
- [ ] Correct role: background, overlay, hero element, icon, or animation.
|
- [ ] Correct role: background, overlay, hero element, icon, frame, badge, prop, or animation.
|
||||||
- [ ] Correct format: PNG/WebP alpha for pictorial overlays, SVG for simple vector, Rive/Lottie for motion.
|
- [ ] 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.
|
- [ ] Correct alpha: overlay corners transparent unless intentionally full-canvas.
|
||||||
|
- [ ] Generated art quality: looks like polished game art, not a procedural placeholder.
|
||||||
|
- [ ] Project-bound generated images are copied into `assets/ui/...` or another registered asset directory.
|
||||||
- [ ] Centered visible content; no neighboring sprite fragments.
|
- [ ] Centered visible content; no neighboring sprite fragments.
|
||||||
- [ ] No unnecessary 2048+ px icons.
|
- [ ] No unnecessary 2048+ px icons.
|
||||||
- [ ] Registered in `pubspec.yaml` only at the needed directory level.
|
- [ ] Registered in `pubspec.yaml` only at the needed directory level.
|
||||||
- [ ] Integrated through `Stack`/widgets, not as a full-screen screenshot.
|
- [ ] Integrated through `Stack`/widgets, not as a full-screen screenshot.
|
||||||
|
- [ ] Compared against the mockup/prototype; if the result still looks like plain Flutter with glows, it is not done.
|
||||||
|
|
||||||
|
## Screen Workflow
|
||||||
|
|
||||||
|
1. Define the screen as layers: background, atmosphere, hero, panels, controls, motion.
|
||||||
|
2. Generate the required high-quality image assets first.
|
||||||
|
3. Remove/challenge any opaque background on overlays; validate transparent corners.
|
||||||
|
4. Integrate assets with real Flutter widgets in `Stack`.
|
||||||
|
5. Compare against the prototype and iterate asset-by-asset.
|
||||||
|
6. Only then update the screen code and verification notes.
|
||||||
|
|
||||||
## Flutter Integration Pattern
|
## Flutter Integration Pattern
|
||||||
|
|
||||||
```dart
|
```dart
|
||||||
Stack(
|
Stack(
|
||||||
children: [
|
children: [
|
||||||
const PremiumBackground(), // opaque base
|
const PremiumBackground(), // opaque generated or painted base
|
||||||
Positioned.fill(child: Image.asset('assets/fx/vignette.png', fit: BoxFit.cover)),
|
Positioned.fill(child: Image.asset('assets/ui/premium/vignette.png', fit: BoxFit.cover)),
|
||||||
Positioned(top: 80, left: 0, right: 0, child: RewardHero()),
|
Positioned(top: 80, left: 0, right: 0, child: RewardHero()), // generated transparent hero asset + Flutter text/state
|
||||||
Positioned.fill(child: IgnorePointer(child: ConfettiWidget(confettiController: controller))),
|
Positioned.fill(child: IgnorePointer(child: ConfettiWidget(confettiController: controller))),
|
||||||
SafeArea(child: RealScreenContent()), // text/buttons/progress remain widgets
|
SafeArea(child: RealScreenContent()), // text/buttons/progress remain widgets
|
||||||
],
|
],
|
||||||
|
|||||||