fabricjs - creating endless animation loop without lags - javascript

I am trying to create an endless animation loop of rotation with FabricJS. It works like this: the rotate function gets animation details (object to animate, duration of one animation loop, starting angle of rotation) as parameters, and invokes inner function rotateInner to launch the animation itself. rotateInner rotates given object using fabric.util.animate with linear easing function (to make rotation speed constant) to run one animation loop and to invoke itself after the loop ends, which results in an infinite animation loop.
The problem is that rotateInner lags every time between animation loops when it invokes itself through fabric.util.animate.
For example, I use animation loop with duration = 1 second and rotation angle = 180 degrees. In that case, animation makes lag every 1 second after rotating by 180 degrees.
I've found a solution to make lag occur N times less. Instead of rotating by 180 degrees for 1 second, I rotate object by 180*N degrees for N seconds. If I pick enough big N, the user will meet lag potentially never. But only potentially.
What are reasons of lags and is it possible to remove lags between animation loops completely? Or, perhaps, I went wrong way, and I should use something completely different to create endless animation loop?
Here's example which demonstrates lag (on jsfiddle). (The rotate function has additional argument - multiplier. It's N I've just wrote about above) Left rectangle rotates 180 degrees per 800 milliseconds, and the right rectangle rotates 180*10 degrees per 800*10 milliseconds, and thus the right one rotates quicker than the left one.
And, if you don't want to use jsfiddle or if example became inaccessible for some reason, here's the example itself in code:
<!DOCTYPE html>
<html>
<head>
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.4.0/fabric.min.js"></script>
<style>canvas {border: 5px dotted black;}</style>
<title>FabricJS animation loop lag</title>
</head>
<body>
<canvas id="mycanvas" width="700" height="300"></canvas>
<script>
var canvas = new fabric.StaticCanvas('mycanvas');
var rectCreationOptions = {
left: canvas.getWidth()/4,
top: canvas.getHeight()/2,
fill: '#0bb',
width: canvas.getWidth()/5,
height: canvas.getHeight()/5,
originX: 'center',
originY: 'center'
};
// creating left rectangle:
var rect1 = new fabric.Rect(rectCreationOptions);
rectCreationOptions.left *= 3;
// creating right rectangle:
var rect2 = new fabric.Rect(rectCreationOptions);
canvas.add(rect1);
canvas.add(rect2);
function rotate(animatedObject, duration = 1000, multiplier = 1, startAngle = 0) {
duration *= multiplier;
var addAngle = 180 * multiplier;
(function rotateInner(startAngle) {
fabric.util.animate({
startValue: startAngle,
endValue: startAngle+addAngle,
duration: duration,
// linear easing function:
easing: function(t, b, c, d) { return c*t/d + b; },
onChange: function (value) {
animatedObject.angle = value;
canvas.renderAll();
},
onComplete: function() {
rotateInner(startAngle+addAngle);
}
});
})(startAngle);
}
rotate(rect1, 800, 1); // lags every 800 ms
rotate(rect2, 800, 10); // lags every 800*10 ms
</script>
</body>
</html>
I will be thankful for help.
Please, don't throw rotten tomatoes at me if I made tiny mistake. This is my first question on stackoverflow. :)
Best wishes everyone.

Related

Ratio smoothing with delta time in JavaScript is buggy

