一、canvas简介
参考H5之canvas
Canvas是HTML5新增的组件,它就像一块幕布,可以用JavaScript在上面绘制各种图表、动画等。
没有Canvas的年代,绘图只能借助Flash插件实现,页面不得不用JavaScript和Flash进行交互。有了Canvas,我们就再也不需要Flash了,直接使用JavaScript完成绘制。
一个Canvas定义了一个指定尺寸的矩形框,在这个范围内我们可以随意绘制:
<canvas id="test-canvas" width="300" height="200"></canvas>
由于浏览器对HTML5标准支持不一致,所以,通常在<canvas>内部添加一些说明性HTML代码,如果浏览器支持Canvas,它将忽略<canvas>内部的HTML,如果浏览器不支持Canvas,它将显示<canvas>内部的HTML:
<canvas id="test-stock" width="300" height="200">
<p>Current Price: 25.51</p>
</canvas>
在使用Canvas前,用canvas.getContext来测试浏览器是否支持Canvas:
<!-- HTML代码 -->
<canvas id="test-canvas" width="200" heigth="100">
<p>你的浏览器不支持Canvas</p>
</canvas>
'use strict';
getContext('2d')方法让我们拿到一个CanvasRenderingContext2D对象,所有的绘图操作都需要通过这个对象完成。
var ctx = canvas.getContext('2d');
如果需要绘制3D怎么办?HTML5还有一个WebGL规范,允许在Canvas中绘制3D图形:
gl = canvas.getContext("webgl");
二、以下参考《WebGL编程指南》读书笔记
WebGL概述
认识Canvas
在Canvas中使用WebGL
绘制一个点
HTML5中新加入了<canvas>标签,它定义了网页上的绘图区域。如果没有WebGL,JavaScript只能在<canvas>上绘制二维图形,有了WebGL,就可以在上面绘制三维图形了。换句话说,<canvas>就是承载WebGL的容器。
在HTML5之前,如果需要在网页上显示图像,只能使用img标签,但只能显示静态图片,不能提供实时绘制和渲染。因此,后来出现了一些第三方解决方案,如Flash、Silverlight等;但是这些第三方解决方案的最大问题就是需要安装插件。
HTML5引入了<canvas>标签,允许JavaScript动态地绘制图形,并且不需要按照任何插件。
上面说到,<canvas>定义了一个绘图区域,在这个区域中,使用JavaScript可以绘制任何你想画的东西,比如:点、线、矩形、圆。
<html>
<head>
<meta charset="UTF-8">
<meta name="Generator" content="EditPlus®">
<meta name="Author" content="Mirror">
<meta name="Keywords" content="">
<meta name="Description" content="">
<title>Draw a red rectangle </title>
<script>
function main() {
//获取<canvas>标签
var canvas = document.getElementById("myCanvas");
//如果没找到<canvas>标签,则输出错误信息
if (!canvas) {
console.log('Failed to retrieve the <canvas> element.');
return;
}
//要在<canvas>上绘制图像,须先获取绘图上下文,“2d”代表我们要绘制二维图形。
var ctx = canvas.getContext("2d");
//fillstyle:设置或返回用于填充绘画的颜色、渐变或模式;这里我们设置填充颜色为红色。
ctx.fillStyle = "red";
/*
使用填充颜色填充矩形。
fillRect(x,y,width,height)
x 矩形左上角的 x 坐标
y 矩形左上角的 y 坐标
width 矩形的宽度
height 矩形的高度
*/
ctx.fillRect(120, 10, 150, 150);
}
</script>
</head>
<!--页面加载完成后,执行JavaScript中的main()函数-->
<body onload="main()">
<!--定义<canvas>标签,通过width属性和height属性规定它是一片400×400的绘制区域-->
<canvas id="myCanvas" width="400" height="400">
<!--当浏览器不支持时,会直接忽略<canvas>标签,而直接显示下面这一行提示-->
Please use a browser that supports "canvas".
</canvas>
</body>
</html>
在绘制2d图形的时候,我们是通过调用canvas的getContext方法,传入“2d”参数来获取2d图形的回绘图上下文的;这里需要解释一下,通常来说,我们应该使用canvas.getContext()函数来获取绘图上下文(就像之前那样)。但是在获取WebGL绘图上下文时,canvas.getContext()函数接收的参数,在不同浏览器中会不同(虽然大部分浏览器都接收字符串“expeimental-webgl”或“webgl”,但并非所有浏览器都这样
在前面,我们使用2d上下文来绘制了一个矩形;先指定了绘图颜色,然后进行绘制。你可能认为WebGL也差不多,不幸的是,没那么简单。WebGL依赖于一种新的称为着色器的绘图机制。着色器提供了灵活且强大的绘制二维或三维图形的方法,所有WebGL必须使用它。正因为强大,所以更复杂。
要使用WebGL绘图,必须使用着色器,哪怕是一个点(矩形);着色器程序是以字符串的形式“嵌入”在JavaScript文件中,并且在程序开始运行前就已经设置好了。WebGL需要使用两种着色器:顶点着色器、片源着色器;下面分别介绍:
顶点着色器:顶点着色器是用来描述顶点特性(如位置、颜色等)的程序。顶点是指二维或三维空间中的一个点,比如二维或三维图形的端点或交点。
片元着色器:进行逐片元处理过程(如光源)的程序。片元是一个WebGL术语,你可以将其理解为像素(图像的单元)。
//顶点着色器程序
var VSHADER_SOURCE =
"void main() { \n" +
//设置坐标
"gl_Position = vec4(0.0, 0.0, 0.0, 1.0); \n" +
//设置尺寸
"gl_PointSize = 10.0; \n" +
"} \n";
//片元着色器
var FSHADER_SOURCE =
"void main() {\n" +
//设置颜色
"gl_FragColor = vec4(1.0, 0.0, 0.0, 1.0);\n" +
"}\n";
function main() {
//获取<canvas>标签。
var canvas = document.getElementById("myCanvas");
//获取WebGL绘图上下文。
var gl = getWebGLContext(canvas);
//如果浏览器不支持WebGL则提示错误。
if (!gl) {
console.log("Failed to get the rendering context for WebGL.");
return;
}
//初始化着色器
if (!initShaders(gl, VSHADER_SOURCE, FSHADER_SOURCE)) {
console.log("Faile to initialize shaders.");
return;
}
//设置<canvas>的背景色
gl.clearColor(0.0, 0.0, 0.0, 1.0);
//清空<canvas>
gl.clear(gl.COLOR_BUFFER_BIT);
//绘制一个点
gl.drawArrays(gl.POINTS, 0, 1);
}
这个文件包含三个部分,顶点着色器程序(GLSL ES 语言),片元着色器程序(GLSL ES 语言)和主程序(JavaScript语言)。着色器程序代码必须预先处理成单个字符串的形式,所以我们用+号将多行字符串连成一个长字符串。每一行以\n结束,这是由于当着色器内部出错时,就能获取出错的行号,这对于检查源代码中的错误很有帮助;但是,\n并不是必须的。为了更容易维护,也可以把着色器代码写到单独的文件中(就像javaScript文件一样),然后通过javaScript程序从文件中读取出来加载。
根据程序流程,加载页面----->执行main()函数----->获取<canvas>标签----->获取绘图上下文;到这里,都跟之前的流程一样;接下来,会执行名为initShaders()的函数。这个函数是《WebGL编程指南》的作者专门写的一个辅助函数
Web系统由两部分组成,即顶点着色器和片元着色器。在初始化着色器之前,顶点着色器和片元着色器都是空白的,我们需要将字符串形式的着色器代码从JavaScript传给WebGL系统,并建立着色器,这就是initShaders()函数要做的事情。着色器运行在WebGL系统中,而不是JavaScript程序中。
三、canvas 绘图性能
参考
Html5 canvas画图教程22:获取与操作像素信息getImageData
Canvas 最佳实践(性能篇)
new ImageData(array, width, height);
new ImageData(width, height);
- array 包含图像隐藏像素的 Uint8ClampedArray 数组。如果数组没有给定,指定大小的黑色矩形图像将会被创建。
- width 无符号长整型(unsigned long)数值,描述图像的宽度。
- height 无符号长整型(unsigned long)数值,描述图像的高度。
// Creates a 100x100 black rectangle
var imageData = new ImageData(100, 100);
// ImageData { width: 100, height: 100,
// data: Uint8ClampedArray[40000] }
2.getImageData putImageData
getImageData 和 putImageData 这一对 API,前者可以将某个 Canvas 上的某一块区域保存为 ImageData 对象,后者可以将 ImageData 对象重新绘制到 Canvas 上面去。但实际上,putImageData 是一项开销极为巨大的操作,它根本就不适合在每一帧里面去调用。
getImageData的直译即“获得图像数据”,他的作用是从canvas中提取像素出来。所以,他是有返回值的。
ctx.getImageData(x,y,width,height);```//这就是他的语法。而他的返回值是一个ImageData对象:
ImageData { width=10, height=10, data=Uint8ClampedArray}
这个ImageData对象包括:width,height,以及一个像素信息数组data。这个像素信息数组是重中之重。他包括red,green,blue,和alpha,且每个都是0-255的数值——连alpha也是。这个data数组的大小由像素个数决定,即ImageData的widthheight.如getImageData(1,1,1,1),那data就只有一个像素信息;而getImageData(1,1,10,10),则有1010=100个。
而像素信息在data中是怎么保存的呢?本来我的理解,data可以是一个二维数组,每个元素就是一个数组,每个元素保存一个像素信息。但是,我错了。data只是个一维数组!data的元素始终依次是red,green,blue,alpha,red,green,blue。。。一直重复到最后一个。所以,data的length始终是像素个数*4.而在循环data的时候,也是以4为步进。
了解了这个ImageData对象,那么,像素提取出来有什么用呢?我们可以把提出来的像素进行一些操作,然后放回canvas里,就能得到一幅新的图像——比如类似PS的滤镜效果。而把ImageData放入canvas的方法,则是putImageData.ctx.putImageData(imagedata,x,y)
注意,putImageData就没有width,height参数了,因为他们已经包含在了imagedata对象中。
3.canvas上下文是状态机
我们需要知道的第一件事就是,context 是一个状态机。你可以改变 context 的若干状态,而几乎所有的渲染操作,最终的效果与 context 本身的状态有关系。我们需要了解,改变 context 的属性并非是完全无代价的。我们可以通过适当地安排调用绘图 API 的顺序,降低 context 状态改变的频率。
4.分层 Canvas
分层 Canvas 的出发点是,动画中的每种元素(层),对渲染和动画的要求是不一样的。对很多游戏而言,主要角色变化的频率和幅度是很大的(他们通常都是走来走去,打打杀杀的),而背景变化的频率或幅度则相对较小(基本不变,或者缓慢变化,或者仅在某些时机变化)。很明显,我们需要很频繁地更新和重绘人物,但是对于背景,我们也许只需要绘制一次,也许只需要每隔 200ms 才重绘一次,绝对没有必要每 16ms 就重绘一次。
5.drawImage
// drawImage函数有三种函数原型:
// drawImage(image, dx, dy)
// drawImage(image, dx, dy, dw, dh)
// drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh)
// img 规定要使用的图像、画布或视频。可以用HTMLImageElement,HTMLCanvasElement或者HTMLVideoElement作为参数。
// sx 可选。开始剪切的 x 坐标位置。
// sy 可选。开始剪切的 y 坐标位置。
// sw 可选。被剪切图像的宽度。
// sh 可选。被剪切图像的高度。
// dx 在画布上放置图像的 x 坐标位置。
// dy 在画布上放置图像的 y 坐标位置。
// dw 可选。要使用的图像的宽度。(伸展或缩小图像)
// dh 可选。要使用的图像的高度。(伸展或缩小图像)
经过实际测试,drawImage的8参数比4参数性能差很多
我尝试绘制 104 次一块 320x180 的矩形区域,如果数据源是一张 320x180 的图片,花费了 40ms,而如果数据源是一张 800x800 图片中裁剪出来的 320x180 的区域,需要花费 70ms。
由于我们具备「把图片中的某一部分绘制到 Canvas 上」的能力,所以很多时候,我们会把多个游戏对象放在一张图片里面,以减少请求数量。这通常被称为「精灵图」。然而,这实际上存在着一些潜在的性能问题。我发现,使用 drawImage 绘制同样大小的区域,数据源是一张和绘制区域尺寸相仿的图片的情形,比起数据源是一张较大图片(我们只是把数据扣下来了而已)的情形,前者的开销要小一些。可以认为,两者相差的开销正是「裁剪」这一个操作的开销。
也许,我们可以先把待绘制的区域裁剪好,保存起来,这样每次绘制时就能轻松很多。这被称作离屏绘制。
drawImage 方法的第一个参数不仅可以接收 Image 对象,也可以接收另一个 Canvas 对象。而且,使用 Canvas 对象绘制的开销与使用 Image 对象的开销几乎完全一致。我们只需要实现将对象绘制在一个未插入页面的 Canvas 中,然后每一帧使用这个 Canvas 来绘制。
// 在离屏 canvas 上绘制
var canvasOffscreen = document.createElement('canvas');
canvasOffscreen.width = dw;
canvasOffscreen.height = dh;
canvasOffscreen.getContext('2d').drawImage(image, sx, sy, sw, sh, dx, dy, dw, dh);
// 在绘制每一帧的时候,绘制这个图形
context.drawImage(canvasOffscreen, x, y);
离屏绘制的好处远不止上述。有时候,游戏对象是多次调用 drawImage 绘制而成,或者根本不是图片,而是使用路径绘制出的矢量形状,那么离屏绘制还能帮你把这些操作简化为一次 drawImage 调用。
6.视野之外的绘制
有时候,Canvas 只是游戏世界的一个「窗口」,如果我们在每一帧中,都把整个世界全部画出来,势必就会有很多东西画到 Canvas 外面去了,同样调用了绘制 API,但是并没有任何效果。我们知道,判断对象是否在 Canvas 中会有额外的计算开销(比如需要对游戏角色的全局模型矩阵求逆,以分解出对象的世界坐标,这并不是一笔特别廉价的开销),而且也会增加代码的复杂程度,所以关键是,是否值得。
我做了一个实验,绘制一张 320x180 的图片 104 次,当我每次都绘制在 Canvas 内部时,消耗了 40ms,而每次都绘制在 Canvas 外时,仅消耗了 8ms。大家可以掂量一下,考虑到计算的开销与绘制的开销相差 2~3 个数量级,我认为通过计算来过滤掉哪些画布外的对象,仍然是很有必要的。
7.最后总结一下,在大部分情况下,需要遵循的「最佳实践」。
- 将渲染阶段的开销转嫁到计算阶段之上。
- 使用多个分层的 Canvas 绘制复杂场景。
- 不要频繁设置绘图上下文的 font 属性。
- 不在动画中使用 putImageData 方法。
- 通过计算和判断,避免无谓的绘制操作。
- 将固定的内容预先绘制在离屏 Canvas 上以提高性能。
- 使用 Worker 和拆分任务的方法避免复杂算法阻塞动画运行。
四、laya相关
1.在Browser.as的__init__
方法中
Render._mainCanvas = Render._mainCanvas || HTMLCanvas.create('2D');
2.在Render.as中
public static var _mainCanvas:HTMLCanvas;
public function Render(width:Number, height:Number) {
var style:* = _mainCanvas.source.style;
style.position = 'absolute';
style.top = style.left = "0px";
style.background = "#000000";
_mainCanvas.source.id = "layaCanvas";
var isWebGl:Boolean = Render.isWebGL;
_mainCanvas.source.width = width;
_mainCanvas.source.height = height;
isWebGl && WebGL.init(_mainCanvas, width, height);
Browser.container.appendChild(_mainCanvas.source);
…………
}
3.所以在项目中,使用Laya.Render.canvas可以引用到这个canvas
/** 渲染使用的原生画布引用。 */
public static function get canvas():* {
return _mainCanvas.source;
}
4.参考LayaAir加速引擎同样加速2D加速canvas更快还是加速webGL更快?
layaAir引擎同时支持WebGL和Canvas模式,还可以自动切换.webgl模式下会更快些,但是其实我们做项目两种模式都支持的话在推广上也更有利些,因为并不是所有的手机都支持webgl.两种模式都兼容的目的就是保证在手机上最起码有一种模式保证游戏是可以正常流畅运行的。