Consistent FPS in frame by frame video with <canvas> - javascript
I'm trying to display precisely enough a video that I can stop on or jump to a specific frame. For now my approach is to display a video frame by frame on a canvas (I do have the list of images to display, I don't have to extract them from the video). The speed doesn't really matter as long as it's consistent and around 30fps. Compatibility somewhat matters (we can ignore IE≤8).
So first off, I'm pre-loading all the images:
var all_images_loaded = {};
var all_images_src = ["Continuity_0001.png","Continuity_0002.png", ..., "Continuity_0161.png"];
function init() {
for (var i = all_images_src.length - 1; i >= 0; i--) {
var objImage = new Image();
objImage.onload = imagesLoaded;
objImage.src = 'Continuity/'+all_images_src[i];
all_images_loaded[all_images_src[i]] = objImage;
}
}
var loaded_count = 0;
function imagesLoaded () {
console.log(loaded_count + " / " + all_images_src.length);
if(++loaded_count === all_images_src.length) startvid();
}
init();
and once that's done, the function startvid() is called.
Then the first solution I came up with was to draw on requestAnimationFrame() after a setTimeout (to tame the fps):
var canvas = document.getElementsByTagName('canvas')[0];
var ctx = canvas.getContext("2d");
var video_pointer = 0;
function startvid () {
video_pointer++;
if(all_images_src[video_pointer]){
window.requestAnimationFrame((function (video_pointer) {
ctx.drawImage(all_images_loaded[all_images_src[video_pointer]], 0, 0);
}).bind(undefined, video_pointer))
setTimeout(startvid, 33);
}
}
but that felt somewhat slow and irregular...
So second solution is to use 2 canvases and draw on the one being hidden and then switch it to visible with the proper timing:
var canvas = document.getElementsByTagName('canvas');
var ctx = [canvas[0].getContext("2d"), canvas[1].getContext("2d")];
var curr_can_is_0 = true;
var video_pointer = 0;
function startvid () {
video_pointer++;
curr_can_is_0 = !curr_can_is_0;
if(all_images_src[video_pointer]){
ctx[curr_can_is_0?1:0].drawImage(all_images_loaded[all_images_src[video_pointer]], 0, 0);
window.requestAnimationFrame((function (curr_can_is_0, video_pointer) {
ctx[curr_can_is_0?0:1].canvas.style.visibility = "visible";
ctx[curr_can_is_0?1:0].canvas.style.visibility = "hidden";
}).bind(undefined, curr_can_is_0, video_pointer));
setTimeout(startvid, 33);
}
}
but that too feels slow and irregular...
Yet, Google Chrome (which I'm developing on) seems to have plenty of idle time:
So what can I do?
The Problem:
Your main issue is setTimeout and setInterval are not guaranteed to fire at exactly the delay specified, but at some point after the delay.
From the MDN article on setTimeout (emphasis added by me).
delay is the number of milliseconds (thousandths of a second) that the function call should be delayed by. If omitted, it defaults to 0. The actual delay may be longer; see Notes below.
Here are the relevant notes from MDN mentioned above.
Historically browsers implement setTimeout() "clamping": successive setTimeout() calls with delay smaller than the "minimum delay" limit are forced to use at least the minimum delay. The minimum delay, DOM_MIN_TIMEOUT_VALUE, is 4 ms (stored in a preference in Firefox: dom.min_timeout_value), with a DOM_CLAMP_TIMEOUT_NESTING_LEVEL of 5.
In fact, 4ms is specified by the HTML5 spec and is consistent across browsers released in 2010 and onward. Prior to (Firefox 5.0 / Thunderbird 5.0 / SeaMonkey 2.2), the minimum timeout value for nested timeouts was 10 ms.
In addition to "clamping", the timeout can also fire later when the page (or the OS/browser itself) is busy with other tasks.
The Solution:
You would be better off using just requestAnimationFrame, and inside the callback using the timestamp arguments passed to the callback to compute the delta time into the video, and drawing the necessary frame from the list. See working example below. As a bonus, I've even included code to prevent re-drawing the same frame twice.
Working Example:
var start_time = null;
var frame_rate = 30;
var canvas = document.getElementById('video');
var ctx = canvas.getContext('2d');
var all_images_loaded = {};
var all_images_src = (function(frames, fps){//Generate some placeholder images.
var a = [];
var zfill = function(s, l) {
s = '' + s;
while (s.length < l) {
s = '0' + s;
}
return s;
}
for(var i = 0; i < frames; i++) {
a[i] = 'http://placehold.it/480x270&text=' + zfill(Math.floor(i / fps), 2) + '+:+' + zfill(i % fps, 2)
}
return a;
})(161, frame_rate);
var video_duration = (all_images_src.length / frame_rate) * 1000;
function init() {
for (var i = all_images_src.length - 1; i >= 0; i--) {
var objImage = new Image();
objImage.onload = imagesLoaded;
//objImage.src = 'Continuity/'+all_images_src[i];
objImage.src = all_images_src[i];
all_images_loaded[all_images_src[i]] = objImage;
}
}
var loaded_count = 0;
function imagesLoaded () {
//console.log(loaded_count + " / " + all_images_src.length);
if (++loaded_count === all_images_src.length) {
startvid();
}
}
function startvid() {
requestAnimationFrame(draw);
}
var last_frame = null;
function draw(timestamp) {
//Set the start time on the first call.
if (!start_time) {
start_time = timestamp;
}
//Find the current time in the video.
var current_time = (timestamp - start_time);
//Check that it is less than the end of the video.
if (current_time < video_duration) {
//Find the delta of the video completed.
var delta = current_time / video_duration;
//Find the frame for that delta.
var current_frame = Math.floor(all_images_src.length * delta);
//Only draw this frame if it is different from the last one.
if (current_frame !== last_frame) {
ctx.drawImage(all_images_loaded[all_images_src[current_frame]], 0, 0);
last_frame = current_frame;
}
//Continue the animation loop.
requestAnimationFrame(draw);
}
}
init();
<canvas id="video" width="480" height="270"></canvas>
Related
Chrome Performance Issue With Bitmap Area Sampling Effect (JavaScript)
I am writing an HTML5 game using the engine Phaser, in which I am implementing what are essentially live backgrounds, backgrounds that respond to the movements of the game objects. The first I am working with is a water ripple effect that uses area sampling on the bitmapData object. I thought I had a performance issue in my code, but it turns out that Firefox runs it like a dream. Chrome runs a little slower to begin with and slows to less than 10 FPS when my game objects go too close to the top or bottom of the screen. (I am at a loss for why that makes a difference.) This thread suggests that Chrome has poor image processing performance and suggests to break large image data up into smaller pieces. I don't know if this is possible in my case, because this is not simply an image displaying on the screen but an effect based on pixels next to each other that refreshes each frame. Even if it is possible, I think Chrome would end up having to do the same amount of work or more to get the four individual bitmaps to interact with each other as if they were one. I've been doing performance tests in Chrome for a few hours, and the issue is definitely that it is getting caught up on the method that actually creates the effect by reading pixels from a source imageData and writing them to another location in a target imageData (the ws.displace(x,y) method below). function waterStage(canvas) { var ws = new Object(); ws.dampFactor = 16; ws.magFactor = 150; ws.dispFactor = 0.5; ws.lumFactor = 1; ws.width = canvas.width; ws.height = canvas.height; // Initialize height data caches ws.pMaps = []; var map1 = new Array(ws.width+2); var map2 = new Array(ws.width+2); for (x=0; x < map1.length; x++) { map1[x] = new Array(ws.height+2); map2[x] = new Array(ws.height+2); } for (x=0; x < map1.length; x++) { for (y=0; y < map1[x].length; y++) { map1[x][y] = 0; map2[x][y] = 0; } } ws.pMaps.push(map1, map2); ws.stageInit = function(canvas) { canvas.fill(100,100,100); canvas.ctx.strokeStyle = "#000000"; canvas.ctx.lineWidth = 2; canvas.ctx.moveTo(0,0); for (y=0; y < ws.height; y+=10) { canvas.ctx.beginPath(); canvas.ctx.moveTo(0,y); canvas.ctx.lineTo(ws.width,y); canvas.ctx.closePath(); canvas.ctx.stroke(); } ws.sourceData = canvas.ctx.getImageData(0, 0, ws.width, ws.height); ws.targetData = canvas.ctx.getImageData(0, 0, ws.width, ws.height); } ws.setWave = function(pnt) { ws.pMaps[0][pnt.x-1][pnt.y-1] = ws.magFactor//*pnt.magnitude; } ws.resolveWaves = function(x,y) { // Calculate the net result of the wave heights ws.pMaps[1][x][y] = ((ws.pMaps[0][x-1][y]+ws.pMaps[0][x+1][y]+ws.pMaps[0][x][y-1]+ws.pMaps[0][x][y+1]) / 2) -ws.pMaps[1][x][y]; ws.pMaps[1][x][y] -= (ws.pMaps[1][x][y]/ws.dampFactor); } ws.displace = function(x,y) { var displace = Math.floor(ws.pMaps[1][x][y]*ws.dispFactor); var xCorrect = x-1, yCorrect = y-1; var targetIndex = (xCorrect + yCorrect * ws.width)*4; if (displace == 0) { ws.targetData.data[targetIndex] = ws.sourceData.data[targetIndex]; ws.targetData.data[targetIndex+1] = ws.sourceData.data[targetIndex+1]; ws.targetData.data[targetIndex+2] = ws.sourceData.data[targetIndex+2]; } else { if (displace < 0) { displace += 1; } var sourceX = displace+xCorrect; var sourceY = displace+yCorrect; var sourceIndex = (sourceX + sourceY * ws.width)*4; //var lum = ws.pMaps[1][x][y]*ws.lumFactor; ws.targetData.data[targetIndex] = ws.sourceData.data[sourceIndex];//+lum; ws.targetData.data[targetIndex+1] = ws.sourceData.data[sourceIndex+1];//+lum; ws.targetData.data[targetIndex+2] = ws.sourceData.data[sourceIndex+2];//+lum; } } ws.stageRefresh = function(moves, canvas) { canvas.clear(); for (j=0; j < moves.length; j++) { ws.setWave(moves[j]); } for (x=1; x <= ws.width; x++) { if (ws.pMaps[1][x][0] != 0 || ws.pMaps[0][x][0] != 0) { alert("TOP ROW ANOMALY"); } for (y=1; y <= ws.height; y++) { ws.resolveWaves(x,y); ws.displace(x,y); } } ws.pMaps.sort(function(a,b) { return 1 }); //ws.pMaps[0] = ws.pMaps[1]; //ws.pMaps[1] = temp; canvas.ctx.putImageData(ws.targetData, 0, 0); } return ws; } canvas is the bitmapData that is given as the texture for the background (not an HTML5 canvas; sorry if that's confusing). ws.stageRefresh(moves,canvas) is called on every frame update. Before I try to make the split-into-four-bitmaps solution work, does anyone have any guidance for other ways to improve the performance of this effect on Chrome?
How to make javascript canvas draw faster?
I have the following code to display an ECG. I use the canvas to draw the graph background (each grid of 2.5 mm dimension). Later I'm taking the y coordinates from an array array_1 (x coordinates are calculated within the program). The problem with this approach is it will take around 40 seconds to plot the entire graph since there are 1250 values within array array_1. What I could do is I could do the plotting part within a loop in which case, the entire graph is plotted as soon as the page is loaded. But, I need the plotting to happen over the course of 5 seconds. Not more. Not less. How would I alter the code to do this? Please help. <!DOCTYPE html> <html> <head> <title></title> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> </head> <body> <canvas id="canvas" width="1350" height="1300" style="background-color: white;"></canvas> <script type='text/javascript'> var canvas = document.getElementById("canvas"); var ctxt = canvas.getContext("2d"); var n1 = 1; var n1_x=49; //Graph x coordinate starting pixel. var n1_y=72;//Graph y coordinate starting pixel. var array_1 = []// array from which y coordinates are taken. Has 1250 elements var ctx = canvas.getContext("2d"); var x=0; var y=0; var Line_position=-1; while(x<=1350)//graph width { ctxt.lineWidth = "0.5"; Line_position=Line_position+1; if(Line_position%5==0) { ctxt.lineWidth = "1.5"; } ctxt.strokeStyle = "black"; ctxt.beginPath(); ctxt.moveTo(x, 0); ctxt.lineTo(x, 1300); ctxt.stroke(); x=x+9.43; } Line_position=-1; while(y<=1300)//graph height { ctxt.lineWidth = "0.5"; Line_position=Line_position+1; if(Line_position%5==0) { ctxt.lineWidth = "1.5"; } ctxt.strokeStyle = "black"; ctxt.beginPath(); ctxt.moveTo(0, y); ctxt.lineTo(1350,y); ctxt.stroke(); y=y+9.43; } drawWave(); function drawWave() { requestAnimationFrame(drawWave); ctx.lineWidth = "1"; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo(n1_x- 1, n1_y+array_1[n1-1]);//move to the pixel position ctx.lineTo(n1_x, n1_y+array_1[n1]);//Draw to the pixel position ctx.stroke(); n1_x=n1_x+0.374;//Incrementing pixel so as to traverse x axis. n1++; } </script> </body> </html> Here is the array: array_1 = [69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,72,72,72,72,72,72,72,73,73,74,74,74,74,74,74,74,73,73,73,73,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,74,74,74,73,73,73,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,71,72,72,72,73,73,73,72,72,72,73,73,73,74,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,73,73,73,72,72,72,71,101,71,70,70,70,69,68,68,67,67,66,66,67,67,69,70,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,74,76,77,76,70,57,40,22,11,11,22,40,57,69,73,73,71,71,71,72,72,73,73,74,74,74,73,72,72,72,72,72,72,72,72,72,72,72,72,71,71,70,70,71,71,71,71,70,70,69,69,69,69,69,69,69,68,68,68,67,67,66,66,65,65,64,63,63,62,62,62,62,62,62,62,62,63,63,64,65,66,67,68,68,69,70,71,72,72,72,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,73,73,73,73,72,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,73,73,74,74,74,74,74,74,73,73,72,73,73,73,74,73,73,72,72,72,73,73,73,72,72,73,73,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,70,70,70,70,70,69,69,68,67,67,67,67,68,69,71,72,72,73,73,73,73,74,74,74,74,74,73,73,73,73,75,77,78,76,67,53,35,18,8,10,23,41,58,69,73,72,71,70,71,72,73,73,73,73,73,73,73,73,72,72,73,73,73,73,72,71,71,70,70,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,68,67,67,67,67,67,66,65,65,65,64,63,62,61,61,61,60,60,60,59,60,60,60,61,62,63,65,66,66,67,68,69,70,71,72,72,72,72,73,73,73,72,72,72,72,72,72,72,73,73,73,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,71,72,72,73,73,73,72,72,72,72,72,72,73,73,73,73,73,73,73,73,73,72,73,73,73,73,73,73,72,73,73,73,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,71,71,70,70,69,69,69,68,67,67,66,65,66,66,68,69,70,71,72,72,73,73,73,73,73,73,74,74,74,74,74,74,76,78,78,74,64,48,29,13,5,10,26,45,62,71,73,72,71,71,72,73,73,73,73,73,74,74,74,73,72,72,72,73,73,73,73,73,73,73,72,72,72,72,71,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,67,66,66,66,66,65,65,64,63,62,62,61,61,60,60,60,60,61,62,62,63,64,65,66,67,68,70,71,72,72,72,72,72,72,73,73,73,73,73,73,73,74,74,75,75,74,74,74,73,73,73,74,73,73,73,73,73,74,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,74,74,74,73,73,73,73,73,73,73,73,73,73,72,72,72,72,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,72,72,71,70,70,70,69,69,68,68,67,67,66,67,67,68,69,70,71,72,73,73,74,74,73,73,73,74,75,75,74,73,73,74,76,78,75,67,52,32,15,5,8,22,41,59,69,73,72,71,70,71,72,72,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,72,72,72,71,71,71,70,70,70,70,70,70,70,69,69,69,69,68,68,68,68,67,67,66,65,65,64,64,64,63,62,61,60,60,60,60,60,61,61,62,62,63,64,65,65,66,67,68,69,70,71,71,71,71,71,71,72,72,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,72,71,71,71,72,72,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,71,71,71,70,70,70,70,69,69,68,67,67,68,69,71,72,73,73,73,73,73,73,73,73,74,75,75,75,74,74,74,75,77,77,75,67,52,34,18,10,12,26,45,62,71,74,73,72,72,72,73,74,74,74,75,75,74,74,74,74,74,74,74,74,74,73,73,73,73,74,74,73,73,73,73,73,73,73,72,72,71,71,71,71,71,70,70,70,69,69,69,68,68,68,68,67,66,65,64,63,63,62,62,62,63,63,63,63,64,65,66,67,69,69,70,71,72,72,73,73,74,74,74,74,75,75,76,76,74,72,70,70,69,69 ];
I'd probably go about the task something like this. As mentioned in a comment, we need to draw a number of the data-points per-frame. How many we draw depends on the speed that the browser is able to supply an animation frame. I've hard-coded the value to 4, since that seems to work on my machine, but with not much more work you can probably make the code time itself and adjust this value on the fly so that your animation runs for as close as possible to the target time. I had a quick go, but the results were awful, I'll leave that as an exercise in research or thought for the reader. By keeping track of how many frames we've already drawn for the current 'refresh-cycle', we know how far to index into the array for the first point to be drawn for each frame. I've tried to parameterize the code as much as possible, but it's late and I'm tired, I may have overlooked something somewhere. <!doctype html> <html> <head> <script> function byId(id,parent){return (parent == undefined ? document : parent).getElementById(id);} window.addEventListener('load', onDocLoaded, false); function onDocLoaded(evt) { drawBkg(byId('canvas'), 9.43, 5, "0.5", "1.5", "black"); drawCurFrame(); } var dataSamples = [69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,20,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,69,72,72,72,72,72,72,72,73,73,74,74,74,74,74,74,74,73,73,73,73,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,74,74,74,73,73,73,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,71,72,72,72,73,73,73,72,72,72,73,73,73,74,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,73,73,73,72,72,72,71,101,71,70,70,70,69,68,68,67,67,66,66,67,67,69,70,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,74,76,77,76,70,57,40,22,11,11,22,40,57,69,73,73,71,71,71,72,72,73,73,74,74,74,73,72,72,72,72,72,72,72,72,72,72,72,72,71,71,70,70,71,71,71,71,70,70,69,69,69,69,69,69,69,68,68,68,67,67,66,66,65,65,64,63,63,62,62,62,62,62,62,62,62,63,63,64,65,66,67,68,68,69,70,71,72,72,72,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,73,73,73,73,72,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,73,73,74,74,74,74,74,74,73,73,72,73,73,73,74,73,73,72,72,72,73,73,73,72,72,73,73,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,70,70,70,70,70,69,69,68,67,67,67,67,68,69,71,72,72,73,73,73,73,74,74,74,74,74,73,73,73,73,75,77,78,76,67,53,35,18,8,10,23,41,58,69,73,72,71,70,71,72,73,73,73,73,73,73,73,73,72,72,73,73,73,73,72,71,71,70,70,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,68,67,67,67,67,67,66,65,65,65,64,63,62,61,61,61,60,60,60,59,60,60,60,61,62,63,65,66,66,67,68,69,70,71,72,72,72,72,73,73,73,72,72,72,72,72,72,72,73,73,73,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,71,71,72,72,73,73,73,72,72,72,72,72,72,73,73,73,73,73,73,73,73,73,72,73,73,73,73,73,73,72,73,73,73,73,73,73,73,72,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,71,71,70,70,69,69,69,68,67,67,66,65,66,66,68,69,70,71,72,72,73,73,73,73,73,73,74,74,74,74,74,74,76,78,78,74,64,48,29,13,5,10,26,45,62,71,73,72,71,71,72,73,73,73,73,73,74,74,74,73,72,72,72,73,73,73,73,73,73,73,72,72,72,72,71,71,71,71,71,71,71,71,71,70,70,69,69,69,69,68,67,66,66,66,66,65,65,64,63,62,62,61,61,60,60,60,60,61,62,62,63,64,65,66,67,68,70,71,72,72,72,72,72,72,73,73,73,73,73,73,73,74,74,75,75,74,74,74,73,73,73,74,73,73,73,73,73,74,74,74,74,74,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,74,74,74,73,73,73,73,73,73,73,73,73,73,72,72,72,72,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,73,73,72,72,71,70,70,70,69,69,68,68,67,67,66,67,67,68,69,70,71,72,73,73,74,74,73,73,73,74,75,75,74,73,73,74,76,78,75,67,52,32,15,5,8,22,41,59,69,73,72,71,70,71,72,72,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,72,72,72,71,71,71,70,70,70,70,70,70,70,69,69,69,69,68,68,68,68,67,67,66,65,65,64,64,64,63,62,61,60,60,60,60,60,61,61,62,62,63,64,65,65,66,67,68,69,70,71,71,71,71,71,71,72,72,73,73,73,72,72,73,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,72,72,72,71,71,71,71,71,71,71,72,72,72,72,72,72,72,72,72,71,71,71,72,72,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,72,72,73,73,73,73,73,72,72,72,73,73,74,73,73,73,73,73,73,73,73,73,73,73,73,72,72,72,72,71,71,71,70,70,70,70,69,69,68,67,67,68,69,71,72,73,73,73,73,73,73,73,73,74,75,75,75,74,74,74,75,77,77,75,67,52,34,18,10,12,26,45,62,71,74,73,72,72,72,73,74,74,74,75,75,74,74,74,74,74,74,74,74,74,73,73,73,73,74,74,73,73,73,73,73,73,73,72,72,71,71,71,71,71,70,70,70,69,69,69,68,68,68,68,67,66,65,64,63,63,62,62,62,63,63,63,63,64,65,66,67,69,69,70,71,72,72,73,73,74,74,74,74,75,75,76,76,74,72,70,70,69,69 ]; function drawBkg(canvasElem, squareSize, numSquaresPerBlock, minorLineWidthStr, majorLineWidthStr, lineColStr) { var nLinesDone = 0; var i, curX, curY; var ctx = canvasElem.getContext('2d'); ctx.clearRect(0,0,canvasElem.width,canvasElem.height); // draw the vertical lines curX=0; ctx.strokeStyle = lineColStr; while (curX < canvasElem.width) { if (nLinesDone % numSquaresPerBlock == 0) ctx.lineWidth = majorLineWidthStr; else ctx.lineWidth = minorLineWidthStr; ctx.beginPath(); ctx.moveTo(curX, 0); ctx.lineTo(curX, canvasElem.height); ctx.stroke(); curX += squareSize; nLinesDone++; } // draw the horizontal lines curY=0; nLinesDone = 0; while (curY < canvasElem.height) { if (nLinesDone % numSquaresPerBlock == 0) ctx.lineWidth = majorLineWidthStr; else ctx.lineWidth = minorLineWidthStr; ctx.beginPath(); ctx.moveTo(0, curY); ctx.lineTo(canvasElem.width, curY); ctx.stroke(); curY += squareSize; nLinesDone++; } } // position that will be treated as 0,0 when drawing our points. var originX=49; var originY=72; function drawSamples(nSamplesToDraw, firstSample, lineWidthStr, lineColourStr) { var can = byId('canvas'); var ctx = can.getContext('2d'); ctx.strokeStyle = lineColourStr; ctx.lineWidth = lineWidthStr; console.log(firstSample); ctx.beginPath(); ctx.moveTo( originX+firstSample-1, dataSamples[firstSample-1]+originY ); for (var i=0; i<nSamplesToDraw; i++) { var curSample = dataSamples[i + firstSample]; ctx.lineTo( originX+firstSample+i, curSample+originY ); } ctx.stroke(); } var curFrame=0; var nPointsPerFrame = 4; function drawCurFrame() { if ((dataSamples.length - (nPointsPerFrame * curFrame)) < nPointsPerFrame) // will we over-run the end of the array of datapoints? { curFrame = 0; // if so, reset drawBkg(byId('canvas'), 9.43, 5, "0.5", "1.5", "black"); } drawSamples(nPointsPerFrame, nPointsPerFrame*curFrame, "1", "blue"); curFrame++; requestAnimationFrame( drawCurFrame ); } </script> <style> #canvas { border: solid 1px black; background-color: #FFFFFF; } </style> </head> <body> <div id='txt'></div> <canvas id="canvas" width="1350" height="1300"></canvas> </body> </html>
Update Now that I see you have provided some more info I get what you want. The problem is you need to draw a fixed number of line segments within time t. As you do not know how long each frame could take you can not rely on a fixed frame rate. The alternative it to just use the current time and save the end time. Get the start time and then each frame draw all the should be drawn until the current time. As the line segments being drawn will not be displayed until the next screen refresh the time you get will be approx 16ms behind so will need to adjust for that. What I have done is keep track of the average frame time and used half that time to estimate when the new canvas update will be displayed. Its a bit pedantic but might as well show how to get a required time as close as possible. If you dont care its a few ms out then just remove the average frame time stuff. You will be at most 30ms off on a slow machine. var canvas; // canvas var ctx; function getCanvas () { // to do // get canvas and context } function drawGrid () { // to do // draw the grid } function drawTimedLine(){ if(canvas === undefined){ // if the canvas not available get it getCanvas(); } // clear the canvas is repeating animation ctx.clearRect(0, 0, canvas.width, canvas.height); drawGrid(); var array_1 = ; // your data // All stuff needed for timed animation. // The frame you render will not be displayed untill the next // vertical refresh which is unknown, Assume it is one frame. var startDelay = 1000; // if Needed time in milliseconds to delay start var endTime; // hold the time that the animation has to end var lastDataPoint; // holds the last point drawn to var timeToDraw = 5 * 1000; // how long the animation should last var repeatAfter = 1 *1000; // if you want to repeat the animatoin var frameCount = 0; // count the frames to get average frame time var startTime; //the start time; var numberPoints = array_1.length; // number of points; var startX = 49; // line starts at var yOffset = 72; // line Y offset var endX = 512; // line ends at. var width = endX - startX; // width var xStep = width / numberPoints; // X step per point var pointsPerMS = numberPoints / timeToDraw; // get how many points per ms should be drawn // function to draw function drawWave() { // variable needed var averageframeTime, timeLeft, i, currentTime; currentTime = new Date().valueOf(); // gets the time in millisecond; if (startTime === undefined) { // Is this the first frame startTime = currentTime; // save the start time; endTime = currentTime + timeToDraw; // workout when the end time is; lastDataPoint = 0; // set the data position to the start; averageframeTime = 0; // no frames counted so frame time is zero } else { frameCount += 1; // count the frames // get the average frame time averageframeTime = (currentTime - startTime) / frameCount; } // get the time this frame // will most likely be displayed // then calculate how long // till the end timeLeft = endTime - Math.min(endTime, currentTime + averageframeTime / 2); // now get where we should // be when the frame is presented pointPos = Math.floor(pointsPerMS * (timeToDraw - timeLeft)); // now draw the points from where we last left of // till the new pos; ctx.lineWidth = 4; ctx.strokeStyle = 'blue'; ctx.beginPath(); ctx.moveTo( // move to first point lastDataPoint * xStep + startX, array_1[lastDataPoint] + yOffset ); // draw each line from the last drawn to the new position for (i = lastDataPoint + 1; i <= pointPos && i < numberPoints; i++) { // Add the line segment ctx.lineTo( i * xStep + startX, array_1[i] + yOffset ); } ctx.stroke(); // execute the render commands lastDataPoint = pointPos; // update the last point if (pointPos < numberPoints) { // are we there yet??? requestAnimationFrame(drawWave); // no so request another frame }else{ // if you want to repeat the animation setTimeout(drawTimedLine , repeatAfter ); } } // start the line animation with delay if needed setTimeout(drawWave,startDelay); } // use this if you want it to start as soon as page is ready. document.addEventListener("DOMContentLoaded",drawTimedLine); // or use if you want it to start when page has images loaded and is ready // document.addEventListener("load",drawTimedLine); I have also added the ability to repeat the animation. If not needed just remove that code My original answer Dont know what the problem is with speed as it runs quite well on my machine. To set up a better start use function startFunction(){ // your code } document.addEventListener("DOMContentLoaded",startFunction); This will wait until the page has loaded and parsed the page. Images and other media may not have loaded but the page is ready to be manipulated. Not sure what you mean with 5 seconds. Assuming you may want the thing to sart in 5 seconds. The following will do that. document.addEventListener("DOMContentLoaded",function() {setTimeout(startFunction,5000);}); I would ask why plot the graph one entry at a time with requestAnimationFrame 1250 is not that many lines to draw. If you add ctx.beginPath() ctx.moveTo(/*first point*/) then loop all points with ctx.moveTo(/*points*/) then ctx.stroke() will run realtime on but the slowest of devices. BTW ctx.lineWidth is a Number not a string. Also you have two context? Use the one context for the canvas. Remove ctxt and just use ctx and finally you don't need to add type='text/javascript' to the script tag as Javascript is the default.
1) It cannot take that long to draw 1000 lines, even 100000 lines won't take more than 10 ms on any decent Browser. Look else where the time is lost. 2) The core issue of your code is that it lacks modularity. Split your code into a few clear functions, group the parameters into a few objects only, name and indent things properly. Below an (incomplete but working) example of how this might look. var cv, ctx; var data = null; var debug = true; // --------------------------------------- // define here all graphic related parameters var gfxParams = { canvasWidth: 600, canvasHeight: 600, gridColor: '#A66', gridSpacing: 10, gridLineWidth: 0.5, gridStrongLinesEvery: 5, lineColor: '#AEB', lastLineColor: '#8A9' // , ... }; // define here all animation related parameters var animationParams = { duration: 5, startTime: -1 } // --------------------------------------- // main // --------------------------------------- window.onload = function() { data = getData(); setupCanvas(data); launchAnimation(); } // --------------------------------------- // function setupCanvas(data) { cv = document.getElementById('cv'); cv.width = gfxParams.canvasWidth; cv.height = gfxParams.canvasHeight; ctx = cv.getContext('2d'); // here you should translate and scale the context // so that it shows your data. } function drawGrid(ctx) { var i = 0, pos = 0, lw = gfxParams.gridLineWidth; ctx.fillStyle = gfxParams.gridColor; var vLineCount = gfxParams.canvasWidth / gfxParams.gridSpacing; for (i = 0; i < vLineCount; i++) { pos = i * gfxParams.gridSpacing; ctx.fillRect(pos, 0, lw, gfxParams.canvasHeight); } var hLineCount = gfxParams.canvasHeight / gfxParams.gridSpacing; for (i = 0; i < hLineCount; i++) { pos = i * gfxParams.gridSpacing; ctx.fillRect(0, pos, gfxParams.canvasWidth, lw); } } function animate() { requestAnimationFrame(animate); var now = Date.now(); // erase screen ctx.clearRect(0, 0, gfxParams.canvasWidth, gfxParams.canvasHeight); // draw grid drawGrid(ctx); // draw lines var lastIndex = getLastDrawnIndex(data, now - animationParams.startTime); drawLines(ctx, data, lastIndex); if (debug) { ctx.save(); ctx.fillStyle = '#000'; ctx.fillText(lastIndex + ' lines drawn. Time elapsed : ' + (now - animationParams.startTime), 10, 10); ctx.restore(); } } // comment function launchAnimation() { requestAnimationFrame(animate); animationParams.startTime = Date.now(); } // comment function getData() { var newData = []; for (var i = 0; i < 500; i++) { newData.push([Math.random() * 600, Math.random() * 600]); } return newData; } // comment function getLastDrawnIndex(data, timeElapsed_ms) { var timeElapsed = timeElapsed_ms / 1000; if (timeElapsed >= animationParams.duration) return data.length - 1; return Math.floor(data.length * timeElapsed / animationParams.duration); } function drawLines(ctx, data, lastIndex) { ctx.strokeStyle = gfxParams.lineColor; // other ctx setup here. for (var i = 0; i < lastIndex - 1; i++) { drawLine(ctx, data[i], data[i + 1]); } ctx.strokeStyle = gfxParams.lastLineColor; drawLine(ctx, data[lastIndex - 1], data[lastIndex]); } function drawLine(ctx, p1, p2) { ctx.beginPath(); ctx.moveTo(p1[0], p1[1]); ctx.lineTo(p2[0], p2[1]); ctx.stroke(); } <canvas id='cv'></canvas>
How to control animation speed (requestAnimationFrame)?
I change the text color with requestAnimationFrame(animate); function: requestAnimationFrame(animate); function animate(time){ ... // change text color here if (offset_s < offset_e) {requestAnimationFrame(animate);} } offset_s and offset_s indicates start and end positions of the text for color change. In some cases the animation should last for 2 seconds, but in order cases - for 5 seconds, but offset_e - offset_s could be the same in these two cases. What can I do to control the speed of animation based on given time in seconds/milliseconds?
From the tags of the question i can only see that you animate something drawn on canvas and thats why u cannot use css-animation or jquery-animation. You have to control the length of the animation by calculating the time difference. u can do it similar to this example function start_animate(duration) { var requestID; var startTime =null; var time ; var animate = function(time) { time = new Date().getTime(); //millisecond-timstamp if (startTime === null) { startTime = time; } var progress = time - startTime; if (progress < duration ) { if(offset_s < offset_e){ // change text color here } requestID= requestAnimationFrame(animate); } else{ cancelAnimationFrame(requestID); } requestID=requestAnimationFrame(animate); } animate(); } trigger your animation and call start_animate(2000) //duration in millisecond 1000=1 sec
You should separate concerns clearly. Have a single requestAnimationFrame running, which computes the current animation time and calls every update and draw related functions. Then your animations would be handled by a function (or class instance if you go OOP) that deals with the current animation time. Just some direction for the code : var animationTime = -1; var _lastAnimationTime = -1; function launchAnimation() { requestAnimationFrame(_launchAnimation); } function _launchAnimation(time) { animationTime = 0; _lastAnimationTime = time; requestAnimationFrame(animate); } function animate(time){ requestAnimationFrame(animate); var dt = time - _lastAnimationTime ; _lastAnimationTime = time; animationTime += dt; // here call every draw / update functions // ... animationHandler.update(animationTime); animationHandler.draw(context); } To start your 'engine', just call launchAnimation then you'll have a valid animationTime and dt to deal with. I'd make animationHandler an instance of an AnimationHandler class, that allows to add/remove/update/draw animations.
I suggest to use setInterval function in JavaScript. requestAnimationFrame really needs some 'ugly' calculations. Don't believe me? Scroll up, you will see... So, to make setInterval function as handy as rAF(requestAnimationFrame) store the function inside of variable. Here is an example: var gameLoop = setInterval(function() { update(); draw(); if (gameOver) clearInterval(gameLoop); }, 1000/FPS); given way, you can control your FPS and pick correct velocity for your objects.
I typically do something like es6 constructor() { this.draw(); } draw() { const fps30 = 1000 / 30; const fps60 = 1000 / 60; window.requestAnimationFrame(() => { setTimeout(this.draw.bind(this), fps30); }); } es5 function DrawingProgram() { this.draw(); } DrawingProgram.prototype.draw = function() { var fps30 = 1000/30; var fps60 = 1000/60; var self = this; window.requestAnimationFrame(function() { window.setTimeout(function() { self.draw(); // you could also use apply/call here if you want }, fps30) }); }
HTML Canvas animation wont run on mobile devices
The canvas animation runs effectively on web browsers but when testing on mobile browsers with iPad and iPhone the animation never starts. It just simply displays the background image. There are no error messages given. The animation is basically an image that moves from offscreen on the left hand side of the canvas and stops when it reaches 75% of the canvas width. Heres the code <script> window.addEventListener("load", eventWindowLoaded, false); function eventWindowLoaded () { start(); } function canvasSupport () { return Modernizr.canvas; } var canvas = document.getElementById("canvas"); var ctx=canvas.getContext("2d"); var cw=canvas.width; var ch=canvas.height; var image1 = new Image(); image1.onload = function() { ctx.clearRect(0, 0, 600, 400); ctx.drawImage(image1, 0, 0); } image1.src="images/oven.jpg"; ctx.fillStyle = image1; var currentX=cw; var continueAnimating=true; var nextMoveTime,maxMoves; var expX = 50; var expY = 200; var image2 = new Image(); image2.onload=start; image2.src="images/pies.png"; var image = new Image(); image.onload=start; image.src="images/pies.png"; function start(){ maxMoves=(cw+image.width)*0.5; nextMoveTime=performance.now(); requestAnimationFrame(animate); function animate(currentTime){ if(continueAnimating){ requestAnimationFrame(animate); } if(currentTime<nextMoveTime){return;} nextMoveTime=currentTime; // + delay; ctx.drawImage(image,currentX,193); if(--currentX<-image.width){ currentX=cw; } if(--maxMoves<0){continueAnimating=false;} } } </script>
So the issue comes from your use of performance.now, which is not always implemented, especially on mobile devices where the power drain of a precise timer is too high. Just use the time provided with the requestAnimationFrame : on accurate browsers/devices, it will use the sub-millisecond accuracy, otherwise it will have only millisecond accuracy. (accurate = Chrome desktop for sure,... others ???) I'll let you see below how i use the time of rAF to build the current 'dt' = elapsed time since last frame, and 'applicationTime' = time elapsed in the application (not counting when you tabbed out the app). A secondary benefit of this method is that you can change easily the application speed to have 'bullet-time' or speed up (or even rewind if speed is <0). fiddle is here : http://jsfiddle.net/gamealchemist/KVDsc/ // current application time, in milliseconds. var applicationTime = 0; // scale applied to time. // 1 means no scale, <1 is slower, >1 faster. var timeSpeed = 1; // after launchAnimation is called, // draw/handleInput/update will get called on each rAF function launchAnimation() { requestAnimationFrame(_launchAnimation); } // ------------- Private methods ---------------- function _launchAnimation(now) { _lastTime = now; applicationTime = 0 requestAnimationFrame(_animate); } // ---------------------------------------------- // Animation. // Use launchAnimate() to start the animation. // draw, handleInput, update will be called every frame. // ---------------------------------------------- function _animate(now) { requestAnimationFrame(_animate); // _______________________ var dt = now - _lastTime; if (dt < 12) return; // 60 HZ max if (dt > 200) dt = 16; // consider 1 frame elapse on tab-out _lastTime = now; dt *= timeSpeed; applicationTime += dt; // _______________________ handleInput(); // ... // update everything with this frame time step. update(dt); // draw everything draw(); } var _lastTime = 0; (( Notice that to handle most gracefully the tab-out, you have to handle the blur event, cancel the rAF, then set-it again on focus. ))
var now = ( typeof performance === 'object' && 'now' in performance ) ? function() { return performance.now(); } : function() { return ( new Date ).getTime(); };
Webworker canvas performance terrible
I'm trying to use webworkers to render parts of the frames for an animated mandelbrot zoomer, since there is a lot of calculating involved, and since this can be easily split up in blocks this should be an ideal situation for parallel processing. But no matter what I try I do not get any performance in return for the extra cpu the workers use. Compared to a non worker version, in Chrome my benchmark is somewhat slower, in Firefox it is much slower. My guess is that transferring the image data to the webworkers is incredibly expensive, I tried just receiving raw data and using that to render frames but the result is much the same. I don't think this is the ideal way to send and receive imagedata to the workers (in fact I only need to receive it, but I have not been able to create a buffer inside the workers that can be used for the canvas directly). So at it stands sending any serious amount of data creates a real bottleneck. Dear stackoverflow, please help me answer these two questions: What am I doing wrong here, and what can be improved? A demo can be found here for workers, and for reference a non worker version on jsfiddle. Code is as follows: "use strict"; /*global $*/ $(function() { var mandelbrot = new Mandelbrot(); }); var Mandelbrot = function() { // set some values this.width = 500; this.height = 500; this.x_center = -1.407566731001088; this.y_center = 2.741525895538953e-10; this.iterations = 250; this.escape = 4, this.zoom = 10; this.count = 0; this.worker_size = 10; this.received = 0; this.refresh = true; //let's go - create canvas, image data and workers this.init(); //start animation loop this.animate(); }; Mandelbrot.prototype = { init: function() { var self = this; //create main canvas and append it to div var container = $("#content"); this.canvas = document.createElement("canvas"); this.canvas.width = this.width; this.canvas.height = this.height; container.append(this.canvas); //create imagedata this.context = this.canvas.getContext("2d"); this.image = this.context.getImageData(0, 0, this.width, this.height); this.data = new Int32Array(this.image.data.buffer); //create imagedata for webworkers this.worker_data = this.context.getImageData(0, 0, this.width, this.height / this.worker_size); //create webworkers drop them in array this.pool = []; for (var i = 0; i < this.worker_size; i++) { this.pool[i] = new Worker("js/worker.js"); this.pool[i].idle = true; this.pool[i].id = i; //on webworker finished this.pool[i].onmessage = function(e) { self.context.putImageData(e.data, 0, self.height / self.worker_size * e.target.id); self.received++; }; } }, iterate: function() { for (var i = 0; i < this.pool.length; i++) { this.pool[i].postMessage({ image: this.worker_data, id: this.pool[i].id, worker_size: this.worker_size, width: this.width, height: this.height, x_center: this.x_center, y_center: this.y_center, iterations: this.iterations, escape: this.escape, zoom: this.zoom }); } }, animate: function() { requestAnimationFrame(this.animate.bind(this)); //poor man's benchmark over 250 frames if (this.count === 0) { console.time("timer"); } if (this.count === 250) { console.timeEnd("timer"); } //refresh at init, then refresh when all webworkers are done and reset if (this.received === this.worker_size | this.refresh) { this.received = 0; this.refresh = false; this.count++; this.zoom *= 0.95; this.iterate(); } } }; and worker.js: self.onmessage = function(e) { "use strict"; var x_step = e.data.zoom / e.data.width; var y_step = e.data.zoom / e.data.height; var y_start = e.data.height / e.data.worker_size * e.data.id; var y_end = e.data.height / e.data.worker_size; var data = new Int32Array(e.data.image.data.buffer); for (var y = 0; y < y_end; y++) { var iy = e.data.y_center - e.data.zoom / 2 + (y + y_start) * y_step; for (var x = 0; x < e.data.width; x++) { var rx = e.data.x_center - e.data.zoom / 2 + x * x_step; var zx = rx; var zy = iy; var zx2 = 0; var zy2 = 0; for (var i = 0; zx2 + zy2 < e.data.escape && i < e.data.iterations; ++i) { zx2 = zx * zx; zy2 = zy * zy; zy = (zx + zx) * zy + iy; zx = zx2 - zy2 + rx; } data[y * e.data.width + x] = (255 << 24) | (i << 16) | (i << 8) | i; } } self.postMessage(e.data.image); };
The problem is that you are iterating over every pixel in the parent picture. If you restrict the iteration to the smaller of the two images, things will be much faster. Also, if you tile the drawing, each tile could be handled in a separate web worker, thus increasing the palletization of each section of the image. I wrote this: http://robertleeplummerjr.github.io/CanvasWorker/ which does exactly what you want.
I actually tried the same thing on this experiment, this is a displacement filter: http://www.soundstep.com/blog/experiments/displacement-js/heart/ http://www.soundstep.com/blog/2012/04/25/javascript-displacement-mapping/ I created a worker in the filter and I compute the pixel together before posting them back to the main app. Basically iterating on all the pixels inside a worker. Before the worker, I have in a loop 4 getImageData, this can't be done in the worker. It takes around 15% CPU on chrome no matter what. So, overall I get 70% CPU without the worker, and I get 90% CPU with the worker. I suppose the actions that cannot be done in the worker, such as "getImageData" AND "putImageData", plus the fact of having the worker itself, takes more CPU than not having a worker. It would probably be better if we were able to send other types of data so we could do the getImageData and putImageData inside the worker. Not sure there's another way sending and receiving bytes to treat and reconstruct the canvas content. http://typedarray.org/concurrency-in-javascript/