//*** This library is copyright 2004 Gavin Kistner, gavin@refinery.com //*** It is covered under the license viewable at http://phrogz.net/SVG/libraries/_ReuseLicense.txt //*** Reuse or modification is free provided you abide by the terms of that license. //*** (Including the first two lines above in your source code mostly satisfies the conditions.) /******************************************************************************* Springz, a re-usable library to implement spring-like behaviors in SVG. v1.0 20040614 Initial Release v1.0.1 20040626 Collisions push in both directions; some speed improvements Feature Overview ================================================================================= - The strength of each Spring may be varied. - The library performs (simple) collision prevention, if desired. - The desired length of a Spring may be non-zero, pushing/pulling objects to achieve a desired separation distance. - Objects can be set to have 'mass', which effects how far each object moves when the Spring pulls on it. (Light objects are pushed/pulled more easily than massive ones.) (Note: this does not impart inertia to the movement.) - Springs are graphically depicted as lines on screen, placed below all other elements. - Objects can be temporarily prevented from moving (by returning false from their .moveTo() method, and/or each Spring may be temporarily inactivated. - The overall strength of Springs can be scaled using Springz.scale Requirements Overview ================================================================================= - You need to create global svgDoc and svgRoot variables. - Each object used on the ends of a Spring needs to have: a .loc property (which is an object with .x and .y properties) a .moveTo() method (which takes x and y parameters) a .collisionRadius property (if using noCollisions) a .mass property (if using Springz.useMass) Quick Start Example ================================================================================= ... function Init(evt){ svgDoc = evt.target.ownerDocument; svgRoot = svgDoc.documentElement; } ... var s = new Spring( obj1, obj2 ); s.apply(); Creating a New Spring ================================================================================= Springz.set( obj1, obj2 [,strength [,distance]] ) new Spring( obj1, obj2 [,strength [,distance]] ) Springs can be created using either new Spring(...) or Springz.set(...); in either case, the parameters are the same: - obj1, obj2 The objects on either end of the string. As noted above, these objects must respond to .loc and .moveTo(x,y); they also must respond to .collisionRadius (for collision detection) and .mass (if using Springz.useMass) - strength (optional) The numeric strength of the spring. The stronger a spring, the harder it pulls on the objects attached to it. (All strengths are scaled by the global Stringz.strengthScale property.) If omitted, an initial strength of 1 will be used for new Springs. - distance (optional) The ideal distance between the objects attached to the spring. If the objects are farther than this number, they will be pulled towards each other; if they are closer than this number, they will push apart. If omitted, an initial distance of 0 will be used for new Springs. Springz.set(...) will find the Spring corresponding to the two objects supplied (if it exists) and return it. If no Spring exists between them, it will create a new Spring and set the values. (If you do not supply strength and/or distance to Springz.set AND a Spring already exists between the options, its strength and distance will not be altered.) The lines for all Springs will be placed inside a element with id="springGroup"; if this group does not exist, it will be created and placed at the start of the document (causing the lines to draw underneath everything else). The .moveTo() Method and .loc Property ================================================================================ As noted above, each object on the spring ends MUST have a .moveTo(x,y) method and a .loc property. .moveTo(x,y) is only responsible for moving the object itself; the library will take care of moving the Spring end(s) for the object when applying the Spring. If you don't want the spring ends to move, return false (not null or undefined) from a call to .moveTo() and they won't be moved. If you move the object yourself and want to get the Spring lines to move to the correct location, call Springz.moveSpringsFor(myObj) The .loc property required on every object must contain an object with .x and .y properties, used to determine the location of the object. You must keep this information up-to-date yourself; for example, your .moveTo() method might look like: MyClass.prototype.moveTo=function(x,y){ this.loc.x=x; this.loc.y=y; this.circle.setAttribute('cx',x); this.circle.setAttribute('cy',y); } Modifying a Spring ================================================================================ mySpring.setStrength( newStrength ) ----------------------------------- Sets the spring strength, and sets the class on the spring line object to 'spring strengthN' where N is the strength. For example, a line corresponding to a Spring with strength 3 can be styled via the CSS selector: #springGroup line.strength3 { stroke:red; stroke-width:4 } mySpring.setDistance( newDistance ) ----------------------------------- Set the ideal distance the Spring is attempting to achieve. mySpring.hide() mySpring.show() ----------------------------------- Modify the line depicting the Spring: hide() removes it from the document show() re-adds it to the document Springs' lines are initially shown. Hiding the line improves performance. mySpring.inactivate() mySpring.reactive() ----------------------------------- Cause a spring to not take effect until re-activated. Calls to mySpring.apply() will return false, and the Spring will be skipped in calls to Springz.applyAll(); mySpring.removeThyself() ----------------------------------- Permanently remove a Spring. Using 'Mass' for the Objects ================================================================================ You can give your objects 'mass', so that some objects appear heavy. For example, if you have two objects attached by a Spring, and one has a mass of 300 while the other has a mass of 100, the heavy object will move 1/3 the distance that the light object does. To use mass: a) Set Springz.useMass=true; b) Make sure that every object attached by a Spring has a .mass property. Locking an Object ================================================================================ To make the object at one end of the Spring not move at all, you can either give it a REALLY enormous mass compared to other objects (see above), or you can cause its moveTo() method to return false. Using just the latter will cause objects at the other end of the Spring to move more slowly than if you set the mass to an enormous value; using both techniques at once may desirable. Animating the Springs ================================================================================ mySpring.apply( [noCollisions] ); //Apply the force of one Spring one time Springz.applyAll( [noCollisions] ); //Apply the force of every Spring once To cause your created Springs to actually DO something, call one of the methods listed above. In both cases, the single parameter is the same: - noCollisions Prevent the objects from drawing over top of one another (or attempt to). For collision prevention to work, each object on the Spring ends must have a .collisionRadius property. If you want to perform collision prevention without actually applying any Springs (you just want objects to stop overlapping) you can call: Springz.preventCollisions(); To let the Springs fully animate, simply call Springz.applyAll() repeatedly using setTimeout(); for example: function Animate(){ Springz.applyAll(true); if (keepAnimating) setTimeout('Animate()',100); } *******************************************************************************/ var Mathsqrt = Math.sqrt; // speeds things up 10-15% var pccall=0; Springz = { strengthScale:0.5, useMass:false, all:[],objs:[],lookup:{},byID:{}, applyAll : function(noCollisions){ var t0,t1,t2; noCollisions = !!noCollisions; //DebugOut(this+'.applyAll('+noCollisions+')',2); //t0 = new Date(); for (var i=0,spring,len=this.all.length;i0;i--) for (var j=i-1;j>=0;j--){ var o1 = Springz.objs[i]; var o2 = Springz.objs[j]; var loc1 = o1.loc; var loc2 = o2.loc; var dx = loc2.x-loc1.x; var dy = loc2.y-loc1.y; var combinedRadius = o1.collisionRadius+o2.collisionRadius; var negCombined = -combinedRadius; if (dx>combinedRadius || dxcombinedRadius || dy=0;i--){ var spring = springs[i]; if (!spring.showing) continue; var n = spring.o1==o?1:2; spring.g.setAttribute('x'+n,x); spring.g.setAttribute('y'+n,y); } }, toString:function(){ return '[Springz]' } } Spring.set=function(o1,o2,strength,distance){ //*** Find the spring between two elements; //*** if none exists, create a new Spring and return that. //DebugOut('Spring.set('+o1+','+o2+','+strength+','+distance+')',4); var spring; if (spring=Spring.find(o1,o2)){ if (strength!==undefined) spring.setStrength(strength); if (strength!==distance) spring.setDistance(distance); return spring; } return new Spring(o1,o2,strength,distance); } Spring.find=function(o1,o2){ //*** Find the spring between two elements; //*** if none exists, return null //DebugOut('Spring.find('+o1+','+o2+')',4); if (o1.uniqueSpringID!=null && o2.uniqueSpringID!=null){ var i1 = o1.uniqueSpringIDo2.uniqueSpringID?o1.uniqueSpringID:o2.uniqueSpringID; return Springz.lookup[i1] && Springz.lookup[i1][i2]; } return null; } function Spring(o1,o2,strength,distance){ //*** Create a new Spring between two objects //DebugOut('new Spring('+o1+','+o2+','+strength+','+distance+')',4); var oldSpring,springGroup,tmp; var svgNS = 'http://www.w3.org/2000/svg'; if (o1.uniqueSpringID==null) o1.uniqueSpringID = Springz.objs.push(o1)-1; if (o2.uniqueSpringID==null) o2.uniqueSpringID = Springz.objs.push(o2)-1; this.o1 = o1.uniqueSpringIDo2.uniqueSpringID?o1:o2; this.strength=0,this.showing=true,this.distance=0; var i1 = o1.uniqueSpringID; var i2 = o2.uniqueSpringID; if (!(tmp=Springz.lookup[i1])) tmp=Springz.lookup[i1]={}; if (oldSpring=tmp[i2]) oldSpring.removeThyself(); tmp[i2]=this; if (!(tmp=Springz.byID[i1])) tmp=Springz.byID[i1]=[]; tmp.push(this); if (!(tmp=Springz.byID[i2])) tmp=Springz.byID[i2]=[]; tmp.push(this); if (!(springGroup=svgDoc.getElementById('springGroup'))) (springGroup=svgRoot.insertBefore(svgDoc.createElementNS(svgNS,'g'),svgRoot.firstChild)).setAttribute('id','springGroup'); this.g=springGroup.appendChild(svgDoc.createElementNS(svgNS,'line')); this.g.setAttribute('class','spring'); this.g.setAttribute('x1',this.o1.loc.x); this.g.setAttribute('y1',this.o1.loc.y); this.g.setAttribute('x2',this.o2.loc.x); this.g.setAttribute('y2',this.o2.loc.y); this.setStrength(strength || 1); this.setDistance(distance || 0); Springz.all.push(this); //DebugOut('new '+this,2); } Spring.prototype.setStrength=function(strength){ this.strength=strength; if (this.showing) this.g.setAttribute('class','spring strength'+strength.toString().replace(/\W/g,'-')); } Spring.prototype.setDistance=function(distance){ this.distance=distance; } Spring.prototype.hide=function(){ this.showing=false; this.g.parentNode.removeChild(this.g); this.setStrength(this.strength); } Spring.prototype.show=function(){ this.showing=true; svgDoc.getElementById('springGroup').appendChild(this.g); } Spring.prototype.apply=function(noCollisions){ //DebugOut(this+'.apply('+noCollisions+')',4); if (this.inactive) return false; var l1 = this.o1.loc; var l2 = this.o2.loc; var dx = l2.x-l1.x; var dy = l2.y-l1.y; var minDistance = this.distance; if (noCollisions){ var combinedRadius = this.o1.collisionRadius+this.o2.collisionRadius; if (minDistance