Eloquent JavaScript 笔记 十六:Drawing on Canvas
来源:互联网 发布:sql select where and 编辑:程序博客网 时间:2024/06/04 19:09
在HTML中绘图有三种方式:
1. 使用DOM element,定制它的样式;
2. 使用SVG;
3. 使用canvas。
1. SVG
svg是特殊形式的xml文档,可以直接插入HTML文档中,当然,也可以单独写一个svg文件,作为<img>的src引入。看个例子:
<p>Normal HTML here.</p><svg xmlns="http://www.w3.org/2000/svg"> <circle r="50" cx="50" cy="50" fill="red"/> <rect x="120" y="5" width="90" height="90" stroke="blue" fill="none"/></svg>在HTML中嵌入svg,实际上是创建了一个特殊的element,如:<circle>, <rect>。我们也可以用 js 管理/修改 这些element,如:
var circle = document.querySelector("circle");circle.setAttribute("fill", "cyan");
2. The Canvas Element
<p>Before canvas.</p><canvas width="120" height="60"></canvas><p>After canvas.</p><script> var canvas = document.querySelector("canvas"); var context = canvas.getContext("2d"); context.fillStyle = "red"; context.fillRect(10, 10, 100, 50);</script>canvas是一种HTML element,它所拥有的属性和普通element差不多,例如:width、height等。
若要在canvas上绘图,首先需要获取context对象,所有的绘图函数都是context的属性。context有两种类型:2d 和 webgl。WebGL 提供3D绘图接口,它使用OpenGL接口。本章只讨论2d context。
3. Filling and Stroking
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.strokeStyle = "blue"; cx.strokeRect(5, 5, 50, 50); cx.lineWidth = 5; cx.strokeRect(135, 5, 50, 50);</script>fill 绘制实心的图形,stroke 绘制空心的图形。
相关方法:fillRect() , fillStyle(), strokeRect(), strokeStyle(), lineWidth 等等。
如果canvas没有指定width和height,默认大小是 300 x 150 。
4. Paths
path是一组线的集合。虽说是集合,但它不能存储在一个数组中,只能用一组函数临时生成。例如:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); for (var y = 10; y < 100; y += 10) { cx.moveTo(10, y); cx.lineTo(90, y); } cx.stroke();</script>如果一个path是封闭的,它可以被fill。如果path不是封闭的,fill时会自动添加一条线把path的起点和终点连接起来。
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(50, 10); cx.lineTo(10, 70); cx.lineTo(90, 70); cx.fill();</script>也可以调用closePath() 方法显式的封闭一个path,这个函数会真的在path中添加一条线,也就是说,调用cx.stroke() 能把它画出来。而对于上面的代码,没有调用 closePath(), 那么,调用 cx.stroke() 就不会画出最后那条封闭线。
5. Curves
画曲线主要有三种方法:二次曲线、贝塞尔曲线、弧线
要真正理解曲线的形状和参数的关系,需要一些数学功底,这个我不懂,暂且放一放。分别看看例子。
二次曲线:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(10, 90); // control=(60,10) goal=(90,90) cx.quadraticCurveTo(60, 10, 90, 90); cx.lineTo(60, 10); cx.closePath(); cx.stroke();</script>
贝塞尔曲线:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(10, 90); // control1=(10,10) control2=(90,10) goal=(50,90) cx.bezierCurveTo(10, 10, 90, 10, 50, 90); cx.lineTo(90, 10); cx.lineTo(10, 10); cx.closePath(); cx.stroke();</script>
弧线:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); cx.moveTo(10, 10); // control=(90,10) goal=(90,90) radius=20 cx.arcTo(90, 10, 90, 90, 20); cx.moveTo(10, 10); // control=(90,10) goal=(90,90) radius=80 cx.arcTo(90, 10, 90, 90, 80); cx.stroke();</script>
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.beginPath(); // center=(50,50) radius=40 angle=0 to 7 cx.arc(50, 50, 40, 0, 7); // center=(150,50) radius=40 angle=0 to ½π cx.arc(150, 50, 40, 0, 0.5 * Math.PI); cx.stroke();</script>注意,arcTo() 和 arc() 的参数不同。
6. Drawing a Pie Chart
用调查问卷的数据,画一张饼图。
数据:
var results = [ {name: "Satisfied", count: 1043, color: "lightblue"}, {name: "Neutral", count: 563, color: "lightgreen"}, {name: "Unsatisfied", count: 510, color: "pink"}, {name: "No comment", count: 175, color: "silver"}];算法:
每一块饼的弧度 = count / total * 2π
<canvas width="200" height="200"></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); var total = results.reduce(function(sum, choice) { return sum + choice.count; }, 0); // Start at the top var currentAngle = -0.5 * Math.PI; results.forEach(function(result) { var sliceAngle = (result.count / total) * 2 * Math.PI; cx.beginPath(); // center=100,100, radius=100 // from current angle, clockwise by slice's angle cx.arc(100, 100, 100, currentAngle, currentAngle + sliceAngle); currentAngle += sliceAngle; cx.lineTo(100, 100); cx.fillStyle = result.color; cx.fill(); });</script>
7. Text
如何绘制文字?fillText() , strokeText() 。
先看一个简单例子:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.font = "28px Georgia"; cx.fillStyle = "fuchsia"; cx.fillText("I can draw text, too!", 10, 50);</script>
8. Images
drawImage() 用来在canvas上绘制位图。drawImage() 的源,可以是<img>,或其它<canvas>,源element在DOM中不必显示。如:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); var img = document.createElement("img"); img.src = "img/hat.png"; img.addEventListener("load", function() { for (var x = 10; x < 200; x += 30) cx.drawImage(img, x, 10); });</script>
默认,drawImage() 按图片原始大小绘制。
drawImage() 的第2、3、4、5个参数指定源图片的区域,第6、7、8、9个参数指定canvas上的区域。
我们可以把一串图片放在一张图上,通过绘制图片的指定区域,实现动画效果。类似于使用css属性background-position实现动画。
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); var img = document.createElement("img"); img.src = "img/player.png"; var spriteW = 24, spriteH = 30; img.addEventListener("load", function() { var cycle = 0; setInterval(function() { cx.clearRect(0, 0, spriteW, spriteH); cx.drawImage(img, // source rectangle cycle * spriteW, 0, spriteW, spriteH, // destination rectangle 0, 0, spriteW, spriteH); cycle = (cycle + 1) % 8; }, 120); });</script>说明:
1. 一个小人儿的宽度是24,高度是30。
2. 在<img> 的 "load" 事件中绘制动画。
3. 每120ms绘制一幅图。
4. 绘制新的图片时,需要用 clearRect() 清除上一幅图,否则会叠加。
5. 只用了前8幅图,后面两个有其他用途,下面会讲。
9. Transformation
上面的动画中,小人儿从左向右跑动,如何让它从右向左跑呢? 可以做一组反转的图片,也可以使用canvas的变换函数。
canvas 有一组图形变换函数:scale(), rotate(), translate().
9.1. scale
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); cx.scale(3, .5); cx.beginPath(); cx.arc(50, 50, 40, 0, 7); cx.lineWidth = 3; cx.stroke();</script>scale() 会影响后面所有绘图行为。上面的代码画一个圆,scale() 把它的宽度x3,高度 /2 。
如果scale() 的参数是负值,会把图形沿坐标轴翻转,也就是把图形的x、y坐标乘以 -1。
例如,把上面的 cx.scale(3, 0.5) 改成 cx.scale(-3, 0.5) ,相当于把后面的arc() 的x坐标变成 (-50, 50)。看不见了。
9.2. transformations stack
调用多个变换函数会产生叠加效果,而且,叠加的顺序会影响绘图的结果。看下图:
左图先做 translate,后rotate。右图先rotate,后translate,最终得到的坐标系原点不同,从而导致以后的所有绘图行为的坐标都不一样。
9.3. 翻转图片
function flipHorizontally(context, around) { context.translate(around, 0); context.scale(-1, 1); context.translate(-around, 0);}通过三次transform,可以翻转上面小人儿的奔跑方向。
原理:
第一张绿色三角形是原始图片。
第二张做了translate。
第三张做 scale(-1, 1) 镜像。
第四张translate,回到原始位置。
为什么要有around这个参数呢? 看下面翻转小人儿的代码:
<canvas></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); var img = document.createElement("img"); img.src = "img/player.png"; var spriteW = 24, spriteH = 30; img.addEventListener("load", function() { flipHorizontally(cx, 100 + spriteW / 2); cx.drawImage(img, 0, 0, spriteW, spriteH, 100, 0, spriteW, spriteH); });</script>这个around的参数,就是原始图片中心点的x坐标。(还是没想明白 。。。)
10. Storing and Clearing Transformations
变换函数会影响后面所有的绘图行为,有时候我们需要清除它的影响,或者,在循环中反复使用一组transformations。
为此,canvas 提供了两个函数:context.save(), context.restore()。 实际上,它使用了一个堆栈,用于保存当前所有的context配置,不仅限于transformations。save 相当于push,restore相当于pop。
看一个例子,在循环中绘制如下图形(树枝?树根?):
<canvas width="600" height="300"></canvas><script> var cx = document.querySelector("canvas").getContext("2d"); function branch(length, angle, scale) { cx.fillRect(0, 0, 1, length); if (length < 8) return; cx.save(); cx.translate(0, length); cx.rotate(-angle); branch(length * scale, angle, scale); cx.rotate(2 * angle); branch(length * scale, angle, scale); cx.restore(); } cx.translate(300, 0); branch(60, 0.5, 0.8);</script>注意,每一次循环都需要save和restore,否则,在循环中多次transform,变换会一次次叠加,鬼才知道会变换成什么样子。
11. Back to The Game
把上一章的DOMDisplay改成CanvasDisplay,使用canvas显示场景和sprites。
相当长的代码,留待以后再看吧。
12. Choosing a Graphics Interface
三种方式:
DOM: 简单,文档结构清晰,可以自动布局。
SVG: 作为矢量图
canvas: 大量小元素的绘制、刷新。适合游戏?
13. Exercise: Shapes
绘制梯形:
var cx = document.querySelector("canvas").getContext("2d"); function parallelogram(x, y) { cx.beginPath(); cx.moveTo(x, y); cx.lineTo(x + 50, y); cx.lineTo(x + 70, y + 50); cx.lineTo(x - 20, y + 50); cx.closePath(); cx.stroke(); } parallelogram(30, 30);绘制diamond:
function diamond(x, y) { cx.translate(x + 30, y + 30); cx.rotate(Math.PI / 4); cx.fillStyle = "red"; cx.fillRect(-30, -30, 60, 60); cx.resetTransform(); } diamond(140, 30);注意,一定先做 transform (translate, rotate) ,真正的绘图函数写在后面。 resetTransform() 会清除掉所以的transform配置。
绘制折线:
function zigzag(x, y) { cx.beginPath(); cx.moveTo(x, y); for (var i = 0; i < 8; i++) { cx.lineTo(x + 80, y + i * 8 + 4); cx.lineTo(x, y + i * 8 + 8); } cx.stroke(); } zigzag(240, 20);绘制螺旋线:
function spiral(x, y) { var radius = 50, xCenter = x + radius, yCenter = y + radius; cx.beginPath(); cx.moveTo(xCenter, yCenter); for (var i = 0; i < 300; i++) { var angle = i * Math.PI / 30; var dist = radius * i / 300; cx.lineTo(xCenter + Math.cos(angle) * dist, yCenter + Math.sin(angle) * dist); } cx.stroke(); } spiral(340, 20);绘制星星:
function star(x, y) { var radius = 50, xCenter = x + radius, yCenter = y + radius; cx.beginPath(); cx.moveTo(xCenter + radius, yCenter); for (var i = 1; i <= 8; i++) { var angle = i * Math.PI / 4; cx.quadraticCurveTo(xCenter, yCenter, xCenter + Math.cos(angle) * radius, yCenter + Math.sin(angle) * radius); } cx.fillStyle = "gold"; cx.fill(); } star(440, 20);
数学没学好,理解起来很困难,不仔细看它了。
14. Exercise: The Pie Chart
给前面画的饼图加上文字:
<script> var results = [ {name: "Satisfied", count: 1043, color: "lightblue"}, {name: "Neutral", count: 563, color: "lightgreen"}, {name: "Unsatisfied", count: 510, color: "pink"}, {name: "No comment", count: 175, color: "silver"} ]; var cx = document.querySelector("canvas").getContext("2d"); var total = results.reduce(function(sum, choice) { return sum + choice.count; }, 0); var currentAngle = -0.5 * Math.PI; var centerX = 300, centerY = 150; results.forEach(function(result) { var sliceAngle = (result.count / total) * 2 * Math.PI; cx.beginPath(); cx.arc(centerX, centerY, 100, currentAngle, currentAngle + sliceAngle); var middleAngle = currentAngle + 0.5 * sliceAngle; var textX = Math.cos(middleAngle) * 120 + centerX; var textY = Math.sin(middleAngle) * 120 + centerY; cx.textBaseLine = "middle"; if (Math.cos(middleAngle) > 0) cx.textAlign = "left"; else cx.textAlign = "right"; cx.font = "15px sans-serif"; cx.fillStyle = "black"; cx.fillText(result.name, textX, textY); currentAngle += sliceAngle; cx.lineTo(centerX, centerY); cx.fillStyle = result.color; cx.fill(); });</script>
15. Exercise: A Bouncing Ball
<script> var cx = document.querySelector("canvas").getContext("2d"); var lastTime = null; function frame(time) { if (lastTime != null) updateAnimation(Math.min(100, time - lastTime) / 1000); lastTime = time; requestAnimationFrame(frame); } requestAnimationFrame(frame); var x = 100, y = 300; var radius = 10; var speedX = 100, speedY = 60; function updateAnimation(step) { cx.clearRect(0, 0, 400, 400); cx.strokeStyle = "blue"; cx.lineWidth = 4; cx.strokeRect(25, 25, 350, 350); x += step * speedX; y += step * speedY; if (x < 25 + radius || x > 375 - radius) speedX = -speedX; if (y < 25 + radius || y > 375 - radius) speedY = -speedY; cx.fillStyle = "red"; cx.beginPath(); cx.arc(x, y, radius, 0, 7); cx.fill(); }</script>
- Eloquent JavaScript 笔记 十六:Drawing on Canvas
- Python学习笔记(4)Drawing on Canvas
- 《Eloquent JavaScript》笔记--函数;
- Eloquent JavaScript 笔记 三: Functions
- Eloquent JavaScript 笔记 十: Modules
- Eloquent JavaScript 笔记 十三:DOM
- Eloquent JavaScript 笔记 十七:HTTP
- 《Eloquent JavaScript》笔记--对象与数组
- 《Eloquent JavaScript》笔记--程序的结构;
- Eloquent JavaScript 笔记 二:Program Structure
- Eloquent JavaScript 笔记 五: High-Order Functions
- Eloquent JavaScript 笔记 四:Objects and Arrays
- Eloquent JavaScript 笔记 七: Electronic Life
- Eloquent JavaScript 笔记 十一:A Programming Language
- Eloquent JavaScript 笔记 十四:Handling Event
- Eloquent JavaScript 笔记 十五:A Platform Game
- Eloquent JavaScript 笔记 十九:Node.js
- Eloquent JavaScript 笔记 二十:略有遗憾
- java JSON操作所要用到的jar包
- FtpUtil
- 一个人的朝圣
- jsp中的include标签引用页面需注意路径问题
- hiberbate学习之一对多
- Eloquent JavaScript 笔记 十六:Drawing on Canvas
- HttpClientUtil
- 程序员、技术主管和架构师
- 什么是面向对象?(javascript里面的面向对象是指的什么)
- 网页版时钟----简单版
- mysql多实例介绍
- Python设计模式-享元模式
- 刷清橙OJ--A1088.差分计算
- FastDfsUtil