Add app source and tests
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
197
test/tools/generate_icon_test.dart
Normal file
197
test/tools/generate_icon_test.dart
Normal file
@@ -0,0 +1,197 @@
|
||||
// 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);
|
||||
}
|
||||
Reference in New Issue
Block a user