Having problems emmiting 2 arrays to the client.
I have 2 objects and all instances are created on Server side . The socket.emit is from what I understood done on the Server 25s/s and per socket that exist.
Now i created bullets in each ship and want to get these to the client.
The thing that makes me headache is I am waiting on the Client side with socket.on per ship which is a 1:1 per each emitted socket from the server.
And now come a 1:n per each socket on the Server that is shooting the bullets. Can I actually emit 2 array with 2 emit execution or does it Need to be ohne emit from each socket with all data in one array?
My Problem is that the array bullet doesnt exist on the Client side !
So my ship values are at the client side anyhow I dont have a bullet array on the Client side
I tested it with a draw in a fixed x and y
for (var i in bullet){
ctx.fillText("X",100,100);
}
but the client draws nothing means I have no array at the client side. Also did a alert if I would have more then 10 bullets but even that one doesn’t pop up
Appreciate any help
App.js
for(var q in SOCKET_LIST){
var socket = SOCKET_LIST[q];
var f =0;
for(var k = 0;k<allbullets.length;k++)
{
if (allbullets[k].user_id === q)
{
packbul= {
x:allbullets[k].xtype.x,
y:allbullets[k].xtype.y,
userid:allbullets[k].user_id,
}// array end
}
f++;
} //end for
socket.broadcast.emit('newBullet',packbul);
if (allobjects[q] === undefined)
{
}
else{
console.log("q:"+q);
pack[q] = {
x:allobjects[q].xtype.x,
y:allobjects[q].xtype.y,
userid:q,
}// array end
socket.broadcast.emit('newClientship',pack[q]);
} // else end
} // For ebf.
},1000/25); // Set Interval End
Client
var ship = Array();
var bullet = Array();
socket.on('newClientship',function(data){
ship[data.userid]= data;
});
socket.on('newBullet',function(data){
bullet= data;
});
var previous; // var for renderworld
renderWorld = function(timestamp){
//setInterval(function(){
if (!previous) previous = timestamp;
elapsed = timestamp - previous;
updateFps();
ctx.font="150px Arial";
ctx.fillStyle = "white";
ctx.clearRect(0,0,canvas.width,canvas.height);
for ( var i in ship){ ctx.fillText(ship[i].userid,ship[i].x,ship[i].y);
}
if (bullet.length > 10)
{
alert("ted");
}
for (var i in bullet){
ctx.fillText("X",100,100);
}
drawFps(200,20) ;
previous = timestamp;
window.requestAnimationFrame(renderWorld);
//},1000/25);
}
Just found out that the for loop on Server side doesn’t work
It gives me a „undefined“ if I console.log (allbullets.length)
If I do a console.log(allbullets) it shows me that it exists .
Bulletobj {
user_id: 47,
type: 'Bullet',
radius: 2,
basespeed: 1,
speed: 1,
velX: 0.2979165941720021,
velY: 0.9545919038609927,
targetX: 863,
targetY: 2429,
xtype: Obj { x: 153, y: 154, radius: 3, selected: 0 },
angleDeg: 1.268286927738952,
realAngle: 72.66748817105554 }
Now found a formula to count the items of an obj
console.log("length:"+Object.keys(allbullets).length);
But even that only counts the items in one of the objects and always shows 12
I wanted to have the count of all bullets that have an instance
FYI My ship has a procedure that creates a new bullet
ClientObj.prototype.fire =function (x,y){
allbullets = new Bulletobj(this.xtype.x,this.xtype.y,x,y,1,1, this.user_id);
}
// had put this var outside outside functions for global
allbullets = Array();
App.js
var allbullets = [];
Bulletobj = function(x,y,targetX,targetY,shipid,speed,user_id){
this.user_id = user_id;
this.type = 'Bullet';
this.radius = 2;
this.basespeed = speed;
this.speed = 4;
this.velX = 0;
this.velY = 0;
this.targetX = targetX;
this.targetY = targetY;
this.xtype = new Obj(x,y,3);
w.objects.push(this);
Bulletobj.prototype.update =function (){
tx = this.targetX - this.xtype.x;
ty = this.targetY - this.xtype.y;
dist = Math.sqrt(tx * tx + ty * ty);
this.angleDeg = Math.atan2(ty,tx) ;
this.realAngle = (Math.atan2(ty,tx) * 180/Math.PI + 360 ) % 360 ;
this.velX = (tx / dist) * this.speed ;
this.velY = (ty / dist) * this.speed ;
if (parseInt(dist) > parseInt(this.radius / 2)) {
this.xtype.x += parseInt(this.velX);
this.xtype.y += parseInt(this.velY);
} // if distance schleife end
} // Bulletobj update end
} // Bulletobj end
setInterval(function(){
var pack = Array();
var packbul = Array();
var packbularray = Array();
var spliceArray = Array();
objcnt = w.objects.length;
var i=0;
while (i < objcnt)
{
w.objects[i].update();
if(w.objects[i].hitpoints <= 0 )
spliceArray.push(i);
i++;
} // endwhile
for(var k = 0;k<spliceArray.length;k++)
{
w.objects.splice(spliceArray[k],1);
}
for(var q in SOCKET_LIST){
var socket = SOCKET_LIST[q];
if (allbullets === undefined)
{
}
else {
for(var k = 0;k<allbullets.length;k++)
{
if(allbullets[k].user_id == q)
{
packbul= {
x:allbullets[k].xtype.x,
y:allbullets[k].xtype.y,
userid:allbullets[k].user_id,
}// array end
} // if end
//obj end
} // end else undefined objects
} //end for
console.log(packbul);
socket.emit('newBullet',packbul);
if (allobjects[q] === undefined)
{
}
else{
console.log("q:"+q);
pack[q] = {
x:allobjects[q].xtype.x,
y:allobjects[q].xtype.y,
userid:q,
}// array end
socket.broadcast.emit('newClientship',pack[q]);
} // else end
} // For socket
},1000/25); // Set Interval End
Index.html
var ship = Array();
var bullet= Array();
var bulletdata =Array();
socket.on('newClientship',function(data){
ship[data.userid]= data;
});
socket.on('newBullet',function(data){
;
bulletdata.push(data);
});
var previous; // var for renderworld
renderWorld = function(timestamp){
if (!previous) previous = timestamp;
elapsed = timestamp - previous;
updateFps();
ctx.font="150px Arial";
ctx.fillStyle = "white";
ctx.clearRect(0,0,canvas.width,canvas.height);
for ( var i in ship){ ctx.fillText(ship[i].userid,ship[i].x,ship[i].y);
}
for (var i in bulletdata){
ctx.fillText(".",bulletdata[i].x,bulletdata[i].y);
}
bulletdata = [];
drawFps(200,20) ;
previous = timestamp;
window.requestAnimationFrame(renderWorld);
}
So I managed to get one bullet over to the client. Problem now is that each time a player shoots a bullet the old bullet vanish and the new start from beginning same with another client . 1 st client shoots . If the second client starts shooting the bullet from player 1 is deleted. Normally I wanted to have each client with his own bullets and all bullets that are created as objects on Server side to be drawn on the clients.
The ships work perfect but somehow the bullet flickers and doesn’t fit into the canvas 😔
Thank you
Solved !!! it after 3 frustrating days omg
again thx for the push without it I would have been lost
The problem was mixing object array and standard array.
E.g var x = {} —> Object Array
var x = []. —> Standard Array
People keep saying that you shouldn’t emit objects but atm I am glad the bullets arrived
at client . Also with standard arrays I would need to split after a choosen index and have to set the dataset points by myself . With object arrays it is easier to access them.
Maybe somebody can tell me what I emit when I push an object array into a standard array at the Server side . Is this an object or standard array ? Are the object headers that create performance issues (this is what in read somewhere) included in that array I emit to the client?
I mean on the client I side I receive an array but in the for loop with the ctx I have an object array again which was inside the standard array
if(allbullets[k].user_id == q)
{
packbul= {
x:allbullets[k].xtype.x,
y:allbullets[k].xtype.y,
userid:allbullets[k].user_id,
}// array end
packbularray.push(packbul);
} // if end
Index.html
var ship = Array();
var bullet= Array();
var bulletdata =Array();
socket.on('newClientship',function(data){
ship[data.userid]= data;
//ship.x = data.x;
//ship.y = data.y;
//ship.user = data.userid;
});
socket.on('newBullet',function(data){
bullet= data;
//bulletdata.push(bullet);
//ship.x = data.x;
//ship.y = data.y;
//ship.user = data.userid;
});
I have been working on an issue for a few days now and have been unable to solve it. Please note that I am relatively new to Javascript so not sure if what I have below is the best way to accomplish this but going through this over the last few days has definitely helped me learn some new things.
Also, please note I know I could achieve this very easily using CSS but I wanted to know if there was a Javascript/JQuery solution.
The Issue:
I am attempting to simulate a fadeIn animation on a canvas for some text.
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
The numbers represent font size.
Here is the full code:
$(document).ready(function ()
{
var canvas = $('#myCanvas')[0];
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
var ctx = canvas.getContext('2d');
var introText =
{
"Welcome To A New Day...": "75",
"Full Service Web Design": "50",
"by": "50",
"J. David Hock": "50"
};
function fadeText(timeStamp, t, x, y)
{
var opacity = timeStamp / 1000;
console.log('Timestamp: ' + timeStamp + ' Opacity: ' + opacity);
console.log('t, x, y |' + t +' | ' + x + ' | ' + y)
//ctx.clearRect(0, 0, canvas.width, canvas.height);
//ctx.fillStyle = 'rgba(178, 34, 34, ' + opacity + ')';
//ctx.fillText(t, x, y);
if (opacity < 1)
{
requestAnimationFrame(function (timestamp)
{
fadeText(timestamp, t, x, y)
});
}
}
function MessageObject(x, y, f, fs, t)
{
this.x = x;
this.y = y;
this.f = f;
this.fs = fs;
this.t = t;
}
var msgArray = [];
function CreateMessageArray(myArray, callback)
{
var i = 0;
var v = 75;
var x = 0;
var y = 0;
var f = '';
var fs = '';
var t = '';
for (t in myArray)
{
fs = myArray[t]; //font size
f = 'italic ' + fs + 'px Bradley Hand'; //font type
x = (canvas.width / 2) //x pos of text
msgArray.push(new MessageObject(x, y, f, fs, t))
y = Number(fs);
//alert('x, y, f, t | ' + x + ' | ' + y + ' | ' + f + ' | ' + t);
}
return callback(msgArray);
}
let ProcessMessageArray = function (myArray)
{
var xPrime = 0;
var yPrime = 0;
for (i = 0; i < myArray.length; i++)
{
var msgObject = myArray[i];
var x = msgObject.x;
var y = msgObject.y;
var f = msgObject.f;
var fs = msgObject.fs;
var t = msgObject.t;
ctx.textBaseline = 'top';
ctx.font = f;
var txtWidth = ctx.measureText(t).width
xPrime = x - (txtWidth / 2);
requestAnimationFrame(function(timestamp)
{
fadeText(timestamp, t, x, y)
});
//ctx.fillStyle = 'rgba(178, 34, 34, 1)';
//ctx.fillText(t, xPrime, yPrime);
if (i === 0)
{
yPrime = Number(yPrime) + (2 * Number(fs) - 35);
}
else
{
yPrime = Number(yPrime) + Number(fs);
}
}
}
CreateMessageArray(introText, ProcessMessageArray)
});
The way it is supposed to work is that the CreateMessageArray function creates an array of objects that contain the x-pos, y-pos, etc. for each of the lines of text in the introTextArray.
The ProcessMessageArray is then responsible for outputting each line of text in the introTextArray into it proper position on the screen.
In the ProcessMessageArray there is a call to a requestAnimationFrame function where I was hoping it would "fade in" each line of the text but what is actually occurring is that I am only getting the last line of text in the IntroTextArray.
I am sure it has to do with the fact that I am calling a requestAnimationFrame within a loop but I am not sure how to accomplish what I want to otherwise. Any advice would be appreciated.
Animating with requestAnimationFrame
RAF requestAnimationFrame
There are many ways that you can use RAF to animate. You can have many RAF functions that will all present the content to the display at the same time if they are all called before the next refresh. But this does incur extra overhead (especially if the rendering load per item is small) and extra head pain. If you have a slow render (for whatever reason, browsers can hang for a 1/60th second without warning) it can mess up the presentation order and you will only get part of the frame rendered.
The easiest way to manage any type of animation is to have a single animation loop that is called via RAF. From that function you call your animations. This is safe from presentation order problems, and can easily switch state. (eg after text fade you may want something else fading in)
Fix
Your code was not able to be saved, and sorry, was DOA, so I rewrote the whole thing. I have added some notes in the comments as to why and what for but if you have questions please do ask in the comments.
The example displays the fading text as two sets (to illustrate changing display state, a state is an abstract and reference to related elements or render (eg intro, middle, end))
The function has one main loop that handles the rendering calls and the timing.
Text is displayed by a function that updates and then displays. it will return true when text has faded in so main loop can set up the next render state.
// Removed jQuery and I dont see the need to use it
// The convention in JavaScript is NOT to capitalize the first character
// unless you function / object is created using new ObjectName
const ctx = canvas.getContext("2d"); // canvas is named in HTML via its id
canvas.width = innerWidth; // window is the global scope you do not need
canvas.height = innerHeight; //to prefix when accesing its properties
// helper
//const log = (...data) => console.log(data.join(","));
// use an array it is better suited to the data you are storing
const introText = [
["Testing one two.", 75],
["Testing..." , 50],
["One, one two.", 50],
["Is this thing on?", 50],
["",1], // to delay next state
["",1], // to delay next state
];
const introText1 = [
["Second stage, state 2", 20],
["The End" , 40],
[":)", 30],
["",10],
["Thanks for watching..",12],
["",1],
["",1],
["Dont forget to vote for any answers you find helpfull..",10],
["",1],
["",10],
["This intro was brought to you by",12],
["",10],
["requestAnimationFrame",12],
["HTML5",12],
["canvas",12],
["JavaScript (AKA) ECMAScript6",12],
["",10],
["Staring...",14],
["Stackoverflow.com",18],
];
const TEXT_FADE_TIME = 1000; // in ms
// create the array of display arrays
const displayLists = [
createDisplayList(8, introText), // converts simple text array to display list
createDisplayList(8, introText1),
];
var curretDisplayListIdx = 0;
requestAnimationFrame(mainLoop); // will start when all code has been parsed
var startTime;
function mainLoop(time){ // keep it simple use one animation frame per frame
if(startTime === undefined) { startTime = time }
const timeSinceStart = time - startTime;
ctx.clearRect(0,0, canvas.width, canvas.height);
if (textFadeIn(timeSinceStart, curretDisplayListIdx )) {
if (curretDisplayListIdx < displayLists.length - 1) {
curretDisplayListIdx += 1;
startTime = time;
}
}
requestAnimationFrame(mainLoop);
}
// creates a display list from text array. top is the start y pos
function createDisplayList(top, array) {
const result = [];
var y = top;
var fontSize;
for (const item of array) {
const fontSize = item[1];
result.push({
font : 'italic ' + fontSize + 'px Bradley Hand',
text : item[0],
x : canvas.width / 2,
y , fontSize,
startTime : null,
fadeTime : TEXT_FADE_TIME,
alpha : 0, // starting alpha
});
y += fontSize;
}
return result;
}
// displays a text display list from the displayLists array.
// time is time since starting to display the list
// displayListIdx is the index
// returns true when text has faded in
function textFadeIn(time, displayListIdx) {
// complete will be true when all have faded in
const complete = updateDisplayList(displayLists[displayListIdx], time);
renderDisplayList(displayLists[displayListIdx]);
return complete;
}
// separate logic from rendering
function updateDisplayList(array, time) {
var fadeNext = true; // used to indicate that the next item should fade in
// for each item
for (const text of array) {
if (fadeNext) { // has the previous items done its thing?
if (text.startTime === null) { text.startTime = time }
}
if(text.startTime !== null){ // if start time is set then fade it in
text.alpha = Math.min(1, (time - text.startTime) / text.fadeTime);
if (text.alpha < 1) { fadeNext = false } // if not complete flag fadeNext to stop next text fading in
}
}
// if last item is fully faded in return true
return array[array.length - 1].alpha === 1;
}
// seperate rendering from logic
function renderDisplayList(array) {
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillStyle = "black";
for (const text of array) {
ctx.font = text.font;
ctx.globalAlpha = text.alpha;
ctx.fillText(text.text,text.x,text.y);
}
}
canvas {
position: absolute;
top: 0px;
left: 0px;
}
<canvas id="canvas"></canvas>
I'm building an application in three.js, however I'm having real problems with performance. This part of the application is based upon the Voxel Painter example. In my version, the user clicks on a cell to begin placement, drags the cursor to where they wish to end placement, and clicks to end.
function onDocumentMouseMove(event) {
//set up mouse and raycaster
event.preventDefault();
mouse.set((event.clientX / window.innerWidth) * 2 - 1, -(event.clientY / window.innerHeight) * 2 + 1);
raycaster.setFromCamera(mouse, camera);
switch (buildMode) {
case buildModes.CORRIDOR:
scene.add(rollOverFloor);
var intersects = raycaster.intersectObjects(gridObject);
if (intersects.length > 0) {
var intersect = intersects[0];
if (beginPlace == true) {
//store the intersection position
var endPlace = new THREE.Vector3(0, 0, 0);
endPlace.copy(intersect.point).add(intersect.face.normal);
endPlace.divideScalar(step).floor().multiplyScalar(step).addScalar(step / step);
endPlace.set(endPlace.x, 0, endPlace.z);
corridorDrag(endPlace);
}
//if user hasn't begun to place the wall
else {
//show temporary wall on grid
rollOverFloor.position.copy(intersect.point).add(intersect.face.normal);
rollOverFloor.position.divideScalar(step).floor().multiplyScalar(step).addScalar(step / step);
rollOverFloor.position.set(rollOverFloor.position.x, 0, rollOverFloor.position.z);
}
}
break;
}
render();
}
The code above is called when the user moves the mouse (there are many buildmodes in the main application, but I have not included them here). This function simply gets a start and end point, the corridorDrag() function fills in the cells between the start and end points:
function corridorDrag(endPlace) {
deleteFromScene(stateType.CORRIDOR_DRAG);
var startPoint = startPlace;
var endPoint = endPlace;
var zIntersect = new THREE.Vector3(startPoint.x, 0, endPoint.z);
var xIntersect = new THREE.Vector3(endPoint.x, 0, startPoint.z);
var differenceZ = Math.abs(startPlace.z - zIntersect.z);
var differenceX = Math.abs(startPlace.x - xIntersect.x);
var mergedGeometry = new THREE.Geometry();
for (var i = 0; i <= (differenceZ / step); i++) {
for (var j = 0; j <= (differenceX / step); j++) {
var x = startPlace.x;
var y = startPlace.y;
var z = startPlace.z;
if (endPoint.x <= (startPlace.x )) {
if (endPoint.z <= (startPlace.z)) {
x = x - (step * j);
z = z - (step * i);
}
else if (endPoint.z >= (startPlace.z)) {
x = x - (step * j);
z = z + (step * i);
}
} else if (endPoint.x >= (startPlace.x)) {
if (endPoint.z <= (startPlace.z)) {
x = x + (step * j);
z = z - (step * i);
}
else if (endPoint.z >= (startPlace.z)) {
x = x + (step * j);
z = z + (step * i);
}
}
floorGeometry.translate(x, y, z);
mergedGeometry.merge(floorGeometry);
floorGeometry.translate(-x, -y, -z);
}
}
var voxel = new THREE.Mesh(mergedGeometry, tempMaterial);
voxel.state = stateType.CORRIDOR_DRAG;
scene.add(voxel);
tempObjects.push(voxel);
}
Firstly, the deleteFromScene() function removes all current highlighted cells from the scene (see below). The code then (I believe), should create a number of meshes, depending on the start and end points, and add them to the scene.
function deleteFromScene(state) {
tempObjects = [];
var i = scene.children.length;
while (i--) {
if (scene.children[i].state != undefined)
if (scene.children[i].state == state)
scene.children.splice(i, 1);
}
}
As I said, it is very, very slow. It also appears to be adding an obscene amount of vertices to the renderer, as seen in the WebGLRenderer stats window. I have no idea why it's adding so many vertices, but I'm assuming that's why it's rendering so slowly.
The application can be viewed here - the problem can be seen by clicking on one cell, dragging the cursor to the other end of the grid, and observing the time taken to fill in the cells.
Thank you in advance, this really is a last resort.
A few years ago Twitter put out an update. In this update they had just introduced infinite scrolling and on the day of its release the update was crashing users browsers. Twitter engineers did some investigating and found that the crashes were the result of the scroll event firing hundreds of times a second.
Mouse events can fire many MANY times a second and can cause your code to execute too often, which slows down the browser and (in many cases) crashes it. The solution for Twitter (and hopefully you) was simple: Poll your event.
Inside your mousemove event handler check that it has been some number of milliseconds since the last move event.
var lastMove = Date.now();
function onDocumentMouseMove(event) {
if (Date.now() - lastMove < 31) { // 32 frames a second
return;
} else {
lastMove = Date.now();
}
// your code here
}
I hope that helps!
Background: Over the last week I've been working on a game that is essentially multi-directional Tron, using Canvas and JavaScript. I opted not to clear the Canvas every frame so that my little line segments leave a trail. For collision detection, I use this function:
// 8 sensors for collision testing, positioned evenly around the brush point
var detectionRadius = this.width / 2 + 1; //points just outside the circumference
var counter = 0;
var pixelData;
for (var i = 0; i < 16; i += 2) {
//collisionPixels[] is an array of 8 (x, y) offsets, spaced evenly around the center of the circle
var x = this.x + collisionPixels[i] * detectionRadius;
var y = this.y + collisionPixels[i + 1] * detectionRadius;
pixelData = context.getImageData(x,y,1,1).data; //pixel data at each point
if (pixelData[3] != 0) {
counter++;
}
}
if (counter > 4) {
this.collision();
}
The purpose here is to get the alpha values of 8 pixels around the brushpoint's surface; alpha values of 0 are just on the background. If the number of colliding pixels, out of the total 8, is greater than 4 (this is including the trail behind the player) then I call the collision() method. This function actually works really well (and this IS inside a function, so these declarations are local).
The problem is that context.getImageData() skyrockets my memory usage, and after 3 or 4 games tanks the framerate. Cutting just that line out and assigning pixelData some other value makes everything run very smoothly, even while doing the other computations.
How do I fix this memory leak? And, if there's a less convoluted way to do collision detection of this type, what is it?
EDIT: at request, here is my loop:
function loop() {
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
}
requestAnimationFrame(loop);
}
EDIT 2: So I tried Patrick's UInt8ClampedArrays idea:
//8 sensors for collision testing, positioned evenly around the brush point
var detectionRadius = this.width / 2 + 1;
var counter = 0;
for (var i = 0; i < 16; i += 2) {
var x = this.x + collisionPixels[i] * detectionRadius;
var y = this.y + collisionPixels[i + 1] * detectionRadius;
//translate into UInt8ClampedArray for data
var index = (y * canvas.width + x) * 4 + 3; //+3 so we're at the alpha index
if (canvasArray[index] != 0) {
counter++;
}
}
And, at the top of my loop I added a new global variable, updated once per frame:
var canvasArray = context.getImageData(0,0,canvas.width,canvas.height).data;
Hope I did that right. It works, but the memory and framerate still get worse each round you play. Going to upload some heap snapshots.
EDIT 3:
Snapshot 1: https://drive.google.com/open?id=0B-8p3yyYzRjeY2pEa2Z5QlgxRUk&authuser=0
Snapshot 2: https://drive.google.com/open?id=0B-8p3yyYzRjeV2pJb1NyazY3OWc&authuser=0
Snapshot 1 is after the first game, 2 is after the second.
EDIT 4: Tried capping the framerate:
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
//lastUpdate = now;
if (delta > interval) {
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
}
}
}
Where
interval = 1000 / fps;
It delays the eventual performance hit, but memory is still climbing with this option.
EDIT 5: While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
if (now - lastCollisionUpdate > collisionInterval) {
canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
if (players[i].detectCollisions()) {
players[i].collision();
}
}
}
lastCollisionUpdate = now;
}
canvasData = null;
}
}
Thanks for the answers.. a lot of your ideas found their way into the final(?) product, and I appreciate that. Closing this thread.
Is there some point at which you could call context.getImageData(0, 0, context.canvas.width, context.canvas.height).data so that you can use that single UInt8ClampedArray instead of however many you're using? Also when you're done with the image data (the ImageData that is, not the TypedArray inside it), you could try calling delete on it, though I'm not certain if that will deallocate the memory.
While I'm sure there must be a better way, I found a solution that works reasonably well. Capping the framerate around 30 actually worked in terms of long-term performance, but I hated the way the game looked at 30 FPS.. so I built a loop that had an uncapped framerate for all updating and rendering EXCEPT for collision handling, which I updated at 30 FPS.
//separate update cycle for collision detection
var collisionFPS = 30;
var lastCollisionUpdate;
var collisionInterval = 1000 / collisionFPS;
var canvasData;
function loop() {
requestAnimationFrame(loop);
now = Date.now();
delta = now - lastUpdate;
lastUpdate = now;
if (!paused) {
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
players[i].update(delta);
players[i].render();
}
}
if (now - lastCollisionUpdate > collisionInterval) {
canvasData = context.getImageData(0, 0, context.canvas.width, context.canvas.height).data;
for (var i = 0; i < numPlayers; i++) {
if (players[i].alive) {
if (players[i].detectCollisions()) {
players[i].collision();
}
}
}
lastCollisionUpdate = now;
}
canvasData = null;
}
}
Might not be the best solution, but it's consistent.
It seems like requestAnimationFrame is the de facto way to animate things now. It worked pretty well for me for the most part, but right now I'm trying to do some canvas animations and I was wondering: Is there any way to make sure it runs at a certain fps? I understand that the purpose of rAF is for consistently smooth animations, and I might run the risk of making my animation choppy, but right now it seems to run at drastically different speeds pretty arbitrarily, and I'm wondering if there's a way to combat that somehow.
I'd use setInterval but I want the optimizations that rAF offers (especially automatically stopping when the tab is in focus).
In case someone wants to look at my code, it's pretty much:
animateFlash: function() {
ctx_fg.clearRect(0,0,canvasWidth,canvasHeight);
ctx_fg.fillStyle = 'rgba(177,39,116,1)';
ctx_fg.strokeStyle = 'none';
ctx_fg.beginPath();
for(var i in nodes) {
nodes[i].drawFlash();
}
ctx_fg.fill();
ctx_fg.closePath();
var instance = this;
var rafID = requestAnimationFrame(function(){
instance.animateFlash();
})
var unfinishedNodes = nodes.filter(function(elem){
return elem.timer < timerMax;
});
if(unfinishedNodes.length === 0) {
console.log("done");
cancelAnimationFrame(rafID);
instance.animate();
}
}
Where Node.drawFlash() is just some code that determines radius based off a counter variable and then draws a circle.
How to throttle requestAnimationFrame to a specific frame rate
Demo throttling at 5 FPS: http://jsfiddle.net/m1erickson/CtsY3/
This method works by testing the elapsed time since executing the last frame loop.
Your drawing code executes only when your specified FPS interval has elapsed.
The first part of the code sets some variables used to calculate elapsed time.
var stop = false;
var frameCount = 0;
var $results = $("#results");
var fps, fpsInterval, startTime, now, then, elapsed;
// initialize the timer variables and start the animation
function startAnimating(fps) {
fpsInterval = 1000 / fps;
then = Date.now();
startTime = then;
animate();
}
And this code is the actual requestAnimationFrame loop which draws at your specified FPS.
// the animation loop calculates time elapsed since the last loop
// and only draws if your specified fps interval is achieved
function animate() {
// request another frame
requestAnimationFrame(animate);
// calc elapsed time since last loop
now = Date.now();
elapsed = now - then;
// if enough time has elapsed, draw the next frame
if (elapsed > fpsInterval) {
// Get ready for next frame by setting then=now, but also adjust for your
// specified fpsInterval not being a multiple of RAF's interval (16.7ms)
then = now - (elapsed % fpsInterval);
// Put your drawing code here
}
}
I suggest wrapping your call to requestAnimationFrame in a setTimeout:
const fps = 25;
function animate() {
// perform some animation task here
setTimeout(() => {
requestAnimationFrame(animate);
}, 1000 / fps);
}
animate();
You need to call requestAnimationFrame from within setTimeout, rather than the other way around, because requestAnimationFrame schedules your function to run right before the next repaint, and if you delay your update further using setTimeout you will have missed that time window. However, doing the reverse is sound, since you’re simply waiting a period of time before making the request.
Update 2016/6
The problem throttling the frame rate is that the screen has a constant update rate, typically 60 FPS.
If we want 24 FPS we will never get the true 24 fps on the screen, we can time it as such but not show it as the monitor can only show synced frames at 15 fps, 30 fps or 60 fps (some monitors also 120 fps).
However, for timing purposes we can calculate and update when possible.
You can build all the logic for controlling the frame-rate by encapsulating calculations and callbacks into an object:
function FpsCtrl(fps, callback) {
var delay = 1000 / fps, // calc. time per frame
time = null, // start time
frame = -1, // frame count
tref; // rAF time reference
function loop(timestamp) {
if (time === null) time = timestamp; // init start time
var seg = Math.floor((timestamp - time) / delay); // calc frame no.
if (seg > frame) { // moved to next frame?
frame = seg; // update
callback({ // callback function
time: timestamp,
frame: frame
})
}
tref = requestAnimationFrame(loop)
}
}
Then add some controller and configuration code:
// play status
this.isPlaying = false;
// set frame-rate
this.frameRate = function(newfps) {
if (!arguments.length) return fps;
fps = newfps;
delay = 1000 / fps;
frame = -1;
time = null;
};
// enable starting/pausing of the object
this.start = function() {
if (!this.isPlaying) {
this.isPlaying = true;
tref = requestAnimationFrame(loop);
}
};
this.pause = function() {
if (this.isPlaying) {
cancelAnimationFrame(tref);
this.isPlaying = false;
time = null;
frame = -1;
}
};
Usage
It becomes very simple - now, all that we have to do is to create an instance by setting callback function and desired frame rate just like this:
var fc = new FpsCtrl(24, function(e) {
// render each frame here
});
Then start (which could be the default behavior if desired):
fc.start();
That's it, all the logic is handled internally.
Demo
var ctx = c.getContext("2d"), pTime = 0, mTime = 0, x = 0;
ctx.font = "20px sans-serif";
// update canvas with some information and animation
var fps = new FpsCtrl(12, function(e) {
ctx.clearRect(0, 0, c.width, c.height);
ctx.fillText("FPS: " + fps.frameRate() +
" Frame: " + e.frame +
" Time: " + (e.time - pTime).toFixed(1), 4, 30);
pTime = e.time;
var x = (pTime - mTime) * 0.1;
if (x > c.width) mTime = pTime;
ctx.fillRect(x, 50, 10, 10)
})
// start the loop
fps.start();
// UI
bState.onclick = function() {
fps.isPlaying ? fps.pause() : fps.start();
};
sFPS.onchange = function() {
fps.frameRate(+this.value)
};
function FpsCtrl(fps, callback) {
var delay = 1000 / fps,
time = null,
frame = -1,
tref;
function loop(timestamp) {
if (time === null) time = timestamp;
var seg = Math.floor((timestamp - time) / delay);
if (seg > frame) {
frame = seg;
callback({
time: timestamp,
frame: frame
})
}
tref = requestAnimationFrame(loop)
}
this.isPlaying = false;
this.frameRate = function(newfps) {
if (!arguments.length) return fps;
fps = newfps;
delay = 1000 / fps;
frame = -1;
time = null;
};
this.start = function() {
if (!this.isPlaying) {
this.isPlaying = true;
tref = requestAnimationFrame(loop);
}
};
this.pause = function() {
if (this.isPlaying) {
cancelAnimationFrame(tref);
this.isPlaying = false;
time = null;
frame = -1;
}
};
}
body {font:16px sans-serif}
<label>Framerate: <select id=sFPS>
<option>12</option>
<option>15</option>
<option>24</option>
<option>25</option>
<option>29.97</option>
<option>30</option>
<option>60</option>
</select></label><br>
<canvas id=c height=60></canvas><br>
<button id=bState>Start/Stop</button>
Old answer
The main purpose of requestAnimationFrame is to sync updates to the monitor's refresh rate. This will require you to animate at the FPS of the monitor or a factor of it (ie. 60, 30, 15 FPS for a typical refresh rate # 60 Hz).
If you want a more arbitrary FPS then there is no point using rAF as the frame rate will never match the monitor's update frequency anyways (just a frame here and there) which simply cannot give you a smooth animation (as with all frame re-timings) and you can might as well use setTimeout or setInterval instead.
This is also a well known problem in the professional video industry when you want to playback a video at a different FPS then the device showing it refresh at. Many techniques has been used such as frame blending and complex re-timing re-building intermediate frames based on motion vectors, but with canvas these techniques are not available and the result will always be jerky video.
var FPS = 24; /// "silver screen"
var isPlaying = true;
function loop() {
if (isPlaying) setTimeout(loop, 1000 / FPS);
... code for frame here
}
The reason why we place setTimeout first (and why some place rAF first when a poly-fill is used) is that this will be more accurate as the setTimeout will queue an event immediately when the loop starts so that no matter how much time the remaining code will use (provided it doesn't exceed the timeout interval) the next call will be at the interval it represents (for pure rAF this is not essential as rAF will try to jump onto the next frame in any case).
Also worth to note that placing it first will also risk calls stacking up as with setInterval. setInterval may be slightly more accurate for this use.
And you can use setInterval instead outside the loop to do the same.
var FPS = 29.97; /// NTSC
var rememberMe = setInterval(loop, 1000 / FPS);
function loop() {
... code for frame here
}
And to stop the loop:
clearInterval(rememberMe);
In order to reduce frame rate when the tab gets blurred you can add a factor like this:
var isFocus = 1;
var FPS = 25;
function loop() {
setTimeout(loop, 1000 / (isFocus * FPS)); /// note the change here
... code for frame here
}
window.onblur = function() {
isFocus = 0.5; /// reduce FPS to half
}
window.onfocus = function() {
isFocus = 1; /// full FPS
}
This way you can reduce the FPS to 1/4 etc.
These are all good ideas in theory, until you go deep. The problem is you can't throttle an RAF without de-synchronizing it, defeating it's very purpose for existing. So you let it run at full-speed, and update your data in a separate loop, or even a separate thread!
Yes, I said it. You can do multi-threaded JavaScript in the browser!
There are two methods I know that work extremely well without jank, using far less juice and creating less heat. Accurate human-scale timing and machine efficiency are the net result.
Apologies if this is a little wordy, but here goes...
Method 1: Update data via setInterval, and graphics via RAF.
Use a separate setInterval for updating translation and rotation values, physics, collisions, etc. Keep those values in an object for each animated element. Assign the transform string to a variable in the object each setInterval 'frame'. Keep these objects in an array. Set your interval to your desired fps in ms: ms=(1000/fps). This keeps a steady clock that allows the same fps on any device, regardless of RAF speed. Do not assign the transforms to the elements here!
In a requestAnimationFrame loop, iterate through your array with an old-school for loop-- do not use the newer forms here, they are slow!
for(var i=0; i<sprite.length-1; i++){ rafUpdate(sprite[i]); }
In your rafUpdate function, get the transform string from your js object in the array, and its elements id. You should already have your 'sprite' elements attached to a variable or easily accessible through other means so you don't lose time 'get'-ing them in the RAF. Keeping them in an object named after their html id's works pretty good. Set that part up before it even goes into your SI or RAF.
Use the RAF to update your transforms only, use only 3D transforms (even for 2d), and set css "will-change: transform;" on elements that will change. This keeps your transforms synced to the native refresh rate as much as possible, kicks in the GPU, and tells the browser where to concentrate most.
So you should have something like this pseudocode...
// refs to elements to be transformed, kept in an array
var element = [
mario: document.getElementById('mario'),
luigi: document.getElementById('luigi')
//...etc.
]
var sprite = [ // read/write this with SI. read-only from RAF
mario: { id: mario ....physics data, id, and updated transform string (from SI) here },
luigi: { id: luigi .....same }
//...and so forth
] // also kept in an array (for efficient iteration)
//update one sprite js object
//data manipulation, CPU tasks for each sprite object
//(physics, collisions, and transform-string updates here.)
//pass the object (by reference).
var SIupdate = function(object){
// get pos/rot and update with movement
object.pos.x += object.mov.pos.x; // example, motion along x axis
// and so on for y and z movement
// and xyz rotational motion, scripted scaling etc
// build transform string ie
object.transform =
'translate3d('+
object.pos.x+','+
object.pos.y+','+
object.pos.z+
') '+
// assign rotations, order depends on purpose and set-up.
'rotationZ('+object.rot.z+') '+
'rotationY('+object.rot.y+') '+
'rotationX('+object.rot.x+') '+
'scale3d('.... if desired
; //...etc. include
}
var fps = 30; //desired controlled frame-rate
// CPU TASKS - SI psuedo-frame data manipulation
setInterval(function(){
// update each objects data
for(var i=0; i<sprite.length-1; i++){ SIupdate(sprite[i]); }
},1000/fps); // note ms = 1000/fps
// GPU TASKS - RAF callback, real frame graphics updates only
var rAf = function(){
// update each objects graphics
for(var i=0; i<sprite.length-1; i++){ rAF.update(sprite[i]) }
window.requestAnimationFrame(rAF); // loop
}
// assign new transform to sprite's element, only if it's transform has changed.
rAF.update = function(object){
if(object.old_transform !== object.transform){
element[object.id].style.transform = transform;
object.old_transform = object.transform;
}
}
window.requestAnimationFrame(rAF); // begin RAF
This keeps your updates to the data objects and transform strings synced to desired 'frame' rate in the SI, and the actual transform assignments in the RAF synced to GPU refresh rate. So the actual graphics updates are only in the RAF, but the changes to the data, and building the transform string are in the SI, thus no jankies but 'time' flows at desired frame-rate.
Flow:
[setup js sprite objects and html element object references]
[setup RAF and SI single-object update functions]
[start SI at percieved/ideal frame-rate]
[iterate through js objects, update data transform string for each]
[loop back to SI]
[start RAF loop]
[iterate through js objects, read object's transform string and assign it to it's html element]
[loop back to RAF]
Method 2. Put the SI in a web-worker. This one is FAAAST and smooth!
Same as method 1, but put the SI in web-worker. It'll run on a totally separate thread then, leaving the page to deal only with the RAF and UI. Pass the sprite array back and forth as a 'transferable object'. This is buko fast. It does not take time to clone or serialize, but it's not like passing by reference in that the reference from the other side is destroyed, so you will need to have both sides pass to the other side, and only update them when present, sort of like passing a note back and forth with your girlfriend in high-school.
Only one can read and write at a time. This is fine so long as they check if it's not undefined to avoid an error. The RAF is FAST and will kick it back immediately, then go through a bunch of GPU frames just checking if it's been sent back yet. The SI in the web-worker will have the sprite array most of the time, and will update positional, movement and physics data, as well as creating the new transform string, then pass it back to the RAF in the page.
This is the fastest way I know to animate elements via script. The two functions will be running as two separate programs, on two separate threads, taking advantage of multi-core CPU's in a way that a single js script does not. Multi-threaded javascript animation.
And it will do so smoothly without jank, but at the actual specified frame-rate, with very little divergence.
Result:
Either of these two methods will ensure your script will run at the same speed on any PC, phone, tablet, etc (within the capabilities of the device and the browser, of course).
How to easily throttle to a specific FPS:
// timestamps are ms passed since document creation.
// lastTimestamp can be initialized to 0, if main loop is executed immediately
var lastTimestamp = 0,
maxFPS = 30,
timestep = 1000 / maxFPS; // ms for each frame
function main(timestamp) {
window.requestAnimationFrame(main);
// skip if timestep ms hasn't passed since last frame
if (timestamp - lastTimestamp < timestep) return;
lastTimestamp = timestamp;
// draw frame here
}
window.requestAnimationFrame(main);
Source: A Detailed Explanation of JavaScript Game Loops and Timing by Isaac Sukin
The simplest way
note: It might behave differently on different screens with different frame rate.
const FPS = 30;
let lastTimestamp = 0;
function update(timestamp) {
requestAnimationFrame(update);
if (timestamp - lastTimestamp < 1000 / FPS) return;
/* <<< PUT YOUR CODE HERE >>> */
lastTimestamp = timestamp;
}
update();
var time = 0;
var time_framerate = 1000; //in milliseconds
function animate(timestamp) {
if(timestamp > time + time_framerate) {
time = timestamp;
//your code
}
window.requestAnimationFrame(animate);
}
A simple solution to this problem is to return from the render loop if the frame is not required to render:
const FPS = 60;
let prevTick = 0;
function render()
{
requestAnimationFrame(render);
// clamp to fixed framerate
let now = Math.round(FPS * Date.now() / 1000);
if (now == prevTick) return;
prevTick = now;
// otherwise, do your stuff ...
}
It's important to know that requestAnimationFrame depends on the users monitor refresh rate (vsync). So, relying on requestAnimationFrame for game speed for example will make it unplayable on 200Hz monitors if you're not using a separate timer mechanism in your simulation.
Skipping requestAnimationFrame cause not smooth(desired) animation at custom fps.
// Input/output DOM elements
var $results = $("#results");
var $fps = $("#fps");
var $period = $("#period");
// Array of FPS samples for graphing
// Animation state/parameters
var fpsInterval, lastDrawTime, frameCount_timed, frameCount, lastSampleTime,
currentFps=0, currentFps_timed=0;
var intervalID, requestID;
// Setup canvas being animated
var canvas = document.getElementById("c");
var canvas_timed = document.getElementById("c2");
canvas_timed.width = canvas.width = 300;
canvas_timed.height = canvas.height = 300;
var ctx = canvas.getContext("2d");
var ctx2 = canvas_timed.getContext("2d");
// Setup input event handlers
$fps.on('click change keyup', function() {
if (this.value > 0) {
fpsInterval = 1000 / +this.value;
}
});
$period.on('click change keyup', function() {
if (this.value > 0) {
if (intervalID) {
clearInterval(intervalID);
}
intervalID = setInterval(sampleFps, +this.value);
}
});
function startAnimating(fps, sampleFreq) {
ctx.fillStyle = ctx2.fillStyle = "#000";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx2.fillRect(0, 0, canvas.width, canvas.height);
ctx2.font = ctx.font = "32px sans";
fpsInterval = 1000 / fps;
lastDrawTime = performance.now();
lastSampleTime = lastDrawTime;
frameCount = 0;
frameCount_timed = 0;
animate();
intervalID = setInterval(sampleFps, sampleFreq);
animate_timed()
}
function sampleFps() {
// sample FPS
var now = performance.now();
if (frameCount > 0) {
currentFps =
(frameCount / (now - lastSampleTime) * 1000).toFixed(2);
currentFps_timed =
(frameCount_timed / (now - lastSampleTime) * 1000).toFixed(2);
$results.text(currentFps + " | " + currentFps_timed);
frameCount = 0;
frameCount_timed = 0;
}
lastSampleTime = now;
}
function drawNextFrame(now, canvas, ctx, fpsCount) {
// Just draw an oscillating seconds-hand
var length = Math.min(canvas.width, canvas.height) / 2.1;
var step = 15000;
var theta = (now % step) / step * 2 * Math.PI;
var xCenter = canvas.width / 2;
var yCenter = canvas.height / 2;
var x = xCenter + length * Math.cos(theta);
var y = yCenter + length * Math.sin(theta);
ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
ctx.lineTo(x, y);
ctx.fillStyle = ctx.strokeStyle = 'white';
ctx.stroke();
var theta2 = theta + 3.14/6;
ctx.beginPath();
ctx.moveTo(xCenter, yCenter);
ctx.lineTo(x, y);
ctx.arc(xCenter, yCenter, length*2, theta, theta2);
ctx.fillStyle = "rgba(0,0,0,.1)"
ctx.fill();
ctx.fillStyle = "#000";
ctx.fillRect(0,0,100,30);
ctx.fillStyle = "#080";
ctx.fillText(fpsCount,10,30);
}
// redraw second canvas each fpsInterval (1000/fps)
function animate_timed() {
frameCount_timed++;
drawNextFrame( performance.now(), canvas_timed, ctx2, currentFps_timed);
setTimeout(animate_timed, fpsInterval);
}
function animate(now) {
// request another frame
requestAnimationFrame(animate);
// calc elapsed time since last loop
var elapsed = now - lastDrawTime;
// if enough time has elapsed, draw the next frame
if (elapsed > fpsInterval) {
// Get ready for next frame by setting lastDrawTime=now, but...
// Also, adjust for fpsInterval not being multiple of 16.67
lastDrawTime = now - (elapsed % fpsInterval);
frameCount++;
drawNextFrame(now, canvas, ctx, currentFps);
}
}
startAnimating(+$fps.val(), +$period.val());
input{
width:100px;
}
#tvs{
color:red;
padding:0px 25px;
}
H3{
font-weight:400;
}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<h3>requestAnimationFrame skipping <span id="tvs">vs.</span> setTimeout() redraw</h3>
<div>
<input id="fps" type="number" value="33"/> FPS:
<span id="results"></span>
</div>
<div>
<input id="period" type="number" value="1000"/> Sample period (fps, ms)
</div>
<canvas id="c"></canvas><canvas id="c2"></canvas>
Original code by #tavnab.
For throttling FPS to any value, pls see jdmayfields answer.
However, for a very quick and easy solution to halve your frame rate, you can simply do your computations only every 2nd frame by:
requestAnimationFrame(render);
function render() {
// ... computations ...
requestAnimationFrame(skipFrame);
}
function skipFrame() { requestAnimationFrame(render); }
Similarly you could always call render but use a variable to control whether you do computations this time or not, allowing you to also cut FPS to a third or fourth (in my case, for a schematic webgl-animation 20fps is still enough while considerably lowering computational load on the clients)
I always do it this very simple way without messing with timestamps:
let fps, eachNthFrame, frameCount;
fps = 30;
//This variable specifies how many frames should be skipped.
//If it is 1 then no frames are skipped. If it is 2, one frame
//is skipped so "eachSecondFrame" is renderd.
eachNthFrame = Math.round((1000 / fps) / 16.66);
//This variable is the number of the current frame. It is set to eachNthFrame so that the
//first frame will be renderd.
frameCount = eachNthFrame;
requestAnimationFrame(frame);
//I think the rest is self-explanatory
function frame() {
if (frameCount === eachNthFrame) {
frameCount = 0;
animate();
}
frameCount++;
requestAnimationFrame(frame);
}
Here is an idea to reach desired fps:
detect browser's animationFrameRate (typically 60fps)
build a bitSet, according to animationFrameRate and your disiredFrameRate (say 24fps)
lookup bitSet and conditionally "continue" the animation frame loop
It uses requestAnimationFrame so the actual frame rate won't be greater than animationFrameRate. you may adjust disiredFrameRate according to animationFrameRate.
I wrote a mini lib, and a canvas animation demo.
function filterNums(nums, jitter = 0.2, downJitter = 1 - 1 / (1 + jitter)) {
let len = nums.length;
let mid = Math.floor(len % 2 === 0 ? len / 2 : (len - 1) / 2), low = mid, high = mid;
let lower = true, higher = true;
let sum = nums[mid], count = 1;
for (let i = 1, j, num; i <= mid; i += 1) {
if (higher) {
j = mid + i;
if (j === len)
break;
num = nums[j];
if (num < (sum / count) * (1 + jitter)) {
sum += num;
count += 1;
high = j;
} else {
higher = false;
}
}
if (lower) {
j = mid - i;
num = nums[j];
if (num > (sum / count) * (1 - downJitter)) {
sum += num;
count += 1;
low = j;
} else {
lower = false;
}
}
}
return nums.slice(low, high + 1);
}
function snapToOrRound(n, values, distance = 3) {
for (let i = 0, v; i < values.length; i += 1) {
v = values[i];
if (n >= v - distance && n <= v + distance) {
return v;
}
}
return Math.round(n);
}
function detectAnimationFrameRate(numIntervals = 6) {
if (typeof numIntervals !== 'number' || !isFinite(numIntervals) || numIntervals < 2) {
throw new RangeError('Argument numIntervals should be a number not less than 2');
}
return new Promise((resolve) => {
let num = Math.floor(numIntervals);
let numFrames = num + 1;
let last;
let intervals = [];
let i = 0;
let tick = () => {
let now = performance.now();
i += 1;
if (i < numFrames) {
requestAnimationFrame(tick);
}
if (i === 1) {
last = now;
} else {
intervals.push(now - last);
last = now;
if (i === numFrames) {
let compareFn = (a, b) => a < b ? -1 : a > b ? 1 : 0;
let sortedIntervals = intervals.slice().sort(compareFn);
let selectedIntervals = filterNums(sortedIntervals, 0.2, 0.1);
let selectedDuration = selectedIntervals.reduce((s, n) => s + n, 0);
let seletedFrameRate = 1000 / (selectedDuration / selectedIntervals.length);
let finalFrameRate = snapToOrRound(seletedFrameRate, [60, 120, 90, 30], 5);
resolve(finalFrameRate);
}
}
};
requestAnimationFrame(() => {
requestAnimationFrame(tick);
});
});
}
function buildFrameBitSet(animationFrameRate, desiredFrameRate){
let bitSet = new Uint8Array(animationFrameRate);
let ratio = desiredFrameRate / animationFrameRate;
if(ratio >= 1)
return bitSet.fill(1);
for(let i = 0, prev = -1, curr; i < animationFrameRate; i += 1, prev = curr){
curr = Math.floor(i * ratio);
bitSet[i] = (curr !== prev) ? 1 : 0;
}
return bitSet;
}
let $ = (s, c = document) => c.querySelector(s);
let $$ = (s, c = document) => Array.prototype.slice.call(c.querySelectorAll(s));
async function main(){
let canvas = $('#digitalClock');
let context2d = canvas.getContext('2d');
await new Promise((resolve) => {
if(window.requestIdleCallback){
requestIdleCallback(resolve, {timeout:3000});
}else{
setTimeout(resolve, 0, {didTimeout: false});
}
});
let animationFrameRate = await detectAnimationFrameRate(10); // 1. detect animation frame rate
let desiredFrameRate = 24;
let frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate); // 2. build a bit set
let handle;
let i = 0;
let count = 0, then, actualFrameRate = $('#actualFrameRate'); // debug-only
let draw = () => {
if(++i >= animationFrameRate){ // shoud use === if frameBits don't change dynamically
i = 0;
/* debug-only */
let now = performance.now();
let deltaT = now - then;
let fps = 1000 / (deltaT / count);
actualFrameRate.textContent = fps;
then = now;
count = 0;
}
if(frameBits[i] === 0){ // 3. lookup the bit set
handle = requestAnimationFrame(draw);
return;
}
count += 1; // debug-only
let d = new Date();
let text = d.getHours().toString().padStart(2, '0') + ':' +
d.getMinutes().toString().padStart(2, '0') + ':' +
d.getSeconds().toString().padStart(2, '0') + '.' +
(d.getMilliseconds() / 10).toFixed(0).padStart(2, '0');
context2d.fillStyle = '#000000';
context2d.fillRect(0, 0, canvas.width, canvas.height);
context2d.font = '36px monospace';
context2d.fillStyle = '#ffffff';
context2d.fillText(text, 0, 36);
handle = requestAnimationFrame(draw);
};
handle = requestAnimationFrame(() => {
then = performance.now();
handle = requestAnimationFrame(draw);
});
/* debug-only */
$('#animationFrameRate').textContent = animationFrameRate;
let frameRateInput = $('#frameRateInput');
let frameRateOutput = $('#frameRateOutput');
frameRateInput.addEventListener('input', (e) => {
frameRateOutput.value = e.target.value;
});
frameRateInput.max = animationFrameRate;
frameRateOutput.value = frameRateOutput.value = desiredFrameRate;
frameRateInput.addEventListener('change', (e) => {
desiredFrameRate = +e.target.value;
frameBits = buildFrameBitSet(animationFrameRate, desiredFrameRate);
});
}
document.addEventListener('DOMContentLoaded', main);
<div>
Animation Frame Rate: <span id="animationFrameRate">--</span>
</div>
<div>
Desired Frame Rate: <input id="frameRateInput" type="range" min="1" max="60" step="1" list="frameRates" />
<output id="frameRateOutput"></output>
<datalist id="frameRates">
<option>15</option>
<option>24</option>
<option>30</option>
<option>48</option>
<option>60</option>
</datalist>
</div>
<div>
Actual Frame Rate: <span id="actualFrameRate">--</span>
</div>
<canvas id="digitalClock" width="240" height="48"></canvas>
Simplified explanation of earlier answer. At least if you want real-time, accurate throttling without the janks, or dropping frames like bombs. GPU and CPU friendly.
setInterval and setTimeout are both CPU-oriented, not GPU.
requestAnimationFrame is purely GPU-oriented.
Run them separately. It's simple and not janky. In your setInterval, update your math and create a little CSS script in a string. With your RAF loop, only use that script to update the new coordinates of your elements. Don't do anything else in the RAF loop.
The RAF is tied inherently to the GPU. Whenever the script does not change (i.e. because the SI is running a gazillion times slower), Chromium-based browsers know they do not need to do anything, because there are no changes. So the on-the-fly script created each "frame", say 60 times per second, is still the same for say 1000 RAF GPU frames, but it knows nothing has changed, and the net result is it wastes no energy on this. If you check in DevTools, you will see your GPU frame-rate registers at the rate delineated by the setInterval.
Truely, it is just that simple. Separate them, and they will cooperate.
No jankies.
I tried multiple solutions provided on this question. Even though the solutions work as expected, they result in not so professional output.
Based on my personal experience, I would highly recommend not to control FPS on the browser side, especially using requestAnimationFrame. Because, when you do that, it'll make the frame rendering experience very choppy, users will clearly see the frames jumping and finally, it won't look real or professional at all.
So, my advice would be to control the FPS from the server side at the time of sending itself and simply render the frames as soon as you receive them on the browser side.
Note: if you still want to control on the client-side, try avoiding
usage of setTimeout or Date object in your logic of controlling fps.
Because, when the FPS is high, these will introduce their own delay in
terms of event loops or object creations.
Here's a good explanation I found: CreativeJS.com, to wrap a setTimeou) call inside the function passed to requestAnimationFrame. My concern with a "plain" requestionAnimationFrame would be, "what if I only want it to animate three times a second?" Even with requestAnimationFrame (as opposed to setTimeout) is that it still wastes (some) amount of "energy" (meaning that the Browser code is doing something, and possibly slowing the system down) 60 or 120 or however many times a second, as opposed to only two or three times a second (as you might want).
Most of the time I run my browsers with JavaScript intentially off for just this reason. But, I'm using Yosemite 10.10.3, and I think there's some kind of timer problem with it - at least on my old system (relatively old - meaning 2011).