JavaScript Animation Basics: Easing, Carousels, Throttling, and Scroll Effects

Published:

Wrapping animation logic into a reusable function

A basic animation usually moves an element at a constant speed. Easing changes that behavior by making the speed gradually slow down, which is especially useful when an element needs to come to a natural stop.

How easing works

The main idea is simple:

  1. Reduce the distance moved on each tick so the element slows down over time.
  2. Use (target - current position) / 10 as the step size.
  3. Stop the timer once the current position reaches the target.
  4. Since the step may become a decimal, round it to an integer before applying it.

This kind of calculation makes the element move quickly at first, then more slowly as it approaches the destination.

Moving between different target positions

The same animation function should not only move an element from left to right once; it should also handle movement between any two target values, such as from 800 back to 500.

That means the step can be either positive or negative:

  • if the step is positive, round it upward with Math.ceil()
  • if the step is negative, round it downward with Math.floor()

This avoids the problem of a tiny decimal step preventing the element from ever landing exactly on the target.

Adding a callback

A callback function is simply a function passed into another function as an argument. After the animation finishes, that function can be executed.

The correct place to run the callback is where the timer stops.

Complete animation function

function animate(obj, target, callback) {
    // console.log(callback);  callback = function() {}  调用的时候 callback()

    // 先清除以前的定时器,只保留当前的一个定时器执行
    clearInterval(obj.timer);
    obj.timer = setInterval(function() {
        // 步长值写到定时器的里面
        // 把我们步长值改为整数 不要出现小数的问题
        // var step = Math.ceil((target - obj.offsetLeft) / 10);
        var step = (target - obj.offsetLeft) / 10;
        step = step > 0 ? Math.ceil(step) : Math.floor(step);
        if (obj.offsetLeft == target) {
            // 停止动画 本质是停止定时器
            clearInterval(obj.timer);
            // 回调函数写到定时器结束里面
            // if (callback) {
            //     // 调用函数
            //     callback();
            // }
            callback && callback();
        }
        // 把每次加1 这个步长值改为一个慢慢变小的值  步长公式:(目标值 - 现在的位置) / 10
        obj.style.left = obj.offsetLeft + step + 'px';

    }, 15);
}

Common web animation examples

Carousel slider

A carousel, also called a focus image slider, is one of the most common UI effects on the web.

The required behavior includes:

  1. Show the left and right arrows when the mouse enters the carousel area, and hide them when it leaves.
  2. Clicking the right arrow shifts the images left by one slide each time; the left arrow does the reverse.
  3. The small indicator dots update as the images change.
  4. Clicking a dot jumps directly to the corresponding image.
  5. When the mouse is not over the carousel, it should autoplay.
  6. When the mouse enters the carousel, autoplay should pause.

A typical implementation uses the animation function above, dynamically generates the indicator dots, clones the first image and appends it to the end for seamless looping, and keeps track of both the image index and the dot index.

