A Basic Slider Script

1. Designing The Slider Object

The slider object is designed to draw a list of images into another image and 'slide' the list of images along. The first step is to sketch out the main methods this object will use to do its thing. The two main methods of this object will be, naturally enough, "loadImages" and "SlideImages":

on LoadImages (me, aListOfMembers)
  -- create a list of images to display
end

on Slide (me, shiftAmt)
  -- Shifts the display left or right by the specified amount. 
  -- and draw the visible portion of the map onto the 
end

In order to interact with this object, we are going to create a method which we can use to determine which tile is clicked. We are going to create another method to 'hilight' a selected tile.

on GetTileAtPoint (me, pntOnCanvas)
  -- determine which tile is at the specified point  
end

on SelectTile (me, whichTileID)
  -- show the specified has been 'selected'
end

To keep this object portable and generic, we are going to pass in the output image - and the rect to use on this image - as parameters to a generic 'start' method:

on Initialise (me, aCanvas, aRectOnCanvas)
  -- initialise the object, specifying the image to draw to
  -- and the rect on that image to use
end

Most imaging-lingo code at the Lingoworkshop uses an Initialise method like this. It helps keep our imaging object portable - it doesn't care whether the canvas is the stage, the image of a sprite's cast member - or an image object controlled by some other object.

You might notice that there are no methods in this object to "grab and drag" the strip, or "slide it more quickly as you move further away from the centre" - this object will simply provide a method for sliding the thumbnails a specified amount. This one simple method provides a sufficient enough interface to the Slider Object to enable us to create a whole range of behaviours - click and drag, auto-scroll, follow the mouse and so on. If we can disentangle the application specific behaviour (the way the object interacts with the particular application it is used in) and put that code in a separate script, then the SliderObject remains portable and can be easily used in different applications.

2. Fleshing out the script

So far, we have identified the 5 main methods of the object. Now it is time to flesh out the methods in more detail.

Initialising the Slider

To initialise the Slider, we need to specify the output canvas and the rect on that canvas. We can also include an optional list of 'settings' which over-ride the default settings for things like border thickness and colours.

This object keeps a reference to the output canvas, and then it makes a 'buffer' image the same size as the output rect. The various thumbnail images are copied to this buffer image, and then after all the thumbnails, borders and hilights have been painted to the buffer, the buffer is then painted to the output image.

There are two main reasons for using a buffer image rather than painting the thumbnails directly to the output. Firstly, it is usually quicker to manipulate images 'off-screen' (so director doesn't need to update the stage as theimage is manipulated); secondly, we can paint on to the buffer without having to worry about masking parts of the thumbnails that run over the edges of the buffer (the buffer image is of a fixed size, therefore any thumbnails that do not fit on it are effectively cropped)

on Initialise (me, aCanvas, aRectOnCanvas, settings)
  
  myCanvas = aCanvas
  myDestRect = aRectOnCanvas
  
  me._LoadSettings(settings)
  
  myWidth = myDestRect.width
  myHeight = myDestRect.height
  myBuffer = image(myWidth, myHeight, 24)
  myBuffer.fill(myBuffer.rect, rgb(0,0,0))
  myMapH= 0
  myMap = []
  
  -- create a pixel image (used for hilighting etc)
  myPixel = image(1,1,1)
  myPixel.setPixel(0,0,1)
  
end 

on _LoadSettings (me, settings)

  -- firstly, set the defaults
  mySettings = [:]
  mySettings[#backColour] = rgb("#f1f1f1")
  mySettings[#borderColour] = rgb("#000000")
  mySettings[#BorderSize] = 1
  
  -- now, over-ride them 
  if settings.ilk = #PropList then 
    mx = settings.count
    repeat with i = 1 to mx
      thisProp = settings.getPropAt(i)
      thisValue = settings.getAt(i)
      mySettings.setAProp(thisProp, thisValue)
    end repeat
  end if
  
end

The LoadImages method

This method accepts a list of cast members that are the source images. Assuming that these images are different sizes and dimensions, we are going to resize them to fit on the output image. If the source image is too tall, we will scale it down. If it is not as tall as the output image, we will centre it vertically.

In this version, we are going to create new images for each thumbnail (if there many source images, it might be better to create the thumbnails 'on-the-fly' as they are need since it would be memory intensive to create and keep in memory all the thumbnails). We are also going to keep some additional information about the tile - such as the name of the source member, its position in the list and its selected state.

on LoadImages (me, aListOfMembers)
  
  myMap = []
  myTileRects = []
  mx = aListOfMembers.count
  
  repeat with i = 1 to mx
    m = aListOfMembers[i]
    if m.ilk = #member then 
      if m.type = #bitmap then
        img = m.image
        if img.height > myHeight then 
          -- need to scale the image down
          originalRect = img.rect
          thescale = float(myHeight)/img.height
          scaledRect = rect(0, 0, integer(img.width*thescale), myHeight)
        else if img.height < myHeight then 
          -- need to centre the rect vertically
          vOff = (myHeight-img.height)/2
          scaledRect = img.rect.offset(0, vOff)
        else
          -- perfect fit!
          scaledRect = img.rect
        end if
        -- create the small version of the image
        tileImg = image(scaledRect.width, myHeight, 24)
        tileImg.fill(tileImg.rect, mySettings.backColour)
        tileImg.copyPixels(img, scaledRect, img.rect)
        if mySettings.BorderSize <> 0 then 
          tileImg.draw(tileImg.rect, [#ShapeType: \
                  #Rect, #LineSize:mySettings.BorderSize, 
                  #Color: mySettings.borderColour])
        end if
        -- store all the information we have about this tile
        tile = [\
                #Image: tileImg, \
                #ID: m.name, \
                #DRect: tileImg.rect, \
                #Indx: i, \
                #Selected: 0\
                ]
        -- add it to the map
        myMap.append(tile)
      end if
    end if
  end repeat
  
end 

The Slide Method

The slide method takes a parameter which is how many pixels to slide the images along. This is added to 'myMapH' which tracks the total amount the images have been slid.

When we slide the images to the left (by moving the 'MapH' position to the right), this method checks whether the left most tile has been pushed 'off' and if so, moves that tile from the start of the list and adds it to the end. Similarly, when sliding the images to the right, we check whether the right most tile has been pushed off, and if so, move it back to the start of the list.

on Slide (me, shiftAmt)
  -- Shifts the display left or right by the specified amount. 
  -- Specify a positive amount to slide right
  
  -- first check that there are some images to slide
  if myMap.count < 1 then exit -- no images

  -- now adjust the current mapH
  myMapH = myMapH + shiftAmt

  if shiftAmt > 0 then 
    -- check whether we have moved past the left most tile
    -- if so, move it from the start of the list and
    -- add it to the end
    r = myMap[1][#DRect]
    w = r.width
    if myMapH >= w then 
      aTile = myMap[1]
      myMap.deleteAt(1)
      myMap.append(aTile)
      myMapH = myMapH - w
    end if
  else if shiftAmt < 0 then 
    -- check whether we have moved past the right most tile
    -- if it is, move it from the end to the start of the list
    if myMapH <= 0 then 
      lastPos = myMap.count
      aTile = myMap[lastPos]
      myMap.deleteAt(lastPos)
      myMap.addAt(1, aTile)
      myMapH = myMapH + aTile[#DRect].width
    end if
  end if

  -- now draw the visible portion of the map onto the buffer image
  rectLeft = -myMapH
  buffer = myBuffer.duplicate()
  repeat with i = 1 to myMap.count
    img = myMap[i][#Image]
    destRect = myMap[i][#DRect].offset(rectLeft, 0)
    buffer.copyPixels(img, destRect, img.rect)
    if myMap[i][#selected] then 
      buffer.copyPixels(myPixel, destRect, myPixel.rect, [#BlendLevel: 100])
    end if
    rectLeft = rectLeft + destRect.width
    if rectLeft > myWidth then exit repeat
  end repeat
  myCanvas.copyPixels(buffer, myDestRect, buffer.rect)
  
end

Methods for getting a tile at a point

Because the tiles may be of different widths, this method loop through the list of tiles and checks whether the supplied point is inside the rect of each tile.

on GetTileAtPoint (me, pntOnCanvas)
  
  rectLeft = -myMapH
  repeat with i = 1 to myMap.Count
    aRect = myMap[i][#DRect].offset(rectLeft, 0)
    if inside(pntOnCanvas, aRect) then 
      thisTile = myMap[i][#ID]
      return thisTile
    end if
    rectLeft = rectLeft + aRect.width
    if rectLeft > myWidth then exit repeat
  end repeat  
  
end

Selecting a Tile

The SelectTile method accepts a tileID and then searches the list of tiles, toggling the tile's Selected property on. If the second multiSelect parameter is not true, all other tiles have their Selected property set to 0.

on SelectTile (me, whichTileID, multiSelect)
  
  repeat with i = 1 to myMap.count
    if myMap[i][#ID] = whichTileID then myMap[i][#Selected] = 1
    else if NOT(multiSelect) then myMap[i][#Selected] = 0
  end repeat
  
end

Downloads
Source Movie is available here

Last updated 29th of June, 2005

© 2006 MeccaMedialight. Site Powered by Wrangler 8.