canvas-sketch
canvas-sketch copied to clipboard
Canvas context cleared before running async code
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.
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:
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..
Something like that is certainly possible. Here are two different approaches:
- 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);
- 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.