Skip to content

Commit d70c883

Browse files
committed
Add another exercise to Chapter 19
1 parent f66df6d commit d70c883

File tree

4 files changed

+115
-6
lines changed

4 files changed

+115
-6
lines changed

19_paint.md

Lines changed: 78 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ materials, ((skill)), or talent. You just start smearing.
3131

3232
{{index drawing, "select (HTML tag)", "canvas (HTML tag)", component}}
3333

34-
The interface for the drawing program shows a big `<canvas>` element
35-
on top, with a number of form ((field))s below it. The user draws on
36-
the ((picture)) by selecting a tool from a `<select>` field and then
34+
The interface for the application shows a big `<canvas>` element on
35+
top, with a number of form ((field))s below it. The user draws on the
36+
((picture)) by selecting a tool from a `<select>` field and then
3737
clicking or dragging across the canvas. There are ((tool))s for
3838
drawing single pixels or rectangles, for filling an area, and for
3939
picking a color from the picture.
@@ -350,7 +350,8 @@ events, and making sure we call `preventDefault` on the `"touchstart"`
350350
event to prevent ((panning)).
351351

352352
```{includeCode: true}
353-
PictureCanvas.prototype.touch = function(startEvent, onDown) {
353+
PictureCanvas.prototype.touch = function(startEvent,
354+
onDown) {
354355
let rect = this.dom.getBoundingClientRect();
355356
let pos = pointerPosition(startEvent.touches[0], rect);
356357
let onMove = onDown(pos);
@@ -876,8 +877,8 @@ const baseControls = [ToolSelect, ColorSelect, SaveButton,
876877
LoadButton, UndoButton];
877878
878879
function startPixelEditor({state=startState,
879-
tools=baseTools,
880-
controls=baseControls}) {
880+
tools=baseTools,
881+
controls=baseControls}) {
881882
let app = new PixelEditor(state, {
882883
tools,
883884
controls,
@@ -1025,6 +1026,77 @@ dispatch the appropriate update.
10251026

10261027
hint}}
10271028

1029+
### Efficient drawing
1030+
1031+
During regular drawing, the majority of work that our application does
1032+
happens in `drawPicture`. Creating a new state and updating the rest
1033+
of the DOM isn't very expensive, but repainting all the pixels is
1034+
quite a bit of work.
1035+
1036+
Find a way to make the `setState` method of `PictureCanvas` faster by
1037+
redrawing only the pixels that actually changed.
1038+
1039+
Remember that `drawPicture` is also used by the save button, so if you
1040+
change it, either make sure the changes don't break the old use, or
1041+
create a new version with a different name.
1042+
1043+
Also note that changing the size of a `<canvas>` element clears it,
1044+
making it entirely transparent again.
1045+
1046+
{{if interactive
1047+
1048+
```{test: no, lang: "text/html"}
1049+
<div></div>
1050+
<script>
1051+
// Change this method
1052+
PictureCanvas.prototype.setState = function(picture) {
1053+
if (this.picture == picture) return;
1054+
this.picture = picture;
1055+
drawPicture(this.picture, this.dom, scale);
1056+
}
1057+
1058+
// You may want to use or change this as well
1059+
function drawPicture(picture, canvas, scale) {
1060+
canvas.width = picture.width * scale;
1061+
canvas.height = picture.height * scale;
1062+
let cx = canvas.getContext("2d");
1063+
1064+
for (let y = 0; y < picture.height; y++) {
1065+
for (let x = 0; x < picture.width; x++) {
1066+
cx.fillStyle = picture.pixel(x, y);
1067+
cx.fillRect(x * scale, y * scale, scale, scale);
1068+
}
1069+
}
1070+
}
1071+
1072+
document.querySelector("div")
1073+
.appendChild(startPixelEditor({}));
1074+
</script>
1075+
```
1076+
1077+
if}}
1078+
1079+
{{hint
1080+
1081+
This exercise is a good example of how immutable data structures can
1082+
make code _faster_. Because we have both the old and the new picture,
1083+
we can compare them and only redraw the pixels that changed color,
1084+
saving over 99% of the drawing work in most cases.
1085+
1086+
You can either write a new function `updatePicture`, or have
1087+
`drawPicture` take an extra argument, which may be either undefined or
1088+
a picture. For each pixel, it checks whether a previous picture was
1089+
passed, and if that previous picture has the same color at this
1090+
position, and skips the pixel when that is the case.
1091+
1092+
Because the canvas gets cleared when we change its size, you should
1093+
also avoid touching its `width` and `height` properties when the old
1094+
and the new picture have the same size. If they do not, you can set
1095+
the binding holding the old picture to null, because you shouldn't
1096+
skip any pixels after you've changed the canvas size.
1097+
1098+
hint}}
1099+
10281100
### Circles
10291101

10301102
Define a new tool called `circle` that draws a filled circle when you
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<!doctype html>
2+
3+
<base href="http://eloquentjavascript.net/">
4+
<script src="code/chapter/19_paint.js"></script>
5+
6+
<div></div>
7+
<script>
8+
PictureCanvas.prototype.setState = function(picture) {
9+
if (this.picture == picture) return;
10+
drawPicture(picture, this.dom, scale, this.picture);
11+
this.picture = picture;
12+
}
13+
14+
function drawPicture(picture, canvas, scale, previous) {
15+
if (previous == null ||
16+
previous.width != picture.width ||
17+
previous.height != picture.height) {
18+
canvas.width = picture.width * scale;
19+
canvas.height = picture.height * scale;
20+
previous = null;
21+
}
22+
23+
let cx = canvas.getContext("2d");
24+
for (let y = 0; y < picture.height; y++) {
25+
for (let x = 0; x < picture.width; x++) {
26+
let color = picture.pixel(x, y);
27+
if (previous == null || previous.pixel(x, y) != color) {
28+
cx.fillStyle = color;
29+
cx.fillRect(x * scale, y * scale, scale, scale);
30+
}
31+
}
32+
}
33+
}
34+
35+
document.querySelector("div")
36+
.appendChild(startPixelEditor({}));
37+
</script>

0 commit comments

Comments
 (0)