HTML5 Canvas redraw-cycle performance optimisations
Asked Answered
F

2

16

We are building a CAD app that runs in a browser.

It's based on Paper.js, a very neat Canvas library that allows you to manipulate vectors programmatically.


The problem

The major issue I am having at the moment is redraw cycle performance.

The redraw algorithm is 'dumb' (in terms of clever hacks to improve performance) and thus inefficient and slow - Rendering the Scene Graph items is dependent on a progressively slower redraw-cycle.

As points-to-draw accumulate, each redraw cycle becomes slower and slower.

The redraw scheme is as simple as it gets:

  • clear the whole area
  • take all Items from the Scene Graph
  • redraw all Items.

The question

Are there any classroom examples of rendering optimizations in such cases - assuming I'd like to stop short of implementing a dirty-rectangles algorithm (drawing only areas that have changed)

Edit: I've experimented with manual on-the-spot rasterisation which works pretty good, I've posted an answer below.

Furnishings answered 27/5, 2014 at 8:3 Comment(8)
Why do you think paper.js is highly optimized or even optimized? The vast majority of libraries have extremely poor runtime performance: good performance requires huge amounts of code which is in conflict with having smallest size possiblePostmaster
Well, It might be, it might be not. I just trust what it's creators say. Combine that with the fact that it's oriented towards animation and I concluded that what they say is true. Even if it's not, there is no library out there to compete in terms of features I need for my app. It's API has me covered at least 80% this far.Furnishings
At least in npm, every library says that even if they are absurdly slow. But if paper.js has monopoly I guess it doesn't matter.Postmaster
Did you contact the developer(s) of paper.js? We often have a lot of communication with open source project members to talk about this kind of stuff. Also, supporting them might help them focus on the things you think are important for you, and the rest of the world.Blowzy
old school 3d engines used to do wonders using mesh simplification + level of detail handling based on camera/polygon distance.Not sure it is appliable in your case though.Kass
I am talking about 2D graphics, not 3D.Furnishings
Should not matter 2D or 3D...Kass
@NicholasKyriakides: You still should involve the developers of the library, they likely can show you how to integrate performance optimisations best in their models. Maybe they even already have ideas on what to do, but just didn't have the time for implementing them or thought they didn't needed them.Kletter
F
9

This can be done with rasterization in a process/technique similar to Bitmap Caching.

The issue with high node-count Scene Graphs is that rendering them causes the rendering engine to groan. The browser has to traverse their nodes and render their pixels on the canvas.

So here's a nice solution:


1. Render a bitmap but keep the original shape below, hidden

The solution is to replace the vectors with images, rasterizing them - only when rendering, but still keeping the original shape below it's image copy, in a hidden state only when inactive(not being currently manipulated).

On clicking the images - we remove them and toggle the visibility of the original shape. This way inactive shapes are rendered as images and active shapes are released from their bitmap representation and act as vectors, free to be manipulated around. When not active they just sit there invinsible with their Raster copy on top of them.

This allows the engine to keep the vector representation of the shapes but avoids rendering them as vectors - instead images that look similar to them are layered on top of them.

1000's of path commands are essentially replaced by a single image - but only when rendering - the original path actually exists as an object in the Scene Graph, or whatever type of DOM you are using

2. Rasterize in groups

The trick is to perform the rasterization in groups - group 10-15 shapes together and rasterize them as a single image. This keeps the raster count low. On clicking an image - we can release the whole group or just the item that was clicked on.

3. Attach click handlers on the group to reinstate the vector copy when reactivated

When rasterizing a group, we can simply attach a click handler on it, so when clicked we toggle bitmap with vector. Images do not behave the same as vectors when hit testing - images are squares by nature and cannot be assymetrically hit-tested. While a vector considers it's edges to be on it's path boundaries - an image considers it's boundaries to be it's whole bounding box. The solution is when clicking on the image to actually hit-test the click point with the vector path below the image - if it returns true then perform a release.

Furnishings answered 29/12, 2014 at 10:18 Comment(0)
L
9

Useful tool

My branch of paper.js could help, but it is maybe not the best fit for you.

It enables you to prevent paper.js to redraw everything every frames (use paper.view.persistence = 1;).

This way you have better control over what to clear and should be redrawn: for example when you move a shape, you can clear the area where it was (using native canvas drawRect for instance), and update it once it is moved (use path.needsUpdate();).

Drawback

The problems come when shapes intersect. If you want to modify a shape which intersect another one, you will have to update both. Same thing if the second shape intersects a third one, and so one and so forth.

