搜索
首页web前端js教程用canvas绘制一个曲线动画实例
用canvas绘制一个曲线动画实例Jan 03, 2018 pm 03:38 PM
canvas动画

在前端开发中,贝赛尔曲线无处不在,本文主要和大家分享用canvas绘制一个曲线动画实例,希望能帮助到大家。
  • 它可以用来绘制曲线,在svg和canvas中,原生提供的曲线绘制都是使用贝赛尔曲线

  • 它也可以用来描述一个缓动算法,设置css的transition-timing-function属性,可以使用贝塞尔曲线来描述过渡的缓动计算

  • 几乎所有前端2D或3D图形图表库(echarts,d3,three.js)都会使用到贝塞尔曲线

这篇文章我准备从实现一个非常简单的曲线动画效果入手,帮助大家彻底地弄懂什么是贝塞尔曲线,以及它有哪些特性,文章中有一点点数学公式,但是都非常简单:)。

用canvas绘制一个曲线动画实例实现这样一个曲线动画

可以点击这里查看在线演示

在写代码之前,先了解一下什么是贝塞尔曲线吧。

贝塞尔曲线

贝塞尔曲线(Bezier curve)是计算机图形学中相当重要的参数曲线,它通过一个方程来描述一条曲线,根据方程的最高阶数,又分为线性贝赛尔曲线,二次贝塞尔曲线、三次贝塞尔曲线和更高阶的贝塞尔曲线。

下面详细介绍一下用得比较多的二次贝塞尔曲线和三次贝塞尔曲线

二次贝塞尔曲线

二次贝塞尔曲线由三个点P0,P1,P2来确定,这些点也被称作控制点。曲线的方程为:

用canvas绘制一个曲线动画实例

这个方程其实有它的几何意义,它表示可以通过这样的步骤来绘制一条曲线:

  • 选定一个0-1t

  • 通过P0P1计算出点Q0Q0P0 P1连成的直线上,并且length( P0, Q0 ) = length( P0, P1 ) * t

  • 同样,通过P1P2计算出Q1,使得length( P1, Q1 ) = length( P1, P2 ) * t

  • 再重复一次这个步骤,通过Q1Q2计算出B,使得length( Q0, Q1 ) = length( Q0, B ) * tB就为当前曲线上的点

注:上面的length表示两点之间的长度

用canvas绘制一个曲线动画实例
图:二次贝塞尔曲线结构

有了曲线方程,我们直接代入具体的t值就能算出点B了。

如果将t的值从0过渡到1,不断计算点B,就可以得到一条二次贝塞尔曲线:

用canvas绘制一个曲线动画实例
图:二次贝塞尔线绘制过程

在canvas中,绘制二次贝塞尔曲线的方法为

ctx.quadraticCurveTo( p1x, p1y, p2x, p2y )

其中p1x, p1y, p2x, p2y为后两个控制点(P1P2)的横纵坐标,它默认将当前路径的起点作为一个控制点(P0)。

三次贝塞尔曲线

三次贝塞尔曲线需要四个点P0,P1,P2,P3来确定,曲线方程为

用canvas绘制一个曲线动画实例

它的计算过程和二次贝塞尔曲线类似,这里不再赘述,可以看下图:

用canvas绘制一个曲线动画实例
图:三次贝塞尔曲线结构

同样,将t的值从0过渡到1,就可以绘制出一条三次贝塞尔曲线:

用canvas绘制一个曲线动画实例
图:三次贝塞尔曲线绘制过程

在canvas中,绘制三次贝塞尔曲线的方法为

ctx.bezierCurveTo( p1x, p1y, p2x, p2y, p3x, p3y )

其中p1x, p1y, p2x, p2y, p3x, p3y为后三个控制点(P1,P2P3)的横纵坐标,它默认将当前路径的起点作为一个控制点(P0)。

贝塞尔曲线的特征

在三次贝塞尔曲线后面,还有更高阶的贝塞尔曲线,同样它们绘制的过程也更加复杂

四次贝塞尔曲线

用canvas绘制一个曲线动画实例
图:四次贝塞尔曲线

五次贝塞尔曲线

用canvas绘制一个曲线动画实例图:五次贝塞尔曲线

我们可以归纳出贝塞尔曲线有几个重要的特征:

  1. n阶贝塞尔曲线需要n+1个点来确定

  2. 贝塞尔曲线是平滑的

  3. 贝塞尔曲线的起点和终点与对应控制点的连线相切

绘制贝塞尔曲线

复习完基础概念,接下来就要讲如果绘制贝塞尔曲线啦

