Canvas实现球体碰撞交互效果(三)

336 阅读3分钟

书接上文,再来欣赏一下最终要实现的效果 球体碰撞交互效果.gif

一、实现鼠标交互

要实现鼠标交互就必须通过监听Canvas的mousemove事件,拿到鼠标的实时位置,然后每个小球在每一帧去检测鼠标是否在小球内,判断是否有鼠标与其交互。

  • 检测鼠标交互必须要捕获鼠标的两个点,且这两个点都要在小球内,这样就可以通过这两个点的距离和方向,去计算出鼠标对小球作用力的大小,以及力的方向。

  • 增加了一个最大外力限制的变量,因为我们用鼠标去hover小球时有可能移动速度会非常快,计算出来的力就会非常大,小球的速度也会非常快,需要限制一下小球速度。

  • 小球增加firstHoverX和firstHoverY两个属性,保存检测到的鼠标第一次在小球上的点位置,当检测到第二个点时才触发交互效果,给小球添加外力。

  • 小球增加outForceLastTime属性,保存小球最后一次被添加鼠标外力的时间,如果outForceLastTime的时间与下次检测到hover的时间间隔不超过400毫秒,则不触发交互效果,防止小球短时间内连续触发鼠标外力引发的BUG。

  • 计算小球获取的外力(也就是小球在水平和垂直方向上速度的变化量)都是调用getOutForce方法获取,计算外力时是通过速度值变化的多少来表示的,因为外力最终是作用在小球速度上的变化,后面实现小球碰撞时也会共用这个方法,只不过会通过传入的isMouse这个标识来额外增加一些判断逻辑。

  • 计算小球获取的鼠标外力时,作用力的点位置应该是第一次捕获到hover小球的点位置。

  • 计算外力时有一个相对速度的概念,试想一下,当外力与小球运动方向一致且速度相同时,其实小球是没有受到外力的,所以当外力与小球运动方向一致时,就要通过外力速度与小球速度的差值来最终确定外力的大小。

  • 当外力速度为0时,说明是鼠标或其他小球处于静止状态,而当前小球与其发送碰撞,此时计算最终外力时给它赋值与小球速度相反的两倍大小即可,让小球回弹回去。

    const fontSize = 40; // 文字大小
    const textColor = "#E76F5A"; // 文字颜色
+   const maxOutForce = 25; // 最大外力限制

    const canvas = document.getElementById("myCanvas");
    canvas.width = canvasWidth;
    canvas.height = canvasHeight;
    const ctx = canvas.getContext("2d");
    const globuleList = []; // 保存所有小球实例
