I have been trying to develop a drawing app and all the example codes including the one's from The Boring Flutter Development Show don't translate well into real-world usage.
The main problem being that CustomPaint's paint operation is too expensive and re-draws every point, every frame. And as the points increase the flutter app's render time per frame increases significantly
From the time I spent finding a solution to this problem, I found these
RepaintBoundary : Rasterizes layers
Custom Widget using SingleChildRenderObjectWidget and RenderProxyBox : must implement a paint method and no way of passing a controller
I don't think any of the above solutions work well for my needs: smooth canvas drawing operations without re-paint. I even tried simplifying the points and that didn't work either because of the inherent mechanism of CustomPaint
If there was a way to pass a canvas as a widget and attaching a controller it'll be easy to store the captured points and use basic canvas operations like canvas.drawPath() or canvas.drawLine() much efficiently
Any suggestion would be helpful. Thank you!
This is the Controller class
class DrawingController extends ChangeNotifier {
List<Offset> _points = []
.... // Perform operations on data points
....
void add(Offset point) {
_points.add(point);
notifyListeners();
}
}
This is the CustomPaint class
class Painter extends CustomPainter {
DrawingController controller;
Painter(this.controller) : super(repaint: controller); //This is important
#override
void paint(Canvas canvas, Size size) {
//Paint function
}
#override
bool shouldRepaint(CustomPainter oldDelegate) => true;
}
And in the Gesture detector simply call controller.add(point) to add new points as you grab them
I am not sure this is the most efficient way to paint but it reduced the paint times down to 13ms on a 60hz screen and 9ms on a 120Hz screen. One noticeable drawback is that a single stroke (onTapdownEvent to an onDragEndEvent) renders quite slowly(up to 18ms) when the stroke has many points. This problem disappears as soon as you start a new stroke. I tried asking on several forums and this is the best I could come up with after digging through the flutter source code.
I profiled this functionality on a snapdragon 855 device so the processor is not a bottleneck. If anyone finds a better solution please post it.
A helpful tip is to add a Timer.run(setState()). This way it only updates when it can. Instead of loading update1 update2 update3...etc. It loads update1 then if it can it loads update2 but if update1 took to long it goes to update3. In general it just looks smoother but doesn't necessarily speed it up. Sample:
scaleStart(ScaleStartDetails d) => startPoint = d.focalPoint - _offset;
scaleUpdate(ScaleUpdateDetails d) {
_offset = d.focalPoint - startPoint;
scale = min(max(1, scale * (d.scale + 13) / 14), 128);
Timer.run(() => setState(() {}));
}
Related
I'm trying to build a simple side-scroller game with a level built of square tiles using Flame.
#override
Future<void> onLoad() async {
final orange = Paint()..color = Color(0xFFFF9000);
final double tileSize = 64;
for (var i = 0; i <= 100; i++) {
add(RectangleComponent(position: Vector2(i * tileSize, 100), size: Vector2.all(tileSize), paint: orange));
}
}
When the screen is static, everything works as expected. However, when I add camera movement, I see vertical lines between the tiles.
#override
void update(double dt) {
camera.moveTo((Vector2(camera.position.x + 1, 0)));
super.update(dt);
}
I suspect it might be something to do with Flutter antialiasing bug. Does anybody know if there's a workaround? Thanks!
This is indeed that bug, we have it documented on a few of our issues too (like this one for example https://github.com/flame-engine/flame/issues/1888)
You can try to use Impeller if that is an option for you, where this bug shouldn't be present.
Other workarounds would be to draw the same color underneath the tiles where they are overlapping, that is of course not always possible though.
And the other option is to use a camera that only moves in full pixels, so that there are no rounding errors.
From this example you can drag around(not at the draggable point but some factor) a BodyComponent by setting body.applyLinearImpulse.
If I try to drag around, setting body.position I get no effect. I was hoping to be able to drag the BodyComponent around at the specific drag point.
Sample code line 37:
#override
bool onDragUpdate(DragUpdateInfo info) {
// final worldDelta = Vector2(1, -1)..multiply(info.delta.game);
// body.applyLinearImpulse(worldDelta * 1000);
body.position.setFrom(info.delta.game);
return false;
}
You should never manipulate body.position directly, since that could break the stability of the physics engine.
For your use-case you probably want to use a MouseJoint instead:
https://github.com/flame-engine/flame/blob/ba617790e4a7ca4dc03f4a2e29de43d42efd3482/packages/flame_forge2d/example/lib/mouse_joint_sample.dart
I am trying to apply a sequence of ImageFilters to the currently drawn Canvas layer.
canvas.drawCircle(...)
canvas.drawCircle(...)
// TODO Apply ImageFilter 1: Blur
// TODO Apply ImageFilter 2: Blur again
Is there an easy way to do that? I'm likely missing some simple method, but I could not find anything...
Things I have tried:
I know that I can specify the combined ImageFilter in Paint, but this does not work - I want to apply them in sequence after all shapes are drawn, rather to each shape separately.
saveLayer seems to be the closest to achieve what I need, but according to docs it only applies ColorFilter. They also say it is a bit inefficient.
saveLayer(null, Paint()..colorFilter = filter2)
saveLayer(null, Paint()..colorFilter = filter1)
..
canvas.draw
canvas.draw
...
restore()
restore()
Thank you!
For the future reference, I was able to achieve the desired effect by using the RenderBox (this can be used for example from LeafRenderObjectWidget).
The RenderBox has the paint method:
#override
void paint(PaintingContext context, Offset offset) {
context.pushLayer(
ColorFilterLayer(colorFilter: ColorFilter.matrix(...)),
(PaintingContext context2, Offset offset2) =>
context2.pushLayer(
ImageFilterLayer(imageFilter: ui.ImageFilter.blur(...)),
(PaintingContext context3, Offset offset3) =>
simulation.draw(context3.canvas),
offset2),
offset);
Not that the simulation.draw(context3.canvas) will draw in a canvas that is followed by the blur ImageFilter and then by the ColorFilter. Any number of Layers can be added, the syntax is a bit painful though. It can be simplified with some effort if needed:
/// Returns the painter with the given layer applied.
PaintingContextCallback withLayer(
ContainerLayer layer, PaintingContextCallback painter) {
return (PaintingContext context, Offset offset) =>
context.pushLayer(layer, painter, offset);
}
...
final List<ContainerLayer> layers = [...];
var painter = ...
for (final layer in layers) {
painter = withLayer(layer, painter);
}
painter(context, offset);
I've been working on switching my applications from Swing to JavaFX. I've been working on a room escape game which displays a description of the item on which the user clicks. In Swing, I'd subclass JComponent and override the paintComponent(Graphics) method. I could draw the text there, knowing that the method is constantly called to update the screen. However, using the JavaFX Canvas, there is no method that is called constantly, which makes this task harder. I attempted save()ing the GraphicsContext after I drew the images and called restore() when I wanted to remove the text, but to no avail. Here's the important code:
package me.nrubin29.jescape;
import javafx.application.Platform;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.shape.Rectangle;
import java.util.Timer;
import java.util.TimerTask;
public class RoomPane extends Canvas {
private Room room;
private Toast toast;
public RoomPane() {
super(640, 480);
setOnMouseClicked(e -> {
for (JObject o : room.getObjects()) {
if (o.getBounds().contains(e.getX(), e.getY())) {
toast = new Toast(o.getDescription());
}
}
});
new Timer().schedule(new TimerTask() {
#Override
public void run() {
if (toast == null) {
return;
}
if (toast.decrement()) { // Decrements the internal counter. If the count is 0, this method returns true.
toast = null;
Platform.runLater(() -> getGraphicsContext2D().restore());
}
else {
Platform.runLater(() -> getGraphicsContext2D().strokeText(toast.getText(), 300, 100));
}
}
}, 0, 1000);
}
public void changeRoom(Room room) {
this.room = room;
GraphicsContext g = getGraphicsContext2D();
g.drawImage(room.getBackground(), 0, 0);
for (JObject o : room.getObjects()) {
g.drawImage(o.getImage(), getCenterX(o.getBounds()), getCenterY(o.getBounds()));
}
g.save();
}
}
I attempted save()ing the GraphicsContext after I drew the images and called restore() when I wanted to remove the text, but to no avail.
save and restore have nothing to with removing things like text, what they do is save in a stack the state of various settings like a stroke or fill to use to draw shapes and allow them to be popped off the stack for application later. Those routines don't effect the pixels drawn on the canvas at all.
To remove something from a GraphicsContext, you can either draw over the of it, or clear it. For your code, what you could do is snapshot the canvas node where you are trying to save it, then draw your snapshot image onto the canvas where you are trying to restore it. It is probably not the most efficient way of handling drawing (a smarter routine which just draws only damaged area where the text is would be better, but probably not required for your simple game).
However, using the JavaFX Canvas, there is no method that is called constantly
Rather than using a timer to trigger canvas calls, use a AnimationTimer or a Timeline. The AnimationTimer has a callback method which is invoked every pulse (60 times a second, or as fast as JavaFX can render frames, whichever is the lesser), so it gives you an efficient hook into the JavaFX pulse based rendering system. The Timeline can have keyframes which are invoked at user specified durations and each keyframe can have an event handler callback which is invoked at that duration.
Using the built-in JavaFX animation framework, you don't have to worry about multi-threading issues and doing things like Platform.runLater which overly complicate your code and can easily lead to subtle and serious errors.
On a kind of unrelated note, for a simple game like this, IMO you are probably better off recoding it completely to use the JavaFX scene graph rather than a canvas. That way you will be working at a higher level of abstraction rather than clip areas and repainting damaged paint components.
I'm looking for best solution - I'm drawing a hexagonal map (Civilization like :) ) for my browser based game in GWT. Currently I'm drawing it on canvas which basically works.
However - I'm also able to slide map left,right,down and up to see another fragment of the map. In such situation I'm forced to redraw the whole map - which doesn't look too good, and will probaly cause preformance issues when the map gets more complex.
Is there a better approach for that? Some library? Or maybe I should make every hex a button like widget. so I could be able to move it instead creating from the scratch... but what about different resolutions then... I'm affraid the map could tear apart sometimes...
You could try doing it will simple DIVs (FlowPanel, HTMLPanel, etc) and style the hexagons using CSS. See this tutorial: http://jtauber.github.com/articles/css-hexagon.html
There are some neat suggestions here as well: html/css hexagon with image inside and hexagonal shaped cells in html
You could draw the entire map (usually just the relatively static background, not the animations!) in a hidden canvas, and then copy the parts you need to your visible canvas:
final Canvas hiddenCanvas = buildCanvas(1000, 1000);
drawHexPattern(hiddenCanvas);
// don't add the hiddenCanvas to your DOM
final Canvas visibleCanvas = buildCanvas(320, 200);
RootPanel.get().add(visibleCanvas); // add the visibleCanvas to the DOM
showArea(hiddenCanvas, visibleCanvas, 0, 0);
...
showArea(hiddenCanvas, visibleCanvas, 20, 10);
...
With a showArea() method like
private void showArea(final Canvas hiddenCanvas, final Canvas visibleCanvas,
final double x, final double y) {
final Context2d hiddenCtx = hiddenCanvas.getContext2d();
final Context2d visibleCtx = visibleCanvas.getContext2d();
final ImageData imageData = hiddenCtx.getImageData(x, y,
visibleCanvas.getCoordinateSpaceWidth(),
visibleCanvas.getCoordinateSpaceHeight());
visibleCtx.putImageData(imageData, 0, 0);
}
I created a pastebin with a working example: http://pastebin.com/tPN2093a