198 lines
5.9 KiB
Dart
198 lines
5.9 KiB
Dart
// 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<void> _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<Uint8List> _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);
|
|
}
|