IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    css视口中盒子边界碰撞回弹效果,定时器方式和requestAnimationFrame方式,如漂浮公告场景

    水冗水孚发表于 2024-04-09 16:03:21
    love 0

    看完本文的收获

    1. CV一份碰撞回弹动画代码(可封装成组件用在工作项目中)
    2. 进一步理解requestAnimationFrame相较于setInterval的‘丝滑’优势
    3. 觉得mouseenter和mouseleave(约等于css中的hover)比mouseover和mouseout好用

    需求描述

    • 公司首页网站上,要加一个漂浮公告功能
    • 就是一个醒目的盒子在来回漂浮
    • 遇到边界再碰撞一下
    • 就是重要信息的提醒
    • 如下效果图

    代码实现——requestAnimationFrame方式

    • 建议复制粘贴运行一下,理解更加直观
    • 相关的逻辑见注释...
    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            body {
                background-color: #e9e9e9;
            }
    
            #box {
                position: absolute;
                width: 120px;
                height: 120px;
                background-color: #baf;
                line-height: 120px;
                text-align: center;
            }
        </style>
    </head>
    
    <body>
        <div id="box">
            <a href="http://ashuai.work" target="_blank">公告!速点!</a>
        </div>
        <script>
            let box = document.getElementById('box');
            let xSpeed = 4; // x轴方向移动的速度
            let ySpeed = 2.4; // y轴方向移动的速度(类似于向量)
            let animationFrameId;
            // 兼容性的浏览器视口高度和宽度
            let width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
            let height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
            // 动画运行函数
            function run() {
                let rect = box.getBoundingClientRect(); // 返回盒子的位置对象信息
                // 下一瞬/帧 要修改盒子的left或top的值,等于当前位置加速度
                let newX = rect.left + xSpeed;
                let newY = rect.top + ySpeed;
                // 位置判断,到达边界,碰撞更改
                if (newX + box.offsetWidth >= width) {
                    console.log('到达右边界');
                    newX = width - box.offsetWidth; // 右边界位置是视口宽度去除自身宽度
                    xSpeed = -xSpeed; // 移动方向颠倒过来
                } else if (newX <= 0) {
                    console.log('到达左边界');
                    newX = 0; // 左边界位置是起始位置是0
                    xSpeed = -xSpeed; // 移动方向颠倒过来
                }
                // Y轴同理不赘述
                if (newY + box.offsetHeight >= height) {
                    console.log('到达下边界');
                    newY = height - box.offsetHeight;
                    ySpeed = -ySpeed;
                } else if (newY <= 0) {
                    console.log('到达上边界');
                    newY = 0;
                    ySpeed = -ySpeed;
                }
                // 更改位置即为移动dom元素
                box.style.left = `${newX}px`;
                box.style.top = `${newY}px`;
                // 再次run
                animationFrameId = requestAnimationFrame(run);
            }
            // 开始动画
            run();
            /**
             * 添加事件监听器,使用mouseenter和mouseleave
             *                即为鼠标hover效果
             * */
            box.addEventListener('mouseenter', () => {
                console.log('移入暂停');
                cancelAnimationFrame(animationFrameId); // 取消动画帧
            });
            box.addEventListener('mouseleave', () => {
                console.log('移出继续');
                run();
            });
    
        </script>
    </body>
    
    </html>

    注意,想要hover效果,就用mouseenter和mouseleave

    /**
     * 不要使用mouseover和mouseout,因为其内部子元素也会触发这个事件
     * 把上面的两个事件绑定注释掉,把下面的解开,当鼠标在box的子元素a上
     * 轻轻来回划过(要在box内操作)会出现动画抖动现象,即为触发了事件的暂停和启动
     * */ 
    // box.addEventListener('mouseover', () => {
    //     console.log('移入暂停');
    //     cancelAnimationFrame(animationFrameId); // 取消当前等待执行的动画帧
    // });
    // box.addEventListener('mouseout', () => {
    //     console.log('移出继续');
    //     run();
    // });

    代码实现——定时器方式

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <style>
            body {
                background-color: #e9e9e9;
            }
    
            #box {
                position: absolute;
                width: 120px;
                height: 120px;
                background-color: #baf;
                line-height: 120px;
                text-align: center;
            }
        </style>
    </head>
    
    <body>
        <div id="box">
            <a href="http://ashuai.work" target="_blank">公告!速点!</a>
        </div>
        <script>
            let box = document.getElementById('box');
            let xSpeed = 4; // x轴方向移动的速度
            let ySpeed = 2.4; // y轴方向移动的速度(类似于向量)
            let timer;
            // 兼容性的浏览器视口高度和宽度
            let width = window.innerWidth || document.documentElement.clientWidth || document.body.clientWidth;
            let height = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight;
            // 定时器运行函数
            function run() {
                let rect = box.getBoundingClientRect(); // 返回盒子的位置对象信息
                // 下一瞬/帧 要修改盒子的left或top的值,等于当前位置加速度
                let newX = rect.left + xSpeed;
                let newY = rect.top + ySpeed;
                // 位置判断,到达边界,碰撞更改
                if (newX + box.offsetWidth >= width) {
                    newX = width - box.offsetWidth;
                    xSpeed = -xSpeed;
                } else if (newX <= 0) {
                    newX = 0;
                    xSpeed = -xSpeed;
                }
                if (newY + box.offsetHeight >= height) {
                    newY = height - box.offsetHeight;
                    ySpeed = -ySpeed;
                } else if (newY <= 0) {
                    newY = 0;
                    ySpeed = -ySpeed;
                }
                // 更改位置即为移动dom元素
                box.style.left = `${newX}px`;
                box.style.top = `${newY}px`;
            }
            // 开始动画
            timer = setInterval(run, 16.7); // 大约每秒60帧
            // 添加事件监听器
            box.addEventListener('mouseenter', () => {
                clearInterval(timer); // 鼠标悬停时,清除定时器
            });
            box.addEventListener('mouseleave', () => {
                timer = setInterval(run, 16.7); // 鼠标离开时,重新设置定时器
            });
        </script>
    </body>
    
    </html>
    大家可以进一步把逻辑抽离,封装成一个组件,通过传参配置化调用,从而达到我们想要的效果。比如盒子回弹两个方向的移动速度,比如盒子的相关样式等... 这里不赘述,本文主打一个抛砖引玉,引起思考...

    附录

    • 关于这个盒子边界碰撞回弹效果,在一些网站上也有这个需求,如汇金公司网站:http://www.huijin-inv.cn/
    • 关于requestAnimationFrame的优势特色的进一步理解,可参见笔者的这篇文章:https://segmentfault.com/a/1190000043350645
    • 笔者空闲时间,也会整理点css动画,参见笔者的这篇文章,代码会在github仓库中一点点更新:https://segmentfault.com/a/1190000042969886


沪ICP备19023445号-2号
友情链接