View Single Post
Old 05-22-2016, 04:35 PM   #61
Human being with feelings
Lokasenna's Avatar
Join Date: Sep 2008
Location: Calgary, AB, Canada
Posts: 6,251

12. Using images

Today we're going to learn how to work with images:

Dude. It's just like the first time I saw someone playing a Super Nintendo. Pants were crapped that day, let me tell you.

First off, I guess we're going to need some images. If you're like me and couldn't draw a picture of the broad side of a barn even if you had a photo to trace over, allow me to direct you here:


This is a really handy little app that makes the process of drawing knobs, sliders, and buttons much easier. There's a bit of a learning curve, though.

More Knobman resources: (ignore the bits about Reaktor)

Many people find Photoshop easier for making buttons, as well.

To help you get started, I recommend grabbing Leslie Sanford's Knobman files. You just download the file you want, open it in Knobman, and export it with the appropriate number of frames. Alternatively, I've already converted a few of his files to .png form for this lesson's example, so you could just use those if you wanted.

Speaking of which, this lesson is NOT going to use the LS GUI library we've been working with previously. Because images are such a different beast, I've rewritten a portion of the library specifically for today's examples. Just download the .zip below, unzip it your scripts folder, and run the "images usage demo" script.


Reaper uses a very common technique to draw images, known as blitting. Blitting is, basically, telling a program to copy/paste a particular section of one image into another.

Images for blitting are typically laid out with all of their frames in one big row or column - in this case, I've opted for columns. If you open knob.png in the lesson files, for instance, you'll see that contains 64 frames of a knob slowly turning around.

Accessing these in our script is relatively easy:

gfx.loadimg(buffer, file)

"I know what a file is, Lokasenna, but what the hell is a buffer?"

Easy. Buffers are a bunch of off-screen places for us to store our images. We can manipulate them too, rotating, scaling, drawing new stuff on them, but for this lesson we're just going to put each image in its own buffer and leave it that way.

In the images library demo script, you'll see a new table with all of our file names in it:
GUI.images = {
	bg = "\\tutorial images\\bg.png",
	btn = "\\tutorial images\\button.png",
	knb = "\\tutorial images\\knob.png",
	sldr = "\\tutorial images\\slider.png",
	opt = "\\tutorial images\\opt.png",
	lbl = "\\tutorial images\\label.png",
The \\s are necessary because Lua has a few special commands that are written as \n, \s, etc. \\ tells Lua that no, this really is supposed to be a \.

-- Load all our images into buffers
local function load_images()
	local info = debug.getinfo(1,'S')
	script_path = info.source:match[[^@?(.*[\/])[^\/]-$]]
	local idx = 0
	for key, file in pairs(GUI.images) do
		retval = gfx.loadimg(idx, script_path..file)
		GUI.images[key] = idx
		idx = idx + 1

GUI.load_images = load_images
- First we use the same functions for loading a library from a user script, in this case to find out where the script is being run from.

- Next we loop through each item in the table above. The script path and file name are combined and loaded into sequentially-numbered buffers.

- Since we don't need the file names anymore, each entry in the image table is instead used to save its buffer number.

Once images are loaded, another new function handles drawing them:

gfx.blit(source, scale, rotation[, srcx, srcy, srcw, srch, destx, desty, destw, desth, rotxoffs, rotyoffs])

The parameters:

- A buffer number
- How much to scale the image up or down
- How much to rotate the image
- Coordinates and dimensions in the buffer to copy from
- Coordinates and dimensions in the GUI window to copy to
- A couple of optional parameters that let you do fancier transformations with the image. We can ignore them.

The source coordinates are of particular importance here, because that's how we specify what frame to grab. If a knob is 64 pixels tall, the first frame would be at y = 0, the second at y = 64, and so on.

I think we can agree that gfx.blit() is pretty scary, and it would be nice if we could avoid ever having to call it directly. Good thing some enterprising young (fine, I'm 32) gentleman wrote a helper function for all of this:

-- Draw the specified frame from a given image buffer
local function img(x, y, w, h, step, buffer)
	gfx.mode = 0
	local img_y = h * GUI.round(step)
	gfx.blit(buffer, 1, 0, 0, img_y, w, h, x, y, w, h)

GUI.img = img
- gfx.mode gives you a few options for how to combine the copied image with whatever's already been drawn in the GUI window. We're making sure it stays set to 0, for normal drawing.

- Using the element height h and frame number step, we figure out a y-coordinate in the buffer to get our current frame from. Note that all of the classes in this example have been rewritten so that h specifies the image height, not the element as a whole. For instance, specifying h = 50 for the OptLst class tells it that each individual button in the list is 50 pixels tall, not the entire group.

- Finally we blit from the source buffer to the window.

Drawing our elements with images

Before we do anything else, I suppose we need to have a background. This gets done in the Main() loop, prior to drawing the individual elements:

	GUI.img(0, 0, GUI.w, GUI.h, 0,
Well that was thrilling.

As you can see in the images library demo, each class's :draw() method has been rewritten. Let's start with the Knob and Slider, since they're drawn the same way:

	local frame = (curstep / steps) * 63	
	GUI.img(x, y, w, w, frame, GUI.images.knb)
Rather than having to muck around with tick marks, shadows, circles, or polar coordinates, life here has gotten WAY easier. Some basic math tells us what frame we want, and then we phone up GUI.img to get things done. Simple as that.

Btn is pretty much the same; its image file only has two frames, not-pushed and pushed, so we just draw whichever one matches the button's current state.

The Lbl up top uses frames to do a nice visual trick. The image is, again, two frames - the background, and a sort of frosted-over cover. I was trying to make it look like a little LED readout, but it didn't quite work. Oh well.

To create this effect, we just draw the first frame, print our text on top of it, and draw the second on top of that. Because the second frame's pixels are mostly transparent, you can still read the text.

Lastly we've got OptLst, which coincidentally requires the most work to draw. Almost like I planned it this way, right?

The first thing you might notice if you compare the source image to the functioning script is that, in the source, there's a lot of empty space around the button. This wasn't my choice, it's just how the button came when I grabbed it from the link above. So why isn't there any empty space around the options in our GUI? Simple - :new() takes an extra parameter, img_pad, that lets us control the spacing between the options. For this image, setting the image pad to -12 sticks them all together.

:new() also keeps track of the individual option height separately from their collective height, since we'll need to know both.

Moving on to :draw(), we just have a loop drawing each button and then hopping down by one button height for the next one. It also checks if the current button is pushed and grabs the second frame when needed.

The button text also moves over when it's being pushed, both here and in the original LS GUI library. To do so, we just add 2 to the x and y coordinates before drawing the text. I think it looks nice.

And with that, we've come to the end of my lesson plan. Hopefully you learned something.

Last edited by Lokasenna; 04-16-2018 at 07:16 AM.
Lokasenna is online now   Reply With Quote