Creating Custom BrightScript UIs with Image Canvas

To round out our introductory coverage of BrightScript custom user interface development, we turn our attention to the image canvas component. Although not technically part of the 2D API that we have discussed in the last three posts, image canvas is related in that it provides another way for developers to implement a high degree of customization in their channels. Common uses for the image canvas are custom splash screens, service activation screens (like the Activate or Browse content screens in some channels,) custom popup style menus, and custom video loading screens.

I’ve provided a simple sample channel with this article which can be downloaded here. The sample lets you navigate around a set of 5 billiard balls (I liked the graphics from the Collisions post so much I had to reuse them!) As you right or left arrow between the balls, the selected one is highlighted with a ring image as shown below. Pressing the OK button then displays a second image canvas that implements a custom dialog. We will get to that a bit later.

The roImageCanvas Component

A BrightScript image canvas is simply a surface for rendering arbitrary graphics and text as part of a channel.  It is implemented by the roImageCanvas component.  roImageCanvas is in some ways similar to roScreen of 2D API fame.  However, whereas roScreen can be used for basic 2D drawing, as well as hosting compositors for sprite animations and the like, roImageCanvas is generally used for content that is static.  An image canvas can be assigned a message port so that it can respond to events such as remote key presses.  Creating an image canvas looks just like creating a more customary BrightScript screen type:

canvas = CreateObject("roImageCanvas")
port = CreateObject("roMessagePort")
canvas.SetMessagePort(port)

‘Add content to the canvas
canvas.Show()
…
while true
    event = wait(0, port)
    if (event <> invalid)
        ‘Handle events directed at the canvas…
    endif
end while

Image canvas gets more interesting when it comes to adding content.  Elements such as text and images are added to an image canvas as a set of z-ordered layers.  Each layer can contain an arbitrary number of items.  Each item is defined by an associated array with elements for the text string or url of the image to render, the target rectangle specifying where on the canvas to render the item, text fonts, and the like.  More details on the kinds of information that can be specified in image canvas items can be found in the component metadata documentation at this link.

As an example, to render the text string “Hello Image Canvas!” and an image located at pkg:/images/icon.png on a canvas, your BrightScript code can define an array of items like this:

items = []
items.Push({
    url: "pkg:/images/icon.png"
    TargetRect: {x: 600, y: 40, w: 128, h: 128}
})

items.Push({
    Text: “Hello Image Canvas!”
    TextAttrs: { font: "large", color: "#a0a0a0" }
    TargetRect: {x: 200, y: 75, w: 300, h: 200}
})

The canvas then adds these items as a layer with the SetLayer() function:

Void SetLayer(int zOrder, roAssociativeArray contentMetaData)
Void SetLayer(int zOrder, roArray contentList)

There are two version of this function, allowing you to add one item to a layer at a time (version 1) or numerous items with one call (the second version.)  The first argument specifies the z-order of the layer on the canvas.  The content is then rendered to the screen by calling Show().

In addition to creating layers that contain the canvas content, it is common to set the layer at z-order 0 to control the canvas background, opacity, and composition mode of the background.  So, a complete example would look like this:

canvas = CreateObject("roImageCanvas")    
port = CreateObject("roMessagePort")
canvas.SetMessagePort(port)
items = []
items.Push({
    url: "pkg:/images/icon.png"
    TargetRect: {x: 600, y: 40, w: 128, h: 128}
})
items.Push({
    Text: “Hello Image Canvas!”
    TextAttrs: { font: "large", color: "#a0a0a0" }
    TargetRect: {x: 200, y: 75, w: 300, h: 200}
})
canvas.SetLayer(0, { Color: "#ff000000", CompositionMode: "Source" })
canvas.SetLayer(1, items)
canvas.Show()
while true
    event = wait(0, port)
    if (event  invalid)
       ‘Handle events directed at the canvas…         
    endif
end while

The call that sets layer 0 tells the canvas to render a fully opaque black background on which item pixels completely replace destination pixels.  Alternatively, the CompositionMode can be set to Source_Over, in which case the color alpha value is used to alpha blend the source and destination pixels.  We’ll go into this in more detail a bit later when we add a custom image canvas based dialog to the channel.

By default images are rendered as soon as they are downloaded and available in memory.  Thus, for a mixture of large and small images, you might see some images load before others, in orders that you may not expect.  This behavior can be overridden by calling the image canvas SetRequireAllImagesToDraw() function:

Void SetRequireAllImagesToDraw (Boolean requireAllImages)

When set to true, all images must be downloaded (or loaded from the package if local,) decoded, and loaded into memory before drawing.  The tradeoff here is that performance is impacted when setting this to true.

Using Multiple Image Canvases

BrightScript channels can use more than one image canvas to implement a variety of features and effects. Let’s add a custom message dialog box like the one shown below in the sample channel to see how this is done. In the sample channel, this message dialog appears when you press the remote’s OK button. The array of balls in the channel is 0-index based. Thus the screen shot below shows ball[2] as selected.


Basically all we do is create another image canvas while the first one is displayed. The second image canvas gets populated with the specific graphical elements by creating an associative array of the text and image items, and then assigning them to a layer by calling SetLayer(). Most of the rest of the code defines the target rectangles for the dialog border image and the message text.

Function onOK(selectedIndex as integer) as integer    
    canvas = CreateObject("roImageCanvas")
    port = CreateObject("roMessagePort")
    canvas.SetMessagePort(port)
    canvasRect = canvas.GetCanvasRect()
    dlgRect = {x: 0, y: 0, w: 600, h: 300}
    txtRect = {}
    txt = "Ball #" + stri(selectedIndex) + " Selected"
    fontRegistry = CreateObject("roFontRegistry")
    font = fontRegistry.GetDefaultFont()
    txtRect.w = font.GetOneLineWidth(txt, canvasRect.w)
    txtRect.h = font.GetOneLineHeight()
    txtRect.x = int((canvasRect.w - txtRect.w) / 2)
    txtRect.y = int((canvasRect.h - txtRect.h) / 2)
    dlgRect.x = int((canvasRect.w - dlgRect.w) / 2)
    dlgRect.y = int((canvasRect.h - dlgRect.h) / 2)    
    items = []
    items.Push({
        url: "pkg:/assets/dialog.png"
        TargetRect: dlgRect
    })
    items.Push({
        Text: txt
        TextAttrs: { font: "large", color: "#a0a0a0" }
        TargetRect: txtRect
    })
    canvas.SetLayer(0, { Color: "#a0000000", CompositionMode: "Source_Over" })
    canvas.SetLayer(1, items)
    canvas.Show()
    ….
End Function

Perhaps the most interesting part of the rendering of this second image canvas is the assignment of the z-order 0 layer.  The composition mode used here is Source_Over.  This means that the source pixels, i.e., those in the new canvas, are alpha blended with the destination pixels in the first image canvas that we are drawing on top of.  With the Source_Over composition mode, the alpha value in the color value becomes important.  Specifically, we use a value of #a0000000.  A0 is the alpha value, defining the level of transparency.  The RGB values in the 3rd through 8th digits specify the background color of the new image canvas.  In this case we get the nice visual effect of the screen below the dialog appearing inactive while the dialog is displayed.

The best way to really understand the nuances of using the image canvas is to experiment with some code.  Download the sample channel and dig in.  For example, in the code above change the first SetLayer() call to the following to get an alpha blended green background on the screen when the dialog is displayed:

canvas.SetLayer(0, { Color: "#4000ff00", CompositionMode: "Source_Over" })

The possibilities are endless.  The roImageCanvas component includes more functionality than I could cover here, but this article should get you started using image canvas in your channels.  Visit this link for a complete view of what the image canvas can do.  See what you can come up with, and feel free to let me know how it goes.

About Robert Burdick

Roku Developer Support Manager
This entry was posted in Uncategorized. Bookmark the permalink.
  • Klimov

    code-stuffed.net

  • Geebs

    Seems the TargetRotate attribute for an “item” rotates the entire canvas layer. I was under the impression that it rotates only the *target* item. In my case i just wanted my rectangle (TargetRect) to rotate.
    Am i missing something?
    Great tut. Got me up and learning brightscript fast.