DrawingLayer Objects

PinballY lets you add your own custom graphics to any of its windows from Javascript using "drawing layers". A drawing layer is a virtual image, stored in memory, where you can load a video or image file, or draw your own complex graphics using the Custom Drawing facility.

We use the term "layer" because PinballY thinks about its graphics as a set of transparencies stacked one on top of the next. What you actually see on the screen is the result of combining all of the layers making up a window. In the main window, for example, the system's standard graphics include a background layer that displays a video or image of the current game's playfield, a separate layer that displays the wheel icons, another layer with the status text messages, and so on.

Event target

DrawingLayer objects are event targets for certain events, so you can use the standard event methods (on(), off(), addEventListener(), etc) to add and remove event listeners. See EventTarget.

DrawingTarget objects serve as the event target for the following event types:

How to create a drawing layer

You create a custom drawing layer in a given window by calling the createDrawingLayer() method on the window object. For example, this creates a new drawing layer in the DMD window:

let overlay = dmdWindow.createDrawingLayer(1000);

The argument specifies the desired Z index for the layer. See below for more on what the Z index means.

The return value from the method is a DrawingLayer object, which you can use to load image files or video files into the layer, or to draw more complex graphics using the Custom Drawing facility. The DrawingLayer object returned provides methods and properties, listed below, that let you control what it displays.

Removing a layer

When you're done with a layer, you can remove it by calling the removeDrawingLayer() method on the same window object that you used to create the layer.

dmdWindow.removeDrawingLayer(overlay);

Removing a layer removes its on-screen graphics and makes the Javascript object representing the layer invalid. Any attempts to load media into the layer or draw graphics into it will simply be ignored.

Temporarily hiding a layer

In some cases, you might want to create a layer that comes and goes, according to what's going on in the user interface. One way to do this is to create and remove the layer as needed. However, if the layer will be re-activated frequently, it's more efficient to keep the layer object around and simply remove its on-screen graphics. An easy and efficient way to do this is to set the layer to display an image that's completely transparent, which you can do with the clear() method:

overlay.clear(0x00000000);

This simply fills the whole layer with a fixed color with a zero "alpha" value. Zero alpha means complete transparency, so the layer will be invisible on-screen.

Z index

The order of the layers is important, because it determines which graphics are drawn in front of (and therefore block or occlude) other graphics. The order is deterministic, and entirely under your control, because it's determined by the "Z index" of each layer. You specify the desired Z index when you create a layer. The Z index is just an arbitrary integer value that's up to you to define, but the important thing about it is that layers with higher Z index values are drawn in front of layers with lower Z index values.

For most of the windows, there's only one "system" drawing layer, which is the main background layer where the background image or video or that window is drawn. This is the layer where the backglass image or video is drawn in the backglass window, for example. This standard system drawing layer has a Z index of 0 (zero). Any custom drawing layer you create with a positive Z index is drawn in front of this main background layer, and any layer you create with a negative Z index is drawn behind it. There's usually no point in using a negative Z index, since the default layer is usually opaque, and fills the entire window. However, it might be useful to create a negative Z layer for a custom window, since you could conceivably want to load transparent or partially transparent media, such as PNG files, into the main layer.

The main window is a little different from the others in that it has a whole stack of drawing layers of its own, which it uses to overlay the numerous elements of the main user interface. In the main window, you can insert your custom layers between system layers, by choosing Z index values between the Z indices of the standard layers. For example, you could place a custom layer immediately in front of or immediately behind the status line text. The order of layers for the main window is listed in Drawing layer ordering in the mainWindow object section.

Sizing and scaling

By default, the graphics in a custom drawing layer are stretched to exactly fill the entire window, in both width and height. If the user resizes the window, the system stretches the graphics to match the new window size. This is just the default, though; you can change it so that the image only occupies a portion of the window, and you can also make the image preserve a fixed aspect ratio, rather than distorting the geometry to fit the window size.

If you're familiar with basic Windows graphics programming, you probably think of the size of an image or text display in terms of pixels. PinballY takes a different, more abstract approach, based on scaling everything relative to the window size. This will be a little strange if you're used to working in terms of raw pixels, but it simplifies things by letting the system take care of resizing everything to maintain a constant scale when the window layout changes.

There are two key concepts to PinballY's approach to sizing. The first is the "aspect ratio" of the original graphics loaded into a layer. This is simply the ratio of the width to the height of the original image or video loaded into the layer. The second is the "span" of the layer, which is the fraction of the window's width or height that the layer occupies. A span of 1.0 means that the layer is stretched to exactly fill the window's width or height; a span of 0.5 means that it's scaled to 1/2 of the window's size.

