H5 canvas

canvas元素的基础知识

在页面上放置一个canvas元素,就相当于在页面上放置了一块画布,可以在其中进行图形的描绘。
canvas元素只是一块无色透明的区域,需要利用JS编写在其中进行绘画的脚本。

在页画中放置canvas元素

首先,应该要指定的是·idwidthheight三个属性 。

<canvas id="canvas" width="400" height="300" />

绘制矩形

canvas元素绘制图形时,需要经过几道步骤:

  • 取得canvas元素
  • 取得上下文 (context):图形上下文中是一个封装了很多绘图功能的对象。需要使用canvas对象的getContext方法来获得图形上下文。将getContext的参数设为2d
  • 填充与绘制边框:填充(fill)是指填满图形内部,绘制边框(stroke)只绘制图形的外边框。
  • 设定绘图样式 (style),指定颜色值:fillStyle属性用来设置填充的样式和颜色,strokeStyle属性用来设置边框的样式和颜色。
  • 指定线宽:lineWidth属性用来设置图形边框的宽度。
  • 绘制矩形:分别使用fillRect方法与strokeRect方法来填充矩形和绘制矩形边框。

与矩形相关的方法有:fillRect()strokeRect()clearRect()

context.fillRect(x, y, width, height);
context.strokeRect(x, y, width, height);
context.clearRect(x, y, width, height);

这里的context指的是图形上下文对象,这三个方法使用同样的参数,x指矩形起点的横坐标,y指矩形起点的纵坐标,坐标原点为canvas画布的最左上角,width指矩形的宽度,height指矩形的高度。
fillRect()方法在画布上绘制的矩形会填充指定的颜色。
strokeRect()方法在画布上绘制的矩形区域会使用指定的颜色描边。
clearRect()方法会擦除指定的矩形区域中的图形,使得矩形区域中的颜色全部变为透明。

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);
  context.fillStyle = 'red';
  context.strokeStyle = 'blue';
  context.lineWidth = 1;
  context.fillRect(50, 50, 100, 100);
  context.strokeRect(50, 50, 100, 100);
}

运行效果如图:

使用路径

绘制圆形

需要执行如下步骤:

开始创建路径

使用图形上下文对象的beginPath方法。

context.beginPath();

该方法不使用参数,通过该方法开始路径的创建。在几次循环的创建路径的过程中,每次开始创建时都要调用beginPath()方法。

创建图形的路径

创建圆形路径时,需要使用图形上下文对象的arc方法。

context.arc(x, y, radius, startAngle, endAngle, anticlockwise);

该方法使用6个参数,x为绘制圆形的起点横坐标,y为绘制圆形的起点纵坐标,radius为圆形半径,startAngle为开始角度,endAngle为结束角度,anticlockwise为是否按顺时针方向进行绘制。anticlockwise参数为一个布尔值的参数,为true时,按顺时针绘制,为false时,按逆时针绘制。

路径创建完成后,关闭路径

使用图形上下文对象的closePath方法将路径闭合。

context.closePath();

将路径闭合后,路径的创建工作就完成了,但这时只是路径创建完毕,还没有真正绘制图形。

设定绘制样式,调用绘制方法,绘制路径

使用fill()stroke()fillStyle()strokeStyle()指定绘制样式。

// 绘制圆形
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);
  var n = 0;
  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();
  }
}

效果如图:

路径未闭合

如果把上例中的开始创建路径语句和闭合路径语句删除,在画布中先是绘制一个深红色的半径最小的圆,然后每次半径变大的同时,圆的颜色也在逐渐变淡。
在循环时的具体绘制过程:

  • 创建并绘制第一个圆
  • 创建第二个圆,这时因为没有把第一个圆的路径闭合,所以第一个圆的路径也保留着。绘制第二个圆的时候,第一个圆会根据该路径重复绘制,第二个圆只绘制一次,而第一个圆绘制了两次。
  • 创建第三个圆,绘制时,第三个圆只绘制了一次,第二个圆绘制两次,第一个圆绘制了三次。
  • 同上......

所以如果不闭合路径,已经创建的路径会永远保留着。
效果如图:

moveTo与lineTo

绘制直线时,一般会用到moveTolineTo两种方法。
moveTo方法的作用是将光标移动到指定坐标点,绘制直线的时候以这个坐标点为起点。

moveTo(x, y);

lineTo方法在moveTo方法中指定的直线起点与参数中指定的直线终点之间绘制一条直线。

lineTo(x, y);

使用该方法绘制完直线后,光标自动移动到lineTo方法的参数所指定的直线终点。
因此,在创建路径时,需要使用moveTo方法将光标移动到指定的直线起点,然后使用lineTo方法在直线起点与直线终点之间创建路径,然后将光标移动到直线终点,在下一次使用lineTo方法的时候,会以当前光标所在坐标点为直线起点,并在下一个用lineTo方法指定的直线终点之间创建路径,它会不断重复这个过程,来完成复杂图形的路径绘制。

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);  
    var n = 0;  
    var dx = 150;  
    var dy = 150;  
    var s = 100;  
    context.beginPath();  
    context.fillStyle = 'rgb(100,255,100)';  
    context.strokeStyle = 'rgb(0,0,100)';  
    var x = Math.sin(0);  
    var y = Math.cos(0);  
    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);  
        context.lineTo( dx + x * s,dy + y * s);  
    }     
    context.closePath();  
    context.fill();  
    context.stroke();  
}

运行效果如图:

使用bezierCurveTo绘制贝济埃曲线

绘制贝济埃曲线需要使用bezierCurveTo()方法,该方法可以说是lineTo的曲线版。

context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y);

该方法有6个参数。绘制曲线时,需要两个控制点,cp1x为第一个控制点的横坐标,cp1y为第一个控制点的纵坐标;cp2x为第二个控制点的横坐标,cp2y为第二个控制点的纵坐标;x为贝济埃曲线的终点横坐标,y为贝济埃曲线的终点纵坐标。

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);  
    var n = 0;  
    var dx = 150;  
    var dy = 150;  
    var s = 100;  
    context.beginPath();  
    context.globalCompositeOperation ='and';  
    context.fillStyle = 'rgb(100,255,100)';  
    var x = Math.sin(0);  
    var y = Math.cos(0);  
    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();  
}

运行效果如图:

另外,还可以使用quadraticCurveTo方法绘制二次样条曲线。

context.quadraticCurveTo(in float cpx,in float cpy,in float x,in float y)

这四个参数分别是控制点的横坐标,控制点的纵坐标,二次样条曲线终点的横坐标,二次样条曲线终点的纵坐标。

绘制渐变图形

绘制线性渐变

渐变是指在填充时从一种颜色慢慢过渡到另一种颜色。最简单的是两点之间的线性渐变。
绘制线性渐变时,需要使用到LinearGradient对象,可以使用图形上下文对象的createLinearGradient()方法创建该对象。

context.createLinearGradient(xStart, yStart, xEnd, yEnd);

该方法有4个参数,xStart为渐变起始点的横坐标,yStart为渐变起始点的纵坐标,xEnd为渐变结束点的横坐标,yEnd为渐变结束点的纵坐标。
在创建linearGradient对象后,使用addColorStop()方法设定渐变的颜色。

context.addColorStop(offset, color);

offset参数为所设定的颜色离开渐变起始点的偏移量,该参数的值是一个范围在0~1之间的浮点值,渐变起始点的偏移量为0,渐变结束点的偏移量为1。color为绘制时的颜色。
因为是渐变,所以至少需要使用两次addColorStop方法以追加两个颜色(开始颜色和结束颜色),可以追加多个颜色。例如从蓝色渐变到白色然后渐变到绿色。这时蓝色的位移量为0,白色的位移量为0.5,绿色的位移量为1。
接着把fillStylestrokeStyle设定为linearGradient对象,然后执行填充的方法,就可以绘制渐变图形了。

function draw(id) {
  var canvas = document.getElementById(id);
  if(canvas == null) { return false; }
  var context = canvas.getContext('2d');
  var g1 = context.createLinearGradient(0, 0, 0, 300);
  g1.addColorStop(0, 'rgb(255, 255, 0)');
  g1.addColorStop(1, 'rgb(0, 255, 255)');
  context.fillStyle = g1;
  context.fillRect(0, 0, 400, 300);
  var n = 0;
  var g2 = context.createLinearGradient(0, 0, 300, 0);
  g2.addColorStop(0, 'rgba(0, 0, 255, 0.5)');
  g2.addColorStop(1, 'rgba(255, 0, 0, 0.5)');
  for(var i = 0; i < 10; i++) {
    context.beginPath();
    context.fillStyle = g2;
    context.arc(i * 25, i * 25, i * 10, 0, Math.PI * 2, true);
    context.closePath();
    context.fill();
  }
}

效果如图:

绘制径向渐变

径向渐变是指沿着圆形的半径方向向外进行扩散的渐变方式。比如在描绘太阳时,沿着太阳的半径方向向外扩散出去的光晕,就是一种径向渐变。
使用图形上下文对象的createRadialGradient方向绘制径向渐变。

context.createRadialGradient(xStart, yStart, radiusStart, xEnd, yEnd, radiusEnd);

该方法使用六个参数,xStart为渐变开始圆的圆心横坐标,yStart为渐变开始圆的圆心纵坐标,radiusStart为开始圆的半径,xEnd为渐变结束圆的圆心横坐标,yEnd为渐变结束圆的圆心纵坐标,radiusEnd为结束圆的半径。
在这个方法中,分别指定了两个圆的大小与位置。从第一个圆的圆心处向外进行扩散渐变,一直扩散到第二个圆的外轮廓处。
在设定颜色时,与线性渐变相同,使用addColorStop方法进行设定。同样也需要设定0~1之间的浮点数来作为渐变转折点的偏移量。

function draw(id) {
  var canvas = document.getElementById(id);
  if(canvas == null) { return false; }
  var context = canvas.getContext('2d');
  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 n = 0;
  var g2 = context.createRadialGradient(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)');
  for(var i = 0; i < 10; i++) {
    context.beginPath();
    context.fillStyle = g2;
    context.arc(i * 25, i * 25, i * 10, 0, Math.PI * 2, true);
    context.closePath();
    context.fill();
  }
}

效果如图:

绘制变换图形

坐标变换

绘制图形的时候,我们经常想要旋转图形,或对图形使用变形处理,使用Canvas的坐标变换处理功能,可以实现这个效果。
在计算机上绘制图形的时候,是以坐标单位为基准来进行图形绘制的。默认情况下,Canvas画布的最左上角对应于坐标轴原点(0, 0)。
对坐标的变形处理有三种方式。

平移

使用图形上下文对象的translate方法移动坐标轴原点。

context.translate(x, y);

x表示坐标轴原点向左移动多少个单位,默认情况下为像素;y表示将坐标轴原点向下移动多少个单位。

扩大

使用图形上下文对象的scale方法将图形放大。

context.scale(x, y);

x是水平方向放大的倍数,y是垂直方向放大的倍数。

旋转

使用图形上下文对象的rotate方法将图形进行旋转。

context.rotate(angle);

rotate方法接收一个参数angleangle是指旋转的角度,旋转的中心点是坐标轴的原点。旋转是以顺时针方向进行的,要想逆时针旋转,将angle设定为负数就可以了。

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);
  // 图形绘制
  context.translate(200, 50);
  context.fillStyle = 'rgba(255, 0, 0, 0.25)';
  for(var i = 0; i <50; i++) {
    context.translate(200, 50);
    context.scale(0.95, 0.95);
    context.rotate(Math.PI / 10);
    context.fillRect(0, 0, 100, 50);
  }
}

运行效果如图:

坐标变换与路径的结合使用

如果要对矩形进行变形,使用坐标变换就行了。但对使用路径绘制出来的图形进行变换的时候,考虑的事情就多了。因为使用了坐标变换之后,已经创建好的路径就不能用了,必须要重新创建路径。重新创建好路径后,坐标变换方法又失效了。
必须先另外写一个创建路径的函数,然后在坐标变换的同时调用该函数,这样才能解决这个问题。

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);
  // 图形绘制
  context.translate(200, 50);
  for(var i = 0; i < 50; i++) {
    context.translate(25, 25);
    context.scale(0.95, 0.95);
    context.rotate(Math.PI / 10);
    create5Star(context);
    context.fill();
  }
}
function create5Star(context) {
  var n = 0;
  var dx = 100;
  var dy = 0;
  var s = 50;
  // 创建路径
  context.beginPath();
  context.fillStyle = 'rgba(255, 0, 0, 0.5)';
  var x = Math.sin(0);
  var y = Math.cos(0);
  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();
}

上面的代码可以绘制一个将五角星一边旋转一边缩小的图形。在create5Star函数中,创建了一个五角星的路径,然后在draw函数的for循环中,首先依次执行translatescalerotate方法,然后执行create5Star函数创建路径,最后执行fill填充。
create5Star函数中,只创建了一个五角星,因坐标轴变换,在画布中,此五角星一边缩小一边旋转,之后产生一个新的五角星新的五角星又采用同样的方法进行绘制,最终绘制出来一串具有变形效果的五角星的图形。
效果如图:

矩阵变换

变换矩阵是专门用来实现图形变形的,它与坐标一起配合使用,以达到变形的目的。当图形上下文被创建完毕时,事实上也创建了一个默认的变换矩阵,如果不对这个变换矩阵进行修改,那么接下来绘制的图形将以画布的最左上角为坐标原点绘制图形,绘制出来的图形也不经过缩放、变形的处理,但如果对这个变换矩阵进行修改,那么情况就完全不一样了。
使用图形上下文对象的transform方法修改变换矩阵。

context.transform(m11, m22, m21, dx, dy);

该方法使用一个新的变换矩阵与当前变换矩阵进行乘法运算。

m11  m21  dx
m12  m22  dy
 0    0   1

m11m21m12m22四个参数用来修改使用这个方法之后,绘制图形时的计算方法,以达到变形目的,dx和dy参数移动坐标原点,dx表示将坐标原点在x轴上向右移动x个单位,默认情况下以像素为单位,dy表示将坐标原点在y轴上向下移动y个单位。
translatescalerotate这三个方法实际上都是隐式的修改了变换矩阵,都可以使用transform方法来进行代替。
translate(x, y)可以使用context.transform(1, 0, 0, 1, x, y)context.transform(0, 1, 1, 0, x, y)方法进行代替,前面四个参数表示不对图形进行缩放、变形,将dx设为x表示将坐标原点向右移动x个单位,dy设为y表示将坐标原点向下移动y个单位。
scale(x, y)可以使用context.transform(x, 0, 0, y, 0, 0)context.transform(0, y, x, 0, 0, 0)方法代替,前面四个参数表示将图形横向扩大x倍,纵向扩大y倍。dx,dy为0表示不移动坐标原点。
rotate(angle)替换方法如下:

context.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);
// 或
context.transform(-Math.sin(angle * Math.PI / 180),
    Math.cos(angle * Math.PI / 180),
    -Math.cos(angle * Math.PI / 180),
    Math.sin(angle * Math.PI / 180), 0, 0);

其中前面四个参数以三角函数的形式结合起来,共同完成图形按angle角度的顺时针旋转处理,dxdy为0表示不移动坐标原点。

function draw(id) {
  var canvas = document.getElementById(id);
  if(canvas == null) { return false; }
  var context = canvas.getContext('2d');
  // 定义颜色
  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++) {
    // 定义每次向下移动10个像素的变换矩阵
    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();
  }
}

上面的代码用循环的方法绘制了几个圆弧,圆弧的大小与位置均不变,只是使用了transform方法让坐标原点每次向下移动10个像素,使得绘制出来的圆弧相互重叠,然后对圆弧设置七彩颜色,使这些圆弧的外观达到彩虹的效果。
效果如图:

使用了transform方法后,接下来要绘制的图形都会按照移动后的坐标原点与新的变换矩阵相结合的方法进行绘制,必要时可以使用setTransform方法将变换矩阵进行重置。

context.setTransform(m11, m12, m21, m22, dx, dy);

该方法的参数与transform相同,事实上,该方法的作用为将画布上的最左上角重置为坐标原点,当图形上下文创建完毕时将所创建的初始变换矩阵设置为当前变换矩阵,然后使用transform方法。

function draw(id) {
  var canvas = document.getElementById(id);
  if(canvas == null) { return false; }
  var context = canvas.getContext('2d');
  // 绘制红色长方形
  context.strokeStyle = 'red';
  context.strokeRect(30, 10, 60, 20);
  // 绘制顺时针旋转45°后的蓝色长方形
  // 绘制45°圆弧
  var rad = 45 * Math.PI / 180;
  // 定义顺时针旋转45°的变换矩阵
  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倍后的绿色长方形
  // 定义放大2.5倍的变换矩阵
  context.setTransform(2.5, 0, 0, 2.5, 0, 0);
  // 绘制图形
  context.strokeStyle = 'green';
  context.strokeRect(30, 10, 60, 20);
  // 将坐标原点向右移动40像素,向下移动80像素后绘制灰色长方形
  // 定义将坐标原点向右移动40像素,向下移动80像素的矩阵
  context.setTransform(1, 0, 0, 1, 40, 80);
  // 绘制图形
  context.strokeStyle = 'gray';
  context.strokeRect(30, 10, 60, 20);
}

上面的代码先创建了一个红色边框的长方形,然后将该长方形顺时针旋转45度,绘制出一个新的长方形,并绘制其边框为蓝色,然后将红色长方形扩大2.5倍绘制新的长方形,边框为绿色,最后在红色长方形右下方绘制同样大小的长方形,边框为灰色。
效果如图:

图形组合

在H5中,只要用图形上下文对象的globalCompositeOperation属性就能自己决定图形的组合方式了。

context.globalCompositeOperation = type;

type的值必须是下面几种字符串之一:

  • source-over(默认值):表示新图形覆盖在原有图形之上
  • destination-over:表示在原有图形之下绘制新图形
  • source-in:新图形与原有图形作in运算,只显示新图形中与原有图形相重叠的部分,新图形与原有图形的其他部分均变成透明
  • destination-in:原有图形与新图形作in运算,只显示原有图形中与新图形相重叠的部分,新图形与原有图形的其他部分均变成透明
  • source-out:新图形与原有图形作out运算,只显示新图形中与原有图形不重叠的部分,新图形与原有图形的其他部分均变成透明
  • destination-out:原有图形与新图形作out运算,只显示原有图形中与新图形不重叠的部分,新图形与原有图形的其他部分均变成透明
  • source-atop:只绘制新图形中与原有图形重叠的部分与未被重叠覆盖的原有图形,新图形的其他部分变成透明
  • destination-atop:只绘制原有图形中被新图形重叠覆盖的部分与新图形的其他部分 ,原有图形中的其他部分变成透明,不绘制新图形中与原有图形相重叠的部分
  • lighter:原有图形与新图形均绘制,重叠部分做加色处理
  • xor:只绘制新图形中与原有图形不重叠的部分,重叠部分变成透明
  • copy:只绘制新图形,原有图形中未与新图形重叠的部分变成透明

如果指定的type不在这几个字符串当中,则按默认方式组合图形。

function draw(id) {  
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d'); 
  var oprtns = new Array(
    "source-atop",
    "source-in",
    "source-out",
    "source-over",
    "destination-atop",
    "destination-in",
    "destination-out",
    "destination-over",
    "lighter",
    "copy",
    "xor"
    );
    i=8;   
    // 显示想要查看的组合效果
    // 绘制原有图形(蓝色长方形)
    context.fillStyle = "blue";
    context.fillRect(10, 10, 60, 60);
    /* 设置组合方式,从组合的参数数组中挑选组合方式,此处因为i是8,
   所以选择oprtns数组中第9(数组从0开始计算)个组合方式lighter */
    context.globalCompositeOperation = oprtns[i];
    //设置新图形(红色圆形)
    context.beginPath();
    context.fillStyle = "red";
    context.arc(60, 60, 30, 0, Math.PI * 2, false);
    context.fill();
} 

