canvas动画:自由落体运动(代码片段)

悠悠i 悠悠i     2022-10-23     403

关键词:

经过前面的文章,我们已经能够在canvas画布上画出各种炫酷的图形和画面,但是这些画面都是禁止的,怎么样才能让他们动起来呢?

动画的基本步骤

我们知道,动画是一帧一帧的画面不断反映实现的,人的眼睛看到一幅画或一个物体后,在0.34秒内不会消失。利用这一原理,在一幅画还没有消失前播放下一幅画,就会给人造成一种流畅的视觉变化效果。在canvas中,就是在绘制完当前画面之后,快速的绘制下一个画面。步骤如下:

  • 清空canvas。
    • 除非接下来要画的内容会完全充满 canvas (例如背景图),否则你需要清空所有画布上的内容。最简单的做法就是用clearRect方法。
  • 保存canvas状态。
    • 如果你要改变一些会改变 canvas 状态的设置(样式,变形之类的),又要在每画一帧之时都是原始状态的话,你需要先保存一下。
  • 绘制动画图形(animated shapes)。
    • 这一步才是重绘动画帧。
  • 恢复 canvas 状态。
    • 如果已经保存了 canvas 的状态,可以先恢复它,然后重绘下一帧。

操纵动画

在 canvas 上绘制内容是用 canvas 提供的或者自定义的方法,而通常,我们仅仅在脚本执行结束后才能看见结果,比如说,在 for 循环里面做完成动画是不太可能的。

因此,为了实现动画,我们需要一些可以定时执行重绘的方法。window对象提供了下面的方法实现定时动画:

  • setInterval(function, delay)当设定好间隔时间后,function会定期执行
  • setTimeout(function, delay)在设定好的时间之后执行函数
  • requestAnimationFrame(callback)告诉浏览器你希望执行一个动画,并在重绘之前,请求浏览器执行一个特定的函数来更新动画。

如果你并不需要与用户互动,你可以使用setInterval()方法,它就可以定期执行指定代码。如果我们需要做一个游戏,我们可以使用键盘或者鼠标事件配合上setTimeout()方法来实现。通过设置事件监听,我们可以捕捉用户的交互,并执行相应的动作。

window.requestAnimationFrame()这个方法提供了更加平缓并更加有效率的方式来执行动画,当系统准备好了重绘条件的时候,才调用绘制动画帧。一般每秒钟回调函数执行60次,也有可能会被降低。

在使用window.requestAnimationFrame()方法的过程中,我推荐使用下面的兼容性方法来代替:

window.requestAnimationFrame = (function()
    return  window.requestAnimationFrame       ||
            window.webkitRequestAnimationFrame ||
            window.mozRequestAnimationFrame    ||
            window.oRequestAnimationFrame      ||
            window.msRequestAnimationFrame     ||
            function (callback) 
                window.setTimeout(callback, 1000 / 60);
            ;
)();

canvas动画实例-模拟小球自由落体运动

上面介绍了canvas动画的基本概念,接下来我们将会在canvas中实现小球下落的动画。小球的完整代码再本文结尾。点击可跳转到结尾

绘制小球

首先需要在canvas上绘制一个小球。

var ctx = document.getElementById('canvas').getContext('2d');
if (!ctx) 
    console.log('您的浏览器不支持canvas');
    // 可以抛出异常强制结束JS执行
    throw new Error("Do not support canvas");


var ball = 
    x: 100,  // 小球的x坐标
    y: 100,  // 小球的y坐标
    radius: 25,  // 小球半径
    color: 'cyan', // 小球颜色
    draw: function()   // 绘制小球的函数
        ctx.beginPath();
        ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
        ctx.closePath();
        ctx.fillStyle = this.color;
        ctx.fill();
    ,
    clear: function()   // 清除小球区域的函数
        ctx.clearRect(this.x - this.radius,
                this.y - this.radius,
                this.radius * 2,
                this.radius * 2);
    

ball.draw(); // 绘制小球

添加运动描述

绘制了小球之后,要添加动画,还需要为小球添加速率矢量进行移动。另外,速度也是变化的量,对于只有落体运动,还有竖直方向的重力加速度,所以还需要为小球加上加速度。

