diff --git a/assets/ui/generated/main/main_cta_frame.webp b/assets/ui/generated/main/main_cta_frame.webp deleted file mode 100644 index 0abdc84..0000000 Binary files a/assets/ui/generated/main/main_cta_frame.webp and /dev/null differ diff --git a/assets/ui/generated/mode/mode_multi_card_frame.webp b/assets/ui/generated/mode/mode_multi_card_frame.webp deleted file mode 100644 index 5634f04..0000000 Binary files a/assets/ui/generated/mode/mode_multi_card_frame.webp and /dev/null differ diff --git a/assets/ui/generated/mode/mode_single_card_frame.webp b/assets/ui/generated/mode/mode_single_card_frame.webp deleted file mode 100644 index 255b6dd..0000000 Binary files a/assets/ui/generated/mode/mode_single_card_frame.webp and /dev/null differ diff --git a/lib/pantallas/pantalla_principal.dart b/lib/pantallas/pantalla_principal.dart index ae79a51..43e475e 100644 --- a/lib/pantallas/pantalla_principal.dart +++ b/lib/pantallas/pantalla_principal.dart @@ -426,80 +426,59 @@ class _BotonInicioPremium extends StatelessWidget { @override Widget build(BuildContext context) { - final radius = BorderRadius.circular(hero ? 26 : 22); + final radius = BorderRadius.circular(hero ? 28 : 24); return Material( color: Colors.transparent, child: InkWell( borderRadius: radius, onTap: onPressed, - child: Ink( + child: SizedBox( height: height, - decoration: BoxDecoration( - gradient: gradient, - borderRadius: BorderRadius.circular(hero ? 26 : 22), - border: Border.all( - color: hero ? const Color(0xFFFFF0B8) : TemaApp.colorDorado.withValues(alpha: 0.34), + child: CustomPaint( + painter: MarcoBotonFaroleroPainter( + gradient: gradient, + destacado: hero, + habilitado: true, + radio: hero ? 28 : 24, ), - boxShadow: [ - BoxShadow( - color: (hero ? TemaApp.colorNaranja : Colors.black).withValues(alpha: hero ? 0.46 : 0.42), - blurRadius: hero ? 34 : 18, - offset: const Offset(0, 12), + child: Padding( + padding: EdgeInsets.symmetric( + horizontal: hero ? 24 : 20, + vertical: hero ? 14 : 11, ), - ], - ), - child: ClipRRect( - borderRadius: radius, - child: Stack( - alignment: Alignment.center, - children: [ - Positioned.fill( - child: Image.asset( - 'assets/ui/generated/main/main_cta_frame.webp', - fit: BoxFit.cover, - opacity: AlwaysStoppedAnimation(hero ? 0.20 : 0.14), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + SizedBox( + width: hero ? 50 : 42, + child: Icon( + icono, + color: foreground, + size: hero ? 38 : 29, + ), ), - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: hero ? 18 : 16, - vertical: hero ? 14 : 11, - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: hero ? 46 : 38, - child: Icon( - icono, - color: foreground, - size: hero ? 36 : 27, + const SizedBox(width: 10), + Expanded( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + texto.toUpperCase(), + maxLines: 1, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleLarge?.copyWith( + color: foreground, + fontSize: hero ? 30 : 19, + fontWeight: FontWeight.w900, + letterSpacing: hero ? 1.4 : 1.0, + ), ), ), - const SizedBox(width: 10), - Expanded( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - texto.toUpperCase(), - maxLines: 1, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleLarge?.copyWith( - color: foreground, - fontSize: hero ? 28 : 18, - fontWeight: FontWeight.w900, - letterSpacing: hero ? 1.4 : 1.0, - ), - ), - ), - ), - ), - SizedBox(width: hero ? 56 : 48), - ], + ), ), - ), - ], + SizedBox(width: hero ? 60 : 50), + ], + ), ), ), ), diff --git a/lib/pantallas/pantalla_seleccion_modo_juego.dart b/lib/pantallas/pantalla_seleccion_modo_juego.dart index ed910a6..078205b 100644 --- a/lib/pantallas/pantalla_seleccion_modo_juego.dart +++ b/lib/pantallas/pantalla_seleccion_modo_juego.dart @@ -33,7 +33,6 @@ class PantallaSeleccionModoJuego extends StatelessWidget { ).animate().fadeIn(duration: 320.ms).slideY(begin: -0.12), const SizedBox(height: 34), _ModoCard( - marcoAsset: 'assets/ui/generated/mode/mode_single_card_frame.webp', icono: Icons.phone_android_rounded, titulo: l10n.singleDevice, subtitulo: l10n.singleDeviceSubtitle, @@ -50,7 +49,6 @@ class PantallaSeleccionModoJuego extends StatelessWidget { ).animate().fadeIn(delay: 120.ms).slideX(begin: -0.08), const SizedBox(height: 16), _ModoCard( - marcoAsset: 'assets/ui/generated/mode/mode_multi_card_frame.webp', icono: Icons.devices_rounded, titulo: l10n.multiDevice, subtitulo: l10n.multiDeviceSubtitle, @@ -126,7 +124,6 @@ class _ModoHero extends StatelessWidget { } class _ModoCard extends StatelessWidget { - final String marcoAsset; final IconData icono; final String titulo; final String subtitulo; @@ -135,7 +132,6 @@ class _ModoCard extends StatelessWidget { final VoidCallback onTap; const _ModoCard({ - required this.marcoAsset, required this.icono, required this.titulo, required this.subtitulo, @@ -179,13 +175,6 @@ class _ModoCard extends StatelessWidget { child: Stack( alignment: Alignment.center, children: [ - Positioned.fill( - child: Image.asset( - marcoAsset, - fit: BoxFit.cover, - opacity: AlwaysStoppedAnimation(destacado ? 0.22 : 0.18), - ), - ), Padding( padding: const EdgeInsets.fromLTRB(18, 18, 14, 18), child: Row( diff --git a/lib/tema/componentes_farolero.dart b/lib/tema/componentes_farolero.dart index ee55bdd..adaa219 100644 --- a/lib/tema/componentes_farolero.dart +++ b/lib/tema/componentes_farolero.dart @@ -34,9 +34,6 @@ class FondoFarolero extends StatelessWidget { ), ), ), - Positioned.fill( - child: CustomPaint(painter: _FondoFaroleroPainter(intenso: intenso)), - ), Positioned.fill(child: child), ], ), @@ -239,6 +236,7 @@ class BotonFarolero extends StatelessWidget { final VoidCallback? onPressed; final LinearGradient gradient; final Color foreground; + final bool destacado; const BotonFarolero({ super.key, @@ -247,6 +245,7 @@ class BotonFarolero extends StatelessWidget { required this.onPressed, this.gradient = TemaApp.gradientePrimario, this.foreground = Colors.black, + this.destacado = true, }); const BotonFarolero.secundario({ @@ -259,7 +258,8 @@ class BotonFarolero extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - foreground = Colors.white; + foreground = Colors.white, + destacado = false; const BotonFarolero.oscuro({ super.key, @@ -271,7 +271,8 @@ class BotonFarolero extends StatelessWidget { begin: Alignment.topCenter, end: Alignment.bottomCenter, ), - foreground = TemaApp.colorTexto; + foreground = TemaApp.colorTexto, + destacado = false; @override Widget build(BuildContext context) { @@ -281,85 +282,49 @@ class BotonFarolero extends StatelessWidget { return Material( color: Colors.transparent, child: InkWell( - borderRadius: BorderRadius.circular(18), + borderRadius: BorderRadius.circular(24), onTap: onPressed, - child: Ink( - decoration: BoxDecoration( - gradient: habilitado - ? gradient - : const LinearGradient( - colors: [TemaApp.colorTarjeta, TemaApp.colorSuperficie], - ), - borderRadius: BorderRadius.circular(18), - border: Border.all( - color: habilitado - ? TemaApp.colorDorado.withValues(alpha: 0.74) - : TemaApp.colorBorde, + child: ConstrainedBox( + constraints: const BoxConstraints(minHeight: 66), + child: CustomPaint( + painter: MarcoBotonFaroleroPainter( + gradient: habilitado + ? gradient + : const LinearGradient( + colors: [TemaApp.colorTarjeta, TemaApp.colorSuperficie], + ), + destacado: destacado, + habilitado: habilitado, ), - boxShadow: [ - BoxShadow( - color: Colors.black.withValues(alpha: 0.34), - blurRadius: 18, - offset: const Offset(0, 10), - ), - if (habilitado) - BoxShadow( - color: TemaApp.colorNaranja.withValues(alpha: 0.16), - blurRadius: 22, - ), - ], - ), - child: ConstrainedBox( - constraints: const BoxConstraints(minHeight: 64), - child: ClipRRect( - borderRadius: BorderRadius.circular(18), - child: Stack( - alignment: Alignment.center, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 22, vertical: 15), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ - Positioned.fill( - child: IgnorePointer( - child: Image.asset( - 'assets/ui/generated/main/main_cta_frame.webp', - fit: BoxFit.fill, - opacity: AlwaysStoppedAnimation(habilitado ? 0.72 : 0.24), - filterQuality: FilterQuality.high, - errorBuilder: (context, error, stackTrace) => - const SizedBox.shrink(), + SizedBox( + width: 44, + child: Icon(icono, color: colorTexto, size: 30), + ), + const SizedBox(width: 10), + Expanded( + child: Center( + child: FittedBox( + fit: BoxFit.scaleDown, + child: Text( + texto.toUpperCase(), + maxLines: 1, + textAlign: TextAlign.center, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + color: colorTexto, + fontSize: 19, + fontWeight: FontWeight.w900, + letterSpacing: 0.9, + ), + ), ), ), ), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 14), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - width: 42, - child: Icon(icono, color: colorTexto, size: 28), - ), - const SizedBox(width: 8), - Expanded( - child: Center( - child: FittedBox( - fit: BoxFit.scaleDown, - child: Text( - texto.toUpperCase(), - maxLines: 1, - textAlign: TextAlign.center, - style: Theme.of(context).textTheme.titleMedium?.copyWith( - color: colorTexto, - fontSize: 18, - fontWeight: FontWeight.w900, - letterSpacing: 0.8, - ), - ), - ), - ), - ), - const SizedBox(width: 50), - ], - ), - ), + const SizedBox(width: 54), ], ), ), @@ -370,6 +335,150 @@ class BotonFarolero extends StatelessWidget { } } +class MarcoBotonFaroleroPainter extends CustomPainter { + final LinearGradient gradient; + final bool destacado; + final bool habilitado; + final double radio; + + const MarcoBotonFaroleroPainter({ + required this.gradient, + required this.destacado, + required this.habilitado, + this.radio = 24, + }); + + @override + void paint(Canvas canvas, Size size) { + final rect = Offset.zero & size; + final cuerpo = rect.deflate(3); + final rrect = RRect.fromRectAndRadius(cuerpo, Radius.circular(radio)); + final alpha = habilitado ? 1.0 : 0.42; + final oro = TemaApp.colorDorado.withValues(alpha: 0.95 * alpha); + final brillo = TemaApp.colorNaranja.withValues(alpha: 0.80 * alpha); + final sombra = Paint() + ..color = Colors.black.withValues(alpha: 0.38) + ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 12); + + canvas.drawRRect(rrect.shift(const Offset(0, 8)), sombra); + + final base = Paint()..shader = gradient.createShader(cuerpo); + canvas.drawRRect(rrect, base); + + final vignette = Paint() + ..shader = LinearGradient( + colors: [ + Colors.white.withValues(alpha: destacado ? 0.34 : 0.08), + Colors.transparent, + Colors.black.withValues(alpha: destacado ? 0.10 : 0.34), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(cuerpo); + canvas.drawRRect(rrect, vignette); + + final bordeExterior = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = destacado ? 2.4 : 1.8 + ..shader = LinearGradient( + colors: [oro, Colors.white.withValues(alpha: 0.72 * alpha), brillo, oro], + ).createShader(cuerpo); + canvas.drawRRect(rrect, bordeExterior); + + final bordeInterior = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.1 + ..color = Colors.white.withValues(alpha: destacado ? 0.38 * alpha : 0.16 * alpha); + canvas.drawRRect( + RRect.fromRectAndRadius(cuerpo.deflate(6), Radius.circular(math.max(8, radio - 7))), + bordeInterior, + ); + + final railPaint = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5 + ..color = oro.withValues(alpha: destacado ? 0.48 * alpha : 0.34 * alpha); + final yTop = cuerpo.top + size.height * 0.25; + final yBottom = cuerpo.bottom - size.height * 0.25; + canvas.drawLine(Offset(cuerpo.left + 54, yTop), Offset(cuerpo.right - 54, yTop), railPaint); + canvas.drawLine( + Offset(cuerpo.left + 54, yBottom), + Offset(cuerpo.right - 54, yBottom), + railPaint, + ); + + _drawSideOrnament(canvas, cuerpo, true, oro, brillo, alpha); + _drawSideOrnament(canvas, cuerpo, false, oro, brillo, alpha); + _drawCenterJewel(canvas, cuerpo.topCenter, 1, oro, brillo, alpha); + _drawCenterJewel(canvas, cuerpo.bottomCenter, -1, oro, brillo, alpha); + } + + void _drawSideOrnament( + Canvas canvas, + Rect rect, + bool left, + Color oro, + Color brillo, + double alpha, + ) { + final cx = left ? rect.left + 24 : rect.right - 24; + final cy = rect.center.dy; + final dir = left ? -1.0 : 1.0; + final path = Path() + ..moveTo(cx, cy - 18) + ..lineTo(cx + dir * 24, cy) + ..lineTo(cx, cy + 18) + ..lineTo(cx - dir * 7, cy) + ..close(); + final paint = Paint() + ..shader = LinearGradient( + colors: [oro, brillo, Colors.white.withValues(alpha: 0.70 * alpha)], + ).createShader(path.getBounds()); + canvas.drawPath(path, paint); + canvas.drawPath( + path, + Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1 + ..color = Colors.white.withValues(alpha: 0.42 * alpha), + ); + } + + void _drawCenterJewel( + Canvas canvas, + Offset anchor, + double direction, + Color oro, + Color brillo, + double alpha, + ) { + final cy = anchor.dy + direction * 3; + final path = Path() + ..moveTo(anchor.dx, cy - direction * 9) + ..lineTo(anchor.dx + 12, cy) + ..lineTo(anchor.dx, cy + direction * 9) + ..lineTo(anchor.dx - 12, cy) + ..close(); + canvas.drawPath( + path, + Paint() + ..shader = LinearGradient( + colors: [Colors.white.withValues(alpha: 0.78 * alpha), oro, brillo], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ).createShader(path.getBounds()), + ); + } + + @override + bool shouldRepaint(covariant MarcoBotonFaroleroPainter oldDelegate) { + return oldDelegate.gradient != gradient || + oldDelegate.destacado != destacado || + oldDelegate.habilitado != habilitado || + oldDelegate.radio != radio; + } +} + class ArteGameplayFarolero extends StatelessWidget { final String assetPath; final double height; @@ -716,123 +825,3 @@ class _FuegoAvatarPainter extends CustomPainter { return oldDelegate.fuego != fuego; } } - -class _FondoFaroleroPainter extends CustomPainter { - final bool intenso; - - const _FondoFaroleroPainter({required this.intenso}); - - @override - void paint(Canvas canvas, Size size) { - final paint = Paint()..isAntiAlias = true; - final alto = size.height; - final ancho = size.width; - - paint.color = const Color(0xFF152845).withValues(alpha: intenso ? 0.34 : 0.22); - canvas.drawCircle(Offset(ancho * 0.78, alto * 0.16), 18, paint); - - paint.color = const Color(0xFF07101A).withValues(alpha: 0.82); - final colinas = Path() - ..moveTo(0, alto * 0.34) - ..quadraticBezierTo(ancho * 0.28, alto * 0.21, ancho * 0.55, alto * 0.33) - ..quadraticBezierTo(ancho * 0.82, alto * 0.43, ancho, alto * 0.26) - ..lineTo(ancho, alto) - ..lineTo(0, alto) - ..close(); - canvas.drawPath(colinas, paint); - - _dibujarCasas(canvas, size, paint); - _dibujarFarol(canvas, size, paint); - - paint.shader = RadialGradient( - colors: [ - TemaApp.colorNaranja.withValues(alpha: intenso ? 0.26 : 0.16), - Colors.transparent, - ], - ).createShader(Rect.fromCircle(center: Offset(ancho * 0.52, alto * 0.36), radius: 160)); - canvas.drawCircle(Offset(ancho * 0.52, alto * 0.36), 160, paint); - paint.shader = null; - } - - void _dibujarCasas(Canvas canvas, Size size, Paint paint) { - final alto = size.height; - final ancho = size.width; - paint.color = const Color(0xFF020407).withValues(alpha: 0.72); - - for (var i = 0; i < 5; i++) { - final w = ancho * (0.16 + i * 0.018); - final h = alto * (0.18 + (i % 2) * 0.05); - final x = -30 + i * ancho * 0.24; - final y = alto * (0.72 - i * 0.02); - final casa = Rect.fromLTWH(x, y - h, w, h); - canvas.drawRect(casa, paint); - final tejado = Path() - ..moveTo(x - 8, y - h) - ..lineTo(x + w * 0.48, y - h - 38) - ..lineTo(x + w + 8, y - h) - ..close(); - canvas.drawPath(tejado, paint); - - final ventana = Paint() - ..color = TemaApp.colorNaranja.withValues(alpha: 0.38) - ..isAntiAlias = true; - for (var j = 0; j < 2; j++) { - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromLTWH(x + 18 + j * 34, y - h + 36, 12, 22), - const Radius.circular(2), - ), - ventana, - ); - } - } - } - - void _dibujarFarol(Canvas canvas, Size size, Paint paint) { - final alto = size.height; - final ancho = size.width; - final centro = Offset(ancho * 0.5, alto * 0.28); - final glow = Paint() - ..shader = RadialGradient( - colors: [ - TemaApp.colorNaranja.withValues(alpha: 0.44), - Colors.transparent, - ], - ).createShader(Rect.fromCircle(center: centro, radius: 92)); - canvas.drawCircle(centro, 92, glow); - - paint - ..shader = null - ..style = PaintingStyle.stroke - ..strokeWidth = 3 - ..color = const Color(0xFF050507).withValues(alpha: 0.82); - canvas.drawArc( - Rect.fromCircle(center: centro.translate(0, -16), radius: 35), - math.pi, - math.pi, - false, - paint, - ); - paint.style = PaintingStyle.fill; - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromCenter(center: centro, width: 38, height: 54), - const Radius.circular(5), - ), - paint, - ); - paint.color = TemaApp.colorNaranja.withValues(alpha: 0.82); - canvas.drawRRect( - RRect.fromRectAndRadius( - Rect.fromCenter(center: centro, width: 21, height: 34), - const Radius.circular(4), - ), - paint, - ); - } - - @override - bool shouldRepaint(covariant _FondoFaroleroPainter oldDelegate) { - return oldDelegate.intenso != intenso; - } -} diff --git a/skills/premium-game-ui/SKILL.md b/skills/premium-game-ui/SKILL.md index 84f0d4d..418cd34 100644 --- a/skills/premium-game-ui/SKILL.md +++ b/skills/premium-game-ui/SKILL.md @@ -6,7 +6,7 @@ description: > license: Apache-2.0 metadata: author: gentleman-programming - version: "1.3" + version: "1.4" --- ## When to Use @@ -41,6 +41,8 @@ metadata: 16. **Do not bundle chroma-source images.** Chroma-key sources are temporary production files. Only the final transparent PNG/WebP and opaque backgrounds belong in registered Flutter asset folders; otherwise green-screen images can be shipped accidentally. 17. **Use shared decorative art widgets for repeated phases.** Debate, word reveal, voting, host-management, notes, rules, and history screens should reuse generated gameplay art through shared components so layouts stay consistent and text/buttons remain real Flutter widgets. 18. **Analyzer cleanliness is mandatory.** When editing Dart, do not introduce unused getters/fields or analyzer-only style issues. Avoid `(_, __)`/`(_, __, ___)` callback parameters because `unnecessary_underscores` is enabled; name unused callback parameters descriptively (`context`, `index`, `error`, `stackTrace`) or use only one `_` where valid. +19. **Premium buttons must be scalable by construction.** Do not place a decorative button PNG over a Flutter button that already has its own shape/background. Either use a true scalable technique (9-slice/centerSlice with protected corners/ornaments) or draw the frame with Flutter `CustomPainter`. Text, icons, hit state, disabled state, and dynamic width must remain real widgets and must not distort the artwork. +20. **Generated backgrounds should not be polluted by procedural placeholders.** Once a screen has a real generated atmosphere background, remove temporary `CustomPainter` silhouettes, confetti rectangles, generic circles, or debug-looking shape layers unless they are intentional premium effects. ## Mandatory Image Generation Rule @@ -84,7 +86,7 @@ If an asset needs transparency and the generator cannot emit native alpha, gener Stack( children: [ const PremiumBackground(), // opaque generated or painted base - Positioned.fill(child: Image.asset('assets/ui/premium/vignette.png', fit: BoxFit.cover)), + Positioned.fill(child: Image.asset('assets/ui/generated/shared/screen_atmosphere_bg.webp', fit: BoxFit.cover)), Positioned(top: 80, left: 0, right: 0, child: RewardHero()), // generated transparent hero asset + Flutter text/state Positioned.fill(child: IgnorePointer(child: ConfettiWidget(confettiController: controller))), SafeArea(child: RealScreenContent()), // text/buttons/progress remain widgets