├── 1 ├── index.html └── line-drawing.js ├── 2 ├── index.html └── line-drawing.js ├── 3 ├── index.html └── line-drawing.js ├── 4 ├── index.html └── line-drawing.js ├── 5 ├── index.html └── line-drawing.js ├── 6 ├── index.html └── line-drawing.js ├── 7 ├── index.html └── line-drawing.js ├── 8 ├── index.html └── line-drawing.js ├── 9 ├── index.html └── line-drawing.js ├── 10 ├── index.html └── line-drawing.js ├── 11 ├── index.html └── line-drawing.js ├── 12 ├── index.html └── line-drawing.js ├── 13 ├── index.html └── line-drawing.js ├── LICENSE ├── README.md ├── exploded-view.css ├── index.org ├── making-of.js └── pagestyle.css /1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | This is a tutorial about line drawing and 15 | line of sight on grids. 16 |

17 |
18 | 19 | 20 |
21 | 22 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /1/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | -------------------------------------------------------------------------------- /10/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 |

Linear interpolation: numbers

24 |

25 | Let's look at linear interpolation, or lerp, for numbers: 26 |

27 |
28 |
29 | lerp(  0,   1, ) = 
30 | lerp(  0, 100, ) = 
31 | lerp(  3,   5, ) = 
32 | lerp(  5,   3, ) = 
33 |       
34 |
35 |

Linear interpolation: points

36 |

37 | Let's look at linear interpolation for points. We can separately 38 | linearly interpolate the x and y values, and it behaves as we 39 | might expect: 40 |

41 |
42 | 43 | 44 |

45 | Try varying t = . 46 |

47 |
48 |

Number of points

49 |

50 | Instead of picking 1 interpolation point, let's pick N of them: 51 |

52 |
53 | 54 | 55 |

56 | Try varying N = . 57 |

58 |
59 | 60 |
61 | 64 | CC0 65 | 66 | This page is in the public domain. 67 | Use it as you wish. Attribution is not necessary, but appreciated. 68 |
69 | 70 | 71 | -------------------------------------------------------------------------------- /10/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | function pointsOnLine(P, Q) { 8 | let points = []; 9 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 10 | for (let i = 0; i <= N; i++) { 11 | let t = i / N; 12 | let x = Math.round(P.x + (Q.x - P.x) * t); 13 | let y = Math.round(P.y + (Q.y - P.y) * t); 14 | points.push({x: x, y: y}); 15 | } 16 | return points; 17 | } 18 | 19 | function lerp(start, end, t) { 20 | return start + t * (end-start); 21 | } 22 | 23 | function lerpPoint(P, Q, t) { 24 | return {x: lerp(P.x, Q.x, t), 25 | y: lerp(P.y, Q.y, t)}; 26 | } 27 | 28 | function interpolationPoints(P, Q, N) { 29 | let points = []; 30 | for (let i = 0; i <= N; i++) { 31 | let t = i / N; 32 | points.push(lerpPoint(P, Q, t)); 33 | } 34 | return points; 35 | } 36 | 37 | 38 | class Diagram { 39 | constructor(containerId) { 40 | this.root = d3.select(`#${containerId}`); 41 | this.A = {x: 2, y: 2}; 42 | this.B = {x: 20, y: 8}; 43 | this.parent = this.root.select("svg"); 44 | this._updateFunctions = []; 45 | } 46 | 47 | onUpdate(f) { 48 | this._updateFunctions.push(f); 49 | this.update(); 50 | } 51 | 52 | update() { 53 | this._updateFunctions.forEach((f) => f()); 54 | } 55 | 56 | addGrid() { 57 | let g = this.parent.append('g'); 58 | for (let x = 0; x < 25; x++) { 59 | for (let y = 0; y < 10; y++) { 60 | g.append('rect') 61 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 62 | .attr('width', scale) 63 | .attr('height', scale) 64 | .attr('fill', "white") 65 | .attr('stroke', "gray"); 66 | } 67 | } 68 | return this; 69 | } 70 | 71 | addTrack() { 72 | let g = this.parent.append('g'); 73 | let line = g.append('line') 74 | .attr('fill', "none") 75 | .attr('stroke', "gray") 76 | .attr('stroke-width', 3); 77 | this.onUpdate(() => { 78 | line 79 | .attr('x1', (this.A.x + 0.5) * scale) 80 | .attr('y1', (this.A.y + 0.5) * scale) 81 | .attr('x2', (this.B.x + 0.5) * scale) 82 | .attr('y2', (this.B.y + 0.5) * scale); 83 | }); 84 | return this; 85 | } 86 | 87 | addLine() { 88 | let g = this.parent.append('g'); 89 | this.onUpdate(() => { 90 | let rects = g.selectAll('rect') 91 | .data(pointsOnLine(this.A, this.B)); 92 | rects.exit().remove(); 93 | rects.enter().append('rect') 94 | .attr('width', scale-1) 95 | .attr('height', scale-1) 96 | .attr('fill', "hsl(0,40%,70%)") 97 | .merge(rects) 98 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 99 | 100 | }); 101 | return this; 102 | } 103 | 104 | addLerpValues() { 105 | /* This is a hack, for the section that has scrubbable 106 | numbers but no actual diagram. It might've been better 107 | to make scrubbable numbers independent of the Diagram 108 | class but this was simpler, and often simple code with 109 | a hack wins over complex code */ 110 | this.t = 0.3; 111 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 112 | this.onUpdate(() => { 113 | let t = this.t; 114 | function set(id, fmt, lo, hi) { 115 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 116 | } 117 | set("#lerp1", ".2f", 0, 1); 118 | set("#lerp2", ".0f", 0, 100); 119 | set("#lerp3", ".1f", 3, 5); 120 | set("#lerp4", ".1f", 5, 3); 121 | }); 122 | return this; 123 | } 124 | 125 | addInterpolated(t, N) { 126 | this.t = t; 127 | this.N = N; 128 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 129 | this.makeScrubbableNumber('N', 1, 30, 0); 130 | let g = this.parent.append('g'); 131 | this.onUpdate(() => { 132 | let points = this.t != null? [lerpPoint(this.A, this.B, this.t)] 133 | : this.N != null? interpolationPoints(this.A, this.B, this.N) 134 | : []; 135 | let circles = g.selectAll("circle").data(points); 136 | circles.exit().remove(); 137 | circles.enter().append('circle') 138 | .attr('fill', "hsl(0,30%,50%)") 139 | .attr('r', 5) 140 | .merge(circles) 141 | .attr('transform', 142 | (p) => `translate(${(p.x+0.5)*scale}, ${(p.y+0.5)*scale})`); 143 | }); 144 | return this; 145 | } 146 | 147 | addInterpolationLabels() { 148 | // only works if we already have called addInterpolated() 149 | let g = this.parent.append('g'); 150 | this.onUpdate(() => { 151 | let points = interpolationPoints(this.A, this.B, this.N); 152 | var offset = Math.abs(this.B.y - this.A.y) 153 | > Math.abs(this.B.x - this.A.x) 154 | ? {x: 0.8 * scale, y: 0} : {x: 0, y: -0.8 * scale}; 155 | let labels = g.selectAll("text").data(points); 156 | labels.exit().remove(); 157 | labels.enter().append('text') 158 | .attr('text-anchor', "middle") 159 | .text((p, i) => i) 160 | .merge(labels) 161 | .attr('transform', 162 | (p) => `translate(${p.x*scale},${p.y*scale}) 163 | translate(${offset.x},${offset.y}) 164 | translate(${0.5*scale},${0.75*scale})`); 165 | }); 166 | return this; 167 | } 168 | 169 | addHandles() { 170 | let g = this.parent.append('g'); 171 | this.makeDraggableCircle(g, this.A); 172 | this.makeDraggableCircle(g, this.B); 173 | return this; 174 | } 175 | 176 | makeDraggableCircle(parent, P) { 177 | let diagram = this; 178 | let circle = parent.append('circle') 179 | .attr('class', "draggable") 180 | .attr('r', scale*0.75) 181 | .attr('fill', "hsl(0,50%,50%)") 182 | .call(d3.drag().on('drag', onDrag)); 183 | 184 | function updatePosition() { 185 | circle.attr('transform', 186 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 187 | } 188 | 189 | function onDrag() { 190 | P.x = Math.floor(d3.event.x / scale); 191 | P.y = Math.floor(d3.event.y / scale); 192 | updatePosition(); 193 | diagram.update(); 194 | } 195 | 196 | updatePosition(); 197 | } 198 | 199 | makeScrubbableNumber(name, low, high, precision) { 200 | let diagram = this; 201 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 202 | let positionToValue = d3.scaleLinear() 203 | .clamp(true) 204 | .domain([-100, +100]) 205 | .range([low, high]); 206 | let formatter = d3.format(`.${precision}f`); 207 | 208 | function updateNumbers() { 209 | elements.text(formatter(diagram[name])); 210 | } 211 | 212 | updateNumbers(); 213 | 214 | elements.call( 215 | d3.drag() 216 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 217 | .on('drag', () => { 218 | diagram[name] = parseFloat(formatter(positionToValue(d3.event.x))); 219 | updateNumbers(); 220 | diagram.update(); 221 | })); 222 | } 223 | } 224 | 225 | 226 | let diagram1 = new Diagram('demo') 227 | .addGrid() 228 | .addLine() 229 | .addHandles(); 230 | 231 | let diagram2 = new Diagram('linear-interpolation') 232 | .addLerpValues(); 233 | 234 | let diagram3 = new Diagram('interpolate-t') 235 | .addGrid() 236 | .addTrack() 237 | .addInterpolated(0.5) 238 | .addHandles(); 239 | 240 | let diagram4 = new Diagram('interpolate-N') 241 | .addGrid() 242 | .addTrack() 243 | .addInterpolated(null, 5) 244 | .addHandles() 245 | .addInterpolationLabels(); 246 | -------------------------------------------------------------------------------- /11/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 |

Linear interpolation: numbers

24 |

25 | Let's look at linear interpolation, or lerp, for numbers: 26 |

27 |
28 |
29 | lerp(  0,   1, ) = 
30 | lerp(  0, 100, ) = 
31 | lerp(  3,   5, ) = 
32 | lerp(  5,   3, ) = 
33 |       
34 |
35 |

Linear interpolation: points

36 |

37 | Let's look at linear interpolation for points. We can separately 38 | linearly interpolate the x and y values, and it behaves as we 39 | might expect: 40 |

41 |
42 | 43 | 44 |

45 | Try varying t = . 46 |

47 |
48 |

Number of points

49 |

50 | Instead of picking 1 interpolation point, let's pick N of them: 51 |

52 |
53 | 54 | 55 |

56 | Try varying N = . 57 |

58 |
59 |

Snap to grid

60 |

61 | Now let's round each of those N points to the closest grid square: 62 |

63 |
64 | 65 | 66 |

67 | Try varying N = until it reaches 68 | the optimal value of = max(Δx,Δy). 69 |

70 |
71 | 72 |
73 | 76 | CC0 77 | 78 | This page is in the public domain. 79 | Use it as you wish. Attribution is not necessary, but appreciated. 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /11/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | function lerp(start, end, t) { 8 | return start + t * (end-start); 9 | } 10 | 11 | function lerpPoint(P, Q, t) { 12 | return {x: lerp(P.x, Q.x, t), 13 | y: lerp(P.y, Q.y, t)}; 14 | } 15 | 16 | function interpolationPoints(P, Q, N) { 17 | let points = []; 18 | for (let i = 0; i <= N; i++) { 19 | let t = N == 0? 0 : i / N; 20 | points.push(lerpPoint(P, Q, t)); 21 | } 22 | return points; 23 | } 24 | 25 | function roundPoint(P) { 26 | return {x: Math.round(P.x), y: Math.round(P.y) }; 27 | } 28 | 29 | function lineDistance(A, B) { 30 | return Math.max(Math.abs(A.x - B.x), Math.abs(A.y - B.y)); 31 | } 32 | 33 | 34 | class Diagram { 35 | constructor(containerId) { 36 | this.root = d3.select(`#${containerId}`); 37 | this.A = {x: 2, y: 2}; 38 | this.B = {x: 20, y: 8}; 39 | this.parent = this.root.select("svg"); 40 | this._updateFunctions = []; 41 | } 42 | 43 | onUpdate(f) { 44 | this._updateFunctions.push(f); 45 | this.update(); 46 | } 47 | 48 | update() { 49 | this._updateFunctions.forEach((f) => f()); 50 | } 51 | 52 | addGrid() { 53 | let g = this.parent.append('g'); 54 | for (let x = 0; x < 25; x++) { 55 | for (let y = 0; y < 10; y++) { 56 | g.append('rect') 57 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 58 | .attr('width', scale) 59 | .attr('height', scale) 60 | .attr('fill', "white") 61 | .attr('stroke', "gray"); 62 | } 63 | } 64 | return this; 65 | } 66 | 67 | addTrack() { 68 | let g = this.parent.append('g'); 69 | let line = g.append('line') 70 | .attr('fill', "none") 71 | .attr('stroke', "gray") 72 | .attr('stroke-width', 3); 73 | this.onUpdate(() => { 74 | line 75 | .attr('x1', (this.A.x + 0.5) * scale) 76 | .attr('y1', (this.A.y + 0.5) * scale) 77 | .attr('x2', (this.B.x + 0.5) * scale) 78 | .attr('y2', (this.B.y + 0.5) * scale); 79 | }); 80 | return this; 81 | } 82 | 83 | addLine() { 84 | let g = this.parent.append('g'); 85 | this.onUpdate(() => { 86 | let rects = g.selectAll('rect') 87 | .data(pointsOnLine(this.A, this.B)); 88 | rects.exit().remove(); 89 | rects.enter().append('rect') 90 | .attr('width', scale-1) 91 | .attr('height', scale-1) 92 | .attr('fill', "hsl(0,40%,70%)") 93 | .merge(rects) 94 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 95 | 96 | }); 97 | return this; 98 | } 99 | 100 | addLerpValues() { 101 | /* This is a hack, for the section that has scrubbable 102 | numbers but no actual diagram. It might've been better 103 | to make scrubbable numbers independent of the Diagram 104 | class but this was simpler, and often simple code with 105 | a hack wins over complex code */ 106 | this.t = 0.3; 107 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 108 | this.onUpdate(() => { 109 | let t = this.t; 110 | function set(id, fmt, lo, hi) { 111 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 112 | } 113 | set("#lerp1", ".2f", 0, 1); 114 | set("#lerp2", ".0f", 0, 100); 115 | set("#lerp3", ".1f", 3, 5); 116 | set("#lerp4", ".1f", 5, 3); 117 | }); 118 | return this; 119 | } 120 | 121 | addInterpolated(t, N, radius) { 122 | this.t = t; 123 | this.N = N; 124 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 125 | this.makeScrubbableNumber('N', 1, 30, 0); 126 | let g = this.parent.append('g'); 127 | this.onUpdate(() => { 128 | let points = this.t != null? [lerpPoint(this.A, this.B, this.t)] 129 | : this.N != null? interpolationPoints(this.A, this.B, this.N) 130 | : []; 131 | let circles = g.selectAll("circle").data(points); 132 | circles.exit().remove(); 133 | circles.enter().append('circle') 134 | .attr('fill', "hsl(0,30%,50%)") 135 | .attr('r', radius) 136 | .merge(circles) 137 | .attr('transform', 138 | (p) => `translate(${(p.x+0.5)*scale}, ${(p.y+0.5)*scale})`); 139 | }); 140 | return this; 141 | } 142 | 143 | addInterpolationLabels() { 144 | // only works if we already have called addInterpolated() 145 | let g = this.parent.append('g'); 146 | this.onUpdate(() => { 147 | let points = interpolationPoints(this.A, this.B, this.N); 148 | var offset = Math.abs(this.B.y - this.A.y) 149 | > Math.abs(this.B.x - this.A.x) 150 | ? {x: 0.8 * scale, y: 0} : {x: 0, y: -0.8 * scale}; 151 | let labels = g.selectAll("text").data(points); 152 | labels.exit().remove(); 153 | labels.enter().append('text') 154 | .attr('text-anchor', "middle") 155 | .text((p, i) => i) 156 | .merge(labels) 157 | .attr('transform', 158 | (p) => `translate(${p.x*scale},${p.y*scale}) 159 | translate(${offset.x},${offset.y}) 160 | translate(${0.5*scale},${0.75*scale})`); 161 | }); 162 | return this; 163 | } 164 | 165 | addLine() { 166 | let g = this.parent.append('g'); 167 | this.onUpdate(() => { 168 | let N = this.N == null? lineDistance(this.A, this.B) : this.N; 169 | let points = interpolationPoints(this.A, this.B, N).map(roundPoint); 170 | let squares = g.selectAll("rect").data(points); 171 | squares.exit().remove(); 172 | squares.enter().append('rect') 173 | .attr('width', scale) 174 | .attr('height', scale) 175 | .attr('fill', "hsl(0,40%,70%)") 176 | .merge(squares) 177 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 178 | }); 179 | return this; 180 | } 181 | 182 | addHandles() { 183 | let g = this.parent.append('g'); 184 | this.makeDraggableCircle(g, this.A); 185 | this.makeDraggableCircle(g, this.B); 186 | return this; 187 | } 188 | 189 | makeDraggableCircle(parent, P) { 190 | let diagram = this; 191 | let circle = parent.append('circle') 192 | .attr('class', "draggable") 193 | .attr('r', scale*0.75) 194 | .attr('fill', "hsl(0,50%,50%)") 195 | .call(d3.drag().on('drag', onDrag)); 196 | 197 | function updatePosition() { 198 | circle.attr('transform', 199 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 200 | } 201 | 202 | function onDrag() { 203 | P.x = Math.floor(d3.event.x / scale); 204 | P.y = Math.floor(d3.event.y / scale); 205 | updatePosition(); 206 | diagram.update(); 207 | } 208 | 209 | updatePosition(); 210 | } 211 | 212 | makeScrubbableNumber(name, low, high, precision) { 213 | let diagram = this; 214 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 215 | let positionToValue = d3.scaleLinear() 216 | .clamp(true) 217 | .domain([-100, +100]) 218 | .range([low, high]); 219 | let formatter = d3.format(`.${precision}f`); 220 | 221 | function updateNumbers() { 222 | elements.text(formatter(diagram[name])); 223 | } 224 | 225 | updateNumbers(); 226 | 227 | elements.call( 228 | d3.drag() 229 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 230 | .on('drag', () => { 231 | diagram[name] = parseFloat(formatter(positionToValue(d3.event.x))); 232 | updateNumbers(); 233 | diagram.update(); 234 | })); 235 | } 236 | } 237 | 238 | 239 | let diagram1 = new Diagram('demo') 240 | .addGrid() 241 | .addLine() 242 | .addHandles(); 243 | 244 | let diagram2 = new Diagram('linear-interpolation') 245 | .addLerpValues(); 246 | 247 | let diagram3 = new Diagram('interpolate-t') 248 | .addGrid() 249 | .addTrack() 250 | .addInterpolated(0.5, null, 4) 251 | .addHandles(); 252 | 253 | let diagram4 = new Diagram('interpolate-N') 254 | .addGrid() 255 | .addTrack() 256 | .addInterpolated(null, 5, 4) 257 | .addHandles() 258 | .addInterpolationLabels(); 259 | 260 | let diagram5 = new Diagram('snap-to-grid') 261 | .addGrid() 262 | .addTrack() 263 | .addLine() 264 | .addInterpolated(null, 5, 2.5) 265 | .addHandles(); 266 | diagram5.onUpdate(() => { 267 | let distance = lineDistance(diagram5.A, diagram5.B); 268 | diagram5.root.selectAll(".optimal-N") 269 | .text(distance); 270 | }); 271 | -------------------------------------------------------------------------------- /12/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 43 | 44 | 45 |
46 |

Line drawing

47 |
48 |

49 | On a grid map, line drawing is useful for for visibility, the 50 | path of an arrow/bullet, and enemy AI. There are several fast 51 | algorithms for this but I prefer using a very simple algorithm. 52 | Here's the algorithm in action: 53 |

54 |
55 | 56 | 57 |
58 |

Linear interpolation: numbers

59 |

60 | Let's look at linear interpolation, or lerp, for numbers: 61 |

62 |
63 |
 64 | lerp(  0,   1, ) = 
 65 | lerp(  0, 100, ) = 
 66 | lerp(  3,   5, ) = 
 67 | lerp(  5,   3, ) = 
 68 |       
69 |
70 |

Linear interpolation: points

71 |

72 | Let's look at linear interpolation for points. We can separately 73 | linearly interpolate the x and y values, and it behaves as we 74 | might expect: 75 |

76 |
77 | 78 | 79 |

80 | Try varying t = . 81 |

82 |
83 |

Number of points

84 |

85 | Instead of picking 1 interpolation point, let's pick N of them: 86 |

87 |
88 | 89 | 90 |

91 | Try varying N = . 92 |

93 |
94 |

Snap to grid

95 |

96 | Now let's round each of those N points to the closest grid square: 97 |

98 |
99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 |

114 | Try varying N = until it reaches 115 | the optimal value of = max(Δx,Δy). 116 |

117 |
118 | 119 |
120 | 123 | CC0 124 | 125 | This page is in the public domain. 126 | Use it as you wish. Attribution is not necessary, but appreciated. 127 |
128 | 129 | 130 | -------------------------------------------------------------------------------- /12/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | function clamp(x, lo, hi) { 8 | if (x < lo) { x = lo; } 9 | if (x > hi) { x = hi; } 10 | return x; 11 | } 12 | 13 | function lerp(start, end, t) { 14 | return start + t * (end-start); 15 | } 16 | 17 | function lerpPoint(P, Q, t) { 18 | return {x: lerp(P.x, Q.x, t), 19 | y: lerp(P.y, Q.y, t)}; 20 | } 21 | 22 | function interpolationPoints(P, Q, N) { 23 | let points = []; 24 | for (let i = 0; i <= N; i++) { 25 | let t = N == 0? 0 : i / N; 26 | points.push(lerpPoint(P, Q, t)); 27 | } 28 | return points; 29 | } 30 | 31 | function roundPoint(P) { 32 | return {x: Math.round(P.x), y: Math.round(P.y) }; 33 | } 34 | 35 | function lineDistance(A, B) { 36 | return Math.max(Math.abs(A.x - B.x), Math.abs(A.y - B.y)); 37 | } 38 | 39 | 40 | class Diagram { 41 | constructor(containerId) { 42 | this.root = d3.select(`#${containerId}`); 43 | this.A = {x: 2, y: 2}; 44 | this.B = {x: 20, y: 8}; 45 | this.parent = this.root.select("svg"); 46 | this._updateFunctions = []; 47 | } 48 | 49 | onUpdate(f) { 50 | this._updateFunctions.push(f); 51 | this.update(); 52 | } 53 | 54 | update() { 55 | this._updateFunctions.forEach((f) => f()); 56 | } 57 | 58 | addGrid() { 59 | let g = this.parent.append('g').attr('class', "grid"); 60 | for (let x = 0; x < 25; x++) { 61 | for (let y = 0; y < 10; y++) { 62 | g.append('rect') 63 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 64 | .attr('width', scale) 65 | .attr('height', scale); 66 | } 67 | } 68 | return this; 69 | } 70 | 71 | addTrack() { 72 | let g = this.parent.append('g').attr('class', "track"); 73 | let line = g.append('line'); 74 | this.onUpdate(() => { 75 | line 76 | .attr('x1', (this.A.x + 0.5) * scale) 77 | .attr('y1', (this.A.y + 0.5) * scale) 78 | .attr('x2', (this.B.x + 0.5) * scale) 79 | .attr('y2', (this.B.y + 0.5) * scale); 80 | }); 81 | return this; 82 | } 83 | 84 | addLerpValues() { 85 | /* This is a hack, for the section that has scrubbable 86 | numbers but no actual diagram. It might've been better 87 | to make scrubbable numbers independent of the Diagram 88 | class but this was simpler, and often simple code with 89 | a hack wins over complex code */ 90 | this.t = 0.3; 91 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 92 | this.onUpdate(() => { 93 | let t = this.t; 94 | function set(id, fmt, lo, hi) { 95 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 96 | } 97 | set("#lerp1", ".2f", 0, 1); 98 | set("#lerp2", ".0f", 0, 100); 99 | set("#lerp3", ".1f", 3, 5); 100 | set("#lerp4", ".1f", 5, 3); 101 | }); 102 | return this; 103 | } 104 | 105 | addInterpolated(t, N, radius) { 106 | this.t = t; 107 | this.N = N; 108 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 109 | this.makeScrubbableNumber('N', 1, 30, 0); 110 | let g = this.parent.append('g').attr('class', "interpolated"); 111 | this.onUpdate(() => { 112 | let points = this.t != null? [lerpPoint(this.A, this.B, this.t)] 113 | : this.N != null? interpolationPoints(this.A, this.B, this.N) 114 | : []; 115 | let circles = g.selectAll("circle").data(points); 116 | circles.exit().remove(); 117 | circles.enter().append('circle') 118 | .attr('r', radius) 119 | .merge(circles) 120 | .attr('transform', 121 | (p) => `translate(${(p.x+0.5)*scale}, ${(p.y+0.5)*scale})`); 122 | }); 123 | return this; 124 | } 125 | 126 | addInterpolationLabels() { 127 | // only works if we already have called addInterpolated() 128 | let g = this.parent.append('g').attr('class', "interpolation-labels"); 129 | this.onUpdate(() => { 130 | let points = interpolationPoints(this.A, this.B, this.N); 131 | var offset = Math.abs(this.B.y - this.A.y) 132 | > Math.abs(this.B.x - this.A.x) 133 | ? {x: 0.8 * scale, y: 0} : {x: 0, y: -0.8 * scale}; 134 | let labels = g.selectAll("text").data(points); 135 | labels.exit().remove(); 136 | labels.enter().append('text') 137 | .attr('text-anchor', "middle") 138 | .text((p, i) => i) 139 | .merge(labels) 140 | .attr('transform', 141 | (p) => `translate(${p.x*scale},${p.y*scale}) 142 | translate(${offset.x},${offset.y}) 143 | translate(${0.5*scale},${0.75*scale})`); 144 | }); 145 | return this; 146 | } 147 | 148 | addLine() { 149 | let g = this.parent.append('g').attr('class', "rounded"); 150 | this.onUpdate(() => { 151 | let N = this.N == null? lineDistance(this.A, this.B) : this.N; 152 | let points = interpolationPoints(this.A, this.B, N).map(roundPoint); 153 | let squares = g.selectAll("rect").data(points); 154 | squares.exit().remove(); 155 | squares.enter().append('rect') 156 | .attr('width', scale) 157 | .attr('height', scale) 158 | .merge(squares) 159 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 160 | }); 161 | return this; 162 | } 163 | 164 | addHandles() { 165 | let g = this.parent.append('g').attr('class', "handles"); 166 | this.makeDraggableCircle(g, this.A); 167 | this.makeDraggableCircle(g, this.B); 168 | return this; 169 | } 170 | 171 | makeDraggableCircle(parent, P) { 172 | let diagram = this; 173 | let circle = parent.append('g') 174 | .attr('class', "draggable") 175 | .call(d3.drag().on('drag', onDrag)); 176 | circle.append('circle') 177 | .attr('class', "invisible") 178 | .attr('r', 20); 179 | circle.append('circle') 180 | .attr('class', "visible") 181 | .attr('r', 6.5); 182 | 183 | function updatePosition() { 184 | circle.attr('transform', 185 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 186 | } 187 | 188 | function onDrag() { 189 | P.x = clamp(Math.floor(d3.event.x / scale), 0, 24); 190 | P.y = clamp(Math.floor(d3.event.y / scale), 0, 9); 191 | updatePosition(); 192 | diagram.update(); 193 | } 194 | 195 | updatePosition(); 196 | } 197 | 198 | makeScrubbableNumber(name, low, high, precision) { 199 | let diagram = this; 200 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 201 | let positionToValue = d3.scaleLinear() 202 | .clamp(true) 203 | .domain([-100, +100]) 204 | .range([low, high]); 205 | let formatter = d3.format(`.${precision}f`); 206 | 207 | function updateNumbers() { 208 | elements.text(formatter(diagram[name])); 209 | } 210 | 211 | updateNumbers(); 212 | 213 | elements.call( 214 | d3.drag() 215 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 216 | .on('drag', () => { 217 | diagram[name] = parseFloat(formatter(positionToValue(d3.event.x))); 218 | updateNumbers(); 219 | diagram.update(); 220 | })); 221 | } 222 | } 223 | 224 | 225 | let diagram1 = new Diagram('demo') 226 | .addGrid() 227 | .addLine() 228 | .addHandles(); 229 | 230 | let diagram2 = new Diagram('linear-interpolation') 231 | .addLerpValues(); 232 | 233 | let diagram3 = new Diagram('interpolate-t') 234 | .addGrid() 235 | .addTrack() 236 | .addInterpolated(0.5, null, 4) 237 | .addHandles(); 238 | 239 | let diagram4 = new Diagram('interpolate-N') 240 | .addGrid() 241 | .addTrack() 242 | .addInterpolated(null, 5, 4) 243 | .addHandles() 244 | .addInterpolationLabels(); 245 | 246 | let diagram5 = new Diagram('snap-to-grid') 247 | .addGrid() 248 | .addTrack() 249 | .addLine() 250 | .addInterpolated(null, 5, 2.5) 251 | .addHandles() 252 | .addInterpolationLabels(); 253 | diagram5.onUpdate(() => { 254 | let distance = lineDistance(diagram5.A, diagram5.B); 255 | diagram5.root.selectAll(".optimal-N") 256 | .text(distance); 257 | }); 258 | -------------------------------------------------------------------------------- /13/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 37 | 38 | 39 |
40 |

Line drawing

41 |
42 |

43 | On a grid map, line drawing is useful for for visibility, the 44 | path of an arrow/bullet, and enemy AI. There are several fast 45 | algorithms for this but I prefer using a very simple algorithm. 46 | Here's the algorithm in action: 47 |

48 |
49 | 50 | 51 |
52 |

Linear interpolation: numbers

53 |

54 | Let's look at linear interpolation, or lerp, for numbers: 55 |

56 |
57 |
 58 | lerp(  0,   1, ) = 
 59 | lerp(  0, 100, ) = 
 60 | lerp(  3,   5, ) = 
 61 | lerp(  5,   3, ) = 
 62 |       
63 |
64 |

Linear interpolation: points

65 |

66 | Let's look at linear interpolation for points. We can separately 67 | linearly interpolate the x and y values, and it behaves as we 68 | might expect: 69 |

70 |
71 | 72 | 73 |

74 | Try varying t = . 75 |

76 |
77 |

Number of points

78 |

79 | Instead of picking 1 interpolation point, let's pick N of them: 80 |

81 |
82 | 83 | 84 |

85 | Try varying N = . 86 |

87 |
88 |

Snap to grid

89 |

90 | Now let's round each of those N points to the closest grid square: 91 |

92 |
93 | 94 | 95 |

96 | Try varying N = until it reaches 97 | the optimal value of = max(Δx,Δy). 98 |

99 |
100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 |
117 | 120 | CC0 121 | 122 | This page is in the public domain. 123 | Use it as you wish. Attribution is not necessary, but appreciated. 124 |
125 | 126 | 127 | -------------------------------------------------------------------------------- /13/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | function clamp(x, lo, hi) { 8 | if (x < lo) { x = lo; } 9 | if (x > hi) { x = hi; } 10 | return x; 11 | } 12 | 13 | function lerp(start, end, t) { 14 | return start + t * (end-start); 15 | } 16 | 17 | function lerpPoint(P, Q, t) { 18 | return {x: lerp(P.x, Q.x, t), 19 | y: lerp(P.y, Q.y, t)}; 20 | } 21 | 22 | function interpolationPoints(P, Q, N) { 23 | let points = []; 24 | for (let i = 0; i <= N; i++) { 25 | let t = N == 0? 0 : i / N; 26 | points.push(lerpPoint(P, Q, t)); 27 | } 28 | return points; 29 | } 30 | 31 | function roundPoint(P) { 32 | return {x: Math.round(P.x), y: Math.round(P.y) }; 33 | } 34 | 35 | function lineDistance(A, B) { 36 | return Math.max(Math.abs(A.x - B.x), Math.abs(A.y - B.y)); 37 | } 38 | 39 | 40 | class Diagram { 41 | constructor(containerId) { 42 | this.root = d3.select(`#${containerId}`); 43 | this.A = {x: 2, y: 2}; 44 | this.B = {x: 20, y: 8}; 45 | this.parent = this.root.select("svg"); 46 | this._updateFunctions = []; 47 | } 48 | 49 | onUpdate(f) { 50 | this._updateFunctions.push(f); 51 | this.update(); 52 | } 53 | 54 | update() { 55 | this._updateFunctions.forEach((f) => f()); 56 | } 57 | 58 | addGrid() { 59 | let g = this.parent.append('g').attr('class', "grid"); 60 | for (let x = 0; x < 25; x++) { 61 | for (let y = 0; y < 10; y++) { 62 | g.append('rect') 63 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 64 | .attr('width', scale) 65 | .attr('height', scale); 66 | } 67 | } 68 | return this; 69 | } 70 | 71 | addTrack() { 72 | let g = this.parent.append('g').attr('class', "track"); 73 | let line = g.append('line'); 74 | this.onUpdate(() => { 75 | line 76 | .attr('x1', (this.A.x + 0.5) * scale) 77 | .attr('y1', (this.A.y + 0.5) * scale) 78 | .attr('x2', (this.B.x + 0.5) * scale) 79 | .attr('y2', (this.B.y + 0.5) * scale); 80 | }); 81 | return this; 82 | } 83 | 84 | addLerpValues() { 85 | /* This is a hack, for the section that has scrubbable 86 | numbers but no actual diagram. It might've been better 87 | to make scrubbable numbers independent of the Diagram 88 | class but this was simpler, and often simple code with 89 | a hack wins over complex code */ 90 | this.t = 0.3; 91 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 92 | this.onUpdate(() => { 93 | let t = this.t; 94 | function set(id, fmt, lo, hi) { 95 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 96 | } 97 | set("#lerp1", ".2f", 0, 1); 98 | set("#lerp2", ".0f", 0, 100); 99 | set("#lerp3", ".1f", 3, 5); 100 | set("#lerp4", ".1f", 5, 3); 101 | }); 102 | return this; 103 | } 104 | 105 | addInterpolated(t, N, radius) { 106 | this.t = t; 107 | this.N = N; 108 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 109 | this.makeScrubbableNumber('N', 1, 30, 0); 110 | let g = this.parent.append('g').attr('class', "interpolated"); 111 | this.onUpdate(() => { 112 | let points = this.t != null? [lerpPoint(this.A, this.B, this.t)] 113 | : this.N != null? interpolationPoints(this.A, this.B, this.N) 114 | : []; 115 | let circles = g.selectAll("circle").data(points); 116 | circles.exit().remove(); 117 | circles.enter().append('circle') 118 | .attr('r', radius) 119 | .merge(circles) 120 | .attr('transform', 121 | (p) => `translate(${(p.x+0.5)*scale}, ${(p.y+0.5)*scale})`); 122 | }); 123 | return this; 124 | } 125 | 126 | addInterpolationLabels() { 127 | // only works if we already have called addInterpolated() 128 | let g = this.parent.append('g').attr('class', "interpolation-labels"); 129 | this.onUpdate(() => { 130 | let points = interpolationPoints(this.A, this.B, this.N); 131 | var offset = Math.abs(this.B.y - this.A.y) 132 | > Math.abs(this.B.x - this.A.x) 133 | ? {x: 0.8 * scale, y: 0} : {x: 0, y: -0.8 * scale}; 134 | let labels = g.selectAll("text").data(points); 135 | labels.exit().remove(); 136 | labels.enter().append('text') 137 | .attr('text-anchor', "middle") 138 | .text((p, i) => i) 139 | .merge(labels) 140 | .attr('transform', 141 | (p) => `translate(${p.x*scale},${p.y*scale}) 142 | translate(${offset.x},${offset.y}) 143 | translate(${0.5*scale},${0.75*scale})`); 144 | }); 145 | return this; 146 | } 147 | 148 | addLine() { 149 | let g = this.parent.append('g').attr('class', "rounded"); 150 | this.onUpdate(() => { 151 | let N = this.N == null? lineDistance(this.A, this.B) : this.N; 152 | let points = interpolationPoints(this.A, this.B, N).map(roundPoint); 153 | let squares = g.selectAll("rect").data(points); 154 | squares.exit().remove(); 155 | squares.enter().append('rect') 156 | .attr('width', scale) 157 | .attr('height', scale) 158 | .merge(squares) 159 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 160 | }); 161 | return this; 162 | } 163 | 164 | addHandles() { 165 | let g = this.parent.append('g').attr('class', "handles"); 166 | this.makeDraggableCircle(g, this.A); 167 | this.makeDraggableCircle(g, this.B); 168 | return this; 169 | } 170 | 171 | makeDraggableCircle(parent, P) { 172 | let diagram = this; 173 | let circle = parent.append('g') 174 | .attr('class', "draggable") 175 | .call(d3.drag().on('drag', onDrag)); 176 | circle.append('circle') 177 | .attr('class', "invisible") 178 | .attr('r', 20); 179 | circle.append('circle') 180 | .attr('class', "visible") 181 | .attr('r', 6.5); 182 | 183 | function updatePosition() { 184 | circle.attr('transform', 185 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 186 | } 187 | 188 | function onDrag() { 189 | P.x = clamp(Math.floor(d3.event.x / scale), 0, 24); 190 | P.y = clamp(Math.floor(d3.event.y / scale), 0, 9); 191 | updatePosition(); 192 | diagram.update(); 193 | } 194 | 195 | updatePosition(); 196 | } 197 | 198 | makeScrubbableNumber(name, low, high, precision) { 199 | let diagram = this; 200 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 201 | let positionToValue = d3.scaleLinear() 202 | .clamp(true) 203 | .domain([-100, +100]) 204 | .range([low, high]); 205 | let formatter = d3.format(`.${precision}f`); 206 | 207 | function updateNumbers() { 208 | elements.text(formatter(diagram[name])); 209 | } 210 | 211 | updateNumbers(); 212 | 213 | elements.call( 214 | d3.drag() 215 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 216 | .on('drag', () => { 217 | diagram[name] = parseFloat(formatter(positionToValue(d3.event.x))); 218 | updateNumbers(); 219 | diagram.update(); 220 | })); 221 | } 222 | } 223 | 224 | 225 | let diagram1 = new Diagram('demo') 226 | .addGrid() 227 | .addLine() 228 | .addHandles(); 229 | 230 | let diagram2 = new Diagram('linear-interpolation') 231 | .addLerpValues(); 232 | 233 | let diagram3 = new Diagram('interpolate-t') 234 | .addGrid() 235 | .addTrack() 236 | .addInterpolated(0.5, null, 4) 237 | .addHandles(); 238 | 239 | let diagram4 = new Diagram('interpolate-N') 240 | .addGrid() 241 | .addTrack() 242 | .addInterpolated(null, 5, 4) 243 | .addHandles() 244 | .addInterpolationLabels(); 245 | 246 | let diagram5 = new Diagram('snap-to-grid') 247 | .addGrid() 248 | .addTrack() 249 | .addLine() 250 | .addInterpolated(null, 5, 2.5) 251 | .addHandles(); 252 | diagram5.onUpdate(() => { 253 | let distance = lineDistance(diagram5.A, diagram5.B); 254 | diagram5.root.selectAll(".optimal-N") 255 | .text(distance); 256 | }); 257 | -------------------------------------------------------------------------------- /2/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 | 22 | CC0 23 | 24 | This page is in the public domain. 25 | Use it as you wish. Attribution is not necessary, but appreciated. 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /2/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | let root = d3.select("#demo svg"); 7 | 8 | for (let x = 0; x < 25; x++) { 9 | for (let y = 0; y < 10; y++) { 10 | root.append('rect') 11 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 12 | .attr('width', scale) 13 | .attr('height', scale) 14 | .attr('fill', "white") 15 | .attr('stroke', "gray"); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /3/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 | 22 | CC0 23 | 24 | This page is in the public domain. 25 | Use it as you wish. Attribution is not necessary, but appreciated. 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /3/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | let root = d3.select("#demo svg"); 7 | 8 | for (let x = 0; x < 25; x++) { 9 | for (let y = 0; y < 10; y++) { 10 | root.append('rect') 11 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 12 | .attr('width', scale) 13 | .attr('height', scale) 14 | .attr('fill', "white") 15 | .attr('stroke', "gray"); 16 | } 17 | } 18 | 19 | let A = {x: 2, y: 2}, B = {x: 20, y: 8}; 20 | let N = Math.max(Math.abs(A.x-B.x), Math.abs(A.y-B.y)); 21 | for (let i = 0; i <= N; i++) { 22 | let t = i / N; 23 | let x = Math.round(A.x + (B.x - A.x) * t); 24 | let y = Math.round(A.y + (B.y - A.y) * t); 25 | root.append('rect') 26 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 27 | .attr('width', scale-1) 28 | .attr('height', scale-1) 29 | .attr('fill', "hsl(0,40%,70%)"); 30 | } 31 | -------------------------------------------------------------------------------- /4/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |
14 | 15 | 16 |
17 | 18 |
19 | 22 | CC0 23 | 24 | This page is in the public domain. 25 | Use it as you wish. Attribution is not necessary, but appreciated. 26 |
27 | 28 | 29 | -------------------------------------------------------------------------------- /4/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | let root = d3.select("#demo svg"); 7 | 8 | for (let x = 0; x < 25; x++) { 9 | for (let y = 0; y < 10; y++) { 10 | root.append('rect') 11 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 12 | .attr('width', scale) 13 | .attr('height', scale) 14 | .attr('fill', "white") 15 | .attr('stroke', "gray"); 16 | } 17 | } 18 | 19 | let A = {x: 2, y: 2}, B = {x: 20, y: 8}; 20 | let N = Math.max(Math.abs(A.x-B.x), Math.abs(A.y-B.y)); 21 | for (let i = 0; i <= N; i++) { 22 | let t = i / N; 23 | let x = Math.round(A.x + (B.x - A.x) * t); 24 | let y = Math.round(A.y + (B.y - A.y) * t); 25 | root.append('rect') 26 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 27 | .attr('width', scale-1) 28 | .attr('height', scale-1) 29 | .attr('fill', "hsl(0,40%,70%)"); 30 | } 31 | 32 | function makeDraggableCircle(point) { 33 | let circle = root.append('circle') 34 | .attr('class', "draggable") 35 | .attr('r', scale*0.75) 36 | .attr('fill', "hsl(0,50%,50%)") 37 | .call(d3.drag().on('drag', onDrag)); 38 | 39 | function updatePosition() { 40 | circle.attr('transform', 41 | `translate(${(point.x+0.5)*scale} ${(point.y+0.5)*scale})`); 42 | } 43 | 44 | function onDrag() { 45 | point.x = Math.floor(d3.event.x / scale); 46 | point.y = Math.floor(d3.event.y / scale); 47 | updatePosition(); 48 | } 49 | 50 | updatePosition(); 51 | } 52 | 53 | makeDraggableCircle(A); 54 | makeDraggableCircle(B); 55 | -------------------------------------------------------------------------------- /5/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 | 24 |
25 | 28 | CC0 29 | 30 | This page is in the public domain. 31 | Use it as you wish. Attribution is not necessary, but appreciated. 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /5/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | let root = d3.select("#demo svg"); 7 | 8 | for (let x = 0; x < 25; x++) { 9 | for (let y = 0; y < 10; y++) { 10 | root.append('rect') 11 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 12 | .attr('width', scale) 13 | .attr('height', scale) 14 | .attr('fill', "white") 15 | .attr('stroke', "gray"); 16 | } 17 | } 18 | 19 | let A = {x: 2, y: 2}, B = {x: 20, y: 8}; 20 | let gPoints = root.append('g'); 21 | 22 | function pointsOnLine(P, Q) { 23 | let points = []; 24 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 25 | for (let i = 0; i <= N; i++) { 26 | let t = i / N; 27 | let x = Math.round(P.x + (Q.x - P.x) * t); 28 | let y = Math.round(P.y + (Q.y - P.y) * t); 29 | points.push({x: x, y: y}); 30 | } 31 | return points; 32 | } 33 | 34 | function redraw() { 35 | let rects = gPoints.selectAll('rect').data(pointsOnLine(A, B)); 36 | rects.exit().remove(); 37 | rects.enter().append('rect') 38 | .attr('width', scale-1) 39 | .attr('height', scale-1) 40 | .attr('fill', "hsl(0,40%,70%)") 41 | .merge(rects) 42 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 43 | } 44 | 45 | 46 | function makeDraggableCircle(point) { 47 | let circle = root.append('circle') 48 | .attr('class', "draggable") 49 | .attr('r', scale*0.75) 50 | .attr('fill', "hsl(0,50%,50%)") 51 | .call(d3.drag().on('drag', onDrag)); 52 | 53 | function updatePosition() { 54 | circle.attr('transform', 55 | `translate(${(point.x+0.5)*scale} ${(point.y+0.5)*scale})`); 56 | } 57 | 58 | function onDrag() { 59 | point.x = Math.floor(d3.event.x / scale); 60 | point.y = Math.floor(d3.event.y / scale); 61 | updatePosition(); 62 | redraw(); 63 | } 64 | 65 | updatePosition(); 66 | } 67 | 68 | makeDraggableCircle(A); 69 | makeDraggableCircle(B); 70 | redraw(); 71 | -------------------------------------------------------------------------------- /6/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 | 24 |
25 | 28 | CC0 29 | 30 | This page is in the public domain. 31 | Use it as you wish. Attribution is not necessary, but appreciated. 32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /6/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | 8 | function pointsOnLine(P, Q) { 9 | let points = []; 10 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 11 | for (let i = 0; i <= N; i++) { 12 | let t = i / N; 13 | let x = Math.round(P.x + (Q.x - P.x) * t); 14 | let y = Math.round(P.y + (Q.y - P.y) * t); 15 | points.push({x: x, y: y}); 16 | } 17 | return points; 18 | } 19 | 20 | 21 | class Diagram { 22 | constructor(containerId) { 23 | this.A = {x: 2, y: 2}; 24 | this.B = {x: 20, y: 8}; 25 | this.parent = d3.select(`#${containerId} svg`); 26 | this.gGrid = this.parent.append('g'); 27 | this.gPoints = this.parent.append('g'); 28 | this.gHandles = this.parent.append('g'); 29 | 30 | this.drawGrid(); 31 | this.makeDraggableCircle(this.A); 32 | this.makeDraggableCircle(this.B); 33 | this.update(); 34 | } 35 | 36 | update() { 37 | let rects = this.gPoints.selectAll('rect') 38 | .data(pointsOnLine(this.A, this.B)); 39 | rects.exit().remove(); 40 | rects.enter().append('rect') 41 | .attr('width', scale-1) 42 | .attr('height', scale-1) 43 | .attr('fill', "hsl(0,40%,70%)") 44 | .merge(rects) 45 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 46 | } 47 | 48 | drawGrid() { 49 | for (let x = 0; x < 25; x++) { 50 | for (let y = 0; y < 10; y++) { 51 | this.gGrid.append('rect') 52 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 53 | .attr('width', scale) 54 | .attr('height', scale) 55 | .attr('fill', "white") 56 | .attr('stroke', "gray"); 57 | } 58 | } 59 | } 60 | 61 | makeDraggableCircle(P) { 62 | let diagram = this; 63 | let circle = this.gHandles.append('circle') 64 | .attr('class', "draggable") 65 | .attr('r', scale*0.75) 66 | .attr('fill', "hsl(0,50%,50%)") 67 | .call(d3.drag().on('drag', onDrag)); 68 | 69 | function updatePosition() { 70 | circle.attr('transform', 71 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 72 | } 73 | 74 | function onDrag() { 75 | P.x = Math.floor(d3.event.x / scale); 76 | P.y = Math.floor(d3.event.y / scale); 77 | updatePosition(); 78 | diagram.update(); 79 | } 80 | 81 | updatePosition(); 82 | } 83 | 84 | } 85 | 86 | 87 | let diagram = new Diagram('demo'); 88 | -------------------------------------------------------------------------------- /7/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 |

24 | Let's look at linear interpolation, or lerp, for numbers: 25 |

26 |
27 |
28 | lerp(  0,   1, ) = 
29 | lerp(  0, 100, ) = 
30 | lerp(  3,   5, ) = 
31 | lerp(  5,   3, ) = 
32 |       
33 |
34 | 35 |
36 | 39 | CC0 40 | 41 | This page is in the public domain. 42 | Use it as you wish. Attribution is not necessary, but appreciated. 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /7/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | 8 | function pointsOnLine(P, Q) { 9 | let points = []; 10 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 11 | for (let i = 0; i <= N; i++) { 12 | let t = i / N; 13 | let x = Math.round(P.x + (Q.x - P.x) * t); 14 | let y = Math.round(P.y + (Q.y - P.y) * t); 15 | points.push({x: x, y: y}); 16 | } 17 | return points; 18 | } 19 | 20 | 21 | function lerp(start, end, t) { 22 | return start + t * (end-start); 23 | } 24 | 25 | class Diagram { 26 | constructor(containerId) { 27 | this.root = d3.select(`#${containerId}`); 28 | this.t = 0.3; 29 | this.A = {x: 2, y: 2}; 30 | this.B = {x: 20, y: 8}; 31 | this.parent = d3.select(`#${containerId} svg`); 32 | this.gGrid = this.parent.append('g'); 33 | this.gPoints = this.parent.append('g'); 34 | this.gHandles = this.parent.append('g'); 35 | 36 | this.drawGrid(); 37 | this.makeDraggableCircle(this.A); 38 | this.makeDraggableCircle(this.B); 39 | 40 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 41 | this.update(); 42 | } 43 | 44 | update() { 45 | let rects = this.gPoints.selectAll('rect') 46 | .data(pointsOnLine(this.A, this.B)); 47 | rects.exit().remove(); 48 | rects.enter().append('rect') 49 | .attr('width', scale-1) 50 | .attr('height', scale-1) 51 | .attr('fill', "hsl(0,40%,70%)") 52 | .merge(rects) 53 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 54 | 55 | let t = this.t; 56 | function set(id, fmt, lo, hi) { 57 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 58 | } 59 | set("#lerp1", ".2f", 0, 1); 60 | set("#lerp2", ".0f", 0, 100); 61 | set("#lerp3", ".1f", 3, 5); 62 | set("#lerp4", ".1f", 5, 3); 63 | } 64 | 65 | drawGrid() { 66 | for (let x = 0; x < 25; x++) { 67 | for (let y = 0; y < 10; y++) { 68 | this.gGrid.append('rect') 69 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 70 | .attr('width', scale) 71 | .attr('height', scale) 72 | .attr('fill', "white") 73 | .attr('stroke', "gray"); 74 | } 75 | } 76 | } 77 | 78 | makeDraggableCircle(P) { 79 | let diagram = this; 80 | let circle = this.gHandles.append('circle') 81 | .attr('class', "draggable") 82 | .attr('r', scale*0.75) 83 | .attr('fill', "hsl(0,50%,50%)") 84 | .call(d3.drag().on('drag', onDrag)); 85 | 86 | function updatePosition() { 87 | circle.attr('transform', 88 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 89 | } 90 | 91 | function onDrag() { 92 | P.x = Math.floor(d3.event.x / scale); 93 | P.y = Math.floor(d3.event.y / scale); 94 | updatePosition(); 95 | diagram.update(); 96 | } 97 | 98 | updatePosition(); 99 | } 100 | 101 | makeScrubbableNumber(name, low, high, precision) { 102 | let diagram = this; 103 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 104 | let positionToValue = d3.scaleLinear() 105 | .clamp(true) 106 | .domain([-100, +100]) 107 | .range([low, high]); 108 | 109 | function updateNumbers() { 110 | elements.text(() => { 111 | let format = `.${precision}f`; 112 | return d3.format(format)(diagram[name]); 113 | }); 114 | } 115 | 116 | updateNumbers(); 117 | 118 | elements.call(d3.drag() 119 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 120 | .on('drag', () => { 121 | diagram[name] = positionToValue(d3.event.x); 122 | updateNumbers(); 123 | diagram.update(); 124 | })); 125 | } 126 | } 127 | 128 | 129 | let diagram1 = new Diagram('demo'); 130 | let diagram2 = new Diagram('linear-interpolation'); 131 | -------------------------------------------------------------------------------- /8/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 |

24 | Let's look at linear interpolation, or lerp, for numbers: 25 |

26 |
27 |
28 | lerp(  0,   1, ) = 
29 | lerp(  0, 100, ) = 
30 | lerp(  3,   5, ) = 
31 | lerp(  5,   3, ) = 
32 |       
33 |
34 |

35 | Let's look at linear interpolation for points. We can separately 36 | linearly interpolate the x and y values, and it behaves as we 37 | might expect: 38 |

39 |
40 | 41 | 42 |

43 | Try varying t = . 44 |

45 |
46 | 47 |
48 | 51 | CC0 52 | 53 | This page is in the public domain. 54 | Use it as you wish. Attribution is not necessary, but appreciated. 55 |
56 | 57 | 58 | -------------------------------------------------------------------------------- /8/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | 8 | function pointsOnLine(P, Q) { 9 | let points = []; 10 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 11 | for (let i = 0; i <= N; i++) { 12 | let t = i / N; 13 | let x = Math.round(P.x + (Q.x - P.x) * t); 14 | let y = Math.round(P.y + (Q.y - P.y) * t); 15 | points.push({x: x, y: y}); 16 | } 17 | return points; 18 | } 19 | 20 | function lerp(start, end, t) { 21 | return start + t * (end-start); 22 | } 23 | 24 | function lerpPoint(P, Q, t) { 25 | return {x: lerp(P.x, Q.x, t), 26 | y: lerp(P.y, Q.y, t)}; 27 | } 28 | 29 | class Diagram { 30 | constructor(containerId) { 31 | this.root = d3.select(`#${containerId}`); 32 | this.t = 0.3; 33 | this.A = {x: 2, y: 2}; 34 | this.B = {x: 20, y: 8}; 35 | this.parent = this.root.select("svg"); 36 | 37 | this.addGrid(); 38 | this.addTrack(); 39 | // this.addLine(); 40 | this.addInterpolated(); 41 | this.addHandles(); 42 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 43 | this.update(); 44 | } 45 | 46 | update() { 47 | this.updateTrack(); 48 | // this.updateLine(); 49 | this.updateInterpolated(); 50 | } 51 | 52 | addGrid() { 53 | this.gGrid = this.parent.append('g'); 54 | for (let x = 0; x < 25; x++) { 55 | for (let y = 0; y < 10; y++) { 56 | this.gGrid.append('rect') 57 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 58 | .attr('width', scale) 59 | .attr('height', scale) 60 | .attr('fill', "white") 61 | .attr('stroke', "gray"); 62 | } 63 | } 64 | } 65 | 66 | addTrack() { 67 | this.gTrack = this.parent.append('line') 68 | .attr('fill', "none") 69 | .attr('stroke', "gray") 70 | .attr('stroke-width', 3); 71 | } 72 | 73 | updateTrack() { 74 | this.gTrack 75 | .attr('x1', (this.A.x + 0.5) * scale) 76 | .attr('y1', (this.A.y + 0.5) * scale) 77 | .attr('x2', (this.B.x + 0.5) * scale) 78 | .attr('y2', (this.B.y + 0.5) * scale); 79 | } 80 | 81 | addLine() { 82 | this.gPoints = this.parent.append('g'); 83 | } 84 | 85 | updateLine() { 86 | let rects = this.gPoints.selectAll('rect') 87 | .data(pointsOnLine(this.A, this.B)); 88 | rects.exit().remove(); 89 | rects.enter().append('rect') 90 | .attr('width', scale-1) 91 | .attr('height', scale-1) 92 | .attr('fill', "hsl(0,40%,70%)") 93 | .merge(rects) 94 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 95 | 96 | } 97 | 98 | updateLerpValues() { 99 | /* This is a hack, for the section that has scrubbable 100 | numbers but no actual diagram. It might've been better 101 | to make scrubbable numbers independent of the Diagram 102 | class but this was simpler, and often simple code with 103 | a hack wins over complex code */ 104 | let t = this.t; 105 | function set(id, fmt, lo, hi) { 106 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 107 | } 108 | set("#lerp1", ".2f", 0, 1); 109 | set("#lerp2", ".0f", 0, 100); 110 | set("#lerp3", ".1f", 3, 5); 111 | set("#lerp4", ".1f", 5, 3); 112 | } 113 | 114 | addInterpolated() { 115 | this.gInterpolated = this.parent.append('circle') 116 | .attr('fill', "hsl(0,30%,50%)") 117 | .attr('r', 5); 118 | } 119 | 120 | updateInterpolated() { 121 | let interpolated = lerpPoint(this.A, this.B, this.t); 122 | this.gInterpolated 123 | .attr('cx', (interpolated.x + 0.5) * scale) 124 | .attr('cy', (interpolated.y + 0.5) * scale); 125 | } 126 | 127 | addHandles() { 128 | this.gHandles = this.parent.append('g'); 129 | this.makeDraggableCircle(this.A); 130 | this.makeDraggableCircle(this.B); 131 | } 132 | 133 | makeDraggableCircle(P) { 134 | let diagram = this; 135 | let circle = this.gHandles.append('circle') 136 | .attr('class', "draggable") 137 | .attr('r', scale*0.75) 138 | .attr('fill', "hsl(0,50%,50%)") 139 | .call(d3.drag().on('drag', onDrag)); 140 | 141 | function updatePosition() { 142 | circle.attr('transform', 143 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 144 | } 145 | 146 | function onDrag() { 147 | P.x = Math.floor(d3.event.x / scale); 148 | P.y = Math.floor(d3.event.y / scale); 149 | updatePosition(); 150 | diagram.update(); 151 | } 152 | 153 | updatePosition(); 154 | } 155 | 156 | makeScrubbableNumber(name, low, high, precision) { 157 | let diagram = this; 158 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 159 | let positionToValue = d3.scaleLinear() 160 | .clamp(true) 161 | .domain([-100, +100]) 162 | .range([low, high]); 163 | 164 | function updateNumbers() { 165 | elements.text(() => { 166 | let format = `.${precision}f`; 167 | return d3.format(format)(diagram[name]); 168 | }); 169 | } 170 | 171 | updateNumbers(); 172 | 173 | elements.call(d3.drag() 174 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 175 | .on('drag', () => { 176 | diagram[name] = positionToValue(d3.event.x); 177 | updateNumbers(); 178 | diagram.update(); 179 | })); 180 | } 181 | } 182 | 183 | 184 | // let diagram1 = new Diagram('demo'); 185 | // diagram1 doesn't work because the Diagram object is hard-coded to support interpolate-t 186 | 187 | let diagram2 = new Diagram('linear-interpolation'); 188 | let diagram3 = new Diagram('interpolate-t'); 189 | -------------------------------------------------------------------------------- /9/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Line drawing 6 | 7 | 8 | 9 | 10 |
11 |

Line drawing

12 |
13 |

14 | On a grid map, line drawing is useful for for visibility, the 15 | path of an arrow/bullet, and enemy AI. There are several fast 16 | algorithms for this but I prefer using a very simple algorithm. 17 | Here's the algorithm in action: 18 |

19 |
20 | 21 | 22 |
23 |

24 | Let's look at linear interpolation, or lerp, for numbers: 25 |

26 |
27 |
28 | lerp(  0,   1, ) = 
29 | lerp(  0, 100, ) = 
30 | lerp(  3,   5, ) = 
31 | lerp(  5,   3, ) = 
32 |       
33 |
34 |

35 | Let's look at linear interpolation for points. We can separately 36 | linearly interpolate the x and y values, and it behaves as we 37 | might expect: 38 |

39 |
40 | 41 | 42 |
43 | 44 |
45 | 48 | CC0 49 | 50 | This page is in the public domain. 51 | Use it as you wish. Attribution is not necessary, but appreciated. 52 |
53 | 54 | 55 | -------------------------------------------------------------------------------- /9/line-drawing.js: -------------------------------------------------------------------------------- 1 | // From http://www.redblobgames.com/making-of/line-drawing/ 2 | // Copyright 2017 Red Blob Games 3 | // License: Apache v2.0 4 | 5 | const scale = 22; 6 | 7 | function pointsOnLine(P, Q) { 8 | let points = []; 9 | let N = Math.max(Math.abs(P.x-Q.x), Math.abs(P.y-Q.y)); 10 | for (let i = 0; i <= N; i++) { 11 | let t = i / N; 12 | let x = Math.round(P.x + (Q.x - P.x) * t); 13 | let y = Math.round(P.y + (Q.y - P.y) * t); 14 | points.push({x: x, y: y}); 15 | } 16 | return points; 17 | } 18 | 19 | function lerp(start, end, t) { 20 | return start + t * (end-start); 21 | } 22 | 23 | function lerpPoint(P, Q, t) { 24 | return {x: lerp(P.x, Q.x, t), 25 | y: lerp(P.y, Q.y, t)}; 26 | } 27 | 28 | 29 | class Diagram { 30 | constructor(containerId) { 31 | this.root = d3.select(`#${containerId}`); 32 | this.A = {x: 2, y: 2}; 33 | this.B = {x: 20, y: 8}; 34 | this.parent = this.root.select("svg"); 35 | this._updateFunctions = []; 36 | } 37 | 38 | onUpdate(f) { 39 | this._updateFunctions.push(f); 40 | this.update(); 41 | } 42 | 43 | update() { 44 | this._updateFunctions.forEach((f) => f()); 45 | } 46 | 47 | addGrid() { 48 | let g = this.parent.append('g'); 49 | for (let x = 0; x < 25; x++) { 50 | for (let y = 0; y < 10; y++) { 51 | g.append('rect') 52 | .attr('transform', `translate(${x*scale}, ${y*scale})`) 53 | .attr('width', scale) 54 | .attr('height', scale) 55 | .attr('fill', "white") 56 | .attr('stroke', "gray"); 57 | } 58 | } 59 | return this; 60 | } 61 | 62 | addTrack() { 63 | let g = this.parent.append('g'); 64 | let line = g.append('line') 65 | .attr('fill', "none") 66 | .attr('stroke', "gray") 67 | .attr('stroke-width', 3); 68 | this.onUpdate(() => { 69 | line 70 | .attr('x1', (this.A.x + 0.5) * scale) 71 | .attr('y1', (this.A.y + 0.5) * scale) 72 | .attr('x2', (this.B.x + 0.5) * scale) 73 | .attr('y2', (this.B.y + 0.5) * scale); 74 | }); 75 | return this; 76 | } 77 | 78 | addLine() { 79 | let g = this.parent.append('g'); 80 | this.onUpdate(() => { 81 | let rects = g.selectAll('rect') 82 | .data(pointsOnLine(this.A, this.B)); 83 | rects.exit().remove(); 84 | rects.enter().append('rect') 85 | .attr('width', scale-1) 86 | .attr('height', scale-1) 87 | .attr('fill', "hsl(0,40%,70%)") 88 | .merge(rects) 89 | .attr('transform', (p) => `translate(${p.x*scale}, ${p.y*scale})`); 90 | 91 | }); 92 | return this; 93 | } 94 | 95 | addLerpValues() { 96 | /* This is a hack, for the section that has scrubbable 97 | numbers but no actual diagram. It might've been better 98 | to make scrubbable numbers independent of the Diagram 99 | class but this was simpler, and often simple code with 100 | a hack wins over complex code */ 101 | this.t = 0.3; 102 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 103 | this.onUpdate(() => { 104 | let t = this.t; 105 | function set(id, fmt, lo, hi) { 106 | d3.select(id).text(d3.format(fmt)(lerp(lo, hi, t))); 107 | } 108 | set("#lerp1", ".2f", 0, 1); 109 | set("#lerp2", ".0f", 0, 100); 110 | set("#lerp3", ".1f", 3, 5); 111 | set("#lerp4", ".1f", 5, 3); 112 | }); 113 | return this; 114 | } 115 | 116 | addInterpolated(t) { 117 | this.t = t; 118 | this.makeScrubbableNumber('t', 0.0, 1.0, 2); 119 | let g = this.parent.append('g'); 120 | let circle = g.append('circle') 121 | .attr('fill', "hsl(0,30%,50%)") 122 | .attr('r', 5); 123 | this.onUpdate(() => { 124 | let interpolated = lerpPoint(this.A, this.B, this.t); 125 | circle 126 | .attr('cx', (interpolated.x + 0.5) * scale) 127 | .attr('cy', (interpolated.y + 0.5) * scale); 128 | }); 129 | return this; 130 | } 131 | 132 | addHandles() { 133 | let g = this.parent.append('g'); 134 | this.makeDraggableCircle(g, this.A); 135 | this.makeDraggableCircle(g, this.B); 136 | return this; 137 | } 138 | 139 | makeDraggableCircle(parent, P) { 140 | let diagram = this; 141 | let circle = parent.append('circle') 142 | .attr('class', "draggable") 143 | .attr('r', scale*0.75) 144 | .attr('fill', "hsl(0,50%,50%)") 145 | .call(d3.drag().on('drag', onDrag)); 146 | 147 | function updatePosition() { 148 | circle.attr('transform', 149 | `translate(${(P.x+0.5)*scale} ${(P.y+0.5)*scale})`); 150 | } 151 | 152 | function onDrag() { 153 | P.x = Math.floor(d3.event.x / scale); 154 | P.y = Math.floor(d3.event.y / scale); 155 | updatePosition(); 156 | diagram.update(); 157 | } 158 | 159 | updatePosition(); 160 | } 161 | 162 | makeScrubbableNumber(name, low, high, precision) { 163 | let diagram = this; 164 | let elements = diagram.root.selectAll(`[data-name='${name}']`); 165 | let positionToValue = d3.scaleLinear() 166 | .clamp(true) 167 | .domain([-100, +100]) 168 | .range([low, high]); 169 | 170 | function updateNumbers() { 171 | elements.text(() => { 172 | let format = `.${precision}f`; 173 | return d3.format(format)(diagram[name]); 174 | }); 175 | } 176 | 177 | updateNumbers(); 178 | 179 | elements.call(d3.drag() 180 | .subject(() => ({x: positionToValue.invert(diagram[name]), y: 0})) 181 | .on('drag', () => { 182 | diagram[name] = positionToValue(d3.event.x); 183 | updateNumbers(); 184 | diagram.update(); 185 | })); 186 | } 187 | } 188 | 189 | 190 | // diagram1 works again, now that the Diagram class lets us choose which layers to use 191 | let diagram1 = new Diagram('demo') 192 | .addGrid() 193 | .addLine() 194 | .addHandles(); 195 | 196 | let diagram2 = new Diagram('linear-interpolation') 197 | .addLerpValues(); 198 | 199 | let diagram3 = new Diagram('interpolate-t') 200 | .addGrid() 201 | .addTrack() 202 | .addInterpolated(0.5) 203 | .addHandles(); 204 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # making-of-line-drawing-d3 2 | How to make an interactive tutorial like http://www.redblobgames.com/grids/line-drawing.html with d3.js 3 | -------------------------------------------------------------------------------- /exploded-view.css: -------------------------------------------------------------------------------- 1 | /* From https://www.redblobgames.com/making-of/line-drawing/ 2 | * Copyright 2017 Red Blob Games 3 | * CSS for the "exploded view" of the layers in a diagram 4 | */ 5 | 6 | .layer { 7 | transition: transform 2s; 8 | transform: none; 9 | } 10 | .rotated .layer-0 { transform: translate(40px,190px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 11 | .rotated .layer-1 { transform: translate(40px,170px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 12 | .rotated .layer-2 { transform: translate(40px,150px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 13 | .rotated .layer-3 { transform: translate(40px,130px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 14 | .rotated .layer-4 { transform: translate(40px,110px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 15 | .rotated .layer-5 { transform: translate(40px, 90px) rotateX(60deg) rotateZ(-30deg) scale(0.8); } 16 | 17 | /* Each layer will have a "glass" underneath its contents */ 18 | .layer .glass { 19 | pointer-events: none; 20 | transition: opacity 2s; 21 | opacity: 0.0; 22 | fill: hsl(120,10%,90%); 23 | fill-opacity: 0.25; 24 | stroke: hsl(120,50%,20%); 25 | stroke-opacity: 0.75; 26 | stroke-width: 2px; 27 | } 28 | .rotated .layer .glass { 29 | opacity: 1.0; 30 | } 31 | -------------------------------------------------------------------------------- /index.org: -------------------------------------------------------------------------------- 1 | #+TITLE: Making of: Line drawing tutorial 2 | #+DATE: <2017-05-15> 3 | 4 | #+begin_comment 5 | is used in my html preprocessor to put stuff into the section of the page; org-mode doesn't have a way to do that normally 6 | #+end_comment 7 | 8 | #+begin_export html 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 69 | #+end_export 70 | 71 | People ask me how I write my interactive tutorials. I can point at the HTML+CSS+JS but that doesn't show the /process/. On this page I'll recreate the first half of my [[https://www.redblobgames.com/grids/line-drawing/][line drawing tutorial]], showing the an implementation using [[https://d3js.org/][D3.js v4]]. The implementation style will be similar if you use jQuery. I also have [[https://www.redblobgames.com/making-of/circle-drawing/][another page showing an implementation using the Vue/React/Svelte declarative style]]. 72 | 73 | In this tutorial I'm implementing diagrams using SVG instead of Canvas or WebGL. I usually choose SVG in part because DOM-manipulating libraries like D3 and Vue make HTML and SVG easier to work with. 74 | 75 | The goal is to implement interactive diagrams like this: 76 | 77 | #+begin_export html 78 |
79 |