整合营销服务商

电脑端+手机端+微信端=数据同步管理

免费咨询热线:

第75节 Canvas绘图(上)-前端开发之JavaScript-王唯

TML5新增一个元素canvas以及与这个元素伴随而来的一套编程接口Canvas API,可以在页面上绘制出任何想要的、动态的图形与图像,创造出丰富多彩的Web页面;它可以用于动画、游戏画面、数据可视化、图片编辑以及实时视频处理等方面;

canvas元素相当于在页面上放置了一块画布,其本身并不能实现图形绘制,也没有任何外观,只是一块无色透明的区域,需要几组API进行操作,最重要的是就是具备基本绘图能力的2D上下文(context)及文本API,利用这些API,可以添加线条、图片、文字、绘画、加入高级动画等;现在还可以使用用于绘制硬件加速的WebGL的3D上下文;

基本用法:
要使用canvas元素,必须先设置其width和height属性,指定可以绘图的区域大小;该标签中的内容,为后备信息,当浏览器不支持canvas时,就会显示这些信息,如:

<canvas id="drawing" width="400" height="300">你的浏览器不支持canvas</canvas>

canvas 的默认大小为 300px×150px,其对应的DOM元素也具有width和height属性,并且,可以通过CSS设置其样式;

<style>
#drawing{background-color: lightblue; width:400px; height:300px;}
</style>
<canvas id="drawing"></canvas>
<script>
var drawing = document.getElementById("drawing");
console.log(drawing);
console.log(drawing.width + ":" + drawing.height); // 还是默认的300pxX150px
</script>

canvas是无色透明的,如果不添加任何样式或不绘制任何图形,在页面上是看不到该元素的;

绘制图形的步骤:
1、取得canvas元素:

var canvas = document.getElementsByTagName("canvas")[0];

2、取得上下文(context):进行图形绘制时,需要使用到图形上下文(graphics context),图形上下文是一个封装了很多绘图功能的对象;使用canvas对象的getContext()方法获得图形上下文,在该方法中需要传入“2d”;

var context = canvas.getContext("2d");

3、设定绘图样式:在绘制的时候,首先要设定好绘图的样式,然后再调用有关方法进行图形的绘制,如:fillStyle属性,设定填充的样式,在该属性中填入颜色值;strokeStyle属性,设置边框的样式,值也是颜色值;使用lineWidth属性设置边框的宽度;

context.fillStyle = "yellow";
context.strokeStyle = "green";
context.lineWidth = 10;

4、填充与绘制边框:使用相应的绘制方法进行绘制,如使用fill()方法填充与stroke()方法描边;填充是指填充图形内部;描边是指不填充图形内部,只绘制图形的边框;对于矩形来说, fillRect()方法绘制矩形内部,strokeRect()方法绘制矩形边框;

context.fillRect(0,0,100,100);
context.strokeRect(0,0,100,100);

2D上下文(context):
使用canvas对象的getContext()方法获得图形上下文,在该方法中需要传入“2d”,就会获取一个在2D环境中绘制图形的CanvasRenderingContext2D类型的上下文对象,被称为渲染上下文(The rendering context),目前只支持2D,没有3D之类的,但不排除今后会扩展;

// 确定浏览器是否支持canvas元素
if(canvas.getContext){
var context = canvas.getContext("2d");
console.log(context); // CanvasRenderingContext2D
// ...
}

使用2D绘图上下文提供的方法,可以绘制简单的2D图形,比如矩形、弧线和路径;如果利用它们结合不同的填充和描边样式,就能绘制出非常复杂的图形;如:

context.fillStyle = "rgba(0, 0, 200, 0.5)";
context.fillRect (50, 50, 100, 100);

每个canvas只能有一个上下文对象,多次调用getContext()返回的是同一个对象;

var context = drawing.getContext("2d");
var context1 = drawing.getContext("2d");
console.log(context === context1); // true

其拥有一个canvas属性,是与当前上下文关联的HTMLCanvasElement对象的只读引用;

canvas的尺寸和坐标:
canvas默认的坐标系是以<canvas>元素的左上角(0, 0)为坐标原点;所有图形元素的坐标都基于这个原点计算,x值越大表示越靠右,y值越大表示越靠下,反之亦然;

画布的尺寸和坐标系都是以CSS像素为单位的;

默认情况下,canvas的width和height表示水平和垂直两个方向上可用的像素数目;

画布的尺寸和CSS定义的尺寸是完全不同的概念:画布的尺寸是由<canvas>元素的width和height属性定义的,而CSS中定义的尺寸是画布元素在页面中显示的尺寸;如果两者定义的尺寸不相同,则画布上的像素会自动缩放,以适合CSS中定义的尺寸;另外,画布中的坐标,也是根据画布的width和height属性定义的;

画布上的点可以使用浮点数来指定坐标,但是它们不会自动转换成整型值,画布会采用反锯齿的方式来模拟部分填充的像素;

画布的尺寸是不能随意更改的,除非完全重置画布;重置画布的width和heigth属性,会清空整个画布,擦除当前的路径并且会重置所有的图形属性(包括当前的变换和裁剪区域)为初始状态;

绘制基本图形:

填充和描边:

2D上下文的基本绘图操作是填充和描边;填充,就是用指定的样式(颜色、渐变或图像)填充图形;描边,就是只在图形的边缘画线;(这个,前面讲过了)

大多数2D上下文操作都会细分为填充和描边两个操作,而操作的结果取决于两个属性:fillStyle和strokeStyle;

这两个属性的值可以是字符串、渐变对象或模式对象,而且它们的默认值都是“#000000”;如果为它们指定表示颜色的字符串值,格式是与CSS一致的,如:

// ...
context.fillStyle = "#0000ff";
context.strokeStyle = "red";
context.fillRect(50,50,200,100);
context.strokeRect(50,200,200,100);
// ...

绘制矩形:
在canvas中,绘图有两种形式:矩形和路径,除了矩形,所有其他类型的图形都是通过一条或者多条路径组合而成的;

矩形是唯一一种可以直接在2D上下文中绘制的形状,其分别使用fillRect(x,y,width,height)、strokeRect(x,y,width,height)方法来绘制填充矩形和绘制矩形边框,两者均接收4个参数:矩形的x、y坐标、矩形的宽和高,单位为像素;填充和描边的颜色通过fillStyle和strokeStyle两个属性指定,如:

function draw(id){
var drawing = document.getElementById(id);
if(drawing.getContext){
var context = drawing.getContext("2d");
// 绘制画布背景
context.fillStyle = "#EEEEFF";
context.fillRect(0,0,canvas.width, canvas.height);
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,0.5)";
context.strokeStyle = "blue";
context.fillRect(50,50,100,100);
context.strokeRect(50,50,100,100);
// 绘制红色矩形
context.strokeStyle = "#ff0000";
context.fillStyle = "rgba(255,0,0,0.2)";
context.strokeRect(100,100,100,100);
context.fillRect(100,100,100,100);
}
}
draw("canvas");

示例:绘制国际象棋棋盘:

for (i=0; i<8; i++){ // 行
for (j=0; j<8; j++){ // 列
if ((i+j) % 2 == 0)
context.fillStyle = 'black';
else
context.fillStyle= 'white';
context.fillRect(j*50, i*50, 50, 50);
}
}

示例:绘制柱状图表

var data = [100, 50, 20, 30, 100];
var colors = ["red", "orange", "yellow", "green", "blue"];
context.fillStyle = "white";
context.fillRect(0, 0, canvas.width, canvas.height);
for(var i=0; i<data.length; i++){
var v = data[i];
context.fillStyle = colors[i];
context.fillRect(25+i*50, 280-v*2, 50, v*2);
}

关于矩形,还有一个clearRect(x, y, width, height)方法,该方法将指定的矩形区域中的图形进行擦除,使得矩形区域中的颜色全部变为透明,其参数与前两个方法一致;如:

// 在两个矩形重叠的部分清除一个小矩形
context.clearRect(110,110,30,30);
// 重新绘制
context.strokeStyle = "black";
context.fillStyle = "black";
context.fillRect(250,25,100,100);
context.clearRect(270,45,60,60);
context.strokeRect(275,50,50,50);

以上绘制的矩形,由于没用到路径,所以一旦绘制,会立即生效,并在canvas中渲染;

清理画布和恢复画布状态:

<input type="button" value="清空画布" onClick="clearMap();" />
<script type="text/javascript">
var c = document.getElementById("canvas");
var context = c.getContext("2d");
context.fillStyle = "red";
context.strokeStyle = "yellow";
context.beginPath();
context.arc(200,150,100,-Math.PI*5/6,true);
context.stroke();
function clearMap() {
context.clearRect(0,0,300,200);
}
</script>
绘制路径:
图形的基本元素是路径,其是通过不同颜色和宽度的线段或曲线相连形成的不同形状的点的集合;一个路径,一般都是闭合的;
使用路径绘制图形需要一些步骤:创建路径起始点、使用相关命令画出路径、之后把路径封闭;一旦路径生成,就可以通过描边或填充路径区域来渲染图形;

2D上下文提供了多个绘制路径的方法,通过这些方法就可以绘制出复杂的图形和线条:
beginPath()方法,新建一条路径,不需要参数,表示要开始绘制新路径,其后再调用一系列方法来实际地绘制路径;其后绘制的路径都是子路径,都属于同一条路径;
再使用moveTo()方法,移动当前点,它并不绘制线条,它的作用就是确定路径起始点;
接着使用绘图方法,例如:lineTo()方法,可以从上一点开始绘制一条线段;或者使用rect()方法,绘制一个矩形路径;再或者使用arc()方法,绘制一条弧线,等等;
closePath()方法,闭合路径;此时,就会自动绘制一条连接到路径起点的线条,路径的创建工作就完成了;如:

context.beginPath();
context.moveTo(50,50);
context.lineTo(100,50);
context.arc(50,50,50,0,Math.PI/2);
context.moveTo(200,70);
context.arc(200,70,50,0,Math.PI/2,true);
context.rect(300,50,100,100);
context.closePath();

如果路径已经完成,可以设置绘图样式,并进行绘图,如用fillStyle设置填充样式,并调用fill()方法填充,生成实心的图形;或使用strokeStyle属性设置边框样式,并调用stroke()方法对路径描边,即勾勒图形轮廓;

// ...
context.fillStyle = "green";
context.fill();
context.strokeStyle = "yellow";
context.lineWidth = 10;
context.stroke();

本质上,路径是由很多子路径构成,这些子路径都是在一个列表中,所有的子路径(线、弧形等)构成一个完整的路径,当调用stroke()或fill()方法后就会绘制图形;

而当每次调用closePath()方法之后,列表就被清空重置,之后就可以重新绘制新的图形,如:

context.beginPath();
context.moveTo(50,50);
context.lineTo(100,50);
context.arc(50,50,50,0,Math.PI/2);
context.closePath();
context.fillStyle = "red";
context.strokeStyle = "blue";
context.lineWidth = 12;
context.fill();
context.stroke();
context.beginPath();
context.moveTo(200,70);
context.arc(200,70,50,0,Math.PI/2,true);
context.closePath();
context.fillStyle = "blue";
context.strokeStyle = "red";
context.lineWidth = 8;
context.fill();
context.stroke();
context.beginPath();
context.rect(300,50,100,100);
context.closePath();
context.fillStyle = "green";
context.strokeStyle = "yellow";
context.lineWidth = 4;
context.fill();
context.stroke();

闭合路径closePath()方法不是必需的,如果不使用closePath(),则路径不闭合,如:

context.beginPath();
context.arc(150, 150,50, 0, Math.PI);
// context.closePath();
context.stroke();

如果不使用beginPath()开始新路径,并不会关闭之前的路径,并且该路径会永远保留着;就算调用fill()方法或stroke()方法进行绘制,路径也不会消失;因此,后续的绘制,还会重复绘制该路径;

移动笔触(当前坐标点):
moveTo(x,y)方法:将光标(笔触)移动到指定坐标点,绘制图形的时候就以这个坐标点为起点,也就是确定一个子路径的起点,当个起点也称为当前点;其并不绘制任何内容,但也属于路径描述的一部分;

context.moveTo(50, 50);
context.lineTo(150, 150);

在画布中第一次调用相关的绘制路径的方法,有时候并不需要使用moveTo()指定起点位置,起点位置是由该方法的参数指定的;但后续的路径绘制,必须要明确起点位置

当调用beginPath()方法开始新的一段路径时,绘制的新路径的起点由当前的绘制方法自动确定,此时,并不需要明确调用moveTo()方法;当 canvas初始化或者beginPath()调用后,通常会使用moveTo()函数设置起点;

示例:笑脸

context.beginPath();
context.arc(75, 75, 50, 0, Math.PI * 2, true);
context.moveTo(110, 75);
context.arc(75, 75, 35, 0, Math.PI); // 口 (顺时针)
context.moveTo(65, 65);
context.arc(60, 65, 5, 0, Math.PI * 2, true); // 左眼
context.moveTo(95, 65);
context.arc(90, 65, 5, 0, Math.PI * 2, true); // 右眼
context.closePath(); // 不是必须的
context.stroke();

绘制直线(线段):
绘制线段需要lineTo()方法:lineTo(x,y)方法:指定从起点到终点(x, y)的一条直线;可重复使用lineTo方法,会以上一个lineTo()方法指定的点为起点,以当前lineTo()为终点再次创建直线;不断重复这个过程,可以绘制复杂的图形;

context.moveTo(50,50);
context.lineTo(100,100);
context.lineTo(100,200);
执行stroke()绘制直线;
context.moveTo(50,50);
context.lineTo(200,200);
context.lineTo(50,200);
context.stroke();

开始点和之前的绘制路径有关,之前路径的结束点就是接下来的开始点,开始点也可以通过moveTo()函数改变;
绘制直线也属于路径,因此在必要的情况下,也需要使用beginPath(),否则也属于连续绘制,或调用closePath()方法闭合路径;

// ...
context.beginPath();
context.moveTo(50,50);
context.lineTo(200,200);
context.lineTo(50,200);
context.lineWidth = 20;
context.closePath();
// ...

示例:绘制三角形

// 绘制三角形
context.beginPath();
context.moveTo(75, 50);
context.lineTo(100, 75);
context.lineTo(100, 25);
context.fill();

示例:绘制多边形

CanvasRenderingContext2D.prototype.polygon = function(n ,x, y, r, angle, anticlockwise){
this.n = n || 3;
this.x = x || 10;
this.y = y || 10;
this.r = r || 50;
this.angle = angle || 0;
this.anticlockwise = anticlockwise || false;

this.moveTo(this.x + this.r * Math.sin(this.angle), this.y - this.r * Math.cos(this.angle));
var delta = 2 * Math.PI / this.n;
for(var i=1; i<this.n; i++){
this.angle += this.anticlockwise ? -delta : delta;
this.lineTo(this.x + this.r * Math.sin(this.angle), this.y - this.r * Math.cos(this.angle));
}
this.closePath();
}
var canvas = document.getElementsByTagName("canvas")[0];
if(canvas.getContext){
var context = canvas.getContext("2d");
context.beginPath();
context.polygon(3, 50, 70, 50); // 三角形
context.polygon(4, 150, 60, 50, Math.PI / 4); // 正方形
context.polygon(5, 255, 55, 50); // 五边形
context.polygon(6, 365, 53, 50, Math.PI / 6); // 六边形
context.polygon(4, 365, 53, 20, Math.PI / 4, true); // 六边形中的小正方形
context.fillStyle = "#ccc";
context.strokeStyle = "#008";
context.lineWidth = 5;
context.fill();
context.stroke();
}
</script>

填充规则:
当用到fill()、clip()和isPointinPath()时,可以选择一个填充规则,即传入一个fillRule参数,该填充规则根据某处在路径的外面或者里面来决定该处是否被填充,这对于自己与自己路径相交或者路径被嵌套的情况是有用的;两个可能的值:

  • "nonzero":non-zero winding rule(非零环绕规则), 默认值;
  • "evenodd":even-odd winding rule;(奇偶环绕规则)

非零环绕原则(nonzero):
主要解决绘图中交叉路径的填充问题;

要检测一个点P是否在路径的内部,使用非零环绕原则:想象一条从点P出发沿着任意方向无限延伸(一直延伸到路径所在的区域外)的射线;首先初始化一个计数器为0,然后对所有穿过这条射线的路径进行枚举;每当一条路径顺时针方向穿过射线的时候,计数器就加1;反之,就减1;最后,枚举完所有路径之后,如果计数器的值不是0,那么就认为P是在路径内;反之,如果计数器的值是0,则认为P在路径外;
总之,由顺、逆时针穿插次数决定是否填充某一区域;

奇偶环绕规则(evenodd):奇数表示在路径内,偶数表示在路径外;
从任意位置p作一条射线,若与该射线相交的路径的数目为奇数,则p是路径内部的点,否则是外部的点;

如:

context.beginPath();
context.arc(50, 50, 30, 0, Math.PI*2);
context.arc(50, 50, 15, 0, Math.PI*2, true);
// context.fill();
// context.fill("nonzero");
context.fill("evenodd");

如:绘制一个五角星

context.arc(100, 100, 100, 0, Math.PI*2);
context.fillStyle = '#D43D59';
context.fill();

context.beginPath();
context.moveTo(100, 0);
context.lineTo(100+Math.cos(Math.PI*3/10)*100, 100+Math.sin(Math.PI*3/10)*100);
context.lineTo(100-Math.cos(Math.PI*1/10)*100, 100-Math.sin(Math.PI*1/10)*100);
context.lineTo(100+Math.cos(Math.PI*1/10)*100, 100-Math.sin(Math.PI*1/10)*100);
context.lineTo(100-Math.cos(Math.PI*3/10)*100, 100+Math.sin(Math.PI*3/10)*100);
context.lineTo(100, 0);
context.closePath();
context.strokeStyle = "rgb(0,0,0)";
context.fillStyle = "#246AB2"
// context.fill('nonzero');
context.fill('evenodd');
context.stroke();

如:绘制一个大箭头图标

context.arc(250, 250, 150, 0, Math.PI * 2);
context.moveTo(250, 150);
context.lineTo(350, 250);
context.lineTo(150, 250);
context.closePath();
context.moveTo(200, 250);
context.lineTo(300, 250);
context.lineTo(300, 350);
context.lineTo(200, 350);
context.closePath();
context.fillStyle = "#0D6EB8";
// context.fill("nonzero");
context.fill("evenodd");

示例:使用数学方程绘制图案图形

var dx = 150, dy = 150;
var s = 100;
context.beginPath();
context.fillStyle = "rgb(100,255,100)";
context.strokeStyle = "rgb(0,0,100)";
var dig = Math.PI / 15 * 11;
for(var i=0; i<30; i++){
var x = Math.sin(i * dig);
var y = Math.cos(i * dig);
console.log(dx + x * s, dy + y * s);
context.lineTo(dx + x * s, dy + y * s);
}
context.closePath();
// context.fill();
context.fill("evenodd");
context.stroke();
// 以下包装成一个函数
function draw(offsetX, n){
var dig = Math.PI / 15 * n;
context.beginPath();
for(var i = 0 ; i < 30 ; i++){
var x = Math.sin(i * dig);
var y = Math.cos(i * dig);
context.lineTo(offsetX + x * 80, 150 + y * 80);
}
context.closePath();
context.fillStyle = "green";
// context.fill();
context.fill("evenodd");
context.strokeStyle = "#666";
context.stroke();
}
var data = [14,13,19,7,26];
data.forEach(function(v, i){
draw((i + 1) * 160, v);
});

示例:使用鼠标实时绘制图形

canvas.onmousedown = function(e) {
this.X1 = e.offsetX;
this.Y1 = e.offsetY;
this.isMouseDown = true;
};
canvas.onmousemove = function(e) {
if (this.isMouseDown) {
this.X2 = e.offsetX;
this.Y2 = e.offsetY;
this.drawing(this.X1, this.Y1, this.X2, this.Y2, e);
}
};
canvas.onmouseup = function(e) {
this.isMouseDown = false;
};
canvas.drawing = function (x1, y1, x2, y2, e) {
if (!context) {
return;
} else {
context.fillStyle = "red";
context.strokeStyle = "blue";
context.lineWidth = 5;
context.beginPath();
context.moveTo(x1, y1);
context.lineTo(x2, y2);
context.stroke();
this.X1 = this.X2;
this.Y1 = this.Y2;
context.closePath();
}
}

例子:迷你图
迷你图(sparkline)是指用于显示少量数据的图形,通常会被嵌入文本流中,形如:server:小图;迷你图是由作者Edward Tufte杜撰的,他将该词用于描述“内嵌在文字、数字、图片中的小且高分辨率的图形”;迷你图是数据密集、设计简单、单词大小的图形,如:

<style>
.sparkline{background-color:#ddd; color:red; display: inline-block;}
</style>
Load:<span class="sparkline">3 5 7 6 6 9 11 15</span>,
source:<span class="sparkline" data-height="36" data-width="100">6 14 8 9 10 13 18</span>
complete:<span class="sparkline" data-ymax="18" data-ymin="6">12,3,8,2,88</span>
<script>
window.onload = function(){
var elts = document.getElementsByClassName("sparkline");
main: 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, " ");
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 main;
}
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")) || 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);
var context = canvas.getContext("2d");
for(var i=0; i<data.length; i++){
var x = width * i / data.length;
var y = (ymax - data[i]) * height / (ymax - ymin);
context.lineTo(x,y);
}
context.strokeStyle = color;
context.stroke();
}
}
</script>

绘制曲线:
角度和弧度:

夹角:从一个点发射(延伸)出两条线段,两条线相交的部分会构成一个夹角;
角度:两条相交直线中的任何一条与另一条相叠合时必须转动的量的量度,单位符号为°;
周角:一条直线围绕起点需要与自己相叠合时必须转动的量的量度被称为周角,周角等分为360度;
弧度:角的度量单位,弧长等于半径的弧其所对的圆心角为1弧度(弧长等于半径时,射线夹角为1弧度);
角度与弧度的换算公式为:弧度 = 角度 * (Math.PI / 180);

在使用JavaScript编写代码进行相关计算的时候,经常需要使用Math提供的方法:

Math.sin(弧度) 夹角对面的边与斜边的比值;
Math.cos(弧度) 夹角侧面的边与斜边的比值;

圆形上点坐标的计算公式:
坐标 = ( x0 + Math.cos(angle) x R,y0 + Math.sin(angle) x R )
其中x0和y0为圆心坐标,angle为弧度,R为圆的半径;

例如:使用三角函数来绘制曲线:

context.beginPath();
for(var x = 30, y = 0; x<1000; x++){
// 高度 * 波长 + 中心轴位置
y = 50 * Math.sin(x / 25) + 100;
context.lineTo(x, y);
}
context.stroke();

绘制弧形和圆形:
arc(x, y, radius, startAngle, endAngle[, anticlockwise])方法:
此方法可以绘制一条弧,其以(x, y)为圆心,以radius为半径绘制一条弧线(圆),从 startAngle开始到endAngle结束的弧度,按照anticlockwise给定的方向(默认为顺时针)来绘制;参数anticlockwise指定是否按逆时针方向进行绘制,为true时,逆时针,反之顺时针;

// context.arc(100,100,50,0, Math.PI*3/2);
context.arc(100,100,50,0, Math.PI*3/2,true);
context.stroke();

arc()方法中表示角的单位是弧度,不是角度;角度都是按顺序时针计算的,无论是按时针还是按逆时针;

由于arc()绘制的是路径,所以在必要的情况下,需要调用beginPath();如果需要扇形,可以使用closePath()方法闭合路径;

当第一个调用arc()或在beginPath()方法后第一个调用arc()方法时,会在确定其圆心、半径及开始角度的情况下,自动确定起点位置;其它情况下,需要使用moveTo()指定起点位置,例如:先将当前点和弧形的起点用一条直线连接,然后再用圆的一部分(圆弧)来连接弧形的起点和终点,并把弧形终点作为新的当前点;

// 绘制一个圆弧
context.beginPath();
context.moveTo(50, 50);
context.arc(100, 100, 50, Math.PI*3/2, 0);
context.lineTo(150,150);
context.stroke();
// 绘制一个san形(楔形)
context.beginPath();
context.moveTo(200, 50);
context.arc(200, 50, 50, 0, Math.PI / 2, false);
context.closePath();
context.stroke();
// 同样的san形(楔形),方向不同
context.beginPath();
context.moveTo(350, 100);
context.arc(350, 100, 50, 0, Math.PI / 2, true);
context.closePath();
context.stroke();

arc()不仅可以绘制圆弧,也可以用来绘制圆形,即startAngle, endAngle形成Math.PI*2(360度),如:

context.beginPath();
context.arc(100, 100, 80, 0, Math.PI*2);
// context.arc(100, 100, 80, Math.PI / 4, Math.PI * 2 + Math.PI / 4);
context.closePath(); // 没有必要调用这一句
context.lineWidth = 10;
context.fillStyle = "red";
context.fill();
context.stroke();

绘制有交叉路径时弧时,要注意填充规则,如:

context.beginPath();
context.arc(100, 100, 50, 0, Math.PI * 2, true);
context.arc(100, 100, 60, 0, Math.PI * 2, false);
context.fill();
context.beginPath();
context.arc(300, 100, 50, 0, Math.PI, true);
context.arc(300, 100, 60, 0, Math.PI * 2, true);
// 使用奇偶环绕规则
context.fill('evenodd');
context.beginPath();
context.arc(100, 100, 60, 0, Math.PI * 2, true);
context.arc(140, 100, 60, 0, Math.PI * 2, false);
context.arc(180, 100, 60, 0, Math.PI * 2, true);
context.fill();

循环绘制多个,如:

for(var i=0;i<15;i++){
context.strokeStyle = "#FF00FF";
context.beginPath();
context.arc(0,350,i*10,0,Math.PI*3/2,true);
context.closePath();
context.stroke();
}
for(var i=0;i<10;i++){
context.beginPath();
context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
context.closePath();
context.fillStyle = 'rgba(255,0,0,0.25)';
context.fill();
}

绘制不同的圆弧:

for(var i = 0; i < 4; i++){
for(var j = 0; j < 3; j++){
context.beginPath();
var x = 25 + j * 50; // x 坐标值
var y = 25 + i * 50; // y 坐标值
var radius = 20; // 圆弧半径
var startAngle = 0; // 开始点
var endAngle = Math.PI + (Math.PI * j) / 2; // 结束点
var anticlockwise = i % 2 == 0 ? false : true; // 顺时针或逆时针
context.arc(x, y, radius, startAngle, endAngle, anticlockwise);
if (i>1){
context.fill();
} else {
context.stroke();
}
}
}

绘制不同圆角的矩形:

// 绘制圆角矩形
context.beginPath();
context.moveTo(360, 0);
context.arc(380, 30, 30, Math.PI * 3 / 2, 0); // 上边和右上角
context.arc(390, 100, 20, 0, Math.PI / 2); // 右上角和右下角
context.arc(340, 110, 10, Math.PI / 2, Math.PI); // 底边和左下角
context.arc(330, 0, 0, Math.PI, 0); // 左边和左上角
context.closePath();
context.fill();
context.stroke();

示例:绘制一个WIFI图标:

// wifi图标
context.lineWidth = 3;
for(var i=0;i<3;i++){
context.beginPath();
context.arc(100, 150, 15 + i*12, Math.PI, Math.PI*3/2);
context.stroke();
}
context.beginPath();
context.arc(98, 148, 3, 0, Math.PI*2);
context.fill();

示例:绘制一个时钟表盘

context.beginPath();
// 外圆
context.arc(100,100,99,0,Math.PI * 2, false);
// 内圆
context.moveTo(194, 100);
context.arc(100,100,94,0,Math.PI * 2, false);
// 分针
context.moveTo(100,100);
context.lineTo(100,15);
// 时针
context.moveTo(100,100);
context.lineTo(35,100);
context.closePath();
context.stroke();

示例:绘制一个手镯

// 使用了非零环绕规则
function bracelet(){
context.beginPath();
context.arc(300,190,150,0,Math.PI*2,false); //顺时针
context.arc(300,190,100,0,Math.PI*2,true); //逆时针
context.fillStyle="rgba(100,140,230,0.5)";
context.strokeStyle = context.fillStyle;
context.shadowColor="rgba(0,0,0,0.8)";
context.shadowOffsetX = 12;
context.shadowOffsetY = 12;
context.shadowBlur = 15;
context.fill();
context.stroke();
}
bracelet();

示例:绘制等分圆:

//描边
drawCircle(100,100,40,2,true);
drawCircle(200,100,40,3,true);
drawCircle(300,100,40,4,true);
drawCircle(400,100,40,20,true);
drawCircle(500,100,40,100,true);
drawCircle(600,100,40,200,true);
//填充
drawCircle(100,200,40,2);
drawCircle(200,200,40,3);
drawCircle(300,200,40,4);
drawCircle(400,200,40,20);
drawCircle(500,200,40,100);
drawCircle(600,200,40,200);
function drawCircle(x,y,r,n,isStroke){
for(var i = 0 ; i < n ; i++){
//计算开始和结束的角度
var angle = 2 * Math.PI / n;
var startAngle = angle * i;
var endAngle = angle * (i + 1);
context.beginPath();
//设置绘制圆的起点
context.moveTo(x,y);
context.arc(x,y,r,startAngle,endAngle,false);
if(isStroke){
// context.strokeStyle = getRandomColor();
context.stroke();
}else{
context.fillStyle = getRandomColor();
context.fill();
}
}
}
//获取填充的颜色/随机
function getRandomColor(){
var r = getRandom();
var g = getRandom();
var b = getRandom();
return "rgb("+r+","+g+","+b+")";
}
function getRandom(){
return Math.floor(Math.random() * 256);
}

示例:绘制饼形图

var data = [100, 50, 20, 30, 100];
context.fillStyle = "white";
context.fillRect(0,0,canvas.width,canvas.height);
var colors = [ "red","orange", "yellow","green", "blue"];
var total = 0;
for(var i=0; i<data.length; i++)
total += data[i];
var prevAngle = 0;
for(var i=0; i<data.length; i++) {
var fraction = data[i]/total; 
var angle = prevAngle + fraction * Math.PI * 2;
context.fillStyle = colors[i];
context.beginPath();
context.moveTo(150,150);
context.arc(150,150, 100, prevAngle, angle, false);
context.lineTo(150,150);
context.fill();
context.strokeStyle = "black";
context.stroke();
prevAngle = angle;
}

示例:饼形的类

function PieChart(context){ 
this.context = context || document.getElementById("canvas").getContext("2d");
this.x = this.context.canvas.width / 2;
this.y = this.context.canvas.height / 2;
this.r = 120;
this.outLine = 20;
this.dataList = null;
}
PieChart.prototype = {
constructor:PieChart,
init:function(dataList){
this.dataList = dataList || [{value:100}];

this.transformAngle();
this.drawPie();
},
drawPie:function(){
var startAngle = 0,endAngle;
for(var i = 0 ; i < this.dataList.length ; i++){
var item = this.dataList[i];
endAngle = startAngle + item.angle;
this.context.beginPath();
this.context.moveTo(this.x,this.y);
this.context.arc(this.x,this.y,this.r,startAngle,endAngle,false);
var color= this.context.strokeStyle= this.context.fillStyle= this.getRandomColor();
this.context.stroke();
this.context.fill();
this.drawPieLegend(i);
startAngle = endAngle;
}
},
drawPieLegend:function(index){
var space = 10;
var rectW = 40;
var rectH = 20;
var rectX = this.x + this.r + 80;
var rectY = this.y + (index * 30);
this.context.fillRect(rectX,rectY,rectW,rectH);
// this.context.beginPath();
},
getRandomColor:function(){
var r = Math.floor(Math.random() * 256);
var g = Math.floor(Math.random() * 256);
var b = Math.floor(Math.random() * 256);
return 'rgb('+r+','+g+','+b+')';
},
transformAngle:function(){
var self = this;
var total = 0;
this.dataList.forEach(function(item,i){
total += item.value;
})
this.dataList.forEach(function(item,i){
self.dataList[i].angle = 2 * Math.PI * item.value/total;
})
},
}
var data = [{value:20},{value:26},
{value:20},{value:63},{value:25}]
var pie = new PieChart().init(data);

arcTo(x1, y1, x2, y2, radius)方法:
根据控制点(x1, y1)、(x2, y2)和半径绘制圆弧路径;其根据当前点与给定的控制点1连接的直线,和控制点1与控制点2连接的直线,作为使用指定半径的圆的切线,画出两条切线之间的弧线路径;

说明:图例中的x0和y0,为当前点,x1和y1代表第一个控制点的坐标,x2和y2代表第二个控制点的坐标,radius代表圆弧半径;

context.beginPath();
context.moveTo(50,50);
context.arcTo(200, 60, 250, 300, 60);
context.stroke();
// 绘制提示点
context.fillRect(48,48, 4, 4); // 起点
context.fillRect(198,58, 4, 4); // 起点
context.fillRect(248,298, 4, 4); // 起点
// 绘制条切线
context.setLineDash([3]);
context.beginPath();
context.moveTo(50, 50);
context.lineTo(200, 60);
context.lineTo(250, 300);
context.stroke();

如果半径radius为0,则会绘制一点直线;

如,绘制一个圆角矩形:

context.moveTo(400, 50);
context.arcTo(500, 50, 500, 150, 30);
context.arcTo(500, 150, 400, 150, 20);
context.arcTo(400, 150, 400, 50, 10);
context.arcTo(400, 50, 500, 50, 0);
context.stroke();

bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)方法:
贝塞尔曲线(也称为三次贝塞尔曲线),将从当前点到指定坐标点中间的贝塞尔曲线添加到路径中,贝塞尔曲线需要两个控制点,分别为cp1x、cp1y和cp2x、cp2y,x和y是贝塞尔曲线的终点坐标;

context.moveTo(100, 300);
context.bezierCurveTo(160,200,280,300,320,250)
context.stroke();
context.fillRect(97, 297, 6, 6); // 起点
context.fillRect(317, 247, 6, 6); // 终点
context.fillRect(160-3, 200-3, 6,6); // 标记控制点
context.fillRect(280-3, 300-3, 6,6); // 标记控制点
// 线
context.beginPath();
context.strokeStyle = "red";
context.moveTo(100, 300);
context.lineTo(160, 200);
context.moveTo(320, 250);
context.lineTo(280, 300);
context.stroke();

结合正反弦函数绘图:

var dx = dy = 150;
var s = 100;
context.beginPath();
context.fillStyle = "lightgreen";
var dig = Math.PI / 15 * 11;
context.moveTo(dx,dy);
for(var i=0; i<30; i++){
var x = Math.sin(i * dig);
var y = Math.cos(i * dig);
context.bezierCurveTo(dx+x*s, dy+y*s-100, dx+x*s+100, dy+y*s, dx+x*s, dy+y*s);
}
context.closePath();
context.fill();
context.stroke();

示例:绘制心形

//三次贝塞尔曲线,绘制心形
context.beginPath();
context.moveTo(75, 40);
context.bezierCurveTo(75, 37, 70, 25, 50, 25);
context.bezierCurveTo(20, 25, 20, 62.5, 20, 62.5);
context.bezierCurveTo(20, 80, 40, 102, 75, 120);
context.bezierCurveTo(110, 102, 130, 80, 130, 62.5);
context.bezierCurveTo(130, 62.5, 130, 25, 100, 25);
context.bezierCurveTo(85, 25, 75, 37, 75, 40);
context.fill();

quadraticCurveTo(cx, cy, x, y)方法:
绘制二次贝塞尔曲线,相对来说,二次贝塞尔曲线的绘制比贝塞尔曲线的绘制容易一些,因为绘制贝塞尔曲线需要两个控制点,而绘制二次贝塞尔曲线时只需要一个控制点;因此quadraticCurveTo方法只需要四个参数就可以了,分别是控制点的坐标(cx, cy)、二次贝塞尔曲线终点的坐标(x,y);

context.moveTo(75, 250);
context.quadraticCurveTo(100,200,175,250);
context.stroke();
context.fillRect(72, 247, 6, 6); // 起点
context.fillRect(172, 247, 6, 6); // 起点
context.fillRect(100-3, 200-3, 6,6); // 控制点
// 线
context.beginPath();
context.strokeStyle = "red";
context.moveTo(75, 250);
context.lineTo(100, 200);
context.lineTo(175, 250);
context.stroke();

示例:对话气泡

context.beginPath();
context.moveTo(75, 25);
context.quadraticCurveTo(25, 25, 25, 62.5);
context.quadraticCurveTo(25, 100, 50, 100);
context.quadraticCurveTo(50, 120, 30, 125);
context.quadraticCurveTo(60, 120, 65, 100);
context.quadraticCurveTo(125, 100, 125, 62.5);
context.quadraticCurveTo(125, 25, 75, 25);
context.stroke();

绘制圆弧或椭圆:
ellipse(x, y, radiusX, radiusY, rotation, startAngle, endAngle, anticlockwise)方法:
绘制一条椭圆路径;椭圆的圆心在(x,y)位置,半径分别是radiusX 和 radiusY ,按照anticlockwise (默认顺时针)指定的方向,从 startAngle 开始绘制,到 endAngle 结束;参数rotation表示椭圆的旋转角度(单位是度,不是弧度),而startAngle和endAngle单位是弧度;

context.lineWidth = 20;
context.beginPath();
context.ellipse(200,100,100,50,0,0,Math.PI*2);
context.stroke();
context.beginPath();
context.ellipse(200,250,50,30,Math.PI / 4,0,Math.PI*2,true);
context.stroke();

如果radiusX与radiusY的值一致,绘制出来的是一个正圆;

// 把上例代码改成
context.ellipse(i*25,i*25,i*10,i*20,30,0,Math.PI*2,true);

该方法一开始不是标准方法,现在还未完全标准化,一开始只有Chrome支持,IE不支持此方法;

参数方程法绘制椭圆:

function ParamEllipse(context, x, y, a, b){
var step = (a > b ) ? 1 / a : 1 / b;
context.beginPath();
context.moveTo(x + a, y);
for(var i=0; i<2*Math.PI; i += step){
context.lineTo(x + a * Math.cos(i), y + b * Math.sin(i));
}
context.closePath();
context.stroke();
}
context.lineWidth = 10;
ParamEllipse(context, 130, 80, 100,20);

均匀压缩法绘制椭圆:

function EvenCompEllipse(context, x, y, a, b){
context.save();
var r = (a > b) ? a : b;
var ratioX = a / r; // x轴缩放比
var ratioY = b / r; // y轴缩放比
context.scale(ratioX, ratioY); // 进行缩放(均匀压缩)
context.beginPath();
// 从椭圆的左端点开始逆时针绘制
context.moveTo((x + a) / ratioX, y / ratioY);
context.arc(x / ratioX, y / ratioY, r, 0 , Math.PI*2);
context.closePath();
context.stroke();
context.restore();
}
context.lineWidth = 10;
EvenCompEllipse(context, 130, 200, 100, 20);

使用三次贝塞尔曲线模拟椭圆:

function BezierEllipse(context, x, y, a, b){
// 0.5和0.6是两个关键系数
var ox = 0.5 * a, oy = 0.6 * b;
context.save();
context.translate(x, y);
context.beginPath();
// 从椭圆的下端点开始逆时针绘制
context.moveTo(0, b);
context.bezierCurveTo(ox, b, a, oy, a, 0);
context.bezierCurveTo(a, -oy, ox, -b, 0, -b);
context.bezierCurveTo(-ox, -b, -a, -oy, -a, 0);
context.bezierCurveTo(-a, oy, -ox, b, 0, b);
context.closePath();
context.stroke();
context.restore();
}
context.lineWidth = 10;
BezierEllipse(context, 470, 80, 100, 20);

二次贝塞尔曲线模拟椭圆:

// 贝塞尔控制点x=(椭圆宽度/0.75)/2
CanvasRenderingContext2D.prototype.oval = function (x, y, width, height) {
var k = (width/0.75)/2,
w = width/2,
h = height/2;
this.beginPath();
this.moveTo(x, y-h);
this.bezierCurveTo(x+k, y-h, x+k, y+h, x, y+h);
this.bezierCurveTo(x-k, y+h, x-k, y-h, x, y-h);
this.closePath(); return this;
}
context.oval(300,100,200,50);
context.lineWidth = 10;
context.stroke();

rect(x, y, width, height)方法:
绘制一个左上角坐标为(x,y),宽高为width以及height的矩形路径,其路径会自动闭合;
当该方法执行的时候,moveTo()方法自动设置坐标参数(0,0),即当前笔触自动重置回默认坐标;

context.rect(10, 10, 100, 100);
context.fill();

roundrect(x, y, width, height)方法:
这是由Chrome实现的绘制圆角矩形的方法,其它浏览器都不支持,如:

context.beginPath();
context.roundRect(50,40,100,100,50);
context.closePath();
context.stroke();

兼容处理:

if(!CanvasRenderingContext2D.prototype.roundRect){
CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r){
this.x = x;
this.y = y;
this.w = w;
this.h = h;
this.r = r;

this.moveTo(this.x + this.r ,this.y);
this.arcTo(this.x + this.w , this.y , this.x + this.w , this.y + this.h , this.r);
this.arcTo(this.x + this.w , this.y + this.h , this.x , this.y + this.h , this.r);
this.arcTo(this.x , this.y + this.h , this.x , this.y , this.r);
this.arcTo(this.x , this.y , this.x + this.r , this.y , this.r);
}
}
var canvas = document.getElementsByTagName("canvas")[0];
if(canvas.getContext){
var context = canvas.getContext("2d");
context.beginPath();
context.roundRect(50,40,100,100,50);
context.closePath();
context.stroke();
context.beginPath();
context.roundRect(200,40,100,100,50);
context.closePath();
context.fill();

context.beginPath();
context.roundRect(350,40,100,100,10);
context.stroke();

context.beginPath();
context.roundRect(500,40,100,100,20);
context.closePath();
context.fill();

context.beginPath();
context.roundRect(650,40,120,100,30);
context.closePath();
context.stroke();
}

组合应用:

var canvas = document.getElementsByTagName("canvas")[0];
if(canvas.getContext){
var context = canvas.getContext("2d");
roundedRect(context, 12, 12, 150, 150, 15);
roundedRect(context, 19, 19, 150, 150, 9);
roundedRect(context, 53, 53, 49, 33, 10);
roundedRect(context, 53, 119, 49, 16, 6);
roundedRect(context, 135, 53, 49, 33, 10);
roundedRect(context, 135, 119, 25, 49, 10);
context.beginPath();
context.arc(37, 37, 13, Math.PI / 7, -Math.PI / 7);
context.lineTo(31, 37);
context.fill();
for(var i = 0; i < 8; i++){
context.fillRect(51 + i * 16, 35, 4, 4);
}
for(i = 0; i < 6; i++){
context.fillRect(115, 51 + i * 16, 4, 4);
}
for(i = 0; i < 8; i++){
context.fillRect(51 + i * 16, 99, 4, 4);
}
context.beginPath();
context.moveTo(83, 116);
context.lineTo(83, 102);
context.bezierCurveTo(83, 94, 89, 88, 97, 88);
context.bezierCurveTo(105, 88, 111, 94, 111, 102);
context.lineTo(111, 116);
context.lineTo(106.333, 111.333);
context.lineTo(101.666, 116);
context.lineTo(97, 111.333);
context.lineTo(92.333, 116);
context.lineTo(87.666, 111.333);
context.lineTo(83, 116);
context.fill();
context.fillStyle = "white";
context.beginPath();
context.moveTo(91, 96);
// context.bezierCurveTo(88, 96, 87, 99, 87, 101);
// context.bezierCurveTo(87, 103, 88, 106, 91, 106);
// context.bezierCurveTo(94, 106, 95, 103, 95, 101);
// context.bezierCurveTo(95, 99, 94, 96, 91, 96);
// 使用这一句代替以上的4个方法
context.ellipse(91,101,4,5,0,0,Math.PI*2);
context.moveTo(103, 96);
// context.bezierCurveTo(100, 96, 99, 99, 99, 101);
// context.bezierCurveTo(99, 103, 100, 106, 103, 106);
// context.bezierCurveTo(106, 106, 107, 103, 107, 101);
// context.bezierCurveTo(107, 99, 106, 96, 103, 96);
// 使用这一句代替以上的4个方法
context.ellipse(103,101,4,5,0,0,Math.PI*2);
context.fill();
context.fillStyle = "black";
context.beginPath();
context.arc(101, 102, 2, 0, Math.PI * 2, true);
context.fill();
context.beginPath();
context.arc(89, 102, 2, 0, Math.PI * 2, true);
context.fill();
}
// 封装的一个用于绘制圆角矩形的函数
function roundedRect(context, x, y, width, height, radius){
context.beginPath();
context.moveTo(x, y + radius);
context.lineTo(x, y + height - radius);
context.quadraticCurveTo(x, y + height, x + radius, y + height);
context.lineTo(x + width - radius, y + height);
context.quadraticCurveTo(x + width, y + height, x + width, y + height - radius);
context.lineTo(x + width, y + radius);
context.quadraticCurveTo(x + width, y, x + width - radius, y);
context.lineTo(x + radius, y);
context.quadraticCurveTo(x, y, x, y + radius);
context.stroke();
}

Path2D:
Canvas 2D API包含一个Path2D的接口,此接口用来声明路径,此路径会被2D上下文使用; Path2D拥有2D上下文的相同的路径方法,它允许在canvas中根据需要创建可以保留并重用的路径;

可以使用Path2D对象的各种方法绘制直线、矩形、圆形、椭圆以及曲线;如:moveTo(x,y)、lineTo(x,y)、rect(x,y,w,h)、arc(x,y,radius,startAngle,endAngle[,anticlockwise])、arcTo(x1,1,x2,y2,radiusX[,radius,rotation])、ellipse(x,y,radius,radius,rotation,startAngle,endAngle[,anticlockwise])、bezierCurveTo(cp1x,cp1y,cp2x,cp2y,x,y)、quadraticCurveTo(cpx,cpy,x,y)、closePath()

构造函数:Path([path | svgPath]),返回一个新的 Path2D 对象;参数path为另一个Path2D对象,这将该对象所代表的路径复制给新创建的Path2D对象;

new Path2D();
new Path2D(path);
new Path2D(d);

还可以在Path2D的构造函数中传递一个代表SVG路径的字符串;

var path = new Path2D("M10 10 h 80 v 80 h -80 Z");
context.fill(path);

创建完Path2D对象后,就可以利用所有绘制路径的方法绘制路径,并可以调用closePath()方法闭合路径,如:

var path1 = new Path2D();
path1.rect(10,10,100,100);
var path2 = new Path2D(path1);
path2.moveTo(220,60);
path2.arc(170,60,50,0,Math.PI*2);

之后可以使用context对象的fill(path)和stroke(path)方法进行填充和描边,参数path指向的Path2D对象;

context.stroke(path2);
// ...
for(var i=0;i<10;i++){
var path = new Path2D();
path.arc(i*25,i*25,i*10,0,Math.PI*2,true);
path.closePath();
context.fillStyle = "rgba(255,0,0,0.25)";
context.fill(path);
}

如:

var rectangle = new Path2D();
rectangle.rect(10, 10, 50, 50);
var circle = new Path2D();
circle.moveTo(125, 35);
circle.arc(100, 35, 25, 0, 2 * Math.PI);
context.stroke(rectangle);
context.fill(circle);

addPath(path [, transform])方法:
添加一条新路径到对当前路径;参数path为需要添加的 Path2D 路径,参数transform是可选的,作为新增路径的变换矩阵;

var p1 = new Path2D();
p1.rect(0,0,100,100);
var p2 = new Path2D();
p2.rect(0,0,100,100);
var m = document.createElementNS("http://www.w3.org/2000/svg", "svg").createSVGMatrix();
m.a = 1; m.b = 0;
m.c = 0; m.d = 1;
m.e = 300; m.f = 0;
p1.addPath(p2, m);
context.fill(p1);

图形属性:
图形属性指定了画布的通用图形状态,例如fillStyle、strokeStyle等属性,绘图方法并没有设置这些属性,所以这些属性应该在调用相关绘图方法之前进行设置;这种将从图形状态和绘制指令分离的思想是画布API很重要的概念,和在HTML文档中应用CSS样式来实现结构和样式分离是类似的;

context对象上定义了15个图形属性:

  • fillStyle:填充颜色、渐变和图案
  • strokeStyle:勾勒线段时的颜色、渐变或图案
  • linewidth属性:指定线条的宽度,值可以是任意整数;
  • lineCap属性:如何渲染线段的末端,即为直线添加线帽,值:butt默认值,不为直线添加线帽,round圆形线帽,square正方形线帽;
  • lineJoin属性:如何渲染顶点,即指定两条直线交汇时的拐角形状,值:miter默认值,尖角;round圆角;bevel斜角;
  • miterLimit:斜接顶点的最大长度;
  • font:绘制文本时的CSS字体;
  • textAlign:文本水平对齐;
  • textBaseline:垂直对齐方式;
  • shadowBlur:阴影模糊程序;
  • shadowColor:阴影颜色;
  • shadowOffsetX:阴影水平偏移量;
  • shadowOffsetY:阴影垂直偏移量;
  • globalAlpha:绘制像素时要添加的透明度;
  • globalCompositeOperation:如何合并新的像素点和下面的像素点;

fillStyle和strokeStyle属性:指定了区域填充和线条勾勒的样式;即可以指定颜色,也可以把它们设置为CanvasPattern或者CanvasGradient对象,以实现背景图案或渐变色;
如果使用颜色,其使用标准的CSS颜色值,默认为“#000000”;具体的颜色格式有:

var colors = [
"#f44", // 16进制RGB,红色
"#44ff44", // 16进制RRGGBB,绿色
"rgb(60,60,255)", // 0-255之间的整数表示的RGB,蓝色
"rgb(100%, 25%, 100%)", // 百分比表示的RGB,紫色
"rgba(100%, 25%, 100%, 0.5)", // RGB加上0-1的alpha,半透明紫色
"rgba(0,0,0,0)", // 全透明黑色
"transparent", // 全透明
"hsl(60,100%, 50%)", // 全饱和黄色
"hsl(60, 75%, 50%)", // 低包包黄色
"hsl(60, 100%, 75%)", // 全饱和暗黄色
"hsl(60, 100%, 25%)", // 全饱和亮黄色
"hsla(60, 100%, 50%, 0.5)" // 全饱和黄色,50%透明
];

一旦设置了strokeStyle或fillStyle的值,那么这个新值就会成为新绘制的图形的默认值,如果要给后续绘制的图形使用不同的颜色,需要重新设置fillStyle或strokeStyle的值;

for (var i=0;i<6;i++){
for (var j=0;j<6;j++){ // 255/6=42.5
context.fillStyle = 'rgb(' + Math.floor(255-42.5*i) + ',' +
Math.floor(255-42.5*j) + ',0)';
context.fillRect(j*25,i*25,25,25);
}
}

如:

for (var i=0;i<6;i++){
for (var j=0;j<6;j++){
context.strokeStyle = 'rgb(0,' + Math.floor(255-42.5*i) + ',' +
Math.floor(255-42.5*j) + ')';
context.beginPath();
context.arc(12.5+j*25,12.5+i*25,10,0,Math.PI*2,true);
context.stroke();
}
}

如:

context.fillStyle = 'rgb(255,221,0)';
context.fillRect(0,0,150,37.5);
context.fillStyle = 'rgb(102,204,0)';
context.fillRect(0,37.5,150,37.5);
context.fillStyle = 'rgb(0,153,255)';
context.fillRect(0,75,150,37.5);
context.fillStyle = 'rgb(255,51,0)';
context.fillRect(0,112.5,150,37.5);
// 画半透明矩形
for (var i=0; i<10; i++){
context.fillStyle = 'rgba(255,255,255,'+(i+1)/10+')';
for (var j=0; j<4; j++){
context.fillRect(5+i*14, 5+j*37.5, 14, 27.5)
}
}

globalAlpha属性:全局透明度,其值是介于0到1之间的值,用于指定所有绘制的透明度,默认值为1;如:

context.fillStyle = "#ff0000";
context.fillRect(50,50,100,100);
// 修改全局透明度
context.globalAlpha= 0.5;
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(50,100,100,100);

如果将其设为0,所有绘制的图形都会变成全透明;
绘制的每个像素都会将其alpha值乘以设置的globalAlpha值,例如:如果设置为0.5,那么所有绘制的原本不透明的像素都会变成50%的透明度,而原本是50%透明的,就变成25%的不透明度;
globalAlpha 属性在需要绘制大量拥有相同透明度的图形时候相当高效;如:

// 画背景
context.fillStyle = '#FD0';
context.fillRect(0,0,75,75);
context.fillStyle = '#6C0';
context.fillRect(75,0,75,75);
context.fillStyle = '#09F';
context.fillRect(0,75,75,75);
context.fillStyle = '#F30';
context.fillRect(75,75,75,75);
context.fillStyle = '#FFF';
context.globalAlpha = 0.2;
// 画半透明圆
for (var i=0;i<7;i++){
context.beginPath();
context.arc(75, 75, 10+10*i, 0, Math.PI*2, true);
context.fill();
}

线型Line styles:
lineWidth属性:指定线条的宽度,值可以是任意整数;实际上可以是小于1的小数

context.lineWidth = 0.5;
context.strokeRect(100,50, 300, 200);

线可以通过stroke()、strokeRect()和strokeText()方法绘制;

lineWidth属性没有相像中的那么简单:我们可以将路径视为一个无限细的线条,当调用stroke()方法进行轮廓描边时,它们是处于路径的中间,两边都是lineWidth宽度的一半;

for (var i = 0; i < 10; i++){
context.lineWidth = 1+i;
context.beginPath();
context.moveTo(5+i*14, 5);
context.lineTo(5+i*14, 140);
context.stroke();
}

示意图:

如果勾勒一条闭合的路径并只希望线段出现在路径之外,那么首先勾勒该路径,然后用不透明颜色填充闭合区域,将出现在路径内的勾勒部分隐藏;又或者如果只希望出在闭合路径内,可以调用clip()方法;

线段宽度是受当前坐标系变换影响的,如果通过坐标系变换来对坐标轴进行缩放,例如调用scale(2,1)方法就会对X轴进行缩放,但是对Y轴不产生影响,如此,垂直的线段要比原先和它一样宽的水平线段宽一倍;

lineCap属性:为直线添加线帽,值:butt默认值,不为直线添加线帽,即在线段端直接结束;round圆形线帽,即在原端点的基础上延长一个半圆;square正方形线帽,即在原端点的基础上,再延长线段宽度一半的长度;如图:

context.beginPath();
context.lineWidth = 10;
context.lineCap = "round";
context.moveTo(20,20);
context.lineTo(20,200);
context.stroke();
var lineCap = ['butt','round','square'];
// 绘制上下两条水平线条
context.strokeStyle = '#09f';
context.beginPath();
context.moveTo(60,10);
context.lineTo(200,10);
context.moveTo(60,140);
context.lineTo(200,140);
context.stroke();
// 绘制垂直三条不同端点的线条
context.strokeStyle = 'black';
for (var i = 0; i < lineCap.length; i++) {
context.lineWidth = 15;
context.lineCap = lineCap[i];
context.beginPath();
context.moveTo(80+i*50,10);
context.lineTo(80+i*50,140);
context.stroke();
}

lineJoin属性:指定两条线段交汇时的拐角形状,即在顶点处如何连接,值:miter默认值,尖角,表示一直延伸两条线段的外侧边缘直到在某一点汇合;round圆角,表示将汇合的顶点变成圆形;bevel斜角,表示把汇合的顶点切除;

context.beginPath();
context.lineWidth = 10;
context.lineJoin = "bevel";
context.moveTo(20,150);
context.lineTo(80,50);
context.lineTo(160,150);
context.stroke();
// 分别使用3种不同的lineJoin
var lineJoin = ['round','bevel','miter'];
context.lineWidth = 10;
for (var i = 0; i < lineJoin.length; i++) {
context.lineJoin = lineJoin[i];
context.beginPath();
context.moveTo(200, 50+i*40);
context.lineTo(250, 95+i*40);
context.lineTo(300, 50+i*40);
context.lineTo(350, 95+i*40);
context.lineTo(400, 50+i*40);
context.stroke();
}

miterLimit属性:当只有lineJoin属性值是miter时才会起作用;当两条线段相交的夹角为锐角的时候,两条线段的斜接部分可以变得很长,该属性可以指定斜接部分长度的上限,默认值是10.0;

context.lineWidth = 20;
console.log(context.miterLimit); // 10
context.miterLimit = 7;
context.moveTo(50,50);
context.lineTo(150,50);
// context.lineTo(50,150);
// context.lineTo(50,120);
context.lineTo(50,80); // 夹角小,斜接长度更长
context.stroke();

斜接最大长度是当前线宽与miterLimit属性值的乘积的一半,即斜接限定值(miterLimit)是斜接长度与一半线宽的比值;如果指定的miterLimit值比这个比值还小的话,最终绘制出来的顶点就会是斜切的(bevel)而不是斜接的;
当给属性赋值时,0、负数、 Infinity 和 NaN 都会被忽略;

setLineDash(segments)方法:自定义虚线形状;参数segments为一个数组,描述了线段和线段间隙的交替长度;

context.beginPath();
context.setLineDash([5, 15]);
context.moveTo(0, 50);
context.lineTo(300, 50);
context.stroke();

如果数组只存在一个值,则表示线段与间隙长度都等于这个值;
如果数组中的元素超过2个,则数组中的数值数量为偶数,即第奇个数代表线段长度,第偶数个数表示间隙长度;
如果数组中的元素超过2个,且不为偶数,会自动复制一份该值,使其成为偶数量;
如果要切换回至实线模式,将 dash list 设置为一个空数组即可;

context.setLineDash([5]);
context.setLineDash([5,10,15,20]);
// context.setLineDash([5,10,15])变成context.setLineDash([5,10,15,5,10,15]);
context.setLineDash([5,10,15]);
context.setLineDash([]);

常见的虚线模式,如:

var y = 15;
drawDashedLine([]);
drawDashedLine([1, 1]); // 或[1]
drawDashedLine([10, 10]);
drawDashedLine([20, 5]);
drawDashedLine([15, 3, 3, 3]);
drawDashedLine([20, 3, 3, 3, 3, 3, 3, 3]);
drawDashedLine([12, 3, 3]); // Equals [12, 3, 3, 12, 3, 3]
function drawDashedLine(pattern) {
context.beginPath();
context.setLineDash(pattern);
context.moveTo(0, y);
context.lineTo(300, y);
context.stroke();
y += 20;
}

getLineDash方法:返回一个包含当前虚线样式、长度为非负偶数的数组,如果数组元素的数量是奇数,数组元素会被复制并重复,如:

console.log(context.getLineDash()); // [12, 3, 3, 12, 3, 3]

lineDashOffset属性:设置虚线偏移量,值是浮点数,初始值为0.0,可以为负;正值向左偏,负值向右偏,如:

context.lineDashOffset = 5;

示例:蚂蚁线效果:

function draw(id){
var canvas = document.getElementById(id);
if(canvas==null){
return false;
}
var context = canvas.getContext("2d");
context.fillStyle = "#EEEEFF";
context.fillRect(0,0,400,300);

march(context); // [mɑːtʃ]
}
var i=0;
function march(context){
i++;
if(i>16){
i=0;
}
console.log(i);
context.clearRect(5,5,110,110);
context.setLineDash([4,2]);
context.lineDashOffset = -i;
context.strokeRect(10,10,100,100);
setTimeout(function(){
march(context);
},50);
}
draw("canvas");

绘制渐变:
绘制线性渐变:
fillStyle方法除了填充指定颜色外,还可以用来指定填充的对象,比如渐变对象;
createLinearGradient(x1, y1, x2, y2)方法:
返回一个CanvasGradient渐变对象,参数x1与y1为渐变起始点坐标,x2和y2为结束点坐标;如:

var gradient = context.createLinearGradient(30, 30, 70, 70);
console.log(gradient); // CanvasGradient

使用该对象的addColorStop(pos, color)方法指定色标,参数pos指定色标位置,即一个偏移量,值为0到1之间的浮动点,如0.5 表示颜色会出现在正中间,第二个参数color指定颜色;该方法必须调用二次以上;

gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");

创建了渐变对象并指定了颜色后,再把该对象赋值给fillStyle或strokeStyle属性,从而使用渐变来进行填充或描边;

context.fillStyle = gradient;
context.strokeStyle = gradient;
context.fillRect(0, 0, 50, 50);
context.lineWidth = 20;
context.strokeRect(30, 30, 50, 50);

渐变的坐标位置是相对于画布的,所以绘制的图形和渐变对象的坐标必须匹配;否则,有可能只会显示部分渐变效果;因此,确保渐变与图形对齐非常重要,如:

function createRectLinearGradient(context, x, y, width, height){
return context.createLinearGradient(x, y, x + width, y + height);
}
// ... 使用
var gradient = createRectLinearGradient(context, 30, 30, 50, 50);
console.log(gradient); // CanvasGradient

如使用两个渐变对象分别进行填充和描边:

// Create gradients
var lingrad = context.createLinearGradient(0,0,0,150);
lingrad.addColorStop(0, '#00ABEB');
lingrad.addColorStop(0.5, '#fff');
lingrad.addColorStop(0.5, '#26C000');
lingrad.addColorStop(1, '#fff');
var lingrad2 = context.createLinearGradient(0,50,0,95);
lingrad2.addColorStop(0.5, '#000');
lingrad2.addColorStop(1, 'rgba(0,0,0,0)');
context.fillStyle = lingrad;
context.strokeStyle = lingrad2;
context.fillRect(10,10,130,130);
context.strokeRect(50,50,50,50);

如绘制若干个渐变圆和矩形:

var g = context.createLinearGradient(0,0,300,0);
g.addColorStop(0,"rgba(0,0,255,0.5)");
g.addColorStop(1,"rgba(255,0,0,0.5)");
context.fillStyle = g;
for(var i=0; i<10; i++){
// 渐变圆
// context.beginPath();
// context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
// context.closePath();
// context.fill();
// 渐变矩形
context.fillRect(i*25, i*25, i*10, i*10);
}

绘制径向渐变:
同理,可以使用createRadialGradient(x1, y1, radius1, x2, y2, radius2)方法来绘制径向渐变; x1和y1为开始圆的圆心坐标,radius1为开始圆的半径,x2和y2为结果圆的圆心坐标,radius2为结束圆的半径;其他同线性渐变相同,也是使用addColorStop()方法添加色标;

var g = context.createRadialGradient(300,300,100,300,300,300);
g.addColorStop(0.0, "transparent");
g.addColorStop(0.7, "rgba(100,100,100,.9)");
g.addColorStop(1.0, "rgba(0,0,0,0)");
context.fillStyle = g;
context.fillRect(0,0,drawing.width, drawing.height);

如,绘制渐变圆:

var g1 = context.createRadialGradient(400,0,0,400,0,400);
g1.addColorStop(0.1,'rgb(255,255,0)');
g1.addColorStop(0.3,'rgb(255,0,255)');
g1.addColorStop(1,'rgb(0,255,255)');
context.fillStyle = g1;
context.fillRect(0,0,400,300);
var g2 = context.createLinearGradient(250,250,0,250,250,300);
g2.addColorStop(0.1,"rgba(255,0,0,0.5)");
g2.addColorStop(0.7,"rgba(255,255,0,0.5)");
g2.addColorStop(1,"rgba(0,0,255,0.5)");
context.fillStyle = g2;
for(var i=0;i<10;i++){
context.beginPath();
context.arc(i*25,i*25,i*10,0,Math.PI*2,true);
context.closePath();
context.fill();
}

如:

// 创建渐变
var radgrad = context.createRadialGradient(40,40,0,52,50,30);
radgrad.addColorStop(0, '#A7D30C');
radgrad.addColorStop(0.9, '#019F62');
radgrad.addColorStop(1, 'rgba(1,159,98,0)');
var radgrad2 = context.createRadialGradient(105,105,20,112,120,50);
radgrad2.addColorStop(0, '#FF5F98');
radgrad2.addColorStop(0.75, '#FF0188');
radgrad2.addColorStop(1, 'rgba(255,1,136,0)');
var radgrad3 = context.createRadialGradient(95,15,10,102,20,40);
radgrad3.addColorStop(0, '#00C9FF');
radgrad3.addColorStop(0.8, '#00B5E2');
radgrad3.addColorStop(1, 'rgba(0,201,255,0)');
var radgrad4 = context.createRadialGradient(0,150,50,0,140,90);
radgrad4.addColorStop(0, '#F4F201');
radgrad4.addColorStop(0.8, '#E4C700');
radgrad4.addColorStop(1, 'rgba(228,199,0,0)');
// 画图形
context.fillStyle = radgrad4;
context.fillRect(0,0,150,150);
context.fillStyle = radgrad3;
context.fillRect(0,0,150,150);
context.fillStyle = radgrad2;
context.fillRect(0,0,150,150);
context.fillStyle = radgrad;
context.fillRect(0,0,150,150);

模式(图案)Pattern:
模式就是重复的图像,也称为填充图案,可以用来填充或描边图形;使用createPattern(image,type)方法即可创建新模式,参数image为一个<img>元素或image对象,参数type指定重复的类型,其值与CSS的background-repeat值相同:no-repeat、repeat-x、repeat-y、repeat;返回的对象是CanvasPattern类型的模式对象;
创建完模式对象后,再赋给fillStyle即可,如:

var image = document.getElementsByTagName("img")[0];
var pattern = context.createPattern(image, "repeat");
console.log(pattern); // CanvasPattern
context.fillStyle = pattern;
context.fillRect(50, 50, 100, 100);

模式与渐变一样,都是从画布的原点(0,0)开始的;将模式对象赋给fillStyle属性,只表示在某个特定的区域内显示重复的图像,而不是要从某个位置开始绘制重复的图像;

还可以采用一个<canvas>元素作为另外一个<canvas>元素的图案,如:

var offscreen = document.createElement("canvas"); // 创建一个屏幕外canvas
offscreen.width = offscreen.height = 10; // 设置大小
offscreen.getContext("2d").strokeRect(0,0,6,6); // 绘制
var pattern = context.createPattern(offscreen, "repeat");
context.fillStyle = pattern;
context.fillRect(0,0, drawing.width,drawing.height);

CanvasPattern类型的模式对象没有任何属性,只有一个setTransform()方法,其用于对图案进行变形;
createPattern()方法的第一个参数,也可以是一个video元素,或者另一个canvas元素

绘制阴影:
2D上下文可以给图形绘制阴影效果,其使用context对象的关于阴影属性进行设置:shadowOffsetX、shadowOffsetY:横向或纵向位移,负值表示阴影会往上或左位移,正值则表示会往下或右位移,默认值为0;
shadowColor:阴影颜色,默认是完全透明的,如果不指定透明度或颜色,该阴影是不可见的;
shadowBlur:可选属性,表示图形阴影边缘的模糊范围,值设定在0到10之间;

context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
context.shadowBlur = 4;
context.fillStyle = '#FF0000';
context.fillRect(10,10, 50, 50);
context.fillStyle = 'rgba(0, 0, 255, 0.5)';
context.fillRect(10, 100, 50, 50);
context.shadowOffsetX = 10;
context.shadowOffsetY = 10;
context.shadowColor = "rgba(100,100,100,0.5)";
context.shadowBlur = 7.5;
context.translate(0,50);
for(var i=0;i<3;i++){
context.translate(50,50);
createStar(context);
context.fill();
}
function createStar(context){
var dx = 100;
var dy = 0;
var s = 50;
context.beginPath();
context.fillStyle = "rgba(255,0,0,0.5)";
var dig = Math.PI / 5 * 4;
for(var i=0;i<5;i++){
var x = Math.sin(i*dig);
var y = Math.cos(i*dig);
context.lineTo(dx+x*s,dy+y*s);
}
context.closePath();
}

shadowColor不允许使用图案和渐变色;
shadowOffsetX和shdowOffsetY属性总是在默认的坐标空间中度量的,它不受rotate()和scale()方法的影响;

保存与恢复状态:
使用save()和restore()两个方法,可以分别用于保存和恢复图形上下文的当前绘制状态;这里的绘画状态指的是坐标原点、变换矩阵,以及图形上下文对象的当前属性值等很多内容;
canvas状态存储在栈中,当调用save()方法时会将当前状态设置保存到栈中,当多次调用save()时,会在栈中保存多个状态,之后,在做完想做的工作后,再调用restore()从栈中取出之前保存的状态进行恢复,即在栈结构中向前返回一级,连续调用restore()则可以逐级返回;如:

context.fillStyle = "#FF0000";
context.save();
context.fillStyle = "#00FF00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000FF";
// 从点(100,100)开始绘制蓝色矩形
context.fillRect(0,0, 100, 100);
context.restore();
// 从点(110,110)开始绘制绿色矩形
context.fillRect(10,10,100,200);
context.restore();
// 从点(0,0,)开始绘制红色矩形
context.fillRect(0,0,100,200);
save()保存的只是对绘图上下文的设置和变换,不会保存绘图的内容;具体可以应用在以下场合:图像或图形变形(包括即移动,旋转和缩放)、图像裁剪、改变图形上下文的属性(strokeStyle、fillStyle、globalAlpha、lineWidth、lineCap、lineJoin、miterLimit、lineDashOfffset、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、globalCompositeOperation、font、textAlign、textBaseline、direction、imageSmothingEnabled);

示例:

context.fillRect(0,0,150,150); // 使用默认设置绘制一个矩形
context.save(); // 保存默认状态
context.fillStyle = '#09F' // 在原有配置基础上对颜色做改变
context.fillRect(15,15,120,120); // 使用新的设置绘制一个矩形
context.save(); // 保存当前状态
context.fillStyle = '#FFF' // 再次改变颜色配置
context.globalAlpha = 0.5;
context.fillRect(30,30,90,90); // 使用新的配置绘制一个矩形
context.restore(); // 重新加载之前的颜色状态
context.fillRect(45,45,60,60); // 使用上一次的配置绘制一个矩形
context.restore(); // 加载默认颜色配置
context.fillRect(60,60,30,30); // 使用加载的配置绘制一个矩形

绘制变换图形:
在绘制图形的时候,有可能需要旋转、缩放图形等变形处理;

变换方法:
translate(dx, dy):移动坐标原点到(dx, dy)处;参数dx和dy为X轴和Y轴的偏移量;

context.translate(100,100)
context.strokeRect(0,0,200,100);
context.beginPath();
context.arc(100,100,100,0,Math.PI*2);
context.stroke();

示例:

for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++) {
context.save();
context.fillStyle = 'rgb(' + (51 * i) + ', ' + (255 - 51 * i) + ', 255)';
context.translate(10 + j * 50, 10 + i * 50);
context.fillRect(0, 0, 25, 25); // 只关注宽和高,不再关注起始坐标
context.restore();
}
}

在一般的使用中,在使用变换时,会与状态的保存与恢复配合使用,便于下一次的绘制;

rotate(angle):以坐标原点为中心点旋转,angle为弧度,为正以顺时针旋转,为负则以逆时针旋转;

context.rotate(Math.PI / 4);
context.strokeRect(200,50,200,100);

context.beginPath();
context.moveTo(200, 20);
context.lineTo(300, 20);
context.stroke();

在旋转时,是以原点为中心点旋转的,必要时,需要移动原点,否则有可能会旋转到画布的外侧;

context.translate(100,100);
context.rotate(Math.PI / 4);
context.fillRect(0,0,200,100);

示例:绘制若干小不同颜色的小圆

context.translate(75,75);
for (var i=1; i<6; i++){
context.save();
context.fillStyle = 'rgb('+(51*i)+','+(255-51*i)+',255)';
for (var j=0; j<i*6; j++){
context.rotate(Math.PI*2/(i*6));
context.beginPath();
context.arc(0, i*12.5, 5, 0, Math.PI*2, true);
context.fill();
}
context.restore();
}

scale(scaleX, scaleY):放大缩小,两个参数都是实数,值为0到1,默认值为1;可以为负数,x 为水平缩放因子,y 为垂直缩放因子,如果比1小,会缩小图形,如果比1大会放大图形;

context.scale(2, 1);
context.lineWidth = 20;
context.strokeRect(50,50,100,50);

默认情况下,canvas的1个单位为1个像素,如果设置的缩放因子是0.5,那么1个单位就变成对应0.5个像素,这样绘制出来的形状就会是原先的一半;同理,设置为2.0时,1个单位就对应变成了2像素,绘制的结果就是图形放大了2倍;
如果线宽为1像素,且缩放因子小于1,此时,还是1像素呈现

context.save();
context.scale(0.5, 1); // 线宽为1的,因子小于1的还是1像素呈现
// context.lineWidth = 2;
context.strokeRect(50, 50, 100, 50);
context.restore();
context.save();
context.scale(10, 3);
context.fillRect(1, 100, 10, 10);
context.restore();

画布初始情况下,是以左上角坐标为原点的第一象限,如果参数为负实数,相当于以 x 或 y 轴作为对称轴镜像反转;例如:

context.translate(0, canvas.height);
context.scale(1,-1);
context.save();
context.lineWidth = 20;
context.moveTo(180,10);
context.lineTo(330,150);
context.lineTo(30,150);
context.closePath();
context.stroke(); // 变成倒三角了
context.restore();
context.translate(canvas.width, 0);
context.scale(-1, 1);
context.font = '48px serif';
context.fillText('ZERO', 10, 200);

变换有可能很简单,有可能很复杂,这都要视情况而定,如绘制螺旋的长方形:

context.fillStyle = "#EEEEFF";
context.fillRect(0,0,400,300);
context.translate(200,50);
// context.strokeRect(0, 0, 4, 4); // 现原点
context.fillStyle = 'rgba(255,0,0,0.25)';
for(var i=0; i<50; i++){ // 把50转成3,观察绘制过程
context.translate(25,25);
// context.strokeRect(0, 0, 4, 4); // 现原点
context.scale(0.95,0.95);
context.rotate(Math.PI / 10);
context.fillRect(0,0,100,50);
}

示例:绘制表盘和表针

// ...把前的代码换成以下的
context.translate(100,100);
context.rotate(1);
// 分针
context.moveTo(0, 0);
context.lineTo(0, -85);
// 时针
context.moveTo(0,0);
context.lineTo(-65,0);
// ...

示例:科赫雪花分形:

var deg = Math.PI / 180;
function snowflake(c, n, x, y, len){
c.save();
c.translate(x, y);
c.moveTo(0, 0);
draw(n);
c.rotate(-120 * deg);
draw(n);
c.rotate(-120 * deg);
draw(n);
c.closePath();
c.restore();
function draw(n){
c.save();
if(n == 0){
c.lineTo(len, 0);
}else{
c.scale(1/3, 1/3);
draw(n - 1);
c.rotate(60 * deg);
draw(n - 1);
c.rotate(-120 * deg);
draw(n - 1);
c.rotate(60 * deg);
draw(n - 1);
}
c.restore();
c.translate(len, 0);
}
}
snowflake(context, 0, 5, 115, 125);
snowflake(context, 1, 145, 115, 125);
snowflake(context, 2, 285, 115, 125);
snowflake(context, 3, 425, 115, 125);
snowflake(context, 4, 565, 115, 125);
context.stroke();

矩阵变换:
当利用坐标变换不能满足我们的需要时,可以利用矩阵变换的技术;矩阵变换是专门用来实现图形变形的,它与坐标一起配合使用,以达到变形的目的;
当context创建完毕后,进行一系列的属性设置和绘图操作,都是使用默认的坐标系;除了这个默认的坐标系,还有一个默认的“当前变换矩阵“,其作为当前图形状态的一部分,定义了画布的当前坐标系;如果不对这个变换矩阵进行修改,那么接下来绘制的图形将以画布的最左上角为坐标原点绘制图形,绘制出来的图形也不经过缩放、变形处理,会被直接绘制,但是如果对这个变换矩阵进行修改,会导致使用不同的变换矩阵应用处理,从而产生不同的效果;当前变换矩阵是用来将指定的坐标转换成为默认坐标系中的等价坐标。

transform(a, b, c, d, e, f),变换矩阵;
其使用一个新的变换矩阵与当前变换矩阵进行乘法运算,该变换矩阵的描述是:
a(m11) c(m21) e(dx)
b(m12) d(m22) f(dy)
0 0 1
其中,a(m11)、b(m12)、c(m21)、d(m22)这4个参数用来决定如何变形,分别表示水平缩放、垂直倾斜、水平倾斜、垂直缩放;e(dx)和f(dy)参数表示水平和垂直移动坐标原点;

context.save();
context.fillStyle = "red";
context.fillRect(0,0,100,100);
context.fillRect(100,120,100,100);
context.restore();
context.translate(100,120);
context.transform(2, Math.PI / 4, -Math.PI / 4, 0.5, 50, 50);
context.fillRect(0,0,100,100);

示例:通过变换绘制螺旋

context.translate(200,50);
for(var i=0; i<50; i++){
context.save();
context.transform(0.95,0,0,0.95,30,30);
context.rotate(Math.PI/12);
context.beginPath();
context.fillStyle = 'rgba(255,0,0,0.5)';
context.arc(0,0,50,0,Math.PI*2,true);
context.closePath();
context.fill();
}

示例:多条彩色弧

var colors = ["red","orange","yellow","green","blue","navy","purple"];
context.lineWidth = 10;
context.transform(1,0,0,1,100,0);
for(var i=0;i<colors.length;i++){
context.transform(1,0,0,1,0,10);
context.strokeStyle = colors[i];
context.beginPath();
context.arc(50,100,100,0,Math.PI,true);
context.stroke();
}

setTransform(a, b, c, d, e, f)方法:这个方法会将当前的变形矩阵重置为默认矩阵,然后用相同的参数调用transform()方法;从根本上来说,该方法是取消了当前变形,然后再设置为指定的变形,如:

// context.transform(1,1,0,1,0,0);
context.setTransform(1,1,0,1,0,0); // 起到相同的效果
context.fillRect(0,0,100,100);

它经常被用在,临时将画布重置为默认坐标系,如:

context.strokeStyle = "red";
context.strokeRect(30,10,60,20);
// 旋转45度
var rad = 45 * Math.PI / 180;
context.setTransform(Math.cos(rad),Math.sin(rad),-Math.sin(rad),Math.cos(rad),0,0);
context.strokeStyle = "blue";
context.strokeRect(30,10,60,20);
// 放大2.5倍
context.setTransform(2.5,0,0,2.5,0,0);
context.strokeStyle = "green";
context.strokeRect(30,10,60,20);
// 移动坐标原点
context.setTransform(1,0,0,1,40,80);
context.strokeStyle = "gray";
context.strokeRect(30,10,60,20);
context.save(); // 保存当前默认坐标系
context.setTransform(1,0,0,1,0,0); // 恢复到默认坐标系
// 使用默认的坐标进行绘图操作
context.restore(); // 恢复保存的坐标系
setTransform()与transform()区别:
// context.transform(2,0,0,1,0,0);
context.setTransform(2,0,0,1,0,0);
context.fillRect(0,0,100,100);
// context.transform(2,0,0,2,0,0);
context.setTransform(2,0,0,2,0,0);
context.fillStyle = "red";
context.fillRect(50,50,100,100);

再如:

var sin = Math.sin(Math.PI/6);
var cos = Math.cos(Math.PI/6);
context.translate(100, 100);
var c = 0;
for (var i=0; i <= 12; i++) {
c = Math.floor(255 / 12 * i);
context.fillStyle = "rgb(" + c + "," + c + "," + c + ")";
context.fillRect(0, 0, 100, 10);
context.transform(cos, sin, -sin, cos, 0, 0);
}
context.setTransform(-1, 0, 0, 1, 100, 100);
context.fillStyle = "rgba(255, 128, 255, 0.5)";
context.fillRect(0, 50, 100, 100);

resetTransform():方法
重置当前变形矩阵,它和调用以下语句是一样的:context.setTransform(1, 0, 0, 1, 0, 0);

context.rotate(45 * Math.PI / 180);
context.fillRect(70,0,100,30);
context.resetTransform();

深入讨论:
translate、scale、rotate三个方法,实际上都是隐式的修改了变换矩阵;
translate()方法只是简单的将坐标原点进行上下左右移动;而rotate()方法会将坐标轴根据指定的角度进行顺时针旋转;scale()方法实现对X轴或Y轴上的距离进行延长和缩短;
假定变换后坐标系中的点的坐标是(x, y),其对应的默认坐标系的坐标为(x‘, y‘);

调用translate(dx, dy)方法等效于:

x’ = x + dx;
y’ = y + dy;

调用scale(sx, sy)方法等效于:
x’ = sx * x;
y’ = sy * y;

调用rotate(a)方法,可以通过三角法则计算,如:
x’ = x * cos(a) – y * sin(a);
y’ = y * cos(a) + x * sin(a);

坐标系变换是与变换顺序相关的;假设从画布默认的坐标系开始,先进行位移,再进行伸缩;如此操作后,要想将现有坐标系中的点(x, y)映射成默认坐标系中的点(x’’, y’’),必须首先应用等效缩放等式把该点映射到未缩放坐标系中的一个中间点(x’, y’),然后再使用等效的位移将中间点再映射到原来坐标系中的点(x’’, y’’),结果是:
x’’ = sx * x + dx;
y’’ = sy * y + dy;

如果先调用scale()再调用translate()的话,那等效的结果就不同了,结果是:
x’’ = sx * (x + dx);
y’’ = sy * (y + dy);

translate、scale、rotate三个方法都可以使用transform方法进行处理:
translate(x, y)可以使用transform(1, 0, 0, 1, x, y)代替,参数1,0,0,1表示不对图形进行缩放倾斜操作,只是位移x和y;
scale(x, y)可以使用transform(x, 0, 0, y, 0, 0)代替,参数x,0,0,y表示将图形横向扩大x倍,纵向扩大y倍;
rotate(angle)替换方法:
transform(Math.cos(angle * Math.PI / 180), Math.sin(angle * Math.PI / 180),-Math.sin(angle * Math.PI / 180), Math.cos(angle * Math.PI / 180),0,0)

其中前4个参数以三角函数的形式结合起来,共同完成图形按angle角度的顺时针旋转处理;

者:前端藏经阁

转发链接:https://www.yuque.com/xwifrr/qbgcq0/vkczro

目录

JS正则表达式完整教程(一)「值得收藏」本篇

JS正则表达式完整教程(二)「值得收藏」

小编建议小伙们从第一篇开始看起,更清晰明了

引言

亲爱的读者朋友,如果你点开了这篇文章,说明你对正则很感兴趣。

想必你也了解正则的重要性,在我看来正则表达式是衡量程序员水平的一个侧面标准。

关于正则表达式的教程,网上也有很多,相信你也看了一些。

与之不同的是,本文的目的是希望所有认真读完的童鞋们,都有实质性的提高。

本文内容共有七章,用JavaScript语言完整地讨论了正则表达式的方方面面。

如果觉得文章某块儿没有说明白清楚,欢迎留言,能力范围之内,老姚必做详细解答。

具体章节如下:

  • 引言
  • 第一章 正则表达式字符匹配攻略
  • 第二章 正则表达式位置匹配攻略
  • 第三章 正则表达式括号的作用
  • 第四章 正则表达式回溯法原理
  • 第五章 正则表达式的拆分
  • 第六章 正则表达式的构建
  • 第七章 正则表达式编程
  • 后记

下面简单地说说每一章都讨论了什么?

正则是匹配模式,要么匹配字符,要么匹配位置。

第1章和第2章以这个角度去讲解了正则的基础。

在正则中可以使用括号捕获数据,要么在API中进行分组引用,要么在正则里进行反向引用。

这是第3章的主题,讲解了正则中括号的作用。

学习正则表达式,是需要了解其匹配原理的。

第4章,讲解了正则了正则表达式的回溯法原理。另外在第6章里,也讲解了正则的表达式的整体工作原理。

不仅能看懂别人的正则,还要自己会写正则。

第5章,是从读的角度,去拆分一个正则表达式,而第6章是从写的角度,去构建一个正则表达式。

学习正则,是为了在真实世界里应用的。

第7章讲解了正则的用法,和相关API需要注意的地方。

如何阅读本文?

我的建议是阅读两遍。第一遍,不求甚解地快速阅读一遍。阅读过程中遇到的问题不妨记录下来,也许阅读完毕后就能解决很多。然后有时间的话,再带着问题去精读第二遍。

深呼吸,开始我们的正则表达式旅程吧。我在终点等你。

第一章 正则表达式字符匹配攻略

正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

然而关于正则如何匹配字符的学习,大部分人都觉得这块比较杂乱。

毕竟元字符太多了,看起来没有系统性,不好记。本章就解决这个问题。

内容包括:

  1. 两种模糊匹配
  2. 字符组
  3. 量词
  4. 分支结构
  5. 案例分析

1 两种模糊匹配

如果正则只有精确匹配是没多大意义的,比如/hello/,也只能匹配字符串中的"hello"这个子串。

var regex = /hello/;
console.log( regex.test("hello") ); 
// => true
复制代码

正则表达式之所以强大,是因为其能实现模糊匹配。

而模糊匹配,有两个方向上的“模糊”:横向模糊和纵向模糊。

1.1 横向模糊匹配

横向模糊指的是,一个正则可匹配的字符串的长度不是固定的,可以是多种情况的。

其实现的方式是使用量词。譬如{m,n},表示连续出现最少m次,最多n次。

比如/ab{2,5}c/表示匹配这样一个字符串:第一个字符是“a”,接下来是2到5个字符“b”,最后是字符“c”。测试如下:

var regex = /ab{2,5}c/g;
var string = "abc abbc abbbc abbbbc abbbbbc abbbbbbc";
console.log( string.match(regex) ); 
// => ["abbc", "abbbc", "abbbbc", "abbbbbc"]
复制代码

注意:案例中用的正则是/ab{2,5}c/g,后面多了g,它是正则的一个修饰符。表示全局匹配,即在目标字符串中按顺序找到满足匹配模式的所有子串,强调的是“所有”,而不只是“第一个”。g是单词global的首字母。

1.2 纵向模糊匹配

纵向模糊指的是,一个正则匹配的字符串,具体到某一位字符时,它可以不是某个确定的字符,可以有多种可能。

其实现的方式是使用字符组。譬如[abc],表示该字符是可以字符“a”、“b”、“c”中的任何一个。

比如/a[123]b/可以匹配如下三种字符串:"a1b"、"a2b"、"a3b"。测试如下:

var regex = /a[123]b/g;
var string = "a0b a1b a2b a3b a4b";
console.log( string.match(regex) ); 
// => ["a1b", "a2b", "a3b"]
复制代码

以上就是本章讲的主体内容,只要掌握横向和纵向模糊匹配,就能解决很大部分正则匹配问题。

接下来的内容就是展开说了,如果对此都比较熟悉的话,可以跳过,直接看本章案例那节。

2. 字符组

需要强调的是,虽叫字符组(字符类),但只是其中一个字符。例如[abc],表示匹配一个字符,它可以是“a”、“b”、“c”之一。

2.1 范围表示法

如果字符组里的字符特别多的话,怎么办?可以使用范围表示法。

比如[123456abcdefGHIJKLM],可以写成[1-6a-fG-M]。用连字符-来省略和简写。

因为连字符有特殊用途,那么要匹配“a”、“-”、“z”这三者中任意一个字符,该怎么做呢?

不能写成[a-z],因为其表示小写字符中的任何一个字符。

可以写成如下的方式:[-az]或[az-]或[a\-z]。即要么放在开头,要么放在结尾,要么转义。总之不会让引擎认为是范围表示法就行了。

2.2 排除字符组

纵向模糊匹配,还有一种情形就是,某位字符可以是任何东西,但就不能是"a"、"b"、"c"。

此时就是排除字符组(反义字符组)的概念。例如[^abc],表示是一个除"a"、"b"、"c"之外的任意一个字符。字符组的第一位放^(脱字符),表示求反的概念。

当然,也有相应的范围表示法。

2.3 常见的简写形式

有了字符组的概念后,一些常见的符号我们也就理解了。因为它们都是系统自带的简写形式。

\d就是[0-9]。表示是一位数字。记忆方式:其英文是digit(数字)。

\D就是[^0-9]。表示除数字外的任意字符。

\w就是[0-9a-zA-Z_]。表示数字、大小写字母和下划线。记忆方式:w是word的简写,也称单词字符。

\W是[^0-9a-zA-Z_]。非单词字符。

\s是[ \t\v\n\r\f]。表示空白符,包括空格、水平制表符、垂直制表符、换行符、回车符、换页符。记忆方式:s是space character的首字母。

\S是[^ \t\v\n\r\f]。 非空白符。

.就是[^\n\r]。通配符,表示几乎任意字符。换行符、回车符、行分隔符和段分隔符除外。记忆方式:想想省略号...中的每个点,都可以理解成占位符,表示任何类似的东西。

如果要匹配任意字符怎么办?可以使用[\d\D]、[\w\W]、[\s\S]和[^]中任何的一个。

3. 量词

量词也称重复。掌握{m,n}的准确含义后,只需要记住一些简写形式。

3.1 简写形式

{m,} 表示至少出现m次。

{m} 等价于{m,m},表示出现m次。

? 等价于{0,1},表示出现或者不出现。记忆方式:问号的意思表示,有吗?

+ 等价于{1,},表示出现至少一次。记忆方式:加号是追加的意思,得先有一个,然后才考虑追加。

* 等价于{0,},表示出现任意次,有可能不出现。记忆方式:看看天上的星星,可能一颗没有,可能零散有几颗,可能数也数不过来。

3.2 贪婪匹配和惰性匹配

看如下的例子:

var regex = /\d{2,5}/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); 
// => ["123", "1234", "12345", "12345"]
复制代码

其中正则/\d{2,5}/,表示数字连续出现2到5次。会匹配2位、3位、4位、5位连续数字。

但是其是贪婪的,它会尽可能多的匹配。你能给我6个,我就要5个。你能给我3个,我就3万个。反正只要在能力范围内,越多越好。

我们知道有时贪婪不是一件好事(请看文章最后一个例子)。而惰性匹配,就是尽可能少的匹配:

var regex = /\d{2,5}?/g;
var string = "123 1234 12345 123456";
console.log( string.match(regex) ); 
// => ["12", "12", "34", "12", "34", "12", "34", "56"]
复制代码

其中/\d{2,5}?/表示,虽然2到5次都行,当2个就够的时候,就不再往下尝试了。

通过在量词后面加个问号就能实现惰性匹配,因此所有惰性匹配情形如下:

{m,n}? {m,}???+?*?

对惰性匹配的记忆方式是:量词后面加个问号,问一问你知足了吗,你很贪婪吗?

4. 多选分支

一个模式可以实现横向和纵向模糊匹配。而多选分支可以支持多个子模式任选其一。

具体形式如下:(p1|p2|p3),其中p1、p2和p3是子模式,用|(管道符)分隔,表示其中任何之一。

例如要匹配"good"和"nice"可以使用/good|nice/。测试如下:

var regex = /good|nice/g;
var string = "good idea, nice try.";
console.log( string.match(regex) ); 
// => ["good", "nice"]
复制代码

但有个事实我们应该注意,比如我用/good|goodbye/,去匹配"goodbye"字符串时,结果是"good":

var regex = /good|goodbye/g;
var string = "goodbye";
console.log( string.match(regex) ); 
// => ["good"]
复制代码

而把正则改成/goodbye|good/,结果是:

var regex = /goodbye|good/g;
var string = "goodbye";
console.log( string.match(regex) ); 
// => ["goodbye"]
复制代码

也就是说,分支结构也是惰性的,即当前面的匹配上了,后面的就不再尝试了。

5. 案例分析

匹配字符,无非就是字符组、量词和分支结构的组合使用罢了。

下面找几个例子演练一下(其中,每个正则并不是只有唯一写法):

5.1 匹配16进制颜色值

要求匹配:

#ffbbad

#Fc01DF

#FFF

#ffE

分析:

表示一个16进制字符,可以用字符组[0-9a-fA-F]。

其中字符可以出现3或6次,需要使用量词和分支结构。

使用分支结构时,需要注意顺序。

正则如下:

var regex = /#([0-9a-fA-F]{6}|[0-9a-fA-F]{3})/g;
var string = "#ffbbad #Fc01DF #FFF #ffE";
console.log( string.match(regex) ); 
// => ["#ffbbad", "#Fc01DF", "#FFF", "#ffE"]
复制代码

5.2 匹配时间

以24小时制为例。

要求匹配:

23:59

02:07

分析:

共4位数字,第一位数字可以为[0-2]。

当第1位为2时,第2位可以为[0-3],其他情况时,第2位为[0-9]。

第3位数字为[0-5],第4位为[0-9]

正则如下:

var regex = /^([01][0-9]|[2][0-3]):[0-5][0-9]$/;
console.log( regex.test("23:59") ); 
console.log( regex.test("02:07") ); 
// => true
// => true复制代码

如果也要求匹配7:9,也就是说时分前面的0可以省略。

此时正则变成:

var regex = /^(0?[0-9]|1[0-9]|[2][0-3]):(0?[0-9]|[1-5][0-9])$/;
console.log( regex.test("23:59") ); 
console.log( regex.test("02:07") ); 
console.log( regex.test("7:9") ); 
// => true
// => true
// => true复制代码

5.3 匹配日期

比如yyyy-mm-dd格式为例。

要求匹配:

2017-06-10

分析:

年,四位数字即可,可用[0-9]{4}。

月,共12个月,分两种情况01、02、……、09和10、11、12,可用(0[1-9]|1[0-2])。

日,最大31天,可用(0[1-9]|[12][0-9]|3[01])。

正则如下:

var regex = /^[0-9]{4}-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])$/;
console.log( regex.test("2017-06-10") ); 
// => true
复制代码

5.4 window操作系统文件路径

要求匹配:

F:\study\javascript\regex\regular expression.pdf

F:\study\javascript\regex\

F:\study\javascript

F:\

分析:

整体模式是: 盘符:\文件夹\文件夹\文件夹\

其中匹配F:\,需要使用[a-zA-Z]:\,其中盘符不区分大小写,注意\字符需要转义。

文件名或者文件夹名,不能包含一些特殊字符,此时我们需要排除字符组[^\:*<>|"?\r\n/]来表示合法字符。另外不能为空名,至少有一个字符,也就是要使用量词+。因此匹配“文件夹\”,可用[^\:*<>|"?\r\n/]+\。

另外“文件夹\”,可以出现任意词。也就是([^\:*<>|"?\r\n/]+\)*。其中括号提供的表达式。

路径的最后一部分可以是“文件夹”,没有\,因此需要添加([^\:*<>|"?\r\n/]+)?。

最后拼接成了一个看起来比较复杂的正则:

var regex = /^[a-zA-Z]:\\([^\\:*<>|"?\r\n/]+\\)*([^\\:*<>|"?\r\n/]+)?$/;
console.log( regex.test("F:\\study\\javascript\\regex\\regular expression.pdf") ); 
console.log( regex.test("F:\\study\\javascript\\regex\\") ); 
console.log( regex.test("F:\\study\\javascript") ); 
console.log( regex.test("F:\\") ); 
// => true
// => true
// => true
// => true复制代码

其中,JS中字符串表示\时,也要转义。

5.5 匹配id

要求从

<div id="container" class="main"></div>

提取出id="container"。

可能最开始想到的正则是:

var regex = /id=".*"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]); 
// => id="container" class="main"
复制代码

因为.是通配符,本身就匹配双引号的,而量词*又是贪婪的,当遇到container后面双引号时,不会停下来,会继续匹配,直到遇到最后一个双引号为止。

解决之道,可以使用惰性匹配:

var regex = /id=".*?"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]); 
// => id="container"
复制代码

当然,这样也会有个问题。效率比较低,因为其匹配原理会涉及到“回溯”这个概念(这里也只是顺便提一下,第四章会详细说明)。可以优化如下:

var regex = /id="[^"]*"/
var string = '<div id="container" class="main"></div>';
console.log(string.match(regex)[0]); 
// => id="container"复制代码

第1章 小结

字符匹配相关的案例,挺多的,不一而足。

掌握字符组和量词就能解决大部分常见的情形,也就是说,当你会了这二者,JS正则算是入门了。


第二章 正则表达式位置匹配攻略

正则表达式是匹配模式,要么匹配字符,要么匹配位置。请记住这句话。

然而大部分人学习正则时,对于匹配位置的重视程度没有那么高。

本章讲讲正则匹配位置的总总。

内容包括:

  1. 什么是位置?
  2. 如何匹配位置?
  3. 位置的特性
  4. 几个应用实例分析

1. 什么是位置呢?

位置是相邻字符之间的位置。比如,下图中箭头所指的地方:

2. 如何匹配位置呢?

在ES5中,共有6个锚字符:

^ $ \b \B (?=p) (?!p)

2.1 ^和$

^(脱字符)匹配开头,在多行匹配中匹配行开头。

$(美元符号)匹配结尾,在多行匹配中匹配行结尾。

比如我们把字符串的开头和结尾用"#"替换(位置可以替换成字符的!):

var result = "hello".replace(/^|$/g, '#');
console.log(result); 
// => "#hello#"
复制代码

多行匹配模式时,二者是行的概念,这个需要我们的注意:

var result = "I\nlove\njavascript".replace(/^|$/gm, '#');
console.log(result);
/*
#I#
#love#
#javascript#
*/
复制代码

2.2 \b和\B

\b是单词边界,具体就是\w和\W之间的位置,也包括\w和^之间的位置,也包括\w和$之间的位置。

比如一个文件名是"[JS] Lesson_01.mp4"中的\b,如下:

var result = "[JS] Lesson_01.mp4".replace(/\b/g, '#');
console.log(result); 
// => "[#JS#] #Lesson_01#.#mp4#"
复制代码

为什么是这样呢?这需要仔细看看。

首先,我们知道,\w是字符组[0-9a-zA-Z_]的简写形式,即\w是字母数字或者下划线的中任何一个字符。而\W是排除字符组[^0-9a-zA-Z_]的简写形式,即\W是\w以外的任何一个字符。

此时我们可以看看"[#JS#] #Lesson_01#.#mp4#"中的每一个"#",是怎么来的。

  • 第一个"#",两边是"["与"J",是\W和\w之间的位置。
  • 第二个"#",两边是"S"与"]",也就是\w和\W之间的位置。
  • 第三个"#",两边是空格与"L",也就是\W和\w之间的位置。
  • 第四个"#",两边是"1"与".",也就是\w和\W之间的位置。
  • 第五个"#",两边是"."与"m",也就是\W和\w之间的位置。
  • 第六个"#",其对应的位置是结尾,但其前面的字符"4"是\w,即\w和$之间的位置。

知道了\b的概念后,那么\B也就相对好理解了。

\B就是\b的反面的意思,非单词边界。例如在字符串中所有位置中,扣掉\b,剩下的都是\B的。

具体说来就是\w与\w、\W与\W、^与\W,\W与$之间的位置。

比如上面的例子,把所有\B替换成"#":

var result = "[JS] Lesson_01.mp4".replace(/\B/g, '#');
console.log(result); 
// => "#[J#S]# L#e#s#s#o#n#_#0#1.m#p#4"
复制代码

2.3 (?=p)和(?!p)

(?=p),其中p是一个子模式,即p前面的位置。

比如(?=l),表示'l'字符前面的位置,例如:

var result = "hello".replace(/(?=l)/g, '#');
console.log(result); 
// => "he#l#lo"
复制代码

而(?!p)就是(?=p)的反面意思,比如:

var result = "hello".replace(/(?!l)/g, '#');
console.log(result); 
// => "#h#ell#o#"
复制代码

二者的学名分别是positive lookahead和negative lookahead。

中文翻译分别是正向先行断言和负向先行断言。

ES6中,还支持positive lookbehind和negative lookbehind。

具体是(?<=p)和(?<!p)。

也有书上把这四个东西,翻译成环视,即看看右边或看看左边。

但一般书上,没有很好强调这四者是个位置。

比如(?=p),一般都理解成:要求接下来的字符与p匹配,但不能包括p的那些字符。

而在本人看来(?=p)就与^一样好理解,就是p前面的那个位置。

3. 位置的特性

对于位置的理解,我们可以理解成空字符""。

比如"hello"字符串等价于如下的形式:

"hello" == "" + "h" + "" + "e" + "" + "l" + "" + "l" + "o" + "";
复制代码

也等价于:

"hello" == "" + "" + "hello"
复制代码

因此,把/^hello$/写成/^^hello$$$/,是没有任何问题的:

var result = /^^hello$$$/.test("hello");
console.log(result); 
// => true
复制代码

甚至可以写成更复杂的:

var result = /(?=he)^^he(?=\w)llo$\b\b$/.test("hello");
console.log(result); 
// => true
复制代码

也就是说字符之间的位置,可以写成多个。

把位置理解空字符,是对位置非常有效的理解方式。

4. 相关案例

4.1 不匹配任何东西的正则

让你写个正则不匹配任何东西

easy,/.^/

因为此正则要求只有一个字符,但该字符后面是开头。

4.2 数字的千位分隔符表示法

比如把"12345678",变成"12,345,678"。

可见是需要把相应的位置替换成","。

思路是什么呢?

4.2.1 弄出最后一个逗号

使用(?=\d{3}$)就可以做到:

var result = "12345678".replace(/(?=\d{3}$)/g, ',')
console.log(result); 
// => "12345,678"
复制代码

4.2.2 弄出所有的逗号

因为逗号出现的位置,要求后面3个数字一组,也就是\d{3}至少出现一次。

此时可以使用量词+:

var result = "12345678".replace(/(?=(\d{3})+$)/g, ',')
console.log(result); 
// => "12,345,678"
复制代码

4.2.3 匹配其余案例

写完正则后,要多验证几个案例,此时我们会发现问题:

var result = "123456789".replace(/(?=(\d{3})+$)/g, ',')
console.log(result); 
// => ",123,456,789"
复制代码

因为上面的正则,仅仅表示把从结尾向前数,一但是3的倍数,就把其前面的位置替换成逗号。因此才会出现这个问题。

怎么解决呢?我们要求匹配的到这个位置不能是开头。

我们知道匹配开头可以使用^,但要求这个位置不是开头怎么办?

easy,(?!^),你想到了吗?测试如下:

var string1 = "12345678",
string2 = "123456789";
reg = /(?!^)(?=(\d{3})+$)/g;
var result = string1.replace(reg, ',')
console.log(result); 
// => "12,345,678"
result = string2.replace(reg, ',');
console.log(result); 
// => "123,456,789"
复制代码

4.2.4 支持其他形式

如果要把"12345678 123456789"替换成"12,345,678 123,456,789"。

此时我们需要修改正则,把里面的开头^和结尾$,替换成\b:

var string = "12345678 123456789",
reg = /(?!\b)(?=(\d{3})+\b)/g;
var result = string.replace(reg, ',')
console.log(result); 
// => "12,345,678 123,456,789"
复制代码

其中(?!\b)怎么理解呢?

要求当前是一个位置,但不是\b前面的位置,其实(?!\b)说的就是\B。

因此最终正则变成了:/\B(?=(\d{3})+\b)/g。

4.3 验证密码问题

密码长度6-12位,由数字、小写字符和大写字母组成,但必须至少包括2种字符。

此题,如果写成多个正则来判断,比较容易。但要写成一个正则就比较困难。

那么,我们就来挑战一下。看看我们对位置的理解是否深刻。

4.3.1 简化

不考虑“但必须至少包括2种字符”这一条件。我们可以容易写出:

var reg = /^[0-9A-Za-z]{6,12}$/;
复制代码

4.3.2 判断是否包含有某一种字符

假设,要求的必须包含数字,怎么办?此时我们可以使用(?=.*[0-9])来做。

因此正则变成:

var reg = /(?=.*[0-9])^[0-9A-Za-z]{6,12}$/;
复制代码

4.3.3 同时包含具体两种字符

比如同时包含数字和小写字母,可以用(?=.*[0-9])(?=.*[a-z])来做。

因此正则变成:

var reg = /(?=.*[0-9])(?=.*[a-z])^[0-9A-Za-z]{6,12}$/;
复制代码

4.3.4 解答

我们可以把原题变成下列几种情况之一:

  1. 同时包含数字和小写字母
  2. 同时包含数字和大写字母
  3. 同时包含小写字母和大写字母
  4. 同时包含数字、小写字母和大写字母

以上的4种情况是或的关系(实际上,可以不用第4条)。

最终答案是:

var reg = /((?=.*[0-9])(?=.*[a-z])|(?=.*[0-9])(?=.*[A-Z])|(?=.*[a-z])(?=.*[A-Z]))^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有
复制代码

4.3.5 解惑

上面的正则看起来比较复杂,只要理解了第二步,其余就全部理解了。

/(?=.*[0-9])^[0-9A-Za-z]{6,12}$/

对于这个正则,我们只需要弄明白(?=.*[0-9])^即可。

分开来看就是(?=.*[0-9])和^。

表示开头前面还有个位置(当然也是开头,即同一个位置,想想之前的空字符类比)。

(?=.*[0-9])表示该位置后面的字符匹配.*[0-9],即,有任何多个任意字符,后面再跟个数字。

翻译成大白话,就是接下来的字符,必须包含个数字。

4.3.6 另外一种解法

“至少包含两种字符”的意思就是说,不能全部都是数字,也不能全部都是小写字母,也不能全部都是大写字母。

那么要求“不能全部都是数字”,怎么做呢?(?!p)出马!

对应的正则是:

var reg = /(?!^[0-9]{6,12}$)^[0-9A-Za-z]{6,12}$/;
复制代码

三种“都不能”呢?

最终答案是:

var reg = /(?!^[0-9]{6,12}$)(?!^[a-z]{6,12}$)(?!^[A-Z]{6,12}$)^[0-9A-Za-z]{6,12}$/;
console.log( reg.test("1234567") ); // false 全是数字
console.log( reg.test("abcdef") ); // false 全是小写字母
console.log( reg.test("ABCDEFGH") ); // false 全是大写字母
console.log( reg.test("ab23C") ); // false 不足6位
console.log( reg.test("ABCDEF234") ); // true 大写字母和数字
console.log( reg.test("abcdEF234") ); // true 三者都有
复制代码

第二章小结

位置匹配相关的案例,挺多的,不一而足。

掌握匹配位置的这6个锚字符,给我们解决正则问题一个新工具。


第三章 正则表达式括号的作用

不管哪门语言中都有括号。正则表达式也是一门语言,而括号的存在使这门语言更为强大。

对括号的使用是否得心应手,是衡量对正则的掌握水平的一个侧面标准。

括号的作用,其实三言两语就能说明白,括号提供了分组,便于我们引用它。

引用某个分组,会有两种情形:在JavaScript里引用它,在正则表达式里引用它。

本章内容虽相对简单,但我也要写长点。

内容包括:

  1. 分组和分支结构
  2. 捕获分组
  3. 反向引用
  4. 非捕获分组
  5. 相关案例

1. 分组和分支结构

这二者是括号最直觉的作用,也是最原始的功能。

1.1 分组

我们知道/a+/匹配连续出现的“a”,而要匹配连续出现的“ab”时,需要使用/(ab)+/。

其中括号是提供分组功能,使量词+作用于“ab”这个整体,测试如下:

var regex = /(ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); 
// => ["abab", "ab", "ababab"]
复制代码

1.2 分支结构

而在多选分支结构(p1|p2)中,此处括号的作用也是不言而喻的,提供了子表达式的所有可能。

比如,要匹配如下的字符串:

I love JavaScript

I love Regular Expression

可以使用正则:

var regex = /^I love (JavaScript|Regular Expression)$/;
console.log( regex.test("I love JavaScript") );
console.log( regex.test("I love Regular Expression") );
// => true
// => true复制代码

如果去掉正则中的括号,即/^I love JavaScript|Regular Expression$/,匹配字符串是"I love JavaScript"和"Regular Expression",当然这不是我们想要的。

2. 引用分组

这是括号一个重要的作用,有了它,我们就可以进行数据提取,以及更强大的替换操作。

而要使用它带来的好处,必须配合使用实现环境的API。

以日期为例。假设格式是yyyy-mm-dd的,我们可以先写一个简单的正则:

var regex = /\d{4}-\d{2}-\d{2}/;
复制代码

然后再修改成括号版的:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
复制代码

为什么要使用这个正则呢?

2.1 提取数据

比如提取出年、月、日,可以这么做:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( string.match(regex) ); 
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
复制代码

match返回的一个数组,第一个元素是整体匹配结果,然后是各个分组(括号里)匹配的内容,然后是匹配下标,最后是输入的文本。(注意:如果正则是否有修饰符g,match返回的数组格式是不一样的)。

另外也可以使用正则对象的exec方法:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
console.log( regex.exec(string) ); 
// => ["2017-06-12", "2017", "06", "12", index: 0, input: "2017-06-12"]
复制代码

同时,也可以使用构造函数的全局属性至来获取:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
regex.test(string); // 正则操作即可,例如
//regex.exec(string);
//string.match(regex);
console.log(RegExp.$1); // "2017"
console.log(RegExp.$2); // "06"
console.log(RegExp.$3); // "12"
复制代码

2.2 替换

比如,想把yyyy-mm-dd格式,替换成mm/dd/yyyy怎么做?

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, "$2/$3/$1");
console.log(result); 
// => "06/12/2017"
复制代码

其中replace中的,第二个参数里用、、指代相应的分组。等价于如下的形式:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function() {
    return RegExp.$2 + "/" + RegExp.$3 + "/" + RegExp.$1;
});
console.log(result); 
// => "06/12/2017"
复制代码

也等价于:

var regex = /(\d{4})-(\d{2})-(\d{2})/;
var string = "2017-06-12";
var result = string.replace(regex, function(match, year, month, day) {
    return month + "/" + day + "/" + year;
});
console.log(result); 
// => "06/12/2017"
复制代码

3. 反向引用

除了使用相应API来引用分组,也可以在正则本身里引用分组。但只能引用之前出现的分组,即反向引用。

还是以日期为例。

比如要写一个正则支持匹配如下三种格式:

2016-06-12

2016/06/12

2016.06.12

最先可能想到的正则是:

var regex = /\d{4}(-|\/|\.)\d{2}(-|\/|\.)\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // true
复制代码

其中/和.需要转义。虽然匹配了要求的情况,但也匹配"2016-06/12"这样的数据。

假设我们想要求分割符前后一致怎么办?此时需要使用反向引用:

var regex = /\d{4}(-|\/|\.)\d{2}\1\d{2}/;
var string1 = "2017-06-12";
var string2 = "2017/06/12";
var string3 = "2017.06.12";
var string4 = "2016-06/12";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // true
console.log( regex.test(string4) ); // false
复制代码

注意里面的,表示的引用之前的那个分组(-|\/|\.)。不管它匹配到什么(比如-),都匹配那个同样的具体某个字符。

我们知道了的含义后,那么和的概念也就理解了,即分别指代第二个和第三个分组。

看到这里,此时,恐怕你会有三个问题。

3.1 括号嵌套怎么办?

以左括号(开括号)为准。比如:

var regex = /^((\d)(\d(\d)))\1\2\3\4$/;
var string = "1231231233";
console.log( regex.test(string) ); // true
console.log( RegExp.$1 ); // 123
console.log( RegExp.$2 ); // 1
console.log( RegExp.$3 ); // 23
console.log( RegExp.$4 ); // 3
复制代码

我们可以看看这个正则匹配模式:

  • 第一个字符是数字,比如说1,
  • 第二个字符是数字,比如说2,
  • 第三个字符是数字,比如说3,
  • 接下来的是,是第一个分组内容,那么看第一个开括号对应的分组是什么,是123,
  • 接下来的是,找到第2个开括号,对应的分组,匹配的内容是1,
  • 接下来的是,找到第3个开括号,对应的分组,匹配的内容是23,
  • 最后的是,找到第3个开括号,对应的分组,匹配的内容是3。

这个问题,估计仔细看一下,就该明白了。

3.2 表示什么呢?

另外一个疑问可能是,即是表示第10个分组,还是和0呢?

答案是前者,虽然一个正则里出现比较罕见。测试如下:

