Canvas系列(15):实战-小球拖拽
在上一章中我们实现的小球的碰撞,这章中我们继续玩玩小球,讲解一下小球的拖拽,为了避免代码的混乱本章中就不考虑小球碰撞的情况了,有兴趣的自己看看上一章。
在本章开始的时候,我必须告诉大家一个沮丧事实,Canvas绘制的图形并没有事件来直接操作改图形,这是因为Canvas的整个标签是一个DOM元素,所以DOM操作的事件是作用的整个Canvas标签的,而不是绘制的图形。就比如我们点击Canvas中的小球,并没有直接的事件来监听小球被点击了;我们只能监听Canvas这个DOM元素被点击了,,但是我们可以通过其他方法来模拟一些事件来操作它们,比如我们可以计算鼠标在DOM元素中的位置来判断是否点击到小球上了。好了,开始本章吧!
继续上章刚开始的例子
小球基本操作与上章刚开始的代码是差不多的,唯一的不同是checkWalls
函数我们给x轴碰撞到墙壁的时候也添加了能量的损耗,具体代码如下:
// 获取元素
let canvas = document.getElementById('canvas');
// 获取上下文
let context = canvas.getContext('2d');
class Ball {
constructor(context, options = {}){
this.context = context;
this.x = options.x || 0;
this.y = options.y || 0;
this.radius = options.radius || 20;
this.color = options.color || '#000';
this.vx = options.vx || 0;
this.vy = options.vy || 0;
this.ax = options.ax || 0;
this.ay = options.ay || 0;
}
update() {
this.vx += this.ax;
this.vy += this.ay;
this.x += this.vx;
this.y += this.vy;
}
draw() {
this.context.beginPath();
this.context.arc(this.x, this.y, this.radius, Math.PI / 180 * 0, Math.PI / 180 * 360);
this.context.fillStyle = this.color;
this.context.closePath();
this.context.fill();
}
}
let balls = []
balls.push(new Ball(context,{
x:20,
y:20,
vx:3,
vy:2,
ay:0.5,
color:'red',
}));
balls.push(new Ball(context,{
x:canvas.width - 20,
y:20,
vx:-3,
vy:2,
ay:0.5,
color:'blue',
}));
function checkWalls(ball){
// 边界反弹
if (ball.x < ball.radius) {
ball.x = ball.radius;
ball.vx *= -0.95;
} else if (ball.x > canvas.width - ball.radius) {
ball.x = canvas.width - ball.radius;
ball.vx *= -0.95;
}
if (ball.y < ball.radius) {
ball.y = ball.radius;
ball.vy *= -0.95;
} else if (ball.y > canvas.height - ball.radius) {
ball.y = canvas.height - ball.radius;
ball.vy *= -0.95; // 假设能量损耗是0.05
ball.vx *= 0.99; // 摩擦力
}
}
function draw(ball){
ball.draw();
}
function animate (){
requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
balls.forEach(ball=>{
// 更新小球的速度
ball.update();
// 检测是否碰撞到边界
checkWalls(ball);
});
// 绘制
balls.forEach(draw);
}
animate();
检测小球与鼠标接触
小球与鼠标接触很简单,只要判断鼠标的位置是否在小球所在的圆内就可以了,这里给小球添加一个方法,用来判断点是否在圆内。
class Ball {
// ... 其他代码相同 这里就不再重复
isContainsPoint(x,y){
return Math.hypot(this.x - x ,this.y - y) < this.radius;
}
}
这里使用了一个Math.hypot
函数,这个函数是用来求平方根的,如Math.hypot(3,4)
的结果是5
;它的参数可以有多个,这里只用了2个。
接下来就是需要获取鼠标的x
和y
坐标了,这里就监听mousemove
事件来获取。
let offsetLeft = canvas.offsetLeft;
let offsetTop = canvas.offsetTop;
canvas.addEventListener('mousemove', (e) => {
let x = e.pageX - offsetLeft;
let y = e.pageY - offsetTop;
balls.some(ball=>{
if (ball.isContainsPoint(x,y)) {
console.log('小球与鼠标接触了');
return true;
}
})
}, false);
上述代码中我们通过鼠标在页面的坐标,然后减去Canvas左上角的位置来获取鼠标在Canvas中的位置,最后判断这个位置是否在小球内。
封装获取鼠标在Canvas位置的方法
鼠标在Canvas中的位置对于Canvas的操作非常重要,所以我们这里就封装一个方法来获取鼠标的位置,具体如下:
function captureMouse (element) {
let mouse = {x: 0, y: 0, event: null};
let offsetLeft = element.offsetLeft;
let offsetTop = element.offsetTop;
element.addEventListener('mousemove', (e) => {
let x = e.pageX - offsetLeft;
let y = e.pageY - offsetTop;
mouse.x = x;
mouse.y = y;
mouse.event = e;
}, false);
return mouse;
};
let mouse = captureMouse(canvas)
canvas.addEventListener('mousemove', (e) => {
let x = mouse.x;
let y = mouse.y;
balls.some(ball=>{
if (ball.isContainsPoint(x,y)) {
console.log('指针在小球内了!');
return true;
}
})
}, false);
我们定义了一个captureMouse
的方法,它返回一个对象mouse
,只要在任何地方使用mouse.x
和mouse.y
就可以获取到当前鼠标在Canvas中的位置,是不是很方便?当然pageX和pageY存在一定的兼容性问题,为了保证在更多的浏览器中使用,需要对其做兼容性处理,如下:
function captureMouse (element) {
let mouse = {x: 0, y: 0, event: null};
let body_scrollLeft = document.body.scrollLeft;
let element_scrollLeft = document.documentElement.scrollLeft;
let body_scrollTop = document.body.scrollTop;
let element_scrollTop = document.documentElement.scrollTop;
let offsetLeft = element.offsetLeft;
let offsetTop = element.offsetTop;
element.addEventListener('mousemove', (e) => {
let x, y;
if (e.pageX || e.pageY) {
x = e.pageX;
y = e.pageY;
} else {
x = e.clientX + body_scrollLeft + element_scrollLeft;
y = e.clientY + body_scrollTop + element_scrollTop;
}
x -= offsetLeft;
y -= offsetTop;
mouse.x = x;
mouse.y = y;
mouse.event = e;
}, false);
return mouse;
};
模拟拖拽
拖拽的过程是这样的,当鼠标按在小球上,那么选中小球;然后鼠标按着并移动鼠标的时候,小球也跟着移动,也就是拖
的过程;最后松开鼠标,就是把小球释放了。这个过程可以通过mousedown
,mousemove
,mouseup
三个事件来模拟。前面的过程也就是,当Canvasmousedown
的时候,记录一下选中的小球;然后mousedown
并且mousemove
的时候移动小球;最后mouseup
的时候释放选中的小球。这里有一个问题就是怎么能够既是mousedown
又是mousemove
呢?这里有2中方法,第一种就是监听mousedown
并定义一个变量,然后再监听mousemove
,并判断刚才定义的变量;第二种是在mousedown
的事件处理程序中去监听mousemove
,然后在mouseup
的时候清除事件。由于mousemove
是一个触发次数比较多的事件,为了保证性能,我们采用第二种办法,具体代码如下:
// Canvas中的坐标
let mouse = captureMouse(canvas);
// 选中的小球
let selectedBall = null;
// 拖拽
canvas.addEventListener('mousedown', () => {
balls.some(ball=>{
if (ball.isContainsPoint(mouse.x, mouse.y)) {
// 记录下选中的小球
selectedBall = ball;
// 添加事件来模拟拖拽
canvas.addEventListener('mousemove', onMouseMove, false);
canvas.addEventListener('mouseup', onMouseUp, false);
return true;
}
})
function onMouseMove () {
selectedBall.x = mouse.x;
selectedBall.y = mouse.y;
selectedBall.vx = 0;
selectedBall.vy = 0;
}
function onMouseUp () {
selectedBall = null;
// 清除事件
canvas.removeEventListener('mousemove', onMouseMove, false);
canvas.removeEventListener('mouseup', onMouseUp, false);
}
}, false);
现在还有一个问题,就是当小球拖拽的时候,不应该再受到重力和自己的速度运动了,所以需要修改animate
函数,只有当选中的小球和当前遍历的小球不相等的时候才去更新新的坐标,否则就用鼠标的坐标(上述代码也实现):
function animate (){
requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
balls.forEach(ball=>{
if (selectedBall !== ball) {
// 更新小球的速度
ball.update();
// 检测是否碰撞到边界
checkWalls(ball);
}
});
// 绘制
balls.forEach(draw);
}
投掷
我们刚才拖拽完了以后,由于速度设为了0,所以小球是做自由落体运动,而大多数情况下,我们更希望可以把小球投掷出去,那么当小球投掷的时候,需要计算小球的瞬时速度,这时我们就需要定义拖拽时上一次小球的坐标,并拖过简单的减法计算出瞬时速度,具体代码如下:
// 旧的坐标位置
let oldX = 0;
let oldY = 0;
function animate (){
requestAnimationFrame(animate);
context.clearRect(0, 0, canvas.width, canvas.height);
balls.forEach(ball=>{
if (selectedBall === ball) {
trackVelocity();
} else {
// 更新小球的速度
ball.update();
// 检测是否碰撞到边界
checkWalls(ball);
}
});
// 绘制
balls.forEach(draw);
}
animate();
// 拖拽
canvas.addEventListener('mousedown', () => {
balls.some(ball=>{
if (ball.isContainsPoint(mouse.x, mouse.y)) {
// 记录下选中的小球
selectedBall = ball;
oldX = ball.x;
oldY = ball.y;
// 添加事件来模拟拖拽
canvas.addEventListener('mousemove', onMouseMove, false);
canvas.addEventListener('mouseup', onMouseUp, false);
return true;
}
})
// 其他代码相同
}, false);
function trackVelocity () {
selectedBall.vx = selectedBall.x - oldX;
selectedBall.vy = selectedBall.y - oldY;
oldX = selectedBall.x;
oldY = selectedBall.y;
}
好了小球拖拽就完全做完了,完整代码请点这里。
- Spring Cloud(五)断路器监控(Hystrix Dashboard)
- 微信技术团队的又一力作,WCDB 简单易用的数据库框架
- Redis特性和应用场景
- Spring Cloud(四)服务提供者 Eureka + 服务消费者 Feign
- 智能下拉刷新框架-SmartRefreshLayout
- Spring Cloud(三)服务提供者 Eureka + 服务消费者(rest + Ribbon)
- Spring Cloud(二)Consul 服务治理实现
- Spring Cloud(一)服务的注册与发现(Eureka)
- Shard 分片集群
- 面试官最爱的volatile关键字
- 玩转 WebView ,突破系统限制,让缓存更简单,更灵活
- Mycat 读写分离 数据库分库分表 中间件 安装部署,及简单使用
- 50道Java线程题
- Jrebel6.3.3破解,配置图文教程
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 【即时通信IM】红包消息如何构建?
- YOLOv4损失函数全面解析
- Pandas进阶修炼120题,给你深度和广度的船新体验
- 5万字、97 张图总结操作系统核心知识点
- C++核心准则CP.100:不要使用无锁编程方式,除非绝对必要
- 神了,Excel的这个操作我今天才知道
- DataFrame(7):DataFrame运算——逻辑运算
- 高性能网关设计实践
- LASSO回归姊妹篇:R语言实现岭回归分析
- 学了这个,三歪再也不想写各种setter了
- 使用 GitLab CI 与 Argo CD 进行 GitOps 实践
- Java 语言中十大“坑爹”功能!
- 面试:说说啥是一致性哈希算法?
- 问一下,线程池里面到底该设置多少个线程?
- 进程和线程基础知识全家桶,30 张图一套带走