为简单起见,我们选择使用二次贝塞尔曲线

我们先不考虑动画的事,我们先将问题简化成:给定一个起点和一个终点,需要实现一个函数,它能够绘制出一条曲线。

也就是说我们需要实现一个函数drawCurvePath,除渲染上下文ctx外(不清楚ctx是什么的同学可以先熟悉下canvas的基本概念),它接受三个参数,分别为二次贝塞尔曲线的三个控制点。我们将样式控制移到函数外,drawCurvePath只用来绘制路径。

/**
 * 绘制二次贝赛尔曲线路径
 * @param  {Object} ctx
 * @param  {Array<number>} p0
 * @param  {Array<number>} p1
 * @param  {Array<number>} p2
 */
function drawCurvePath( ctx, p0, p1, p2 ) {
    // ...
}

前文提到过,在canvas中,绘制二次贝赛尔曲线的方法是quadraticCurveTo,所以只要短短两行就能完成这个方法。

/**
 * 绘制二次贝赛尔曲线路径
 * @param  {CanvasRenderingContext2D} ctx
 * @param  {Array<number>} p0
 * @param  {Array<number>} p1
 * @param  {Array<number>} p2
 */
function drawCurvePath( ctx, p0, p1, p2 ) {
    ctx.moveTo( p0[ 0 ], p0[ 1 ] );
    ctx.quadraticCurveTo( 
        p1[ 0 ], p1[ 1 ],
        p2[ 0 ], p2[ 1 ]
    );
}

这样就完成了基本的绘制二次贝塞尔曲线的方法了。

但是函数这样设计有点小问题

如果我们是在做一个图形库,我们想给使用者提供一个绘制曲线的方法。

对于使用者来说,他只想在给定的起点和终点间间绘制一条曲线,他想要得到的曲线尽量美观,但是又不想关心具体的实现细节,如果还需要给第三个点,使用者会有一定的学习成本(至少需要弄明白什么是贝塞尔曲线)。

看到这里你可能会比较疑惑,即使是二次贝塞尔曲线也需要三个控制点,只有起点和终点怎么绘制曲线呢。

我们可以在起点和终点的垂直平分线上选一点作为第三个控制点,可以提供给使用者一个参数来控制曲线的弯曲程度,现在函数就变成了这样

/**
 * 绘制一条曲线路径
 * @param  {CanvasRenderingContext2D} ctx
 * @param  {Array<number>} start 起点
 * @param  {Array<number>} end 终点
 * @param  {number} curveness 曲度(0-1)
 */
function drawCurvePath( ctx, start, end, curveness ) {
    // ...
}

我们用curveness来表示曲线的弯曲程度,也就是第三个控制点的偏离程度。这样很容易就能计算出中间点。
现在完整的函数变成了这样:

/**
 * 绘制一条曲线路径
 * @param  {Object} ctx canvas渲染上下文
 * @param  {Array<number>} start 起点
 * @param  {Array<number>} end 终点
 * @param  {number} curveness 曲度(0-1)
 */
function drawCurvePath( ctx, start, end, curveness ) {
    // 计算中间控制点
    var cp = [
         ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
         ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
    ];
    ctx.moveTo( start[ 0 ], start[ 1 ] );
    ctx.quadraticCurveTo( 
        cp[ 0 ], cp[ 1 ],
        end[ 0 ], end[ 1 ]
    );
}

对,就这么短短几行,接下来我们就可以通过它来绘制一条曲线了,代码如下

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>draw curve</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
        <script>
            var canvas = document.getElementById( 'canvas' );
            var ctx = canvas.getContext( '2d' );
            
            ctx.lineWidth = 2;
            ctx.strokeStyle = '#000';
            ctx.beginPath();
    
            drawCurvePath( 
                ctx,
                [ 100, 100 ],
                [ 200, 300 ],
                0.4
            );
            
            ctx.stroke();
            
            function drawCurvePath( ctx, start, end, curveness ) {
                // ...
            }
        </script>
    </body>
</html>

绘制结果:

用canvas绘制一个曲线动画实例
绘制一条曲线

绘制贝塞尔曲线动画

终于来到文章的本体啦,我们的目的不是绘制一条静态的曲线,我们想绘制一条有过渡效果的曲线。

简化一下问题,那就是我们希望绘制曲线的函数还接受另一个参数,表示绘制曲线的百分比。我们定时去调用这个函数,递增百分比这个参数,就能画出动画了。

我们新增一个参数percent来表示百分比,现在函数变成了这样:

/**
 * 绘制一条曲线路径
 * @param  {Object} ctx canvas渲染上下文
 * @param  {Array<number>} start 起点
 * @param  {Array<number>} end 终点
 * @param  {number} curveness 曲度(0-1)
 * @param  {number} percent 绘制百分比(0-100)
 */
function drawCurvePath( ctx, start, end, curveness, percent ) {
    // ...
}

但是canvas提供的quadraticCurveTo方法只能绘制一条完整的二次贝赛尔曲线,没有办法去控制它只画一部分。

画完后用clearRect擦除掉一部分?这不太可行,因为很难确定要擦除的范围。如果曲线的线宽比较宽,就还需要保证擦除的边界和曲线末端垂直,问题就变得很复杂了。

现在再重新看看这张图

用canvas绘制一个曲线动画实例

我们是不是可以将percent这个参数理解成t值,然后通过贝赛尔曲线方程去计算出中间所有的点,用直线连接起来,以此模拟绘制贝赛尔曲线的一部分呢?

方法一

我们不再用canvas提供的quadraticCurveTo来绘制曲线,而是通过贝赛尔曲线的方程计算出一系列点,用多端直线来模拟曲线。

这样做的好处时,我们可以很容易的控制绘制的范围。

那么函数实现就变成了这样:

/**
 * 绘制一条曲线路径
 * @param  {Object} ctx canvas渲染上下文
 * @param  {Array<number>} start 起点
 * @param  {Array<number>} end 终点
 * @param  {number} curveness 曲度(0-1)
 * @param  {number} percent 绘制百分比(0-100)
 */
function drawCurvePath( ctx, start, end, curveness, percent ) {

    var cp = [
         ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
         ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
    ];
    
    ctx.moveTo( start[ 0 ], start[ 1 ] );
    
    for ( var t = 0; t <= percent / 100; t += 0.01 ) {

        var x = quadraticBezier( start[ 0 ], cp[ 0 ], end[ 0 ], t );
        var y = quadraticBezier( start[ 1 ], cp[ 1 ], end[ 1 ], t );
        
        ctx.lineTo( x, y );
    }
    
}

function quadraticBezier( p0, p1, p2, t ) {
    var k = 1 - t;
    return k * k * p0 + 2 * ( 1 - t ) * t * p1 + t * t * p2;    // 这个方程就是二次贝赛尔曲线方程
}

接下来就可以通过设置定时器,每隔一段时间调用一次这个方法,并且递增percent

为了动画更加平滑,我们使用requestAnimationFrame来代替定时器

<!DOCTYPE html>
<html lang="en">
    <head>
        <title>draw curve</title>
    </head>
    <body>
        <canvas id="canvas" width="800" height="800"></canvas>
        <script>
            var canvas = document.getElementById( 'canvas' );
            var ctx = canvas.getContext( '2d' );
            
            ctx.lineWidth = 2;
            ctx.strokeStyle = '#000';
            
            var percent = 0;
            
            function animate() {
                
                ctx.clearRect( 0, 0, 800, 800 );
                ctx.beginPath();

                drawCurvePath( 
                    ctx,
                    [ 100, 100 ],
                    [ 200, 300 ],
                    0.2,
                    percent
                );
    
                ctx.stroke();
    
                percent = ( percent + 1 ) % 100;
                
                requestAnimationFrame( animate );
                
            }
            
            animate();
            
            function drawCurvePath( ctx, start, end, curveness, percent ) {
                // ...
            }
        </script>
    </body>
</html>

得到的结果:

用canvas绘制一个曲线动画实例

这样基本实现了我们的需求,但它有一个问题:

测试发现,进行一次lineTo的时间和一次quadraticCurveTo的时间差不多,但是quadraticCurveTo只需要一次就能画出曲线,而使用lineTo则需要数十次。

换言之,用这样的方式绘制曲线,和我们前面的实现方式相比性能下降了数十倍之多。在绘制一条曲线时可能感觉不到区别,但是如果需要同时绘制上千条曲线,性能就会受到很大的影响。

方法二

那有没有什么方法可以做到用quadraticCurveTo来实现绘制完整曲线的一部分呢?

我们再次回到这张图

用canvas绘制一个曲线动画实例

在中间的某一时刻,例如t=0.25时,它是这样的:

用canvas绘制一个曲线动画实例

我们注意到,曲线P0-B这一段似乎也是贝赛尔曲线,它的控制点变成了P0,Q0,B

现在问题就迎刃而解了,我们只需要每次计算出Q0,B,就能得到其中一小段贝赛尔曲线的控制点,然后就可以通过quadraticCurveTo来绘制它了。

代码如下:

/**
 * 绘制一条曲线路径
 * @param  {Object} ctx canvas渲染上下文
 * @param  {Array<number>} start 起点
 * @param  {Array<number>} end 终点
 * @param  {number} curveness 曲度(0-1)
 * @param  {number} percent 绘制百分比(0-100)
 */
function drawCurvePath( ctx, start, end, curveness, percent ) {

    var cp = [
         ( start[ 0 ] + end[ 0 ] ) / 2 - ( start[ 1 ] - end[ 1 ] ) * curveness,
         ( start[ 1 ] + end[ 1 ] ) / 2 - ( end[ 0 ] - start[ 0 ] ) * curveness
    ];
    
    var t = percent / 100;
    
    var p0 = start;
    var p1 = cp;
    var p2 = end;
    
    var v01 = [ p1[ 0 ] - p0[ 0 ], p1[ 1 ] - p0[ 1 ] ];     // 向量<p0, p1>
    var v12 = [ p2[ 0 ] - p1[ 0 ], p2[ 1 ] - p1[ 1 ] ];     // 向量<p1, p2>

    var q0 = [ p0[ 0 ] + v01[ 0 ] * t, p0[ 1 ] + v01[ 1 ] * t ];
    var q1 = [ p1[ 0 ] + v12[ 0 ] * t, p1[ 1 ] + v12[ 1 ] * t ];
    
    var v = [ q1[ 0 ] - q0[ 0 ], q1[ 1 ] - q0[ 1 ] ];       // 向量<q0, q1>

    var b = [ q0[ 0 ] + v[ 0 ] * t, q0[ 1 ] + v[ 1 ] * t ];
    
    ctx.moveTo( p0[ 0 ], p0[ 1 ] );

    ctx.quadraticCurveTo( 
        q0[ 0 ], q0[ 1 ],
        b[ 0 ], b[ 1 ]
    );

}

将前面写的页面替换成上面的代码,可以看到得到的结果是一样的:

用canvas绘制一个曲线动画实例

绘制动画

现在已经解决了最关键的问题,我们可以绘制动画啦。
不过这一部分并不重要,我就不贴代码了。

完整代码可以看这里

用canvas绘制一个曲线动画实例

相关推荐:

Python之正弦曲线实现方法分析

使用CSS做贝塞尔曲线

css动画之模拟正余弦曲线的实例分享


以上是用canvas绘制一个曲线动画实例的详细内容。更多信息请关注PHP中文网其他相关文章!

声明
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系admin@php.cn
Vue和Canvas:如何实现手写签名和手势识别功能Vue和Canvas:如何实现手写签名和手势识别功能Jul 18, 2023 am 08:49 AM

Vue和Canvas:如何实现手写签名和手势识别功能引言:手写签名和手势识别功能在现代应用程序中越来越常见,它们可以为用户提供更加直观和自然的交互方式。Vue.js作为一款流行的前端框架,搭配Canvas元素可以实现这两个功能。本文将介绍如何使用Vue.js和Canvas元素来实现手写签名和手势识别功能,并给出相应的代码示例。一、手写签名功能实现要实现手写签

Canvas的优势有哪些Canvas的优势有哪些Aug 17, 2023 pm 04:52 PM

canvas的优势有强大的绘图功能、高性能、跨平台兼容性、支持多种图形格式、可以与其他Web技术集成、可以实现动态效果和可以实现复杂的图像处理。详细介绍:1、Canvas提供了丰富的绘图功能,可以绘制各种形状、线条、文本、图像等;2、Canvas在浏览器中直接操作像素,因此具有很高的性能;3、Canvas是基于HTML5标准的一部分,可以在各种现代浏览器上运行等等。

如何利用Vue和Canvas创建逼真的天气动态背景如何利用Vue和Canvas创建逼真的天气动态背景Jul 17, 2023 am 08:33 AM

如何利用Vue和Canvas创建逼真的天气动态背景引言:在现代网页设计中,动态背景效果是吸引用户眼球的重要元素之一。本文将介绍如何利用Vue和Canvas技术来创建一个逼真的天气动态背景效果。通过代码示例,你将学习如何编写Vue组件和利用Canvas绘制不同天气场景,从而实现一个独特而吸引人的背景效果。步骤一:创建Vue项目首先,我们需要创建一个Vue项目。

canvas特效有哪些canvas特效有哪些Aug 18, 2023 pm 04:12 PM

canvas特效有粒子效果、线条动画、图片处理、文字动画、音频可视化、3D效果、游戏开发等。详细介绍:1、粒子效果,通过控制粒子的位置、速度和颜色等属性来实现各种效果,如烟花、雨滴、星空等;2、线条动画,通过在画布上绘制连续的线条,创建出各种动态的线条效果;3、图片处理,通过对图片进行处理,可以实现各种炫酷的效果,如图片切换、图片特效等;4、文字动画等等特性。

canvas插件有哪些canvas插件有哪些Aug 17, 2023 pm 05:00 PM

canvas插件有Fabric.js、EaselJS、Konva.js、Three.js、Paper.js、Chart.js和Phaser。详细介绍:1、Fabric.js 是一个基于Canvas的开源 JavaScript 库,它提供了一些强大的功能;2、EaselJS是CreateJS库中的一个模块,它提供了一套简化了Canvas编程的API;3、Konva.js等等。

Vue和Canvas:如何实现视频播放器的定制化界面Vue和Canvas:如何实现视频播放器的定制化界面Jul 18, 2023 pm 02:49 PM

Vue和Canvas:如何实现视频播放器的定制化界面引言:在现代互联网时代,视频已经成为人们生活中必不可少的一部分。为了提供良好的用户体验,许多网站和应用程序都提供了自定义的视频播放器界面。本文将介绍如何使用Vue和Canvas技术实现一个定制化的视频播放器界面。一、前期准备在开始之前,您需要确保您已经安装了Vue和Canvas,并且熟悉这两种技术的基本用法

Vue和Canvas:如何实现图片的马赛克效果Vue和Canvas:如何实现图片的马赛克效果Jul 16, 2023 pm 10:17 PM

Vue和Canvas:如何实现图片的马赛克效果引言:随着Web技术的不断发展,越来越多的人开始使用Vue框架来构建交互式的前端应用。而在前端开发中,常常需要为用户提供图片处理的功能。本文将介绍如何利用Vue和Canvas实现图片的马赛克效果,为用户带来更好的视觉体验。一、马赛克效果概述马赛克效果是一种将图像的细节部分进行像素化处理,使得整个图像变得模糊和抽象

canvas引擎有哪些canvas引擎有哪些Aug 17, 2023 pm 05:29 PM

canvas引擎有Three.js、Pixi.js、EaselJS、Konva.js、Paper.js等。详细介绍:1、Pixi.js,提供了简单易用的API,支持精灵、纹理、滤镜等功能,同时还提供了丰富的工具和插件,方便开发者进行交互、动画和优化等操作;2、Pixi.js,提供了简单易用的API,支持精灵、纹理、滤镜等功能,还提供了丰富的工具和插件;3、EaselJS等等。

See all articles

热AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智能驱动的应用程序,用于创建逼真的裸体照片

AI Clothes Remover

AI Clothes Remover

用于从照片中去除衣服的在线人工智能工具。

Undress AI Tool

Undress AI Tool

免费脱衣服图片

Clothoff.io

Clothoff.io

AI脱衣机

AI Hentai Generator

AI Hentai Generator

免费生成ai无尽的。

热门文章

R.E.P.O.能量晶体解释及其做什么(黄色晶体)
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳图形设置
3 周前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您听不到任何人,如何修复音频
3 周前By尊渡假赌尊渡假赌尊渡假赌

热工具

Atom编辑器mac版下载

Atom编辑器mac版下载

最流行的的开源编辑器

Dreamweaver Mac版

Dreamweaver Mac版

视觉化网页开发工具

安全考试浏览器

安全考试浏览器

Safe Exam Browser是一个安全的浏览器环境,用于安全地进行在线考试。该软件将任何计算机变成一个安全的工作站。它控制对任何实用工具的访问,并防止学生使用未经授权的资源。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一个PHP/MySQL的Web应用程序,非常容易受到攻击。它的主要目标是成为安全专业人员在合法环境中测试自己的技能和工具的辅助工具,帮助Web开发人员更好地理解保护Web应用程序的过程,并帮助教师/学生在课堂环境中教授/学习Web应用程序安全。DVWA的目标是通过简单直接的界面练习一些最常见的Web漏洞,难度各不相同。请注意,该软件中

mPDF

mPDF

mPDF是一个PHP库,可以从UTF-8编码的HTML生成PDF文件。原作者Ian Back编写mPDF以从他的网站上“即时”输出PDF文件,并处理不同的语言。与原始脚本如HTML2FPDF相比,它的速度较慢,并且在使用Unicode字体时生成的文件较大,但支持CSS样式等,并进行了大量增强。支持几乎所有语言,包括RTL(阿拉伯语和希伯来语)和CJK(中日韩)。支持嵌套的块级元素(如P、DIV),