Skip to content

Instantly share code, notes, and snippets.

@tan86
Created October 8, 2023 08:19
Show Gist options
  • Save tan86/cdf42ed368e50d7172ef63f01844f6f8 to your computer and use it in GitHub Desktop.
Save tan86/cdf42ed368e50d7172ef63f01844f6f8 to your computer and use it in GitHub Desktop.
flow-fields
import 'package:flutter/material.dart';
import 'package:flutter/scheduler.dart';
import 'dart:math';
import 'dart:ui';
void main() => runApp(const App());
class App extends StatelessWidget {
const App({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
home: Scaffold(
backgroundColor: Colors.black,
body: Center(
child: FlowField(),
),
),
);
}
}
class FlowField extends StatefulWidget {
const FlowField({super.key});
@override
State<StatefulWidget> createState() => FlowFieldState();
}
class FlowFieldState extends State<FlowField>
with SingleTickerProviderStateMixin {
static const double w = 300;
static const double h = 300;
static const n = 25; // no. of fields per row
static const pn = 200; // no. of particles
static const fieldSize = w / n;
late final Ticker _ticker;
///
static Vec2 angleVec2(double radian) => (x: cos(radian), y: sin(radian));
static Vec2 rndAngleVec2() => angleVec2(Random().nextDouble() * pi * 2);
static Vec2 windowToField(Vec2 v) {
final fv = (
x: (v.x / fieldSize).floorToDouble(),
y: (v.y / fieldSize).floorToDouble(),
);
return fv.clamp(min: 0, max: n - 1);
}
@override
void initState() {
super.initState();
_ticker = createTicker(_onTick)..start();
}
@override
void dispose() {
_ticker.dispose();
super.dispose();
}
void _onTick(Duration elapsed) {
for (var i = 0; i < pn; i++) {
final p = particles[i];
final fp = windowToField(p);
final f = fields[fp.x.toInt() * fp.y.toInt()];
particles[i] = p + f;
}
setState(() {});
}
///
final fields = List<Vec2>.generate(
n * n,
(_) => rndAngleVec2(),
growable: false,
);
final particles = List<Vec2>.generate(
pn,
(_) => (
x: Random().nextDouble() * w,
y: Random().nextDouble() * h,
),
growable: false,
);
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: FlowFieldPainter(
n: n,
fs: fieldSize,
fields: fields,
particles: particles,
),
size: const Size(w, h),
);
}
}
class FlowFieldPainter extends CustomPainter {
FlowFieldPainter({
required this.n,
required this.fs,
required this.fields,
required this.particles,
});
final int n;
final double fs;
final List<Vec2> fields;
final List<Vec2> particles;
///
late final hfs = fs / 2;
final p = Paint()..color = Colors.white;
@override
void paint(Canvas canvas, Size size) {
for (var i = 0; i < particles.length; i++) {
final pp = particles[i];
canvas.drawCircle(Offset(pp.x, pp.y), 2, p);
}
// canvas.saveLayer(null, Paint()..blendMode = BlendMode.multiply);
}
void drawFlowLines(Canvas canvas) {
final p = Paint()..color = Colors.red;
for (var y = 0; y < n; y++) {
for (var x = 0; x < n; x++) {
final os = Offset(hfs, hfs) + Offset(x * fs, y * fs);
final f = fields[x * y];
final upper = os + Offset(hfs + f.x, hfs * f.y);
final lower = os + Offset(-hfs + f.x, -hfs * f.y);
canvas.drawLine(upper, lower, p);
canvas.drawCircle(upper, 1, p);
}
}
}
@override
bool shouldRepaint(covariant FlowFieldPainter oldDelegate) => true;
}
typedef Vec2 = ({double x, double y});
extension Vec2Ext on Vec2 {
Vec2 operator +(Vec2 o) => (x: x + o.x, y: y + o.y);
Vec2 clamp({required double min, required double max}) => (
x: clampDouble(x, min, max),
y: clampDouble(y, min, max),
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment