├── AlexNet.html ├── AlexNet.js ├── FCNN.js ├── LICENSE ├── LeNet.html ├── LeNet.js ├── OrbitControls.js ├── Projector.js ├── README.md ├── SVGRenderer.js ├── about.html ├── example.svg ├── fonts ├── LICENSE ├── README └── helvetiker_regular.typeface.json ├── index.html ├── paper.bib ├── paper.md ├── paper.pdf └── util.js /AlexNet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 72 | 73 | NN SVG 74 | 75 | 76 | 77 | 78 |
79 | 80 |
81 | 82 |
83 |
84 |
85 |
86 | 87 |

NN-SVG

88 |

Publication-ready NN-architecture schematics. Download SVG

89 | 90 | 97 | 98 |
99 |
100 |
101 | 102 | 103 |

Style:

104 |
105 | 106 |
107 | 110 |
111 |
112 | 115 |
116 | The SVG renderer is required to download SVG, however the WebGL renderer is required to show tensor dimensions. 117 |
118 | 119 |
120 |
121 | 122 | 123 |
124 |
125 | 126 | 127 |
128 |
129 | 130 | 131 |
132 | 133 |
134 | 135 | 136 |
137 |
138 |
139 | 140 | 141 |
142 | 146 |
147 | 148 | 149 |
150 | 151 |
152 |
153 | 154 | 155 |
156 |
157 | 158 | 159 | 10 160 |
161 |
162 | 163 | 164 |
165 |
166 | 167 | 168 | 10 169 |
170 |
171 | 172 | 173 |
174 |
175 | 176 | 177 | 1 178 |
179 | 180 |
181 |
182 | 183 | 184 |
185 |
186 | 187 | 188 |
189 |
190 | 191 | 192 |
193 | 194 |
195 |

Architecture:

196 |
197 |

Height | Width | Depth | filter Height | filter Width

198 |
199 |
200 | 201 | 202 | 203 | 204 | 205 | 206 |
207 |
208 |
209 |
210 | 211 | 212 | 213 | 214 | 215 | 216 |
217 |
218 |
219 |
220 | 221 | 222 | 223 | 224 | 225 | 226 |
227 |
228 |
229 |
230 | 231 | 232 | 233 | 234 | 235 | 236 |
237 |
238 |
239 |
240 | 241 | 242 | 243 | 244 | 245 | 246 |
247 |
248 |
249 |
250 | 251 | 252 | 253 | 254 | 255 | 256 |
257 |
258 |
259 |
260 | 261 | 262 | 263 | 264 | 265 | 266 |
267 |
268 |
269 | 270 |
271 |
272 |

Vector Length

273 |
274 |
275 | 276 | 277 |
278 |
279 |
280 |
281 | 282 | 283 |
284 |
285 |
286 |
287 | 288 | 289 |
290 |
291 |
292 |
293 | 294 | 295 |
296 |
297 | 298 |
299 | 300 |
301 |
302 |
303 |
304 |
305 |
306 | 307 |
About
308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 463 | 464 | 465 | 466 | -------------------------------------------------------------------------------- /AlexNet.js: -------------------------------------------------------------------------------- 1 | 2 | function AlexNet() { 3 | 4 | // ///////////////////////////////////////////////////////////////////////////// 5 | // /////// Variables /////// 6 | // ///////////////////////////////////////////////////////////////////////////// 7 | 8 | var w = window.innerWidth; 9 | var h = window.innerHeight; 10 | 11 | var color1 = '#eeeeee'; 12 | var color2 = '#99ddff'; 13 | var color3 = '#ffbbbb'; 14 | 15 | var rectOpacity = 0.4; 16 | var filterOpacity = 0.4; 17 | var fontScale = 1; 18 | 19 | var line_material = new THREE.LineBasicMaterial( { 'color':0x000000 } ); 20 | var box_material = new THREE.MeshBasicMaterial( {'color':color1, 'side':THREE.DoubleSide, 'transparent':true, 'opacity':rectOpacity, 'depthWrite':false, 'needsUpdate':true} ); 21 | var conv_material = new THREE.MeshBasicMaterial( {'color':color2, 'side':THREE.DoubleSide, 'transparent':true, 'opacity':filterOpacity, 'depthWrite':false, 'needsUpdate':true} ); 22 | var pyra_material = new THREE.MeshBasicMaterial( {'color':color3, 'side':THREE.DoubleSide, 'transparent':true, 'opacity':filterOpacity, 'depthWrite':false, 'needsUpdate':true} ); 23 | 24 | var architecture = []; 25 | var architecture2 = []; 26 | var betweenLayers = 20; 27 | 28 | var logDepth = true; 29 | var depthScale = 10; 30 | var logWidth = true; 31 | var widthScale = 10; 32 | var logConvSize = false; 33 | var convScale = 1; 34 | 35 | var showDims = false; 36 | var showConvDims = false; 37 | 38 | let depthFn = (depth) => logDepth ? (Math.log(depth) * depthScale) : (depth * depthScale); 39 | let widthFn = (width) => logWidth ? (Math.log(width) * widthScale) : (width * widthScale); 40 | let convFn = (conv) => logConvSize ? (Math.log(conv) * convScale) : (conv * convScale); 41 | 42 | function wf(layer) { return widthFn(layer['width']); } 43 | function hf(layer) { return widthFn(layer['height']); } 44 | 45 | var layers = new THREE.Group(); 46 | var convs = new THREE.Group(); 47 | var pyramids = new THREE.Group(); 48 | var sprites = new THREE.Group(); 49 | 50 | 51 | var scene = new THREE.Scene(); 52 | scene.background = new THREE.Color( 0xffffff ); 53 | 54 | // var camera = new THREE.PerspectiveCamera( 75, window.innerWidth/window.innerHeight, 0.1, 100000 ); 55 | var camera = new THREE.OrthographicCamera( w / - 2, w / 2, h / 2, h / - 2, -10000000, 10000000 ); 56 | camera.position.set(-219, 92, 84); 57 | 58 | var renderer; 59 | var rendererType = 'webgl'; 60 | 61 | var controls; 62 | 63 | 64 | // ///////////////////////////////////////////////////////////////////////////// 65 | // /////// Methods /////// 66 | // ///////////////////////////////////////////////////////////////////////////// 67 | 68 | function restartRenderer({rendererType_=rendererType}={}) { 69 | 70 | rendererType = rendererType_; 71 | 72 | clearThree(scene); 73 | 74 | if (rendererType === 'webgl') { renderer = new THREE.WebGLRenderer( { 'alpha':true } ); } 75 | else if (rendererType === 'svg') { renderer = new THREE.SVGRenderer(); } 76 | 77 | renderer.setPixelRatio(window.devicePixelRatio || 1); 78 | renderer.setSize( window.innerWidth, window.innerHeight ); 79 | 80 | graph_container = document.getElementById('graph-container') 81 | while (graph_container.firstChild) { graph_container.removeChild(graph_container.firstChild); } 82 | graph_container.appendChild( renderer.domElement ); 83 | 84 | if (controls) { controls.dispose(); } 85 | controls = new THREE.OrbitControls( camera, renderer.domElement ); 86 | 87 | animate(); 88 | 89 | } 90 | 91 | function animate() { 92 | requestAnimationFrame( animate ); 93 | sprites.children.forEach(sprite => { 94 | sprite.quaternion.copy(camera.quaternion); 95 | }); 96 | renderer.render(scene, camera); 97 | }; 98 | 99 | restartRenderer(); 100 | 101 | function redraw({architecture_=architecture, 102 | architecture2_=architecture2, 103 | betweenLayers_=betweenLayers, 104 | logDepth_=logDepth, 105 | depthScale_=depthScale, 106 | logWidth_=logWidth, 107 | widthScale_=widthScale, 108 | logConvSize_=logConvSize, 109 | convScale_=convScale, 110 | showDims_=showDims, 111 | showConvDims_=showConvDims}={}) { 112 | 113 | architecture = architecture_; 114 | architecture2 = architecture2_; 115 | betweenLayers = betweenLayers_; 116 | logDepth = logDepth_; 117 | depthScale = depthScale_; 118 | logWidth = logWidth_; 119 | widthScale = widthScale_; 120 | logConvSize = logConvSize_; 121 | convScale = convScale_; 122 | showDims = showDims_; 123 | showConvDims = showConvDims_; 124 | 125 | clearThree(scene); 126 | 127 | z_offset = -(sum(architecture.map(layer => depthFn(layer['depth']))) + (betweenLayers * (architecture.length - 1))) / 3; 128 | layer_offsets = pairWise(architecture).reduce((offsets, layers) => offsets.concat([offsets.last() + depthFn(layers[0]['depth'])/2 + betweenLayers + depthFn(layers[1]['depth'])/2]), [z_offset]); 129 | layer_offsets = layer_offsets.concat(architecture2.reduce((offsets, layer) => offsets.concat([offsets.last() + widthFn(2) + betweenLayers]), [layer_offsets.last() + depthFn(architecture.last()['depth'])/2 + betweenLayers + widthFn(2)])); 130 | 131 | architecture.forEach( function( layer, index ) { 132 | 133 | // Layer 134 | layer_geometry = new THREE.BoxGeometry( wf(layer), hf(layer), depthFn(layer['depth']) ); 135 | layer_object = new THREE.Mesh( layer_geometry, box_material ); 136 | layer_object.position.set(0, 0, layer_offsets[index]); 137 | layers.add( layer_object ); 138 | 139 | layer_edges_geometry = new THREE.EdgesGeometry( layer_geometry ); 140 | layer_edges_object = new THREE.LineSegments( layer_edges_geometry, line_material ); 141 | layer_edges_object.position.set(0, 0, layer_offsets[index]); 142 | layers.add( layer_edges_object ); 143 | 144 | if (index < architecture.length - 1) { 145 | 146 | // Conv 147 | conv_geometry = new THREE.BoxGeometry( convFn(layer['filterWidth']), convFn(layer['filterHeight']), depthFn(layer['depth']) ); 148 | conv_object = new THREE.Mesh( conv_geometry, conv_material ); 149 | conv_object.position.set(layer['rel_x'] * wf(layer), layer['rel_y'] * hf(layer), layer_offsets[index]); 150 | convs.add( conv_object ); 151 | 152 | conv_edges_geometry = new THREE.EdgesGeometry( conv_geometry ); 153 | conv_edges_object = new THREE.LineSegments( conv_edges_geometry, line_material ); 154 | conv_edges_object.position.set(layer['rel_x'] * wf(layer), layer['rel_y'] * hf(layer), layer_offsets[index]); 155 | convs.add( conv_edges_object ); 156 | 157 | // Pyramid 158 | pyramid_geometry = new THREE.Geometry(); 159 | 160 | base_z = layer_offsets[index] + (depthFn(layer['depth']) / 2); 161 | summit_z = layer_offsets[index] + (depthFn(layer['depth']) / 2) + betweenLayers; 162 | next_layer_wh = widthFn(architecture[index+1]['width']) 163 | 164 | pyramid_geometry.vertices = [ 165 | new THREE.Vector3( (layer['rel_x'] * wf(layer)) + (convFn(layer['filterWidth'])/2), (layer['rel_y'] * hf(layer)) + (convFn(layer['filterHeight'])/2), base_z ), // base 166 | new THREE.Vector3( (layer['rel_x'] * wf(layer)) + (convFn(layer['filterWidth'])/2), (layer['rel_y'] * hf(layer)) - (convFn(layer['filterHeight'])/2), base_z ), // base 167 | new THREE.Vector3( (layer['rel_x'] * wf(layer)) - (convFn(layer['filterWidth'])/2), (layer['rel_y'] * hf(layer)) - (convFn(layer['filterHeight'])/2), base_z ), // base 168 | new THREE.Vector3( (layer['rel_x'] * wf(layer)) - (convFn(layer['filterWidth'])/2), (layer['rel_y'] * hf(layer)) + (convFn(layer['filterHeight'])/2), base_z ), // base 169 | new THREE.Vector3( (layer['rel_x'] * next_layer_wh), (layer['rel_y'] * next_layer_wh), summit_z) // summit 170 | ]; 171 | pyramid_geometry.faces = [new THREE.Face3(0,1,2),new THREE.Face3(0,2,3),new THREE.Face3(1,0,4),new THREE.Face3(2,1,4),new THREE.Face3(3,2,4),new THREE.Face3(0,3,4)]; 172 | 173 | pyramid_object = new THREE.Mesh( pyramid_geometry, pyra_material ); 174 | pyramids.add( pyramid_object ); 175 | 176 | pyramid_edges_geometry = new THREE.EdgesGeometry( pyramid_geometry ); 177 | pyramid_edges_object = new THREE.LineSegments( pyramid_edges_geometry, line_material ); 178 | pyramids.add( pyramid_edges_object ); 179 | 180 | } 181 | 182 | if (showDims) { 183 | 184 | // Dims 185 | sprite = makeTextSprite(rendererType === 'svg', layer['depth'].toString(), layer_object.position, new THREE.Vector3( wf(layer)/2 + 2, hf(layer)/2 + 2, 0 )); 186 | 187 | sprite = makeTextSprite(rendererType === 'svg', layer['width'].toString(), layer_object.position, new THREE.Vector3( wf(layer)/2 + 3, 0, depthFn(layer['depth'])/2 + 3 )); 188 | 189 | sprite = makeTextSprite(rendererType === 'svg', layer['height'].toString(), layer_object.position, new THREE.Vector3( 0, -hf(layer)/2 - 3, depthFn(layer['depth'])/2 + 3 )); 190 | 191 | } 192 | 193 | if (showConvDims && index < architecture.length - 1) { 194 | 195 | // Conv Dims 196 | sprite = makeTextSprite(rendererType === 'svg', layer['filterHeight'].toString(), conv_object.position, new THREE.Vector3( convFn(layer['filterWidth'])/2, -3, depthFn(layer['depth'])/2 + 3 )); 197 | 198 | sprite = makeTextSprite(rendererType === 'svg', layer['filterWidth'].toString(), conv_object.position, new THREE.Vector3( -1, convFn(layer['filterHeight'])/2, depthFn(layer['depth'])/2 + 3 )); 199 | 200 | } 201 | 202 | }); 203 | 204 | architecture2.forEach( function( layer, index ) { 205 | 206 | // Dense 207 | layer_geometry = new THREE.BoxGeometry( widthFn(2), depthFn(layer), widthFn(2) ); 208 | layer_object = new THREE.Mesh( layer_geometry, box_material ); 209 | layer_object.position.set(0, 0, layer_offsets[architecture.length + index]); 210 | layers.add( layer_object ); 211 | 212 | layer_edges_geometry = new THREE.EdgesGeometry( layer_geometry ); 213 | layer_edges_object = new THREE.LineSegments( layer_edges_geometry, line_material ); 214 | layer_edges_object.position.set(0, 0, layer_offsets[architecture.length + index]); 215 | layers.add( layer_edges_object ); 216 | 217 | direction = new THREE.Vector3( 0, 0, 1 ); 218 | origin = new THREE.Vector3( 0, 0, layer_offsets[architecture.length + index] - betweenLayers - widthFn(2)/2 + 1 ); 219 | length = betweenLayers - 2; 220 | headLength = betweenLayers/3; 221 | headWidth = 5; 222 | arrow = new THREE.ArrowHelper( direction, origin, length, 0x000000, headLength, headWidth ); 223 | pyramids.add( arrow ); 224 | 225 | if (showDims) { 226 | 227 | // Dims 228 | sprite = makeTextSprite(rendererType === 'svg', layer.toString(), layer_object.position, new THREE.Vector3( 3, depthFn(layer)/2 + 4, 3 )); 229 | 230 | } 231 | 232 | 233 | }); 234 | 235 | scene.add( layers ); 236 | scene.add( convs ); 237 | scene.add( pyramids ); 238 | scene.add( sprites ); 239 | 240 | } 241 | 242 | function clearThree(obj) { 243 | 244 | while(obj.children.length > 0) { 245 | clearThree( obj.children[0] ) 246 | obj.remove( obj.children[0] ); 247 | } 248 | 249 | if ( obj.geometry ) { obj.geometry.dispose(); } 250 | if ( obj.material ) { obj.material.dispose(); } 251 | if ( obj.texture ) { obj.texture.dispose(); } 252 | } 253 | 254 | 255 | function makeTextSprite(should_make_geometry, message, copy_pos, sub_pos, opts) { 256 | if (should_make_geometry) { 257 | const loader = new THREE.FontLoader(); 258 | loader.load('fonts/helvetiker_regular.typeface.json', function (font) { 259 | let geometry = new THREE.TextGeometry(message, { 260 | font: font, 261 | size: 3 * fontScale, 262 | height: 0.01, 263 | }); 264 | 265 | let material = new THREE.MeshBasicMaterial({ color: 0x000000 }); 266 | let sprite = new THREE.Mesh(geometry, material); 267 | sprite.matrixAutoUpdate = true; 268 | sprite.up.set(0, 1, 0); 269 | sprite.scale.set(1, 1, 0.1); 270 | 271 | sprite.position.copy(copy_pos).sub(sub_pos); 272 | sprites.add(sprite); 273 | }); 274 | 275 | } else { 276 | var parameters = opts || {}; 277 | var fontface = parameters.fontface || 'Helvetica'; 278 | var fontsize = parameters.fontsize || 120; 279 | var canvas = document.createElement('canvas'); 280 | var context = canvas.getContext('2d'); 281 | context.font = fontsize + "px " + fontface; 282 | 283 | // get size data (height depends only on font size) 284 | var metrics = context.measureText(message); 285 | var textWidth = metrics.width; 286 | 287 | // text color 288 | context.fillStyle = 'rgba(0, 0, 0, 1.0)'; 289 | context.fillText(message, 0, fontsize); 290 | 291 | // canvas contents will be used for a texture 292 | var texture = new THREE.Texture(canvas) 293 | texture.minFilter = THREE.LinearFilter; 294 | texture.needsUpdate = true; 295 | 296 | var spriteMaterial = new THREE.SpriteMaterial({ map: texture }); 297 | var sprite = new THREE.Sprite( spriteMaterial ); 298 | sprite.scale.set( 10 * fontScale, 5* fontScale, 1.0 ); 299 | sprite.center.set( 0,1 ); 300 | 301 | sprite.position.copy(copy_pos).sub(sub_pos); 302 | sprites.add(sprite); 303 | } 304 | } 305 | 306 | function style({color1_=color1, 307 | color2_=color2, 308 | color3_=color3, 309 | rectOpacity_=rectOpacity, 310 | filterOpacity_=filterOpacity, 311 | fontScale_ =fontScale, 312 | }={}) { 313 | color1 = color1_; 314 | color2 = color2_; 315 | color3 = color3_; 316 | rectOpacity = rectOpacity_; 317 | filterOpacity = filterOpacity_; 318 | fontScale = fontScale_; 319 | 320 | box_material.color = new THREE.Color(color1); 321 | conv_material.color = new THREE.Color(color2); 322 | pyra_material.color = new THREE.Color(color3); 323 | 324 | box_material.opacity = rectOpacity; 325 | 326 | conv_material.opacity = filterOpacity; 327 | pyra_material.opacity = filterOpacity; 328 | } 329 | 330 | // ///////////////////////////////////////////////////////////////////////////// 331 | // /////// Window Resize /////// 332 | // ///////////////////////////////////////////////////////////////////////////// 333 | 334 | function onWindowResize() { 335 | 336 | renderer.setSize(window.innerWidth, window.innerHeight); 337 | 338 | camFactor = window.devicePixelRatio || 1; 339 | camera.left = -window.innerWidth / camFactor; 340 | camera.right = window.innerWidth / camFactor; 341 | camera.top = window.innerHeight / camFactor; 342 | camera.bottom = -window.innerHeight / camFactor; 343 | camera.updateProjectionMatrix(); 344 | 345 | } 346 | 347 | window.addEventListener('resize', onWindowResize, false); 348 | 349 | 350 | ///////////////////////////////////////////////////////////////////////////// 351 | /////// Return /////// 352 | ///////////////////////////////////////////////////////////////////////////// 353 | 354 | return { 355 | 'redraw' : redraw, 356 | 'restartRenderer' : restartRenderer, 357 | 'style' : style, 358 | 359 | } 360 | 361 | } 362 | -------------------------------------------------------------------------------- /FCNN.js: -------------------------------------------------------------------------------- 1 | 2 | function FCNN() { 3 | 4 | let randomWeight = () => Math.random() * 2 - 1; 5 | 6 | 7 | ///////////////////////////////////////////////////////////////////////////// 8 | /////// Variables /////// 9 | ///////////////////////////////////////////////////////////////////////////// 10 | 11 | var w = window.innerWidth; 12 | var h = window.innerHeight; 13 | 14 | var svg = d3.select("#graph-container").append("svg").attr("xmlns", "http://www.w3.org/2000/svg"); 15 | var g = svg.append("g"); 16 | svg.style("cursor", "move"); 17 | 18 | var edgeWidthProportional = false; 19 | var edgeWidth = 0.5; 20 | var weightedEdgeWidth = d3.scaleLinear().domain([0, 1]).range([0, edgeWidth]); 21 | 22 | var edgeOpacityProportional = false; 23 | var edgeOpacity = 1.0 24 | var weightedEdgeOpacity = d3.scaleLinear().domain([0, 1]).range([0, 1]); 25 | 26 | var edgeColorProportional = false; 27 | var defaultEdgeColor = "#505050"; 28 | var negativeEdgeColor = "#0000ff"; 29 | var positiveEdgeColor = "#ff0000"; 30 | var weightedEdgeColor = d3.scaleLinear().domain([-1, 0, 1]).range([negativeEdgeColor, "white", positiveEdgeColor]); 31 | 32 | var nodeDiameter = 20; 33 | var nodeColor = "#ffffff"; 34 | var nodeBorderColor = "#333333"; 35 | 36 | var betweenLayers = 160; 37 | 38 | var architecture = [8, 12, 8]; 39 | var betweenNodesInLayer = [20, 20, 20]; 40 | var graph = {}; 41 | var layer_offsets = []; 42 | var largest_layer_width = 0; 43 | var nnDirection = 'right'; 44 | var showBias = false; 45 | var showLabels = true; 46 | var showArrowheads = false; 47 | var arrowheadStyle = "empty"; 48 | var bezierCurves = false; 49 | 50 | let sup_map = {'0': '⁰', '1': '¹', '2': '²', '3': '³', '4': '⁴', '5': '⁵', '6': '⁶', '7': '⁷', '8': '⁸', '9': '⁹'}; 51 | let sup = (s) => Array.prototype.map.call(s, (d) => (d in sup_map && sup_map[d]) || d).join(''); 52 | 53 | let textFn = (layer_index, layer_width) => ((layer_index === 0 ? "Input" : (layer_index === architecture.length-1 ? "Output" : "Hidden")) + " Layer ∈ ℝ" + sup(layer_width.toString())); 54 | var nominal_text_size = 12; 55 | var textWidth = 70; 56 | 57 | var marker = svg.append("svg:defs").append("svg:marker") 58 | .attr("id", "arrow") 59 | .attr("viewBox", "0 -5 10 10") 60 | .attr("markerWidth", 7) 61 | .attr("markerHeight", 7) 62 | .attr("orient", "auto"); 63 | 64 | var arrowhead = marker.append("svg:path") 65 | .attr("d", "M0,-5L10,0L0,5") 66 | .style("stroke", defaultEdgeColor); 67 | 68 | var link = g.selectAll(".link"); 69 | var node = g.selectAll(".node"); 70 | var text = g.selectAll(".text"); 71 | 72 | ///////////////////////////////////////////////////////////////////////////// 73 | /////// Methods /////// 74 | ///////////////////////////////////////////////////////////////////////////// 75 | 76 | function redraw({architecture_=architecture, 77 | showBias_=showBias, 78 | showLabels_=showLabels, 79 | bezierCurves_=bezierCurves, 80 | }={}) { 81 | 82 | architecture = architecture_; 83 | showBias = showBias_; 84 | showLabels = showLabels_; 85 | bezierCurves = bezierCurves_; 86 | 87 | graph.nodes = architecture.map((layer_width, layer_index) => range(layer_width).map(node_index => {return {'id':layer_index+'_'+node_index,'layer':layer_index,'node_index':node_index}})); 88 | graph.links = pairWise(graph.nodes).map((nodes) => nodes[0].map(left => nodes[1].map(right => {return right.node_index >= 0 ? {'id':left.id+'-'+right.id, 'source':left.id,'target':right.id,'weight':randomWeight()} : null }))); 89 | graph.nodes = flatten(graph.nodes); 90 | graph.links = flatten(graph.links).filter(l => (l && (showBias ? (parseInt(l['target'].split('_')[0]) !== architecture.length-1 ? (l['target'].split('_')[1] !== '0') : true) : true))); 91 | 92 | label = architecture.map((layer_width, layer_index) => { return {'id':'layer_'+layer_index+'_label','layer':layer_index,'text':textFn(layer_index, layer_width)}}); 93 | 94 | link = link.data(graph.links, d => d.id); 95 | link.exit().remove(); 96 | link = link.enter() 97 | .insert("path", ".node") 98 | .attr("class", "link") 99 | .merge(link); 100 | 101 | node = node.data(graph.nodes, d => d.id); 102 | node.exit().remove(); 103 | node = node.enter() 104 | .append("circle") 105 | .attr("r", nodeDiameter/2) 106 | .attr("class", "node") 107 | .attr("id", function(d) { return d.id; }) 108 | .on("mousedown", set_focus) 109 | .on("mouseup", remove_focus) 110 | .merge(node); 111 | 112 | text = text.data(label, d => d.id); 113 | text.exit().remove(); 114 | text = text.enter() 115 | .append("text") 116 | .attr("class", "text") 117 | .attr("dy", ".35em") 118 | .style("font-size", nominal_text_size+"px") 119 | .merge(text) 120 | .text(function(d) { return (showLabels ? d.text : ""); }); 121 | 122 | style(); 123 | } 124 | 125 | function redistribute({betweenNodesInLayer_=betweenNodesInLayer, 126 | betweenLayers_=betweenLayers, 127 | nnDirection_=nnDirection, 128 | bezierCurves_=bezierCurves}={}) { 129 | 130 | betweenNodesInLayer = betweenNodesInLayer_; 131 | betweenLayers = betweenLayers_; 132 | nnDirection = nnDirection_; 133 | bezierCurves = bezierCurves_; 134 | 135 | layer_widths = architecture.map((layer_width, i) => layer_width * nodeDiameter + (layer_width - 1) * betweenNodesInLayer[i]) 136 | 137 | largest_layer_width = Math.max(...layer_widths); 138 | 139 | layer_offsets = layer_widths.map(layer_width => (largest_layer_width - layer_width) / 2); 140 | 141 | let indices_from_id = (id) => id.split('_').map(x => parseInt(x)); 142 | 143 | let x = (layer, node_index) => layer * (betweenLayers + nodeDiameter) + w/2 - (betweenLayers * layer_offsets.length/3); 144 | let y = (layer, node_index) => layer_offsets[layer] + node_index * (nodeDiameter + betweenNodesInLayer[layer]) + h/2 - largest_layer_width/2; 145 | 146 | let xt = (layer, node_index) => layer_offsets[layer] + node_index * (nodeDiameter + betweenNodesInLayer[layer]) + w/2 - largest_layer_width/2; 147 | let yt = (layer, node_index) => layer * (betweenLayers + nodeDiameter) + h/2 - (betweenLayers * layer_offsets.length/3); 148 | 149 | if (nnDirection == 'up') { x = xt; y = yt; } 150 | 151 | node.attr('cx', function(d) { return x(d.layer, d.node_index); }) 152 | .attr('cy', function(d) { return y(d.layer, d.node_index); }); 153 | 154 | if(bezierCurves) { 155 | link.attr("d", (d) => { 156 | let source = [x(...indices_from_id(d.source)), y(...indices_from_id(d.source))]; 157 | let target = [x(...indices_from_id(d.target)), y(...indices_from_id(d.target))]; 158 | 159 | // control points 160 | let cp1 = [(source[0] + target[0]) / 2, source[1]]; 161 | let cp2 = [(source[0] + target[0]) / 2, target[1]]; 162 | 163 | return "M" + source[0] + "," + source[1] 164 | + "C" + cp1[0] + "," + cp1[1] 165 | + " " + cp2[0] + "," + cp2[1] 166 | + " " + target[0] + "," + target[1]; 167 | }); 168 | } else { 169 | link.attr("d", (d) => "M" + x(...indices_from_id(d.source)) + "," + 170 | y(...indices_from_id(d.source)) + ", " + 171 | x(...indices_from_id(d.target)) + "," + 172 | y(...indices_from_id(d.target))); 173 | } 174 | 175 | text.attr("x", function(d) { return (nnDirection === 'right' ? x(d.layer, d.node_index) - textWidth/2 : w/2 + largest_layer_width/2 + 20 ); }) 176 | .attr("y", function(d) { return (nnDirection === 'right' ? h/2 + largest_layer_width/2 + 20 : y(d.layer, d.node_index) ); }); 177 | 178 | } 179 | 180 | function style({edgeWidthProportional_=edgeWidthProportional, 181 | edgeWidth_=edgeWidth, 182 | edgeOpacityProportional_=edgeOpacityProportional, 183 | edgeOpacity_=edgeOpacity, 184 | negativeEdgeColor_=negativeEdgeColor, 185 | positiveEdgeColor_=positiveEdgeColor, 186 | edgeColorProportional_=edgeColorProportional, 187 | defaultEdgeColor_=defaultEdgeColor, 188 | nodeDiameter_=nodeDiameter, 189 | nodeColor_=nodeColor, 190 | nodeBorderColor_=nodeBorderColor, 191 | showArrowheads_=showArrowheads, 192 | arrowheadStyle_=arrowheadStyle, 193 | bezierCurves_=bezierCurves}={}) { 194 | // Edge Width 195 | edgeWidthProportional = edgeWidthProportional_; 196 | edgeWidth = edgeWidth_; 197 | weightedEdgeWidth = d3.scaleLinear().domain([0, 1]).range([0, edgeWidth]); 198 | // Edge Opacity 199 | edgeOpacityProportional = edgeOpacityProportional_; 200 | edgeOpacity = edgeOpacity_; 201 | // Edge Color 202 | defaultEdgeColor = defaultEdgeColor_; 203 | edgeColorProportional = edgeColorProportional_; 204 | negativeEdgeColor = negativeEdgeColor_; 205 | positiveEdgeColor = positiveEdgeColor_; 206 | weightedEdgeColor = d3.scaleLinear().domain([-1, 0, 1]).range([negativeEdgeColor, "white", positiveEdgeColor]); 207 | // Node Styles 208 | nodeDiameter = nodeDiameter_; 209 | nodeColor = nodeColor_; 210 | nodeBorderColor = nodeBorderColor_; 211 | // Arrowheads 212 | showArrowheads = showArrowheads_; 213 | arrowheadStyle = arrowheadStyle_; 214 | // Bezier curves 215 | bezierCurves = bezierCurves_; 216 | 217 | link.style("stroke-width", function(d) { 218 | if (edgeWidthProportional) { return weightedEdgeWidth(Math.abs(d.weight)); } else { return edgeWidth; } 219 | }); 220 | 221 | link.style("stroke-opacity", function(d) { 222 | if (edgeOpacityProportional) { return weightedEdgeOpacity(Math.abs(d.weight)); } else { return edgeOpacity; } 223 | }); 224 | 225 | link.style("stroke", function(d) { 226 | if (edgeColorProportional) { return weightedEdgeColor(d.weight); } else { return defaultEdgeColor; } 227 | }); 228 | 229 | link.style("fill", "none"); 230 | 231 | link.attr('marker-end', showArrowheads ? "url(#arrow)" : ''); 232 | marker.attr('refX', nodeDiameter*1.4 + 12); 233 | arrowhead.style("fill", arrowheadStyle === 'empty' ? "none" : defaultEdgeColor); 234 | 235 | node.attr("r", nodeDiameter/2); 236 | node.style("fill", nodeColor); 237 | node.style("stroke", nodeBorderColor); 238 | 239 | } 240 | 241 | ///////////////////////////////////////////////////////////////////////////// 242 | /////// Focus /////// 243 | ///////////////////////////////////////////////////////////////////////////// 244 | 245 | function set_focus(d) { 246 | d3.event.stopPropagation(); 247 | node.style("opacity", function(o) { return (d == o || o.layer == d.layer - 1) ? 1 : 0.1; }); 248 | link.style("opacity", function(o) { return (o.target == d.id) ? 1 : 0.02; }); 249 | } 250 | 251 | function remove_focus() { 252 | d3.event.stopPropagation(); 253 | node.style("opacity", 1); 254 | link.style("opacity", function () { return edgeOpacity; }) 255 | } 256 | 257 | ///////////////////////////////////////////////////////////////////////////// 258 | /////// Zoom & Resize /////// 259 | ///////////////////////////////////////////////////////////////////////////// 260 | 261 | svg.call(d3.zoom() 262 | .scaleExtent([1 / 2, 8]) 263 | .on("zoom", zoomed)); 264 | 265 | function zoomed() { g.attr("transform", d3.event.transform); } 266 | 267 | function resize() { 268 | w = window.innerWidth; 269 | h = window.innerHeight; 270 | svg.attr("width", w).attr("height", h); 271 | } 272 | 273 | d3.select(window).on("resize", resize) 274 | 275 | resize(); 276 | 277 | ///////////////////////////////////////////////////////////////////////////// 278 | /////// Return /////// 279 | ///////////////////////////////////////////////////////////////////////////// 280 | 281 | return { 282 | 'redraw' : redraw, 283 | 'redistribute' : redistribute, 284 | 'style' : style, 285 | 286 | 'graph' : graph, 287 | 'link' : link 288 | } 289 | 290 | } 291 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Alexander Lenail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /LeNet.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 68 | 69 | NN SVG 70 | 71 | 72 | 73 | 74 | 75 |
76 | 77 |
78 | 79 |
80 |
81 |
82 |
83 | 84 |

NN-SVG

85 |

Publication-ready NN-architecture schematics. Download SVG

86 | 87 | 94 | 95 |
96 |
97 | 98 |
99 |

Style:

100 | 101 |
102 | 103 | 104 |
105 |
106 | 107 | 108 |
109 |
110 | 111 | 112 |
113 |
114 | 115 | 116 |
117 |
118 | 119 | 120 |
121 |
122 | 123 | 124 |
125 | 126 |
127 |
128 | 129 | 130 |
131 |
132 | 133 | 134 | 100 135 | % 136 |
137 | 138 |
139 |

Architecture:

140 |
141 |

Depth | Height | Width | filter Height | filter Width

142 |
143 |
144 | 145 | 146 | 147 | 148 | 149 | 150 |
151 |
152 | 153 | 154 | 155 |
156 |
157 |
158 |
159 | 160 | 161 | 162 | 163 | 164 | 165 |
166 |
167 | 168 | 169 | 170 |
171 |
172 |
173 |
174 | 175 | 176 | 177 | 178 | 179 | 180 |
181 |
182 | 183 | 184 | 185 |
186 |
187 |
188 |
189 | 190 | 191 | 192 | 193 | 194 | 195 |
196 |
197 | 198 | 199 | 200 |
201 |
202 |
203 |
204 | 205 | 206 | 207 | 208 | 209 | 210 |
211 |
212 | 213 | 214 | 215 |
216 |
217 | 218 |
219 | 220 |
221 |
222 |

Vector Length

223 |
224 |
225 | 226 | 227 |
228 |
229 |
230 |
231 | 232 | 233 |
234 |
235 |
236 |
237 | 238 | 239 |
240 |
241 | 242 |
243 | 244 |
245 | 246 |
247 |
248 |
249 |
250 | 251 |
About
252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 404 | 405 | 406 | 407 | 408 | -------------------------------------------------------------------------------- /LeNet.js: -------------------------------------------------------------------------------- 1 | 2 | function LeNet() { 3 | 4 | ///////////////////////////////////////////////////////////////////////////// 5 | /////// Variables /////// 6 | ///////////////////////////////////////////////////////////////////////////// 7 | 8 | var w = window.innerWidth; 9 | var h = window.innerHeight; 10 | 11 | var svg = d3.select("#graph-container").append("svg").attr("xmlns", "http://www.w3.org/2000/svg"); 12 | var g = svg.append("g"); 13 | svg.style("cursor", "move"); 14 | 15 | var color1 = '#e0e0e0'; 16 | var color2 = '#a0a0a0'; 17 | var borderWidth = 1.0; 18 | var borderColor = "black"; 19 | var rectOpacity = 0.8; 20 | var betweenSquares = 8; 21 | var betweenLayers = []; 22 | var betweenLayersDefault = 12; 23 | 24 | var architecture = []; 25 | var architecture2 = []; 26 | var lenet = {}; 27 | var layer_offsets = []; 28 | var largest_layer_width = 0; 29 | var showLabels = true; 30 | 31 | var sqrtLength = false; 32 | var lengthScale = 100; 33 | 34 | let lengthFn = (length) => sqrtLength ? (Math.sqrt(length) * lengthScale/10) : (length * lengthScale/100); 35 | 36 | let textFn = (layer) => (typeof(layer) === "object" ? layer['numberOfSquares']+'@'+layer['squareHeight']+'x'+layer['squareWidth'] : "1x"+layer) 37 | 38 | var rect, conv, link, poly, line, text, info; 39 | 40 | ///////////////////////////////////////////////////////////////////////////// 41 | /////// Methods /////// 42 | ///////////////////////////////////////////////////////////////////////////// 43 | 44 | function redraw({architecture_=architecture, 45 | architecture2_=architecture2, 46 | sqrtLength_=sqrtLength, 47 | lengthScale_=lengthScale,}={}) { 48 | 49 | architecture = architecture_; 50 | architecture2 = architecture2_; 51 | sqrtLength = sqrtLength_; 52 | lengthScale = lengthScale_; 53 | 54 | lenet.rects = architecture.map((layer, layer_index) => range(layer['numberOfSquares']).map(rect_index => {return {'id':layer_index+'_'+rect_index,'layer':layer_index,'rect_index':rect_index,'width':layer['squareWidth'],'height':layer['squareHeight']}})); 55 | lenet.rects = flatten(lenet.rects); 56 | 57 | lenet.convs = architecture.map((layer, layer_index) => Object.assign({'id':'conv_'+layer_index,'layer':layer_index}, layer)); lenet.convs.pop(); 58 | lenet.convs = lenet.convs.map(conv => Object.assign({'x_rel':rand(0.1, 0.9),'y_rel':rand(0.1, 0.9)}, conv)) 59 | 60 | lenet.conv_links = lenet.convs.map(conv => {return [Object.assign({'id':'link_'+conv['layer']+'_0','i':0},conv), Object.assign({'id':'link_'+conv['layer']+'_1','i':1},conv)]}); 61 | lenet.conv_links = flatten(lenet.conv_links); 62 | 63 | lenet.fc_layers = architecture2.map((size, fc_layer_index) => {return {'id': 'fc_'+fc_layer_index, 'layer':fc_layer_index+architecture.length, 'size': lengthFn(size)}}); 64 | lenet.fc_links = lenet.fc_layers.map(fc => { return [Object.assign({'id':'link_'+fc['layer']+'_0','i':0,'prevSize':10},fc), Object.assign({'id':'link_'+fc['layer']+'_1','i':1,'prevSize':10},fc)]}); 65 | lenet.fc_links = flatten(lenet.fc_links); 66 | 67 | // hacks 68 | if (lenet.rects.length > 0 && lenet.fc_layers.length > 0) { 69 | lenet.fc_links[0]['prevSize'] = 0; 70 | lenet.fc_links[1]['prevSize'] = lenet.rects.last()['width']; 71 | } 72 | 73 | label = architecture.map((layer, layer_index) => { return {'id':'data_'+layer_index+'_label','layer':layer_index,'text':textFn(layer)}}) 74 | .concat(architecture2.map((layer, layer_index) => { return {'id':'data_'+layer_index+architecture.length+'_label','layer':layer_index+architecture.length,'text':textFn(layer)}}) ); 75 | 76 | g.selectAll('*').remove(); 77 | 78 | rect = g.selectAll(".rect") 79 | .data(lenet.rects) 80 | .enter() 81 | .append("rect") 82 | .attr("class", "rect") 83 | .attr("id", d => d.id) 84 | .attr("width", d => d.width) 85 | .attr("height", d => d.height); 86 | 87 | conv = g.selectAll(".conv") 88 | .data(lenet.convs) 89 | .enter() 90 | .append("rect") 91 | .attr("class", "conv") 92 | .attr("id", d => d.id) 93 | .attr("width", d => d.filterWidth) 94 | .attr("height", d => d.filterHeight) 95 | .style("fill-opacity", 0); 96 | 97 | link = g.selectAll(".link") 98 | .data(lenet.conv_links) 99 | .enter() 100 | .append("line") 101 | .attr("class", "link") 102 | .attr("id", d => d.id); 103 | 104 | poly = g.selectAll(".poly") 105 | .data(lenet.fc_layers) 106 | .enter() 107 | .append("polygon") 108 | .attr("class", "poly") 109 | .attr("id", d => d.id); 110 | 111 | line = g.selectAll(".line") 112 | .data(lenet.fc_links) 113 | .enter() 114 | .append("line") 115 | .attr("class", "line") 116 | .attr("id", d => d.id); 117 | 118 | text = g.selectAll(".text") 119 | .data(architecture) 120 | .enter() 121 | .append("text") 122 | .text(d => (showLabels ? d.op : "")) 123 | .attr("class", "text") 124 | .attr("dy", ".35em") 125 | .style("font-size", "16px") 126 | .attr("font-family", "sans-serif"); 127 | 128 | info = g.selectAll(".info") 129 | .data(label) 130 | .enter() 131 | .append("text") 132 | .text(d => (showLabels ? d.text : "")) 133 | .attr("class", "info") 134 | .attr("dy", "-0.3em") 135 | .style("font-size", "16px") 136 | .attr("font-family", "sans-serif"); 137 | 138 | style(); 139 | 140 | } 141 | 142 | function redistribute({betweenLayers_=betweenLayers, 143 | betweenSquares_=betweenSquares}={}) { 144 | 145 | betweenLayers = betweenLayers_; 146 | betweenSquares = betweenSquares_; 147 | 148 | layer_widths = architecture.map((layer, i) => (layer['numberOfSquares']-1) * betweenSquares + layer['squareWidth']); 149 | layer_widths = layer_widths.concat(lenet.fc_layers.map((layer, i) => layer['size'])).concat([0]); 150 | 151 | largest_layer_width = Math.max(...layer_widths); 152 | 153 | layer_x_offsets = layer_widths.reduce((offsets, layer_width, i) => offsets.concat([offsets.last() + layer_width + (betweenLayers[i] || betweenLayersDefault) ]), [0]).concat([0]); 154 | layer_y_offsets = layer_widths.map(layer_width => (largest_layer_width - layer_width) / 2).concat([0]); 155 | 156 | screen_center_x = w/2 - architecture.length * largest_layer_width/2; 157 | screen_center_y = h/2 - largest_layer_width/2; 158 | 159 | let x = (layer, node_index) => layer_x_offsets[layer] + (node_index * betweenSquares) + screen_center_x; 160 | let y = (layer, node_index) => layer_y_offsets[layer] + (node_index * betweenSquares) + screen_center_y; 161 | 162 | rect.attr('x', d => x(d.layer, d.rect_index)) 163 | .attr('y', d => y(d.layer, d.rect_index)); 164 | 165 | let xc = (d) => (layer_x_offsets[d.layer]) + ((d['numberOfSquares']-1) * betweenSquares) + (d['x_rel'] * (d['squareWidth'] - d['filterWidth'])) + screen_center_x; 166 | let yc = (d) => (layer_y_offsets[d.layer]) + ((d['numberOfSquares']-1) * betweenSquares) + (d['y_rel'] * (d['squareHeight'] - d['filterHeight'])) + screen_center_y; 167 | 168 | conv.attr('x', d => xc(d)) 169 | .attr('y', d => yc(d)); 170 | 171 | link.attr("x1", d => xc(d) + d['filterWidth']) 172 | .attr("y1", d => yc(d) + (d.i ? 0 : d['filterHeight'])) 173 | .attr("x2", d => (layer_x_offsets[d.layer+1]) + ((architecture[d.layer+1]['numberOfSquares']-1) * betweenSquares) + architecture[d.layer+1]['squareWidth'] * d.x_rel + screen_center_x) 174 | .attr("y2", d => (layer_y_offsets[d.layer+1]) + ((architecture[d.layer+1]['numberOfSquares']-1) * betweenSquares) + architecture[d.layer+1]['squareHeight'] * d.y_rel + screen_center_y); 175 | 176 | 177 | poly.attr("points", function(d) { 178 | return ((layer_x_offsets[d.layer]+screen_center_x) +','+(layer_y_offsets[d.layer]+screen_center_y)+ 179 | ' '+(layer_x_offsets[d.layer]+screen_center_x+10) +','+(layer_y_offsets[d.layer]+screen_center_y)+ 180 | ' '+(layer_x_offsets[d.layer]+screen_center_x+d.size+10) +','+(layer_y_offsets[d.layer]+screen_center_y+d.size)+ 181 | ' '+(layer_x_offsets[d.layer]+screen_center_x+d.size) +','+(layer_y_offsets[d.layer]+screen_center_y+d.size)); 182 | }); 183 | 184 | line.attr("x1", d => layer_x_offsets[d.layer-1] + (d.i ? 0 : layer_widths[d.layer-1]) + d.prevSize + screen_center_x) 185 | .attr("y1", d => layer_y_offsets[d.layer-1] + (d.i ? 0 : layer_widths[d.layer-1]) + screen_center_y) 186 | .attr("x2", d => layer_x_offsets[d.layer] + (d.i ? 0 : d.size) + screen_center_x) 187 | .attr("y2", d => layer_y_offsets[d.layer] + (d.i ? 0 : d.size) + screen_center_y) 188 | .style('opacity', d => +(d.layer > 0)); 189 | 190 | text.attr('x', d => (layer_x_offsets[d.layer] + layer_widths[d.layer] + layer_x_offsets[d.layer+1] + layer_widths[d.layer+1]/2)/2 + screen_center_x -15) 191 | .attr('y', d => layer_y_offsets[0] + screen_center_y + largest_layer_width) 192 | .style('opacity', d => +(d.layer+1 < architecture.length || architecture2.length > 0)); 193 | 194 | info.attr('x', d => layer_x_offsets[d.layer] + screen_center_x) 195 | .attr('y', d => layer_y_offsets[d.layer] + screen_center_y - 15); 196 | 197 | } 198 | 199 | function style({color1_=color1, 200 | color2_=color2, 201 | borderWidth_=borderWidth, 202 | rectOpacity_=rectOpacity, 203 | showLabels_=showLabels}={}) { 204 | color1 = color1_; 205 | color2 = color2_; 206 | borderWidth = borderWidth_; 207 | rectOpacity = rectOpacity_; 208 | showLabels = showLabels_; 209 | 210 | rect.style("fill", d => d.rect_index % 2 ? color1 : color2); 211 | poly.style("fill", color1); 212 | 213 | rect.style("stroke", borderColor); 214 | conv.style("stroke", borderColor); 215 | link.style("stroke", borderColor); 216 | poly.style("stroke", borderColor); 217 | line.style("stroke", borderColor); 218 | 219 | rect.style("stroke-width", borderWidth); 220 | conv.style("stroke-width", borderWidth); 221 | link.style("stroke-width", borderWidth / 2); 222 | poly.style("stroke-width", borderWidth); 223 | line.style("stroke-width", borderWidth / 2); 224 | 225 | rect.style("opacity", rectOpacity); 226 | conv.style("stroke-opacity", rectOpacity); 227 | link.style("stroke-opacity", rectOpacity); 228 | poly.style("opacity", rectOpacity); 229 | line.style("stroke-opacity", rectOpacity); 230 | 231 | text.text(d => (showLabels ? d.op : "")); 232 | info.text(d => (showLabels ? d.text : "")); 233 | } 234 | 235 | ///////////////////////////////////////////////////////////////////////////// 236 | /////// Zoom & Resize /////// 237 | ///////////////////////////////////////////////////////////////////////////// 238 | 239 | svg.call(d3.zoom() 240 | .scaleExtent([1 / 2, 8]) 241 | .on("zoom", zoomed)); 242 | 243 | function zoomed() { g.attr("transform", d3.event.transform); } 244 | 245 | function resize() { 246 | w = window.innerWidth; 247 | h = window.innerHeight; 248 | svg.attr("width", w).attr("height", h); 249 | } 250 | 251 | d3.select(window).on("resize", resize) 252 | 253 | resize(); 254 | 255 | 256 | ///////////////////////////////////////////////////////////////////////////// 257 | /////// Return /////// 258 | ///////////////////////////////////////////////////////////////////////////// 259 | 260 | return { 261 | 'redraw' : redraw, 262 | 'redistribute' : redistribute, 263 | 'style' : style, 264 | } 265 | 266 | } 267 | -------------------------------------------------------------------------------- /OrbitControls.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author qiao / https://github.com/qiao 3 | * @author mrdoob / http://mrdoob.com 4 | * @author alteredq / http://alteredqualia.com/ 5 | * @author WestLangley / http://github.com/WestLangley 6 | * @author erich666 / http://erichaines.com 7 | */ 8 | 9 | // This set of controls performs orbiting, dollying (zooming), and panning. 10 | // Unlike TrackballControls, it maintains the "up" direction object.up (+Y by default). 11 | // 12 | // Orbit - left mouse / touch: one-finger move 13 | // Zoom - middle mouse, or mousewheel / touch: two-finger spread or squish 14 | // Pan - right mouse, or arrow keys / touch: two-finger move 15 | 16 | THREE.OrbitControls = function ( object, domElement ) { 17 | 18 | this.object = object; 19 | 20 | this.domElement = ( domElement !== undefined ) ? domElement : document; 21 | 22 | // Set to false to disable this control 23 | this.enabled = true; 24 | 25 | // "target" sets the location of focus, where the object orbits around 26 | this.target = new THREE.Vector3(); 27 | 28 | // How far you can dolly in and out ( PerspectiveCamera only ) 29 | this.minDistance = 0; 30 | this.maxDistance = Infinity; 31 | 32 | // How far you can zoom in and out ( OrthographicCamera only ) 33 | this.minZoom = 0; 34 | this.maxZoom = Infinity; 35 | 36 | // How far you can orbit vertically, upper and lower limits. 37 | // Range is 0 to Math.PI radians. 38 | this.minPolarAngle = 0; // radians 39 | this.maxPolarAngle = Math.PI; // radians 40 | 41 | // How far you can orbit horizontally, upper and lower limits. 42 | // If set, must be a sub-interval of the interval [ - Math.PI, Math.PI ]. 43 | this.minAzimuthAngle = - Infinity; // radians 44 | this.maxAzimuthAngle = Infinity; // radians 45 | 46 | // Set to true to enable damping (inertia) 47 | // If damping is enabled, you must call controls.update() in your animation loop 48 | this.enableDamping = false; 49 | this.dampingFactor = 0.25; 50 | 51 | // This option actually enables dollying in and out; left as "zoom" for backwards compatibility. 52 | // Set to false to disable zooming 53 | this.enableZoom = true; 54 | this.zoomSpeed = 1.0; 55 | 56 | // Set to false to disable rotating 57 | this.enableRotate = true; 58 | this.rotateSpeed = 1.0; 59 | 60 | // Set to false to disable panning 61 | this.enablePan = true; 62 | this.panSpeed = 1.0; 63 | this.screenSpacePanning = false; // if true, pan in screen-space 64 | this.keyPanSpeed = 20.0; // pixels moved per arrow key push 65 | 66 | // Set to true to automatically rotate around the target 67 | // If auto-rotate is enabled, you must call controls.update() in your animation loop 68 | this.autoRotate = false; 69 | this.autoRotateSpeed = 2.0; // 30 seconds per round when fps is 60 70 | 71 | // Set to false to disable use of the keys 72 | this.enableKeys = true; 73 | 74 | // The four arrow keys 75 | this.keys = { LEFT: 37, UP: 38, RIGHT: 39, BOTTOM: 40 }; 76 | 77 | // Mouse buttons 78 | this.mouseButtons = { ORBIT: THREE.MOUSE.LEFT, ZOOM: THREE.MOUSE.MIDDLE, PAN: THREE.MOUSE.RIGHT }; 79 | 80 | // for reset 81 | this.target0 = this.target.clone(); 82 | this.position0 = this.object.position.clone(); 83 | this.zoom0 = this.object.zoom; 84 | 85 | // 86 | // public methods 87 | // 88 | 89 | this.getPolarAngle = function () { 90 | 91 | return spherical.phi; 92 | 93 | }; 94 | 95 | this.getAzimuthalAngle = function () { 96 | 97 | return spherical.theta; 98 | 99 | }; 100 | 101 | this.saveState = function () { 102 | 103 | scope.target0.copy( scope.target ); 104 | scope.position0.copy( scope.object.position ); 105 | scope.zoom0 = scope.object.zoom; 106 | 107 | }; 108 | 109 | this.reset = function () { 110 | 111 | scope.target.copy( scope.target0 ); 112 | scope.object.position.copy( scope.position0 ); 113 | scope.object.zoom = scope.zoom0; 114 | 115 | scope.object.updateProjectionMatrix(); 116 | scope.dispatchEvent( changeEvent ); 117 | 118 | scope.update(); 119 | 120 | state = STATE.NONE; 121 | 122 | }; 123 | 124 | // this method is exposed, but perhaps it would be better if we can make it private... 125 | this.update = function () { 126 | 127 | var offset = new THREE.Vector3(); 128 | 129 | // so camera.up is the orbit axis 130 | var quat = new THREE.Quaternion().setFromUnitVectors( object.up, new THREE.Vector3( 0, 1, 0 ) ); 131 | var quatInverse = quat.clone().inverse(); 132 | 133 | var lastPosition = new THREE.Vector3(); 134 | var lastQuaternion = new THREE.Quaternion(); 135 | 136 | return function update() { 137 | 138 | var position = scope.object.position; 139 | 140 | offset.copy( position ).sub( scope.target ); 141 | 142 | // rotate offset to "y-axis-is-up" space 143 | offset.applyQuaternion( quat ); 144 | 145 | // angle from z-axis around y-axis 146 | spherical.setFromVector3( offset ); 147 | 148 | if ( scope.autoRotate && state === STATE.NONE ) { 149 | 150 | rotateLeft( getAutoRotationAngle() ); 151 | 152 | } 153 | 154 | spherical.theta += sphericalDelta.theta; 155 | spherical.phi += sphericalDelta.phi; 156 | 157 | // restrict theta to be between desired limits 158 | spherical.theta = Math.max( scope.minAzimuthAngle, Math.min( scope.maxAzimuthAngle, spherical.theta ) ); 159 | 160 | // restrict phi to be between desired limits 161 | spherical.phi = Math.max( scope.minPolarAngle, Math.min( scope.maxPolarAngle, spherical.phi ) ); 162 | 163 | spherical.makeSafe(); 164 | 165 | 166 | spherical.radius *= scale; 167 | 168 | // restrict radius to be between desired limits 169 | spherical.radius = Math.max( scope.minDistance, Math.min( scope.maxDistance, spherical.radius ) ); 170 | 171 | // move target to panned location 172 | scope.target.add( panOffset ); 173 | 174 | offset.setFromSpherical( spherical ); 175 | 176 | // rotate offset back to "camera-up-vector-is-up" space 177 | offset.applyQuaternion( quatInverse ); 178 | 179 | position.copy( scope.target ).add( offset ); 180 | 181 | scope.object.lookAt( scope.target ); 182 | 183 | if ( scope.enableDamping === true ) { 184 | 185 | sphericalDelta.theta *= ( 1 - scope.dampingFactor ); 186 | sphericalDelta.phi *= ( 1 - scope.dampingFactor ); 187 | 188 | panOffset.multiplyScalar( 1 - scope.dampingFactor ); 189 | 190 | } else { 191 | 192 | sphericalDelta.set( 0, 0, 0 ); 193 | 194 | panOffset.set( 0, 0, 0 ); 195 | 196 | } 197 | 198 | scale = 1; 199 | 200 | // update condition is: 201 | // min(camera displacement, camera rotation in radians)^2 > EPS 202 | // using small-angle approximation cos(x/2) = 1 - x^2 / 8 203 | 204 | if ( zoomChanged || 205 | lastPosition.distanceToSquared( scope.object.position ) > EPS || 206 | 8 * ( 1 - lastQuaternion.dot( scope.object.quaternion ) ) > EPS ) { 207 | 208 | scope.dispatchEvent( changeEvent ); 209 | 210 | lastPosition.copy( scope.object.position ); 211 | lastQuaternion.copy( scope.object.quaternion ); 212 | zoomChanged = false; 213 | 214 | return true; 215 | 216 | } 217 | 218 | return false; 219 | 220 | }; 221 | 222 | }(); 223 | 224 | this.dispose = function () { 225 | 226 | scope.domElement.removeEventListener( 'contextmenu', onContextMenu, false ); 227 | scope.domElement.removeEventListener( 'mousedown', onMouseDown, false ); 228 | scope.domElement.removeEventListener( 'wheel', onMouseWheel, false ); 229 | 230 | scope.domElement.removeEventListener( 'touchstart', onTouchStart, false ); 231 | scope.domElement.removeEventListener( 'touchend', onTouchEnd, false ); 232 | scope.domElement.removeEventListener( 'touchmove', onTouchMove, false ); 233 | 234 | document.removeEventListener( 'mousemove', onMouseMove, false ); 235 | document.removeEventListener( 'mouseup', onMouseUp, false ); 236 | 237 | window.removeEventListener( 'keydown', onKeyDown, false ); 238 | 239 | //scope.dispatchEvent( { type: 'dispose' } ); // should this be added here? 240 | 241 | }; 242 | 243 | // 244 | // internals 245 | // 246 | 247 | var scope = this; 248 | 249 | var changeEvent = { type: 'change' }; 250 | var startEvent = { type: 'start' }; 251 | var endEvent = { type: 'end' }; 252 | 253 | var STATE = { NONE: - 1, ROTATE: 0, DOLLY: 1, PAN: 2, TOUCH_ROTATE: 3, TOUCH_DOLLY_PAN: 4 }; 254 | 255 | var state = STATE.NONE; 256 | 257 | var EPS = 0.000001; 258 | 259 | // current position in spherical coordinates 260 | var spherical = new THREE.Spherical(); 261 | var sphericalDelta = new THREE.Spherical(); 262 | 263 | var scale = 1; 264 | var panOffset = new THREE.Vector3(); 265 | var zoomChanged = false; 266 | 267 | var rotateStart = new THREE.Vector2(); 268 | var rotateEnd = new THREE.Vector2(); 269 | var rotateDelta = new THREE.Vector2(); 270 | 271 | var panStart = new THREE.Vector2(); 272 | var panEnd = new THREE.Vector2(); 273 | var panDelta = new THREE.Vector2(); 274 | 275 | var dollyStart = new THREE.Vector2(); 276 | var dollyEnd = new THREE.Vector2(); 277 | var dollyDelta = new THREE.Vector2(); 278 | 279 | function getAutoRotationAngle() { 280 | 281 | return 2 * Math.PI / 60 / 60 * scope.autoRotateSpeed; 282 | 283 | } 284 | 285 | function getZoomScale() { 286 | 287 | return Math.pow( 0.95, scope.zoomSpeed ); 288 | 289 | } 290 | 291 | function rotateLeft( angle ) { 292 | 293 | sphericalDelta.theta -= angle; 294 | 295 | } 296 | 297 | function rotateUp( angle ) { 298 | 299 | sphericalDelta.phi -= angle; 300 | 301 | } 302 | 303 | var panLeft = function () { 304 | 305 | var v = new THREE.Vector3(); 306 | 307 | return function panLeft( distance, objectMatrix ) { 308 | 309 | v.setFromMatrixColumn( objectMatrix, 0 ); // get X column of objectMatrix 310 | v.multiplyScalar( - distance ); 311 | 312 | panOffset.add( v ); 313 | 314 | }; 315 | 316 | }(); 317 | 318 | var panUp = function () { 319 | 320 | var v = new THREE.Vector3(); 321 | 322 | return function panUp( distance, objectMatrix ) { 323 | 324 | if ( scope.screenSpacePanning === true ) { 325 | 326 | v.setFromMatrixColumn( objectMatrix, 1 ); 327 | 328 | } else { 329 | 330 | v.setFromMatrixColumn( objectMatrix, 0 ); 331 | v.crossVectors( scope.object.up, v ); 332 | 333 | } 334 | 335 | v.multiplyScalar( distance ); 336 | 337 | panOffset.add( v ); 338 | 339 | }; 340 | 341 | }(); 342 | 343 | // deltaX and deltaY are in pixels; right and down are positive 344 | var pan = function () { 345 | 346 | var offset = new THREE.Vector3(); 347 | 348 | return function pan( deltaX, deltaY ) { 349 | 350 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 351 | 352 | if ( scope.object.isPerspectiveCamera ) { 353 | 354 | // perspective 355 | var position = scope.object.position; 356 | offset.copy( position ).sub( scope.target ); 357 | var targetDistance = offset.length(); 358 | 359 | // half of the fov is center to top of screen 360 | targetDistance *= Math.tan( ( scope.object.fov / 2 ) * Math.PI / 180.0 ); 361 | 362 | // we use only clientHeight here so aspect ratio does not distort speed 363 | panLeft( 2 * deltaX * targetDistance / element.clientHeight, scope.object.matrix ); 364 | panUp( 2 * deltaY * targetDistance / element.clientHeight, scope.object.matrix ); 365 | 366 | } else if ( scope.object.isOrthographicCamera ) { 367 | 368 | // orthographic 369 | panLeft( deltaX * ( scope.object.right - scope.object.left ) / scope.object.zoom / element.clientWidth, scope.object.matrix ); 370 | panUp( deltaY * ( scope.object.top - scope.object.bottom ) / scope.object.zoom / element.clientHeight, scope.object.matrix ); 371 | 372 | } else { 373 | 374 | // camera neither orthographic nor perspective 375 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - pan disabled.' ); 376 | scope.enablePan = false; 377 | 378 | } 379 | 380 | }; 381 | 382 | }(); 383 | 384 | function dollyIn( dollyScale ) { 385 | 386 | if ( scope.object.isPerspectiveCamera ) { 387 | 388 | scale /= dollyScale; 389 | 390 | } else if ( scope.object.isOrthographicCamera ) { 391 | 392 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom * dollyScale ) ); 393 | scope.object.updateProjectionMatrix(); 394 | zoomChanged = true; 395 | 396 | } else { 397 | 398 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 399 | scope.enableZoom = false; 400 | 401 | } 402 | 403 | } 404 | 405 | function dollyOut( dollyScale ) { 406 | 407 | if ( scope.object.isPerspectiveCamera ) { 408 | 409 | scale *= dollyScale; 410 | 411 | } else if ( scope.object.isOrthographicCamera ) { 412 | 413 | scope.object.zoom = Math.max( scope.minZoom, Math.min( scope.maxZoom, scope.object.zoom / dollyScale ) ); 414 | scope.object.updateProjectionMatrix(); 415 | zoomChanged = true; 416 | 417 | } else { 418 | 419 | console.warn( 'WARNING: OrbitControls.js encountered an unknown camera type - dolly/zoom disabled.' ); 420 | scope.enableZoom = false; 421 | 422 | } 423 | 424 | } 425 | 426 | // 427 | // event callbacks - update the object state 428 | // 429 | 430 | function handleMouseDownRotate( event ) { 431 | 432 | //console.log( 'handleMouseDownRotate' ); 433 | 434 | rotateStart.set( event.clientX, event.clientY ); 435 | 436 | } 437 | 438 | function handleMouseDownDolly( event ) { 439 | 440 | //console.log( 'handleMouseDownDolly' ); 441 | 442 | dollyStart.set( event.clientX, event.clientY ); 443 | 444 | } 445 | 446 | function handleMouseDownPan( event ) { 447 | 448 | //console.log( 'handleMouseDownPan' ); 449 | 450 | panStart.set( event.clientX, event.clientY ); 451 | 452 | } 453 | 454 | function handleMouseMoveRotate( event ) { 455 | 456 | //console.log( 'handleMouseMoveRotate' ); 457 | 458 | rotateEnd.set( event.clientX, event.clientY ); 459 | 460 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 461 | 462 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 463 | 464 | // rotating across whole screen goes 360 degrees around 465 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth ); 466 | 467 | // rotating up and down along whole screen attempts to go 360, but limited to 180 468 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 469 | 470 | rotateStart.copy( rotateEnd ); 471 | 472 | scope.update(); 473 | 474 | } 475 | 476 | function handleMouseMoveDolly( event ) { 477 | 478 | //console.log( 'handleMouseMoveDolly' ); 479 | 480 | dollyEnd.set( event.clientX, event.clientY ); 481 | 482 | dollyDelta.subVectors( dollyEnd, dollyStart ); 483 | 484 | if ( dollyDelta.y > 0 ) { 485 | 486 | dollyIn( getZoomScale() ); 487 | 488 | } else if ( dollyDelta.y < 0 ) { 489 | 490 | dollyOut( getZoomScale() ); 491 | 492 | } 493 | 494 | dollyStart.copy( dollyEnd ); 495 | 496 | scope.update(); 497 | 498 | } 499 | 500 | function handleMouseMovePan( event ) { 501 | 502 | //console.log( 'handleMouseMovePan' ); 503 | 504 | panEnd.set( event.clientX, event.clientY ); 505 | 506 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 507 | 508 | pan( panDelta.x, panDelta.y ); 509 | 510 | panStart.copy( panEnd ); 511 | 512 | scope.update(); 513 | 514 | } 515 | 516 | function handleMouseUp( event ) { 517 | 518 | // console.log( 'handleMouseUp' ); 519 | 520 | } 521 | 522 | function handleMouseWheel( event ) { 523 | 524 | // console.log( 'handleMouseWheel' ); 525 | 526 | if ( event.deltaY < 0 ) { 527 | 528 | dollyOut( getZoomScale() ); 529 | 530 | } else if ( event.deltaY > 0 ) { 531 | 532 | dollyIn( getZoomScale() ); 533 | 534 | } 535 | 536 | scope.update(); 537 | 538 | } 539 | 540 | function handleKeyDown( event ) { 541 | 542 | //console.log( 'handleKeyDown' ); 543 | 544 | switch ( event.keyCode ) { 545 | 546 | case scope.keys.UP: 547 | pan( 0, scope.keyPanSpeed ); 548 | scope.update(); 549 | break; 550 | 551 | case scope.keys.BOTTOM: 552 | pan( 0, - scope.keyPanSpeed ); 553 | scope.update(); 554 | break; 555 | 556 | case scope.keys.LEFT: 557 | pan( scope.keyPanSpeed, 0 ); 558 | scope.update(); 559 | break; 560 | 561 | case scope.keys.RIGHT: 562 | pan( - scope.keyPanSpeed, 0 ); 563 | scope.update(); 564 | break; 565 | 566 | } 567 | 568 | } 569 | 570 | function handleTouchStartRotate( event ) { 571 | 572 | //console.log( 'handleTouchStartRotate' ); 573 | 574 | rotateStart.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 575 | 576 | } 577 | 578 | function handleTouchStartDollyPan( event ) { 579 | 580 | //console.log( 'handleTouchStartDollyPan' ); 581 | 582 | if ( scope.enableZoom ) { 583 | 584 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 585 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 586 | 587 | var distance = Math.sqrt( dx * dx + dy * dy ); 588 | 589 | dollyStart.set( 0, distance ); 590 | 591 | } 592 | 593 | if ( scope.enablePan ) { 594 | 595 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 596 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 597 | 598 | panStart.set( x, y ); 599 | 600 | } 601 | 602 | } 603 | 604 | function handleTouchMoveRotate( event ) { 605 | 606 | //console.log( 'handleTouchMoveRotate' ); 607 | 608 | rotateEnd.set( event.touches[ 0 ].pageX, event.touches[ 0 ].pageY ); 609 | 610 | rotateDelta.subVectors( rotateEnd, rotateStart ).multiplyScalar( scope.rotateSpeed ); 611 | 612 | var element = scope.domElement === document ? scope.domElement.body : scope.domElement; 613 | 614 | // rotating across whole screen goes 360 degrees around 615 | rotateLeft( 2 * Math.PI * rotateDelta.x / element.clientWidth ); 616 | 617 | // rotating up and down along whole screen attempts to go 360, but limited to 180 618 | rotateUp( 2 * Math.PI * rotateDelta.y / element.clientHeight ); 619 | 620 | rotateStart.copy( rotateEnd ); 621 | 622 | scope.update(); 623 | 624 | } 625 | 626 | function handleTouchMoveDollyPan( event ) { 627 | 628 | //console.log( 'handleTouchMoveDollyPan' ); 629 | 630 | if ( scope.enableZoom ) { 631 | 632 | var dx = event.touches[ 0 ].pageX - event.touches[ 1 ].pageX; 633 | var dy = event.touches[ 0 ].pageY - event.touches[ 1 ].pageY; 634 | 635 | var distance = Math.sqrt( dx * dx + dy * dy ); 636 | 637 | dollyEnd.set( 0, distance ); 638 | 639 | dollyDelta.set( 0, Math.pow( dollyEnd.y / dollyStart.y, scope.zoomSpeed ) ); 640 | 641 | dollyIn( dollyDelta.y ); 642 | 643 | dollyStart.copy( dollyEnd ); 644 | 645 | } 646 | 647 | if ( scope.enablePan ) { 648 | 649 | var x = 0.5 * ( event.touches[ 0 ].pageX + event.touches[ 1 ].pageX ); 650 | var y = 0.5 * ( event.touches[ 0 ].pageY + event.touches[ 1 ].pageY ); 651 | 652 | panEnd.set( x, y ); 653 | 654 | panDelta.subVectors( panEnd, panStart ).multiplyScalar( scope.panSpeed ); 655 | 656 | pan( panDelta.x, panDelta.y ); 657 | 658 | panStart.copy( panEnd ); 659 | 660 | } 661 | 662 | scope.update(); 663 | 664 | } 665 | 666 | function handleTouchEnd( event ) { 667 | 668 | //console.log( 'handleTouchEnd' ); 669 | 670 | } 671 | 672 | // 673 | // event handlers - FSM: listen for events and reset state 674 | // 675 | 676 | function onMouseDown( event ) { 677 | 678 | if ( scope.enabled === false ) return; 679 | 680 | event.preventDefault(); 681 | 682 | switch ( event.button ) { 683 | 684 | case scope.mouseButtons.ORBIT: 685 | 686 | if ( scope.enableRotate === false ) return; 687 | 688 | handleMouseDownRotate( event ); 689 | 690 | state = STATE.ROTATE; 691 | 692 | break; 693 | 694 | case scope.mouseButtons.ZOOM: 695 | 696 | if ( scope.enableZoom === false ) return; 697 | 698 | handleMouseDownDolly( event ); 699 | 700 | state = STATE.DOLLY; 701 | 702 | break; 703 | 704 | case scope.mouseButtons.PAN: 705 | 706 | if ( scope.enablePan === false ) return; 707 | 708 | handleMouseDownPan( event ); 709 | 710 | state = STATE.PAN; 711 | 712 | break; 713 | 714 | } 715 | 716 | if ( state !== STATE.NONE ) { 717 | 718 | document.addEventListener( 'mousemove', onMouseMove, false ); 719 | document.addEventListener( 'mouseup', onMouseUp, false ); 720 | 721 | scope.dispatchEvent( startEvent ); 722 | 723 | } 724 | 725 | } 726 | 727 | function onMouseMove( event ) { 728 | 729 | if ( scope.enabled === false ) return; 730 | 731 | event.preventDefault(); 732 | 733 | switch ( state ) { 734 | 735 | case STATE.ROTATE: 736 | 737 | if ( scope.enableRotate === false ) return; 738 | 739 | handleMouseMoveRotate( event ); 740 | 741 | break; 742 | 743 | case STATE.DOLLY: 744 | 745 | if ( scope.enableZoom === false ) return; 746 | 747 | handleMouseMoveDolly( event ); 748 | 749 | break; 750 | 751 | case STATE.PAN: 752 | 753 | if ( scope.enablePan === false ) return; 754 | 755 | handleMouseMovePan( event ); 756 | 757 | break; 758 | 759 | } 760 | 761 | } 762 | 763 | function onMouseUp( event ) { 764 | 765 | if ( scope.enabled === false ) return; 766 | 767 | handleMouseUp( event ); 768 | 769 | document.removeEventListener( 'mousemove', onMouseMove, false ); 770 | document.removeEventListener( 'mouseup', onMouseUp, false ); 771 | 772 | scope.dispatchEvent( endEvent ); 773 | 774 | state = STATE.NONE; 775 | 776 | } 777 | 778 | function onMouseWheel( event ) { 779 | 780 | if ( scope.enabled === false || scope.enableZoom === false || ( state !== STATE.NONE && state !== STATE.ROTATE ) ) return; 781 | 782 | event.preventDefault(); 783 | event.stopPropagation(); 784 | 785 | scope.dispatchEvent( startEvent ); 786 | 787 | handleMouseWheel( event ); 788 | 789 | scope.dispatchEvent( endEvent ); 790 | 791 | } 792 | 793 | function onKeyDown( event ) { 794 | 795 | if ( scope.enabled === false || scope.enableKeys === false || scope.enablePan === false ) return; 796 | 797 | handleKeyDown( event ); 798 | 799 | } 800 | 801 | function onTouchStart( event ) { 802 | 803 | if ( scope.enabled === false ) return; 804 | 805 | event.preventDefault(); 806 | 807 | switch ( event.touches.length ) { 808 | 809 | case 1: // one-fingered touch: rotate 810 | 811 | if ( scope.enableRotate === false ) return; 812 | 813 | handleTouchStartRotate( event ); 814 | 815 | state = STATE.TOUCH_ROTATE; 816 | 817 | break; 818 | 819 | case 2: // two-fingered touch: dolly-pan 820 | 821 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 822 | 823 | handleTouchStartDollyPan( event ); 824 | 825 | state = STATE.TOUCH_DOLLY_PAN; 826 | 827 | break; 828 | 829 | default: 830 | 831 | state = STATE.NONE; 832 | 833 | } 834 | 835 | if ( state !== STATE.NONE ) { 836 | 837 | scope.dispatchEvent( startEvent ); 838 | 839 | } 840 | 841 | } 842 | 843 | function onTouchMove( event ) { 844 | 845 | if ( scope.enabled === false ) return; 846 | 847 | event.preventDefault(); 848 | event.stopPropagation(); 849 | 850 | switch ( event.touches.length ) { 851 | 852 | case 1: // one-fingered touch: rotate 853 | 854 | if ( scope.enableRotate === false ) return; 855 | if ( state !== STATE.TOUCH_ROTATE ) return; // is this needed? 856 | 857 | handleTouchMoveRotate( event ); 858 | 859 | break; 860 | 861 | case 2: // two-fingered touch: dolly-pan 862 | 863 | if ( scope.enableZoom === false && scope.enablePan === false ) return; 864 | if ( state !== STATE.TOUCH_DOLLY_PAN ) return; // is this needed? 865 | 866 | handleTouchMoveDollyPan( event ); 867 | 868 | break; 869 | 870 | default: 871 | 872 | state = STATE.NONE; 873 | 874 | } 875 | 876 | } 877 | 878 | function onTouchEnd( event ) { 879 | 880 | if ( scope.enabled === false ) return; 881 | 882 | handleTouchEnd( event ); 883 | 884 | scope.dispatchEvent( endEvent ); 885 | 886 | state = STATE.NONE; 887 | 888 | } 889 | 890 | function onContextMenu( event ) { 891 | 892 | if ( scope.enabled === false ) return; 893 | 894 | event.preventDefault(); 895 | 896 | } 897 | 898 | // 899 | 900 | scope.domElement.addEventListener( 'contextmenu', onContextMenu, false ); 901 | 902 | scope.domElement.addEventListener( 'mousedown', onMouseDown, false ); 903 | scope.domElement.addEventListener( 'wheel', onMouseWheel, false ); 904 | 905 | scope.domElement.addEventListener( 'touchstart', onTouchStart, false ); 906 | scope.domElement.addEventListener( 'touchend', onTouchEnd, false ); 907 | scope.domElement.addEventListener( 'touchmove', onTouchMove, false ); 908 | 909 | window.addEventListener( 'keydown', onKeyDown, false ); 910 | 911 | // force an update at start 912 | 913 | this.update(); 914 | 915 | }; 916 | 917 | THREE.OrbitControls.prototype = Object.create( THREE.EventDispatcher.prototype ); 918 | THREE.OrbitControls.prototype.constructor = THREE.OrbitControls; 919 | 920 | Object.defineProperties( THREE.OrbitControls.prototype, { 921 | 922 | center: { 923 | 924 | get: function () { 925 | 926 | console.warn( 'THREE.OrbitControls: .center has been renamed to .target' ); 927 | return this.target; 928 | 929 | } 930 | 931 | }, 932 | 933 | // backward compatibility 934 | 935 | noZoom: { 936 | 937 | get: function () { 938 | 939 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 940 | return ! this.enableZoom; 941 | 942 | }, 943 | 944 | set: function ( value ) { 945 | 946 | console.warn( 'THREE.OrbitControls: .noZoom has been deprecated. Use .enableZoom instead.' ); 947 | this.enableZoom = ! value; 948 | 949 | } 950 | 951 | }, 952 | 953 | noRotate: { 954 | 955 | get: function () { 956 | 957 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 958 | return ! this.enableRotate; 959 | 960 | }, 961 | 962 | set: function ( value ) { 963 | 964 | console.warn( 'THREE.OrbitControls: .noRotate has been deprecated. Use .enableRotate instead.' ); 965 | this.enableRotate = ! value; 966 | 967 | } 968 | 969 | }, 970 | 971 | noPan: { 972 | 973 | get: function () { 974 | 975 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 976 | return ! this.enablePan; 977 | 978 | }, 979 | 980 | set: function ( value ) { 981 | 982 | console.warn( 'THREE.OrbitControls: .noPan has been deprecated. Use .enablePan instead.' ); 983 | this.enablePan = ! value; 984 | 985 | } 986 | 987 | }, 988 | 989 | noKeys: { 990 | 991 | get: function () { 992 | 993 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 994 | return ! this.enableKeys; 995 | 996 | }, 997 | 998 | set: function ( value ) { 999 | 1000 | console.warn( 'THREE.OrbitControls: .noKeys has been deprecated. Use .enableKeys instead.' ); 1001 | this.enableKeys = ! value; 1002 | 1003 | } 1004 | 1005 | }, 1006 | 1007 | staticMoving: { 1008 | 1009 | get: function () { 1010 | 1011 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1012 | return ! this.enableDamping; 1013 | 1014 | }, 1015 | 1016 | set: function ( value ) { 1017 | 1018 | console.warn( 'THREE.OrbitControls: .staticMoving has been deprecated. Use .enableDamping instead.' ); 1019 | this.enableDamping = ! value; 1020 | 1021 | } 1022 | 1023 | }, 1024 | 1025 | dynamicDampingFactor: { 1026 | 1027 | get: function () { 1028 | 1029 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1030 | return this.dampingFactor; 1031 | 1032 | }, 1033 | 1034 | set: function ( value ) { 1035 | 1036 | console.warn( 'THREE.OrbitControls: .dynamicDampingFactor has been renamed. Use .dampingFactor instead.' ); 1037 | this.dampingFactor = value; 1038 | 1039 | } 1040 | 1041 | } 1042 | 1043 | } ); 1044 | -------------------------------------------------------------------------------- /Projector.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | * @author supereggbert / http://www.paulbrunt.co.uk/ 4 | * @author julianwa / https://github.com/julianwa 5 | */ 6 | 7 | THREE.RenderableObject = function () { 8 | 9 | this.id = 0; 10 | 11 | this.object = null; 12 | this.z = 0; 13 | this.renderOrder = 0; 14 | 15 | }; 16 | 17 | // 18 | 19 | THREE.RenderableFace = function () { 20 | 21 | this.id = 0; 22 | 23 | this.v1 = new THREE.RenderableVertex(); 24 | this.v2 = new THREE.RenderableVertex(); 25 | this.v3 = new THREE.RenderableVertex(); 26 | 27 | this.normalModel = new THREE.Vector3(); 28 | 29 | this.vertexNormalsModel = [ new THREE.Vector3(), new THREE.Vector3(), new THREE.Vector3() ]; 30 | this.vertexNormalsLength = 0; 31 | 32 | this.color = new THREE.Color(); 33 | this.material = null; 34 | this.uvs = [ new THREE.Vector2(), new THREE.Vector2(), new THREE.Vector2() ]; 35 | 36 | this.z = 0; 37 | this.renderOrder = 0; 38 | 39 | }; 40 | 41 | // 42 | 43 | THREE.RenderableVertex = function () { 44 | 45 | this.position = new THREE.Vector3(); 46 | this.positionWorld = new THREE.Vector3(); 47 | this.positionScreen = new THREE.Vector4(); 48 | 49 | this.visible = true; 50 | 51 | }; 52 | 53 | THREE.RenderableVertex.prototype.copy = function ( vertex ) { 54 | 55 | this.positionWorld.copy( vertex.positionWorld ); 56 | this.positionScreen.copy( vertex.positionScreen ); 57 | 58 | }; 59 | 60 | // 61 | 62 | THREE.RenderableLine = function () { 63 | 64 | this.id = 0; 65 | 66 | this.v1 = new THREE.RenderableVertex(); 67 | this.v2 = new THREE.RenderableVertex(); 68 | 69 | this.vertexColors = [ new THREE.Color(), new THREE.Color() ]; 70 | this.material = null; 71 | 72 | this.z = 0; 73 | this.renderOrder = 0; 74 | 75 | }; 76 | 77 | // 78 | 79 | THREE.RenderableSprite = function () { 80 | 81 | this.id = 0; 82 | 83 | this.object = null; 84 | 85 | this.x = 0; 86 | this.y = 0; 87 | this.z = 0; 88 | 89 | this.rotation = 0; 90 | this.scale = new THREE.Vector2(); 91 | 92 | this.material = null; 93 | this.renderOrder = 0; 94 | 95 | }; 96 | 97 | // 98 | 99 | THREE.Projector = function () { 100 | 101 | var _object, _objectCount, _objectPool = [], _objectPoolLength = 0, 102 | _vertex, _vertexCount, _vertexPool = [], _vertexPoolLength = 0, 103 | _face, _faceCount, _facePool = [], _facePoolLength = 0, 104 | _line, _lineCount, _linePool = [], _linePoolLength = 0, 105 | _sprite, _spriteCount, _spritePool = [], _spritePoolLength = 0, 106 | 107 | _renderData = { objects: [], lights: [], elements: [] }, 108 | 109 | _vector3 = new THREE.Vector3(), 110 | _vector4 = new THREE.Vector4(), 111 | 112 | _clipBox = new THREE.Box3( new THREE.Vector3( - 1, - 1, - 1 ), new THREE.Vector3( 1, 1, 1 ) ), 113 | _boundingBox = new THREE.Box3(), 114 | _points3 = new Array( 3 ), 115 | 116 | _viewMatrix = new THREE.Matrix4(), 117 | _viewProjectionMatrix = new THREE.Matrix4(), 118 | 119 | _modelMatrix, 120 | _modelViewProjectionMatrix = new THREE.Matrix4(), 121 | 122 | _normalMatrix = new THREE.Matrix3(), 123 | 124 | _frustum = new THREE.Frustum(), 125 | 126 | _clippedVertex1PositionScreen = new THREE.Vector4(), 127 | _clippedVertex2PositionScreen = new THREE.Vector4(); 128 | 129 | // 130 | 131 | this.projectVector = function ( vector, camera ) { 132 | 133 | console.warn( 'THREE.Projector: .projectVector() is now vector.project().' ); 134 | vector.project( camera ); 135 | 136 | }; 137 | 138 | this.unprojectVector = function ( vector, camera ) { 139 | 140 | console.warn( 'THREE.Projector: .unprojectVector() is now vector.unproject().' ); 141 | vector.unproject( camera ); 142 | 143 | }; 144 | 145 | this.pickingRay = function () { 146 | 147 | console.error( 'THREE.Projector: .pickingRay() is now raycaster.setFromCamera().' ); 148 | 149 | }; 150 | 151 | // 152 | 153 | var RenderList = function () { 154 | 155 | var normals = []; 156 | var colors = []; 157 | var uvs = []; 158 | 159 | var object = null; 160 | var material = null; 161 | 162 | var normalMatrix = new THREE.Matrix3(); 163 | 164 | function setObject( value ) { 165 | 166 | object = value; 167 | material = object.material; 168 | 169 | normalMatrix.getNormalMatrix( object.matrixWorld ); 170 | 171 | normals.length = 0; 172 | colors.length = 0; 173 | uvs.length = 0; 174 | 175 | } 176 | 177 | function projectVertex( vertex ) { 178 | 179 | var position = vertex.position; 180 | var positionWorld = vertex.positionWorld; 181 | var positionScreen = vertex.positionScreen; 182 | 183 | positionWorld.copy( position ).applyMatrix4( _modelMatrix ); 184 | positionScreen.copy( positionWorld ).applyMatrix4( _viewProjectionMatrix ); 185 | 186 | var invW = 1 / positionScreen.w; 187 | 188 | positionScreen.x *= invW; 189 | positionScreen.y *= invW; 190 | positionScreen.z *= invW; 191 | 192 | vertex.visible = positionScreen.x >= - 1 && positionScreen.x <= 1 && 193 | positionScreen.y >= - 1 && positionScreen.y <= 1 && 194 | positionScreen.z >= - 1 && positionScreen.z <= 1; 195 | 196 | } 197 | 198 | function pushVertex( x, y, z ) { 199 | 200 | _vertex = getNextVertexInPool(); 201 | _vertex.position.set( x, y, z ); 202 | 203 | projectVertex( _vertex ); 204 | 205 | } 206 | 207 | function pushNormal( x, y, z ) { 208 | 209 | normals.push( x, y, z ); 210 | 211 | } 212 | 213 | function pushColor( r, g, b ) { 214 | 215 | colors.push( r, g, b ); 216 | 217 | } 218 | 219 | function pushUv( x, y ) { 220 | 221 | uvs.push( x, y ); 222 | 223 | } 224 | 225 | function checkTriangleVisibility( v1, v2, v3 ) { 226 | 227 | if ( v1.visible === true || v2.visible === true || v3.visible === true ) return true; 228 | 229 | _points3[ 0 ] = v1.positionScreen; 230 | _points3[ 1 ] = v2.positionScreen; 231 | _points3[ 2 ] = v3.positionScreen; 232 | 233 | return _clipBox.intersectsBox( _boundingBox.setFromPoints( _points3 ) ); 234 | 235 | } 236 | 237 | function checkBackfaceCulling( v1, v2, v3 ) { 238 | 239 | return ( ( v3.positionScreen.x - v1.positionScreen.x ) * 240 | ( v2.positionScreen.y - v1.positionScreen.y ) - 241 | ( v3.positionScreen.y - v1.positionScreen.y ) * 242 | ( v2.positionScreen.x - v1.positionScreen.x ) ) < 0; 243 | 244 | } 245 | 246 | function pushLine( a, b ) { 247 | 248 | var v1 = _vertexPool[ a ]; 249 | var v2 = _vertexPool[ b ]; 250 | 251 | // Clip 252 | 253 | v1.positionScreen.copy( v1.position ).applyMatrix4( _modelViewProjectionMatrix ); 254 | v2.positionScreen.copy( v2.position ).applyMatrix4( _modelViewProjectionMatrix ); 255 | 256 | if ( clipLine( v1.positionScreen, v2.positionScreen ) === true ) { 257 | 258 | // Perform the perspective divide 259 | v1.positionScreen.multiplyScalar( 1 / v1.positionScreen.w ); 260 | v2.positionScreen.multiplyScalar( 1 / v2.positionScreen.w ); 261 | 262 | _line = getNextLineInPool(); 263 | _line.id = object.id; 264 | _line.v1.copy( v1 ); 265 | _line.v2.copy( v2 ); 266 | _line.z = Math.max( v1.positionScreen.z, v2.positionScreen.z ); 267 | _line.renderOrder = object.renderOrder; 268 | 269 | _line.material = object.material; 270 | 271 | if ( object.material.vertexColors === THREE.VertexColors ) { 272 | 273 | _line.vertexColors[ 0 ].fromArray( colors, a * 3 ); 274 | _line.vertexColors[ 1 ].fromArray( colors, b * 3 ); 275 | 276 | } 277 | 278 | _renderData.elements.push( _line ); 279 | 280 | } 281 | 282 | } 283 | 284 | function pushTriangle( a, b, c ) { 285 | 286 | var v1 = _vertexPool[ a ]; 287 | var v2 = _vertexPool[ b ]; 288 | var v3 = _vertexPool[ c ]; 289 | 290 | if ( checkTriangleVisibility( v1, v2, v3 ) === false ) return; 291 | 292 | if ( material.side === THREE.DoubleSide || checkBackfaceCulling( v1, v2, v3 ) === true ) { 293 | 294 | _face = getNextFaceInPool(); 295 | 296 | _face.id = object.id; 297 | _face.v1.copy( v1 ); 298 | _face.v2.copy( v2 ); 299 | _face.v3.copy( v3 ); 300 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; 301 | _face.renderOrder = object.renderOrder; 302 | 303 | // use first vertex normal as face normal 304 | 305 | _face.normalModel.fromArray( normals, a * 3 ); 306 | _face.normalModel.applyMatrix3( normalMatrix ).normalize(); 307 | 308 | for ( var i = 0; i < 3; i ++ ) { 309 | 310 | var normal = _face.vertexNormalsModel[ i ]; 311 | normal.fromArray( normals, arguments[ i ] * 3 ); 312 | normal.applyMatrix3( normalMatrix ).normalize(); 313 | 314 | var uv = _face.uvs[ i ]; 315 | uv.fromArray( uvs, arguments[ i ] * 2 ); 316 | 317 | } 318 | 319 | _face.vertexNormalsLength = 3; 320 | 321 | _face.material = object.material; 322 | 323 | _renderData.elements.push( _face ); 324 | 325 | } 326 | 327 | } 328 | 329 | return { 330 | setObject: setObject, 331 | projectVertex: projectVertex, 332 | checkTriangleVisibility: checkTriangleVisibility, 333 | checkBackfaceCulling: checkBackfaceCulling, 334 | pushVertex: pushVertex, 335 | pushNormal: pushNormal, 336 | pushColor: pushColor, 337 | pushUv: pushUv, 338 | pushLine: pushLine, 339 | pushTriangle: pushTriangle 340 | }; 341 | 342 | }; 343 | 344 | var renderList = new RenderList(); 345 | 346 | function projectObject( object ) { 347 | 348 | if ( object.visible === false ) return; 349 | 350 | if ( object instanceof THREE.Light ) { 351 | 352 | _renderData.lights.push( object ); 353 | 354 | } else if ( object instanceof THREE.Mesh || object instanceof THREE.Line || object instanceof THREE.Points ) { 355 | 356 | if ( object.material.visible === false ) return; 357 | if ( object.frustumCulled === true && _frustum.intersectsObject( object ) === false ) return; 358 | 359 | addObject( object ); 360 | 361 | } else if ( object instanceof THREE.Sprite ) { 362 | 363 | if ( object.material.visible === false ) return; 364 | if ( object.frustumCulled === true && _frustum.intersectsSprite( object ) === false ) return; 365 | 366 | addObject( object ); 367 | 368 | } 369 | 370 | var children = object.children; 371 | 372 | for ( var i = 0, l = children.length; i < l; i ++ ) { 373 | 374 | projectObject( children[ i ] ); 375 | 376 | } 377 | 378 | } 379 | 380 | function addObject( object ) { 381 | 382 | _object = getNextObjectInPool(); 383 | _object.id = object.id; 384 | _object.object = object; 385 | 386 | _vector3.setFromMatrixPosition( object.matrixWorld ); 387 | _vector3.applyMatrix4( _viewProjectionMatrix ); 388 | _object.z = _vector3.z; 389 | _object.renderOrder = object.renderOrder; 390 | 391 | _renderData.objects.push( _object ); 392 | 393 | } 394 | 395 | this.projectScene = function ( scene, camera, sortObjects, sortElements ) { 396 | 397 | _faceCount = 0; 398 | _lineCount = 0; 399 | _spriteCount = 0; 400 | 401 | _renderData.elements.length = 0; 402 | 403 | if ( scene.autoUpdate === true ) scene.updateMatrixWorld(); 404 | if ( camera.parent === null ) camera.updateMatrixWorld(); 405 | 406 | _viewMatrix.copy( camera.matrixWorldInverse ); 407 | _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix ); 408 | 409 | _frustum.setFromMatrix( _viewProjectionMatrix ); 410 | 411 | // 412 | 413 | _objectCount = 0; 414 | 415 | _renderData.objects.length = 0; 416 | _renderData.lights.length = 0; 417 | 418 | projectObject( scene ); 419 | 420 | if ( sortObjects === true ) { 421 | 422 | _renderData.objects.sort( painterSort ); 423 | 424 | } 425 | 426 | // 427 | 428 | var objects = _renderData.objects; 429 | 430 | for ( var o = 0, ol = objects.length; o < ol; o ++ ) { 431 | 432 | var object = objects[ o ].object; 433 | var geometry = object.geometry; 434 | 435 | renderList.setObject( object ); 436 | 437 | _modelMatrix = object.matrixWorld; 438 | 439 | _vertexCount = 0; 440 | 441 | if ( object instanceof THREE.Mesh ) { 442 | 443 | if ( geometry instanceof THREE.BufferGeometry ) { 444 | 445 | var attributes = geometry.attributes; 446 | var groups = geometry.groups; 447 | 448 | if ( attributes.position === undefined ) continue; 449 | 450 | var positions = attributes.position.array; 451 | 452 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 453 | 454 | renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] ); 455 | 456 | } 457 | 458 | if ( attributes.normal !== undefined ) { 459 | 460 | var normals = attributes.normal.array; 461 | 462 | for ( var i = 0, l = normals.length; i < l; i += 3 ) { 463 | 464 | renderList.pushNormal( normals[ i ], normals[ i + 1 ], normals[ i + 2 ] ); 465 | 466 | } 467 | 468 | } 469 | 470 | if ( attributes.uv !== undefined ) { 471 | 472 | var uvs = attributes.uv.array; 473 | 474 | for ( var i = 0, l = uvs.length; i < l; i += 2 ) { 475 | 476 | renderList.pushUv( uvs[ i ], uvs[ i + 1 ] ); 477 | 478 | } 479 | 480 | } 481 | 482 | if ( geometry.index !== null ) { 483 | 484 | var indices = geometry.index.array; 485 | 486 | if ( groups.length > 0 ) { 487 | 488 | for ( var g = 0; g < groups.length; g ++ ) { 489 | 490 | var group = groups[ g ]; 491 | 492 | for ( var i = group.start, l = group.start + group.count; i < l; i += 3 ) { 493 | 494 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] ); 495 | 496 | } 497 | 498 | } 499 | 500 | } else { 501 | 502 | for ( var i = 0, l = indices.length; i < l; i += 3 ) { 503 | 504 | renderList.pushTriangle( indices[ i ], indices[ i + 1 ], indices[ i + 2 ] ); 505 | 506 | } 507 | 508 | } 509 | 510 | } else { 511 | 512 | for ( var i = 0, l = positions.length / 3; i < l; i += 3 ) { 513 | 514 | renderList.pushTriangle( i, i + 1, i + 2 ); 515 | 516 | } 517 | 518 | } 519 | 520 | } else if ( geometry instanceof THREE.Geometry ) { 521 | 522 | var vertices = geometry.vertices; 523 | var faces = geometry.faces; 524 | var faceVertexUvs = geometry.faceVertexUvs[ 0 ]; 525 | 526 | _normalMatrix.getNormalMatrix( _modelMatrix ); 527 | 528 | var material = object.material; 529 | 530 | var isMultiMaterial = Array.isArray( material ); 531 | 532 | for ( var v = 0, vl = vertices.length; v < vl; v ++ ) { 533 | 534 | var vertex = vertices[ v ]; 535 | 536 | _vector3.copy( vertex ); 537 | 538 | if ( material.morphTargets === true ) { 539 | 540 | var morphTargets = geometry.morphTargets; 541 | var morphInfluences = object.morphTargetInfluences; 542 | 543 | for ( var t = 0, tl = morphTargets.length; t < tl; t ++ ) { 544 | 545 | var influence = morphInfluences[ t ]; 546 | 547 | if ( influence === 0 ) continue; 548 | 549 | var target = morphTargets[ t ]; 550 | var targetVertex = target.vertices[ v ]; 551 | 552 | _vector3.x += ( targetVertex.x - vertex.x ) * influence; 553 | _vector3.y += ( targetVertex.y - vertex.y ) * influence; 554 | _vector3.z += ( targetVertex.z - vertex.z ) * influence; 555 | 556 | } 557 | 558 | } 559 | 560 | renderList.pushVertex( _vector3.x, _vector3.y, _vector3.z ); 561 | 562 | } 563 | 564 | for ( var f = 0, fl = faces.length; f < fl; f ++ ) { 565 | 566 | var face = faces[ f ]; 567 | 568 | material = isMultiMaterial === true 569 | ? object.material[ face.materialIndex ] 570 | : object.material; 571 | 572 | if ( material === undefined ) continue; 573 | 574 | var side = material.side; 575 | 576 | var v1 = _vertexPool[ face.a ]; 577 | var v2 = _vertexPool[ face.b ]; 578 | var v3 = _vertexPool[ face.c ]; 579 | 580 | if ( renderList.checkTriangleVisibility( v1, v2, v3 ) === false ) continue; 581 | 582 | var visible = renderList.checkBackfaceCulling( v1, v2, v3 ); 583 | 584 | if ( side !== THREE.DoubleSide ) { 585 | 586 | if ( side === THREE.FrontSide && visible === false ) continue; 587 | if ( side === THREE.BackSide && visible === true ) continue; 588 | 589 | } 590 | 591 | _face = getNextFaceInPool(); 592 | 593 | _face.id = object.id; 594 | _face.v1.copy( v1 ); 595 | _face.v2.copy( v2 ); 596 | _face.v3.copy( v3 ); 597 | 598 | _face.normalModel.copy( face.normal ); 599 | 600 | if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) { 601 | 602 | _face.normalModel.negate(); 603 | 604 | } 605 | 606 | _face.normalModel.applyMatrix3( _normalMatrix ).normalize(); 607 | 608 | var faceVertexNormals = face.vertexNormals; 609 | 610 | for ( var n = 0, nl = Math.min( faceVertexNormals.length, 3 ); n < nl; n ++ ) { 611 | 612 | var normalModel = _face.vertexNormalsModel[ n ]; 613 | normalModel.copy( faceVertexNormals[ n ] ); 614 | 615 | if ( visible === false && ( side === THREE.BackSide || side === THREE.DoubleSide ) ) { 616 | 617 | normalModel.negate(); 618 | 619 | } 620 | 621 | normalModel.applyMatrix3( _normalMatrix ).normalize(); 622 | 623 | } 624 | 625 | _face.vertexNormalsLength = faceVertexNormals.length; 626 | 627 | var vertexUvs = faceVertexUvs[ f ]; 628 | 629 | if ( vertexUvs !== undefined ) { 630 | 631 | for ( var u = 0; u < 3; u ++ ) { 632 | 633 | _face.uvs[ u ].copy( vertexUvs[ u ] ); 634 | 635 | } 636 | 637 | } 638 | 639 | _face.color = face.color; 640 | _face.material = material; 641 | 642 | _face.z = ( v1.positionScreen.z + v2.positionScreen.z + v3.positionScreen.z ) / 3; 643 | _face.renderOrder = object.renderOrder; 644 | 645 | _renderData.elements.push( _face ); 646 | 647 | } 648 | 649 | } 650 | 651 | } else if ( object instanceof THREE.Line ) { 652 | 653 | _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix ); 654 | 655 | if ( geometry instanceof THREE.BufferGeometry ) { 656 | 657 | var attributes = geometry.attributes; 658 | 659 | if ( attributes.position !== undefined ) { 660 | 661 | var positions = attributes.position.array; 662 | 663 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 664 | 665 | renderList.pushVertex( positions[ i ], positions[ i + 1 ], positions[ i + 2 ] ); 666 | 667 | } 668 | 669 | if ( attributes.color !== undefined ) { 670 | 671 | var colors = attributes.color.array; 672 | 673 | for ( var i = 0, l = colors.length; i < l; i += 3 ) { 674 | 675 | renderList.pushColor( colors[ i ], colors[ i + 1 ], colors[ i + 2 ] ); 676 | 677 | } 678 | 679 | } 680 | 681 | if ( geometry.index !== null ) { 682 | 683 | var indices = geometry.index.array; 684 | 685 | for ( var i = 0, l = indices.length; i < l; i += 2 ) { 686 | 687 | renderList.pushLine( indices[ i ], indices[ i + 1 ] ); 688 | 689 | } 690 | 691 | } else { 692 | 693 | var step = object instanceof THREE.LineSegments ? 2 : 1; 694 | 695 | for ( var i = 0, l = ( positions.length / 3 ) - 1; i < l; i += step ) { 696 | 697 | renderList.pushLine( i, i + 1 ); 698 | 699 | } 700 | 701 | } 702 | 703 | } 704 | 705 | } else if ( geometry instanceof THREE.Geometry ) { 706 | 707 | var vertices = object.geometry.vertices; 708 | 709 | if ( vertices.length === 0 ) continue; 710 | 711 | v1 = getNextVertexInPool(); 712 | v1.positionScreen.copy( vertices[ 0 ] ).applyMatrix4( _modelViewProjectionMatrix ); 713 | 714 | var step = object instanceof THREE.LineSegments ? 2 : 1; 715 | 716 | for ( var v = 1, vl = vertices.length; v < vl; v ++ ) { 717 | 718 | v1 = getNextVertexInPool(); 719 | v1.positionScreen.copy( vertices[ v ] ).applyMatrix4( _modelViewProjectionMatrix ); 720 | 721 | if ( ( v + 1 ) % step > 0 ) continue; 722 | 723 | v2 = _vertexPool[ _vertexCount - 2 ]; 724 | 725 | _clippedVertex1PositionScreen.copy( v1.positionScreen ); 726 | _clippedVertex2PositionScreen.copy( v2.positionScreen ); 727 | 728 | if ( clipLine( _clippedVertex1PositionScreen, _clippedVertex2PositionScreen ) === true ) { 729 | 730 | // Perform the perspective divide 731 | _clippedVertex1PositionScreen.multiplyScalar( 1 / _clippedVertex1PositionScreen.w ); 732 | _clippedVertex2PositionScreen.multiplyScalar( 1 / _clippedVertex2PositionScreen.w ); 733 | 734 | _line = getNextLineInPool(); 735 | 736 | _line.id = object.id; 737 | _line.v1.positionScreen.copy( _clippedVertex1PositionScreen ); 738 | _line.v2.positionScreen.copy( _clippedVertex2PositionScreen ); 739 | 740 | _line.z = Math.max( _clippedVertex1PositionScreen.z, _clippedVertex2PositionScreen.z ); 741 | _line.renderOrder = object.renderOrder; 742 | 743 | _line.material = object.material; 744 | 745 | if ( object.material.vertexColors === THREE.VertexColors ) { 746 | 747 | _line.vertexColors[ 0 ].copy( object.geometry.colors[ v ] ); 748 | _line.vertexColors[ 1 ].copy( object.geometry.colors[ v - 1 ] ); 749 | 750 | } 751 | 752 | _renderData.elements.push( _line ); 753 | 754 | } 755 | 756 | } 757 | 758 | } 759 | 760 | } else if ( object instanceof THREE.Points ) { 761 | 762 | _modelViewProjectionMatrix.multiplyMatrices( _viewProjectionMatrix, _modelMatrix ); 763 | 764 | if ( geometry instanceof THREE.Geometry ) { 765 | 766 | var vertices = object.geometry.vertices; 767 | 768 | for ( var v = 0, vl = vertices.length; v < vl; v ++ ) { 769 | 770 | var vertex = vertices[ v ]; 771 | 772 | _vector4.set( vertex.x, vertex.y, vertex.z, 1 ); 773 | _vector4.applyMatrix4( _modelViewProjectionMatrix ); 774 | 775 | pushPoint( _vector4, object, camera ); 776 | 777 | } 778 | 779 | } else if ( geometry instanceof THREE.BufferGeometry ) { 780 | 781 | var attributes = geometry.attributes; 782 | 783 | if ( attributes.position !== undefined ) { 784 | 785 | var positions = attributes.position.array; 786 | 787 | for ( var i = 0, l = positions.length; i < l; i += 3 ) { 788 | 789 | _vector4.set( positions[ i ], positions[ i + 1 ], positions[ i + 2 ], 1 ); 790 | _vector4.applyMatrix4( _modelViewProjectionMatrix ); 791 | 792 | pushPoint( _vector4, object, camera ); 793 | 794 | } 795 | 796 | } 797 | 798 | } 799 | 800 | } else if ( object instanceof THREE.Sprite ) { 801 | 802 | _vector4.set( _modelMatrix.elements[ 12 ], _modelMatrix.elements[ 13 ], _modelMatrix.elements[ 14 ], 1 ); 803 | _vector4.applyMatrix4( _viewProjectionMatrix ); 804 | 805 | pushPoint( _vector4, object, camera ); 806 | 807 | } 808 | 809 | } 810 | 811 | if ( sortElements === true ) { 812 | 813 | _renderData.elements.sort( painterSort ); 814 | 815 | } 816 | 817 | return _renderData; 818 | 819 | }; 820 | 821 | function pushPoint( _vector4, object, camera ) { 822 | 823 | var invW = 1 / _vector4.w; 824 | 825 | _vector4.z *= invW; 826 | 827 | if ( _vector4.z >= - 1 && _vector4.z <= 1 ) { 828 | 829 | _sprite = getNextSpriteInPool(); 830 | _sprite.id = object.id; 831 | _sprite.x = _vector4.x * invW; 832 | _sprite.y = _vector4.y * invW; 833 | _sprite.z = _vector4.z; 834 | _sprite.renderOrder = object.renderOrder; 835 | _sprite.object = object; 836 | 837 | _sprite.rotation = object.rotation; 838 | 839 | _sprite.scale.x = object.scale.x * Math.abs( _sprite.x - ( _vector4.x + camera.projectionMatrix.elements[ 0 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 12 ] ) ); 840 | _sprite.scale.y = object.scale.y * Math.abs( _sprite.y - ( _vector4.y + camera.projectionMatrix.elements[ 5 ] ) / ( _vector4.w + camera.projectionMatrix.elements[ 13 ] ) ); 841 | 842 | _sprite.material = object.material; 843 | 844 | _renderData.elements.push( _sprite ); 845 | 846 | } 847 | 848 | } 849 | 850 | // Pools 851 | 852 | function getNextObjectInPool() { 853 | 854 | if ( _objectCount === _objectPoolLength ) { 855 | 856 | var object = new THREE.RenderableObject(); 857 | _objectPool.push( object ); 858 | _objectPoolLength ++; 859 | _objectCount ++; 860 | return object; 861 | 862 | } 863 | 864 | return _objectPool[ _objectCount ++ ]; 865 | 866 | } 867 | 868 | function getNextVertexInPool() { 869 | 870 | if ( _vertexCount === _vertexPoolLength ) { 871 | 872 | var vertex = new THREE.RenderableVertex(); 873 | _vertexPool.push( vertex ); 874 | _vertexPoolLength ++; 875 | _vertexCount ++; 876 | return vertex; 877 | 878 | } 879 | 880 | return _vertexPool[ _vertexCount ++ ]; 881 | 882 | } 883 | 884 | function getNextFaceInPool() { 885 | 886 | if ( _faceCount === _facePoolLength ) { 887 | 888 | var face = new THREE.RenderableFace(); 889 | _facePool.push( face ); 890 | _facePoolLength ++; 891 | _faceCount ++; 892 | return face; 893 | 894 | } 895 | 896 | return _facePool[ _faceCount ++ ]; 897 | 898 | 899 | } 900 | 901 | function getNextLineInPool() { 902 | 903 | if ( _lineCount === _linePoolLength ) { 904 | 905 | var line = new THREE.RenderableLine(); 906 | _linePool.push( line ); 907 | _linePoolLength ++; 908 | _lineCount ++; 909 | return line; 910 | 911 | } 912 | 913 | return _linePool[ _lineCount ++ ]; 914 | 915 | } 916 | 917 | function getNextSpriteInPool() { 918 | 919 | if ( _spriteCount === _spritePoolLength ) { 920 | 921 | var sprite = new THREE.RenderableSprite(); 922 | _spritePool.push( sprite ); 923 | _spritePoolLength ++; 924 | _spriteCount ++; 925 | return sprite; 926 | 927 | } 928 | 929 | return _spritePool[ _spriteCount ++ ]; 930 | 931 | } 932 | 933 | // 934 | 935 | function painterSort( a, b ) { 936 | 937 | if ( a.renderOrder !== b.renderOrder ) { 938 | 939 | return a.renderOrder - b.renderOrder; 940 | 941 | } else if ( a.z !== b.z ) { 942 | 943 | return b.z - a.z; 944 | 945 | } else if ( a.id !== b.id ) { 946 | 947 | return a.id - b.id; 948 | 949 | } else { 950 | 951 | return 0; 952 | 953 | } 954 | 955 | } 956 | 957 | function clipLine( s1, s2 ) { 958 | 959 | var alpha1 = 0, alpha2 = 1, 960 | 961 | // Calculate the boundary coordinate of each vertex for the near and far clip planes, 962 | // Z = -1 and Z = +1, respectively. 963 | 964 | bc1near = s1.z + s1.w, 965 | bc2near = s2.z + s2.w, 966 | bc1far = - s1.z + s1.w, 967 | bc2far = - s2.z + s2.w; 968 | 969 | if ( bc1near >= 0 && bc2near >= 0 && bc1far >= 0 && bc2far >= 0 ) { 970 | 971 | // Both vertices lie entirely within all clip planes. 972 | return true; 973 | 974 | } else if ( ( bc1near < 0 && bc2near < 0 ) || ( bc1far < 0 && bc2far < 0 ) ) { 975 | 976 | // Both vertices lie entirely outside one of the clip planes. 977 | return false; 978 | 979 | } else { 980 | 981 | // The line segment spans at least one clip plane. 982 | 983 | if ( bc1near < 0 ) { 984 | 985 | // v1 lies outside the near plane, v2 inside 986 | alpha1 = Math.max( alpha1, bc1near / ( bc1near - bc2near ) ); 987 | 988 | } else if ( bc2near < 0 ) { 989 | 990 | // v2 lies outside the near plane, v1 inside 991 | alpha2 = Math.min( alpha2, bc1near / ( bc1near - bc2near ) ); 992 | 993 | } 994 | 995 | if ( bc1far < 0 ) { 996 | 997 | // v1 lies outside the far plane, v2 inside 998 | alpha1 = Math.max( alpha1, bc1far / ( bc1far - bc2far ) ); 999 | 1000 | } else if ( bc2far < 0 ) { 1001 | 1002 | // v2 lies outside the far plane, v2 inside 1003 | alpha2 = Math.min( alpha2, bc1far / ( bc1far - bc2far ) ); 1004 | 1005 | } 1006 | 1007 | if ( alpha2 < alpha1 ) { 1008 | 1009 | // The line segment spans two boundaries, but is outside both of them. 1010 | // (This can't happen when we're only clipping against just near/far but good 1011 | // to leave the check here for future usage if other clip planes are added.) 1012 | return false; 1013 | 1014 | } else { 1015 | 1016 | // Update the s1 and s2 vertices to match the clipped line segment. 1017 | s1.lerp( s2, alpha1 ); 1018 | s2.lerp( s1, 1 - alpha2 ); 1019 | 1020 | return true; 1021 | 1022 | } 1023 | 1024 | } 1025 | 1026 | } 1027 | 1028 | }; 1029 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [NN-SVG](https://alexlenail.me/NN-SVG/) 2 | 3 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 4 | [![status](https://joss.theoj.org/papers/52b511ab107595a805107aa4ad70161d/status.svg)](https://joss.theoj.org/papers/52b511ab107595a805107aa4ad70161d) 5 | | [Docs](https://github.com/zfrenchee/NN-SVG/wiki) | [Contributing](https://github.com/zfrenchee/NN-SVG/wiki/Contributing) 6 | 7 | Illustrations of Neural Network architectures are often time-consuming to produce, and machine learning researchers all too often find themselves constructing these diagrams from scratch by hand. 8 | 9 | NN-SVG is a tool for creating Neural Network (NN) architecture drawings parametrically rather than manually. It also provides the ability to export those drawings to Scalable Vector Graphics (SVG) files, suitable for inclusion in academic papers or web pages. 10 | 11 | The tool provides the ability to generate figures of three kinds: classic Fully-Connected Neural Network (FCNN) figures, Convolutional Neural Network (CNN) figures of the sort introduced in [the LeNet paper](http://yann.lecun.com/exdb/publis/pdf/lecun-01a.pdf), and Deep Neural Network figures following the style introduced in [the AlexNet paper](https://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf). The former two are accomplished using the [D3 javascript library](https://d3js.org/) and the latter with the javascript library [Three.js](https://threejs.org/). NN-SVG provides the ability to style the figure to the user's liking via many size, color, and layout parameters. 12 | 13 | I hope this tool will save machine learning researchers time, and I hope this software might also serve as a pedagogical tool in some contexts. 14 | 15 | 16 | 17 | 18 | ### Citation 19 | 20 | > LeNail, (2019). NN-SVG: Publication-Ready Neural Network Architecture Schematics.
21 | > Journal of Open Source Software, 4(33), 747, https://doi.org/10.21105/joss.00747 22 | 23 | ### Related 24 | 25 | - [vdumoulin/conv_arithmetic](https://github.com/vdumoulin/conv_arithmetic) 26 | - [TensorSpace](https://github.com/tensorspace-team/tensorspace) 27 | -------------------------------------------------------------------------------- /SVGRenderer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author mrdoob / http://mrdoob.com/ 3 | */ 4 | 5 | THREE.SVGObject = function ( node ) { 6 | 7 | THREE.Object3D.call( this ); 8 | 9 | this.node = node; 10 | 11 | }; 12 | 13 | THREE.SVGObject.prototype = Object.create( THREE.Object3D.prototype ); 14 | THREE.SVGObject.prototype.constructor = THREE.SVGObject; 15 | 16 | THREE.SVGRenderer = function () { 17 | 18 | console.log( 'THREE.SVGRenderer', THREE.REVISION ); 19 | 20 | var _this = this, 21 | _renderData, _elements, _lights, 22 | _projector = new THREE.Projector(), 23 | _svg = document.createElementNS( 'http://www.w3.org/2000/svg', 'svg' ), 24 | _svgWidth, _svgHeight, _svgWidthHalf, _svgHeightHalf, 25 | 26 | _v1, _v2, _v3, 27 | 28 | _clipBox = new THREE.Box2(), 29 | _elemBox = new THREE.Box2(), 30 | 31 | _color = new THREE.Color(), 32 | _diffuseColor = new THREE.Color(), 33 | _ambientLight = new THREE.Color(), 34 | _directionalLights = new THREE.Color(), 35 | _pointLights = new THREE.Color(), 36 | _clearColor = new THREE.Color(), 37 | _clearAlpha = 1, 38 | 39 | _vector3 = new THREE.Vector3(), // Needed for PointLight 40 | _centroid = new THREE.Vector3(), 41 | _normal = new THREE.Vector3(), 42 | _normalViewMatrix = new THREE.Matrix3(), 43 | 44 | _viewMatrix = new THREE.Matrix4(), 45 | _viewProjectionMatrix = new THREE.Matrix4(), 46 | 47 | _svgPathPool = [], 48 | _svgNode, _pathCount = 0, 49 | 50 | _currentPath, _currentStyle, 51 | 52 | _quality = 1, _precision = null; 53 | 54 | this.domElement = _svg; 55 | 56 | this.autoClear = true; 57 | this.sortObjects = true; 58 | this.sortElements = true; 59 | 60 | this.info = { 61 | 62 | render: { 63 | 64 | vertices: 0, 65 | faces: 0 66 | 67 | } 68 | 69 | }; 70 | 71 | this.setQuality = function ( quality ) { 72 | 73 | switch ( quality ) { 74 | 75 | case "high": _quality = 1; break; 76 | case "low": _quality = 0; break; 77 | 78 | } 79 | 80 | }; 81 | 82 | this.setClearColor = function ( color, alpha ) { 83 | 84 | _clearColor.set( color ); 85 | _clearAlpha = alpha !== undefined ? alpha : 1; 86 | 87 | }; 88 | 89 | this.setPixelRatio = function () {}; 90 | 91 | this.setSize = function ( width, height ) { 92 | 93 | _svgWidth = width; _svgHeight = height; 94 | _svgWidthHalf = _svgWidth / 2; _svgHeightHalf = _svgHeight / 2; 95 | 96 | _svg.setAttribute( 'viewBox', ( - _svgWidthHalf ) + ' ' + ( - _svgHeightHalf ) + ' ' + _svgWidth + ' ' + _svgHeight ); 97 | _svg.setAttribute( 'width', _svgWidth ); 98 | _svg.setAttribute( 'height', _svgHeight ); 99 | 100 | _clipBox.min.set( - _svgWidthHalf, - _svgHeightHalf ); 101 | _clipBox.max.set( _svgWidthHalf, _svgHeightHalf ); 102 | 103 | }; 104 | 105 | this.setPrecision = function ( precision ) { 106 | 107 | _precision = precision; 108 | 109 | }; 110 | 111 | function removeChildNodes() { 112 | 113 | _pathCount = 0; 114 | 115 | while ( _svg.childNodes.length > 0 ) { 116 | 117 | _svg.removeChild( _svg.childNodes[ 0 ] ); 118 | 119 | } 120 | 121 | } 122 | 123 | function getSvgColor( color, opacity ) { 124 | 125 | var arg = Math.floor( color.r * 255 ) + ',' + Math.floor( color.g * 255 ) + ',' + Math.floor( color.b * 255 ); 126 | 127 | if ( opacity === undefined || opacity === 1 ) return 'rgb(' + arg + ')'; 128 | 129 | return 'rgb(' + arg + '); fill-opacity: ' + opacity; 130 | 131 | } 132 | 133 | function convert( c ) { 134 | 135 | return _precision !== null ? c.toFixed( _precision ) : c; 136 | 137 | } 138 | 139 | this.clear = function () { 140 | 141 | removeChildNodes(); 142 | _svg.style.backgroundColor = getSvgColor( _clearColor, _clearAlpha ); 143 | 144 | }; 145 | 146 | this.render = function ( scene, camera ) { 147 | 148 | if ( camera instanceof THREE.Camera === false ) { 149 | 150 | console.error( 'THREE.SVGRenderer.render: camera is not an instance of THREE.Camera.' ); 151 | return; 152 | 153 | } 154 | 155 | var background = scene.background; 156 | 157 | if ( background && background.isColor ) { 158 | 159 | removeChildNodes(); 160 | _svg.style.backgroundColor = getSvgColor( background ); 161 | 162 | } else if ( this.autoClear === true ) { 163 | 164 | this.clear(); 165 | 166 | } 167 | 168 | _this.info.render.vertices = 0; 169 | _this.info.render.faces = 0; 170 | 171 | _viewMatrix.copy( camera.matrixWorldInverse ); 172 | _viewProjectionMatrix.multiplyMatrices( camera.projectionMatrix, _viewMatrix ); 173 | 174 | _renderData = _projector.projectScene( scene, camera, this.sortObjects, this.sortElements ); 175 | _elements = _renderData.elements; 176 | _lights = _renderData.lights; 177 | 178 | _normalViewMatrix.getNormalMatrix( camera.matrixWorldInverse ); 179 | 180 | calculateLights( _lights ); 181 | 182 | // reset accumulated path 183 | 184 | _currentPath = ''; 185 | _currentStyle = ''; 186 | 187 | for ( var e = 0, el = _elements.length; e < el; e ++ ) { 188 | 189 | var element = _elements[ e ]; 190 | var material = element.material; 191 | 192 | if ( material === undefined || material.opacity === 0 ) continue; 193 | 194 | _elemBox.makeEmpty(); 195 | 196 | if ( element instanceof THREE.RenderableSprite ) { 197 | 198 | _v1 = element; 199 | _v1.x *= _svgWidthHalf; _v1.y *= - _svgHeightHalf; 200 | 201 | renderSprite( _v1, element, material ); 202 | 203 | } else if ( element instanceof THREE.RenderableLine ) { 204 | 205 | _v1 = element.v1; _v2 = element.v2; 206 | 207 | _v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf; 208 | _v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf; 209 | 210 | _elemBox.setFromPoints( [ _v1.positionScreen, _v2.positionScreen ] ); 211 | 212 | if ( _clipBox.intersectsBox( _elemBox ) === true ) { 213 | 214 | renderLine( _v1, _v2, element, material ); 215 | 216 | } 217 | 218 | } else if ( element instanceof THREE.RenderableFace ) { 219 | 220 | _v1 = element.v1; _v2 = element.v2; _v3 = element.v3; 221 | 222 | if ( _v1.positionScreen.z < - 1 || _v1.positionScreen.z > 1 ) continue; 223 | if ( _v2.positionScreen.z < - 1 || _v2.positionScreen.z > 1 ) continue; 224 | if ( _v3.positionScreen.z < - 1 || _v3.positionScreen.z > 1 ) continue; 225 | 226 | _v1.positionScreen.x *= _svgWidthHalf; _v1.positionScreen.y *= - _svgHeightHalf; 227 | _v2.positionScreen.x *= _svgWidthHalf; _v2.positionScreen.y *= - _svgHeightHalf; 228 | _v3.positionScreen.x *= _svgWidthHalf; _v3.positionScreen.y *= - _svgHeightHalf; 229 | 230 | _elemBox.setFromPoints( [ 231 | _v1.positionScreen, 232 | _v2.positionScreen, 233 | _v3.positionScreen 234 | ] ); 235 | 236 | if ( _clipBox.intersectsBox( _elemBox ) === true ) { 237 | 238 | renderFace3( _v1, _v2, _v3, element, material ); 239 | 240 | } 241 | 242 | } 243 | 244 | } 245 | 246 | flushPath(); // just to flush last svg:path 247 | 248 | scene.traverseVisible( function ( object ) { 249 | 250 | if ( object instanceof THREE.SVGObject ) { 251 | 252 | _vector3.setFromMatrixPosition( object.matrixWorld ); 253 | _vector3.applyMatrix4( _viewProjectionMatrix ); 254 | 255 | var x = _vector3.x * _svgWidthHalf; 256 | var y = - _vector3.y * _svgHeightHalf; 257 | 258 | var node = object.node; 259 | node.setAttribute( 'transform', 'translate(' + x + ',' + y + ')' ); 260 | 261 | _svg.appendChild( node ); 262 | 263 | } 264 | 265 | } ); 266 | 267 | }; 268 | 269 | function calculateLights( lights ) { 270 | 271 | _ambientLight.setRGB( 0, 0, 0 ); 272 | _directionalLights.setRGB( 0, 0, 0 ); 273 | _pointLights.setRGB( 0, 0, 0 ); 274 | 275 | for ( var l = 0, ll = lights.length; l < ll; l ++ ) { 276 | 277 | var light = lights[ l ]; 278 | var lightColor = light.color; 279 | 280 | if ( light.isAmbientLight ) { 281 | 282 | _ambientLight.r += lightColor.r; 283 | _ambientLight.g += lightColor.g; 284 | _ambientLight.b += lightColor.b; 285 | 286 | } else if ( light.isDirectionalLight ) { 287 | 288 | _directionalLights.r += lightColor.r; 289 | _directionalLights.g += lightColor.g; 290 | _directionalLights.b += lightColor.b; 291 | 292 | } else if ( light.isPointLight ) { 293 | 294 | _pointLights.r += lightColor.r; 295 | _pointLights.g += lightColor.g; 296 | _pointLights.b += lightColor.b; 297 | 298 | } 299 | 300 | } 301 | 302 | } 303 | 304 | function calculateLight( lights, position, normal, color ) { 305 | 306 | for ( var l = 0, ll = lights.length; l < ll; l ++ ) { 307 | 308 | var light = lights[ l ]; 309 | var lightColor = light.color; 310 | 311 | if ( light.isDirectionalLight ) { 312 | 313 | var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ).normalize(); 314 | 315 | var amount = normal.dot( lightPosition ); 316 | 317 | if ( amount <= 0 ) continue; 318 | 319 | amount *= light.intensity; 320 | 321 | color.r += lightColor.r * amount; 322 | color.g += lightColor.g * amount; 323 | color.b += lightColor.b * amount; 324 | 325 | } else if ( light.isPointLight ) { 326 | 327 | var lightPosition = _vector3.setFromMatrixPosition( light.matrixWorld ); 328 | 329 | var amount = normal.dot( _vector3.subVectors( lightPosition, position ).normalize() ); 330 | 331 | if ( amount <= 0 ) continue; 332 | 333 | amount *= light.distance == 0 ? 1 : 1 - Math.min( position.distanceTo( lightPosition ) / light.distance, 1 ); 334 | 335 | if ( amount == 0 ) continue; 336 | 337 | amount *= light.intensity; 338 | 339 | color.r += lightColor.r * amount; 340 | color.g += lightColor.g * amount; 341 | color.b += lightColor.b * amount; 342 | 343 | } 344 | 345 | } 346 | 347 | } 348 | 349 | function renderSprite( v1, element, material ) { 350 | 351 | var scaleX = element.scale.x * _svgWidthHalf; 352 | var scaleY = element.scale.y * _svgHeightHalf; 353 | 354 | if ( material.isPointsMaterial ) { 355 | 356 | scaleX *= material.size; 357 | scaleY *= material.size; 358 | 359 | } 360 | 361 | var path = 'M' + convert( v1.x - scaleX * 0.5 ) + ',' + convert( v1.y - scaleY * 0.5 ) + 'h' + convert( scaleX ) + 'v' + convert( scaleY ) + 'h' + convert( - scaleX ) + 'z'; 362 | var style = ""; 363 | 364 | if ( material.isSpriteMaterial || material.isPointsMaterial ) { 365 | 366 | style = 'fill:' + getSvgColor( material.color, material.opacity ); 367 | 368 | } 369 | 370 | addPath( style, path ); 371 | 372 | } 373 | 374 | function renderLine( v1, v2, element, material ) { 375 | 376 | var path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y ); 377 | 378 | if ( material.isLineBasicMaterial ) { 379 | 380 | var style = 'fill:none;stroke:' + getSvgColor( material.color, material.opacity ) + ';stroke-width:' + material.linewidth + ';stroke-linecap:' + material.linecap; 381 | 382 | if ( material.isLineDashedMaterial ) { 383 | 384 | style = style + ';stroke-dasharray:' + material.dashSize + "," + material.gapSize; 385 | 386 | } 387 | 388 | addPath( style, path ); 389 | 390 | } 391 | 392 | } 393 | 394 | function renderFace3( v1, v2, v3, element, material ) { 395 | 396 | _this.info.render.vertices += 3; 397 | _this.info.render.faces ++; 398 | 399 | var path = 'M' + convert( v1.positionScreen.x ) + ',' + convert( v1.positionScreen.y ) + 'L' + convert( v2.positionScreen.x ) + ',' + convert( v2.positionScreen.y ) + 'L' + convert( v3.positionScreen.x ) + ',' + convert( v3.positionScreen.y ) + 'z'; 400 | var style = ''; 401 | 402 | if ( material.isMeshBasicMaterial ) { 403 | 404 | _color.copy( material.color ); 405 | 406 | if ( material.vertexColors === THREE.FaceColors ) { 407 | 408 | _color.multiply( element.color ); 409 | 410 | } 411 | 412 | } else if ( material.isMeshLambertMaterial || material.isMeshPhongMaterial || material.isMeshStandardMaterial ) { 413 | 414 | _diffuseColor.copy( material.color ); 415 | 416 | if ( material.vertexColors === THREE.FaceColors ) { 417 | 418 | _diffuseColor.multiply( element.color ); 419 | 420 | } 421 | 422 | _color.copy( _ambientLight ); 423 | 424 | _centroid.copy( v1.positionWorld ).add( v2.positionWorld ).add( v3.positionWorld ).divideScalar( 3 ); 425 | 426 | calculateLight( _lights, _centroid, element.normalModel, _color ); 427 | 428 | _color.multiply( _diffuseColor ).add( material.emissive ); 429 | 430 | } else if ( material.isMeshNormalMaterial ) { 431 | 432 | _normal.copy( element.normalModel ).applyMatrix3( _normalViewMatrix ); 433 | 434 | _color.setRGB( _normal.x, _normal.y, _normal.z ).multiplyScalar( 0.5 ).addScalar( 0.5 ); 435 | 436 | } 437 | 438 | if ( material.wireframe ) { 439 | 440 | style = 'fill:none;stroke:' + getSvgColor( _color, material.opacity ) + ';stroke-width:' + material.wireframeLinewidth + ';stroke-linecap:' + material.wireframeLinecap + ';stroke-linejoin:' + material.wireframeLinejoin; 441 | 442 | } else { 443 | 444 | style = 'fill:' + getSvgColor( _color, material.opacity ); 445 | 446 | } 447 | 448 | addPath( style, path ); 449 | 450 | } 451 | 452 | function addPath( style, path ) { 453 | 454 | if ( _currentStyle === style ) { 455 | 456 | _currentPath += path; 457 | 458 | } else { 459 | 460 | flushPath(); 461 | 462 | _currentStyle = style; 463 | _currentPath = path; 464 | 465 | } 466 | 467 | } 468 | 469 | function flushPath() { 470 | 471 | if ( _currentPath ) { 472 | 473 | _svgNode = getPathNode( _pathCount ++ ); 474 | _svgNode.setAttribute( 'd', _currentPath ); 475 | _svgNode.setAttribute( 'style', _currentStyle ); 476 | _svg.appendChild( _svgNode ); 477 | 478 | } 479 | 480 | _currentPath = ''; 481 | _currentStyle = ''; 482 | 483 | } 484 | 485 | function getPathNode( id ) { 486 | 487 | if ( _svgPathPool[ id ] == null ) { 488 | 489 | _svgPathPool[ id ] = document.createElementNS( 'http://www.w3.org/2000/svg', 'path' ); 490 | 491 | if ( _quality == 0 ) { 492 | 493 | _svgPathPool[ id ].setAttribute( 'shape-rendering', 'crispEdges' ); //optimizeSpeed 494 | 495 | } 496 | 497 | return _svgPathPool[ id ]; 498 | 499 | } 500 | 501 | return _svgPathPool[ id ]; 502 | 503 | } 504 | 505 | }; 506 | -------------------------------------------------------------------------------- /about.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 22 | NN SVG 23 | 24 | 25 | 26 |
27 |
28 |
29 |
30 | 31 |

NN-SVG

32 |
33 |

This tool was built to save people time.
Hopefully it accomplishes that goal.

34 | 35 |

Time was saved building this tool with the help of
d3, three.js, bootstrap, jquery, and font-awesome.

36 | 37 |

If there are additional features you'd like to see,
feel free to open an issue on github.
PRs welcome too.

38 | 39 | home 40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /fonts/LICENSE: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlenail/NN-SVG/603cfa6e2de8d7b1e99e2322ad393ddb256b843f/fonts/LICENSE -------------------------------------------------------------------------------- /fonts/README: -------------------------------------------------------------------------------- 1 | Use Facetype.js to generate typeface.json fonts. 2 | http://gero3.github.io/facetype.js/ 3 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 69 | 70 | NN SVG 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 |
79 | 80 |
81 |
82 |
83 |
84 | 85 |

NN-SVG

86 |

Publication-ready NN-architecture schematics. Download SVG

87 | 88 | 95 | 96 |
97 |
98 |
99 | 100 |

Style:

101 | 102 | 103 |
104 |
105 | 106 | 107 |
108 |
109 | 110 | 111 |
112 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | 120 | 121 |
122 | 123 |
124 |
125 | 126 | 127 |
128 |
129 | 130 | 131 |
132 |
133 | 134 | 135 |
136 |
137 | 138 | 139 |
140 | 141 |
142 |
143 | 144 | 145 |
146 | 147 | 148 | 149 |
150 |
151 | 152 | 153 |
154 |
155 | 156 | 157 |
158 |
159 | 160 | 161 |
162 | 163 | 164 |
165 |
166 | 167 | 168 |
169 |
170 | 171 |
172 | 175 |
176 |
177 | 180 |
181 |
182 |
183 | 184 | 185 |
186 |
187 | 188 | 189 |
190 |
191 | 192 |
193 | 194 | 195 |
196 | 199 |
200 |
201 | 204 |
205 |
206 | 207 |
208 | 209 |
210 |

Architecture:

211 | 212 |
213 |
214 | 215 |
216 |
217 | 218 | 219 |
220 | 221 |
222 |
223 |
224 | 225 | 226 |
227 | 228 |
229 |
230 |
231 | 232 | 233 |
234 | 235 |
236 |
237 |
238 | 239 | 240 |
241 | 242 |
243 |
244 |
245 | 246 | 247 |
248 | 249 |
250 | 251 |
252 | 253 |
254 | 255 | 256 |
257 |
258 |
259 |
260 |
261 |
262 | 263 |
About
264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 369 | 370 | 371 | 372 | 373 | -------------------------------------------------------------------------------- /paper.bib: -------------------------------------------------------------------------------- 1 | @INCOLLECTION{AlexNet, 2 | title = {ImageNet Classification with Deep Convolutional Neural Networks}, 3 | author = {Alex Krizhevsky and Sutskever, Ilya and Hinton, Geoffrey E}, 4 | booktitle = {Advances in Neural Information Processing Systems 25}, 5 | year = {2012}, 6 | url = {http://papers.nips.cc/paper/4824-imagenet-classification-with-deep-convolutional-neural-networks.pdf} 7 | } 8 | @INPROCEEDINGS{LeNet, 9 | title = {Gradient-based learning applied to document recognition}, 10 | author = {Yann Lecun and Léon Bottou and Yoshua Bengio and Patrick Haffner}, 11 | booktitle = {Proceedings of the IEEE}, 12 | year = {1998}, 13 | pages = {2278--2324} 14 | } 15 | @ARTICLE{d3, 16 | title = {D3 Data-Driven Documents}, 17 | author = {Bostock, Michael and Ogievetsky, Vadim and Heer, Jeffrey}, 18 | journal = {IEEE Transactions on Visualization and Computer Graphics}, 19 | volume = {17}, 20 | number = {12}, 21 | year = {2011}, 22 | url = {http://dx.doi.org/10.1109/TVCG.2011.185}, 23 | doi = {10.1109/TVCG.2011.185}, 24 | } 25 | 26 | -------------------------------------------------------------------------------- /paper.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'NN-SVG: Publication-Ready Neural Network Architecture Schematics' 3 | tags: 4 | - machine learning 5 | - deep learning 6 | - neural networks 7 | - visualization 8 | authors: 9 | - name: Alexander LeNail 10 | orcid: 0000-0001-8173-2315 11 | affiliation: 1 12 | affiliations: 13 | - name: Massachusetts Institute of Technology, dept of Biological Engineering 14 | index: 1 15 | date: 14 May 2018 16 | bibliography: paper.bib 17 | --- 18 | 19 | # Summary 20 | 21 | Illustrations of Neural Network architectures are often time-consuming to produce, and machine learning researchers all too often find themselves constructing these diagrams from scratch by hand. 22 | 23 | NN-SVG is a tool for creating Neural Network (NN) architecture drawings parametrically rather than manually. It then provides the ability to export those drawings to Scalable Vector Graphics (SVG) files, suitable for inclusion in academic papers or as figures on web pages. 24 | 25 | The tool provides the ability to generate figures of three kinds: classic Fully-Connected Neural Network (FCNN) figures, Convolutional Neural Network (CNN) figures of the sort introduced in [@LeNet], and Deep Neural Network figures following the style introduced in [@AlexNet]. The former two are accomplished using the D3 javascript library [@d3] and the latter with the javascript library Three.js. NN-SVG provides the ability to style the figure to the user's liking via many size, color, and layout parameters. 26 | 27 | We hope this tool will save machine learning researchers time, and we hope this software might also serve as a pedagogical tool in some contexts. 28 | 29 | # References 30 | -------------------------------------------------------------------------------- /paper.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/alexlenail/NN-SVG/603cfa6e2de8d7b1e99e2322ad393ddb256b843f/paper.pdf -------------------------------------------------------------------------------- /util.js: -------------------------------------------------------------------------------- 1 | 2 | let nWise = (n, array) => { 3 | iterators = Array(n).fill().map(() => array[Symbol.iterator]()); 4 | iterators.forEach((it, index) => Array(index).fill().forEach(() => it.next())); 5 | return Array(array.length - n + 1).fill().map(() => (iterators.map(it => it.next().value))); 6 | }; 7 | 8 | let pairWise = (array) => nWise(2, array); 9 | 10 | let sum = (arr) => arr.reduce((a,b)=>a+b); 11 | 12 | let range = n => [...Array(n).keys()]; 13 | 14 | let rand = (min, max) => Math.random() * (max - min) + min; 15 | 16 | Array.prototype.last = function() { return this[this.length - 1]; }; 17 | 18 | let flatten = (array) => array.reduce((flat, toFlatten) => (flat.concat(Array.isArray(toFlatten) ? flatten(toFlatten) : toFlatten)), []); 19 | 20 | --------------------------------------------------------------------------------