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

First published 07/06/2005