Smoothing normally works as follows:
Some variable v has a target t. Every tick, v makes progress toward t based on a ratio. For example, a ratio of 2:3 or ⅗ will use this formula:
v=(v*3+t*2)/5;
Here you multiply the variables to make the ratio, and then divide it all up to make the smoothing ratio. t can be modified as you want and v will handle smoothing and output.
So you have delta time. You pick a target framerate and go from there. I figured it out on my own so I might be wrong about some terminology but whatever. In one frame, the smoothing will have progressed by ⅖ which means you have a remainder of ⅗. In two frames, you will have a remainder of ⅗×⅗. And I thought: how could I do that fractionally, though? And then I was like "oh wait, you can just make powers of these." So I just got the idea to use insertRatioHere**deltaTime and multiply it by the difference between the current and target state. At first it was something like
v=t-v+(v*(0.6**delta))
but that didn't really work, so I tried a lot of different formulas with small changes, none of which worked. Here's my current test code:
<HTML>
<head>
<title>delta</title>
</head>
<body>
<canvas id=myCanvas width=400 height=300 style="background-color:white;"></canvas>
<script>
var ctx=myCanvas.getContext("2d");
var time={first:new Date(),now:new Date(),delta:new Date()};
time.since=time.now-time.first;
time.delta=time.delta.getTime();
const frame=1000/60;
var lag=100;//change between 0, frame, 100 and other values when desired
var boxA={xPos:200,yPos:150,xAccel:0,yAccel:0,xSpeed:0,ySpeed:0,speed:0};
var boxB={xPos:200,yPos:150,xSpeed:0,ySpeed:0,speed:0};
var keysDown=[];
var keyBind={upA:{key:87,on:false},leftA:{key:65,on:false},
downA:{key:83,on:false},rightA:{key:68,on:false},
upB:{key:38,on:false},leftB:{key:37,on:false},
downB:{key:40,on:false},rightB:{key:39,on:false}};
var varLog=[];
addEventListener("keydown",whenKeyDown);
addEventListener("keyup",whenKeyUp);
tick=setInterval(tickBoxA,lag);
tick=setInterval(tickBoxB,frame);
function tickBoxA()
{time.now=new Date();
time.delta=time.now-time.first-time.since;
time.since=time.now-time.first;
if(keysDown.includes(keyBind.upA.key)){
if(!keyBind.upA.on){keyBind.upA.on=true}
}else{keyBind.upA.on=false}
if(keysDown.includes(keyBind.leftA.key)){
if(!keyBind.leftA.on){keyBind.leftA.on=true}
}else{keyBind.leftA.on=false}
if(keysDown.includes(keyBind.downA.key)){
if(!keyBind.downA.on){keyBind.downA.on=true}
}else{keyBind.downA.on=false}
if(keysDown.includes(keyBind.rightA.key)){
if(!keyBind.rightA.on){keyBind.rightA.on=true}
}else{keyBind.rightA.on=false}
boxA.xAccel=(keyBind.rightA.on-keyBind.leftA.on)*3*((keyBind.upA.on-keyBind.downA.on!=0)?Math.sin(45):1);
boxA.xSpeed=boxA.xAccel-boxA.xSpeed+(boxA.xSpeed*0.9**(time.delta/frame));
//boxA.xSpeed=boxA.xAccel+boxA.xSpeed-(boxA.xSpeed*0.9**(time.delta/frame));
//boxA.xSpeed=boxA.xAccel+(boxA.xSpeed-boxA.xAccel*0.9**(time.delta/frame));
//boxA.xSpeed=boxA.xAccel+(boxA.xAccel-boxA.xSpeed*0.9**(time.delta/frame));
boxA.yAccel=(keyBind.downA.on-keyBind.upA.on)*3*((keyBind.rightA.on-keyBind.leftA.on!=0)?Math.sin(45):1);
boxA.ySpeed=boxA.yAccel-boxA.ySpeed+(boxA.ySpeed*0.9**(time.delta/frame));
boxA.speed=Math.sqrt((boxA.xSpeed**2)+(boxA.ySpeed**2));
boxA.xPos+=boxA.xSpeed*(time.delta/frame);
boxA.yPos+=boxA.ySpeed*(time.delta/frame);
render()}
function tickBoxB()
{if(keysDown.includes(keyBind.upB.key)){
if(!keyBind.upB.on){keyBind.upB.on=true}
}else{keyBind.upB.on=false}
if(keysDown.includes(keyBind.leftB.key)){
if(!keyBind.leftB.on){keyBind.leftB.on=true}
}else{keyBind.leftB.on=false}
if(keysDown.includes(keyBind.downB.key)){
if(!keyBind.downB.on){keyBind.downB.on=true}
}else{keyBind.downB.on=false}
if(keysDown.includes(keyBind.rightB.key)){
if(!keyBind.rightB.on){keyBind.rightB.on=true}
}else{keyBind.rightB.on=false}
boxB.xSpeed=(boxB.xSpeed*9+(keyBind.rightB.on-keyBind.leftB.on)*3*((keyBind.upB.on-keyBind.downB.on!=0)?Math.sin(45):1))/10;
boxB.ySpeed=(boxB.ySpeed*9+(keyBind.downB.on-keyBind.upB.on)*3*((keyBind.rightB.on-keyBind.leftB.on!=0)?Math.sin(45):1))/10;
boxB.xPos+=boxB.xSpeed;
boxB.yPos+=boxB.ySpeed;
render()}
function whenKeyDown(keyDownEvent){
check=keyDownEvent.keyCode;
if(keysDown.findIndex(isSame)==-1)
{keysDown.push(keyDownEvent.keyCode)}}
function whenKeyUp(keyUpEvent){
check=keyUpEvent.keyCode;
keysDown.splice(keysDown.findIndex(isSame),1)}
function isSame(num){return num==check}
function render()
{ctx.clearRect(0,0,400,300);
ctx.fillStyle="#F00";
ctx.fillRect(boxA.xPos,boxA.yPos,20,20);
ctx.fillStyle="#0F0";
ctx.fillRect(boxB.xPos,boxB.yPos,20,20);
varLog.length=0;
varLog.push("Time: "+time.since);
varLog.push("Delta: "+time.delta);
varLog.push("Frame accuracy: "+time.delta/frame);
varLog.push("Standard framerate is 60FPS");
varLog.push("Red box target X: "+boxA.xAccel);
varLog.push("Red box current X: "+boxA.xSpeed);
varLog.push("Red box target Y: "+boxA.yAccel);
varLog.push("Red box current Y: "+boxA.ySpeed);
varLog.push("Red box speed: "+boxA.speed);
varLog.push("The red box is trying to handle smooth");
varLog.push("acceleration in delta time");
varLog.push("and yeah I messed up");
varLog.push("Green box speed X: "+boxB.xSpeed);
varLog.push("Green box speed Y: "+boxB.ySpeed);
varLog.push("Green box speed: "+boxB.speed);
varLog.push("The green box works as desired at 60FPS");
varLog.push("Keys pressed: "+keysDown);
varLog.push("Red=WASD, green=↑←↓→");
ctx.font="15px Courier";
ctx.fillStyle="#000";
for(run=0;run<varLog.length;run++){ctx.fillText(varLog[run],0,(run+1)*15)}}
</script>
</body>
</html>

