// Renders the Peach app icon to assets/icon/ using Flutter's software // rasterizer (full anti-aliasing, no external image tools needed). // // flutter test test/tools/generate_icon_test.dart // // Then apply everywhere with: dart run flutter_launcher_icons import 'dart:io'; import 'dart:typed_data'; import 'dart:ui' as ui; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; const _size = 1024.0; // Brand gradient (matches PeachTheme.brandGradient / the splash tile). const _g1 = Color(0xFFFF6A3D); const _g2 = Color(0xFFE23C16); void main() { TestWidgetsFlutterBinding.ensureInitialized(); test('generate app icon', () async { // Full-bleed source (square gradient, no alpha) — iOS-safe, universal. await _write('assets/icon/app_icon.png', await _render(_Mode.bleed)); // Rounded-tile variant with transparent margin (macOS / marketing). await _write('assets/icon/app_icon_tile.png', await _render(_Mode.tile)); // Android adaptive-icon foreground: rocket only, padded into the safe zone. await _write('assets/icon/app_icon_fg.png', await _render(_Mode.foreground)); }); } enum _Mode { bleed, tile, foreground } Future _write(String path, Uint8List bytes) async { final f = File(path); await f.create(recursive: true); await f.writeAsBytes(bytes); // ignore: avoid_print print('wrote $path (${bytes.length} bytes)'); } Future _render(_Mode mode) async { final recorder = ui.PictureRecorder(); final canvas = Canvas(recorder); const rect = Rect.fromLTWH(0, 0, _size, _size); // ---- background tile ---- final bg = Paint() ..shader = ui.Gradient.linear( const Offset(0, 0), const Offset(_size, _size), [_g1, _g2], ); switch (mode) { case _Mode.bleed: canvas.drawRect(rect, bg); _sheen(canvas); break; case _Mode.tile: const margin = 96.0; final tileR = RRect.fromRectAndRadius( const Rect.fromLTWH( margin, margin, _size - 2 * margin, _size - 2 * margin), const Radius.circular(224), ); canvas.drawRRect(tileR, bg); canvas.drawRRect( tileR.deflate(3), Paint() ..style = PaintingStyle.stroke ..strokeWidth = 3 ..color = Colors.white.withValues(alpha: 0.16), ); _sheen(canvas); break; case _Mode.foreground: // transparent background — foreground carries the mark only break; } // The mark: filled confidently. The adaptive foreground fills ~2/3 of the // frame (the launcher XML then adds a 16% safe-zone inset on top). final scale = mode == _Mode.foreground ? 1.15 : 1.14; canvas.save(); canvas.translate(512, 524); canvas.scale(scale); canvas.translate(-512, -524); _drawRocket(canvas); canvas.restore(); final pic = recorder.endRecording(); final img = await pic.toImage(_size.toInt(), _size.toInt()); final data = await img.toByteData(format: ui.ImageByteFormat.png); return data!.buffer.asUint8List(); } /// Soft top-left highlight for depth. void _sheen(Canvas canvas) { canvas.drawCircle( const Offset(300, 250), 620, Paint() ..shader = ui.Gradient.radial( const Offset(300, 250), 620, [Colors.white.withValues(alpha: 0.16), Colors.white.withValues(alpha: 0)], ), ); } /// A clean, upright rocket — the Peach mark — in white with an accent window, /// fins and an exhaust flame. Centered on the 1024 canvas. void _drawRocket(Canvas canvas) { const cx = 512.0; // subtle drop shadow behind the whole mark canvas.save(); canvas.translate(0, 16); _rocketBodyAndFins(canvas, Paint() ..color = Colors.black.withValues(alpha: 0.16) ..maskFilter = const MaskFilter.blur(BlurStyle.normal, 22)); canvas.restore(); // flame first (behind body base) final flame = Path() ..moveTo(cx, 828) ..cubicTo(cx - 62, 748, cx - 46, 712, cx - 30, 706) ..lineTo(cx + 30, 706) ..cubicTo(cx + 46, 712, cx + 62, 748, cx, 828) ..close(); canvas.drawPath( flame, Paint() ..shader = ui.Gradient.linear( const Offset(cx, 700), const Offset(cx, 828), [const Color(0xFFFFD48A), const Color(0xFFF5A623)], )); // inner flame final flame2 = Path() ..moveTo(cx, 790) ..cubicTo(cx - 26, 742, cx - 20, 720, cx - 12, 716) ..lineTo(cx + 12, 716) ..cubicTo(cx + 20, 720, cx + 26, 742, cx, 790) ..close(); canvas.drawPath(flame2, Paint()..color = Colors.white.withValues(alpha: 0.85)); // white body + fins _rocketBodyAndFins(canvas, Paint()..color = Colors.white); // window (accent blue with a soft rim) canvas.drawCircle(const Offset(cx, 432), 70, Paint()..color = const Color(0xFF1F2937).withValues(alpha: 0.10)); canvas.drawCircle(const Offset(cx, 430), 66, Paint() ..shader = ui.Gradient.linear( const Offset(cx - 60, 370), const Offset(cx + 60, 490), [const Color(0xFF5AA0FF), const Color(0xFF2F6FE0)], )); // window glint canvas.drawCircle(const Offset(cx - 20, 408), 18, Paint()..color = Colors.white.withValues(alpha: 0.55)); } void _rocketBodyAndFins(Canvas canvas, Paint paint) { const cx = 512.0; // fins (drawn under the body edges) final rightFin = Path() ..moveTo(cx + 96, 556) ..lineTo(cx + 208, 712) ..lineTo(cx + 78, 676) ..close(); final leftFin = Path() ..moveTo(cx - 96, 556) ..lineTo(cx - 208, 712) ..lineTo(cx - 78, 676) ..close(); canvas.drawPath(rightFin, paint); canvas.drawPath(leftFin, paint); // body (symmetric capsule with a pointed nose) final body = Path() ..moveTo(cx, 244) // nose tip ..cubicTo(cx + 92, 300, cx + 116, 372, cx + 116, 452) ..cubicTo(cx + 116, 566, cx + 96, 646, cx + 60, 700) ..cubicTo(cx + 34, 714, cx - 34, 714, cx - 60, 700) ..cubicTo(cx - 96, 646, cx - 116, 566, cx - 116, 452) ..cubicTo(cx - 116, 372, cx - 92, 300, cx, 244) ..close(); canvas.drawPath(body, paint); }