canvas翻角效果

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>canvas翻角效果</title>
<style>
.container{background: #abcdef;display: inline-block;font-size:0;position:relative;}
.container:before{content:"";position:absolute;left:50px;top:0;1px;height:102px;background:#fff;}
.container:after{content:"";position:absolute;left:0px;top:50px;102px;height:1px;background:#fff;}
*{margin:0;padding:0;}
.myCanvas{100px;height:100px;border:0px solid #000;display: inline-block;}
</style>
</head>
<body>
<div class="container">
<canvas class="myCanvas" width="100" height="100"></canvas>
</div>
<!-- <script>
var canvas = document.querySelector('.myCanvas'); //获取canvas对应dom
var ctx = canvas.getContext('2d'); //此方法较为基础 , 意为获取canvas绘画2d内容的工具(上下文)
var cw = 100; //分辨率 , 其实直接从dom上获取可能更好些
var ch = 100; //分辨率 , 其实直接从dom上获取可能更好些
 
 
// ctx.save() //保存上下文状态 (比如画笔尺寸 颜色 旋转角度)
//    ctx.restore() //返回上次保存的上下文状态
//    ctx.moveTo(x,y) //上下文移动到具体位置
//    ctx.lineTo(x,y) //上下文以划线的形式移动到某位置
//    ctx.stroke() // 画线动作
//    ctx.quadraticCurveTo() //上下文(画笔)按贝塞尔曲线移动(简单理解为可控的曲线即可)
//    ctx.arc() //画圆
//    ctx.beginPath() //开启新的画笔路径
//    ctx.closePath() //关闭当前画笔路径
//     ctx.createLinearGradient() //创建canvas渐变对象
//     ctx.fill() //对闭合区域进行填充
//     ctx.globalCompositeOperation //画笔的重叠模式
// 阴影部分
ctx.moveTo(50,0);
ctx.quadraticCurveTo(55,5,55,25);//可以理解为从(50,0)这个点划线到(55,25)这个点,中间会受到(55,5)这个店将直线像磁贴一样“吸”成曲线
ctx.lineTo(55,40);
ctx.arc(60,40,5,Math.PI,Math.PI/2,true);
ctx.lineTo(75,45);
ctx.quadraticCurveTo(95,45,100,50);
ctx.lineTo(50,0);
var gradient = ctx.createLinearGradient(50,50,75,75);
gradient.addColorStop(0,'#ccc');
gradient.addColorStop(0.7,'#111');
gradient.addColorStop(1,'#000');
ctx.fillStyle = gradient;
ctx.fill();
// 橘黄部分
ctx.beginPath();//新建路径
ctx.moveTo(50,0);
ctx.lineTo(100,50);
ctx.lineTo(100,0);
ctx.lineTo(50,0);
ctx.closePath();//闭合路径
ctx.fillStyle = '#ff6600';
ctx.fill();
// 文字绘制
var deg = Math.PI/180;
ctx.globalCompositeOperation = 'source-atop';//canvas层叠模式
ctx.beginPath();
ctx.font = '14px Arial';//设置字体大小 字体
ctx.textAlign = 'center';//字体对齐方式
ctx.translate(78,22);//移动canvas画布圆点
ctx.rotate(45*deg);//旋转画布
ctx.fillStyle = '#fff';//设置字体颜色
ctx.fillText('NEW',0,0);//文字绘制动作
ctx.closePath();

function draw(){
drawMethod();//绘制三角等内容
window.requestAnimationFrame(function(){
draw();
})
function drawMethod(){

}
}
</script> -->
<script>
var canvas = document.querySelector('.myCanvas');
        var ctx = canvas.getContext('2d');
        var cw = 100;
        var ch = 100;
        var percent = 0;
        var points = {
            x1 : 100,
            y1 : 0,
            x2 : 100,
            y2 : 0
        }
        var speed = 1;
        var aSpeed = 0.1;
        ctx.moveTo(0,0);
        ctx.strokeStyle = 'black';
        ctx.strokeWidth= 1;
        ctx.save();
        var deg = Math.PI / 180;
        
        function start(type){
            if(type === 'show'){
                points = {
                    x1 : 100,
                    y1 : 0,
                    x2 : 100,
                    y2 : 0
                }
                aSpeed = .1;
                speed = 1;
            }else{
                points = {
                    x1 : 50,
                    y1 : 0,
                    x2 : 100,
                    y2 : 50
                }
                aSpeed = -.1;
                speed = -1;
            }
            draw(points , type);
        }
        
        function draw(points , type){
            var disX = Math.floor(points.x2 - points.x1);
            var disY = Math.floor(points.y2 - points.y1);
            if(disY < 0 && type == 'hide'){
//              console.log('改展开动画了');
                ctx.clearRect(0,0,cw,ch);
                setTimeout(function(){
                    start('show');
                } , 2000)
                return ;
            }else if(disY > 50 && type == 'show'){
//              console.log('改收起动画了');
                setTimeout(function(){
                    start('hide');
                } , 2000)
                return ;
            }
            ctx.clearRect(0,0,cw,ch);
            drawPageCorShow(points , disX , disY);
            drawPageCor(points, disX , disY);
            window.requestAnimationFrame(function(){
                draw(points , type);
            })
        }
        
        function drawPageCorShow(points, disX , disY){
            ctx.save();
            ctx.beginPath();
            //闭合三角形
            ctx.moveTo(points.x1 , points.y1);
            ctx.lineTo(points.x2 , points.y2);
            ctx.lineTo(points.x2 , points.y1);
            ctx.lineTo(points.x1 , points.y1);

            ctx.closePath();
            ctx.strokeStyle = "#080";
            ctx.stroke();
            
            ctx.fillStyle = '#ff6600';
            ctx.fill();
            //重叠模式
            ctx.globalCompositeOperation = 'source-atop';
            
            ctx.beginPath();
            ctx.font = '14px Arial';
            ctx.textAlign = 'center';
            ctx.translate(78 , 22);
            ctx.rotate(45 * deg);
            ctx.fillStyle = '#fff';
            ctx.fillText('NEW' , 0 , 0);
            ctx.closePath();
            ctx.restore();
            
        }
        
        function drawPageCor(points, disX , disY){
            ctx.save();
            ctx.beginPath();
            //移動到位置 左上
            ctx.moveTo(points.x1,points.y1);
            //画第一个曲线
            ctx.quadraticCurveTo(points.x1 + (disX/10),points.y1 + disY/10 ,(points.x1 + disX/10),points.y1 + disY/2);
            //直线向下
            ctx.lineTo(points.x1 + disX / 10 , points.y2 - (disY/5));
            //半圆向右
            ctx.arc(points.x1+disX/5,points.y2 - (disY/5),disY/10,deg*180 , deg*90,true);
            // 直线向右
            ctx.lineTo(points.x2 - disX/2 , points.y2 - (disY / 10))
            //曲线向右
            ctx.quadraticCurveTo(points.x2 -disX/10,points.y2 - (disY/10) ,points.x2,points.y2 );
            //闭合图形
            ctx.lineTo(points.x1,points.y1);
            
            ctx.closePath();
            
            var gradient = ctx.createLinearGradient(points.x1 , points.y2 , points.x1 + (disX/2) , points.y1 + disY/2);
            gradient.addColorStop(0 , '#ccc');
            gradient.addColorStop(0.7 , '#111');
            gradient.addColorStop(1 , '#000');
            
            ctx.fillStyle = gradient;
            ctx.fill();
            ctx.restore();
            //更新速度位置
            points.x1 -= speed;
            points.y2 += speed;
            speed += aSpeed;
        }
        start('show');
</script>
</body>
</html>

右上角需要从无的状态撕开一个标记,且有动画过程,上图是实现的效果图,不是gif。

对这个翻角效果的难点在于没有翻开的时候露出的是dom下面的内容,实现角度来说纯dom + css动画的设计方案并没有相出一个好的对策: 于是捡起了好久之前学的入门级别的canvas:

下面说一下实现思路。

动画拆分

  • 将此动画分解成两部分,一部分是翻页出现的黑色三角区域,另一个是露出的橘色展示内容

  • 对于橘色的展示内容区域相对好一些,因为是一个规则图形,而黑色区域相对较难

先从基础canvas使用方法说起

  1.    <div class="container">

  2.        <canvas class="myCanvas" width="100" height="100"></canvas>

  3.    </div>

布局如上,这里要说一点踩过的坑是,canvas必须要设置上width 与 height,此处并非为css中的width与height,而是写在dom上的属性。 因为dom上的width与height标识了canvas的分辨率(个人理解), 所以此canvas画布分辨率为100*100,而展示尺寸是可以通过css控制。

js中首先要做的是获取canvas对象

  1.    var canvas = document.querySelector('.myCanvas'); //获取canvas对应dom

  2.    var ctx = canvas.getContext('2d'); //此方法较为基础 , 意为获取canvas绘画2d内容的工具(上下文)

  3.    var cw = 100; //分辨率 , 其实直接从dom上获取可能更好些

  4.    var ch = 100; //分辨率 , 其实直接从dom上获取可能更好些

ctx这个绘画上下文在这个教程中起到的作用至关重要,它提供了非常强大的api,比如用于画线、填充、写文字等,这样看来理解为画笔会更为简明一些。

此处效果需要用到的api如下(不做详细解释,可w3c自行查询):

  1.    ctx.save() //保存上下文状态 (比如画笔尺寸 颜色 旋转角度)

  2.    ctx.restore() //返回上次保存的上下文状态

  3.    ctx.moveTo(x,y) //上下文移动到具体位置

  4.    ctx.lineTo(x,y) //上下文以划线的形式移动到某位置

  5.    ctx.stroke() // 画线动作

  6.    ctx.quadraticCurveTo() //上下文(画笔)按贝塞尔曲线移动(简单理解为可控的曲线即可)

  7.    ctx.arc() //画圆

  8.    ctx.beginPath() //开启新的画笔路径

  9.    ctx.closePath() //关闭当前画笔路径

  10.    ctx.createLinearGradient() //创建canvas渐变对象

  11.    ctx.fill() //对闭合区域进行填充

  12.    ctx.globalCompositeOperation //画笔的重叠模式

可能方法列举的不够详尽,见谅。

首先是绘制黑色翻出的部分,图形分解为如下几部分(请根据上图脑补):

  1. 左上角向右下的半弧 ╮

  2. 然后是竖直向下的竖线 |

  3. 然后是向右的半圆 ╰

  4. 再然后是向右的横线

  5. 接着还是向右下的半弧 ╮

  6. 最后是将线连接会起点

于是第一步 我们要先将画笔移动到起始位置:

  1.       ctx.moveTo(50,0);

然后

  1.    ctx.quadraticCurveTo(55 , 5 , 55 , 25); // 可以理解为从(50,0)这个点划线到(55,25)这个点,中间会受到(55,5)这个点将直线想磁铁一样"吸"成曲线

于是第一个向右下的半弧完成,此时canvas上没有任何绘制内容,因为还没有执行过绘制方法例如stroke或fill。

接下来直线向下就是简单的移动:

  1.    ctx.lineTo(55 , 40);

这个时候我们接下来应该画向右的半圆,这个时候再用贝塞尔曲线绘制实在有些不太合适,因为从图上来看,这里完全是1/4的圆,所以要使用canvas提供的画圆的api。

  1.    ctx.arc(60 , 40 , 5 , Math.PI , Math.PI / 2 , true);

上述画圆的代码意为:以(60,40)点为圆心,5为半径,逆时针从180度绘制到90度,180度就是圆心的水平向左 到达点(55,40),与上一步连接上,然后又因为屏幕向下为正,90度在圆心正下方,所以绘制出此半圆。

于是按照相同的步骤,水平向右:

  1.    ctx.lineTo(75 , 45);


然后再次使用贝塞尔曲线用第一步的思路画出向右下的弧:

  1.    ctx.quadraticCurveTo( 95 , 45 , 100 , 50 );

同理,上述贝塞尔曲线可以理解为一条从( 75 , 45 ) 到 ( 100 , 50 )的线被 ( 95 , 45 )"吸"成曲线。

最后链接起点,闭合绘画区域:

  1.    ctx.lineTo(50 , 0);

这个时候黑色区域的翻页就画完了,然后此时开始填充颜色:

  1.    var gradient = ctx.createLinearGradient(50 , 50 , 75 , 75);

  2.    gradient.addColorStop(0 , '#ccc');

  3.    gradient.addColorStop(0.7 , '#111');

  4.    gradient.addColorStop(1 , '#000');

我们通过上述代码创建一个从( 50 , 50 )点到(75 , 75)点的线性渐变,颜色从 #ccc 到 #111 到 #000,创建高光效果。

然后填充:

  1.    ctx.fillStyle = gradient;

  2.    ctx.fill();

于是翻页效果的一半就算完成了。

至此,我要说一点我领悟的canvas的绘画"套路"。

对于上述教程中,有一步我们使用了一个词叫做闭合,闭合的概念在canvas中是真是存在的,对于fill方法来说,填充的区间是有一个空间尺寸才可以的,比如我们绘画的这个黑色的三角形,加入我们最后没有将终点与起点相连接,同样canvas会自动帮我们链接最后一笔绘画的位置到起点,强制行程闭合空间,而这样我们想再多画几个新的闭合空间就麻烦了,所以canvas提供了如下api 新建闭合路径:

  1.    ctx.beginPath(); //新建路径

  2.    ctx.closePath(); //闭合路径

所以对于我们接下来要绘制右上角橘色区域来说,我们在绘制黑色区域之前首先要做的是:

  1.    ctx.beginPath();

  2.    ...

然后在fill之前我们应该 :

  1.    ctx.closePath();

也就是说beginPath到closePath之间标识着我们自己的一个完整的绘画阶段。

那么接下来绘制右上角的橘色区域就简单很多了:

  1.    ctx.beginPath();

  2.    ctx.moveTo(50,0);

  3.    ctx.lineTo(100,50);

  4.    ctx.lineTo(100,0);

  5.    ctx.lineTo(50,0);

  6.    ctx.closePath();

  7.    ctx.fillStyle = '#ff6600';

  8.    ctx.fill();

于是右上角的橘色区域我们就绘制完成了。

文字绘制

接下来绘制"new",实际上是使用canvas简单的文本绘制,代码如下:

  1.    var deg = Math.PI / 180;

  2.    ctx.globalCompositeOperation = 'source-atop'; //canvas层叠模式

  3.    ctx.beginPath();

  4.    ctx.font = '14px Arial'; //设置字体大小 字体

  5.    ctx.textAlign = 'center'; // 字体对齐方式

  6.    ctx.translate(78 , 22);  // 移动canvas画布圆点

  7.    ctx.rotate(45 * deg);    // 旋转画布

  8.    ctx.fillStyle = '#fff';  // 设置文字颜色

  9.    ctx.fillText('NEW' , 0 , 0); //文字绘制动作

  10.    ctx.closePath();

对于上述代码中,文字的相关api是属于没有难度的,只是设置而已,需要理解的部分在于translate和rotate。

这两个方法中,translate的意思为移动canvas画布的( 0 , 0 )点到 (78,22),然后旋转45度,再将文字渲染在原点,实际就是(78,22)这个点上,此时我们对canvas的画笔做出了非常大的修改。

比如我们修改了旋转角度以及画布圆点,这种操作或许只在我们需要绘制倾斜的new的时候需要,后期可能就不需要使用了。

还好canvas的画笔是存在"状态"的,通过 ctx.save();可以保存当前画笔的状态,通过ctx.restore();可以恢复到上次画笔保存的状态。

于是我个人理解到,在开发canvas动画时,一个较好的习惯就是,在beginPath之前先ctx.save();保存画笔状态,在closePath后 ctx.restore();恢复之前的画笔状态,这样我们的每一个绘制阶段对于画笔的修改都将是不会有影响的(个人经验)。

  1.    ctx.globalCompositeOperation = 'source-atop'; //canvas层叠模式

代码中这部分是指 我们绘制的文字new与橘色三角形区域的重叠关系,此方法取值较多,此处不做过多介绍,source-atop值可以使重叠区域保留,新绘制的内容在重叠区域以外的部分消失,以此达到new在里面的效果。

到这里我们就开发好了翻角效果的完全展示的状态,那么如何让这个区域动起来呢?

此处需要使用h5提供的用于刷帧的函数 requestAnimationFrame

此方法可简单理解为16毫秒的定时器,但是厉害的是可以再各个环境中自动匹配到可达到的相对顺畅的帧率,实际并不是定时器哈。

我们需要在这个循环执行的函数中,将上述的绘制内容重复绘制,例如 :

  1.    function draw(){

  2.        drawMethod(); //绘制三角等内容

  3.        window.requestAnimationFrame(function(){

  4.            draw();

  5.        })

  6.    }

  7.    function drawMethod(){

  8.        //...

  9.    }

这样我们就可以达到刷帧的效果了,于是接着我们要做的就是控制绘制时各个数值的参数。

比如我们是以(50,0)为起点,(100,50)为终点这样的两个移动点为绘制标记的,如果我们将两个点进行存储,并且每次执行drawMethod的时候更新点的位置,然后清空canvas,再绘制新的点 那么就可以达到canvas动起来的目的了。

原文地址:https://www.cnblogs.com/lzq035/p/8005363.html