CanvasDraw - A powerful pixel-based graphics library (Draw pixels, lines, triangles, read/modify image data, and much more!)


Now I run into the issue where it doesn’t seem to fill the cursor boundary. Have you got any ideas on how I might fix this?

Nevermind fixed it by just adding in the radius // 10. Idk why that works but it does.

ignore ^

function Canvas:FillCircle(circle:Frame, Colour:Color3)
		
		local Position = circle.AbsolutePosition --Top left of circle
		
		local CenterX = Position.X + (circle.AbsoluteSize.X / 2)
		local CenterY = Position.Y + (circle.AbsoluteSize.Y / 2)
		
		
	
		local RelativePosition = Vector2New(CenterX, CenterY)
		RelativePosition = Canvas:GetPointFromPosition(RelativePosition,false) --Convert it into pixel space. Not clamped
		
		
		local StartX = Canvas:GetPointFromPosition(Position,false) --Find the top left in pixel space 
		
		local Radius = RelativePosition.X - StartX.X --Minus to get the radius in pixels
	
		return Canvas:DrawCircle(RelativePosition,Radius,Colour,true) --Return drawing the circle
	end
	

I came up a script which basically finds the pixel size of the radius of the pointer itself and then slightly tweaked the GetPointFromPosition to not clamp the values.

Thanks so much for you previous I probably wouldn’t of thought of this without that!

1 Like

Hi! I come here with two feature requests that would be helpful for folks trying to use the DrawImageRect function for 2D projects:

  1. Transparent Color Filter
    An image should have an Optional Color3 variable that if set should make it so any pixel similar to that color (FuzzyEq Vector3) would be completely invisible (return R,G,B,0)

  2. Alpha Blending/Transparency
    The cool thing about the Image Rect function is that you can easily manipulate it to also make animated sprite sheet characters. Unfortunately I have stumbled upon a limitation where any pixels with alpha would be either drawn as 0 or 1 with no in-betweens. It would be cool if there was an optional boolean argument that would blend the color that was already there for a simple sort of “transparency” effect.

1 Like

I’ve added it myself cuz i was bored and found something similar in a different function.
This adds Transparency as an optional argument for DrawTexturedTriangleXY and DrawImageRectXY.

The catch is that since this implementation adds a new argument to texturetriXY, any older project converting to the new one would have to remember to add that extra argument. so it’s really up to you on adding it (I.e break backwards compatibility)

but regardless, it’s a small edit but it serves my usecase and maybe it would for others.

function Canvas:DrawTexturedTriangleXY(
	X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number,
	U1: number, V1: number, U2: number, V2: number, U3: number, V3: number,
	ImageData, Brightness: number?, TransparenyEnabled: boolean?
)
	local TexResX, TexResY = ImageData.Width, ImageData.Height

	if Y2 < Y1 then
		Y1, Y2 = Swap(Y1, Y2)
		X1, X2 = Swap(X1, X2)
		U1, U2 = Swap(U1, U2)
		V1, V2 = Swap(V1, V2)
	end

	if Y3 < Y1 then
		Y1, Y3 = Swap(Y1, Y3)
		X1, X3 = Swap(X1, X3)
		U1, U3 = Swap(U1, U3)
		V1, V3 = Swap(V1, V3)
	end

	if Y3 < Y2 then
		Y2, Y3 = Swap(Y2, Y3)
		X2, X3 = Swap(X2, X3)
		U2, U3 = Swap(U2, U3)
		V2, V3 = Swap(V2, V3)
	end

	if Y3 == Y1 then
		Y3 += 1
	end
	
	Brightness = Brightness or 1

	local dy1 = Y2 - Y1
	local dx1 = X2 - X1
	local dv1 = V2 - V1
	local du1 = U2 - U1

	local dy2 = Y3 - Y1
	local dx2 = X3 - X1
	local dv2 = V3 - V1
	local du2 = U3 - U1

	local TexU, TexV = 0, 0

	local dax_step, dbx_step = 0, 0
	local du1_step, dv1_step = 0, 0
	local du2_step, dv2_step = 0, 0

	dax_step = dx1 / math.abs(dy1)
	dbx_step = dx2 / math.abs(dy2)

	du1_step = du1 / math.abs(dy1)
	dv1_step = dv1 / math.abs(dy1)

	du2_step = du2 / math.abs(dy2)
	dv2_step = dv2 / math.abs(dy2)

	local function Plotline(ax, bx, tex_su, tex_eu, tex_sv, tex_ev, Y, IsBot)
		if ax > bx then
			ax, bx = Swap(ax, bx)
			tex_su, tex_eu = Swap(tex_su, tex_eu)
			tex_sv, tex_ev = Swap(tex_sv, tex_ev)
		end

		local ScanlineLength = bx - ax
		if ScanlineLength == 0 then return end -- Avoid divide-by-zero

		-- Calculate UV increments per pixel
		local Step = 1 / ScanlineLength
		local du = (tex_eu - tex_su) * Step
		local dv = (tex_ev - tex_sv) * Step

		-- Clip X right
		if bx > self.CurrentResX then
			ScanlineLength = self.CurrentResX - ax
		end
		
		-- Clip X left
		local StartOffsetX = 0
		local t = 0
		
		if ax < 1 then	
			StartOffsetX = -(ax - 1)
			t = Step * StartOffsetX
		end

		-- Initialize UV coordinates with offset
		TexU = tex_su + t * (tex_eu - tex_su)
		TexV = tex_sv + t * (tex_ev - tex_sv)
		
		TexU = TexU * TexResX + 1
		TexV = TexV * TexResY + 1
		
		du *= TexResX
		dv *= TexResY

		-- Main loop to draw pixels across the scanline
		if TransparenyEnabled then
			-- Perform alpha blending
			for j = StartOffsetX, ScanlineLength do
				local SampleX = ClampN(FloorN(TexU), 1, TexResX)
				local SampleY = ClampN(FloorN(TexV), 1, TexResY)

				local ImgR, ImgG, ImgB, ImgA = ImageData:GetRGBA(SampleX, SampleY)

				if ImgA > 0 then -- No need to do any calculations for completely transparent pixels
					local BgR, BgG, BgB = InternalCanvas:GetRGB(ax + j, Y)

					if ImgA < 1 then
						ImgR = Lerp(BgR, ImgR, ImgA)
						ImgG = Lerp(BgG, ImgG, ImgA)
						ImgB = Lerp(BgB, ImgB, ImgA)
					end
					
					if Brightness < 1 then
						ImgR *= Brightness
						ImgG *= Brightness
						ImgB *= Brightness
					end

					InternalCanvas:SetRGB(ax + j, Y, ImgR, ImgG, ImgB)
				end
				
				-- Increment UV values
				TexU += du
				TexV += dv
			end
		else
			-- Normal render loop
			for j = StartOffsetX, ScanlineLength do
				local SampleX = ClampN(FloorN(TexU), 1, TexResX)
				local SampleY = ClampN(FloorN(TexV), 1, TexResY)
				
				local R, G, B, A = ImageData:GetRGBA(SampleX, SampleY)

				if Brightness < 1 then
					R *= Brightness
					G *= Brightness
					B *= Brightness
				end

				InternalCanvas:SetRGB(ax + j, Y, R, G, B)

				-- Increment UV values
				TexU += du
				TexV += dv
			end
		end
	end
	
	-- Clip Y top
	local YStart = 1
	
	if Y1 < 1 then
		YStart = 1 - Y1
	end
	
	-- Clip Y top
	local TopYDist = math.min(Y2 - Y1, self.CurrentResY - Y1)

	-- Draw top triangle
	for i = YStart, TopYDist do
		--task.wait(1)
		local ax = RoundN(X1 + i * dax_step)
		local bx = RoundN(X1 + i * dbx_step)

		-- Start values
		local tex_su = U1 + i * du1_step
		local tex_sv = V1 + i * dv1_step

		-- End values
		local tex_eu = U1 + i * du2_step
		local tex_ev = V1 + i * dv2_step

		-- Scan line
		Plotline(ax, bx, tex_su, tex_eu, tex_sv, tex_ev, Y1 + i)
	end

	dy1 = Y3 - Y2
	dx1 = X3 - X2
	dv1 = V3 - V2
	du1 = U3 - U2

	dax_step = dx1 / math.abs(dy1)
	dbx_step = dx2 / math.abs(dy2)

	du1_step, dv1_step = 0, 0

	du1_step = du1 / math.abs(dy1)
	dv1_step = dv1 / math.abs(dy1)

	-- Draw bottom triangle
	
	-- Clip Y bottom
	local BottomYDist = math.min(Y3 - Y2, self.CurrentResY - Y2)
	
	local YStart = 0

	if Y2 < 1 then
		YStart = 1 - Y2
	end
	
	for i = YStart, BottomYDist do
		i = Y2 + i
		--task.wait(1)
		local ax = RoundN(X2 + (i - Y2) * dax_step)
		local bx = RoundN(X1 + (i - Y1) * dbx_step)

		-- Start values
		local tex_su = U2 + (i - Y2) * du1_step
		local tex_sv = V2 + (i - Y2) * dv1_step

		-- End values
		local tex_eu = U1 + (i - Y1) * du2_step
		local tex_ev = V1 + (i - Y1) * dv2_step

		Plotline(ax, bx, tex_su, tex_eu, tex_sv, tex_ev, i, true)
	end
