HTML5 canvas tearing with requestAnimationFrame - javascript

UPDATE: This entire issue ended up being a problem with the systems graphics driver, and not (seemingly) a browser / API issue. The torn frames came down to the actual display updating. Thank you again to those who were a part of the discussion and attempts to help.
I have page that uses a canvas and 2d context to display a pre-rendered frame at 720p. I'm rendering the frames separately and updating a variable with the new ImageData. Then, within requestAnimationFrame I simply do context.putImageData(cached_image_data);. Despite having the frame fully rendered in advance and effectively double buffered, I still get tearing far too often. There are a few other questions along these lines that I've found on SO, but they all end in "Use RAF". The code comes down to this:
var canvas = document.getElementById("canvas");
var cached_frame = new ImageData(new Uint8ClampedArray(canvas.width * canvas.height * 4), canvas.width, canvas.height);
var context = canvas.getContext("2d");
var framerate = 30;
function draw() {
if (cached_frame)
context.putImageData(cached_frame, 0, 0);
requestAnimationFrame(draw);
}
setInterval(function() {
var frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data.
// Save the resultant pixel data for the cached_frame.
cached_frame.data = frame.data;
}, 1000 / framerate);
draw();
Is there anything more that I can do without turning to webgl?
Any suggestions appreciated. TY all :D

I don't think the code is doing what you think it's doing
First off, as far as I know you can't assign new data to an ImageData so this line
cached_frame.data = frame.data;
Doesn't do anything. We can test that which shows it doens't work
var ctx = document.createElement("canvas").getContext("2d");
document.body.appendChild(ctx.canvas);
var imageData = ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
var data = new Uint8ClampedArray(imageData.length);
// fill imageData.data with red
fillWithColor(255, 0, 0, 255, imageData.data);
// fill data with green
fillWithColor(0, 255, 0, 255, data);
// assign imageData.data to data
imageData.data = data;
// Draw. If assigning imageData.data works result will
// be green, if not result will be red
ctx.putImageData(imageData, 0, 0);
function fillWithColor(r, g, b, a, dst) {
for (ii = 0; ii < dst.length; ii += 4) {
dst[ii + 0] = r;
dst[ii + 1] = g;
dst[ii + 2] = b;
dst[ii + 3] = a;
}
}
Second, your draw function is drawing continuously, at least from the code you posted cached_frame is set on line 2 so it's always going to be true and always going to be drawing. If you're somehow partially updating the actual data in cached_frame then it's going to draw when there are only partial results.
I think you want something like this instead
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
function draw() {
context.putImageData(frame, 0, 0);
}
setInterval(function() {
frame = context.getImageData(0, 0, canvas.width, canvas.height);
// Do things to manipulate frame.data
// frame is ready, draw it at next rAF
requestAnimationFrame(draw);
}, 1000 / framerate);
You might want to check if a draw it is already queued if you think decoding will ever happen faster than raf. I don't think you actually need rAF in this case though. I'm pretty sure you could just draw at the end of your setInterval and it will show up the next frame, no tearing.
Here's a test, it's not tearing for me.
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
var frame;
var framerate = 30;
var frameCount = 0;
setInterval(function() {
++frameCount;
frame = context.getImageData(0, 0, canvas.width, canvas.height);
var data = frame.data;
var width = frame.width;
var height = frame.height;
// Do things to manipulate frame.data
for (var yy = 0; yy < height; ++yy) {
for (var xx = 0; xx < width; ++xx) {
var offset = (yy * width + xx) * 4;
data[offset + 0] = ((xx >> 2 & 0x1) ^ frameCount & 0x1) ? 255 : 0;
data[offset + 3] = 255;
}
}
// frame is ready, draw it at next rAF
context.putImageData(frame, 0, 0);
}, 1000 / framerate);
<canvas id="canvas" width="1280" height="720"></canvas>

Related

How to make pixel operations on canvas faster in Javascript

