jonobr1 / two.js

A renderer agnostic two-dimensional drawing api for the web.
https://two.js.org
MIT License
8.27k stars 454 forks source link

[Question] Best way to render ~250k polygons #715

Closed paulers closed 5 months ago

paulers commented 7 months ago

I'm attempting to render around 250,000 polygons with various node counts (3-35) in Two.JS. Read a few open questions here already and it seems that the canvas is the best rendering mode for such a large amount of items. A problem I'm running into is that it's taking forever to render, hanging the UI thread in the browser.

Here's the code to generate just triangles, so only 3 points and 3 lines:

var json = { Polygons: [] };
for (var i = 0; i < 13920; i += 24) {
    for (var j = 0; j < 10200; j += 24) {
        json.Polygons.push({
            Points: [
                { X: i + 12, Y: j },
                { X: i + 24, Y: j + 24 },
                { X: i + 0, Y: j + 24 }
            ], Box: { L: i, T: i, R: i + 24, B: j + 24 }
        })
    }
}

// write these to a flattened file for use in 2js
const mappedCoordinates = json.Polygons.map(poly => poly.Points.map(pt => [pt.X, pt.Y]).flat());

This creates a file that looks like this (with obviously a lot more polygon data points in it):

[[12,0,24,24,0,24],[12,24,24,48,0,48],[12,48,24,72,0,72],[12,72,24,96,0,96]]

I then instantiate Two, create a group and iterate over the dataset above (flatData) to add shapes to the group. Fill and stroke are pulled in from some variables I omitted for brevity's sake.

const two = new Two({ fullscreen: false, type: Two.Types.canvas }).appendTo(this._container);
const scene = two.makeGroup();

for (let i = 0; i < flatData; i++) {
        var p = two.makePath(...flatData[i]);
        p.fill = fill;
        p.stroke = stroke;
        p.linewidth = 1;
        scene.add(p);
}
two.update();

This is where I run into trouble. The above loop takes somewhere in the vicinity of 8 minutes on a modern laptop. Is there a better way to approach this?

Environment (please select one):


Desktop (please complete the following information):

Additional context The Two.JS canvas is rendered as an overlay on top of OpenSeadragon, but this issue occurs without OSD too. Creating a canvas manually and rendering the triangles on the canvas inside a loop takes a second or two compared to Two.js so I feel like I'm missing something.

Thank you

jonobr1 commented 7 months ago

If you're going to render 10s of thousands of objects in real-time and be able to zoom in-and-out as the interaction model, then something like Three.js will suit you much better.

If you still want to move forward with Two.js then on initialization you'll want to defer how many Two.Paths you make per frame. This is a snippet how you'd be able to run the loop asynchronously:

let perFrame = 250;
let i = 0;

function add() {
  if (i >= flatData.length) {
    console.log('complete');
  }
  const p = new Two.Path(flatData[i]);
  scene.add(p);
  requestAnimationFrame(add);
}

Though, if you only need to render a static image (one that isn't interactive and animating in real-time). As hinted by the use of two.update() at the end of your snippet. Then you can combine this async logic and only use a handful of paths to keep the application running without hiccups. This example demonstrates that: https://codepen.io/jonobr1/pen/xxBqQxB

paulers commented 7 months ago

Thanks for your response. I don't need animation or interaction right now. I do however need to be able to render hundreds of thousands of polygons. Thanks for your example, I will apply it to my use case and report back!

jonobr1 commented 7 months ago

Cool, the way that it works is that it uses 1000 cached Two.Paths to draw 1M objects over the course of a few seconds.

paulers commented 7 months ago

Am I able to use the same strategy with drawing multi-vertex polygons? That is, I see the Two.Path constructor takes in vertices as an array, but it also has a vertices property (like stroke, fill, etc). Can I use the same strategy to reuse 1000 cached Two.Paths to update vertices and render the polygons I need?

jonobr1 commented 7 months ago

Yeah, you would use Two.Path and set the vertices property to a new array of Two.Anchors based on your data array.

paulers commented 5 months ago

Ended up going with manual canvas population for my specific scenario. Ends up taking ~50-200ms painting half a million non-complex polygons, which is acceptable.