Add app source and tests

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
alvocool
2026-10-11 10:15:30 +03:00
parent 7e246255c2
commit 38881d1fe1
135 changed files with 21057 additions and 0 deletions

View 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);
}