📜 ⬆️ ⬇️

Javascript animation of elements as in jQuery, only with your own hands

Often, when searching for the answer to the question of how to do one or another unfamiliar thing, the programmer gets acquainted with the experience of colleagues. And quite often here, in front-end development, you can see the following tips: they say, just plug in one or another library, just install this or that plugin, just rewrite the project on Angulyar (just_Angulyare)))), and you don’t need to score head by other things.

However, sometimes there are truly creative tasks, and the usual copy-paste does not save democracy (to be honest, it almost never saves). I want to tell one of such cases to a respected public.

The background is as follows. We in the company developed a product, the client part of which was to be written in pure Javascipt. At some point, to realize all the capabilities of the product, we realized that we could not do without animating the elements (revealing the elements, dissolving, smoothly moving around the screen, etc.)

The most logical step was to simply go and see how the animation is arranged in the same jQuery, and, if possible, repeat the solution in your code. However, even with a quick glance it became clear that the jQuery code is not so easy to understand all the dependencies. And then, the project management has allocated enough time to sit down and make a decision without looking back at the experience of colleagues. Looking back now, I understand that it was a wonderful experience of solving a really difficult task in client programming.
')
So, consider the problem at the top level.

We have the following conditions:

1) Any element on the page is animated; the number of elements is unlimited.
2) The list of animated properties is set (linear dimensions, position, indents, transparency)
3) Animation should be controlled through parameters (time of execution, function of speed of performance)
4) Upon completion of the animation, any arbitrary callback is called.
5) Animation can be interrupted at any time.

Most likely, one global function will be responsible for items 1-4, called with a list of parameters, and a separate special function will be responsible for item 5. Only two (in fact, it turned out that three :)). Next, I will tell you how to write functions with brief explanations of why a particular block exists and how it works, and at the end I will give a link to the full code, as well as an example.

Let's start

Immediately create a var $ = {d: window.document, w: window}, so as not to go straight into the window.

Create the main animation function:

$.w.ltAnimate = function (el, props, opts, cb) {}); 

Here el is the element to be animated, props is the essence of the animation, opts are the characteristics, cb is the callback.

Begin to fill it. To save the context, use self, to uniquely define the animation id. Immediately perform the necessary checks:

Code 1
 var self = this, id = new Date().getTime(); // id  self._debug = true; //   if ((typeof el == "string") && el) el = this.ltElem(el); if ((typeof el != "object") || !el || (typeof el.nodeType != "number") || (el.nodeType > 1)) { doFail("  "); return; } //   opts switch (typeof opts) { case "number": opts = {duration: opts}; break; case "function": opts = {cbDone: opts}; break; case "object": if (!opts) opts = {}; break; default: opts = {}; } if (typeof cb == "function") opts.cbDone = cb; //   var defaultOptions = { tick : 30, //      ,    duration : 1000, //    easing : 'linear', //    cbDone : function() { //     if (self._debug) $.w.console.log(" [id: " + id + "]  "); }, cbFail : function() { //     if (self._debug) $.w.console.log(" [id: " + id + "]  "); } } 


Please note : error handling is convenient through the special doFail function.

Now the actions that need to be done with the element are entered into the array:

Code 2
 //   ,    var instructions = []; for (var key in props) { if (!props.hasOwnProperty(key)) continue; instructions.push([key, props[key]]); } //   ,   if (instructions.length === 0) { doFail(" ,    "); return; } 


The default options are overwritten by the client where indicated (of course, with checks):