var ball = 
        x: 100,  // 小球的x坐标
        y: 100,  // 小球的y坐标
        vx: 0,   // 小球水平方向速度
        vy: 0,   // 小球竖直方向速度
        ax: 0,   // 小球水平方向加速度
        ay: 0,   // 小球竖直方向加速度
        dt: 1,   // 两帧之间的时间为1个单位时间
        radius: 25,  // 小球半径
        color: 'cyan', // 小球颜色
        s: function(v, a, t) 
            // 匀加速直线运动的位移公式:s=vt+1/2at^2
            return v * t + (1 / 2.0) * a * t * t;
        ,
        dx: function() 
            // 计算水平方向的位移
            return this.s(this.vx, this.ax, this.dt);
        ,
        dy: function() 
            // 计算竖直方向的位置
            return this.s(this.vy, this.ay, this.dt);
        ,
        next: function() 
            // 计算小球下一时刻的位移
            this.x += this.dx();
            this.y += this.dy();

            // 计算小球下一时刻的速度:v_t = v_0 + a*t
            this.vx = this.vx + this.ax * this.dt;
            this.vy = this.vy + this.ay * this.dt;

            this.boundary(0, canvas.width, canvas.height, 0);
        ,
    ;

假设每一帧之间的时间是单位时间,那么根据当前小球的位置速度和加速度,我们就可以计算下一帧的小球的位置和速度,此时清空上一帧的canvas,再绘制下一帧,即可实现动画效果。

var animate; // 记录动画
ball.draw();

// 绘制一帧
function draw() 
    // 1:清空画布
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    // 2:绘制小球
    ball.draw();
    // 3:计算小球的下一个状态
    ball.next();
    // 4:进入下一帧
    animate = window.requestAnimationFrame(draw);

边界处理

若没有任何的碰撞检测,我们的小球很快就会超出画布。我们需要检查小球的 x 和 y 位置是否已经超出画布的尺寸以及是否需要将速度矢量反转。

boundary: function(top, right, bottom, left) 
    // 检测小球下一帧是否出界,出界则补正
    if (this.y > bottom)   // 下边界越界
        this.vy = -this.vy;  // 速度反向
     else if (this.y < top) 
        this.vy = -this.vy;
     else if (ball.x > right) 
        this.vx = -this.vx;  // 速度反向
     else if (ball.x < left) 
        this.vx = -this.vx;
    

添加拖尾效果

为了使得小球运动更加逼真,可以添加拖尾效果。使用clearRect函数清除前一帧动画时,若用一个半透明的fillRect函数取代之,就可轻松制作长尾效果。

ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fillRect(0,0,canvas.width,canvas.height);

移动鼠标到canvas内可让小球动起来!

遗留问题和优化

在实际的生活中,小球碰撞到地面反弹的时候,反弹的高度会越来越低,因为碰撞地面损失了一部分速度。

boundary: function(top, right, bottom, left) 
    // 检测小球下一帧是否出界,出界则补正
    if (this.y > bottom)   // 下边界越界
        this.vy = -this.vy;  // 速度反向
        this.vy = 0.9 * this.vy; // 速度损失
     else if (this.y < top) 
        this.vy = -this.vy;
     else if (ball.x > right) 
        this.vx = -this.vx;  // 速度反向
     else if (ball.x < left) 
        this.vx = -this.vx;
    

上面这种方式会偶尔使得小球无法反弹。

在碰撞地面的时候,小球的反弹之后的速度和位移,准确值需要根据严格的匀加速公式以及损失之后的速度来计算。

边界检查时上述方法是检查圆心和边界的位置,更好的方式是检查圆周和边界的距离。

源码可以以及效果可以参考这儿:本文实例

上述所有方式的源代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>ball animate</title>
    <style>
        body 
            margin:0;
            padding:0;
            height: 100%;
            /*background: #000;*/
            overflow: hidden;
        
        canvas 
            padding: 0;
            background: #000;
            border: 1px solid;
        
    </style>
