Skip to content

Instantly share code, notes, and snippets.

@bambuh
Created August 4, 2025 14:26
Show Gist options
  • Select an option

  • Save bambuh/51f11583091bc798936c7ebe6532c5ac to your computer and use it in GitHub Desktop.

Select an option

Save bambuh/51f11583091bc798936c7ebe6532c5ac to your computer and use it in GitHub Desktop.
import 'dart:async';
import 'package:flutter/material.dart';
class GraphView extends StatefulWidget {
const GraphView({super.key});
@override
State<GraphView> createState() => _GraphViewState();
}
class _GraphViewState extends State<GraphView>
with SingleTickerProviderStateMixin {
List<Offset> nodes = [
const Offset(100, 100),
const Offset(200, 150),
const Offset(300, 100),
const Offset(400, 200),
const Offset(150, 300),
];
List<Offset> velocities = [
Offset.zero,
Offset.zero,
Offset.zero,
Offset.zero,
Offset.zero,
];
List<List<int>> edges = [
[0, 1],
[1, 2],
[2, 3],
[3, 4],
[4, 0],
];
late List<double> initialEdgeLengths;
Offset? draggingNode;
int? draggingNodeIndex;
late Timer timer;
@override
void initState() {
super.initState();
initialEdgeLengths =
edges.map((e) => (nodes[e[0]] - nodes[e[1]]).distance).toList();
timer = Timer.periodic(
const Duration(milliseconds: 16), (timer) => _updateNodes());
}
@override
void dispose() {
timer.cancel();
super.dispose();
}
void _updateNodes() {
setState(() {
const double springConstant = 0.01;
const double damping = 0.85;
for (int i = 0; i < nodes.length; i++) {
if (i == draggingNodeIndex) continue; // Skip the dragged node
Offset force = Offset.zero;
for (int j = 0; j < edges.length; j++) {
List<int> edge = edges[j];
if (edge[0] == i || edge[1] == i) {
int connectedNodeIndex = (edge[0] == i) ? edge[1] : edge[0];
Offset displacement = nodes[connectedNodeIndex] - nodes[i];
double currentLength = displacement.distance;
double initialLength = initialEdgeLengths[j];
double deltaLength = currentLength - initialLength;
// Apply Hooke's law: F = -k * x
force +=
(displacement / currentLength) * springConstant * deltaLength;
}
}
velocities[i] += force;
velocities[i] *= damping;
nodes[i] += velocities[i];
}
});
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onPanStart: (details) {
for (int i = 0; i < nodes.length; i++) {
if ((nodes[i] - details.localPosition).distance < 20.0) {
draggingNode = nodes[i];
draggingNodeIndex = i;
break;
}
}
},
onPanUpdate: (details) {
if (draggingNode != null && draggingNodeIndex != null) {
setState(() {
nodes[draggingNodeIndex!] = details.localPosition;
});
}
},
onPanEnd: (details) {
draggingNode = null;
draggingNodeIndex = null;
},
child: CustomPaint(
key: UniqueKey(),
painter: GraphPainter(nodes: nodes, edges: edges),
size: const Size(double.infinity, double.infinity),
),
);
}
}
class GraphPainter extends CustomPainter {
final List<Offset> nodes;
final List<List<int>> edges;
GraphPainter({required this.nodes, required this.edges});
@override
void paint(Canvas canvas, Size size) {
final Paint edgePaint = Paint()
..color = Colors.grey
..strokeWidth = 2.0;
final Paint nodePaint = Paint()
..color = Colors.blue
..style = PaintingStyle.fill;
final Paint nodeStrokePaint = Paint()
..color = Colors.black
..strokeWidth = 2.0
..style = PaintingStyle.stroke;
// Draw edges
for (List<int> edge in edges) {
final Offset p1 = nodes[edge[0]];
final Offset p2 = nodes[edge[1]];
canvas.drawLine(p1, p2, edgePaint);
}
// Draw nodes
for (Offset node in nodes) {
canvas.drawCircle(node, 10.0, nodePaint);
canvas.drawCircle(node, 10.0, nodeStrokePaint);
}
}
@override
bool shouldRepaint(covariant GraphPainter oldDelegate) {
return true;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment