I have a class that takes some coordinate and duration data. I want to use it to animate an svg. In more explicit terms, I want to use that data to change svg attributes over a time frame.
I'm using a step function and requestAnimationFrame outside the class:
function step(timestamp) {
if (!start) start = timestamp
var progress = timestamp - start;
var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
var moveX = distancePerFrame(circleMove.totalFrames(), circleMove.xLine);
document.querySelector('#start').setAttribute('cx', currentX + moveX);
if (progress < circleMove.duration) {
window.requestAnimationFrame(step);
}
}
var circleMove = new SingleLineAnimation(3000, startXY, endXY)
var start = null
function runProgram() {
window.requestAnimationFrame(step);
}
I can make it a method, replacing the circleLine with this. That works fine for the first run through, but when it calls the this.step callback a second time, well, we're in a callback black hole and the reference to this is broken. Doing the old self = this won't work either, once we jump into the callback this is undefined(I'm not sure why). Here it is as a method:
step(timestamp) {
var self = this;
if (!start) start = timestamp
var progress = timestamp - start;
var currentX = parseInt(document.querySelector('#start').getAttribute('cx'));
var moveX = distancePerFrame(self.totalFrames(), self.xLine);
document.querySelector('#start').setAttribute('cx', currentX + moveX);
if (progress < self.duration) {
window.requestAnimationFrame(self.step);
}
}
Any ideas on how to keep the "wiring" inside the Object?
Here's the code that more or less works with the step function defined outside the class.
class SingleLineAnimation {
constructor(duration, startXY, endXY) {
this.duration = duration;
this.xLine = [ startXY[0], endXY[0] ];
this.yLine = [ startXY[1], endXY[1] ];
}
totalFrames(framerate = 60) { // Default to 60htz ie, 60 frames per second
return Math.floor(this.duration * framerate / 1000);
}
frame(progress) {
return this.totalFrames() - Math.floor((this.duration - progress) / 17 );
}
}
This will also be inserted into the Class, for now it's just a helper function:
function distancePerFrame(totalFrames, startEndPoints) {
return totalFrames > 0 ? Math.floor(Math.abs(startEndPoints[0] - startEndPoints[1]) / totalFrames) : 0;
}
And click a button to...
function runProgram() {
window.requestAnimationFrame(step);
}
You need to bind the requestAnimationFrame callback function to a context. The canonical way of doing this is like this:
window.requestAnimationFrame(this.step.bind(this))
but it's not ideal because you're repeatedly calling .bind and creating a new function reference over and over, once per frame.
If you had a locally scoped variable set to this.step.bind(this) you could pass that and avoid that continual rebinding.
An alternative is this:
function animate() {
var start = performance.now();
el = document.querySelector('#start');
// use var self = this if you need to refer to `this` inside `frame()`
function frame(timestamp) {
var progress = timestamp - start;
var currentX = parseInt(el.getAttribute('cx'));
var moveX = distancePerFrame(circleMove.totalFrames(), circleMove.xLine);
el.setAttribute('cx', currentX + moveX);
if (progress < circleMove.duration) {
window.requestAnimationFrame(frame);
}
}
window.requestAnimationFrame(frame);
}
i.e. you're setting up the initial state, and then doing the animation within a purely locally scoped function that's called pseudo-recursively by requestAnimationFrame.
NB: either version of the code will interact badly if you inadvertently call another function that initiates an animation at the same time.
Related
Hi I'm making a pomodoro clock. I want to allow the timer to increase or decrease every 100 milliseconds when the user holds down the button. The running conditions for mousedown and clickUpdate are very similar.
The entire code of clickUpdate relies on using this keyword to achieve that goal. But how can I let setInterval inherit or have access to this keyword? This referring to the button object that mousedown is a method of.
https://codepen.io/jenlky/pen/ypQjPa?editors=0010
var timer;
const session = document.getElementById("session");
const breaktime = document.getElementById("break");
const mins = document.getElementById("mins");
const secs = document.getElementById("secs");
function clickUpdate () {
// if data-action = increase and its under session-input, increase session.value else increase breaktime.value
if (this.dataset.action === "increase") {
if (this.parentElement.className === "session-input") {
// if session.value is 60 mins, this increase click changes it to 1 min
if (session.value === "60") {
session.value = 1;
} else {
session.value = Number(session.value) + 1;
}
mins.innerText = session.value;
// if breaktime.value is 60 mins, this increase click changes it to 1 min
} else {
if (breaktime.value === "60") {
breaktime.value = 1;
} else {
breaktime.value = Number(breaktime.value) + 1;
}
}
}
// if data-action = decrease and its under session-input, decrease session.value else decrease breaktime.value
if (this.dataset.action === "decrease") {
if (this.parentElement.className === "session-input") {
// if session.value is 1 min, this decrease click changes it to 60 mins
if (session.value === "1") {
session.value = 60;
} else {
session.value = Number(session.value) - 1;
}
mins.innerText = session.value;
// if breaktime.value is 1 min, this decrease click changes it to 60 mins
} else {
if (breaktime.value === "1") {
breaktime.value = 60;
} else {
breaktime.value = Number(breaktime.value) - 1;
}
}
}
console.log(this);
}
// Problem is how can I let clickUpdate or setInterval(function(){},100) inherit this
// setInterval's function doesn't seem to inherit or have any parameters
// I'm not sure how forEach thisArg parameter works, or how to use bind, or how to use addEventListener last parameter
function mousedown() {
var obj = this;
timer = setInterval(clickUpdate, 100);
}
function mouseup() {
if (timer) {
clearInterval(timer);
}
}
const buttons = Array.from(document.getElementsByClassName("symbol"));
mins.innerText = session.value;
//buttons.forEach(button => button.addEventListener("click", clickUpdate));
buttons.forEach(button => button.addEventListener("mousedown", mousedown));
buttons.forEach(button => button.addEventListener("mouseup", mouseup));
console.log(session);
The this value within functions defined using function () ... is usually dependent on how the function is called. If you call a.myFunction() then this within the function will be a reference to a. If you call myFunction(), then this will either be undefined or the global object depending on whether you are using strict or sloppy mode.
Usually the most straightforward way to get a callback function to use a particular this value is to use .bind(n). This basically creates a wrapped version of the original function, with the this value locked in as n
setInterval(clickUpdate.bind(this), 100);
Then again, my preference would be to not use this at all. It's confusing and has wacky behavior that often requires all sorts of contrived workarounds (as you have experienced). You could just as easily pass in the value as a parameter here:
function mousedown(e) {
timer = setInterval(clickUpdate, 100, e.target);
}
One trick is to create a separate reference (either global or at least on a common ancestor scope) holding the reference to the "this" reference from the level you want.
Once you do that, you can refer to it within the setInterval function.
Example:
var scope = this;
this.name = 'test';
setInterval(function() {
console.log(scope.name); // should print 'test' every 1 second
},1000);
For the sake of learning I am prototyping an animate function for all HTMLElements, inspired by jQuery. The animation starts up just fine, but I want it to stop after the requestAnimationFrame's time = the duration given in the function. I am using cancelAnimationFrame inside the animation loop, but it doesn't stop the loop.
HTMLElement.prototype.animate = function(properties,duration){
for(prop in properties){
var last = 0,
fps = 60;
function ani(time){
requestAnimationFrame(ani);
if ((time - last) > fps ){
last = time
console.log(time)
if(time >= (duration*1000)){
window.cancelAnimationFrame(aniLoop)
}
}
}
var aniLoop = requestAnimationFrame(ani)
}
}
The function is called like this
c.animate({"property":"value"},1)
At the core of the problem lies that fact that you're only getting the ID of the first animation frame (the var aniLoop = (...) line) and that's what you're trying to cancel - except that each call to requestAnimationFrame has a different ID, thus you'd need to store the return value of the last call and cancel that instead:
HTMLElement.prototype.animate = function(properties,duration) {
"use strict";
var aniLoop,
prop,
last = 0,
fps = 60;
for (prop in properties) {
function ani(time) {
aniLoop = requestAnimationFrame(ani);
if ((time - last) > fps) {
last = time;
console.log(time);
if (time >= (duration * 1000)) {
cancelAnimationFrame(aniLoop);
}
}
}
aniLoop = requestAnimationFrame(ani);
}
};
There are, however, several other problems with your code that need to be tackled as well, otherwise your function will blow up rather thoroughly:
:1 Function declaration in a loop
I would recommend reading about differences between function declaration and expression to get a better picture, but the problem here is that you're doing function declaration in a loop, which is considered undefined behaviour (some engines will replace the functions, some will not, some will blow up). Given that the animations have only single duration given and thus, are probably synchronised, it'd be a better option to iterate over properties to animate inside of a single animation function, like so:
HTMLElement.prototype.animate = function(properties,duration) {
"use strict";
var aniLoop,
last = 0,
fps = 60;
function ani(time) {
var prop;
aniLoop = requestAnimationFrame(ani);
if ((time - last) > fps) {
last = time;
for (prop in properties) {
console.log(prop + ': ' + time);
}
if (time >= (duration * 1000)) {
cancelAnimationFrame(aniLoop);
}
}
}
aniLoop = requestAnimationFrame(ani);
}
:2 Animation timestamping
As it looks currently, your animation function will probably not run more than one frame anyway - if you look at requestAnimationFrame documentation on MDN, you'll notice that the callback given to requestAnimationFrame is given a timestamp, i.e. value in milliseconds from the beginning of UNIX epoch (1st of January 1970) - therefore the condition of time >= (duration * 1000) will always be true. Instead of that, register the starting time when you kick the animation off and compare the timestamp within the callback to it, like so:
HTMLElement.prototype.animate = function(properties,duration) {
"use strict";
var aniLoop,
start,
last = 0,
fps = 60;
function ani(time) {
var prop,
progress;
aniLoop = requestAnimationFrame(ani);
if ((time - last) > fps) {
last = time;
progress = time - start;
for (prop in properties) {
console.log(prop + ': ' + progress + ' out of ' + (duration * 1000));
}
// This is where we get a difference between current and starting time
if (progress >= (duration * 1000)) {
cancelAnimationFrame(aniLoop);
}
}
}
start = Date.now();
aniLoop = requestAnimationFrame(ani);
}
:3 Animation throttling
This one is not as crucial, but still worth a consideration - requestAnimationFrame is intended to be automatically throttled and regulated by the browser, thus you don't need to apply your own conditions on whether animation should run (it won't go over 60FPS anyway, as that's the specification's ceiling). Instead, it should simply work on difference of current time from starting time, to make sure your animation still ends up in correct place even if for some reason, there is a lag in animation:
HTMLElement.prototype.animate = function(properties,duration) {
"use strict";
var aniLoop,
start;
function ani(time) {
var prop,
progress;
aniLoop = requestAnimationFrame(ani);
progress = time - start;
for (prop in properties) {
console.log(prop + ': ' + progress + ' out of ' + (duration * 1000));
}
// This is where we get a difference between current and starting time
if (progress >= (duration * 1000)) {
cancelAnimationFrame(aniLoop);
}
}
start = Date.now();
aniLoop = requestAnimationFrame(ani);
}
I'm trying to record a user's mouse and key inputs, then "play them back" to the user by triggering events at chronologically the same time they occurred. That sentence is a little confusing so I'll explain my code:
var $dancerContainer = $('.dancerContainer');
var count = 3;
var captured;
var countInterval;
$dancerContainer is the element I want to animate, which is a div.
count is the duration of the 'recording' phase.
captured will eventually be an object that holds the events, keyed by the elapsed time in milliseconds they occurred since the start of the recording.
function captureInput() {
var mouseCapture = [];
var keyCapture = [];
var start = new Date().getTime();
$(document).on('mousemove.capture', function(event) {
event.t = new Date().getTime() - start;
mouseCapture.push(event);
});
$(document).on('keyup.capture', function(event) {
event.t = new Date().getTime() - start;
keyCapture.push(event);
});
setTimeout(function() {
$(document).off('.capture');
captured = chronoCaptures(mouseCapture, keyCapture);
}, 3000);
}
The captureInput func tags an elapsed time on the event before pushing them to mouse and keyup arrays, mouseCapture and keyCapture, then unbinds the listeners.
function chronoCaptures(mouse, keyboard) {
var greater = (mouse.length > keyboard.length) ? mouse.length : keyboard.length;
var chrono = {};
var j = 0;
var k = 0;
for (var i = 0; i < greater; i++) {
if (keyboard[k] == undefined) {
chrono[mouse[j].t] = mouse[j];
j++;
} else if (mouse[j] == undefined) {
chrono[keyboard[k].t] = keyboard[k];
k++;
} else {
if (mouse[j].t < keyboard[k].t) {
chrono[mouse[j].t] = mouse[j];
j++;
} else {
chrono[keyboard[k].t] = keyboard[k];
k++;
}
}
}
return chrono;
}
chronoOrderCaptures then takes the two arrays of events and returns an object whose keys are the times each event occurred. Now that I look at this code again, since I'm putting the events into an object anyway, it doesn't matter what order they get put in. I might overwrite an event with another, in which case I want the key event to take precedence (going to refactor this, but this is beside the point).
function replayDance(captured, duration) {
var d = duration * 1000;
var start = new Date().getTime();
var elapsed = 0;
while (elapsed <= d) {
elapsed = new Date().getTime() - start;
var c = captured[elapsed];
if (c) {
$dancerContainer.trigger(c)
}
}
}
Finally, replayDance waits for the duration of the recording and checks how much time has elapsed. If the captured object contains an entry # that amount of elapsed time, I trigger the event on the document.
WHEW. Thank you if you've gotten this far. Now to the problem!! (what?) The problem I'm having is the mouse events all get played back 'at once'. There's no pausing occurring, I don't see them executed in sequence as if I was actually moving the mouse, although it seems like they ought to be getting triggered at roughly the same time they were recorded.
Finally here is the handler for mousemove events:
$(document).on('mousemove', followMouse);
function followMouse(event) {
var width = $(window).width();
var height = $(window).height();
var mouseX = event.pageX - (width * 0.25);
var mouseY = event.pageY - (height * 0.25);
var angleX = (mouseY / height) * 45;
var angleY = (mouseX / width) * 45;
dancer.style.webkitTransform = "rotateX(" + angleX + "deg) rotateY(" + angleY + "deg)";
}
this file
http://www.iguanademos.com/Jare/docs/html5/Lessons/Lesson2/js/GameLoopManager.js
taken from this site
Here is the code:
// ----------------------------------------
// GameLoopManager
// By Javier Arevalo
var GameLoopManager = new function() {
this.lastTime = 0;
this.gameTick = null;
this.prevElapsed = 0;
this.prevElapsed2 = 0;
I understand the declaration of variables,
and they are used to record the time between frames.
this.run = function(gameTick) {
var prevTick = this.gameTick;
this.gameTick = gameTick;
if (this.lastTime == 0)
{
// Once started, the loop never stops.
// But this function is called to change tick functions.
// Avoid requesting multiple frames per frame.
var bindThis = this;
requestAnimationFrame(function() { bindThis.tick(); } );
this.lastTime = 0;
}
}
I don't understand why he uses var bindThis = this
this.stop = function() {
this.run(null);
}
This function set's gameTick to null, breaking the loop in this.tick function.
this.tick = function () {
if (this.gameTick != null)
{
var bindThis = this;
requestAnimationFrame(function() { bindThis.tick(); } );
}
else
{
this.lastTime = 0;
return;
}
var timeNow = Date.now();
var elapsed = timeNow - this.lastTime;
if (elapsed > 0)
{
if (this.lastTime != 0)
{
if (elapsed > 1000) // Cap max elapsed time to 1 second to avoid death spiral
elapsed = 1000;
// Hackish fps smoothing
var smoothElapsed = (elapsed + this.prevElapsed + this.prevElapsed2)/3;
this.gameTick(0.001*smoothElapsed);
this.prevElapsed2 = this.prevElapsed;
this.prevElapsed = elapsed;
}
this.lastTime = timeNow;
}
}
}
Most of this code is what I don't understand, I can see he is recording the time elapsed between frames, but the rest of the code is lost to me.
On the website he uses the term singleton, which is used to prevent the program trying to update the same frame twice?
I have a bit of experience with the javascript syntax, but the concepts of singleton, and the general goal/function of this file is lost to me.
Why is the above code needed instead of just calling
requestAnimationFrame(function() {} );
The reason he uses bindThis is that he is passing a method into an anonymous function on the next line. If he merely used this.tick(), this would be defined as the context of requestAnimationFrame. He could achieve the same thing by using call or apply.
Singletons are classes that are only instantiated once. This is a matter of practice, and not a matter of syntax - javascript doesn't know what a singleton is. By calling it a "Singleton", he is merely communicating that this is a class that is instantiated only once, and everything that needs it will reference the same instance.
I want to use setInterval to animate a couple things. First I'd like to be able to specify a series of page elements, and have them set their background color, which will gradually fade out. Once the color returns to normal the timer is no longer necessary.
So I've got
function setFadeColor(nodes) {
var x = 256;
var itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// would like to call
clearInterval(itvlH);
// but itvlH isn't in scope...?
}
},50);
}
Further complicating the situation is I'd want to be able to have multiple instances of this going on. I'm thinking maybe I'll push the live interval handlers into an array and clean them up as they "go dead" but how will I know when they do? Only inside the interval closure do I actually know when it has finished.
What would help is if there was a way to get the handle to the interval from within the closure.
Or I could do something like this?
function intRun() {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// now I can access an array containing all handles to intervals
// but how do I know which one is ME?
clearInterval(itvlH);
}
}
var handlers = [];
function setFadeColor(nodes) {
var x = 256;
handlers.push(setInterval(intRun,50);
}
Your first example will work fine and dandy ^_^
function setFadeColor(nodes) {
var x = 256;
var itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
clearInterval(itvlH);
// itvlH IS in scope!
}
},50);
}
Did you test it at all?
I've used code like your first block, and it works fine. Also this jsFiddle works as well.
I think you could use a little trick to store the handler. Make an object first. Then set the handler as a property, and later access the object's property. Like so:
function setFadeColor(nodes) {
var x = 256;
var obj = {};
// store the handler as a property of the object which will be captured in the closure scope
obj.itvlH = setInterval(function () {
for (i in nodes) {
nodes[i].style.background-color = "rgb(0,"+(--x)+",0);";
}
if (x <= 0) {
// would like to call
clearInterval(obj.itvlH);
// but itvlH isn't in scope...?
}
},50);
}
You can write helper function like so:
function createDisposableTimerInterval(closure, delay) {
var cancelToken = {};
var handler = setInterval(function() {
if (cancelToken.cancelled) {
clearInterval(handler);
} else {
closure(cancelToken);
}
}, delay);
return handler;
}
// Example:
var i = 0;
createDisposableTimerInterval(function(token) {
if (i < 10) {
console.log(i++);
} else {
// Don't need that timer anymore
token.cancelled = true;
}
}, 2000);