/* GearToy v 1.1 by Dan Efran Code and example gear data are Copyright (c) 2006 Dan Efran. Please learn from this source code, but please don't use it in your own work without my permission. Thanks. created in SWiSHmax (a Flash-compatible animation editor) and coded in SWiSHscript (an approximation of Flash ActionScript) This Flash movie simulates a system of gears. You can move them around by dragging them with the mouse. Gears can be placed anywhere, not just on certain 'shafts'. When their toothed areas overlap, gears will drive one another. (The actual teeth need not mesh precisely.) The groovy green gears are self-powered; the rest are naturally idle. The demo gear configuration includes clock hands that move correctly relative to one another, though not in real time. The color-coded '+' buttons create new gears of various types. The '-' button deletes all gears in the current system. Single gears can be deleted by dragging them to the trash icon. Note: this code assumes the existence of three sprites already on the stage. The first two should be correctly placed; the third will be hidden when the code runs and later positioned by code: trashcan_mc - the icon for deleting individual gears by dragging reset_mc - the button to delete all gears wedgeWarn_mc - will be placed at the overlap of two jammed gears */ // We keep a list of gear records and a parallel list of sprites. // gear records are plain Objects, but are assumed to have // certain fields: /* public var xx:Number; public var yy:Number; public var rings:Array; // array of radii, sorted largest to smallest. // maximum 16 rings per gear, so they can be called 0x0-0xF internally. public var v:Number; // velocity (signed, in clockwise RPM, set by code) public var p:Number; // power, 0 for most gears public var style:Number; // 0 = plain public var toothColr:Number; // 0xRRGGBB public var shadowColr:Number; // darker version for outlines, shadows, etc. public var hiliteColr:Number; // lighter version for kicks, details, etc. public var theta:Number; // current rotation angle // used for animation, based on v and initial theta public var placed:Boolean; // if false, being moved by user */ // (simulate multiple constructors, for questionable convenience) // // construct an empty gear record function GearCtor0() : Object { var o:Object = new Object; o.xx=0; o.yy=0; o.rings=new Array; o.v=0; o.p=0; o.style=0; o.toothColr=0; o.shadowColr=0; o.hiliteColr=0; o.theta=0; o.placed=true; return o; } // // construct a gear with parameters function GearCtor(xx:Number, yy:Number, firstRing:Number, pp:Number, sty:Number, toothCol:Number, shadowCol:Number, hiliteCol:Number) { var o:Object = new Object; o.xx = xx; o.yy = yy; o.rings = new Array(); o.rings.push(firstRing); // add any further rings after construction o.p = pp; o.style = sty; o.toothColr = toothCol; o.shadowColr = shadowCol; o.hiliteColr = hiliteCol; o.v = 0; o.theta = 0; o.placed = true; return o; } // // for simplicity, tooth-rings beyond the first // are added after construction function addRing(thisg:Object, rr:Number) { thisg.rings.push(rr); } // // // remove all rings from a gear function clearRings(thisg:Object) { while (thisg.rings.length > 0) { thisg.rings.pop(); } } // // we'll keep a set of prototype gears // for the '+' buttons to clone from. function copySettingsFromGear(thisg:Object, gg:Object) { thisg.xx = gg.xx; thisg.yy = gg.yy; thisg.rings = new Array(); var ii:Number; for (ii=0;ii=0;ii--) { if (theClips[ii]==mc) { theGears.splice(ii,1); theClips[ii].removeSprite(); theClips.splice(ii,1); recalcDepths(); return; } } } // // set up some example gears as prototypes function createPrototypeGears() { // power gear thePrototypeGears.push(GearCtor(140, 70, 30, 10, 1, 0x339900, 0x003333, 0x99FF00)); // basic two-ring gear thePrototypeGears.push(GearCtor(45, 115, 60, 0, 0, 0x9900FF, 0x660099, 0x9966FF)); addRing(thePrototypeGears[thePrototypeGears.length-1],20); // clock driver two-ring gear thePrototypeGears.push(GearCtor(200, 120, 36, 0, 0, 0x0066FF, 0x003399, 0x0099FF)); addRing(thePrototypeGears[thePrototypeGears.length-1],6); // hour hand wheel thePrototypeGears.push(GearCtor(200, 200, 60, 0, 2, 0x999999, 0x000000, 0x666666)); // minute hand wheel thePrototypeGears.push(GearCtor(200, 200, 30, 0, 3, 0xCCCCCC, 0x000000, 0x999999)); // harlequin gears thePrototypeGears.push(GearCtor(300, 200, 50, 0, 4, 0x333399, 0x99AAFF, 0x3366CC)); thePrototypeGears.push(GearCtor(390, 200, 25, 0, 4, 0x9900FF, 0x330099, 0x6600AA)); // half-clear three-ring gear thePrototypeGears.push(GearCtor(435, 180, 50, 0, 5, 0x333399, 0x6699CC, 0x99AAFF)); addRing(thePrototypeGears[thePrototypeGears.length-1],30); addRing(thePrototypeGears[thePrototypeGears.length-1],10); } // // set up an example gear system based on the prototypes function createStarterGears() { addGearFromPrototype(140, 70, 0); addGearFromPrototype(45, 115, 1); addGearFromPrototype(200, 120, 2); addGearFromPrototype(200, 200, 3); addGearFromPrototype(200, 200, 4); addGearFromPrototype(300, 200, 5); addGearFromPrototype(480, 70, 5); addGearFromPrototype(390, 200, 6); addGearFromPrototype(436, 180, 7); } // // create a '+' button for each prototype gear function createAddButtons() { var ii:Number; for (ii=0; ii < thePrototypeGears.length; ii++) { var pb:MovieClip=null; pb = createEmptyMovieClip(String.fromCharCode(65+ii), maxZ+40+ii); pb.onRelease = function() { // slight random displacement so you can see more than one appear. var jitterx:Number = Math.random() * 20 - 10; var jittery:Number = Math.random() * 20 - 10; _root.addGearFromPrototype(jitterx + this._x, jittery + this._y - 50, this._name.charCodeAt(0) - 65); }; drawAddButton(pb, ii); } } // // render a '+' button into pb based on prototype number ii function drawAddButton( pb:MovieClip, ii:Number) { var g:Object= thePrototypeGears[ii]; var ss:Number = 4; var ll:Number = 18; var xx:Number = (ii+0.5)*(ll*2+ss*4); var yy:Number = 365; pb._x = xx; pb._y = yy; xx=yy=0; pb.beginFill( g.shadowColr ); drawCircle(pb, xx,yy, ll+ss); pb.endFill(); pb.beginFill( g.hiliteColr ); pb.moveTo(xx-ss, yy-ss); pb.lineTo(xx-ss, yy-ll); pb.lineTo(xx+ss, yy-ll); pb.lineTo(xx+ss, yy-ss); pb.lineTo(xx+ll, yy-ss); pb.lineTo(xx+ll, yy+ss); pb.lineTo(xx+ss, yy+ss); pb.lineTo(xx+ss, yy+ll); pb.lineTo(xx-ss, yy+ll); pb.lineTo(xx-ss, yy+ss); pb.lineTo(xx-ll, yy+ss); pb.lineTo(xx-ll, yy-ss); pb.endFill(); } // // add a gear to the system based on a prototype gear function addGearFromPrototype(ix:Number, iy:Number, ip:Number) { if (theGears.length > 100) { trace("Okay, that's really enough gears."); // no real error handling here - I hope // nobody's really that bored. return; } var g:Object = GearCtor0(); copySettingsFromGear(g,thePrototypeGears[ip]); theGears.push(g); var ii:Number = theGears.length; theGears[ii-1].xx = ix; theGears[ii-1].yy = iy; theClips.push(this.createEmptyMovieClip("g_"+(nextZ++)+"_mc", ii)); drawGear(theClips[ii-1], theGears[ii-1]); } // // render gear g into sprite mc // a few simple visual styles are supported; it would be nice // to have more styles function drawGear(mc:MovieClip, g:Object) { mc.onPress = function() { this.startDragUnlocked(); _root.place(mc, false); if (wedged) { // hide wedged warning while user is working on it _root.wedgeWarn_mc._visible=false; } }; mc.onRelease = function() { var dt:String = mc._droptarget; this.stopDrag(); if (dt == _root.trashcan_mc._target) { _root.trashMC(mc); } else { _root.place(mc, true); } }; var ii:Number; if (g.style == 0) { // // very plain // mc.beginFill(g.shadowColr, 100); drawCircle(mc, 0, 0, g.rings[0]); mc.endFill(); // var rl = g.rings.length; for (ii=0; ii= 0; --ii) { if (theClips[ii] == mc) { theGears[ii].placed = down; if (down) { // depth is ok } else { var g:Object = theGears[ii]; theClips.splice(ii,1); theGears.splice(ii,1); theClips.push(mc); theGears.push(g); recalcDepths(); } return; } } } // // make each sprite's z-depth match its position in our list function recalcDepths() { var jj:Number; for (jj=theClips.length-1;jj>=0;--jj){ theClips[jj].swapDepths(jj); } } // // do gears ga and gb touch (that is, some rings mesh)? // return value is 0 if they don't touch. If they do, // it's a 32-bit integer 0x00iijj01, where ii and jj are the // contacting ring numbers for ga and gb respectively, 0x0-0xF each. // (This fn imposes a 16-ring limit on gears due to this result-packing) function gearsTouch(ga:Gear, gb:Gear):Number { var ii:Number; var jj:Number; // center-to-center distance, squared var c2c2:Number = (gb.xx-ga.xx)*(gb.xx-ga.xx)+(gb.yy-ga.yy)*(gb.yy-ga.yy); for (ii=0; ii=min_dist2) { var result32 = (ii << 8) | (jj << 4) | 0x01; // bit 0 = hit. bits 11-8 = ii, 7-4 = jj return result32; } } } } return 0; } // // when there's a conflict between gear velocities, // we place a warning symbol at one of the problematic connections function drawWedgeEffect(gear_i:Number, ring_i:Number, gear_j:Number, ring_j:Number) { var aa:Number = theGears[gear_i].rings[ring_i] / (theGears[gear_i].rings[ring_i] + theGears[gear_j].rings[ring_j]); _root.wedgeWarn_mc._x = int( theGears[gear_j].xx * aa + theGears[gear_i].xx *(1-aa) ); _root.wedgeWarn_mc._y = int( theGears[gear_j].yy * aa + theGears[gear_i].yy *(1-aa) ); _root.wedgeWarn_mc._visible=true; } // // update velocities and rotate the gear sprites function animateGears() { recalculateGearConnections() ; var d_theta:Number; var ii:Number; for (ii=theGears.length-1; ii>=0; --ii) { d_theta = theGears[ii].v; theClips[ii]._rotation += d_theta; } } // // trace out the current velocity of all gears, // starting from the p of any power gears. function recalculateGearConnections() { var ii:Number; // reset all v's for (ii=theGears.length-1; ii>=0; --ii) { theGears[ii].xx = theClips[ii]._x; theGears[ii].yy = theClips[ii]._y; theGears[ii].v = theGears[ii].p; } wedged = false; // trace out connections, starting from powered gears for (ii=theGears.length-1; ii>=0; --ii) { if ((theGears[ii].p) ) { propagateConnectionsFrom(ii); if (wedged) { return; } } } // still here -> not wedged _root.wedgeWarn_mc._visible = false; } // // recursively transfer V to touching gears function propagateConnectionsFrom(ii:Number) { var touched:Array = new Array(); var jj:Number; var touch:Number; for (jj=theGears.length-1; jj>=0; --jj) { if ((jj != ii) ) { touch = gearsTouch(theGears[ii], theGears[jj]); if (touch) { // at which radii? var ri:Number = (touch & (0xF << 8)) >> 8; var rj:Number = (touch & (0xF << 4)) >> 4; var ra:Number = theGears[ii].rings[ri]; var rb:Number = theGears[jj].rings[rj]; var gear_ratio = -ra/rb; var diff:Number = Math.abs(theGears[jj].v-gear_ratio*theGears[ii].v); if ((theGears[jj].v<>0) && diff>epsilon) { wedged = true; drawWedgeEffect(ii, ri, jj, rj); for (jj=theGears.length; jj>=0; --jj) { theGears[jj].v = 0; } return; } else { if (theGears[jj].v == 0) { touched.push(jj); } theGears[jj].v = gear_ratio*theGears[ii].v; } } } } // recursion on those we touched (that were not already set - e.g. the one that set this one) for (jj=touched.length-1; jj>=0; --jj) { propagateConnectionsFrom(touched[jj]); if (wedged) { return; } } } // // set up trash can icon and clear-all button function createTrashcanEtc() { reset_mc.onRelease = function () { _root.clearAllGears(); }; trashcan_mc.swapDepths(maxZ + 10); } // // reset the whole system function clearAllGears() { var ii:Number; for (ii=theClips.length-1; ii >=0; ii--) { theClips[ii].removeSprite(); } theGears.splice(0,theGears.length); theClips.splice(0,theClips.length); } // // start of main execution path onLoad () { // first shut down the movie timeline, we don't use it stop(); // init z-depth limits var maxZ = 10000; // near the top of legal z-space var nextZ = 1; // really just used for unique sprite names // if abs(A-B) < epsilon, we pretend A==B. var epsilon:Number = 0.000001; // precalculated for drawing var TP8:Number = Math.tan(Math.PI/8); var SP4:Number = Math.sin(Math.PI/4); // set up wedged mark var wedged:Boolean = false; _root.wedgeWarn_mc._visible=false; _root.wedgeWarn_mc.swapDepths(maxZ+20); // a visual detail but also a key factor in gear meshing: // how thick is a ring of teeth? This value is added to a // ring's inner radius to make its outer radius. var toothRingThickness:Number = 10; // refresh rate - tune for performance var tickRate = 50; // e.g. 50 ms = 20 fps var ticksPerSecond = 1000/tickRate; // init the gear data var thePrototypeGears:Array = new Array(); var theGears:Array = new Array(); var theClips:Array = new Array(); createPrototypeGears(); createStarterGears(); // set up the GUI createAddButtons(); createTrashcanEtc(); // prime the pump recalculateGearConnections(); // start the main loop var intervalID:Number = setInterval(animateGears, tickRate); }