├── LICENSE ├── README.md ├── gear-toy.css ├── gear-toy.js ├── gears.d3.js └── index.html /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Liam Brummitt 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 13 | all 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 21 | THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gears.d3.js # 2 | 3 | _Gears.d3.js_ is a [d3.js](http://d3js.org/) example for creating simulated gear animations with SVG. 4 | 5 | See the [demo](https://codepen.io/liabru/pen/Edinj). Based on this [example](http://bl.ocks.org/mbostock/1353700) by [Mike Bostock](https://github.com/mbostock). 6 | 7 | ## License ## 8 | 9 | MIT (with permission granted from [Mike Bostock](https://github.com/mbostock)). 10 | 11 | Note: the Fractal Gears implementation, as it is on my personal website, is not open source. 12 | -------------------------------------------------------------------------------- /gear-toy.css: -------------------------------------------------------------------------------- 1 | * { 2 | padding: 0; 3 | margin: 0; 4 | } 5 | 6 | body { 7 | background: #222; 8 | font-family: Georgia, Times, "Times New Roman", serif; 9 | color: #aaa; 10 | } 11 | 12 | body.dragging { 13 | cursor: -webkit-grabbing !important; 14 | } 15 | 16 | .container { 17 | max-width: 1024px; 18 | margin: 0 auto; 19 | padding: 70px 5%; 20 | } 21 | 22 | .container h1 { 23 | color: #fff; 24 | display: block; 25 | margin: 0 0 1em 0; 26 | font-weight: normal; 27 | font-size: 30px; 28 | } 29 | 30 | .container p, 31 | .container ul li { 32 | margin: 0 0 1em 0; 33 | color: #bbb; 34 | font-size: 12px; 35 | } 36 | 37 | .container ul { 38 | list-style: square; 39 | } 40 | 41 | .nav { 42 | margin: 0 0 2em 0; 43 | font-size: 16px; 44 | } 45 | 46 | .container a, 47 | .container a:link, 48 | .container a:visited, 49 | .container a:active, 50 | .container a:hover { 51 | color: #aaa; 52 | text-decoration: none; 53 | border-bottom: 1px solid #555; 54 | padding: 0 0 2px 0; 55 | } 56 | 57 | .nav-sep { 58 | padding: 0 5px; 59 | } 60 | 61 | .ie .gears-d3-canvas { 62 | /* ie fix :( */ 63 | height: 768px; 64 | } 65 | 66 | .gears-d3-canvas svg { 67 | border: 1px solid #333; 68 | background: #252525; 69 | margin: 30px 0 0 0; 70 | overflow: hidden; 71 | max-width: 1024px; 72 | max-height: 768px; 73 | } 74 | 75 | .gear.dragging path, 76 | .gear path:active { 77 | opacity: 0.5; 78 | cursor: -webkit-grabbing !important; 79 | } 80 | 81 | .gear path { 82 | cursor: move; 83 | } 84 | 85 | .gear:hover path { 86 | opacity: 0.9; 87 | stroke: #F6B25D; 88 | stroke-width: 2px; 89 | } 90 | 91 | .gear .bulb-path .bulb-light { 92 | opacity: 0; 93 | 94 | transition: all 800ms ease-in-out; 95 | -webkit-transition: all 800ms ease-in-out; 96 | -moz-transition: all 800ms ease-in-out; 97 | -o-transition: all 800ms ease-in-out; 98 | } 99 | 100 | .gear.powered path { 101 | fill: #FFAE21 !important; 102 | } 103 | 104 | .style-0 path { 105 | fill: #556270; 106 | } 107 | 108 | .style-1 path { 109 | fill: #4ECDC4; 110 | } 111 | 112 | .style-2 path { 113 | fill: #C7F464; 114 | } 115 | 116 | .style-3 path { 117 | fill: #FF6B6B; 118 | } 119 | 120 | .style-4 path { 121 | fill: #C44D58; 122 | } -------------------------------------------------------------------------------- /gear-toy.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gear-toy.js 3 | * http://brm.io/gears-d3-js/ 4 | * License: MIT 5 | */ 6 | 7 | (function($) { 8 | 9 | var _svg, 10 | _allGears = [], 11 | _randomiseInterval, 12 | _canvasWidth = 1024, 13 | _canvasHeight = 768, 14 | _xOffset = _canvasWidth * 0.5, 15 | _yOffset = _canvasHeight * 0.5, 16 | _gearFactors = [64, 64, 32, 48, 48, 96, 112, 256], 17 | _gearStyles = ['style-0', 'style-1', 'style-2', 'style-3', 'style-4'], 18 | _autoShuffle = true, 19 | _dragBehaviour; 20 | 21 | var _options = { 22 | radius: 16, 23 | holeRadius: 0.4, 24 | transition: true, 25 | speed: 0.01, 26 | autoShuffle: true, 27 | number: 10, 28 | addendum: 8, 29 | dedendum: 3, 30 | thickness: 0.7, 31 | profileSlope: 0.5 32 | }; 33 | 34 | $(function () { 35 | // prevent canvas doing odd things on click 36 | $('.gears-d3-canvas').on('mousedown', function(e) { 37 | e.originalEvent.preventDefault(); 38 | }); 39 | 40 | // start the demo 41 | main(); 42 | _initGui(); 43 | 44 | // for ie css fix 45 | var isIE = window.ActiveXObject || "ActiveXObject" in window; 46 | if (isIE) 47 | $('body').addClass('ie'); 48 | }); 49 | 50 | function main() { 51 | 52 | // set up our d3 svg element 53 | _svg = d3.select('.gears-d3-canvas') 54 | .append('svg') 55 | .attr('viewBox', '0 0 ' + _canvasWidth + ' ' + _canvasHeight) 56 | .attr('preserveAspectRatio', 'xMinYMin slice'); 57 | 58 | // get a d3 dragBehaviour using Gear helper 59 | _dragBehaviour = Gear.dragBehaviour(_allGears, _svg); 60 | 61 | // extend the dragbehaviour to disable randomise while dragging 62 | _dragBehaviour 63 | .on("dragstart.i", function() { 64 | _autoShuffle = false; 65 | }) 66 | .on("dragend.i", function() { 67 | _autoShuffle = false; 68 | }); 69 | 70 | // generate and randomise scene 71 | _generateScene(_options); 72 | _randomiseScene(false); 73 | 74 | // start a timer to randomise every few secs 75 | _randomiseInterval = setInterval(function() { 76 | if (_autoShuffle) 77 | _randomiseScene(true); 78 | }, 4000); 79 | 80 | // start the d3 animation timer 81 | d3.timer(function () { 82 | _svg.selectAll('.gear-path') 83 | .attr('transform', function (d) { 84 | d.angle += d.speed; 85 | return 'rotate(' + d.angle * (180 / Math.PI) + ')'; 86 | }); 87 | }); 88 | } 89 | 90 | var _generateScene = function(options) { 91 | var holeRadius, 92 | teeth, 93 | radius, 94 | factor, 95 | newGear, 96 | innerRadius; 97 | 98 | _gearStyles = Gear.Utility.arrayShuffle(_gearStyles); 99 | 100 | for (var i = 0; i < options.number; i++) { 101 | factor = _gearFactors[i % _gearFactors.length]; 102 | radius = factor / 2; 103 | teeth = radius / 4; 104 | innerRadius = radius - options.addendum - options.dedendum; 105 | holeRadius = factor > 96 ? innerRadius * 0.5 + innerRadius * 0.5 * options.holeRadius : innerRadius * options.holeRadius; 106 | 107 | _allGears.push(newGear = Gear.create(_svg, { 108 | radius: radius, 109 | teeth: teeth, 110 | x: 0, 111 | y: 0, 112 | holeRadius: holeRadius, 113 | addendum: options.addendum, 114 | dedendum: options.dedendum, 115 | thickness: options.thickness, 116 | profileSlope: options.profileSlope 117 | })); 118 | 119 | newGear.call(_dragBehaviour); 120 | newGear.classed(_gearStyles[i % _gearStyles.length], true); 121 | } 122 | }; 123 | 124 | var _randomiseScene = function(transition) { 125 | _allGears = Gear.Utility.arrayShuffle(_allGears); 126 | Gear.randomArrange(_allGears, _xOffset, _yOffset); 127 | Gear.setPower(_allGears[0], 0.01); 128 | Gear.updateGears(_allGears); 129 | 130 | _svg.selectAll('.gear') 131 | .each(function(d, i) { 132 | if (transition) { 133 | d3.select(this) 134 | .transition() 135 | .ease('elastic') 136 | .delay(i * 80 + Math.random() * 80) 137 | .duration(1500) 138 | .attr('transform', function(d) { 139 | return 'translate(' + [ d.x, d.y ] + ')'; 140 | }); 141 | } else { 142 | d3.select(this) 143 | .attr('transform', function(d) { 144 | return 'translate(' + [ d.x, d.y ] + ')'; 145 | }); 146 | } 147 | }); 148 | }; 149 | 150 | var _clear = function() { 151 | // clear the array and keep the original reference! 152 | _allGears.length = 0; 153 | _svg.selectAll('.gear').remove(); 154 | }; 155 | 156 | var _scrollToDemo = function() { 157 | $('html, body') 158 | .stop() 159 | .animate({ 160 | scrollTop: $(".gears-d3-canvas").offset().top 161 | }, 200); 162 | }; 163 | 164 | var _initGui = function() { 165 | if (!window.dat) { 166 | console.log("Could not create GUI. Check dat.gui library is loaded first."); 167 | return; 168 | } 169 | 170 | _options.datGui = new dat.GUI(); 171 | 172 | var funcs = { 173 | randomise: function() { 174 | funcs.randomiseSettings(); 175 | funcs.randomiseScene(); 176 | }, 177 | 178 | randomiseSettings: function() { 179 | _options.addendum = 2 + Math.random() * 6; 180 | _options.dedendum = 2 + Math.random() * 2; 181 | _options.thickness = 0.5 + Math.random() * 0.1; 182 | _options.profileSlope = _options.thickness * 0.9; 183 | _options.holeRadius = Math.random() * 0.9; 184 | _updateGui(_options.datGui); 185 | }, 186 | 187 | randomiseScene: function() { 188 | _clear(); 189 | 190 | _generateScene(_options); 191 | _randomiseScene(false); 192 | _scrollToDemo(); 193 | _autoShuffle = false; 194 | }, 195 | 196 | shuffle: function() { 197 | _randomiseScene(true); 198 | _scrollToDemo(); 199 | _autoShuffle = false; 200 | }, 201 | 202 | updateSpeed: function(speed) { 203 | $.each(_allGears, function() { 204 | var datum = this.datum(); 205 | if (datum.power !== 0) 206 | datum.power = speed; 207 | }); 208 | 209 | Gear.updateGears(_allGears); 210 | _autoShuffle = false; 211 | _scrollToDemo(); 212 | }, 213 | 214 | onFinishChange: function() { 215 | funcs.randomiseScene(); 216 | _autoShuffle = false; 217 | } 218 | }; 219 | 220 | var controls = _options.datGui.addFolder('Gears'); 221 | 222 | controls.add(_options, 'number', 1, 50).onFinishChange(funcs.onFinishChange); 223 | controls.add(_options, 'holeRadius', 0, 0.9).onFinishChange(funcs.onFinishChange); 224 | controls.add(_options, 'addendum', 0, 16).onFinishChange(funcs.onFinishChange); 225 | controls.add(_options, 'dedendum', 0, 16).onFinishChange(funcs.onFinishChange); 226 | controls.add(_options, 'thickness', 0, 0.8).onFinishChange(funcs.onFinishChange); 227 | controls.add(_options, 'profileSlope', 0, 0.8).onFinishChange(funcs.onFinishChange); 228 | 229 | controls.add(_options, 'speed', 0.001, 0.2) 230 | .step(0.001) 231 | .onChange(function(speed) { 232 | funcs.updateSpeed(speed); 233 | }); 234 | 235 | controls.add(funcs, 'shuffle'); 236 | controls.add(funcs, 'randomise'); 237 | 238 | controls.open(); 239 | }; 240 | 241 | var _updateGui = function(datGui) { 242 | var i; 243 | 244 | for (i in datGui.__folders) { 245 | _updateGui(datGui.__folders[i]); 246 | } 247 | 248 | for (i in datGui.__controllers) { 249 | var controller = datGui.__controllers[i]; 250 | if (controller.updateDisplay) 251 | controller.updateDisplay(); 252 | } 253 | }; 254 | 255 | })(jQuery); -------------------------------------------------------------------------------- /gears.d3.js: -------------------------------------------------------------------------------- 1 | /** 2 | * gears.d3.js 3 | * http://brm.io/gears-d3-js/ 4 | * License: MIT 5 | */ 6 | 7 | var Gear = { 8 | 9 | nextGearId: 0, 10 | 11 | create: function(svg, options) { 12 | var datum = { 13 | teeth: Math.round(options.teeth) || 16, 14 | radius: options.radius || 200, 15 | x: options.x || 0, 16 | y: options.y || 0, 17 | speed: options.power || 0, 18 | power: options.power || 0, 19 | angle: options.angle || 0, 20 | addendum: options.addendum || 8, 21 | dedendum: options.dedendum || 3, 22 | thickness: options.thickness || 0.7, 23 | profileSlope: options.profileSlope || 0.5, 24 | holeRadius: options.holeRadius || 5, 25 | dragEvent: 'dragend', 26 | id: Gear.nextGearId 27 | }; 28 | 29 | Gear.nextGearId += 1; 30 | 31 | datum.rootRadius = datum.radius - datum.dedendum; 32 | datum.outsideRadius = datum.radius + datum.addendum; 33 | datum.circularPitch = (1 - datum.thickness) * 2 * Math.PI / datum.teeth; 34 | datum.pitchAngle = datum.thickness * 2 * Math.PI / datum.teeth; 35 | datum.slopeAngle = datum.pitchAngle * datum.profileSlope * 0.5; 36 | datum.addendumAngle = datum.pitchAngle * (1 - datum.profileSlope); 37 | 38 | var gear = svg.append('g') 39 | .attr('class', 'gear') 40 | .attr('transform', 'translate(' + datum.x + ', ' + datum.y + ')') 41 | .datum(datum); 42 | 43 | gear.on('mouseover', function() { 44 | var $this = d3.select(this); 45 | $this.attr('transform', $this.attr('transform') + ' scale(1.06)'); 46 | }); 47 | 48 | gear.on('mouseout', function() { 49 | var $this = d3.select(this); 50 | $this.attr('transform', $this.attr('transform').replace(' scale(1.06)', '')); 51 | }); 52 | 53 | gear.append('path') 54 | .attr('class', 'gear-path') 55 | .attr('d', Gear.path); 56 | 57 | return gear; 58 | }, 59 | 60 | setPower: function(gear, power) { 61 | gear.datum().power = power; 62 | }, 63 | 64 | randomArrange: function(gears, xOffset, yOffset, angleMin, angleMax) { 65 | var xx = xOffset || 0, 66 | yy = yOffset || 0, 67 | angle = 0, 68 | prevGear, 69 | nextGear, 70 | distance, 71 | collision = false, 72 | unplacedGears, 73 | placedGears = [], 74 | randomPlaced, 75 | placed; 76 | 77 | // params 78 | angleMin = angleMin || 0.9; 79 | angleMax = angleMax || 1.2; 80 | 81 | // first clone and shuffle all the gears 82 | unplacedGears = Gear.Utility.arrayClone(gears); 83 | Gear.Utility.arrayShuffle(unplacedGears); 84 | 85 | // place the first gear 86 | unplacedGears[0].datum().x = xx; 87 | unplacedGears[0].datum().y = yy; 88 | placedGears.push(unplacedGears[0]); 89 | 90 | // try place the gears randomly 91 | // this is a bit hit and miss... but it mostly works 92 | 93 | // for every other gear 94 | for (var i = 1; i < unplacedGears.length; i++) { 95 | nextGear = unplacedGears[i].datum(); 96 | randomPlaced = Math.floor(Math.random() * placedGears.length); 97 | placed = false; 98 | collision = 2; 99 | nextGear.power = 0; 100 | 101 | // try mesh to each placed gear until find one that works 102 | for (var j = 0; j < placedGears.length; j += 1) { 103 | if (placed === true) 104 | break; 105 | 106 | // get the gear in question and find potential position to test 107 | prevGear = placedGears[(randomPlaced + j) % placedGears.length].datum(); 108 | distance = prevGear.radius + nextGear.radius - nextGear.addendum; 109 | angle = Math.random() * 2 * Math.PI; 110 | 111 | // try at angular intervals around the gear in question 112 | // until we find an empty spot where we can place the gear (no collisions) 113 | for (var k = 0; k < 2 * Math.PI; k += 0.5) { 114 | if (placed === true) 115 | break; 116 | 117 | nextGear.x = prevGear.x + Math.cos(angle + k) * distance; 118 | nextGear.y = prevGear.y + Math.sin(angle + k) * distance; 119 | collision = Gear.anyGearCollides(nextGear, placedGears, 10); 120 | 121 | if (collision <= 1) { 122 | Gear.mesh(prevGear, nextGear); 123 | placedGears.push(unplacedGears[i]); 124 | placed = true; 125 | } 126 | } 127 | } 128 | 129 | // the above may fail on rare occasion 130 | // can't fit so ditch it! 131 | if (placed !== true) { 132 | nextGear.x = -100; 133 | nextGear.y = -100; 134 | } 135 | } 136 | }, 137 | 138 | dragBehaviour: function(gears, svg) { 139 | return d3.behavior.drag() 140 | .origin(function(d) { return d; }) 141 | .on('dragstart', function (d, i) { 142 | d.dragEvent = 'dragstart'; 143 | d3.select(this).classed('dragging', true); 144 | d3.select('body').classed('dragging', true); 145 | }) 146 | .on('drag', function (d, i) { 147 | var collision = false, 148 | oldX = d.x, 149 | oldY = d.y; 150 | 151 | d.x = d3.event.x; 152 | d.y = d3.event.y; 153 | d.x = Math.max(d.radius, d.x); 154 | d.y = Math.max(d.radius, d.y); 155 | 156 | d.dragEvent = 'drag'; 157 | collision = Gear.anyGearCollides(d3.select(this).datum(), gears); 158 | 159 | if (!collision) { 160 | d3.select(this).attr('transform', function(d, i){ 161 | return 'translate(' + [ d.x, d.y ] + ')'; 162 | }); 163 | 164 | Gear.updateGears(gears); 165 | } else { 166 | d.x = oldX; 167 | d.y = oldY; 168 | } 169 | }) 170 | .on('dragend', function (d, i) { 171 | d.dragEvent = 'dragend'; 172 | d3.select(this).classed('dragging', false); 173 | d3.select('body').classed('dragging', false); 174 | Gear.updateGears(gears); 175 | }); 176 | }, 177 | 178 | anyGearCollides: function(gearA, gears, tolerance) { 179 | var collisions = 0; 180 | tolerance = tolerance || 0; 181 | 182 | for (var i = 0; i < gears.length; i++) { 183 | var gearB = gears[i]; 184 | 185 | if (Gear.gearCollides(gearA, gearB.datum(), tolerance)) 186 | collisions += 1; 187 | } 188 | 189 | return collisions; 190 | }, 191 | 192 | gearCollides: function(gearA, gearB, tolerance) { 193 | var threshold = gearA.radius + gearB.radius - Math.max(gearA.addendum, gearB.addendum) + tolerance; 194 | tolerance = tolerance || 0; 195 | 196 | if (gearA.id === gearB.id || 197 | Math.abs(gearA.x - gearB.x) > threshold || 198 | Math.abs(gearA.y - gearB.y) > threshold) 199 | return false; 200 | 201 | if (Gear.Utility.distanceSquared(gearA.x, gearA.y, gearB.x, gearB.y) < threshold * threshold) 202 | return true; 203 | 204 | return false; 205 | }, 206 | 207 | propagateGears: function(gear, visited) { 208 | var connected = gear.connected; 209 | 210 | visited = visited || {}; 211 | visited[gear.id] = true; 212 | 213 | for (var nextGearId in connected) { 214 | if (connected.hasOwnProperty(nextGearId)) { 215 | var nextGear = connected[nextGearId]; 216 | 217 | if (nextGear.id === gear.id || nextGear.id in visited) 218 | continue; 219 | 220 | visited[nextGear.id] = true; 221 | 222 | nextGear.speed -= gear.speed * (gear.teeth / nextGear.teeth); 223 | 224 | Gear.propagateGears(nextGear, visited); 225 | } 226 | } 227 | }, 228 | 229 | updateGears: function(gears) { 230 | var gearA, 231 | gearB, 232 | datum; 233 | 234 | for (var i = 0; i < gears.length; i += 1) { 235 | datum = gears[i].datum(); 236 | datum.connected = {}; 237 | datum.speed = datum.power; 238 | } 239 | 240 | for (i = 0; i < gears.length; i += 1) { 241 | for (var j = i + 1; j < gears.length; j += 1) { 242 | gearA = gears[i]; 243 | gearB = gears[j]; 244 | 245 | var datumA = gearA.datum(), 246 | datumB = gearB.datum(), 247 | collides = Gear.gearCollides(datumA, datumB, Math.max(datumA.addendum, + datumB.addendum)); 248 | 249 | if (collides) { 250 | datumA.connected[datumB.id] = datumB; 251 | datumB.connected[datumA.id] = datumA; 252 | } 253 | } 254 | } 255 | 256 | for (i = 0; i < gears.length; i += 1) { 257 | gearA = gears[i]; 258 | 259 | if (!gearA.classed('dragging')) 260 | continue; 261 | 262 | var nextGear = gearA.datum(), 263 | connectedKeys = Gear.Utility.keys(nextGear.connected); 264 | 265 | if (connectedKeys.length === 0) 266 | continue; 267 | 268 | var gear = nextGear.connected[connectedKeys[0]]; 269 | 270 | Gear.mesh(gear, nextGear); 271 | } 272 | 273 | var visited = {}; 274 | 275 | for (i = 0; i < gears.length; i += 1) { 276 | datum = gears[i].datum(); 277 | 278 | if (Math.abs(datum.power) > 0) { 279 | Gear.propagateGears(datum, visited); 280 | gears[i].classed('powered', true); 281 | } else { 282 | gears[i].classed('powered', false); 283 | } 284 | } 285 | 286 | for (i = 0; i < gears.length; i += 1) { 287 | datum = gears[i].datum(); 288 | if (Math.abs(datum.speed) > 0) { 289 | gears[i].classed('moving', true); 290 | } else { 291 | gears[i].classed('moving', false); 292 | } 293 | } 294 | }, 295 | 296 | mesh: function(gear, nextGear) { 297 | var theta = Gear.Utility.angle(gear.x, gear.y, nextGear.x, nextGear.y), 298 | pitch = nextGear.circularPitch + nextGear.slopeAngle * 2 + nextGear.addendumAngle, 299 | radiusRatio = gear.radius / nextGear.radius; 300 | nextGear.angle = -(gear.angle % (2 * Math.PI)) * radiusRatio + theta + theta * radiusRatio + pitch * 0.5; 301 | }, 302 | 303 | path: function(options) { 304 | var addendum = options.addendum, 305 | dedendum = options.dedendum, 306 | thickness = options.thickness, 307 | profileSlope = options.profileSlope, 308 | holeRadius = options.holeRadius, 309 | teeth = options.teeth, 310 | radius = options.radius - addendum, 311 | rootRadius = radius - dedendum, 312 | outsideRadius = radius + addendum, 313 | circularPitch = (1 - thickness) * 2 * Math.PI / teeth, 314 | pitchAngle = thickness * 2 * Math.PI / teeth, 315 | slopeAngle = pitchAngle * profileSlope * 0.5, 316 | addendumAngle = pitchAngle * (1 - profileSlope), 317 | theta = (addendumAngle * 0.5 + slopeAngle), 318 | path = ['M', rootRadius * Math.cos(theta), ',', rootRadius * Math.sin(theta)]; 319 | 320 | for(var i = 0; i < teeth; i++) { 321 | theta += circularPitch; 322 | 323 | path.push( 324 | 'A', rootRadius, ',', rootRadius, ' 0 0,1 ', rootRadius * Math.cos(theta), ',', rootRadius * Math.sin(theta), 325 | 'L', radius * Math.cos(theta), ',', radius * Math.sin(theta) 326 | ); 327 | 328 | theta += slopeAngle; 329 | path.push('L', outsideRadius * Math.cos(theta), ',', outsideRadius * Math.sin(theta)); 330 | theta += addendumAngle; 331 | path.push('A', outsideRadius, ',', outsideRadius, ' 0 0,1 ', outsideRadius * Math.cos(theta), ',', outsideRadius * Math.sin(theta)); 332 | theta += slopeAngle; 333 | 334 | path.push( 335 | 'L', radius * Math.cos(theta), ',', radius * Math.sin(theta), 336 | 'L', rootRadius * Math.cos(theta), ',', rootRadius * Math.sin(theta) 337 | ); 338 | } 339 | 340 | path.push('M0,', -holeRadius, 'A', holeRadius, ',', holeRadius, ' 0 0,0 0,', holeRadius, 'A', holeRadius, ',', holeRadius, ' 0 0,0 0,', -holeRadius, 'Z'); 341 | 342 | return path.join(''); 343 | } 344 | }; 345 | 346 | Gear.Utility = { 347 | keys: function(object) { 348 | if (Object.keys) 349 | return Object.keys(object); 350 | 351 | var keys = []; 352 | for (var key in object) { 353 | if (Object.prototype.hasOwnProperty.call(object, key)) { 354 | keys.push(key); 355 | } 356 | } 357 | return keys; 358 | }, 359 | 360 | distanceSquared: function(x1, y1, x2, y2) { 361 | var xs = x2 - x1, 362 | ys = y2 - y1; 363 | return (xs * xs) + (ys * ys); 364 | }, 365 | 366 | angle: function(x1, y1, x2, y2) { 367 | var angle = Math.atan2(y2 - y1, x2 - x1); 368 | return angle > 0 ? angle : 2 * Math.PI + angle; 369 | }, 370 | 371 | sign: function(x) { 372 | return x < 0 ? -1 : 1; 373 | }, 374 | 375 | arrayShuffle: function(array) { 376 | for (var i = array.length - 1; i > 0; i--) { 377 | var j = Math.floor(Math.random() * (i + 1)); 378 | var temp = array[i]; 379 | array[i] = array[j]; 380 | array[j] = temp; 381 | } 382 | return array; 383 | }, 384 | 385 | arrayClone: function(array) { 386 | return array.slice(0); 387 | } 388 | }; -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
4 | 5 | 6 | 7 | 8 |