四、画布
当 HTML5 首次公布时,最令人兴奋的功能可能是新的canvas
元素——页面上的一个区域,您可以使用绘图上下文 API 中的各种命令在其上绘制位图图形。这意味着第一次有了用 JavaScript 创建动态图形的官方方法。
元素最初由苹果公司在 2004 年创建,作为 WebKit 的专有附加物。它后来被其他浏览器制造商采用,然后被 W3C 作为 HTML5 的一部分。今天,canvas
在现代浏览器中享有广泛的支持。
维持价
优秀
至少在最近的三个版本中,所有的现代浏览器都支持canvas
元素和本章涵盖的所有特性。
WHATWG 生活水平:http://www.whatwg.org/specs/web-apps/current-work/multipage/the-canvas-element.html
W3C 草案:http://www.w3.org/html/wg/drafts/2dcontext/html5_canvas_CR/
Canvas
绘图模式
如果你熟悉图形库,你可能听说过术语即时模式和保留模式来描述事物如何在屏幕上呈现。在即时模式呈现中,图形在 API 调用启动时呈现,绘图上下文不存储任何有关图形的内容。
在保留模式呈现中,对 API 的调用不会立即在屏幕上呈现。相反,API 的结果存储在由库维护的内部模型中,从而允许库在绘制所有内容时进行各种优化。
canvas
标签以即时模式呈现:只要调用 API,结果就会呈现在屏幕上,而canvas
不会存储任何关于刚刚绘制的内容的信息。如果你想重画同样的东西,你将不得不再次发出同样的命令。
Canvas
绘图上下文
元素可以像其他 HTML 元素一样通过 DOM 访问。然而,每个canvas
元素都公开了一个或多个绘图上下文,这些上下文可以用来以各种方式在canvas
上绘图。目前,标准中规定的并且浏览器支持的唯一上下文是二维(或 2d )上下文。
2d 上下文公开了一个令人印象深刻的 API,用于在canvas
元素上绘制直线、曲线、形状、文本等等。每个canvas
都有一个坐标系,原点(0,0)在左上角。2d 绘图上下文使用一个虚构的笔隐喻作为其基本的绘图功能,因此在canvas
上绘图的命令类似于“将笔移动到这些坐标,然后绘制这个东西”另外,画东西和填东西或者划东西是分开的概念,是由分开的命令来执行的。当您第一次绘制路径时,它不会显示在屏幕上,您必须应用填充或描边才能使它可见。这是为了提高效率,因为这样你可以画一个由许多部分组成的复杂路径,然后一次性描边或填充整个东西。
首先,画一条简单的线。语法非常简单,如清单 4-1 中的所示。
清单 4-1 。canvas
的基本绘图语法
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
width: 200px;
height: 200px;
}
</style>
</head>
<body>
<canvas id="myCanvas"></canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
这个例子在页面上有一个基本的canvas
元素。它使用 CSS 给出了canvas
的尺寸和边框,这样你就可以看到它了。该脚本获取对canvas
元素的引用,然后使用该引用获取绘图上下文。然后它使用moveTo
方法将笔移动到canvas
的左上角,然后指示上下文在(200,200)处画一条线(作为路径)到右下角。最后,它将描边样式设置为黑色,并指示上下文描边路径。
图 4-1 显示的结果有些出乎意料。
图 4-1 。清单 4-1 的结果
你期望直线从 0,0 到 200,200。。。事实上它做到了。一个canvas
元素的默认大小是 200 像素高 400 像素宽。您使用 CSS 来指定canvas
的尺寸,这只是让canvas
调整其纵横比,而不是实际减少其默认宽度。这给我们带来了一个重要的细节:在一个canvas
中,坐标系不一定与屏幕像素相对应。
这是画布的一个常见错误,它的发生是因为我们都被训练使用 CSS 来改变 HTML 元素的外观。但是,对于canvas
元素,您需要使用它的width
和height
属性来指定它的尺寸。清单 4-2 将这些添加到标记中。
清单 4-2 。指定画布元素的宽度和高度
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200"></canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
如您所见,这从 CSS 规则中移除了宽度和高度声明,而是使用width
和height
属性将尺寸直接应用于canvas
元素。然后它绘制并描边路径,结果如预期,如图图 4-2 所示。
图 4-2 。小小的胜利
如您所见,canvas
现在真正是 200 像素乘 200 像素,并且您的线条完全按照您的预期绘制。
canvas
标签不是自动结束的,所以结束标签是强制的。您可以在canvas
标签中包含替代内容,如果浏览器不支持canvas
元素,就会呈现替代内容。你可以很容易地扩展这个简单的例子,使其包含一些老浏览器的替代内容,如清单 4-3 所示。
清单 4-3 。不支持 Canvas 的浏览器的替代内容
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.moveTo(0, 0);
myContext.lineTo(200, 200);
myContext.strokeStyle = ’#000’;
myContext.stroke();
</script>
</body>
</html>
如你所见,我们都应该为小猫着想。
现在,您已经对 canvas 标签和绘图上下文有了一个基本的概念,接下来将深入研究可用的绘图命令。
基本绘图命令
Canvas
提供一组绘图命令,可用于构建复杂图形。大多数绘图命令用于构建路径。事实上,canvas
只包含一个形状原语的命令:矩形。你将不得不使用更简单的曲线组合来构建任何其他形状。
给定一张图纸Context
,基本曲线为:
Context.lineTo(x, y)
:从当前笔位置到指定坐标画一条线。Context.arc(x, y, radius, startAngle, endAngle, anticlockwise)
:以(x, y)
为圆心,以指定的半径画一个圆弧。startAngle
和endAngle
参数是以弧度表示的开始和结束角度,可选的anticlockwise
参数是一个布尔值,指示曲线是否应该逆时针绘制(默认为false
,因此默认情况下圆弧是顺时针绘制的)。Context.quadraticCurveTo(cp1x, cp1y, x, y)
:画一条二次曲线,从当前笔的位置开始,到坐标(x, y)
结束,控制点在(cp1x, cp1y)
。Context.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y)
:从当前笔的位置开始,到坐标(x, y)
结束,用(cp1x, cp1y)
指定控制点 1,用(cp2x, cp2y)
指定控制点 2,绘制一条贝塞尔曲线。Context.rect(x, y, width, height)
:从(x, y)
开始画一个指定宽度和高度的矩形。
使用两个简单的命令来声明路径:
Context.beginPath()
:开始新的路径定义。路径闭合前的所有曲线都将包含在路径中。Context.closePath()
:结束路径定义,从当前笔位置到路径起点画一条直线,关闭路径。
路径本身是看不见的。你必须告诉canvas
要么抚摸它们,要么填满它们:
Context.strokeStyle
:该属性定义了调用 stroke 方法时,在当前路径上描边的样式。该属性可以接受任何有效的 CSS 颜色字符串(例如,’red’
、’#000’
或’rgb(30, 50, 100)’
)、渐变对象或图案对象。Context.stroke()
:用Context.strokeStyle
中指定的样式对当前路径进行描边。Context.fillStyle
:该属性定义了调用fill
方法时填充到当前路径中的样式。该属性可以接受 CSS 颜色字符串、渐变对象或图案对象。Context.fill()
:用Context.fillStyle
中指定的样式填充当前路径。Context.lineWidth
:该属性定义应用于路径的笔画粗细。默认为 1 个单位。Context.lineCap
:该属性定义线条如何封顶。有效值包括:butt
:线端被切成方形,并精确地终止于指定的端点。这是默认值。round
:线条末端是圆形的,稍微超过指定的端点。square
:在线条末端加一个宽度等于线条宽度、高度为线条宽度一半的方框,使线条末端呈方形。
Context.lineJoin
:该属性定义连接线如何连接在一起。有效值包括:bevel
:接头是斜的。- 接头是斜接的。
round
:关节呈圆形。
清单 4-4 给出了lineCap
属性的示例。
清单 4-4 。线帽
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.lineWidth = 20;
// Set up an array of valid ending types.
var arrEndings = [’round’, ’square’, ’butt’];
var i = 0, arrEndingsLength = arrEndings.length;
for (i = 0; i < arrEndingsLength; i++){
myContext.lineCap = arrEndings[i];
myContext.beginPath();
myContext.moveTo(50 + (i * 50), 35);
myContext.lineTo(50 + (i * 50), 170);
myContext.stroke();
}
</script>
</body>
</html>
本例使用canvas
绘制粗线,以更好地说明线帽。和往常一样,首先获取目标canvas
的绘图上下文,并为绘图设置lineWidth
。然后利用一个行结束值的数组,遍历数组为每个值画一条线,如图图 4-3 所示。
图 4-3 。帆布线帽
你可以看到圆形和方形的帽线比实际的线端多一点。有时,如果你的划水需要特别紧,这会产生奇怪的效果。如果是这种情况,只需减少一点路径的长度,以解决额外的行程。
清单 4-5 展示了lineJoin
属性的各种值。
清单 4-5 。线条连接
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.lineWidth = 20;
// Set up an array of valid ending types.
var arrJoins = [’round’, ’miter’, ’bevel’];
var i = 0, arrJoinsLength = arrJoins.length;
for (i = 0; i < arrJoinsLength; i++){
myContext.lineJoin = arrJoins[i];
myContext.beginPath();
myContext.moveTo(55, 60 + (i * 60));
myContext.lineTo(95, 20 + (i * 60));
myContext.lineTo(135, 60 + (i * 60));
myContext.stroke();
}
</script>
</body>
</html>
与前面的例子相似,清单 4-5 使用一个有效连接值的数组来提供这个演示的结构。它循环遍历数组,并绘制出每个数组的一个例子,如图图 4-4 所示。
图 4-4 。画布线条连接
你可以看到round
接头在接头的钝角侧提供了一个稍微圆的盖子,而miter
接头在钝角侧稍微成方形。
清单 4-6 显示了在弧线上使用笔画属性。
清单 4-6 。随机圆发生器
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a loop that will draw a random circle on the canvas.
var cycles = 10,
i = 0;
for (i = 0; i < cycles; i++) {
var randX = getRandomIntegerBetween(50, 150);
var randY = getRandomIntegerBetween(50, 150);
var randRadius = getRandomIntegerBetween(10, 100);
myContext.beginPath();
myContext.arc(randX, randY, randRadius, 0, 6.3);
randStroke();
}
/**
* Returns a random integer between the specified minimum and maximum values.
* @param {number} min The lower boundary for the random number.
* @param {number} max The upper boundary for the random number.
* @return {number}
*/
function getRandomIntegerBetween(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
/**
* Returns a random color formatted as an rgb string.
* @return {string}
*/
function getRandRGB() {
var randRed = getRandomIntegerBetween(0, 255);
var randGreen = getRandomIntegerBetween(0, 255);
var randBlue = getRandomIntegerBetween(0, 255);
return ’rgb(’ + randRed + ’, ’ + randGreen + ’, ’ + randBlue + ’)’;
}
/**
* Performs a randomized stroke on the current path.
*/
function randStroke() {
myContext.lineWidth = getRandomIntegerBetween(1, 10);
myContext.strokeStyle = getRandRGB();
myContext.stroke();
}
</script>
</body>
</html>
在本例中,您在canvas
上创建了十个随机的圆,每个圆位于一个随机的位置,具有随机的半径、线宽和描边颜色。getRandomIntegerBetween
功能可以轻松获得您需要的号码。你也有一个randStroke
功能,用随机的宽度和颜色描绘当前路径。结果如图 4-5 所示。
图 4-5 。清单 4-6 的结果
我之前提到过canvas
也可以画矩形。命令很简单:
Context.fillRect(x, y, width, height)
:在指定坐标绘制一个指定宽度和高度的矩形,用当前填充样式填充。Context.strokeRect(x, y, width, height)
:在指定坐标处,用当前笔画样式绘制一个指定宽度和高度的矩形。Context.clearRect(x, y, width, height)
:清除任何其他图形的指定矩形区域。
清单 4-7 展示了矩形的绘制。
清单 4-7 。在画布上绘制矩形
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Set a stroke style and stroke a rectangle.
myContext.strokeStyle = ’green’;
myContext.strokeRect(30, 30, 50, 100);
// Set a fill style and fill a rectangle.
myContext.fillStyle = ’rgba(200, 100, 75, 0.5)’;
myContext.fillRect(20, 20, 50, 50);
// Clear a rectangle.
myContext.clearRect(25, 25, 25, 25);
</script>
</body>
</html>
在这个例子中,你没有做任何花哨的事情,只是描边、填充和清除矩形。结果就像你预期的那样(图 4-6 )。
图 4-6 。长方形——耶!
渐变和图案
你已经看到了canvas
如何设置不同的笔画和填充样式,我提到过这些样式可以是任何有效的 CSS 颜色字符串(例如green
或rgba(100, 100, 100, 0.3)
)。此外,canvas
可以定义用于填充和描边路径的gradient
和pattern
对象。
梯度
Canvas
可以创建线性和径向渐变:
Context.createLinearGradient
(x, y, x1, y1)
:创建一个线性渐变,从坐标(x, y)
开始,到坐标(x1, y1)
结束。返回一个可用作描边或填充样式的Gradient
对象。Context.createRadialGradient
(x, y, r, x1, y1, r1)
:创建由两个圆组成的径向渐变,第一个以(x, y)
为中心,半径r
,另一个以(x1, y1)
为中心,半径r1
。返回一个可用作笔画或填充样式的Gradient
对象。Gradient.addColorStop
(position, color)
:给Gradient
增加一个色标。位置参数必须介于 0 和 1 之间;它定义了色标渐变中的相对位置。您可以向特定的Gradient
添加任意数量的色标。
清单 4-8 显示了一个简单的三停止渐变被用来描边矩形。
清单 4-8 。三站坡度
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a gradient object and add color stops.
var myGradient = myContext.createLinearGradient(0, 0, 200, 200);
myGradient.addColorStop(0, ’#000’);
myGradient.addColorStop(0.6, ’green’);
myGradient.addColorStop(1, ’blue’);
// Set the stroke styles and stroke some rectangles.
myContext.strokeStyle = myGradient;
myContext.lineWidth = 20;
myContext.strokeRect(10, 10, 110, 110);
myContext.strokeRect(80, 80, 110, 110);
</script>
</body>
</html>
这个例子创建了一个线性的gradient
对象并添加了三个色标,然后用它作为两个矩形的描边样式。结果如图图 4-7 所示。
图 4-7 。线性梯度
模式
Canvas
还支持图案作为填充或描边样式的概念:
Context.createPattern
(Image, repeat)
:创建可用作填充或描边样式的Pattern
对象。Image
参数必须是任何有效的Image
(详见下一节“图像”)。repeat
参数指定图案图像如何重复。必须是下列之一:repeat
:水平和垂直平铺图像。repeat-x
:仅水平重复图像。repeat-y
:仅垂直重复图像。no-repeat
:完全不重复图像。
清单 4-9 展示了使用一个简单的图像作为模式。
清单 4-9 。创建图案
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/50/50’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
// Create a pattern with the image and use it as the fill style.
var myPattern = myContext.createPattern(myImage, ’repeat’);
myContext.fillStyle = myPattern;
myContext.fillRect(5, 5, 150, 150);
};
</script>
</body>
</html>
此示例创建一个新的image
元素,并将其 URL 设置为一个占位符图像服务。在继续之前,您必须等待图像完成加载,因此您为它附加了一个onload
事件处理程序,在其中您创建了pattern
,并将其用作矩形的填充样式。
结果看起来和你预期的一样可爱,如图图 4-8 所示。
图 4-8 。一只小猫作为图案
图像
元素还可以加载和操作图像。一旦图像被加载到canvas
中,您也可以使用绘图命令在其上绘图。
canvas
元素可以将这些来源用于图像:
- 一个
img
元素, - 一个视频元素,以及
- 另一个
canvas
元素。
Canvas
有一种绘制图像的方法,但它可以接受许多不同的参数,因此具有多种功能:
Context.drawImage
(CanvasImageSource, x, y)
:在坐标(x, y)
处从CanvasImageSource
处画出图像。Context.drawImage(CanvasImageSource, x, y, width, height)
:在坐标(x, y)
绘制图像,将图像缩放到指定的width
和height
。Context.drawImage(CanvasImageSource, sliceX, sliceY, sliceWidth, sliceHeight, x, y, width, height)
:用sliceWidth
和sliceHeight
从(sliceX, sliceY)
开始的矩形指定的图像区域进行切片,然后在x
、y
处的canvas
上绘制该切片,将切片缩放到指定的width
和height
。
清单 4-10 展示了drawImage
的基本功能。
清单 4-10 。在画布上绘制图像
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/150/150’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25);
};
</script>
</body>
</html>
在这个例子中,你所做的就是为一个占位符图像创建一个新的img
元素。一旦图像加载完毕,在你的canvas
上绘制,如图图 4-9 所示。
图 4-9 。画在画布上的图像
清单 4-11 演示了在画布上缩放图像。
清单 4-11 。用画布缩放图像
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/50/50’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25, 50, 150);
};
</script>
</body>
</html>
这个例子给你一个 100 像素乘 100 像素的占位符,但是当你在canvas
上画它的时候,你把它缩放到 50 像素×150 像素,如图图 4-10 所示。
图 4-10 。在画布中缩放图像
最后,清单 4-12 显示了在canvas
上切片一个更大的图像并缩放切片。
清单 4-12 。对canvas
上的图像进行切片和缩放
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/300/300’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
myContext.drawImage(myImage, 25, 25, 150, 150, 0, 0, 150, 50);
};
</script>
</body>
</html>
这里你加载了一个 300px × 300px 的占位符图像,但是从(25, 25)
开始只截取了它的 75px × 75px 部分。然后你把这个切片渲染到canvas
中,缩放到 150 像素×50 像素。结果相当失真,如图图 4-11 所示。
图 4-11 。可怜的小猫
保存 Canvas
内容
一旦你在canvas
上画了一幅画,你可能想以某种方式保存它。这将包括获取图像数据,并将其传输到服务器,从服务器可以重建和显示图像数据。canvas
API 确实提供了保存渲染位图的方法:
Context.toDataUrl(opt_type, opt_quality)
:将渲染后的位图转换成数据 URI。数据 URIs 是一种将数据直接嵌入网页的方式,定义在 RFC 2397 中,您可以在http://tools.ietf.org/html/rfc2397
阅读。有效类型包括image/png
(默认)、image/jpeg
和(对于 Chrome 和基于 Chrome 的浏览器,image/webp
)。如果类型是image/jpeg
或image/webp
,可以提供一个可选的 0 到 1 之间的第二参数来表示质量。此方法返回编码为数据 URI 的渲染位图,然后您可以将它传输回服务器,甚至在同一页面的其他地方使用。
请注意,如果您已经将一个来自不同于宿主页面的图像加载到canvas
中,或者如果您已经将一个图像从您的硬盘加载到canvas
中,这个方法将抛出一个安全错误。这样做是为了防止通过粗心或恶意脚本泄漏信息。
正文
除了绘图和图像,canvas
元素还可以呈现文本。文本呈现的方法和属性如下:
Context.fillText
(textString, x, y, opt_maxWidth)
:用当前填充样式填充从(x, y)
开始的canvas
上的textString
。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这样的方式来呈现文本,以使其适合指定的宽度(例如,如果可用,使用压缩的字体,使用较小的字体大小,等等)。).Context.measureText
(textString)
:测量使用当前样式渲染指定的textString
时的宽度。返回一个TextMetrics
对象,该对象的 width 属性包含值。Context.strokeText
(textString, x, y, opt_maxWidth)
:以当前笔画风格从(x, y)
开始在canvas
上划textString
。如果指定了可选的maxWidth
参数,并且呈现的文本将超过该宽度,浏览器将尝试以这样的方式来呈现文本,以使其适合指定的宽度(例如,如果可用,使用压缩的字体,使用较小的字体大小,等等)。).Context.font
:设置文本渲染的字体。允许任何有效的 CSS 字体字符串。Context.textAlign
:按指定对齐文本。有效值包括:left
:左对齐文本。right
:右对齐文本。center
:文本居中。start
:将文本从当前语言环境的起始端对齐(即,从左到右的语言靠左,从右到左的语言靠右)。这是默认值。end
:将文本在当前语言环境的结束端对齐。
Context.textBaseline
:为指定的文本设置基线。有效值包括:alphabetic
:对文本使用正常的字母基线。这是默认值。bottom
:基线是 em 方块的底部。hanging
:对文本使用悬挂基线。ideographic
:使用字符体的底部(假设它们突出于字母基线之下)。middle
:文本基线是 em 方块的中间。top
:文本基线是 em 方块的顶部。
清单 4-13 展示了在canvas
上绘制文本是多么容易。
清单 4-13 。在画布上呈现文本
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.strokeStyle = ’#000’;
myContext.strokeText(’Hello World’, 0, 40);
myContext.textAlign = ’center’;
myContext.fillStyle = ’rgba(200, 50, 25, 0.8)’;
myContext.fillText(’HTML5’, 100, 100);
</script>
</body>
</html>
这个例子对canvas
上的一些文本进行描边和填充。字体足够大,可以显示字母边缘的实际笔划,如图图 4-12 所示。
图 4-12 。呈现在画布上的文本
阴影
canvas
元素也可以根据其上绘制的元素投射阴影。这通常用于文本,但也适用于形状和路径。如果你已经熟悉 CSS 阴影,那么canvas
阴影的参数将会非常熟悉:
Context.shadowBlur
:虚化效果的大小。默认值为 0。Context.shadowColor
:阴影的颜色。可以是任何有效的 CSS 颜色字符串。默认为’rgba(0, 0, 0, 0)’
。Context.shadowOffsetX
:阴影的 x 轴偏移量。默认值为 0。Context.shadowOffsetY
:阴影的 y 轴偏移量。默认值为 0。
清单 4-14 演示了在一些文本上投射阴影。
清单 4-14 。投影
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Add some shadow!
myContext.shadowOffsetX = 2;
myContext.shadowOffsetY = 2;
myContext.shadowBlur = 2;
myContext.shadowColor = "rgba(0, 0, 0, 0.8)";
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.strokeStyle = ’#000’;
myContext.strokeText(’Hello World’, 0, 40);
myContext.textAlign = ’center’;
myContext.fillStyle = ’rgba(200, 50, 25, 0.8)’;
myContext.shadowOffsetX = 4;
myContext.shadowOffsetY = 4;
myContext.fillText(’HTML5’, 100, 100);
</script>
</body>
</html>
这个例子简单地给清单 4-13 中的代码添加了阴影。它增加了两个不同的阴影偏移量,一个很近,一个很远,如图图 4-13 所示。
图 4-13 。画布上呈现的阴影
保存绘图状态
canvas
API 提供了一种存储绘图上下文当前状态信息的方法。信息存储在一个堆栈中,您可以根据需要从堆栈中推入和拉出状态。可存储的绘图上下文属性如下:
globalAlpha
的当前值- 电流
strokeStyle
和fillStyle
lineCap
、lineJoin
、lineWidth
和miterLimit
中的当前线路设置shadowBlur
、shadowColor
、shadowOffsetX
和shadowOffsetY
中的当前阴影设置- 在
globalCompositeOperation
中设置的当前合成操作 - 当前剪辑路径
- 已应用于绘图上下文的任何转换
这些值共同构成了绘图状态。保存和恢复状态的方法很简单:
Context.save()
:拍摄当前绘图状态的快照,并将值保存在堆栈中。Context.restore()
:从堆栈中删除最近存储的绘图状态,并将其恢复到上下文中。
绘图状态保存在先进先出堆栈中。save 和 restore 方法是访问堆栈和其中存储的状态的仅有的两种方法。
清单 4-15 提供了一个有点做作的保存和恢复绘图状态的演示。
清单 4-15 。保存和恢复绘图状态
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="210">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create an array of colors to load into the stack.
var allTheColors = [’#ff0000’, ’#ff8800’, ’#ffff00’, ’#00ff00’, ’#0000ff’,
’#4b0082’, ’#8f00ff’];
// Load the colors and stroke style into the stack.
for (var i = 0; i < allTheColors.length; i++) {
myContext.strokeStyle = allTheColors[i];
myContext.lineWidth = 30;
myContext.save();
}
// Restore colors from the stack and draw.
for (var i = 0; i < 8; i++) {
myContext.restore();
myContext.beginPath();
myContext.moveTo(0, ((30 * i) + 15));
myContext.lineTo(200, ((30 * i) + 15));
myContext.stroke();
}
</script>
</body>
</html>
本示例以编程方式创建一组具有不同颜色和特定线宽的绘图状态。然后它一次恢复一个状态,并画一条线。
您会注意到每一行的 y 坐标都是基于循环索引的。每条线的描边宽度为 30 个单位:线上 15 个单位,线下 15 个单位。如果你只是从(0, 0)
到(200, 0)
画第一条线,然后描边,你不会看到笔画的前 15 个单元。将每一行下移 15 个单位可确保您将看到第一行和后续每一行的完整笔画宽度。
合成
到目前为止,在你所有的canvas
例子中,当你在canvas
上绘制多个项目时,它们只是一个在另一个之上。canvas
API 提供了在绘制时组合项目的能力,这使您能够进行一些相当复杂的操作。
每当你在canvas
上绘制一个新元素时,合成器会查看已经存在于canvas
上的内容。这个当前内容被称为目的地??。新内容被称为源。然后合成器根据当前活动的合成器参照目的地绘制源。
合成器是使用当前上下文的globalCompositeOperation
属性指定的。可用的合成器如下:
source-over
:在目标内容上绘制源内容。这是默认的合成器。source-atop
:源内容仅在与目标内容重叠的地方绘制。source-in
:仅在源内容和目标内容重叠的地方绘制源内容。其他一切都是透明的。source-out
:源内容仅在不与目标内容重叠的地方绘制。其他一切都是透明的。destination-over
:源内容绘制在目的内容的下面。destination-atop
:源内容仅保留在与目标内容重叠的地方。目标内容绘制在源内容的下方。其他一切都是透明的。destination-in
:源内容只保留在与目的内容重叠的地方。其他一切都是透明的。destination-out
:源内容只保存在不与目的内容重叠的地方。其他一切都是透明的。copy
:仅绘制目标内容。其他一切都是透明的。lighter
:当目标内容和源内容重叠时,通过将两个内容的值相加来确定颜色。xor
:目标内容正常呈现,除非它与源内容重叠,在这种情况下,两者都呈现为透明。
要指定合成器,只需将Context.
globalCompositeOperation
设置为所需的值:
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.globalCompositeOperation = ’lighter’;
清单 4-16 提供了一种查看不同合成器的方法。
清单 4-16 。画布合成器演示
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<br>
<select id="compositor">
<option value="source-over" selected>source-over</option>
<option value="destination-atop">destination-atop</option>
<option value="destination-in">destination-in</option>
<option value="destination-out">destination-out</option>
<option value="destination-over">destination-over</option>
<option value="source-atop">source-atop</option>
<option value="source-in">source-in</option>
<option value="source-out">source-out</option>
<option value="copy">copy</option>
<option value="lighter">lighter</option>
<option value="xor">xor</option>
</select>
<button id="toggle-triangle">Toggle Triangle</button>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Get references to the form elements.
var mySelector = document.getElementById(’compositor’);
var toggleTriangle = document.getElementById(’toggle-triangle’);
/**
* Draws the example shapes with the specified compositor.
*/
function drawExample() {
// First set the compositing to source-over so we can guarantee drawing the
// first shape.
myContext.globalCompositeOperation = ’source-over’;
myContext.clearRect(0, 0, 200, 200);
myContext.beginPath();
// Draw the circle first.
myContext.arc(60, 100, 40, 0, 7);
myContext.fillStyle = ’#ff0000’;
myContext.fill();
// Change the compositing to the chosen value.
myContext.globalCompositeOperation = mySelector.value;
// Draw a rectangle on top of the circle.
myContext.beginPath();
myContext.fillStyle = ’#0000ff’;
myContext.rect(60, 60, 80, 80);
myContext.fill();
}
/**
* Whether or not to show the triangle.
* @type {boolean}
*/
var showTriangle = false;
/**
* Shows or hides the triangle.
*/
function showHideTriangle() {
if (showTriangle) {
myContext.fillStyle = ’#00ff00’;
myContext.beginPath();
myContext.moveTo(40, 80);
myContext.lineTo(170, 100);
myContext.lineTo(40, 120);
myContext.lineTo(40, 80);
myContext.fill();
} else {
drawExample();
}
}
// Draw the example for the first time.
drawExample();
// Add a change event handler to the selector to redraw the example with the
// chosen compositor.
mySelector.addEventListener(’change’, function() {
showTriangle = false;
drawExample();
}, false);
// Add a click event handler to the toggle button to show or hide the triangle.
toggleTriangle.addEventListener(’click’, function() {
showTriangle = showTriangle ? false : true;
showHideTriangle();
}, false);
</script>
</body>
</html>
这个示例创建了一个简单的选择字段,其中包含所有可用的合成器可供选择。当您选择合成器时,形状将重新绘制。第一个形状(红色圆圈)将总是用source-over
绘制。第二个形状(蓝色方块)将使用新选择的合成器绘制。您可以打开和关闭绿色三角形,以查看它将如何与第一次合成的结果合成。
合成器适用于任何可以在canvas
上绘制的东西,甚至是图像,如清单 4-17 所示。
清单 4-17 。合成照片
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a new image element and fill it with a kitten.
var myImage = new Image();
myImage.src = ’http://www.placekitten.com/g/150/150’;
// We can’t do anything until the image has successfully loaded.
myImage.onload = function() {
// Create a simple gray linear gradient and set it to the fill style.
var myGradient = myContext.createLinearGradient(25, 25, 25, 175);
myGradient.addColorStop(0.1, ’#000’);
myGradient.addColorStop(1, ’rgba(200, 200, 200, 1)’);
myContext.fillStyle = myGradient;
// Draw a square that almost fills the region where the image will be rendered
// and fill it with the gradient.
myContext.beginPath();
myContext.rect(30, 30, 140, 140);
myContext.fill();
// Set the compositor to lighter.
myContext.globalCompositeOperation = ’lighter’;
// Draw the kitten.
myContext.drawImage(myImage, 25, 25);
};
</script>
</body>
</html>
本示例创建一个简单的线性渐变并将其用作正方形的填充样式,然后使用较亮的合成器在其上合成一只小猫的图像。结果示例如图 4-14 所示。
图 4-14 。将渐变与图像合成的结果
使用带有渐变、图案和图像的合成器,你可以用你的canvas
图创建一些非常复杂的效果。
剪辑
您可以将canvas
的绘图区域限制在您定义的任何闭合路径上。这被称为 ?? 削波。通过首先在canvas
上绘制一条路径,然后调用Context.clip()
方法来创建一个裁剪区域,这将把绘制限制在那个区域。您仍然可以描边和填充路径,也可以创建新的路径或其他绘图。可见性将被限制在剪辑区域。
有三种方法可以重置裁剪区域:
- 您可以定义一个包含整个
canvas
的路径,然后剪切到该路径。 - 可以用不同的剪辑区域恢复到以前的绘图状态。
- 您可以通过调整大小来重置整个
canvas
。
清单 4-18 演示了创建一个裁剪区域来限制绘图。
清单 4-18 。创建一个裁剪区域
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Create a square clipping area.
myContext.beginPath();
myContext.rect(50, 50, 50, 50);
myContext.clip();
// Draw a large circle in the canvas and fill it. Only the portion within
// the clipping area will be visible. myContext.beginPath();
myContext.arc(75, 75, 100, 0, 7);
myContext.fillStyle = ’red’;
myContext.fill();
</script>
</body>
</html>
这个简单的例子首先使用rect
方法创建一个正方形路径,然后将其设置为剪辑区域。然后它画了一个大圆并用红色填充,但是唯一可见的区域是在裁剪区域内,如图图 4-15 所示。
图 4-15 。剪裁的效果
转换
canvas
API 包含了一组改变图形在canvas
上呈现方式的方法:旋转它们,缩放它们,甚至任意改变,如反射或剪切。这些变化被称为变换??。设置转换后,将以指定的方式修改更多的图形。canvas
API 为一些常见的转换提供了一套简化方法:
Context.translate
(translateX, translateY)
:将canvas
的原点从当前位置移动到距离当前原点translateX
单位的新 x 位置和距离当前原点translateY
单位的新 y 位置。Context.rotate
(angle)
:以弧度为单位将canvas
绕原点旋转指定角度。Context.scale
(scaleX, scaleY)
:水平scaleX
和垂直scaleY
缩放canvas
单位。
此外,您可以使用变换方法指定任意变换矩阵:
-
Context.transform(scaleX, skewX, skewY, scaleY, translateX, translateY)
: Transform thecanvas
by applying a transformation matrix specified as:。
rotate
、translate
和scale
的简写方法都映射到转换矩阵,从而调用转换方法。比如Context.translate(translateX, translateY)
映射到Context.transform(1, 0, 0, 1, translateX, translateY)
,Context.scale(scaleX, scaleY)
映射到Context.transform(scaleX, 0, 0, scaleY, 0, 0)
。
注意如果你是线性代数爱好者,所有的
canvas
变换都是仿射变换。
关于canvas
转换,需要记住的重要一点是,它们会影响整个canvas
——一旦转换被实现,它会影响从该点开始绘制的所有内容。Canvas
当你应用两个不同的变换时,第二个变换的结果会基于第一个变换。如果您不仔细管理活动转换并根据需要重置它们,这可能会导致一些意想不到的结果。您可以通过以下三种方式之一重置转换:
- 指定一个称为“单位变换矩阵”的特殊变换,它对绘图没有影响。您可以使用变换方法:
Context.transform(1, 0, 0, 1, 0, 0)
指定该矩阵。 - 恢复先前保存的绘图状态,这会将转换设置为该状态的转换。
- 通过调整大小重置整个
canvas
。
在探索一些更复杂的转换之前,先看一些简单的例子。清单 4-19 演示了一个简单的translate
转换:
清单 4-19 。简单的平移变换
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
这个例子(将构成接下来几个例子的基础)创建了一个在canvas
的原点画一个正方形的简单方法。该函数用指定的颜色填充正方形(或者您可以传入任何有效的fillStyle
)。为了帮助跟踪原点,该函数还在原点处的正方形的角上创建一个小的后凹口。
首先,它在原点画一个正方形,并把它涂成红色。然后它翻译canvas
,并绘制一个蓝色方块。最后,它再次翻译canvas
并绘制一个绿色方块。结果如图图 4-16 所示。
图 4-16 。清单 4-18 的结果
如你所见,平移导致canvas
的原点按照指定移动。
接下来,清单 4-20 在这个例子的基础上应用了旋转和变换:
清单 4-20 。在平移上叠加旋转
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Rotate the canvas 45 degrees (about 0.785 radians).
myContext.rotate(0.785);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Rotate the canvas 45 degrees (about 0.785 radians).
myContext.rotate(0.785);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
它使用和以前一样的平移,但是在绘制新的方块之前也增加了一个旋转。结果如图 4-17 所示。
图 4-17 。旋转和平移
这里你可以看到同样的平移和旋转。你可以看到每个方块都绕着它的原点旋转。
最后,你可以看看清单 4-21 中的一些比例变换。
清单 4-21 。缩放和平移变换
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
/**
* Draws a 100x100 square at (0, 0) in the specified color. Indicates the origin
* corner with a small black square.
* @param {string} color A valid CSS color string.
*/
function drawSquare(color) {
myContext.fillStyle = color;
myContext.beginPath();
myContext.rect(0, 0, 100, 100);
myContext.fill();
myContext.fillStyle = ’#000’;
myContext.beginPath();
myContext.rect(0, 0, 5, 5);
myContext.fill();
}
// Draw a square, fill it with red.
drawSquare(’rgba(255, 0, 0, 0.5)’);
// Translate the canvas.
myContext.translate(20, 40);
// Scale the canvas.
myContext.scale(1, 1.5);
// Draw the same square again, fill it with blue.
drawSquare(’rgba(0, 0, 255, 0.5)’);
// Translate the canvas again.
myContext.translate(50, -20);
// Scale the canvas again.
myContext.scale(1.5, 1);
// Draw the same square again, fill it with green.
drawSquare(’rgba(0, 255, 0, 0.5)’);
</script>
</body>
</html>
同样,这个例子建立在清单 4-19 的之上,并使用相同的函数和翻译。这次在画第二个和第三个方块之前增加了一个缩放平移,如图图 4-18 所示。
图 4-18 。缩放和平移
如果仔细观察,您会发现蓝色正方形的原点标记根据您对其应用的缩放变换而略微拉长。如果你比较绿色方块和红色方块的原点标记,你会发现前者的大小是后者的两倍。
对于一个更实际的例子,考虑创建元素的动态反射。转换很容易,如清单 4-22 所示。
清单 4-22 。简单的文字反映
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Draw some text!
myContext.font = ’35px sans-serif’;
myContext.fillStyle = ’#000’;
myContext.fillText(’Hello World’, 10, 100);
// Set a reflection transform.
myContext.setTransform(1, 0, 0, -1, 0, 0);
// Set a slight scale transform.
myContext.scale(1, 1.2);
// Draw the text again with the transforms in place and a light gray fill style.
myContext.fillStyle = ’rgba(100, 100, 100, 0.4)’;
myContext.fillText(’Hello World’, 10, -85);
</script>
</body>
</html>
该示例绘制一些文本,然后对canvas
应用反射变换和缩放变换,然后用浅灰色重新绘制相同的文本。结果如图 4-19 中的所示。
图 4-19 。文本反射
您甚至可以使用渐变作为反射文本的填充样式,从而产生从上到下渐变的阴影
动画
API 没有为动画提供任何本地支持。它没有为其内容增加动画效果的方法,而且正如您所看到的,一旦内容被渲染,它也没有提供引用内容的方法。然而,canvas
提供的绘图工具是如此的低级和高效,以至于你可以用canvas
通过单独绘制每个动画帧来创建动画。
正如你将在第五章的的“动画计时”中看到的,大多数基于 JavaScript 的动画是在计时循环中完成的,用canvas
制作动画也没什么不同。事实上,为了简化动画示例,您将使用您在清单 5-5 中构建的 DrawCycle 构造函数。这将允许你创建一个绘制周期管理器,使用requestAnimationFrame
来最大化你的动画的效率。关于requestAnimationFrame
的详细信息,参见第五章中的“动画计时”。
要使用canvas
制作动画,您必须单独绘制动画的每一帧,清除帧之间的canvas
(如果需要,保存/恢复动画状态)。清单 4-23 说明了这个循环。
清单 4-23 。用画布制作动画
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="500">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script src="drawcycle.js"></script>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
// Set the stroke style.
myContext.strokeStyle = ’#000’;
// Create a new draw cycle object that we can use for our animation.
var myDrawCycle = new DrawCycle();
/**
* Draws a circle of specified radius at the specified coordinates.
* @param {number} x The x-coordinate of the center of the circle.
* @param {number} y The y-coordinate of the center of the circle.
* @param {number} rad The radius of the circle.
*/
function drawCircle(x, y, rad) {
myContext.beginPath();
myContext.moveTo(x + rad, y);
myContext.arc(x, y, rad, 0, 7);
myContext.stroke();
}
// Counter for the x-coordinate.
var x = 0;
/**
* Animates a circle from one corner of the canvas to another. Used as an
* animation function for the draw cycle object.
*/
function animateCircle() {
if (x < 500) {
myContext.clearRect(0, 0, 500, 500);
drawCircle(x, x, 10);
x++;
} else {
myDrawCycle.stopAnimation();
}
}
// Add the animation function to the draw cycle object.
myDrawCycle.addAnimation(animateCircle);
// Begin the animation.
myDrawCycle.startAnimation();
</script>
</body>
</html>
如上所述,在制作任何动画之前,您将加载您的绘制周期构造函数。有关绘制循环构造器如何工作的详细信息,请参见第五章。本示例创建一个新的绘制循环实例,并使用它来管理动画计时。
首先创建一个在指定位置画圆的函数。然后创建实际的动画函数,在每个循环中在新的位置画圆。然后,用绘制周期注册动画函数,并开始动画。这个例子简单地从canvas
的一个角到另一个角制作了一个圆的动画。
因为你必须在canvas
上分别绘制每一帧,而且因为动画帧的计时如此之快,你将很快碰到效率极限。要制作复杂的动画,您通常需要一个框架来帮助您管理效率,提供基本的动画功能,如运动、弹跳和摩擦的物理功能,并使创建和管理单个动画更加容易。
互动
由于canvas
是 DOM 中的一个元素,用户可以像其他 DOM 元素一样与它交互。一个canvas
元素将调度所有常见的 DOM 事件,如鼠标事件和触摸事件;您可以像附加任何其他元素一样附加事件处理程序。然而,canvas
不分派任何新的事件,也不提供访问任何内部绘制的东西的方法。
使用鼠标事件,创建一个允许用户在canvas
上绘图的应用是非常容易的,如清单 4-24 所示。
清单 4-24 。用鼠标在画布上绘图
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
cursor: crosshair;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="500" height="500">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
// Get the context we will be using for drawing.
var myCanvas = document.getElementById(’myCanvas’);
var myContext = myCanvas.getContext(’2d’);
myContext.strokeStyle = ’#000’;
// Whether or not the mouse button is being pressed.
var isMouseDown = false;
// Add a mousedown event listener that will set the isMouseDown flag to true,
// and move the pen to the new starting location.
myCanvas.addEventListener(’mousedown’, function(event) {
myContext.moveTo(event.clientX, event.clientY);
isMouseDown = true;
}, false);
// Add a mouseup event handler that will set the isMouseDown flag to false.
myCanvas.addEventListener(’mouseup’, function(event) {
isMouseDown = false;
}, false);
// Add a mousemove event handler that will draw a line to the current mouse
// coordinates.
myCanvas.addEventListener(’mousemove’, function(event) {
if (isMouseDown) {
window.requestAnimationFrame(function() {
myContext.lineTo(event.clientX, event.clientY);
myContext.stroke();
});
}
}, false);
</script>
</body>
</html>
要在canvas
上绘图,您希望当用户按下鼠标按钮时开始绘图,当用户松开鼠标按钮时停止绘图。因此,您需要 mousedown 和 mouseup 事件处理程序来设置一个标志,指示鼠标按钮的状态。mousedown 事件处理程序还将笔移动到新的位置,这样您就不会意外地从最后一个停止点到新的起点绘制一条线。然后,您需要一个 mousemove 事件处理程序,该处理程序绘制一条指向当前鼠标指针坐标的线,假设用户按住鼠标按钮。为了保持效率,使用requestAnimationFrame
方法;关于这种方法如何工作的详细信息,请参见第五章的中的“动画计时”。最后,使用 CSS 将光标变为canvas
元素的十字准线。
当你使用这个例子时,你会注意到它没有画在光标的正中间。相反,它绘制在光标的右下角附近。mousemove 事件处理程序从 DOM 传递给它的事件对象接收坐标,这些坐标有一点偏差,因为光标本身的大小是非零的。为了说明这一点,您所要做的就是将坐标偏移几个像素——准确地说,是光标宽度和高度的一半。新的事件处理程序如下所示:
// Add a mousemove event handler that will draw a line to the current mouse
// coordinates, with a slight offset.
myCanvas.addEventListener(’mousemove’, function(event) {
if (isMouseDown) {
window.requestAnimationFrame(function() {
myContext.lineTo(event.clientX - 7, event.clientY - 7);
myContext.stroke();
});
}
}, false);
现在,该示例将直接绘制在十字线下。
摘要
本章深入探讨了 HTML5 canvas
元素。它涵盖了所有重要功能,包括:
- 绘制形状和线条
- 绘图文本
- 对图像使用
canvas
元素 - 剪裁和遮罩
- 转换
- 带有
canvas
元素的基本动画 - 处理用户与
canvas
元素的交互
HTML5 canvas
元素为直接在网页上绘图提供了一个相当低级但灵活的 API。它在桌面和移动浏览器中也享有广泛的支持,这使它成为移动应用的一个很好的候选。
在第五章中,你会看到一些与 HTML5 相关的 JavaScript APIs,但它们不是规范的直接组成部分。
五、相关标准
HTML5 标准涵盖了大量内容,但它并不是 W3C 开发的唯一新的 web 技术。有一系列技术也是对网络平台的增强,但不属于 HTML5 的范畴。在这一章中,我将介绍一些更令人兴奋的新技术,特别关注为移动设备设计的技术。
地理位置〔??〕
维持价
优秀
所有现代浏览器都支持这些特性,并且在最近的三个版本中都有。
W3C 推荐:http://www.w3.org/TR/geolocation-API/
地理定位是确定托管浏览器的设备的物理位置的能力,通常是根据纬度和经度。地理定位对于移动设备非常重要,它与地图应用、提醒、紧急应答器甚至游戏(如 Ingress 参见https://www.ingress.com/
。
设备可以使用多种技术来确定您的位置:
- GPS 卫星 : 几乎所有现代智能手机和其他移动设备都有能够与全球定位系统卫星通信的收发器。
- 蜂窝塔 : 使用三角测量算法,可以根据蜂窝设备与蜂窝塔的通信来确定其位置(大致)。
- Wi-Fi 地图 : Wi-Fi 接入点往往非常固定且范围有限,因此只需带着支持 Wi-Fi 的设备四处行驶,就可以创建 Wi-Fi 接入点的“地图”。使用这样的地图,人们可以根据给定设备的范围内有哪些 Wi-Fi 接入点来确定该设备的大致位置。
- 蓝牙映射 : 类似 Wi-Fi 映射;最适合近距离地理定位。
- IP 地址映射 : 对于非移动设备,可以根据其外部 IP 地址确定其位置。一些公司提供 IP 地址映射服务。
所有这些方法都不精确,并且有它们自己的局限性,但是当一起使用时,它们可以提供设备的精确位置。但是,不能保证它们将返回设备的实际位置,或者以有用的准确度返回。
不过,当它们配合良好时,就有可能相当精确地定位设备。这就是为什么当你的 Wi-Fi 关闭时,大多数移动设备会警告你地理定位的准确性会受到影响。例如,当你关闭 iPhone 上的 Wi-Fi 无线电时,iOS 会警告你,你的定位精度会降低,如图图 5-1 所示。
图 5-1 。iOS 定位精度
隐私考虑
显然,地理定位有严重的隐私问题。定位和跟踪设备——以及携带设备的人——是一项强大的功能。因此,所有浏览器都实现了警告系统,通知用户他们的位置将被跟踪。当你的应用第一次访问地理定位 API 时,浏览器会通知用户并给他们阻止定位的选项。这些警告旨在引起注意,但因浏览器而异(图 5-2 )
图 5-2 。来自各种浏览器的地理位置警告
在所有的浏览器中,你的脚本都会暂停并等待用户对对话框做出响应。如果用户选择允许地理定位,脚本将继续。如果用户决定阻止地理定位,API 将抛出一个错误。
当您构建支持地理定位的应用时,考虑用户的隐私和安全需求非常重要:
- 您应该只在需要时请求位置数据。这对于隐私/安全和移动设备电池寿命都很重要,因为地理位置查询会激活移动设备中的多个无线电,因此会非常消耗电池。
- 您应该只请求满足您特定目的所需的足够的地理位置信息。
- 您应该仅将该信息用于特定目的,并且一旦达到目的,您应该从内存中清除地理位置数据。
- 您应该小心您的应用如何共享和传输地理位置数据。地理位置数据在任何网络上的任何传输都应该是安全的,以防止未经授权的访问。
- 如果您的应用需要将地理位置数据发送到服务器进行进一步处理,那么您应该更加小心服务器软件处理和存储数据的方式,牢记物理安全和法律后果。
这些似乎是显而易见的准则,事实上它们是处理任何敏感信息的基本准则。但是当你忙于编码时,很容易忽略这些简单的想法,所以一定要从一开始就把它们包含在你的工作中。
你也应该对你的用户透明,你的应用收集和处理地理位置数据。你应该告诉他们:
- 你收集什么数据;
- 你为什么收集它;
- 您是否共享或传输数据,以及您采取了哪些安全措施来保护该通信;和
- 您是否存储数据,以及您采取什么安全措施来保护存储。如果您确实存储了这些信息,您应该告诉他们您如何保护这些信息,以及用户如何从您的存储中删除他们的信息。
如果可能的话,您还应该为用户提供一种选择退出应用地理定位功能的方式。当然,有时这是不实际的,但是为用户提供一种控制这种特性的方法将对建立信任有很大帮助。
地理定位 API
地理定位 API 指定了一个新的navigator.geolocation
对象。这个对象有三个新方法来访问浏览器和托管设备 的地理定位功能。由于解析设备的位置可能需要未知的时间量(脚本将第一次暂停,并在继续之前等待用户响应许可对话框,然后必须查询各种定位方法,每个方法可能需要未知的时间量),这些方法是异步的,并提供了注册成功和错误回调函数的方法。
提示你可以使用承诺(在移动浏览器中得到很好的支持)来帮助简化异步动作的代码。参见附录 A 中关于承诺的部分。
navigator.geolocation.getCurrentPosition
(successCallback, errorCallback, PositionOptions)
:成功返回位置时调用successCallback
,出错时调用errorCallback
。当调用successCallback
时,它将接收一个Position
对象作为参数,当调用errorCallback
时,它将接收一个PositionError
对象作为参数。navigator.geolocation.watchPosition
(successCallback, errorCallback, PositionOptions)
:立即返回一个PositionWatch
标识,然后每次设备位置变化时调用successCallback
函数。如果尝试解析位置失败,则调用errorCallback
。当调用successCallback
时,它将接收一个Position
对象作为参数,当调用errorCallback
时,它将接收一个PositionError
对象作为参数。navigator.geolocation.clearWatch
(PositionWatch)
:停止由PositionWatch
值指定的watchPosition
呼叫。
此外,API 定义了三个新的对象模板:PositionOptions
对象、Position
对象和PositionError
对象。PositionOptions
对象为getCurrentPosition
和watchPosition
方法提供了一个接口来微调查询和结果,如下所示。
PositionOptions = {
// Specifies whether the query should return the most accurate location possible
boolean enableHighAccuracy,
// The number of milliseconds to wait for the device to return a location
number timeout,
// The number of milliseconds a cached value can be used.
number maximumAge
}
Position
对象定义了在成功解析主机设备的位置时将由getCurrentPosition
和watchPosition
方法返回的响应,如下所示。
Position = {
object coords : {
// The latitude in decimal degrees.
number latitude,
// The longitude in decimal degrees.
number longitude,
// The altitude in meters above nominal sea level.
number altitude,
// The accuracy of the latitude and longitude values, in meters.
number accuracy,
// The accuracy of the altitude value, in meters.
number altitudeAccuracy,
// The current heading of the device in degrees clockwise from true north.
number heading,
// The current ground speed, in meters per second.
number speed,
},
// The time when the location query was successfully created.
date timestamp
}
注意,根据浏览器对地理定位标准的实现和主机设备的能力,用于altitude
、accuracy
、altitudeAccuracy
、heading
和speed
的值可以作为null
返回。
PositionError
对象定义了如果用户拒绝允许地理定位,或者如果设备无法解析其位置时将返回的响应,如下所示。
PositionError = {
// The numeric code of the error (see table below).
number code,
// A human-readable error message.
string message
}
PositionError.code
的有效代码为整数,如表 5-1 所示。
表 5-1 。有效PositionError
代码
|
密码
|
常数
|
描述
|
| — | — | — |
| Zero | UNKNOWN_ERROR
| 由于未知错误,设备无法解析其位置。 |
| one | PERMISSION_DENIED
| 应用没有使用地理定位服务的权限,通常是由于用户拒绝权限。 |
| Two | POSITION_UNAVAILABLE
| 设备无法解析其位置,因为服务不可用。(通常在各种所需的无线电被停用时返回,例如当移动设备处于“飞行模式”时) |
| three | TIMEOUT
| 设备无法在PositionOptions.timeout
指定的超时限制内解析其位置。 |
使用这个 API 最简单的例子是做一个简单的位置查询 并显示所有返回的值,如清单 5-1 所示。
清单 5-1 。地理定位 API 的基本查询
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation
* services.
*/
function successCallback(position) {
console.log(’success’)
// Get a reference to the div we’re going to be manipulating.
var locationValues = document.getElementById(’locationValues’);
// Create a new unordered list that we can append new items to as we enumerate
// the coords object.
var myUl = document.createElement(’ul’);
// Enumerate the properties on the position.coords object, and create a list
// item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) {
var newItem = document.createElement(’li’);
newItem.innerHTML = geoValue + ’ : ’ + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement(’li’);
newItem.innerHTML = ’timestamp : ’ + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
}
/**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the
* geolocation services.
*/
function errorCallback(error) {
var myError = document.getElementById(’error’);
var myParagraph = document.createElement(’p’);
myParagraph.innerHTML = ’Error code ’ + error.code + ’\n’ + error.message;
myError.appendChild(myParagraph);
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
</script>
</body>
</html>
首先,这个例子创建了一个成功的回调函数,它枚举了Position
对象的属性。这样做的时候,它会将它们添加到一个无序列表中,这个列表会附加到 DOM 中,这样您就可以看到它了。错误回调的行为与此类似,只是它不生成列表,而是简单地更新一个段落的内容。
第一次运行这个示例时,您的浏览器应该提示您获得访问地理位置 API 的权限。第一次通过时,拒绝权限,这样您就可以看到错误情况是什么样子了。图 5-3 显示了在 Chrome 中生成的页面。
图 5-3 。Chrome 中列表 5-1 的错误情况
您可以看到错误处理程序是用错误代码 1 调用的。错误消息的实际文本因浏览器而异(例如,Internet Explorer 11 使用错误消息“此站点无权使用地理定位 API。”)但是错误码是一样的。
地理定位规范没有定义必须呈现给用户的权限模型,这就是为什么每个浏览器都有不同的做法。说明书只是说,
未经用户明确许可,用户代理不得向网站发送位置信息。用户代理必须通过用户界面获得许可,除非他们与用户有预先安排的信任关系,如下所述。用户界面必须包括文档 URI 的主机组件。通过用户界面获得的并且在当前浏览会话之后(即,在浏览上下文被导航到另一个 URL 的时间之后)被保留的那些权限必须是可撤销的,并且用户代理必须尊重被撤销的权限。
一些用户代理将具有预先安排的信任关系,不需要这样的用户界面。例如,当网站执行地理定位请求时,网络浏览器将呈现用户界面,而当使用位置信息来执行 E911 功能时,VOIP 电话可能不呈现任何用户界面。
因此,用户如何授予或拒绝地理位置许可、该决定被记住多长时间以及用户如何在以后改变主意,都取决于浏览器制造商来决定和实现。
例如,在 Internet Explorer 中,用户会看到一个弹出窗口,允许他们选择一些有趣的选项,如图 5-4 所示。
图 5-4 。Internet Explorer 中的地理位置许可选项 11
如果用户选择“允许一次”或“总是允许”,脚本将继续运行,浏览器将尝试解析客户端的位置。“允许一次”选项可能应该读作“允许此浏览会话”,因为该权限一直有效,直到用户关闭并重新启动浏览器。此时,重新访问页面会重新提示用户。选项“Always allow”如你所料地起作用:一旦用户选择了它,他们将不再被提示许可。“总是拒绝且不要告诉我”选项在此时以及用户每次访问该页面时拒绝权限。他们永远不会被重新提示权限,他们可以撤销此决定的唯一方法是打开 Windows 的 Internet 选项对话框,选择“隐私”选项卡,然后单击“位置”部分中的“清除站点”按钮,这将清除授予或拒绝所有站点的所有永久权限。
Firefox 呈现给用户的是完全不同的交互,如图图 5-5 所示。
图 5-5 。火狐 29 中的地理位置许可选项
如果用户选择“共享位置” ,脚本将继续运行,浏览器将尝试解析客户端的位置。但是,与 Internet Explorer 不同的是,此权限不适用于当前的浏览器会话,而仅适用于当前对网站的访问。重新加载页面将立即再次提示用户权限。用户不必重启浏览器。“总是共享位置”选项授予共享位置的永久权限,而“从不共享位置”则作为页面权限的永久拒绝。选择“不是现在”或单击弹出窗口右上角的×图标,或单击弹出窗口之外的任何地方,将关闭弹出窗口,而不授予或拒绝权限,并将使您的应用挂起。可以通过点击 URL 旁边的“目标”图标来重新打开弹出窗口,但这并不一定是显而易见的。这种行为是有意的;参见相关的 Bugzilla bug,https://bugzilla.mozilla.org/show_bug.cgi?id=675533
,获取解释。
只有在 iOS 上的 Safari Mobile 中,权限弹出窗口才是真正的模态弹出窗口,需要用户做出响应,除非他们做出选择,否则不能被取消。在所有其他情况下,用户可以忽略(在 Firefox 中完全忽略)弹出窗口,让您的脚本等待执行回调。更糟糕的是,处于这种未定义状态的时间不计入您可能用PositionOption.timeout
指定的任何超时——只有在用户授予权限并且浏览器开始尝试解析位置后,计时器才开始运行。
为了解决这个问题,您需要实现一个全局超时计时器,该计时器在脚本访问地理位置 API 时开始运行。如果用户授予(或拒绝)权限,我们的常规回调应该发生,这个全局计时器应该被取消。如果用户不授予(或拒绝)权限,全局计时器应该执行一个回调来做一些事情—例如,将浏览器重定向到一个错误页面,向用户解释他们需要做什么才能继续。或者,如果您的应用不需要 GPS,全局计时器回调应该取消成功和错误回调,您的应用可以继续。
很容易将这样的全局计时器添加到清单 5-1 的中,如清单 5-2 中的所示。
清单 5-2 。注册全局超时
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Geolocation Example</h1>
<div id="locationValues">
</div>
<div id="error">
</div>
<script>
// Create the variable that will hold the timer reference.
var globalTimeout = null;
/**
* The success callback function for getCurrentPosition.
* @param {Position} position The position object returned by the geolocation
* services.
*/
function successCallback(position) {
// Check the state of the global timeout. If it is null, the application has
// timed out and we should not continue. If it isn’t null, the timeout timer
// is still running, so we should cancel it and continue.
if (globalTimeout == null) {
return;
} else {
clearTimeout(globalTimeout);
}
// Get a reference to the div we’re going to be manipulating.
var locationValues = document.getElementById(’locationValues’);
// Create a new unordered list that we can append new items to as we enumerate
// the coords object.
var myUl = document.createElement(’ul’);
// Enumerate the properties on the position.coords object, and create a list
// item for each one. Append the list item to our unordered list.
for (var geoValue in position.coords) {
var newItem = document.createElement(’li’);
newItem.innerHTML = geoValue + ’ : ’ + position.coords[geoValue];
myUl.appendChild(newItem);
}
// Add the timestamp.
newItem = document.createElement(’li’);
newItem.innerHTML = ’timestamp : ’ + position.timestamp;
myUl.appendChild(newItem);
// Enumeration complete. Append myUl to the DOM.
locationValues.appendChild(myUl);
}
/**
* The error callback function for getCurrentPosition.
* @param {PositionError} error The position error object returned by the
* geolocation services.
*/
function errorCallback(error) {
// Check the state of the global timeout. If it is null, the application has
// timed out and we should not continue. If it isn’t null, the timeout timer
// is still running, so we should cancel it and continue.
if (globalTimeout == null) {
return;
} else {
clearTimeout(globalTimeout);
}
var myError = document.getElementById(’error’);
var myParagraph = document.createElement(’p’);
myParagraph.innerHTML = ’Error code ’ + error.code + ’\n’ + error.message;
myError.appendChild(myParagraph);
}
/**
* The callback to execute if the whole process times out, specifically in the
* situation where a user ignores the permissions pop-ups long enough.
*/
function globalTimeoutCallback() {
alert(’Error: GPS permission not given, exiting application.’);
globalTimeout = null;
}
// Call the geolocation services.
navigator.geolocation.getCurrentPosition(successCallback, errorCallback);
// Start the timer for the global timeout call.
globalTimeout = setTimeout(globalTimeoutCallback.bind(this), 5000);
</script>
</body>
</html>
这个例子做的第一件事是定义一个globalTimeout
变量,它将保存计时器的标识符,当它启动地理定位请求时,计时器将启动。接下来,注意在successCallback
和errorCallback
函数中,它检查了globalTimeout
变量的状态。如果变量是null
,全局超时已经过期,代码不应该继续执行那些函数。如果不是null
,定时器仍然是活动的,所以代码应该取消它并继续。
接下来,它提供了一个globalTimeoutCallback
函数 ,简单地向用户发出一条消息。在实际的应用中,您可能希望在这里做一些更有用的事情——例如,将用户重定向到另一个页面。代码还将globalTimeout
变量设置为null
,这样,如果任何一个回调以某种方式被执行,它们将不会继续通过初始的全局超时检查。
最后,它在调用地理位置 API 后立即设置运行 的计时器。计时器设定为五秒钟。当您加载此页面时,您将看到以下内容之一:
- 如果您已经永久拒绝了对该页面的地理位置许可,那么
errorCallback
将会执行并且全局计时器将会被取消。将不显示任何权限弹出窗口。 - 如果您已经永久地允许了对页面的地理位置许可,那么
successCallback
将会执行,全局计时器将会被取消。将不显示任何权限弹出窗口。 - 如果您尚未永久授予或拒绝权限,将显示权限弹出窗口。您可以选择在全局超时计时器过期之前授予或拒绝权限,在这种情况下,将执行适当的回调,全局计时器将被取消。或者您可以什么都不做,等待全局计时器超时。发生这种情况时,将会出现警告消息。
在任何情况下,都不能以编程方式强制用户选择权限。他们必须通过浏览器提供的对话框进行权限选择。
从用户交互的角度来看,这有点令人遗憾,因为这意味着你的应用会让浏览器显示一个你无法控制的通知。一些用户可能会发现这令人担忧,并选择拒绝许可,甚至完全关闭浏览器,永远不返回您的应用。如果你已经向你的用户公开了你的应用是如何收集和存储地理位置信息的,他们会为这种互动做好准备,并且更愿意给予许可,因为他们知道你的应用将如何处理这些数据。
动画定时
维持价
好的
所有现代的浏览器都支持这些特性,并且在最近的两个版本中都有。
W3C 候选推荐:http://www.w3.org/TR/animation-timing/
动画计时标准旨在帮助您构建基于 JavaScript 的可视化动画。如果您曾经尝试过使用 JavaScript 手工制作动画,您可能对绘制周期的简单模式很熟悉:
- 创建一个 draw 函数,负责增量地“绘制”动画项目:定位元素、更改元素属性、在一个
canvas
元素上绘制等等。每次调用这个函数时,它都会产生一个完整的动画“帧”,就像您正在手工绘制动画帧,然后在电影中放映一样。 - 每隔几毫秒调用一次 draw 函数。
JavaScript 绘制周期通常使用计时器实现,计时器每隔几毫秒调用一次绘制函数。在清单 5-3 中可以看到一个例子。
清单 5-3 。基于定时器的绘制周期的 JavaScript 实现
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
#target-element {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 100px;
left: 0px;
}
</style>
</head>
<body>
<h1>Simple Animation Example</h1>
<div id="target-element"></div>
<script>
// Get a reference to the element we want to move.
var targetEl = document.getElementById(’target-element’);
// Create a variable to keep track of its position.
var currentPosition = 0;
/**
* Draws the animation by updating the position on the target element and incrementing
* the position variable by 1.
*/
function draw() {
if (currentPosition > 500) {
// Stop the animation, otherwise it would run indefinitely.
clearInterval(animInterval);
} else {
// Update the element’s position.
targetEl.style.left = currentPosition++ + ’px’;
}
}
// Initiate the animation timer.
var animInterval = setInterval(draw, 17);
</script>
</body>
</html>
这个例子使用一个 JavaScript 定时器来更新页面上一个 div 的位置。更新之间的间隔是 17 毫秒。这不是一个任意的数字。大多数显示器的刷新频率是 60Hz,因此大多数浏览器都试图将屏幕重绘频率限制在 60Hz 以内。每秒 60 个周期大约是 17 毫秒。再快一点,你就会丢失“帧”
根据运行本示例所使用的浏览器和系统,这个动画可能看起来很流畅,也可能有点不流畅。那是因为这是一个动画的蛮力方法,没有考虑到浏览器如何重绘页面。它只是命令屏幕更新,浏览器必须尽最大努力。此外,不能保证动画更新之间的时间是 17 毫秒。setInterval
方法只是将更新添加到浏览器的 UI 队列中,如果浏览器忙于做其他事情(比如调整窗口大小,或者可能在后台获取和呈现其他内容),这很容易陷入困境,从而延迟屏幕呈现。
总的来说,这种方法扩展性不好。随着动画数量和复杂性的增加,以及它们所在的页面的复杂性和交互能力的增加,这些基于定时器的动画队列变得越来越低效。
动画计时规范通过提供一个新的计时器:requestAnimationFrame
解决了基于 JavaScript 的计时器的问题。从语法上来说,这个方法的使用类似于现有的 JavaScript 定时器方法setInterval
和setTimeout
。然而,在幕后,这种新方法与浏览器的屏幕管理算法紧密相连。因此,requestAnimationFrame
有一些重要的好处:
- 用
requestAnimationFrame
排队的动画被浏览器优化成一个回流/重画周期。 - 使用
requestAnimationFrame
排队的动画可以很好地播放来自其他来源的动画,比如 CSS 过渡。 - 浏览器将停止不可见的浏览器标签中的动画。这在移动设备上非常重要,因为密集的动画会快速消耗电池电量。
该规范在全局上下文中创建了两个新方法:
requestAnimationFrame(callback)
:请求将功能callback
作为下一个动画周期的一部分执行。回调将接收一个时间戳作为参数。像setTimeout
和setInterval
,requestAnimationFrame
返回一个可以用来停止循环的标识符。cancelAnimationFrame(identifier)
:取消标识符标识的动画帧请求。
将清单 5-3 更新为使用requestAnimationFrame
很容易,如清单 5-4 所示。
清单 5-4 。清单 5-3 使用requestAnimationFrame
重写
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
#target-element {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 100px;
left: 0px;
}
</style>
</head>
<body>
<h1>Simple requestAnimationFrame Example</h1>
<div id="target-element"></div>
<script>
var targetEl = document.getElementById(’target-element’);
var currentPosition = 0;
/**
* Updates the position on the target element, the increments the position
* counter by 1.
*/
function animateElement() {
// Stop the animation, otherwise it would run indefinitely.
if (currentPosition <= 500) {
requestAnimationFrame(animateElement);
}
// Update the element’s position.
targetEl.style.left = currentPosition++ + ’px’;
}
// Initiate the animation timer.
animateElement();
</script>
</body>
</html>
这个例子更新了animateElement
函数来使用requestAnimationFrame
。每次调用该方法时,它都会更新元素的位置并递增位置计数器。它还安排自己通过requestAnimationFrame
再次呼叫。一旦元素到达 500 像素的位置,动画就会停止。
使用动画计时构建绘制周期管理器也很容易。绘制周期管理器将允许你注册动画函数(如清单 5-4 中的animateElement
函数),并开始、停止和暂停绘制周期。清单 5-5 展示了一个简单的绘制周期管理器。
清单 5-5 。拉伸循环管理器
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
.animatable {
width: 100px;
height: 100px;
background-color: #ccc;
position: absolute;
top: 110px;
left: 0px;
}
#elementTwo {
top: 220px;
}
</style>
</head>
<body>
<h1>Simple Animation Framework Example</h1>
<div class="animatable" id="elementOne"></div>
<div class="animatable" id="elementTwo"></div>
<button id="startAnimation">Start Animation</button>
<button id="togglePause">Toggle Pause</button>
<button id="stopAnimation">Stop Animation</button>
<button id="registerOne">Register Animation One</button>
<button id="unregisterOne">Unregister Animation One</button>
<button id="registerTwo">Register Animation Two</button>
<button id="unregisterTwo">Unregister Animation Two</button>
<script>
// Get references to the elements we will be animating, and create position
// tracking variables for them.
var elementOne = document.getElementById(’elementOne’);
var elOnePosition = 0;
var elementTwo = document.getElementById(’elementTwo’);
var elTwoPosition = 0;
/**
* Animates Element One by incrementally updating its left position. Animation
* stops at 500px.
*/
function animateElementOne() {
if (elOnePosition <= 500) {
elementOne.style.left = elOnePosition++ + ’px’;
} else {
// Done animating, so remove this animation from the draw cycle manager.
myCycle.removeAnimation(animateElementOne);
// Reset the counter so we can animate again. The function can be
// re-registered and will work as before.
elOnePosition = 0;
}
}
/**
* Animates Element Two by incrementally updating its left position. Animation
* stops at 500px.
*/
function animateElementTwo() {
if (elTwoPosition <= 500) {
elementTwo.style.left = elTwoPosition++ + ’px’;
} else {
// Done animating, so remove this animation from the draw cycle manager.
myCycle.removeAnimation(animateElementTwo);
// Reset the counter so we can animate again. The function can be
// re-registered and will work as before.
elTwoPosition = 0;
}
}
/**
* Creates a draw cycle object that will repetitively draw animation functions.
* @constructor
* @returns {Object} A new draw cycle object.
*/
var DrawCycle = function() {
var newCycle = {
/**
* The identifier for the current animation frame loop.
* @type {Number}
*/
animationPointer: null,
/**
* @type {Boolean}
*/
isPaused: false,
/**
* The array of animation callbacks.
* @type {!Array.<Function>}
*/
arrCallbacks: [],
/**
* Starts the animation cycle.
*/
startAnimation: function() {
// Like other JavaScript timers, requestAnimationFrame sets the execution
// context of its callbacks to the global execution context (the window
// object). We need the execution context to be ’this’, the newCycle
// object we’re creating. By using the bind method (which exists on
// Function.prototype) we are able to override the default execution
// context with the one we need.
this.animationPointer = window.requestAnimationFrame(this.draw.bind(this));
},
/**
* Stops the animation cycle.
*/
stopAnimation: function() {
window.cancelAnimationFrame(this.animationPointer);
},
/**
* Pauses the invocation of the animation functions each draw cycle. If set
* to true, the animation functions will not be invoked. If set to false,
* the functions will be invoked.
* @type {Boolean}
*/
pauseAnimation: function(boolPause) {
this.isPaused = boolPause;
},
/**
* Adds an animation function to the draw cycle.
* @param {Function}
*/
addAnimation: function(callback) {
if (this.arrCallbacks.indexOf(callback) == -1) {
this.arrCallbacks.push(callback);
}
},
/**
* Removes an animation function from the draw cycle.
* @param {Function}
*/
removeAnimation: function(callback) {
var targetIndex = this.arrCallbacks.indexOf(callback);
if (targetIndex > -1) {
this.arrCallbacks.splice(targetIndex, 1);
}
},
/**
* Draws any registered animation functions (assuming they are not paused)
* and then kicks off another animation cycle.
* You should not need to call this method directly.
* @private
*/
draw: function() {
if (!this.isPaused) {
var i = 0, arrCallbacksLength = this.arrCallbacks.length;
for (i = 0; i < arrCallbacksLength; i++) {
this.arrCallbacks[i]();
}
}
this.startAnimation();
}
};
return newCycle;
};
// Create a new draw cycle object.
var myCycle = new DrawCycle();
// Register a callback for the Start Animation button that starts the animation
// cycle.
var startAnimation = document.getElementById(’startAnimation’);
startAnimation.addEventListener(’click’, function() {
myCycle.startAnimation();
}, false);
// Register a callback for the Pause Animation button that pauses/unpauses the
// animation cycle.
var pauseAnimation = document.getElementById(’togglePause’);
pauseAnimation.addEventListener(’click’, function() {
myCycle.pauseAnimation(!myCycle.isPaused);
}, false);
// Register a callback for the Stop Animation button that stops the animation
// cycle.
var stopAnimation = document.getElementById(’stopAnimation’);
stopAnimation.addEventListener(’click’, function() {
myCycle.stopAnimation();
}, false);
// Register a callback for the Register Animation One button that adds the
// animation function for element one to the draw cycle object.
var registerOne = document.getElementById(’registerOne’);
registerOne.addEventListener(’click’, function() {
myCycle.addAnimation(animateElementOne);
}, false);
// Register a callback for the Unregister Animation One button that removes the
// animation function for element one from the draw cycle object.
var unregisterOne = document.getElementById(’unregisterOne’);
unregisterOne.addEventListener(’click’, function() {
myCycle.removeAnimation(animateElementOne);
}, false);
// Register a callback for the Register Animation Two button that adds the
// animation function for element two to the draw cycle object.
var registerTwo = document.getElementById(’registerTwo’);
registerTwo.addEventListener(’click’, function() {
myCycle.addAnimation(animateElementTwo);
}, false);
// Register a callback for the Unregister Animation Two button that removes the
// animation function for element two from the draw cycle object.
var unregisterTwo = document.getElementById(’unregisterTwo’);
unregisterTwo.addEventListener(’click’, function() {
myCycle.removeAnimation(animateElementTwo);
}, false);
</script>
</body>
</html>
此示例创建一个构造函数,为您提供一个新的绘制周期对象,该对象为处理动画提供了一个简化的 API。主要的 API 方法有:
addAnimation(animationFunction)
:注册绘图循环的动画功能。每次绘制周期运行时,animationFunction
将被调用。removeAnimation(animationFunction):
取消绘制循环的动画功能。startAnimation()
:开始动画绘制循环。当被调用时,这个方法将使用对象的draw
方法作为回调来调用requestAnimationFrame
,从而启动一个单独的循环。该方法存储循环的标识符,以便以后需要时可以取消它。stopAnimation()
:停止动画绘制循环。当被调用时,这个方法用由startAnimation
存储的标识符调用cancelAnimationFrame
。pauseAnimation(boolPause)
:暂停或不暂停调用已注册的动画功能。绘制周期仍在运行,但没有调用任何动画功能。
使用这个动画 API 很简单:
- 使用构造函数创建绘制循环的新实例。
- 注册一个或多个动画回调,您希望在每个绘制周期调用它们。
- 开始动画循环。
当你调用startAnimation
时,它从浏览器请求一个动画帧,用draw
方法作为回调。浏览器在适当的时候调用draw
方法。draw
方法调用所有注册的动画函数(假设动画没有暂停),从而完成一个循环。然后它调用startAnimation
开始新的周期。
您可以根据需要动态添加新的动画功能;它们将在下一个绘制周期自动调用。您也可以根据需要移除动画功能。每个动画方法还会在完成时将其自身从绘制循环中移除,并重置其计数器。您可以在此时重新注册动画功能,动画将再次出现。注意,即使没有注册动画函数,绘制周期也会继续运行,所以当你移除最后一个动画函数时,你也应该确保调用stopAnimation
方法 。
选择器
维持价
优秀
所有现代浏览器都支持这些特性,并且在最近的四个版本中都有。
W3C 候选人推荐:http://www.w3.org/TR/selectors/
新的选择器标准提供了访问 DOM 中元素的新方法。以前,访问 DOM 中元素的主要方式是使用getElementById
方法、使用遍历或者两者的组合。有了新的选择器标准,您可以基于元素的 CSS 选择器直接访问元素。
选择器标准从 jQuery 等流行的 JavaScript 框架中得到启示,这些框架大量使用了选择器。如果您熟悉 jQuery、Prototype、Dojo 或任何其他使用选择器的 JavaScript 库,您会发现新的选择器 API 非常熟悉。
选择器标准定义了元素抽象类的两个新方法:
querySelector(cssSelectorList)
:返回对第一个元素的直接引用,该元素匹配指定的逗号分隔的cssSelectorList
中的所有 CSS 选择器。如果没有匹配,返回null
。querySelectorAll(cssSelectorList)
:返回一个NodeList
对象,该对象包含所有在逗号分隔的cssSelectorList
中指定的 CSS 选择器的匹配。如果没有匹配的元素,返回一个没有成员的NodeList
。
注意
NodeList
对象看起来很像数组,因为它们有可以通过数字索引访问的成员元素,以及一个反映成员数量的length
属性。然而,NodeList
对象直接从Object
原型继承,而不是从Array
原型继承,所以它们没有任何您可能会想到的数组方法(例如Array.forEach
)。
使用新的选择器 API,您可以轻松地获得对 DOM 元素的直接引用,而不需要大量遍历,也不需要在标记中添加只用于 JavaScript 选择器的 id。这可以帮助您保持标记和 JavaScript 代码的整洁。此外,您会发现自己经常在 JavaScript 和 CSS 中使用相同的选择器,因为您需要设计样式的元素通常也是脚本需要访问的元素。
我在书中的例子中一直使用选择器 API。下面是一些其他的例子,可以帮助说明这个 API 有多么强大:
- 属性选择器:
[attribute=value]
允许您根据 DOM 元素的指定属性来定位它们。这在选择分配了数据属性的元素时特别有用。您也可以使用模式匹配:[att^=’val’]
选择属性以字母“val”开头的元素[att$=’lue’]
选择其att
属性以字母“lue”结尾的元素[att*=’val’]
选择其att
属性包含字母“val”的元素
- 元素状态伪类允许您根据状态伪类定位 DOM 元素。特别有用的是
:enabled
(选择启用的表单域)、:disabled
(选择禁用的表单域)和:checked
(选择选中的复选框和单选按钮)。 - 否定伪类 :
not(selector)
以不匹配指定选择器的 DOM 元素为目标。 - 结构化伪类允许您根据 DOM 元素在 DOM 结构中的位置来定位它们。特别有用的是:
:nth-child(n)
选择其父元素的第 n 个子元素:nth-last-child(n)
选择其父元素的第 n 个子元素,从最后一个子元素开始向后计数:nth-of-type(n)
选择其类型的第 n 个兄弟元素:nth-last-of-type(n)
选择其类型的第 n 个兄弟元素,从最后一个兄弟元素开始向后计数:last-child
选择父元素的最后一个子元素:first-of-type
和:last-of-type
选择其类型的第一个或最后一个同级元素:only-child
选择其父元素的唯一子元素
因为querySelector
和querySelectorAll
方法是元素方法,所以可以在任何元素上使用它们。这将匹配选择器的搜索限制在该元素的后代,如清单 5-6 中的所示。
清单 5-6 。将选择器查询限制到包含元素
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<p class="selectme">This has the selectme class, but will not be clickable.</p>
<div class="noselect">
<p class="selectme">This has the selectme class, but will not be clickable.</p>
</div>
<div class="selectable">
<p class="selectme">This has the selectme class, and will be clickable.</p>
</div>
<script>
// Get a reference to the containing element we want to search.
var selectable = document.querySelector(’.selectable’);
// Get a reference to the paragraph.
var targetPar = selectable.querySelector(’.selectme’);
// Give the target paragraph an event handler for the click event.
targetPar.addEventListener(’click’, function() {
alert(’I was clicked!’);
});
</script>
</body>
</html>
这个简单的例子将所需选择器的搜索限制在指定的div
元素。这相当于使用选择器".selectable .selectme"
。这种技术对于选择事件目标的后代特别有用。
设备方向
维持价
穷
该 API 仅在具有必要硬件的设备上有用,这些设备通常是移动设备。移动浏览器对这个 API 的支持相当不错,除了 Internet Explorer Mobile,它根本没有实现这个 API。Internet Explorer 11 支持 API,Chrome 和 Firefox 也支持,但 Safari 不支持。
W3C 工作草案:http://www.w3.org/TR/orientation-event/
大多数移动和手持设备都包含灵敏的陀螺仪,使设备能够知道自己在空间中的方向。设备定向 API 为主机设备提供了一个标准 API,以便与基于浏览器的应用共享这些信息。
注意这个特殊的标准已经经历了频繁的变化,以响应行业反馈。因此,大多数浏览器制造商都没有全面实施。我在新闻发布时展示了该标准的当前版本,因为我认为这是一个值得报道的重要特性,尽管它还处于草案状态。
该标准指定了一组在窗口对象上触发的新事件,以及结果事件对象上的新数据属性。通过为这些事件注册事件侦听器,您可以访问这些数据属性。
compassneedscalibration
事件
根据标准,compassneedscalibration
事件在window
对象上触发,“当用户代理确定用于获取方位数据的罗盘需要校准时”然而,没有说明用户代理应该做什么来校准自己,或者如何与开发人员或最终用户沟通。出于这个原因,这个事件目前在 Firefox 中被禁用(参见https://bugzilla.mozilla.org/show_bug.cgi?id=738121
)。其他移动用户代理可能会触发这个事件,尽管我从未见过它发生。
像任何其他事件一样,您只需在window
对象上为它注册一个事件处理程序,如下面的代码片段所示:
window.addEventListener(’compassneedscalibration’, function(event) {
alert(’Your compass needs calibration. Wave your device in a figure-8 motion.’);
}, false);
deviceorientation
事件
根据该标准,deviceorientation
事件在window
对象上触发“每当发生显著的方向变化”,但是将“显著变化”的定义留给浏览器制造商。实际上,这个事件似乎会定期在window
对象上触发,甚至对于完全静止在桌子上的设备也是如此。
deviceorientation
事件的事件对象是一个DeviceOrientationEvent
对象,它具有以下属性:
DeviceOrientationEvent.alpha
:旋转的阿尔法角度。DeviceOrientationEvent.beta
:旋转的β角。DeviceOrientationEvent.gamma
:旋转的伽玛角度。
如果你熟悉欧拉角,那么阿尔法角、贝塔角和伽马角就是 Z-X’-Y "类型的泰特-布赖恩角。为了形象化这些角度,想象一个设备平放在桌子上,如图图 5-6 所示。
图 5-6 。平放在桌子上的装置
围绕 z 轴旋转将使 x 轴和 y 轴平移旋转量,如图图 5-7 所示。
图 5-7 。绕 z 轴旋转
由此产生的角度被称为阿尔法角度。
围绕 x 轴旋转将使 z 轴和 y 轴平移旋转量,如图图 5-8 所示。
图 5-8 。绕 x 轴旋转
由此产生的角度被称为β角。
最后,绕 y 轴旋转会使 x 轴和 z 轴平移旋转量,如图图 5-9 所示。
图 5-9 。绕 y 轴旋转
由此产生的角度被称为伽玛角度。
如何使用这些角度的明确示例是根据 gamma 和 beta 角度在屏幕上移动 DOM 元素。因为角度从正到负变化,您可以简单地将角度的舍入值添加到相关纵坐标的当前值:对于 x 纵坐标,您使用伽马角度,对于 y 纵坐标,您使用β角度。设备倾斜得越多,角度越大,坐标上的增量越大,元素移动得越快,如清单 5-7 所示。
清单 5-7 。在屏幕上移动球
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>The HTML5 Programmer’s Reference</title>
<style>
#container {
position: absolute;
top: 220px;
left: 50px;
width: 204px;
height: 204px;
border: 1px solid red;
}
#ball {
width: 10px;
height: 10px;
position: absolute;
top: 0px;
left: 0px;
background-color: red;
border-radius: 50%;
}
</style>
</head>
<body>
<h1>Device Orientation Demonstration</h1>
<ul>
<li>Alpha: <span id="alpha"></span></li>
<li>Beta: <span id="beta"></span></li>
<li>Gamma <span id="gamma"></span></li>
<li>y-pos <span id="ypos"></span></li>
<li>x-pos <span id="xpos"></span></li>
</ul>
<div id="container">
<div id="ball"></div>
</div>
<script>
// Get references to the various DOM elements we will be manipulating.
var alpha = document.getElementById(’alpha’);
var beta = document.getElementById(’beta’);
var gamma = document.getElementById(’gamma’);
var ypos = document.getElementById(’ypos’);
var xpos = document.getElementById(’xpos’);
var ball = document.getElementById(’ball’);
// Initialize x and y coordinates.
var yposit = 0;
var xposit = 0;
/**
* Handles a deviceorientation event on the window object.
* @param {DeviceOrientationEvent} event A standard device orientation event.
*/
function handleDeviceOrientation(event) {
// Update the DOM with the raw event data.
alpha.innerHTML = event.alpha;
beta.innerHTML = event.beta;
gamma.innerHTML = event.gamma;
// Use the raw data to get x and y coordinates for the ball.
xposit = getCoord(event.gamma, xposit);
xpos.innerHTML = xposit;
yposit = getCoord(event.beta, yposit);
ypos.innerHTML = yposit;
ball.style.top = yposit + ’px’;
ball.style.left = xposit + ’px’;
}
/**
* Increments a coordinate based on an angle from the device orientation event.
* @param {number} angle The orientation angle.
* @param {number} coord The coordinate to increment.
*/
function getCoord(angle, coord) {
// First, get a delta value from the angle.
var delta = Math.round(angle);
var tempVal = coord + delta;
// Limit the incremented value to between 0 and 194.
if (tempVal > 0) {
coord = Math.min(194, tempVal);
} else {
coord = 0;
}
return coord;
}
// Register the event handler.
window.addEventListener(’deviceorientation’, handleDeviceOrientation, false);
</script>
</body>
</html>
此示例在屏幕上显示原始事件数据,并使用该原始事件数据来确定元素在屏幕上的坐标。在这种情况下,它会限制元素的位置,使其保持在其包含元素的内部。
devicemotion
事件
devicemotion
事件定期在window
对象上触发,并产生一个DeviceMotionEvent
类型的事件。DeviceMotionEvent
有四个属性:acceleration
(其值代表设备沿 x、y 和 z 轴的加速度,单位为米/秒平方)、accelerationIncludingGravity
(包括地球重力影响的加速度值,如果有的话)、rotationRate
(α、β和γ角度的旋转速率,单位为度/秒)和interval
(该信息从硬件刷新的频率,单位为毫秒)。总的来说,DeviceMotionEvent
的模式如下所示:
object DeviceMotionEvent = {
object acceleration: {
number x,
number y,
number z
},
object accelerationIncludingGravity: {
number x,
number y,
number z
},
object rotationRate: {
number alpha,
number beta,
number gamma
}
number interval
}
你可以很容易地显示这些值,如清单 5-8 所示。
清单 5-8 。显示devicemotion
事件的值
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>The HTML5 Programmer’s Reference</title>
</head>
<body>
<h1>Device Motion Demonstration</h1>
<ul>
<li>acceleration:
<ul>
<li id="accX">x: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="accY">y: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="accZ">z: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
<li>accelerationIncludingGravity:
<ul>
<li id="aigX">x: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="aigY">y: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="aigZ">z: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
<li>rotationRate:
<ul>
<li id="rrAlpha">alpha: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="rrBeta">beta: <span class="current"></span>,<br>
max: <span class="max"></span></li>
<li id="rrGamma">gamma: <span class="current"></span>,<br>
max: <span class="max"></span></li>
</ul>
</li>
</ul>
<script>
// Create a data structure to store the references to the various DOM elements
// we will be manipulating, as well as associated maximum values. The structure
// also includes an interface method for processing incoming data and mapping
// it to the correct DOM elements.
var motionValues = {
acceleration : {
x : {
domCurr : document.querySelector(’#accX .current’),
domMax : document.querySelector(’#accX .max’),
maxVal : 0
},
y : {
domCurr : document.querySelector(’#accY .current’),
domMax : document.querySelector(’#accY .max’),
maxVal : 0
},
z : {
selector : ’#accZ’,
domCurr : document.querySelector(’#accZ .current’),
domMax : document.querySelector(’#accZ .max’),
maxVal : 0
}
},
accelerationIncludingGravity : {
x : {
domCurr : document.querySelector(’#aigX .current’),
domMax : document.querySelector(’#aigX .max’),
maxVal : 0
},
y : {
domCurr : document.querySelector(’#aigY .current’),
domMax : document.querySelector(’#aigY .max’),
maxVal : 0
},
z : {
selector : ’#accZ’,
domCurr : document.querySelector(’#aigZ .current’),
domMax : document.querySelector(’#aigZ .max’),
maxVal : 0
}
},
rotationRate : {
alpha : {
domCurr : document.querySelector(’#rrAlpha .current’),
domMax : document.querySelector(’#rrAlpha .max’),
maxVal : 0
},
beta : {
domCurr : document.querySelector(’#rrBeta .current’),
domMax : document.querySelector(’#rrBeta .max’),
maxVal : 0
},
gamma : {
selector : ’#accZ’,
domCurr : document.querySelector(’#rrGamma .current’),
domMax : document.querySelector(’#rrGamma .max’),
maxVal : 0
}
},
/**
* Processes an acceleration value object of a specific type. The values are
* enumerated and mapped to their associated DOM elements for display.
* @param {string} valueType The type of the value object, one of
* ’acceleration’, ’accelerationIncludingGravity’, or ’rotationRate’.
* @param {object} valueObject The object containing the acceleration data.
*/
processValues : function(valueType, valueObject) {
// First, get a reference to the subproperty of the motionValues object we
// will be manipulating.
var mvRef = this[valueType];
// Enumerate the valueObject and process each property.
for (property in valueObject) {
// Convenience references to the current values we’re working with.
var currMVRef = mvRef[property];
var currVal = valueObject[property];
// Update the DOM to display the current value.
currMVRef.domCurr.innerHTML = currVal;
// If the current value is larger than the last stored maximum value,
// update the stored max value to match and display it in the DOM.
if (currVal > currMVRef.maxVal) {
currMVRef.maxVal = currVal;
currMVRef.domMax.innerHTML = currVal;
}
}
}
};
/**
* Handles a devicemotion event on the window object.
* @param {DeviceMotionEvent} event A standard device motion event object.
*/
function handleDeviceMotion(event) {
motionValues.processValues(’acceleration’, event.acceleration);
motionValues.processValues(’accelerationIncludingGravity’,
event.accelerationIncludingGravity);
motionValues.processValues(’rotationRate’, event.rotationRate);
}
// Register the event handler.
window.addEventListener(’devicemotion’, handleDeviceMotion, false);
</script>
</body>
</html>
因为有许多值要显示,而且由于有了DeviceMotionEvent
模式,许多数据都是专门构造的,清单 5-8 通过创建一个具有相似模式的对象来开始这个例子。对于每个单独的属性,它存储一个显示其当前值的元素的 DOM 引用,一个显示所达到的最大值的元素的 DOM 引用,以及最大值本身。它还包括一个简单的接口方法,将DeviceMotionEvent
子属性映射到对象中相关联的子属性,并更新 DOM 以反映新信息。
为了使用这个例子,你需要移动你的设备。这些值是加速度的,加速度是速度的变化率(而速度是位置的变化率)。为了看到可观的价值,你需要相当快地移动你的设备。沿着不同的运动轴摇动你的设备就足够了。小心握紧你的手机,以免不小心把它扔了。将为您记录各个轴上的最大加速度值,以便您在移动设备后可以看到它们。您也可以旋转设备来查看旋转速度。
web GL(web GL)
维持价
好的
所有现代桌面浏览器至少在最近两个版本中都支持这些特性,只有 Internet Explorer 例外,它从版本 11 开始才支持这些特性。移动支持很差,因为 iOS 的移动 Safari 目前不支持 WebGL,尽管苹果已经承诺在 iOS 版本 8 中提供全面支持。
规格:http://www.khronos.org/webgl/
Web Graphics Library (WebGL) 是一个 API,用于在 HTML canvas
元素中绘制复杂的 2d 和 3d 图形。WebGL API 在给定的canvas
元素上呈现为绘图上下文,就像你在第四章中探索的标准绘图上下文一样。就像标准的canvas
绘图上下文一样,WebGL 绘图上下文可以通过一个扩展的 API 在 JavaScript 中访问。许多 WebGL 任务,如图像处理,被委托给主机系统的图形处理单元,而不是由系统的主 CPU 处理,因此提供了显著的速度提升。
与本书中涉及的大多数其他标准不同,WebGL 标准不是由 W3C 或 WHATWG 维护的。该标准由非营利技术联盟 Khronos Group 维护。这种语言本身基于 OpenGL 语言,是 2009 年 Mozilla 在 3d 渲染方面的实验的产物。WebGL 目前的稳定版本是 1.0.2。WebGL 2 的工作始于 2013 年。
初始化 WebGL 绘图上下文非常类似于在canvas
元素中初始化标准绘图上下文,如清单 5-9 所示。
清单 5-9 。初始化 WebGL 绘图上下文
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
var myCanvas = document.getElementById(’myCanvas’);
var myGlContext = myCanvas.getContext(’webgl’);
</script>
</body>
</html>
这个例子使用了getContext
方法,就像你在第四章中所做的一样。不同之处在于,它没有为 2d 绘图上下文提供参数’2d’
,而是提供了’webgl’
参数来指定 WebGL 绘图上下文。你可以很容易地将它扩展成一个函数,它甚至提供了一个根据需要初始化上下文的地方,如清单 5-10 所示。
清单 5-10 。WebGL 初始化函数
<!DOCTYPE html>
<html>
<head>
<title>The HTML5 Programmer’s Reference</title>
<style>
canvas {
border: 1px solid #000;
}
</style>
</head>
<body>
<canvas id="myCanvas" width="200" height="200">Did You Know: Every time
you use a browser that doesn’t support HTML5, somewhere a kitten
cries. Be nice to kittens, upgrade your browser!
</canvas>
<script>
/**
* Returns a WebGL drawing context on a specified canvas element. If opt_setup
* is provided and set to true, this method also performs some basic
* initialization on the context.
* @param {!Element} targetCanvas The reference to the desired canvas element.
* @param {boolean} opt_setup Whether or not to perform additional setup on the
* context.
* @return {Object} The WebGL drawing context, or null if WebGL is not supported
* or was otherwise unavailable.
*/
function initWebGLOnCanvas(targetCanvas, opt_setup) {
// If opt_setup was not specified, set it to false. In JavaScript, null and
// undefined are == to each other and nothing else, so:
if (opt_setup == null) {
opt_setup = false;
}
// Try and get the context.
var glContext = targetCanvas.getContext(’webgl’);
if (glContext == null) {
// Try falling back to an experimental version, works on some older browsers.
glContext = targetCanvas.getContext(’experimental-webgl’);
if (glContext == null) {
// We were unable to get a WebGL context. Provide a warning diagnostic
// message on the console, in case anyone is looking.
console.warn(’WebGL is not supported in this browser.’);
}
}
// If there is a context and setup was requested, do the setup.
if ((opt_setup === true) && (glContext != null)) {
// Set the clear color to black (rgba).
glContext.clearColor(0.0, 0.0, 0.0, 1.0);
// Initialize the depth function so that objects that are closer in
// perspective hide things that are further away.
glContext.depthFunc(glContext.EQUAL);
// Enable depth testing.
glContext.enable(glContext.DEPTH_TEST);
// Clear both the color and the depth buffer.
glContext.clear(glContext.COLOR_BUFFER_BIT|glContext.DEPTH_BUFFER_BIT);
}
return glContext;
}
var myCanvas = document.getElementById(’myCanvas’);
var myGLContext = initWebGLOnCanvas(myCanvas, true);
</script>
</body>
</html>
这个示例扩展了初始化函数,以检测获取 WebGL 上下文是否有问题,并回退到较旧浏览器上存在的较旧语法。如果根本无法获取上下文,就会向控制台输出一个警告。运行此示例将在浏览器中产生一个黑色方块。
在撰写本文时,Firefox 正在 WebGL 初始化过程中将大量 Windows、MacOS、Linux 和 Android 图形驱动程序列入黑名单。如果你有这些驱动,Firefox 默认情况下不会初始化一个 WebGL 绘图上下文。如果您在 Firefox 中运行这个示例,并且在控制台中看到警告消息,那么您的设置很可能被列入黑名单。有关如何覆盖该块的详细信息和说明,请参见https://wiki.mozilla.org/Blocklisting/Blocked_Graphics_Drivers
。另一种选择是使用具有不太脆弱的 WebGL 实现的浏览器(Chrome 的 WebGL 实现相当稳固)。
注意直到最近,iOS 上的 Safari Mobile 还不支持 WebGL。Safari 8.1 引入了完整的 WebGL 支持。
WebGL 是一种广泛的语言,完全涵盖它以及您可以用它做的一切超出了本书的范围。如果你想了解更多,请查看 Brian Danchilla 的《HTML5 的 WebGL 入门》。
挽救(saving 的简写)
维持价
优秀
所有现代浏览器都支持 SVG,并且至少支持最近的三个版本。
W3C 推荐:http://www.w3.org/TR/SVG11/
可缩放矢量图形(SVG) 是一种用于创建光栅图形、矢量图形和文本的图形格式。使用 SVG 可以轻松地对图形对象(在 SVG 中定义的和从外部文件导入的,如常规图像文件)进行分组和操作。
大多数图形格式(如可移植网络图形[PNG]格式)由二进制数据组成。SVG 图形是使用 XML 标记定义的,因此可以使用简单的文本编辑器轻松创建,就像网页一样。因为 SVG 图形是在 XML 标记中定义的,所以内容可以很容易地被扫描和索引。这使得 SVG 比其他图形格式更容易访问。
如上所述,SVG 标记可以像canvas
元素一样生成光栅图形。它还可以生成矢量图形,矢量图形是由包括点、线和曲线的数学函数定义的图形。光栅图形和矢量图形的主要区别在于矢量图形比光栅图形的缩放性更好。因此,SVG 定义的矢量图形是移动应用的最佳选择,因为它们在任何分辨率和大小下都保持清晰。
与 WebGL 一样,SVG 是一个大型标准,全面介绍它超出了本文的范围。
摘要
在这一章中,我探索了一些 JavaScript APIs,它们不是 HTML5 标准的一部分,但经常与 HTML5 特性结合使用。他们中的许多人也有令人兴奋的移动用途。
- 地理定位 API 让您的 JavaScript 应用能够访问移动设备的地理定位功能。您可以使用这个 API 来编写激动人心的新的位置感知移动应用。我还介绍了使用地理定位时重要的隐私考虑事项。
- 动画计时通过提供与浏览器窗口绘制直接相关的新计时器,提供了制作平滑动画的工具。
- 选择器 API 提供了一种使用 CSS 选择器轻松访问 DOM 元素的方法。
- 设备定位 API 让您的 JavaScript 应用能够访问移动设备的定位特性。您可以使用这个 API 来创建响应宿主移动设备移动的应用。
- 最后,我简要介绍了两项激动人心的新技术,WebGL 和 SVG。
使用这些具有 HTML5 特性的 API 将使您能够在各种设备上构建激动人心的动态应用。
在第六章中,你将深入 HTML5 的实际开发,包括从头开始构建一个完整的 HTML5 手机游戏。