So you need a recursive function, not hard to code, but it can be costly if there are many complexe shapes intersecting, and so you might not gain performances in this case.

(Update) Bitmap caching

As suggested by Nicholas Kyriakides in the following answer, Bitmap caching is a very good solution.

One canvas per shape

An alternative would be to draw each shape on a separate canvas (working as layers). This way you can freely clear and redraw each shape independently. You can detach the onFrame event of the views which are not changing (all canvas except the one on which the user is working). This should be easier, but it leads to other small problems such as sharing the same project view parameters (in the case of zoom), and it might be costly with many shapes (which means many canvas).

Static and dynamic canvas

A (probably) better approach would be to have only two canvas, one for the static shapes, and one for the active shape. The static shapes canvas would contain all shapes (except the one being edited) and would be redrawn just when the user start and stop editing the active shape. When the user starts editing a shape it would be transferred from the static canvas to the dynamic one, and the the other way when the user stops.

Lonnylonslesaunier answered 29/5, 2014 at 13:46 Comment(8)
The first method is basically a no-no, but its more due to me wanting a silver bullet - Shapes are expected to intersect due to the nature of the concept for which I am building the app for.Therefore I see adding a lot of complexity for minimal-to-no gains in performance. The ''alternative way'' has many traps. I would also need to pass the hit tests, intersections tests etc to the canvasses below since the user selects items with the mouse - adding to a lot of complexity. I guess only an SVG backend can solve my problems.I'll upvote this so if no better answers come up you get the bounty thoFurnishings
Yes, SVG is maybe the best option. Altough performances with SVG may not be the best, it might be better in case of complexe dynamic drawings ( see second question on paperjs.org/about/faq ). Did you ask Jürg Lehni on the paper.js mailing list about the SVG backend? I think he told me that he planned to implement it, but I don't know how far it is completed.Lonnylonslesaunier
Yes I did mail him, but I am guessing that he has other stuff to tend to other than questions about the library - In short he did not reply as of yet. Would you reckon that I would see a significant performance increase with an SVG backend though or should I start holding back from expecting too much out of a CAD application in JavaScript?Furnishings
paperjs.org/about/roadmap (I didn't see SVG backend, but the last point is interesting)Lonnylonslesaunier
The last point would still embed a Javascript engine, so I don't think it would be any more different than a browser environment. Same speed. It is however very interesting!Furnishings
Well it should be easy to try loading many complex SVG curves, and manipulate them to see how it goes. You can even export the curves you built with stylii.Lonnylonslesaunier
Thanks for the persistence modification @Lonnylonslesaunier - are you planning to make a PR for this?Gambrinus
"PR" is for push request? No I won't since the authors do not want to support this feature. My branch is mostly usefull when you want to have persistance (trails) in paper.js, but it is often better to use bitmap caching if you want to avoid redrawing everything at each frame.Lonnylonslesaunier
F
9

This can be done with rasterization in a process/technique similar to Bitmap Caching.

The issue with high node-count Scene Graphs is that rendering them causes the rendering engine to groan. The browser has to traverse their nodes and render their pixels on the canvas.

So here's a nice solution:


1. Render a bitmap but keep the original shape below, hidden

The solution is to replace the vectors with images, rasterizing them - only when rendering, but still keeping the original shape below it's image copy, in a hidden state only when inactive(not being currently manipulated).

On clicking the images - we remove them and toggle the visibility of the original shape. This way inactive shapes are rendered as images and active shapes are released from their bitmap representation and act as vectors, free to be manipulated around. When not active they just sit there invinsible with their Raster copy on top of them.

This allows the engine to keep the vector representation of the shapes but avoids rendering them as vectors - instead images that look similar to them are layered on top of them.

1000's of path commands are essentially replaced by a single image - but only when rendering - the original path actually exists as an object in the Scene Graph, or whatever type of DOM you are using

2. Rasterize in groups

The trick is to perform the rasterization in groups - group 10-15 shapes together and rasterize them as a single image. This keeps the raster count low. On clicking an image - we can release the whole group or just the item that was clicked on.

3. Attach click handlers on the group to reinstate the vector copy when reactivated

When rasterizing a group, we can simply attach a click handler on it, so when clicked we toggle bitmap with vector. Images do not behave the same as vectors when hit testing - images are squares by nature and cannot be assymetrically hit-tested. While a vector considers it's edges to be on it's path boundaries - an image considers it's boundaries to be it's whole bounding box. The solution is when clicking on the image to actually hit-test the click point with the vector path below the image - if it returns true then perform a release.

Furnishings answered 29/12, 2014 at 10:18 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.