We are building a CAD app that runs in a browser.
- C.A.D stands for Computer Aided Design.
- Illustrator, CorelDraw, AutoCAD etc are some examples of CAD apps.
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.
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.
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.