Flutter CustomPaint Moving a circle along a path - flutter

I'm learning CustomPaint by making my own line chart. I have already made the chart itself so now i'm making the things around it. One them is a trackball/line whenever you pan. So far i've made the dashed line from top to bottom, and it works perfectly, now i want to make the the ball that's gonna follow along with the line. My problem is getting the ball and dashed line to always be in sync. right now it's the dashed line that follows my finger and the ball that does'nt match up. I found this answer on stack overflow but it's not working quite right for me. This video is how it currently functions with the following code,
where dragX is the current x-axis position of your finger in pixels.
void drawTrackballLine(Canvas canvas, Size size) {
double dashHeight = 5;
double dashSpace = trackBallOptions.dashSpacing;
double y = 0;
final paint = Paint()
..color = trackBallOptions.color
..strokeWidth = trackBallOptions.strokeWidth;
while (y < size.height) {
canvas.drawLine(
Offset(dragX, y),
Offset(dragX, y + dashHeight),
paint,
);
y += dashSpace + dashHeight;
}
}
void drawTrackball(Canvas canvas, Size size, Path path) {
final pathMetric = path.computeMetrics().elementAt(0);
final value = pathMetric.length * (dragX / size.width);
final pos = pathMetric.getTangentForOffset(value);
canvas.drawCircle(pos!.position, 5, Paint());
}

Related

How Can I Optimally Draw Lines Using Flutter's CustomPainter Widget?

I am attempting to figure out the most efficient way to draw lines with the CustomPainter widget.
I have everything set up and it works great as I pass in points to the custompainter and it paints all of the lines perfectly using this class:
class MyPainter extends CustomPainter {
List<Offset?> points;
MyPainter({required this.points});
#override
void paint(Canvas canvas, Size size) {
Paint background = Paint()..color = Colors.grey.shade50;
Rect rect = Rect.fromLTWH(0, 0, size.width, size.height);
canvas.drawRect(rect, background);
Paint paint = Paint()
..color = Colors.black
..strokeCap = StrokeCap.round
..strokeWidth = 3;
for(int i = 0; i < points.length - 1; i++){
if(points[i] != null && points[i + 1] != null){
canvas.drawLine(points[i]!, points[i + 1]!, paint);
}
else if (points[i] != null && points[i + 1] == null){
canvas.drawPoints(PointMode.points, [points[i]!], paint);
}
}
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
return true;
}
}
My issue becomes apparent when the number of points becomes increasingly larger with time. I have 2 dials that the user spins and every frame it changes the "cursor" position based on the dial that is being spun for dx and dy respectively. Every time the dial changes it's angle and the cursor position changes this function is called:
void cursorChange(double distanceX, double distanceY){
setState(() {
cursor = Offset(distanceX, distanceY);
points.add(cursor);
points = removeDuplicatePoints(points);
});
}
Which repaints the canvas appropriately based on the points list. When this list becomes bigger it lowers performance when it really shouldn't. For example if I rotated the left dial 100 degrees, it would move the cursor to the right by 100, which should draw a line from origin to where the cursor is. Logically, because it's a line you'd only need 2 points, but because I have to redraw the new line at the new cursor spot, my logic just adds another point resulting into like 200 points for one line. It is most easiest to imagine this like an etch-a-sketch game!
In Short: I need to find an algorithm that optimally draws different lines while ignoring any points that are on an existing line.
I have tried just removing all of the points in between the origin and the current cursor to make the line but the user can also move vertically creating a new line. 2 things prevent this solution:
The user can move backwards too and deleting the points in between would just remove the furthest point and make it smaller.
If the user moved the dial vertically, the line would just rotate to the cursor
I then tried to patch these issues with some conditionals to check for direction, then add a null point and treat it like a new origin. This only led me down a game of cat and mouse that seemed to never end.
Any help would be so incredibly appreciated.

flutter) How do I make arrow lines with canvas?

I want to put an arrow at the end (or on both sides) of a line drawn with a drawLine() of canvas.
Like this
Can I know how to do this?
That's actually not that hard to do - there's just a little math involved.
Let's have a line and paint set up like this:
final paint = Paint()
..color = Colors.black
..strokeWidth = 2;
final p1 = Offset(50, 50);
final p2 = Offset(250, 150);
canvas.drawLine(p1, p2, paint);
this will give us:
Now if we think of this line as a vector, we remember a vector has a direction. To get the direction we must find the angle between the start and the end of the line. This can be done using the atan2 function of dart's math library.
So import the math library
import 'dart:math' as math;
and add the following lines
final dX = p2.dx - p1.dx;
final dY = p2.dy - p1.dy;
final angle = math.atan2(dY, dX);
Afterwards let's determine a size and an angle for the arrow we're about to put at the end of the line:
final arrowSize = 15;
final arrowAngle= 25 * math.pi / 180;
Everything that's left is actually drawing the arrow. This is basically done by drawing a triangle at the proper angle the line is facing using a path.
final path = Path();
path.moveTo(p2.dx - arrowSize * math.cos(angle - arrowAngle),
p2.dy - arrowSize * math.sin(angle - arrowAngle));
path.lineTo(p2.dx, p2.dy);
path.lineTo(p2.dx - arrowSize * math.cos(angle + arrowAngle),
p2.dy - arrowSize * math.sin(angle + arrowAngle));
path.close();
canvas.drawPath(path, paint);
Which will finally give us:

In Flutter, is it possible to draw an image with a replaced color and opacity?

I have a grey image. I want to draw it using a color but with opacity to a canvas. By using Color Filter, I was able to draw it on the canvas by replacing the color. If I adjust the alpha value, it changes image opacity (partial grey/red). The following code snippet shows how to draw a grey image as red onto a canvas. But how can I draw the red image as transparent to the canvas?
static void drawBrushDetail(Canvas canvas, double cx, double cy, double brushSize, double imageAngleRad) {
canvas.translate(cx + brushSize / 2, cy + brushSize / 2);
ui.Image bt = imgBrushTip; //this is a gray image
Color selectedColor = getDrawColor(cx,cy);
final _paint = Paint();
var alphaVal=255;
//if I change alphaVal to 100, the final draw looks more gray instead of red with 100 Alpha
_paint.colorFilter = ColorFilter.mode(selectedColor.withAlpha(alphaVal), BlendMode.srcATop);
if (imageAngleRad != 0.0) canvas.rotate(imageAngleRad);
canvas.translate(-brushSize / 2, -brushSize / 2);
// draw image with specified brushSize
canvas.drawImageRect(bx, Rect.fromLTRB(0, 0, bt.width, bt.height),
Rect.fromLTRB(0, 0, brushSize, brushSize), _paint);
//traslate back, don't want to use save and restore layer since it's expensive
canvas.translate(brushSize / 2, brushSize / 2);
if (imageAngleRad != 0.0) canvas.rotate(-imageAngleRad);
canvas.translate(-cx - brushSize / 2, -cy - brushSize / 2);
}
See the image below. The above code will paint the image with solid red color. (image pointed by the red arrow). But I want the result to be an image with red color and transparent. Since in Flutter, there is no API to set alpha for canvas, don't know if it is possible.
I figured it out but the credit goes to pskink
I only need to draw one image(gray color) but I need to do the followings:
Resize and rotate the image.
Replace the gray image with red color.
Draw the image (now in red) with opacity.
Here is the code snippet.
static void drawBrushDetail1(Canvas canvas, double cx, double cy, double size) {
final _paint = Paint();
const opacity = 0.5;
_paint.color = Color.fromRGBO(0, 0, 0, opacity);
ui.Image bx = uiImg[0];
double scale = size / 60; //size is brush tip size and 60x60 is gray image size
canvas.drawAtlas(
bx,
[
RSTransform.fromComponents(
rotation: imageAngleRad,
scale: scale,
anchorX: 30,
anchorY: 30,
translateX: cx,
translateY: cy)
],
[
/* The size of gray image is 60 x 60 */
const Rect.fromLTWH(0, 0, 60, 60)
],
[Colors.red],
BlendMode.dstIn, /*This is replace the gray image wiht red color
null /* No need for cullRect */,
_paint); //_paint will paint the red image in 0.5 opacity
}

Flutter bezier curves automatically deciding the control point

I want to draw a bezier curve between the given two locations (offsets) with the flutter. I can see the following function available to do so,
https://api.flutter.dev/flutter/dart-ui/Path/quadraticBezierTo.html
Here we have to specify start and end points those I already know. And also have to specify the control point (x1,y1). I there a way we can generate this control point? May be to have more control of the curve we can have a factor (that decides how far away this point from the baseline) and based on that factor we can generate the control point.
Please find my scretch below
You can use cubicTo function
https://api.flutter.dev/flutter/dart-ui/Path/cubicTo.html
void cubicTo (
double x1,
double y1,
double x2,
double y2,
double x3,
double y3
)
this creates a bezier curve from the baseline (x1,y1) to (x2,y2) with (x3,y3) as control point.
class HalfCirclePainter extends CustomPainter {
HalfCirclePainter({this.scrollOffset});
final double scrollOffset;
#override
void paint(Canvas canvas, Size size) {
Paint circle = new Paint()
..color = Colors.red
..style = PaintingStyle.fill;
Path topCirclePath = new Path()
..moveTo(0, 0)
..lineTo(0, scrollOffset < size.height ? scrollOffset : size.height)
..cubicTo(
20,
size.height,
size.width - 20,
size.height,
size.width,
scrollOffset < size.height ? scrollOffset : size.height,
)
..lineTo(size.width, 0)
..lineTo(0, 0)
..close();
canvas.drawPath(topCirclePath, circle);
}
}
This code draws a semi circle, I hope this will be helpful.
Thank you for #pskink to help me. As you mentioned your answer as a comment and the code is not that readable when displaying in the comments. Here I am listing the code as the answer. So it is easy to grasp for someone who looking into this.
void drawBezierLine(Canvas canvas, Offset start, Offset end){
Offset middle = end/2 + start/2;
Offset baselineVector = end - start;
Offset controlPoint = middle + Offset(-baselineVector.dy, baselineVector.dx);
Paint paint = Paint()
..color = Colors.red
..style = PaintingStyle.stroke
..strokeWidth = 8.0;
var path = Path();
path.moveTo(start.dx, start.dy);
path.quadraticBezierTo(controlPoint.dx, controlPoint.dy, end.dx, end.dy);
canvas.drawPath(path, paint);
}
So here we can give start and end offsets to the function and it will draw the Bezier curve based on it. The hight of the Bezier(factor) curve corresponds to the length of the base line.

JavaFX 8, how to get center location of Scrollpane's Viewport

The Problem
I'm trying to figure out a way to get at which point in the content node the scroll pane's viewport is centered on.
To elaborate on the picture above, the big rectangle is the content (let's say a large image), and the small rectangle is the portion that is shown by the scroll pane. I'm trying to find x and y which would be coordinates from the top left of the content.
What I've Tried
My first thought was to use the getViewportBounds() method of the scroll pane and use its minX and maxX properties to determine the center x point:
Bounds b = scrollPane.getViewportBounds();
double centerX = (b.getMinX() + b.getMaxX()) / 2;
double centerY = (b.getMinY() + b.getMaxY()) / 2;
However, this doesn't work because these numbers are negative and don't seem to accurately describe the x and y I'm looking for anyways.
My next thought was to use the scroll pane's hValue and vValue to get the top left corner of the viewport relative to the content:
Bounds b = scrollPane.getViewportBounds();
double centerX = scrollPane.getHvalue() + b.getWidth() / 2;
double centerY = scrollPane.getVvalue() + b.getHeight() / 2;
This didn't work either though as the hValue and vValue seem to be way too large (when scrolled in only a few pixels, I'm getting numbers like 1600).
My Questions
I seem to have a fundamental misunderstanding of how the viewport works with a scroll pane.
What am I doing wrong here? Can someone explain where these numbers come from? How do I find x and y like in the picture above?
Let (x, y) be the be coordinates of the top, left point shown in the viewport. You can write this as
((contentWidth - viewportWidth) * hValueRel, (contentHeight - viewportHeight) * vValueRel)
vValueRel = vValue / vMax
hValueRel = hValue / hMax
This means assuming hmin and vmin remain 0 you can keep a circle in the center of like this:
// update circle position to be centered in the viewport
private void update() {
Bounds viewportBounds = scrollPane.getViewportBounds();
Bounds contentBounds = content.getBoundsInLocal();
double hRel = scrollPane.getHvalue() / scrollPane.getHmax();
double vRel = scrollPane.getVvalue() / scrollPane.getVmax();
double x = Math.max(0, (contentBounds.getWidth() - viewportBounds.getWidth()) * hRel) + viewportBounds.getWidth() / 2;
double y = Math.max(0, (contentBounds.getHeight() - viewportBounds.getHeight()) * vRel) + viewportBounds.getHeight() / 2;
Point2D localCoordinates = content.parentToLocal(x, y);
circle.setCenterX(localCoordinates.getX());
circle.setCenterY(localCoordinates.getY());
}
private Circle circle;
private Pane content;
private ScrollPane scrollPane;
#Override
public void start(Stage primaryStage) {
// create ui
circle = new Circle(10);
content = new Pane(circle);
content.setPrefSize(4000, 4000);
scrollPane = new ScrollPane(content);
Scene scene = new Scene(scrollPane, 400, 400);
// add listener to properties that may change
InvalidationListener l = o -> update();
content.layoutBoundsProperty().addListener(l);
scrollPane.viewportBoundsProperty().addListener(l);
scrollPane.hvalueProperty().addListener(l);
scrollPane.vvalueProperty().addListener(l);
scrollPane.hmaxProperty().addListener(l);
scrollPane.vmaxProperty().addListener(l);
scrollPane.hminProperty().addListener(l);
scrollPane.vminProperty().addListener(l);
primaryStage.setScene(scene);
primaryStage.show();
}