orbitcontrols.js: How to Zoom In/ Zoom Out

Using orbitcontrols.js (with THREE.js), I want to achieve the same effect in code as rotating the mouse wheel. For example, I want to call something like camera.zoomIn() and have it move a set distance toward the target. Anyone know how to do this?
At least for simple cases, you can change the position of the camera (e.g. multiply x, y and z by a factor), which automagically updates the OrbitControls.
Example with a <input type="range"> slider:
zoomer.addEventListener('input', function(e) {
var zoomDistance = Number(zoomer.value),
currDistance = camera.position.length(),
factor = zoomDistance/currDistance;
camera.position.x *= factor;
camera.position.y *= factor;
camera.position.z *= factor;
});
https://codepen.io/Sphinxxxx/pen/yPZQMV
From looking at the source code you can call .dollyIn(dollyScale) and .dollyOut(dollyScale) on the OrbitalControls object.
Edit: these aren't public methods; one can access them by editing OrbitalControls.js though.
I added this.dIn = dollyIn; to the THREE.OrbitControls function, I can now zoom in by calling controls.dIn(1.05); controls.update(); from outside.
You can set values on OrbitControls.maxDistance and OrbitControls.minDistance to zoom in and out programmatically.
For example, if you wanted to animate a zoom out with gsap you could do something like this:
gsap.to(myOrbitControls, {
minDistance: 20, // target min distance
duration: 1,
overwrite: 'auto',
ease: 'power1.inOut',
onComplete: () => {
myOrbitControls.minDistance = 0 // reset to initial min distance
},
})

Buggy canvas animation on click with JavaScript

I'm trying to run a simple animation each time when user clicks on canvas. I'm sure I did something wrong as the animation doesn't even fire at times. I have never used canvas animation before and have difficulty understanding how it should be constructed within a for loop.
fgCanvas.on('mousedown', function(e) {
var cX = Math.round((e.offsetX - m) / gS),
cY = Math.round((e.offsetY - m) / gS);
clickDot({x:cX,y:cY});
});
function clickDot(data) {
for (var i = 1; i < 100; i++) {
fctx.clearRect(0, 0, pW, pH);
fctx.beginPath();
fctx.arc(data.x * gS + m, data.y * gS + m, i/10, 0, Math.PI * 2);
fctx.strokeStyle = 'rgba(255,255,255,' + i/10 + ')';
fctx.stroke();
}
requestAnimationFrame(clickDot);
}
Full code is here: http://jsfiddle.net/3Nk4A/
The other question is how can slow down the animation or add some easing, so the rings are drawn slower towards the end when they disappear?
You can use requestAnimationFrame plus easing functions to create your desired effect:
A Demo: http://jsfiddle.net/m1erickson/cevGf/
requestAnimationFrame creates an animation loop by itself--so there's no need to use a for-loop inside requestAnimationFrame's animation loop.
In its simplest form, this requestAnimationFrame loop will animate your circle:
var counter=1;
animate();
function animate(){
// stop the animation after it has run 100 times
if(counter>100){return;}
// there's more animating to do, so request another loop
requestAnimationFrame(animate);
// calc the circle radius
var radius=counter/10;
// draw your circle
}
To get the animation to speed-up or slow-down, you can use easings. Easings change a value (like your radius) over time, but they change that value unevenly. Easings speed-up and slow-down over the duration of the animation.
Robert Penner made a great set of easing algorithms. Dan Rogers coded them in javascript:
https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
You can see working examples of his easing functions here:
http://easings.net/
Here's annotated code using requestAnimationFrame plus easings to animate your circles.
<!doctype html>
<html>
<head>
<link rel="stylesheet" type="text/css" media="all" href="css/reset.css" /> <!-- reset css -->
<script type="text/javascript" src="http://code.jquery.com/jquery.min.js"></script>
<style>
#canvas{border:1px solid red;}
</style>
<script>
$(function(){
// canvas related variables
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var scrollX=$canvas.scrollLeft();
var scrollY=$canvas.scrollTop();
// set the context styles
ctx.lineWidth=1;
ctx.strokeStyle="gold";
ctx.fillStyle="#888";
// variables used to draw & animate the ring
var PI2=Math.PI*2;
var ringX,ringY,ringRadius,ingCounter,ringCounterVelocity;
// fill the canvas with a background color
ctx.fillRect(0,0,canvas.width,canvas.height);
// tell handleMouseDown to handle all mousedown events
$("#canvas").mousedown(function(e){handleMouseDown(e);});
// set the ring variables and start the animation
function ring(x,y){
ringX=x;
ringY=y;
ringRadius=0;
ringCounter=0;
ringCounterVelocity=4;
requestAnimationFrame(animate);
}
// the animation loop
function animate(){
// return if the animation is complete
if(ringCounter>200){return;}
// otherwise request another animation loop
requestAnimationFrame(animate);
// ringCounter<100 means the ring is expanding
// ringCounter>=100 means the ring is shrinking
if(ringCounter<100){
// expand the ring using easeInCubic easing
ringRadius=easeInCubic(ringCounter,0,15,100);
}else{
// shrink the ring using easeOutCubic easing
ringRadius=easeOutCubic(ringCounter-100,15,-15,100);
}
// draw the ring at the radius set using the easing functions
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.beginPath();
ctx.arc(ringX,ringY,ringRadius,0,PI2);
ctx.closePath();
ctx.stroke();
// increment the ringCounter for the next loop
ringCounter+=ringCounterVelocity;
}
// Robert Penner's easing functions coded by Dan Rogers
//
// https://github.com/danro/jquery-easing/blob/master/jquery.easing.js
//
// now=elapsed time,
// startValue=value at start of easing,
// deltaValue=amount the value will change during the easing,
// duration=total time for easing
function easeInCubic(now, startValue, deltaValue, duration) {
return deltaValue*(now/=duration)*now*now + startValue;
}
function easeOutCubic(now, startValue, deltaValue, duration) {
return deltaValue*((now=now/duration-1)*now*now + 1) + startValue;
}
// handle mousedown events
function handleMouseDown(e){
// tell the browser we'll handle this event
e.preventDefault();
e.stopPropagation();
// calc the mouse position
mouseX=parseInt(e.clientX-offsetX);
mouseY=parseInt(e.clientY-offsetY);
// animate a ring at the mouse position
ring(mouseX,mouseY);
}
}); // end $(function(){});
</script>
</head>
<body>
<h4>Click in the canvas to draw animated circle with easings.</h4>
<canvas id="canvas" width=300 height=300></canvas>
</body>
</html>

Plotting with HTML5 Canvas

I decided to day to embark on element and I can say so far it have been nightmare to get it work. All I want is to plot a sine graph. So after good reading I still cannot either get origins nor get it plot. Below is what I have tried (my first time ever with that tag so excuse my ignorance). What makes me wonder is the guy here have it but the codes are hard to understand for beginner like me.
HTML
<!DOCTYPE html>
<html>
<head>
<title>Graphing</title>
<link type="text/css" rel="Stylesheet" href="graph.css" />
<script type="text/JavaScript" src="graph.js" ></script>
</head>
<body>
<canvas id="surface">Canvas not Supported</canvas>
</body>
</html>
CSS
#surface
{
width:300;
height:225;
border: dotted #FF0000 1px;
}
JavScript
window.onload = function()
{
var canvas = document.getElementById("surface");
var context = canvas.getContext("2d");
arr = [0,15, 30,45,60, 90,105, 120, 135, 150, 165, 180 ];
var x=0;
var y = 0;
for(i=0; i<arr.length; i++)
{
angle = arr[i]*(Math.PI/180); //radians
sine = Math.sin(angle);
context.moveTo(x,y);
context.lineTo(angle,sine);
context.stroke();
//set current varibles for next move
x = angle;
y = sine;
}
}
Since the range of sin x is [-1,1], it will only return numbers between -1 and 1, and that means all you will be drawing is a dot on the screen.
Also I see that you have an array ranging from 0 to 180. I believe you are trying to draw the curve with x from 0 degree to 180 degree? You don't really need to do this (anyway 12 points are not enough to draw a smooth line). Just do it with a for loop, with lines being the number of fragments.
First we start off by moving the point to the left of the canvas:
context.moveTo(0, 100 /*somewhere in the middle*/); //initial point
In most cases the first point won't be in the middle. But for sine it is. (You might want to fix it later though.)
for (var i = 0; i < lines; i++) {
//draw line
}
That's the loop drawing the curve. But what should we put inside? Well you can just take the number returned by the sine function and scale it up, flip it upside down, and shift it down half the way. I do that because the coordinate system in JavaScript is 0,0 in the top left instead of in the bottom left.
var sine = Math.sin(i/scale*2)*scale;
context.lineTo(i*frag, -sine+scale);
//i * frag = the position of x scaled up
//-sine + scale = the position of y, flipped, scaled, shifted down
//i/scale*2 = random scale I put in... you might want to figure out the
// correct scale with some math
So that's it. Viola, you have successfully plotted a graph in JavaScript.
Oh yes, don't forget to actually tell it to draw it on the canvas after the for loop has done its job:
context.stroke();
The demo: http://jsfiddle.net/DerekL/hK5rC/
PS: I see that you are trying to resize the canvas using CSS. Trust me, it won't work. :) You will have to define the dimension in HTML.

How to Optimize js animation with requestAnimationFrame and introduce delta?

I have a basic animation of a ball falling with a gradually increasing velocity.
DEMO
It is somewhat working as expected with an expected velocity initially. So on click on the canvas we can repeat the animation , so each time we can see the ball speed keeps increasing. After 5-6 click the ball is moving with high velocity.
So is the issue is with frame rate , and expected unit move per frame. ?
1. How to reduce/optimize the loop call ? , and get a constant velocity in each click.
I wont prefer to change the value inside Ball model, but the time of ball.update() must be reduced.
var ball = new Ball(10,10,20);
function animate() {
ctx.fillStyle = "#CCC";
ctx.fillStyle = "#CCC";
ctx.fillRect(0, 0, 500, 500);
ball.update();
ball.draw(ctx);
requestAnimationFrame(animate); // add timestamp to optimize ?
}
and on click the reset animation is like this
$("#element").on('click', function () {
ball = new Ball(10,10,20)
requestAnimationFrame(animate);
});
and 2. Why it is working with expected speed initialy ? and I prefer to clear the animation after the ball moves out of canvas.
The main reason is that you are combining setTimeout with requestAnimationFrame which kind of defeat its purpose.
Not only are your rAF triggered by an inaccurate timer but when rAF kicks in it is not sure you will get a frame for that round which can produce jerky result.
Change these lines:
setTimeout(function () {
animationId = requestAnimationFrame(animate);
}, 1000/60);
to
//setTimeout(function () {
animationId = requestAnimationFrame(animate);
//}, 1000/60);
Then reset your ball as with Greg's answer. There is no need to call rAF again as it is already running:
$(function(){
requestAnimationFrame(animate);
$("#element").on('click', function () {
ball = new Ball(10,10,20);
});
});
Modified fiddle
You could also reset the ball by updating its properties.
Here's a fork of your fiddle that works: http://jsfiddle.net/8egrw/
Comment out this line:
$("#element").on('click', function () {
ball = new Ball(10,10,20)
//animationId = requestAnimationFrame(animate);
});

Categories

Resources