I am writing a program to convert three equations into an image by using them to generate RGB values for each pixel on a canvas. This is the code of my first test:
const canvas = document.getElementById("game")
const context = canvas.getContext("2d")
time = new Date()
canvas.width = 500
canvas.height = 500
for (x = 0; x < canvas.width; x++) {
for (y = 0; y < canvas.height; y++) {
context.fillStyle = "red"
context.fillRect(x, y, 1, 1)
}
}
console.log(new Date() - time)
<canvas id="game"></canvas>
However, when I run this code it takes a few seconds to generate the canvas. I added a basic way to see how long the code takes to run, and the console appears to say it only takes 200 milliseconds. I have noo idea why it says it only takes 200 milliseconds as I do not see the canvas until a few seconds after the console log.
How can I make these pixel operations faster?
If you're just using a single color, you can speed it up a bit by setting the fillStyle before the loop instead of setting it every iteration. That obviously won't work if you want to set different colors for different pixels, however.
You could also set the colors in an imageData object, then draw it all at once with putImageData. This is significantly faster for me in both Chrome and Firefox.
const canvas = document.getElementById("game")
const context = canvas.getContext("2d")
time = new Date()
canvas.width = 500
canvas.height = 500
let imageData = context.createImageData(canvas.width, canvas.height)
let data = imageData.data;
let i = 0;
while (i < data.length) {
data[i++] = 255; // Red value
data[i++] = 0; // Green value
data[i++] = 0; // Blue value
data[i++] = 255; // Alpha (opacity)
}
context.putImageData(imageData, 0, 0)
console.log(new Date() - time)
<canvas id="game"></canvas>

Efficient HTML5 Canvas Glow Effect without shape

I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>

Fast image scale down?

I'm writing a "TV filter" (you know the kind, RGB bars as it zooms in), for a video file and I've been having a look at some ways of shrinking the image that retains as much detail as possible.
For testing I'm drawing the sampled image back to the screen to see the quality - in the actual filter, I'll just be sampling pixels and getting the RGB values of the resultant computed color.
I've tried three, and the Hermite filter looks good, but compared to the speed "hardware" nearest neighbour version, it's not going to be suitable for processing video.
Is there any "tricks" in JavaScript that can be used to get accelerated image shrinking like 2, but with a quality like 1 or 3?
1: Brute force: http://codepen.io/SarahC/pen/VpvWvb?editors=1010
2: Internal nearest neighbour: http://codepen.io/SarahC/pen/ryeQgN?editors=1010
3: Hermite filter: http://codepen.io/SarahC/pen/ryMNWZ?editors=1010
Here's the "hardware"? version:
function processResize(percent) {
var size = percent * 0.01;
var sw = canvas.width * size;
var sh = canvas.height * size;
ctx.drawImage(canvas2, 0, 0, sw, sh);
ctx.drawImage(canvas, 0, 0, sw, sh, 0, 0, w, h);
}
I am not entirely sure from the description what you try to achieve, but from the codepens it seems as you try to create a mosaic effect.
You can use the built-in interpolation setting of the canvas context to use nearest-neighbor by turning image smoothing off, then draw the image to a small size representing how many "blocks" you want. Then draw back that version to full size again:
// blocks = initial number of pixels (video aspect is usually 16:9 so you may want
// to calculate a separate values for height.
var blocks = 24;
// draw initial size representing "blocks"
ctx.drawImage(video, 0, 0, blocks, blocks);
// turn off image smoothing (see below for prefixing)
// This uses nearest neighbor
ctx.imageSmoothingEnabled = false;
// enlarge the mosaic back to full size
ctx.drawImage(c, 0, 0, blocks, blocks, 0, 0, c.width, c.height);
Video Example
(the video may take a few seconds to load...)
var ctx = null;
var blocks = 24;
var video = document.createElement("video");
video.preload = "auto"; video.muted = video.autoplay = video.loop = true;
video.oncanplay = function() { // initialize for demo
if (!ctx) {
c.width = this.videoWidth;
c.height = this.videoHeight;
ctx = c.getContext("2d");
document.querySelector("input").oninput = function() {blocks = +this.value};
requestAnimationFrame(loop);
}
}
video.src = "//media.w3.org/2010/05/sintel/trailer.mp4";
function smoothing(state) {
ctx.oImageSmoothingEnabled = ctx.msImageSmoothingEnabled =
ctx.mozImageSmoothingEnabled = ctx.webkitImageSmoothingEnabled =
ctx.imageSmoothingEnabled = state;
}
function loop() {
smoothing(true); // improve quality of first step
ctx.drawImage(video, 0, 0, blocks, blocks);
smoothing(false); // mosaic step
ctx.drawImage(c, 0, 0, blocks, blocks, 0, 0, c.width, c.height);
// loop and throttle to 30 fps
requestAnimationFrame(function() {requestAnimationFrame(loop)});
}
<label>Blocks: <input type=range min=8 max=128 value=24></label><br>
<canvas id=c></canvas>

