:DrawLine() Thickness

While messing around with EditableImages for the first time, I noticed the DrawLine function doesn’t have a thickness/radius parameter. Is there any great way to add thickness to a line, and if not, are they planning on adding the parameter?

1 Like

The line is one pixel wide, so you can thicken it by drawing more parallel lines connecting two neighbouring pixels of p1 and p2.

I haven’t read about any plans for thickness param.

Would I do that by just increasing/decreasing the x and y by one?

Yes, by one, but depending on the angle either both or only x or y. For example, one pixel to the right or left (x-axis) on both points if the angle is 90 degrees.

At the moment I’m typing on mobile, so I can’t really experiment, but I’ve thickened the line like that before.

1 Like

You can simply use :DrawRectangle()

1 Like

DrawRectangle() would be superb if only it could be rotated.
For that I wrote some code to draw it from parallel lines.

1. Using DrawLine()

Code
local function DrawThickLine(
	p1: Vector2, p2: Vector2, color: Color3, transparency: number, thickness: number
): ()
	local angle = math.atan2(p2.Y - p1.Y, p2.X - p1.X)
	
	local side1 = math.ceil(thickness/2)
	local side2 = math.floor(thickness/2)
	
	for i=-side2, side1, 1 do
		EditableImage:DrawLine(
			p1 + Vector2.new(
				math.round(i * math.cos(math.pi/2 + angle) - 0 * math.sin(math.pi/2 + angle)),
				math.round(i * math.sin(math.pi/2 + angle) + 0 * math.cos(math.pi/2 + angle))
			),
			p2 + Vector2.new(
				math.round(i * math.cos(math.pi/2 + angle) - 0 * math.sin(math.pi/2 + angle)),
				math.round(i * math.sin(math.pi/2 + angle) + 0 * math.cos(math.pi/2 + angle))
			),
			color, transparency
		)
	end
end

Steps:

  • Calculate the signed angle between the points.
  • Place the lines on both sides of the base line (i = 0) as evenly as possible.
  • Translate each new point around the original by the calculated angle.
    math.pi/2 rad respectively 90 degrees is added to the angle because the coordinate system is rotated and the origin is in the top left corner.

DrawLine() performs anti-aliasing on each line. If you would for whatever reason like to avoid it, you should probably draw each pixel of the line yourself using WritePixels().

In action:

image

2. Using WritePixels() and Bresenham line drawing algorithm

→ (Optional and most likely not necessary.)

Bitmap images consist of square pixels. This algorithm aims for a precise approximation of pixels to draw to in order to render as straight line as possible between two pixels. Roblox engine probably uses it internally too.

I took the derivation from Wikipedia - Bresenham’s line algorithm to cover positive and negative slopes.

Code
local function PlotBresenhamLineLow(x1: number, y1: number, x2: number, y2: number): {Vector2}
	local pixels = {}
	local dx, dy = x2 - x1, y2 - y1
	local yi = 1
	local D: number, y: number
	
	if dy < 0 then
		yi = -1
		dy = -dy
	end
	D = 2 * dy - dx
	y = y1
	
	for x=x1, x2, (if x1 < x2 then 1 else -1) do
		table.insert(pixels, Vector2.new(x, y))
		if D > 0 then
			y += yi
			D = D + 2 * (dy - dx)
		else
			D = D + 2 * dy
		end
	end
	
	return pixels
end

local function PlotBresenhamLineHigh(x1: number, y1: number, x2: number, y2: number): {Vector2}
	local pixels = {}
	local dx, dy = x2 - x1, y2 - y1
	local xi = 1
	local D: number, x: number
	
	if dx < 0 then
		xi = -1
		dx = -dx
	end
	D = 2 * dx - dy
	x = x1
	
	for y=y1, y2, (if y1 < y2 then 1 else -1) do
		table.insert(pixels, Vector2.new(x, y))
		if D > 0 then
			x += xi
			D = D + 2 * (dx - dy)
		else
			D = D + 2 * dx
		end
	end
	
	return pixels
end

local function PlotBresenhamLine(p1: Vector2, p2: Vector2): {Vector2}
	local x1, y1, x2, y2 = p1.X, p1.Y, p2.X, p2.Y
	if math.abs(y2 - y1) < math.abs(x2 - x1) then
		if x1 > x2 then
			return PlotBresenhamLineLow(x2, y2, x1, y1)
		else
			return PlotBresenhamLineLow(x1, y1, x2, y2)
		end
	else
		if y1 > y2 then
			return PlotBresenhamLineHigh(x2, y2, x1, y1)
		else
			return PlotBresenhamLine(x1, y1, x2, y2)
		end
	end
end

local function DrawThickLine(
	p1: Vector2, p2: Vector2, color: Color3, transparency: number, thickness: number
): ()
	local angle = math.atan2(p2.Y - p1.Y, p2.X - p1.X)

	local side1 = math.ceil(thickness/2)
	local side2 = math.floor(thickness/2)
	
	local formattedColor = {color.R, color.G, color.B, 1}
	local pixelSize = Vector2.new(1,1)
	
	for i=-side2, side1, 1 do
		local pixels = PlotBresenhamLine(
			p1 + Vector2.new(
				math.round(i * math.cos(math.pi/2 + angle) - 0 * math.sin(math.pi/2 + angle)),
				math.round(i * math.sin(math.pi/2 + angle) + 0 * math.cos(math.pi/2 + angle))
			),
			p2 + Vector2.new(
				math.round(i * math.cos(math.pi/2 + angle) - 0 * math.sin(math.pi/2 + angle)),
				math.round(i * math.sin(math.pi/2 + angle) + 0 * math.cos(math.pi/2 + angle))
			)
		)
		for _,pixel in pixels do
			EditableImage:WritePixels(pixel, pixelSize, formattedColor)
		end
	end
end

Using this option is a trade-off between avoiding anti-aliasing and performance. I’m sure the code can be optimised in the slowest link - the drawing - by attempting to form pixels into larger areas and reducing the number of calls. Nevertheless, that is way too complicated for a readable example.

In action:

image

Looking at the second image, I’m thinking that in case of opting for aliasing, another option is to only WritePixels() to outermost pixels.

Edit @SkrubDaNub

When I give a post a like or reply in a topic, I personally prefer to keep track or be notified of new replies to see if anyone has given any additional info to what I said or liked. Since this thread is small with only the OP, Aqua, and me, and you liked the reply I’m replying to with some more info, I thought it fair to treat you as a minor participant and ping you. I hope you don’t mind. I suppose it wasn’t really necessary. :smile:

5 Likes

erm, hello. Why have you summoned me?