autocam off
sync rate 60
sync on
hide mouse
 
global gParticleColour as DWORD
 
`------------------
` Config variables
 
` If set to 1 we'll use cubes, otherwise we'll use spheres
global gUseCubes = 0
 
` If set to 1 keep the particles rotating to add some movement to the static numbers
global gRotateParticles = 0
 
` Colour of the particles
gParticleColour = rgb(0, 196, 255)
 
` Fade the particles with distance from the numbers - NOTE: This will make all the particles white again for some reason
global gFadeParticles = 1
 
` Controls the scale of the numbers
global gCameraHeight# = 125.0
 
` End of config
`------------------
 
`set display mode 1600, 1200, 32
 
global gCurrentFreeObject = 0    ` Keep track of the object numbers we've used
 
global gTimePassed#     ` Can't assign an expression on the same line as a global variable definition
gTimePassed# = 1.0/60.0
 
global gRotSpeed# = 2.8    ` How fast the particles can turn towards their destination
global gMoveSpeed# = 35.0  ` How fast the particles can move towards their destination
global gRotSpeed_Seconds# = 12.0    ` How fast the particles used in the last digit can turn towards their destination
global gMoveSpeed_Seconds# = 150.0  ` How fast the particles used in the last digit can move towards their destination
 
global gSecondsSpeedScale# = 0.0     ` The seconds move faster, but we want to scale them so they don't jump
                                    ` into position as soon as the clock starts
 
global gTimeZPos# = -20.0   ` Z Coordinate for the numbers
global gSpareZOffset# = 0.0 ` Z Coordinate for the spare particles
 
` Simple 3D vertex type
type Vertex
   x as float
   y as float
   z as float
endtype
 
` Type for each number that will appear on the screen, there will be 8 of these, 6 numbers
` for hours, minutes and seconds and a couple of colons to separate them.
type Number
   Position as Vertex
   NumPositions as integer
   StartObject as integer
endtype
 
` Type for the particles
type Particle
   Position as Vertex      ` Position in 3D space
   Velocity as Vertex      ` How much we move per frame
   Rotation as Vertex      ` Which direction we're currently pointing
   RotationVelocity as Vertex ` How fast we're rotating if gRotateParticles is 1
   Destination as Vertex   ` Where we're heading in 3D space
   ObjectIndex as integer  ` The object index that we're controlling
   UsedInLastDigit as integer `Whether this particles needs to use different speed and rotation values
endtype
 
` There can only be a maximum of 94 particles in use (00:00:00), add a couple of spares for effect
#constant NumParticles 96
 
` 8 characters on screen "00:00:00"
#constant NumNumbers 8
 
` Create and initialise the numbers
dim Numbers(NumNumbers) as Number
 
` Set the positions of each of the numbers in 3D space
NumberStartX# = -65.0
NumberSpacing# = 17.0
current# = NumberStartX#
 
for i=1 to NumNumbers
   Number_Construct(i,current#,-10)
   current# = current# + NumberSpacing#
next i
 
`Can't have arrays inside types so we'll have to keep it external
dim Number_Positions(NumNumbers, 25) as Vertex
 
` Read the data statements containing the number pixel offsets into an array
dim NumberTemplates(11, 25) as integer
restore NumberData
for i=1 to 11
   for j = 1 to 25
      read NumberTemplates(i, j)
   next j
next i
 
` Create and initialise the particles
dim Particles(NumParticles) as Particle
for i=1 to NumParticles
   Particle_Construct(i)
next i
 
` Make a top down camera
position camera 0, 0, gCameraHeight#, 0
point camera 0, 0, 0, 0
 
color backdrop RGB(0,0,0)
 
` Store the minute count last frame so we can toggle the clock between the top and the bottom of the screen
LastMinute = -1
 
Pressed1LastFrame = 0
Pressed2LastFrame = 0
Pressed3LastFrame = 0
 
