1
0
mirror of https://github.com/apachecn/eloquent-js-3e-zh.git synced 2025-05-23 20:02:20 +00:00
eloquent-js-3e-zh/diff-en/2ech16-3ech17.diff
wizardforcel 9faf107133 diff
2018-04-28 15:20:25 +08:00

886 lines
63 KiB
Diff

diff --git a/2ech16.md b/3ech17.md
index b3a1f00..4814dc7 100644
--- a/2ech16.md
+++ b/3ech17.md
@@ -1,20 +1,20 @@
-# Chapter 16Drawing on Canvas
+# Chapter 17Drawing on Canvas
> Drawing is deception.
>
> <footer>M.C. Escher, <cite>cited by Bruno Ernst in The Magic Mirror of M.C. Escher</cite></footer>
-Browsers give us several ways to display graphics. The simplest way is to use styles to position and color regular DOM elements. This can get you quite far, as the game in the [previous chapter](15_game.html#game) showed. By adding partially transparent background images to the nodes, we can make them look exactly the way we want. It is even possible to rotate or skew nodes by using the `transform` style.
+Browsers give us several ways to display graphics. The simplest way is to use styles to position and color regular DOM elements. This can get you quite far, as the game in the [previous chapter](16_game.html) shows. By adding partially transparent background images to the nodes, we can make them look exactly the way we want. It is even possible to rotate or skew nodes with the `transform` style.
But we'd be using the DOM for something that it wasn't originally designed for. Some tasks, such as drawing a line between arbitrary points, are extremely awkward to do with regular HTML elements.
-There are two alternatives. The first is DOM-based but utilizes _Scalable Vector Graphics (SVG)_, rather than HTML elements. Think of SVG as a dialect for describing documents that focuses on shapes rather than text. You can embed an SVG document in an HTML document, or you can include it through an `<img>` tag.
+There are two alternatives. The first is DOM-based but utilizes _Scalable Vector Graphics_ (SVG), rather than HTML. Think of SVG as a document-markup dialect that focuses on shapes rather than text. You can embed an SVG document directly in an HTML document or include it with an `<img>` tag.
The second alternative is called a _canvas_. A canvas is a single DOM element that encapsulates a picture. It provides a programming interface for drawing shapes onto the space taken up by the node. The main difference between a canvas and an SVG picture is that in SVG the original description of the shapes is preserved so that they can be moved or resized at any time. A canvas, on the other hand, converts the shapes to pixels (colored dots on a raster) as soon as they are drawn and does not remember what these pixels represent. The only way to move a shape on a canvas is to clear the canvas (or the part of the canvas around the shape) and redraw it with the shape in a new position.
## SVG
-This book will not go into SVG in detail, but I will briefly explain how it works. At the [end of the chapter](16_canvas.html#graphics_tradeoffs), I'll come back to the trade-offs that you must consider when deciding which drawing mechanism is appropriate for a given application.
+This book will not go into SVG in detail, but I will briefly explain how it works. At the [end of the chapter](17_canvas.html#graphics_tradeoffs), I'll come back to the trade-offs that you must consider when deciding which drawing mechanism is appropriate for a given application.
This is an HTML document with a simple SVG picture in it:
@@ -29,10 +29,10 @@ This is an HTML document with a simple SVG picture in it:
The `xmlns` attribute changes an element (and its children) to a different _XML namespace_. This namespace, identified by a URL, specifies the dialect that we are currently speaking. The `<circle>` and `<rect>` tags, which do not exist in HTML, do have a meaning in SVG—they draw shapes using the style and position specified by their attributes.
-These tags create DOM elements, just like HTML tags. For example, this changes the `<circle>` element to be colored cyan instead:
+These tags create DOM elements, just like HTML tags, that scripts can interact with. For example, this changes the `<circle>` element to be colored cyan instead:
```
-var circle = document.querySelector("circle");
+let circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");
```
@@ -40,21 +40,21 @@ circle.setAttribute("fill", "cyan");
Canvas graphics can be drawn onto a `<canvas>` element. You can give such an element `width` and `height` attributes to determine its size in pixels.
-A new canvas is empty, meaning it is entirely transparent and thus shows up simply as empty space in the document.
+A new canvas is empty, meaning it is entirely transparent and thus shows up as empty space in the document.
-The `<canvas>` tag is intended to support different styles of drawing. To get access to an actual drawing interface, we first need to create a _context_, which is an object whose methods provide the drawing interface. There are currently two widely supported drawing styles: `"2d"` for two-dimensional graphics and `"webgl"` for three-dimensional graphics through the OpenGL interface.
+The `<canvas>` tag is intended to allow different styles of drawing. To get access to an actual drawing interface, we first need to create a _context_, an object whose methods provide the drawing interface. There are currently two widely supported drawing styles: `"2d"` for two-dimensional graphics and `"webgl"` for three-dimensional graphics through the OpenGL interface.
-This book won't discuss WebGL. We stick to two dimensions. But if you are interested in three-dimensional graphics, I do encourage you to look into WebGL. It provides a very direct interface to modern graphics hardware and thus allows you to render even complicated scenes efficiently, using JavaScript.
+This book won't discuss WebGL—we'll stick to two dimensions. But if you are interested in three-dimensional graphics, I do encourage you to look into WebGL. It provides a very direct interface to graphics hardware and allows you to render even complicated scenes efficiently, using JavaScript.
-A context is created through the `getContext` method on the `<canvas>` element.
+You create a context with the `getContext` method on the `<canvas>` DOM 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");
+ let canvas = document.querySelector("canvas");
+ let context = canvas.getContext("2d");
context.fillStyle = "red";
context.fillRect(10, 10, 100, 50);
</script>
@@ -64,22 +64,22 @@ After creating the context object, the example draws a red rectangle 100 pixels
Just like in HTML (and SVG), the coordinate system that the canvas uses puts (0,0) at the top-left corner, and the positive y-axis goes down from there. So (10,10) is 10 pixels below and to the right of the top-left corner.
-## Filling and stroking
+## Lines and surfaces
In the canvas interface, a shape can be _filled_, meaning its area is given a certain color or pattern, or it can be _stroked_, which means a line is drawn along its edge. The same terminology is used by SVG.
The `fillRect` method fills a rectangle. It takes first the x- and y-coordinates of the rectangle's top-left corner, then its width, and then its height. A similar method, `strokeRect`, draws the outline of a rectangle.
-Neither method takes any further parameters. The color of the fill, thickness of the stroke, and so on are not determined by an argument to the method (as you might justly expect) but rather by properties of the context object.
+Neither method takes any further parameters. The color of the fill, thickness of the stroke, and so on are not determined by an argument to the method (as you might reasonably expect) but rather by properties of the context object.
-Setting `fillStyle` changes the way shapes are filled. It can be set to a string that specifies a color, and any color understood by CSS can also be used here.
+The `fillStyle` property controls the way shapes are filled. It can be set to a string that specifies a color, using the color notation used by CSS.
The `strokeStyle` property works similarly but determines the color used for a stroked line. The width of that line is determined by the `lineWidth` property, which may contain any positive number.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
@@ -87,7 +87,7 @@ The `strokeStyle` property works similarly but determines the color used for a s
</script>
```
-When no `width` or `height` attribute is specified, as in the previous example, a canvas element gets a default width of 300 pixels and height of 150 pixels.
+When no `width` or `height` attribute is specified, as in the example, a canvas element gets a default width of 300 pixels and height of 150 pixels.
## Paths
@@ -96,9 +96,9 @@ A path is a sequence of lines. The 2D canvas interface takes a peculiar approach
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
- for (var y = 10; y < 100; y += 10) {
+ for (let y = 10; y < 100; y += 10) {
cx.moveTo(10, y);
cx.lineTo(90, y);
}
@@ -113,7 +113,7 @@ When filling a path (using the `fill` method), each shape is filled separately.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(50, 10);
cx.lineTo(10, 70);
@@ -122,20 +122,20 @@ When filling a path (using the `fill` method), each shape is filled separately.
</script>
```
-This example draws a filled triangle. Note that only two of the triangle's sides are explicitly drawn. The third, from the bottom-right corner back to the top, is implied and won't be there when you stroke the path.
+This example draws a filled triangle. Note that only two of the triangle's sides are explicitly drawn. The third, from the bottom-right corner back to the top, is implied and wouldn't be there when you stroke the path.
You could also use the `closePath` method to explicitly close a path by adding an actual line segment back to the path's start. This segment _is_ drawn when stroking the path.
## Curves
-A path may also contain curved lines. These are, unfortunately, a bit more involved to draw than straight lines.
+A path may also contain curved lines. These are unfortunately a bit more involved to draw.
-The `quadraticCurveTo` method draws a curve to a given point. To determine the curvature of the line, the method is given a control point as well as a destination point. Imagine this control point as _attracting_ the line, giving the line its curve. The line won't go through the control point. Rather, the direction of the line at its start and end points will be such that it aligns with the line from there to the control point. The following example illustrates this:
+The `quadraticCurveTo` method draws a curve to a given point. To determine the curvature of the line, the method is given a control point as well as a destination point. Imagine this control point as _attracting_ the line, giving it its curve. The line won't go through the control point, but its direction at the start and end points will be such that a straight in that direction would point towards the control point. The following example illustrates this:
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control=(60,10) goal=(90,90)
@@ -153,7 +153,7 @@ The `bezierCurveTo` method draws a similar kind of curve. Instead of a single co
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
cx.moveTo(10, 90);
// control1=(10,10) control2=(90,10) goal=(50,90)
@@ -169,33 +169,14 @@ The two control points specify the direction at both ends of the curve. The furt
Such curves can be hard to work with—it's not always clear how to find the control points that provide the shape you are looking for. Sometimes you can compute them, and sometimes you'll just have to find a suitable value by trial and error.
-_Arcs_—fragments of a circle—are easier to reason about. The `arcTo` method takes no less than five arguments. The first four arguments act somewhat like the arguments to `quadraticCurveTo`. The first pair provides a sort of control point, and the second pair gives the line's destination. The fifth argument provides the radius of the arc. The method will conceptually project a corner—a line going to the control point and then to the destination point—and round the corner's point so that it forms part of a circle with the given radius. The `arcTo` method then draws the rounded part, as well as a line from the starting position to the start of the rounded part.
+The `arc` method is a way to draw a line that curves along the edge of a circle. It takes a pair of coordinates for the arc's center, a radius, and then a start and end angle.
-```
-<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>
-```
-
-The `arcTo` method won't draw the line from the end of the rounded part to the goal position, though the word _to_ in its name would suggest it does. You can follow up with a call to `lineTo` with the same goal coordinates to add that part of the line.
-
-To draw a circle, you could use four calls to `arcTo` (each turning 90 degrees). But the `arc` method provides a simpler way. It takes a pair of coordinates for the arc's center, a radius, and then a start and end angle.
-
-Those last two parameters make it possible to draw only part of circle. The angles are measured in radians, not degrees. This means a full circle has an angle of 2π, or `2 * Math.PI`, which is about 6.28\. The angle starts counting at the point to the right of the circle's center and goes clockwise from there. You can use a start of 0 and an end bigger than 2π (say, 7) to draw a full circle.
+Those last two parameters make it possible to draw only part of the circle. The angles are measured in radians, not degrees. This means a full circle has an angle of 2π, or `2 * Math.PI`, which is about 6.28\. The angle starts counting at the point to the right of the circle's center and goes clockwise from there. You can use a start of 0 and an end bigger than 2π (say, 7) to draw a full circle.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.beginPath();
// center=(50,50) radius=40 angle=0 to 7
cx.arc(50, 50, 40, 0, 7);
@@ -205,16 +186,16 @@ Those last two parameters make it possible to draw only part of circle. The angl
</script>
```
-The resulting picture contains a line from the right of the full circle (first call to `arc`) to the right of the quarter-circle (second call). Like other path-drawing methods, a line drawn with `arc` is connected to the previous path segment by default. You'd have to call `moveTo` or start a new path if you want to avoid this.
+The resulting picture contains a line from the right of the full circle (first call to `arc`) to the right of the quarter-circle (second call). Like other path-drawing methods, a line drawn with `arc` is connected to the previous path segment. You can call `moveTo` or start a new path to avoid this.
## Drawing a pie chart
Imagine you've just taken a job at EconomiCorp, Inc., and your first assignment is to draw a pie chart of their customer satisfaction survey results.
-The `results` variable contains an array of objects that represent the survey responses.
+The `results` binding contains an array of objects that represent the survey responses.
```
-var results = [
+const results = [
{name: "Satisfied", count: 1043, color: "lightblue"},
{name: "Neutral", count: 563, color: "lightgreen"},
{name: "Unsatisfied", count: 510, color: "pink"},
@@ -227,14 +208,13 @@ To draw a pie chart, we draw a number of pie slices, each made up of an arc and
```
<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);
+ let cx = document.querySelector("canvas").getContext("2d");
+ let total = results
+ .reduce((sum, {count}) => sum + count, 0);
// Start at the top
- var currentAngle = -0.5 * Math.PI;
- results.forEach(function(result) {
- var sliceAngle = (result.count / total) * 2 * Math.PI;
+ let currentAngle = -0.5 * Math.PI;
+ for (let result of results) {
+ let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
// center=100,100, radius=100
// from current angle, clockwise by slice's angle
@@ -244,75 +224,76 @@ To draw a pie chart, we draw a number of pie slices, each made up of an arc and
cx.lineTo(100, 100);
cx.fillStyle = result.color;
cx.fill();
- });
+ }
</script>
```
-But a chart that doesn't tell us what it means isn't very helpful. We need a way to draw text to the canvas.
+But a chart that doesn't tell us what the slices mean isn't very helpful. We need a way to draw text to the canvas.
## Text
-A 2D canvas drawing context provides the methods `fillText` and `strokeText`. The latter can be useful for outlining letters, but usually `fillText` is what you need. It will fill the given text with the current `fillColor`.
+A 2D canvas drawing context provides the methods `fillText` and `strokeText`. The latter can be useful for outlining letters, but usually `fillText` is what you need. It will fill the outline of the given text with the current `fillStyle`.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.font = "28px Georgia";
cx.fillStyle = "fuchsia";
cx.fillText("I can draw text, too!", 10, 50);
</script>
```
-You can specify the size, style, and font of the text with the `font` property. This example just gives a font size and family name. You can add `italic` or `bold` to the start of the string to select a style.
+You can specify the size, style, and font of the text with the `font` property. This example just gives a font size and family name. It is also possible to add `italic` or `bold` to the start of the string to select a style.
-The last two arguments to `fillText` (and `strokeText`) provide the position at which the font is drawn. By default, they indicate the position of the start of the text's alphabetic baseline, which is the line that letters “stand” on, not counting hanging parts in letters like _j_ or _p_. You can change the horizontal position by setting the `textAlign` property to `"end"` or `"center"` and the vertical position by setting `textBaseline` to `"top"`, `"middle"`, or `"bottom"`.
+The last two arguments to `fillText` and `strokeText` provide the position at which the font is drawn. By default, they indicate the position of the start of the text's alphabetic baseline, which is the line that letters “stand” on, not counting hanging parts in letters like _j_ or _p_. You can change the horizontal position by setting the `textAlign` property to `"end"` or `"center"` and the vertical position by setting `textBaseline` to `"top"`, `"middle"`, or `"bottom"`.
-We will come back to our pie chart, and the problem of labeling the slices, in the [exercises](16_canvas.html#exercise_pie_chart) at the end of the chapter.
+We'll come back to our pie chart, and the problem of labeling the slices, in the [exercises](17_canvas.html#exercise_pie_chart) at the end of the chapter.
## Images
In computer graphics, a distinction is often made between _vector_ graphics and _bitmap_ graphics. The first is what we have been doing so far in this chapter—specifying a picture by giving a logical description of shapes. Bitmap graphics, on the other hand, don't specify actual shapes but rather work with pixel data (rasters of colored dots).
-The `drawImage` method allows us to draw pixel data onto a canvas. This pixel data can originate from an `&lt;img&gt;` element or from another canvas, and neither has to be visible in the actual document. The following example creates a detached `&lt;img&gt;` element and loads an image file into it. But it cannot immediately start drawing from this picture because the browser may not have fetched it yet. To deal with this, we register a `"load"` event handler and do the drawing after the image has loaded.
+The `drawImage` method allows us to draw pixel data onto a canvas. This pixel data can originate from an `&lt;img&gt;` element or from another canvas. The following example creates a detached `&lt;img&gt;` element and loads an image file into it. But it cannot immediately start drawing from this picture because the browser may not have loaded it yet. To deal with this, we register a `"load"` event handler and do the drawing after the image has loaded.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
- var img = document.createElement("img");
+ let cx = document.querySelector("canvas").getContext("2d");
+ let img = document.createElement("img");
img.src = "img/hat.png";
- img.addEventListener("load", function() {
- for (var x = 10; x < 200; x += 30)
+ img.addEventListener("load", () => {
+ for (let x = 10; x < 200; x += 30) {
cx.drawImage(img, x, 10);
+ }
});
</script>
```
-By default, `drawImage` will draw the image at its original size. You can also give it two additional arguments to dictate a different width and height.
+By default, `drawImage` will draw the image at its original size. You can also give it two additional arguments to set a different width and height.
When `drawImage` is given _nine_ arguments, it can be used to draw only a fragment of an image. The second through fifth arguments indicate the rectangle (x, y, width, and height) in the source image that should be copied, and the sixth to ninth arguments give the rectangle (on the canvas) into which it should be copied.
This can be used to pack multiple _sprites_ (image elements) into a single image file and then draw only the part you need. For example, we have this picture containing a game character in multiple poses:
-![Various poses of a game character](img/player_big.png)
+<figure>![Various poses of a game character](img/player_big.png)</figure>
By alternating which pose we draw, we can show an animation that looks like a walking character.
-To animate the picture on a canvas, the `clearRect` method is useful. It resembles `fillRect`, but instead of coloring the rectangle, it makes it transparent, removing the previously drawn pixels.
+To animate a picture on a canvas, the `clearRect` method is useful. It resembles `fillRect`, but instead of coloring the rectangle, it makes it transparent, removing the previously drawn pixels.
-We know that each _sprite_, each subpicture, is 24 pixels wide and 30 pixels high. The following code loads the image and then sets up an interval (repeated timer) to draw the next _frame_:
+We know that each _sprite_, each subpicture, is 24 pixels wide and 30 pixels high. The following code loads the image and then sets up an interval (repeated timer) to draw the next frame:
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
- var img = document.createElement("img");
+ let cx = document.querySelector("canvas").getContext("2d");
+ let img = document.createElement("img");
img.src = "img/player.png";
- var spriteW = 24, spriteH = 30;
- img.addEventListener("load", function() {
- var cycle = 0;
- setInterval(function() {
+ let spriteW = 24, spriteH = 30;
+ img.addEventListener("load", () => {
+ let cycle = 0;
+ setInterval(() => {
cx.clearRect(0, 0, spriteW, spriteH);
cx.drawImage(img,
// source rectangle
@@ -325,18 +306,18 @@ We know that each _sprite_, each subpicture, is 24 pixels wide and 30 pixels hig
</script>
```
-The `cycle` variable tracks our position in the animation. Each frame, it is incremented and then clipped back to the 0 to 7 range by using the remainder operator. This variable is then used to compute the x-coordinate that the sprite for the current pose has in the picture.
+The `cycle` binding tracks our position in the animation. Each frame, it is incremented and then clipped back to the 0 to 7 range by using the remainder operator. This binding is then used to compute the x-coordinate that the sprite for the current pose has in the picture.
## Transformation
-But what if we want our character to walk to the left instead of to the right? We could add another set of sprites, of course. But we can also instruct the canvas to draw the picture the other way round.
+But what if we want our character to walk to the left instead of to the right? We could draw another set of sprites, of course. But we can also instruct the canvas to draw the picture the other way round.
Calling the `scale` method will cause anything drawn after it to be scaled. This method takes two parameters, one to set a horizontal scale and one to set a vertical scale.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
cx.scale(3, .5);
cx.beginPath();
cx.arc(50, 50, 40, 0, 7);
@@ -351,9 +332,9 @@ So to turn a picture around, we can't simply add `cx.scale(-1, 1)` before the ca
There are several other methods besides `scale` that influence the coordinate system for a canvas. You can rotate subsequently drawn shapes with the `rotate` method and move them with the `translate` method. The interesting—and confusing—thing is that these transformations _stack_, meaning that each one happens relative to the previous transformations.
-So if we translate by 10 horizontal pixels twice, everything will be drawn 20 pixels to the right. If we first move the center of the coordinate system to (50,50) and then rotate by 20 degrees (0.1π in radians), that rotation will happen _around_ point (50,50).
+So if we translate by 10 horizontal pixels twice, everything will be drawn 20 pixels to the right. If we first move the center of the coordinate system to (50,50) and then rotate by 20 degrees (about 0.1π radians), that rotation will happen _around_ point (50,50).
-![Stacking transformations](img/transform.svg)
+<figure>![Stacking transformations](img/transform.svg)</figure>
But if we _first_ rotate by 20 degrees and _then_ translate by (50,50), the translation will happen in the rotated coordinate system and thus produce a different orientation. The order in which transformations are applied matters.
@@ -369,20 +350,20 @@ function flipHorizontally(context, around) {
We move the y-axis to where we want our mirror to be, apply the mirroring, and finally move the y-axis back to its proper place in the mirrored universe. The following picture explains why this works:
-![Mirroring around a vertical line](img/mirror.svg)
+<figure>![Mirroring around a vertical line](img/mirror.svg)</figure>
-This shows the coordinate systems before and after mirroring across the central line. If we draw a triangle at a positive x position, it would, by default, be in the place where triangle 1 is. A call to `flipHorizontally` first does a translation to the right, which gets us to triangle 2\. It then scales, flipping the triangle back to position 3\. This is not where it should be, if it were mirrored in the given line. The second `translate` call fixes this—it “cancels” the initial translation and makes triangle 4 appear exactly where it should.
+This shows the coordinate systems before and after mirroring across the central line. The triangles are numbered to illustrate each step. If we draw a triangle at a positive x position, it would, by default, be in the place where triangle 1 is. A call to `flipHorizontally` first does a translation to the right, which gets us to triangle 2\. It then scales, flipping the triangle over to position 3\. This is not where it should be, if it were mirrored in the given line. The second `translate` call fixes this—it “cancels” the initial translation and makes triangle 4 appear exactly where it should.
We can now draw a mirrored character at position (100,0) by flipping the world around the character's vertical center.
```
<canvas></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
- var img = document.createElement("img");
+ let cx = document.querySelector("canvas").getContext("2d");
+ let img = document.createElement("img");
img.src = "img/player.png";
- var spriteW = 24, spriteH = 30;
- img.addEventListener("load", function() {
+ let spriteW = 24, spriteH = 30;
+ img.addEventListener("load", () => {
flipHorizontally(cx, 100 + spriteW / 2);
cx.drawImage(img, 0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH);
@@ -392,11 +373,11 @@ We can now draw a mirrored character at position (100,0) by flipping the world a
## Storing and clearing transformations
-Transformations stick around. Everything else we draw after drawing that mirrored character would also be mirrored. That might be a problem.
+Transformations stick around. Everything else we draw after drawing that mirrored character would also be mirrored. That might be inconvenient.
It is possible to save the current transformation, do some drawing and transforming, and then restore the old transformation. This is usually the proper thing to do for a function that needs to temporarily transform the coordinate system. First, we save whatever transformation the code that called the function was using. Then, the function does its thing (on top of the existing transformation), possibly adding more transformations. And finally, we revert to the transformation that we started with.
-The `save` and `restore` methods on the 2D canvas context perform this kind of transformation management. They conceptually keep a stack of transformation states. When you call `save`, the current state is pushed onto the stack, and when you call `restore`, the state on top of the stack is taken off and used as the context's current transformation.
+The `save` and `restore` methods on the 2D canvas context do this transformation management. They conceptually keep a stack of transformation states. When you call `save`, the current state is pushed onto the stack, and when you call `restore`, the state on top of the stack is taken off and used as the context's current transformation. You can also call `resetTransform` to fully reset the transformation.
The `branch` function in the following example illustrates what you can do with a function that changes the transformation and then calls another function (in this case itself), which continues drawing with the given transformation.
@@ -405,7 +386,7 @@ This function draws a treelike shape by drawing a line, moving the center of the
```
<canvas width="600" height="300"></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
function branch(length, angle, scale) {
cx.fillRect(0, 0, 1, length);
if (length < 8) return;
@@ -426,114 +407,111 @@ If the calls to `save` and `restore` were not there, the second recursive call t
## Back to the game
-We now know enough about canvas drawing to start working on a canvas-based display system for the game from the [previous chapter](15_game.html#game). The new display will no longer be showing just colored boxes. Instead, we'll use `drawImage` to draw pictures that represent the game's elements.
+We now know enough about canvas drawing to start working on a canvas-based display system for the game from the [previous chapter](16_game.html). The new display will no longer be showing just colored boxes. Instead, we'll use `drawImage` to draw pictures that represent the game's elements.
-We will define an object type `CanvasDisplay`, supporting the same interface as `DOMDisplay` from [Chapter 15](15_game.html#domdisplay), namely, the methods `drawFrame` and `clear`.
+We define another display object type called `CanvasDisplay`, supporting the same interface as `DOMDisplay` from [Chapter 16](16_game.html#domdisplay), namely the methods `setState` and `clear`.
-This object keeps a little more information than `DOMDisplay`. Rather than using the scroll position of its DOM element, it tracks its own viewport, which tells us what part of the level we are currently looking at. It also tracks time and uses that to decide which animation frame to use. And finally, it keeps a `flipPlayer` property so that even when the player is standing still, it keeps facing the direction it last moved in.
+This object keeps a little more information than `DOMDisplay`. Rather than using the scroll position of its DOM element, it tracks its own viewport, which tells us what part of the level we are currently looking at. And finally, it keeps a `flipPlayer` property so that even when the player is standing still, it keeps facing the direction it last moved in.
```
-function CanvasDisplay(parent, level) {
- this.canvas = document.createElement("canvas");
- this.canvas.width = Math.min(600, level.width * scale);
- this.canvas.height = Math.min(450, level.height * scale);
- parent.appendChild(this.canvas);
- this.cx = this.canvas.getContext("2d");
+class CanvasDisplay {
+ constructor(parent, level) {
+ this.canvas = document.createElement("canvas");
+ this.canvas.width = Math.min(600, level.width * scale);
+ this.canvas.height = Math.min(450, level.height * scale);
+ parent.appendChild(this.canvas);
+ this.cx = this.canvas.getContext("2d");
- this.level = level;
- this.animationTime = 0;
- this.flipPlayer = false;
+ this.flipPlayer = false;
- this.viewport = {
- left: 0,
- top: 0,
- width: this.canvas.width / scale,
- height: this.canvas.height / scale
- };
+ this.viewport = {
+ left: 0,
+ top: 0,
+ width: this.canvas.width / scale,
+ height: this.canvas.height / scale
+ };
+ }
- this.drawFrame(0);
+ clear() {
+ this.canvas.remove();
+ }
}
-
-CanvasDisplay.prototype.clear = function() {
- this.canvas.parentNode.removeChild(this.canvas);
-};
```
-The `animationTime` counter is the reason we passed the step size to `drawFrame` in [Chapter 15](15_game.html#domdisplay), even though `DOMDisplay` does not use it. Our new `drawFrame` function uses the counter to track time so that it can switch between animation frames based on the current time.
+The `setState` method first computes a new viewport, and then draws the game scene at the appropriate position.
```
-CanvasDisplay.prototype.drawFrame = function(step) {
- this.animationTime += step;
-
- this.updateViewport();
- this.clearDisplay();
- this.drawBackground();
- this.drawActors();
+CanvasDisplay.prototype.setState = function(state) {
+ this.updateViewport(state);
+ this.clearDisplay(state.status);
+ this.drawBackground(state.level);
+ this.drawActors(state.actors);
};
```
-Other than tracking time, the method updates the viewport for the current player position, fills the whole canvas with a background color, and draws the background and actors onto that. Note that this is different from the approach in [Chapter 15](15_game.html#domdisplay), where we drew the background once and scrolled the wrapping DOM element to move it.
-
-Because shapes on a canvas are just pixels, after we draw them, there is no way to move them (or remove them). The only way to update the canvas display is to clear it and redraw the scene.
+Contrary to `DOMDisplay`, this display style _does_ have to redraw the background on every update. Because shapes on a canvas are just pixels, after we draw them, there is no good way to move them (or remove them). The only way to update the canvas display is to clear it and redraw the scene. We may also have scrolled, which requires the background to be in a different position.
The `updateViewport` method is similar to `DOMDisplay`'s `scrollPlayerIntoView` method. It checks whether the player is too close to the edge of the screen and moves the viewport when this is the case.
```
-CanvasDisplay.prototype.updateViewport = function() {
- var view = this.viewport, margin = view.width / 3;
- var player = this.level.player;
- var center = player.pos.plus(player.size.times(0.5));
+CanvasDisplay.prototype.updateViewport = function(state) {
+ let view = this.viewport, margin = view.width / 3;
+ let player = state.player;
+ let center = player.pos.plus(player.size.times(0.5));
- if (center.x < view.left + margin)
+ if (center.x < view.left + margin) {
view.left = Math.max(center.x - margin, 0);
- else if (center.x > view.left + view.width - margin)
+ } else if (center.x > view.left + view.width - margin) {
view.left = Math.min(center.x + margin - view.width,
- this.level.width - view.width);
- if (center.y < view.top + margin)
+ state.level.width - view.width);
+ }
+ if (center.y < view.top + margin) {
view.top = Math.max(center.y - margin, 0);
- else if (center.y > view.top + view.height - margin)
+ } else if (center.y > view.top + view.height - margin) {
view.top = Math.min(center.y + margin - view.height,
- this.level.height - view.height);
+ state.level.height - view.height);
+ }
};
```
-The calls to `Math.max` and `Math.min` ensure that the viewport does not end up showing space outside of the level. `Math.max(x, 0)` ensures that the resulting number is not less than zero. `Math.min`, similarly, ensures a value stays below a given bound.
+The calls to `Math.max` and `Math.min` ensure that the viewport does not end up showing space outside of the level. `Math.max(x, 0)` makes sure the resulting number is not less than zero. `Math.min`, similarly, guarantees that a value stays below a given bound.
When clearing the display, we'll use a slightly different color depending on whether the game is won (brighter) or lost (darker).
```
-CanvasDisplay.prototype.clearDisplay = function() {
- if (this.level.status == "won")
+CanvasDisplay.prototype.clearDisplay = function(status) {
+ if (status == "won") {
this.cx.fillStyle = "rgb(68, 191, 255)";
- else if (this.level.status == "lost")
+ } else if (status == "lost") {
this.cx.fillStyle = "rgb(44, 136, 214)";
- else
+ } else {
this.cx.fillStyle = "rgb(52, 166, 251)";
+ }
this.cx.fillRect(0, 0,
this.canvas.width, this.canvas.height);
};
```
-To draw the background, we run through the tiles that are visible in the current viewport, using the same trick used in `obstacleAt` in the [previous chapter](15_game.html#viewport).
+To draw the background, we run through the tiles that are visible in the current viewport, using the same trick used in the `touches` method from the [previous chapter](16_game.html#touches).
```
-var otherSprites = document.createElement("img");
+let otherSprites = document.createElement("img");
otherSprites.src = "img/sprites.png";
-CanvasDisplay.prototype.drawBackground = function() {
- var view = this.viewport;
- var xStart = Math.floor(view.left);
- var xEnd = Math.ceil(view.left + view.width);
- var yStart = Math.floor(view.top);
- var yEnd = Math.ceil(view.top + view.height);
-
- for (var y = yStart; y < yEnd; y++) {
- for (var x = xStart; x < xEnd; x++) {
- var tile = this.level.grid[y][x];
- if (tile == null) continue;
- var screenX = (x - view.left) * scale;
- var screenY = (y - view.top) * scale;
- var tileX = tile == "lava" ? scale : 0;
+CanvasDisplay.prototype.drawBackground = function(level) {
+ let {left, top, width, height} = this.viewport;
+ let xStart = Math.floor(left);
+ let xEnd = Math.ceil(left + width);
+ let yStart = Math.floor(top);
+ let yEnd = Math.ceil(top + height);
+
+ for (let y = yStart; y < yEnd; y++) {
+ for (let x = xStart; x < xEnd; x++) {
+ let tile = level.rows[y][x];
+ if (tile == "empty") continue;
+ let screenX = (x - left) * scale;
+ let screenY = (y - top) * scale;
+ let tileX = tile == "lava" ? scale : 0;
this.cx.drawImage(otherSprites,
tileX, 0, scale, scale,
screenX, screenY, scale, scale);
@@ -542,44 +520,45 @@ CanvasDisplay.prototype.drawBackground = function() {
};
```
-Tiles that are not empty (null) are drawn with `drawImage`. The `otherSprites` image contains the pictures used for elements other than the player. It contains, from left to right, the wall tile, the lava tile, and the sprite for a coin.
+Tiles that are not empty are drawn with `drawImage`. The `otherSprites` image contains the pictures used for elements other than the player. It contains, from left to right, the wall tile, the lava tile, and the sprite for a coin.
-![Sprites for our game](img/sprites_big.png)
+<figure>![Sprites for our game](img/sprites_big.png)</figure>
-Background tiles are 20 by 20 pixels, since we will use the same scale that we used in `DOMDisplay`. Thus, the offset for lava tiles is 20 (the value of the `scale` variable), and the offset for walls is 0.
+Background tiles are 20 by 20 pixels, since we will use the same scale that we used in `DOMDisplay`. Thus, the offset for lava tiles is 20 (the value of the `scale` binding), and the offset for walls is 0.
We don't bother waiting for the sprite image to load. Calling `drawImage` with an image that hasn't been loaded yet will simply do nothing. Thus, we might fail to draw the game properly for the first few frames, while the image is still loading, but that is not a serious problem. Since we keep updating the screen, the correct scene will appear as soon as the loading finishes.
-The walking character shown earlier will be used to represent the player. The code that draws it needs to pick the right sprite and direction based on the player's current motion. The first eight sprites contain a walking animation. When the player is moving along a floor, we cycle through them based on the display's `animationTime` property. This is measured in seconds, and we want to switch frames 12 times per second, so the time is multiplied by 12 first. When the player is standing still, we draw the ninth sprite. During jumps, which are recognized by the fact that the vertical speed is not zero, we use the tenth, rightmost sprite.
+The walking character shown earlier will be used to represent the player. The code that draws it needs to pick the right sprite and direction based on the player's current motion. The first eight sprites contain a walking animation. When the player is moving along a floor, we cycle through them based on the current time. We want to switch frames every 60 milliseconds, so the time is divided by 60 first. When the player is standing still, we draw the ninth sprite. During jumps, which are recognized by the fact that the vertical speed is not zero, we use the tenth, rightmost sprite.
Because the sprites are slightly wider than the player object—24 instead of 16 pixels, to allow some space for feet and arms—the method has to adjust the x-coordinate and width by a given amount (`playerXOverlap`).
```
-var playerSprites = document.createElement("img");
+let playerSprites = document.createElement("img");
playerSprites.src = "img/player.png";
-var playerXOverlap = 4;
+const playerXOverlap = 4;
-CanvasDisplay.prototype.drawPlayer = function(x, y, width,
- height) {
- var sprite = 8, player = this.level.player;
+CanvasDisplay.prototype.drawPlayer = function(player, x, y,
+ width, height){
width += playerXOverlap * 2;
x -= playerXOverlap;
- if (player.speed.x != 0)
+ if (player.speed.x != 0) {
this.flipPlayer = player.speed.x < 0;
+ }
- if (player.speed.y != 0)
- sprite = 9;
- else if (player.speed.x != 0)
- sprite = Math.floor(this.animationTime * 12) % 8;
+ let tile = 8;
+ if (player.speed.y != 0) {
+ tile = 9;
+ } else if (player.speed.x != 0) {
+ tile = Math.floor(Date.now() / 60) % 8;
+ }
this.cx.save();
- if (this.flipPlayer)
+ if (this.flipPlayer) {
flipHorizontally(this.cx, x + width / 2);
-
- this.cx.drawImage(playerSprites,
- sprite * width, 0, width, height,
- x, y, width, height);
-
+ }
+ let tileX = tile * width;
+ this.cx.drawImage(playerSprites, tileX, 0, width, height,
+ x, y, width, height);
this.cx.restore();
};
```
@@ -587,21 +566,21 @@ CanvasDisplay.prototype.drawPlayer = function(x, y, width,
The `drawPlayer` method is called by `drawActors`, which is responsible for drawing all the actors in the game.
```
-CanvasDisplay.prototype.drawActors = function() {
- this.level.actors.forEach(function(actor) {
- var width = actor.size.x * scale;
- var height = actor.size.y * scale;
- var x = (actor.pos.x - this.viewport.left) * scale;
- var y = (actor.pos.y - this.viewport.top) * scale;
+CanvasDisplay.prototype.drawActors = function(actors) {
+ for (let actor of actors) {
+ let width = actor.size.x * scale;
+ let height = actor.size.y * scale;
+ let x = (actor.pos.x - this.viewport.left) * scale;
+ let y = (actor.pos.y - this.viewport.top) * scale;
if (actor.type == "player") {
- this.drawPlayer(x, y, width, height);
+ this.drawPlayer(actor, x, y, width, height);
} else {
- var tileX = (actor.type == "coin" ? 2 : 1) * scale;
+ let tileX = (actor.type == "coin" ? 2 : 1) * scale;
this.cx.drawImage(otherSprites,
tileX, 0, width, height,
x, y, width, height);
}
- }, this);
+ }
};
```
@@ -609,7 +588,7 @@ When drawing something that is not the player, we look at its type to find the o
We have to subtract the viewport's position when computing the actor's position since (0,0) on our canvas corresponds to the top left of the viewport, not the top left of the level. We could also have used `translate` for this. Either way works.
-The tiny document shown next plugs the new display into `runGame`:
+This document plugs the new display into `runGame`:
```
<body>
@@ -621,25 +600,25 @@ The tiny document shown next plugs the new display into `runGame`:
## Choosing a graphics interface
-Whenever you need to generate graphics in the browser, you can choose between plain HTML, SVG, and canvas. There is no single _best_ approach that works in all situations. Each option has strengths and weaknesses.
+So when you need to generate graphics in the browser, you can choose between plain HTML, SVG, and canvas. There is no single _best_ approach that works in all situations. Each option has strengths and weaknesses.
-Plain HTML has the advantage of being simple. It also integrates well with text. Both SVG and canvas allow you to draw text, but they won't help you position that text or wrap it when it takes up more than one line. In an HTML-based picture, it is easy to include blocks of text.
+Plain HTML has the advantage of being simple. It also integrates well with text. Both SVG and canvas allow you to draw text, but they won't help you position that text or wrap it when it takes up more than one line. In an HTML-based picture, it is much easier to include blocks of text.
-SVG can be used to produce crisp graphics that look good at any zoom level. It is more difficult to use than plain HTML but also much more powerful.
+SVG can be used to produce crisp graphics that look good at any zoom level. Contrary to HTML, it is actually designed for drawing, and thus more suitable for that purpose.
-Both SVG and HTML build up a data structure (the DOM) that represents the picture. This makes it possible to modify elements after they are drawn. If you need to repeatedly change a small part of a big picture in response to what the user is doing or as part of an animation, doing it in a canvas can be needlessly expensive. The DOM also allows us to register mouse event handlers on every element in the picture (even on shapes drawn with SVG). You can't do that with canvas.
+Both SVG and HTML build up a data structure (the DOM) that represents your picture. This makes it possible to modify elements after they are drawn. If you need to repeatedly change a small part of a big picture in response to what the user is doing or as part of an animation, doing it in a canvas can be needlessly expensive. The DOM also allows us to register mouse event handlers on every element in the picture (even on shapes drawn with SVG). You can't do that with canvas.
But canvas's pixel-oriented approach can be an advantage when drawing a huge amount of tiny elements. The fact that it does not build up a data structure but only repeatedly draws onto the same pixel surface gives canvas a lower cost per shape.
-There are also effects, such as rendering a scene one pixel at a time (for example, using a ray tracer) or postprocessing an image with JavaScript (blurring or distorting it), that can only be realistically handled by a pixel-based technique.
+There are also effects, such as rendering a scene one pixel at a time (for example using a ray tracer) or postprocessing an image with JavaScript (blurring or distorting it), that can only be realistically handled by a pixel-based approach.
In some cases, you may want to combine several of these techniques. For example, you might draw a graph with SVG or canvas but show textual information by positioning an HTML element on top of the picture.
-For nondemanding applications, it really doesn't matter much which interface you choose. The [second display](16_canvas.html#canvasdisplay) we built for our game in this chapter could have been implemented using any of these three graphics technologies since it does not need to draw text, handle mouse interaction, or work with an extraordinarily large amount of elements.
+For nondemanding applications, it really doesn't matter much which interface you choose. The display we built for our game in this chapter could have been implemented using any of these three graphics technologies since it does not need to draw text, handle mouse interaction, or work with an extraordinarily large amount of elements.
## Summary
-In this chapter, we discussed techniques for drawing graphics in the browser, focusing on the `&lt;canvas&gt;` element.
+In this chapter we discussed techniques for drawing graphics in the browser, focusing on the `&lt;canvas&gt;` element.
A canvas node represents an area in a document that our program may draw on. This drawing is done through a drawing context object, created with the `getContext` method.
@@ -653,7 +632,7 @@ Moving pixels from an image or another canvas onto our canvas is done with the `
Transformations allow you to draw a shape in multiple orientations. A 2D drawing context has a current transformation that can be changed with the `translate`, `scale`, and `rotate` methods. These will affect all subsequent drawing operations. A transformation state can be saved with the `save` method and restored with the `restore` method.
-When drawing an animation on a canvas, the `clearRect` method can be used to clear part of the canvas before redrawing it.
+When showing an animation on a canvas, the `clearRect` method can be used to clear part of the canvas before redrawing it.
## Exercises
@@ -671,50 +650,51 @@ Write a program that draws the following shapes on a canvas:
5. A yellow star
-![The shapes to draw](img/exercise_shapes.png)
+<figure>![The shapes to draw](img/exercise_shapes.png)</figure>
-When drawing the last two, you may want to refer to the explanation of `Math.cos` and `Math.sin` in [Chapter 13](13_dom.html#sin_cos), which describes how to get coordinates on a circle using these functions.
+When drawing the last two, you may want to refer to the explanation of `Math.cos` and `Math.sin` in [Chapter 14](14_dom.html#sin_cos), which describes how to get coordinates on a circle using these functions.
I recommend creating a function for each shape. Pass the position, and optionally other properties, such as the size or the number of points, as parameters. The alternative, which is to hard-code numbers all over your code, tends to make the code needlessly hard to read and modify.
```
<canvas width="600" height="200"></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
// Your code here.
</script>
```
-The trapezoid (1) is easy to draw using a path. Pick suitable center coordinates and add each of the four corners around that.
+The trapezoid (1) is easiest to draw using a path. Pick suitable center coordinates and add each of the four corners around that.
-The diamond (2) can be drawn the easy way, with a path, or the interesting way, with a `rotate` transformation. To use rotation, you will have to apply a trick similar to what we did in the `flipHorizontally` function. Because you want to rotate around the center of your rectangle and not around the point (0,0), you must first `translate` to there, then rotate, and then translate back.
+The diamond (2) can be drawn the straightforward way, with a path, or the interesting way, with a `rotate` transformation. To use rotation, you will have to apply a trick similar to what we did in the `flipHorizontally` function. Because you want to rotate around the center of your rectangle and not around the point (0,0), you must first `translate` to there, then rotate, and then translate back.
+
+Make sure you reset the transformation after drawing any shape that creates one.
For the zigzag (3) it becomes impractical to write a new call to `lineTo` for each line segment. Instead, you should use a loop. You can have each iteration draw either two line segments (right and then left again) or one, in which case you must use the evenness (`% 2`) of the loop index to determine whether to go left or right.
You'll also need a loop for the spiral (4). If you draw a series of points, with each point moving further along a circle around the spiral's center, you get a circle. If, during the loop, you vary the radius of the circle on which you are putting the current point and go around more than once, the result is a spiral.
-The star (5) depicted is built out of `quadraticCurveTo` lines. You could also draw one with straight lines. Divide a circle into eight pieces, or a piece for each point you want your star to have. Draw lines between these points, making them curve toward the center of the star. With `quadraticCurveTo`, you can use the center as the control point.
+The star (5) depicted is built out of `quadraticCurveTo` lines. You could also draw one with straight lines. Divide a circle into eight pieces for a star with eight points, or however many pieces you want. Draw lines between these points, making them curve toward the center of the star. With `quadraticCurveTo`, you can use the center as the control point.
### The pie chart
-[Earlier](16_canvas.html#pie_chart) in the chapter, we saw an example program that drew a pie chart. Modify this program so that the name of each category is shown next to the slice that represents it. Try to find a pleasing-looking way to automatically position this text, which would work for other data sets as well. You may assume that categories are no smaller than 5 percent (that is, there won't be a bunch of tiny ones next to each other).
+[Earlier](17_canvas.html#pie_chart) in the chapter, we saw an example program that drew a pie chart. Modify this program so that the name of each category is shown next to the slice that represents it. Try to find a pleasing-looking way to automatically position this text, which would work for other data sets as well. You may assume that categories are big enough to leave ample room for their labels.
-You might again need `Math.sin` and `Math.cos`, as described in the previous exercise.
+You might again need `Math.sin` and `Math.cos`, as described in [Chapter 14](14_dom.html#sin_cos).
```
<canvas width="600" height="300"></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
- var total = results.reduce(function(sum, choice) {
- return sum + choice.count;
- }, 0);
+ let cx = document.querySelector("canvas").getContext("2d");
+ let total = results
+ .reduce((sum, {count}) => sum + count, 0);
+ let currentAngle = -0.5 * Math.PI;
+ let centerX = 300, centerY = 150;
- var currentAngle = -0.5 * Math.PI;
- var centerX = 300, centerY = 150;
// Add code to draw the slice labels in this loop.
- results.forEach(function(result) {
- var sliceAngle = (result.count / total) * 2 * Math.PI;
+ for (let result of results) {
+ let sliceAngle = (result.count / total) * 2 * Math.PI;
cx.beginPath();
cx.arc(centerX, centerY, 100,
currentAngle, currentAngle + sliceAngle);
@@ -722,7 +702,7 @@ You might again need `Math.sin` and `Math.cos`, as described in the previous exe
cx.lineTo(centerX, centerY);
cx.fillStyle = result.color;
cx.fill();
- });
+ }
</script>
```
@@ -730,31 +710,32 @@ You will need to call `fillText` and set the context's `textAlign` and `textBase
A sensible way to position the labels would be to put the text on the line going from the center of the pie through the middle of the slice. You don't want to put the text directly against the side of the pie but rather move the text out to the side of the pie by a given number of pixels.
-The angle of this line is `currentAngle + 0.5 * sliceAngle`. The following code finds a position on this line, 120 pixels from the center:
+The angle of this line is `currentAngle + 0.&lt;wbr&gt;5 * sliceAngle`. The following code finds a position on this line, 120 pixels from the center:
```
-var middleAngle = currentAngle + 0.5 * sliceAngle;
-var textX = Math.cos(middleAngle) * 120 + centerX;
-var textY = Math.sin(middleAngle) * 120 + centerY;
+let middleAngle = currentAngle + 0.5 * sliceAngle;
+let textX = Math.cos(middleAngle) * 120 + centerX;
+let textY = Math.sin(middleAngle) * 120 + centerY;
```
For `textBaseline`, the value `"middle"` is probably appropriate when using this approach. What to use for `textAlign` depends on the side of the circle we are on. On the left, it should be `"right"`, and on the right, it should be `"left"` so that the text is positioned away from the pie.
-If you are not sure how to find out which side of the circle a given angle is on, look to the explanation of `Math.cos` in the previous exercise. The cosine of an angle tells us which x-coordinate it corresponds to, which in turn tells us exactly which side of the circle we are on.
+If you are not sure how to find out which side of the circle a given angle is on, look to the explanation of `Math.cos` in [Chapter 14](14_dom.html#sin_cos). The cosine of an angle tells us which x-coordinate it corresponds to, which in turn tells us exactly which side of the circle we are on.
### A bouncing ball
-Use the `requestAnimationFrame` technique that we saw in [Chapter 13](13_dom.html#animationFrame) and [Chapter 15](15_game.html#runAnimation) to draw a box with a bouncing ball in it. The ball moves at a constant speed and bounces off the box's sides when it hits them.
+Use the `requestAnimationFrame` technique that we saw in [Chapter 14](14_dom.html#animationFrame) and [Chapter 16](16_game.html#runAnimation) to draw a box with a bouncing ball in it. The ball moves at a constant speed and bounces off the box's sides when it hits them.
```
<canvas width="400" height="400"></canvas>
<script>
- var cx = document.querySelector("canvas").getContext("2d");
+ let cx = document.querySelector("canvas").getContext("2d");
- var lastTime = null;
+ let lastTime = null;
function frame(time) {
- if (lastTime != null)
+ if (lastTime != null) {
updateAnimation(Math.min(100, time - lastTime) / 1000);
+ }
lastTime = time;
requestAnimationFrame(frame);
}
@@ -766,15 +747,15 @@ Use the `requestAnimationFrame` technique that we saw in [Chapter 13](13_dom.htm
</script>
```
-A box is easy to draw with `strokeRect`. Define a variable that holds its size or define two variables if your box's width and height differ. To create a round ball, start a path, call `arc(x, y, radius, 0, 7)`, which creates an arc going from zero to more than a whole circle, and fill it.
+A box is easy to draw with `strokeRect`. Define a binding that holds its size or define two bindings if your box's width and height differ. To create a round ball, start a path and call `arc(x, y, radius, 0, 7)`, which creates an arc going from zero to more than a whole circle. Then fill the path.
-To model the ball's position and speed, you can use the `Vector` type from [Chapter 15](15_game.html#vector)(which is available on this page). Give it a starting speed, preferably one that is not purely vertical or horizontal, and every frame, multiply that speed with the amount of time that elapsed. When the ball gets too close to a vertical wall, invert the x component in its speed. Likewise, invert the y component when it hits a horizontal wall.
+To model the ball's position and speed, you can use the `Vec` class from [Chapter 16](16_game.html#vector) (which is available on this page). Give it a starting speed, preferably one that is not purely vertical or horizontal, and every frame, multiply that speed with the amount of time that elapsed. When the ball gets too close to a vertical wall, invert the x component in its speed. Likewise, invert the y component when it hits a horizontal wall.
After finding the ball's new position and speed, use `clearRect` to delete the scene and redraw it using the new position.
### Precomputed mirroring
-One unfortunate thing about transformations is that they slow down drawing of bitmaps. For vector graphics, the effect is less serious since only a few points (for example, the center of a circle) need to be transformed, after which drawing can happen as normal. For a bitmap image, the position of each pixel has to be transformed, and though it is possible that browsers will get more clever about this in the future, this currently causes a measurable increase in the time it takes to draw a bitmap.
+One unfortunate thing about transformations is that they slow down drawing of bitmaps. The position and size of each pixel has to be transformed, and though it is possible that browsers will get more clever about this in the future, this currently causes a measurable increase in the time it takes to draw a bitmap.
In a game like ours, where we are drawing only a single transformed sprite, this is a nonissue. But imagine that we need to draw hundreds of characters or thousands of rotating particles from an explosion.