清单 3. 使用平移 / 旋转变形方法绘制复杂位图
function drawPointCircle(){ var canvas = document.getElementById('canvas'); if (canvas.getContext){ var ctx = canvas.getContext('2d'); ctx.translate(150,150); // 将 canvas 的原点从 (0,0) 平移至(150,150) for (i=1;i<=2;i++){ // 绘制内外 2 层 if ((i % 2) == 1) {ctx.fillStyle = '#00f';} else{ ctx.fillStyle = '#f00'; } ctx.save(); // 保持开始绘制每一层时的状态一致 for (j=0;j<=i*6;j++){ // 每层生成点的数量 ctx.rotate(Math.PI/(3*i)); // 绕当前原点将坐标系顺时针旋转 Math.Pi/(3*i) 度 ctx.beginPath(); ctx.arc(0,20*i,5,0,Math.PI*2,true); ctx.fill(); // 使用 fillType 值填充每个点 } ctx.restore(); } } }
像素级绘图
像素级别的绘图操作是 canvas 绘图区别于 SVG,VML 等绘图技术的最为明显特征之一,渲染上下文提供了 createImageData, getImageData, 和 putImageData 三种方法来进行针对像素的操作,所基于的对象都是 imageData 对象。imageData 对象包含 width、height 和 data 三个属性,其中 data 包含了 width × height × 4 个像素值,之所以乘以 4,在于每个像素都有 RGB 值和透明度 alpha 值。
清单 4 中所示代码为上一节中示例图形增添了简单的颜色反转滤镜效果,通过调用 getImageData(x,y,width,height) 方法获取以(x,y)为左上坐标的矩形区域内所有像素,而后对所有像素的 RGB 值做取反操作,最后通过 putImageData(imageData, x, y)将修改后的像素值重新绘制到在 canvas 上。
图 4. 清单 4 所示示例图形
清单 4. 实现简单滤镜效果
function revertImage(){ var canvas = document.getElementById('canvas'); if (canvas.getContext){ var context = canvas.getContext('2d'); // 从指定的矩形区域获取 canvas 像素数组 var imgdata = context.getImageData(100, 100, 100, 100); var pixels = imgdata.data; // 遍历每个像素并对 RGB 值进行取反 for (var i=0, n=pixels.length; i<n; i+= 4){ pixels[i] = 255-pixels[i]; pixels[i+1] = 255-pixels[i+1]; pixels[i+2] = 255-pixels[i+2]; } // 在指定位置进行像素重绘 context.putImageData(imgdata, 100, 100); } }
实现动画效果
Canvas 并非为了制作动画而出现,自然没有动画制作中帧的概念。因而,使用定时器不断的重绘 canvas 画面成为了实现动画效果的通用解决方式。Javascript 中的 setInterval(code,millisec) 方法可以按照指定的时间间隔 millisec 来反复调用 code 所指向的函数或代码串,这样,通过将绘图函数作为第一个参数传给 setInterval 方法,在每次被调用的过程中移动画面中图形的位置,来最终达到一种动画的体验。需要注意的一点是,虽然 setinterval 方法的第二个参数允许开发人员对绘图函数的调用频率进行设定,但这始终都是一种最为理想的情况,由于这种绘图频率很大程度上取决于支持 canvas 的底层 JavaScript 引擎的渲染速度以及相应绘图函数的复杂性,因而实际运行的结果往往都是要慢于指定绘图频率的。
清单 5 显示了一个小弹力球动画效果,在球没有到达四周边界时,绘图方法不断的移动所绘小球的横纵坐标。并且,在每次重绘之前,都是用 clear 方法将之前的画面清除。
清单 5. 实现小弹力球动画
<script type="text/javascript"> var x=0,y=0,dx=2,dy=3,context2D; // 小球从(0,0)开始移动,横向步长为 2,纵向步长为 3 function draw(){ context2D.clearRect(0, 0, canvas.width, canvas.height); // 清除整个 canvas 画面 drawCircle(x, y); // 使用自定义的画圆方法,在当前(x,y)坐标出画一个圆 // 判断边界值,调整 dx/dy 以改变 x/y 坐标变化方向。 if (x + dx > canvas.width || x + dx < 0) dx = -dx; if (y + dy > canvas.height || y + dy < 0) dy = -dy; x += dx; y += dy; } window.onload = function (){ var canvas = document.getElementById('canvas'); context2D = canvas.getContext('2d'); setInterval(draw, 20); // 设置绘图周期为 20 毫秒 } </script>
提高可访问性
一款优秀的 Web 应用必须要做到的就是提供给用户很好的可访问性,这包括对鼠标,键盘以及快捷键等操作的响应,canvas 画面的本质仍是一个 DOM 节点,因而开发人员可以通过常规的方法来处理响应。这里,与基于 SVG 的绘图不同,由于 SVG 是一种基于 XML 的声明式的绘图方式,因而,SVG 中任何的图形都可以作为一个独立的 DOM 节点去接收并响应特定事件,而 canvas 由于其像素绘图的本质,则只可以在 canvas 元素节点去处理。
图 5 所示示例代码,当鼠标在 canvas 中移动时,鼠标当前相对于 canvas 中的横纵坐标将实时输出到上方提示信息区域;当用户在 canvas 中单击鼠标左键,将在相应位置创建一个蓝色小球,而后用户可以通过键盘上的左 / 右方向键对蓝色小球进行控制,使其进行横向的移动。示例代码如清单 6 所示。
图 5. 清单 6 所示示例展现
清单 6. 实现 canvas 对方向键和鼠标点击事件的响应
<script type="text/javascript"> var g_x,g_y; // 鼠标当前的坐标 var g_pointx, g_pointy; // 蓝色小球当前的坐标 var canvas; function drawCircle(x,y){ // 以鼠标当前位置为原点绘制一个蓝色小球 var ctx = canvas.getContext('2d'); ctx.clearRect(0,0,300,300); ctx.fillStyle = '#00f'; ctx.beginPath(); ctx.arc(x,y,20,0,Math.PI*2,true); ctx.fill(); g_pointx = x; g_pointy = y } function onMouseMove(evt) { // 获取鼠标在 canvas 中的坐标位置 if (evt.layerX || evt.layerX == 0) { // FireFox g_x = evt.layerX; g_y = evt.layerY; } document.getElementById("xinfo").innerHTML = g_x; document.getElementById("yinfo").innerHTML = g_y; } function onKeyPress(evt) { var dx = 3; // 横向平移步长 var kbinfo = document.getElementById("kbinfo"); if (evt.keyCode == 39){ kbinfo.innerHTML="right"; if (g_x<300-dx) drawCircle(g_pointx+dx,g_pointy); document.getElementById("xinfo").innerHTML = g_pointx; }else if (evt.keyCode == 37){ kbinfo.innerHTML = "left"; if (g_x>dx) drawCircle(g_pointx-dx,g_pointy); document.getElementById("xinfo").innerHTML = g_pointx; } } window.onload = function(){ canvas = document.getElementById('canvas'); // 增加 canvas 节点对鼠标单击,移动以及键盘事件的响应函数 canvas.addEventListener('click', function(evt){drawCircle(g_x, g_y);} , false); canvas.addEventListener('mousemove', onMouseMove, false); canvas.addEventListener('keypress', onKeyPress, false); canvas.focus(); // 获得焦点之后,才能够对键盘事件进行捕获 } </script>
这里我们对鼠标的移动,单击操作进行响应,在实际应用中可以视特定应用的需求,增加对鼠标摁下,松开或双击等更为丰富操作的响应,增强应用的可访问性。