import 'dart:math'; import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; void main() { runApp(const MyHomePage()); } class MyHomePage extends StatefulWidget { const MyHomePage({super.key}); @override State createState() => _MyHomePageState(); } class _MyHomePageState extends State with SingleTickerProviderStateMixin { late Ticker _ticker; double _t = 0.0; @override void initState() { super.initState(); _ticker = createTicker((Duration elapsed) { setState(() { _t = elapsed.inMilliseconds.toDouble(); }); }); _ticker.start(); } @override void dispose() { _ticker.dispose(); super.dispose(); } @override Widget build(BuildContext context) { const double zoom = 0.2; const Offset offset = Offset(2.0, 2.0); const double timeScale = 0.001; return ColoredBox( color: Colors.black, child: Stack( textDirection: TextDirection.ltr, fit: StackFit.expand, children: [ CustomPaint(painter: Painter(t: _t * timeScale, period: 1.0, semiMajorAxis: 0.5, thetaZero: 0, eccentricity: 0.0, omega: 1.0, color: Colors.orange, zoom: zoom, offset: offset)), CustomPaint(painter: Painter(t: _t * timeScale, period: 2.0, semiMajorAxis: 1.0, thetaZero: -pi / 2.0, eccentricity: 0.1, omega: 1.0, color: Colors.green, zoom: zoom, offset: offset)), CustomPaint(painter: Painter(t: _t * timeScale, period: 3.0, semiMajorAxis: 1.5, thetaZero: -pi, eccentricity: 0.2, omega: 1.0, color: Colors.teal, zoom: zoom, offset: offset)), CustomPaint(painter: Painter(t: _t * timeScale, period: 20.0, semiMajorAxis: 2.0, thetaZero: -pi/5.0, eccentricity: 0.95, omega: pi / 4.0, color: Colors.yellow, zoom: zoom, offset: offset)), CustomPaint(painter: Painter(t: _t * timeScale, period: 4.0, semiMajorAxis: 1.0, thetaZero: 3*pi/4, eccentricity: 0.99, omega: -3*pi/4, color: Colors.red, zoom: zoom, offset: offset)), ], ), ); } } class Painter extends CustomPainter { Painter({ required this.t, required this.period, required this.semiMajorAxis, required this.thetaZero, required this.eccentricity, required this.omega, required this.color, required this.zoom, required this.offset, }) : assert(eccentricity >= 0.0), assert(eccentricity < 1.0); final double t; final double period; final double semiMajorAxis; final double thetaZero; final double eccentricity; final double omega; final Color color; final Offset offset; final double zoom; static const Offset unit = Offset(1.0, 1.0); @override void paint(Canvas canvas, Size size) { final double scale = size.shortestSide * zoom; // EVERYTHING BELOW IS FLIPPED BECAUSE WE'RE USING THE WRONG FOCAL POINT double semiMinorAxis = semiMajorAxis * sqrt(1 - eccentricity*eccentricity); double length = semiMajorAxis * (1 - eccentricity*eccentricity); // length of the semi latus rectum double time = (t % period) / period; // 0..1 double theta = (2*pi*(((atan(time*50-25)+1.53081764)/3.06163528)*(eccentricity*0.25)+(1-eccentricity*0.25)*time) + omega); // theta = omega is the apoapsis (furthest point) // theta = omega + pi is the periapsis (nearest point) double r = length / (1 + eccentricity * cos(theta - omega)); // current position of body Offset p = Offset(r * cos(theta), r * sin(theta)); // axis aligned bounding box of ellipse double c = eccentricity * semiMajorAxis; // distance from focal point to center of ellipse, along major axis double sinOmega = sin(omega); double cosOmega = cos(omega); Offset cOffset = Offset(c * cosOmega, c * sinOmega); Rect oval = Rect.fromCenter(center: Offset(-c, 0.0) * scale, width: semiMajorAxis * 2.0 * scale, height: semiMinorAxis * 2.0 * scale); // double halfWidth = sqrt(semiMajorAxis * semiMajorAxis * cosOmega * cosOmega + semiMinorAxis * semiMinorAxis * sinOmega * sinOmega); // double halfHeight = sqrt(semiMajorAxis * semiMajorAxis * sinOmega * sinOmega + semiMinorAxis * semiMinorAxis * cosOmega * cosOmega); // Rect boundingBox = Rect.fromCenter(center: (offset + cOffset) * scale, width: (halfWidth * 2.0) * scale, height: (halfHeight * 2.0) * scale); // canvas.drawRect(boundingBox, Paint()..color = Color(0x08FFFFFF)); canvas.drawPoints( PointMode.points, [ offset * scale, (p + offset + cOffset * 2.0) * scale, ], Paint() ..color = color ..strokeWidth = 10.0 ..strokeCap = StrokeCap.round, ); canvas.save(); canvas.translate(offset.dx * scale, offset.dy * scale); canvas.rotate(omega + pi); canvas.drawOval(oval, Paint()..style= PaintingStyle.stroke..color = color); canvas.restore(); } @override bool shouldRepaint(Painter oldDelegate) => oldDelegate.t != t; }