The original aspect ratio is determined in terms of traditional pixel sizes. When you load an image or video from a file, the aspect ratio is determined based on the native size of the image stored in the file. For example, if you load a PNG file with a pixel size of 640 pixels wide by 480 pixels tall, it has an aspect ratio of 640/480 or 1.333.

The "span" is something you can control for each layer, using the the setScale() method on the layer. You pass this method an object with properties that specify the constraints on how the layer is sized. There are several ways to set the span, with different results when the window is resized:

How scaling interacts with custom drawing

Things get weird when we start talking about custom drawing, because now we have to start thinking in terms of pixels in addition to the constraint-based scaling. The custom drawing system is based on plain old pixels, like PNG files or conventional Windows graphics programming. How do we reconcile the pixel-based drawing with the span-based scaling?

It's actually not that difficult if you think about the right way. Think about the custom drawing system as working on a "canvas" - basically a virtual PNG file that you're creating on the fly in memory. Like a PNG file, the canvas has a width and height in pixels. You can plop text onto the canvas at any pixel position, and you can draw graphics primitives and copy other images into pixel areas on the canvas.

Whatever you do with the canvas, when you're done, you have a rectangular image that's X pixels by Y pixels in size. This is just like a PNG file that's X pixels by Y pixels in size, except that it only exists in memory, not as a disk file. We now load this X-by-Y-pixel image into a drawing layer. It's only at this point that the more abstract window-based scaling kicks in. PinballY now takes that X-by-Y-pixel image and stretches it to fill the desired fraction of the window's width and height, according to the "span" values in your drawing layer's scale settings. Whenever the window size or layout changes, PinballY rescales the image, going back to the same X-by-Y-pixel original, and stretching it to match the new window layout.

This dual sizing scheme - pixel sizing for the underlying image, window percentages for the final display - gives you a lot of flexibility. A really nice feature is that it lets you create custom drawings at a fixed pixel size, without having to worry about how large or small the image will be when actually displayed. That lets you arrange the drawing with the exact proportions you want, based on pre-determined sizes for the elements within the drawing, such as text and external images you draw onto the canvas. The finished image will then be displayed at whatever size is needed in the UI, thanks to the window-based scaling.

Methods and properties

layer.alpha: Gets or sets the layer's alpha transparency value. This must be a value from 0 (fully transparent) to 1 (fully opaque). Values between 0 and 1 represent partial transparency; 0.25 represents 25% opacity, for example. This controls the transparency of the whole layer; it's combined with the transparency of each individual pixel. You can use this to perform effects such as fading the layer in or out.

layer.clear(argb): Clears any video or graphics displayed in the layer, and fills the entire layer with the given background color. This is specified as a hex value in the format 0xAARRGGBB, where AA is the alpha value, RR is the red component, GG is the green component, and BB is the blue component. Each component ranges from 00 to FF. An alpha value of 00 means fully transparent, and FF means fully opaque. Here are some examples of how you can use this:

You can alternatively specify the color as a string in HTML "#RGB" or "#RRGGBB" format. With these string formats, the alpha is implicitly FF for fully opaque. You can also simply specify the name of a configuration variable in the settings file, in which case it'll be parsed as an HTML color value, and the result used as the color.

layer.draw(func, width, height): Clears any prior video or graphics displayed in the launch overlay, and calls the provided function to perform custom drawing. The function is called with a "drawing context" object as its parameter. See Custom Drawing for details on how this works.

Your custom graphics are drawn into an in-memory image with the given width and height in pixels. If you don't specify these arguments, the window's current size in pixels is used by default. Remember that the pixel size doesn't determine the actual display size of the graphics. The actual display size is determined by the scaling settings made via setScale(). The pixel sizing is important, however, because it specifies the size of the "canvas" that you're working with when laying out text and other graphics in your custom drawing function. All of those low-level drawing operations work in terms of pixels. The finished result will be automatically scaled up or down to match the current window size, according to the scaling settings you specify in setScale().

layer.drawDMDText(text, options): Draws a DMD-style text screen. This generates a still image that looks like a score-area text display, and then loads the image into the drawing layer. (Even though this uses the DMD display style, you can use it in any window to create a DMD-like text effect in that window.) This is effectively an image-loader function, so like the other media loader functions (loadImage(), loadVideo(), etc), it replaces any previous image that the layer was displaying.

