画布 - Canvas

# 画布 canvas提供了使用画布进行2D画图的支持,可用于简单的小游戏开发或者图片编辑。使用canvas可以轻松地在一张图片或一个界面上绘制各种线与图形。 > **注意!** Canvas模块本质上是将 [Android Canvas](https://developer.android.google.cn/reference/android/graphics/Canvas) 进行包装后的结果。本模块的部分用法和文档暂时缺失,但可以在 Android 文档中找到。请参阅 [Android Canvas](https://developer.android.google.cn/reference/android/graphics/Canvas)、[Android Paint](https://developer.android.google.cn/reference/android/graphics/Paint) 与 [Android Path](https://developer.android.google.cn/reference/android/graphics/Path) 了解更多细节。 canvas的坐标系为平面直角坐标系,以控件左上角为原点,控件上边沿为x轴正方向,控件左边沿为y轴正方向。例如分辨率为1920*1080的屏幕上,canvas控件覆盖全屏,画一条从屏幕左上角到屏幕右下角的线段为: ```javascript canvas.drawLine(0, 0, 1080, 1920, paint); ``` canvas的绘制依赖于画笔Paint, 通过设置画笔的粗细、颜色、填充等可以改变绘制出来的图形。例如绘制一个红色实心正方形为: ```javascript let paint = new Paint(); // 设置画笔为填充,则绘制出来的图形都是实心的 paint.setStyle(Paint.STYLE.FILL); // 设置画笔颜色为红色 paint.setColor(colors.RED); // 绘制一个从坐标(0, 0)到坐标(100, 100)的正方形 canvas.drawRect(0, 0, 100, 100, paint); ``` 如果要绘制正方形的边框,则通过设置画笔的Style来实现: ```javascript let paint = new Paint(); // 设置画笔为描边,则绘制出来的图形都是轮廓 paint.setStyle(Paint.STYLE.STROKE); // 设置画笔颜色为红色 paint.setColor(colors.RED); // 绘制一个从坐标(0, 0)到坐标(100, 100)的正方形 canvas.drawRect(0, 0, 100, 100, paint); ``` 结合画笔,canvas可以绘制基本图形、图片等。 ## 常用方法索引 绘制颜色: - 根据R、G、B分量绘制颜色:[canvas.drawRGB(r, g, b)](#canvasdrawrgbr-g-b) - 根据A、R、G、B分量绘制颜色:[canvas.drawARGB(r, g, b)](#canvasdrawargba-r-g-b) - 根据颜色值绘制颜色:[canvas.drawColor(color)](#canvasdrawcolorcolor) - 根据颜色值与混合模式绘制颜色:[canvas.drawColor(color, mode)](#canvasdrawcolorcolor-mode) 绘制画笔: - [canvas.drawPaint(paint)](#canvasdrawpaintpaint) 绘制几何图形: - 绘制一个点:[canvas.drawPoint(x, y, paint)](#canvasdrawpointx-y-paint) - 绘制多个点:[canvas.drawPoints(pts, paint)](#canvasdrawpointspts-paint) - 绘制一条线:[canvas.drawLine(startX, startY, stopX, stopY, paint)](#canvasdrawlinestartx-starty-stopx-stopy-paint) - 绘制多条线:[canvas.drawLines(pts, paint)](#canvasdrawlinespts-paint) - 绘制矩形:[canvas.drawRect(r, paint)](#canvasdrawrectr-paint) - 绘制椭圆:[canvas.drawOval(oval, paint)](#canvasdrawovaloval-paint) - 绘制圆:[canvas.drawCircle(cx, cy, radius, paint)](#canvasdrawcirclecx-cy-radius-paint) - 绘制弧:[canvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint)](#canvasdrawarcoval-startangle-sweepangle-usecenter-paint) - 绘制圆角矩形:[canvas.drawRoundRect(rect, rx, ry, paint)](#canvasdrawroundrectrect-rx-ry-paint) - 绘制路径:[canvas.drawPath(path, paint)](#canvasdrawpathpath-paint) 绘制文字: - 沿直线绘制文字:[canvas.drawText(text, x, y, paint)](#canvasdrawtexttext-x-y-paint) - 沿路径绘制文字:[canvas.drawTextOnPath(text, path, hOffset, vOffset, paint)](#canvasdrawtextonpathtext-path-hoffset-voffset-paint) 绘制图片: - 绘制位图:[canvas.drawBitmap(bitmap, left, top, paint)](#canvasdrawbitmapbitmap-left-top-paint) - 绘制图章:[canvas.drawPicture(picture)](#canvasdrawpicturepicture) 获取画布大小: - 获取宽度:[canvas.getWidth()](#canvasgetwidth) - 获取高度:[canvas.getHeight()](#canvasgetheight) 矩阵变换: - 平移:[canvas.translate(dx, dy)](#canvastranslatedx-dy) - 缩放:[canvas.scale(sx, sy[, px, py])](#canvasscalesx-sy-px-py) - 旋转:[canvas.rotate(degrees[, px, py])](#canvasrotatedegrees-px-py) - 切变变换:[canvas.skew(sx, sy)](#canvasskewsx-sy) ## canvas.getWidth() * 返回 {number} 返回画布当前图层的宽度。 ## canvas.getHeight() * 返回 {number} 返回画布当前图层的高度。 ## canvas.drawRGB(r, g, b) * `r` {number} 红色通道值 * `g` {number} 绿色通道值 * `b` {number} 蓝色通道值 将整个可绘制区域填充为r、g、b指定的颜色。相当于 `canvas.drawColor(colors.rgb(r, g, b))`。 ## canvas.drawARGB(a, r, g, b) * `a` {number} 透明通道值 * `r` {number} 红色通道值 * `g` {number} 绿色通道值 * `b` {number} 蓝色通道值 将整个可绘制区域填充为a、r、g、b指定的颜色。相当于 `canvas.drawColor(colors.argb(a, r, g, b))`。 ## canvas.drawColor(color) * `color` {number} 颜色值 将整个可绘制区域填充为color指定的颜色。 ## canvas.drawColor(color, mode) * `color` {number} 颜色值 * `mode` {PorterDuff.Mode} 混合模式 将整个可绘制区域按mode指定的混合模式填充为color指定的颜色。 ## canvas.drawPaint(paint) * `paint` {Paint} 画笔 将整个可绘制区域用paint指定的画笔填充。相当于绘制一个无限大的矩形,但是更快。通过该方法可以绘制一个指定的着色器的图案。 ## canvas.drawPoint(x, y, paint) * `x` {number} x坐标 * `y` {number} y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制由坐标(x, y)指定的点。 点的形状由画笔的线帽决定(参见paint.setStrokeCap(cap))。 点的大小由画笔的宽度决定(参见paint.setStrokeWidth(width))。 > 如果画笔宽度为0,则也会绘制1个像素(若抗锯齿启用则绘制至多4个像素)。 相当于 `canvas.drawPoints([x, y], paint)`。 ## canvas.drawPoints(pts, paint) * `pts` {Array<number>} 点坐标数组 [x0, y0, x1, y1, x2, y2, ...] * `paint` {Paint} 画笔 在可绘制区域绘制由坐标数组指定的多个点。 ## canvas.drawLine(startX, startY, stopX, stopY, paint) * `startX` {number} 起点x坐标 * `startY` {number} 起点y坐标 * `endX` {number} 终点x坐标 * `endY` {number} 终点y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制由起点坐标(startX, startY)和终点坐标(endX, endY)指定的线。 绘制时会忽略画笔的样式(Style)。也就是说,即使样式设为“仅填充(FILL)”也会绘制。 退化为点的线(长度为0)不会被绘制。 ## canvas.drawLines(pts, paint) * `pts` {Array<number>} 点坐标数组 [x0, y0, x1, y1, x2, y2, ...] * `paint` {Paint} 画笔 在可绘制区域绘制由坐标数组指定的点两两连成的一系列线。 每条线需要点坐标数组中的四个连续的值,因此要绘制一条线,数组必须至少包含四个值。 相当于 `canvas.drawLine(pts[0], pts[1], pts[2], pts[3], paint)` 然后 `canvas.drawLine(pts[4], pts[5], pts[6], pts[7], paint)`,之后以此类推。 绘制时会忽略画笔的样式(Style)。也就是说,即使样式设为“仅填充(FILL)”也会绘制。 ## canvas.drawRect(r, paint) * `r` {Rect|RectF} 矩形边界 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界r指定的矩形。 绘制时画笔的样式(Style)决定了是否绘制矩形界线和填充矩形。 相当于 `canvas.drawRect(r.left, r.top, r.right, r.bottom, paint)`。 ## canvas.drawRect(left, top, right, bottom, paint) * `left` {number} 矩形左边界x坐标 * `top` {number} 矩形上边界y坐标 * `right` {number} 矩形右边界x坐标 * `bottom` {number} 矩形下边界y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界(left, top) - (right, bottom)指定的矩形。 绘制时画笔的样式(Style)决定了是否绘制矩形界线和填充矩形。 ## canvas.drawOval(oval, paint) * `oval` {RectF} 椭圆的外接矩形的边界 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界oval指定的椭圆。 绘制时画笔的样式(Style)决定了是否绘制椭圆界线和填充椭圆。 相当于 `canvas.drawOval(oval.left, oval.top, oval.right, oval.bottom, paint)`。 ## canvas.drawOval(left, top, right, bottom, paint) * `left` {number} 椭圆外接矩形的左边界x坐标 * `top` {number} 椭圆外接矩形的上边界y坐标 * `right` {number} 椭圆外接矩形的右边界x坐标 * `bottom` {number} 椭圆外接矩形的下边界y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界(left, top) - (right, bottom)指定的椭圆。 绘制时画笔的样式(Style)决定了是否绘制椭圆界线和填充椭圆。 ## canvas.drawCircle(cx, cy, radius, paint) * `cx` {number} 圆心的x坐标 * `cy` {number} 圆心的y坐标 * `radius` {number} 圆的半径 * `paint` {Paint} 画笔 在可绘制区域绘制由圆心(cx, cy)与半径radius指定的圆。如果半径小于等于0则不会绘制。 绘制时画笔的样式(Style)决定了是否绘制圆的界线和填充圆。 ## canvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint) * `oval` {RectF} 圆弧对应的椭圆外接矩形的边界 * `startAngle` {number} 圆弧的起始角(以角度计算) * `sweepAngle` {number} 圆弧所对的圆心角(顺时针方向,以角度计算) * `useCenter` {boolean} 绘制路径时是否连接圆弧对应的椭圆圆心 * `paint` {Paint} 画笔 在可绘制区域绘制指定的圆弧。 这一圆弧是由矩形边界oval指定的椭圆的一部分,起始于startAngle指定的角度,并以顺时针方向扫过sweepAngle指定的角度。 如果起始角startAngle为负数或大于等于360,则绘制时起始角将对360取模。 圆弧是沿顺时针方向绘制的。起始角0°相当于从直角坐标系的0°开始绘制(即从3点钟方向沿顺时针方向绘制)。 如果圆心角sweepAngle大于等于360,则此函数将绘制一个完整的椭圆。 注意,这与path.arcTo方法不同,path.arcTo会将圆心角对360取模。 如果圆心角为负数则会将圆心角对360取模。 如果指定useCenter为true,在描边和填充时将会连接圆弧对应的椭圆圆心,最终将绘制出一个扇形。 相当于 `canvas.drawArc(oval.left, oval.top, oval.right, oval.bottom, startAngle, sweepAngle, useCenter, paint)`。 ## canvas.drawArc(left, top, right, bottom, startAngle, sweepAngle, useCenter, paint) * `left` {number} 椭圆外接矩形的左边界x坐标 * `top` {number} 椭圆外接矩形的上边界y坐标 * `right` {number} 椭圆外接矩形的右边界x坐标 * `bottom` {number} 椭圆外接矩形的下边界y坐标 * `startAngle` {number} 圆弧的起始角(以角度计算) * `sweepAngle` {number} 圆弧所对的圆心角(顺时针方向,以角度计算) * `useCenter` {boolean} 绘制路径时是否连接圆弧对应的椭圆圆心 * `paint` {Paint} 画笔 在可绘制区域绘制指定的圆弧。这一圆弧是由矩形边界(left, top) - (right, bottom)指定的椭圆的一部分,其余参数请参见 [canvas.drawArc(oval, startAngle, sweepAngle, useCenter, paint)](#canvasdrawarcoval-startangle-sweepangle-usecenter-paint)。 ## canvas.drawRoundRect(rect, rx, ry, paint) * `rect` {RectF} 矩形边界 * `rx` {number} 圆角在x轴上的半径 * `ry` {number} 圆角在y轴上的半径 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界rect、圆角半径rx、ry指定的圆角矩形。 绘制时画笔的样式(Style)决定了是否绘制圆角矩形界线和填充圆角矩形。 相当于 `canvas.drawRoundRect(rect.left, rect.top, rect.right, rect.bottom, rx, ry, paint)`。 ## canvas.drawRoundRect(left, top, right, bottom, rx, ry, paint) * `left` {number} 圆角矩形左边界x坐标 * `top` {number} 圆角矩形上边界y坐标 * `right` {number} 圆角矩形右边界x坐标 * `bottom` {number} 圆角矩形下边界y坐标 * `rx` {number} 圆角在x轴上的半径 * `ry` {number} 圆角在y轴上的半径 * `paint` {Paint} 画笔 在可绘制区域绘制由矩形边界(left, top) - (right - bottom)、圆角半径rx、ry指定的圆角矩形。 绘制时画笔的样式(Style)决定了是否绘制圆角矩形界线和填充圆角矩形。 ## canvas.drawPath(path, paint) * `path` {Path} 路径 * `paint` {Paint} 画笔 在可绘制区域绘制指定的路径。 绘制时画笔的样式(Style)决定了是否描边路径和填充路径。 ## canvas.drawBitmap(bitmap, left, top, paint) * `bitmap` {Bitmap} 位图 * `left` {number} x坐标 * `top` {number} y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制指定的位图,使它的左上角位于(left, top)指定的坐标,并应用画布的变换矩阵。 > **注意!** 如果画笔指定了遮罩过滤器并且该过滤器范围比位图大(超出位图的长/宽)(如 BlurMaskFilter),位图将会像被应用了CLAMP铺展模式的着色器一样绘制。因此超出位图原始范围的颜色将会重复使用边缘的颜色。 如果位图bitmap与画布canvas的密度不同,在绘制时位图将会被缩放至与画布相同密度再绘制。 ## canvas.drawPicture(picture) * `picture` {Picture} 图章 保存画布的变换矩阵与可绘制区域范围,在可绘制区域绘制指定的图章,随后恢复画布的变换矩阵与可绘制区域范围。 该过程与 `picture.draw(canvas)` 不同,因为后者不会进行保存与恢复的操作。 > **注意!**绘制图章将会强制图章退出录制模式(即调用 `picture.endRecording()`)以准备之后的绘制。 ## canvas.drawText(text, x, y, paint) * `text` {string} 文字 * `x` {number} x坐标 * `y` {number} y坐标 * `paint` {Paint} 画笔 在可绘制区域绘制指定的文字,使文字的原点位于(x, y)。文字的起始位置取决于paint的对齐选项。 文字的样式取决于paint的相关设置。 ## canvas.drawTextOnPath(text, path, hOffset, vOffset, paint) * `text` {string} 文字 * `path` {Path} 路径 * `hOffset` {number} 在与路径平行方向的偏移,取正为沿路径方向平移 * `vOffset` {number} 在与路径垂直方向的偏移,取正为向文字的下方平移 * `paint` {Paint} 画笔 在可绘制区域沿着指定的路径绘制指定的文字。文字的起始位置取决于paint的对齐选项。 文字的样式取决于paint的相关设置。 ## canvas.translate(dx, dy) * `dx` {number} 向x轴正方向平移的距离,负数表示反方向平移 * `dy` {number} 向y轴正方向平移的距离,负数表示反方向平移 将当前的变换矩阵右乘指定的平移变换矩阵。相当于将坐标系平移指定距离。 ## canvas.scale(sx, sy[, px, py]) * `sx` {number} 在x轴上缩放的倍数,负数表示沿x轴翻转 * `sy` {number} 在y轴上缩放的倍数,负数表示沿y轴翻转 * `px` {number} 缩放中心的x坐标,默认为0 * `py` {number} 缩放中心的y坐标,默认为0 将当前的变换矩阵右乘指定的缩放变换矩阵。相当于以(px, py)为中心将坐标系缩放指定的倍数。 倍数大于 1 表示放大,小于 1 表示缩小。 ## canvas.rotate(degrees[, px, py]) * `degrees` {number} 旋转的角度(以角度计算) * `px` {number} 旋转中心的x坐标,默认为0 * `py` {number} 旋转中心的y坐标,默认为0 将当前的变换矩阵右乘指定的旋转变换矩阵。相当于以(px, py)为中心将坐标系旋转指定的角度。 ## canvas.skew(sx, sy) * `sx` {number} x轴方向的切变系数 * `sy` {number} y轴方向的切变系数 将当前的变换矩阵右乘指定的切变变换矩阵。 # 画笔 # 变换矩阵 # 路径 # Porter-Duff模式与混合模式 # 图章 # 着色器 # 遮罩过滤器 # 颜色过滤器 # 路径特效 # 区域 # 字体 # 位图与图像解码器 # .9图 # 示例 ## 函数图像高级版 ```js "ui"; //ui布局为一块画布和一些函数调整控件 ui.layout( <vertical> <linear> <input id="fx" textSize="16sp" text="x*x+3*x-4" layout_weight="1"/> <button id="ok" w="50dp"/> </linear> <linear> <button id="left" text="←" layout_weight="1"/> <button id="right" text="→" layout_weight="1"/> <button id="up" text="↓" layout_weight="1"/> <button id="down" text="↑" layout_weight="1"/> <button id="zoom_in" text="+" layout_weight="1"/> <button id="zoom_out" text="-" layout_weight="1"/> </linear> <canvas id="board" w="*" h="*"/> </vertical> ); //函数表达式 var f = "x*x+3*x-4"; //绘制区间 var minX = -5; var maxX = 5; var minY; var h = 1; var w = 1; //画笔 var paint = new Paint(); paint.setStrokeWidth(2); ui.board.on("draw", function(canvas){ w = canvas.getWidth(); h = canvas.getHeight(); if(minY == undefined){ minY = -(maxX - minX) * h / w / 2; } //计算y轴区间上限 var maxY = minY + (maxX - minX) * h / w; //设置画笔颜色为黑色 paint.setColor(colors.parseColor("#000000")); //绘制两个坐标轴 var x0 = parseInt(- minX / (maxX - minX) * w); canvas.drawLine(x0, 0, x0, h, paint); var y0 = parseInt(h + minY / (maxY - minY) * h); canvas.drawLine(0, y0, w, y0, paint); //设置画笔颜色为红色 paint.setColor(colors.parseColor("#ff0000")); //绘制图像 for(var i = 0; i < w; i++){ var x = minX + i / w * (maxX - minX); var y = eval(f); var j = h - (y - minY) / (maxY - minY) * h; canvas.drawPoint(i, j, paint); } }); ui.ok.click(()=>{ f = String(ui.fx.text()); }); ui.left.click(()=>{ var d = maxX - minX; maxX -= d / 10; minX -= d / 10; }); ui.right.click(()=>{ var d = maxX - minX; maxX += d / 10; minX += d / 10; }); ui.up.click(()=>{ var d = maxX - minX; minY += d / 8; }); ui.down.click(()=>{ var d = maxX - minX; minY -= d / 8; }); ui.zoom_in.click(()=>{ var d = maxX - minX; var a = (maxX + minX) / 2; maxX = a + d; minX = a - d; minY *= (maxX - minY) / d * h / w; }); ui.zoom_out.click(()=>{ var d = maxX - minX; maxX -= d / 2; minX += d / 2; }); ``` ## 函数图像简单版 ```js "ui"; //ui布局为一块画布 ui.layout( <frame> <canvas id="board" w="*" h="*"/> </frame> ); //要绘制的函数,这里是一个一元二次函数 var f = function(x){ return x * x + 3 * x - 4; } //绘制区间 var minX = -5; var maxX = 5; var minY = -10; //画笔 var paint = new Paint(); ui.board.on("draw", function(canvas){ var w = canvas.getWidth(); var h = canvas.getHeight(); //计算y轴区间上限 var maxY = minY + (maxX - minX) * h / w; //设置画笔颜色为黑色 paint.setColor(colors.parseColor("#000000")); //绘制两个坐标轴 canvas.drawLine(w / 2, 0, w / 2, h, paint); canvas.drawLine(0, h / 2, w, h / 2, paint); //设置画笔颜色为红色 paint.setColor(colors.parseColor("#ff0000")); //绘制图像 for(var i = 0; i < w; i++){ var x = minX + i / w * (maxX - minX); var y = f(x); var j = h - (y - minY) / (maxY - minY) * h; canvas.drawPoint(i, j, paint); } }); ``` ## 贪吃蛇 ```js "ui"; ui.layout( <vertical> <canvas id="board" layout_weight="1"/> <relative h="120"> <button id="up" text="↑" w="60" h="60" layout_alignParentTop="true" layout_centerHorizontal="true"/> <button id="left" text="←" w="60" h="60" layout_alignParentBottom="true" layout_toLeftOf="@id/down"/> <button id="down" text="↓" w="60" h="60" layout_alignParentBottom="true" layout_centerHorizontal="true"/> <button id="right" text="→" w="60" h="60" layout_alignParentBottom="true" layout_toRightOf="@id/down"/> </relative> </vertical> ); //蛇的颜色 const SNAKE_COLOR = colors.parseColor("#4caf50"); //背景色 const BG_COLOR = colors.parseColor("#ffffff"); //苹果颜色 const APPLE_COLOR = colors.parseColor("#f44336"); //墙的颜色 const WALL_COLOR = colors.parseColor("#607d8b"); //文本颜色 const TEXT_COLOR = colors.parseColor("#03a9f4"); //蛇自动移动的时间间隔,调小可以增加难度 const MOVE_INTERVAL = 500; //方块宽度 const BLOCK_WIDTH = 40; //游戏区域宽高 const GAME_BOARD_HEIGHT = 20; const GAME_BOARD_WIDTH = 15; //蛇的四个移动方向 const DIRECTION_LEFT = {x: -1, y: 0}; const DIRECTION_RIGHT = {x: 1, y: 0}; const DIRECTION_UP = {x: 0, y: -1}; const DIRECTION_DOWN = {x: 0, y: 1}; //蛇,是一个蛇身的坐标的数组 var snake = [{x: 4, y: 2}, {x: 3, y: 2}, {x: 2, y: 2}]; //苹果的坐标 var apple = generateApple(); //当前蛇的移动方向 var direction = DIRECTION_RIGHT; //标记游戏是否结束 var isGameOver = false; //分数 var score = 0; var paint = new Paint(); ui.board.on("draw", function(canvas){ //绘制背景色 canvas.drawColor(BG_COLOR); //绘制分数 paint.setColor(TEXT_COLOR); paint.setTextSize(50); canvas.drawText("分数: " + score, 30, 70, paint); //如果游戏结束则绘制游戏结束字样 if(isGameOver){ canvas.drawText("游戏结束!", canvas.getWidth() - 280, 70, paint); } //计算坐标偏移,是的游戏区域绘制在画面的水平居中位置 var offset = { x: (canvas.getWidth() - (GAME_BOARD_WIDTH + 2) * BLOCK_WIDTH) / 2, y: 100 }; //偏移坐标 canvas.translate(offset.x, offset.y); //绘制围墙 paint.setColor(WALL_COLOR); for(var i = 0; i <= GAME_BOARD_WIDTH + 1; i++){ //上围墙 drawBlock(canvas, paint, i, 0); //下围墙 drawBlock(canvas, paint, i, GAME_BOARD_HEIGHT + 1); } for(var i = 0; i <= GAME_BOARD_HEIGHT + 1; i++){ //左围墙 drawBlock(canvas, paint, 0, i); //右围墙 drawBlock(canvas, paint, GAME_BOARD_WIDTH + 1, i); } //绘制蛇身 paint.setColor(SNAKE_COLOR); for(var i = 0; i < snake.length; i++){ drawBlock(canvas, paint, snake[i].x, snake[i].y); } //绘制苹果 paint.setColor(APPLE_COLOR); drawBlock(canvas, paint, apple.x, apple.y); }); //启动游戏线程 var gameThread = threads.start(game); //按键点击时改变蛇的移动方向 ui.left.on("click", ()=> direction = DIRECTION_LEFT); ui.right.on("click", ()=> direction = DIRECTION_RIGHT); ui.up.on("click", ()=> direction = DIRECTION_UP); ui.down.on("click", ()=> direction = DIRECTION_DOWN); function game(){ //每隔一段时间让蛇自动前进 setInterval(()=>{ move(direction.x, direction.y); }, MOVE_INTERVAL); } function move(dx, dy){ log("move: %d, %d", dx, dy); direction.x = dx; direction.y = dy; //蛇前进时把一个新的方块添加到蛇头前面 var head = snake[0]; snake.splice(0, 0, { x: head.x + dx, y: head.y + dy }); //如果蛇头吃到了苹果 if(snakeEatsApple()){ //添加分数和重新生成苹果 score += 5; apple = generateApple(); }else{ //没有吃到苹果的情况下把蛇尾去掉保持蛇身长度不变 snake.pop(); } //碰撞检测 collisionTest(); } function snakeEatsApple(){ return snake[0].x == apple.x && snake[0].y == apple.y; } function generateApple(){ //循环生成苹果直至苹果不会生成在蛇身上 var x, y; do{ x = random(1, GAME_BOARD_WIDTH); y = random(1, GAME_BOARD_HEIGHT); }while(!isAppleValid(x, y)); return {x: x, y: y}; } function isAppleValid(x, y){ for (var i = 0; i < snake.length; i++) { if (snake[i].x == x && snake[i].y == y) { return false; } } return true; } function collisionTest(){ //检测蛇有没有撞到墙上 var head = snake[0]; if(head.x < 1 || head.x > GAME_BOARD_WIDTH || head.y < 1 || head.y > GAME_BOARD_HEIGHT){ gameOver(); return; } //检测蛇有没有撞到自己 for(var i = 1; i < snake.length; i++){ if(snake[i].x == head && snake[i].y == head){ gameOver(); return; } } } function gameOver(){ gameThread.interrupt(); isGameOver = true; } function drawBlock(canvas, paint, x, y){ x *= BLOCK_WIDTH; y *= BLOCK_WIDTH; canvas.drawRect(x, y, x + BLOCK_WIDTH, y + BLOCK_WIDTH, paint); } ```