feat: glossy tile rendering and per-season theme colors
Introduces candy-gloss tile rendering (diagonal gradient + glass highlight + optional glow) via a shared paintGlossyTile() in tile_painter.dart, applied to board filled-cells and tray/drag-overlay pieces. Adds ThemeColors to palette.dart for UI-layer season color resolution, and activeThemeProvider for one-call access to the active season's theme. Regenerates the game_screen golden to reflect the new look.
This commit is contained in:
@@ -45,3 +45,10 @@ final seasonsProvider = FutureProvider<List<SeasonPack>>(
|
|||||||
final streakProvider = NotifierProvider<StreakNotifier, StreakState>(
|
final streakProvider = NotifierProvider<StreakNotifier, StreakState>(
|
||||||
StreakNotifier.new,
|
StreakNotifier.new,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/// The visual theme of whatever season is in play; fallback outside seasons
|
||||||
|
/// (home, endless). Pure model — UI converts via ThemeColors.
|
||||||
|
final activeThemeProvider = Provider<SeasonTheme>((ref) {
|
||||||
|
final flow = ref.watch(seasonFlowProvider);
|
||||||
|
return flow?.pack.theme ?? SeasonTheme.fallback;
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
import '../../game/models/season.dart';
|
||||||
|
|
||||||
/// Season-themeable color set. Season 1 default: vivid candy tones on a
|
/// Season-themeable color set. Season 1 default: vivid candy tones on a
|
||||||
/// deep navy board.
|
/// deep navy board.
|
||||||
class GamePalette {
|
class GamePalette {
|
||||||
@@ -24,3 +26,26 @@ class GamePalette {
|
|||||||
static const ghostLegal = Color(0x66FFFFFF);
|
static const ghostLegal = Color(0x66FFFFFF);
|
||||||
static const ghostIllegal = Color(0x55FF5252);
|
static const ghostIllegal = Color(0x55FF5252);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolved per-season colors for the UI layer. Built from a SeasonTheme;
|
||||||
|
/// falls back to the GamePalette constants.
|
||||||
|
class ThemeColors {
|
||||||
|
ThemeColors(SeasonTheme theme)
|
||||||
|
: gradient = [for (final c in theme.backgroundGradient) Color(c)],
|
||||||
|
accent = Color(theme.accentColor),
|
||||||
|
particleType = theme.particleType,
|
||||||
|
board = theme.boardTint != null
|
||||||
|
? Color(theme.boardTint!)
|
||||||
|
: GamePalette.boardBackground,
|
||||||
|
tiles = theme.tilePalette != null
|
||||||
|
? [for (final c in theme.tilePalette!) Color(c)]
|
||||||
|
: GamePalette.tileColors;
|
||||||
|
|
||||||
|
final List<Color> gradient;
|
||||||
|
final Color accent;
|
||||||
|
final String particleType;
|
||||||
|
final Color board;
|
||||||
|
final List<Color> tiles;
|
||||||
|
|
||||||
|
Color tile(int colorId) => tiles[colorId % tiles.length];
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import '../../game/models/piece.dart';
|
|||||||
import '../theme/palette.dart';
|
import '../theme/palette.dart';
|
||||||
import 'board_geometry.dart';
|
import 'board_geometry.dart';
|
||||||
import 'piece_painter.dart';
|
import 'piece_painter.dart';
|
||||||
|
import 'tile_painter.dart';
|
||||||
|
|
||||||
/// Drag ghost preview: a piece hovering at a snapped anchor.
|
/// Drag ghost preview: a piece hovering at a snapped anchor.
|
||||||
class GhostSpec {
|
class GhostSpec {
|
||||||
@@ -55,27 +56,20 @@ class BoardPainter extends CustomPainter {
|
|||||||
for (var x = 0; x < GridState.size; x++) {
|
for (var x = 0; x < GridState.size; x++) {
|
||||||
final rect = geo.cellRect(x, y).deflate(inset);
|
final rect = geo.cellRect(x, y).deflate(inset);
|
||||||
final cell = grid.cellAt(x, y);
|
final cell = grid.cellAt(x, y);
|
||||||
final paint = Paint()
|
switch (cell.type) {
|
||||||
..color = switch (cell.type) {
|
case CellType.empty:
|
||||||
CellType.empty => GamePalette.emptyCell,
|
canvas.drawRRect(
|
||||||
CellType.filled => GamePalette.tile(cell.colorId),
|
RRect.fromRectAndRadius(rect, radius),
|
||||||
CellType.gem => GamePalette.emptyCell,
|
Paint()..color = GamePalette.emptyCell,
|
||||||
};
|
);
|
||||||
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
|
case CellType.filled:
|
||||||
|
paintGlossyTile(canvas, rect, GamePalette.tile(cell.colorId));
|
||||||
if (cell.type == CellType.gem) {
|
case CellType.gem:
|
||||||
_paintGem(canvas, rect);
|
canvas.drawRRect(
|
||||||
} else if (cell.type == CellType.filled) {
|
RRect.fromRectAndRadius(rect, radius),
|
||||||
final highlight = Paint()
|
Paint()..color = GamePalette.emptyCell,
|
||||||
..color = Colors.white.withValues(alpha: 0.15);
|
);
|
||||||
canvas.drawRRect(
|
_paintGem(canvas, rect);
|
||||||
RRect.fromRectAndRadius(
|
|
||||||
Rect.fromLTWH(
|
|
||||||
rect.left, rect.top, rect.width, rect.height * 0.32),
|
|
||||||
radius,
|
|
||||||
),
|
|
||||||
highlight,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -111,6 +105,10 @@ class BoardPainter extends CustomPainter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _paintGem(Canvas canvas, Rect rect) {
|
void _paintGem(Canvas canvas, Rect rect) {
|
||||||
|
final glowPaint = Paint()
|
||||||
|
..color = GamePalette.gem.withValues(alpha: 0.45)
|
||||||
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, rect.width * 0.25);
|
||||||
|
canvas.drawCircle(rect.center, rect.width * 0.32, glowPaint);
|
||||||
final center = rect.center;
|
final center = rect.center;
|
||||||
final r = rect.width * 0.32;
|
final r = rect.width * 0.32;
|
||||||
final path = Path()
|
final path = Path()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import 'package:flutter/material.dart';
|
|||||||
|
|
||||||
import '../../game/models/piece.dart';
|
import '../../game/models/piece.dart';
|
||||||
import '../theme/palette.dart';
|
import '../theme/palette.dart';
|
||||||
|
import 'tile_painter.dart';
|
||||||
|
|
||||||
/// Draws a piece as rounded tiles at a given cell size; reused by the tray,
|
/// Draws a piece as rounded tiles at a given cell size; reused by the tray,
|
||||||
/// the drag overlay, and ghost previews.
|
/// the drag overlay, and ghost previews.
|
||||||
@@ -12,8 +13,6 @@ void paintPiece(
|
|||||||
Offset origin = Offset.zero,
|
Offset origin = Offset.zero,
|
||||||
Color? overrideColor,
|
Color? overrideColor,
|
||||||
}) {
|
}) {
|
||||||
final paint = Paint()
|
|
||||||
..color = overrideColor ?? GamePalette.tile(piece.colorId);
|
|
||||||
final inset = cellSize * 0.05;
|
final inset = cellSize * 0.05;
|
||||||
final radius = Radius.circular(cellSize * 0.18);
|
final radius = Radius.circular(cellSize * 0.18);
|
||||||
for (final (dx, dy) in piece.offsets) {
|
for (final (dx, dy) in piece.offsets) {
|
||||||
@@ -23,17 +22,13 @@ void paintPiece(
|
|||||||
cellSize - inset * 2,
|
cellSize - inset * 2,
|
||||||
cellSize - inset * 2,
|
cellSize - inset * 2,
|
||||||
);
|
);
|
||||||
canvas.drawRRect(RRect.fromRectAndRadius(rect, radius), paint);
|
if (overrideColor != null) {
|
||||||
if (overrideColor == null) {
|
|
||||||
// Subtle top highlight for depth.
|
|
||||||
final highlight = Paint()..color = Colors.white.withValues(alpha: 0.18);
|
|
||||||
canvas.drawRRect(
|
canvas.drawRRect(
|
||||||
RRect.fromRectAndRadius(
|
RRect.fromRectAndRadius(rect, radius),
|
||||||
Rect.fromLTWH(rect.left, rect.top, rect.width, rect.height * 0.32),
|
Paint()..color = overrideColor,
|
||||||
radius,
|
|
||||||
),
|
|
||||||
highlight,
|
|
||||||
);
|
);
|
||||||
|
} else {
|
||||||
|
paintGlossyTile(canvas, rect, GamePalette.tile(piece.colorId));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
Color lighten(Color c, double amount) => Color.lerp(c, Colors.white, amount)!;
|
||||||
|
Color darken(Color c, double amount) => Color.lerp(c, Colors.black, amount)!;
|
||||||
|
|
||||||
|
/// Candy-gloss tile: diagonal gradient body, glass top highlight, optional
|
||||||
|
/// colored glow (used for gems and clear flashes). Shared by the board,
|
||||||
|
/// tray, and drag overlay so every tile in the game matches.
|
||||||
|
void paintGlossyTile(
|
||||||
|
Canvas canvas,
|
||||||
|
Rect rect,
|
||||||
|
Color color, {
|
||||||
|
double radiusFactor = 0.18,
|
||||||
|
double glow = 0,
|
||||||
|
}) {
|
||||||
|
final radius = Radius.circular(rect.width * radiusFactor);
|
||||||
|
final rrect = RRect.fromRectAndRadius(rect, radius);
|
||||||
|
|
||||||
|
if (glow > 0) {
|
||||||
|
final glowPaint = Paint()
|
||||||
|
..color = color.withValues(alpha: 0.55 * glow)
|
||||||
|
..maskFilter =
|
||||||
|
MaskFilter.blur(BlurStyle.normal, rect.width * 0.28 * glow);
|
||||||
|
canvas.drawRRect(rrect.inflate(rect.width * 0.05), glowPaint);
|
||||||
|
}
|
||||||
|
|
||||||
|
final body = Paint()
|
||||||
|
..shader = LinearGradient(
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
colors: [lighten(color, 0.28), color, darken(color, 0.22)],
|
||||||
|
stops: const [0.0, 0.45, 1.0],
|
||||||
|
).createShader(rect);
|
||||||
|
canvas.drawRRect(rrect, body);
|
||||||
|
|
||||||
|
final highlight = Paint()..color = Colors.white.withValues(alpha: 0.30);
|
||||||
|
final hl = Rect.fromLTWH(
|
||||||
|
rect.left + rect.width * 0.10,
|
||||||
|
rect.top + rect.height * 0.07,
|
||||||
|
rect.width * 0.80,
|
||||||
|
rect.height * 0.30,
|
||||||
|
);
|
||||||
|
canvas.drawRRect(
|
||||||
|
RRect.fromRectAndRadius(hl, Radius.circular(rect.width * 0.12)),
|
||||||
|
highlight,
|
||||||
|
);
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 31 KiB |
Reference in New Issue
Block a user