</head>
<body>
<canvas id="canvas"></canvas>
<script>
    var canvas = document.getElementById('canvas');
    var SCREEN_WIDTH = window.innerWidth,
            SCREEN_HEIGHT = window.innerHeight,
            ACCURACY = 1, ACCURACY_COMPARE = 1e-5;
    canvas.width = 500;
    canvas.height = 500;
    var context = canvas.getContext('2d'),
            animation, running = false;

    var Ball = function()
        // x 坐标
        this.x = 100;
        // y 坐标
        this.y = 100;

        // x 方向初始速度
        this.vx = 10;
        // y 方向初始速度
        this.vy = 25;

        // x 方向位移
        this.dx = function() 
            return this.s(this.vx, this.ax, this.dt);
        ;
        // y 方向位移
        this.dy = function() 
            return this.s(this.vy, this.ay, this.dt);
        ;
        // 计算位移
        this.s = function(v, a, t)
            return v * t + (1 / 2.0) * a * t * t;
        ;

        // x 方向碰撞速度损失
        this.loss_x = 0.8;
        // y 方向碰撞速度损失
        this.loss_y = 0.8;

        // 水平方向加速度
        this.ax = 0;
        // 竖直方向加速度
        this.ay = 1;

        // 两帧之间的时间间隔
        this.dt = 1;
        // 小球半径
        this.radius = 20;
        // 小球质量
        this.m = 1;
        // 小球颜色
        this.color = 'cyan';
    ;
    // 小球绘制函数
    Ball.prototype.draw = function() 
        context.beginPath();
        context.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
        context.closePath();
        context.fillStyle = this.color;
        context.fill();
    ;
    // 清除小球
    Ball.prototype.clear = function() 
        context.clearRect(this.x - this.radius,
                this.y - this.radius,
                this.radius * 2,
                this.radius * 2);
    ;
    // 获取小球下一个状态
    Ball.prototype.next = function() 
        // 记录当前状态值
        var current_x = this.x, current_vx = this.vx, t1, self = this,
                current_y = this.y, current_vy = this.vy;

        // 下一状态理想坐标
        this.x += this.dx();
        this.y += this.dy();

        // 下一状态理想速度
        this.vx = this.vx + this.ax * this.dt;
        this.vy = this.vy + this.ay * this.dt;

        function boundary(top, right, bottom, left) 
            // 边界判断,预判下一状态是否碰撞下方墙壁
            if (self.y > bottom) 
                // 越界需要重新计算速度和坐标
                // 计算刚好碰到边界之前的速度: vt = sqrt(2aS + v0^2)
                self.vy = Math.sqrt(Math.abs(2 * self.ay * (bottom - current_y) + current_vy * current_vy));
                self.vy = current_vy > 0 ? self.vy : -self.vy;
                if (isNaN(self.vy)) 
                    self.vy = 0;
                    self.ay = 0;
                    self.y = bottom;
                    return false;
                
                // 计算从当前位置到碰撞位置的时间
                if (self.ay == 0) 
                    if (self.vy == 0) 
                        t1 = 0;
                     else 
                        t1 = (bottom - current_y) / self.vy;
                    
                 else 
                    t1 = Math.abs((self.vy - current_vy) / self.ay);
                
                // 碰撞后速度衰减并反向
                self.vy = -self.vy * self.loss_y;
                // 碰撞后反弹的位移位置
                self.y = bottom + self.s(self.vy, self.ay, self.dt - t1);
                // 位移小于阈值,则速度归零
                if (bottom - self.y < ACCURACY) 
                    self.vy = 0;
                    self.y = bottom;
                    self.ay = 0;
                
            
            else if (self.x > right) 
                // 越界需要重新计算速度和坐标
                // 计算刚好碰到边界之前的速度: vt = sqrt(2aS + v0^2)
                self.vx = Math.sqrt(Math.abs(2 * self.ax * (right - current_x) + current_vx * current_vx));
                self.vx = current_vx > 0 ? self.vx : -self.vx;
                if (isNaN(self.vx)) 
                    self.vx = 0;
                    self.ax = 0;
                    self.x = right;
                    return false;
                
                // 计算从当前位置到碰撞位置的时间
                if (self.ax == 0) 
                    if (self.vx == 0) 
                        t1 = 0;
                     else 
                        t1 = (right - current_x) / self.vx;
                    
                 else 
                    t1 = Math.abs((self.vx - current_vx) / self.ax);
                
                // 碰撞后速度衰减并反向
                self.vx = -self.vx * self.loss_x;
                // 碰撞后反弹的位移位置
                self.x = right + self.s(self.vx, self.ax, self.dt - t1);
                // 位移小于阈值,则速度归零
                if (right - self.x < ACCURACY) 
                    self.vx = 0;
                    self.ax = 0;
                    self.x = right;
                
            
            else if (self.x < left) 
                // 越界需要重新计算速度和坐标
                // 计算刚好碰到边界之前的速度: vt = sqrt(2aS + v0^2)
                self.vx = Math.sqrt(Math.abs(2 * self.ax * (current_x - left) + current_vx * current_vx));
                self.vx = current_vx > 0 ? self.vx : -self.vx;
                if (isNaN(self.vx)) 
                    self.vx = 0;
                    self.ax = 0;
                    self.x = left;
                    return false;
                
                // 计算从当前位置到碰撞位置的时间
                if (self.ax == 0) 
                    if (self.vx == 0) 
                        t1 = 0;
                     else 
                        t1 = (current_x - left) / self.vx;
                    
                 else 
                    t1 = Math.abs((self.vx - current_vx) / self.ax);
                
                // 碰撞后速度衰减并反向
                self.vx = -self.vx * self.loss_x;
                // 碰撞后反弹的位移位置
                self.x = self.s(self.vx, self.ax, self.dt - t1);
                // 位移小于阈值,则速度归零
                if (self.x - left < ACCURACY) 
                    self.vx = 0;
                    self.ax = 0;
                    self.x = left;
                
            
            else if (self.y < top) 
                // 越界需要重新计算速度和坐标
                // 计算刚好碰到边界之前的速度: vt = sqrt(2aS + v0^2)
                self.vy = Math.sqrt(Math.abs(2 * self.ay * (current_y - top) + current_vy * current_vy));
                self.vy = current_vy > 0 ? self.vy : -self.vy;
                if (isNaN(self.vy)) 
                    self.vy = 0;
                    self.ay = 0;
                    self.y = top;
                    return false;
                
                // 计算从当前位置到碰撞位置的时间
                if (self.ay == 0) 
                    if (self.vy == 0) 
                        t1 = 0;
                     else 
                        t1 = (top - current_y) / self.vy;
                    
                 else 
                    t1 = Math.abs((self.vy - current_vy) / self.ay);
                
                // 碰撞后速度衰减并反向
                self.vy = -self.vy * self.loss_y;
                // 碰撞后反弹的位移位置
                self.y = self.s(self.vy, self.ay, self.dt - t1);
                // 位移小于阈值,则速度归零
                if (self.y - top < ACCURACY) 
                    self.vy = 0;
                    self.ay = 0;
                    self.y = top;
                
            
            else 
        

        boundary(0, canvas.width - this.radius, canvas.height - this.radius, 0);

    ;

    function clear() 
        context.fillStyle = 'rgba(0,0,0,0.3)';
        context.fillRect(0,0,canvas.width,canvas.height);
    

    var ball = new Ball(), animate;
    function draw() 
        clear();
        ball.draw();
        ball.next();
    

    canvas.addEventListener('click',function(e)
        if (!running) 
            ball.x = e.clientX;
            ball.y = e.clientY;
            animate = setInterval(function() 
                draw();
            , 20);
            running = true;
         else 
            running = false;
            clearInterval(animate);
        
    );

    ball.draw();
</script>
</body>
</html>

如何在canvas中实现自定义路径动画(代码片段)

...目中笔者需要做一个新需求:在canvas中实现自定义的路径动画。这里所谓的自定义路径不单单包括一条直线,也许是多条直线的运动组合,甚至还包含了贝塞尔曲线,因此,这个动画也许是下面这个样子的:那么如何才能在canvas... 查看详情

《每周一点canvas动画》——速度与加速度(代码片段)

在上一节中我们介绍了速度的基本概念,包括沿坐标轴的速度,和更普适的任意方向的速度,在文章的最后我们做了一个鼠标跟随的示例,以及通过改变物体的rotation属性做了一个关于速度的扩展。通过上一节的学习你会发现我... 查看详情

kalman滤波用于自由落体运动目标跟踪问题(代码片段)

某一物体在重力场做自由落体运动、观测装置对其位移进行检测,在传感器受到未知的独立分布随机信号的干扰下,我们需要估计该物体的运动位移和速度,该系统是二维状态估计系统。不考虑x方向上位置变化,... 查看详情

用canvas实现红心飘飘的动画效果(代码片段)

两周前,项目里需要实现一个红心飘飘的点赞效果。抓耳挠腮了老半天,看了几篇大佬的文章,终于算是摸了个七七八八。不禁长叹一声,还是菜啊。先来看一下效果:(传送门进去点一波)一、Bezier曲线运动轨迹其实用大白话描述... 查看详情

强大的css:用纯css模拟下雪的效果

