Realbasic: Canvas Tutorial Lesson 6

Animation! Now that we have all our Comics, CDs and DVDs on the screen, it looks a little bit chaotic. What if we could make it so that when the user right clicks on e.g. a CD, all CDs group together in one fluent motion? Same for the Comics and DVDs?

Within our framework, this is possible and we’re going to build it now!

This is how we are going to do it. We will give every ABElement a WalkPath. A Walkpath is a path our Element has to follow from its original position to its destination. Every ‘Walk’ should take the same time, no matter the starting position of the ABElement. Let me show you in this illustration:

Note: To explain it each walkpath is only four steps (pink segments). We will of course program it with a lot more steps.

As you can see WalkPath A is much longer than WalkPath C. However, as it needs to take exactly the same time for both Elements to reach their destination, the ‘steps’ to take are different. So for every ABElement from type CD we will calculate its own step distance.

Don’t worry, It will get clearer once we dive into the code. You can skip to the video at the end of this article if you want to see it working.

We need a new class: ABVector. This class will hold a vector x,y to mark one step in the WalkPath.

X As Integer
Y as Integer

Sub Constructor(X as integer, Y as integer)
  me.X = X
  me.Y = Y
End Sub

In our ABElement, we’ll define our WalkPath, a table of ABVectors. We also need a function to add a Walk step and one to reset the complete walk. We make sure we clean up when our object is destroyed:

WalkPath(-1) As ABVector

Sub AddWalkPosition(X as integer, Y as integer)
  Dim tmpVector as new ABVector(X,Y)
  WalkPath.Append tmpVector
End Sub

Sub ResetWalkPath()
  redim WalkPath(-1)
End Sub

Sub CleanUp()
  gBuffer = nil
  pBuffer = nil
  Image = nil
  MyABCanvas = nil
  MyGlow = nil
  MyMask = nil
  ResetWalkPath ' new
End Sub

In the MouseDown of ABCanvas we’re going to catch our right mouse button. We bring the Element to the front and start the grouping animation in the GroupMyType() function:

Sub MouseDown(X as integer, Y as integer) as Boolean
  Dim tmpElem as ABElement
  tmpElem = ElementHit(X,Y)
  if tmpElem <> nil then
    if IsContextualClick then 'new
      ' right mouse button
      
      ' bring the found ABelement to the front
      BringToFront tmpElem
      
      ' group them together
      GroupMyType tmpElem.type, X, Y, 500
      
      return false
    else
      ' left mouse button
      
      mLastX = X
      mLastY = Y
      
      BringToFront tmpElem
      
      RefreshElement tmpElem
      
      DragElem = tmpElem
      Return true
    end if
  end if
  
  return MouseDown(X,Y)
End Sub

Now for the hard bit 🙂

I’ve added some comments to the code to explain what actually happens, but I’ll give you a little walk-through.

To get a smooth animation, we want to have 25 frames per second. Calculating the path while the animation runs will take time so we precalculate this path first. We calculate the number of Elements that are of the Type we clicked. In a real app, you should save this value and change it when elements are added or removed.

for a = UBound(MyElements) to 0 step -1
    tmpElem = MyElements(a)
    if tmpElem.Type = Type then
      Total = Total + 1
    end if
next

Next, we calculate the radius of the circle where we will group the elements. The size of the circle will depend on the number of elements. (fewer elements are grouped closes together).

radius = (Total - 1) * 15 + 1

We want our elements to be spread evenly around the center so we calculate the angle between two elements on our circle:

Dim PlusAngle as integer
PlusAngle = 360 / (Total + 1) ' +1 makes it more irrigular in some cases

Also, we calculate the number of steps needed for the given time. This way, all elements arrive at their destination at the same time. Note that the time is the time of the actual animation. The time to do the pre-calculation is not included.

Dim Frames as integer
Frames = FramesPerSec / 1000 * MilliSecs

Now, we are ready to calculate the Walkpath for each Element:

