app/tool/generate_icon.dart
John Mizerek 151106aa8e Initial commit: Add Months MVP
Local-first Flutter app that identifies the single behavioral change
most likely to extend lifespan using hazard-based modeling.

Features:
- Risk engine with hazard ratios from meta-analyses
- 50 countries mapped to 4 mortality groups
- 6 modifiable factors: smoking, alcohol, sleep, activity, driving, work hours
- SQLite local storage (no cloud, no accounts)
- Muted clinical UI theme
- 23 unit tests for risk engine

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-20 21:25:00 -08:00

198 lines
5.5 KiB
Dart

import 'dart:io';
import 'dart:typed_data';
import 'dart:math';
/// Generates a simple tree icon PNG
void main() async {
const size = 1024;
final pixels = Uint8List(size * size * 4);
// Fill with white background
for (var i = 0; i < pixels.length; i += 4) {
pixels[i] = 255; // R
pixels[i + 1] = 255; // G
pixels[i + 2] = 255; // B
pixels[i + 3] = 255; // A
}
// Tree colors (muted teal from our theme)
const treeR = 74; // 0x4A
const treeG = 144; // 0x90
const treeB = 164; // 0xA4
// Trunk color (darker)
const trunkR = 90;
const trunkG = 70;
const trunkB = 55;
// Draw tree trunk (rectangle)
final trunkLeft = (size * 0.44).round();
final trunkRight = (size * 0.56).round();
final trunkTop = (size * 0.65).round();
final trunkBottom = (size * 0.85).round();
for (var y = trunkTop; y < trunkBottom; y++) {
for (var x = trunkLeft; x < trunkRight; x++) {
final i = (y * size + x) * 4;
pixels[i] = trunkR;
pixels[i + 1] = trunkG;
pixels[i + 2] = trunkB;
pixels[i + 3] = 255;
}
}
// Draw tree canopy (three triangles stacked)
void drawTriangle(int centerX, int topY, int height, int baseWidth) {
for (var y = topY; y < topY + height; y++) {
final progress = (y - topY) / height;
final halfWidth = (baseWidth * progress / 2).round();
for (var x = centerX - halfWidth; x <= centerX + halfWidth; x++) {
if (x >= 0 && x < size && y >= 0 && y < size) {
final i = (y * size + x) * 4;
pixels[i] = treeR;
pixels[i + 1] = treeG;
pixels[i + 2] = treeB;
pixels[i + 3] = 255;
}
}
}
}
final centerX = size ~/ 2;
// Top triangle (smallest)
drawTriangle(centerX, (size * 0.15).round(), (size * 0.20).round(), (size * 0.35).round());
// Middle triangle
drawTriangle(centerX, (size * 0.28).round(), (size * 0.22).round(), (size * 0.48).round());
// Bottom triangle (largest)
drawTriangle(centerX, (size * 0.42).round(), (size * 0.26).round(), (size * 0.58).round());
// Encode as PNG
final png = encodePng(size, size, pixels);
// Write to file
final file = File('assets/icon/app_icon.png');
await file.writeAsBytes(png);
print('Generated app_icon.png');
// Also create foreground version (same but smaller for adaptive icons)
final foregroundFile = File('assets/icon/app_icon_foreground.png');
await foregroundFile.writeAsBytes(png);
print('Generated app_icon_foreground.png');
}
/// Simple PNG encoder (no compression for simplicity)
Uint8List encodePng(int width, int height, Uint8List rgba) {
final output = BytesBuilder();
// PNG signature
output.add([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
// IHDR chunk
final ihdr = BytesBuilder();
ihdr.add(_int32be(width));
ihdr.add(_int32be(height));
ihdr.addByte(8); // bit depth
ihdr.addByte(6); // color type (RGBA)
ihdr.addByte(0); // compression
ihdr.addByte(0); // filter
ihdr.addByte(0); // interlace
_writeChunk(output, 'IHDR', ihdr.toBytes());
// IDAT chunk (image data with zlib compression)
// For simplicity, we'll use store (no compression)
final rawData = BytesBuilder();
for (var y = 0; y < height; y++) {
rawData.addByte(0); // filter type: None
for (var x = 0; x < width; x++) {
final i = (y * width + x) * 4;
rawData.addByte(rgba[i]); // R
rawData.addByte(rgba[i + 1]); // G
rawData.addByte(rgba[i + 2]); // B
rawData.addByte(rgba[i + 3]); // A
}
}
final compressed = _deflateStore(rawData.toBytes());
_writeChunk(output, 'IDAT', compressed);
// IEND chunk
_writeChunk(output, 'IEND', Uint8List(0));
return output.toBytes();
}
Uint8List _int32be(int value) {
return Uint8List.fromList([
(value >> 24) & 0xFF,
(value >> 16) & 0xFF,
(value >> 8) & 0xFF,
value & 0xFF,
]);
}
void _writeChunk(BytesBuilder output, String type, Uint8List data) {
output.add(_int32be(data.length));
final typeBytes = type.codeUnits;
output.add(typeBytes);
output.add(data);
// CRC32 of type + data
final crcData = Uint8List(typeBytes.length + data.length);
crcData.setAll(0, typeBytes);
crcData.setAll(typeBytes.length, data);
output.add(_int32be(_crc32(crcData)));
}
/// Simple deflate with store (no compression)
Uint8List _deflateStore(Uint8List data) {
final output = BytesBuilder();
// zlib header
output.addByte(0x78); // CMF
output.addByte(0x01); // FLG (no dict, fastest)
// Split into blocks of max 65535 bytes
const maxBlock = 65535;
var offset = 0;
while (offset < data.length) {
final remaining = data.length - offset;
final blockSize = remaining > maxBlock ? maxBlock : remaining;
final isLast = offset + blockSize >= data.length;
output.addByte(isLast ? 0x01 : 0x00); // BFINAL + BTYPE (store)
output.addByte(blockSize & 0xFF);
output.addByte((blockSize >> 8) & 0xFF);
output.addByte((~blockSize) & 0xFF);
output.addByte(((~blockSize) >> 8) & 0xFF);
output.add(data.sublist(offset, offset + blockSize));
offset += blockSize;
}
// Adler-32 checksum
var s1 = 1;
var s2 = 0;
for (var i = 0; i < data.length; i++) {
s1 = (s1 + data[i]) % 65521;
s2 = (s2 + s1) % 65521;
}
final adler = (s2 << 16) | s1;
output.add(_int32be(adler));
return output.toBytes();
}
int _crc32(Uint8List data) {
var crc = 0xFFFFFFFF;
for (var byte in data) {
crc ^= byte;
for (var i = 0; i < 8; i++) {
crc = (crc & 1) != 0 ? (crc >> 1) ^ 0xEDB88320 : crc >> 1;
}
}
return crc ^ 0xFFFFFFFF;
}