----------------------------------------------------------------------------- -- 'threesome ball' lingo script for the Threesome artwork -- © Antoine Schmitt 2003 -- -- The Threesome art piece features three balls embedded in -- a physical space (the screen) and that explore the realm -- of the attraction/repulsion/indifference relationships. -- Threesome is an abstraction of an aspect of human and -- animal intersubjectivity. -- -- 'Threesome' complies to the '3rd manifesto of the artwork on computer' -- (http://www.gratin.org/as/txts/3emebrouillon.html) -- -- -- -- Technical description : All 3 balls sprites have this same script attached. -- This is classical realtime setting : about 60 times per second (more than the human -- perception threshold), a new image is computed, giving the illusion of movement. -- The image is made of the 3 sprites (balls). -- Each one of them computes its own new position, and the resulting image is the drawing -- of these three sprites on the screen at their new position. -- -- All the numbers that define the behaviors and their dynamics result from a heuristic -- process : I tuned and finetuned them until I was pleased with the result. -- -- Every line that starts with "--" is a comment that is here to explain the program -- and is not executed by the computer. ----------------------------------------------------------------------------- -- The list of all balls, useful to get the position of the other balls -- and to send them messages global gBalls -- the properties of each ball property pSp, pX, pY, pVX, pVY, pLastTime, pW, pDmin2, pDmin, pRelationships, pNextRelChange, pFirst, pRepulsDist2 -- executed once at the beginning on beginSprite(me) -- the sprite and its position pSp = sprite(me.spriteNum) pX = float(pSp.locH) pY = float(pSp.locV) -- the internal values and states -- some are precomputed for efficiency. -- speed pVX = 0. pVY = 0. -- width and distances pW = float(pSp.width)/2. pDmin2 = pW*pW*4.*0.92 pDmin = sqrt(pDmin2) -- repulsion distance drepuls = 11.3 pRepulsDist2 = drepuls*drepuls*pW -- the relationships and the next time they will change pRelationships = VOID pNextRelChange = VOID -- the list of balls if voidp(gBalls) then gBalls =  gBalls.append(me) -- whether or not I am the first ball pFirst = (gBalls.count() = 1) -- time management : now pLastTime = the milliseconds end -- executed once at the end on endSprite(me) -- cleanup if not voidp(gBalls) then gBalls.deleteOne(me) end -- The 'prepareFrame' function is executed about 60 times -- per second, before the image is rendered to the screen. -- It pilots the movements of the ball, according to its internal -- states and to the position of the other balls. -- It defines the next position of the ball, just before it is drawn -- to the screen. on prepareFrame(me) -- time management : how much time was exactely elapsed since the last frame t = float(the milliseconds) dt = float(t - pLastTime) if dt < 1 then return pLastTime = t -- reduction factor : finetuning of the general dynamics dt = dt*0.7 -- modify relationships ? changeRelations(me, FALSE) -- Classical euler equations : -- integration of forces to acceleration to speed to positions -- First compute the forces -- brown force : small random movements brown = 0.0011 fBrownX = brown*float(random(101)-51)/50.0 fBrownY = brown*float(random(101)-51)/50.0 -- attractions and repulsions for other balls attract = 0.0003 fAttX = 0.0 fAttY = 0.0 -- for each ball repeat with i = 1 to count(gBalls) ball = gBalls[i] -- (but not myself) if ball = me then next repeat -- get the attraction force for this ball ballAttract = pRelationships[i] if ballAttract > 0 then -- I am attracted: -- a constant force toward the ball fAttX = fAttX + attract*ballAttract*sign(ball.pX - pX) fAttY = fAttY + attract*ballAttract*sign(ball.pY - pY) else if ballAttract < 0 then -- I am repulsed: -- only below a certain distance dx = ball.pX - pX dy = ball.pY - pY dd = dx*dx + dy*dy if dd < pRepulsDist2 then -- really repulsed -- a constant force away from the ball fAttX = fAttX + attract*ballAttract*sign(ball.pX - pX) fAttY = fAttY + attract*ballAttract*sign(ball.pY - pY) end if end if end repeat -- friction slows down the ball for more control of its movements friction = 0.002 -- euler integration to get the speed pVX = pVX*(1.0 - dt*friction) + dt*(fBrownX + fAttX) pVY = pVY*(1.0 - dt*friction) + dt*(fBrownY + fAttY) -- euler integration to get the new position pX = pX + dt*pVX pY = pY + dt*pVY -- manage collisions with the other balls -- this is not a 'natural' collision, but a simplified one -- the balls don't bounce on each other, they roll around each other repeat with ball in gBalls if ball = me then next repeat -- compute the distance to the other ball dx = ball.pX - pX dy = ball.pY - pY dd2 = dx*dx + dy*dy if dd2 < pDmin2 then -- less than the minimum distance : we collide dd = sqrt(dd2) if dd = 0.0 then -- very rare : we are at the same position : I move aside a little bit pX = pX + random(3) - 2 pY = pY + random(3) - 2 else -- normal case : I move back rat = (pDmin - dd)/dd pX = pX - dx*rat pY = pY - dy*rat end if end if end repeat -- manage borders -- classical bounce equations bounceCoef = 0.2 -- how much bounce rr = the stage.rect -- the screen rectangle if pX < pW then -- too much left -- we change the position, bouncing pX = 2.*pW - pX -- the speed is reduced by the bounce coeficient pVX = -pVX*bounceCoef else if pX > rr.width - pW then -- too much right pX = 2.*(rr.width - pW) - pX pVX = -pVX*bounceCoef end if if pY < pW then -- to high pY = 2.*pW - pY pVY = -pVY*bounceCoef else if pY > rr.height - pW then -- too low pY = 2.*(rr.height - pW) - pY pVY = -pVY*bounceCoef end if -- we really move the sprite now according to the new computed position pSp.locH = pX pSp.locV = pY end -- executed by the prepareFrame function -- or called by the first ball sometimes. -- This changes the relationships of the ball to the other balls -- if force is FALSE, it does it only if the time has come. -- if force is TRUE, it does it in any case on changeRelations(me, force) -- do we change them now ? doChange = force if voidp(pRelationships) then doChange = TRUE else if voidp(pNextRelChange) then doChange = TRUE else if the milliseconds > pNextRelChange then doChange = TRUE if not doChange then return -- no -- yes : change the relationships -- We choose at random from a predefined set of values (defined by the author) -- and we don't want twice the same value for 2 different balls pRelationships =  -- possible attraction/repulsion values attractions = [0.0, 1.0, -7.0, 0.0, 2.0, -3.0] -- for each ball repeat with ball in gBalls if ball = me then -- I am nor attracted nor repulsed by myself pRelationships.append(0.0) else -- choose an attraction/repulsion value at random rr = random(count(attractions)) -- remember it for processing in the prepareFrame function pRelationships.append(attractions[rr]) -- remove it from the available attraction/repulsion list so that -- we don't use it again for the other ball attractions.deleteAt(rr) end if end repeat -- If I am the first ball, I sometimes (one chance out of 5) force the other -- balls to change their relationships at the same time. This way, the global scenario -- may change radically from time to time. if pFirst then if random(5) = 1 then repeat with ball in gBalls if not (ball = me) then changeRelations(ball, TRUE) end if end repeat end if end if -- compute the next time I will change relationships -- at random bewteen a minimum and a maximum value mint = 10000 -- 10 seconds maxt = 50000 -- 50 seconds pNextRelChange = the milliseconds + mint + random(maxt - mint) end -- definition of the 'sign' function that is not defined in lingo on sign(x) if x = 0 then return 0 if x > 0 then return 1 return -1 end
Note: You can download Threesome here as uncompiled Director movie.