How to record and display FPS on WebGL? [duplicate] - javascript

This question already has answers here:
Recording FPS in webGL
(5 answers)
Closed 5 years ago.
I am trying to display the frames per second on my html canvas. I dont mind where its placed on the canvas for now as I can tweak it at later period. Here what I have so far;
var updateAnimation = function () {
requestAnimFrame(updateAnimation);
var anim = global.animation;
var e = global.events;
//Set current time
anim.animationCurrentTime = Date.now();
//Set start time
if (anim.animationStartTime === undefined) {
anim.animationStartTime = anim.animationCurrentTime;
}
//Clear the animationStage
webgl.clear(webgl.COLOR_BUFFER_BIT | webgl.DEPTH_BUFFER_BIT);
//Draw scene
drawScene();
//Set previous time as current time to use in next frame
anim.animationLastTime = anim.animationCurrentTime;
}
global.document.animationStage = document.getElementById("animation-scene");
webgl = setupScene(global.document.animationStage);
setupShaders();
setupAllBuffers();
setupEvents();
setupLight();
setupTextures();
initScene();
}
<header>
<h1 style="text-align: center">Applied Computer Graphics and Vision</h1>
<p>Instructions<span>
<br />
<br />
Rotation - Click and drag in the direction of rotation <br />
Increase/Decrease Orbit Radius - Up and Down Keys <br />
Increase/Decrease Orbit Speed - Left and Right Keys <br />
Translation Of X - Shift plus mouse drag <br />
Translation Of Y - Alt plus mouse drag <br />
Translation Of Z - Mouse scroll
</span></p>
</header>
<canvas style="float:left" ; id="animation-scene"></canvas>
<canvas id="myCanvas" width="1400" height="800"></canvas>
<script>
/* Sets */
var area = document.getElementById('animation-scene');
area.setAttribute('height', window.innerHeight);
area.setAttribute('width', window.innerWidth);
</script>
</body>
</html>
Any help or advice would be great. I know the basic idea of having to count the number of frames rendered and once one second has passed store that in the fps variable but not sure on how to implement this through my update animation function.
I also have methods that sets the current/start time for the scene within the update animation function.

Displaying FPSs is pretty simple and has really nothing to do with WebGL other than it's common to want to know. Here's a small FPS display
const fpsElem = document.querySelector("#fps");
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then; // compute time since last frame
then = now; // remember time for next frame
const fps = 1 / deltaTime; // compute frames per second
fpsElem.textContent = fps.toFixed(1); // update fps display
requestAnimationFrame(render);
}
requestAnimationFrame(render);
<div>fps: <span id="fps"></span></div>
You should probably not use Date.now() for computing FPS as Date.now() only returns milliseconds. requestAnimationFrame already gets passed the time in microseconds since the page loaded.
Also you don't really "place it on the canvas". Just use another HTML element separate from the canvas. If you want them to overlap then use CSS to make them overlap
const gl = document.querySelector("#c").getContext("webgl");
const fpsElem = document.querySelector("#fps");
let then = 0;
function render(now) {
now *= 0.001; // convert to seconds
const deltaTime = now - then; // compute time since last frame
then = now; // remember time for next frame
const fps = 1 / deltaTime; // compute frames per second
fpsElem.textContent = fps.toFixed(1); // update fps display
drawScene(now);
requestAnimationFrame(render);
}
requestAnimationFrame(render);
function drawScene(time) {
gl.disable(gl.SCISSOR_TEST);
gl.clearColor(1, 0, 0, 1);
gl.clear(gl.COLOR_BUFFER_BIT);
const halfWidth = gl.canvas.width / 2;
const halfHeight = gl.canvas.height / 2
const x = halfWidth - f(time) * halfWidth;
const y = halfHeight - f(time * 1.17) * halfHeight;
const w = (halfWidth - x) * 2;
const h = (halfHeight - y ) * 2;
gl.scissor(x, y, w, h);
gl.enable(gl.SCISSOR_TEST);
gl.clearColor(f(time * 1.1), f(time * 1.3), f(time * 1.2), 1);
gl.clear(gl.COLOR_BUFFER_BIT);
}
function f(v) {
return Math.sin(v) * .5 + .5;
}
#container {
position: relative; /* needed so child elements use this as their base */
}
#hud {
position: absolute;
left: 5px;
top: 5px;
background: rgba(0, 0, 0, 0.5); /* 50% opaque black */
color: white;
padding: .5em;
font-family: monospace;
border-radius: .5em;
}
<div id="container">
<canvas id="c"></canvas>
<div id="hud">fps: <span id="fps"></span></div>
</div>

Related

How to translate Mouse X,Y Coordinates to move HTML element inside a container where container is rotated to some angle

