Quantcast What parts of an addon's GUI should be "pixel perfect"? - WoWInterface
Thread Tools Display Modes
10-15-17, 02:25 PM   #1
CC_WOW
A Deviate Faerie Dragon
 
CC_WOW's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2016
Posts: 18
What parts of an addon's GUI should be "pixel perfect"?

I've recently tried to build a more elaborate GUI, and run into so many glitches that I'm entirely stumped. There's always some pixel or another that looks off and this is seemingly unrelated to the resolution or UI scaling (I tried several). I just read this thread here (http://www.wowinterface.com/forums/s...416#post325416) and now I'm confused; Do I really need to round down any and all coordinates everywhere in the entire GUI?

What I've done so far:
* Made sure that the addon scales with a pixel-perfect factor, like so:

Code:
function GetScaleFactor()
    
    local screenResolution = ({GetScreenResolutions()})[GetCurrentResolution()]
    local screenWidth, screenHeight = strsplit("x", screenResolution)
    local scale = 768/screenHeight -- This is the scale factor that will scale textures to the native resolution that the client uses internally... Or so I believe

    return (1/scale)

end
I have applied this scale factor to EVERYTHING so that I can use basic pixel width, height etc. in my settings. For my usual 1080y this scales the frames by 1/0.71111..., which is similar to 1/UIParent:GetScale() when disabling the "UI Scale" manual override option. I believe this to be correct, however it clearly is not enough!

* Made sure the main (parent) frames are not positioned between pixels by rounding them to the nearest integer value. This seemed to have fixed the issue at first, except... now it is back.

I have many GUI elements that are aligned relative to their parent (e.g, centered), but font sizes, icons, and many parent elements themselves are using the same scaleFactor * size (from settings) calculation. The only idea I have left now would be to round everything and somehow hope it will look as it does when unscaled (i.e., in my sketches), so that 1px margins are actually 1px. Does that actually do anything if things are already using 1px borders etc.? Anything else that I missed?

Basically, I would try and write a function that applies the scale and rounds down, like this:

Code:
function Scale(x)

    return math.floor(GetScaleFactor() * x + 0.5)

end
I would apply this to all SetPoint() calls that use offsets, although I'm not sure if that'll do when also using "centering" via SetPoint("LEFT"), SetPoint("RIGHT") to center vertically without any offset being given. In my mind, if the parent is positioned on an integer coordinate, and the child is of an integer size (after applying Scale(x) on those values), then it should work UNLESS (always something... sigh) the child's height is not even. Surely there must be an easier way to not have a GUI look like crap?
  Reply With Quote
10-15-17, 03:25 PM   #2
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
Pixel perfection is like counting money: start at the largest possible then work down to the smallest. The first thing is to get your perfect scale as you've done (0.71111) and apply that to the SetCVars as appropriate.

Next, assuming you want to release your code to the public, is check if your new scale is <= 0.64 which is the lowest WoW allows for the CVars. If your scale is lower, as will happen with 4K screens, apply your new scale to UIParent.
Code:
-- already set the CVars, now check UIParent
if newScale <= 0.64 then
    UIParent:SetScale(newScale)
end
Now you are ready to begin. Create a frame and set its scale to 1, which if you read above, is actually 1/newScale behind the scenes. If you applied newScale to myFrame a second time, that is exactly what you would be doing effectively setting the scale on myFrame twice. If myFrame is too large or too small, then SetScale(0.5) or SetScale(2) that way.

If you set the scale on myFrame the way most people think, you'd be setting it as 1/newScale/newScale which is probably not what you want. Let the CVar and UIParent's scale do most of the work for you.

Okay, good. Next you will need to get the TOPLEFT and BOTTOMRIGHT attributes of myFrame, possibly height and width as well, and either floor or ceil to make sure they are sitting on pixel integers. The other method is SetPoint one corner of myFrame, usually myFrame's TOPLEFT directly on a pixel integer, then calculate if BOTTOMRIGHT is also sitting on a pixel integer. If not, adjust the height or width using floor or ceil until that is the case. TOPLEFT should still be anchored correctly.

Voila, myFrame is now pixel perfect.
  Reply With Quote
10-15-17, 03:38 PM   #3
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
A few follow-up notes:
  • When creating myFrame, do all things you would do normally, including SetSize or SetHeight/SetWidth, SetScale, and SetPoint, then pass myFrame to your pixel perfection function.
  • Your pixel perfection function ought to be exactly that a separate function, which could be a local function.
  • Said function will change myFrame's height and/or width. That is normal.
  • During all stages where you want to either floor or ceil a measurement, be consistent. Stick to floor or ceil and do not alternate between the two unless absolutely necessary.
  • The finished myFrame will have a different height or width or both than you originally created, which again, is normal.
  Reply With Quote
10-15-17, 04:02 PM   #4
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
Here's where it gets complicated. If your project is an entire UI replacement, you are going to forget what I said about setting the scale of a frame twice. In this case, you would want to use
Code:
myFrame(SetScale(UIParent:GetEffectiveScale()))
which is the same as 1/newScale/newScale. The truly complicated bit at this point is that you will have to constantly adjust myFrame's height or width and possibly SetPoint in your pixel perfection function each time the user adjusts myFrame's height or width.

For the record, it would best to not offer adjusting the scale of a frame in your project's options, and only offer height and width as options to keep your sanity, along with enforcing your new scale.

Pixel perfection is a truly massive undertaking, but the results can be beautiful if you put the time and effort into making it work correctly.
  Reply With Quote
10-15-17, 04:19 PM   #5
CC_WOW
A Deviate Faerie Dragon
 
CC_WOW's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2016
Posts: 18
Thanks, but wouldn't setting the CVar change the entire UI's scaling? That seems like something the user ought to do for themselves, I merely want my UI to not glitch at whatever scale is currently set.

Example: With my resolution, the proper (and automatic) UI scale is 0.71111. However, I normally have set mine to about 0.89999 to scale things the way I like them, which has now caused at least some parts of the frames to not be on integer pixels (apparently). Setting UI scale to 1 works fine (the frame is perfect, but way too big), and on 0.7111 it also is perfect, but way too small. Thusly, I want to simply take the UI scale that the user set, and maybe a separate setting to scale the addon itself, and apply that to the frames without the glitches.

From your reply, I believe I can just take the same approach you did except using those values (and not setting the CVar) to calculate the proper scale (BEFORE fixing the floating point pixel coordinates). I would then run the "pixel perfect" function on the window frames, which seems reasonable. However, does that also fix all the child frames (which are the main concern here) with their 1px margins etc? Or do I run that function on every single frame? Also, why do I want to SetScale, twice? What I did was simply

Code:
    local scaleFactor = GetScaleFactor()
    local padding = settings.someValue * scaleFactor
    
    frame:SetPoint("TOPLEFT", padding, -padding)
and that would basically turn a 2 px padding into 2 / (768/1080) = 2.8125 - which is obviously not an integer value.

My idea as described would then simply be to round it via math.floor(2.8125 + 0.5) = 3 and get a 3 px padding instead on the given resolution, which should (?) be able to be positioned correctly if my main frame is using integer coords and everything is rounded like that.

The only issue then is centering elements with odd sizes, but I can maybe force them to always be even values (if they need to be centered, or contain elements that need to be centered).

PS: The addon in question is not an actual UI, it just has a bunch of frames with finely-tuned elements and the glitching drives me nuts. Here is how an early draft looked (no visible glitches, but to give an idea of the type of child elements that need to be placed correctly from using pixel settings):


Last edited by CC_WOW : 10-15-17 at 04:22 PM.
  Reply With Quote
10-15-17, 06:59 PM   #6
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
Your project doesn't sound like a full UI replacement, and just a handful of frames comprising an addon. In that case, you don't need to set the CVars or scale UIParent; you are only concerned with the frames themselves.

AFAIK, if the parent frame is sized and parented correctly, child frames will adjust properly. It would be in your best interests to use frame sizes in multiples of 2 for the parent in that case, because you mentioned (correctly) that odd numbers like 3 will mean the children will be off-point. You can use math.mod, math.fmod, or % to fix that.
  Reply With Quote
10-15-17, 10:18 PM   #7
semlar
A Pyroguard Emberseer
 
semlar's Avatar
AddOn Author - Click to view addons
Join Date: Sep 2007
Posts: 1,014
Originally Posted by CC_WOW View Post
Do I really need to round down any and all coordinates everywhere in the entire GUI?
No, none of this is something the average addon author should even be thinking about.

Unless you have a very compelling reason to ignore the user's UI scale, all you're doing is making your addon less accessible and more annoying to use.

As long as your frame elements are all anchored correctly, it will look right regardless of what scale they're using. Things shouldn't be shifting around when you scale them, it should look exactly the same at every scale level, just smaller or larger.
  Reply With Quote
10-16-17, 07:30 AM   #8
Resike
A Pyroguard Emberseer
AddOn Author - Click to view addons
Join Date: Mar 2010
Posts: 1,222
Originally Posted by semlar View Post
No, none of this is something the average addon author should even be thinking about.
While i agree with you, i have to say there are some frames that are always visible (unit frames/action bars) where such issue can be really annoying since you see them all the time.

Originally Posted by semlar View Post
As long as your frame elements are all anchored correctly, it will look right regardless of what scale they're using. Things shouldn't be shifting around when you scale them, it should look exactly the same at every scale level, just smaller or larger.
This is sadly not true, lets say you have frame with 2 child frames and there is a 1 pixel space between them. What happens when you scale the frame? The frames gets scaled, child1 will get scaled, child2 will get scaled, and that 1 pixel space will also get scaled (lets say scaled up to 1.5 px) which will break child2's pixel perfection, even it the frame and child1 are pixel perfect.

This also gets applied to model camera distances. If you start scaling a model, then all of it's camera vectors get scaled by the same value (vector distance * UIParentScale * frameEffectiveScale), and if you want to keep the original values, you gonna have to fix this vector distances by yourself.
  Reply With Quote
10-16-17, 04:48 PM   #9
CC_WOW
A Deviate Faerie Dragon
 
CC_WOW's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2016
Posts: 18
I just have a bunch of frames, with lots of smaller (child) elements inside. Can I simply do a SetScale(UIParent:GetScale()) tfor he parent or do I still have to round the coordinates for everything?

I'm somewhat confused still, but my understanding is that there are several different issues at hand.

1) Scaling according to the user interface settings

No CVar is needed, I simply do SetScale on all top-level addon frames. This should always scale them properly so they will appear with the same relative size (but can have glitches due to floating point pixel coordinates). Also, their actual position may shift, but I think there's a library called LibWindow to handle that properly across resolutions.

Example:

MainFrame:SetWidth(500) as hardcoded value without scaling => results in exactly 500px no matter what BEFORE applying the UIScale. That is, it will be exactly 500 pixels at a 1.0 scale.
Now, the user sets their scale to 0.8, which means the frame will be 500px on the relative coordinates of the UIParent viewport, resulting in 500 * 1/0.8 = 600 px actual screen size. If 0.8 is the "correct" scale for their resolution there should be no problem, because every pixel on the viewport would align with one on the actual screen. If it isn't, there would be glitches and it gets tricky...

2) Pixel perfection

At a pixel perfect scale (i.e., one that matches the 768/screenWidth calculation in my first post), no glitches would occur (1:1 mapping between virtual/actual pixels). However, if the scale is arbitrary then frames will be sized and positioned "between" pixels, causing the shifting borders/alignment issues. To fix this, I could either "undo" the scaling for my frame, by multiplying by the UIScale which gives it a pixel perfect look, but also remains static - that is not really an option, or I can attempt to reposition it correctly on the <height>x768 pixel grid used internally (height based on aspect ratio - does that even matter here?).

Therefore, I have to round the main frames' coordinates and always use integer sizes (ideally, even ones so children can be positioned correctly) AFTER applying the user's UIScale to size them. I should also avoid elements such as FontStrings having an odd size if they are to be centered, but otherwise I could map out the children's layout exactly like on paper without changing anything in the code (using values directly, merely the parent would need to be scaled/rounded).

Is that correct or am I still missing something? I have played around with it, but the GUI code is incredibly messy so I'd rather do it right.
  Reply With Quote
10-16-17, 09:56 PM   #10
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
To be clear about something, no matter what your frame's scale, if your frame is 500 x 600 pixels (or whatever height and width you are using) and you apply a scale, the frame is still 500 x 600. Scaling does not change the number of pixels used, only the render size.

This is because your monitor only has a set amount of pixels, 1920 x 1080 on my 1080p screen. If I set the scale of either my entire UI or an individual frame (that may or may not have child frames), the number of pixels on my monitor never change.

Setting a scale on a parent frame will affect the child frames by the exact same scale automatically.

The CVars to enable manual scaling and setting the game's scale is mostly only used by full UI replacement addons. Some time ago I tried an experiment. First, I set the built-in scale for the game to 1, which rendered all the default frames like the minimap and user frames a bit larger than I had previously, because the game had guessed my scale as less than 1.

Then I created a test frame and set its scale to 1. Next I changed the game's scale to 0.75 and my test frame and built-in frames rendered 25% smaller. I then reset the game's scale back to 1, and directly changed the test frame scale to 0.75.

What do you think happened? The test frame at 0.75 directly was rendered smaller than game scale 0.75 and test frame of 1. Lastly, I did both, and the test frame became 0.75 of 0.75.

All that said, my test frame was still 400 x 400 and no matter what scale was set where, it was always dead centre of my screen because that is where I told it to render via SetPoint. If I had any child frames set at decreasing 25 height/width intervals until 25 x 25 and told them to be in the centre of the test frame, then that is exactly where they would stay no matter what scale the parent frame would be.

So again, no matter what your scale is for any UI element or frame, the number of pixels for both your screen and your element never change because that is mechanically impossible. The monitor you are using has a set number of pixels based on whether it is 720, 1080, 4K, or something else. You cannot change the number of pixels and scaling has absolutely zero effect on the number of pixels.

Scaling is only, repeat, only, changing the render size.
  Reply With Quote
10-16-17, 10:07 PM   #11
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
Pixel perfection is a visual trick. All it means is "to display my frame at a scale of 1 on a 1080p screen use a scale of 0.711111111 to make it look correct" hence multiplying the scale you set with SetScale by the "perfect" scale for your screen, ie:// scale of 1 == 1 * 0.71111. At no point has the actual height or width of the frame changed, and why SetHeight and SetWidth only accept integers without looking fuzzy.
  Reply With Quote
10-16-17, 10:16 PM   #12
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
This also means that unless you do set the entire UI to the pixel perfect scale, you should instead set any given frame's scale by UIParent:GetEffectiveScale() otherwise you will get unexpected display results.

After all, if UIParent is not "pixel perfect", then you would want to use whatever scale the user prefers.

Since you are not writing a full UI replacement and thus not adjusting UIParent or the game's scale using CVars, and you are designing an addon that displays some info, I highly suggest you use GetEffectiveScale.

Pixel perfection, as I mentioned a couple of times, is arguably not for individual addons, and is best served for full UI replacements. If you try to make your addon pixel perfect it will not look "right" to other users who desire a different scale for their game than you use.
  Reply With Quote
10-17-17, 07:29 AM   #13
Resike
A Pyroguard Emberseer
AddOn Author - Click to view addons
Join Date: Mar 2010
Posts: 1,222
Seems like many people are interested about this, i'll share my pixel perfect code that works perfectly with custom UIParent and frame scale too.
  Reply With Quote
10-17-17, 08:48 AM   #14
CC_WOW
A Deviate Faerie Dragon
 
CC_WOW's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2016
Posts: 18
Yes, I understand how the internal resolution is different from the actual pixels on the screen. However, Resike is right and the children will not scale properly according to my tests - I managed to get most of the glitches fixed by rounding the dimensions and then making it an even number AFTER scaling, but that only works for the container frames and not the pixep-perfect alignment of children inside of them apparently.

I just can't seem to get the points sorted out, or maybe I am missing something else, as there is still some glitches with the borders of various child elements. I tried doing rounding on all points but it doesn't seem to be doing anything.

Maybe this is also caused by the structure imposed by AceGUI, if some of the internal frames aren't scaled properly? (I've created custom widgets for the display elements and each one of them uses 3 frames: The border, the content, and the anchor (?), each of which may cause issues that I can't seem to figure out)

PS: Yes, I would be very interested in your solution to this problem - I must be missing something but I don't know what.
  Reply With Quote
10-17-17, 10:46 AM   #15
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
@Resilke I'd love to see your code!

@CC_WOW I think it is time to post your entire code. It feels like I am shooting into the dark at this juncture.
  Reply With Quote
10-17-17, 11:55 AM   #16
Resike
A Pyroguard Emberseer
AddOn Author - Click to view addons
Join Date: Mar 2010
Posts: 1,222
This is my hack for it, it works on every full screen resolution on every UIParent scale ratio. Currently not working in windowed mode with a custom screen size.

There are 2 frames a PixelPerfect one and a standard game scaled one (NotPixelPerfect).

How does it works?

Instead of scaling you basically let a base frame do the sizing (which is always pixel perfect) and you do the rounded anchoring yourself, instead of letting the game do it.
There are other safety roundings for the base frame, however those only works properly if you use the default UIParent scale for your screen resolution. Generalizing that for every UIParent scale needs more math.

Lua Code:
  1. local AddonName, Addon = ...
  2.  
  3. local AddOn = { }
  4.  
  5. math.round = function(self)
  6.     return math.floor(self + 0.5)
  7. end
  8.  
  9. AddOn.events = CreateFrame("Frame")
  10. AddOn.events:RegisterEvent("ADDON_LOADED")
  11.  
  12. AddOn.events:SetScript("OnEvent", function(self, event, ...)
  13.     AddOn[event](AddOn, ...)
  14. end)
  15.  
  16.  
  17. function AddOn:ADDON_LOADED(addon)
  18.     if addon == AddonName then
  19.         self:OnLoad()
  20.         self:OnLoad2()
  21.     end
  22. end
  23.  
  24. function AddOn:OnLoad()
  25.     local frame = CreateFrame("Frame", "TestFrame", UIParent)
  26.     frame:SetSize(84, 84)
  27.     frame.width = 84
  28.     frame.height = 84
  29.     frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", (UIParent:GetWidth() / 2) - (frame:GetWidth() / 2), (-UIParent:GetHeight() / 2) + (frame:GetHeight() / 2))
  30.     frame:SetClampedToScreen(true)
  31.  
  32.     frame.backdrop = {
  33.         bgFile = "Interface\\Buttons\\WHITE8X8",
  34.         edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
  35.         tile = true,
  36.         tileSize = 16,
  37.         edgeSize = 14,
  38.         insets = {left = 3, right = 3, top = 3, bottom = 3},
  39.     }
  40.  
  41.     frame:SetBackdrop(frame.backdrop)
  42.     frame:SetBackdropColor(0.0, 0.0, 0.0, 0.5)
  43.     frame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0)
  44.  
  45.     frame.scaled = CreateFrame("Frame", nil, frame)
  46.     frame.scaled:SetAllPoints(frame)
  47.  
  48.     frame.texture1 = frame:CreateTexture(nil, "ARTWORK", nil, -8)
  49.     frame.texture1:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -10)
  50.     frame.texture1:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -10, 10)
  51.     frame.texture1:SetColorTexture(1, 1, 1, 1)
  52.  
  53.     frame.texture2 = frame:CreateTexture(nil, "ARTWORK", nil, -7)
  54.     frame.texture2:SetPoint("TOPLEFT", frame.texture1, "TOPLEFT", 2, -2)
  55.     frame.texture2:SetPoint("BOTTOMRIGHT", frame.texture1, "BOTTOMRIGHT", -2, 2)
  56.     frame.texture2:SetColorTexture(0, 0, 0, 1)
  57.  
  58.     frame.texture3 = frame:CreateTexture(nil, "ARTWORK", nil, -6)
  59.     frame.texture3:SetPoint("TOPLEFT", frame.texture2, "TOPLEFT", 2, -2)
  60.     frame.texture3:SetPoint("BOTTOMRIGHT", frame.texture2, "BOTTOMRIGHT", -2, 2)
  61.     frame.texture3:SetColorTexture(1, 1, 1, 1)
  62.  
  63.     frame.texture4 = frame:CreateTexture(nil, "ARTWORK", nil, -5)
  64.     frame.texture4:SetPoint("TOPLEFT", frame.texture3, "TOPLEFT", 2, -2)
  65.     frame.texture4:SetPoint("BOTTOMRIGHT", frame.texture3, "BOTTOMRIGHT", -2, 2)
  66.     frame.texture4:SetColorTexture(0, 0, 0, 1)
  67.  
  68.     frame.texture5 = frame:CreateTexture(nil, "ARTWORK", nil, -4)
  69.     frame.texture5:SetPoint("TOPLEFT", frame.texture4, "TOPLEFT", 2, -2)
  70.     frame.texture5:SetPoint("BOTTOMRIGHT", frame.texture4, "BOTTOMRIGHT", -2, 2)
  71.     frame.texture5:SetColorTexture(1, 1, 1, 1)
  72.  
  73.     frame.texture6 = frame:CreateTexture(nil, "ARTWORK", nil, -3)
  74.     frame.texture6:SetPoint("TOPLEFT", frame.texture5, "TOPLEFT", 2, -2)
  75.     frame.texture6:SetPoint("BOTTOMRIGHT", frame.texture5, "BOTTOMRIGHT", -2, 2)
  76.     frame.texture6:SetColorTexture(0, 0, 0, 1)
  77.  
  78.     frame.texture7 = frame:CreateTexture(nil, "ARTWORK", nil, -2)
  79.     frame.texture7:SetPoint("TOPLEFT", frame.texture6, "TOPLEFT", 2, -2)
  80.     frame.texture7:SetPoint("BOTTOMRIGHT", frame.texture6, "BOTTOMRIGHT", -2, 2)
  81.     frame.texture7:SetColorTexture(1, 1, 1, 1)
  82.  
  83.     frame.texture8 = frame:CreateTexture(nil, "ARTWORK", nil, -1)
  84.     frame.texture8:SetPoint("TOPLEFT", frame.texture7, "TOPLEFT", 2, -2)
  85.     frame.texture8:SetPoint("BOTTOMRIGHT", frame.texture7, "BOTTOMRIGHT", -2, 2)
  86.     frame.texture8:SetColorTexture(0, 0, 0, 1)
  87.  
  88.     frame.texture9 = frame:CreateTexture(nil, "ARTWORK", nil, 0)
  89.     frame.texture9:SetPoint("TOPLEFT", frame.texture8, "TOPLEFT", 2, -2)
  90.     frame.texture9:SetPoint("BOTTOMRIGHT", frame.texture8, "BOTTOMRIGHT", -2, 2)
  91.     frame.texture9:SetColorTexture(1, 1, 1, 1)
  92.  
  93.     frame.texture10 = frame:CreateTexture(nil, "ARTWORK", nil, 1)
  94.     frame.texture10:SetPoint("TOPLEFT", frame.texture9, "TOPLEFT", 2, -2)
  95.     frame.texture10:SetPoint("BOTTOMRIGHT", frame.texture9, "BOTTOMRIGHT", -2, 2)
  96.     frame.texture10:SetColorTexture(0, 0, 0, 1)
  97.  
  98.     frame.texture11 = frame:CreateTexture(nil, "ARTWORK", nil, 2)
  99.     frame.texture11:SetPoint("TOPLEFT", frame.texture10, "TOPLEFT", 2, -2)
  100.     frame.texture11:SetPoint("BOTTOMRIGHT", frame.texture10, "BOTTOMRIGHT", -2, 2)
  101.     frame.texture11:SetColorTexture(1, 1, 1, 1)
  102.  
  103.     frame.pixel = frame.scaled:CreateFontString(nil, "ARTWORK")
  104.     frame.pixel:SetPoint("TOPLEFT", frame.texture11, "TOPLEFT", 2, -2)
  105.     frame.pixel:SetPoint("BOTTOMRIGHT", frame.texture11, "BOTTOMRIGHT", -2, 2)
  106.     frame.pixel:SetFontObject(GameFontNormalSmall)
  107.     frame.pixel:SetText("PP")
  108.  
  109.     -- Hooks
  110.     -- These roundings only work properly on the default UIParent scales:
  111.     hooksecurefunc(frame, "SetPoint", function(self)
  112.         if self.moving then
  113.             return
  114.         end
  115.  
  116.         if math.round(768 / (select(2, GetPhysicalScreenSize())) * 1000) / 1000 ~= math.round(UIParent:GetScale() * 1000) / 1000 then
  117.             return
  118.         end
  119.  
  120.         self.moving = true
  121.         self:SetMovable(true)
  122.  
  123.         local left, bottom = self:GetLeft(), self:GetBottom()
  124.  
  125.         if left and bottom then
  126.             local x = math.round(left)
  127.             local y = math.round(-UIParent:GetHeight() + bottom + self:GetHeight())
  128.  
  129.             self:ClearAllPoints()
  130.             self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", x, y)
  131.         end
  132.  
  133.         self:SetMovable(false)
  134.         self.moving = nil
  135.     end)
  136.  
  137.     -- These roundings only work properly on the default UIParent scales:
  138.     hooksecurefunc(frame, "StopMovingOrSizing", function(self)
  139.         if math.round(768 / (select(2, GetPhysicalScreenSize())) * 1000) / 1000 ~= math.round(UIParent:GetScale() * 1000) / 1000 then
  140.             return
  141.         end
  142.  
  143.         self:SetMovable(true)
  144.  
  145.         local left, bottom = self:GetLeft(), self:GetBottom()
  146.  
  147.         if left and bottom then
  148.             local x = math.round(left)
  149.             local y = math.round(-UIParent:GetHeight() + bottom + self:GetHeight())
  150.  
  151.             self:ClearAllPoints()
  152.             self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", x, y)
  153.         end
  154.  
  155.         self:SetMovable(false)
  156.     end)
  157.  
  158.     frame:HookScript("OnSizeChanged", function(self)
  159.         -- These roundings only work properly on the default UIParent scales:
  160.         if math.round(768 / (select(2, GetPhysicalScreenSize())) * 1000) / 1000 == math.round(UIParent:GetScale() * 1000) / 1000 then
  161.             self:SetSize(math.round(self:GetWidth()), math.round(self:GetHeight()))
  162.         end
  163.  
  164.         local UIScale = (768 / (select(2, GetPhysicalScreenSize())) / UIParent:GetScale())
  165.  
  166.         local scale = self:GetWidth() / self.width
  167.  
  168.         self.scaled:SetScale(scale)
  169.  
  170.         self.texture2:SetPoint("TOPLEFT", self.texture1, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  171.         self.texture2:SetPoint("BOTTOMRIGHT", self.texture1, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  172.  
  173.         self.texture3:SetPoint("TOPLEFT", self.texture2, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  174.         self.texture3:SetPoint("BOTTOMRIGHT", self.texture2, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  175.  
  176.         self.texture4:SetPoint("TOPLEFT", self.texture3, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  177.         self.texture4:SetPoint("BOTTOMRIGHT", self.texture3, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  178.  
  179.         self.texture5:SetPoint("TOPLEFT", self.texture4, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  180.         self.texture5:SetPoint("BOTTOMRIGHT", self.texture4, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  181.  
  182.         self.texture6:SetPoint("TOPLEFT", self.texture5, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  183.         self.texture6:SetPoint("BOTTOMRIGHT", self.texture5, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  184.  
  185.         self.texture7:SetPoint("TOPLEFT", self.texture6, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  186.         self.texture7:SetPoint("BOTTOMRIGHT", self.texture6, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  187.  
  188.         self.texture8:SetPoint("TOPLEFT", self.texture7, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  189.         self.texture8:SetPoint("BOTTOMRIGHT", self.texture7, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  190.  
  191.         self.texture9:SetPoint("TOPLEFT", self.texture8, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  192.         self.texture9:SetPoint("BOTTOMRIGHT", self.texture8, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  193.  
  194.         self.texture10:SetPoint("TOPLEFT", self.texture9, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  195.         self.texture10:SetPoint("BOTTOMRIGHT", self.texture9, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  196.  
  197.         self.texture11:SetPoint("TOPLEFT", self.texture10, "TOPLEFT", math.round(2 * scale) * UIScale, math.round(-2 * scale) * UIScale)
  198.         self.texture11:SetPoint("BOTTOMRIGHT", self.texture10, "BOTTOMRIGHT", math.round(-2 * scale) * UIScale, math.round(2 * scale) * UIScale)
  199.     end)
  200.  
  201.     -- Moving and Resizing functions
  202.     frame:EnableMouse(true)
  203.     --frame:SetMaxResize(800, 400)
  204.     frame:SetMinResize(42, 42)
  205.  
  206.     frame:SetScript("OnMouseDown", function(self, button)
  207.         if button == "LeftButton" then
  208.             self:SetMovable(true)
  209.             self:StartMoving()
  210.         end
  211.     end)
  212.     frame:SetScript("OnMouseUp", function(self, button)
  213.         if button == "LeftButton" then
  214.             self:StopMovingOrSizing()
  215.             self:SetMovable(false)
  216.  
  217.             -- These roundings only work properly on the default UIParent scales:
  218.             if math.round(768 / (select(2, GetPhysicalScreenSize())) * 1000) / 1000 ~= math.round(UIParent:GetScale() * 1000) / 1000 then
  219.                 return
  220.             end
  221.  
  222.             local left, bottom = self:GetLeft(), self:GetBottom()
  223.  
  224.             if left and bottom then
  225.                 local x = math.round(left)
  226.                 local y = math.round(-UIParent:GetHeight() + bottom + self:GetHeight())
  227.  
  228.                 self:ClearAllPoints()
  229.                 self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", x, y)
  230.             end
  231.         end
  232.     end)
  233.  
  234.     frame.br = CreateFrame("Frame", nil, frame)
  235.     frame.br:SetPoint("TOPLEFT", frame, "BOTTOMRIGHT", 0, 0)
  236.     frame.br:SetSize(12, 12)
  237.     frame.br:EnableMouse(true)
  238.  
  239.     frame.br.texture = frame.br:CreateTexture(nil, "OVERLAY")
  240.     frame.br.texture:SetPoint("TOPLEFT", frame.br, "TOPLEFT", 0, 0)
  241.     frame.br.texture:SetSize(12, 12)
  242.     frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  243.  
  244.     frame.br:SetScript("OnEnter", function(self)
  245.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
  246.     end)
  247.     frame.br:SetScript("OnLeave", function(self)
  248.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  249.     end)
  250.  
  251.     frame.br:SetScript("OnMouseDown", function(self, button)
  252.         if button == "LeftButton" then
  253.             frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
  254.  
  255.             frame:SetResizable(true)
  256.             frame:StartSizing("BottomRight")
  257.         end
  258.     end)
  259.     frame.br:SetScript("OnMouseUp", function(self, button)
  260.         frame:StopMovingOrSizing()
  261.         frame:SetResizable(false)
  262.  
  263.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  264.     end)
  265. end
  266.  
  267. function AddOn:OnLoad2()
  268.     local frame = CreateFrame("Frame", "TestFrame2", UIParent)
  269.     frame:SetSize(84, 84)
  270.     frame.width = 84
  271.     frame.height = 84
  272.     frame:SetPoint("TOPLEFT", UIParent, "TOPLEFT", (UIParent:GetWidth() / 2) - (frame:GetWidth() / 2) + 100, (-UIParent:GetHeight() / 2) + (frame:GetHeight() / 2))
  273.     frame:SetClampedToScreen(true)
  274.  
  275.     frame.backdrop = {
  276.         bgFile = "Interface\\Buttons\\WHITE8X8",
  277.         edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
  278.         tile = true,
  279.         tileSize = 16,
  280.         edgeSize = 14,
  281.         insets = {left = 3, right = 3, top = 3, bottom = 3},
  282.     }
  283.  
  284.     frame:SetBackdrop(frame.backdrop)
  285.     frame:SetBackdropColor(0.0, 0.0, 0.0, 0.5)
  286.     frame:SetBackdropBorderColor(0.3, 0.3, 0.3, 1.0)
  287.  
  288.     frame.scaled = CreateFrame("Frame", nil, frame)
  289.     frame.scaled:SetAllPoints(frame)
  290.  
  291.     frame.texture1 = frame:CreateTexture(nil, "ARTWORK", nil, -8)
  292.     frame.texture1:SetPoint("TOPLEFT", frame, "TOPLEFT", 10, -10)
  293.     frame.texture1:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -10, 10)
  294.     frame.texture1:SetColorTexture(1, 1, 1, 1)
  295.  
  296.     frame.texture2 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -7)
  297.     frame.texture2:SetPoint("TOPLEFT", frame.texture1, "TOPLEFT", 2, -2)
  298.     frame.texture2:SetPoint("BOTTOMRIGHT", frame.texture1, "BOTTOMRIGHT", -2, 2)
  299.     frame.texture2:SetColorTexture(0, 0, 0, 2)
  300.  
  301.     frame.texture3 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -6)
  302.     frame.texture3:SetPoint("TOPLEFT", frame.texture2, "TOPLEFT", 2, -2)
  303.     frame.texture3:SetPoint("BOTTOMRIGHT", frame.texture2, "BOTTOMRIGHT", -2, 2)
  304.     frame.texture3:SetColorTexture(1, 1, 1, 1)
  305.  
  306.     frame.texture4 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -5)
  307.     frame.texture4:SetPoint("TOPLEFT", frame.texture3, "TOPLEFT", 2, -2)
  308.     frame.texture4:SetPoint("BOTTOMRIGHT", frame.texture3, "BOTTOMRIGHT", -2, 2)
  309.     frame.texture4:SetColorTexture(0, 0, 0, 1)
  310.  
  311.     frame.texture5 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -4)
  312.     frame.texture5:SetPoint("TOPLEFT", frame.texture4, "TOPLEFT", 2, -2)
  313.     frame.texture5:SetPoint("BOTTOMRIGHT", frame.texture4, "BOTTOMRIGHT", -2, 2)
  314.     frame.texture5:SetColorTexture(1, 1, 1, 1)
  315.  
  316.     frame.texture6 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -3)
  317.     frame.texture6:SetPoint("TOPLEFT", frame.texture5, "TOPLEFT", 2, -2)
  318.     frame.texture6:SetPoint("BOTTOMRIGHT", frame.texture5, "BOTTOMRIGHT", -2, 2)
  319.     frame.texture6:SetColorTexture(0, 0, 0, 1)
  320.  
  321.     frame.texture7 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -2)
  322.     frame.texture7:SetPoint("TOPLEFT", frame.texture6, "TOPLEFT", 2, -2)
  323.     frame.texture7:SetPoint("BOTTOMRIGHT", frame.texture6, "BOTTOMRIGHT", -2, 2)
  324.     frame.texture7:SetColorTexture(1, 1, 1, 1)
  325.  
  326.     frame.texture8 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, -1)
  327.     frame.texture8:SetPoint("TOPLEFT", frame.texture7, "TOPLEFT", 2, -2)
  328.     frame.texture8:SetPoint("BOTTOMRIGHT", frame.texture7, "BOTTOMRIGHT", -2, 2)
  329.     frame.texture8:SetColorTexture(0, 0, 0, 1)
  330.  
  331.     frame.texture9 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, 0)
  332.     frame.texture9:SetPoint("TOPLEFT", frame.texture8, "TOPLEFT", 2, -2)
  333.     frame.texture9:SetPoint("BOTTOMRIGHT", frame.texture8, "BOTTOMRIGHT", -2, 2)
  334.     frame.texture9:SetColorTexture(1, 1, 1, 1)
  335.  
  336.     frame.texture10 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, 1)
  337.     frame.texture10:SetPoint("TOPLEFT", frame.texture9, "TOPLEFT", 2, -2)
  338.     frame.texture10:SetPoint("BOTTOMRIGHT", frame.texture9, "BOTTOMRIGHT", -2, 2)
  339.     frame.texture10:SetColorTexture(0, 0, 0, 1)
  340.  
  341.     frame.texture11 = frame.scaled:CreateTexture(nil, "ARTWORK", nil, 2)
  342.     frame.texture11:SetPoint("TOPLEFT", frame.texture10, "TOPLEFT", 2, -2)
  343.     frame.texture11:SetPoint("BOTTOMRIGHT", frame.texture10, "BOTTOMRIGHT", -2, 2)
  344.     frame.texture11:SetColorTexture(1, 1, 1, 1)
  345.  
  346.     frame.pixel = frame.scaled:CreateFontString(nil, "ARTWORK")
  347.     frame.pixel:SetPoint("TOPLEFT", frame.texture11, "TOPLEFT", 2, -2)
  348.     frame.pixel:SetPoint("BOTTOMRIGHT", frame.texture11, "BOTTOMRIGHT", -2, 2)
  349.     frame.pixel:SetFontObject(GameFontNormalSmall)
  350.     frame.pixel:SetText("NPP")
  351.  
  352.     frame:HookScript("OnSizeChanged", function(self)
  353.         local scale = self:GetWidth() / self.width
  354.  
  355.         self.scaled:SetScale(scale)
  356.     end)
  357.  
  358.     -- Moving and Resizing functions
  359.     frame:EnableMouse(true)
  360.     --frame:SetMaxResize(800, 400)
  361.     frame:SetMinResize(42, 42)
  362.  
  363.     frame:SetScript("OnMouseDown", function(self, button)
  364.         if button == "LeftButton" then
  365.             self:SetMovable(true)
  366.             self:StartMoving()
  367.         end
  368.     end)
  369.     frame:SetScript("OnMouseUp", function(self, button)
  370.         if button == "LeftButton" then
  371.             self:StopMovingOrSizing()
  372.             self:SetMovable(false)
  373.         end
  374.     end)
  375.  
  376.     frame.br = CreateFrame("Frame", nil, frame)
  377.     frame.br:SetPoint("TOPLEFT", frame, "BOTTOMRIGHT", 0, 0)
  378.     frame.br:SetSize(12, 12)
  379.     frame.br:EnableMouse(true)
  380.  
  381.     frame.br.texture = frame.br:CreateTexture(nil, "OVERLAY")
  382.     frame.br.texture:SetPoint("TOPLEFT", frame.br, "TOPLEFT", 0, 0)
  383.     frame.br.texture:SetSize(12, 12)
  384.     frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  385.  
  386.     frame.br:SetScript("OnEnter", function(self)
  387.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Highlight")
  388.     end)
  389.     frame.br:SetScript("OnLeave", function(self)
  390.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  391.     end)
  392.  
  393.     frame.br:SetScript("OnMouseDown", function(self, button)
  394.         if button == "LeftButton" then
  395.             frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Down")
  396.  
  397.             frame:SetResizable(true)
  398.             frame:StartSizing("BottomRight")
  399.         end
  400.     end)
  401.     frame.br:SetScript("OnMouseUp", function(self, button)
  402.         frame:StopMovingOrSizing()
  403.         frame:SetResizable(false)
  404.  
  405.         frame.br.texture:SetTexture("Interface\\ChatFrame\\UI-ChatIM-SizeGrabber-Up")
  406.     end)
  407. end

Edit: Made some fixes.

Last edited by Resike : 10-17-17 at 03:30 PM.
  Reply With Quote
10-17-17, 12:02 PM   #17
CC_WOW
A Deviate Faerie Dragon
 
CC_WOW's Avatar
AddOn Author - Click to view addons
Join Date: Nov 2016
Posts: 18
Right, here is the current (experimental) branch: GitHub link
It's a giant mess right now, which I am going to sort out after I fixed the glitching. You have been warned!

Resike apparently posted some code which is no longer visible here, maybe it was filtered? I've stored it here in the meantime: https://gist.github.com/SacredDuckwh...8b9558fb67fd20

What I meant earlier was that the internal resolution doesn't match the actual screen size (as you have said), so I am looking for a way to map it to that cleanly after scaling it according to the user's settings. This is a direct comparison of the frame at a pixel-perfect scale and upscaled with glitching:



Due to the rounding taking place every once in a while pixels will glitch. I tried applying the rounding that was mentioned here, and it did help, but it isn't quite enough... so I figured I need to make sure the points are anchored to integer coordinates (and I rounded them, which again helped somewhat). For some reason, there are still some glitched borders though.

Examples: Proper UI scale of 768/1080 and then my normal UI scale of ~0.889 (not pixel-perfect and glitched after scaling) - as well as the zoomed-in comparison
  Reply With Quote
10-17-17, 02:26 PM   #18
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
I will look more in depth later, but I can tell you that line 3 is not needed, as you already have the table as the second return from line 1.

Also since math.round does not exist normally, that should be a local variable; otherwise you run the risk of someone else doing something similar and cross-polluting each other.

Next you use SetSize, but you also use SetHeight and SetWidth. Use either SetSize or both SetHeight/SetWdith, not both.

frame.backdrop can be just local backdrop = blah.

Why are you securely hooking StopMovingOrSizing? I'm pretty certain you can use a normal hook for that, although you will have to exit out on InCombatLockdown().

Getting the scale on line 154 is all right, but you do not need to get the return every time the frame is resized. The scale will always be the scale. Move that line into your main chunk. You can apply the scale when the frame is resized, however.

There are possibly other things but that is at a glance.

Last edited by myrroddin : 10-17-17 at 02:30 PM. Reason: wrong API, oops.
  Reply With Quote
10-17-17, 03:03 PM   #19
Resike
A Pyroguard Emberseer
AddOn Author - Click to view addons
Join Date: Mar 2010
Posts: 1,222
Originally Posted by myrroddin View Post
I will look more in depth later, but I can tell you that line 3 is not needed, as you already have the table as the second return from line 1.

Also since math.round does not exist normally, that should be a local variable; otherwise you run the risk of someone else doing something similar and cross-polluting each other.

Next you use SetSize, but you also use SetHeight and SetWidth. Use either SetSize or both SetHeight/SetWdith, not both.

frame.backdrop can be just local backdrop = blah.

Why are you securely hooking StopMovingOrSizing? I'm pretty certain you can use a normal hook for that, although you will have to exit out on InCombatLockdown().

Getting the scale on line 154 is all right, but you do not need to get the return every time the frame is resized. The scale will always be the scale. Move that line into your main chunk. You can apply the scale when the frame is resized, however.

There are possibly other things but that is at a glance.
1. It's two different table.
2. Adding math.round to the global table is not much of a risk since it should be present there. It's a good question why the hell it's not present by default?
3. I didn't use any SetHeight.
4. Yes, but if i want to access those values it better to keep this way. I personally don't use SetBackdrop at all, i use custom borders but that would be too much garbage code here.
5. Copy paste.
6. I need to get the scale every time the frames size is changed since it's get generated from the (current size / base size) formula.
  Reply With Quote
10-17-17, 06:16 PM   #20
myrroddin
A Molten Giant
 
myrroddin's Avatar
AddOn Author - Click to view addons
Join Date: Oct 2008
Posts: 836
Originally Posted by Resike View Post
1. It's two different table.
2. Adding math.round to the global table is not much of a risk since it should be present there. It's a good question why the hell it's not present by default?
3. I didn't use any SetHeight.
4. Yes, but if i want to access those values it better to keep this way. I personally don't use SetBackdrop at all, i use custom borders but that would be too much garbage code here.
5. Copy paste.
6. I need to get the scale every time the frames size is changed since it's get generated from the (current size / base size) formula.
  1. I'll have to study your code to see why. That's for later.
  2. Maybe math.round should be, but it isn't and there are way too many ways to accomplish rounding. If I use a global version that is different than yours, there could be conflicts.
  3. Oops, I misread some lines, sorry!
  4. Ah, okay.
  5. Ditto okay.
  Reply With Quote

WoWInterface » Developer Discussions » Graphics Help » What parts of an addon's GUI should be "pixel perfect"?

Thread Tools
Display Modes

Posting Rules
You may not post new threads
You may not post replies
You may not post attachments
You may not edit your posts

vB code is On
Smilies are On
[IMG] code is On
HTML code is Off