Skip to content

Instantly share code, notes, and snippets.

@filiph
Last active October 11, 2022 13:23
Show Gist options
  • Save filiph/6ebac40b98e2c6e2826893d43aeae2e0 to your computer and use it in GitHub Desktop.
Save filiph/6ebac40b98e2c6e2826893d43aeae2e0 to your computer and use it in GitHub Desktop.
Human Life Counter
// This now lives in https://github.com/filiph/human-life/blob/main/lib/main.dart as source
// and at https://filiph.github.io/human-life/ as an app.
import 'dart:math';
import 'package:flutter/material.dart';
void main() {
runApp(HumanLifeApp());
}
class HumanLifeApp extends StatelessWidget {
final settings = HumanLifeSettings();
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Human Life Counter',
theme: ThemeData(
primaryColor: Color(0xFF38618C),
visualDensity: VisualDensity.adaptivePlatformDensity,
bottomSheetTheme: BottomSheetThemeData(
backgroundColor: Color(0xFFC0FF99),
),
),
home: HumanLifePage(settings: settings),
);
}
}
class HumanLifePage extends StatelessWidget {
final HumanLifeSettings settings;
const HumanLifePage({this.settings});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('Human Life Counter'),
),
body: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(flex: 17, child: HumanLifeVisualization(settings)),
// Make sure there's enough space at the bottom that the bottom sheet
// can cover.
Expanded(flex: 3, child: SizedBox()),
],
),
bottomSheet: CustomizationDrawer(settings),
);
}
}
class HumanLifeVisualization extends StatelessWidget {
final HumanLifeSettings settings;
const HumanLifeVisualization(this.settings);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(16),
child: AnimatedBuilder(
animation: settings,
builder: (context, child) {
return RepaintBoundary(
child: CustomPaint(
painter: HumanLifePainter(
settings.currentAge, settings.maxAge, settings.maxActiveAge),
),
);
},
),
);
}
}
class HumanLifePainter extends CustomPainter {
final int currentAge, maxAge, maxActiveAge;
const HumanLifePainter(this.currentAge, this.maxAge, this.maxActiveAge);
/// Weeks per year, counting leap years (because why not).
/// https://en.wikipedia.org/wiki/ISO_week_date
///
/// Otherwise, this is just 365/7.
static const weeksPerYear = 52.1775;
@override
void paint(Canvas canvas, Size size) {
final weeks = maxAge * weeksPerYear;
const minimumSquareArea = 3 * 3;
const maximumSquareArea = 50 * 50;
const minColumns = 1;
const maxColumns = 1000;
int bestColumns = -1;
int bestRows = -1;
for (int columns = minColumns; columns < maxColumns; columns++) {
final sqArea = (size.height * size.height) / (columns * columns);
if (minimumSquareArea < sqArea && sqArea < maximumSquareArea) {
final rows = (weeks / columns).ceil();
// Trying to find the closest aspect ratio to the one of the viewport.
final distance = (columns / rows - size.aspectRatio).abs();
final prevDistance = (bestColumns / bestRows - size.aspectRatio).abs();
if (bestColumns != -1 && prevDistance < distance) {
// We've already seen the best ratio. It was the last one.
break;
}
bestColumns = columns;
bestRows = rows;
}
}
if (bestRows == -1 || bestColumns == -1) {
// Escape chute.
bestRows = sqrt(weeks).ceil();
bestColumns = (weeks / bestRows).ceil();
}
final sqSize = (size.height / bestRows).floorToDouble();
final width = sqSize * bestColumns;
final leftPadding = (size.width - width) / 2;
bool currentShownYet = false;
int week = 0;
for (int row = 0; row < bestRows; row++) {
for (int column = 0; column < bestColumns; column++) {
if (week < weeks) {
final age = week / weeksPerYear;
var paint = (age <= currentAge)
? lived
: (age <= maxActiveAge ? active : elderly);
if (paint == active && !currentShownYet) {
// The first square in current life.
paint = current;
currentShownYet = true;
}
final rect = Rect.fromLTWH(leftPadding + column * sqSize,
row * sqSize, sqSize - 1, sqSize - 1);
canvas.drawRect(rect, paint);
}
week++;
}
}
}
static final Paint lived = Paint()..color = Color(0xFFB4C5E4);
static final Paint current = Paint()..color = Colors.white;
static final Paint active = Paint()..color = Color(0xFF63C132);
static final Paint elderly = Paint()..color = Color(0xFF358600);
@override
bool shouldRepaint(HumanLifePainter old) {
return currentAge != old.currentAge ||
maxAge != old.maxAge ||
maxActiveAge != old.maxActiveAge;
}
}
class CustomizationDrawer extends StatelessWidget {
final HumanLifeSettings settings;
CustomizationDrawer(this.settings);
@override
Widget build(BuildContext context) {
return DraggableScrollableSheet(
minChildSize: 0.15,
initialChildSize: 0.15,
maxChildSize: 0.7,
expand: false,
builder: (context, scrollController) {
return Container(
decoration: BoxDecoration(
boxShadow: [
BoxShadow(
blurRadius: 0,
),
],
color: Theme.of(context).bottomSheetTheme.backgroundColor,
),
child: SafeArea(
child: Padding(
padding: const EdgeInsets.only(left: 16, right: 16),
child: ListView(
controller: scrollController,
children: [
SizedBox(height: 32),
Row(
children: [
Text(
'Pull up to customize ',
style: Theme.of(context).textTheme.headline6,
),
Icon(Icons.arrow_upward_rounded),
],
),
SizedBox(height: 16),
Text("What's this about? The app visualizes a human life "
"as a series of squares. Each square represents "
"a single week of life.\n\n"
"Light blue squares are already in the past. "
"Light green squares are upcoming active life. "
"Dark green squares are the weeks after that.\n\n"
"How you define ‘active life’ is up to you.\n\n"
"Try to look past the depressing part and instead "
"think about this as motivational.\n\n"
"Inspired by the article ‘Tail End’ "
"at Wait But Why."),
SizedBox(height: 32),
HumanLifeCustomization(settings),
SizedBox(height: 32),
],
),
),
),
);
},
);
}
}
class HumanLifeCustomization extends StatefulWidget {
final HumanLifeSettings settings;
const HumanLifeCustomization(this.settings);
@override
_HumanLifeCustomizationState createState() => _HumanLifeCustomizationState();
}
class _HumanLifeCustomizationState extends State<HumanLifeCustomization> {
@override
Widget build(BuildContext context) {
return Column(
children: [
AgeSlider(
label: 'Current age',
initialValue: widget.settings.currentAge,
onChanged: (value) => widget.settings.currentAge = value,
),
AgeSlider(
label: 'Active life expectancy',
initialValue: widget.settings.maxActiveAge,
onChanged: (value) => widget.settings.maxActiveAge = value,
),
AgeSlider(
label: 'Life expectancy',
initialValue: widget.settings.maxAge,
onChanged: (value) => widget.settings.maxAge = value,
),
],
);
}
}
class AgeSlider extends StatefulWidget {
final int initialValue;
final int maxValue = 100;
final String label;
final void Function(int) onChanged;
const AgeSlider({
@required this.label,
@required this.initialValue,
@required this.onChanged,
Key key,
}) : super(key: key);
@override
_AgeSliderState createState() => _AgeSliderState();
}
class _AgeSliderState extends State<AgeSlider> {
@override
void initState() {
super.initState();
_value = widget.initialValue.toDouble();
_outValue = _value.round();
}
double _value;
int _outValue = -1;
@override
Widget build(BuildContext context) {
return Column(
children: [
Text(
'${widget.label}: $_outValue years',
style: Theme.of(context).textTheme.headline6,
),
Slider(
value: _value,
max: widget.maxValue.toDouble(),
divisions: widget.maxValue,
onChanged: (value) {
setState(() {
_value = value;
});
if (_outValue != _value.round()) {
_outValue = _value.round();
widget.onChanged(_outValue);
}
},
),
],
);
}
}
class HumanLifeSettings extends ChangeNotifier {
int _currentAge = 18;
int get currentAge => _currentAge;
set currentAge(int value) {
_currentAge = value;
notifyListeners();
}
int _maxAge = 79;
int get maxAge => _maxAge;
set maxAge(int value) {
_maxAge = value;
notifyListeners();
}
int _maxActiveAge = 65;
int get maxActiveAge => _maxActiveAge;
set maxActiveAge(int value) {
_maxActiveAge = value;
notifyListeners();
}
}
@filiph
Copy link
Author

filiph commented Feb 21, 2021

@pratikbutani
Copy link

Thanks!

FWIW, This now lives in https://github.com/filiph/human-life/blob/main/lib/main.dart as source and at https://filiph.github.io/human-life/ as an app.

You should upload this app on Playstore 💯

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment