├── 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 |
81 |
82 |
83 |
84 |
85 |
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 |
208 |
218 |
228 |
238 |
248 |
258 |
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 |
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 |
96 |
97 |
98 |
131 |
132 |
133 |
134 | 100
135 | %
136 |
137 |
138 |
139 |
Architecture:
140 |
141 |
Depth | Height | Width | filter Height | filter Width
142 |
157 |
172 |
187 |
202 |
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 |
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 | [](https://opensource.org/licenses/MIT)
4 | [](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 |
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 |
208 |
209 |
210 |
Architecture:
211 |
212 |
213 |
214 |
215 |
222 |
229 |
236 |
243 |
250 |
251 |
252 |
253 |
254 |
255 |
256 |
257 |
258 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 |
270 |
271 |
272 |
273 |
274 |
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 |
--------------------------------------------------------------------------------