Skip to content

Instantly share code, notes, and snippets.

@PlugFox
Last active April 9, 2024 12:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save PlugFox/9d251f3300804746884e2c2f8824babb to your computer and use it in GitHub Desktop.
Save PlugFox/9d251f3300804746884e2c2f8824babb to your computer and use it in GitHub Desktop.
Sunflower
/*
* Sunflower
* https://gist.github.com/PlugFox/9d251f3300804746884e2c2f8824babb
* https://dartpad.dev?id=9d251f3300804746884e2c2f8824babb
* Mike Matiunin <plugfox@gmail.com>, 06 March 2024
*/
import 'dart:async';
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
void main() => runZonedGuarded<void>(
() {
const max = 250;
final controller = ValueNotifier<int>(max ~/ 2);
runApp(
MaterialApp(
title: 'Material App',
theme: ThemeData.dark(),
debugShowCheckedModeBanner: false,
home: Scaffold(
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(16),
child: Sunflower(
max: max,
seeds: controller,
),
),
),
),
),
);
},
(error, stackTrace) => print(error),
);
class SunflowerController with ChangeNotifier {
SunflowerController({required int max, Curve curve = Curves.easeInOut})
: _curve = curve,
_state = const <double>[],
_progress = const <double>[],
_speed = const <double>[],
_max = max {
changeMax(max);
}
final Curve _curve;
List<double> _state;
List<double> _progress;
List<double> _speed;
List<double> get progress => _progress;
int _max;
int get max => _max;
// Set the maximum number of seeds
void changeMax(int max) {
final oldValue = _state;
final rnd = math.Random();
_max = max;
_state = List<double>.generate(
max, (i) => i >= oldValue.length ? 0 : oldValue[i],
growable: false);
_progress = List<double>.unmodifiable(_state.map<double>(_curve.transform));
_speed = List<double>.generate(max, (_) => (rnd.nextInt(50) + 100) / 100,
growable: false);
}
// Update progress based on the new seed and elapsed time
// Returns true if the progress was updated
bool update(double delta, int seed) {
var updated = false;
for (var i = 0; i < seed; i++) {
if (_state[i] == 1) continue;
_state[i] = math.min(1, _state[i] + delta * _speed[i]);
updated = true;
}
for (var i = seed; i < _max; i++) {
if (_state[i] == 0) continue;
_state[i] = math.max(0, _state[i] - delta * _speed[i]);
updated = true;
}
if (updated) {
_progress =
List<double>.unmodifiable(_state.map<double>(_curve.transform));
notifyListeners();
}
return updated;
}
}
class Sunflower extends StatefulWidget {
Sunflower({
required this.seeds,
this.max = 250,
this.duration = const Duration(milliseconds: 2000),
super.key, // ignore: unused_element
}) : assert(seeds.value <= max, 'Seeds must be less than or equal to max');
/// The maximum number of seeds to show in the sunflower.
final int max;
/// Seeds to show in the sunflower.
final ValueNotifier<int> seeds;
/// The duration of the animation.
final Duration duration;
@override
State<Sunflower> createState() => _SunflowerState();
}
class _SunflowerState extends State<Sunflower>
with SingleTickerProviderStateMixin {
late final SunflowerController _controller;
late final Ticker _ticker;
@override
void initState() {
super.initState();
_controller = SunflowerController(max: widget.max);
widget.seeds.addListener(_onSeedsChanged);
_ticker = createTicker(_onTick());
_onSeedsChanged();
}
@override
void didUpdateWidget(covariant Sunflower oldWidget) {
super.didUpdateWidget(oldWidget);
var changed = false;
if (oldWidget.max != widget.max) {
_controller.changeMax(widget.max);
changed = true;
}
if (!identical(oldWidget.seeds, widget.seeds)) {
oldWidget.seeds.removeListener(_onSeedsChanged);
widget.seeds.addListener(_onSeedsChanged);
changed = true;
}
if (changed) _onSeedsChanged();
}
@override
void dispose() {
widget.seeds.removeListener(_onSeedsChanged);
_controller.dispose();
_ticker.dispose();
super.dispose();
}
void _onSeedsChanged() {
if (_ticker.isActive) return;
_ticker.start();
}
void Function(Duration elapsed) _onTick() {
var prev = Duration.zero;
return (elapsed) {
final delta = ((elapsed.inMilliseconds - prev.inMilliseconds) /
widget.duration.inMilliseconds)
.clamp(0.0, 1.0);
final updated = _controller.update(delta, widget.seeds.value);
if (updated) {
prev = elapsed;
} else {
_ticker.stop();
prev = Duration.zero;
}
};
}
@override
Widget build(BuildContext context) => Center(
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: <Widget>[
Positioned(
left: 0,
right: 0,
top: 16,
bottom: 72 + 16,
child: Center(
child: AspectRatio(
aspectRatio: 1,
child: RepaintBoundary(
child: CustomPaint(
painter: SunflowerPainter(
controller: _controller,
),
),
),
),
),
),
Positioned(
left: 0,
right: 0,
height: 72,
bottom: 16,
child: Center(
child: SizedBox(
width: 300,
child: ValueListenableBuilder<int>(
valueListenable: widget.seeds,
builder: (context, value, child) => Column(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.center,
children: <Widget>[
Slider(
min: 1,
max: widget.max.toDouble(),
value: value.toDouble(),
onChanged: (val) => widget.seeds.value = val.toInt(),
),
Text(
'${value * 100 ~/ widget.max}%',
maxLines: 1,
overflow: TextOverflow.ellipsis,
textAlign: TextAlign.center,
),
],
),
),
),
),
),
],
),
);
}
/// {@template sunflower}
/// SunflowerPainter.
/// {@endtemplate}
class SunflowerPainter extends CustomPainter {
/// {@macro sunflower}
const SunflowerPainter(
{required SunflowerController controller, bool skipFirst = true})
: _controller = controller,
_skipFirst = skipFirst,
super(repaint: controller);
/// The controller for the sunflower
final SunflowerController _controller;
/// Whether to skip the first seed
final bool _skipFirst;
/// The outer paint for the circle
static final _outerPaint = Paint()
..color = Colors.grey.shade700
..style = PaintingStyle.fill;
/// The inner paint for the sunflower
static final _innerPaint = Paint()
..color = Colors.deepOrange
..style = PaintingStyle.fill;
/// Sunflower padding to the edge of the canvas 0..1
static const padding = 0.15;
/// Tau (τ) is the ratio of a circle's circumference to its radius
static const tau = math.pi * 2;
/// Phi (φ) is the golden ratio (1 + √5) / 2
static final phi = (math.sqrt(5) + 1) / 2;
static double _lerpDouble(double a, double b, double t) =>
a * (1.0 - t) + b * t;
static Offset _lerpOffset(Offset a, Offset b, double t) =>
Offset(_lerpDouble(a.dx, b.dx, t), _lerpDouble(a.dy, b.dy, t));
static Offset _evalOuter(double radius, int max, int i) {
final theta = i * tau / (max - 1);
final r = radius;
return Offset(r * math.cos(theta), r * math.sin(theta));
}
static Offset _evalInner(double radius, int max, int i) {
final theta = i * tau / phi;
final r = math.sqrt(i / (max + 0.5)) * radius * (1 - padding);
return Offset(r * math.cos(theta), r * math.sin(theta));
}
@override
void paint(Canvas canvas, Size size) {
if (size.shortestSide < padding * 3) return;
final max = _controller.max;
final radius = size.shortestSide / 2;
final center = Offset(size.width / 2, size.height / 2);
final outerDotRadius = size.shortestSide / _controller.max;
final innerDotRadius = outerDotRadius * 5;
// Draw the seeds
for (var i = _skipFirst ? 1 : 0; i < _controller.max; i++) {
switch (_controller.progress[i]) {
case 0.0:
canvas.drawCircle(
center + _evalOuter(radius, max, i),
outerDotRadius,
_outerPaint,
);
case 1.0:
canvas.drawCircle(
center + _evalInner(radius, max, i),
innerDotRadius,
_innerPaint,
);
case double t:
canvas.drawCircle(
center +
_lerpOffset(
_evalOuter(radius, max, i), _evalInner(radius, max, i), t),
_lerpDouble(outerDotRadius, innerDotRadius, t),
Paint()
..color = Color.lerp(_outerPaint.color, _innerPaint.color, t)!
..style = PaintingStyle.stroke,
);
}
}
}
@override
bool shouldRepaint(covariant SunflowerPainter oldDelegate) =>
!identical(oldDelegate._controller, _controller);
@override
bool shouldRebuildSemantics(covariant SunflowerPainter oldDelegate) => false;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment