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.
Related
If I have the following chart in Flutter:
Where the green graph is a Path object with many lineTo segments, how do I find the y-coordinate for a point with a given x-coordinate?
As you can see in the image, there is a gray dotted line at a specific point on the x-axis and I want to draw a point where it intersects with the green graph.
Here is an example path:
final path = Path();
path.moveTo(0, 200);
path.lineTo(10, 210);
path.lineTo(30, 190);
path.lineTo(55, 150);
path.lineTo(80, 205);
path.lineTo(100, 0);
And I want to find the y-coordinate for the point at dx = 75.
The easiest way to achieve this for any path that only has a single point for every x (i.e. where there is only a single graph / line from left to right) is using the binary search algorithm.
You can then simply use the distance of the path, which is obtained using Path.computeMetrics, to perform binary search and find the offset via Path.getTangentForOffset:
const searchDx = 75;
const iterations = 12;
final pathMetric = path.computeMetrics().first;
final pathDistance = pathMetric.length;
late Offset closestOffset;
var closestDistance = pathDistance / 2;
for (var n = 1; n <= iterations; n++) {
closestOffset = pathMetric.getTangentForOffset(closestDistance)!.position;
if (closestOffset.dx == searchDx) break;
final change = pathDistance / pow(2, n);
if (closestOffset.dx < searchDx) {
closestDistance += change;
} else {
closestDistance -= change;
}
}
print(closestOffset); // Offset(75.0, 193.9)
Note that if you want to run significantly more iterations (which should not be necessary due to the nature of binary search), you should replace final change = pathDistance / pow(2, n); by a cheaper operation like storing the left and right points of your current search interval.
You can find the full working code as an example on Dartpad.
I just started learning monogame a couple of days ago and I was wondering how I can draw a line from a Start Vector2 and an End Vector2 with a specific line thickness?
Should I use a one pixel image to draw it onto the screen and then use a Bresenham's line algorithm to find there positions, or is there a more optimized and less complicated method to do this using monogames built in functions?
One way is to create a Texture2D with a width that is the distance between the two Vector2s and a height that is the desired width. Then you apply a rotation to the texture when you draw it.
Here is an example (as a SpriteBatch extension method):
public static void DrawLineBetween(
this SpriteBatch spriteBatch,
Vector2 startPos,
Vector2 endPos,
int thickness,
Color color)
{
// Create a texture as wide as the distance between two points and as high as
// the desired thickness of the line.
var distance = (int)Vector2.Distance(startPos, endPos);
var texture = new Texture2D(spriteBatch.GraphicsDevice, distance, thickness);
// Fill texture with given color.
var data = new Color[distance * thickness];
for (int i = 0; i < data.Length; i++)
{
data[i] = color;
}
texture.SetData(data);
// Rotate about the beginning middle of the line.
var rotation = (float)Math.Atan2(endPos.Y - startPos.Y, endPos.X - startPos.X);
var origin = new Vector2(0, thickness / 2);
spriteBatch.Draw(
texture,
startPos,
null,
Color.White,
rotation,
origin,
1.0f,
SpriteEffects.None,
1.0f);
}
Example of use:
var startPos = new Vector2(0, 0);
var endPos = new Vector2(800, 480);
_spriteBatch.DrawLineBetween(startPos, endPos, 12, Color.White);
How it looks:
It's not a perfect solution. You'll want to modify it if you want to draw connected lines with different angles without any visible seams. Also not sure about the performance.
I use a library I found. It draws a number of 2D primitives, like lines, boxes, etc. Really easy to use. Called "C3.MonoGame.Primitives2D", it can be found here:
https://github.com/z2oh/C3.MonoGame.Primitives2D
Here's a screenshot of a demo I wrote using many of its methods:
It's just one file of around 500 lines. If you don't like Git or using libraries, you can just copy & paste it into your project.
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());
}
I am re-writing a game a made using easlejs with Box2Dweb to flutter's flame engine with the dart port of box2d. My problem is that the objects are move really really slow. Gravity setting seems to progress in a linear fashion.
I read about scale factors etc... just don't know how to link it all. The World class does not have that,
can someone show me an example of how to setup the initial screen to box2d world ratios?
I get the screenSize using flames resize override I want to use that to set the scale or whatever works.
The examples in GitHub never seem to use that and even when I download then and run them... again painfully slow falling bodies.
A simple screen with a circle or a Square falling (correctly) will be appreciated.
Here's how I instantiate the code. (I need the object to be 80x80 pixels)
class MyGame extends Game with TapDetector {
MyGame() : _world = Box2D.World.withGravity(Box2D.Vector2(0, 10)) {
_world.setAllowSleep(true);
spawnBlocks();
}
void createSquare(int index, double w, double h, double x, double y) {
int randomNumber = random.nextInt(letters.length);
var bodyDef = Box2D.BodyDef();
bodyDef.type = Box2D.BodyType.DYNAMIC;
bodyDef.position = Box2D.Vector2(x - 5, y);
bodyDef.angle = 0;
dynamicBody = _world.createBody(bodyDef);
dynamicBody.userData = letters[randomNumber];
var boxShape = Box2D.PolygonShape();
boxShape.setAsBox(w / 2, h / 2, Box2D.Vector2(w / 2, -h * 2), 0);
var boxFixtureDef = Box2D.FixtureDef();
boxFixtureDef.shape = boxShape;
boxFixtureDef.density = 0;
boxFixtureDef.restitution = 0;
boxFixtureDef.friction = 1;
dynamicBody.createFixtureFromFixtureDef(boxFixtureDef);
blocks.add(dynamicBody);
}
spawnBlocks() {
for (var i = 0; i < 8; i++) {
createSquare(
i, blockWidth, blockHeight, blockWidth * i + 18, -100 * i.toDouble());
}
}
}
It doesn't matter how high I set the gravity, the body still falls at the same speed.
Even when they hit the floor they bounce very slowly, I have used the body.setTransform to increase the position.y etc but it just seems to move right through the static body(floor).
Since the density of your square is 0 it is not affected by gravity, try to set it to something higher and see if the objects are more affected.
Don't use the body.setTransform if you don't really need to, since it will break the physics set up in the world.
Did you try this example?
https://github.com/flame-engine/flame/tree/master/doc/examples/box2d/contact_callbacks
And don't forget to add the scale argument to your world, otherwise you'll hit the speed limits quite fast since you'll be so zoomed out.
I'm the maintainer of box2d for flame (now Forge2D), if you have more questions you can join the box2d channel on our discord and I'll try to help you.
I have an image UI in a canvas with Screen Space - Camera render mode. What I like to do is move my LineRenderer to the image vertical position by looping through all the LineRenderer positions and changing its y axis. My problem is I cant get the correct position of the image that the LineRenderer can understand. I've tried using ViewportToWorldPoint and ScreenToWorldPoint but its not the same position.
Vector3 val = Camera.main.ViewportToWorldPoint(new Vector3(image.transform.position.x, image.transform.position.y, Camera.main.nearClipPlane));
for (int i = 0; i < newListOfPoints.Count; i++)
{
line.SetPosition(i, new Vector3(newListOfPoints[i].x, val.y, newListOfPoints[i].z));
}
Screenshot result using Vector3 val = Camera.main.ScreenToWorldPoint(new Vector3(image.transform.localPosition.x, image.transform.localPosition.y, -10));
The green LineRenderer is the result of changing the y position. It should be at the bottom of the square image.
Wow, this was annoying and complicated.
Here's the code I ended up with. The code in your question is the bottom half of the Update() function. The only thing I changed is what was passed into the ScreenToWorldPoint() method. That value is calculated in the upper half of the Update() function.
The RectTransformToScreenSpace() function was adapted from this Unity Answer post1 about getting the screen space coordinates of a RectTransform (which is exactly what we want in order to convert from screen space coordinates back into world space!) The only difference is that I was getting inverse Y values, so I changed from Screen.height - transform.position.y to just transform.position.y which did the trick perfectly.
After that it was just a matter of grabbing that rectangle's lower left corner, making it a Vector3 instead of a Vector2, and passing it back into ScreenToWorldPoint(). The only trick there was because of the perspective camera, I needed to know how far away the line was from the camera originally in order to maintain that same distance (otherwise the line moves up and down the screen faster than the image). For an orthographic camera, this value can be anything.
void Update () {
//the new bits:
float dist = (Camera.main.transform.position - newListOfPoints[0]).magnitude;
Rect r = RectTransformToScreenSpace((RectTransform)image.transform);
Vector3 v3 = new Vector3(r.xMin, r.yMin, dist);
//more or less original code:
Vector3 val = Camera.main.ScreenToWorldPoint(v3);
for(int i = 0; i < newListOfPoints.Count; i++) {
line.SetPosition(i, new Vector3(newListOfPoints[i].x, val.y, newListOfPoints[i].z));
}
}
//helper function:
public static Rect RectTransformToScreenSpace(RectTransform transform) {
Vector2 size = Vector2.Scale(transform.rect.size, transform.lossyScale);
Rect rect = new Rect(transform.position.x, transform.position.y, size.x, size.y);
rect.x -= (transform.pivot.x * size.x);
rect.y -= ((1.0f - transform.pivot.y) * size.y);
return rect;
}
1And finding that post from a generalized search on "how do I get the screen coordinates of a UI object" was not easy. A bunch of other posts came up and had some code, but none of it did what I wanted (including converting screen space coordinates back into world space coordinates of the UI object which was stupid easy and not reversibe, thanks RectTransformUtility!)