Code 3
 //     var optionsList = [], easing = {linear: 1, swing:1, quad:1, cubic:1}; for (var key in opts) { if (!opts.hasOwnProperty(key)) continue; switch (key) { case "duration": if (typeof opts[key] != "number") { $.w.console.log("ltAnimate(): !    .    "); continue; } break; case "easing": if (typeof easing[opts[key]] == "undefined") { $.w.console.log("ltAnimate(): !   easing.    "); continue; } break; case "cbDone": case "cbFail": if (typeof opts[key] != "function") { $.w.console.log("ltAnimate(): !    !"); continue; } break; default: $.w.console.log("ltAnimate(): !     !"); continue; } optionsList.push([key, opts[key]]) } //  options   defaultOptions var options = defaultOptions; if (optionsList.length) { for (var i=0; i < optionsList.length; i++) { if (optionsList[i][0] == 'duration') options.duration = optionsList[i][1]; if (optionsList[i][0] == 'easing') options.easing = optionsList[i][1]; if (optionsList[i][0] == 'cbDone') options.cbDone = optionsList[i][1]; if (optionsList[i][0] == 'cbFail') options.cbFail = optionsList[i][1]; } } 


Now back to the real world a little bit.

The fact is that the animation is called at any time on any element. Accordingly, we first need to take the parameters of this element at this point in time in order to understand how to modify it.

 // ,         var startParams = {}; 

Here, finally, it comes to us that some additional function will be needed, with the help of which we will describe the animated element (about it a little further). For now we will make one more important check.

The number and queue of animations

Yes, the task is getting harder. The fact is that the animation can be invoked on an element that is already in a state of execution of the previous animation (and has not finished it yet). Our function must be universal, so it must be able to receive and correctly handle a chain of independent calls.

Below is the code. There is a line-by-line explanation to it, so I simply quote this block entirely:

Code 4
 //         if (el.ltAnimateQueue && el.ltAnimateQueue.length > 0) { // ,        (  , ..      ) var animateEnds = 1, timeNow = new Date().getTime(); for (var i=0; i < el.ltAnimateQueue.length; i++) { if (i == 0) { animateEnds = el.ltAnimateQueue[i][1] - timeNow + el.ltAnimateQueue[i][0]; } else { animateEnds += el.ltAnimateQueue[i][1]; } } //      el.ltAnimateQueue.push([timeNow + animateEnds, options.duration]); //    ,            var thisTimeout = $.w.setTimeout(function(){ checkAnimation(); }, animateEnds); //  ,     ,    ltAnimateStop if (!el.ltAnimateTimeouts) { el.ltAnimateTimeouts = []; } el.ltAnimateTimeouts.push(thisTimeout); //     } else { //    ,      el.ltAnimateQueue = [[new Date().getTime(), options.duration]]; startAnimation(); } // ,           function checkAnimation() { //     ,    if (!el.ltAnimateIsDoing) { startAnimation(); } else { //  ,     function _check() { if (!el.ltAnimateIsDoing) { $.w.clearInterval(_checking); startAnimation(); } } var _checking = $.w.setInterval(_check, 30); } } 


We are ready to start the execution of the animation, its launch is performed by the startAnimation () function This is where, at the very beginning, we describe our element through its properties (you can’t do this before, because the element could be changed while the animation was in the queue):

