├── .gitattributes ├── .gitignore ├── README.md ├── Verlet3D.min.js └── verlet3D.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | verlet3D 2 | ========= 3 | 4 | An easy-to-use framework for creating 3D verlet integration simulations 5 | 6 | License 7 | ------- 8 | MIT 9 | 10 | 11 | Examples & Documentation 12 | -------- 13 | [Click](http://mihailtornberg.com/projects/verlet3D) 14 | -------------------------------------------------------------------------------- /Verlet3D.min.js: -------------------------------------------------------------------------------- 1 | window.requestAnimFrame=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(t){window.setTimeout(t,1e3/60)};var Verlet3D=function(t){this.canvas=t.canvas,this.ctx=canvas.getContext("2d"),this.draggedParticle=-1,this.draggedModel=0,this.mouse={down:!1,x:0,y:0,ox:0,oy:0,button:0,clicked:0},this.ctx.lineWidth=t.lineWidth||.7,this.ctx.fillStyle=t.fillStyle||"#00D5B4",this.ctx.strokeStyle=t.strokeStyle||"#666";var e=this;this.canvas.onmousemove=function(t){var s=e.canvas.getBoundingClientRect();e.mouse.ox=e.mouse.x,e.mouse.oy=e.mouse.y,e.mouse.x=t.clientX-s.left,e.mouse.y=t.clientY-s.top,t.preventDefault()},this.canvas.onmousedown=function(t){e.mouse.button=t.which,e.mouse.down=!0,t.preventDefault()},this.canvas.onmouseup=function(t){e.mouse.down=!1,e.draggedParticle=-1,t.preventDefault()},this.canvas.oncontextmenu=function(t){t.preventDefault()}};Verlet3D.prototype.rotateCamera=function(t){t.model.angleY+=t.y||0,t.model.angleX+=t.x||0,t.model.angleZ+=t.z||0},Verlet3D.prototype.calc3D=function(t){t.vertex=[];for(var e=0;et.tear_distance&&t.constraints.splice(r,1)}};var Model=function(t){this.vertex=[],this.particles=[],this.constraints=[],this.xPos=t.xPos||0,this.yPos=t.yPos||0,this.angleX=0,this.angleY=0,this.angleZ=0,this.scale=t.scale||1,this.gravity=t.gravity||.2,this.friction=t.friction||.99,this.iterations=t.iterations||5,this.tear_distance=t.tearDistance||120,this.field_of_view=t.fov||1500};Model.prototype.createParticle=function(t,e,s,i){this.particles.push({x:t,y:e,z:s,ox:t,oy:e,oz:s,lock:i||0})},Model.prototype.createConstraint=function(t,e){this.constraints.push({f:t,s:e,dist:Math.sqrt(Math.pow(this.particles[t].x-this.particles[e].x,2)+Math.pow(this.particles[t].y-this.particles[e].y,2)+Math.pow(this.particles[t].z-this.particles[e].z,2))})},Model.prototype.createConstraintsBasedOnDistance=function(t){for(var e=0;ei&&this.createConstraint(e,s)}},Verlet3D.prototype.draw3D=function(t){if(t.constructor!==Array)var t=[t];this.ctx.clearRect(0,0,this.canvas.width,this.canvas.height),this.ctx.beginPath();for(var e=0;eo;o++){var a=1==o?s.vertex[s.constraints[i].f]:s.vertex[s.constraints[i].s],r=s.field_of_view/(s.field_of_view+a.z),n=a.x*r+s.xPos,l=a.y*r+s.yPos;0===o?this.ctx.moveTo(n,l):this.ctx.lineTo(n,l)}this.ctx.closePath(),this.ctx.stroke()},Verlet3D.prototype.findClosest=function(t){if(t.constructor!==Array)var t=[t];this.smallest=1e6,this.highestZ=-1e6;for(var e=0;el&&o.z>this.highestZ&&(this.highestZ=o.z,this.smallest=l,this.smallestparticle=i,this.model=e)}return void 0==this.smallestparticle?0:{particle:this.smallestparticle,model:this.model}},Verlet3D.prototype.handleMouse=function(t){if(t.constructor!==Array)var t=[t];if(this.mouse.down===!0){if(this.mouse.clicked=1,-1==this.draggedParticle){var e=this.findClosest(t);if(0===e)return;var s=t[e.model],i=t[e.model].vertex[e.particle]}else var s=t[this.draggedModel],i=t[this.draggedModel].vertex[this.draggedParticle];var o=s.field_of_view/(s.field_of_view+i.z),a=i.x*o+s.xPos,r=i.y*o+s.yPos;if(this.ctx.beginPath(),this.ctx.arc(a,r,5,5,0,2*Math.PI),this.ctx.closePath(),this.ctx.fill(),1==this.mouse.button&&(-1==this.draggedParticle&&(this.draggedParticle=e.particle,this.draggedModel=e.model),s.particles[this.draggedParticle].x+=25*(this.mouse.x-this.mouse.ox),s.particles[this.draggedParticle].y+=25*(this.mouse.y-this.mouse.oy)),3==this.mouse.button)for(var n=0;n model.tear_distance) { 179 | model.constraints.splice(c, 1); 180 | } 181 | } 182 | } 183 | }; 184 | 185 | var Model = function(settings) { 186 | this.vertex = []; 187 | this.particles = []; 188 | this.constraints = []; 189 | this.xPos = settings.xPos || 0; 190 | this.yPos = settings.yPos || 0; 191 | this.angleX = 0; 192 | this.angleY = 0; 193 | this.angleZ = 0; 194 | 195 | this.scale = settings.scale || 1; 196 | this.gravity = settings.gravity || 0.2; 197 | this.friction = settings.friction || 0.99; 198 | this.iterations = settings.iterations || 5; 199 | this.tear_distance = settings.tearDistance || 120; 200 | this.field_of_view = settings.fov || 1500; 201 | }; 202 | 203 | Model.prototype.createParticle = function(vx, vy, vz, lockstate) { 204 | this.particles.push({ 205 | x: vx, 206 | y: vy, 207 | z: vz, 208 | ox: vx, 209 | oy: vy, 210 | oz: vz, 211 | lock: (lockstate || 0) 212 | }); 213 | }; 214 | 215 | Model.prototype.createConstraint = function(first, second) { 216 | this.constraints.push({ 217 | f: first, 218 | s: second, 219 | dist: Math.sqrt( 220 | Math.pow(this.particles[first].x - this.particles[second].x, 2) + 221 | Math.pow(this.particles[first].y - this.particles[second].y, 2) + 222 | Math.pow(this.particles[first].z - this.particles[second].z, 2)) 223 | }); 224 | }; 225 | 226 | Model.prototype.createConstraintsBasedOnDistance = function(distance) { 227 | for (var i = 0; i < this.particles.length; i++) { 228 | 229 | for (var c = i + 1; c < this.particles.length; c++) { 230 | 231 | var dist = Math.sqrt( 232 | Math.pow(this.particles[i].x - this.particles[c].x, 2) + 233 | Math.pow(this.particles[i].y - this.particles[c].y, 2) + 234 | Math.pow(this.particles[i].z - this.particles[c].z, 2)); 235 | 236 | 237 | if (dist < distance) { 238 | this.createConstraint(i, c); 239 | } 240 | 241 | } 242 | } 243 | }; 244 | 245 | Verlet3D.prototype.draw3D = function(dmodels) { 246 | if (dmodels.constructor !== Array) var dmodels = [dmodels]; 247 | this.ctx.clearRect(0, 0, this.canvas.width, this.canvas.height); 248 | this.ctx.beginPath(); 249 | 250 | for (var modeln = 0; modeln < dmodels.length; modeln++) { 251 | var model = dmodels[modeln]; 252 | for (var i = 0; i < model.constraints.length; i++) { 253 | 254 | for (var c = 0; c < 2; c++) { 255 | //calculate fov 256 | var data = (c == 1) ? model.vertex[model.constraints[i].f] : model.vertex[model.constraints[i].s]; 257 | 258 | var fov = model.field_of_view / (model.field_of_view + data.z); 259 | 260 | var x = data.x * fov + model.xPos; 261 | var y = data.y * fov + model.yPos; 262 | 263 | if (c === 0) { 264 | this.ctx.moveTo(x, y); 265 | } else { 266 | this.ctx.lineTo(x, y); 267 | } 268 | } 269 | } 270 | } 271 | this.ctx.closePath(); 272 | this.ctx.stroke(); 273 | }; 274 | 275 | 276 | Verlet3D.prototype.findClosest = function(fmodels) { 277 | if (fmodels.constructor !== Array) var fmodels = [fmodels]; 278 | this.smallest = 1000000; 279 | this.highestZ = -1000000; 280 | for (var modeln = 0; modeln < fmodels.length; modeln++) { 281 | 282 | var modeldata = fmodels[modeln]; 283 | 284 | for (var i = 0; i < fmodels[modeln].vertex.length; i++) { 285 | 286 | var data = modeldata.vertex[i]; 287 | 288 | var fov = modeldata.field_of_view / (modeldata.field_of_view + data.z); 289 | 290 | var x = data.x * fov + modeldata.xPos; 291 | var y = data.y * fov + modeldata.yPos; 292 | 293 | var dist = Math.sqrt( 294 | Math.pow(x - (this.mouse.x), 2) + 295 | Math.pow(y - (this.mouse.y), 2)); 296 | if (dist < this.smallest && dist < 10 && data.z > this.highestZ) { 297 | this.highestZ = data.z; 298 | this.smallest = dist; 299 | this.smallestparticle = i; 300 | this.model = modeln; 301 | } 302 | 303 | } 304 | } 305 | if (this.smallestparticle == undefined) return 0; 306 | return { 307 | particle: this.smallestparticle, 308 | model: this.model 309 | }; 310 | }; 311 | 312 | Verlet3D.prototype.handleMouse = function(mmodels) { 313 | if (mmodels.constructor !== Array) var mmodels = [mmodels]; 314 | if (this.mouse.down === true) { 315 | this.mouse.clicked = 1; 316 | if (this.draggedParticle == -1) { 317 | var data = this.findClosest(mmodels); 318 | if (data === 0) return; 319 | var model = mmodels[data.model]; 320 | var pdata = mmodels[data.model].vertex[data.particle]; 321 | } else { 322 | var model = mmodels[this.draggedModel]; 323 | var pdata = mmodels[this.draggedModel].vertex[this.draggedParticle]; 324 | } 325 | var fov = model.field_of_view / (model.field_of_view + pdata.z); 326 | var x = pdata.x * fov + model.xPos; 327 | var y = pdata.y * fov + model.yPos; 328 | this.ctx.beginPath(); 329 | this.ctx.arc(x, y, 5, 5, 0, Math.PI * 2); 330 | this.ctx.closePath(); 331 | this.ctx.fill(); 332 | 333 | if (this.mouse.button == 1) { 334 | if (this.draggedParticle == -1) { 335 | this.draggedParticle = data.particle; 336 | this.draggedModel = data.model; 337 | } 338 | model.particles[this.draggedParticle].x += (this.mouse.x - this.mouse.ox) * 25; 339 | model.particles[this.draggedParticle].y += (this.mouse.y - this.mouse.oy) * 25; 340 | } 341 | 342 | if (this.mouse.button == 3) { 343 | for (var c = 0; c < model.constraints.length; c++) { 344 | if (model.constraints[c].f == data.particle || model.constraints[c].s == data.particle) { 345 | model.constraints.splice(c, 1); 346 | } 347 | } 348 | } 349 | 350 | if (this.mouse.button == 2) { 351 | model.angleX += (this.mouse.y - this.mouse.oy) / 25; 352 | model.angleY += (this.mouse.x - this.mouse.ox) / 25; 353 | } 354 | } else { 355 | var data = this.findClosest(mmodels); 356 | if (data === 0) return; 357 | var model = mmodels[data.model]; 358 | var pdata = mmodels[data.model].vertex[data.particle]; 359 | var fov = model.field_of_view / (model.field_of_view + pdata.z); 360 | var x = pdata.x * fov + model.xPos; 361 | var y = pdata.y * fov + model.yPos; 362 | this.ctx.beginPath(); 363 | this.ctx.arc(x, y, 5, 5, 0, Math.PI * 2); 364 | this.ctx.closePath(); 365 | this.ctx.stroke(); 366 | } 367 | }; --------------------------------------------------------------------------------