Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to animate millions of points on browser canvas using two.js #691

Open
1 of 3 tasks
rjha opened this issue Feb 5, 2023 · 12 comments
Open
1 of 3 tasks

How to animate millions of points on browser canvas using two.js #691

rjha opened this issue Feb 5, 2023 · 12 comments
Labels

Comments

@rjha
Copy link

rjha commented Feb 5, 2023

Describe your question
Hi

What would the best way to output a lot of dots (1-pixel circle or rectangle) using two.js library? I am using two.js to plot bifurcation diagrams and Julia sets that involve outputting a lot of dots on the screen. For example to plot a 500x 500 grid, I need to output 25,0000 pixels.

My current method is to render a portion of grid inside animation update handler by adding dots to two.js canvas using makeRectangle method. While this works for 500 x 500 grid, it fails for 800x 800 grid. The browser tab just keeps snapping. I suspect some call stack is getting filled. Also, the browser tab reload just wont work even for 500 x 500 grid.

There are no external dependencies and I am using simple ES6 Js with Vanilla html. The reason to select two.js was animation and drawing primitives support.

Your code (either pasted here, or a link to a hosted example)
You can simply clone and run mandlebrot.html or any of other examples. The folder is self contained.
https://github.com/rjha/chaos

`drawPixel(config) {

        let side = config.radius || 1.0 ;
        let color = config.color || "black";
        let pixel = this.#mapPixel(this.#current);
        let square = this.#two.makeRectangle(pixel.x, pixel.y, side, side);

        // dot props
        square.fill = color;
        square.opacity = 1.0;
        // stroke will hide 
        // the color for small dots
        square.noStroke();
        
    }`

`

Screenshots

julia-dragon.png

Environment (please select one):

  • Code executes in browser (e.g: using script tag to load library)
  • Packaged software (e.g: ES6 imports, react, angular, vue.js)
  • Running headless (usually Node.js)

If applicable:

Desktop (please complete the following information):

  • OS: [Mac OSX 12.6]
  • Browser [Chrome]
  • Version [109.0.5414.119 (Official Build) (arm64)]

Additional context
The drawing is done by generating a list of commands for each dot on the grid inside the animation update handler. The commands are then processed to output a 1x1 pixel using two.js.

`

const points = 400;
var xp = 0;

  function update_grid(frameCount) {

    // get pixels for [x y] range 
    let pixels = this.plotter.mandlebrot.render(xp,  xp + 10, 0, points);

    for(let i =0; i < pixels.length; i++) {

      let pixel = pixels[i];
      this.plotter.add(['DOT', {
        "x": pixel.x, 
        "y": pixel.y,
        "radius": 1.0,
        "color":pixel.color 
      }]);

    }

    this.plotter.executeAll(); 
    // for next frame 
    xp = xp + 10; 
    
    if(xp > points) {
      this.pause();
      console.log("stop@ xp= %d,", xp);
    }

  }

`

`
// The DOT is processed
case 'DOT':

                let config = {
                    "color": args.color || "black",
                    "radius": args.radius || 1
                }

                this.createDot(args.x, args.y, config);
                break;

// call createDot
this.#moveTo(x, y);

        try {

            this.#drawPixel({
                "color": config.color,
                "radius": config.radius 
            });

....

// two.js primitives
#drawPixel(config) {

        let side = config.radius || 1.0 ;
        let color = config.color || "black";
        let pixel = this.#mapPixel(this.#current);
        let square = this.#two.makeRectangle(pixel.x, pixel.y, side, side);

        // dot props
        square.fill = color;
        square.opacity = 1.0;
        // stroke will hide 
        // the color for small dots
        square.noStroke();
        
    }