CamanJS revert and rotate fix

If you use the rotation plugin in CamanJS there is an issue when you are trying to revert changes. Caman is only implemented in a way that is working good when you crop or resize your image, but not when you rotate it. When you revert and the image is rotated the image reloads distorted, because it doesn't take under consideration that the canvas has rotated and changed size. Also the imageData.data of the canvas are different now. So I think i fixxed it by looking how he implemented the resize. Basicaly what I did (and he does too) is:
Create a canvas in the initial state
Update his pixelData from the initialState
create a new canvas
Rotate him with the initial image
get the ImageData and rerender them
So what I added. I needed to know how many angles was the image rotated so I can get the correct imageData when rotate the new canvas (step 4).
this.angle=0; //added it in the constructor
I also added a new boolean in the constructor to tell me if canvas was rotated
this.rotated = false;
In the rotated plugin:
Caman.Plugin.register("rotate", function(degrees) {
//....
//....
//....
this.angle += degrees;
this.rotated = true;
return this.replaceCanvas(canvas);
}
and on the originalVisiblePixels prototype:
else if (this.rotated){
canvas = document.createElement('canvas');//Canvas for initial state
canvas.width = this.originalWidth; //give it the original width
canvas.height = this.originalHeight; //and original height
ctx = canvas.getContext('2d');
imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
pixelData = imageData.data;//get the pixelData (length equal to those of initial canvas
_ref = this.originalPixelData; //use it as a reference array
for (i = _i = 0, _len = _ref.length; _i < _len; i = ++_i) {
pixel = _ref[i];
pixelData[i] = pixel; //give pixelData the initial pixels
}
ctx.putImageData(imageData, 0, 0); //put it back on our canvas
rotatedCanvas = document.createElement('canvas'); //canvas to rotate from initial
rotatedCtx = rotatedCanvas.getContext('2d');
rotatedCanvas.width = this.canvas.width;//Our canvas was already rotated so it has been replaced. Caman's canvas attribute is allready rotated, So use that width
rotatedCanvas.height = this.canvas.height; //the same
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.width / 2; //same
rotatedCtx.save();
rotatedCtx.translate(x, y);
rotatedCtx.rotate(this.angle * Math.PI / 180); //rotation based on the total angle
rotatedCtx.drawImage(canvas, -canvas.width / 2, -canvas.height / 2, canvas.width, canvas.height); //put the image back on canvas
rotatedCtx.restore(); //restore it
pixelData = rotatedCtx.getImageData(0, 0, rotatedCanvas.width, rotatedCanvas.height).data; //get the pixelData back
width = rotatedCanvas.width; //used for returning the pixels in revert function
}
You also need to add some resets in the reset prototype function. Basicaly reset angle and rotated
Caman.prototype.reset = function() {
//....
//....
this.angle = 0;
this.rotated = false;
}
and that's it.
I used it and works so far. What do you think?Hope it helps
Thanks for this, it worked after one slight change.
in the else if statement inside the originalVisiblePixels prototype I changed:
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.width / 2; //same
to:
x = rotatedCanvas.width / 2; //for translating
y = rotatedCanvas.height/ 2; //same
before this change my images where being cut.

Black resized canvas not completely fading drawings to black over time

I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/

Categories

Resources