My JS code:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var mouse = {x:0,y:0}
const times = [];
let fps;
function refreshLoop() {
window.requestAnimationFrame(() => {
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
refreshLoop();
});
}
refreshLoop();
function draw() {
ctx.fillStyle = "black"
ctx.fillRect(0, 0, c.width, c.height);
ctx.strokeStyle = "white"
ctx.beginPath();
var e = window.event;
ctx.arc(mouse.x, mouse.y, 40, 0, 2*Math.PI);
ctx.stroke();
ctx.font = "30px Comic Sans MS";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.fillText(fps, c.width/2, c.height/2);
}
setInterval(draw, 0);
document.addEventListener('mousemove', function(event){
mouse = { x: event.clientX, y: event.clientY }
})
My HTML is just the canvas declaration.
To my understanding, setinterval(x, 0) is supposed to run as fast as possible but it's never exceeding 60fps. I'm trying to hit 240+ fps to reduce input lag.
First, never use setInterval(fn, lessThan10). There is a great possibility that fn will take more than this time to execute, and you may end up stacking a lot of fn calls with no interval at all, which can result* in the same as the well known while(true) browser crasher®.
*Ok, in correct implementations, that shouldn't happen, but you know...
Now, to your question...
Your code is quite flawn.
You are actually running two different loops concurrently, which will not be called at the same interval.
You are checking the fps in a requestAnimationFrame loop, which will be set at the same frequency than your Browser's painting rate (generally 60*fps*).
You are drawing in the setInterval(fn, 0)
Your two loops are not linked and thus, what you are measuring in the first one is not the rate at which your draw is called.
It's a bit like if you did
setInterval(checkRate, 16.6);
setInterval(thefuncIWantToMeasure, 0);
Obviously, your checkRate will not measure thefuncIWantToMeasure correctly
So just to show that a setTimeout(fn, 0) loop will fire at higher rate:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
var mouse = {
x: 0,
y: 0
}
const times = [];
let fps;
draw();
function draw() {
const now = performance.now();
while (times.length > 0 && times[0] <= now - 1000) {
times.shift();
}
times.push(now);
fps = times.length;
ctx.fillStyle = "black"
ctx.fillRect(0, 0, c.width, c.height);
ctx.strokeStyle = "white"
ctx.beginPath();
ctx.arc(mouse.x, mouse.y, 40, 0, 2 * Math.PI);
ctx.stroke();
ctx.font = "30px Comic Sans MS";
ctx.fillStyle = "red";
ctx.textAlign = "center";
ctx.fillText(fps, c.width / 2, c.height / 2);
setTimeout(draw, 0);
}
<canvas id="myCanvas"></canvas>
Now, even if a nested setTimeout loop is better than setInterval, what you are doing is a visual animation.
It makes no sense to draw this visual animation faster than the browser's painting rate, because what you will have drawn on this canvas won't be painted to screen.
And as said previously, that's exactly the rate at which an requestAnimationFrame loop will fire. So use this method for all your visual animations (At least if it has to be painted to screen, for some rare case there are other methods I could link you to in comments if needed).
Now to solve your actual problem, which is not to render at higher rate, but to handle user's inputs at such rate, then the solution is to split your code.
Keep your drawing part bound to a requestAniamtionFrame loop, doesn't need to get faster.
Update your object's values that should respond to user's gesture synchronously from user's input. Though, beware some user's gestures actually fire at very high rate (e.g WheelEvent, or window's resize Event). Generally, you don't need to get all the values of such events, so you might want to bind these in rAF throttlers instead.
If you need to do collision detection with moving objects, then perform the Math that will update moving objects from inside the user's gesture, but don't draw it on screen.
Related
When creating an HTML canvas I was planning on making these cylinders and animating marbles moving inside them. However, when trying to do so it would just delete everything. After messing around with my code, I discovered the problem was due to the fillStyle which was a CanvasPattern from an image.
This snippet simulates exactly what I am experiencing. The rectangle draws perfectly fine, however, after 1 second, when the interval runs, it disappears and there is no arc or "marble" drawn. There are no errors in console either
With Interval (Not working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
When I get rid of the interval it would work, but when the interval is there, nothing is drawn.
I have absolutely no idea why this is happening and I cannot find anything on the internet regarding this problem. Is there a way I can animate this marble while having the image continue to mask its fillStyle??
Without Interval (Working):
let canv = document.getElementById("canvas");
let ctx = canv.getContext('2d');
let matte = new Image(canv.width, canv.height);
matte.onload = function() {
var pattern = ctx.createPattern(matte, 'repeat');
ctx.globalCompositeOperation = 'source-in';
ctx.rect(0, 0, canv.width, canv.height);
ctx.fillStyle = pattern;
ctx.fill();
};
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
ctx.lineWidth = "5";
ctx.fillRect(0, 0, 50, 50); // This dissapears when the setInterval runs???? Marble doesn't even draw
let x = 60,
y = 20;
//var draw = setInterval(function() { // Drawing the marble
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
y += 1;
//}, 1 * 1000);
<html>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Things I've tried:
Got rid of beginPath and closePath, doesn't make anything disappear but doesn't display arc
Recreating pattern inside the interval
Making the fillstyle a colour for everything (Works)
Making the fillstyle of the marble a colour (Doesnt work)
EDIT: After looking some more, I believe the problem is in the globalCompositeOperation. It's what deals with the pattern intersecting the drawing. When looking at all the types, source-in is the only one that satisfies my expected result, however, it's not working in this situation weirdly.
Thank you in advance
The problem is your ctx.globalCompositeOperation instruction. Using source-in, you're explicitly telling the canvas to make anything that's a different color from the new thing you're drawing (on a per pixel basis) transparent. Since every pixel is different, everything becomes transparent and you're left with what looks like an empty canvas (even if the ImageData will show RGBA data in which the RGB channels have meaningful content, but A is 0).
Remove the globalCompositeOperation rule and you're good to go, but you should probably take some time to rewrite the logic here, so that nothing happens until your image is loaded, because your code is pretty dependent on that pattern existing: wait for the image to load, the build the pattern, assign it to the context, and then start your draw loop.
const canv = document.getElementById("canvas");
const ctx = canv.getContext('2d');
let x = 60, y = 20;
function start() {
const matte = new Image(canv.width, canv.height);
matte.addEventListener(`load`, evt =>
startDrawing(ctx.createPattern(matte, 'repeat'))
);
matte.addEventListener(`load`, evt =>
console.error(`Could not load ${matte.src}...`);
);
matte.src = "https://www.muralswallpaper.com/app/uploads/classic-red-marble-textures-plain-820x532.jpg"; // An image src
}
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
setInterval(() => {
draw();
y += 10;
}, 1 * 1000);
}
function draw() {
ctx.beginPath();
ctx.arc(x, y, 10, 0, 2 * Math.PI);
ctx.closePath();
ctx.fill();
ctx.stroke();
}
// and kick everything off
start();
Although on another note, normally setInterval is not the best choice for animations: you usually want requestAnimationFrame instead, with a "wall time" check (e.g. doing things depending on the actual clock, instead of trusting intervals):
...
function startDrawing(pattern) {
ctx.strokeStyle = `red`;
ctx.fillStyle = pattern;
startAnimation();
}
let playing, lastTime;
function startAnimation() {
playing = true;
lastTime = Date.now();
requestAnimationFrame(nextFrame);
}
function stopAnimation() {
playing = false;
}
function nextFrame() {
let newTime = Date.now();
if (newTime - lastTime >= 1000) {
draw();
}
if (playing) {
lastTime = newTime;
requestAnimationFrame(nextFrame);
}
}
...
https://jsbin.com/wawecedeve/edit?js,output
There are thousands of moving particles on an HTML5 canvas, and my goal is to draw a short fading trail behind each one. A nice and fast way to do this is to not completely clear the canvas each frame, but overlay it with semi-transparent color. Here is an example with just one particle:
var canvas = document.getElementById('display');
var ctx = canvas.getContext('2d');
var displayHeight = canvas.height;
var backgroundColor = '#000000';
var overlayOpacity = 0.05;
var testParticle = {
pos: 0,
size: 3
};
function render(ctx, particle) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
function update(particle) {
particle.pos += 1;
}
// Fill with initial color
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
function mainLoop() {
update(testParticle);
render(ctx, testParticle);
requestAnimationFrame(mainLoop);
}
mainLoop();
<canvas id="display" width="320" height="240"></canvas>
There is an apparent problem: with low opacity values, the trail never fades away completely. You can see the horizontal line that (almost) does not fade in my single-particle example. I understand why this happens. ColorA overlayed by semi-transparent ColorB is basically a linear interpolation, and ColorA never fully converges to ColorB if we repeatedly do the following:
ColorA = lerp(ColorA, ColorB, opacityOfB)
My question is, what can I do to make it converge to the background color, so that trails don't remain there forever? Using WebGL or drawing trails manually are not valid options (because of compatibility and performance reasons respectively). One possibility is to loop over all canvas pixels and manually set pixels with low brightness to background color, although it may get expensive for large canvases. I wonder if there are better solutions.
As a workaround which could work in some cases is to set the overlayOpacity up to 0.1 (this value converges) but draw it only every x times and not in every render call.
So when drawn only every other time it keeps more or less the same trail length.
var renderCount = 0;
var overlayOpacity = 0.1;
function render(ctx, particle) {
if((renderCount++)%2 == 0) {
ctx.globalAlpha = overlayOpacity;
ctx.fillStyle = backgroundColor;
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
ctx.globalAlpha = 1.0;
ctx.fillStyle = '#FFF';
ctx.fillRect(particle.pos, displayHeight / 2, particle.size, particle.size);
}
Obviously the disadvantage is that it looks more jerked and perhaps this may not be acceptable in your case.
Best solution is to use the composite operation "destination-out" and fade to a transparent background. Works well for fade rates down to globalAlpha = 0.01 and event a little lower 0.006 but it can be troublesome below that. Then if you need even slower fade just doe the fade every 2nd or 3rd frame.
ctx.globalAlpha = 0.01; // fade rate
ctx.globalCompositeOperation = "destination-out" // fade out destination pixels
ctx.fillRect(0,0,w,h)
ctx.globalCompositeOperation = "source-over"
ctx.globalAlpha = 1; // reset alpha
If you want a coloured background you will need to render the animation on an offscreen canvas and render it over the onscreen canvas each frame. Or make the canvas background the colour you want.
If someone struggles with this, here is a workaround that worked for me:
// Do this instead of ctx.fillStyle some alpha value and ctx.fillRect
if(Math.random() > 0.8){
ctx.fillStyle = 'rgba(255, 255, 255, '+getRandomNumber(0.1,0.001)+')';
ctx.fillRect(0, 0, canvas.width, canvas.height);
}
// Define this helper function somewhere in your code
function getRandomNumber(minValue, maxValue) {
return Math.random() * (maxValue - minValue) + minValue;
}
It also works for different colored backgrounds. Adjust trail length by playing around with Math.random() > 0.8 and getRandomNumber(0.1,0.001).
Seems like there are other questions like this and I'd like to avoid a buffer and/or requestAnimationFrame().
In a recent project the player is flickering but I cannot find out the reason. You can find the project on JSFiddle: https://jsfiddle.net/90wjetLa/
function gameEngine() {
timer += 1;
timer = Math.round(timer);
// NEWSHOOT?
player.canShoot -= 1;
// MOVE:
movePlayer();
shootEngine(); // Schussbewegung & Treffer-Abfrage
// DRAW:
ctx.beginPath();
canvas.width = canvas.width;
ctx.beginPath();
ctx.fillStyle = 'black';
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.fill();
drawField();
drawPlayer();
drawShoots();
setTimeout(gameEngine, 1000 / 30);
}
Each time you write to a visible canvas the browser want's to update the display. Your drawing routines might be out of sync with the browsers display update. The requestAnimationFrame function allows you to run all your drawing routines before the display refreshes. Your other friend is using an invisible buffer canvas. Draw everything to the buffer canvas and then draw the buffer to the visible canvas. The gameEngine function should only run once per frame and if it runs multiple times you could see flicker. Try the following to clear multiple runs in the same frame.
(edit): You might also want to clear the canvas instead of setting width.
(edit2): You can combine the clearRect, rect, and fill to one command ctx.fillRect(0, 0, canvas.width, canvas.height);.
var gameEngineTimeout = null;
function gameEngine() {
// clear pending gameEngine timeout if it exists.
clearTimeout(gameEngineTimeout);
timer += 1;
timer = Math.round(timer);
// NEWSHOOT?
player.canShoot -= 1;
// MOVE:
movePlayer();
shootEngine(); // Schussbewegung & Treffer-Abfrage
// DRAW:
ctx.beginPath();
//canvas.width = canvas.width;
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//ctx.beginPath();
ctx.fillStyle = 'black';
ctx.fillRect(0, 0, canvas.width, canvas.height);
//ctx.fill();
drawField();
drawPlayer();
drawShoots();
gameEngineTimeout = setTimeout(gameEngine, 1000 / 30);
}
I want to make a simple page that grows circles from its center ad infinitum. I'm almost there, but I can't figure out how to repeatedly grow them (resetting the radius i to 0 at a certain interval and calling the function again). I assume it will require a closure and some recursion, but I can't figure it out.
// Initialize canvas
var canvas = document.createElement("canvas");
var ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
document.getElementsByTagName('body')[0].appendChild(canvas);
// Grow a circle
var i = 0;
var draw = function() {
ctx.fillStyle = '#000';
ctx.beginPath();
ctx.arc(canvas.width / 2, canvas.height / 2, i, 0, 2 * Math.PI);
ctx.fill();
i += 4;
window.requestAnimationFrame(draw);
}
draw();
Two things I'd do...
First, modify your draw function so that if the circle gets to a certain size, the i variable is reset back to zero. That starts the circle over again.
Second, add a setInterval timer to call your draw function at some time interval. See http://www.w3schools.com/js/js_timing.asp for details.
This setup will cause draw() to be called regularly, and the reset of i to zero makes it repeat.
So this did indeed require a closure. We wrap the initial function in a closure, and call it's wrapper function, which reinitializes I every time when called. draw() grows a single circle, and drawIt()() starts a new circle.
var drawIt = function(color) {
var i = 0;
return function draw() {
ctx.fillStyle = color;
ctx.beginPath();
ctx.arc(canvas.width/2, canvas.height/2, i, 0, 2 * Math.PI);
ctx.fill();
i+=1*growthFactor;
// Growing circles until they are huge
if (i < canvas.width) {
window.requestAnimationFrame(draw);
if (i === spacing) {
circles++
drawIt(nextColor())();
}
}
}
};
drawIt(nextColor())();
})();
I am attempting to make a Pong clone using HTML5 canvas. I want to draw a dashed line down the middle of the playing field as is found in the original Pong. I am doing this by extending the CanvasRenderingContext2D object as shown in David Geary's excellent book:
CanvasRenderingContext2D.prototype.dashedLine = function (x1, y1, x2, y2, dashLength) {
dashLength = dashLength === undefined ? 5 : dashLength;
var deltaX = x2 - x1;
var deltaY = y2 - y1;
var numDashes = Math.floor(
Math.sqrt(deltaX * deltaX + deltaY * deltaY) / dashLength);
for (var i=0; i < numDashes; ++i) {
context[ i % 2 === 0 ? 'moveTo' : 'lineTo' ]
(x1 + (deltaX / numDashes) * i, y1 + (deltaY / numDashes) * i);
}
I then have a render() function that actually makes all the calls to render elements on the canvas. Included in this is my renderBackground() function which colors the background and draws the dashed line:
function render() {
ctx.clearRect(0, 0, cWidth, cHeight);
renderBackground();
// Rest removed for brevity
}
function renderBackground() {
ctx.lineWidth = 5;
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, cWidth, cHeight);
ctx.dashedLine(0, 0, 0, cHeight, 10);
ctx.stroke()
}
Then at the end I have a function called animLoop() that actually calls the render() function and makes use of requestAnimationFrame() for smoother animations:
function animLoop() {
render();
requestAnimationFrame(animLoop);
}
window.requestAnimationFrame = (function() {
return (
window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
}
);
})();
If I let my game run for more than 30 seconds it starts slowing down dramatically to the point that it is unplayable and CPU usage by the browser hovers around 134% for both Firefox and Chrome. The slowness is only present when I am rendering the dashed line. I am not sure what is going, but below I also ran my code through Chrome Inspectors profiler and get the following:
My renderBackground() function is only taking .46% of the CPU time. Also I am not sure what the (program) is supposed to signify. Any thoughts on what could be causing the slowness?
Also you can see the complete code I have so far on my Github repo.
You are accumulating all calls of lineTo on the default path each time ctx.dashedLine is called and call stroke will stroke all lines in path since the application start. Because you are running an animation, quickly the path will have a LOT of lines to draw when stroke is called each frame.
Add ctx.beginPath() before ctx.dashedLine to solve the problem.
function renderBackground() {
ctx.lineWidth = 5;
ctx.strokeStyle = '#FFFFFF';
ctx.fillStyle = '#000000';
ctx.fillRect(0, 0, cWidth, cHeight);
ctx.beginPath(); // <-- add it here
ctx.dashedLine(0, 0, 0, cHeight, 10);
ctx.stroke();
}
When drawing using a path, you are using a virtual "pen" or "pointer". So you'll create a virtual path with begin path, draw the lines and finally stroke that lines. In next frame you'll begin a new virtual path, draw the new lines in the path and stroke again. This way the performance stays stable.
Demo