+   let mouseX = 0; // 当前鼠标所在的x位置
+   let mouseY = 0; // 当前鼠标所在的y位置

    class Globule {
        constructor(x, y, radius, color = "blue", text = "", fontSize = 30, textColor = "red") {
            . . .
            this.fontSize = fontSize; // 文字大小
            this.textColor = textColor; // 文字颜色
+           this.firstHoverX = null; // 检测到鼠标第一次hover到小球的x位置
+           this.firstHoverY = null; // 检测到鼠标第一次hover到小球的y位置
+           this.outForceLastTime = null; // 最后一次鼠标施加外力的时间
        }

        . . .

        // 移动
        move() {
            this.draw();
            
+           // 检测鼠标交互
+           this.checkHover();
+           
+           this.x += this.vx;
+           this.y += this.vy;
        }

+       // 计算获取外力
+       getOutForce(forceX, forceY, forceVX, forceVY, isMouse = false) {
+           const {
+               x,
+               y,
+               vx,
+               vy
+           } = this;
+           let outForceVX = 0;
+           let outForceVY = 0;
+           const dist = Math.hypot(forceX - x, forceY - y);
+           if(dist !== 0) {
+               outForceVX = (Math.abs(x - forceX) / dist) * forceVX;
+               outForceVY = (Math.abs(y - forceY) / dist) * forceVY;
+           }
+           if(forceVX > 0) {
+               if(vx > 0) {
+                   outForceVX -= vx;
+               }
+           } else if(forceVX < 0) {
+               if(vx <= 0) {
+                   outForceVX -= vx;
+               }
+           } else {
+               outForceVX = -this.vx * 2;
+           }
+           if(forceVY > 0) {
+               if(vy >= 0) {
+                   outForceVY -= vy;
+               }
+           } else if(forceVY < 0) {
+               if(vy <= 0) {
+                   outForceVY -= vy;
+               }
+           } else {
+               outForceVY = -this.vy * 2;
+           }

+           // 如果是鼠标施加的外力,限制外力大小不能超过一定值
+           if(isMouse) {
+               if(outForceVX > maxOutForce) {
+                   outForceVX = maxOutForce;
+               } else if(outForceVX < -maxOutForce) {
+                   outForceVX = -maxOutForce;
+               }
+               if(outForceVY > maxOutForce) {
+                   outForceVY = maxOutForce;
+               } else if(outForceVY < -maxOutForce) {
+                   outForceVY = -maxOutForce;
+               }
+           }
+
+           return {
+               outForceVX,
+               outForceVY
+           };
+       }

+       // 添加外力
+       addOutForce(outForceVX, outForceVY) {
+           this.vx += outForceVX;
+           this.vy += outForceVY;
+       }
+
+       // 检测鼠标是否在小球内
+       checkHover() {
+           const dist = Math.hypot(mouseX - this.x, mouseY - this.y); // 鼠标与小球圆心的距离
+           if(dist <= this.radius) {
+               // 如果之前最后一次鼠标作用在小球的时间与现在间隔不超过400毫秒,则不触发交互效果
+               if(this.outForceLastTime) {
+                   const nowTime = new Date().getTime();
+                   if(nowTime - this.outForceLastTime < 400) {
+                       return;
+                   }
+               }
+               if(this.firstHoverX !== null && this.firstHoverY !== null) {
+                   const mouseVX = mouseX - this.firstHoverX;
+                   const mouseVY = mouseY - this.firstHoverY;
+                   const {
+                       outForceVX,
+                       outForceVY
+                   } = this.getOutForce(this.firstHoverX, this.firstHoverY, mouseVX, mouseVY, true);
+                   this.addOutForce(outForceVX, outForceVY);
+                   this.outForceLastTime = new Date().getTime();
+                   this.firstHoverX = null;
+                   this.firstHoverY = null;
+               } else {
+                   this.firstHoverX = mouseX;
+                   this.firstHoverY = mouseY;
+               }
+           }
+       }
    }

    . . .
 
    // 循环绘制(loop函数中继续调用requestAnimationFrame函数)
    requestAnimationFrame(loop);

+   const canvasInfo = canvas.getBoundingClientRect();
+   canvas.addEventListener("mousemove", function(e) {
+       mouseX = e.clientX - canvasInfo.left;
+       mouseY = e.clientY - canvasInfo.top;
+   });

二、实现撞墙效果

实现撞墙效果的原理就是,在每一帧中检测小球的位置是否到达了墙体的边缘,如果到达了,则给小球添加一个与之速度相反的两倍的力,让小球回弹回去。

    class Globule {
        . . .
 
        // 移动
        move(index) {
            this.draw();  
            
            // 检测鼠标交互
            this.checkHover();
        
+           // 检测和墙体碰撞
+           this.checkCollisionWall();
        
            this.x += this.vx;
            this.y += this.vy;
        }
        
        . . .

        // 检测鼠标是否在小球内
        checkHover() {
            . . .
        }

+       // 检测是否与墙体发生碰撞
+       checkCollisionWall() {
+           const {
+               x,
+               y,
+               radius,
+               vx,
+               vy
+           } = this;
+           let outForceVX = 0;
+           let outForceVY = 0;
+           let isCollision = false;
+           if((x <= radius || x >= canvasWidth - radius) && vx !== 0) {
+               outForceVX = -vx * 2;
+               isCollision = true;
+           }
+           if((y <= radius || y >= canvasHeight - radius) && vy !== 0) {
+               outForceVY = -vy * 2;
+               isCollision = true;
+           }
+           if(isCollision) {
+               this.addOutForce(outForceVX, outForceVY);
+           }
+       }
    }

想要学习完整的课程,请点击此处查看

更多个人文章

  1. hashHistory和browserHistory的区别
  2. 项目中调试本地组件库的实践方案
  3. 面试秘籍之手写系列
  4. 十分钟带你入门Chrome插件开发
  5. 全网最全Autoit3基础教程及实战案例