var regex = /(1)(2)(3)(4)(5)(6)(7)(8)(9)(#) \10+/;
var string = "123456789# ######"
console.log( regex.test(string) );
// => true复制代码

3.3 引用不存在的分组会怎样?

因为反向引用,是引用前面的分组,但我们在正则里引用了不存在的分组时,此时正则不会报错,只是匹配反向引用的字符本身。例如,就匹配""。注意""表示对"2"进行了转意。

var regex = /\1\2\3\4\5\6\7\8\9/;
console.log( regex.test("\1\2\3\4\5\6\7\8\9") ); 
console.log( "\1\2\3\4\5\6\7\8\9".split("") );
复制代码

chrome浏览器打印的结果:

4. 非捕获分组

之前文中出现的分组,都会捕获它们匹配到的数据,以便后续引用,因此也称他们是捕获型分组。

如果只想要括号最原始的功能,但不会引用它,即,既不在API里引用,也不在正则里反向引用。此时可以使用非捕获分组(?:p),例如本文第一个例子可以修改为:

var regex = /(?:ab)+/g;
var string = "ababa abbb ababab";
console.log( string.match(regex) ); 
// => ["abab", "ab", "ababab"]
复制代码

5. 相关案例

至此括号的作用已经讲完了,总结一句话,就是提供了可供我们使用的分组,如何用就看我们的了。

5.1 字符串trim方法模拟

trim方法是去掉字符串的开头和结尾的空白符。有两种思路去做。

第一种,匹配到开头和结尾的空白符,然后替换成空字符。如:

function trim(str) {
    return str.replace(/^\s+|\s+$/g, '');
}
console.log( trim("  foobar   ") ); 
// => "foobar"
复制代码

第二种,匹配整个字符串,然后用引用来提取出相应的数据:

function trim(str) {
    return str.replace(/^\s*(.*?)\s*$/g, "$1");
}
console.log( trim("  foobar   ") ); 
// => "foobar"
复制代码

这里使用了惰性匹配*?,不然也会匹配最后一个空格之前的所有空格的。

当然,前者效率高。

5.2 将每个单词的首字母转换为大写

function titleize(str) {
    return str.toLowerCase().replace(/(?:^|\s)\w/g, function(c) {
        return c.toUpperCase();
    });
}
console.log( titleize('my name is epeli') ); 
// => "My Name Is Epeli"
复制代码

思路是找到每个单词的首字母,当然这里不使用非捕获匹配也是可以的。

5.3 驼峰化

function camelize(str) {
    return str.replace(/[-_\s]+(.)?/g, function(match, c) {
        return c ? c.toUpperCase() : '';
    });
}
console.log( camelize('-moz-transform') ); 
// => "MozTransform"
复制代码

其中分组(.)表示首字母。单词的界定是,前面的字符可以是多个连字符、下划线以及空白符。正则后面的?的目的,是为了应对str尾部的字符可能不是单词字符,比如str是'-moz-transform '。

5.4 中划线化

function dasherize(str) {
    return str.replace(/([A-Z])/g, '-$1').replace(/[-_\s]+/g, '-').toLowerCase();
}
console.log( dasherize('MozTransform') ); 
// => "-moz-transform"
复制代码

驼峰化的逆过程。

5.5 html转义和反转义

// 将HTML特殊字符转换成等值的实体
function escapeHTML(str) {
    var escapeChars = {
      '¢' : 'cent',
      '£' : 'pound',
      '¥' : 'yen',
      '€': 'euro',
      '©' :'copy',
      '®' : 'reg',
      '<' : 'lt',
      '>' : 'gt',
      '"' : 'quot',
      '&' : 'amp',
      '\'' : '#39'
    };
    return str.replace(new RegExp('[' + Object.keys(escapeChars).join('') +']', 'g'), function(match) {
        return '&' + escapeChars[match] + ';';
    });
}
console.log( escapeHTML('<div>Blah blah blah</div>') );
// => "<div>Blah blah blah</div>";
复制代码

其中使用了用构造函数生成的正则,然后替换相应的格式就行了,这个跟本章没多大关系。

倒是它的逆过程,使用了括号,以便提供引用,也很简单,如下:

// 实体字符转换为等值的HTML。
function unescapeHTML(str) {
    var htmlEntities = {
      nbsp: ' ',
      cent: '¢',
      pound: '£',
      yen: '¥',
      euro: '€',
      copy: '©',
      reg: '®',
      lt: '<',
      gt: '>',
      quot: '"',
      amp: '&',
      apos: '\''
    };
    return str.replace(/\&([^;]+);/g, function(match, key) {
        if (key in htmlEntities) {
            return htmlEntities[key];
        }
        return match;
    });
}
console.log( unescapeHTML('<div>Blah blah blah</div>') );
// => "<div>Blah blah blah</div>"
复制代码

通过key获取相应的分组引用,然后作为对象的键。

5.6 匹配成对标签

要求匹配:

<title>regular expression</title>

<p>laoyao bye bye</p>

不匹配:

<title>wrong!</p>

匹配一个开标签,可以使用正则<[^>]+>,

匹配一个闭标签,可以使用<\/[^>]+>,

但是要求匹配成对标签,那就需要使用反向引用,如:

var regex = /<([^>]+)>[\d\D]*<\/\1>/;
var string1 = "<title>regular expression</title>";
var string2 = "<p>laoyao bye bye</p>";
var string3 = "<title>wrong!</p>";
console.log( regex.test(string1) ); // true
console.log( regex.test(string2) ); // true
console.log( regex.test(string3) ); // false
复制代码

其中开标签<[^>]+>改成<([^>]+)>,使用括号的目的是为了后面使用反向引用,而提供分组。闭标签使用了反向引用,<\/>。

另外[\d\D]的意思是,这个字符是数字或者不是数字,因此,也就是匹配任意字符的意思。

第三章小结

正则中使用括号的例子那可是太多了,不一而足。

重点理解括号可以提供分组,我们可以提取数据,应该就可以了。

例子中的代码,基本没做多少分析,相信你都能看懂的。

本篇未完结,请见下一篇

内容是《Web前端开发之Javascript视频》的课件,请配合大师哥《Javascript》视频课程学习。

JavaScript也可以针对CSS进行编程,也就是所谓的脚本化CSS;通过脚本化CSS,同样可以达到一系列的视觉效果;

在HTML中定义样式的方式有3种:通过<link>元素包含外部样式表文件、使用<style>元素定义嵌入式样式、以及使用style特性定义特定元素的内联样式;

DOM2级样式模块围绕这3种应用样式的机制提供了一套API,可以检测浏览器支持DOM2级定义的CSS能力;

var supportsDOM2CSS = document.implementation.hasFeature("CSS","2.0");
var supportsDOM2CSS2 = document.implementation.hasFeature("CSS2","2.0");

脚本化内联样式:

脚本化CSS最简单直接的方式就是更改文档元素的style属性;任何支持style特性的HTML元素在Javascript中都有一个对应的style属性,这个style属性比较特殊,它不是字符串,而是一个CSSStyleDeclaration对象,包含着通过HTML的style特性指定的所有样式信息,但不包含从外部样式表或嵌入样式表经层叠而来的样式;

var mydiv = document.getElementById("mydiv");
console.log(mydiv.style);  // CSSStyleDeclaration
mydiv.style.fontSize = "24px";
mydiv.style.color = "purple";

style属性作为CSSStyleDeclaration对象,其就代表了内联样式,其保存了所有的样式属性,但如果元素没有设置style特性或没有使用脚本设置该特性,style中的样式属性值均为空字符串;

只要取得一个有效的DOM元素的引用,就可以随时使用Javascript为其设置样式;

var myDiv = document.getElementById("myDiv");
myDiv.style.backgroundColor = "red";
myDiv.style.width = "100px";
myDiv.style.height = "200px";
myDiv.style.border = "1px solid black";

以这种方式改变样式时,元素的外观会自动被更新;

通过style对象,同样可以取得style特性中指定的样式;

var mydiv = document.getElementById("mydiv");
console.log(mydiv.style.backgroundColor);
console.log(mydiv.style.width);
console.log(mydiv.style.height);

脚本化的CSS属性的名称和值:

在style特性中指定的任何CSS属性都将表现为这个style对象的相应属性;对于使用短划线的CSS属性名,必须将其转换成驼峰大小写形式,才能通过js来访问,如:

CSS属性 JS属性
background-imagestyle.backgroundImage
color style.color
display style.display
font-family style.fontFamily

多数情况下,都可以通过简单地转换属性名的格式来实现转换,但是,当一个CSS属性名在Javascript中如果是保留字或关键字时,在其前加”css”前缀,例如float属性,由于float是JS中的保留字,因此不能被用作属性名,其值应为cssFloat;低版本IE支持styleFloat;

mydiv.style.backgroundColor = "purple";
mydiv.style.borderRightStyle = "solid";
mydiv.style.borderRightColor = "green";
mydiv.style.cssFloat = "left";
mydiv.style.styleFloat = "left";

通过CSSStyleDeclaration对象设置style属性值时,值都应该是字符串,都应该放到引号内,并且不带分号;

在标准模式下,所有度量值都必须指定一个度量单位;在混合模式下,可以省略单位;

mydiv.style.width = 300;  // 错误,但在混杂模式下默认为300px
mydiv.style.width = "300";  // 错误,但在混杂模式下默认为300px

通过计算得来的值,也要在最后加上单位;

var x = 10, leftMargin=20, leftBorder=10;
mydiv.style.left = (x + leftMargin + leftBorder) + "px";

带单位唯一不好的地方就是,如果要获取这个值,参与其他的数学运算,因为其是字符串,所以不能直接使用,必须转换为数值才能参与运算,例如使用parseInt()或parseFloat()等方法进行转换;

var rect = mydiv.getBoundingClientRect();
console.log(rect);
var borderWidth = parseFloat(mydiv.style.width);
console.log(borderWidth);

一些CSS属性是复合属性,比如:margin就是margin-top、margin-right、margin-bottom、margin-left的复合属性;CSSStyleDeclaration对象也有与之对应的复合属性;

mydiv.style.margin = "20px 30px 40px 50px";
mydiv.style.marginTop = "50px";
mydiv.style.marginLeft = "50px";

当设置复合属性margin时,其会自动计算其所对应的相关联的属性;

即然style是元素的特性,那么也可以通过setAttribute()、getAttribute()等方法来设置style特性;

mydiv.setAttribute("style","width: 300px; height:200px; background-color: purple;");
console.log(mydiv.style);
console.log(mydiv.getAttribute("style"));

style对象的属性和方法:

style对象中还定义一些属性和方法,这些属性和方法在提供元素的style特性值的同时,也可以修改样式;

cssText:能够访问到style特性中的CSS代码;

length:应用给元素的CSS属性的数量;

parentRule:表示CSS信息的CSSRule对象;

getPropertyCSSValue(propertyName):返回包含给定的属性的值CSSValue对象;已废弃;

getPropertyPriority(propertyName);如果给定的属性使用了!important设置,则返回important;否则返回空字符串;

getPropertyValue(propertyName):返回给定的属性的值;

item(index):返回给定位置的CSS属性的名;

removeProperty(propertyName):从样式中删除给定的属性;

setProperty(propertyName,value,priority):为给定的属性设置为属性值,并加上优先权标志(”important”或一个空字符串);

cssText属性:返回浏览器对style特性中CSS代码的内部表示,在写入模式下,赋给cssText的值会重写整个style特性的值;

mydiv.style.cssText = "width: 300px; height:200px; background-color: purple;";
console.log(mydiv.style.cssText);
console.log(mydiv.getAttribute("style"));

设置cssText属性的目的就是为元素快速应用多个CSS特性,可以一次性应用所有的变化;

item()方法和length属性:设计length属性的目的,就是将其与item()方法配套使用,以便迭代元素中定义的CSS属性名;在使用length和item()时,style对象实际上是一个集合,可以使用方括号来代替item()来取得给定位置的css属性名;

console.log(mydiv.style.length);
for(var i=0; i<mydiv.style.length; i++){
    // console.log(mydiv.style.item(i));
    console.log(mydiv.style[i]);
}

getPropertyValue(propertyName)方法:取得给定的属性的值;

console.log(mydiv.style.getPropertyValue("background-color"));
for(var i=0; i<mydiv.style.length; i++){
    var prop = mydiv.style[i];  // 取得属性名
    var value = mydiv.style.getPropertyValue(prop);  // 取得属性值
    console.log(prop + ":" + value);
}

getPropertyCSSValue(propertyName)方法:该方法会返回一个包含两个属性的CSSValue对象,这两个属性分别是:cssText和cssValueType,其中cssText属性的值与getPropertyValue()方法返回的值相同,而cssValueType属性则是一个数值常量,表示值的类型:0表示继承的值,1表示基本的值,2表示值列表,3表示自定义的值;

var value = mydiv.style.getPropertyCSSValue("background-color");
console.log(value);
console.log(value.cssText);
console.log(value.cssValueType);

这个方法已经被废弃了,几乎所有的浏览器都不支持;

getPropertyPriority(propertyName);如果给定的属性使用了!important设置,则返回important;否则返回空字符串;

console.log(mydiv.style.getPropertyPriority("background-color")); // important

setProperty(propertyName,value,priority):为给定的属性设置为属性值,并加上优先权标志,priority 为可选(”important”或一个空字符串);

mydiv.style.setProperty("background-color","purple","important");

removeProperty(propertyName)方法:可以从元素的样式中移除某个CSS属性,移除一个属性,意味着该属性将应用默认的样式(从其他样式表经层叠而来);

mydiv.style.removeProperty("background-color");

parentRule:只读,表示CSS信息的CSSRule对象;

var rule = mydiv.style.parentRule;
console.log(rule);  // null

计算的样式:

虽然style对象能够提供style特性所有样式信息,但它不包含那些从其他样式表层叠而来并影响到当前元素的实际样式信息;所以,需要使用计算样式;

计算样式是一组属性值,它由浏览器通过把内联样式结合所有链接样式表中所有可应用的样式规则后导出(计算)得到的,也就是一组在呈现元素时实际使用的属性;

getComputedStyle()方法:

该方法是window对象的方法,可以通过window.getComputedStyle(),或者document.defaultView.getComputedStyle()方法调用,而document.defaultView的返回值就是window;

该方法接受两个参数:要取得计算样式的元素和一个伪元素字符串(如::after);如果不需要伪元素,第二个参数可以为null或一个空字符串;该方法返回一个CSSStyleDeclaration对象,与style属性的类型相同,区别在于它是只读的,其中包含当前元素的所有计算的样式;

<style>
    #mydiv{background-color: purple; width: 100px; height: 100px;}
</style>
<div id="mydiv" style="background: pink; border: 1px solid black;">mydiv</div>
<script>
var mydiv = document.getElementById("mydiv");
var computedStyle = document.defaultView.getComputedStyle(mydiv,null);
console.log(computedStyle);
console.log(computedStyle.backgroundColor);
console.log(computedStyle.width);
console.log(computedStyle.height);
console.log(computedStyle.border);
</script>

注:以上border属性可能不会返回实际的border规则(如IE和Firefox返回空字符串),原因是不同浏览中解释复合属性的方式不同,因为设置这种属性实际上会涉及很多其他属性,例如:border,实际上调协了四个边的边框宽度、颜色等,因此border不会返回,但computedStyle.borderleftWidth会返回值;

console.log(computedStyle.borderLeftWidth);
console.log(computedStyle.borderLeftColor);

另外,不同浏览器表示值的方式可能会有所区别;

计算后的样式也包含属于浏览器内部样式表的样式信息,因此任何具有默认值的CSS属性都会表现在计算后的样式中;如visibility属性都有一个默认值,有些浏览器设置为visible,而有些设置为inherit;

计算样式的CSSStyleDeclaration对象和表示内联样式的对象之间有一些重要的区别:

计算样式的属性是只读的;

计算样式的值是绝对值,类似百分比和点之类的相对的单位将全部转换为绝对值;所有指定尺寸,例如外边距大小和字体大小等属性都有一个以像素为度量单位的值;相关颜色的属性将以”rgb(#,#,#)”或”rgba(#,#,#,#)”的格式返回;

不计算复合属性,它们只基于最基础的属性,例如,不要查询margin属性,应该使用marginLeft或marginTop等;

计算样式的cssText属性未定义(也就是该属性返回空字符串);

计算样式和内联样式可以同时使用;

// 用指定的因子缩放元素e的文本尺寸
function scale(e, factor){
    // 用计算样式查询当前文本的尺寸
    var size = parseInt(window.getComputedStyle(e,"").fontSize);
    // 用内联样式来放大尺寸
    e.style.fontSize = factor * size + "px";
}
// 用指定的因子修改元素的背景颜色
// factors > 1 颜色变浅,factors < 1颜色变暗
function scaleColor(e, factor){
    var color = window.getComputedStyle(e,"").backgroundColor;
    var components = color.match(/[\d\.]+/g); // 解析r、g、b分量
    for(var i=0; i<3; i++){  // 循环r,g,b
        var x = Number(components[i]) * factor;  // 缩放每个值
        x = Math.round(Math.min(Math.max(x, 0), 255)); // 设置边界并取整
        components[i] = String(x);
    }
    if(components.length == 3)  // rgb()颜色
        e.style.backgroundColor = "rgb(" + components.join() + ")";
    else  // rgba()颜色
        e.style.backgroundColor = "rgba(" + components.join() + ")";
}
var mydiv = document.getElementById("mydiv");
scale(mydiv, 1.5);
scaleColor(mydiv, .5);

低版本的IE不支持getComputedStyle()方法,但它有一种类似的概念;在IE中,具有style属性的元素还有一个currentStyle属性,该属性是CSSStyleDeclaration的实例,包含当前元素全部计算后的样式,但只有IE支持;

var computedStyle = mydiv.currentStyle;
console.log(computedStyle.backgroundColor);
console.log(computedStyle.width);
console.log(computedStyle.height);
console.log(computedStyle.borderLeftWidth);

兼容函数:

function getStyle(obj, attr){
    if(window.getComputedStyle)
        return getComputedStyle(obj, null)[attr];
    else
        return obj.currentStyle[attr];
}
var mydiv = document.getElementById("mydiv");
var backgroundColor = getStyle(mydiv, "background-color");
console.log(backgroundColor);  // rgb(245, 222, 179)
// 或者
function getCss(obj, css){
    return (document.defaultView.getComputedStyle ? 
        document.defaultView.getComputedStyle(obj,null) : 
        obj.currentStyle)[css];
}
var borderTopWidth = getCss(mydiv, "border-top-width");
console.log(borderTopWidth); // 1px

封装一下函数,用来获取CSS属性值,如:

function getComputedStyles(elem,prop) {
  var cs = window.getComputedStyle(elem,null);
  if (prop) {
        console.log(prop+" : "+cs.getPropertyValue(prop));
        return;
  }
  var len = cs.length;
  for (var i=0;i<len;i++) {
    var style = cs[i];
    console.log(style+" : "+cs.getPropertyValue(style));
  }
}
getComputedStyles(mydiv);  // 查询所有
getComputedStyles(mydiv,"background-color");  // 只查询一个属性

与伪元素一起使用:getComputedStyle可以从伪元素中提取样式信息(例如:::after, ::before, ::marker, ::line-marker);

<style>
    #mydiv::after{content: "大师哥王唯";}
</style>
<div id="mydiv"></div>
<script>
var mydiv = document.getElementById("mydiv");
var computedStyle = document.defaultView.getComputedStyle(mydiv,":after");
console.log(computedStyle.content);
<script>

使用计算样式是可以获取元素的几何尺寸和位置的,但是其获得的结果并不一定是我们所期望的,此时可以使用getBoundingClientRect(),其返回的值是与呈现的效果是一致的;

console.log(computedStyle.left);  // auto
console.log(computedStyle.top);  // auto
console.log(computedStyle.width);  // 300px
// left:8 top:8 width:302,包括了border
var rect = mydiv.getBoundingClientRect();
console.log(rect);

脚本化CSS类:

也可以脚本化元素的class属性,改变元素的class就改变了应用于元素的一组样式表选择器,它能在同一时刻改变多个CSS属性;

className属性:

与元素的class特性对应,即为元素指定的CSS类;由于class是ECMAScript保留字,所以在Javascript中使用className;

在操作类名时,需要通过className属性添加、删除和替换类名;

var mydiv = document.getElementById("mydiv");
mydiv.className = "container"; // 设置
mydiv.className = ""; // 删除
mydiv.className = "other"; // 替换
// 或
if(mydiv.className == ""){
  mydiv.className = "container";
}

元素可以设置多个CSS类,其className中保存的是拥有多个类名的字符串,因此即使只修改字符串一部分,都会覆盖之前的值,所以每次都必须设置整个字符串的值;

var mydiv = document.getElementById("mydiv");
console.log(mydiv.className);  // db user disable
mydiv.className = "container";
console.log(mydiv.className); // container

如果要从class中只删除一个类名,需要把多个类名拆开,删除不想要的那个,然后再把其他类名拼成一个新字符串,如:

// 如,删除user类
// 首先,取得类名字符串并拆分成数组
var mydiv = document.getElementById("mydiv");
var classNames = mydiv.className.split(/\s+/);
// 找到要删的类名
var pos = -1, i, len;
for(i=0, len = classNames.length; i<len; i++){
    if(classNames[i] == "user"){
        pos = i;
        break;
    }
}
// 删除类名
classNames.splice(i,1);
// 把剩下的类名拼成字符串并重新设置
mydiv.className = classNames.join(" ");

如果要添加类名,是可以直接通过拼接字符串完成,但在拼接之前,必须要通过检测确定不会多次添加相同的类名;

Element.classList属性:

HTML5新增了一种操作类名的方式,可以让操作更简单也更安全,即classList属性,其是一个DOMTokenList对象,其是只读的类数组对象,与其他DOM集合类似,它也有一个表示自己包含多少个元素的length属性,而要取得每个元素可以使用item()方法,也可以使用方括号语法,此外,这个新类型还定义如下的方法:

  • add(value):将给定的字符串值添加到列表中,如果值存在,就不添加;
  • contains(value):表示列表中是否存在给定的值,如果存在返回true,否则,返回false;
  • remove(value):从列表中删除给定的字符串;
  • toggle(value):如果列表中已经存在给定的值,删除它,否则,添加它;
console.log(mydiv.classList);  // DOMTokenList
mydiv.classList.add("container");
mydiv.classList.remove("container");

使用classList,可以确保其他类名不受此次修改的影响,可以极大地减少类似的基本操作的复杂性;

mydiv.classList.toggle("user");  // 切换user类
// 确定元素中是否包含既定的类名
if(mydiv.classList.contains("bd") && !mydiv.classList.contains("disabled")){
    // To Do 
}
// 迭代类名
for(var i=0,len=mydiv.classList.length; i<len; i++){
    // To Do
    console.log(mydiv.classList[i]);
}

有了classList属性,除非需要全部删除所有类名,或者完全重写元素的class属性,否则也就用不到className了;

需要确定的一点,classList这个DOMTokenList对象,它是实时的,如果className属性改变了,它也会及时更新,同样的,classList改变了,className属性也会及时更新;

IE9以下的浏览器不支持classList属性,可以自定义一个CSSClassList类,模拟DOMTokenList对象的方法;

function classList(e){
    // 以下两行先注释,否则后面的toArray默认调用的是DOMTokenList对象的的toArray,而它并不存在
    // 或者扩展内置的DOMTokenList的toArray
    // if(e.classList) return e.classList;  // 如果e.classList存在,则返回它
    // else return new CSSClassList(e);  // 否则,说伪造一个
    return new CSSClassList(e);
}
// CSSClassList是一个模拟DOMTokenList的对象
function CSSClassList(e) { this.e = e;}
// 如果e.className包含类名c则返回true,否则返回false
CSSClassList.prototype.contains = function(c){
    // 检查c是否是合法的类名
    if(c.length === 0 || c.indexOf(" ") != -1)
        throw new Error("Invalid class name: '" + c + "'");
    // 首先是常规检查
    var classes = this.e.className;
    if(!classes) return false;  // e不含类名
    if(classes === c) return true; // e有一个完全匹配的类名
    // 否则,把c自身看做一个单词,利用正则表达式搜索c
    return classes.search("\\b" + c + "\\b") != -1;
};
// 如果c不存在,将c添加到e.className中
CSSClassList.prototype.add = function(c){
    if(this.contains(c)) return;  // 如果存在,什么也不做
    var classes = this.e.className;
    if(classes && classes[classes.length - 1] != " ")
        c = " " + c;  // 如果需要加一个空格
    this.e.className += c; // 将c添加到className中
};
// 将在e.className中出现的所有c都删除
CSSClassList.prototype.remove = function(c){
    if(c.length === 0 || c.indexOf(" ") != -1)
        throw new Error("Invalid class name: '" + c + "'");
    // 将所有作为单词的c和多余的尾随空格全部删除
    var pattern = new RegExp("\\b" + c + "\\b\\s*", "g");
    this.e.className = this.e.className.replace(pattern, "");
};
// 如果c不存在,将c添加到e.className中,并返回true
// 否则,将e.className中出现的所有c都删除,并返回false
CSSClassList.prototype.toggle = function(c){
    if(this.contains(c)){ // 如果e.className包含类名c
        thsi.remove();  // 删除它
        return false;
    }else{
        this.add(c); // 添加
        return true;
    }
};
// 返回e.className本身
CSSClassList.prototype.toString = function(){
    return this.e.className;
};
// 返回在e.className中的类名
CSSClassList.prototype.toArray = function(){
    return this.e.className.match(/\b\w+\b/g) || [];
};
// 应用
var mydiv = document.getElementById("mydiv");
var ccl = classList(mydiv);
console.log(ccl);
console.log(ccl.contains("newsdiv"));  // true
ccl.add("topDiv");
ccl.remove("newsdiv");
ccl.toggle("newsdiv");
console.log(ccl.toString());
console.log(ccl.toArray());

脚本化样式表:

在脚本化样式表时,会使用到两种类型的对象:

第一类是元素对象,包括通过<link>元素包含的样式表和在<style>元素中定义的样式表,这两个元素本身是常规的文档元素,分别是由HTMLLinkElement和HTMLStyleElement类型表示,它们都可以修改元素的特性,如果它们有id,可以通过getEleementById()来获取它们;

// var mylink = document.getElementById("mylink");
var mylink = document.getElementsByTagName("link")[0];
console.log(mylink);
var mystyle = document.getElementsByTagName("style")[0];
console.log(mystyle);

所有浏览器都会包含<style>元素和rel特性被设置为stylesheet的<link>元素引入的样式表;

第二类是CSSStyleSheet类型,表示样式表本身;通过document.styleSheets属性会返回一个只读的类数组StyleSheetList对象集合,该集合具有length属性和item()方法,集合内保存着CSSStyleSheet对象,表示与文档关联在一起的样式表;

var styleList = document.styleSheets;
console.log(styleList);  // StyleSheetList
console.log(styleList.length);  // 3
console.log(styleList[0]);  // CSSStyleSheet
// 遍历
var sheet = null;
for(var i=0, len=document.styleSheets.length; i<len; i++){
    sheet = document.styleSheets[i];
    console.log(sheet.href);
}

可以直接通过HTMLLinkElement(<link>)或HTMLStyleElement(<style>)元素的sheet属性获取CSSStyleSheet对象;低版本的IE不支持sheet,但提供了一个同样作用的styleSheet属性;

var mylink = document.getElementById("mylink");
var mylink = document.getElementsByTagName("link")[0];
console.log(mylink.sheet);  // CSSStyleSheet
console.log(mylink.styleSheet); // 在低版本的IE中返回CSSStyleSheet
var mystyle = document.getElementsByTagName("style")[0];
console.log(mystyle.sheet);  // CSSStyleSheet
console.log(mystyle.styleSheet);  // 在低版本的IE中返回CSSStyleSheet

兼容处理:

function getStyleSheet(element){
    return element.sheet || element.styleSheet;
}
var link = document.getElementsByTagName("link")[0];
var sheet = getStyleSheet(link);
console.log(sheet.href);

在使用之前,检测浏览器是否支持DOM2级样式表;

var supportsDOM2StyleSheet = document.implementation.hasFeature("StyleSheets","2.0");

CSSStyleSheet对象:

接口代表一个 CSS 样式表,并允许检查和编辑样式表中的规则列表;其继承自StyleSheet接口,后者可以作为一个基础接口来定义非CSS样式表;从接口继承的属性如下:

disabled:表示样式表是否被禁用的布尔值,R/W,将这个值设置为true可以禁用样式表;

var styleList = document.styleSheets;
var ss = styleList[2];
console.log(ss.disabled);
ss.disabled = true;  // 元素的样式失效
// 封装一个小函数,用来开启或关闭样式表
// 如果传递一个数字,将其当做document.styleSheets对象中一个索引
// 如果传递一个字符串,将其当作CSS选择器并传递给document.querySelectorAll()
function disableStyleSheet(ss){
    if(typeof ss === "number")
        document.styleSheets[ss].disabled = true;
    else{
        var sheets = document.querySelectorAll(ss);
        for(var i=0; i<sheets.length; i++)
            sheets[i].disabled = true;
    }
}
disableStyleSheet(0);
disableStyleSheet("style");
disableStyleSheet("link");

href:如果样式表是通过<link>包含的,则是样式表的URL,否则null;

media:当前样式表支持的所有媒体类型的MediaList类数组集合;与所有DOM集合一样,这个集合也有一个length属性和一个item()方法;如果集合为空,表示样式表适用于所有媒体;

<style media="screen and (min-width: 500px),tv and (max-width: 1000px)">
    .container{width:300px;height:200px;background-color: salmon;}
</style>
<script>
var styleList = document.styleSheets;
var ss = styleList[2];
console.log(ss.media);  // MediaList
console.log(ss.media.length); // 2
console.log(ss.media[0]);  // screen and (min-width:500px)
console.log(ss.media.item(1)); // tv and (max-width:1000px)

MediaList对象还拥有mediaText属性,返回元素media特性的值;

console.log(ss.media.mediaText);

MediaList对象拥有appendMedium(medium)和deleteMedium()方法,分别用作添加和删除媒介;

ss.media.appendMedium("print");
ss.media.deleteMedium("tv and (max-width:1000px)");

一般来说,很少去操作media属性;

ownerNode:指向拥有当前样式表的节点的指针,样式表可能是在HTML中通过<link>或<style>引入的,如果样式表是其他样式表是通过@import导入的,该属性值为null,低版本IE不支持该属性;

console.log(styleList[0].ownerNode); // link
console.log(styleList[1].ownerNode); // style

parentStyleSheet:在当前样式表是通过@import导入的情况下,该属性是一个指向导入它的样式表的指针;

// styleList[1]获取的是一个<style>其中使用@import导入一个CSS文件
console.log(styleList[1].cssRules[0]); 
console.log(styleList[1].cssRules[0].parentStyleSheet);  // CSSStyleSheet

title:ownerNode中title属性的值;

type:表示样式表类型的字符串,对CSS样式表而言,是”text/css”;

注:除了disabled属性之外,其他属性都是只读的;

操作样式表规则:

除了以上所有这些属性,CSSStyleSheet类型还定义了用来查询、插入和删除样式表规则的API;

cssRules:返回样式表中包含的所有样式规则的CSSRuleList类型的实时集合,该集合中的元素为CSSStyleRule对象;低版本的IE不支持,但有个类似的rules属性;

var ss = styleList[1];
console.log(ss);
console.log(ss.cssRules);
console.log(ss.rules);  // 是IE专用
console.log(ss.cssRules[0]);  // CSSStyleRule

ownerRule:如果样式表是通过@import导入的,该属性就是一个指针,返回表示导入的规则的CSSImportRule对象,否则为null,低版本的IE不支持;

insertRule(rule, index):创建(插入)规则,向CSSRules集合中指定的位置插入rule字符串,该方法接受两个参数:规则文本和表示在哪里插入规则的索引;

var sheet = document.styleSheets[1];
sheet.insertRule("body{background-color:silver}",0);
console.log(sheet.cssRules);


低版本的IE支持一个类似的addRule(selector, style, index)方法,接受两个必选和一个可选参数:选择器和CSS样式信息,一个可选参数:插入规则的位置;

document.styleSheets[1].addRule("h1","font-size:3em;color:red;",2);
console.log(document.styleSheets[1].cssRules);

这个方法所有浏览器也都支持;

跨浏览器方式:

var sheet = document.styleSheets[0];
function insertRule(sheet, selectorText, cssText, position){
    if(sheet.insertRule){
        sheet.insertRule(selectorText + "{" + cssText + "}", position);
    }else{
        sheet.addRule(selectorText, cssText, position);
    }
}
insertRule(sheet, "body", "background-color:silver", 0);

deleteRule(index):删除cssRules集合(样式表)中指定位置的规则;低版本的IE不支持,但支持一个类似的removeRule()方法,这个方法所有浏览器也都支持;

document.styleSheets[1].deleteRule(0);
document.styleSheets[1].removeRule(0);
console.log(document.styleSheets[1].cssRules);

跨浏览器方式:

var sheet = document.styleSheets[0];
function deleteRule(sheet, index){
    if(sheet.deleteRule){
        sheet.deleteRule(index);
    }else{
        sheet.removeRule(index);
    }
}
deleteRule(sheet,0);

CSSStyleRule规则对象:

CSSStyleRule对象表示样式表中的每一条规则;继承自CSSRule接口,实际上CSSRule是专供其他类型继承的基类,其中最常见的是CSSStyleRule类型,表示样式信息;

var sheet = document.styleSheets[2];
var rules = sheet.cssRules || sheet.rules;
var rule = rules[0];
console.log(rule); // CSSStyleRule

CSSRule接口属性:

  • cssText:返回整条规则对应的文本;低版本的IE不支持
  • parentRule:只读,如果当前规则是导入的规则,这个属性引用的就是导入规则,否则为null;或此规则是 @media 块中的样式规则, 则其父规则将是该 CSSMediaRule;IE不支持
  • parentStyleSheet:当前规则所属的样式表,低版本的IE不支持;
  • type:表示规则类型的常量值,对于样式规则,值为1;IE不支持;

这些常量被定义在CSSRule接口中,值为:

常量值接口

  • CSSRule.STYLE_RULE1CSSStyleRule
  • CSSRule.IMPORT_RULE3CSSImportRule
  • CSSRule.MEDIA_RULE4CSSMediaRule
  • CSSRule.FONT_FACE_RULE5CSSFontFaceRule
  • CSSRule.PAGE_RULE6CSSPageRule
  • CSSRule.KEYFRAMES_RULE7CSSKeyframesRule
  • CSSRule.KEYFRAME_RULE8CSSKeyframeRule
  • 9 保留供将来使用
  • CSSRule.NAMESPACE_RULE10CSSNamespaceRule
  • CSSRule.COUNTER_STYLE_RULE11CSSCounterStyleRule
  • CSSRule.SUPPORTS_RULE12CSSSupportsRule
  • CSSRule.DOCUMENT_RULE13CSSDocumentRule
  • CSSRule.FONT_FEATURE_VALUES_RULE14
  • CSSRule.VIEWPORT_RULE15CSSViewportRule
  • CSSRule.REGION_STYLE_RULE16CSSRegionStyleRule
  • CSSRule.UNKNOWN_RULE0CSSUnknownRule
  • CSSRule.CHARSET_RULE2CSSCharsetRule

CSSStyleRule对象属性:

  • selectorText:只读,返回当前规则的选择器;
  • style:只读,返回一个CSSStyleDeclaration对象,可以通过它设置和取得规则中特定的样式值;
  • styleMap:一个StylePropertyMap对象;
console.log(rule.cssText);  // 定义规则的字符串
console.log(rule.parentRule);
console.log(rule.parentStyleSheet); // CSSStyleSheet
console.log(rule.selectorText);  // 选择器
console.log(rule.style);  // CSSStyleDeclaration
console.log(rule.styleMap);  // StylePropertyMap
console.log(rule.type);  // 1

最常用的是cssText、selectorText和style这三个属性;cssText属性与style.cssText属性类似,但并不相同,前者包含选择器和围绕样式信息的花括号,后者只包含样式信息;cssText是只读的,style.cssText是可写的;

console.log(rule.cssText);  // .container{color:white}
console.log(rule.style.cssText); // color:white
rule.style.cssText = "background-color:purple";
console.log(rule.cssText);  // .container{background-color:purple;}
console.log(rule.style.cssText); // background-color:purple
rule.style.cssText += "color:white;";

大多数情况下,仅使用style属性就可以满足所有操作样式规则的需求了;这个对象就像每个元素上的style属性一样,可以通过它读取和修改规则中的样式信息;

console.log(rule.style.width);
console.log(rule.style.height);
rule.style.backgroundColor = "lightgray";
console.log(rule.style.cssText);

CSSStyleRule对象的style属性,使用方式和内联style对象的使用方式是一致的,但要注意,一个是规则对象的style属性对象,一个是内联style对象;

// 遍历样式表的规则
var ss = document.styleSheets[0];  // 第一个样式表
var rules = ss.cssRules ? ss.cssRules : ss.rules;  // 样式表规则
for(var i=0; i<rules.length; i++){
    var rule = rules[i];
    if(!rule.selectorText) continue;  // 跳过@import和非样式规则
    var selector = rule.selectorText;  // 选择器
    var ruleText = rule.style.cssText; // 文本形式的样式
    // 如果规则应用在h1元素上,也将其应用到h2元素上
    // 注意,仅当选择器在字面上为h1时这才起作用
    if(selector == "h1"){
        if(ss.insertRule)
            ss.insertRule("h2 {" + ruleText + "}", rules.length);
        else if(ss.addRule)
            ss.addRule("h2", ruleText, rules.length);
    }
    // 如果规则设置了text-decoration属性,则将其删除
    if(rule.style.textDecoration){
        if(ss.deleteRule)
            ss.deleteRule(i);
        else if(ss.removeRule)
            ss.removeRule(i);
            i--; // 调整循环索引,因为以上的规则i+1现在即为规则i
    }
}

创建新样式表:

可以创建一个新样式表并将其添加到文档中;使用DOM技术,创建一个<style>元素,并将其插入到文档的头部,然后再用其innerHTML属性来设置样式表内容;在低版本的IE中,CSSStyleSheet对象通过非标准方法document.createStyleSheet()来创建,其样式文本用cssText属性值为指定;

// 创建一个新样式表
// 对文档添加一个样式表,用指定的样式填充它
// style参数可能是字符串或对象,如果它是字符串,就把它作为样式表的文本
// 如果它是对象,将每个定义样式规则的每个属性添加到样式表中
// 属性名即为选择器,其值即为对应的样式
function addStyles(styles){
    var styleElt, styleeSheet;  // 先创建一个新样式表
    if(document.createStyleSheet)  // 如果是IE
        styleSheet = document.createStyleSheet();
    else{
        var head = document.getElementsByTagName("head")[0];
        styleElt = document.createElement("style"); // 新的<style>元素
        head.appendChild(styleElt);
        // 这个新样式表应该是最后一个
        styleSheet = document.styleSheets[document.styleSheets.length - 1];
    }
    // 向其中插入样式
    if(typeof styles === "string"){
        if(styleElt)
            styleElt.innerHTML = styles;
        else
            styleSheet.cssText = styles;  // IE
    }else{  // 参数是规则对象
        var i = 0;
        for(selector in styles){
            if(styleSheet.insertRule){
                var rule = selector + "{" + styles[selector] + "}";
                styleSheet.insertRule(rule, i++);
            }else{
                styleSheet.addRule(selector, styles[selector], i++);
            }
        }
    }
}
// 应用
var styles = "h2 {font-size: 2em; color: red;}";
addStyles(styles);
var rule = document.styleSheets[1].cssRules[0];
addStyles(rule);

CSS动画:脚本化CSS的最常见的用途之一就是产生视觉动画效果;其原理是使用setTimeout()或setInterval()重复调用函数来修改元素的内联样式,以达到产生视觉差的动画效果;