动画与 Canvas 图形
是 HTML5 最受欢迎的新特性。这个元素会占据一块页面区域,让 JavaScript可以动态在上面绘制图片。
最早是苹果公司提出并准备用在控制面板中的,随着其他浏览器的迅速跟进,HTML5 将其纳入标准。目前所有主流浏览器都在某种程度上支持
元素。
自身提供了一些 API,但并非所有浏览器都支持这些API,其中包括支持基础绘图能力的 2D 上下文和被称为 WebGL 的 3D 上下文。支持的浏览器的最新版本现在都支持 2D 上下文和 WebGL。使用 requestAnimationFrame
早期定时动画
(function() {
function updateAnimations() {
doAnimation1();
doAnimation2();
// 其他任务
}
setInterval(updateAnimations, 100);
})();
时间间隔的问题
的流行和 HTML5 游戏的兴起,开发者发现 setInterval()和setTimeout()的不精确是个大问题。requestAnimationFrame
function updateProgress() {
var div = document.getElementById("status");
div.style.width = (parseInt(div.style.width, 10) + 5) + "%";
if (div.style.left != "100%") {
requestAnimationFrame(updateProgress);
}
}
requestAnimationFrame(updateProgress);
cancelAnimationFrame
let requestID = window.requestAnimationFrame(() => {
console.log('Repaint!');
});
window.cancelAnimationFrame(requestID);
通过 requestAnimationFrame 节流
function expensiveOperation() {
console.log('Invoked at', Date.now());
}
window.addEventListener('scroll', () => {
expensiveOperation();
});
function expensiveOperation() {
console.log('Invoked at', Date.now());
}
window.addEventListener('scroll', () => {
window.requestAnimationFrame(expensiveOperation);
});
let enqueued = false;
function expensiveOperation() {
console.log('Invoked at', Date.now());
enqueued = false;
}
window.addEventListener('scroll', () => {
if (!enqueued) {
enqueued = true;
window.requestAnimationFrame(expensiveOperation);
}
});
let enabled = true;
function expensiveOperation() {
console.log('Invoked at', Date.now());
}
window.addEventListener('scroll', () => {
if (enabled) {
enabled = false;
window.requestAnimationFrame(expensiveOperation);
window.setTimeout(() => enabled = true, 50);
}
});
基本的画布功能
元素时至少要设置其 width 和 height 属性,这样才能告诉浏览器在多大面积上绘图。出现在开始和结束标签之间的内容是后备数据,会在浏览器不支持
元素时显示。比如:
let drawing = document.getElementById("drawing");
// 确保浏览器支持
元素时,最好先测试一下 getContext()方法是否存在。有些浏览器对 HTML 规范中没有的元素会创建默认 HTML 元素对象。这就意味着即使 drawing 包含一个有效的元素引用,getContext()方法也未必存在。
元素上的图像。这个方法接收一个参数:要生成图像的 MIME 类型(与用来创建图形的上下文无关)。例如,要从画布上导出一张 PNG 格式的图片,可以这样做:let drawing = document.getElementById("drawing");
// 确保浏览器支持
2D 绘图上下文
元素的左上角。所有坐标值都相对于该点计算,因此 x 坐标向右增长,y 坐标向下增长。默认情况下,width 和 height 表示两个方向上像素的最大值。填充和描边
let drawing = document.getElementById("drawing");
// 确保浏览器支持
绘制矩形
let drawing = document.getElementById("drawing");
// 确保浏览器支持
let drawing = document.getElementById("drawing");
// 确保浏览器支持
let drawing = document.getElementById("drawing");
// 确保浏览器支持
绘制路径
let drawing = document.getElementById("drawing");
// 确保浏览器支持
if (context.isPointInPath(100, 100)) {
alert("Point (100, 100) is in the path.");
}
绘制文本
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
// 正常
context.font = "bold 14px Arial";
context.textAlign = "center";
context.textBaseline = "middle";
context.fillText("12", 100, 20);
// 与开头对齐
context.textAlign = "start";
context.fillText("12", 100, 40);
// 与末尾对齐
context.textAlign = "end";
context.fillText("12", 100, 60);
let fontSize = 100;
context.font = fontSize + "px Arial";
while(context.measureText("Hello world!").width > 140) {
fontSize--;
context.font = fontSize + "px Arial";
}
context.fillText("Hello world!", 10, 10);
context.fillText("Font size is " + fontSize + "px", 10, 50);
元素的浏览器不一定全部实现了相关的文本绘制 API。变换
m1_1 m1_2 dx
m2_1 m2_2 dy
0 0 1
let drawing = document.getElementById("drawing");
// 确保浏览器支持
let drawing = document.getElementById("drawing");
// 确保浏览器支持
context.fillStyle = "#ff0000";
context.save();
context.fillStyle = "#00ff00";
context.translate(100, 100);
context.save();
context.fillStyle = "#0000ff";
context.fillRect(0, 0, 100, 200); // 在(100, 100)绘制蓝色矩形
context.restore();
context.fillRect(10, 10, 100, 200); // 在(100, 100)绘制绿色矩形
context.restore();
context.fillRect(0, 0, 100, 200); // 在(0, 0)绘制红色矩形
绘制图像
![]()
元素,以及表示绘制目标的 x 和 y 坐标,结果是把图像绘制到指定位置。比如:let image = document.images[0];
context.drawImage(image, 10, 10);
context.drawImage(image, 50, 10, 20, 30);
context.drawImage(image, 0, 10, 50, 50, 0, 100, 40, 60);
![]()
元素,还可以是另一个
元素,这样就会把另一个画布的内容绘制到当前画布上。阴影
let context = drawing.getContext("2d");
// 设置阴影
context.shadowOffsetX = 5;
context.shadowOffsetY = 5;
context.shadowBlur = 4;
context.shadowColor = "rgba(0, 0, 0, 0.5)";
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
渐变
let gradient = context.createLinearGradient(30, 30, 70, 70);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
context.fillStyle = gradient;
context.fillRect(50, 50, 50, 50);
function createRectLinearGradient(context, x, y, width, height) {
return context.createLinearGradient(x, y, x+width, y+height);
}
let gradient = createRectLinearGradient(context, 30, 30, 50, 50);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
let gradient = context.createRadialGradient(55, 55, 10, 55, 55, 30);
gradient.addColorStop(0, "white");
gradient.addColorStop(1, "black");
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 绘制渐变矩形
context.fillStyle = gradient;
context.fillRect(30, 30, 50, 50);
图案
![]()
元素和一个表示该如何重复图像的字符串。第二个参数的值与 CSS 的background-repeat 属性是一样的,包括"repeat"、“repeat-x”、“repeat-y"和"no-repeat”。比如:let image = document.images[0],
pattern = context.createPattern(image, "repeat");
// 绘制矩形
context.fillStyle = pattern;
context.fillRect(10, 10, 150, 150);
元素或者另一个
元素。图像数据
let imageData = context.getImageData(10, 5, 50, 50);
let data = imageData.data,
red = data[0],
green = data[1],
blue = data[2],
alpha = data[3];
let drawing = document.getElementById("drawing");
// 确保浏览器支持
合成
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 修改全局透明度
context.globalAlpha = 0.5;
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
// 重置
context.globalAlpha = 0;
// 绘制红色矩形
context.fillStyle = "#ff0000";
context.fillRect(10, 10, 50, 50);
// 设置合成方式
context.globalCompositeOperation = "destination-over";
// 绘制蓝色矩形
context.fillStyle = "rgba(0,0,255,1)";
context.fillRect(30, 30, 50, 50);
WebGL
WebGL 上下文
let drawing = document.getElementById("drawing");
// 确保浏览器支持
WebGL 基础
let drawing = document.getElementById("drawing");
// 确保浏览器支持
Insert IconMargin [download]let drawing = document.getElementById("drawing"),
gl;
// 确保浏览器支持
常量
方法命名
准备绘图
。为此,要调用clearColor()方法并传入 4 个参数,分别表示红、绿、蓝和透明度值。每个参数必须是 0~1 范围内的值,表示各个组件在最终颜色的强度。比如:gl.clearColor(0, 0, 0, 1); // 黑色
gl.clear(gl.COLOR_BUFFER_BIT);
视口与坐标
区域。要改变视口,可以调用viewport()方法并传入视口相对于
元素的 x、y 坐标及宽度和高度。例如,以下代码表示要使用整个
元素:gl.viewport(0, 0, drawing.width,
drawing.height);
元素的左下角,**向上、向右增长可以用点(width–1, height–1)定义。
元素的一部分来绘图。比如下面的例子:// 视口是
缓冲区
let buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([0, 0.5, 1]), gl.STATIC_DRAW);
gl.deleteBuffer(buffer);
错误
let errorCode = gl.getError();
while (errorCode) {
console.log("Error occurred: " + errorCode);
errorCode = gl.getError();
}
着色器
编写着色器
// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章“Get started with WebGL—draw a square”
attribute vec2 aVertexPosition;
void main() {
gl_Position = vec4(aVertexPosition, 0.0, 1.0);
}
// OpenGL 着色器语言
// 着色器,摘自 Bartek Drozdz 的文章“Get started with WebGL—draw a square”
uniform vec4 uColor;
void main() {
gl_FragColor = uColor;
}
创建着色器程序
元素把着色器代码包含在网页中。如果 type 属性无效,则浏览器不会解析
的内容,但这并不妨碍读写其中的内容:
元素的内容:let vertexGlsl = document.getElementById("vertexShader").text,
fragmentGlsl = document.getElementById("fragmentShader").text;
let vertexShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexGlsl);
gl.compileShader(vertexShader);
let fragmentShader = gl.createShader(gl.FRAGMENT_SHADER);
gl.shaderSource(fragmentShader, fragmentGlsl);
gl.compileShader(fragmentShader);
let program = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);
gl.useProgram(program);
给着色器传值
let uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(uColor, [0, 0, 0, 1]);
let aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, itemSize, gl.FLOAT, false, 0, 0);
调试着色器和程序
if (!gl.getShaderParameter(vertexShader, gl.COMPILE_STATUS)) {
alert(gl.getShaderInfoLog(vertexShader));
}
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
alert(gl.getProgramInfoLog(program));
}
GLSL 100 升级到 GLSL 300
#version 300 es
绘图
// 假设已经使用本节前面的着色器清除了视口
// 定义 3 个顶点的 x 坐标和 y 坐标
let vertices = new Float32Array([ 0, 1, 1, -1, -1, -1 ]),
buffer = gl.createBuffer(),
vertexSetSize = 2,
vertexSetCount = vertices.length/vertexSetSize,
uColor,
aVertexPosition;
// 将数据放入缓冲区
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
// 给片段着色器传入颜色
uColor = gl.getUniformLocation(program, "uColor");
gl.uniform4fv(uColor, [ 0, 0, 0, 1 ]);
// 把顶点信息传给着色器
aVertexPosition = gl.getAttribLocation(program, "aVertexPosition");
gl.enableVertexAttribArray(aVertexPosition);
gl.vertexAttribPointer(aVertexPosition, vertexSetSize, gl.FLOAT, false, 0, 0);
// 绘制三角形
gl.drawArrays(gl.TRIANGLES, 0, vertexSetCount);
纹理
let image = new Image(),
texture;
image.src = "smile.gif";
image.onload = function() {
texture = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, texture);
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, true);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, image);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
// 除当前纹理
gl.bindTexture(gl.TEXTURE_2D, null);
}
元素加载的视频,甚至是别的
元素。视频同样受跨源限制。读取像素
let pixels = new Uint8Array(25*25);
gl.readPixels(0, 0, 25, 25, gl.RGBA, gl.UNSIGNED_BYTE, pixels);
let gl = drawing.getContext("webgl", { preserveDrawingBuffer: true; });
WebGL1 与 WebGL2
let ext = gl.getExtension('WEBGL_draw_buffers');
if (!ext) {
// 没有扩展的代码
} else {
ext.drawBuffersWEBGL([...])
}
gl.drawBuffers([...]);
小结
元素为 JavaScript 提供了动态创建图形的 API。这些图形需要使用特定上下文绘制,主要有两种。第一种是支持基本绘图操作的 2D 上下文:
标签。