Code 5
 function startAnimation() { //    el.ltAnimateIsDoing = true; //   var startStyles = self.ltStyle(el); //      startParams.left = parseInt(startStyles.left); startParams.right = parseInt(startStyles.right); startParams.top = parseInt(startStyles.top) + 0.01; startParams.bottom = parseInt(startStyles.bottom) - 0.01; startParams.width = parseInt(startStyles.width); startParams.height = parseInt(startStyles.height); startParams.opacity = parseFloat(startStyles.opacity); startParams.marginTop = parseInt(startStyles.marginTop); startParams.marginBottom = parseInt(startStyles.marginBottom); startParams.marginLeft = parseInt(startStyles.marginLeft); startParams.marginRight = parseInt(startStyles.marginRight); startParams.parentWidth = parseInt(self.ltStyle(el.parentNode).width); startParams.parentHeight = parseInt(self.ltStyle(el.parentNode).height); //     Chrome  IE for (key in startParams) { if (key == 'left' && !startParams[key]) { startParams.left = startParams.parentWidth - startParams.right - startParams.width || 0; } if (key == 'right' && !startParams[key]) { startParams.right = startParams.parentWidth - startParams.left - startParams.width || 0; } if (key == 'bottom' && !startParams[key]) { startParams.bottom = startParams.parentHeight - startParams.top - startParams.height || 0; } if (key == 'top' && !startParams[key]) { startParams.top = startParams.parentHeight - startParams.bottom - startParams.height || 0; } } //   el.currentAnimation = new doAnimation({ element : el, delay : defaultOptions.delay }); } 


As you can see, here we refer to a special function that will give us a description of the element styles. Bearing in mind that our code should be in pure JS, and the project should work adequately on the IE8 browser (yes, we are writing real commercial code that is sold for money to different countries, so the argument "this is not fashionable" is not accepted! ), squeeze all the will into a fist and go barbar donkey zamorochki:

Code 6
 /** *     (   el)     ( styleName  ). * opts - ,  computed    true.  ,    ,  false - . *     (,  div)    ,      ,   . *  IE8   %, auto, thin/medium/thick   . * Opacity  IE8     ( 0  1) * * @param {DOM} el -   * @param {string} style -  ,     * @param {Object} opts -    * * @returns {(number|string)} value -   */ $.w.ltStyle = function(el, styleName, opts) { if (!opts || typeof opts != 'object' || typeof opts.computed != 'boolean') opts = {computed : true}; if (typeof el == 'string') el = this.ltElem(el); //    (NodeList),     if (!el || !el.nodeType || (el.nodeType != 1)) return ''; var _style; //  IE8  getComputedStyle  currentStyle if (!$.w.getComputedStyle) { var __style = el.currentStyle, _style = {}; for (var i in __style) { _style[i] = __style[i]; } // ,    IE8   : pixelLeft, pixelRight    -    ,   var pixel = { left: 1, right: 1, width: 1, height: 1, top: 1, bottom: 1 }; //      http://erik.eae.net/archives/2007/07/27/18.54.15/#comment-102291 var other = { paddingLeft: 1, paddingRight: 1, paddingTop: 1, paddingBottom: 1, marginLeft: 1, marginRight: 1, marginTop: 1, marginBottom: 1 }; var leftCopy = el.style.left; var runtimeLeftCopy = el.runtimeStyle.left; //     if (!styleName) { //    IE8    ,     for (c in _style) { if (!_style.hasOwnProperty(c)) continue; if (c.indexOf("border") !== 0) continue; switch (_style[c]) { case "thin": _style[c] = 2; break; case "medium": _style[c] = 4; break; case "thick": _style[c] = 6; break; default: _style[c] = 0; } } //pixel for (var key in pixel) { _style[key] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")]; } //   getComputedStyle    for (var key in other) { el.runtimeStyle.left = el.currentStyle.left; el.style.left = _style[key]; _style[key] = el.style.pixelLeft; el.style.left = leftCopy; el.runtimeStyle.left = runtimeLeftCopy; } //     } else { if (_style[styleName]) { if (style.indexOf("border") === 0) switch (_style[styleName]) { case "thin": _style[styleName] = 2; break; case "medium": _style[styleName] = 4; break; case "thick": _style[styleName] = 6; break; default: _style[styleName] = 0; } } else { if (pixel[styleName]) { _style[styleName] = el.style["pixel" + key.charAt(0).toUpperCase() + key.replace(key.charAt(0), "")]; } else { el.runtimeStyle.left = el.currentStyle.left; el.style.left = _style[styleName]; _style[styleName] = el.style.pixelLeft; el.style.left = leftCopy; el.runtimeStyle.left = runtimeLeftCopy; } } } //   opacity IE8 if (_style.filter.match('alpha')) { _style.opacity = _style.filter.substr(14); _style.opacity = parseInt(_style.opacity.substring(0, _style.opacity.length - 1)) / 100; } else { _style.opacity = 1; } //   } else { if (opts.computed) { _style = $.w.getComputedStyle(el, null); } else { _style = el.style.styleName; } } if (!styleName) { return _style || ''; } else { return _style[styleName] || ''; } }; 


Here, it would seem, all business: to receive element styles! But no, and there is a field for creativity.

Finally, the element is fully described, all the parameters of the animation are checked and brought into a normative form. Now we write the execution code. It is entirely enclosed in the doAnimation (params) {} function;

In the first part, which is shown below, the most interesting is deciphering the animation instructions (“what to do with the object?”). I hope everyone remembers that some properties of an element (dimensions, position, indents) can be set not only by pixels, but also by percentages:

Code 7
 //   var val = instructions[i][1].toString(); // ,      val.match(/\%/) ? percent = true : percent = false; val = parseFloat(val); var x; switch (instructions[i][0]) { case 'top' : x = function(factor, val, percent) { element.style.bottom = ''; element.style.top = startParams.top - (startParams.top - (percent ? startParams.parentHeight * val / 100 : val))*factor + 'px'; }; break; case 'bottom' : x = function(factor, val, percent) { element.style.top = ''; element.style.bottom = startParams.bottom - (startParams.bottom - (percent ? (startParams.parentHeight * val / 100) : val))*factor + 'px'; }; break; case 'left' : x = function(factor, val, percent) { element.style.right = ''; element.style.left = startParams.left - (startParams.left - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px'; }; break; case 'right' : x = function(factor, val, percent) { element.style.left = ''; element.style.right = startParams.right - (startParams.right - (percent ? (startParams.parentWidth * val / 100) : val))*factor + 'px'; }; break; case 'width' : x = function(factor, val, percent) { element.style.width = startParams.width - (startParams.width - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; }; break; case 'height' : x = function(factor, val, percent) { element.style.height = startParams.height - (startParams.height - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'opacity' : x = function(factor, val, percent) { // IE8 if (!$.w.getComputedStyle) { element.style.filter = 'alpha(opacity=' + (startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor) * 100 + ')'; } else { element.style.opacity = startParams.opacity - (startParams.opacity - (percent ? (val / 100) : val))*factor; } } break; case 'marginTop' : x = function(factor, val, percent) { element.style.marginBottom = 'auto'; element.style.marginTop = startParams.marginTop - (startParams.marginTop - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'marginBottom' : x = function(factor, val, percent) { element.style.marginTop = 'auto'; element.style.marginBottom = startParams.marginBottom - (startParams.marginBottom - (percent ? (startParams.height * val / 100) : val))*factor + 'px'; }; break; case 'marginLeft' : x = function(factor, val, percent) { element.style.marginRight = 'auto'; element.style.marginLeft = startParams.marginLeft - (startParams.marginLeft - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; }; break; case 'marginRight' : x = function(factor, val, percent) { element.style.marginLeft = 'auto'; element.style.marginRight = startParams.marginRight - (startParams.marginRight - (percent ? (startParams.width * val / 100) : val))*factor + 'px'; } break; //     ,     default : x = function(){}; } //      exec.push([x, val, percent]); } var eLength = exec.length; 


Finally, we got close to the very heart of the mechanism.

Let's think about animation in general? In the real world, any movement is an idea that unfolds in time.

Samurai swings his sword
Like a samurai, not swinging his sword,
But only waved his sword

In the programming world, we have to translate any poetry into a number. That is, more to the point, to describe the element at any time through clear values.

Now think about it - do we need “any moment”? We are able to control the behavior of the element in time intervals of a millisecond. In fact, of course, the required interval in the description of the element is a compromise between the performance of the browser and the ability of the human brain to put separate discrete images into one scene. Experienced, I found that 30 milliseconds will be just right.

In other words, animation is a sequential, at regular intervals, change in the state of an element. And for equal periods of time we have setInterval answers:

 el.ltAnimateInterval = $.w.setInterval(function(){ _animating(); }, options.tick); 

This is our animation engine.

Note : we set the interval as a property of the element, because later we will need access from the outside to stop performing the animation (that is, reset the interval).

Finally, the element drawing function, which runs at specified intervals throughout the animation:

Code 8
 // jumpToEnd - true/false -   ,       ,      //  function _animating(param, jumpToEnd, callback) { counter++; //     0  1 var progress = counter / animationLength; //     stopAnimation if (param == animationLength) { $.w.clearInterval(el.ltAnimateInterval); //       if (jumpToEnd) _step(getProgress(1)); //      el.ltAnimateQueue.splice(0, 1); //     el.ltAnimateIsDoing = false; // ,        if (!callback) { try { options.cbDone(); } catch(e) { doFail(e); } } else { try { callback(); } catch(e) { doFail(e); } } return false; } //  ,     if (progress > 1) { //   ,          (progress  ,   0.99...) _step(getProgress(1)); $.w.clearInterval(el.ltAnimateInterval); //      el.ltAnimateQueue.splice(0, 1); //     el.ltAnimateIsDoing = false; try { options.cbDone(); } catch(e) { doFail(e); } return false; } _step(getProgress(progress)); } 


As you can see, the first two blocks of the function code are the mechanism for turning off the animation (I think you don’t need to disassemble), and the rendering itself is done by the _step function (getProgress (progress)):

 function _step(factor) { for (var i=0; i < eLength; i++) { var s = exec[i][0], val = exec[i][1], percent = exec[i][2]; s(factor, val, percent); } } 

Here we analyze everything as detailed as possible:


Now about the factor with which this function is called. This is a calculated parameter that tells us how much the element's parameter should change (along the path from the initial value to the final value) at this particular point in time, which is understood as a point on the time line in the range from 0 to 1. This was already higher:

 //     0  1 var progress = counter / animationLength; 

You can walk along the segment with equal speed, or by following the behavior of one of the standard animation functions:

 //   ,      function getProgress(p) { switch (options.easing) { case 'linear' : return p; break; case 'swing' : return 0.5 - Math.cos(p * Math.PI ) / 2; break case 'quad' : return Math.pow(p, 2); break; case 'cubic' : return Math.pow(p, 3); break; default : return p; } } 

In Russian, all this sounds complicated, yes (or I just do not know how to use the Russian language pretty). But the picture is clearer, the abscissa axis is time, the ordinate axis is the value of the variable parameter:

image

Thus, the essence of the mechanism is as follows: at regular intervals we ask getProgress what stage of the animation (from the starting point to the final point) we are, and then go with this knowledge in _step and perform the function of changing parameters from the list.

And the last thing we do in doAnimation, is the interface to stop the animation:

Code 9
 //    el.stopAnimation = function(jumpToEnd, callback) { _animating(animationLength, jumpToEnd, callback); //      if (el.ltAnimateTimeouts) { for (var i=0; i < el.ltAnimateTimeouts.length; i++) { $.w.clearTimeout(el.ltAnimateTimeouts[i]) } el.ltAnimateTimeouts = []; } } 


The call to stop the animation is simple: we simply indicate the element, say if we need to go to the end point of the animation at the moment of the stop, and specify, if necessary, a new callback.

To stop the animation, we have a separate global function:

Code 10
 /* *  : ,     (true/false)      (true/false),    */ $.w.ltAnimateStop = function(el, jumpToEnd, callback) { //   ,     if (!el.ltAnimateInterval) return false; el.stopAnimation(jumpToEnd, callback); }; 


Well and the latest - an error handler. It is written in $ .w.ltAnimate and called, if required, from it:

Code 11
 //   function doFail(text) { if (self._debug._enabled) { if ((typeof text != "string") || !text) text = "  [id: " + id + "] -  ."; $.w.console.log("ltAnimate(): ! " + text); } if (opts.cbFail) { try { opts.cbFail(); } catch (e) { $.w.console.log("ltAnimate(): !     [id: " + id + ", " + e.name + ": " + e.message + "]"); } } } 


→ Try to drive the rectangles yourself here

You can also take the full source code of all three functions.

_____________________________________

ps. Since the code was written a year and a half ago, the details of some of the points why it was done this way have already begun to be erased from memory. However, if you ask a question, I will try to remember as precisely as possible what I had in mind when I wrote the code.

Of course, the code presented is not ideal. I would be glad to constructive remark and an indication of errors and additions.

It would also be interesting to discuss how to approach the task of developing animation elements in the browser using a different approach.

Source: https://habr.com/ru/post/315942/


All Articles