text is the text to display. This can contain multiple lines, by using "\n" (newline) characters to separate the lines. options is an object with more information on exactly how to generate the simulated DMD image. If you omit the object entirely, or provide the object but omit any of the individual properties listed below, the system chooses suitable defaults based on the currently selected game. The option properties are:

layer.loadImage(filename): Loads the given image file and displays it in the launch overlay layer. The filename must be a fully qualified Windows path, with drive, directory, and extension. The normal collection of image file formats can be used (JPEG, PNG). The image is stretched to fill the window. You can use the draw() method if you need more control over the image layout, or if you want to use multiple images or mix images with text or other graphics. The new image replaces any prior video that was playing, any prior image, or any custom graphics displayed via draw().

layer.loadVideo(filename, options): Loads and plays the given video in the launch overlay layer. The filename must be a fully qualified path, with drive, directory, and extension. The usual video formats are supported (MP4, MPG, etc). The video is stretched to fill the window. If any prior video was playing or any prior graphics were displayed, this removes the existing video or graphics.

options is an object specifying playback options. If this is omitted, defaults are used for all options; if the object is specified, the defaults are used for any missing properties. The properties are:

Animated GIF files are a special case for this method, because PinballY technically treats them as images rather than videos (which it has to do because libvlc, the codec layer that we use for other videos, doesn't work properly with GIF animation). Because PinballY treats all GIFs as images, even animated ones, you can load an animated GIF through the loadImage() method. But you can also use this method, since it has special handling to detect GIFs and load them as images. So GIFs will load properly with either method. But there is one situation where you might want to use this method explicitly: if you want to specify the play or loop options, you can do that with this method, but not with loadImage(). (The volume and mute options are ignored, though, since GIFs inherently can't have audio tracks.)

mute: This is a read/write boolean property that gets or sets the current muting status for video playback in the layer. True means that the video's audio track is muted. If no video is loaded, this always reads as false, and setting it to a new value has no effect. Animated GIFs don't have audio tracks, so they have the same behavior as if no video is loaded.

layer.pause(): If a video or animated GIF is playing, pauses playback at the current frame. This has no effect for a still image (including custom-drawn graphics and background color fill).

layer.play(): If a video or animated GIF is loaded, and playback is paused, this resumes playback. This has no effect for a still image.

layer.setPos(x, y, align): Sets the layer's position in the window. align determines the alignment reference point. This is a string containing a combination of "top", "middle", or "bottom" and "left", "center", or "right". For example, "top right" sets the alignment point to the top right of the window. If the whole string is omitted, or either the top/middle/bottom or left/center/right parts are missing, center alignment is the default.

In all cases, the alignment specifies the starting point for the image. Once the image has been set to that starting point, it's then moved by the x and y distances from that point.

Once the initial position is determined from the alignment, the image is then moved from that point by the x and y values. These are both on scales from -0.5 to +0.5, representing the edges of the window in that dimension, with 0.0 representing the center of the window.

This coordinate system is probably a bit different from what you're used to if you've done any pixel-oriented programming. One big difference is that the "zero" point on both axes is the center of the window, not the upper left corner. Another is that the vertical axis is positive in the upwards direction. Most graphics programming has the Y axis upside-down, so that row 0 is at the top and increasing Y values represent positions further down on the screen. The way to keep this new coordinate system straight is to think about it like a mathematical graph layout - picture doing a plot of a parabola in an algebra class, say.

As with the sizing and scaling system, the coordinates for the position system are relative to the window size. This might seem confusing at first, but like the scaling system, this makes the layout pretty much automatic once you get the hang of it. For example, X position 0.25 is always halfway between the center of the window and the right edge, no matter what size the window is. That makes it fairly easy to keep an image aligned with a reference point, regardless of the current window size.

layer.setScale(options): Sets the scaling rules for the layer, which determine how the layer's graphics are sized relative to the window. Whenever the window is resized, the layer is resized according to the scale rules, so that the graphics are always sized proportionally to the window. options is an object with any combination of the following properties:

There are several possibilities for how to combine these constraints, described in "Sizing and scaling" above.

The scale can be set before or after loading media or drawing custom graphics. When loading media or drawing, the scale settings currently in effect are applied, but you can change to different scaling rules later while keeping the current contents.

volume: This is a read/write integer property that gets or sets the current volume level for video playback in the layer, from 0 (silent) to 100 (full volume). If no video is loaded, this always reads as 100, and setting it to a new value has no effect. Animated GIFs don't have audio tracks, so they have the same behavior as if no video is loaded.