-----------------------------------------------------------------------------
-- '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.

launch project

artists' comments