Last active
January 19, 2023 19:11
-
-
Save rutvik110/56f4626c95b92b8cf2c95283d4682331 to your computer and use it in GitHub Desktop.
An implementation of catenary algorithm in dart to paint a hanging rope.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// An example of simulating a simple rope. For, the construction of rope, catenary algorithm(https://en.wikipedia.org/wiki/Catenary) is used. | |
// The catenary is a curve that describes the shape of a hanging chain or cable, or the curve described by a freely hanging chain or cable. | |
// The implementation of catenary algorithm is based on the following js implementation : https://github.com/dulnan/catenary-curve/blob/9cb7e53e2db4bd5c499f5051abde8bfd853d946a/src/main.ts#L254 | |
// In action : https://github.com/rutvik110/Flutter-Animations/tree/master/lib/flutter_design_challenges/ropes | |
import 'dart:developer' as dev; | |
import 'dart:math' as math; | |
import 'package:dart_numerics/dart_numerics.dart' as numerics; | |
import 'package:flutter/material.dart'; | |
import 'package:flutter/scheduler.dart'; | |
import 'package:flutter_animations/flutter_shaders/stripes_shader/stripes.dart'; | |
import 'package:flutter_animations/util/extensions/colot_to_vector.dart'; | |
import 'package:scidart/numdart.dart'; | |
const EPSILON = 1e-6; | |
class RopesPainter extends CustomPainter { | |
RopesPainter( | |
this.startPoint, | |
this.endPoint, | |
this.stripes, | |
); | |
final Offset endPoint; | |
final Offset startPoint; | |
final Shader stripes; | |
@override | |
void paint(Canvas canvas, Size size) { | |
// TODO: implement paint | |
Paint paint = Paint() | |
..color = Colors.white | |
..strokeWidth = 10.0 | |
..strokeJoin = StrokeJoin.round | |
..strokeCap = StrokeCap.round | |
..shader = stripes | |
..style = PaintingStyle.stroke; | |
final startingPoint = | |
math.Point(startPoint.dx / size.width, startPoint.dy / size.height); | |
final finalendPoint = | |
math.Point(endPoint.dx / size.width, endPoint.dy / size.height); | |
final curvePoints = getCaternaryCurve(startingPoint, finalendPoint, 1); | |
final points = curvePoints | |
.map((e) => Offset(e.x * size.width, (e.y) * size.height)) | |
.toList(); | |
final path = Path(); | |
path.moveTo(points.first.dx, points.first.dy); | |
for (var point in points) { | |
path.lineTo(point.dx, point.dy); | |
} | |
canvas.drawPath(path, paint); | |
} | |
List<math.Point> getCaternaryCurve( | |
math.Point point1, math.Point point2, double chainLength) { | |
const segments = 40; | |
const iterationLimit = 11; | |
// The curves are reversed | |
final isFlipped = point1.x > point2.x; | |
final p1 = isFlipped ? point2 : point1; | |
final p2 = isFlipped ? point1 : point2; | |
final h = p2.x - p1.x; | |
final v = p2.y - p1.y; | |
final a = -getCatenaryParameter(h.toDouble(), v.toDouble(), chainLength, | |
iterationLimit, point1, point2); | |
// Handle NAN/rope being stretched than its original length case | |
if (a.isNaN) { | |
return [point1, point2]; | |
} | |
final x = (a * math.log((chainLength + v) / (chainLength - v)) - h) * 0.5; | |
final y = a * cosh(x / a); | |
final offsetX = p1.x - x; | |
final offsetY = p1.y - y; | |
final curveData = getCurve( | |
a, | |
p1, | |
p2, | |
offsetX, | |
offsetY, | |
segments, | |
); | |
return curveData; | |
} | |
List<math.Point> getCurve( | |
// The catenary parameter. | |
double a, | |
// First point. | |
math.Point p1, | |
// Second point. | |
math.Point p2, | |
double offsetX, | |
double offsetY, | |
// How many "parts" the chain should be made of. | |
int segments, | |
) { | |
final data = [ | |
// Calculate the first point on the curve | |
math.Point(p1.x, a * cosh((p1.x - offsetX) / a) + offsetY), | |
]; | |
final d = p2.x - p1.x; | |
final length = segments - 1; | |
// Calculate the points in between the first and last point | |
for (var i = 0; i < length; i++) { | |
final x = p1.x + (d * (i + 0.5)) / length; | |
final y = a * cosh((x - offsetX) / a) + offsetY; | |
data.add(math.Point(x, y)); | |
} | |
// Calculate the last point on the curve | |
data.add(math.Point(p2.x, a * cosh((p2.x - offsetX) / a) + offsetY)); | |
return data; | |
} | |
@override | |
bool shouldRepaint(covariant CustomPainter oldDelegate) { | |
return true; | |
} | |
} | |
double getDistanceBetweenPoints(math.Point p1, math.Point p2) { | |
final diff = getDifferenceTo(p1, p2); | |
return math.sqrt(math.pow(diff.x, 2) + math.pow(diff.y, 2)); | |
} | |
math.Point getDifferenceTo(math.Point p1, math.Point p2) { | |
return math.Point(p1.x - p2.x, p1.y - p2.y); | |
} | |
double getCatenaryParameter( | |
double h, | |
double v, | |
double length, | |
int limit, | |
math.Point point, | |
math.Point point2, | |
) { | |
final m = math.sqrt(length * length - v * v) / h; | |
double x = numerics.acosh(m) + 1; | |
double prevx = -1; | |
double count = 0; | |
// Iterate until we find a suitable catenary parameter or reach the iteration | |
// limit | |
while ((x - prevx).abs() > EPSILON && count < limit) { | |
prevx = x; | |
x = x - (sinh(x) - m * x) / (cosh(x) - m); | |
count++; | |
} | |
return h / (2 * x); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment