《HTML5游戏编程核心技术与实战》——2.2 图形API

简介:

本节书摘来自异步社区《HTML5游戏编程核心技术与实战》一书中的第2章,第2.2节,作者: 向峰 更多章节内容可以访问云栖社区“异步社区”公众号查看。

2.2 图形API

创建canvas和获取了canvas的环境上下文之后,就可以开始进行绘图了。绘图的方式有两类:一类是进行图形操作,另一类是图像操作。本小节主要涉及图形相关的API,要使用canvas的API进行绘图,通常需要进行下列步骤。

(1)获取canvas元素。通过document.getElementById()取得元素。

(2)获取canvas元素的环境上下文。通过canvas.getContext ("2d")获取2D图像上下文。

(3)确定绘图模式。使用canvas绘图有两种模式,一种是fill,另外一种是stroke。fill是填充的意思,使用该方式模式进行绘图时候会把颜色填充整个图形,而使用stroke的模式只会进行描边框。

(4)设定绘图样式。通过fillStyle和strokeStyle指定绘图样式,fillStyle和strokeStyle分别对应fill模式和stroke模式。绘图样式包括绘图的颜色及渐变方式,通常情况下默认的绘图样式颜色是#000000,也就是黑色。

(5)指定线宽。可以通过lineWidth设定绘制的线宽,默认值是1.0像素。

在进行绘制图形之前,需要先理解路径的概念。

2.2.1 理解路径
canvas中所有的图形可以看成一条路径,这条路径包含0个或者多个子路径。我们可以把路径看成当前canvas中所有图形的集合,而每一个图形就是一条子路径,一条子路径是由一系列点的集合组成。举个例子,我们在canvas中画了一个圆和一条直线,那么可以认为当前canvas中包含两条子路径,一条是圆,一条是直线,这两个图形都是由一个个点集组成,这个圆称为一个闭合的路径,而线是没有闭合的路径。很明显,所谓闭合就是整个图形是封闭的,图形的开始点和结束点相互连接。

事实上,为了提高绘制的效率,当使用canvas进行绘图的时候,所有的图形操作都只是往当前子路径上填加图形,并不是真正的调用绘图操作。比如使用lineTo()进行画线操作,实际上它只是往当前子路径填加一条直线,最终调用stroke或者fill的时候,才是真正进行硬件操作进行绘图。

2.2.2 路径操作API
canvas中常见的创建和渲染路径的方法如表2-1所示。


5cd63781e1f7c16ee5c0efbfe7fa9a8e7e6a8469


0298280ed0a66db8d75aca78618a45900768b69b

**
2.2.3 绘制线条**
关于线条的绘制主要包含以下两个常用的方法。

context.moveTo (x, y):把画笔移动到(x, y)坐标,建立新的子路径。
context.lineTo (x, y):用于建立上一个点到(x, y)坐标的直线,如果没有上一个点,则等同于moveTo (x, y),把(x ,y)点添加到子路径中。
最后使用stroke()可以对路径进行描边。

使用context.lineTo (x, y)绘制直线,代码如下:

<body>
  <canvas id="can" ></canvas>
</body>
<script>
  //获取2d上下文
  var ctx = can.getContext("2d");
  var width = can.width,
    height = can.height;
  ctx.moveTo(0, 0);
  ctx.lineTo(width, height);
  ctx.lineWidth=6;
  ctx.strokeStyle = “red”
  //开始画线
  ctx.stroke();
</script>

代码首先获取can元素的环境上下文,获得can元素的宽度和高度,把画笔移动到原点处,然后使用lintTo (width, height),建立一条从原点到右下角的直线,设定线的宽度为6个像素,笔的颜色是红色,最后通过stroke对整个路径描边。

最后的效果如图2-2所示。

如果,我们需要绘制更复杂的图形,就需要根据一些点的集合,不断的使用lineTo方法,比如下面的代码就绘制了一个向右的箭头:

<body>
  <canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
  //获取2d上下文
  var ctx = can.getContext("2d");
  var width = can.width,
    height = can.height;
  var pts=[[30, 100], [300, 100], [300, 50], [350, 130], [300, 210], [300, 160], [30, 160]]
  ctx.strokeStyle="red";
  ctx.lineWidth = 2;
  ctx.moveTo(pts[0][0], pts[0][1]);
  for(var i=1;i<pts.length;i++)
  {
   ctx.lineTo(pts[i][0], pts[i][1]);
  }
  ctx.closePath();
  ctx.stroke();
</script>

最后的效果如图2-3所示。


<a href=https://yqfile.alicdn.com/8f3e3ceba43c269ea30c3ea17fccd10f483b1b78.png" >

这里需要注意的是,这里定义了7个点,在进行stroke之前使用closePath()函数闭合了路径,这时就会把最后一个点和第一个点连接起来,形成一个封闭的图形,当然,closePath并不是必需的。

2.2.4 绘制矩形
关于矩形的绘制主要包含以下两个常用的方法。

rect (x, y, w, h):建立两个子路径,一个是以点(x, y)为左上角,w和h分别为宽度和高度的矩形,另一个是点(x, y)。这个方法只是建立路径,所以当进行绘制的时候还需要使用stroke()方法描边。
最直接的方法是使用strokeRect (x, y, w, h),该方法会以(x, y)为左上角,(x+width, y+height)为右下角绘制矩形。
fillRect (x, y, w, h)方法则以(x, y)为左上角,(x+width, y+height)为右下角填充矩形。
clearRect (x, y, w, h)则用来清除以(x, y)为左上角,(x+width, y+height)为右下角的矩形区域。这个方法在进行动画处理的时候非常有用,因为在连续绘制动态图形的时候,需要先清除画布上的一块区域。


e2c7f0755e4c6d546624e1207a4cd5bc55857a4d

对于图2-4所示的图形,代码如下:

<body>
  <h2>画矩形例子</h2>
  <canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
  //获取2d上下文
  var ctx = can.getContext("2d");
  var width = can.width,
    height = can.height;
  //计算最里面矩形左上角坐标,边长为10
  var xOff = width*0.5+5, yOff = height*0.5+5;
  for(var i=0;i<8;i++)
  {
   //以最里面矩形为中心,画同心矩形,边长增加20
   ctx.strokeRect(xOff-10*i, yOff-10*i, i*20+10, i*20+10);
  }
</script>

这段代码使用了strokeRect方法画出了8个同心的矩形。

2.2.5 绘制圆弧
关于圆弧的绘制主要包含以下两个常用的方法。

arc (x, y, radius, startAngle, endAngle, anticlockwise):arc方法用来绘制一段圆弧路径,以(x, y)为圆心位置、radius为半径、startAngle为起始弧度、endAngle为终止弧度来画,而在画圆弧时的旋转方向则由最后一个参数 anticlockwise 来指定,如果为 true 就是逆时针,false则为顺时针,如果startAngle和endAngle分别为0和2*Math.PI,则就变成了绘制圆形。
arcTo (x1, y1, x2, y2, radius):这个函数实际上用来绘制同时和两条直线相切的,半径为radius的最短圆弧,一条直线以上一个点和(x1, y1)构成,另一条直线以(x1, y1)和(x2, y2)构成。
这两个函数只是把圆弧添加到了路径中,如果绘制,则还需要通过stroke或者fill函数。

使用arc方法绘制图2-5所示的8个同心圆的代码如下:

<body>
  <h2>画圆形例子</h2>
  <canvas id="can" width="400" height="300" ></canvas>
</body>
<script>
  //获取2d上下文
  var ctx = can.getContext("2d");
  var width = can.width,
    height = can.height;
  //计算圆心
  var xOff = width*0.5,
    yOff = height*0.5;
  for(var i=1;i<8;i++)
  {   
   //以最里面矩形为中心,画同心圆,半径依次增加15
   ctx.beginPath();
   ctx.arc(xOff, yOff, i*15, 0, Math.PI*2, true);
   ctx.closePath();
   ctx.stroke();
  }
</script>


61e69624ee13da714423659746601c78876adc4c

需要注意的是,代码在for循环中,进行绘制圆形之前,使用了beginPath()方法,beginPath()方法用于清除掉之前的路径。如果不清除的话,那么,每次绘制的时候都会把之前的路径又绘制,这样会降低绘制的效率。所以通常情况下,如果我们决定要绘制一个新的图形,最好先使用beginPath()清除上一次的路径。

2.2.6 绘制贝塞尔曲线
关于贝塞尔曲线的绘制主要包含以下两个常用的方法。