I am developing a card game in HTML that shows real-time perspectives and card movements to all players.
All players are connected via socket.io.
BACKGROUND:
If there are 4 players, table is square and table HTML body is rotated based on player position on their screen.
The structure is like this:
<gameTable style=”position: relative;”>
<card style=”position: absolute; top: 1%, left: 1%;”></card>
</gameTable >
Now in order to move cards, player picks a card with mouse and it
1: Saves that location
2: Based on movement, it converts movement of card (in px), into relatively equal percentage and then moves the card on table that much.
This is so that whether someone is using big screen or small screen, the card movement will be same for all people by using % as top, left coordinates.
-- SQUARE TABLE:
When player is in 0 degree table position, the mouse and card movements are directly linked.
Card top: --mouse y, card left: --mouse x
When player is in 180 degree table position, the mouse movements are reversed:
Card top: ++mouse y, card left: ++mouse x
For 90 degree:
Card top: ++mouse x, card left: --mouse y
Similar translation with small change of coordinates for 270 degree rotated table.
This works perfectly as with this translation, it perfectly translates mouse over all directions in code.
THE PROBLEM:
For a 6 player game, I want to use Hexagonal table. Now in this table, the players table rotate to:
0, 60, 120, 180, 240 and 300 degrees.
0 and 180 degree are fine but I am unable to figure out a proper formula to translate mouse coordinates to card coordinates based on rotation.
-- CURRENT SOLUTION (Not good)
Currently, I did some brute force programming and did it like this: (60 degree table)
If (mouse moving up / down) {
card top: --mouse y/1.7, card left: --mouse y
}
If (mouse moving left / right) {
card top: ++mouse x, card left: --mouse x/1.7
}
Similar approaches for other positions on hexa table with small changes.
This is brute force approach and it blocks me from transitioning from up/down movement to left/right state as I have to leave the mouse and then start again.
In short, it is not working and I would like to know if it is possible to have a formula that can correctly translate mouse coordinates of mouse to cards based on rotation.
Please view following images for visual representation of what I mean
Square Table
Hexa Table
tl;dr See this JSFiddle
To move from four players to six players, you have to understand the math that underlies the four player case and then apply it to the six player case.
Four Player Case
You can think of each player's location as a given number of degrees (or radians) around a circle with an origin at the center of the table. In the four player case, the 1st player (index 0) will be at degrees 0, the 2nd player will be at 90 degrees, etc.
To determine how one player's movement affects the movement of a card on another person's screen that is not at the same angle, you need to break down the movement of the card in terms of its basis. In this case, the basis is composed of two orthogonal vectors that define the current player's movement space. The first vector is the vector generated using the angle. The second vector can be found by taking the current angle and adding 90 degrees (Pi/2 radians). For the four player case, the 1st player will have a primary basis vector of 0 degrees, which is (1,0). The 1st players secondary basis vector will be 0+90=90 degrees, which is (0,1).
To translate the mouse movement (delta screen x and y) into movement using an arbitrary basis, you take the dot product of delta screen x and y and the x and y of each vector in the basis. In the four player case, when the 1st player moves a card, the dot product of the primary and secondary basis result in the x delta and the y delta, respectively.
After you calculate this for the current player, you can to move the cards on other player's screens in terms of their basis. To do this, take the movement that has been converted into movement along the two bases and apply it to each other player's basis. In the four player case, the 2nd player's primary basis is 90 degrees = (1,0) and the secondary basis is 90+90 degrees = (-1,0). Thus to mirror the movement at the 2nd player's position, the original x movement is converted to y movement and the original y movement is converted to -x movement.
Six (or arbitrary number) of Players
To change this to a game with an arbitrary number of players and with a potential offset from 0 degrees for the 1st player, you need to follow the same math. First, calculate the bases for each player. Second, use dot products to calculate the delta of the card being movement in terms of its basis. Third, apply this movement to the bases of the other players.
The example below lets you change the number of players and the starting offset angle. The cards are divs and there is a canvas superimposed over the divs to show the primary axes (red) and the secondary axes (blue) for each basis.
<div>
<div class="table-div">
</div>
<div>
This example shows how the current players card (blue) can be shown moving on other players' (red "ghost" cards).
Below you
can change the number of players and the angle offset of the starting player.
</div>
<div>
<p>Players</p>
<input type="text" onchange="playerChange()" id="playerText" value="6">
</div>
<div>
<p>Angle (Degrees)</p>
<input type="text" onchange="angleChange()" id="angleText" value="45">
</div>
<canvas id="canv"></canvas>
body {
margin: 0;
}
canvas {
position: absolute;
top: 0;
left: 0;
z-index: 9;
}
.table-div {
background-color: lightgreen;
width: 400px;
height: 400px;
}
.card {
position: absolute;
width: 50px;
height: 70px;
}
.current {
background-color: blue;
}
.ghost {
background-color: red;
}
//A simple class to hold an x,y pair
class Vector2 {
constructor(x, y) { [this.x, this.y] = [x, y] }
}
//Set up our parameters
let playerCount = 6; //Can be changed by the user
let offsetAngle = 45; //Can be changed by the user
let width = 400;
let height = width;
let radius = width / 2;
let cardWidth = 50;
let cardHeight = 50 * 1.4;//~70
let cards;
//Track the mouse
let lastMouse = null;
let currentMouse = null;
let mouseDown = false;
//The angle in radians between each player on the table
let angleDelta;
//Grab the table div
let tableElement = document.querySelector(".table-div");
let canvas = document.querySelector("canvas")
let ctx = canvas.getContext("2d");
canvas.style.width = width + "px";
canvas.style.height = height + "px"
canvas.width = width;
canvas.height = height;
function update() {
ctx.clearRect(0, 0, width, height);
for (let i = 0; i < playerCount; i++) {
//Draw the first axis
ctx.strokeStyle = "red";
ctx.beginPath();
ctx.moveTo(radius, radius);
ctx.lineTo(radius + Math.cos(angleDelta * i + offsetAngle) * radius, radius + Math.sin(angleDelta * i + offsetAngle) * radius)
ctx.stroke();
//Draw the second axis
let secondAxisAngle = angleDelta * i + Math.PI / 2;
let startX = radius + Math.cos(angleDelta * i + offsetAngle) * radius / 2;
let startY = radius + Math.sin(angleDelta * i + offsetAngle) * radius / 2;
let endX = Math.cos(secondAxisAngle + offsetAngle) * radius / 4;
let endY = Math.sin(secondAxisAngle + offsetAngle) * radius / 4;
ctx.strokeStyle = "green";
ctx.beginPath();
ctx.moveTo(startX, startY)
ctx.lineTo(startX + endX, startY + endY)
ctx.stroke();
}
}
document.body.addEventListener("mousemove", e => {
//Keep track of the last mouse position so we can calculate a delta
lastMouse = currentMouse;
currentMouse = new Vector2(e.clientX, e.clientY);
if (lastMouse && mouseDown) {
let mouseDelta = new Vector2(currentMouse.x - lastMouse.x, currentMouse.y - lastMouse.y);
if (mouseDown) {
//Determine the movement in the current player's basis
let primaryAxisCurrent = new Vector2(Math.cos(angleDelta * 0 + offsetAngle), Math.sin(angleDelta * 0 + offsetAngle))
let secondAxisCurrent = new Vector2(Math.cos(angleDelta * 0 + Math.PI / 2 + offsetAngle), Math.sin(angleDelta * 0 + Math.PI / 2 + offsetAngle))
//Determine the movement in terms of the primary axes
let primary = (primaryAxisCurrent.x * mouseDelta.x + primaryAxisCurrent.y * mouseDelta.y)// * mouseDelta.x;
let second = (secondAxisCurrent.x * mouseDelta.x + secondAxisCurrent.y * mouseDelta.y)// * mouseDelta.y;
//Update all the cards using the primary and secondary motion
for (let i = 0; i < playerCount; i++) {
//Get the axes for this card
let primaryAxis = new Vector2(Math.cos(angleDelta * i + offsetAngle), Math.sin(angleDelta * i + offsetAngle))
let secondAxis = new Vector2(Math.cos(angleDelta * i + Math.PI / 2 + offsetAngle), Math.sin(angleDelta * i + Math.PI / 2 + offsetAngle))
//Translate the motion into this card's axes
let primaryAxisMovement = new Vector2(primaryAxis.x * primary, primaryAxis.y * primary)
let secondAxisMovement = new Vector2(secondAxis.x * second, secondAxis.y * second)
//Get the current location (strip the 'px') and add the change in position.
let div = cards[i];
let location = new Vector2(parseFloat(div.style.left) + primaryAxisMovement.x + secondAxisMovement.x, parseFloat(div.style.top) + primaryAxisMovement.y + secondAxisMovement.y)
//Reappend 'px'
div.style.left = location.x + "px"
div.style.top = location.y + "px"
}
}
}
})
document.body.addEventListener("mousedown", () => mouseDown = true)
document.body.addEventListener("mouseup", () => mouseDown = false)
function initDivs() {
//Create all the cards
cards = [];
tableElement.innerHTML = "";
for (let i = 0; i < playerCount; i++) {
let div = document.createElement("div");
tableElement.appendChild(div);
div.classList.add("card")
if (i == 0) div.classList.add("current")
else div.classList.add("ghost")
let radians = angleDelta * i;
div.style.left = (radius - cardWidth / 2 + Math.cos(radians + offsetAngle) * (radius - cardWidth / 2)) + "px";
div.style.top = (radius - cardHeight / 2 + Math.sin(radians + offsetAngle) * (radius - cardHeight / 2)) + "px"
cards.push(div);
}
}
function playerChange() {
playerCount = +document.querySelector("#playerText").value;
angleDelta = Math.PI * 2 / playerCount;
initDivs();
angleChange();
}
function angleChange() {
//Convert from degrees to radians
offsetAngle = parseFloat(document.querySelector("#angleText").value) * Math.PI / 180;
initDivs();
update();
}
playerChange();

