HTML5 canvas context does not update in Safari - javascript

UPDATE: This has been submitted as a WebKit bug report: https://bugs.webkit.org/show_bug.cgi?id=246498
UPDATE: I added a code snippet with contributions by #Kaiido, the bug is more apparent on his example fiddle.
I've been experiencing a very weird HTML5 2D Canvas issue in Safari that is not present in Chrome or Firefox. This has started affecting a website that has been in production for 5 years.
I have canvas drawing code like this, which produces a flicker in some of the lines where color is not correctly applied:
const canvasWidth = 600;
const gapBetweenLines = 5;
const nbrLines = canvasWidth / gapBetweenLines;
const canvasHeight = 100;
const canvas = document.getElementById('map');
canvas.width = canvasWidth;
canvas.height = canvasHeight;
// create an array of line objects, each with a with random color
let lines = [];
for (let i = 0; i < nbrLines; i++) {
lines.push({
index: i,
x: i * gapBetweenLines,
color: '#' + Math.floor(Math.random() * 16777215).toString(16)
// force always 6 length
.padStart(6, "0")
});
}
// function to shuffle the given array in place
function shuffle(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
// draw lines on the canvas at specific intervals with the random colors
function drawLines() {
const shuffledLines = [...lines];
shuffle(shuffledLines);
let ctx = canvas.getContext('2d');
for (let i = 0; i < nbrLines; i++) {
const line = shuffledLines[i];
ctx.strokeStyle = line.color;
ctx.beginPath();
ctx.moveTo(line.x, 0);
ctx.lineTo(line.x, canvasHeight);
ctx.stroke();
}
}
// call the drawLines function every 100ms
setInterval(drawLines, 200);
<!DOCTYPE html>
<html>
<body>
<h1>Flickering Lines</h1>
<canvas id="map"></canvas>
<div id="lineinfo"></div>
</body>
</html>
In Safari, the stroke colors do not consistently change to their proper strokeStyle value. It behaves as if some of the strokeStyle assignments are being ignored. All the lines draw, but many of them retain the color from prior loop iterations.
This seems like a fairly basic use case for stroke(), so I cannot figure out what I may be doing wrong here; any suggestions would be greatly appreciated.

Related

How to obtain a gaussian filter in javascript

python has fspecial('gaussian', f_wid, sigma) to make gaussian easy(link). Does Javascript has similar utils?
Yes this is possible with Javascript, but it isn't as easy as it is with Python.
Since you are looking for a javascript solution, it is good to know that HTML5 Canvas element has some built-in filters. An example snippet of Canvas blur looks like this:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const texture = document.querySelector("img");
texture.onload = function(){
canvas.width = this.width;
canvas.height = this.height;
ctx.filter = 'blur(10px)';
ctx.drawImage(this, 0, 0);
}
Other approaches that work well for the web are CSS and SVG filters and they are compatible with Canvas as well. However, they are not well designed for cases where our code runs in Web Worker.
The makeGaussKernel function creates a one dimensional array with the appropriate filter size and coefficients.
function makeGaussKernel(sigma){
const GAUSSKERN = 6.0;
var dim = parseInt(Math.max(3.0, GAUSSKERN * sigma));
var sqrtSigmaPi2 = Math.sqrt(Math.PI*2.0)*sigma;
var s2 = 2.0 * sigma * sigma;
var sum = 0.0;
var kernel = new Float32Array(dim - !(dim & 1)); // Make it odd number
const half = parseInt(kernel.length / 2);
for (var j = 0, i = -half; j < kernel.length; i++, j++)
{
kernel[j] = Math.exp(-(i*i)/(s2)) / sqrtSigmaPi2;
sum += kernel[j];
}
// Normalize the gaussian kernel to prevent image darkening/brightening
for (var i = 0; i < dim; i++) {
kernel[i] /= sum;
}
return kernel;
}
Source Fiveko

Use GPU to draw on HTML5 Canvas on Google Chrome

I'm using a canvas to draw a marker (in SVG) hundreds (sometimes thousands) of times. The size of the canvas is 300x300 pixels and the SVG is 18x25 pixels.
The code is quite straigth forward, I have a for loop where I draw the markers on the canvas:
drawNewTile = (canvas, points) => {
const drawn = {};
const context = canvas.getContext('2d');
if (points.length === 0) return;
for (let i = points.length; i -= 1;) {
const [x, y] = points[i];
if (!drawn[`${x}:${y}`]) {
drawn[`${x}:${y}`] = true;
this.drawMarker(context, x, y);
}
}
};
drawMarker = (context, x, y) => {
const x_ = Math.floor(x - this.MARKER_WIDTH / 2 + this.MAX_DIMENSION_OF_MARKER);
const y_ = Math.floor(y - this.MARKER_HEIGHT + this.MAX_DIMENSION_OF_MARKER);
context.drawImage(this.marker, x_, y_, this.MARKER_WIDTH, this.MARKER_HEIGHT);
};
I have already put in place some optimizations: like the for loop, only draw those points which are not already drawn, use integer coordinates, etc.
After that, I have some good results, but my page it gets a little bit stuck on Google Chrome. Nonetheless, to my surprise, in Firefox it goes fast as hell, like, really really fast. So I made some digging with the performance tab of Google Chrome and I found that my code was using a lot of CPU and that's slow.
I also found this article where it says that Chrome uses some heuristics to determine if it uses a CPU or a GPU to draw the canvas.
So, my question is, how do I force the use of GPU on Chrome? Is there any flag I can set or something similar? Do you any other way to speed that the drawing process?
The problem is that apparently Chrome keeps SVG images in the CPU, and rasterizes it at every new call to drawImage().
Simply rasterizing it yourself will make Chrome's performances grow instantly.
To do that, use the createImageBitmap() method, which will create an ImageBitmap that the browser will be able to store directly in the GPU's memory.
Safari just did expose this method in the newest version of their browser, so you may still want to use a polyfill for it. While in this case, simply drawing on a canvas would be enough, I made such a polyfill which does include a few features that most browsers don't support yet.
(async () => {
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const select = document.querySelector("select");
const html_img = new Image();
const svg_str = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 25" width="18" height="25">
<circle cx="9" cy="9" r="6"/>
</svg>`;
const svg_blob = new Blob([svg_str], {
type: "image/svg+xml"
});
ctx.font = "20px sans-serif";
ctx.fillStyle = "red";
html_img.src = URL.createObjectURL(svg_blob);
const sources = {
html_img,
bitmap: await createImageBitmap(svg_blob)
};
const times = [];
await html_img.decode();
anim();
function anim() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let y = 0; y < canvas.width; y += 10) {
for (let x = 0; x < canvas.width; x += 5) {
ctx.drawImage(sources[select.value], x, y);
}
}
requestAnimationFrame(anim);
// ultra rough FPS counter
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
ctx.fillText(fps + "FPS", 30, 30);
}
})();
<!-- createImageBitmap polyfill for old browsers --> <script src="https://cdn.jsdelivr.net/gh/Kaiido/createImageBitmap/dist/createImageBitmap.js"></script>
source: <select>
<option value="bitmap">ImageBitmap</option>
<option value="html_img">HTMLImage</option>
</select><br>
<canvas width="300" height="300"></canvas>

How to display several different images in canvas?

I get an issue with my javascript code... I'd like to display 5 lines of different pictures in my canvas but only one line is displayed and the LIFE function is only called 16 times (canvas.width / imgW).
Why the LIFE function can't be called after the 'while' loop ?
Thank you !
This is the result i get
<script type="text/javascript">
var canvas = document.getElementById('canvas');
var ctx = canvas.getContext('2d');
var tabImages = ['css3.svg', 'adobe-1.svg', 'html-5.svg', 'jquery.svg'];
var canvasW = canvas.width;
var canvasH = canvas.height;
var imgW = 50;
var imgH = 50;
var x = 0, y = 0, i = 0, posY = 1, increImg = 0, posX = 0;
/* I want to display 5 lines of pictures but only one is displayed */
for(var lignes = 0 ; lignes < 5 ; lignes++) {
i = 0;
while(posX < canvasW) {
/* That function is only called 16 times but i know only that
way to display several differentes images in canvas */
(function(posX, posY) {
var img = new Image();
if(increImg === tabImages.length-1)
increImg = 0;
img.src = 'testanim/img/'+tabImages[increImg];
img.onload = function() {
ctx.drawImage(img, posX, posY, imgW, imgH);
}
})(posX, posY)
i++;
increImg++;
posX = imgW * i;
}
posY = lignes * imgH;
}
</script>
are you looking for something like à kind of gallery :
//our images
let imageArr = ["animage","another","another","another","another","another","another","another","another","another"];
//getting the context (mycanvas is the id of the canvas element)
let context = mycanvas.getContext("2d")
//checking the context
if(context)
// for all images in my array(can't do for in because of the calculation of position later on )
for(let i = 0;i<imageArr.length;i++)
{
//creation of an image html element
let imgel = document.createElement('img');
//addition of src tag
imgel.src=imageArr[i];
//drawing of the image (parameters:(image, xcoordinates,ycoordinates,width,height)
context.drawImage(imgel, (i%5)*100, (Math.floor(i/5))*100,100,100);
//drawing a rectangle around them
context.strokeRect((i%5)*100,(Math.floor(i/5))*100,100,100);
}
canvas{border:1px solid black;}
<canvas id="mycanvas" height="500" width="500"></canvas>
so in order to have several lines of your image in your code you have to use % operator:
for instance if you want 5 lines this gives you :
(i%canvasW)*imgW
what this does it simply stops after 5 images and if you have to go to the line(see below) it will start from 0 again : 0%5 = 0 1%5 = 1 , ... , 5%5 = 0 6%5 = 1
On the other side for the height if you want to go to the line you need to do some kind of oposite calculation which is / this gives you :
Math.floor(i/5)*imgH
This gives you the lines you are hoping for
so if i resume you will have :
ctx.drawImage(img, (i%canvasW)*imgW, Math.floor(i/5)*imgH, imgW, imgH);
and that's also why your while loop is not going through several lines, you have to calculate your posX differently like so :
posX = (i%canvasW)*imgW;
and throw in another variable for the height which will be the condition to stop your while loop
for instance let's say you want to stop once the last image of the last line is hit you would have to calculate this condition like so :
if(canv.getContext("2d"))
{
let max = canv.width*canv.height;
let current = 0;
let imgW=50;
let imgH=50;
let i = 0;
while(current<=max)
{
console.log("stuff");
current = (Math.floor(i/canv.height)*imgH)*((i%canv.width)*imgW);
i++;
}
}
<canvas id="canv" width="500" height="500"></canvas>
i strongly advise using for loops since you almost can calculate the number of steps for every problem :
for(let i = 0; i<(canvas.width*canvas.height);i++)

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>

Drawing in canvas becomes slower and slower when it draws more stuff during loop

I am trying to incrementally draw 3 lines which are 120 degrees from each other from a point using html5 canvas. The vertex of each lines will become another 3 new center point and spawns another 3 lines at each center and it repeats this..
My problem is, the incremental speed becomes slower and slower (or the drawing becomes slower) as more items are drawn. (maybe something happens in my code but I am not quite familiar how canvas exactly works...). You can copy the code and run it in your local browser to see what I means.
Please see my code (it is very easy to understand) and tell me what causes this.
<!DOCTYPE HTML>
<html>
<head>
<style>
body {
margin: 0px;
padding: 0px;
}
</style>
</head>
<body>
<canvas id="canvas" ></canvas>
<script>
window.requestAnimFrame = (function(callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame ||
function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
canvas.width= window.innerWidth;
canvas.height= window.innerHeight;
// some staring test values
var centerPt={x:canvas.width/2,y:canvas.height/2};
var radius=100;
var angle=0;
// calculate the 3 endpoints at 120 degree separations
var endPt000=anglePoint(centerPt,90);
var endPt120=anglePoint(centerPt,210);
var endPt240=anglePoint(centerPt,330);
var length = 0;
var maxLength = 100;
var centreSet = new Array();
centreSet = getCentres();
var counter = 0;
var end = centreSet.length;
init();
function init() {
start(centreSet[0].x, centreSet[0].y);
}
function start(myX, myY) {
centerPt.x = myX;
centerPt.y = myY;
animate(centerPt, length);
}
function animate(centerPt,length) {
// update
// clear
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw stuff
draw(centerPt,length);
length = length + 1;
// request new frame
if(length < maxLength){
requestAnimFrame(function() {
animate(centerPt,length);
});
}
else{
if(counter < end){
counter = counter + 1;
centerPt.x = centreSet[counter].x;
centerPt.y = centreSet[counter].y;
endPt000=anglePoint(centerPt,90);
endPt120=anglePoint(centerPt,210);
endPt240=anglePoint(centerPt,330);
length = 0;
setTimeout(function(){animate(centerPt, length);},600);
}
}
}
// draw a red center dot
// draw 3 blue endpoint dots
// draw 3 lines from center going slider% of the way to the endpoints
function draw(centerPt,sliderValue){
var pct=sliderValue;
ctx.clearRect(0,0,canvas.width,canvas.height);
line(centerPt,pointAtPercent(centerPt,endPt000,pct),"green");
line(centerPt,pointAtPercent(centerPt,endPt120,pct),"green");
line(centerPt,pointAtPercent(centerPt,endPt240,pct),"green");
}
// calc XY at the specified angle off the centerpoint
function anglePoint(centerPt,degrees){
var x=centerPt.x-radius*Math.cos( degrees*Math.PI/180 );
var y=centerPt.y-radius*Math.sin( degrees*Math.PI/180 );
return({x:x,y:y});
}
// just draw a line from point1 to point2
function line(pt1,pt2,color){
// ctx.beginPath();
ctx.moveTo(pt1.x,pt1.y);
ctx.lineTo(pt2.x,pt2.y);
ctx.strokeStyle=color;
ctx.lineWidth=2;
ctx.stroke();
}
// calc XY which is a specified percent distance from pt1 to pt2
function pointAtPercent(pt1,pt2,sliderValue) {
// calculate XY at slider% towards pt2
var x = pt1.x + (pt2.x-pt1.x) * sliderValue/100;
var y = pt1.y + (pt2.y-pt1.y) * sliderValue/100;
return({x:x,y:y});
}
//the following are used to get all the center points...
function getCentres() {
var x = window.innerWidth/2;
var y = window.innerHeight/2;
centreSet[0] = centerPt;
var ref = 0;
var end = 0;
var b = true;
var tempCenter = centerPt;
for(var j = 0; j < 5; j++){
tempCenter = centreSet[ref];
end = end + 1;
centreSet[end] = anglePoint(tempCenter,90);
end = end + 1;
centreSet[end] = anglePoint(tempCenter,210);
end = end + 1;
centreSet[end] = anglePoint(tempCenter,330);
ref = ref+1;
}
return centreSet;
}
</script>
</body>
</html>
The problem is you are appending and appending the path. This means that each time you call stroke() the new line together with all the old lines are stroked. You won't see this clearly as the old lines are drawn on top in the same location. And as more and more lines are added the more time it takes to stroke them..
To prevent this you need to break the path. Do this with beginPath().
If you activate your out-commented line it should work fine:
function line(pt1,pt2,color){
ctx.beginPath(); //<-- activate this
ctx.moveTo(pt1.x,pt1.y);
ctx.lineTo(pt2.x,pt2.y);
ctx.strokeStyle=color;
ctx.lineWidth=2;
ctx.stroke();
}

Categories