end

function Canvas:DrawImageRectXY(ImageData: {}, X: number, Y: number, 
	RectOffsetX: number, RectOffsetY: number, RectSizeX: number, RectSizeY: number, 
	ScaleX: number?, ScaleY: number?, Angle: number?, TransparencyEnabled: boolean?
) -- Contributed by @DukeAunarky
	
	ScaleX = ScaleX or 1
	ScaleY = ScaleY or 1
	Angle = Angle or 0

	local PivotX, PivotY = X, Y

	local ImageSizeX, ImageSizeY = ImageData.Width, ImageData.Height
	local ImageScaledSizeX, ImageScaledSizeY = ImageSizeX * ScaleX, ImageSizeY * ScaleY

	local CosTheta, SinTheta = math.cos(Angle), math.sin(Angle)

	local function RotatePoint(X, Y)
		-- Rotation maths
		local RotX = (CosTheta * (X - PivotX) - SinTheta * (Y - PivotY) + PivotX)
		local RotY = (SinTheta * (X - PivotX) + CosTheta * (Y - PivotY) + PivotY)

		return math.floor(RotX), math.floor(RotY)
	end

	local X1, Y1 = RotatePoint(X, Y)
	local X2, Y2 = RotatePoint(X + ImageScaledSizeX - 1, Y)
	local X3, Y3 = RotatePoint(X + ImageScaledSizeX - 1, Y + ImageScaledSizeY - 1)
	local X4, Y4 = RotatePoint(X, Y + ImageScaledSizeY - 1)

	local Padding = 0.001 -- Small padding to prevent bleeding

	local U1, V1 = (RectOffsetX + Padding) / ImageSizeX, (RectOffsetY + Padding) / ImageSizeY
	local U2, V2 = (RectOffsetX + RectSizeX - Padding) / ImageSizeX, (RectOffsetY + Padding) / ImageSizeY
	local U3, V3 = (RectOffsetX + RectSizeX - Padding) / ImageSizeX, (RectOffsetY + RectSizeY - Padding) / ImageSizeY
	local U4, V4 = (RectOffsetX + Padding) / ImageSizeX, (RectOffsetY + RectSizeY - Padding) / ImageSizeY

	Canvas:DrawTexturedTriangleXY(
		X1, Y1, X2, Y2, X3, Y3,
		U1, V1, U2, V2, U3, V3,
		ImageData, 1, TransparencyEnabled
	)

	Canvas:DrawTexturedTriangleXY(
		X1, Y1, X4, Y4, X3, Y3,
		U1, V1, U4, V4, U3, V3,
		ImageData, 1, TransparencyEnabled
	)
end

Also upon testing it out, there’s this very weird seam looking thing, and I’m gonna assume those are the two triangles overlapping ever so slightly from DrawTriangle
RobloxStudioBeta_NoVfRqeOxx

Alpha blending is definitely something I planned for the other textured methods, I just never really added it due to performance reasons before CanvasDraw v4.0.

But we’ve gotten to a point where CanvasDraw is so fast, I can add in more complex stuff such as alpha blending without having much of a performance impact at all.

As for the transparent color filter, i’m not quite sure what you’d use that for? It’s a really specific suggestion. I’ll keep a note for it however.

1 Like

This is the main reason why I never added alpha blending, as you will always have a seam. Not just caused from alpha blending a 4-point image, but from normal use in a 3D engine via DrawTexturedTriangle.

I’m not quite sure how one would go about fixing this

Primary use-case is spritesheets with a background color that are programmed to be ignored when being drawn, a very common practice (surprisingly) when devs make spritesheets, and many programs (eg: Tiled) supports a color being ignored.

1 Like

then not a bug then, its a feature :fire:
My main motive for adding alpha blending was because pixel art could have “smoothing” on the edges with slightly transparent colors, but rather than being rendered with alpha it’s binary [0-1 transparency] thus resulting in something like this happening:

Adding it, while may be never perfect, was indeed the solution :slight_smile:
RobloxStudioBeta_wC3YtaltYe

1 Like

Ah, yeah that makes sense. You could just modify your image to have transparent pixels instead. My main concern with implementing something like this is the immediate extra per pixel computation. I imagine FuzzyEq Vector3 would not be suitable for sampling every pixel in the image at a high res.

So for now, it’s just an idea i’ll note down

1 Like

