drawing rectangle of a specified width with round borders inside of it

General discussion about LÖVE, Lua, game development, puns, and unicorns.
Post Reply
timehunter
Prole
Posts: 5
Joined: Tue May 24, 2016 1:05 am

drawing rectangle of a specified width with round borders inside of it

Post by timehunter »

Hi everyone,

i'm currently trying to write a small and efficient GUI lib for LÖVE, and i'm struggling with something that i thought would be simple to do at first but ... it isn't :ultraglee:

so basically i consider all my GUI elements to be boxes, with a specified width and height, border size, and border radius (i was inspired by CSS, maybe, :p).

what i want to to is drawing the borders inside the box, so i thought of a very simple script that (imo) would have done that :

Code: Select all

love.draw = function()
	local x, y = 0, 0
	local width, height = 100, 100
	local border_width = 10
	local border_radius = 10

	-- draw the box
	love.graphics.setColor({127,127,127})
	love.graphics.rectangle( "fill", x, y, width, height, border_radius, border_radius)

	-- draw the borders
	love.graphics.setLineWidth(border_width)
	love.graphics.setColor({255, 0, 0})
	love.graphics.rectangle( "line", x, y, width, height, border_radius, border_radius)
end
the problem with this code is that visually it seems ok but the box size changed because of the borders, which are drawn outside of the first rectangle (well half inside and half outside). You can do basic math to shift the border drawing to the correct positions (adding border_width / 2 to the coordinates and substracting border_width to sizes), but then, you can see the first call to love.graphics.rectangle exceeding out of the borders, at the four angles.

Is there a flexible way to achieve that result ? If a guru visits this, i'd like to have tips about handling borders like css do (https://www.w3.org/TR/css3-background/# ... der-radius)

Thank you for your time and help :)
User avatar
pgimeno
Party member
Posts: 3549
Joined: Sun Oct 18, 2015 2:58 pm

Re: drawing rectangle of a specified width with round borders inside of it

Post by pgimeno »

timehunter wrote:the problem with this code is that visually it seems ok but the box size changed because of the borders, which are drawn outside of the first rectangle (well half inside and half outside). You can do basic math to shift the border drawing to the correct positions (adding border_width / 2 to the coordinates and substracting border_width to sizes), but then, you can see the first call to love.graphics.rectangle exceeding out of the borders, at the four angles.
Just do the same in both.
timehunter wrote:Is there a flexible way to achieve that result ? If a guru visits this, i'd like to have tips about handling borders like css do (https://www.w3.org/TR/css3-background/# ... der-radius)
If you mean that the radius should be to the outer edge, just subtract border_width/2 from both radii (in both calls).

Unequal thickness is not directly possible. For different radii for each corner, you'd need a custom round rectangle function.
timehunter
Prole
Posts: 5
Joined: Tue May 24, 2016 1:05 am

Re: drawing rectangle of a specified width with round borders inside of it

Post by timehunter »

Well i thought the fix would have been much harder but, very simple in fact :) thank you for your help it works fine now
timehunter
Prole
Posts: 5
Joined: Tue May 24, 2016 1:05 am

Re: drawing rectangle of a specified width with round borders inside of it

Post by timehunter »

Hello,

after a bit of testing, i realised that the problem is not completely resolved, because the love.rectangle("line"...) function seemingly adds blur to the outer edges of the borders, i did a little screen capture to explain mysef, with some code snippets :

Image

Code: Select all

-- main.lua

love.load = function()
   gui.init()
   gui.newButton("testbutton", "caption", {x = 10, y = 10, width = 120, height = 50, border_radius = 10})
end

Code: Select all

-- gui.lua

local boxCompute = function(box)
	box.computed.x, box.computed.y = calcPosition(box)
	box.computed.width, box.computed.height = calcSize(box.style)
	box.computed.borderL, box.computed.borderT, box.computed.borderR, box.computed.borderB = calcBorders(box.style)
	if box.computed.borderL > box.computed.width then box.computed.borderL = 0 end
	if box.computed.borderT > box.computed.height then box.computed.borderT = 0 end
	if box.computed.borderB > box.computed.height then box.computed.borderB = 0 end
	if box.computed.borderR > box.computed.width then box.computed.borderR = 0 end
end

local boxDraw = function(box)
	local minborder = math.min(box.computed.borderL, box.computed.borderT, box.computed.borderR, box.computed.borderB)
	local maxborder = math.max(box.computed.borderL, box.computed.borderT, box.computed.borderR, box.computed.borderB)
	local radius = box.style.border_radius - minborder / 2

	-- if the radius is too small compared to the border, it will be negative so we fix this manually
	if radius <= 0 then radius = box.style.border_radius / 2 end

	-- draw the inner box
	love.graphics.setColor(box.style.bgcolor)
	love.graphics.rectangle("fill", box.computed.x + minborder / 2, box.computed.y + minborder / 2, box.computed.width - minborder, box.computed.height - minborder, radius, radius)

	-- if there is a border, we change the color to the border's color
	if maxborder > 0 then love.graphics.setColor(box.style.border_color) end

	-- if the smallest border has a width, we set the line width to its size, else we set it to one
	if minborder > 0 then love.graphics.setLineWidth(minborder) else love.graphics.setLineWidth(1) end

	-- draw the outer box
	love.graphics.rectangle("line", box.computed.x + minborder / 2, box.computed.y + minborder / 2, box.computed.width - minborder, box.computed.height - minborder, radius, radius)

	--if the border isn't rounded, we can draw borders with different sizes
	if radius == 0 then
		if box.computed.borderL > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y, box.computed.borderL, box.computed.height)
		end
		if box.computed.borderT > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y, box.computed.width, box.computed.borderT)
		end
		if box.computed.borderR > minborder then
			love.graphics.rectangle("fill", box.computed.x + box.computed.width - box.computed.borderR, box.computed.y, box.computed.borderR, box.computed.height)
		end
		if box.computed.borderB > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y + box.computed.height - box.computed.borderB, box.computed.width, box.computed.borderB)
		end
	end
end
we can see the blur extending the size of the box that should be 50px tall but is 52 ^^

any suggestions ? i attached the code if anyone want to look for errors or give me tips about the way i'm doing things ^^ >>
testgui.love
the full code
(2.13 KiB) Downloaded 167 times
User avatar
pgimeno
Party member
Posts: 3549
Joined: Sun Oct 18, 2015 2:58 pm

Re: drawing rectangle of a specified width with round borders inside of it

Post by pgimeno »

It "blurs" the line because it can't paint half a pixel.

A vertical line with a thickness of 1 px extends 0.5 px to the left and 0.5 px to the right. If the line thickness is odd and the line goes along pixel edges, then it affects half the left pixel and half the right pixel:

Image

But of course, you can't paint half a pixel, so LÖVE fills the pixel with an opacity (alpha) proportional to the area of the pixel filled, in this case 50% alpha to the left and 50% alpha to the right:

Image

But if the line goes through pixel centres, then it affects one and only one full pixel per Y coordinate. For example, the pixel centre of the pixel at (37, 14) is at (37.5, 14.5). This line goes vertically through x=37.5:

Image

Obviously, same goes for horizontal lines. So, the fix is to draw the line through pixel centres if the line thickness is odd, and pixel edges if the line thickness is even (if the line thickness is fractional, there's no way to avoid a partial overlap).

You haven't shown calcPosition() but I suspect it returns integer coordinates, or at least it doesn't consider line thickness. Edit: Sorry, I noticed the .love after posting. I've taken a look. The problem is that you have this line:

Code: Select all

        if minborder > 0 then love.graphics.setLineWidth(minborder) else love.graphics.setLineWidth(1) end
However you make the calculations using minborder, which is 0, causing the line thickness and pixel coordinates to be miscalculated, leading to the problem above. The fix is to replace that line with:

Code: Select all

        if minborder <= 0 then minborder = 1 end
        love.graphics.setLineWidth(minborder)
so that the actual line thickness is used for calculations.

I haven't checked the rest.

(Further edited to fix a grammar issue)
Last edited by pgimeno on Wed May 25, 2016 3:49 pm, edited 2 times in total.
timehunter
Prole
Posts: 5
Joined: Tue May 24, 2016 1:05 am

Re: drawing rectangle of a specified width with round borders inside of it

Post by timehunter »

allright pixel lines :) i've read an article about it a few days ago but didnt pay much attention or didnt understand the full thing ^^ calcposition returns a coordinate as an integer or if it's specified in a string i consider it to be a percentage position. if the value is negative, i start calculating the pos from the top / right edge.

