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.
Related
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>
I am trying to get a shaded trace of a clock face to follow the clock hand when pressing the space bar. It's redrawn every tick:
var stage, cont, arrow, cursorWedge, spacebarState, cursorWedgeStart, fired;
var msg = document.getElementById('state-msg');
var KEYCODE_SPACEBAR = 32;
spacebarState = false; // for cursor shadow wedge
stage = new createjs.Stage("canvas");
cont = stage.addChild(new createjs.Container());
cont.x = stage.canvas.width / 2;
cont.y = 250;
cont.rotation -= 90;
var circle = new createjs.Shape();
circle.graphics.beginFill("DeepSkyBlue").drawCircle(0, 0, 150);
circle.x = 0;
circle.y = 0;
cont.addChild(circle);
cont.setChildIndex(circle, 0);
arrow = new createjs.Shape();
arrow.graphics.beginFill("black").drawRect(0, 0, 150, 2);
cont.addChild(arrow);
cursorWedge = new createjs.Shape();
createjs.Ticker.setFPS(60);
createjs.Ticker.addEventListener("tick", tick);
document.body.addEventListener('keydown', function(e) {
msg.textContent = 'keydown:' + e.keyCode;
keyDown(e);
});
document.body.addEventListener('keyup', function(e) {
msg.textContent = 'keyup:' + e.keyCode;
keyUp(e);
});
function tick() {
arrow.rotation += 1;
if (spacebarState === true) {
cursorWedge.graphics.moveTo(0, 0);
cursorWedge.graphics.arc(0, 0, 150, cursorWedgeStart * Math.PI / 180, arrow.rotation * Math.PI / 180);
}
stage.update();
}
function keyDown(event) {
switch (event.keyCode) {
case KEYCODE_SPACEBAR:
if (!fired) { //avoiding repeated events
fired = true;
cursorWedge.graphics.f(createjs.Graphics.getRGB(0, 0, 0, 0.25));
spacebarState = true;
cursorWedgeStart = arrow.rotation;
cont.addChild(cursorWedge);
cont.setChildIndex(cursorWedge, cont.getNumChildren() - 1);
}
}
}
function keyUp(event) {
switch (event.keyCode) {
case KEYCODE_SPACEBAR:
fired = false;
spacebarState = false;
cursorWedge.graphics.clear();
cont.removeChild(cursorWedge);
}
}
<head>
<script src="https://code.createjs.com/createjs-2015.11.26.min.js"></script>
</head>
<body>
Keyboard message: <span id="state-msg"></span>
<canvas id="canvas" width="500" height="400"> </canvas>
</body>
Is there any way of redrawing the circle segment without it markedly slowing down after the key is held for a longer time?
In your code, you are adding to the graphics each tick. This means each frame is drawing the previous frame's graphics PLUS your new frame's graphics. This is additive, so by frame 100, you are drawing 100 arcs each tick, etc.
If you aren't trying to create an additive effect, simply clear the graphics before redrawing them:
cursorWedge.clear()
.graphics.arc(0, 0, 150, cursorWedgeStart * Math.PI / 180, arrow.rotation * Math.PI / 180);
Another way is to store the graphics command, and just modify it on tick. When the stage redraws, it will use the graphics updated values.
// In the init (at the top in your example)
var arcCommand = cursorWedge.graphics.moveTo(0,0)
.graphics.arc(0, 0, 150, Math.PI / 180, 0).command; // returns the last command
// In your tick
arcCommand.endAngle = arrow.rotation * Math.PI / 180;
If you are going for an additive effect (which I don't believe you are in this case) adding up the graphics each tick is still really expensive. You can instead cache the container the shape lives in, and then just add new graphics commands each tick, and update the cache. Check out the Cache Update demo, which can also be found in GitHub.
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>
I am putting together a simulation of an urban transportation system, and trying to improve my Javascript and Canvas skills. I have provided a bare-bones version here: https://jsfiddle.net/ftmzm9vp/
Two questions:
1) I want the "pods" to run at a uniform rate. Right now they are all arriving at their destinations at the same time, which means they are traveling at different speeds. How do I correct this?
2) There is obviously more I have to do -- get the pods to travel along existing lines, work out the best path to their destination, expand the number of lines and stations -- all of which will increase the computing overhead. Right now, with the 500 pods I want to use, the animation is starting to crawl. I rewrote the whole thing to use requestAnimFrame, as I thought it would be faster, but it doesn't seem to be as smooth at it should be. What can I do to improve this?
Thanks!
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<title>Pod Stations Lines Test</title>
<body>
<canvas id="layer1" style="z-index: 2;
position:absolute;
left:0px;
top:0px;
" height="600px" width="1000">This text is displayed if your browser does not support HTML5 Canvas.</canvas>
<canvas id="layer2" style="z-index: 3;
position:absolute;
left:0px;
top:0px;
" height="600px" width="1000">This text is displayed if your browser does not support HTML5 Canvas.</canvas>
<canvas id="layer3" style="z-index: 1;
position:absolute;
left:0px;
top:0px;
" height="600px" width="1000">This text is displayed if your browser does not support HTML5 Canvas.</canvas>
<script>
//Modified Source: http://jsfiddle.net/m1erickson/HAbfm/
//
layer1 = document.getElementById("layer1");
ctx1 = layer1.getContext("2d");
layer2 = document.getElementById("layer2");
ctx2 = layer2.getContext("2d");
layer3 = document.getElementById("layer3");
ctx3 = layer3.getContext("2d");
window.requestAnimFrame = (function(callback) {
return window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.oRequestAnimationFrame || window.msRequestAnimationFrame || function(callback) {
window.setTimeout(callback, 1000 / 60);
};
})();
//STATION LIST
var station = [
['A', 100, 50],
['B', 300, 50],
['C', 200, 150],
['D', 100, 250],
['E', 300, 250],
['F', 400, 250]
];
//DRAW LINES
function drawLines() {
ctx1.clearRect(0, 0, layer3.width, layer3.height);
var linkAB = ctx1.beginPath();
ctx1.moveTo(station[0][1], station[0][2]);
ctx1.lineTo(station[1][1], station[1][2]);
ctx1.stroke();
var linkBC = ctx1.beginPath();
ctx1.moveTo(station[1][1], station[1][2]);
ctx1.lineTo(station[2][1], station[2][2]);
ctx1.stroke();
var linkCD = ctx1.beginPath();
ctx1.moveTo(station[2][1], station[2][2]);
ctx1.lineTo(station[3][1], station[3][2]);
ctx1.stroke();
var linkDE = ctx1.beginPath();
ctx1.moveTo(station[3][1], station[3][2]);
ctx1.lineTo(station[4][1], station[4][2]);
ctx1.stroke();
var linkCE = ctx1.beginPath();
ctx1.moveTo(station[2][1], station[2][2]);
ctx1.lineTo(station[4][1], station[4][2]);
ctx1.stroke();
var linkEF = ctx1.beginPath();
ctx1.moveTo(station[4][1], station[4][2]);
ctx1.lineTo(station[5][1], station[5][2]);
ctx1.stroke();
}
//CREATE PODS
var podArray = [];
function Pod(startX, startY, endX, endY, riders, color) {
this.startX = startX;
this.startY = startY;
this.endX = endX;
this.endY = endY;
this.riders = riders;
this.color = color;
}
var colorArray = ["gold", "orange", "red", "green", "blue", "black"];
function randomPass() {
occ = 1 + Math.floor(Math.random() * 6);
return occ;
console.log("Riders " + occ);
}
for (i = 0; i < 500; i++) {
var origNum = Math.floor(Math.random() * station.length);
var origin = {
x: station[origNum][1],
y: station[origNum][2]
}
var destNum = Math.floor(Math.random() * station.length);
while (origNum == destNum) {
destNum = Math.floor(Math.random() * station.length);
}
var destination = {
x: station[destNum][1],
y: station[destNum][2]
}
podArray.push(new Pod(
startX = origin.x,
startY = origin.y,
endX = destination.x,
endY = destination.y,
riders = randomPass(),
color = colorArray[riders - 1]
));
}
var pct = 0.00;
var fps = 60;
//CALL DRAWING AND ANIMATION
drawLines();
animate();
function animate() {
setTimeout(function() {
if (pct <= 1.00) {
requestAnimFrame(animate)
};
// increment the percent (from 0.00 to 1.00)
pct += .01;
// clear the canvas
ctx3.clearRect(0, 0, layer3.width, layer3.height);
// draw all podArray
for (var i = 0; i < podArray.length; i++) {
// get reference to next aPod
var aPod = podArray[i];
var dx = aPod.endX - aPod.startX;
var dy = aPod.endY - aPod.startY;
var nextX = aPod.startX + dx * pct;
var nextY = aPod.startY + dy * pct;
//create pod on screen
ctx3.fillStyle = aPod.color;
ctx3.beginPath();
ctx3.arc(nextX, nextY, 5, 0, Math.PI * 2, true);
ctx3.fillStyle = aPod.color;
ctx3.fill();
ctx3.closePath();
//STATION LETTERS
for (s = 0; s < station.length; s++) {
ctx2.font = '12pt Calibri';
ctx2.fillStyle = 'red';
ctx2.textAlign = 'center';
ctx2.fillText(station[s][0], station[s][1], (station[s][2]) + 4);
}
}
}, 1000 / fps);
}
</script>
</body>
Your vehicles all reach their destination at the same time because you are changing their position based on a percentage. So when pct==1.00 all vehicles arrive simultaneously at their own endpoints regardless of the distance they need to travel to get there.
// increment the percent (from 0.00 to 1.00)
pct += .01;
To make a vehicle arrive based on distance traveled
Question#1: You can calculate each waypoint (waypoint==unique pixel) the vehicle must travel to complete it's route. Advance the vehicle to it's next waypoint with each new animation frame. This causes each vehicle to arrive based on the length of their route rather than a uniform percentage.
Question#2: For each vehicle, if you pre-calculate & save its waypoints into an array, you can easily get 500 vehicles drawn on the canvas during each animation frame.
Here's annotated and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
ctx.lineWidth=2;
// define routes
var routes=[];
routes.push({
points:linePoints({x:10,y:10},{x:150,y:10}),
currentPoint:0,
color:'red',
});
routes.push({
points:linePoints({x:10,y:50},{x:250,y:65}),
currentPoint:0,
color:'green',
});
routes.push({
points:linePoints({x:10,y:90},{x:325,y:105}),
currentPoint:0,
color:'blue',
});
// animation related vars
var lastTime=0;
var delay=1000/60*5;
// start animating
requestAnimationFrame(animate);
function animate(time){
// return if the desired time hasn't elapsed
if(time<lastTime){requestAnimationFrame(animate);return;}
// redraw each route
ctx.clearRect(0,0,cw,ch);
// var used to stop animating if all routes are completed
var isComplete=true;
for(var i=0;i<routes.length;i++){
var r=routes[i];
// increase the currentPoint, but not beyond points.length-1
if((r.currentPoint+1)<r.points.length-1){
isComplete=false;
r.currentPoint++;
}
// draw the route to its current point
ctx.strokeStyle=r.color;
ctx.beginPath();
ctx.moveTo(r.points[0].x,r.points[0].y);
ctx.lineTo(r.points[r.currentPoint].x,r.points[r.currentPoint].y);
ctx.stroke();
ctx.fillStyle=r.color;
ctx.beginPath();
ctx.arc(r.points[r.currentPoint].x,r.points[r.currentPoint].y,5,0,Math.PI*2);
ctx.fill();
}
// request another frame unless all routes are completed
if(!isComplete){
requestAnimationFrame(animate);
}
}
function linePoints(p1,p2){
// start building a points array with the starting point
var points=[p1];
var dx=p2.x-p1.x;
var dy=p2.y-p1.y;
var count=Math.sqrt(dx*dx+dy*dy)*3;
for(var pct=0;pct<count;pct++){
// calc next waypoint on the line
var x=p1.x+dx*pct/count;
var y=p1.y+dy*pct/count;
var lastPt=points[points.length-1];
// add new waypoint if the its integer pixel value has
// changed from last waypoint
if(parseInt(x)!==parseInt(lastPt.x) || parseInt(y)!==parseInt(lastPt.y)){
points.push({x:x,y:y});
}
}
// force the last point to be the ending point
points[points.length-1]=p2;
// return a unique points[] forming a line from p1 to p2
return(points);
}
body{ background-color: ivory; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=350 height=300></canvas>
To make your pods go the same speed you will probably want to use the Pythagorean theorem. Hypotenuse is the distance you want the node to travel each time the rAF comes around. Then calculate your x's and y's from that.
I'm not quite sure if I understand what pct does.
To speed up the rAF you'll want to:
kill your set timeout
prerender
Prerending is a bit more work but instead of drawing each circle each and every time you have canvases that aren't in the DOM that you draw to. Then you essentially lay the 'hidden' canvas where ever you want on top of the visible DOM canvas. It keeps the drawing in memory this way.
You also draw each circle on the canvas at the end of the for-loop. Pull the fill method outside of it this way the canvas can batch draw instead of a bunch of little calls (this can really kill performance).
Setting font stuff each time can be removed.
Canvas is awesome for performance but you just have to be careful because one small mistake can lead to a huge bottle-neck.
This is a good article: http://www.html5rocks.com/en/tutorials/canvas/performance/
Lemme know if you have any more questions.
I have a black canvas with things being drawn inside it. I want the things drawn inside to fade to black, over time, in the order at which they are drawn (FIFO). This works if I use a canvas which hasn't been resized. When the canvas is resized, the elements fade to an off-white.
Question: Why don't the white specks fade completely to black when the canvas has been resized? How can I get them to fade to black in the same way that they do when I haven't resized the canvas?
Here's some code which demonstrates. http://jsfiddle.net/6VvbQ/35/
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 150),
2, 2);
context.fillStyle = 'rgba(0,0,0,.02)';
context.fillRect(0, 0, 300, 150);
setTimeout('draw()', 1000 / 20);
}
setTimeout('draw()', 1000 / 20);
The problem is two-parted:
There is a (rather known) rounding error when you draw with low alpha value. The browser will never be able to get the resulting mix of the color and alpha channel equal to 0 as the resulting float value that is mixed will be converted to integer at the time of drawing which means the value will never become lower than 1. Next time it mixes it (value 1, as alpha internally is a value between 0 and 255) will use this value again and it get rounded to again to 1, and forever it goes.
Why it works when you have a resized canvas - in this case it is because you are drawing only half the big canvas to the smaller which result in the pixels being interpolated. As the value is very low this means in this case the pixel will turn "black" (fully transparent) as the average between the surrounding pixels will result in the value being rounded to 0 - sort of the opposite than with #1.
To get around this you will manually have to clear the spec when it is expected to be black. This will involve tracking each particle/spec yourselves or change the alpha using direct pixel manipulation.
Update:
The key is to use tracking. You can do this by creating each spec as a self-updating point which keeps track of alpha and clearing.
Online demo here
A simple spec object can look like this:
function Spec(ctx, speed) {
var me = this;
reset(); /// initialize object
this.update = function() {
ctx.clearRect(me.x, me.y, 1, 1); /// clear previous drawing
this.alpha -= speed; /// update alpha
if (this.alpha <= 0) reset(); /// if black then reset again
/// draw the spec
ctx.fillStyle = 'rgba(255,255,255,' + me.alpha + ')';
ctx.fillRect(me.x, me.y, 1, 1);
}
function reset() {
me.x = (ctx.canvas.width * Math.random())|0; /// random x rounded to int
me.y = (ctx.canvas.height * Math.random())|0; /// random y rounded to int
if (me.alpha) { /// reset alpha
me.alpha = 1.0; /// set to 1 if existed
} else {
me.alpha = Math.random(); /// use random if not
}
}
}
Rounding the x and y to integer values saves us a little when we need to clear the spec as we won't run into sub-pixels. Otherwise you would need to clear the area around the spec as well.
The next step then is to generate a number of points:
/// create 100 specs with random speed
var i = 100, specs = [];
while(i--) {
specs.push(new Spec(ctx, Math.random() * 0.015 + 0.005));
}
Instead of messing with FPS you simply use the speed which can be set individually per spec.
Now it's simply a matter of updating each object in a loop:
function loop() {
/// iterate each object
var i = specs.length - 1;
while(i--) {
specs[i].update(); /// update each object
}
requestAnimationFrame(loop); /// loop synced to monitor
}
As you can see performance is not an issue and there is no residue left. Hope this helps.
I don't know if i have undertand you well but looking at you fiddle i think that, for what you are looking for, you need to provide the size of the canvas in any iteration of the loop. If not then you are just taking the initial values:
EDIT
You can do it if you apply a threshold filter to the canvas. You can run the filter every second only just so the prefromanece is not hit so hard.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0,0,300,150);
//context.globalAlpha=1;
//context.globalCompositeOperation = "source-over";
var canvas2 = document.getElementById('canvas2');
var context2 = canvas2.getContext('2d');
canvas2.width=canvas2.height=canvas.width;
window.draw = function(){
var W = canvas2.width;
var H = canvas2.height;
context2.fillStyle='rgba(255,255,255,1)';
context2.fillRect(
Math.floor(Math.random()*W),
Math.floor(Math.random()*H),
2,2);
context2.fillStyle='rgba(0,0,0,.02)';
context2.fillRect(0,0,W,H);
context.fillStyle='rgba(0,0,0,1)';
context.fillRect(0,0,300,150);
context.drawImage(canvas2,0,0,300,150);
setTimeout('draw()', 1000/20);
}
setTimeout('draw()', 1000/20);
window.thresholdFilter = function () {
var W = canvas2.width;
var H = canvas2.height;
var i, j, threshold = 30, rgb = []
, imgData=context2.getImageData(0,0,W,H), Npixels = imgData.data.length;
for (i = 0; i < Npixels; i += 4) {
rgb[0] = imgData.data[i];
rgb[1] = imgData.data[i+1];
rgb[2] = imgData.data[i+2];
if ( rgb[0] < threshold &&
rgb[1] < threshold &&
rgb[2] < threshold
) {
imgData.data[i] = 0;
imgData.data[i+1] = 0;
imgData.data[i+2] = 0;
}
}
context2.putImageData(imgData,0,0);
};
setInterval("thresholdFilter()", 1000);
Here is the fiddle: http://jsfiddle.net/siliconball/2VaLb/4/
To avoid the rounding problem you could extract the fade effect to a separate function with its own timer, using longer refresh interval and larger alpha value.
var canvas = document.getElementById('canvas');
var context = canvas.getContext('2d');
context.fillRect(0, 0, 300, 150);
// Comment this out and it works as intended, why?
canvas.width = canvas.height = 300;
window.draw = function () {
context.fillStyle = 'rgba(255,255,255,1)';
context.fillRect(
Math.floor(Math.random() * 300),
Math.floor(Math.random() * 300),
2, 2);
setTimeout('draw()', 1000 / 20);
}
window.fadeToBlack = function () {
context.fillStyle = 'rgba(0,0,0,.1)';
context.fillRect(0, 0, 300, 300);
setTimeout('fadeToBlack()', 1000 / 4);
}
draw();
fadeToBlack();
Fiddle demonstrating this: http://jsfiddle.net/6VvbQ/37/