贝塞尔曲线、坐标变换以及图像合成
我们之前说过:canvas的元素大小与绘图环境大小不一定相同,例如我们通过css定义canvas的宽高后,改变的只是canvas元素的宽高,而绘图环境的宽高还是不变的,当我们在canvas内部作图时候,绘图环境会默认的缩放到canvas元素大小,
这时候如果我们绘制1像素那么该1像素就会被缩放同样倍数。很多时候我们需要根据用户鼠标位置去操作canvas环境。这时候获得鼠标位置就至关重要了,然后我们获得的鼠标位置如果都是相对于整个页面而言的,
那么canvas的存在就给计算鼠标位置带来不少复杂度,而且我们操作的鼠标坐标仅仅是在canvas内部,跟外部页面无关,所以利用整个页面的坐标来计算canvas内部的位置显然是不可取的。于是我们封装一个函数,
用来获得当前鼠标针对于canvas绘图环境的坐标位置。(注意这里的canvas.width是绘图环境的宽度)
function htmlToCanvas(canvas,x,y){
var box=canvas.getBoundingClientRect();
return {x:x-box.left*(canvas.width/box.width),y:y-box.top*(canvas.height/box.height)}
}
在这个函数中 我们传入三个参数:分别是canvas绘图环境以及当前鼠标位置一般就是e.clientX,e.clientY.然后我们在函数内部获得元素的宽高左右数据,返回一个对象包含xy坐标,其中xy坐标不仅仅是鼠标位置减去元素的左边距,而是要去判断有没有缩放。
乍一看 这个跟我们目前学的东西不沾边,不过后面牵扯到用户鼠标拖拽剪切等操作就离不开它了。
贝塞尔曲线以及多边形:
贝塞尔曲线
有些时候我们画曲线需要把已有的点连接起来,而贝塞尔曲线则不需要,它只需要控制点以及锚点来控制整个曲线的构造,这样绘出来的曲线不仅平滑而且可以画出很多意想不到的效果。如图实例:
ps中的绘制贝塞尔曲线工具就是钢笔,在canvas中就是贝塞尔曲线,通常canvas中贝塞尔曲线分为两种,平方贝塞尔以及立方贝塞尔曲线,其中平方贝塞尔曲线由三个点来定义,两个锚点以及一个控制点,而立方贝塞尔曲线在平方基础上添加了一个控制点。
平方贝塞尔曲线又叫二次贝塞尔曲线。是一种只向一个方向弯曲的简单曲线。我们绘制二次贝塞尔曲线用到的canvas方法为quadraticCurveTo()接受四个参数:两对xy坐标分别表示曲线的控制点以及曲线锚点,
连续的使用会将锚点与当前路径中最后一个点连接起来:无论控制点还是锚点都不会参与构成曲线,只是决定曲线走向。这里大家可以看看贝塞尔动画效果:查看动画贝塞尔贝塞尔曲线动画
三次贝塞尔曲线也就是立方贝塞尔曲线。其canvas实现方法为:bezierCurveTo()接受六个参数三对坐标,前两个是曲线控制点,最后一个是锚点。三次贝塞尔曲线是三维的也就是说可以向两个方向弯曲,其中两个控制点可以控制曲线的方向,
数学公式表示如下:
P0、P1、P2、P3四个点在平面或在三维空间中定义了三次方贝兹曲线。曲线起始于P0走向P1,并从P2的方向来到P3。P0和P1之间的间距,决定了曲线走向P3的时机。
B(t)=P0(1-t)^3+3P1t(1-t)^2+3P2t_2t^2(1-t)+P3t^3
真实开发中 如果我们需要用到贝塞尔曲线绘画股票走势等图形,建议大家先在ps里面画好然后取得控制点再回到我们程序中进行绘制。
其中的控制点我们要想得到跟屏幕一样效果应该把ps中画布大小设置成屏幕大小,然后在画布中一块区域绘制canvas。这样得到的控制点才会准确。
贝塞尔应用很多,我们可以自定义图形或者曲线去描述股票走势等。有兴趣大家可以做一做试试。
后期我会跟大家一步步做一个拖动端点来编辑贝塞尔曲线的小插件,利用它你可以获得每个控制点以及锚点坐标以便日后开发各种图形。
多边形
我们大致看一下多边形实现方式。多边形没有像贝塞尔曲线那样固定的canvas方法,这样我们就必须借用数学思想以及canvas中其他对象来实现。
其实画多边形也不过是将一个个的点用线段连接起来,这个过程中寻找点就成了最重要的事情了。例如我们画一个正八边形。
思路:八边形当然有八个点了,而且是对称的,正多边形都是内接于圆的,我们可以利用圆来定义每个点的坐标。八个点将一个圆的360度平分,也就是说每个点与点之间都差了45度。
画八边形肯定知道圆心半径,利用三角函数递归求出每个点的坐标连接起来 就over了。
总之:画多边形就是一个寻点的过程。
坐标变换:之前我们大致说过跟坐标变化相关的话题:例如平移、缩放、旋转等。。这些对应的canvas方法分别为translate、scale、以及rotate。这些方法的实现都是特定效果,
例如scale其实是利用坐标变换,将当前坐标扩大或者缩小固定的倍数从而使得变换的像素也缩放从而达到缩放效果。
我们可以用代数方程来解释各个操作效果:
平移:将坐标进行移动x0=(1+d)x,y0=(1+d)y其中d为移动参数,x0,y0为新坐标,xy为旧坐标。
缩放:x0=dx,y0=dy,将原先坐标缩放y倍数得到新坐标x0,y0;
旋转:进行旋转要用到一些三角函数:x0=x*cos(angle)-y*sin(angle),y0=y*cos(angle)+x*sin(angle);将原来坐标经过函数运算旋转一定角度得到新坐标。
可以看出我们的这些操作实际都是在代数方程基础上进行单位坐标值的改变。那么我们如果要综合起来做出这些效果应该怎么做呢?肯定大家会说每个方法都调用一遍,其实canvas中有一个方法专门为坐标转换而生。
确切的说应该是一对儿(但不定同时出现):transfrom()与setTranform();前者是将现有环境进行坐标转换,后者是在操作之前将坐标转换。
transfrom(a,b,c,d,e,f)接受六个参数,这六个参数可以在一组等式中出现:x0=ax+cy+e;y0=bx+dy+f;
分析一下:如果a=1,b=0,c=0且d=1,那么通过参数ef就可以单纯的进行坐标平移操作也就是x0=x+e;y0=y+f;
同样,如果我们将参数cebf都设置为0的话就得到x0=ax,y0=dy;这样一组等式,这不就是我们刚才的等式缩放吗。
而我们要是想让坐标进行旋转(假设是单纯旋转)angle角度,那么最终结果就是x0=x*cos(angle)-y*sin(angle),y0=y*cos(angle)+x*sin(angle);也就是说将a=cons(angle),b=sin(angle),c=-sin(angle),d=cos(angle),e=f=0;即可。
通过我们的分析我们可以看出 无论是哪种操作我们都可以通过改变transfrom中参数值来控制新坐标的位置,从而达到我们预想效果。
而且不仅仅是上述的平移缩放旋转,那三种只是利用了6个参数中的九牛一毛而已,其他大量的效果以及更加精确转换坐标同样都可以用该方法去实现。
我们加一个例子 给大家看看水平方向进行错切的效果
context.transform(1,0,0.5,1,0,0);
context.rect(100,100,100,100);
context.rect(100,200,100,100);
context.stroke();
这段代码中transform的值带入我们的等式会得到:x0=x+0.5y;y0=y;从等式可以看出我们是将x坐标平移了原来y坐标的0.5倍,y坐标值不变,于是就得到了以下效果:
图像合成:
通常情况下我们将一张纸盖在另一张纸上面肯定会覆盖另一张纸,浏览器也是这样默认实现图像合成的,canvas提供一种属性可以改变默认图像合成:globalCompositeOperation,其值可以是下表列出的任意一个值:
source-atop | source-in | source-out | source-over | destination-atop | destination-in | destination-out | destination-over | lighter | copy | xor |
为了更好的给大家演示每个属性值究竟是如何合成的。我们看一段代码程序 大家可以ctrl+c ctrl+v跑一下试试:
html代码:
<!DOCTYPE html>
<html>
<head>
<style>
#canvas{
border:1px solid #000;
position:absolute;
left:150px;
top:10px;
background:#eee;
cursor:pointer;
}</style>
</head>
<select id="mySel"size='11'>
<option value='source-atop'>source-atop</option>
<option value='source-in'>source-in</option>
<option value='source-out'>source-out</option>
<option value='source-over'>source-over</option>
<option value='destination-atop'>destination-atop</option>
<option value='destination-in'>destination-in</option>
<option value='destination-out'>destination-out</option>
<option value='destination-over'>destination-over</option>
<option value='lighter'>lighter</option>
<option value='copy'>copy</option>
<option value='xor'>xor</option>
</select>
<canvas id='canvas' width='600'height='420'></canvas
</html>
js代码:
var canvas=document.getElementById('canvas'),
context=canvas.getContext('2d'),
sel=document.getElementById('mySel');
sel.selectedIndex=10;
context.font='128pt 微软雅黑';
drawText();
function drawText(){
context.save();
context.fillStyle='red';
context.fillText('yuchao',20,250);
context.restore();
}
canvas.οnmοusemοve=function(e){
var loc=htmlToCanvas(canvas,e.clientX,e.clientY);
context.clearRect(0,0,canvas.width,canvas.height);
drawText();
context.save();
context.globalCompositeOperation=sel.value;
context.beginPath();
context.arc(loc.x,loc.y,100,0,Math.PI*2,false);
context.fillStyle='orange';
context.fill();
context.restore();
}
上面的mousemove中我们用到了开始说到的坐标转换方法。下面来看两个截图效果:
另外需要注意的是目前浏览器对于几个图像合成有两种不同支持方式:例如source-in,chrome以及safari就是我们看到的这种效果“局部合成”,而firefox与opera则是选择了全局合成方式,这种方式会将源图像范围之外的那部分擦除。
然而canvas规范目前支持的合成方式竟然是全局合成方式,不过该规范很有可能做出修改。毕竟局部合成才更符合我们预期效果。
下一篇我们着重来学习一下canvas的图像操作。像是ps中滤镜通道之类的效果等都在下一篇,敬请期待哦。