window.addEventListener('load', function() {
    // 1. 获取元素
    var arrow_l = document.querySelector('.arrow-l');
    var arrow_r = document.querySelector('.arrow-r');
    var focus = document.querySelector('.focus');
    var focusWidth = focus.offsetWidth;
    // 2. 鼠标经过focus 就显示隐藏左右按钮
    focus.addEventListener('mouseenter', function() {
        arrow_l.style.display = 'block';
        arrow_r.style.display = 'block';
        clearInterval(timer);
        timer = null; // 清除定时器变量
    });
    focus.addEventListener('mouseleave', function() {
        arrow_l.style.display = 'none';
        arrow_r.style.display = 'none';
        timer = setInterval(function() {
            //手动调用点击事件
            arrow_r.click();
        }, 2000);
    });
    // 3. 动态生成小圆圈  有几张图片,我就生成几个小圆圈
    var ul = focus.querySelector('ul');
    var ol = focus.querySelector('.circle');
    // console.log(ul.children.length);
    for (var i = 0; i < ul.children.length; i++) {
        // 创建一个小li
        var li = document.createElement('li');
        // 记录当前小圆圈的索引号 通过自定义属性来做
        li.setAttribute('index', i);
        // 把小li插入到ol 里面
        ol.appendChild(li);
        // 4. 小圆圈的排他思想 我们可以直接在生成小圆圈的同时直接绑定点击事件
        li.addEventListener('click', function() {
            // 干掉所有人 把所有的小li 清除 current 类名
            for (var i = 0; i < ol.children.length; i++) {
                ol.children[i].className = '';
            }
            // 留下我自己  当前的小li 设置current 类名
            this.className = 'current';
            // 5. 点击小圆圈,移动图片 当然移动的是 ul
            // ul 的移动距离 小圆圈的索引号 乘以 图片的宽度 注意是负值
            // 当我们点击了某个小li 就拿到当前小li 的索引号
            var index = this.getAttribute('index');
            // 当我们点击了某个小li 就要把这个li 的索引号给 num
            num = index;
            // 当我们点击了某个小li 就要把这个li 的索引号给 circle
            circle = index;
            // num = circle = index;
            console.log(focusWidth);
            console.log(index);

            animate(ul, -index * focusWidth);
        })
    }
    // 把ol里面的第一个小li设置类名为 current
    ol.children[0].className = 'current';
    // 6. 克隆第一张图片(li)放到ul 最后面
    var first = ul.children[0].cloneNode(true);
    ul.appendChild(first);
    // 7. 点击右侧按钮, 图片滚动一张
    var num = 0;
    // circle 控制小圆圈的播放
    var circle = 0;
    // flag 节流阀
    var flag = true;
    arrow_r.addEventListener('click', function() {
        if (flag) {
            flag = false; // 关闭节流阀
            // 如果走到了最后复制的一张图片,此时 我们的ul 要快速复原 left 改为 0
            if (num == ul.children.length - 1) {
                ul.style.left = 0;
                num = 0;
            }
            num++;
            animate(ul, -num * focusWidth, function() {
                flag = true; // 打开节流阀
            });
            // 8. 点击右侧按钮,小圆圈跟随一起变化 可以再声明一个变量控制小圆圈的播放
            circle++;
            // 如果circle == 4 说明走到最后我们克隆的这张图片了 我们就复原
            if (circle == ol.children.length) {
                circle = 0;
            }
            // 调用函数
            circleChange();
        }
    });

    // 9. 左侧按钮做法
    arrow_l.addEventListener('click', function() {
        if (flag) {
            flag = false;
            if (num == 0) {
                num = ul.children.length - 1;
                ul.style.left = -num * focusWidth + 'px';

            }
            num--;
            animate(ul, -num * focusWidth, function() {
                flag = true;
            });
            // 点击左侧按钮,小圆圈跟随一起变化 可以再声明一个变量控制小圆圈的播放
            circle--;
            // 如果circle < 0  说明第一张图片,则小圆圈要改为第4个小圆圈(3)
            // if (circle < 0) {
            //     circle = ol.children.length - 1;
            // }
            circle = circle < 0 ? ol.children.length - 1 : circle;
            // 调用函数
            circleChange();
        }
    });

    function circleChange() {
        // 先清除其余小圆圈的current类名
        for (var i = 0; i < ol.children.length; i++) {
            ol.children[i].className = '';
        }
        // 留下当前的小圆圈的current类名
        ol.children[circle].className = 'current';
    }
    // 10. 自动播放轮播图
    var timer = setInterval(function() {
        //手动调用点击事件
        arrow_r.click();
    }, 2000);

})

This example combines several common techniques:

  • event handling for hover and click
  • dynamic DOM creation
  • seamless looping by cloning the first slide
  • keeping image movement and dot state synchronized
  • autoplay control through a timer

Throttling with a flag

A carousel button should not be allowed to trigger new animations continuously before the previous one has completed. Otherwise, repeated rapid clicks can make the slider move too fast or break its state.

That is what the so-called throttle flag is for.

Its purpose is to block repeated triggering until the current animation has finished. The basic approach is to use a variable together with a callback:

var flag= true; // 开始设置一个变量
If (flag) {
    flag = false; // 关闭水龙头
    //do something
}

When the animation ends, set flag = true inside the callback to unlock it again.

In practice, this works like a switch:

  • true: the action is allowed
  • false: the action is temporarily locked

That makes the interaction much more stable.

Animated back-to-top effect

The same animation function can also be reused for a back-to-top feature.

The difference is that instead of dealing with left, the animation now relates to the page's vertical scroll position.

Key points:

  1. A back-to-top action can be animated rather than jumping instantly.
  2. The existing animation function can still be used.
  3. Values related to left need to be replaced with values related to vertical scrolling.
  4. The amount the page has scrolled vertically can be read from window.pageYOffset.
  5. Actual page scrolling is done with window.scroll(x, y).

This example also includes a floating sidebar effect that becomes fixed after the page scrolls past a banner, plus logic to show or hide the back-to-top button once the page reaches a certain section.

  //1. 获取元素
var sliderbar = document.querySelector('.slider-bar');
var banner = document.querySelector('.banner');
// banner.offestTop 就是被卷去头部的大小 一定要写到滚动的外面
var bannerTop = banner.offsetTop
// 当我们侧边栏固定定位之后应该变化的数值
var sliderbarTop = sliderbar.offsetTop - bannerTop;
// 获取main 主体元素
var main = document.querySelector('.main');
var goBack = document.querySelector('.goBack');
var mainTop = main.offsetTop;
// 2. 页面滚动事件 scroll
document.addEventListener('scroll', function() {
    // console.log(11);
    // window.pageYOffset 页面被卷去的头部
    // console.log(window.pageYOffset);
    // 3 .当我们页面被卷去的头部大于等于了 172 此时 侧边栏就要改为固定定位
    if (window.pageYOffset >= bannerTop) {
        sliderbar.style.position = 'fixed';
        sliderbar.style.top = sliderbarTop + 'px';
    } else {
        sliderbar.style.position = 'absolute';
        sliderbar.style.top = '300px';
    }
    // 4. 当我们页面滚动到main盒子,就显示 goback模块
    if (window.pageYOffset >= mainTop) {
        goBack.style.display = 'block';
    } else {
        goBack.style.display = 'none';
    }

})
// 3. 当我们点击了返回顶部模块,就让窗口滚动的页面的最上方
goBack.addEventListener('click', function() {
    // 里面的x和y 不跟单位的 直接写数字即可
    // window.scroll(0, 0);
    // 因为是窗口滚动 所以对象是window
    animate(window, 0);
});

The “cloud” hover effect

Another classic example is the floating cloud effect used in navigation bars.

The logic is straightforward:

  1. Use the animation function to move the cloud element.
  2. Its initial position starts at 0.
  3. When the mouse enters a navigation item, animate the cloud to that item's offsetLeft.
  4. When the mouse leaves, animate it back to the last stored position.
  5. When a navigation item is clicked, store that position as the new starting point.

This creates a smooth hover-following effect while still preserving the selected menu item after a click.

 window.addEventListener('load', function() {
     // 1. 获取元素
     var cloud = document.querySelector('.cloud');
     var c_nav = document.querySelector('.c-nav');
     var lis = c_nav.querySelectorAll('li');
     // 2. 给所有的小li绑定事件
     // 这个current 做为筋斗云的起始位置
     var current = 0;
     for (var i = 0; i < lis.length; i++) {
         // (1) 鼠标经过把当前小li 的位置做为目标值
         lis[i].addEventListener('mouseenter', function() {
             animate(cloud, this.offsetLeft);
         });
         // (2) 鼠标离开就回到起始的位置
         lis[i].addEventListener('mouseleave', function() {
             animate(cloud, current);
         });
         // (3) 当我们鼠标点击,就把当前位置做为目标值
         lis[i].addEventListener('click', function() {
             current = this.offsetLeft;
         });
     }
 })

These examples all build on the same core idea: once the animation function is properly packaged, many interface effects can be implemented by changing only the target value and the object being animated.

image