上面的代码将所有的图形组合方式放在一个数组中,然后通过变量i来指定挑选哪种组合方式进行显示。
效果如图:

给图形绘制阴影

使用Canvas元素可以给图形添加阴影效果。
图形上下文对象的关于阴影绘制的属性:

  • shadowOffsetX:阴影的横向位移量,默认为0。
  • shadowOffsetY:阴影的纵向位移量,默认为0。
  • shadowColor:阴影的颜色。
  • shadowBlur:阴影的模糊范围,可选。属性值是比0大的数字,否则将被忽略。
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);  
  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);  
    create5Star(context);  
    context.fill();  
  }  
}  
function create5Star(context) {  
  var n = 0;  
  var dx = 100;  
  var dy = 0;  
  var s = 50;  
  //创建路径  
  context.beginPath();  
  context.fillStyle = 'rgba(255,0,0,0.5)';  
  var x = Math.sin(0);  
  var y = Math.cos(0);  
  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();  
}

效果如图:

上面的代码使用translate方法绘制了几个呈移动状态的五角星。同时给每个五角星都加上了阴影效果。绘制阴影的时候使用了图形上下文对象的绘制阴影属性,这几个属性与路径无关,只要设定一次之后,全部五角星就都具有阴影效果了。
如果不想让全部五角星都具有阴影效果,需要把shadowColor属性设定为rgba(0, 0, 0, 0)

使用图像

绘制图像

绘制图像使用drawlmage()方法。

context.drawlmage(image,x,y);
context.drawlmage(image,x,y,w,h);
cont ext.drawlmage(image,sx,sy,sw,sh,dx,dy,dw,dh);

第一种方法只使用三个参数,image是一个Image对象,用该对象来装载图像文件。 xy为绘制时该图像在画布中的起始坐标。
第二种方法中前三个参数与第一种方法中的使用方法一样,wh是指绘制时的图像的宽度与高度。第一种方法中省略了这两个参数,所以绘制出来的图像与原图大小相同,而第二种方法可以用来进行图像缩放 。
第三种方法可以用来将画布中已绘制好的图像的全部或者局部区域复制到画布中的另一个位置上。该方法使用九个参数,image代表被复制的图像文件,sxsy分别表示源图像的被复制区域在画布中的起始横坐标与起始纵坐标,swsh表示被复制区域的宽度与高度,dxdy表示复制后的目标图像在画布中的起始横坐标与起始纵坐标,dwdh表示复制后的目标图像的宽度与高度。该方法可以只复制图像的局部,只要将sxsy设为局部区域的起始点坐标, 将swsh设为局部区域的宽度与高度就可以了。该方能也可以用来将源图像进行缩放,只要将dwdh设为缩放后的宽度与高度就可以了。
绘制图像时首先使用不带参数的new方法创建Image对象,然后设定Image对象的src属性为需要绘制的图像文件的路径。

image = new Image();
image.src = "image1.jpg"; //设置图像路径

然后就可以使用drawlmage方能绘制该图像文件了。
事实上,即使设定好Image对象的src属性后,也不一定立刻就能把图像绘制完毕,譬如,有时该图像文件是一个来源于网络的比较大的图像文件,这时就得耐心等待图像全部装载完毕才能看见该图像。
这种情况下,需要先等图形加载完毕再绘制图像文件。

image.onload = function(){ 
  // 绘制图像的函数 
}

Image对象的onload事件中同步执行绘制图像的函数,就可以一边装载一边绘制了。

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);  
  image = new Image(); 
  image.src = "tyl.jpg";  
  image.onload = function() {  
    drawImg(context, image);  
  };       
}  
function drawImg(context, image){  
  for(var i = 0;i < 7;i++)  {
    context.drawImage(image, 0 + i * 50, 0 + i * 25, 100, 100);
  }
}

效果如图:

下面的代码使用了八个参数的drawlmage方法将图像的局部放大,并复制到画布中另一个地方,该示例通常用来做图像局部的特写放大处理。

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);  
  image = new Image(); 
  image.src = "tyl.jpg";  
  image.onload = function() {  
    drawImg(context, image);  
  };       
}  
function drawImg(context, image){  
  var i = 0;
  // 首先调用该方法绘制原始图像
  context.drawImage(image, 0, 0, 100, 100); 
  // 绘制将局部区域进行放大后的图像
  context.drawImage(image, 23, 5, 57, 80, 110, 0, 100, 100);  
}

运行效果如图:

图像平铺

图像平铺就是用按一定比例缩小后的图像将画布填满,有两种方法可以实现图像平铺,一种是使用drawImage方法。

function draw(id) {  
  var image = new Image();      
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }   
  var context = canvas.getContext('2d'); 
  image.src = "tyl2.jpg"; 
  image.onload = function(){  
    drawImg(canvas, context, image);  
  };        
}  
function drawImg(canvas, context, image){  
  //平铺比例
  var scale = 5;
  //缩小后图像宽度
  var n1 = image.width / scale;
   //缩小后图像高度
  var n2 = image.height / scale;
  //平铺横向个数
  var n3 = canvas.width / n1;
  //平铺纵向个数
  var n4 = canvas.height / n2;
  for(var i = 0;i < n3;i++) {
    for(var j = 0;j < n4;j++) {
      context.drawImage(image, i * n1, j * n2, n1, n2);
    }
  }
}

运行效果如图:

在H5中,要达到平铺效果,还可以使用更简便的图形上下文对象的createPattern方法。

context.createPattern(image,type);

该方法使用两个参数,image参数为要平铺的图像 ,type参数的值必须是下面的字符串值之一:

  • no-repeat:不平铺
  • repeat-x:横方向平铺
  • repeat-y:纵方 向平铺
  • repeat:全方向平铺

创建了lmage对象并指定图像文件后,使用createPattern方法创建填充样式,然后将该样式指定绘图形上下文对象的fillStyle属性,最后再填充画布,就可以看到重复填充的效果了。

function draw(id) {  
  var image = new Image();      
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }   
  var context = canvas.getContext('2d'); 
  image.src = "tyl3.jpg"; 
  image.onload = function() {  
    //创建填充样式,全方向平铺
    var ptrn = context.createPattern(image, 'repeat'); 
    //指定填充样式
    context.fillStyle = ptrn;  
    //填充画布
    context.fillRect(0,0,400,300);  
  };        
}

图像裁剪

图像裁剪功能是指在画布内使用路径,只绘制该路径所包括区域内的图像,不绘制路径外部的图像。
使用图形上下文对象的不带参数的clip方法来实现Canvas元素的图像裁剪功能。该方法使用路径来对Canvas画布设置一个裁剪区域。因此,必须先创建好路径。路径创建完成后,调用clip方法设置裁剪区域。

function draw(id) {  
  var canvas = document.getElementById(id);
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d');
  var gr = context.createLinearGradient(0, 400, 300, 0);
  gr.addColorStop(0, 'rgb(255, 255, 0)');
  gr.addColorStop(1, 'rgb(0, 255, 255)');
  context.fillStyle = gr;
  context.fillRect(0, 0, 400, 300);
  image = new Image();
  image.onload = function(){
    drawImg(context, image);
  };
  image.src = "tyl.jpg";  
}
function drawImg(context, image) {  
  create5StarClip(context);  
  context.drawImage(image, -50, -150, 300, 300);  
}  
function create5StarClip(context) {  
  var n = 0;  
  var dx = 100;  
  var dy = 0;  
  var s = 150;  
  context.beginPath();  
  context.translate(100, 150);  
  var x = Math.sin(0);  
  var y = Math.cos(0);  
  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.clip();  
}

运行效果如图:

在上面的代码中,把画布背景绘制完成后,调用create5StarClip函数。在函数中,创建一个五角星的路径,然后使用clip方法设置裁剪区域。
裁剪区域一旦设置好之后,后面绘制的所有图形就都可以用这个裁剪区域。如果要取消这个已经设置好的裁剪区域,需要使用绘制状态的保存与恢复功能。这两个功能保存与恢复图形上下文的临时状态。在设置图像裁剪区域时,首先调用save方法保存图形上下文的当前状态,在绘制完经过裁剪的图像后,再调动restore恢复之前保存的图形上下文的状态,通过这种方法,对之后绘制的图像取消裁剪区域。

function draw(id) {
  var canvas =document.getElementById(id);
  if(canvas == null) {return false; }
  var context = canvas.getContext('2d');
  var gr = context.createLinearGradient(0, 400, 300, 0);
  gr.addColorStop(0, 'rgb(255, 255, 0)');
  gr.addColorStop(1, 'rgb(0, 255, 255)');
  context.fillStyle = gr;
  context.fillRect(0, 0, 400, 300);
  image = new Image();
  image.onload = function() {
    drawImg(context, image);
  };
  image.src = 'tyl.jpg';
}
function drawImg(context, image) {
  create5StarClip(context);
  context.drawImage(image, -50, -150, 300, 300);
}
function create5StarClip(context) {
  var n = 0;
  var dx = 100;
  var dy = 0;
  var s = 150;
  context.beginPath();
  context.translate(100, 150);
  var x = Math.sin(0);
  var y = Math.cos(0);
  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.clip();
}

效果如图:

像素处理

使用Canvas API能够获取图像中的每一个像素,然后得到该像素颜色的rgb值或rgba值。使用图形上下文对象的getImageData方法来获取图像中的像素。

var imageData = context.getImageData(sx, sy, sw, sh);

该方法使用四个参数,sxsy分别表示所获取区域的起点横坐标、起点纵坐标,swsh分别表示所获取区域的宽度和高度。
imagedata变量是一个CanvasPixelArray对象,具有heightwidthdata等属性。data属性是一个保存像素数据的数组,内容类似[ r1, g1, b1, a1, r2, g2 ,b2, a2, r3, g3, b3, a3, ...],其中,r1,g1b1,a1为第一个像素的红色值,绿色值,蓝色值,透明度值;r2g2b2a2分别为第二个像素的红色值,绿色值,蓝色值,透明度值,依此类推。data.length为所取得像素的数量。

var image = new Image();
var context = canvas.getContext('2d'); 
image.onload = function () {
  context.drawImage(image, 0, 0);
  var imageData;
  context.drawImage(image, 0, 0);
  imageData = context.getImageData(0, 0, image.width, image.height);
}

取得了这些像素后,就可以对这些像素进行处理了,接下来可以进行诸如蒙版处理、面部识别等较复杂的图像处理操作。
下面的代码用Canvas API将图像进行了反显操作。在得到像素数组后,将该数组中的每个像素颜色进行了反显操作,然后保存回像素数组,最后使用图形上下文对象的putImageData方法将反显操作后的图形重新绘制在画布上。

context.putImageData(imageData, dx, dy[, dirtyX, dirtyY, dirtyWidth, dirtyHeight]);

该方法使用七个参数,imageData为像素数组,dxdy分别表示重绘图像的起点横坐标、起点纵坐标。后面的四个参数为可选参数,给出一个矩形的起点横坐标、起点纵坐标、宽度和高度,如果加上后边这四个参数,则只绘制像素数组中这个矩形范围内的图像。

function draw(id) {
  var canvas = document.getElementById(id);
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d');
  var image = new Image();
  image.src = 'tyl.jpg';
  image.onload = function () {
    context.drawImage(image, 0, 0);
    var imageData = context.getImageData(0, 0, image.width, image.height);
    for(var i = 0,n = imageData.length; i < n; i += 4) {
      imageData.data[i + 0] = 255 - imageData.data[i + 0];  // red
      imageData.data[i + 1] = 255 - imageData.data[i + 1];  // green
      imageData.data[i + 2] = 255 - imageData.data[i + 2];  // blue
    }
    context.putImageData(imageData, 0, 0);
  }
}

对于像素操作只有部分浏览器支持。

绘制文字

可以在Canvas画布中进行文字的绘制,同时也可以指定绘制文字的字体、大小、对齐方式等,还可以进行文字的纹理填充等。
绘制文字时可以使用fillText方法或strokeText方法。
fillText方法用填充方式绘制字符串。

void fillText(text,x,y,[maxWidth]); 

该方法接受四个参数,第一个参数text表示要绘制的文字,第二个参数x表示绘制文字的起点横坐标,第三个参数y表示绘制文字的起点纵坐标,第四个参数maxWidth为可选参数, 表示显示文字时的最大宽度,可以防止文字溢出。
strokeText方法用轮廓方式绘制字符串。

void strokeText(text,x,y,[maxWidth]);

该方法参数部分的解释与fillText方法相同。
在进行文字的绘制之前,可以先对该对象的有关文字绘制的属性进行设置:

  • font属性:设置文字字体
  • textAlign属性:设置文字水平对齐方式,属性值可以为start 、end、left、right、center。默认值为start
  • textBaseline属性 : 设置文字垂直对齐方式,属性值可以为top、hanging、middle、alphabetic、ideographic、bottom。默认值为alphabetic
function draw(id) {  
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d'); 
  context.fillStyle = '#00f';
  context.font = 'italic 30px sans-serif';
  context.textBaseline = 'top';
  //填充字符串
  context.fillText('示例文字', 0, 0);
  context.font = 'bold  30px sans-serif';
  //轮廓字符串
  context.strokeText('示例文字', 0, 50);
}

运行效果:

在使用CSS样式的时候,有时我们会希望能在文字周围制作一个漂亮的边框,在定义边框宽度的时候,我们需要首先计算出在这个边框里最长一行的文字的宽度。这时,我们可以使用图形上下文对象的measureText方能来得到文字的宽度。

metrics = context.measureText(text)

measureText方法接受一个参数text,该参数为需要绘制的文字,该方法返回一个TextMetrics对象, TextMetrics对象的width属性表示使用当前指定的字体后text参数中指定的文字的总文字宽度。

function draw(id) { 
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d'); 
  context.font = 'italic 20px sans-serif';
  /* 定义绘制文字*/
  var txt = "字符串的宽度为";
  /* 获取文字宽度 */
  var tm1 = context.measureText(txt);
  /* 绘制文字 */
  context.fillText(txt, 10, 30);
  context.fillText(tm1.width, tm1.width + 10, 30);
  /* 改变字体 */
  context.font = "bold  30px sans-serif";
  /* 重新获取文字宽度 */
  var tm2 = context.measureText(txt);
  /* 重新绘制文字*/
  context.fillText(txt, 10, 70);
  context.fillText(tm2.width, tm2.width + 10, 70);
}

效果如图:

补充知识

保存与恢复状态

saverestore这两个方能均不带参数,分别保存与恢复图形上下文的当前绘画状态。在需要保存与恢复当前状态时,首先调用save方法将当前状态、保存到栈中,在做完想做的工作后,再调用restore从栈中取出之前保存的图形上下文的状态进行恢复。

var x, y;
for (var j = 1; j < 50; j++){
  ctx.save() ;
  // 改变绘画状态,进行想要的操作
  ctx.fillStyle = '#fff';
  x = 75 - Math.floor(Math.random() * 150); 
  y = 75 - Math.floor(Math.random() * 150); 
  ctx.translate(x, y);
  drawStar(ctx, Math.floor(Math.random() * 4) + 2); 
  ctx.restore();
}

保存与恢复状态可以应用在以下场合:

  • 图像或图形变形
  • 图像裁剪
  • 改变图形上下文的以下属性:fillStyle、font、lineWidth、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、textAlign、strokeStyle、textBaseline、lineJoin、lineCap、miterLimit、globalAlpha、globalCompositeOperation

保存文件

在画布中绘制完成一幅图形或图像后,很多时候我们需要将该图形或图像保存到文件中, 使用Canvas API可以完成这步工作。
Canvas API保存文件的原理实际上是把当前的绘画状态输出到一个data URL地址所指向的数据中的过程,所谓data URL, 是指目前大多数浏览器能够识别的一种base64位编码的URL,主要用于小型的、可以在网页中直接嵌入,而不需要从外部文件嵌入的数据,比如img元素中的图像文件等。data URL的格式类似于data:image/png;base64,iVBORdfoAAA...
Canvas API使用toDataURL方法把绘画状态输出到一个data URL中,然后重新装载,客户可直接把装载后的文件进行保存。

canvas.toDataURL(type);

参数type表示要输出数据的MIME类型。

function draw(id) {  
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d'); 
  context.fillStyle = "rgb(0, 0, 255)";
  context.fillRect(0, 0, canvas.width, canvas.height);
  context.fillStyle = "rgb(255, 255, 0)";
  context.fillRect(10, 20, 50, 50); 
  window.location = canvas.toDataURL("image/jpeg");     
}

效果如图:

简单动画的制作

在Canvas 中制作动面实际上就是一个不断擦除、重绘、擦除、 重绘的过程,具体步骤如下 :

  • 预先编写好用来绘图的函数,在该函数中先用clearRect方法将画布整体或局部擦除。
  • 使用setInterval方法设置动画的间隔时间。
var context;
var width,height;
var i;
function draw(id) {
  var canvas = document.getElementById(id);  
  if (canvas == null) { return false; }
  context = canvas.getContext('2d'); 
  width = canvas.width;
  height = canvas.height;
  i = 0;
  setInterval(rotate, 100);
}
function rotate() {   
  context.clearRect(0, 0, width, height);
  context.fillStyle = "red";
  context.fillRect(i, 0, 20, 20);
  i = i + 20;
}

上面的代码将绘制一个红色小方块,使其在画布中从左向右缓慢移动。
效果如图:

下面的代码给出一个通过动画来循环显示所有参数组合效果。

var globalId;
var i = 0;
function draw(id) {
  globalId = id;
  setInterval(Composite, 1000);
}
function Composite() {  
  var canvas = document.getElementById(globalId);  
  if (canvas == null) { return false; }
  var context = canvas.getContext('2d'); 
  var oprtns = new Array(
    "source-atop",
    "source-in",
    "source-out",
    "source-over",
    "destination-atop",
    "destination-in",
    "destination-out",
    "destination-over",
    "lighter",
    "copy",
    "xor"
    );
  if(i > 10) { i=0; }
  context.clearRect(0, 0, canvas.width, canvas.height);
  context.save();
  //绘制原有图形(蓝色长方形)
  context.fillStyle = "blue";
  context.fillRect(10, 10, 60, 60);
  //设置组合方式 
  context.globalCompositeOperation = oprtns[i];
  //设置新图形(红色圆形)
  context.beginPath();
  context.fillStyle = "red";
  context.arc(60, 60, 30, 0, Math.PI * 2, false);
  context.fill();
  context.restore();
  i = i + 1;
}

效果如图:

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容

  •   HTML5 添加的最受欢迎的功能就是 元素。这个元素负责在页面中设定一个区域,然后就可以通过 JavaScri...
    霜天晓阅读 2,987评论 0 2
  • canvas基本标签 < /canvas>宽高写在内部跟样式有区别的(样式设置-画出的图形宽高改变,内部设置-画出...
    闫子扬阅读 450评论 0 0
  • --绘图与滤镜全面解析 概述 在iOS中可以很容易的开发出绚丽的界面效果,一方面得益于成功系统的设计,另一方面得益...
    韩七夏阅读 2,701评论 2 10
  • 晚上在为班级整理信息表格时,刚上高一的妹妹冷不丁发了张截图给我,是她在QQ空间在她自己的留言板留了言,内容是她...
    小肥肥肉圆阅读 715评论 0 2
  • 大部分时候都谈不少收获,只是经历过后,发现原来自己还这样生活过。 一个和我一样喜欢看玄幻小说的朋友发问,你说我和你...
    你的眼睛好大阅读 504评论 0 0