├── LICENSE ├── README.md ├── css └── style.css ├── examples ├── cloth.html ├── shapes.html ├── spiderweb.html └── tree.html ├── index.html ├── js ├── verlet-1.0.0.js └── verlet-1.0.0.min.js ├── lib ├── constraint.js ├── dist.js ├── objects.js ├── vec2.js └── verlet.js ├── package.json └── site └── js └── common.js /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013 Sub Protocol and other contributors 2 | http://subprotocol.com/ 3 | 4 | Permission is hereby granted, free of charge, to any person obtaining 5 | a copy of this software and associated documentation files (the 6 | "Software"), to deal in the Software without restriction, including 7 | without limitation the rights to use, copy, modify, merge, publish, 8 | distribute, sublicense, and/or sell copies of the Software, and to 9 | permit persons to whom the Software is furnished to do so, subject to 10 | the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 17 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 19 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 20 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 21 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | verlet-js 2 | ========= 3 | 4 | A simple Verlet (pronounced 'ver-ley') physics engine written in javascript. 5 | 6 | Particles, distance constraints, and angular constraints are all supported by verlet-js. From these primitives it is possible to construct just about anything you can imagine. 7 | 8 | License 9 | ------- 10 | You may use verlet-js under the terms of the MIT License (See [LICENSE](LICENSE)). 11 | 12 | 13 | Examples 14 | -------- 15 | 1. [Shapes (verlet-js Hello world)](http://subprotocol.com/verlet-js/examples/shapes.html) 16 | 2. [Fractal Trees](http://subprotocol.com/verlet-js/examples/tree.html) 17 | 3. [Cloth](http://subprotocol.com/verlet-js/examples/cloth.html) 18 | 4. [Spiderweb](http://subprotocol.com/verlet-js/examples/spiderweb.html) 19 | 20 | 21 | Code Layout 22 | ----------- 23 | 1. js/verlet-js/vec2.js: _2d vector implementation_ 24 | 2. js/verlet-js/constraint.js: _constraint code_ 25 | 3. js/verlet-js/verlet.js: _verlet-js engine_ 26 | 4. js/verlet-js/objects.js: _shapes and objects (triangles, circles, tires..)_ 27 | 28 | Build for npm 29 | ------------- 30 | 31 | ``` js 32 | npm run build 33 | ``` 34 | 35 | -------------------------------------------------------------------------------- /css/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | background: #e4e6e8; 3 | background: -moz-linear-gradient(top, #ffffff 0%, #e4e8ee 20%, #e4e8ee 80%, #ffffff 100%); /* FF3.6+ */ 4 | background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#ffffff), color-stop(20%,#e4e8ee), color-stop(80%,#e4e8ee), color-stop(100%,#ffffff)); /* Chrome,Safari4+ */ 5 | background: -webkit-linear-gradient(top, #ffffff 0%,#e4e8ee 20%,#e4e8ee 80%,#ffffff 100%); /* Chrome10+,Safari5.1+ */ 6 | background: -o-linear-gradient(top, #ffffff 0%,#e4e8ee 20%,#e4e8ee 80%,#ffffff 100%); /* Opera 11.10+ */ 7 | background: -ms-linear-gradient(top, #ffffff 0%,#e4e8ee 20%,#e4e8ee 80%,#ffffff 100%); /* IE10+ */ 8 | background: linear-gradient(to bottom, #ffffff 0%,#e4e8ee 20%,#e4e8ee 80%,#ffffff 100%); /* W3C */ 9 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#ffffff', endColorstr='#ffffff',GradientType=0 ); /* IE6-9 */ 10 | 11 | color: #969ba3; 12 | font-family: "Libre Baskerville", sans-serif; 13 | font-weight: 400; 14 | line-height: 1.2em; 15 | } 16 | 17 | a { 18 | color: #26ae90; 19 | } 20 | 21 | a:hover { 22 | color: #0b6b6a; 23 | } 24 | 25 | b { 26 | color: #696e76; 27 | } 28 | 29 | ul { 30 | list-style: square; 31 | } 32 | 33 | li { 34 | margin: 4px 0px; 35 | } 36 | 37 | #header { 38 | width: 800px; 39 | margin: 0px auto; 40 | font-size: 10pt; 41 | } 42 | 43 | h1 { 44 | display: block; 45 | font-weight: 700; 46 | margin: 50px 0px 0px 0px; 47 | padding-bottom: 3px; 48 | border-bottom: 1px solid rgba(0,0,0,0.05); 49 | text-shadow: 1px 1px 0px #fff; 50 | } 51 | 52 | h1:after { 53 | content: ""; 54 | display: block; 55 | padding-bottom: 20px; 56 | border-bottom: 1px solid rgba(0,0,0,0.05); 57 | } 58 | 59 | h1 a { 60 | color: #969ba3; 61 | text-decoration: none; 62 | } 63 | 64 | h1 a:hover { 65 | color: #585a5d; 66 | text-decoration: none; 67 | } 68 | 69 | h1 em { 70 | color: #444; 71 | font-style: normal; 72 | } 73 | 74 | 75 | 76 | h4 { 77 | color: #444; 78 | font-weight: 700; 79 | font-size: 12pt; 80 | margin: 20px 0px 0px 0px; 81 | } 82 | 83 | 84 | canvas { 85 | display: block; 86 | margin: 34px auto; 87 | background: #fff; 88 | box-shadow: 2px 2px 8px 0px rgba(0,0,0,0.1); 89 | clear: both; 90 | 91 | /* disable selection of canvas */ 92 | -moz-user-select: none; 93 | -webkit-user-select: none; 94 | -o-user-select:none; 95 | -ms-user-select:none; 96 | -khtml-user-select:none; 97 | user-select: none; 98 | } 99 | 100 | 101 | #footer { 102 | line-height: 1.5em; 103 | font-size: 8pt; 104 | border-top: 1px solid rgba(0,0,0,0.05); 105 | padding-top: 3px; 106 | width: 800px; 107 | margin: 0px auto 100px auto; 108 | } 109 | 110 | #footer:before { 111 | content: ""; 112 | display: block; 113 | padding-top: 20px; 114 | border-top: 1px solid rgba(0,0,0,0.05); 115 | } 116 | 117 | 118 | #bsa { 119 | display: inline-block; 120 | float: right; 121 | width: 150px; 122 | padding-top: 30px; 123 | font-family: sans-serif; 124 | } 125 | 126 | body .one .bsa_it_ad { background: transparent; border: none; font-family: inherit; padding: 0 15px 0 10px; margin: 0; text-align: left; } 127 | body .one .bsa_it_ad:hover img { -moz-box-shadow: 0 0 3px #000; -webkit-box-shadow: 0 0 3px #000; box-shadow: 0 0 3px #000; } 128 | body .one .bsa_it_ad .bsa_it_i { display: block; padding: 0; float: none; margin: 0 0 5px; } 129 | body .one .bsa_it_ad .bsa_it_i img { padding: 0; border: none; } 130 | body .one .bsa_it_ad .bsa_it_t { padding: 6px 0; } 131 | body .one .bsa_it_ad .bsa_it_d { padding: 0; font-size: 10px; color: #333; } 132 | body .one .bsa_it_p { display: none; } 133 | body #bsap_aplink, body #bsap_aplink:hover { display: block; font-size: 9px; margin: 12px 15px 0; text-align: right; } 134 | 135 | -------------------------------------------------------------------------------- /examples/cloth.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verlet Cloth Simulation 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 | 117 | 121 | 122 | -------------------------------------------------------------------------------- /examples/shapes.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verlet Shapes 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 | 69 | 73 | 74 | -------------------------------------------------------------------------------- /examples/spiderweb.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verlet Spiderweb 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 34 | 35 | 368 | 372 | 373 | -------------------------------------------------------------------------------- /examples/tree.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verlet Fractal Trees 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 28 | 29 | 159 | 163 | 164 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Verlet-js 5 | 6 | 7 | 8 | 9 | 10 | 11 | 52 | 53 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /js/verlet-1.0.0.js: -------------------------------------------------------------------------------- 1 | ;(function(e,t,n){function i(n,s){if(!t[n]){if(!e[n]){var o=typeof require=="function"&&require;if(!s&&o)return o(n,!0);if(r)return r(n,!0);throw new Error("Cannot find module '"+n+"'")}var u=t[n]={exports:{}};e[n][0].call(u.exports,function(t){var r=e[n][1][t];return i(r?r:t)},u,u.exports)}return t[n].exports}var r=typeof require=="function"&&require;for(var s=0;s= Math.PI) 110 | diff -= 2*Math.PI; 111 | 112 | diff *= stepCoef*this.stiffness; 113 | 114 | this.a.pos = this.a.pos.rotate(this.b.pos, diff); 115 | this.c.pos = this.c.pos.rotate(this.b.pos, -diff); 116 | this.b.pos = this.b.pos.rotate(this.a.pos, diff); 117 | this.b.pos = this.b.pos.rotate(this.c.pos, -diff); 118 | } 119 | 120 | AngleConstraint.prototype.draw = function(ctx) { 121 | ctx.beginPath(); 122 | ctx.moveTo(this.a.pos.x, this.a.pos.y); 123 | ctx.lineTo(this.b.pos.x, this.b.pos.y); 124 | ctx.lineTo(this.c.pos.x, this.c.pos.y); 125 | var tmp = ctx.lineWidth; 126 | ctx.lineWidth = 5; 127 | ctx.strokeStyle = "rgba(255,255,0,0.2)"; 128 | ctx.stroke(); 129 | ctx.lineWidth = tmp; 130 | } 131 | 132 | },{}],5:[function(require,module,exports){ 133 | 134 | /* 135 | Copyright 2013 Sub Protocol and other contributors 136 | http://subprotocol.com/ 137 | 138 | Permission is hereby granted, free of charge, to any person obtaining 139 | a copy of this software and associated documentation files (the 140 | "Software"), to deal in the Software without restriction, including 141 | without limitation the rights to use, copy, modify, merge, publish, 142 | distribute, sublicense, and/or sell copies of the Software, and to 143 | permit persons to whom the Software is furnished to do so, subject to 144 | the following conditions: 145 | 146 | The above copyright notice and this permission notice shall be 147 | included in all copies or substantial portions of the Software. 148 | 149 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 150 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 151 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 152 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 153 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 154 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 155 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 156 | */ 157 | 158 | // A simple 2-dimensional vector implementation 159 | 160 | module.exports = Vec2 161 | 162 | function Vec2(x, y) { 163 | this.x = x || 0; 164 | this.y = y || 0; 165 | } 166 | 167 | Vec2.prototype.add = function(v) { 168 | return new Vec2(this.x + v.x, this.y + v.y); 169 | } 170 | 171 | Vec2.prototype.sub = function(v) { 172 | return new Vec2(this.x - v.x, this.y - v.y); 173 | } 174 | 175 | Vec2.prototype.mul = function(v) { 176 | return new Vec2(this.x * v.x, this.y * v.y); 177 | } 178 | 179 | Vec2.prototype.div = function(v) { 180 | return new Vec2(this.x / v.x, this.y / v.y); 181 | } 182 | 183 | Vec2.prototype.scale = function(coef) { 184 | return new Vec2(this.x*coef, this.y*coef); 185 | } 186 | 187 | Vec2.prototype.mutableSet = function(v) { 188 | this.x = v.x; 189 | this.y = v.y; 190 | return this; 191 | } 192 | 193 | Vec2.prototype.mutableAdd = function(v) { 194 | this.x += v.x; 195 | this.y += v.y; 196 | return this; 197 | } 198 | 199 | Vec2.prototype.mutableSub = function(v) { 200 | this.x -= v.x; 201 | this.y -= v.y; 202 | return this; 203 | } 204 | 205 | Vec2.prototype.mutableMul = function(v) { 206 | this.x *= v.x; 207 | this.y *= v.y; 208 | return this; 209 | } 210 | 211 | Vec2.prototype.mutableDiv = function(v) { 212 | this.x /= v.x; 213 | this.y /= v.y; 214 | return this; 215 | } 216 | 217 | Vec2.prototype.mutableScale = function(coef) { 218 | this.x *= coef; 219 | this.y *= coef; 220 | return this; 221 | } 222 | 223 | Vec2.prototype.equals = function(v) { 224 | return this.x == v.x && this.y == v.y; 225 | } 226 | 227 | Vec2.prototype.epsilonEquals = function(v, epsilon) { 228 | return Math.abs(this.x - v.x) <= epsilon && Math.abs(this.y - v.y) <= epsilon; 229 | } 230 | 231 | Vec2.prototype.length = function(v) { 232 | return Math.sqrt(this.x*this.x + this.y*this.y); 233 | } 234 | 235 | Vec2.prototype.length2 = function(v) { 236 | return this.x*this.x + this.y*this.y; 237 | } 238 | 239 | Vec2.prototype.dist = function(v) { 240 | return Math.sqrt(this.dist2(v)); 241 | } 242 | 243 | Vec2.prototype.dist2 = function(v) { 244 | var x = v.x - this.x; 245 | var y = v.y - this.y; 246 | return x*x + y*y; 247 | } 248 | 249 | Vec2.prototype.normal = function() { 250 | var m = Math.sqrt(this.x*this.x + this.y*this.y); 251 | return new Vec2(this.x/m, this.y/m); 252 | } 253 | 254 | Vec2.prototype.dot = function(v) { 255 | return this.x*v.x + this.y*v.y; 256 | } 257 | 258 | Vec2.prototype.angle = function(v) { 259 | return Math.atan2(this.x*v.y-this.y*v.x,this.x*v.x+this.y*v.y); 260 | } 261 | 262 | Vec2.prototype.angle2 = function(vLeft, vRight) { 263 | return vLeft.sub(this).angle(vRight.sub(this)); 264 | } 265 | 266 | Vec2.prototype.rotate = function(origin, theta) { 267 | var x = this.x - origin.x; 268 | var y = this.y - origin.y; 269 | return new Vec2(x*Math.cos(theta) - y*Math.sin(theta) + origin.x, x*Math.sin(theta) + y*Math.cos(theta) + origin.y); 270 | } 271 | 272 | Vec2.prototype.toString = function() { 273 | return "(" + this.x + ", " + this.y + ")"; 274 | } 275 | 276 | function test_Vec2() { 277 | var assert = function(label, expression) { 278 | console.log("Vec2(" + label + "): " + (expression == true ? "PASS" : "FAIL")); 279 | if (expression != true) 280 | throw "assertion failed"; 281 | }; 282 | 283 | assert("equality", (new Vec2(5,3).equals(new Vec2(5,3)))); 284 | assert("epsilon equality", (new Vec2(1,2).epsilonEquals(new Vec2(1.01,2.02), 0.03))); 285 | assert("epsilon non-equality", !(new Vec2(1,2).epsilonEquals(new Vec2(1.01,2.02), 0.01))); 286 | assert("addition", (new Vec2(1,1)).add(new Vec2(2, 3)).equals(new Vec2(3, 4))); 287 | assert("subtraction", (new Vec2(4,3)).sub(new Vec2(2, 1)).equals(new Vec2(2, 2))); 288 | assert("multiply", (new Vec2(2,4)).mul(new Vec2(2, 1)).equals(new Vec2(4, 4))); 289 | assert("divide", (new Vec2(4,2)).div(new Vec2(2, 2)).equals(new Vec2(2, 1))); 290 | assert("scale", (new Vec2(4,3)).scale(2).equals(new Vec2(8, 6))); 291 | assert("mutable set", (new Vec2(1,1)).mutableSet(new Vec2(2, 3)).equals(new Vec2(2, 3))); 292 | assert("mutable addition", (new Vec2(1,1)).mutableAdd(new Vec2(2, 3)).equals(new Vec2(3, 4))); 293 | assert("mutable subtraction", (new Vec2(4,3)).mutableSub(new Vec2(2, 1)).equals(new Vec2(2, 2))); 294 | assert("mutable multiply", (new Vec2(2,4)).mutableMul(new Vec2(2, 1)).equals(new Vec2(4, 4))); 295 | assert("mutable divide", (new Vec2(4,2)).mutableDiv(new Vec2(2, 2)).equals(new Vec2(2, 1))); 296 | assert("mutable scale", (new Vec2(4,3)).mutableScale(2).equals(new Vec2(8, 6))); 297 | assert("length", Math.abs((new Vec2(4,4)).length() - 5.65685) <= 0.00001); 298 | assert("length2", (new Vec2(2,4)).length2() == 20); 299 | assert("dist", Math.abs((new Vec2(2,4)).dist(new Vec2(3,5)) - 1.4142135) <= 0.000001); 300 | assert("dist2", (new Vec2(2,4)).dist2(new Vec2(3,5)) == 2); 301 | 302 | var normal = (new Vec2(2,4)).normal() 303 | assert("normal", Math.abs(normal.length() - 1.0) <= 0.00001 && normal.epsilonEquals(new Vec2(0.4472, 0.89443), 0.0001)); 304 | assert("dot", (new Vec2(2,3)).dot(new Vec2(4,1)) == 11); 305 | assert("angle", (new Vec2(0,-1)).angle(new Vec2(1,0))*(180/Math.PI) == 90); 306 | assert("angle2", (new Vec2(1,1)).angle2(new Vec2(1,0), new Vec2(2,1))*(180/Math.PI) == 90); 307 | assert("rotate", (new Vec2(2,0)).rotate(new Vec2(1,0), Math.PI/2).equals(new Vec2(1,1))); 308 | assert("toString", (new Vec2(2,4)) == "(2, 4)"); 309 | } 310 | 311 | 312 | },{}],4:[function(require,module,exports){ 313 | 314 | /* 315 | Copyright 2013 Sub Protocol and other contributors 316 | http://subprotocol.com/ 317 | 318 | Permission is hereby granted, free of charge, to any person obtaining 319 | a copy of this software and associated documentation files (the 320 | "Software"), to deal in the Software without restriction, including 321 | without limitation the rights to use, copy, modify, merge, publish, 322 | distribute, sublicense, and/or sell copies of the Software, and to 323 | permit persons to whom the Software is furnished to do so, subject to 324 | the following conditions: 325 | 326 | The above copyright notice and this permission notice shall be 327 | included in all copies or substantial portions of the Software. 328 | 329 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 330 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 331 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 332 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 333 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 334 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 335 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 336 | */ 337 | 338 | // generic verlet entities 339 | 340 | var VerletJS = require('./verlet') 341 | var Particle = VerletJS.Particle 342 | var constraints = require('./constraint') 343 | var DistanceConstraint = constraints.DistanceConstraint 344 | 345 | VerletJS.prototype.point = function(pos) { 346 | var composite = new this.Composite(); 347 | composite.particles.push(new Particle(pos)); 348 | this.composites.push(composite); 349 | return composite; 350 | } 351 | 352 | VerletJS.prototype.lineSegments = function(vertices, stiffness) { 353 | var i; 354 | 355 | var composite = new this.Composite(); 356 | 357 | for (i in vertices) { 358 | composite.particles.push(new Particle(vertices[i])); 359 | if (i > 0) 360 | composite.constraints.push(new DistanceConstraint(composite.particles[i], composite.particles[i-1], stiffness)); 361 | } 362 | 363 | this.composites.push(composite); 364 | return composite; 365 | } 366 | 367 | VerletJS.prototype.cloth = function(origin, width, height, segments, pinMod, stiffness) { 368 | 369 | var composite = new this.Composite(); 370 | 371 | var xStride = width/segments; 372 | var yStride = height/segments; 373 | 374 | var x,y; 375 | for (y=0;y 0) 382 | composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x], composite.particles[y*segments+x-1], stiffness)); 383 | 384 | if (y > 0) 385 | composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x], composite.particles[(y-1)*segments+x], stiffness)); 386 | } 387 | } 388 | 389 | for (x=0;x this.height-1) 491 | particle.pos.y = this.height-1; 492 | 493 | if (particle.pos.x < 0) 494 | particle.pos.x = 0; 495 | 496 | if (particle.pos.x > this.width-1) 497 | particle.pos.x = this.width-1; 498 | } 499 | 500 | var _this = this; 501 | 502 | // prevent context menu 503 | this.canvas.oncontextmenu = function(e) { 504 | e.preventDefault(); 505 | }; 506 | 507 | this.canvas.onmousedown = function(e) { 508 | _this.mouseDown = true; 509 | var nearest = _this.nearestEntity(); 510 | if (nearest) { 511 | _this.draggedEntity = nearest; 512 | } 513 | }; 514 | 515 | this.canvas.onmouseup = function(e) { 516 | _this.mouseDown = false; 517 | _this.draggedEntity = null; 518 | }; 519 | 520 | this.canvas.onmousemove = function(e) { 521 | var rect = _this.canvas.getBoundingClientRect(); 522 | _this.mouse.x = e.clientX - rect.left; 523 | _this.mouse.y = e.clientY - rect.top; 524 | }; 525 | 526 | // simulation params 527 | this.gravity = new Vec2(0,0.2); 528 | this.friction = 0.99; 529 | this.groundFriction = 0.8; 530 | 531 | // holds composite entities 532 | this.composites = []; 533 | } 534 | 535 | VerletJS.prototype.Composite = Composite 536 | 537 | function Composite() { 538 | this.particles = []; 539 | this.constraints = []; 540 | 541 | this.drawParticles = null; 542 | this.drawConstraints = null; 543 | } 544 | 545 | Composite.prototype.pin = function(index, pos) { 546 | pos = pos || this.particles[index].pos; 547 | var pc = new PinConstraint(this.particles[index], pos); 548 | this.constraints.push(pc); 549 | return pc; 550 | } 551 | 552 | VerletJS.prototype.frame = function(step) { 553 | var i, j, c; 554 | 555 | for (c in this.composites) { 556 | for (i in this.composites[c].particles) { 557 | var particles = this.composites[c].particles; 558 | 559 | // calculate velocity 560 | var velocity = particles[i].pos.sub(particles[i].lastPos).scale(this.friction); 561 | 562 | // ground friction 563 | if (particles[i].pos.y >= this.height-1 && velocity.length2() > 0.000001) { 564 | var m = velocity.length(); 565 | velocity.x /= m; 566 | velocity.y /= m; 567 | velocity.mutableScale(m*this.groundFriction); 568 | } 569 | 570 | // save last good state 571 | particles[i].lastPos.mutableSet(particles[i].pos); 572 | 573 | // gravity 574 | particles[i].pos.mutableAdd(this.gravity); 575 | 576 | // inertia 577 | particles[i].pos.mutableAdd(velocity); 578 | } 579 | } 580 | 581 | // handle dragging of entities 582 | if (this.draggedEntity) 583 | this.draggedEntity.pos.mutableSet(this.mouse); 584 | 585 | // relax 586 | var stepCoef = 1/step; 587 | for (c in this.composites) { 588 | var constraints = this.composites[c].constraints; 589 | for (i=0;i=Math.PI)diff-=2*Math.PI;diff*=stepCoef*this.stiffness;this.a.pos=this.a.pos.rotate(this.b.pos,diff);this.c.pos=this.c.pos.rotate(this.b.pos,-diff);this.b.pos=this.b.pos.rotate(this.a.pos,diff);this.b.pos=this.b.pos.rotate(this.c.pos,-diff)};AngleConstraint.prototype.draw=function(ctx){ctx.beginPath();ctx.moveTo(this.a.pos.x,this.a.pos.y);ctx.lineTo(this.b.pos.x,this.b.pos.y);ctx.lineTo(this.c.pos.x,this.c.pos.y);var tmp=ctx.lineWidth;ctx.lineWidth=5;ctx.strokeStyle="rgba(255,255,0,0.2)";ctx.stroke();ctx.lineWidth=tmp}},{}],5:[function(require,module,exports){module.exports=Vec2;function Vec2(x,y){this.x=x||0;this.y=y||0}Vec2.prototype.add=function(v){return new Vec2(this.x+v.x,this.y+v.y)};Vec2.prototype.sub=function(v){return new Vec2(this.x-v.x,this.y-v.y)};Vec2.prototype.mul=function(v){return new Vec2(this.x*v.x,this.y*v.y)};Vec2.prototype.div=function(v){return new Vec2(this.x/v.x,this.y/v.y)};Vec2.prototype.scale=function(coef){return new Vec2(this.x*coef,this.y*coef)};Vec2.prototype.mutableSet=function(v){this.x=v.x;this.y=v.y;return this};Vec2.prototype.mutableAdd=function(v){this.x+=v.x;this.y+=v.y;return this};Vec2.prototype.mutableSub=function(v){this.x-=v.x;this.y-=v.y;return this};Vec2.prototype.mutableMul=function(v){this.x*=v.x;this.y*=v.y;return this};Vec2.prototype.mutableDiv=function(v){this.x/=v.x;this.y/=v.y;return this};Vec2.prototype.mutableScale=function(coef){this.x*=coef;this.y*=coef;return this};Vec2.prototype.equals=function(v){return this.x==v.x&&this.y==v.y};Vec2.prototype.epsilonEquals=function(v,epsilon){return Math.abs(this.x-v.x)<=epsilon&&Math.abs(this.y-v.y)<=epsilon};Vec2.prototype.length=function(v){return Math.sqrt(this.x*this.x+this.y*this.y)};Vec2.prototype.length2=function(v){return this.x*this.x+this.y*this.y};Vec2.prototype.dist=function(v){return Math.sqrt(this.dist2(v))};Vec2.prototype.dist2=function(v){var x=v.x-this.x;var y=v.y-this.y;return x*x+y*y};Vec2.prototype.normal=function(){var m=Math.sqrt(this.x*this.x+this.y*this.y);return new Vec2(this.x/m,this.y/m)};Vec2.prototype.dot=function(v){return this.x*v.x+this.y*v.y};Vec2.prototype.angle=function(v){return Math.atan2(this.x*v.y-this.y*v.x,this.x*v.x+this.y*v.y)};Vec2.prototype.angle2=function(vLeft,vRight){return vLeft.sub(this).angle(vRight.sub(this))};Vec2.prototype.rotate=function(origin,theta){var x=this.x-origin.x;var y=this.y-origin.y;return new Vec2(x*Math.cos(theta)-y*Math.sin(theta)+origin.x,x*Math.sin(theta)+y*Math.cos(theta)+origin.y)};Vec2.prototype.toString=function(){return"("+this.x+", "+this.y+")"};function test_Vec2(){var assert=function(label,expression){console.log("Vec2("+label+"): "+(expression==true?"PASS":"FAIL"));if(expression!=true)throw"assertion failed"};assert("equality",new Vec2(5,3).equals(new Vec2(5,3)));assert("epsilon equality",new Vec2(1,2).epsilonEquals(new Vec2(1.01,2.02),.03));assert("epsilon non-equality",!new Vec2(1,2).epsilonEquals(new Vec2(1.01,2.02),.01));assert("addition",new Vec2(1,1).add(new Vec2(2,3)).equals(new Vec2(3,4)));assert("subtraction",new Vec2(4,3).sub(new Vec2(2,1)).equals(new Vec2(2,2)));assert("multiply",new Vec2(2,4).mul(new Vec2(2,1)).equals(new Vec2(4,4)));assert("divide",new Vec2(4,2).div(new Vec2(2,2)).equals(new Vec2(2,1)));assert("scale",new Vec2(4,3).scale(2).equals(new Vec2(8,6)));assert("mutable set",new Vec2(1,1).mutableSet(new Vec2(2,3)).equals(new Vec2(2,3)));assert("mutable addition",new Vec2(1,1).mutableAdd(new Vec2(2,3)).equals(new Vec2(3,4)));assert("mutable subtraction",new Vec2(4,3).mutableSub(new Vec2(2,1)).equals(new Vec2(2,2)));assert("mutable multiply",new Vec2(2,4).mutableMul(new Vec2(2,1)).equals(new Vec2(4,4)));assert("mutable divide",new Vec2(4,2).mutableDiv(new Vec2(2,2)).equals(new Vec2(2,1)));assert("mutable scale",new Vec2(4,3).mutableScale(2).equals(new Vec2(8,6)));assert("length",Math.abs(new Vec2(4,4).length()-5.65685)<=1e-5);assert("length2",new Vec2(2,4).length2()==20);assert("dist",Math.abs(new Vec2(2,4).dist(new Vec2(3,5))-1.4142135)<=1e-6);assert("dist2",new Vec2(2,4).dist2(new Vec2(3,5))==2);var normal=new Vec2(2,4).normal();assert("normal",Math.abs(normal.length()-1)<=1e-5&&normal.epsilonEquals(new Vec2(.4472,.89443),1e-4));assert("dot",new Vec2(2,3).dot(new Vec2(4,1))==11);assert("angle",new Vec2(0,-1).angle(new Vec2(1,0))*(180/Math.PI)==90);assert("angle2",new Vec2(1,1).angle2(new Vec2(1,0),new Vec2(2,1))*(180/Math.PI)==90);assert("rotate",new Vec2(2,0).rotate(new Vec2(1,0),Math.PI/2).equals(new Vec2(1,1)));assert("toString",new Vec2(2,4)=="(2, 4)")}},{}],4:[function(require,module,exports){var VerletJS=require("./verlet");var Particle=VerletJS.Particle;var constraints=require("./constraint");var DistanceConstraint=constraints.DistanceConstraint;VerletJS.prototype.point=function(pos){var composite=new this.Composite;composite.particles.push(new Particle(pos));this.composites.push(composite);return composite};VerletJS.prototype.lineSegments=function(vertices,stiffness){var i;var composite=new this.Composite;for(i in vertices){composite.particles.push(new Particle(vertices[i]));if(i>0)composite.constraints.push(new DistanceConstraint(composite.particles[i],composite.particles[i-1],stiffness))}this.composites.push(composite);return composite};VerletJS.prototype.cloth=function(origin,width,height,segments,pinMod,stiffness){var composite=new this.Composite;var xStride=width/segments;var yStride=height/segments;var x,y;for(y=0;y0)composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x],composite.particles[y*segments+x-1],stiffness));if(y>0)composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x],composite.particles[(y-1)*segments+x],stiffness))}}for(x=0;xthis.height-1)particle.pos.y=this.height-1;if(particle.pos.x<0)particle.pos.x=0;if(particle.pos.x>this.width-1)particle.pos.x=this.width-1};var _this=this;this.canvas.oncontextmenu=function(e){e.preventDefault()};this.canvas.onmousedown=function(e){_this.mouseDown=true;var nearest=_this.nearestEntity();if(nearest){_this.draggedEntity=nearest}};this.canvas.onmouseup=function(e){_this.mouseDown=false;_this.draggedEntity=null};this.canvas.onmousemove=function(e){var rect=_this.canvas.getBoundingClientRect();_this.mouse.x=e.clientX-rect.left;_this.mouse.y=e.clientY-rect.top};this.gravity=new Vec2(0,.2);this.friction=.99;this.groundFriction=.8;this.composites=[]}VerletJS.prototype.Composite=Composite;function Composite(){this.particles=[];this.constraints=[];this.drawParticles=null;this.drawConstraints=null}Composite.prototype.pin=function(index,pos){pos=pos||this.particles[index].pos;var pc=new PinConstraint(this.particles[index],pos);this.constraints.push(pc);return pc};VerletJS.prototype.frame=function(step){var i,j,c;for(c in this.composites){for(i in this.composites[c].particles){var particles=this.composites[c].particles;var velocity=particles[i].pos.sub(particles[i].lastPos).scale(this.friction);if(particles[i].pos.y>=this.height-1&&velocity.length2()>1e-6){var m=velocity.length();velocity.x/=m;velocity.y/=m;velocity.mutableScale(m*this.groundFriction)}particles[i].lastPos.mutableSet(particles[i].pos);particles[i].pos.mutableAdd(this.gravity);particles[i].pos.mutableAdd(velocity)}}if(this.draggedEntity)this.draggedEntity.pos.mutableSet(this.mouse);var stepCoef=1/step;for(c in this.composites){var constraints=this.composites[c].constraints;for(i=0;i= Math.PI) 91 | diff -= 2*Math.PI; 92 | 93 | diff *= stepCoef*this.stiffness; 94 | 95 | this.a.pos = this.a.pos.rotate(this.b.pos, diff); 96 | this.c.pos = this.c.pos.rotate(this.b.pos, -diff); 97 | this.b.pos = this.b.pos.rotate(this.a.pos, diff); 98 | this.b.pos = this.b.pos.rotate(this.c.pos, -diff); 99 | } 100 | 101 | AngleConstraint.prototype.draw = function(ctx) { 102 | ctx.beginPath(); 103 | ctx.moveTo(this.a.pos.x, this.a.pos.y); 104 | ctx.lineTo(this.b.pos.x, this.b.pos.y); 105 | ctx.lineTo(this.c.pos.x, this.c.pos.y); 106 | var tmp = ctx.lineWidth; 107 | ctx.lineWidth = 5; 108 | ctx.strokeStyle = "rgba(255,255,0,0.2)"; 109 | ctx.stroke(); 110 | ctx.lineWidth = tmp; 111 | } 112 | -------------------------------------------------------------------------------- /lib/dist.js: -------------------------------------------------------------------------------- 1 | 2 | //this exports all the verlet methods globally, so that the demos work. 3 | 4 | var VerletJS = require('./verlet') 5 | var constraint = require('./constraint') 6 | require('./objects') //patches VerletJS.prototype (bad) 7 | window.Vec2 = require('./vec2') 8 | window.VerletJS = VerletJS 9 | 10 | window.Particle = VerletJS.Particle 11 | 12 | window.DistanceConstraint = constraint.DistanceConstraint 13 | window.PinConstraint = constraint.PinConstraint 14 | window.AngleConstraint = constraint.AngleConstraint 15 | 16 | 17 | -------------------------------------------------------------------------------- /lib/objects.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | Copyright 2013 Sub Protocol and other contributors 4 | http://subprotocol.com/ 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | "Software"), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 20 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 21 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 22 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 23 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | // generic verlet entities 27 | 28 | var VerletJS = require('./verlet') 29 | var Particle = VerletJS.Particle 30 | var constraints = require('./constraint') 31 | var DistanceConstraint = constraints.DistanceConstraint 32 | 33 | VerletJS.prototype.point = function(pos) { 34 | var composite = new this.Composite(); 35 | composite.particles.push(new Particle(pos)); 36 | this.composites.push(composite); 37 | return composite; 38 | } 39 | 40 | VerletJS.prototype.lineSegments = function(vertices, stiffness) { 41 | var i; 42 | 43 | var composite = new this.Composite(); 44 | 45 | for (i in vertices) { 46 | composite.particles.push(new Particle(vertices[i])); 47 | if (i > 0) 48 | composite.constraints.push(new DistanceConstraint(composite.particles[i], composite.particles[i-1], stiffness)); 49 | } 50 | 51 | this.composites.push(composite); 52 | return composite; 53 | } 54 | 55 | VerletJS.prototype.cloth = function(origin, width, height, segments, pinMod, stiffness) { 56 | 57 | var composite = new this.Composite(); 58 | 59 | var xStride = width/segments; 60 | var yStride = height/segments; 61 | 62 | var x,y; 63 | for (y=0;y 0) 70 | composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x], composite.particles[y*segments+x-1], stiffness)); 71 | 72 | if (y > 0) 73 | composite.constraints.push(new DistanceConstraint(composite.particles[y*segments+x], composite.particles[(y-1)*segments+x], stiffness)); 74 | } 75 | } 76 | 77 | for (x=0;x this.height-1) 66 | particle.pos.y = this.height-1; 67 | 68 | if (particle.pos.x < 0) 69 | particle.pos.x = 0; 70 | 71 | if (particle.pos.x > this.width-1) 72 | particle.pos.x = this.width-1; 73 | } 74 | 75 | var _this = this; 76 | 77 | // prevent context menu 78 | this.canvas.oncontextmenu = function(e) { 79 | e.preventDefault(); 80 | }; 81 | 82 | this.canvas.onmousedown = function(e) { 83 | _this.mouseDown = true; 84 | var nearest = _this.nearestEntity(); 85 | if (nearest) { 86 | _this.draggedEntity = nearest; 87 | } 88 | }; 89 | 90 | this.canvas.onmouseup = function(e) { 91 | _this.mouseDown = false; 92 | _this.draggedEntity = null; 93 | }; 94 | 95 | this.canvas.onmousemove = function(e) { 96 | var rect = _this.canvas.getBoundingClientRect(); 97 | _this.mouse.x = e.clientX - rect.left; 98 | _this.mouse.y = e.clientY - rect.top; 99 | }; 100 | 101 | // simulation params 102 | this.gravity = new Vec2(0,0.2); 103 | this.friction = 0.99; 104 | this.groundFriction = 0.8; 105 | 106 | // holds composite entities 107 | this.composites = []; 108 | } 109 | 110 | VerletJS.prototype.Composite = Composite 111 | 112 | function Composite() { 113 | this.particles = []; 114 | this.constraints = []; 115 | 116 | this.drawParticles = null; 117 | this.drawConstraints = null; 118 | } 119 | 120 | Composite.prototype.pin = function(index, pos) { 121 | pos = pos || this.particles[index].pos; 122 | var pc = new PinConstraint(this.particles[index], pos); 123 | this.constraints.push(pc); 124 | return pc; 125 | } 126 | 127 | VerletJS.prototype.frame = function(step) { 128 | var i, j, c; 129 | 130 | for (c in this.composites) { 131 | for (i in this.composites[c].particles) { 132 | var particles = this.composites[c].particles; 133 | 134 | // calculate velocity 135 | var velocity = particles[i].pos.sub(particles[i].lastPos).scale(this.friction); 136 | 137 | // ground friction 138 | if (particles[i].pos.y >= this.height-1 && velocity.length2() > 0.000001) { 139 | var m = velocity.length(); 140 | velocity.x /= m; 141 | velocity.y /= m; 142 | velocity.mutableScale(m*this.groundFriction); 143 | } 144 | 145 | // save last good state 146 | particles[i].lastPos.mutableSet(particles[i].pos); 147 | 148 | // gravity 149 | particles[i].pos.mutableAdd(this.gravity); 150 | 151 | // inertia 152 | particles[i].pos.mutableAdd(velocity); 153 | } 154 | } 155 | 156 | // handle dragging of entities 157 | if (this.draggedEntity) 158 | this.draggedEntity.pos.mutableSet(this.mouse); 159 | 160 | // relax 161 | var stepCoef = 1/step; 162 | for (c in this.composites) { 163 | var constraints = this.composites[c].constraints; 164 | for (i=0;i ./js/verlet-$V.min.js" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git://github.com/subprotocol/verlet-js.git" 16 | }, 17 | "keywords": [ 18 | "physics", 19 | "2d", 20 | "verlet" 21 | ], 22 | "author": "subprotocol", 23 | "license": "BSD", 24 | "readmeFilename": "README.md", 25 | "gitHead": "283d23fa020d5147e95e84c5ff3541c17ea2abe5", 26 | "bugs": { 27 | "url": "https://github.com/subprotocol/verlet-js/issues" 28 | }, 29 | "dependencies": { 30 | "vec2": "~1.3.0" 31 | }, 32 | "devDependencies": { 33 | "browserify": "~2.18.1", 34 | "uglify-js": "~2.3.6" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /site/js/common.js: -------------------------------------------------------------------------------- 1 | 2 | if (window.location.host == "subprotocol.com") { 3 | (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ 4 | (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), 5 | m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) 6 | })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); 7 | 8 | ga('create', 'UA-83795-7', 'subprotocol.com'); 9 | ga('send', 'pageview'); 10 | } 11 | --------------------------------------------------------------------------------