for a = UBound(MyElements) to 0 step -1
    tmpElem = MyElements(a)
    if tmpElem.Type = Type then
      ' reset our walk
      tmpElem.ResetWalkPath
      
      ' calculate our end point
      EndX = CenterX + radius * cos(angle*PI/180)
      EndY = CenterY + radius * sin(angle*PI/180)
      
      ' add our steps
      for b = 1 to Frames
        newX = tmpElem.x + (EndX - tmpElem.x)/Frames*b
        newY = tmpElem.y + (EndY - tmpElem.y)/Frames*b
        tmpElem.AddWalkPosition(newX, newY)
      next
      
      Angle = Angle + PlusAngle
      
      TempListOfElems.Append tmpElem
      
    end if
next

We can now draw all the frames in our animation. If we are working on a slower computer, we’ll skip frames:
c = Frames * ((NowTime-BeginTime)/EndTime)
The rest is almost the same as we’ve done in a previous tutorial, except now we get our position from our WalkPath. To make it as fast as possible, we don’t redraw every frame, but remember what needs to be redrawn with the Totalfull… variables.

While c < Frames 
    NowTime = Microseconds 
    
    ' get my frame
    c = Frames * ((NowTime-BeginTime)/EndTime) 
    
    if c <> oldc and c < Frames then
      ' initialize
      TotalfullLeft = me.Width
      TotalfullTop = me.Height
      TotalfullRight = 0
      TotalfullBottom = 0
      
      for a = 0 to UBound(TempListOfElems)
        tmpElem = TempListOfElems(a)
        
        ' we remember the old position
        oldX = tmpElem.X
        oldY = tmpElem.Y
        
        ' get the new position from the walkpath
        newX = tmpElem.WalkPath(c).x
        newY = tmpElem.WalkPath(c).y
        
        ' Find the union between the old and the new position
        fullLeft = Min(oldX,newX) - tmpElem.w \ 2 - Extra
        fullTop = Min(oldY,newY) - tmpElem.h \ 2 - Extra
        fullRight = Max(oldX,newX) - tmpElem.w \ 2 + tmpElem.w  + Extra * 2
        fullBottom = Max(oldY,newY) - tmpElem.h \ 2 + tmpElem.h  + Extra * 2
        
        TotalfullLeft = min(TotalfullLeft, fullLeft)
        Totalfulltop = min(TotalfullTop, fullTop)
        TotalfullRight = max(TotalfullRight, fullRight)
        TotalfullBottom = max(TotalfullBottom, fullBottom)
        
        ' set our new position
        tmpElem.X = newX
        tmpElem.Y = newY
      next
      
      ' redraw only what is needed
      DrawMe TotalfullLeft, TotalfullTop, TotalfullRight - TotalFullLeft, totalfullBottom - TotalfullTop
      oldc = c
    end if
Wend

A lot happended here so take your time to take it all in.

Here is the full listing of this function:

Sub GroupMyType(Type as integer, CenterX as integer, CenterY as integer, MilliSecs as integer)
  ' make the mouse cursor invisible
  self.MouseCursor = System.Cursors.InvisibleCursor
  
  Const PI=3.14159
  
  Dim tmpElem as ABElement
  Dim a as integer
  Dim b as integer
  
  Dim Angle as integer
  Dim newX as integer
  Dim newY as integer
  Dim Radius as integer
  Dim slope as integer
  
  Dim EndX as integer
  Dim EndY as integer
  
  Dim Total as integer
  Dim FramesPerSec as integer
  ' if your pc cannot handle 25 frames per second, lower this
  FramesPerSec = 25
  
  Dim TempListOfElems(-1) as ABElement
  
  ' some pre-calculations, in a real app you could save this value in a variable
  for a = UBound(MyElements) to 0 step -1
    tmpElem = MyElements(a)
    if tmpElem.Type = Type then
      Total = Total + 1
    end if
  next
  
  ' we make the radius dependant of the number of element in the circle. The more, the bigger
  radius = (Total - 1) * 15 + 1
  
  Dim PlusAngle as integer
  PlusAngle = 360 / (Total + 1) ' +1 makes it more irrigular in some cases
  
  ' Calcualte the number of steps needed for the given time so all elements arrive at their destination at the same time
  Dim Frames as integer
  Frames = FramesPerSec / 1000 * MilliSecs 
  
  ' build the walkpaths for all elements of this type
  for a = UBound(MyElements) to 0 step -1
    tmpElem = MyElements(a)
    if tmpElem.Type = Type then
      ' reset our walk
      tmpElem.ResetWalkPath
      
      ' calculate our end point
      EndX = CenterX + radius * cos(angle*PI/180)
      EndY = CenterY + radius * sin(angle*PI/180)
      
      ' add our steps
      for b = 1 to Frames
        newX = tmpElem.x + (EndX - tmpElem.x)/Frames*b
        newY = tmpElem.y + (EndY - tmpElem.y)/Frames*b
        tmpElem.AddWalkPosition(newX, newY)
      next
      
      Angle = Angle + PlusAngle
      
      TempListOfElems.Append tmpElem
      
    end if
  next
  
  Dim oldX, oldY, fullLeft, fullTop, fullRight, fullBottom as integer
  Dim TotalfullLeft, TotalfullTop, TotalfullRight, TotalfullBottom as integer
  Dim Extra as integer
    
  ' we add some extra pixels around the object when we redraw because otherwise we sometimes get a 'trail'
  Extra = 5
  
  ' start the animation
  Dim NowTime as Double
  Dim BeginTime as Double
  Dim EndTime as Double
  
  Dim c as integer
  Dim oldc as integer
  
  BeginTime = Microseconds
  EndTime = MilliSecs * 1000
  
  While c < Frames 
    NowTime = Microseconds 
    
    ' get my frame
    c = Frames * ((NowTime-BeginTime)/EndTime) 
    
    if c <> oldc and c < Frames then
      ' initialize
      TotalfullLeft = me.Width
      TotalfullTop = me.Height
      TotalfullRight = 0
      TotalfullBottom = 0
      
      for a = 0 to UBound(TempListOfElems)
        tmpElem = TempListOfElems(a)
        
        ' we remember the old position
        oldX = tmpElem.X
        oldY = tmpElem.Y
        
        ' get the new position
        newX = tmpElem.WalkPath(c).x
        newY = tmpElem.WalkPath(c).y
        
        ' Find the union between the old and the new position
        fullLeft = Min(oldX,newX) - tmpElem.w \ 2 - Extra
        fullTop = Min(oldY,newY) - tmpElem.h \ 2 - Extra
        fullRight = Max(oldX,newX) - tmpElem.w \ 2 + tmpElem.w  + Extra * 2
        fullBottom = Max(oldY,newY) - tmpElem.h \ 2 + tmpElem.h  + Extra * 2
        
        TotalfullLeft = min(TotalfullLeft, fullLeft)
        Totalfulltop = min(TotalfullTop, fullTop)
        TotalfullRight = max(TotalfullRight, fullRight)
        TotalfullBottom = max(TotalfullBottom, fullBottom)
        
        ' set our new position
        tmpElem.X = newX
        tmpElem.Y = newY
      next
      
      ' redraw only what is needed
      DrawMe TotalfullLeft, TotalfullTop, TotalfullRight - TotalFullLeft, totalfullBottom - TotalfullTop
      oldc = c
    end if
  Wend
  
  ' show the cursor
  self.MouseCursor = System.Cursors.StandardPointer
End Sub

And we’re done. Let’s run our program and enjoy our animated canvas!

The source code for this tutorial: http://www.gorgeousapps.com/Tut6.zip

In our next lesson, we’ll add a menu to each Element so we can play the CD/DVD or open the Comic.

Until next Time!

Click here to Donation if you like my work

Advertisements

About Alwaysbusy

My name is Alain Bailleul and I'm the Senior Software Architect/Engineer at One-Two. I like to experiment with new technologies, Computer Vision and A.I. My projects are programmed in B4X , Xojo, C#, java, HTML, CSS and JavaScript. View all posts by Alwaysbusy

18 responses to “Realbasic: Canvas Tutorial Lesson 6

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: