We're gonna learn a bit about masking, which is a technique for specifying which part of an image you want to draw. Anything that's not in the part that you want to draw, won't be.
In essence, this:
Becomes this:
Masking is a powerful technique that can save you quite a lot of time and resources. If you understand the theory behind how it works, there are a huge number of applications for this technique. Once this particular arrow is in your quiver, I'm sure you'll find plenty of targets to fire it at.
Make the pretty:
Shoutout to Zasif, who not only inspired this topic for the day, but also provided me some examples of impossible healthbars to work from. If you see pretty pictures in this post, that's his fault.
Let's get started with some health bar graphics just as an example:
Let's also take a look at what we want the end result of our process to look like:
Now that we have an idea of what we want to achieve, we can start to think about how to break up the images in such a way that we can get results.
We want to separate our images into at the very least, 3 layers. We're going to do 4 layers here, but the 4th layer is very much optional.
The mask icon should cover the entire fill area of what we want to mask. It should be full brightness (#FFFFFF) or rgb(255,255,255). This is used to determine what part of your graphic is visible.
The bg, or background icon should only be the fillable area when the area is completely empty.
The fill icon should only be the fillable area when the area is completely full.
The fg, or foreground icon should be anything that goes outside, or even covers the fillable area. This area is optional, and will not be masked at all.
Structure:
We're going to use 5 objects together to create a single dynamic health bar. These objects will use a very specific set of drawing rules to create the desired effect. Let's go over the structure first.
root { mask { background { fill { } } } foreground { } }
We also need to understand how these objects work together.
root KEEP_TOGETHER { mask { background BLEND_MULTIPLY KEEP_TOGETHER { fill { } } } foreground { } }
When Dreamseeker goes to draw our structure, it will draw root and all of its children on their own canvas. Mask is then drawn by this process, and then its child, background, which will again create a new canvas and draw itself and fill. When background finishes drawing, it will take that whole canvas and blend it multiplicative against what's already been drawn, which is mask. This will cause the actual masking to happen. Because this is happening already within root's own private canvas, anything outside of mask's white pixel area will actually wind up being completely transparent. Last, foreground gets drawn on root, and root's private canvas will all be drawn onto the screen.
The end result of this, is that all of this:
Becomes this:
Make ugly:
Okay, we understand what we want to achieve, we just need to actually do it.
Let's create objects for all five parts:
//the object prototype for our root object:
obj/maskbar
appearance_flags = KEEP_TOGETHER
mouse_opacity = 0
var
obj/foreground
obj/background
obj/fill
obj/mask
//the object prototypes for our four pieces:
obj/maskpart
layer = FLOAT_LAYER
plane = FLOAT_PLANE
bg
icon_state = "bg"
appearance_flags = KEEP_TOGETHER
blend_mode = BLEND_MULTIPLY
fg
icon_state = "fg"
fill
icon_state = "fill"
mask
icon_state = "mask"
New(loc,icon)
src.icon = icon
..()
Now let's set up maskbar so that it creates and arranges all of its parts into vis_contents:
obj/maskbar
New()
Build()
vis_contents.Add(mask,foreground)
..()
Del()
//break relationships
vis_contents.Remove(mask,foreground)
background.vis_contents -= fill
mask.vis_contents -= background
..()
proc
Build()
//create constituent objects
foreground = new/obj/maskpart/fg(null,icon)
background = new/obj/maskpart/bg(null,icon)
fill = new/obj/maskpart/fill(null,icon)
mask = new/obj/maskpart/mask(null,icon)
//arrange constituent objects
background.vis_contents += fill
mask.vis_contents += background
Last, we need to set up a neat little function we can use to make the fill area move around inside of the masking area:
obj/maskbar
var
width = 0
height = 0
orientation = EAST
proc
setValue(ratio=1.0,duration=0)
//constrain the ratio between 0 and 1
ratio = min(max(ratio,0),1)
//apply orientation factors for fill bar offsets
var/fx = 0, fy = 0
switch(orientation)
if(EAST)
fx = -1
if(WEST)
fx = 1
if(SOUTH)
fy = 1
if(NORTH)
fy = -1
//calculate the offset of the fill bar.
var/invratio = 1-ratio
var/epx = width * invratio * fx
var/epy = height * invratio * fy
//apply the offset to the fill bar
if(duration)
//if a time value has been supplied, animate the transition from the current position
animate(fill,pixel_w=epx,pixel_z=epy,time=duration)
else
//if a time value has not been supplied, instantly set to the new position
fill.pixel_w = epx
fill.pixel_z = epy
Now that we have a generic object for handling masked health bars, we can create a few to test it out:
obj/maskbar/test
icon = 'bartest.dmi'
screen_loc = "CENTER:-81,CENTER"
width = 162
height = 5
orientation = EAST
obj/maskbar/test3
icon = 'bartest.dmi'
screen_loc = "CENTER:-81,CENTER:-14"
width = 162
height = 5
orientation = WEST
obj/maskbar/test2
icon = 'bartest2.dmi'
screen_loc = "CENTER:-50,CENTER:14"
width = 40
height = 38
orientation = NORTH
obj/maskbar/test4
icon = 'bartest2.dmi'
screen_loc = "CENTER:0,CENTER:14"
width = 40
height = 38
orientation = SOUTH
Finally, let's just add some demo code here to make the whole thing work:
mob
var
health = 1
obj/maskbar/bartest
obj/maskbar/bartest2
obj/maskbar/bartest3
obj/maskbar/bartest4
Login()
bartest = new/obj/maskbar/test()
bartest2 = new/obj/maskbar/test2()
bartest3 = new/obj/maskbar/test3()
bartest4 = new/obj/maskbar/test4()
client.screen.Add(bartest,bartest2,bartest3,bartest4)
..()
client
Click()
mob.health = !mob.health
mob.bartest.setValue(mob.health,10)
mob.bartest2.setValue(mob.health,10)
mob.bartest3.setValue(mob.health,10)
mob.bartest4.setValue(mob.health,10)
You can grab a copy of the demo here and try it for yourself:
http://files.byondhome.com/Ter13/UIbars_src.zip
Just click anywhere on the map to toggle the health bars.
Additional notes:
It should be noted that any object you mask will act strangely when interacting with the mouse. You will have to find your own workarounds for dealing with this problem, like creating invisible mouse capture objects that act as proxy objects for the masked ones.