I'm experimenting in Javascript with animating various elements using a cubic-bezier timing function.
(I know this is usually better done with CSS3, or a Javascript animation library. I'm simply using Javascript here to understand how bezier functions work, and teach myself about them.)
So, I get the basic concept, and I'm using a simple bezier curve library written by Stephen McKamey, which is a great Javascript port of the Webkit implementation.
I'm having trouble understanding how I can actually use this function to control an animation in Javascript. So, starting with something very simple: a basic black square that I can animate by moving it to the right, by incrementing its style.left property:
CSS:
.parent {
position: relative;
}
.foo {
border: 1px solid #000000;
background-color: black;
height: 200px;
width: 200px;
position: absolute;
}
HTML:
<div class = "parent">
<div class = "foo" id = "target"></div>
</div>
Okay, so, given a cubic bezier function "bezier", defined as follows:
function bezier(p1x, p1y, p2x, p2y, x, duration) { ... }
Where p1x, p1y, p2x and p2y are the curve control points (between 0 and 1.0), x is the value of the x coordinate, and duration is a duration in milliseconds. The function returns the corresponding y coordinate. I'm trying to simply animate this black box by moving it 400px to the right.
For my first attempt, I use the standard "ease" bezier values, which CSS3 uses, so our ease bezier function could be written as:
function ease(x, duration) {
return function() {
Bezier.cubicBezier(0.25, 0.1, 0.25, 1.0, x, duration);
}
}
So this should give us a slow start, then move fast, then end slowly.
Okay, so I assume the basic way to implement this is to use window.setInterval, and then for each interval, call the bezier function with a new value for x, and then apply the result somehow to the property we want to animate.
The thing is, I'm not sure what my "x" value is here. I assume that in this situation, x is actually the time, and y is the delta between the old position and new position (distance to move), but I'm not really sure. I'm probably wrong.
Anyway, plugging this all in, I'd write a function like:
var startPos = 0;
var endPos = 400; // pixels
var duration = 400; // milliseconds
var millisecondsPerInterval = 10;
var target = document.getElementById("target");
var t = 0;
var pos = 0;
var bezierFunction = Bezier.cubicBezier;
var interval = window.setInterval(
function() {
pos = pos + bezierFunction(0.25, 0.1, 0.25, 1.0, t / 1000, duration); // "ease" values
target.style.left = (pos * millisecondsPerInterval) + "px";
t += millisecondsPerInterval;
if (t === duration) { window.clearInterval(interval); }
},
millisecondsPerInterval
);
This seems to work - the box slowly begins moving and then speeds up. But then it just stops abruptly, rather than easing out. So I'm probably not applying this function correctly. I'm not even certain if "x" is supposed to be my time value here, and "y" is supposed to be the position delta (distance to move), but that seems the only way to apply this that makes any sense.
So, am I doing something wrong here? What is the correct way to apply a cubic bezier function to a property we want to animate using Javascript?
If you use JQuery, it may make the process simpler.
Based on an answer to a similar question (https://stackoverflow.com/a/6824695/363099), you could extend jQuery easing in order to add your custom easing function:
According to the jQuery 1.6.2 source, the meaning of the easing function is as follows. The function is called at various points in time during the animation. At the instants it is called,
x and t both say what the time is now, relative to the start of the animation. x is expressed as a floating point number in the range
[0,1], where 0 is the start and 1 is the end. t is expressed in
milliseconds since the start of the animation.
d is the duration of the animation, as specified in the animate call, in milliseconds.
b=0 and c=1.
So here is how it could work for your code:
$.extend(jQuery.easing,{bezier: function(x,t,b,c,d) {
return (
x <= 0 ? 0 :
x >= 1 ? 1 :
bezierFunction(0.25, 0.1, 0.25, 1.0, x, d)
);
} });
Then you can just use JQuery animation function:
$('#target').animate({right:'400px'},400,'bezier');
Related
I've been reading through guides on the internet, but I haven't been able to find a way to animate a line from one position to a new position.
Apparently this requires "tweening", to create a smooth animation, and isn't built in? And all I ever find is "Check out THIS JavaScript framework that handles that for you!"
Does something exist in vanilla JavaScript (or jQuery) akin to:
animateLine(current, target, duration, easingFunction)
{
move(line.x1, target.x1, duration, easingFunction);
move(line.y1, target.y1, duration, easingFunction);
move(line.x2, target.x2, duration, easingFunction);
move(line.y2, target.y2, duration, easingFunction);
}
I want to just iterate through an array of lines, calling animateLine(lines[i], targets[i], duration, easingFunction) inside of setInterval().
Lots of different ways to skin this cat... However, as far as the frame-by-frame animation, you want requestAnimationFrame. This function calls whatever you pass to it, and passes a timestamp. Then it's just a matter of changing properties of these "lines" to move them across the screen.
If you were animating an <hr> element you could make it absolutely positioned and then just change the top amount each frame:
html:
<hr id="myLine"/>
css:
#myLine { position:absolute; width:100% }
js:
function easeOutQuad(t) { return t*(2-t) };
var startTime = null,
percent, elapsed,
duration = 3000,
end = 400,
hr = document.getElementById('myLine');
function step(timestamp) {
if (startTime === null) startTime = timestamp;
elapsed = timestamp - startTime;
percent = elapsed/duration;
if (elapsed < duration) {
// apply easing fn
percent = easeOutQuad(percent);
var frameDist = end * percent;
hr.style.top = frameDist + 'px';
// next frame
requestAnimationFrame(step);
} else {
// on complete...
}
}
// begin
requestAnimationFrame(step);
Here is a fiddle: http://jsfiddle.net/oytwdk9s/
If you want to support IE9 and older, you'll need a polyfill for requestAnimationFrame.
So, I am just diving into simple web animations for a game, and I am looking for advice. Eventually, I'll get a good grip on beziers and arcs and learn how to animate along a path to get some nice Diablo III-esque curving numbers but, for now, I am just trying to get the fundamentals down.
First (real) attempt
The key code is pretty simple-
paper.text(170, 95, dmgValue).attr({fill:"white", "font-size":16}).animate({
transform:"t0,-50", "fill-opacity":0} ,500).node.setAttribute("class", "no-select");
A CSS styling prevents the text from being highlighted (thanks to a user here for the help). The main issue, is that the text is still there with no opacity- you can hover over it and see the text cursor. Although it works, it' kind of messy looking. Also, since there is no variable assigned, I don't think I can dispose of it with Element.remove();
Where I am at now
There were a lot of small revisions I made in-between saved versions that made the code to the bulkiness that it is now. I wanted the ability to limit the number of numbers flying around at once (for slower computers), so I put them into an array that can be looped endlessly and used, although that probably isn't needed and it wouldn't be a big deal to leave it out.
Also moved from using transform, to setting the y-coords, and placing the .hide() into a separate function for the callback (which, for some reason worked instead of placing it at the end of the animation).
This version appears to work at first, but the animations get interrupted when you click too many times and I'm not sure why. I am sure I can figure it out in the end with enough time, but I might be making this too complicated, anyway. The full code-
var paper = Raphael(0, 0, 350, 350);
paper.canvas.style.backgroundColor = "Black";
var dmgValues = [],
dmgValuesIndex = 0,
maxMsgs = 15,
dmgXMaxOffset = 25,
dmgYMaxOffset = 25,
dmgXRef = 170 - dmgXMaxOffset,
dmgYRef = 250 - dmgYMaxOffset,
dmgMaxDistance = 50;
for (i=0; i< maxMsgs; i++) {
dmgValues[i] = paper.text().attr({fill:"white", "font-size":16});
dmgValues[i].node.setAttribute("class", "no-select");
dmgValues[i].hide();
}
var toggle = paper.rect(150, 270, 50, 25).attr({fill:"green"});
toggle.click(function() { doHit(); });
function doHit() {
var dmgHit = Math.floor(Math.random() * 99) + 1,
xPos = Math.floor(Math.random() * dmgXMaxOffset) + 1,
yPos = Math.floor(Math.random() * dmgYMaxOffset) + 1;
dmgValues[dmgValuesIndex].show();
if (dmgValues[dmgValuesIndex].status() == 1) { dmgValues[dmgValuesIndex].stop(); }
dmgValues[dmgValuesIndex].attr({x:dmgXRef + xPos, y:dmgYRef + yPos, text:dmgHit,
"fill-opacity":1}).animate({y:dmgYRef - dmgMaxDistance, "fill-opacity":0}, 600,
"linear", function() { afterEffects(dmgValues[dmgValuesIndex]) });
}
function afterEffects (afterTarget) {
afterTarget.hide();
dmgValuesIndex++;
if (dmgValuesIndex >= maxMsgs) { dmgValuesIndex = 0; }
}
CSS:
.no-select {
-moz-user-select: none;
-webkit-user-select: none;
}
I think I figured it out!
http://jsfiddle.net/rLcwax9k/10/
One thing I noticed was that that the incrementer was in the callback function that occurred after the animation, so it wasn't really counting right. But, mainly, because the dmgValuesIndex was global, and was getting incremented on each click. So, by the time the animation was done, it was doing functions based on whatever the current count was at the end of the animation in the callback, which may not have been the right one. So, I just put a parameter on the function and used that as the reference throughout the call and passed it to the callback.
Heh, I am sort of beginning to see why a lot of languages need setter and getter methods on their objects. This should be a good lesson to noobs like me on operating with global variable scope and their possible side-effects.
However, before I accept an answer, I am still looking for any other methods that may be more efficient.
Main code-
function doHit(iter) {
this.iter = iter;
var dmgHit = Math.floor(Math.random() * 99) + 1,
xPos = Math.floor(Math.random() * dmgXMaxOffset) + 1,
yPos = Math.floor(Math.random() * dmgYMaxOffset) + 1;
dmgValues[iter].show();
if (dmgValues[iter].status() == 1) { dmgValues[iter].stop(); }
dmgValues[iter].attr({
x:dmgXRef + xPos,
y:dmgYRef + yPos,
text:dmgHit,
"fill-opacity":1
})
.animate({
y:(dmgYRef + yPos) - dmgMaxDistance,
"fill-opacity":0},
1000,
">",
function() {
dmgValues[iter].hide();
}
);
}
I asynchronously receive new positions of element. Each time I receive the position I calculate the offset and move the element (set), like this:
asynchReceiveData(id,function(newposition){
var offset = {};
var oldposition = getOldPosition(markerArray[id]); //returns object with old x and y
offset.x = (newposition.x - oldposition.x);
offset.y = (newposition.y - oldposition.y);
markerArray[id].entireSet.stop()
.animate({ transform:"...T"+offset.x+","+offset.y }, 400);
//i also tried without .stop()
});
For example:
with each update the set should move 50px to the right, after 10 slow updates (lets say one update per 2 seconds), the set is 500px to the right, everything is OK.
Problem is, when I receive too many new positions too fast:
(e.g. one update per 200ms),then the set is 300, or 350 or 400 or 450 instead of 500px to the right.
I think the problem is that the animation does not have enough time to finish before new animation is triggered. I tried lowering the animation time from 400ms to 200, but with little success, it still sometimes happened.
Everything works fine, when I do not use animation and do just this:
markerArray[id].entireSet.transform("...T"+offset.x+","+offset.y);
But I would like to have this animations. Do you have any suggestion how to fix this?
So.. after few tries I came to a solution:
After the end of every animation I check the real position of element (with getBBox()) and compare it with expected position. If it differs I move the element by the difference; In code:
asynchReceiveData(id,function(newposition){
var offset = {};
var oldposition = getOldPosition(markerArray[id]); //returns object with old x and y
offset.x = (newposition.x - oldposition.x);
offset.y = (newposition.y - oldposition.y);
markerArray[id].entireSet.stop().animate({ transform:"...T"+offset.x+","+offset.y}, 500,
function () {
var o = {};
o.x = markerArray[id].x - markerArray[id].circleObj.getBBox().cx;
o.y = markerArray[id].y - markerArray[id].circleObj.getBBox().cy;
markerArray[id].entireSet.transform("...T"+o.x+","+o.y);
});
});
Sometimes it's not very smooth (a bit laggy), but anyway, it solves the problem.
I'm trying to trigger an event half-way through the progress (not time) of a transition. It sounds simple, but since the transition can have any curve it's quite tricky. In my particular case it's not going to be paused or anything so that consideration is out of the way.
(Simplified) essentially I could trigger an animation on a modifier like this:
function scaleModifierTo(stateModifier, scale, animationDuration) {
stateModifier.setTransform(
Transform.scale(scale, scale, scale),
{
duration: animationDuration,
curve: this.options.curve
}
);
}
When the interpolated state of the Transitionable hits 0.5 (half-way through) I want to trigger a function.
I haven't dug that deep behind in the source of famo.us yet, but maybe need to do something like
subclass something and add the possibility to listen when the state passes through a certain point?
reverse the curve defined and use a setTimeout (or try to find a proximity using a few iterations of the chosen curve algorithm (ew))
Is it possible to do this easily? What route should I go down?
I can think of a couple of ways to achieve such, and both lend to the use of Modifier over StateModifier. If you are new, and haven't really had the chance to explore the differences, Modifier consumes state from the transformFrom method which takes a function that returns a transform. This is where we can use our own Transitionable to supply state over the lifetime of our modifier.
To achieve what you wish, I used a Modifier with a basic transformFrom that will alter the X position of the surface based on the value of the Transitionable. I can then monitor the transitionable to determine when it is closest, or in my case greater than or equal to half of the final value. The prerender function will be called and checked on every tick of the engine, and is unbinded when we hit the target.
Here is that example..
var Engine = require('famous/core/Engine');
var Surface = require('famous/core/Surface');
var Modifier = require('famous/core/Modifier');
var Transform = require('famous/core/Transform');
var Transitionable = require('famous/transitions/Transitionable');
var SnapTransition = require('famous/transitions/SnapTransition');
Transitionable.registerMethod('snap',SnapTransition);
var snap = { method:'snap', period:1000, damping:0.6};
var context = Engine.createContext();
var surface = new Surface({
size:[200,200],
properties:{
backgroundColor:'green'
}
});
surface.trans = new Transitionable(0);
surface.mod = new Modifier();
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
context.add(surface.mod).add(surface);
function triggerTransform(newValue, transition) {
var prerender = function(){
var pos = surface.trans.get();
if (pos >= (newValue / 2.0)) {
// Do Something.. Emit event etc..
console.log("Hello at position: "+pos);
Engine.removeListener('prerender',prerender);
}
}
Engine.on('prerender',prerender);
surface.trans.halt();
surface.trans.set(newValue,transition);
}
surface.on('click',function(){ triggerTransform(400, snap); });
The downside of this example is the fact that you are querying the transitionable twice. An alternative is to add your transitionable check right in the transformFrom method. This could get a bit strange, but essentially we are modifying our transformFrom method until we hit our target value, then we revert back to the original transformFrom method.. triggerTransform would be defined as follows..
Hope this helps!
function triggerTransform(newValue, transition) {
surface.mod.transformFrom(function(){
pos = surface.trans.get()
if (pos >= newValue/2.0) {
// Do something
console.log("Hello from position: " + pos)
surface.mod.transformFrom(function(){
return Transform.translate(surface.trans.get(),0,0);
});
}
return Transform.translate(pos,0,0)
})
surface.trans.set(newValue,transition);
}
Thank you for your responses, especially #johntraver for the prerender event, I wasn't aware of the existence of that event.
I realised it made more sense that I should handle this logic together with my move animation, not the scale one. Then, I ended up using a (very hacky) way of accessing the current state of the transition and by defining a threshold in px I can trigger my function when needed.
/**
* Move view at index to a specified offset
* #param {Number} index
* #param {Number} xOffset xOffset to move to
* #param {Object} animation Animation properties
* #return void
*/
function moveView(index, xOffset, animation) {
var rectModifier = this._views[index].modifiers.rect;
var baseXOffset = rectModifier._transformState.translate.state[0];
// After how long movement is reflow needed?
// for the sake of this example I use half the distance of the animation
var moveThreshold = Math.abs(baseXOffset - xOffset)/2;
/**
* Callback function triggered on each animation frame to see if the view is now covering
* the opposite so we can trigger a reflow of the z index
* #return void
*/
var prerender = function() {
var numPixelsMoved = Math.abs(baseXOffset - rectModifier._transformState.translate.state[0]);
if (numPixelsMoved > moveThreshold) {
Engine.removeListener('prerender', prerender);
// trigger a method when this is reached
_reflowZIndex.call(this);
}
}.bind(this);
rectModifier.setTransform(
Transform.translate(xOffset, 0, 0),
animation,
function() {
Engine.removeListener('prerender', prerender);
}
);
Engine.on('prerender', prerender);
}
Obviously the ._transformState.translate.state[0] is a complete hack, but I couldn't figure out of getting this value in a clean way without adding my own Transitionable, which I don't want. If there is a cleaner way of finding the current state as a Number between 0.0-1.0 that would be ace; anyone knows of one?
Easing the excution flow in JS/JQuery
I've loop like this:
for (int i = 0; i < 100; i++)
{
doSomething(...); // returns momentally
}
I'm looking for a way to apply easing to the execution flow - by giving a total duration and an easing pattern (ex. 2 seconds & easeback). Is is something doable in JS (I'm using jQuery too)?
Update 2 Updated to clarify the question - I'm looking for the following:
Update 3 - Sheikh Heera is right, the sample I gave doesn't illustrate the real problem, execute function is updated to call an external module, which is closer to what I have. I don't see how jQuery's animate can be applied directly for calling functions.
easer.ease({ start: 0,
end: 100,
duration: 900,
easing: "easeOutBounce",
execute: function (e) { ExternalModule.DoSomethingUseful(e); } });
where start the end are integers, specifying the animated range, duration is animation duration in milliseconds, easing is the easing pattern used to animate the values within a range, execute - the function which gets called with values from 0 to 100, using the easing pattern supplied in the sample above it will animate myDiv's height from 0 to 100 within 0.9 seconds using easeOutBounce easing function.
Ideally as a small standalone plugin based on jQuery, definitely not part of Mootools or any other heavy hitters I can't afford bringing them in just for that.
To my best, I tried to achieve the thing you want using jQuery "animate" property.
Using this jQuery property will allow to add "easing", "duration", "callback" etc as needed by you. I used the "step" property to achieve this.
In order to work, you need to add a "dummy" tag to the HTML and hide it.
DEMO: http://jsfiddle.net/vaakash/Wtqm3/
HTML
<!-- Add a dummy tag to apply animate property -->
<span class="holder" style="display:none" ></span>
jQuery
$('.holder').animate(
{
'end': 100 // There is no such "end" css property just using as an end mark
},
{
duration: 500,
step: function(now, fx) {
myFunction(now); // Call your function here with the "now" param (i.e ExternalModule.DoSomethingUseful(now) ) in your case
}
// Add easing property if wanted
}
);
// The function
function myFunction(param){
$('body').append('Value now: ' + param + '</br/>');
}
Hope this helps.
If I understand your question correctly....You could try using .delay(100) or .delay(xmilliseconds) so it takes longer at each step.
Read more about delay on : http://api.jquery.com/delay/
Easing in jQuery
jQuery only has two easings, linear and swing. What you are looking for is the functions used in the jQuery UI. They can be accessed from $.easing.
$.easing demo where you can play with them.
You can call any function you'd like by name $.easing.easeOutBounce(e, t, n, r). The only confusing part is that they are actually 4 variable functions. From the jQuery UI docs:
based on easing equations from Robert Penner
The "standard" way to use them in f(x, 0, 0, 1) since e is the variable we typically want to change. n seems to be a "start point" for most functions, t looks to be a "power" in many of them, and r a linear scale factor.
Disclaimer
This is only my best guess from looking at the jquery and jquery-ui source files. I'd recommend if you want to do easing that you just write your own functions instead of relying on internal parts that are certainly not part of the stable API.
Your ease function
Although I wouldn't recommend making a function like this, it was an interesting experiment. Demo.
var ease = function(options) {
var t = 0;
// we need a time interval for animating
var tstep = options.interval || 10;
var duration = options.duration || 500
var i = options.start || 0;
var end = options.end || 100;
// the easing functions only work over x=0..1
var scale = end - i;
// one divided by the number of tsteps
var interval = tstep/duration;
var easing = options.easing || 'linear';
var callback = options.execute || function(){};
var timeout = function() {
// we call the callback but pass it the scale result of our easing
callback(scale*$.easing[easing](Math.min(i, 1)));
// if we haven't reached the end of the animation, queue up another frame
if (t <= duration) {
window.setTimeout(function() {
timeout();
}, tstep);
i += interval;
t += tstep;
}
};
timeout();
};
ease({
start: 0,
end: 100,
duration: 900,
easing: 'easeOutBounce',
// we'll print to the screen the results of the easing
execute: function(e) {$('body').append('<p>' + e)}
});