下雪效果只是一类效果的名称,可以是红包雨等一些自由落体的运动效果,本文就是用纯css模拟下雪的效果,更多效果大家可以自行发挥。  1.前言由于公司产品的活动,需要模拟类似下雪的效果。浏览器实现动画无非css3... 查看详情

canvas链式弹性运动

...定义了一个球的model,之后添加了4个球,在加载中调用了动画事件,之后在动画事件里面加载了画线的事件,画完线之后才开始画球,而且专门用了一个函数来计算,也就是完全分开了层次,以免自己的逻辑思维混乱,这是一种... 查看详情

运动学基于matlabgui模拟小球自由落体含matlab源码1630期(代码片段)

...码已上传我的资源:【运动学】基于matlabGUI模拟小球自由落体【含Matlab源码1630期】获取代码方式2:通过订阅紫极神光博客付费专栏,凭支付凭证,私信博主,可获得此代码。备注:订阅紫极神光博客付费... 查看详情

每天一点点之css-动画-一个圆绕着另一个圆动(绕着轨迹运动)(代码片段)

...现过程中发现这个实现起来不是很灵活,然后想到css3有动画也可以实现,下面是效果注:图2是多个的效果,没有代码  html<divclass="s"><divclass="m"><divclass="smallsmall1"> 查看详情

[js高手之路]html5canvas动画教程-匀速运动

匀速运动:指的是物体在一条直线上运动,并且物体在任何相等时间间隔内通过的位移都是相等的。其实就是匀速直线运动,它的特点是加速度为0,从定义可知,在任何相等的时间间隔内,速度大小和方向是相同的。1<head>2... 查看详情

《每周一点canvas动画》——修改增强版

...对删除的文章着手回复,目前进度如下:《每周一点canvas动画》——序《每周一点canvas动画》——用户交户《每周一点canvas动画》——三角函数《每周一点canvas动画》——波形运动(新增平滑运动DEMO,以及各项运动形式的动画效... 查看详情

[js高手之路]html5canvas动画教程-重力摩擦力加速抛物线运动

上节,我们讲了匀速运动,本节分享的运动就更有意思了:加速运动重力加速度抛物线运动摩擦力 加速运动:1<head>2<metacharset=‘utf-8‘/>3<style>4#canvas{5border:1pxdashed#aaa;6}7</style>8<scriptsrc="./ball.js"></script& 查看详情

canvas动画循环

参考技术A动画的绘制就是一个重复的过程,其原理就是重复的绘制和清除,就如同下面这张图形主要有三种:[图片上传中...(QQ20171228-194308-HD.gif-2daec8-1514461554620-0)][图片上传中...(QQ20171228-194308-HD.gif-9c4529-1514461563882-0)]我们在进行... 查看详情

canvas动画时钟(代码片段)

<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="utf-8"/><title>Canvas时钟</title><style>#clockmargin-left:350px;</style>< 查看详情

canvas动画之二--创建动态粒子网格动画(代码片段)

最近看到一个粒子网格动画挺炫的,自己也就做了一个,当背景挺不错的。CSDN不能上传超过2M的图片,所以就简单截了一个静态图片:可以点击这里查看动画.下面就开始说怎么实现这个效果吧:首先当然是添... 查看详情

javascript画布动画(棒图运动)

我一直在寻找一个地方来展示如何做动画。我已经看到你可以将积木移动到一个区域并向后移动,一个圆圈上下移动,但没有任何东西可以移动他的身体。我使用css动画,但想尝试javascript画布。这个数字有什么方法可以在Canvas... 查看详情

canvas动画:气泡上升效果(代码片段)

...个很强大的东西呢!这几天突发奇想想做一个气泡上升的动画,经过许久的思考和多次失败,终于做出了如下效果由于是录制的gif图,看着会有点卡顿,实际演示是很自然的想要做出这种效果需要用到大量的随机数先上代码:CSS... 查看详情

canvas动画+canvas离屏技术(代码片段)

动画<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><title>canvas</title><style>.canvasborder:1pxsolid#abcdef;background-color:#a9add2;</style></head><body><canvasclass="canvas"id="canvas"width="600"height="400">您的浏览... 查看详情

canvas动画之一--百分比进度加载(代码片段)

...其他图像,我们使用脚本来绘制图形。先看一下这次动画的结果:gif图可能不完整,可以点击这里查看完整效果。canvas的API较多,这里我们只介 查看详情