diff --git a/assets/icon/icon.png b/assets/icon/icon.png new file mode 100644 index 0000000..9516762 Binary files /dev/null and b/assets/icon/icon.png differ diff --git a/assets/icon/icon_background.png b/assets/icon/icon_background.png new file mode 100644 index 0000000..cdddaae Binary files /dev/null and b/assets/icon/icon_background.png differ diff --git a/assets/icon/icon_foreground.png b/assets/icon/icon_foreground.png new file mode 100644 index 0000000..7b192e0 Binary files /dev/null and b/assets/icon/icon_foreground.png differ diff --git a/lib/ui/branding/app_icon_painter.dart b/lib/ui/branding/app_icon_painter.dart new file mode 100644 index 0000000..b7a6941 --- /dev/null +++ b/lib/ui/branding/app_icon_painter.dart @@ -0,0 +1,45 @@ +// lib/ui/branding/app_icon_painter.dart +import 'dart:ui'; + +import 'package:flutter/material.dart'; + +import '../widgets/tile_painter.dart'; + +/// Draws the Block Seasons brand mark: deep-navy field + a 2×2 grid of glossy +/// brand-color blocks. Shared by the launcher-icon and feature-graphic +/// generators so the brand stays identical everywhere. +class AppIconMark { + static const navy = [Color(0xFF101736), Color(0xFF192555), Color(0xFF2C3168)]; + static const pink = Color(0xFFFF7EB3); + static const yellow = Color(0xFFFFD166); + static const cyan = Color(0xFF6FCDF5); + static const green = Color(0xFF7EDB9C); + + /// Fills [rect] with the navy gradient. + static void paintBackground(Canvas canvas, Rect rect) { + final paint = Paint() + ..shader = const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: navy, + ).createShader(rect); + canvas.drawRect(rect, paint); + } + + /// Paints the 2×2 glossy blocks centered in a square of side [size], the + /// block group occupying [groupFraction] of the side. + static void paintBlocks(Canvas canvas, double size, + {double groupFraction = 0.6}) { + final group = size * groupFraction; + final gap = size * 0.05; + final block = (group - gap) / 2; + final m = (size - group) / 2; + final far = m + block + gap; + void tile(double x, double y, Color c) => paintGlossyTile( + canvas, Rect.fromLTWH(x, y, block, block), c, radiusFactor: 0.24); + tile(m, m, pink); + tile(far, m, yellow); + tile(m, far, cyan); + tile(far, far, green); + } +} diff --git a/test/tool/generate_brand_assets_test.dart b/test/tool/generate_brand_assets_test.dart new file mode 100644 index 0000000..4ef9a89 --- /dev/null +++ b/test/tool/generate_brand_assets_test.dart @@ -0,0 +1,44 @@ +// test/tool/generate_brand_assets_test.dart +import 'dart:io'; +import 'dart:ui'; + +import 'package:block_seasons/ui/branding/app_icon_painter.dart'; +import 'package:flutter_test/flutter_test.dart'; + +Future _writePng(String path, int size, void Function(Canvas) draw) async { + final recorder = PictureRecorder(); + final canvas = Canvas(recorder); + draw(canvas); + final picture = recorder.endRecording(); + final image = await picture.toImage(size, size); + final bytes = await image.toByteData(format: ImageByteFormat.png); + File(path).parent.createSync(recursive: true); + File(path).writeAsBytesSync(bytes!.buffer.asUint8List()); +} + +void main() { + testWidgets('generate launcher icon PNGs', (tester) async { + const s = 1024; + final full = Rect.fromLTWH(0, 0, s.toDouble(), s.toDouble()); + + await tester.runAsync(() async { + // Master (iOS + fallback): opaque navy + blocks at 60%. + await _writePng('assets/icon/icon.png', s, (c) { + AppIconMark.paintBackground(c, full); + AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.6); + }); + // Adaptive background: navy only. + await _writePng('assets/icon/icon_background.png', s, (c) { + AppIconMark.paintBackground(c, full); + }); + // Adaptive foreground: blocks only (transparent), 52% for the safe zone. + await _writePng('assets/icon/icon_foreground.png', s, (c) { + AppIconMark.paintBlocks(c, s.toDouble(), groupFraction: 0.52); + }); + }); + + for (final f in ['icon.png', 'icon_background.png', 'icon_foreground.png']) { + expect(File('assets/icon/$f').existsSync(), isTrue, reason: f); + } + }); +}