I think I could make alpha blending work under very specific scenarios. Specifically for DrawDistortedImage and DrawImageRect. I’ll keep you up to date, i’ll test out some ideas

1 Like

I’m not sure if this is even fixable but I might report it to see if this is doable:
When resizing the sprites (even when just multiplying with base numbers like x2, x3, x4, etc) there tends to be artifacting/“missing pixels” or “extra pixels” when being drawn at resolutions not original to its source

Multiplied by x2:
RobloxStudioBeta_hZKjR2xfIC

It should look like this:
paintdotnet_tfdLHzwJXf

That’s odd… I never had that issue. Base numbers shouldn’t be affected by something like that. I’ll take a look at it later

1 Like

your triangle rasterizer is not supposed to be drawing pixels that are on the bottom-right edge, which is why its overdrawing on that image

Yeah, i’d have to make a special case for my 4 point image method

1 Like

this is for any given triangle, so you need to directly add it to your draw triangle functions

Oh? How do I do that without having an issue with triangle corners not exactly meeting the given 3 points? This was one of the reasons I kind of left :DrawTexturedTriangle as is

(Apologies if this was already answered)

Will there be Wally support for this in the future?

As currently get image data from texture is limited to v 4.0 I made a rudimentary version which uses v 3. It’s quite laggy and you need the actual url of the image not the assetid.

Proxy Code: Glitch :・゚✧

(do not use this proxy in the example I will be password protecting at some point)

Example code for getting all of a users friends

local HttpService = game:GetService('HttpService')
local RunService = game:GetService('RunService')

local CanvasDraw = require(game.ReplicatedStorage.Modules.CanvasDraw)

local EndPoint = "https://thumbnails.roproxy.com/v1/users/avatar-headshot?userIds={userId}&size=180x180&format=Png&isCircular=false"



local function GetAllFriendHeadshots(id)
	
	
	
	local success, AllFriends = pcall(function() return game.Players:GetFriendsAsync(id) end)
	
	if not success then
		warn(AllFriends)
		return nil
	end
	
	local Ids = ""
	
	repeat
		
		for i,v in pairs(AllFriends:GetCurrentPage()) do
			Ids = Ids..v.Id..","
		end
		
		if not AllFriends.IsFinished then
			AllFriends:AdvanceToNextPageAsync()
		end
		
	until AllFriends.IsFinished == true
	
	
	
	local Url = EndPoint:gsub("{userId}",Ids)
	local success,res = pcall(function()
		return HttpService:JSONDecode(HttpService:GetAsync(Url))
	end)
	
	if not success then
		warn(res)
		return nil
	else
		local Stuff = {}
		
		for i,v in pairs(res.data) do
			table.insert(Stuff,v.imageUrl)
		end
		return Stuff
	end
end

local function FastWait(Count) -- Avoid lag spikes
	
	local FastWaitCount = 0
	
	if FastWaitCount >= Count then
		FastWaitCount = 0
		RunService.Heartbeat:Wait()
	else
		FastWaitCount += 1
	end
end

local function Simplify(data)
	
	local Newpixels = {}
	local NewAlphas = {}
	
	
	
	for i,v in pairs(data.colours) do
		pcall(function()
			--print(Color3.fromRGB(table.unpack(v)))
			table.insert(Newpixels,Color3.fromRGB(table.unpack(v)))
		end)
		FastWait(4000)
	end
	
	for i,v in pairs(data.alphas) do
		pcall(function()
			--print(v / 255)
			table.insert(NewAlphas,v)
		end)
		FastWait(4000)
	end
	
	
	
	return Newpixels,NewAlphas
end

local function GetImageData(link)
	
	local url = "https://image-to-pixels-proxy.glitch.me/image-to-pixel?url="..link 
	
	local success,res = pcall(function()
		return HttpService:JSONDecode(HttpService:GetAsync(url))
	end)
	
	if not success then
		warn(res)
		return nil
	else
		
		local Pixels,Alphas = Simplify(res)
		
		local CanvasData = {
			ImageColours = Pixels,
			ImageAlphas = Alphas,
			ImageResolution = Vector2.one * 180
		}
		
		local obj = CanvasDraw.CreateSaveObject(CanvasData,true)
		obj.Parent = game.Workspace.Saves
		obj.Name = link
		
		
		return res
	end
	
end

local Main = {}

function Main.CreateSaveOfFriends(userId)
	local SaveObject = {}
	local AllFriends = GetAllFriendHeadshots(userId)
	
	
	
	for i,v in pairs(AllFriends) do
		if not v then continue end
		coroutine.wrap(GetImageData)(v)
		FastWait(500)
	end
	
end


return Main

you can use an png image decoder to get it without needing the proxy