使用JavaScript来操作图片、控制音频和视频流以及画图。
能够在浏览器中动态生成复杂图形是非常重要的,因为:
- 用于在客户端生成图形的代码大小要比图片本身小很多
- 通过一些实时数据来动态生成图形,需要消耗大量的CPU周期。放到客户端做可以有效地减轻服务器的负担
- 在客户端生成图形也是符合现代Web应用的架构:服务器提供数据,然后客户端负责展现这些数据。
可伸缩的矢量图形(Scalable Vector Graphics,SVG)。SVG是一种基于XML的并且用于描述图形的语言,SVG图形可以通过JavaScript和DOM来创建和操控。
<canvas>元素是一项革命性的技术
1. 脚本化图片
<img>元素也是可以通过Js来操控的:设置元素的src属性,将其指向一个新的URL会导致浏览器载入(如果需要的话)并展示一张新的图片。
在HTML文档中动态替换图片,最常用的特效就是图片翻转,图片会随着鼠标指针划过进行替换
- 使用CSS中的:hover伪类,替换元素的背景图片来实现。
- 当鼠标指针经过或者离开<img>元素时候,事件处理程序会重新设置其src属性。
<img
src="./address.png"
onmouseover="this.src='./youbian.png'"
onmouseout="this.src='./address.png'"
/>
像图片翻转这样的效果需要较高响应度。这也意味着需要确保一些必要的图片要预提取,让浏览器缓存起来。
- 首先利用Image()构造函数来创建一个屏幕外图片对象,之后,将该对象的src属性设置成期望的URL。
<script> (new Image()).src = "./youbian.png"; </script>
一开始,由于图片元素并没有添加到文档中,它是不可见的,但是浏览器还是会加载图片并将其缓存起来。之后当设置成同样的URL来显示该屏幕内图片的时候,它就能很快从浏览器缓存中加载,而不需要再通过网络加载。
2. 脚本化音频和视频
HTML5引入的<audio>和<video>元素不再需要使用插件(像Flash)来在HTML文档中嵌入音频和视频
<audio src="background_music.mp3" />
<video src="https://www.w3school.com.cn/i/movie.ogg" controls width="320" height="240"></video>
<audio>和<video>元素支持一个controls属性。如果设置了该属性(或者对应的JavaScript属性设置为true),它们将会显示一系列播放控件,包括播放、暂停按钮、音量控制等。
HTML5中的媒体API同样也允许使用Audio()构造函数,并将媒体源URL作为参数,来创建一个屏幕外音频元素:
new Audio("chime.wav").play();//载入并播放声音效果
//视频元素是没有类似Video()这样的构造函数的。
2.1 支持播放
想要测试一个媒体元素能否播放指定类型的媒体文件,可以调用canPlayType()方法并将媒体的MIME类型传递进去。如果它不能播放该类型的媒体文件,该方法会返回一个空的字符串(一个假值);反之,它会返回一个字符串:"maybe"或者"probably"。
2.2 控制媒体播放
- <audio>和<video>元素最重要的方法是play()和pause()方法,它们用来控制媒体开始和暂停媒体的播放:
- 还可以通过设置currentTime属性来进行定点播放。该属性指定了播放器应该跳过播放的时间(单位为秒),可以在媒体播放或者暂停的时候设置该属性。
- (initialTime和duration属性确定了currentTime的有效取值范围
- volume属性表示播放音量,介于0(静音)~1(最大音量)之间。
- 将muted属性设置为true则会进入静音模式,设置为false则会恢复之前指定的音量继续播放。
- playbackRate属性用于指定媒体播放的速度。该属性值为1.0表示正常速度,大于1则表示“快进”,0~1之间的值则表示“慢放”。
- controls属性指定是否在浏览器中显示播放控件。设置该属性值为true表示显示控件,反之表示隐藏控件。
- loop属性是布尔类型,它指定媒体是否需要循环播放,true表示需要循环播放,false则表示播放到最后就停止。
- preload属性指定在用户开始播放媒体前,是否或者多少媒体内容需要预加载。该属性值为"none"则表示不需要预加载数据。为"metadata"则表示诸如时长、比特率、帧大小这样的元数据而不是媒体内容需要加载。
- autoplay属性指定当已经缓存足够多的媒体内容时是否需要自动开始播放。
2.3 查询媒体状态
<audio>和<video>元素有一些只读属性,描述媒体以及播放器当前的状态:
- 如果播放器暂停,那么paused属性的值就为"true"。
- 如果播放器正在跳到一个新的播放点,那么seeking属性的值就为"true"。
- 如果播放器播放完媒体并且停下来,那么ended属性的值就为"true"(如果设置loop属性值为true,那么ended属性值永远不为"true"。)
- duration属性指定了媒体的时长,单位是秒。如果在媒体元数据还未载入前查询该属性,它会返回NaN。对于像Internet广播这样有无限时长的流媒体而言,该属性会返回Infinity。
- initialTime属性指定了媒体的开始时间,单位也是秒。对于固定时长的媒体剪辑而言,该属性值通常是0。
- played属性返回已经播放的时间段。
- buffered属性返回当前已经缓冲的时间段,
- seekable属性则返回当前播放器需要跳到的时间段。 (可以使用这些属性来实现一个进度条,显示currentTime、duration以及媒体的播放量和缓冲量。)
played、buffered和seekable都是TimeRanges对象。每个对象都有一个length属性以及start()和end()方法,前者表示当前的一个时间段,后者分别返回当前时间段的起始时间点和结束时间点
readyState、networkState和error,它们包含<audio>和<video>元素更加底层的一些状态细节。每个属性都是数字类型的,而且为每个有效值都定义了对应的常量。
- readyState属性指定当前已经加载了多少媒体内容,因此同时也暗示着是否已经准备好可以播放了
- NetworkState属性指定媒体元素是否使用网络或者为什么媒体文件不使用网络:
- 当在加载媒体或者播放媒体过程中发生错误时,浏览器就会设置<audio>或者<video>元素的error属性。
2.3 媒体相关事件
<audio>和<video>元素在它们状态发生改变的时候,都会触发一些相应的事件,这些事件不能通过属性来注册事件,只能通过<audio>和<video>元素的addEventListener()方法来注册处理程序函数。
3. SVG可伸缩矢量图形
SVG是一种用于描述图形的XML语法。Scalable Vector Graphics,其中"vector"一词表示它完全不同于诸如GIF、JPEG和PNG(用像素值来描绘的矩阵)光栅图像格式。
一个"SVG"图形是对画该图形时的必要路径的一种精准、分辨率无关(因此是可伸缩的)的描述。
一个简单的SVG文件如下所示:
<!DOCTYPE html>
<html>
<body>
<div id="map">
<!--SVG图形一开始声明命名空间-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000">
<!--图形的坐标系-->
<defs>
<!--设置后面要用到的一些定义-->
<linearGradient id="fade">
<!--将一种渐变色命名为"fade"-->
<stop offset="0%" stop-color="#008" />
<!--深蓝-->
<stop offset="100%" stop-color="#ccf" />
<!--渐变到浅蓝-->
</linearGradient>
</defs>
<!--画一个具有宽的黑色边框并且渐变色为填充色的矩形-->
<rect
x="100"
y="200"
width="800"
height="600"
stroke="black"
stroke-width="25"
fill="url(#fade)"
/>
</svg>
</div>
</body>
</html>
HTML5将XML和HTML的区别进一步缩小,允许SVG(和MathML)标记直接在HTML文件中使用,不需要命名空间的声明或者标签前缀:
<!DOCTYPE html>
<html>
<body>
This is a red square:<svg width="10" height="10">
<rect x="0" y="0" width="10" height="10" fill="red" />
</svg>
This is a blue circle:<svg width="10" height="10">
<circle cx="5" cy="5" r="5" fill="blue" />
</svg>
</body>
</html>
SVG就是一种XML语法,因此画SVG图形其实就相当于是在使用DOM创建相应的XML元素。
3.1 SVG绘制饼状图
<!DOCTYPE html>
<html>
<script>
/**创建一个<svg>元素,并在其中绘制一个饼状图
*参数:
*data:用于绘制的数字类型的数组,数组每一项都表示饼状图的一个楔
*width,height:SVG图形的大小,单位为像素
*cx,cy,r:饼状图的圆心以及半径
*colors:一个包含HTML颜色信息的数组,每种颜色代表饼状图每个楔的颜色
*labels:一个标签数组,该信息说明饼状图中每个楔代表的含义
*lx,ly:饼状图的左上角
*返回:
*一个保存饼状图的<svg>元素
*调用者必须将返回的元素插入到文档中
*/
function pieChart(data, width, height, cx, cy, r, colors, labels, lx, ly) {
//这个是表示svg元素的XML命名空间
var svgns = "http://www.w3.org/2000/svg";
//创建一个<svg>元素,同时指定像素大小和用户坐标
var chart = document.createElementNS(svgns, "svg:svg");
chart.setAttribute("width", width);
chart.setAttribute("height", height);
chart.setAttribute("viewBox", "0 0 " + width + " " + height); //累加data的值,以便于知道饼状图的大小
var total = 0;
for (var i = 0; i < data.length; i++) total += data[i]; //现在计算出饼状图每个分片的大小,其中角度以弧度制计算
var angles = [];
for (var i = 0; i < data.length; i++)
angles[i] = (data[i] / total) * Math.PI * 2; //遍历饼状图的每个分片
startangle = 0;
for (var i = 0; i < data.length; i++) {
//这里表示楔的结束位置
var endangle = startangle + angles[i]; //计算出楔和圆相交的两个点
//这些计算公式都是以12点钟方向为0o
//顺时针方向角度递增
var x1 = cx + r * Math.sin(startangle);
var y1 = cy - r * Math.cos(startangle);
var x2 = cx + r * Math.sin(endangle);
var y2 = cy - r * Math.cos(endangle); //这个标记表示角度大于半圆
//此标记在绘制SVG弧形组件的时候需要
var big = 0;
if (endangle - startangle > Math.PI) big = 1; //使用<svg:path>元素来描述楔
//要注意的是,使用createElementNS()来创建该元素
var path = document.createElementNS(svgns, "path"); //下面的字符串包含路径的详细信息
var d =
"M" +
cx +
"," +
cy + //从圆心开始
"L" +
x1 +
"," +
y1 + //画一条到(x1,y1)的线段
"A" +
r +
"," +
r + //再画一条半径为r的弧
" 0 " +
big +
" 1 " + //弧的详细信息
x2 +
"," +
y2 + //弧到(x2,y2)结束
"Z"; //当前路径到(cx,cy)结束
//设置<svg:path>元素的属性
console.log("d" + i + "=", d); //***d属性
path.setAttribute("d", d); //设置路径
path.setAttribute("fill", colors[i]); //设置楔的颜色
path.setAttribute("stroke", "black"); //楔的外边框为黑色
path.setAttribute("stroke-width", "2"); //两个单位宽
chart.appendChild(path); //将楔加入到饼状图中
//当前楔的结束就是下一个楔的开始
startangle = endangle; //现在绘制一些相应的小方块来表示图例
var icon = document.createElementNS(svgns, "rect");
icon.setAttribute("x", lx); //定位小方块
icon.setAttribute("y", ly + 30 * i);
icon.setAttribute("width", 20); //设置小方块的大小
icon.setAttribute("height", 20);
icon.setAttribute("fill", colors[i]); //填充小方块的颜色和对应的楔的颜色相同
icon.setAttribute("stroke", "black"); //子外边框颜色也相同
icon.setAttribute("stroke-width", "2");
chart.appendChild(icon); //添加到饼状图中
//在小方块的右边添加标签
var label = document.createElementNS(svgns, "text");
label.setAttribute("x", lx + 30); //定位标签文本
label.setAttribute("y", ly + 30 * i + 18); //文本样式属性还可以通过CSS来设置
label.setAttribute("font-family", "sans-serif");
label.setAttribute("font-size", "16"); //在<svg:text>元素中添加一个DOM文本节点
label.appendChild(document.createTextNode(labels[i]));
chart.appendChild(label); //将文本添加到饼状图中
}
return chart;
}
</script>
<body
onload="document.body.appendChild(
pieChart([12,23,34,45],640,400,200,200,150,
['red','blue','yellow','green'],['North','South','East','West'],400,100));"
>
</body>
</html>
SVG路径中的d属性
3.2 SVG绘制时钟
SVG提供了一个范围广泛stroke 属性:
- stroke
- stroke-width
- stroke-linecap
- stroke-dasharray
<!DOCTYPE html>
<html>
<head>
<title>Analog Clock</title>
<script>
function updateTime() {
//更新SVG时钟来显示当前时间
var now = new Date(); //当前时间
var min = now.getMinutes(); //分钟
var hour = (now.getHours() % 12) + min / 60; //转换成可以在时钟上表示的时间
var minangle = min * 6; //每6o表示一分钟
var hourangle = hour * 30; //每30o表示一个小时
//获取表示时钟时针和分针的SVG元素
var minhand = document.getElementById("minutehand");
var hourhand = document.getElementById("hourhand"); //设置这些元素的SVG属性,将它们移动到钟面上
minhand.setAttribute("transform", "rotate(" + minangle + ",50,50)");
hourhand.setAttribute("transform", "rotate(" + hourangle + ",50,50)"); //每一分钟更新下时钟显示时间
setTimeout(updateTime, 60000);
}
</script>
<style>
/*下面定义的所有CSS样式都会作用在SVG元素上*/
#clock {
/*用于时钟的全局样式*/
stroke: black; /*黑线*/
stroke-linecap: round; /*圆角*/
fill: #eef; /*以浅蓝灰色为背景*/
}
#face {
stroke-width: 3px;
} /*时钟的外边框*/
#ticks {
stroke-width: 2;
} /*标记每个小时的线段*/
#hourhand {
stroke-width: 5px;
} /*相对较粗的时针*/
#minutehand {
stroke-width: 3px;
} /*相对较细的分针*/
#numbers {
/*如何绘制数字*/
font-family: sans-serif;
font-size: 7pt;
font-weight: bold;
text-anchor: middle;
stroke: none;
fill: black;
}
</style>
</head>
<body onload="updateTime()">
<!--viewBox是坐标系,width和height是指屏幕大小-->
<svg id="clock" viewBox="0 0 100 100" width="500" height="500">
<defs>
<!--定义下拉阴影的滤镜-->
<filter id="shadow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur in="SourceAlpha" stdDeviation="1" result="blur" />
<feOffset in="blur" dx="1" dy="1" result="shadow" />
<feMerge>
<feMergeNode in="SourceGraphic" />
<feMergeNode in="shadow" />
</feMerge>
</filter>
</defs>
<circle id="face" cx="50" cy="50" r="45" />
<!--钟面-->
<g id="ticks">
<!--12小时的刻度-->
<line x1="50" y1="5.000" x2="50.00" y2="10.00" />
<line x1="72.50" y1="11.03" x2="70.00" y2="15.36" />
<line x1="88.97" y1="27.50" x2="84.64" y2="30.00" />
<line x1="95.00" y1="50.00" x2="90.00" y2="50.00" />
<line x1="88.97" y1="72.50" x2="84.64" y2="70.00" />
<line x1="72.50" y1="88.97" x2="70.00" y2="84.64" />
<line x1="50.00" y1="95.00" x2="50.00" y2="90.00" />
<line x1="27.50" y1="88.97" x2="30.00" y2="84.64" />
<line x1="11.03" y1="72.50" x2="15.36" y2="70.00" />
<line x1="5.000" y1="50.00" x2="10.00" y2="50.00" />
<line x1="11.03" y1="27.50" x2="15.36" y2="30.00" />
<line x1="27.50" y1="11.03" x2="30.00" y2="15.36" />
</g>
<g id="numbers">
<!--标记重要的几个刻度值-->
<text x="50" y="18">12</text>
<text x="85" y="53">3</text>
<text x="50" y="88">6</text>
<text x="15" y="53">9</text>
</g>
<!--初始绘制成竖直的指针,之后通过JavaScript代码来做旋转-->
<g id="hands" filter="url(#shadow)">
<!--给指针添加阴影-->
<line id="hourhand" x1="50" y1="50" x2="50" y2="24" />
<line id="minutehand" x1="50" y1="50" x2="50" y2="20" />
</g>
</svg>
</body>
</html>
4 <canvas>画布
<canvas>元素自身是没有任何外观的,但是它在文档中创建了一个画板,同时还提供了很多强大的绘制客户端JavaScript的API。
- <canvas>元素和SVG之间一个重要的区别是:使用canvas来绘制图形是通过调用它提供的方法而使用SVG绘制图形是通过构建一棵XML元素树来实现的。
- 使用SVG来绘制图形,可以很简单地通过移除相应的元素来编辑图片。而使用<canvas>来绘制,要移除图片中的元素就不得不把当前的擦除再重新绘制一遍。
- Canvas的绘制API是基于JavaScript的,并且相对比较简洁(不像SVG语法那么复杂) 大部分的画布绘制API都不是在<canvas>元素自身上定义的,而是定义在一个“绘制上下文”对象上,获取该对象可以通过调用画布对象的getContext()方法。调用画布的getContext()方法时,传递一个"2d"参数,会获得一个CanvasRenderingContext2D对象,使用该对象可以在画布上绘制二维图形。
画布元素和它的上下文对象是两个完全不同的对象。由于CanvasRenderingContext2D名字太长了,简称为“上下文对象”。同样地,“画布API”指的也就是CanvasRenderingContext2D对象的方法。
如下代码是一个使用画布API的简单例子,它在<canvas>元素中绘制一个红色的正方形和一个蓝色的圆
<!DOCTYPE html>
<html>
<body>
This is a red square:<canvas id="square" width="10" height="10"></canvas>.
This is a blue circle:<canvas id="circle" width="10" height="10"></canvas>.
<script>
var canvas = document.getElementById("square"); //获取第一个画布元素
var context = canvas.getContext("2d"); //获取2D绘制上下文
context.fillStyle = "#f00"; //设置填充色为红色
context.fillRect(0, 0, 10, 10); //填充一个正方形
canvas = document.getElementById("circle"); //第二个画布元素
context = canvas.getContext("2d"); //获取它的绘制上下文
context.beginPath(); //开始一条新的路径
context.arc(5, 5, 5, 0, 2 * Math.PI, true); //将圆形添加到该路径中
context.fillStyle = "#00f"; //设置填充色为蓝色
context.fill(); //填充路径
</script>
</body>
</html>
- 相比SVG使用一个包含了字母和数字的字符串来描述路径,画布API是通过一系列方法调用来定义路径的,如上述代码中的beginPath()和arc()方法调用。
- 一旦定义了路径,其他的诸如fill()这样的方法就可以在该路径上操作了。
- 而像fillStyle这样的上下文对象的属性则是指定了如何进行这些操作。
下面大部分的<canvas>例子都使用到了变量c。该变量保存画布的CanvasRenderingContext2D对象
var canvas=document.getElementById("my_canvas_id");
var c=canvas.getContext('2d');
4.1 绘制线段stoke()和填充多边形fill()
要在画布上绘制线段以及填充这些线段闭合的区域,从定义一条路径开始。路径有许多子路径组成,子路径又是由两个或多个点之间连接而成的线段组成(或者后面将介绍的曲线段)。
- 调用beginPath()方法开始定义一条新的路径,
- 而调用moveTo()方法则开始定义一条新的子路径。
- 一旦使用moveTo()方法确定了子路径的起点,接下来就可以调用lineTo()方法来将该点与新的一个点通过直线连接起来。
c.beginPath(); //开始一条新路径
c.moveTo(100, 100); //从(100,100)开始定义一条新的子路径
c.lineTo(200, 200); //从(100,100)到(200,200)绘制一条线段
c.lineTo(100, 200); //从(200,200)到(100,200)绘制一条线段
上述代码只是简单地定义一条路径,并没有在画布上绘制任何图形。
- 要在路径中绘制(或者勾勒)两条线段,可以通过调用stroke()方法,要填充这些线段闭合的区域可以通过调用fill()方法:
c.stroke();//绘制三角形的两条边
要注意的是上述定义的子路径是“未闭合”的。它只包含两条线段,线段的终点并没有和起点汇合。也就是,它并没有闭合一个区域。对于这样“未闭合”的子路径,调用fill()方法填充的时候,会假设子路径的终点和子路径的起点是连接起来(但实际上没有连接)。
c.fill();//填充一个三角形区域
- 想要勾勒出上述三角形的三条边,可以调用closePath()方法将子路径的起点和终点真正连接起来
c.closePath();
关于stoke()方法和fill()方法还有另外非常重要的两点。
- 第一点是:这两个方法都是作用在当前路径上的所有子路径。但需要先定义路径,再调用stoke()、fill()
- 第二点是:stroke()方法和fill()方法都不更改当前路径。可以调用fill()方法,但是之后调用stroke()方法时候当前路径不变。完成一条路径后要再重新开始另一条路径,必须要记得调用beginPath()方法。如果没有调用beginPath()方法,那么之后添加的所有子路径都是添加在已有路径上,并且有可能重复绘制这些子路径。
例子:使用moveTo()、lineTo()和closePath()方法绘制规则多边形
<!DOCTYPE html>
<html>
<body>
<canvas id="my_canvas_id" width="800px" height="800px"></canvas>.
</body>
<script>
var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext("2d");
//定义一个以(x,y)为中心,半径为r的规则n边形
//每个顶点都是均匀分布在圆周上
//将第一个顶点放置在最上面,或者指定一定角度
//除非最后一个参数是true,否则顺时针旋转
function polygon(c, n, x, y, r, angle, counterclockwise) {
angle = angle || 0;
counterclockwise = counterclockwise || false;
c.moveTo(
x + r * Math.sin(angle), //从第一个顶点开始一条新的子路径
y - r * Math.cos(angle)
); //使用三角法计算位置
var delta = (2 * Math.PI) / n; //两个顶点之间的夹角
for (var i = 1; i < n; i++) {
//循环剩余的每个顶点
angle += counterclockwise ? -delta : delta; //调整角度
c.lineTo(
x + r * Math.sin(angle), //以下个顶点为端点添加线段
y - r * Math.cos(angle)
);
}
c.closePath(); //将最后一个顶点和起点连接起来
}
//开始一个新的路径并添加一条多边形子路径
c.beginPath();
polygon(c, 3, 50, 70, 50); //三角形
polygon(c, 4, 150, 60, 50, Math.PI / 4); //正方形
polygon(c, 5, 255, 55, 50); //五边形
polygon(c, 6, 365, 53, 50, Math.PI / 6); //六边形
polygon(c, 4, 365, 53, 20, Math.PI / 4, true); //六边形中的小正方形
//设置属性来控制图形外观
c.fillStyle = "#ccc"; //内部使用浅灰色
c.strokeStyle = "#008"; //深蓝色外边框
c.lineWidth = 5; //5个像素宽
//调用如下函数绘制所有这些多边形(每个分别定义在自己的子路径中)
c.fill(); //填充图形
c.stroke(); //勾勒外边框
</script>
</html>
要注意的是上述例子绘制了一个内部包含正方形的六边形。正方形和六边形是两条独立的子路径,但它们互相重叠。
- 当单条子路径与自身相交时,画布会采用“非零绕数原则”测试来判断它们哪些区域在路径里面,哪些在外面。 要检测一个点P是否在路径的内部,使用非零绕数原则:
- 想象一条从点P出发沿着任意方向无限延伸(或者一直延伸到路径所在的区域外某点)的射线。
- 现在从0开始初始化一个计数器,然后对所有穿过这条射线的路径进行枚举。
- 每当一条路径顺时针方向穿过射线的时候,计数器就加1;反之,就减1。
- 最后,枚举完所有的路径之后,如果计数器的值不是0,那么就认为P是在路径内。反之,如果计数器的值是0,则认为P在路径外。
在上述例子中,由于六边形的顶点是沿着顺时针方向来连接的,而正方形顶点则是沿着逆时针连接的,因此根据“非零绕数原则”,认为内部正方形在路径外,因此对内部的正方形不进行填充。换句话说,如果正方形也沿着顺时针方向连接的话,调用fill()方法的时候就会对正方形也进行填充了。
// polygon(c, 4, 365, 53, 20, Math.PI / 4, true); //六边形中的小正方形
polygon(c, 4, 365, 53, 20, Math.PI / 4, false); //六边形中的小正方形
4.2 图形属性
画布的上下文对象的fillStyle、strokeStyle以及lineWidth属性。这些属性都是图形属性,分别指定了调用fill()和stroke()时候要采用的颜色以及调用stroke()方法绘制线段时的线段宽度。
要注意的是,这些参数不是传递给fill()和stroke()方法的,而是作为画布的通用图形状态。
可以在调用绘制形状方法前,设置上下文对象的strokeStyle、fillStyle属性。这种将图形状态和绘制指令分离的思想是画布API中很重要的概念,同时也和通过在HTML文档中应用CSS样式来实现表现和内容分离是类似的。
画布API中在CanvasRenderingContext2D对象上定义了15个图形属性。
画布API在上下文对象上定义图形属性,每个<canvas>元素只有一个上下文对象,因此每次调用getContext()方法都会返回相同的CanvasRenderingContext2D对象
尽管画布API只允许一次设置单一的图形属性集合,但是它允许保存当前图形状态,这样就可以在多个状态之间切换,之后也可以很方便地恢复。
- 调用save()方法会将当前图形状态压入用于已保存状态的栈上。
- 调用restore()方法会从栈中弹出并恢复最近一次保存的状态。
- 上面表格中所有图形属性都是已保存状态的一部分,包括当前的转换信息以及裁剪区域等信息都是已保存状态的一部分。
- 但是,很重要的一点是:当前定义的路径以及不属于图形状态的当前点都不能保存和恢复。
图形状态管理工具--保存和恢复
//恢复最后一次保存的图形状态,但是让该状态从栈中弹出
CanvasRenderingContext2D.prototype.revert = function() {
this.restore(); //恢复最后一次保存的图形状态
this.save(); //再次保存它以便后续使用
return this; //允许方法链
};
//通过o对象的属性来设置图形属性
//或者,如果没有提供参数,就以对象的方式返回当前属性
//要注意的是,它不处理变换和裁剪区域
CanvasRenderingContext2D.prototype.attrs = function(o) {
if (o) {
for (var a in o) //遍历o对象中的每个属性
this[a] = o[a]; //将它设置成图形属性
return this; //启用方法链
} else
return {
fillStyle: this.fillStyle,
font: this.font,
globalAlpha: this.globalAlpha,
globalCompositeOperation: this.globalCompositeOperation,
lineCap: this.lineCap,
lineJoin: this.lineJoin,
lineWidth: this.lineWidth,
miterLimit: this.miterLimit,
textAlign: this.textAlign,
textBaseline: this.textBaseline,
shadowBlur: this.shadowBlur,
shadowColor: this.shadowColor,
shadowOffsetX: this.shadowOffsetX,
shadowOffsetY: this.shadowOffsetY,
strokeStyle: this.strokeStyle
};
};
4.3 画布的尺寸和坐标
- <canvas>元素的width以及height属性决定了画布的尺寸。
- 画布的默认坐标系是以画布最左上角为坐标原点(0,0)。越往右X轴的数值越大,越往下Y轴的数值越大。
- 画布上的点可以使用浮点数来指定坐标,但是它们不会自动转换成整型值
- 画布采用反锯齿的方式来模拟部分填充的像素。(反锯齿(anti-aliasing)绘图给我们带来更好的视觉体验,其实质就是把要绘制的颜色边缘和背景颜色做适当的融合)
画布的尺寸是不能随意更改的,除非完全重置画布。重置画布的width属性或者height属性(哪怕重置的时候属性值不变),都会清空整个画布,擦除当前的路径并且会重置所有的图形属性(包括当前的变换和裁剪区域)为初始状态。
4.4 坐标系转换
在默认坐标系中,每一个点的坐标都是直接映射到一个CSS像素上(CSS像素之后再映射到一个或者多个设备像素)。
除了默认的坐标系之外,每个画布还有一个“当前变换矩阵”,作为图形状态的一部分。该矩阵定义了画布的当前坐标系。
- 当指定了一个点的坐标后,画布的大部分操作都会将该点映射到当前的坐标系中,而不是默认的坐标系。
- 当前变换矩阵是用来将指定的坐标转换成为默认坐标系中的等价坐标。 通过调用setTransform()方法能够直接设置画布的变换矩阵,但是通过转换、旋转和缩放操作更容易实现坐标系变换。 要注意的是,坐标的变换还影响了文本和线段的绘制。
- 调用translate()方法只是简单地将坐标原点进行上、下、左、右移动。
- 调用rotate()方法会将坐标轴根据指定角度进行顺时针旋转(画布API总是以弧度制来表示角度。要将角度制转换成弧度制,可以通过Math.PI来对180进行乘除来实现)。
- 调用scale()方法实现对X轴或者Y轴上的距离进行延长和缩短。
- 调用scale()方法的时候传递负值会实现以坐标原点做参照点将坐标轴进行翻转,就好像是镜子中的镜像 setTransform()方法和transform()方法接受同样的参数,但不同的是,前者不是对当前坐标系进行变换,而是对默认坐标系进行变换,并将结果映射到新的坐标系中。setTransform()对临时将画布重置为默认坐标系是很有用的:
c.save();//保存当前坐标系
c.setTransform(1,0,0,1,0,0);//恢复到默认坐标系
//使用默认的CSS像素坐标进行操作
c.restore();//恢复保存的坐标系
4.4.1 坐标系变换例子
通过递归调用translate()方法、rotate()方法以及scale()方法来实现绘制科赫雪花分形
<!DOCTYPE html>
<html>
<body>
<canvas id="my_canvas_id" width="800px" height="800px"></canvas>.
</body>
<script>
var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext("2d");
var deg = Math.PI / 180; //用于角度制到弧度制的转换
//在画布的上下文c中,以左下角的点(x,y)和边长len,绘制一个n级别的科赫雪花分形
function snowflake(c, n, x, y, len) {
c.save(); //保存当前变换
c.translate(x, y); //变换原点为起始点
c.moveTo(0, 0); //从新的原点开始一条新的子路径
leg(n); //绘制雪花的第一条边
c.rotate(-120 * deg); //现在沿着逆时针方向旋转120o
leg(n); //绘制第二条边
c.rotate(-120 * deg); //再次旋转
leg(n); //画最后一条边
c.closePath(); //闭合子路径
c.restore(); //恢复初始的变换
//绘制n级别的科赫雪花的一条边
//此函数在画完一条边的时候就离开当前点,
//然后通过坐标系变换将当前点又转换成(0,0,)
//这意味着画完一条边之后可以很简单地调用rotate()进行旋转
function leg(n) {
c.save(); //保存当前坐标系变换
if (n == 0) {
//不需要递归的情况下:
c.lineTo(len, 0); //就绘制一条水平线段
} else {
//递归情况下:绘制4条子边,类似这个样子:-\/-
c.scale(1 / 3, 1 / 3); //子边长度为原边长的1/3
leg(n - 1); //递归第一条子边
c.rotate(60 * deg); //顺时针旋转60o
leg(n - 1); //第二条子边
c.rotate(-120 * deg); //逆时针旋转120o
leg(n - 1); //第三条子边
c.rotate(60 * deg); //通过旋转回到初始状态
leg(n - 1); //最后一条边
}
c.restore(); //恢复坐标系变换
c.translate(len, 0); //但是通过转换使得边的结束点为(0,0)
}
}
snowflake(c, 0, 5, 115, 125); //0级别的雪花就是一个三角形
snowflake(c, 1, 145, 115, 125); //1级别的雪花就是一个六角星
snowflake(c, 2, 285, 115, 125); //依次类推
snowflake(c, 3, 425, 115, 125);
snowflake(c, 4, 565, 115, 125); //4级别的雪花看起来真的像一朵雪花了
c.stroke(); //勾勒当前复杂的路径
</script>
</html>
4.5 绘制、填充曲线
路径由子路径组成,子路径又由连接的点组成。路径中,点与点之间并不总是通过直线段连接的。
CanvasRenderingContext2D对象定义了一些方法,用于在子路径中添加新的点,并用曲线将当前点和新增的点连接起来。
arc(x,y,radius,a,b,anclockwise)
context.arc(x,y,radius,a,b,anclockwise)
x : 起始点横坐标
y : 起始点纵坐标
radius : 半径
a : 开始角度
b : 结束角度
anticlockwise : 是否逆时针
- 在当前子路径中添加一条弧。
- 它首先将当前点和弧形的起点用一条直线连接,然后用圆的一部分来连接弧形的起点和终点,并把弧形终点作为新的当前点。
- 要绘制一个弧形需要指定6个参数:圆心的X、Y坐标、圆的半径、弧形的起始和结束的角度以及弧形的方向(顺时针还是逆时针)
arcTo(x1,y1,x2,y2,r)绘制一条直线和一段圆弧(和arc()方法一样),但是,不同的是,绘制圆弧的时候指定的参数不同
context.arcTo(x1,y1,x2,y2,r)
x1:起始点横坐标
y1:起始点纵坐标
x2:结束点横坐标
y2:结束点纵坐标
r:半径
- arcTo()方法参数需要指定点P1和P2以及半径。
- 绘制的圆弧有指定的半径并且和当前点到P1的直线以及经过P1和P2的直线都相切。
- 此种绘制圆弧的方法看似有点儿奇怪,但是对于绘制带有圆角的形状是非常有用的。
- 当指定的半径为0时,此方法只会绘制一条从当前点到P1的直线。
- 而当半径值非零时,此方法会绘制一条从当前点到P1的直线,然后将这条直线按照圆形形状变成曲线,一直到它指向P2方向。
bezierCurveTo(x1, y1, x2, y2, x, y) - 在当前子路径中添加一个新的点,并利用三次贝赛尔曲线将它和当前点相连。
- (x1,y1)控制点C1,(x2,y2)控制点C2,(x,y)即为结束点P
- 曲线的形状由两个“控制点”C1和C2确定。曲线从当前点开始,沿着C1点的方向延伸,再沿着C2的方向延伸一直到点P。
- 曲线在这些点之间的过渡都是很平滑的。最后点P会成为当前点。
quadraticCurveTo(cpx, cpy, x, y) - 此方法和bezierCurveTo()方法类似,不同的是它使用的是二次贝塞尔曲线而不是三次贝塞尔曲线并且只有一个控制点。
- 方法参数包含两个点,一个是(cpx,cpy)控制点,这一点控制曲线(Curve)的角度
- (x,y)表示终点
4.5.1 在路径中添加曲线
<!DOCTYPE html>
<html>
<body>
<canvas id="my_canvas_id" width="800px" height="800px"></canvas>.
</body>
<script>
var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext("2d");
//一个工具函数,用于将角度从角度制转化成弧度制
function rads(x) {
return (Math.PI * x) / 180;
}
//绘制一个圆形,如果需要椭圆的话则进行相应的缩放和旋转即可
//由于没有当前点,因此绘制的圆形不需要当前点到圆形起点之间的直线
c.beginPath();
//圆心位于(75,100),半径为50
//从0度到360度顺时针旋转
c.arc(75, 100, 50, 0, rads(360), false);
//绘制一个楔,角度从x轴正向顺时针度量
//要注意的是arc()方法会将当前点和弧形起点用直线相连
c.moveTo(200, 100); //从圆心开始
//圆心和半径
//从-60度开始一直到0
c.arc(200, 100, 50, rads(-60), rads(0), false); //false表示顺时针
c.closePath(); //将半径添加到圆心
//同样的楔,但是方向不同
c.moveTo(325, 100);
c.arc(325, 100, 50, rads(-60), rads(0), true); //逆时针
c.closePath();
//使用arcTo()方法来绘制圆角,绘制一个以点(400,50)为左上角同时还带有不同半径角的正方形
c.moveTo(450, 50); //从上边的中点开始
c.arcTo(500, 50, 500, 150, 30); //添加部分上边和右上角
c.arcTo(500, 150, 400, 150, 20); //添加右上角和右下角
c.arcTo(400, 150, 400, 50, 10); //添加底边和左下角
c.arcTo(400, 50, 500, 50, 0); //添加左边和左上角
c.closePath(); //闭合路径来添加其余的上边
//二次贝塞尔曲线:一个控制点
c.moveTo(75, 250); //从点(75,250)开始
c.quadraticCurveTo(100, 200, 175, 250); //画一条以一直到点(175,250)结束的曲线
c.fillRect(100 - 3, 200 - 3, 6, 6); //标记控制点(100,200)
//三次贝塞尔曲线
c.moveTo(200, 250); //从点(200,250)开始
c.bezierCurveTo(220, 220, 280, 280, 300, 250); //画一条以一直到点(300,250)结束的曲线
c.fillRect(220 - 3, 220 - 3, 6, 6); //标记控制点
c.fillRect(280 - 3, 280 - 3, 6, 6); //定义一些图形属性并绘制曲线
c.fillStyle = "#aaa"; //填充灰色
c.lineWidth = 5; //5个像素宽的黑色(默认颜色)线段
c.fill(); //填充该曲线
c.stroke(); //勾勒外边框
</script>
</html>
4.6 矩形
CanvasRenderingContext2D对象定义了4个用于绘制矩形的方法。这4个绘制矩形的方法都接受两个参数,其中一个指定矩形的一个顶点(x,y)坐标,另一个参数指定矩形的宽和高。一般都是指定矩形的左上角顶点,然后再传递表示一个宽度和高度的正值
fillRect()方法使用当前的fillStyle来填充指定的矩形。strokeRect()方法使用当前的strokeStyle和其他线段的属性来勾勒指定矩形的外边框。clearRect()方法 清空给定矩形(fillRect()方法绘制的矩形)内的指定像素,和fillRect()方法类似,但是不同的是,它会忽略当前填充样式,采用透明的黑色像素来填充矩形。这里重要的一点是:这三个方法都不影响当前路径以及路径中的当前点。rect(),此方法会对当前路径产生影响:它会将指定的矩形添加到当前路径的子路径中。和其他用于定义路径的方法一样,它本身不会自动做任何和填充以及勾勒相关的事情。
c.fillRect(0,0,300,150);
c.clearRect(20,20,100,50);
c.rect(280, 280, 6, 6);
c.strokeRect(280, 280, 6, 6);
4.7 颜色、透明度、渐变、图案
4.7.1 颜色
- strokeStyle和fillStyle属性指定了线条勾勒的样式和区域填充的样式。
- strokeStyle和fillStyle属性的默认值都是"#000000":不透明黑色
context.strokeStyle="blue";//用蓝色勾勒线段
context.fillStyle="#aaa";//用浅灰色填充区域
- 也可以将它们设置成CanvasPattern或者CanvasGradient对象,以实现采用重复的背景图片或线性或辐射型的渐变色来进行勾勒或者填充。
4.7.2 透明度
设置globalAlpha属性。这样,每一个绘制的像素都会将其alpha值乘以设置的globaAlpha值。
- globalAlpha属性默认值是1,表示不透明。
- 如果将其值设置为0的话,那么所有绘制的图形都会变成全透明,
- 如果设置为0.5的话,那么所有绘制的原本不透明的像素都会变成50%的不透明度
- 而如果原本像素是50%不透明度的话就变成25%的不透明度。
4.7.3 背景图片填充
要使用背景图片的图案而不是颜色来填充或者勾勒,可以将fillStyle或者strokeStyle属性设置成CanvasPattern对象,该对象可以通过调用上下文对象的createPattern()方法返回。
createPattern()方法
- 第一个参数指定了用做图案的图片。它必须是文档中的一个<img>元素、<canvas>元素或者<video>元素(或者是通过Image()构造函数创建出来的图片对象)。
- 第二个参数通常是"repeat",表示采用重复的图片填充,这和图片大小是无关的。除此之外,还可以使用"repeat-x"、"repeat-y"或者"no-repeat"。
- 可以采用一个<canvas>元素(甚至是一个从未添加到文档中并且不可见的<canvas>元素)作为另外一个<canvas>元素的图案:
var offscreen = document.createElement("canvas"); //创建一个屏幕外画布
offscreen.width = offscreen.height = 10; //设置它的大小
offscreen.getContext("2d").strokeRect(0, 0, 6, 6); //获取它的上下文并进行绘制
var pattern = c.createPattern(offscreen, "repeat"); //将它用做图案
4.7.4 渐变色填充
要使用渐变色来进行填充或勾勒,可以将fillStyle属性(或者strokeStyle属性)设置为一个CanvasGradient对象,该对象可以通过调用上下文对象上的createLinearGradient()或createRadialGradient()方法来返回。
第一步是要创建一个CanvasGradient对象。
- createLinearGradient()方法需要的参数是定义一条线段(不一定要水平或者垂直)两个点的坐标,这条线段上每个点的颜色都不同。
- createRadialGradient()方法需要的参数是两个圆(这两个圆不一定要同心圆,但是一般第二个圆完全包含第一个圆)的圆心和半径。小圆内的区域和大圆外的区域都会用纯色来填充:而两圆之间的区域会用渐变色来填充。
在创建了CanvasGradient对象以及定义了画布中要填充的区域之后,必须通过调用CanvasGradient对象的
addColorStop()方法来定义渐变色。
- 该方法的第一个参数是0.0~1.0之间的一个数字,第二个参数是一个CSS颜色值。
- 必须至少调用该方法两次来定义一个简单的颜色渐变,但是可以调用它多次。
- 在0.0位置的颜色会出现在渐变的起始,在1.0位置的颜色会出现在渐变色最后。
- 如果还指定其他的颜色,那么它们会出现在渐变指定的小数位置。其他地方的颜色会进行平滑的过渡。
<!DOCTYPE html>
<html>
<body>
<canvas id="my_canvas_id" width="800px" height="800px"></canvas>.
</body>
<script>
var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext("2d");
//一个线性渐变,沿着画布的对角线(假设没有进行坐标系变换)
var bgfade = c.createLinearGradient(0, 0, canvas.width, canvas.height);
bgfade.addColorStop(0.0, "#88f"); //以左上角为亮蓝色开始
bgfade.addColorStop(1.0, "#fff"); //一直到右下角以白色结束
//两个同心圆之间的一种渐变,中间为透明色,然后慢慢变为灰色半透明,最后再回到透明色
var peekhole = c.createRadialGradient(300, 300, 100, 300, 300, 300);
peekhole.addColorStop(0.0, "transparent"); //透明
peekhole.addColorStop(0.7, "rgba(100,100,100,.9)"); //灰色半透明
peekhole.addColorStop(1.0, "rgba(0,0,0,0)"); //再次透明
c.fillStyle = bgfade; //以线性渐变开始
c.fillRect(0, 0, 600, 600); //填充整个画布
var offscreen = document.createElement("canvas"); //创建一个屏幕外画布
offscreen.width = offscreen.height = 10; //设置它的大小
offscreen.getContext("2d").strokeRect(0, 0, 6, 6); //获取它的上下文并进行绘制
var pattern = c.createPattern(offscreen, "repeat"); //将它用做图案
c.strokeStyle = pattern; //使用图案来勾勒线段
c.lineWidth = 100; //使用非常宽的线段
c.strokeRect(100, 100, 400, 400); //绘制一个大的正方形
c.fillStyle = peekhole; //切换到辐射状渐变
c.fillRect(0, 0, 600, 600); //使用半透明填充来遮罩画布
</script>
</html>
4.8 绘制线段的相关属性
lineWidth属性
- 它用于指定通过stroke()方法和strokeRect()方法绘制时线段的宽度。
- lineWidth属性的默认值是1,可以将该属性设置成任意正数,甚至是小于1的小数。
- (小于1个像素宽的线段会绘制成半透明色的)
- 当调用stroke()方法时候,线段宽度是由lineWidth属性以及当前的坐标系变换决定的,而与lineTo()方法或者其他用于创建路径的方法无关。 另外三个与线段绘制相关的属性影响路径中未连接的端点的外观以及两条路径相交顶点的外观。它们对于很窄的线段的影响很小,相比而言,对于相对较宽的线段的影响很大。
lineCap属性
- 指定了一个未封闭的子路径段的端点如何“封顶”。
- 该属性的默认值"butt"表示线段端点直接结束。
- "square"值则表示在端点的基础上,再继续延长线段宽度一半的长度。
- "round"值则表示在端点的基础上延长一个半圆(圆的半径是线段宽度的一半)。
c.beginPath();
c.moveTo(100, 100);
c.lineTo(200, 100);
c.lineWidth = 100;
c.lineCap = "round";
c.strokeStyle = "blue";
c.stroke();
lineJoin属性
- 指定了子路径顶点之间如何连接。
- 其默认值是"miter",表示一直延伸两条路径段的外侧边缘直到在某一点汇合。
- "round"值则表示将汇合的顶点变得圆润,
- "bevel"值则表示用一条直线将汇合的顶点切除。
c.lineJoin = "round";
最后一个与线段绘制相关的属性是miterLimit,它只有当lineJoin属性值是"miter"才会起作用。miterLimit属性指定斜接部分长度的上限。
4.9 文本
- 通常使用
fillText()方法来使用fillStyle属性指定的颜色(渐变或者图案)绘制文本。 - 要想在大字号文本上加特效,可以使用
strokeText()方法,该方法会在每个字形外边绘制轮廓。 - fillText()方法和strokeText()方法都接受要绘制的文本内容作为第一个参数,以及文本绘制位置的X轴坐标和Y轴坐标作为第二个和第三个参数。但是这两个方法都不会对当前路径和当前点产生影响。
font属性指定了绘制文本时候采用的字体。该属性值是一个字符串,语法和CSS的font属性一致。
c.font = "bold 20px Times Roman"; //fillText()和strokeText()字体大小
c.fillStyle = "red"; //fillText()字体颜色
c.fillText("lxl", 200, 120);
c.lineWidth = 2; //strokeText()字体轮廓宽度
c.strokeStyle = "blue"; //strokeText()字体颜色
c.strokeText("long", 200, 150);
- textAlign属性指定了文本应当参照X轴坐标(调用fillText()或者strokeText()方法时候传入的参数)如何进行水平对齐。
- textBaseline属性则指定了文本应当参照Y轴坐标如何进行垂直对齐。
- fillText()方法和strokeText()方法同时还接受第4个可选的参数。该参数指定文本展现的最大宽度。
- measureText()方法返回一个TextMetrics对象,它指定在使用当前字体绘制文本时的尺寸。
4.10 裁剪
在定义一条路径之后,通常会调用stroke()方法或者fill()方法。还可以调用clip()方法来定义一个裁剪区域。一旦定义了一个裁剪区域,在该区域外将不会绘制任何内容。
- 要注意很重要的一点是:当调用clip()方法时,当前路径自身就会裁剪到当前裁剪区域中,之后,被裁剪的路径就变成了新的裁剪区域。
- 这意味着,clip()方法只会缩小裁剪区域,永远不会放大裁剪区域。
- 由于没有提供重置裁剪区域的方法,因此在调用clip()之前通常要调用save()方法,以便于之后恢复未裁剪区域。
<!DOCTYPE html>
<html>
<body>
<canvas id="my_canvas_id" width="800px" height="800px"></canvas>.
</body>
<script>
var canvas = document.getElementById("my_canvas_id");
var c = canvas.getContext("2d");
//定义一个以(x,y)为中心,半径为r的规则n边形
//每个顶点都是均匀分布在圆周上
//将第一个顶点放置在最上面,或者指定一定角度
//除非最后一个参数是true,否则顺时针旋转
function polygon(c, n, x, y, r, angle, counterclockwise) {
angle = angle || 0;
counterclockwise = counterclockwise || false;
c.moveTo(
x + r * Math.sin(angle), //从第一个顶点开始一条新的子路径
y - r * Math.cos(angle)
); //使用三角法计算位置
var delta = (2 * Math.PI) / n; //两个顶点之间的夹角
for (var i = 1; i < n; i++) {
//循环剩余的每个顶点
angle += counterclockwise ? -delta : delta; //调整角度
c.lineTo(
x + r * Math.sin(angle), //以下个顶点为端点添加线段
y - r * Math.cos(angle)
);
}
c.closePath(); //将最后一个顶点和起点连接起来
}
c.font = "bold 60pt sans-serif"; //大号字体
c.lineWidth = 2; //窄线段
c.strokeStyle = "#000"; //黑色线段
//勾勒矩形轮廓和文本轮廓
c.strokeRect(175, 25, 50, 325); //中间竖直的条带
c.strokeText("<canvas>", 15, 330); //注意使用的是strokeText()方法而不是fillText()方法
//在外部定义一条包含内部的复杂路径
polygon(c, 3, 200, 225, 200); //大三角形
polygon(c, 3, 200, 225, 100, 0, true); //在内部再绘制一个小三角形
//将该路径定义成裁剪区域
c.clip(); //用5个像素宽的线段来勾勒路径,完全在裁剪区域内
c.lineWidth = 10; //另外5个像素的线段被裁剪了
c.stroke(); //填充在裁剪区域内的矩形部分和文本部分
c.fillStyle = "#aaa"; //暗灰色
c.fillRect(175, 25, 50, 325); //填充竖直的条带
c.fillStyle = "#888"; //深灰色
c.fillText("<canvas>", 15, 330); //填充文本
</script>
</html>
4.11 阴影
CanvasRenderingContext2D对象定义了4个图形属性用于控制绘制下拉阴影。通过设置这些属性,绘制的任何线段、区域、文本以及图片都可以拥有下拉阴影
- shadowColor属性指定阴影的颜色。其默认值是完全透明的黑色,使用半透明色的阴影可以产生很逼真的阴影效果,因为透过它还能够看到背景
- shadowOffsetX属性和shadowOffsetY属性指定阴影的X轴和Y轴的偏移量。这两个属性的默认值都是0,表示直接将阴影绘制在图形正下方,在这种位置阴影是不可见的。如果将这两个属性都设置为一个正值,那么阴影会出现在图形的右下角位置,就好像有一个左上角的光源从计算机屏幕外面照射到画布上。偏移量越大,产生的阴影也越大,同时会感觉绘制的物体在画布上浮得也越高。
- shadowBlur属性指定了阴影边缘的模糊程度。其默认值为0,表示产生一个清晰明亮的阴影。该属性值越大表示阴影越模糊。该属性是高斯模糊函数的一个参数,和像素的大小以及长度无关。
//定义阴影
c.shadowColor = "rgba(100,100,100,.4)"; //半透明灰色
c.shadowOffsetX = c.shadowOffsetY = 10; //偏移阴影到右下角部分
c.shadowBlur = 5; //柔化阴影的边缘
//使用阴影在一个蓝色的方框中绘制一些文本
c.lineWidth = 10;
c.strokeStyle = "blue";
c.strokeRect(100, 100, 300, 200); //绘制一个矩形
c.font = "Bold 36pt Helvetica";
c.fillText("Hello World", 115, 225); //绘制一些文本
//定义一个模糊点的阴影。较大的偏移量使绘制的物体浮得越高
//要注意透明的阴影是如何和蓝色的方框重叠的
c.shadowOffsetX = c.shadowOffsetY = 20;
c.shadowBlur = 10;
c.fillStyle = "red"; //绘制一个纯红色的矩形
c.fillRect(50, 25, 200, 65); //该红色矩形浮在蓝色方框上面
4.12 图片
drawImage()用于将源图片(或者源图片中的矩形区域中)的像素内容复制到画布上,有需要的时候可以对图片进行缩放和旋转。
调用drawImage()方法的时候可以传递3个、5个或者9个参数。
- 其中第一个参数是要将其像素复制到画布上的源图片。这个图片参数通常是一个<img>元素或者通过Image()构造函数创建的一张屏幕外图片,但是它还可以是另一个<canvas>元素或者甚至是一个<video>元素。
- 如果传递3个参数给drawImage()方法,那么第二个和第三个参数指定待绘制图片的左上角位置的X轴和Y轴坐标。以这种方式调用,源图片的所有内容都会复制到画布上。
- 如果传递5个参数给drawImage()方法,那么另外两个参数分别是宽度和高度。X轴和Y轴坐标以及宽度和高度,这4个参数在画布上定义了一个目标矩形局域。这种调用方式也会复制整个源图片。
- 如果传递9个参数给drawImage()方法,那么这些参数还同时指定了一个源矩形区域和一个目标矩形区域,并且只会复制源矩形区域内的像素。其中第2~5个参数指定了源矩形区域。它们是以CSS像素来度量的。第6~9个参数指定了图片要绘制在的目标矩形区域,该区域是在画布当前的坐标系
//在左上角绘制一条线段
c.moveTo(5, 5);
c.lineTo(45, 45);
c.lineWidth = 8;
c.lineCap = "round";
c.stroke(); //定义一个变换
c.translate(50, 100);
c.rotate((-45 * Math.PI) / 180); //让线段变得更直
c.scale(10, 10); //将它放大到能够看到每个像素
//使用drawImage()(九个参数)方法来复制该线段
//第2-5参数(0,0,50,50)源矩形区域:未变换
c.drawImage(c.canvas, 0, 0, 50, 50, 0, 0, 50, 50); //目标矩形区域:变换过
toDataURL()方法
toDataURL()方法将画布中内容抽取成一张图片。
- 和其他方法不同,toDataURL()方法是画布元素自身的方法,而不是CanvasRenderingContext2D对象的方法。
- 通常调用toDataURL()方法的时候不传递任何参数,
- 它会将画布内容以PNG图片Base64的形式返回,同时编码成一个字符串数据,用URL表示。返回的URL可以直接用于<img>元素的src属性值
// 实现画布静态截图
var img = document.createElement("img"); //创建一个<img>元素
img.src = canvas.toDataURL(); //设置其src属性
document.body.appendChild(img); //把它追加到文档后面
- 可以通过利用toDataURL()方法的第一个可选参数来指定需要图片格式的MIME类型
img.src = canvas.toDataURL("image/jpeg"); //设置其src属性
4.13 合成
当勾勒线段、填充区域或者复制图片的时候,如果绘制一个不透明的像素,它们会替换同一位置原有的像素。如果绘制的是半透明的像素,那么新(“源”)像素会和原(“目标”)像素进行合并,原像素可以透过新像素看到,而清晰程度取决于像素的透明度。
合并新的半透明像素和已有原像素的过程称为“合成”,上面描述的合成过程也是画布API定义的默认像素合并方式。
要指定合成的方式,可以设置globalCompositeOperation属性。
- 该属性的默认值是"source-over",表示将新像素绘制在原像素上,对于半透明的新像素就直接合并。
- 如果将该属性设置为"copy",则表示关闭合成:新像素将原封不动地复制到画布上,直接忽略原像素。
- globalCompositeOperation属性值为"destination-over",表示将新像素绘制在已有原像素的下面。如果原像素是半透明或者透明的话,所有或者部分新像素的颜色在最终颜色上就是可见的。
4.14 像素操作
像素操作方法提供了对画布的底层访问。
- 调用getImageData()方法会返回一个ImageData对象,该对象表示画布矩形区域中的原始(没有预先进行像素增加处理的)像素信息(由R、G、B和A分量组成)。
- 使用createImageData()方法可以创建一个空的ImageData对象。ImageData对象中的像素是可写的,因此可以对它们进行随心所欲的设置,
- 然后再通过putImageData()方法将这些像素复制回画布中。putImageData()方法会忽略所有的图形属性。它不会进行任何合成操作,也不会用globalAlpha乘以像素来显示,更不会绘制阴影。
使用ImageData实现动态模糊
//将矩形区域的像素向右进行涂抹,
//来产生动态模糊效果,就好像物体正在从右到左移动
//n必须要大于或等于2,该值越大,涂抹区域就越大
//矩形是在默认坐标系中指定的
function smear(c, n, x, y, w, h) {
//获取表示矩形区域内像素的ImageData对象来实现涂抹效果
var pixels = c.getImageData(x, y, w, h); //就地实现涂抹效果并且只需要ImageData对象数据
//一些图片处理算法要求额外的ImageData对象来存储变换后的像素值
//如果需要输出缓冲区,可以以如下方式创建一个新的同样尺寸的ImageData对象
//var output_pixels=c.createImageData(pixels);
//这些尺寸可能和w和h之类的参数不同:有可能是每个CSS像素要表示多个设备像素
var width = pixels.width,
height = pixels.height; //data变量包含所有原始的像素信息:从左到右,从上到下
//每个像素按照R、G、B、A的顺序共占据4个
//每个像素按照R、G、B、A的顺序共占据4个字节
var data = pixels.data; //每一行第一个像素之后的像素都通过将其色值替换成
//其色素值的1/n+原色素值的m/n
var m = n - 1;
for (var row = 0; row < height; row++) {
//循环每一行
var i = row * width * 4 + 4; //每行第二个元素的偏移量
for (var col = 1; col < width; col++, i += 4) {
//循环每一列
data[i] = (data[i] + data[i - 4] * m) / n; //像素中红色分量
data[i + 1] = (data[i + 1] + data[i - 3] * m) / n; //绿色
data[i + 2] = (data[i + 2] + data[i - 2] * m) / n; //蓝色
data[i + 3] = (data[i + 3] + data[i - 1] * m) / n; //Alpha分量
}
}
//现在将涂抹过的图片数据复制回画布相同的位置
c.putImageData(pixels, x, y);
}
4.15 命中检测
isPointInPath()方法确定一个指定的点是否落在(或者在边界上)当前路径中,如果该方法返回true则表示落在当前路径中,反之则返回false。
// 检测一个鼠标事件是否发生在当前路径上isPointInPath()
//如果鼠标事件发生指定的CanvasRenderingContext2D对象的当前路径上则返回true
function hitpath(context, event) {
//从<canvas>对象中获取<canvas>元素
var canvas = context.canvas; //获取画布尺寸和位置
var bb = canvas.getBoundingClientRect(); //将鼠标事件坐标通过转换和缩放变换成画布坐标
var x = (event.clientX - bb.left) * (canvas.width / bb.width);
var y = (event.clientY - bb.top) * (canvas.height / bb.height);
//用这些变换后的坐标来调用isPointInPath()方法
return context.isPointInPath(x, y);
}
- 必须要将鼠标事件的坐标转换成相应的画布元素,而不是Window对象。
- 可以使用
getImageData()方法来检测鼠标点下的像素是否已经绘制过了。如果返回的像素(单个或多个)是完全透明的,则表示该像素上没有绘制任何内容,并且鼠标事件点空了。
// 检测鼠标事件触发点的元素是否绘制过了getImageData()
//如果指定的鼠标事件点下的像素不是透明的则返回true
function hitpaint(context, event) {
//通过转换和缩放将鼠标事件坐标转换成画布坐标
var canvas = context.canvas;
var bb = canvas.getBoundingClientRect();
var x = (event.clientX - bb.left) * (canvas.width / bb.width);
var y = (event.clientY - bb.top) * (canvas.height / bb.height);
//获取像素(或者多个设备像素映射到一个CSS像素的像素)
var pixels = c.getImageData(x, y, 1, 1);
//如果任何像素的alpha值非0,则返回true(命中)
for (var i = 3; i < pixels.data.length; i += 4) {
if (pixels.data[i] !== 0) return true;
}
//否则,表示不命中
return false;
}
canvas.onclick = function(event) {
//检测一个鼠标事件是否发生在当前路径上
console.log(hitpath(this.getContext("2d"), event));
// 检测鼠标事件触发点的元素是否绘制过了
console.log(hitpaint(this.getContext("2d"), event));
};
4.16 画布例子:迷你图
迷你图(sparkline)是指用于显示少量数据的图形,通常会和嵌入在文本流中。内嵌在文字、数字、图片中的小且高分辨率的图形。迷你图是数据密集、设计简单、单词大小的图形。
<!DOCTYPE html>
<html>
<body>
<span class="sparkline">3 5 8 13 6 6 9 11 15</span>
</body>
<script>
/*
*找到所有有"sparkline"CSS类的元素,将它们的内容解析成一系列数字
*最后替换成图形化的表示方式
*
*将使用标记将迷你图定义成如下形式:
*<span class="sparkline">3 5 7 6 6 9 11 15</span>
*
*使用CSS对迷你图进行样式设置,如下所示:
*.sparkline{background-color:#ddd;color:red;}
*
*-迷你图的颜色是根据CSS的color属性计算出来的
*-迷你图是透明的,因此可以显示正常的背景色
*-如果设置了data-height属性,迷你图的高度则由该属性指定,
*如果没有设置,则根据font-size属性计算得出
*-如果设置了data-width属性,迷你图的宽度则由该属性指定
*如果没有设置该属性,而设置了data-dx属性,则迷你图的宽度等于数据点的个数乘以
*data-dx的值;否则,图表的宽度等于数据点的个数乘以图表的高度再除以6
*-如果设置了data-ymin属性和data-ymax属性,则最小值和最大值由这两个属性值指定
*否则,最小值和最大值等于数据的最小值和最大值
*/
onload = function() {
//当文档第一次载入时
//找到所有有"sparkline"类的元素
var elts = document.getElementsByClassName("sparkline");
for (var e = 0; e < elts.length; e++) {
//循环每个元素
var elt = elts[e]; //获取元素内容并转换成一个包含数字的数组
//如果转换失败,则跳过该元素
var content = elt.textContent || elt.innerText; //元素内容
var content = content.replace(/^\s+|\s+$/g, ""); //去除空格
var text = content.replace(/#.*$/gm, ""); //去除注释
text = text.replace(/[\n\r\t\v\f]/g, ""); //将\n等转换成空格
var data = text.split(/\s+|\s*,\s*/); //以空格或者逗号进行分隔
for (var i = 0; i < data.length; i++) {
//循环每个数据块
data[i] = Number(data[i]); //转换成一个数字
if (isNaN(data[i])) continue; //转换失败则中止
}
//现在根据数据和元素的data-属性以及元素的计算样式,来计算
//迷你图的颜色、宽度、高度和Y轴的范围
var style = getComputedStyle(elt, null);
var color = style.color;
var height =
parseInt(elt.getAttribute("data-height")) ||
parseInt(style.fontSize) ||
20;
var width =
parseInt(elt.getAttribute("data-width")) ||
data.length * (parseInt(elt.getAttribute("data-dx")) || height / 6);
var ymin =
parseInt(elt.getAttribute("data-ymin")) || Math.min.apply(Math, data);
var ymax =
parseInt(elt.getAttribute("data-ymax")) || Math.max.apply(Math, data);
if (ymin >= ymax) ymax = ymin + 1; //创建一个画布元素
var canvas = document.createElement("canvas");
canvas.width = width; //设置画布尺寸
canvas.height = height;
canvas.title = content; //将元素内容作为工具提示
elt.innerHTML = ""; //将现有的元素内容抹除
elt.appendChild(canvas); //将该元素插入到画布中
//现在绘制点(i,data[i]),转换成画布坐标
var context = canvas.getContext("2d");
for (var i = 0; i < data.length; i++) {
//循环每个数据点
var x = (width * i) / data.length; //缩放i倍
var y = ((ymax - data[i]) * height) / (ymax - ymin); //缩放data[i]
context.lineTo(x, y); //首先调用lineTo()方法而不是moveTo()方法
}
context.strokeStyle = color; //设置迷你图的颜色
context.stroke(); //并将它绘制出来
}
};
</script>
</html>