I'm not considering line thickness in this function because I want my borders to be included inside the element i'm drawing.

Code: Select all

 
local convertPos = function(coord, axisSize)
	if type(coord) == "string" then
		local value = tonumber(coord)
		if value >= 0 then 
			return value * axisSize / 100
		else
			return axisSize + value * axisSize / 100
		end
	else
		if coord >= 0 then
			return coord
		else
			return axisSize + coord
		end
	end
end

local calcPosition = function(component)
	local x, y = 0, 0
	
	if component.parent then
		x = x + componentTable[component.parent].computed.x
		y = y + componentTable[component.parent].computed.y
	end
	x = x + convertPos(component.style.x, screen.width)
	y = y + convertPos(component.style.y, screen.height)
	return x, y
end
Thank you for your help, i'm going back to my little experimentations to get perfect boxes 8)
timehunter
Prole
Posts: 5
Joined: Tue May 24, 2016 1:05 am

Re: drawing rectangle of a specified width with round borders inside of it

Post by timehunter »

pgimeno wrote:Don't miss my edit!
i didn't, i'm glad you took time to help resolve my problems (and greatly increasing my undestanding of love coordinates system :))

edit : the problem is now completely solved, but i had to launch a trick at the engine to get rid of those blurry lines... the math for positions was correct as described in the first post, but integer border sizes always cross the famous pixel lines, the solution is to always withdraw 0.5 to the border width when you call love.setLineWidth()

the working boxDraw function looks like this :

Code: Select all

local boxDraw = function(box)
	local minborder = math.min(box.computed.borderL, box.computed.borderT, box.computed.borderR, box.computed.borderB)
	local maxborder = math.max(box.computed.borderL, box.computed.borderT, box.computed.borderR, box.computed.borderB)

	if minborder == 0 then minborder = 1 end

	love.graphics.setColor(box.style.bgcolor)
	love.graphics.rectangle("fill", box.computed.x + minborder / 2, box.computed.y + minborder / 2, box.computed.width - minborder, box.computed.height - minborder, box.style.border_radius, box.style.border_radius)

	if maxborder > 0 then love.graphics.setColor(box.style.border_color) end

	love.graphics.setLineWidth(minborder - 0.5) -- withdrawing 0.5 so that the line's width never crosses pixel lines. this feels kinda weird but seems to be the only solution to obtain clean lines

	love.graphics.rectangle("line", box.computed.x + minborder / 2, box.computed.y + minborder / 2, box.computed.width - minborder, box.computed.height - minborder, box.style.border_radius, box.style.border_radius)

	if box.style.border_radius == 0 then
		if box.computed.borderL > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y, box.computed.borderL, box.computed.height)
		end
		if box.computed.borderT > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y, box.computed.width, box.computed.borderT)
		end
		if box.computed.borderR > minborder then
			love.graphics.rectangle("fill", box.computed.x + box.computed.width - box.computed.borderR, box.computed.y, box.computed.borderR, box.computed.height)
		end
		if box.computed.borderB > minborder then
			love.graphics.rectangle("fill", box.computed.x, box.computed.y + box.computed.height - box.computed.borderB, box.computed.width, box.computed.borderB)
		end
	end
end
Post Reply

Who is online

Users browsing this forum: No registered users and 96 guests