`

@rjha rjha added the question label Feb 5, 2023
@rjha
Copy link
Author

rjha commented Feb 5, 2023

updated screenshot image

@jonobr1
Copy link
Owner

jonobr1 commented Feb 7, 2023

Thanks for posting your question. Two.js is unlikely to be able to render millions of points every frame and stay at 30+ FPS. There are two approaches to get you closer depending on the types of animation that you're trying to do.

If the animation is like a timelapse adding new points to a still image then the way to render this in Two.js is to have a couple of rectangles and render them in different positions over time without clearing the background. Like so:

const two = new Two({
  type: Two.Types.canvas,
  overdraw: true,
}).appendTo(document.body);

const shape = new Two.Rectangle(0, 0, 1, 1);
two.bind('update', function() {
  const x = Math.random() * two.width;
  const y = Math.random() * two.height;
  shape.position.x = x;
  shape.position.y = y;
});

two.play();

If you're using the CanvasRenderer, then there's also the option to use the Two.Points object (link). Here's an example of how points work: https://jsfiddle.net/jonobr1/r2zumg0w/9/

Hope this helps

@rjha
Copy link
Author

rjha commented Feb 8, 2023

Hi Jono

Thanks for the quick response. I really appreciate that. My use case aligns with the keep adding new points to the canvas approach. The Two.Points approach would render all at once so I do not want to take that plus I want more control on the points. I will try out the first approach and update.

However, in the snippet, should the shape not be a variable instead of a constant? Also, how would the two instance know about shape?

@jonobr1
Copy link
Owner

jonobr1 commented Feb 8, 2023

Yes sorry, here's a working Codepen example to expand on that snippet: https://codepen.io/jonobr1/pen/MWBdPBy

You'd wanna replace the random positioning of the shape to whatever data points you have from your algorithm.

@rjha
Copy link
Author

rjha commented Feb 10, 2023

Hi Jono
Many thanks. I got that working. Now I am tiling using a vertical line across the screen on each frame update. So I need (num_x_pixels / 60) seconds for rendering. The problem now is showing the rendering for high resolutions on physical screen.

(a) To show bigger images inside the viewport, I tried passing a canvas ID as domElement whose height and width I can control using css. However it looks like two.js is overriding this canvas height and width.

(b) Tried using fitted, however I still paint outside my physical screen.

What would be the recommended way to show the canvas inside the physical screen dimensions?

Thanks

@rjha
Copy link
Author

rjha commented Feb 10, 2023

result at 800 x800

Screen-Shot-2023-02-10-at-11-23-21-AM.png

@rjha
Copy link
Author

rjha commented Feb 10, 2023

I am testing with ratio for canvas renderer. I will update my findings.

@jonobr1
Copy link
Owner

jonobr1 commented Feb 10, 2023

You can pass the canvas element (not id) to control afterwards. Or you can also use two.renderer.setSize method. Like so:

const two = new Two({
  domElement: document.querySelector('canvas')
});

// or

const two = new Two({
  width: 800,
  height: 800
});

// At some later point in your application
// life cycle

two.renderer.setSize(4096, 4096);

@rjha
Copy link
Author

rjha commented Feb 14, 2023

Hi Jono
I have played with the options and looked at the Canvas renderer code. The above fix will not work because the setSize() method is forcing the DOM element css property. So even if I set the DOM element for a certain size via css, the setSize() will overwrite that.

    // @debug
    // comment this to honor css styles!
    if (this.domElement.style) {
      _.extend(this.domElement.style, {
        width: width + "px",
        height: height + "px"
      });
    }

    // @debug - should we pass this.ratio instead of ratio
    return this.trigger(Events.Types.resize, width, height, ratio);

IMHO, this is a bug because user may want to keep the original applied css and there should be a flag to honor that. Should I file an issue?

Also, 2 more observations.

(a) should we not passing this.ratio instead of ratio to the trigger? We have already figured out the ratio here and then why are we passing the old value downstream?

(b) getBackingStoreRatio() can be deprecated. from what I could see in Chrome bug reports, getRatio() can simply be,
devicePixelRatio || 1;

backingStoreRatio is never different from 1 and broken test cases is the only reason to carry it forward.

@jonobr1
Copy link
Owner

jonobr1 commented Feb 16, 2023

Thanks for the input @rjha.

Two.js manages the dimensions and the styles so you don't have to. That being said, you can always modify the <canvas /> element yourself via two.renderer.domElement and bypass any other methods.

However, what is the use case you're trying to set the size for? If the dimensions are fixed then I recommend simply passing them directly when instantiating Two.js:

// Makes an instance of Two.js that
// has width = 800
// and height = 800
// where under the hood
// the entire scene is scaled up
// to match the `devicePixelRatio`
// This is great for matching two.js
// to whatever device capabilities
// are available.
const two = new Two({
  type: Two.Types.canvas,
  width: 800,
  height: 800
});

// Makes an instance of Two.js that
// has a higher (and more explicit) ratio
// This is great for print, or rendering out
// scenes for use on other applications
const two = new Two({
  type: Two.Types.canvas,
  width: 800,
  height: 800,
  ratio: 6
});

We should remove getRatio now that getBackingStoreRatio is deprecated. Thanks for that!

@rjha
Copy link
Author

rjha commented Feb 22, 2023

Hi Jono
sorry for late reply. The problem was showing a huge canvas in a smaller display region. The only way I can do that with two.js is to set styles directly on renderer.domElement. Here is an example to show a circle on 3200x 3200 css pixels canvas inside a 400 x 400 css pixels DIV.

Since two.js sets css styles directly using width and height,

  • I cannot use setSize()
  • Any style set in html template will be overwritten
     // Huge canvas demo 
        // we render a huge circle that goes 
        // outside the browser screen but we display 
        // it inside a smaller region by setting domElement css
        
        import Two from '/js/two.module.js';

        function animation_update(frameCount) {
            if(frameCount % 441 == 0) {
                circle.fill = colors[color_index++ % 2];
            }
        }

        const pixels = 3200;
        const DISPLAY_REGION = {
          "height": 400,
          "width": 400
        }

        // if canvas height OR width is less than 
        // pixels then we can only render partial 
        // scene. set width, height to 800 to see 
        // a rectangle instead of a circle 
        // 
        const canvas_width = 3200;
        const canvas_height = 3200;

        const container =  document.getElementById("art");
        const canvasElement = document.getElementById("canvas1");
        
        var  two = new Two({
            fullscreen: false,
            type: Two.Types.canvas,
            overdraw: true,
            domElement: canvasElement,
            width: canvas_width, 
            height: canvas_height 
        }).appendTo(container);

        // make a big circle
        var radius = Math.floor(pixels * 0.5);
        var x = two.width * 0.5;
        var y = two.height * 0.5;
        var circle = two.makeCircle(x, y, radius);
        // pen stroke
        circle.stroke = 'black';
        circle.linewidth = 10;
        // fill 
        var color_index = 0;
        var colors = ["red", "orange"];
        two.bind('update', animation_update);
        

        two.play();
        
        
        // set css to bring it inside region
        two.renderer.domElement.style.width = DISPLAY_REGION.width + "px";
        two.renderer.domElement.style.height = DISPLAY_REGION.height + "px";
        // two.renderer.setSize(3200, 3200, 2.5);

        console.log("css width -> " + two.renderer.domElement.style.width);




from an API perspective, I am not able to see the utility of renderer.setSize(). If I am targeting a device with high [css : actual device pixels] ratio, e.g., If width = 500 and height = 500 and ratio = 8 and you will set the canvas width and height to 500 x 8 = 4000. However I can only draw till max (x | y) = 500 with two.js and cannot access co-ordinates like [2000, 2000]

If I change the earlier example to use setSize(), like below then I will only get a portion of the circle drawn even though I have a canvas having 8000 device pixels as earlier. The only way to get earlier canvas results would be to call setSize(3200, 3200, actual_device_pixel_ratio)

Unless you map the two.js co-ordinates to new ratio space, the bigger canvas size cannot be utilized.

   two.play();
        
        // comment dom element css 
        // set css to bring it inside region
        // two.renderer.domElement.style.width = DISPLAY_REGION.width + "px";
        // two.renderer.domElement.style.height = DISPLAY_REGION.height + "px";

        // call renderer setSize with custom ratio
        two.renderer.setSize(800, 800, 10);
        
        console.log("css width -> " + two.renderer.domElement.style.width);

@jonobr1
Copy link
Owner

jonobr1 commented Feb 24, 2023

I see. You can always do this:

two.renderer.setSize(800, 800, 1);
two.renderer.domElement.style.width = '10px';
two.renderer.domElement.style.height = '10px';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants