forked from marijnh/Eloquent-JavaScript
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy path16_canvas.txt
929 lines (775 loc) · 32.4 KB
/
16_canvas.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
:chap_num: 16
:prev_link: 15_game
:next_link: 17_FIXME
:load_files: ["js/15_game.js", "js/code/game_levels.js", "js/16_canvas.js"]
= Drawing on Canvas =
[quote,M.C. Escher]
____
Drawing is deception
____
Browsers provide several ways to create graphics. The simplest
approach is to use regular DOM elements, and make them look the way we
want by applying styles. This can get us quite far, as the previous
chapter showed. By adding partially transparent background images to
the nodes, we can make then look exactly the way we want to. It is
even possible to rotate or skew nodes by using the `transform` style.
But we'd still be using the DOM in a way not originally intended. Some
things, like drawing a line between arbitrary points, are extremely
awkward to do using regular HTML elements.
There are two alternatives. The first is DOM-based, but utilizing
_SVG_, rather than HTML elements, which you can think of as a
different dialect for describing documents, one that focuses on shapes
rather than text. An SVG document can be embedded inside an HTML
document, but also included through an `<img>` tag.
The second alternative is called a _canvas_. A canvas is a single DOM
node that encapsulates a whole 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. 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 area around
the shape) and redraw it with the shape in its new position.
== SVG ==
This book will not go into SVG in detail, but I will briefly try to
give you a general idea of the way it works. At the end of the
chapter, I'll come back to the trade-offs that must be considered when
deciding which mechanism is the most appropriate for a given
application.
This is an HTML document with a simple SVG picture inside of it:
[sandbox="svg"]
[source,text/html]
----
<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>
----
The `xmlns` attribute changes a node (and its children) to a different
_XML namespace_. This namespace, identified by a URL, is what
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. This changes the
`<circle>` element to be colored cyan instead:
[sandbox="svg"]
[source,javascript]
----
var circle = document.querySelector("circle");
circle.setAttribute("fill", "cyan");
----
== The canvas node ==
A canvas element is created with the `<canvas>` tag. You can give it
`width` and `height` attributes to determine its size, in pixels.
[sandbox="element"]
[source,text/html]
----
<p>Before canvas.</p>
<canvas width="200" height="100"></canvas>
<p>After canvas.</p>
----
A new canvas is empty, meaning it is entirely transparent, and thus
shows up simply 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 widely used OpenGL interface.
This book won't discuss WebGL. We will stick to two dimensions here.
But if you are interested in three-dimensional graphics, do look into
WebGL. It provides a very direct interface to modern graphics
hardware, and thus allows you to render complicated scenes very
efficiently, from JavaScript.
A context is created through the `getContext` method on the `<canvas>`
element.
[sandbox="element"]
[source,javascript]
----
var canvas = document.querySelector("canvas");
var context = canvas.getContext("2d");
context.fillStyle = "red";
context.fillRect(10, 10, 180, 80);
----
After creating the context object, the example draws a red rectangle
of 180 by 80 pixels, with its top left corner at coordinates 10,10.
Just like in HTML (and SVG), the coordinate system that the canvas
puts (0,0) at the top left corner, and the positive y axis goes down
from there, so (10,10) is ten pixels below and to the right of at
corner.
== Filling and stroking ==
In the terminology used by the canvas interface (as well as by SVG),
there are two things that can be done with a shape. It can be either
_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 `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 of these methods take any parameters beyond the dimensions of
the rectangle. The way in which the filling or stroking happens is not
determined by an argument to the method (as you might justly expect),
but rather by properties of the drawing context object.
Setting `fillStyle` changes the way shapes are filled. It can be set
to a string that specifies a color (any color understood by CSS can
also be used here).
The `strokeStyle` property work 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.
[source,text/html]
----
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
cx.strokeStyle = "blue";
cx.strokeRect(5, 5, 50, 50);
cx.lineWidth = 5;
cx.strokeRect(35, 5, 50, 50);
</script>
----
When no `width` or `height` attributes are specified, the canvas will
get a default width of 300 and height of 150 pixels.
== Paths ==
A path is a sequence of lines. The 2d canvas interface's approach to
specifying such a path is rather peculiar. It is done entirely through
side effects.
[source,text/html]
----
<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>
----
The example creates a path with a number of horizontal line segments,
and then strokes it using the `stroke` method. Each segment created
with `lineTo` starts at the path's _current_ position, which is the
end of the last segment, unless that position was moved using
`moveTo`.
When filling a path (using the `fill` method), each shape is filled
separately. A path can contain multiple shapes—each `moveTo` motion
starts a new one. If the path is not already _closed_ (its start and
end are in different positions), a line is added from its end to its
start, and the shape enclosed by the resulting line is filled.
[source,text/html]
----
<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>
----
This 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 we stroke the
path.
The `closePath` method explicitly closes a path by adding an actual
line segment back to its 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.
The `quadraticCurveTo` method draws a curve to a given point. To
determine the curvature of the line, it is given a control point as
well as a destination point. You can imagine this control point as
“attracting” the line, giving it its curve. The line won't go through
the control point. Rather, the direction of the line at its start and
end point will be such that it aligns with the line from there to the
control point. The following picture illustrates this:
[source,text/html]
----
<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>
----
We draw a quadratic curve from the left to the right, with (60,10) as
control point, and then draw two line segments, going through that
control point and back to the start of the line. The result somewhat
resembles a Star Trek insigna. You can see that the lines leaving the
lower corners start off in the same direction.
A similar kind of curve is drawn with `bezierCurve`. Instead of a
single control point, this one has two—one for each end of the line.
Here is a similar sketch to illustrates the behavior of such a curve:
[source,text/html]
----
<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(50, 10);
cx.lineTo(10, 10);
cx.closePath();
cx.stroke();
</script>
----
The two control points specify the direction at both ends of the curve.
The further they are away from their corresponding point, the more the
curve will “bulge” in that direction.
Such curves are unfortunately not very easy to work with—finding the
control points that provide the shape you are looking for can
sometimes be done manually, by trial and error, and sometimes be
computed.
Easier to reason about are arcs—fragments of a circle. The `arcTo`
method method takes five arguments. The first four act somewhat like
the arguments to `quadraticCurve`—the first two are a sort of control
point, and the second two are the line's destination. The fifth
argument provides the radius of the arc. The method will conceptually
take a corner, a line going to the control point and then the
destination point, and round its point so that it forms part of a
circle with the given radius. It then draws this rounded part, as well
as a line from the starting position to the start of the rounded part.
It will not draw the line from the end of the rounded part to the goal
position, though the word “to” in its same 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.
[source,text/html]
----
<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>
----
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 allow us to draw parts of a circle, but do
make drawing a whole circle slightly more involved. The angles are
measured in radians, not degrees. This means that a full circle has an
angle of 2π (`2 * Math.PI`, 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 zero and an end bigger than 2π (say, 7)
to draw a full circle.
[source,text/html]
----
<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>
----
The resulting picture contains a line from the left of the full circle
(first call to `arc`) to the left 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, and you have to use `moveTo`
(or start a new path) if you want to avoid this.
== Text ==
A 2d canvas drawing context provides the methods `fillText` and
`strokeText`. The latter can be used for outlining letters, but
usually `fillText` is what you want. It will fill the given text with
the current `fillColor`.
[source,text/html]
----
<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>
----
The size, style, and font in which to draw can be specified using the
`font` property. The example gives just a font size and family name.
You can add `italic` or `bold` to the start of the string to select a
style.
The last to 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 (the line on
which the letters “stand”, not counting hanging parts in letters like
“j” or “p”). The horizontal position is can be changed by setting the
`textAlign` property to `"end"` or `"center"`. The vertical position
by setting `textBaseLine` to `"top"`, `"middle"`, or `"bottom"`.
== 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 `<img>` element or from another
canvas (neither have to be visible in the actual document). The
example below creates a detached `<img>` element and loads an image
file into it. But it can not 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.
[source,text/html]
----
<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>
----
By default, `drawImage` will draw the image at its original size. You
can give it two additional arguments to determine the width and height
with which it is drawn.
When `drawImage` is given _nine_ arguments, it can be used to draw
only a fragment of an image. The second to fifth argument indicate the
rectangle (x, y, width, and height) in the source image that should be
copied, and the sixth to ninth argument 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 copy out the part you need. For example,
we have this picture containing a game character in multiple poses.
image::img/player.png[alt="Various poses of a game character"]
If we alternate 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 resets it back to transparent.
We know that each “sprite”, each sub-picture, is 16 pixels wide and 30
pixels high. The code below loads the image, and then sets up an
interval (repeated timer) to draw the next frame.
[source,text/html]
----
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 16, 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>
----
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 posture 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 him the other way round.
Calling the `scale` method will cause anything drawn after it to be
scaled. It takes two parameters, one to set a horizontal scale and one
to set a vertical scale.
[source,text/html]
----
<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>
----
Scaling will cause everything about the drawn image, including the
line width, to be stretched out or squeezed together as specified.
Scaling by a negative amount will flip the picture around. The
flipping happens around point (0,0), which means that it will also
flip the direction of the coordinate system. When a horizontal scaling
of -1 is applied, a shape drawn at x position 100 will end up at what
used to be position -100.
So to turn a picture around, we can not simply add `cx.scale(-1, 1)`
before the call to `drawImage`, since that would move our picture to
the left of the canvas (where it won't be visible).
One way to fix this is by adjusting the coordinates given to
`drawImage` to compensate for this (by drawing it at x position -50
instead of 0). Another way, which requires less changes to other code,
is to adjust the axis around which the scaling happens.
There are several other methods, besides `scale` that influence the
coordinate system for a canvas. It can be rotated with the `rotate`
method, and moved 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 (move) by 10 horizontal pixels twice, everything
will be drawn 20 pixels to the right. If we first move the origin to
(50,50) and then rotate by 20 degrees (0.1π in radians), that rotation
will happen around point (50,50).
image::img/transform.svg[alt="Stacking transformations"]
But if, instead, we first rotated by 20 degrees, and then translated
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.
To flip a picture around the vertical line at a given x position, we
can do the following:
// include_code
[source,javascript]
----
function flipHorizontally(context, around) {
context.translate(around, 0);
context.scale(-1, 1);
context.translate(-around, 0);
}
----
We first move the y axis to where we want our mirror to be, then apply
the mirroring, and finally move the y axis back to its proper place in
the mirrored universe. The picture below tries to explain why this
works.
image::img/mirror.svg[alt="Mirroring around a vertical line"]
This shows the coordinate systems, before and after mirroring in the
central line. If we draw a triangle at a positive x position, it
would, by default, be in the place where the green 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.
We can now draw a mirrored character at position (100,0) by flipping
the world around the character's vertical center:
[source,text/html]
----
<canvas></canvas>
<script>
var cx = document.querySelector("canvas").getContext("2d");
var img = document.createElement("img");
img.src = "img/player.png";
var spriteW = 16, spriteH = 30;
img.addEventListener("load", function() {
flipHorizontally(cx, 100 + spriteW / 2);
cx.drawImage(img, 0, 0, spriteW, spriteH,
100, 0, spriteW, spriteH);
});
</script>
----
== Storing and clearing transformations ==
Transformations stick around. Everything else we draw after drawing
that mirrored character would also be mirrored. That might be a bit of
a problem.
The `resetTransform` method (which takes no arguments) clears the
canvas' transformations entirely. It is also 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 save whatever the code that called the function was
using, then do its thing (on top of the existing transformation), and
then revert to what it started with.
The `save` and `restore` methods on the 2d canvas context perform this
kind of transformation tracking. They conceptually maintain 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 applied to the context.
The example below illustrates what you can do with a function that
changes the transformation and then calls another function (in this
case itself) that continues drawing with the given transformation.
The `branch` function draws a tree-like shape by first drawing a line,
and then moving the coordinate system to the end of the line and
calling itself twice, first rotated to the left, and then rotated to
the right. Every call reduces the length of the branch drawn, and the
recursion stops when the length drops below 8.
[source,text/html]
----
<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>
----
If the calls to `save` and `restore` were not there, the second
recursive call to `branch` would end up with the position and rotation
created by the first call—it would not be connected to the current
branch, but rather to the outmost, rightmost branch drawn by the first
call. The resulting shape might also be interesting, but it is
definitly not a tree.
== Back to the game ==
We now know enough about canvas drawing to start working on the
canvas-based display system for the game from the last chapter. The
new display will no longer be showing just colored boxes. Instead,
we'll use `drawImage` to draw the game's elements.
We must define an object type `CanvasDisplay`, supporting the same
interface (`drawFrame` and `clear` methods) as `DOMDisplay` from
Chapter 15.
This object tracks a little more information that `DOMDisplay`. Rather
than using the scroll position of its DOM node, 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, they keep facing the
direction they last moved in.
// include_code
[sandbox="game"]
[source,javascript]
----
function CanvasDisplay(parent, level) {
this.canvas = document.createElement("canvas");
this.canvas.width = 600;
this.canvas.height = 450;
parent.appendChild(this.canvas);
this.cx = this.canvas.getContext("2d");
this.level = level;
this.animationTime = 0;
this.flipPlayer = false;
this.viewport = {
left: 0,
top: 0,
width: this.canvas.width / scale,
height: this.canvas.height / scale
};
this.drawFrame(0);
}
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, even though `DOMDisplay` does not use it.
Our new `drawFrame` function uses it to keep track of the time the
game has been running (in seconds).
// include_code
[sandbox="game"]
[source,javascript]
----
CanvasDisplay.prototype.drawFrame = function(step) {
this.animationTime += step;
this.updateViewport();
this.clearDisplay();
this.drawBackground();
this.drawActors();
};
----
Other than that, the method updates the viewport for the current
player position, clears the whole canvas, and draws the background and
actors onto that. Note that this is different from the approach in
Chapter 15, where we drew the background once and scrolled the
wrapping DOM node 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). In most cases, the only way
to update a canvas display is to clear it and redraw the scene.
The `updateViewport` method is very 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.
// include_code
[sandbox="game"]
[source,javascript]
----
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));
if (center.x < view.left + margin)
view.left = Math.max(center.x - margin, 0);
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)
view.top = Math.max(center.y - margin, 0);
else if (center.y > view.top + view.height - margin)
view.top = Math.min(center.y + margin - view.height,
this.level.height - view.height);
};
----
The calls to `Math.max` and `Math.min` are used to ensure that the
viewport does not end up showing space outside of the level.
`Math.max(x, 0)` has the effect of ensuring the resulting number is
not less than zero. `Math.min`, similarly, ensures 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).
// include_code
[sandbox="game"]
[source,javascript]
----
CanvasDisplay.prototype.clearDisplay = function() {
if (this.level.status == "won")
this.cx.fillStyle = "#44bfff";
else if (this.level.status == "lost")
this.cx.fillStyle = "#2c88d6";
else
this.cx.fillStyle = "#34a6fb";
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.
// include_code
[sandbox="game"]
[source,javascript]
----
var 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 tilePos = tile == "lava" ? scale : 0;
this.cx.drawImage(otherSprites,
tilePos, 0, scale, scale,
screenX, screenY, scale, scale);
}
}
};
----
Tiles that are not empty (null) are drawn with `drawImage`. The
`otherSprites` image contains the pictures used for the elements that
are not the player. It contains, from left to right, the wall tile,
the laval tile, and then the sprite for a coin. 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 zero.
We do not bother waiting for the sprite image to load in this program.
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 much of a problem. Since we keep updating the screen, the correct
scene will appear as soon as the loading finishes.
ifdef::html_target[]
// FIXME this looks awful, needs a black background, ×2 scale
image::img/sprites.png[alt="Sprites for our game"]
endif::html_target[]
The walking character we used before 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. When the player is
standing still, we draw the leftmost sprite. During jumps (when the
vertical speed is not zero), we use the rightmost sprite. When
walking, we cycle through the first 8 sprites based on the display's
`animationTime` property. This is measured in seconds, and we want to
switch frames ofter than once a second, so the time is multiplied by
12 first.
// include_code
[sandbox="game"]
[source,javascript]
----
var playerSprites = document.createElement("img");
playerSprites.src = "img/player.png";
CanvasDisplay.prototype.drawPlayer = function(x, y, width,
height) {
var sprite = 0, player = this.level.player;
if (player.speed.x)
this.flipPlayer = player.speed.x < 0;
if (player.speed.y)
sprite = 8;
else if (player.speed.x)
sprite = Math.floor(this.animationTime * 12) % 8;
if (this.flipPlayer)
flipHorizontally(this.cx, x + width / 2);
this.cx.drawImage(playerSprites,
sprite * width, 0, width, height,
x, y, width, height);
if (this.flipPlayer)
this.cx.resetTransform();
};
----
We use `resetTransform` instead of `save` and `restore`, because our
drawing system will not use transformations in any other places, and
thus there is no transformation to save.
The function above is called by `drawActors`, which is responsible for
drawing the all the actors in the game.
// include_code
[sandbox="game"]
[source,javascript]
----
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;
if (actor.type == "player") {
this.drawPlayer(x, y, width, height);
} else {
var tile = (actor.type == "coin" ? 2 : 1) * scale;
this.cx.drawImage(otherSprites,
tile, 0, width, height,
x, y, width, height);
}
}, this);
};
----
When drawing something that is not the player, we look at its type to
find the offset of the correct sprite. The lava tile is found at
offset 20, and the coin sprite at 40 (two times `scale`).
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.
And to plug the new display into `runGame`:
[sandbox="game"]
[focus="yes"]
[source,text/html]
----
<body>
<script>
runGame(GAME_LEVELS, CanvasDisplay);
</script>
</body>
----
== Summary ==
FIXME
== Exercises ==
=== Precomputed mirroring ===
FIXME One unfortunate thing about transformations is that they considerably
slow down drawing of bitmaps. For vector graphics, the effect is less
serious, since there only a few points (for example the center of a
circle) need to be transformed, and then drawing continues 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, that currently