canvas-sketch icon indicating copy to clipboard operation
canvas-sketch copied to clipboard

Canvas context cleared before running async code

Open Bjvanminnen opened this issue 6 years ago • 3 comments

const sketch = () => {
  return ({ context: ctx, width, height }) => {
    ctx.fillStyle = 'red';

    setTimeout(() => {
      console.log('I expect red');
      ctx.fillRect(200, 200, 200, 200);
    }, 100);
  };
};

I have the above code. I was expect to see a red rect drawn to the screen after 100ms, however it was instead black.

It looks like we call canvas.restore during postRender here: https://github.com/mattdesl/canvas-sketch/blob/master/lib/core/SketchManager.js#L355 which results in my setTimeout callback no longer having the expected context state.

I'm not familiar enough with all the usage scenarios to understand if/why the canvas.restore is necessary here, but I wonder if it could at least be made configurable.

Another option might be to just make it so that our sketch method returns a promise in this case and SketchManager doesn't call postRender until the promise has resolved. Not sure if makingSubmit async would lead to additional challenges further down the line.

Bjvanminnen avatar Aug 07 '18 03:08 Bjvanminnen

Interesting, can you tell me a bit more about what you are trying to do with the timeout/delay?

Using timeout like this is antithetical to the architecture/design of the tool, and will break features like exporting, animation frames, and so forth. In canvas-sketch, sorta like in React, your render function is meant to be a pure function that does not introduce any side-effects. In other words, the render function should produce the same output each time it's called with the same props.

For example, when you hit Cmd + S, the canvas may be re-sized (i.e. from a browser size to a higher-quality export size), in which case the render function has to be called with new props. Immediately after this render, the tool has to grab the DataURI of the canvas, so adding async code to your renderer will not work. Example:

function exportFrame () { // <-- called on Cmd + S
  renderWithExportSize(); // <-- render high resolution
  const dataURL = canvas.toDataURL(); // <-- grab data
  renderWithBrowserSize(); // <-- render normal resolution
}

Making the renderer function support a promise is interesting but I'm not sure that will help you if you are trying to do animations.

There's probably a way to achieve what you are aiming for, I just need to know a bit more about what you're trying to do. :smile:

mattdesl avatar Aug 07 '18 08:08 mattdesl

It's entirely possible that my use case it outside of the goals of this project.

The way that this came about is that I was working on a sketch where I was rendering about a million points as small little circles while playing with De Jong attractors (http://www.algosome.com/articles/strange-attractors-de-jong.html). In trying to better understand them, I wanted to progressively render the points in batches inside of a setInterval loop rather than rendering all at once. When doing so, I was surprised to have lost all of my styling data..

Bjvanminnen avatar Aug 07 '18 15:08 Bjvanminnen

Something like that is certainly possible. Here are two different approaches:

  1. Maintain a list of points and progressively add to it, then your renderer just renders the current state. This will slow down eventually (when you have thousands of points), but because your renderer is pure, you will be able to take advantage of all of canvas-sketch features like inch/meter scaling, higher resolution export, exporting animation frames, etc.
const canvasSketch = require('canvas-sketch');

const settings = {
  dimensions: [ 2048, 2048 ]
};

const sketch = ({ context, width, height, render }) => {
  const points = [];

  setInterval(() => {
    // push a new point
    points.push([ Math.random() * width, Math.random() * height ]);

    // trigger a re-render
    render();
  }, 10);

  return ({ context, width, height }) => {
    // draw background color
    context.fillStyle = 'white';
    context.fillRect(0, 0, width, height);

    // draw all points so far
    points.forEach(point => {
      context.beginPath();
      context.arc(point[0], point[1], 10, 0, Math.PI * 2, false);
      context.fillStyle = 'black';
      context.fill();
    });
  };
};

canvasSketch(sketch, settings);
  1. Disable context scaling with { scaleContext: false } setting, scale the context yourself manually, and don't bother returning a render function. Just do all your rendering code inside the sketch function. You might run into some issues here and there, for example you won't be able to export an animation sequence very easily, but it should work for screen shots.
const canvasSketch = require('canvas-sketch');

const settings = {
  // disable automatic 2D context scaling
  scaleContext: false,
  // set up your canvas size
  dimensions: 'a4',
  units: 'in',
  pixelsPerInch: 300
};

const sketch = ({ context, width, height, scaleX, scaleY }) => {
  // scale context to correct inches / pixelsPerInch
  context.scale(scaleX, scaleY);

  // draw background color
  context.fillStyle = 'white';
  context.fillRect(0, 0, width, height);

  setInterval(() => {
    // draw a new point
    const point = [ Math.random() * width, Math.random() * height ];
    context.beginPath();
    context.arc(point[0], point[1], 0.1, 0, Math.PI * 2, false);
    context.fillStyle = 'black';
    context.fill();
  }, 10);
};

canvasSketch(sketch, settings);

Since this feature is pretty common with generative art, I will think of another way it could be achieved without introducing too many API changes.

mattdesl avatar Aug 07 '18 16:08 mattdesl