~ outside the svg. For this page, I want interactive elements outside the svg so I'm putting the id on the ~

~.
#+BEGIN_SRC html
Line drawing
# Line drawing

This page is in the public domain.
Use it as you wish. Attribution is not necessary, but appreciated.
#+END_SRC
I'll omit the header and footer from the rest of the examples. Click the filename on the upper right to see the entire page up to that point. Using a ~viewBox~ on ~~ tells it the coordinate system to for drawing. We can use that to keep a consistent coordinate system even if the diagram is resized.
* Diagram
:PROPERTIES:
:CUSTOM_ID: diagram
:END:
Sometimes I'll add a diagram first and then add some text; other times I'll start with the text and then figure out the diagrams. For this page I'll start with a diagram.
The tutorial is about drawing lines on a square grid, so I need to draw a grid and also draw lines. I'll draw a grid with Javascript:
#+begin_export html
#+end_export
Although d3 has a pattern for creating multiple elements with =selectAll()= + =data()= + =enter()=, I don't need that here so I'm just creating the elements directly:
#+BEGIN_SRC js
const scale = 22;
let root = d3.select("#demo svg");
for (let x = 0; x < 25; x++) {
for (let y = 0; y < 10; y++) {
root.append('rect')
.attr('transform', `translate(${x*scale}, ${y*scale})`)
.attr('width', scale)
.attr('height', scale)
.attr('fill', "white")
.attr('stroke', "gray");
}
}
#+END_SRC
I tried a few different grid sizes. I could parameterize it and calculate it properly but often times I will hard-code it when I'm just starting out, and only calculate it if I need to. Here my svg is 550px wide and I picked squares that are 22px, so 25 of them fit across. Vertically I can fit 10 squares in 220px so I changed the svg height from 200 to 220 to fit.
Those of you who know SVG might choose to use =viewBox= or =transform= to change the coordinate system to place points at the center of each grid square instead of at the top left, and also to scale things so that each grid square is 1 unit across instead of =scale= pixels. I did this in the original article but I didn't for this tutorial.
* Algorithm
:PROPERTIES:
:CUSTOM_ID: algorithm
:END:
The main algorithm I'm trying to demonstrate on the page is drawing a line on a grid. I need to implement that algorithm and a visualization for it.
#+begin_export html
#+end_export
#+BEGIN_SRC js
let A = {x: 2, y: 2}, B = {x: 20, y: 8};
let N = Math.max(Math.abs(A.x-B.x), Math.abs(A.y-B.y));
for (let i = 0; i <= N; i++) {
let t = i / N;
let x = Math.round(A.x + (B.x - A.x) * t);
let y = Math.round(A.y + (B.y - A.y) * t);
root.append('rect')
.attr('transform', `translate(${x*scale}, ${y*scale})`)
.attr('width', scale-1)
.attr('height', scale-1)
.attr('fill', "hsl(0,40%,70%)");
}
#+END_SRC
Hooray, it works!
This is just the beginning. It's a working implementation of the algorithm and a working diagram. But it's not /interactive/.
* Interaction
:PROPERTIES:
:CUSTOM_ID: interaction
:END:
What I most often do for interaction is let the reader change the /inputs/ to the algorithm and then I show the /outputs/. For line drawing, the inputs are the two endpoints, =A= and =B= in the code.
#+begin_export html
#+end_export
#+BEGIN_SRC js
function makeDraggableCircle(point) {
let circle = root.append('circle')
.attr('class', "draggable")
.attr('r', scale*0.75)
.attr('fill', "hsl(0,50%,50%)")
.call(d3.drag().on('drag', onDrag));
function updatePosition() {
circle.attr('transform',
`translate(${(point.x+0.5)*scale} ${(point.y+0.5)*scale})`);
}
function onDrag() {
point.x = Math.floor(d3.event.x / scale);
point.y = Math.floor(d3.event.y / scale);
updatePosition();
}
updatePosition();
}
makeDraggableCircle(A);
makeDraggableCircle(B);
#+END_SRC
Great! It's pretty easy with [[https://github.com/d3/d3-drag][d3-drag]]. To help the reader know which elements are interactive, I set the CSS =cursor:move= over the draggable circles.
This code lets me update the inputs =A= and =B= but it doesn't recalculate the output line.
* Redraw function
:PROPERTIES:
:CUSTOM_ID: redraw
:END:
To be able to update the line, I need to move the drawing code into a function that I can call again, and I also need to reuse the == elements I've previously created. It's useful to use [[https://bost.ocks.org/mike/circles/][d3's enter-exit pattern]] here; it will let me reuse, create, or remove elements as my data changes. To use it, I need a container for the == elements; I put it in a variable ~gPoints~. I also need to separate the logic for the /algorithm/ (function ~pointsOnLine~) from the logic for /drawing/ (function ~redraw~).
#+begin_export html
#+end_export
#+BEGIN_SRC js
let A = {x: 2, y: 2}, B = {x: 20, y: 8};
let gPoints = root.append('g');
function pointsOnLine(P, Q) {
let points = [];
let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y));
for (let i = 0; i <= N; i++) {
let t = i / N;
let x = Math.round(P.x + (Q.x - P.x) * t);
let y = Math.round(P.y + (Q.y - P.y) * t);
points.push({x: x, y: y});
}
return points;
}
function redraw() {
let rects = gPoints.selectAll('rect').data(pointsOnLine(A, B));
rects.exit().remove();
rects.enter().append('rect')
.attr('width', scale-1)
.attr('height', scale-1)
.attr('fill', "hsl(0,40%,70%)")
.merge(rects)
.attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`);
}
function makeDraggableCircle(point) {
let circle = root.append('circle')
.attr('class', "draggable")
.attr('r', scale*0.75)
.attr('fill', "hsl(0,50%,50%)")
.call(d3.drag().on('drag', onDrag));
function updatePosition() {
circle.attr('transform',
`translate(${(point.x+0.5)*scale} ${(point.y+0.5)*scale})`);
}
function onDrag() {
point.x = Math.floor(d3.event.x / scale);
point.y = Math.floor(d3.event.y / scale);
updatePosition();
redraw();
}
updatePosition();
}
makeDraggableCircle(A);
makeDraggableCircle(B);
redraw();
#+END_SRC
Great! Now I have an interactive diagram. But this isn't an /explanation/.
* Steps
:PROPERTIES:
:CUSTOM_ID: steps
:END:
To explain how an algorithm works, I sometimes break it down into the steps of the execution and sometimes into the steps of the code. For a tutorial like [[https://www.redblobgames.com/pathfinding/a-star/introduction.html][my introduction to A*]], I showed the execution. For line drawing, I want to show the steps of the code:
- Linear interpolation of numbers
- Linear interpolation of points
- Number of steps in the line
- Snap to grid
Since I'm going to have multiple diagrams, it'll be useful to encapsulate all those global variables and functions into a /diagram object/.
#+begin_export html
#+end_export
#+BEGIN_SRC js
class Diagram {
constructor(containerId) {
this.A = {x: 2, y: 2};
this.B = {x: 20, y: 8};
this.parent = d3.select(`#${containerId} svg`);
this.gGrid = this.parent.append('g');
this.gPoints = this.parent.append('g');
this.gHandles = this.parent.append('g');
this.drawGrid();
this.makeDraggableCircle(this.A);
this.makeDraggableCircle(this.B);
this.update();
}
update() {
let rects = this.gPoints.selectAll('rect')
.data(pointsOnLine(this.A, this.B));
rects.exit().remove();
rects.enter().append('rect')
.attr('width', scale-1)
.attr('height', scale-1)
.attr('fill', "hsl(0,40%,70%)")
.merge(rects)
.attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`);
}
drawGrid() {
for (let x = 0; x < 25; x++) {
for (let y = 0; y < 10; y++) {
this.gGrid.append('rect')
.attr('transform', `translate(${x*scale}, ${y*scale})`)
.attr('width', scale)
.attr('height', scale)
.attr('fill', "white")
.attr('stroke', "gray");
}
}
}
makeDraggableCircle(P) {
let diagram = this;
let circle = this.gHandles.append('circle')
.attr('class', "draggable")
.attr('r', scale*0.75)
.attr('fill', "hsl(0,50%,50%)")
.call(d3.drag().on('drag', onDrag));
function updatePosition() {
circle.attr('transform',
`translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`);
}
function onDrag() {
P.x = Math.floor(d3.event.x / scale);
P.y = Math.floor(d3.event.y / scale);
updatePosition();
diagram.update();
}
updatePosition();
}
}
let diagram = new Diagram('demo');
#+END_SRC
A pattern is starting to form, but I haven't made use of it yet. There's a == for each visual layer:
- the grid
- the line
- the draggable handles
Each of these layers has some code to draw it initially and sometimes some code to update it. As I add more layers to the diagram I'll do something better with the draw and update code.
* Linear interpolation of numbers
:PROPERTIES:
:CUSTOM_ID: lerp-numbers
:END:
In this section I don't actually have a diagram, but I do have some interaction, so I'm going to use a diagram object anyway without an svg. I want to drag a number left and right to change it, and see how it affects some calculations. Take a look at Bret Victor's [[http://worrydream.com/Tangle/][Tangle library]] for inspiration. You might want to use his library directly. For this page I'm using [[https://github.com/d3/d3-drag][d3-drag]] instead.
How do I want this to work?
- I want to be able to “scrub” (drag) a number left/right.
- I want to choose the formatting (e.g. =1.00= vs =1.0= vs =1=).
- I want to be able to run the update function when the number changes.
There are other things that you may want for scrubbable numbers but these are all I need for this tutorial. Within a named ~

This is a tutorial about line drawing and line of sight on grids.

~ section I'll find all the ~~ and turn them into scrubable numbers stored in field XYZ of the diagram object.
#+begin_export html
#+end_export
Scrubbable numbers are cool but a little bit tricky. I'm using d3-drag to tell me how far left/right the mouse was dragged. Then I scale the relative mouse position from -100 pixels to +100 pixels to the desired low–high range, using a linear scaling (see =positionToValue=):
#+BEGIN_SRC js
makeScrubbableNumber(name, low, high, precision) {
let diagram = this;
let elements = diagram.root.selectAll(`[data-name='${name}']`);
let positionToValue = d3.scaleLinear()
.clamp(true)
.domain([-100, +100])
.range([low, high]);
function updateNumbers() {
elements.text(() => {
let format = `.${precision}f`;
return d3.format(format)(diagram[name]);
});
}
updateNumbers();
elements.call(d3.drag()
.subject(() => ({x: positionToValue.invert(diagram[name]), y: 0}))
.on('drag', () => {
diagram[name] = positionToValue(d3.event.x);
updateNumbers();
diagram.update();
}));
}
#+END_SRC
When the reader drags the number, I update the display of the number, and I also call the diagram's =update= function to update any other aspect of the diagram:
#+BEGIN_SRC js
let t = this.t;
function set(id, fmt, lo, hi) {
d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t)));
}
set("#lerp1", ".2f", 0, 1);
set("#lerp2", ".0f", 0, 100);
set("#lerp3", ".1f", 3, 5);
#+END_SRC
I set the CSS to =cursor:col-resize= so that the reader can see it's interactive.
This "diagram" doesn't really fit the model that the rest of the diagrams use so it is a bit hacky. That's ok though. Sometimes it's easier to keep the hack than to try to build a general abstraction that's only used once.
* Linear interpolation of points
:PROPERTIES:
:CUSTOM_ID: lerp-points
:END:
Using linear interpolation of numbers, I want to display the linear interpolation of points. I want a diagram that lets you modify 0 ≤ t ≤ 1, and shows the resulting point. Until now I had the final algorithm written in =pointsOnLine=. To split up the diagrams I also need to split the line drawing algorithm into separate steps.
#+BEGIN_SRC js
function lerp(start, end, t) {
return start + t * (end-start);
}
function lerpPoint(P, Q, t) {
return {x: lerp(P.x, Q.x, t),
y: lerp(P.y, Q.y, t)};
}
#+END_SRC
#+begin_export html
#+end_export
I'm starting to organize things in terms of layers, which have the /creation/ code and the /update/ code:
#+BEGIN_SRC js
addTrack() {
this.gTrack = this.parent.append('line')
.attr('fill', "none")
.attr('stroke', "gray")
.attr('stroke-width', 3);
}
updateTrack() {
this.gTrack
.attr('x1', (this.A.x + 0.5) * scale)
.attr('y1', (this.A.y + 0.5) * scale)
.attr('x2', (this.B.x + 0.5) * scale)
.attr('y2', (this.B.y + 0.5) * scale);
}
#+END_SRC
#+BEGIN_SRC js
addInterpolated() {
this.gInterpolated = this.parent.append('circle')
.attr('fill', "hsl(0,30%,50%)")
.attr('r', 5);
}
updateInterpolated() {
let interpolated = lerpPoint(this.A, this.B, this.t);
this.gInterpolated
.attr('cx', (interpolated.x + 0.5) * scale)
.attr('cy', (interpolated.y + 0.5) * scale);
}
#+END_SRC
Note that this diagram does /not/ show the line drawn on a grid. That's another reason I want to use diagram layers for this page. While working on this diagram I commented out the code for drawing the line.
* Layers
:PROPERTIES:
:CUSTOM_ID: layers
:END:
There are now two diagrams on the page. Both display the grid. The first diagram displays the grid line. The second diagram shows a non-grid line and also the interpolated point. Well, that's what should happen, but I broke the first diagram while making the second one work. There will be more diagrams soon. I need a way to make all of them work.
When I'm writing a tutorial that requires multiple diagrams, each with different features, I like to divide the diagrams into /layers/, and then stack them on top of each other. There are four layers in the previous diagram: /grid/, /track/, /interpolation point/, and /drag handle/. Click “Show layers” to see them:
#+begin_export html
#+end_export
I'll create each layer by a method that adds a == for a layer, then adds its /update/ function to the diagram object. Here's the code for managing the update functions:
#+BEGIN_SRC js
onUpdate(f) {
this._updateFunctions.push(f);
this.update();
}
update() {
this._updateFunctions.forEach((f) => f());
}
#+END_SRC
I no longer need to put the == elements into fields in the diagram object (e.g. =gGrid=, =gHandles=, etc.); they can remain local variables in the =add= functions. Look at =addTrack()= now:
#+BEGIN_SRC js
addTrack() {
let g = this.parent.append('g');
let line = g.append('line')
.attr('fill', "none")
.attr('stroke', "gray")
.attr('stroke-width', 3);
this.onUpdate(() => {
line
.attr('x1', (this.A.x + 0.5) * scale)
.attr('y1', (this.A.y + 0.5) * scale)
.attr('x2', (this.B.x + 0.5) * scale)
.attr('y2', (this.B.y + 0.5) * scale);
});
return this;
}
#+END_SRC
I can now assemble a diagram by calling the =addXYZ()= functions:
#+BEGIN_SRC js
let diagram3 = new Diagram('interpolate-t')
.addGrid()
.addTrack()
.addInterpolated(0.5)
.addHandles();
#+END_SRC
I /don't/ have a generic layer system that I use across my tutorials. I make one specific for each tutorial that needs it. Each tutorial's needs have been different. The one here is only 8 lines of code; it's not worth writing a separate library for that.
* Number of steps in the line
:PROPERTIES:
:CUSTOM_ID: number-of-steps
:END:
The third diagram has yet a different visualization layer, but it will be easier to implement now that I have layers. Until now I had the line drawing algorithm choose N. I need to separate that out too.
#+BEGIN_SRC js
function interpolationPoints(P, Q, N) {
let points = [];
for (let i = 0; i <= N; i++) {
let t = i / N;
points.push(lerpPoint(P, Q, t));
}
return points;
}
#+END_SRC
#+begin_export html
#+end_export
The drawing code turns out to be very similar to the previous case so I made it read either /t/ or /N/:
#+BEGIN_SRC js
addInterpolated(t, N) {
this.t = t;
this.N = N;
this.makeScrubbableNumber('t', 0.0, 1.0, 2);
this.makeScrubbableNumber('N', 1, 30, 0);
let g = this.parent.append('g');
this.onUpdate(() => {
let points = this.t != null? [lerpPoint(this.A, this.B, this.t)]
: this.N != null? interpolationPoints(this.A, this.B, this.N)
: [];
let circles = g.selectAll("circle").data(points);
circles.exit().remove();
circles.enter().append('circle')
.attr('fill', "hsl(0,30%,50%)")
.attr('r', 5)
.merge(circles)
.attr('transform',
(p) => `translate(${(p.x+0.5)*scale}, ${(p.y+0.5)*scale})`);
});
return this;
}
#+END_SRC
I also need to extend my scrubbable number implementation. Previously, I wanted floating point values but here I want it to round to an integer. An easy way to implement this is to =parseFloat= the formatted output; that way it works no matter how many digits I'm rounding to. See the =formatter= in =makeScrubbableNumber()=.
The labels are another layer:
#+BEGIN_SRC js
addInterpolationLabels() {
// only works if we already have called addInterpolated()
let g = this.parent.append('g');
this.onUpdate(() => {
let points = interpolationPoints(this.A, this.B, this.N);
var offset = Math.abs(this.B.y - this.A.y)
> Math.abs(this.B.x - this.A.x)
? {x: 0.8 * scale, y: 0} : {x: 0, y: -0.8 * scale};
let labels = g.selectAll("text").data(points);
labels.exit().remove();
labels.enter().append('text')
.attr('text-anchor', "middle")
.text((p, i) => i)
.merge(labels)
.attr('transform',
(p) => `translate(${p.x*scale},${p.y*scale})
translate(${offset.x},${offset.y})
translate(${0.5*scale},${0.75*scale})`);
});
return this;
}
#+END_SRC
The labels are overlapping the drag handles. I'll fix this soon.
* Snap to grid
:PROPERTIES:
:CUSTOM_ID: rounding
:END:
We already know how to round numbers to the nearest integer. To snap points to the grid we can round both the =x= and =y= values.
#+BEGIN_SRC js
function roundPoint(P) {
return {x: Math.round(P.x), y: Math.round(P.y) };
}
#+END_SRC
I drew this on another layer.
#+begin_export html
#+end_export
I also want to tell people when they've reached the optimal N, which is max(Δx,Δy).
This diagram has all the essential components but it could look nicer.
* Finishing touches
:PROPERTIES:
:CUSTOM_ID: extras
:END:
Although I have the essentials working, I usually spend some time making the diagrams look good and feel good.
- Special cases: the code I originally wrote fails when the line distance is zero, e.g. when the start and end points are the same. I added a special case in =interpolationPoints= for ~N == 0~.
- Bounds checking: the reader can drag the markers outside the grid; I restricted that movement.
- Ease of styling: it's often better to specify the styles in CSS so that I can override them for a specific diagram.
- Ease of choosing colors: it's usually better to use HSL colors than RGB colors; they're much easier to work with. My rule of thumb is to start with medium colors =hsl(h,50%,50%)= for any 0 ≤ h < 360, and then tweak saturation and lightness.
- Relative importance: the drag handles should have a bolder look than minor elements like the numeric labels on the interpolation points.
- Grid style: I think the grid looks better when the borders /aren't/ drawn. It also makes it easier to see the numeric labels.
- Track style: I made the "track" line wider, lighter, and translucent so that the reader can see the grid underneath.
- Midpoints style: with a larger track, I made the interpolation points smaller to fit inside the track.
- Drawn line style: I made the line squares translucent so the reader can see the track underneath. It also lets the reader see when the same square gets drawn twice.
- Drag handle style: the handles look nicer when they fit into the track, and they also stop interfering with the labels. The problem is then the mouse sensitive area becomes too small. I solved this by having /two/ circles. The visible circle is small and fits into the track; there's also an invisible circle that catches the mouse events.
- Scrubbable number style: the number is way too small and it's especially hard to get on touch devices. I made the area larger and forced it to keep a consistent size as the number of digits changes.
- Interaction feedback: I change the CSS =cursor= when the mouse is over an interactable element (drag handles and the scrubbable numbers). I also draw drop shadows. The combination makes the interactive elements respond immediately, telling the reader that they're the right things to click on.
#+begin_export html
#+end_export
There are lots more ways to make the page better but these will give a lot of benefit for little effort.
* Putting it all together
:PROPERTIES:
:CUSTOM_ID: final
:END:
Here's what the diagrams look like with the nicer styling.
#+begin_export html
#+end_export
Click the "13/index.html" link to see the full page and the "source" link to see the source code. The source code for the example tutorial is public domain, so please copy/fork it and use it to make your own tutorials!
This is my first full tutorial about making tutorials. Feedback? Comment below, or comment on the [[https://trello.com/c/VaeEm3iT/141-making-of-line-drawing-tutorial-v2][trello card]].
#+begin_export html
#+end_export
#+begin_footer
Created May 2017
with [[http://orgmode.org/][Emacs Org-mode]], from [[file:index.org][index.org]]. The diagrams use [[https://d3js.org/][d3.js v4]]. All the HTML, CSS, and Javascript on the page is available [[https://github.com/redblobgames/making-of-line-drawing][on github/redblobgames]].
Page last modified 2019-02-09.
#+end_footer