Optimise javascript canvas for mass-drawing of tiny objects

I've been working on a game which requires thousands of very small images (20^20 px) to be rendered and rotated each frame. A sample snippet is provided.
I've used every trick I know to speed it up to increase frame rates but I suspect there are other things I can do to optimise this.
Current optimisations include:
Replacing save/restore with explicit transformations
Avoiding scale/size-transformations
Being explicit about destination sizes rather than letting the browser guess
requestAnimationFrame rather than set-interval
Tried but not present in example:
Rendering objects in batches to other offscreen canvases then compiling later (reduced performance)
Avoiding floating point locations (required due to placement precision)
Not using alpha on main canvas (not shown in snippet due to SO snippet rendering)
//initial canvas and context
var canvas = document.getElementById('canvas');
canvas.width = 800;
canvas.height = 800;
var ctx = canvas.getContext('2d');
//create an image (I) to render
let myImage = new OffscreenCanvas(10,10);
let myImageCtx = myImage.getContext('2d');
myImageCtx.fillRect(0,2.5,10,5);
myImageCtx.fillRect(0,0,2.5,10);
myImageCtx.fillRect(7.5,0,2.5,10);
//animation
let animation = requestAnimationFrame(frame);
//fill an initial array of [n] object positions and angles
let myObjects = [];
for (let i = 0; i <1500; i++){
myObjects.push({
x : Math.floor(Math.random() * 800),
y : Math.floor(Math.random() * 800),
angle : Math.floor(Math.random() * 360),
});
}
//render a specific frame
function frame(){
ctx.clearRect(0,0,canvas.width, canvas.height);
//draw each object and update its position
for (let i = 0, l = myObjects.length; i<l;i++){
drawImageNoReset(ctx, myImage, myObjects[i].x, myObjects[i].y, myObjects[i].angle);
myObjects[i].x += 1; if (myObjects[i].x > 800) {myObjects[i].x = 0}
myObjects[i].y += .5; if (myObjects[i].y > 800) {myObjects[i].y = 0}
myObjects[i].angle += .01; if (myObjects[i].angle > 360) {myObjects[i].angle = 0}
}
//reset the transform and call next frame
ctx.setTransform(1, 0, 0, 1, 0, 0);
requestAnimationFrame(frame);
}
//fastest transform draw method - no transform reset
function drawImageNoReset(myCtx, image, x, y, rotation) {
myCtx.setTransform(1, 0, 0, 1, x, y);
myCtx.rotate(rotation);
myCtx.drawImage(image, 0,0,image.width, image.height,-image.width / 2, -image.height / 2, image.width, image.height);
}
<canvas name = "canvas" id = "canvas"></canvas>
You are very close to the max throughput using the 2D API and a single thread, however there are some minor points that can improve performance.
WebGL2
First though, if you are after the best performance possible using javascript you must use WebGL
With WebGL2 you can draw 8 or more times as many 2D sprites than with the 2D API and have a larger range of FX (eg color, shadow, bump, single call smart tile maps...)
WebGL is VERY worth the effort
Performance related points
globalAlpha is applied every drawImage call, values other than 1 do not affect performance.
Avoid the call to rotate The two math calls (including a scale) are a tiny bit quicker than the rotate. eg ax = Math..cos(rot) * scale; ay = Math.sin(rot) * scale; ctx.setTransform(ax,ay,-ay,ax,x,y)
Rather than use many images, put all the images in a single image (sprite sheet). Not applicable in this case
Don`t litter the global scope. Keep object close as possible to functions scope and pass object by reference. Access to global scoped variable is MUCH slower the local scoped variables.
Best to use modules as they hove their own local scope
Use radians. Converting angles to deg and back is a waste of processing time. Learn to use radians Math.PI * 2 === 360 Math.PI === 180 and so on
For positive integers don't use Math.floor use a bit-wise operator as they automatically convert Doubles to Int32 eg Math.floor(Math.random() * 800) is faster as Math.random() * 800 | 0 ( | is OR )
Be aware of the Number type in use. Converting to an integer will cost cycles if every time you use it you convert it back to double.
Always Pre-calculate when ever possible. Eg each time you render an image you negate and divide both the width and height. These values can be pre calculated.
Avoid array lookup (indexing). Indexing an object in an array is slower than direct reference. Eg the main loop indexes myObject 11 times. Use a for of loop so there is only one array lookup per iteration and the counter is a more performant internal counter. (See example)
Though there is a performance penalty for this, if you separate update and render loops on slower rendering devices you will gain performance, by updating game state twice for every rendered frame. eg Slow render device drops to 30FPS and game slows to half speed, if you detect this update state twice, and render once. The game will still present at 30FPS but still play and normal speed (and may even save the occasional drooped frame as you have halved the rendering load)
Do not be tempted to use delta time, there are some negative performance overheads (Forces doubles for many values that can be Ints) and will actually reduce animation quality.
When ever possible avoid conditional branching, or use the more performant alternatives. EG in your example you loop object across boundaries using if statements. This can be done using the remainder operator % (see example)
You check rotation > 360. This is not needed as rotation is cyclic A value of 360 is the same as 44444160. (Math.PI * 2 is same rotation as Math.PI * 246912)
Non performance point.
Each animation call you are preparing a frame for the next (upcoming) display refresh. In your code you are displaying the game state then updating. That means your game state is one frame ahead of what the client sees. Always update state, then display.
Example
This example has added some additional load to the objects
can got in any direction
have individual speeds and rotations
don`t blink in and out at edges.
The example includes a utility that attempts to balance the frame rate by varying the number of objects.
Every 15 frames the (work) load is updated. Eventually it will reach a stable rate.
DON`T NOT gauge the performance by running this snippet, SO snippets sits under all the code that runs the page, the code is also modified and monitored (to protect against infinite loops). The code you see is not the code that runs in the snippet. Just moving the mouse can cause dozens of dropped frames in the SO snippet
For accurate results copy the code and run it alone on a page (remove any extensions that may be on the browser while testing)
Use this or similar to regularly test your code and help you gain experience in knowing what is good and bad for performance.
Meaning of rate text.
1 +/- Number Objects added or removed for next period
2 Total number of objects rendered per frame during previous period
3 Number Running mean of render time in ms (this is not frame rate)
4 Number FPS is best mean frame rate.
5 Number Frames dropped during period. A dropped frame is the length of the reported frame rate. I.E. "30fps 5dropped" the five drop frames are at 30fps, the total time of dropped frames is 5 * (1000 / 30)
const IMAGE_SIZE = 10;
const IMAGE_DIAGONAL = (IMAGE_SIZE ** 2 * 2) ** 0.5 / 2;
const DISPLAY_WIDTH = 800;
const DISPLAY_HEIGHT = 800;
const DISPLAY_OFFSET_WIDTH = DISPLAY_WIDTH + IMAGE_DIAGONAL * 2;
const DISPLAY_OFFSET_HEIGHT = DISPLAY_HEIGHT + IMAGE_DIAGONAL * 2;
const PERFORMANCE_SAMPLE_INTERVAL = 15; // rendered frames
const INIT_OBJ_COUNT = 500;
const MAX_CPU_COST = 8; // in ms
const MAX_ADD_OBJ = 10;
const MAX_REMOVE_OBJ = 5;
canvas.width = DISPLAY_WIDTH;
canvas.height = DISPLAY_HEIGHT;
requestAnimationFrame(start);
function createImage() {
const image = new OffscreenCanvas(IMAGE_SIZE,IMAGE_SIZE);
const ctx = image.getContext('2d');
ctx.fillRect(0, IMAGE_SIZE / 4, IMAGE_SIZE, IMAGE_SIZE / 2);
ctx.fillRect(0, 0, IMAGE_SIZE / 4, IMAGE_SIZE);
ctx.fillRect(IMAGE_SIZE * (3/4), 0, IMAGE_SIZE / 4, IMAGE_SIZE);
image.neg_half_width = -IMAGE_SIZE / 2; // snake case to ensure future proof (no name clash)
image.neg_half_height = -IMAGE_SIZE / 2; // use of Image API
return image;
}
function createObject() {
return {
x : Math.random() * DISPLAY_WIDTH,
y : Math.random() * DISPLAY_HEIGHT,
r : Math.random() * Math.PI * 2,
dx: (Math.random() - 0.5) * 2,
dy: (Math.random() - 0.5) * 2,
dr: (Math.random() - 0.5) * 0.1,
};
}
function createObjects() {
const objects = [];
var i = INIT_OBJ_COUNT;
while (i--) { objects.push(createObject()) }
return objects;
}
function update(objects){
for (const obj of objects) {
obj.x = ((obj.x + DISPLAY_OFFSET_WIDTH + obj.dx) % DISPLAY_OFFSET_WIDTH);
obj.y = ((obj.y + DISPLAY_OFFSET_HEIGHT + obj.dy) % DISPLAY_OFFSET_HEIGHT);
obj.r += obj.dr;
}
}
function render(ctx, img, objects){
for (const obj of objects) { drawImage(ctx, img, obj) }
}
function drawImage(ctx, image, {x, y, r}) {
const ax = Math.cos(r), ay = Math.sin(r);
ctx.setTransform(ax, ay, -ay, ax, x - IMAGE_DIAGONAL, y - IMAGE_DIAGONAL);
ctx.drawImage(image, image.neg_half_width, image.neg_half_height);
}
function timing(framesPerTick) { // creates a running mean frame time
const samples = [0,0,0,0,0,0,0,0,0,0];
const sCount = samples.length;
var samplePos = 0;
var now = performance.now();
const maxRate = framesPerTick * (1000 / 60);
const API = {
get FPS() {
var time = performance.now();
const FPS = 1000 / ((time - now) / framesPerTick);
const dropped = ((time - now) - maxRate) / (1000 / 60) | 0;
now = time;
if (FPS > 30) { return "60fps " + dropped + "dropped" };
if (FPS > 20) { return "30fps " + (dropped / 2 | 0) + "dropped" };
if (FPS > 15) { return "20fps " + (dropped / 3 | 0) + "dropped" };
if (FPS > 10) { return "15fps " + (dropped / 4 | 0) + "dropped" };
return "Too slow";
},
time(time) { samples[(samplePos++) % sCount] = time },
get mean() { return samples.reduce((total, val) => total += val, 0) / sCount },
};
return API;
}
function updateStats(CPUCost, objects) {
const fps = CPUCost.FPS;
const mean = CPUCost.mean;
const cost = mean / objects.length; // estimate per object CPU cost
const count = MAX_CPU_COST / cost | 0;
const objCount = objects.length;
var str = "0";
if (count < objects.length) {
var remove = Math.min(MAX_REMOVE_OBJ, objects.length - count);
str = "-" + remove;
objects.length -= remove;
} else if (count > objects.length + MAX_ADD_OBJ) {
let i = MAX_ADD_OBJ;
while (i--) {
objects.push(createObject());
}
str = "+" + MAX_ADD_OBJ;
}
info.textContent = str + ": " + objCount + " sprites " + mean.toFixed(3) + "ms " + fps;
}
function start() {
var frameCount = 0;
const CPUCost = timing(PERFORMANCE_SAMPLE_INTERVAL);
const ctx = canvas.getContext('2d');
const image = createImage();
const objects = createObjects();
function frame(time) {
frameCount ++;
const start = performance.now();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, DISPLAY_WIDTH, DISPLAY_WIDTH);
update(objects);
render(ctx, image, objects);
requestAnimationFrame(frame);
CPUCost.time(performance.now() - start);
if (frameCount % PERFORMANCE_SAMPLE_INTERVAL === 0) {
updateStats(CPUCost, objects);
}
}
requestAnimationFrame(frame);
}
#info {
position: absolute;
top: 10px;
left: 10px;
background: #DDD;
font-family: arial;
font-size: 18px;
}
<canvas name = "canvas" id = "canvas"></canvas>
<div id="info"></div>

Javascript canvas animation not appearing

I'm trying to create a canvas animation with 2 objects: a circumference and a filled circle. My objective is to make it seem that the circumference represents the circles orbit. However when trying to animate there's no animation and only when I click to stop the page does the image appear with the circle in a random position in the orbit (this means that the moving part works).
Thank you for your time and here's the code:
function restartAnimate(){
runAnimation(0);
setTimeout(restartAnimate(),1000);
}
function runAnimation(i){
let animation = document.getElementById("Animation");
let anim = animation.getContext("2d");
anim.clearRect(0,0,300,150);
anim.save();
anim.strokeStyle = "#99ebff";
anim.lineWidth = 10;
anim.beginPath();
anim.arc(150, 75, 40, 0, 2 * Math.PI);
anim.stroke();
anim.restore();
anim.save()
anim.fillStyle = "#000000";
anim.translate(150,75);
anim.rotate(2 * Math.PI * i / 1000);
anim.translate(-150,-75);
anim.beginPath();
anim.arc(150 + 36.5, 75 ,13, 0, 2 * Math.PI);
anim.fill();
anim.restore();
i += 16;
if(i < 1000) setTimeout(runAnimation(i),16);
}
You should use requestAnimationFrame to animate so that the render results are displayed in sync with the display hardware refresh.
setTimeout is very inaccurate and your function will fall behind over time. If you use requestAnimationFrame you can use the first argument (time in ms) to keep precisely on time.
ctx.save, and ctx.restore can be very expensive calls and should be avoided if you can. As you are only restoring the transform you can set it manually as needed with ctx.setTransform()
There is no need to restart the animation, just let it cycle.
Example rewrites your code with above points in mind and some other changes. See code comments for more info.
// Define constants and query DOM outside animation functions
const canvas = document.getElementById("animCanvas");
const ctx = canvas.getContext("2d");
Math.PI2 = Math.PI * 2;
var startTime;
restartAnimate();
function restartAnimate() {
if (startTime === undefined) {
requestAnimationFrame(runAnimation);
} else {
startTime = 0; // next frame animation we have restarted
}
// setTimeout(restartAnimate(),1000); // No need to restart as angle is cyclic.
}
function runAnimation(time) {
if (!startTime) { startTime = time }
const currentTime = time - startTime;
ctx.setTransform(1,0,0,1,0,0); // resets transform, better than using save and restore
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height); // avoid magic numbers
//ctx.save(); // not needed
ctx.setTransform(1,0,0,1,150, 75); // last two values set the origin
// and is the point we rotate around
ctx.strokeStyle = "#99ebff";
ctx.lineWidth = 10;
ctx.beginPath();
ctx.arc(0, 0, 40, 0, Math.PI2); // rendering at the origin
ctx.stroke();
//ctx.restore(); // not needed
//ctx.save(); // not needed
ctx.fillStyle = "#000000";
//ctx.translate(150,75); // working from origin so don't need to translate
ctx.rotate(Math.PI2 * currentTime / 1000);
//ctx.translate(-150,-75); // working from origin so don't need to translate
ctx.beginPath();
ctx.arc(36.5, 0 ,13, 0, Math.PI2);
ctx.fill();
//ctx.restore(); not needed
requestAnimationFrame(runAnimation);
}
<canvas id="animCanvas"></canvas>

HTML slider doubles pendulum speed

I am trying to make a pendulum in HTML/JS and have its angular velocity and angle controlled by ranges. both of the ranges work changing what they're intend to, but each time the value of the ranges is changed it seems to heavily increase the speed of the pendulum if the speed or angle is increased
or decreased the speed.
Here's the HTML/JavaScript Snippet
var canvas = ctx = false;
var frameRate = 1/40;
var frameDelay = frameRate * 1000;
/*used to change the angle and the velocity of the pendulum*/
var arcSlider = document.getElementById("arc");
var velocitySlider = document.getElementById('velocity');
var arcNumber = document.getElementById("arcNum");
var velocityNumber = document.getElementById("velocityNum");
var arc = (arcSlider.value / 100);
var velocity = velocitySlider.value;
/*sets the values for the pendulum*/
var pendulum = {mass: 100, length:300, theta: (Math.PI/2) - arc , omega: 0, alpha:0, J:0};
/*listener for angl slider*/
arcSlider.addEventListener("change", function(){
arcNumber.innerHTML = "arc: " + (arcSlider.value / 100);
arc = arcSlider.value / 100;
init();
});
/*listener for velocity slider*/
velocitySlider.addEventListener("change", function(){
velocityNumber.innerHTML = "velocity: " + velocitySlider.value;
velocity = velocitySlider.value;
init();
});
window.requestAnimFrame = (function(){
return window.requestAnimationFrame ||
window.webkitRequestAnimationFrame ||
window.mozRequestAnimationFrame ||
window.oRequestAnimationFrame ||
window.msRequestAnimationFrame ||
function( callback ){
window.setTimeout(callback, 1000 / 60);
};
})();
function init() {
pendulum.J = pendulum.mass * pendulum.length * pendulum.length / velocity;
lastTime = new Date();
requestAnimFrame(draw);
}
/*loop for pendulum*/
function draw(){
var width = 1000, height = 600;
var len = 150;
var timeMs = (new Date()).getTime();
var deltaT = (timeMs - lastTime.getTime()) / 1000;
canvas = document.getElementById("myCanvas");
let ctx = canvas.getContext("2d");
if (deltaT > 0.050)
{
deltaT = 0.050;
}
deltaT = 0.01;
/* Calculate current position*/
pendulum.theta += pendulum.omega * deltaT + (.5 * pendulum.alpha * deltaT * deltaT );
/* calculates force */
var T = pendulum.mass * 9.81 * Math.cos(pendulum.theta) * pendulum.length;
/* Current acceleration */
var alpha = T / pendulum.J;
/*Calculate current velocity*/
pendulum.omega += .5 * (alpha + pendulum.alpha) * deltaT;
/* Update acceleration */
pendulum.alpha = alpha;
/*sets the current x and y for the pendulum*/
var bobX = width/2 + pendulum.length * Math.cos(pendulum.theta);
var bobY = pendulum.length * Math.sin(pendulum.theta);
/*clears the canvas*/
ctx.clearRect(0,0,width,height)
/*canvas line*/
ctx.strokeStyle = "green";
ctx.beginPath();
ctx.moveTo(width/2,0);
ctx.lineTo(bobX,bobY);
ctx.stroke();
ctx.closePath();
ctx.fillStyle = "red";
/*canvas pendulum*/
ctx.beginPath();
ctx.arc(bobX,bobY,16,0 ,Math.PI * 2 , false);
ctx.fill();
ctx.closePath();
requestAnimationFrame(draw);
}
init();
<div href="#0" class="button">
<canvas id="myCanvas" width="1000" height="400">
</canvas>
</div>
<div class="sliderOutline">
<div class="sliderContainer">
<p>Change the arc of the pendulum:</p>
<input type="range" name="arcRange"min="5" max="80" value="40" class="slider" id="arc">
<p>Change the velocity:</p>
<input type="range" min="0" max="1000" value="500" class="slider" id="velocity" >
<div>
<p id="arcNum">arc: 0.4 </p>
</div>
<div>
<p id="velocityNum">velocity: 500</p>
</div>
</div>
</div>
Each time you call init(), init() calls requestAnimationFrame(draw) and of course draw calls requestAnimationFrame(draw) as is a common pattern.
However when you call init() a second time (e.g. when the user modifies a slider) then you call requestAnimationFrame(draw) again. When the browser determines it's time for an animation frame, draw will get called twice.
There's a stack of functions that will get called every time an animation frame is ready to be requested. If you call requestAnimationFrame once, then the stack get's one entry of draw pushed on to it. When the animation frame is ready that stack is popped until it's empty, and the functions get called one at a time. It's common practice to push on one function that will push itself back onto that stack with another call to requestAnimationFrame right at the end.
When you call requestAnimationFrame multiple times from outside this normal loop, you end up with multiple functions in that stack that all get called each animation frame. Because your draw function modifies the state of your pendulum each time it's called, calling it twice as often will make the pendulum move twice as fast. Click around on your slider 5 or 6 times and it'll rock back and forth like crazy.
As a quick fix, you can modify init like so:
function init(begin) {
pendulum.mass = 100;
pendulum.theta = (Math.PI/2) - arc;
pendulum.omega = 0;
pendulum.alpha = 0;
pendulum.J = pendulum.mass * pendulum.length * pendulum.length / velocity;
lastTime = new Date();
if (begin) requestAnimFrame(draw);
}
And at the very bottom of your javascript you'd call:
init(true);
As shown with an updated codepen. It's not the best solution, just a minimal solution to show that not calling requestAnimationFrame multiple times should provide the results you're looking for.

One canvas to interact, three to display

i'm creating a browser game which is meant to be played as a hologram.
The screen should be displaying something like this:
https://www.youtube.com/watch?v=Y60mfBvXCj8
Therefore i thought i have to create 4 canvas (no problem), but three of them should only display whats happening on the first.
I've tried to let it draw an Image of the canvas and let it display to the other canvas.
Any help would be appreciated!
The game is created with Box2D.
edit:
i want the space ship to be drawn in every canvas, but only controlled in one.
my code: http://s000.tinyupload.com/index.php?file_id=68837773176112789787
the problem is, that its only displaying on one canvas!
what i've put in the HTML:
<canvas id="canvas1" width="500" height="500"></canvas>
<canvas id="canvas2" width="500" height="500"></canvas>
<canvas id="canvas3" width="500" height="500"></canvas>
<canvas id="canvas4" width="500" height="500"></canvas>
what is meant to print it to the others:
JS
var sourceCtx, destinationCtx, imageData;
//get the context of each canvas
sourceCtx = canvas2.getContext('2d');
canvas2Ctx = canvas3.getContext('2d');
//copy the data
imageData = sourceCtx.getImageData(0, 0, canvas2.width - 1, canvas2.height - 1);
//apply the image data
canvas3Ctx.putImageData(imageData, 0, 0);
//done
Holographic pyramid display
How to render for a pyramid reflecting display.
To do this use a single display canvas in the HTML and a canvas stored in memory for rendering.
Mirrored render canvas
The rendering canvas is clipped to a triangle to prevent pixels overlapping and the transform is mirrored so that the final effect is correctly seen. Eg text is back to front.
The offscreen rendering canvas is then rendered to the display canvas, starting at the top and making a total of 4 copies each rotated 90deg.
The rendering canvas width will be the minimum of the display width or height and half that for the height in order to fit the display.
Needs fullscreen mode
For the FX to work you will need to enter fullscreen mode. I have not included how this is done but I am sure there is a QA on stackoverflow that will step you through the process.
Dead zone
At the center of the display is a area on which the pyramid will rest (I call it the dead zone) As many of these displays are homemade the size of the dead zone will vary. In the very first line of the demo below is a constant deadZoneSize that will set the dead zone size. It is currently set at 0.1 which is 10% of the view size. You may need to adjust this value to suit your particular reflecting display.
Example code
The code example is full of comments in the relevant parts. It will create and setup the display canvas and render canvas. Create the clip area and set up the mirrored rendering transform, so you can render as normal. A mainLoop function will call a function called renderContent with the first argument as being the context of the render canvas. Just render your content as normal (use size and hSize for the width and height of the visible render area (maybe I should have used a better name))
The demo includes an example rendering just for the fun of it, that is all at the bottom and has minimum comments as not really relevant to the question.
const deadZoneSize = 0.1; // As fraction of fitted box size
// for FX em and em4 are just custom unit size and 1/4 size
var em,em4;
// to fit all four views use the min width or height
var size = Math.min(innerWidth,innerHeight);
// half size
var hSize = size / 2 | 0;
// there is a small area where nothing should be displayed.
// This will depend on the pyrimide being used.
var deadZone = size * 0.1 | 0; // about 10% of view area
// Display canvas d for display
const dCanvas = document.createElement("canvas");
// Render canvas
const rCanvas = document.createElement("canvas");
// get rendering context for both
const dCtx = dCanvas.getContext("2d");
const rCtx = rCanvas.getContext("2d");
// Set the display canvas to fill the page
Object.assign(dCanvas.style,{
position : "absolute",
zIndex : 10, // place above
top : "0px",
left : "0px",
background : "black",
})
// add the display canvas to the DOM
document.body.appendChild(dCanvas);
//Size function resizes canvases when needed
function resize(){
startTime = undefined;
size = Math.min(innerWidth,innerHeight);
hSize = size / 2 | 0;
deadZone = size * deadZoneSize | 0; // about 10% of view area
dCanvas.width = innerWidth;
dCanvas.height = innerHeight;
rCanvas.width = size;
rCanvas.height = hSize; // half height
em = size * 0.1 | 0; // define our own unit size
em4 = Math.max(1,em * 0.25 | 0); // define quarter unit size min of 1
}
// To ensure pixels do not stray outside the view area and overlap use a clip on the render canvas
// ctx the context to appy the clip path to
function defineClip(ctx){
ctx.beginPath();
ctx.lineTo(0,0);
ctx.lineTo(size,0);
ctx.lineTo(hSize + deadZone, hSize - deadZone);
ctx.lineTo(hSize - deadZone, hSize - deadZone);
ctx.clip();
// The rendering is mirrored from the holo pyramid
// to avoid seeing text mirrored you need to mirror the
// rendering transform
ctx.setTransform(-1,0,0,1,size,0); // x axis from right to left, origin at top right
}
// Copying the rendered canvas to the display canvas
// ctx is the display canvas context
// image is the rendered canvas
function display(ctx,image) {
// for each face of the pyramid render a view
// Each image is just rotated 90 deg
// first clear the canvas
ctx.clearRect(0,0,ctx.canvas.width, ctx.canvas.height);
// top
// use the center of the display canvas as the origin
ctx.setTransform(1,0,0,1,ctx.canvas.width / 2 | 0, ctx.canvas.height / 2 | 0);
// draw the image
ctx.drawImage(image,-hSize,-hSize);
// Right
ctx.transform(0,1,-1,0,0,0); // rotate 90 deg. This is better than ctx.rotate as it can have slight
// problems due to floating point errors if not done correctly
ctx.drawImage(image,-hSize,-hSize);
// bottom
ctx.transform(0,1,-1,0,0,0);
ctx.drawImage(image,-hSize,-hSize);
// left
ctx.transform(0,1,-1,0,0,0);
ctx.drawImage(image,-hSize,-hSize);
// restore the default transform;
ctx.setTransform(1,0,0,1,0,0);
}
// the main render loop
var globalTime;
var startTime;
function mainLoop(time){
// check canvas size. If not matching page then resize
if(dCanvas.width !== innerWidth || dCanvas.height !== innerHeight) {
resize();
}
if(startTime === undefined){ startTime = time }
globalTime = time - startTime;
// clear the render canvas ready for next render
rCtx.setTransform(1,0,0,1,0,0); // reset transform
rCtx.globalAlpha = 1; // reset alpha
rCtx.clearRect(0,0,size,hSize);
// save the context state so that the clip can be removed
rCtx.save();
defineClip(rCtx); // set the clip
renderContent(rCtx); // call the rendering function
// restore the context state which removes the clip
rCtx.restore();
// rendering is ready for display so render the holo view
// on to the display canvas's context
display(dCtx, rCanvas);
requestAnimationFrame(mainLoop);
}
requestAnimationFrame(mainLoop);
//=====================================================================================================
// The following is just something interesting to display and is not directly related to the answer
//=====================================================================================================
// The main rendering function
// This is where you render your content. It can be anything from a game to just plain old text
// You can even use a video element and display a video.
// The rendering context is already set up to correctly mirror the content so just render everything as normal
const randG = (min, max , p = 2) => (max + min) / 2 + (Math.pow(Math.random(), p) * (max - min) * 0.5) * (Math.random() < 0.5 ? 1 : -1);
const bootUp = ["Power On",1,1000,"Sub system test",0.5, 3000, "Calibrating scanner",0.5, 6000, "Welcome",1,8000];
function noisyText(ctx){
var textTime = globalTime / 8000; // 8 second boot up
if(screenFlashDone){
if(globalTime > screenFlashes[0]) { // play screen flash seq
screenFlashes.shift();
screenFlash(ctx,true,screenFlashes.shift(),screenFlashes.shift());
}
}else{
screenFlash(ctx);
}
ctx.font = ((bootUp[1] * em) | 0) + "px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "center";
var tx = randG(-em4 * 4, em4 * 4, 64); // G for kind of a bit like gausian. Last num controls distrubution
var ty = randG(-em4 * 4, em4 * 4, 64);
var xx = size / 2 + tx;
var yy = em * 2 + ty;
ctx.fillStyle = `hsl(${randG(160,250,32)|0},100%,50%)`;
if(bootUp[2] < globalTime){
bootUp.shift();
bootUp.shift();
bootUp.shift();
}
ctx.fillText(bootUp[0], xx, yy);
ctx.save(); // need the normal non mirror transform for the noise FX
ctx.setTransform(1,0,0,1,0,0);
for(var y = -em/1.2|0; y < em/2; y += 1){
if((yy+y) % 3 === 0){
ctx.clearRect(0,yy+y,size,1); // give scan line look
}else{
if(Math.random() < 0.1){ // only on 10% of lines.
ctx.drawImage(ctx.canvas,0,yy + y, size, 2,randG(-em4 * 4,em4 * 4,32),yy + y, size, 2);
}
}
}
ctx.fillRect(0,((globalTime / 4000) * hSize)%hSize,size,2);
ctx.filter = `blur(${randG(em4/2,em4,2)|0}px)`;
ctx.drawImage(ctx.canvas,0,0);
ctx.restore();
}
const screenFlashes = [0,500,3,1000,200,2,4000,100,3,6000,100,1,7500,50,1,7800,50,1, 9000];
var screenFlashStart;
var screenFlashLen;
var screenFlashDone = true;
var screenFlashLayers = 1;
function screenFlash(ctx,start,length,layers){
if(start){
screenFlashStart = globalTime;
screenFlashLen = length;
screenFlashDone = false;
screenFlashLayers = layers;
}
var normTime = (globalTime - screenFlashStart) / screenFlashLen;
if(normTime >= 1){
screenFlashDone = true;
normTime = 1;
}
for(var i = 0; i < screenFlashLayers; i++){
var tx = randG(-em4 * 4, em4 * 4, 64); // G for kind of a bit like gausian. Last num controls distrubution
var ty = randG(-em4 * 4, em4 * 4, 64);
ctx.globalAlpha = (1-normTime) * Math.random();
ctx.fillStyle = `hsl(${randG(160,250,32)|0},100%,50%)`;
ctx.fillRect(tx,ty,size,hSize);
}
ctx.globalAlpha = 1;
}
function randomBlur(ctx) {
ctx.save(); // need the normal non mirror transform for the noise FX
ctx.filter = `blur(${randG(em4/2,em4,2)|0}px)`;
ctx.drawImage(ctx.canvas,0,0);
ctx.restore();
}
function ready(ctx) {
ctx.fillStyle = "#0F0";
ctx.font = em + "px monospace";
ctx.textAlign = "center";
ctx.textBaseline = "center";
ctx.fillText("Holographic",hSize,em);
ctx.font = em/2 + "px monospace";
ctx.fillText("display ready.",hSize,em * 2);
// draw edges
ctx.strokeStyle = "#0F0";
ctx.lineWidth = em4;
ctx.beginPath();
ctx.lineTo(0,0);
ctx.lineTo(size,0);
ctx.lineTo(hSize + deadZone, hSize - deadZone);
ctx.lineTo(hSize - deadZone, hSize - deadZone);
ctx.closePath();
ctx.stroke();
}
function renderContent(ctx){
// all rendering is mirrored, but the transform takes care of that for you
// just render as normal. Remember you can only see the
// triangular area with the wide part at the top
// and narrow at the bottom.
// Anything below hSize - deadZone will also not appear
if(globalTime < 8000){
noisyText(ctx);
randomBlur(ctx);
}else{
ready(ctx);
}
randomBlur(ctx);
}
A quick side note. I feel your question meets the SO requirements and is not off topic, nor are you asking for someone to write the code. You have shown that you have put some effort into research. This question will be of interest to others. I hope this answer helps, good luck in your project and welcome to SO.

Categories

Resources