bezierCurveTo (cp1x, cp1y, cp2x, cp2y, x, y):绘制一条三次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cp1x, cp1y)和(cp2x, cp2y)。
quadraticCurveTo (cpx, cpy, x, y):绘制一条二次贝塞尔曲线,这条曲线的开始点是子路径的最后一个点,结束点是(x, y),而贝塞尔曲线的控制点是(cpx, cpy)。
贝塞尔曲线是应用非常广泛的函数曲线,通常在计算机图形中用来为平滑曲线建立模型,图2-6分别显示了三次和二次的贝塞尔曲线,区别在于三次的贝塞尔曲线多了一个控制点。


14e216191bcb75c364fb4c0628c5ebf287d1fc2e

以下代码在canvas中显示了一个可以调节控制点的贝塞尔曲线,c1和c2表示控制点,s和e表示曲线的开始和终止点:

<!DOCTYPE html>
<meta charset="utf-8" />
<style type="text/css">
 body{text-align:center;}
 #can{border:1px solid black}
</style>
<body>
  <h2>贝塞尔曲线</h2>  
  <canvas id="can" width="400" height="300"></canvas>
</body>
<script>
 var ctx = can.getContext("2d");
 //定义Point对象
 var Point = function(x, y){
   this.x = x;
   this.y = y;
 }  
 //定义控制点,前面两个是开始和结束点,最后两个是控制点
 var cPt =[];
 //产生控制点
 function createControlPt(x, y)
 {
  if(cPt.length<4)
   {
    cPt.push(new Point(x, y));
   }
 }
 //绘制控制点
 function drawPt()
 {        
  for(var i=0;i<cPt.length;i++)
   {
    var c = "red";
    if(i<2)
    {
     c = "green";
    }
    ctx.strokeStyle = c;
    ctx.strokeRect(cPt[i].x-5, cPt[i].y-5, 10, 10);
   }
 }
 //判断一个点是否在一个以p2为中心的矩形中
 function isInRect(p1, p2, w, h)
 {
   return p1.x>=p2.x-w&&p1.x<=p2.x+w&&p1.y>=p2.y-h&&p1.y<=p2.y+h;
 }
 //判断一个点在哪一个控制区域中
 function getIdxCpt(p)
 {
  var idx = -1;
   for(var i=0;i<cPt.length;i++)
   {
    if(isInRect(p, cPt[i], 5, 5))
    {
     return i;
    }
   }
   return idx;
 }
 //绘制控制点和起始点连线
 function drawBLine()
 {
   ctx.strokeStyle = "gray";
   ctx.beginPath();   
   ctx.moveTo(cPt[0].x, cPt[0].y);
   ctx.lineTo(cPt[2].x, cPt[2].y);
   ctx.stroke();
   ctx.moveTo(cPt[1].x, cPt[1].y);
   ctx.lineTo(cPt[3].x, cPt[3].y);
   ctx.stroke(); 
 }
 //绘制贝塞尔曲线
 function drawBei()
 {
   ctx.beginPath();
   ctx.strokeStyle = "red";
   ctx.moveTo(cPt[0].x, cPt[0].y);
   ctx.bezierCurveTo(cPt[2].x, cPt[2].y, cPt[3].x, cPt[3].y, cPt[1].x, cPt[1].y);
   ctx.stroke();
 }
 //绘制所有的图形
 function draw()
 {
  drawPt();
  if(cPt.length>3)
  {
   drawBLine();
   drawBei();
  }   
 }
 //设置鼠标点下和移动事件 
 var selPt = new Point(-1, -1), sIdx;
 can.onmousedown = function(e){   
   var x = e.offsetX, y = e.offsetY;
   selPt.x = x;selPt.y = y;
   createControlPt(x, y);
   draw(); 
   //判断是否点在控制点中
   if(cPt.length>3)
   {
    sIdx = getIdxCpt(selPt);
    if(sIdx>=0)
    {
     can.onmousemove = function(e){
       cPt[sIdx].x = e.offsetX;
       cPt[sIdx].y = e.offsetY;   
       ctx.clearRect(0, 0, 400, 300);
       draw();
    }     
   }
  }   
 }
 can.onmouseup = function(){
   this.onmousemove = null;
 } 
</script>
</html>

注意在绘制所有图形之前一定要使用clearRect()方法来清除屏幕,因为如果不清除屏幕,将会在屏幕上留下所有的绘制图像。

2.2.7 线条属性
在进行图形绘制的时候,线条有一些常用的属性会影响到线条的样式。

lineWidth:该属性用来设置线条的粗细,默认为1个像素大小,小于0的值将被忽略。
这里有一个比较经典的问题,就是绘制1像素大小的直线。如果绘制1像素大小的线条,看起来像2个像素,当直线呈水平或者垂直方向时,这个现象非常明显,这是为什么呢?W3C在canvas规范中解释到,当使用canvas绘制图形时候,它是由路径向两边扩展的,各占绘制线条宽度的一半,但canvas的坐标并不是直接和屏幕上的像素对应,假设需要绘制一条(3, 1)到(3, 5)的直线,把屏幕放大,得到图2-7。

图2-7中的每一个格子代表显示屏的一个像素,当绘制(3, 1)到(3, 5)的直线的时候,首先,路径就定位在屏幕中第三列像素和第四列像素的中间位置。此时,绘制1像素的时候,就需要从这条路径分别向两边扩展0.5个像素,但实际上显示屏是不可能绘制半个像素的,这个时候就只能同时绘制第三列和第四列两列像素。所以如果需要屏幕绘制一个像素大小的线,只需要把canvas的路径定位到某一个像素的中间位置,这时候刚好向两边扩展为一个像素,如图2-8所示。


<a href=https://yqfile.alicdn.com/bd4e6b1536bc727fde07c79a3df2fac7849fa47d.png" >

所以,如果需要绘制1像素大小的直线,需要把坐标加上0.5的偏移,这时候就显示正常了,当然,如果画大于1像素的或者绘制斜线就没有必要额外处理了。

lineCap:lineCap用来指定线条两端的端点,常用的值有3个,分别是butt(无端点)、round(圆端点)以及square(方端点),其中默认值是butt,三种样式显示的效果如图2-9所示。
lineJoin:lingJoin用来设置两条线连接的方式,常用值有round(圆角)、bevel(斜角)以及miter(尖角),其中miter是默认值,三种样式如图2-10所示。


<a href=https://yqfile.alicdn.com/9be8a2c38c190e999497bbac404b4452d2fc265f.png" >

miterLimit:当lineJoin为miter时有效,表示的是斜面长度和线宽的比例,默认为10。
2.2.8 线条颜色
线条的颜色使用stokeStyle属性指定,颜色的值可以使用类似CSS的方式指定,比如红色可以采用以下3种方式:
  • context.stokeStyle = ‘red‘
  • context.stokeStyle = ‘#ff0000’
  • context.stokeStyle = ‘rgba(255, 0, 0, 1.0)’
    2.2.9 填充

前面所介绍的绘图方式都是适用描边处理(stroke),我们可以通过fill方法进行图形填充。

fill():该方法使用当前的fillStyle填充当前路径,通过fillStyle = 颜色值可以指定填充的颜色,颜色表示和strokeStyle一样。
以下代码就填充了8个红色的同心圆:

<body>
  <h2>填充圆形例子</h2>
  <canvas id="can" width="400" height="300" >
  </canvas>
</body>
<script>
  //获取2d上下文
  var ctx = can.getContext("2d");
  var width = can.width,
    height = can.height;
  //计算圆心
  var xOff = width*0.5,
    yOff = height*0.5;
  for(var i=1;i<=8;i++)
  {   
   //以最里面矩形为中心,画同心圆,半径依次减少15
   ctx.beginPath();
   ctx.fillStyle = "rgba(255, 0, 0, "+(30*i)/500+")";
   ctx.arc(xOff, yOff, 120-i*15, 0, Math.PI*2, true);
   ctx.closePath();
   ctx.fill();
  }
</script>

最后的效果如图2-11所示。


25d8950e319b6e50c8d9db442c6da655870881a3

除了可以填充纯色以外,canvas还提供了填充渐变色以及填充贴图,先来看看渐变对象。

经常使用Photoshop处理图像的读者知道,在Photoshop中就有这种渐变工具,可以通过拖拉一条辅助线来实现渐变。在canvas中提供的渐变对象有两种,一种是线性渐变,另一种是径向渐变。

createLinearGradient (x0, y0, x1, y1):创建一个线性的渐变对象,开始点是(x0, y0),结束点是(x1, y1)。
createRadialGradient (x0, y0, r0, x1, y1, r1):创建一个径向渐变对象,开始点以(x0, y0)为圆心,r0为半径,结束点以(x1, y1)为圆心,r1为半径。
一旦创建完了渐变对象之后,就可以通过该对象的addColorStop()方法,在渐变的某一点中增加一个颜色值,这个点可以认为是关键点,这样,每个关键点之间的色彩就会出现渐变效果。

addColorStop(offset, color):offset表示偏移大小,值在0.0~1.0之间,其实就是一个百分比值;color是使用类似CSS字符串描述的色彩颜色,比如addColorStop (0, "red")表示初始关键点是一个红色点。
当使用fillStyle属性指定一个渐变对象的时候,就可以使用渐变的方式填充路径了,以下代码以渐变对象填充了两个矩形区域。

<body>
  <h2>渐变</h2>  
  <canvas id="can" width=600 height=300></canvas>
</body>
<script>  
  var ctx = can.getContext("2d");
  var w = 480, h=60;
  ctx.beginPath();  
  //创建线性渐变
  var g = ctx.createLinearGradient(0, 0, 480, 0);
  //创建径向渐变
  var g1 = ctx.createRadialGradient(300, 160, 10, 300, 160, 240);  
  //设置颜色
  g.addColorStop(0, "black");
  g.addColorStop(1, "white");  
  ctx.fillStyle = g;
  //绘制矩形
  ctx.rect((600-w)*0.5, 30, w, 80);
  ctx.fill();
  ctx.beginPath();
  //定义基本色
  var colors=["aqua", "black", "blue", "fuchsia", "gray", "green", "lime", "maroon",  
    "navy", "olive", "purple", "red", "silver", "teal", "white", "yellow"];
  var step = 1/colors.length;
  for(var i=0;i<colors.length;i++)
  {
   g1.addColorStop(i*step, colors[i]);
  }  
  //ctx.arc(300, 200, 100, 0, Math.PI*2, true);
  //绘制矩形
  ctx.rect((600-w)*0.5, 120, w, 80);
  ctx.fillStyle = g1;
  ctx.fill();
</script>

效果如图2-12所示。

以上是使用渐变颜色进行填充,另外一种填充方式是使用一张图片作为贴图进行填充,使用的API如下。

createPattern (image,repetition):image表示需要填充的图像,可以是img、canvas、video元素等;repetition定义图像按照什么方式贴图,通常的贴图方式有以下几种。

  • repeat:水平和垂直方向重复贴图,默认值。
  • repeat-x:水平方向重复贴图。
  • repeat-y:垂直方向重复贴图。
  • no-repeat:使用一次贴图。

通过createPattern方法创建了一个模式对象后,就可以通过fillStyle或者strokeStyle等属性指定,然后就可以使用指定的图形进行填充。

以下代码创建了两个分别使用stroke()和fill()填充的图形:

<body>
  <h2>Pattern</h2>  
  <canvas id="can" width="600" height="300"></canvas>  
</body>
<script>  
  var ctx = can.getContext('2d');
  var imgSrc =["img/t1.png", "img/f1.png"];
  var ctx = can.getContext("2d");  
  //创建Image对象和pattern对象
  for(var i=0;i<imgSrc.length;i++)
  {
   var img = new Image();
   img.src = imgSrc[i];
   img.onload = (function(im, i){
    var self = im;
    return function(){
     var p = ctx.createPattern(self, "repeat");
     if(i==0)
     {
       ctx.beginPath();
       ctx.fillStyle = p;
       ctx.fillRect(38, 38, 520, 232); 
      }   
      else
      {
       ctx.beginPath();
       ctx.strokeStyle = p;
       ctx.lineWidth = 18; 
       ctx.strokeRect(28, 28, 540, 250); 
      }
    }
   }(img, i)); 
  }
</script>

效果如图2-13所示。


8fcf7ef34a751957c0e0a8ac5a687b05ca65df85

2.2.10 绘图状态
conext中有一些全局的属性,如前面提到的strokeStyle、fillStyle、lineWidth等。当我们进行绘图的时候,有时,在改变这些值之前,需要保存上一次绘图的状态,下次绘制的时候又需要进行恢复,这种情况很常见。当然,不需要我们自己定义一个全局的对象进行保存,context中本身定义了以下方法用于保存和恢复canvas的状态。

save():把当前绘图状态压到绘图状态堆中。
restore():弹出绘图状态堆最上面保存的绘图状态。
状态堆中包含以下部分。

当前的transformation matrix(换矩阵)前的clipping region(区域)。
当前的属性值:fillStyle、font、globalAlpha、globalCompositeOperation、lineCap、lineJoin、lineWidth、miterLimit、shadowBlur、shadowColor、shadowOffsetX、shadowOffsetY、strokeStyle、textAlign、textBaseline。
为了避免本次绘图状态影响到下次绘图,通常情况下在绘图之前,都会使用contex.save()方法保存当前绘图状态,绘制完成后再使用context.restore()进行恢复。

对于图形的操作,在HTML4时代可以使用SVG进行矢量绘图,而canvas除了支持矢量图形外,还可以直接针对图像以及像素操作,这才是canvas强大的地方。接下来,看看canvas关于图像处理的部分。

相关文章
|
17天前
|
监控 前端开发 JavaScript
实战篇:商品API接口在跨平台销售中的有效运用与案例解析
随着电子商务的蓬勃发展,企业为了扩大市场覆盖面,经常需要在多个在线平台上展示和销售产品。然而,手工管理多个平台的库存、价格、商品描述等信息既耗时又容易出错。商品API接口在这一背景下显得尤为重要,它能够帮助企业在不同的销售平台之间实现商品信息的高效同步和管理。本文将通过具体的淘宝API接口使用案例,展示如何在跨平台销售中有效利用商品API接口,以及如何通过代码实现数据的统一管理。
|
29天前
|
数据采集 JSON API
如何实现高效率超简洁的实时数据采集?——Python实战电商数据采集API接口
你是否曾为获取重要数据而感到困扰?是否因为数据封锁而无法获取所需信息?是否因为数据格式混乱而头疼?现在,所有这些问题都可以迎刃而解。让我为大家介绍一款强大的数据采集API接口。
|
3月前
|
JavaScript API
【vue实战项目】通用管理系统:api封装、404页
【vue实战项目】通用管理系统:api封装、404页
37 3
|
5月前
|
小程序 JavaScript API
小程序云开发实战五:如何将获取到的API数据存入云数据库里面
小程序云开发实战五:如何将获取到的API数据存入云数据库里面
87 0
|
5月前
|
小程序 前端开发 API
小程序云开发实战四:调用豆瓣API获取具体的数据
小程序云开发实战四:调用豆瓣API获取具体的数据
75 0
|
4月前
|
分布式计算 Java 大数据
【大数据技术Hadoop+Spark】HDFS Shell常用命令及HDFS Java API详解及实战(超详细 附源码)
【大数据技术Hadoop+Spark】HDFS Shell常用命令及HDFS Java API详解及实战(超详细 附源码)
161 0
|
4月前
|
安全 大数据 API
elasticsearch|大数据|elasticsearch的api部分实战操作以及用户和密码的管理
elasticsearch|大数据|elasticsearch的api部分实战操作以及用户和密码的管理
62 0
|
10天前
|
JavaScript API UED
Vue3.0新特性解析与实战:Composition API、Teleport与Suspense
【4月更文挑战第6天】Vue3.0引入了颠覆性的Composition API,通过函数式方法提升代码可读性和复用性,例如`setup()`、`ref`等,便于逻辑模块化。实战中,自定义的`useUser`函数可在多个组件中共享用户信息逻辑。另外,Teleport允许组件渲染到DOM特定位置,解决模态框等场景的上下文问题。再者,Suspense提供异步组件加载的延迟渲染,使用fallback内容改善用户体验。这些新特性显著优化了开发和性能,适应现代Web需求。
17 0
|
21天前
|
机器学习/深度学习 前端开发 API
实现以图搜货功能,淘宝API开发实战分享
实现以图搜货功能,淘宝API开发实战分享
24 0
|
1月前
|
移动开发 API HTML5
HTML5响应式自动采集API壁纸系统源码自适应手机端
HTML5响应式自动采集API壁纸系统源码自适应手机端
41 11
HTML5响应式自动采集API壁纸系统源码自适应手机端