` ----------------------------------
` ----------------------------------
` Main loop
` ----------------------------------
` ----------------------------------
 
do
   ` Get the time as a string
   t$ = get time$()
 
   ` Run along the string character by character assigning the correct number template to each of our 8 numbers
   for i=1 to NumNumbers
      char$ = mid$(t$, i)
      if char$ = ":"
         Number_SetTemplate(i, 1)
      else
           index = val(char$)+2
           Number_SetTemplate(i, index)
      endif
   next i
 
   ` Move the clock on the screen every minute
   minute = val(mid$(t$,5))
 
   if minute <> LastMinute
      if gTimeZPos# = -20.0
         gTimeZPos# = 30.0    ` Clock at the top of the screen
         gSpareZOffset# = -50.0 ` Spare particles at the bottom
      else
         gTimeZPos# = -20.0      ` Clock at the bottom of the screen
         gSpareZOffset# = 0.0   ` Spare particles at the top
      endif
 
      ` Set the positions of the numbers
      for i=1 to NumNumbers
         Numbers(i).Position.z = gTimeZPos#
      next i
 
      ` Slow down the rate of the seconds particles so they don't jump to their new position immediately
      gSecondsSpeedScale# = 0.0
   endif
 
   LastMinute = minute
 
   ` Assign particles to the valid positions in the numbers
   current = 1;
 
   for i=1 to NumNumbers
      for j=1 to Numbers(i).NumPositions
          Particle_SetDestination(current, Number_Positions(i, j).x, Number_Positions(i, j).z)
 
          if i = 8
            Particles(current).UsedInLastDigit = 1
          else
             Particles(current).UsedInLastDigit = 0
          endif
 
          current = current + 1
       next j
    next i
 
   ` Send the rest of the particles off into the spare particles area
   for i=current to NumParticles
      Particle_SetDestination(i, Rnd(50)-25.0, Rnd(50)+gSpareZOffset#)
      Particles(i).UsedInLastDigit = 0
   next i
 
   ` Scale up the speed of the particles displaying the seconds over time
   if gSecondsSpeedScale# < 1.0
      gSecondsSpeedScale# = gSecondsSpeedScale# + gTimePassed#/4.0
      if gSecondsSpeedScale# > 1.0
         gSecondsSpeedScale# = 1.0
      endif
   endif
 
   ` Update all of the particles
   for i=1 to NumParticles
      Particle_Tick(i)
   next i
 
   ` Process the keys
 
   ` Key '1' toggle between cubes and spheres
   if Keystate(2) = 1
      if Pressed1LastFrame = 0
         gUseCubes = 1-gUseCubes
         Particle_ChangeObjects()
      endif
      Pressed1LastFrame = 1
   else
      Pressed1LastFrame = 0
   endif
 
   ` Key '2' toggles between rotating and not rotating
   if Keystate(3) = 1
      if Pressed2LastFrame = 0
         gRotateParticles = 1-gRotateParticles
      endif
      Pressed2LastFrame = 1
   else
      Pressed2LastFrame = 0
   endif
 
   ` Key '3' toggles between fading and not fading the particles
   if Keystate(4) = 1
      if Pressed3LastFrame = 0
         gFadeParticles = 1-gFadeParticles
         Particle_ResetFade()
      endif
      Pressed3LastFrame = 1
   else
      Pressed3LastFrame = 0
   endif
 
   ` PGUP to move the camera in
   if Keystate(201) = 1
      gCameraHeight# = gCameraHeight# - 50.0 * gTimePassed#
      position camera 0, 0, gCameraHeight#, 0
   endif
 
   ` PGDN to move the camera out
   if Keystate(209) = 1
      gCameraHeight# = gCameraHeight# + 50.0 * gTimePassed#
      position camera 0, 0, gCameraHeight#, 0
   endif
sync
loop
 
`----------------------
` End of Mainloop
`----------------------
 
` Initialisation function for the Number type
function Number_Construct(i as integer, x as float, z as float)
   Numbers(i).Position.x = x
   Numbers(i).Position.z = z
 
   Numbers(i).StartObject = GetFreeObject()
   gCurrentFreeObject = gCurrentFreeObject-1
endfunction
 
` Function to work out the 3D positions of the pixels of a number and store them in the Number_Positions array
function Number_SetTemplate(i as integer, number as integer)
   numberDotSpacing# = 3.0
   Numbers(i).NumPositions = 0
 
   j=1
   for z = 0 to 4
      for x = 0 to 4
         if NumberTemplates(number, j) = 1
            Number_Positions(i, Numbers(i).NumPositions+1).x = Numbers(i).Position.x + x*numberDotSpacing#
            Number_Positions(i, Numbers(i).NumPositions+1).z = Numbers(i).Position.z + -(z*numberDotSpacing#)
 
            Numbers(i).NumPositions = Numbers(i).NumPositions + 1
         endif
 
         j = j + 1
      next x
   next y
endfunction
 
` Initialisation function for the particles
function Particle_Construct(i as integer)
   Particles(i).ObjectIndex = GetFreeObject()
 
   if gUseCubes = 1
      Make Object Cube Particles(i).ObjectIndex, 2
   else
      Make Object Sphere Particles(i).ObjectIndex, 2.5
   endif
 
   Color Object Particles(i).ObjectIndex, gParticleColour
   Ghost Object On Particles(i).ObjectIndex
 
   Particles(i).Position.x = 0
   Particles(i).Position.z = 0
 
   Particles(i).Velocity.x = Particles(i).Velocity.z = 0
   Particles(i).Destination.x = Particles(i).Destination.z  = 0
   Particles(i).Rotation.x = Particles(i).Rotation.y = Particles(i).Rotation.z  = 0
 
   Particles(i).RotationVelocity.x = Rnd(40)+10
   Particles(i).RotationVelocity.z = Rnd(40)+10
endfunction
 
` Remake all of the objects
function Particle_ChangeObjects()
   for i=1 to NumParticles
      delete object Particles(i).ObjectIndex
      if gUseCubes = 1
         Make Object Cube Particles(i).ObjectIndex, 2
      else
         Make Object Sphere Particles(i).ObjectIndex, 2.5
      endif
 
      Color Object Particles(i).ObjectIndex, gParticleColour
      Ghost Object On Particles(i).ObjectIndex
   next i
endfunction
 
` Reset the fade values of all of the particles
function Particle_ResetFade()
   for i=1 to NumParticles
      Fade Object Particles(i).ObjectIndex, 100
      ` Need to reset the colour as the fade turns it off
      Color Object Particles(i).ObjectIndex, gParticleColour
   next i
endfunction
 
` Update function for the paritlces
function Particle_Tick(i as integer)
   arrivedDist# = 0.1   ` How far away from the destination we need to be before we say we're there
 
   diff as Vertex
   angle as float
   angleDiff as float
 
   ` Work out our distance from the destinayion
   diff.x = Particles(i).Destination.x - Particles(i).Position.x
   diff.z = Particles(i).Destination.z - Particles(i).Position.z
   dist#  = Sqrt(diff.x*diff.x + diff.z*diff.z)
 
   if gFadeParticles = 1
      ` Alter the alpha based on distance from destination
      diffZ# = abs(gTimeZPos# - Particles(i).Position.z)
 
      if diffZ# > 60.0
         Fade Object Particles(i).ObjectIndex, 40
      else
         if diffZ# < 10.0
            Fade Object Particles(i).ObjectIndex, 100
         else
            alpha# = (60.0-diffZ#) + 40.0
            Fade Object Particles(i).ObjectIndex, alpha#
         endif
      endif
   endif
 
   ` Clamp the distance between 0.01 and 10 so we can vary the approach speed within a sensible range
   if dist# < 0.01
      dist# = 0.01
   endif
 
   if dist# > 10.0
      dist# = 10.0
   endif
 
   if dist# > arrivedDist#
      ` Work out the angle we need to be facing to point at our destination
      angle = AtanFull(diff.x, diff.z)
      angle = WrapValue(angle)
 
      angleDiff = angle-Particles(i).Rotation.y
      angleDiff = WrapAngleForInterpolation(angleDiff)
 
      if Particles(i).UsedInLastDigit = 1
         ` Ease up to our top rotation speed over a few seconds
         tempRotSpeed# = (gRotSpeed_Seconds#-gRotSpeed#) * gSecondsSpeedScale# + gRotSpeed#
      else
         tempRotSpeed# = gRotSpeed#
      endif
 
      ` Rotate towards the destination
      Particles(i).Rotation.y = Particles(i).Rotation.y + angleDiff * gTimePassed# * tempRotSpeed#
      Particles(i).Rotation.y = WrapValue(Particles(i).Rotation.y)
 
      ` Work out how fast we want to be moving. The closer we are the slower we go
      tempMoveSpeed# = dist# / 10.0
 
      if Particles(i).UsedInLastDigit = 1
         ` Ease up to our top move speed over a few seconds
         interpPos# = (gMoveSpeed_Seconds#-gMoveSpeed#) * gSecondsSpeedScale# + gMoveSpeed#
         tempMoveSpeed# = tempMoveSpeed# * interpPos#
      else
         tempMoveSpeed# = tempMoveSpeed# * gMoveSpeed#
      endif
 
      ` Get a vector pointing in the same dierection as us and apply velocity
      ` in that direction
      pointing as Vertex
      rot# = -Particles(i).Rotation.y
 
      pointing.x = -sin(rot#)
      pointing.z = cos(rot#)
 
      Particles(i).Velocity.x = pointing.x * tempMoveSpeed#
      Particles(i).Velocity.z = pointing.z * tempMoveSpeed#
 
      Particles(i).Position.x = Particles(i).Position.x + (Particles(i).Velocity.x * gTimePassed#)
      Particles(i).Position.z = Particles(i).Position.z + (Particles(i).Velocity.z * gTimePassed#)
   endif
 
   ` Put the 3D object in the right place
   Position Object Particles(i).ObjectIndex, Particles(i).Position.x, 0, Particles(i).Position.z
 
   ` Rotate the particles if we've got it set on
   if gRotateParticles = 1
      Particles(i).Rotation.x = Particles(i).Rotation.x + Particles(i).RotationVelocity.x * gTimePassed#
      Particles(i).Rotation.x = WrapValue(Particles(i).Rotation.x)
      Particles(i).Rotation.z = Particles(i).Rotation.z + Particles(i).RotationVelocity.z * gTimePassed#
      Particles(i).Rotation.z = WrapValue(Particles(i).Rotation.z)
      Rotate Object Particles(i).ObjectIndex, Particles(i).Rotation.x, Particles(i).Rotation.y, Particles(i).Rotation.z
   endif
 
endfunction
 
` Function to set the destination coordinate of a particle
function Particle_SetDestination(i as integer, x as float, z as float)
   Particles(i).Destination.x = x
   Particles(i).Destination.z = z
endfunction
 
` Return the next free object number
function GetFreeObject()
   gCurrentFreeObject = gCurrentFreeObject + 1
endfunction gCurrentFreeObject
 
` Wrap an angle to fall in the range -180 to 180 degrees. Avoids having to deal with wrap around from 359->0
function WrapAngleForInterpolation(angle as float)
   sanityCheck = 0   ` This will stop us getting into an infinite loop if we get a bad angle from ATanFull
   while angle < -180.0
      angle = angle + 360.0
 
      sanityCheck = sanityCheck + 1
      if sanityCheck > 5
         exitfunction 0.0
      endif
   endwhile
 
   sanityCheck = 0
   while angle > 180.0
      angle = angle - 360.0
      sanityCheck = sanityCheck + 1
      if sanityCheck > 5
         exitfunction 0.0
      endif
   endwhile
endfunction angle
 
` Data for a 5x5 number only font
NumberData:
Data 0,0,0,0,0,  0,0,1,0,0,  0,0,0,0,0,  0,0,1,0,0,  0,0,0,0,0 ` :
Data 0,1,1,1,0,  1,0,0,1,1,  1,0,1,0,1,  1,1,0,0,1,  0,1,1,1,0 ` 0
Data 0,0,1,0,0,  0,1,1,0,0,  0,0,1,0,0,  0,0,1,0,0,  1,1,1,1,1 ` 1
Data 0,1,1,1,0,  1,0,0,0,1,  0,0,1,1,0,  0,1,0,0,0,  1,1,1,1,1 ` 2
Data 1,1,1,1,0,  0,0,0,0,1,  0,1,1,1,0,  0,0,0,0,1,  1,1,1,1,0 ` 3
Data 1,0,0,0,1,  1,0,0,0,1,  0,1,1,1,1,  0,0,0,0,1,  0,0,0,0,1 ` 4
Data 1,1,1,1,1,  1,0,0,0,0,  1,1,1,1,0,  0,0,0,0,1,  1,1,1,1,0 ` 5
Data 0,1,1,1,0,  1,0,0,0,0,  1,1,1,1,0,  1,0,0,0,1,  0,1,1,1,0 ` 6
Data 1,1,1,1,1,  0,0,0,0,1,  0,0,0,1,0,  0,0,0,1,0,  0,0,1,0,0 ` 7
Data 0,1,1,1,0,  1,0,0,0,1,  0,1,1,1,0,  1,0,0,0,1,  0,1,1,1,0 ` 8
Data 0,1,1,1,0,  1,0,0,0,1,  0,1,1,1,1,  0,0,0,0,1,  0,1,1,1,0 ` 9