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

Oh, that’s odd. You should be able to draw on the entire surface. Just as long as it’s a either the front of back part of a surface GUI

Is this for version 4.0?? cause if so then please do free to send the fix! I will mention you as a contributor!

1 Like

Yes it indeed is, it actually took me some time to find the exact cause of the issue, it does not happen usually. The issue happens when you put a surface inside a ScreenGui. I don’t have an exact explanation on why the Mouse position bugs happen if for that specific scenario, but I suppose its because the ScreenGui affects the absolute sizes and positions very weirdly.

Here is a video of mine showcasing it
Watch 0531 | Streamable

Fixed function + place showcasing the issue/bug

	function Canvas:GetMousePoint(): Vector2?
		if RunService:IsClient() then
			local MouseLocation = UserInputService:GetMouseLocation()
			local GuiInset = game.GuiService:GetGuiInset()

			local CanvasFrameSize = self.CurrentCanvasFrame.AbsoluteSize
			local FastCanvasFrameSize = self.CurrentCanvasFrame.FastCanvas.AbsoluteSize
			local CanvasPosition = self.CurrentCanvasFrame.AbsolutePosition

			local SurfaceGui = Frame:FindFirstAncestorOfClass("SurfaceGui")

			MouseLocation -= GuiInset

			if not SurfaceGui then
				-- Gui
				local MousePoint = MouseLocation - CanvasPosition

				local TransformedPoint = (MousePoint / FastCanvasFrameSize) -- Normalised

				TransformedPoint *= self.Resolution -- Canvas space

				-- Make sure everything is aligned when the canvas is at different aspect ratios
				local RatioDifference = Vector2New(CanvasFrameSize.X / FastCanvasFrameSize.X, CanvasFrameSize.Y / FastCanvasFrameSize.Y) - Vector2New(1, 1)
				TransformedPoint -= (RatioDifference / 2) * self.Resolution
				
				local UnroundedVec = TransformedPoint
				local RoundX = math.ceil(TransformedPoint.X)
				local RoundY = math.ceil(TransformedPoint.Y)
				
				TransformedPoint = Vector2.new(RoundX, RoundY)
				
				-- If the point is within the canvas, return it.
				--if TransformedPoint.X > 0 and TransformedPoint.Y > 0 and TransformedPoint.X <= self.CurrentResX and TransformedPoint.Y <= self.CurrentResY then
					return TransformedPoint
				--end
			else
				-- SurfaceGui
				local Part = SurfaceGui.Adornee or SurfaceGui:FindFirstAncestorWhichIsA("BasePart") 
				local Camera = workspace.CurrentCamera

				local FastCanvasFrame = Frame:FindFirstChild("FastCanvas")

				if Part and FastCanvasFrame then
					local mouse = UserInputService:GetMouseLocation()
					local params = RaycastParams.new()
					params.FilterDescendantsInstances = {Part}
					params.FilterType = Enum.RaycastFilterType.Include

					local unitRay = Camera:ViewportPointToRay(mouse.X, mouse.Y)
					unitRay = Ray.new(unitRay.Origin, unitRay.Direction*1000)

					local result = workspace:Raycast(unitRay.Origin, unitRay.Direction) --, params) you can enable them but my system does not need it

					if result and result.Instance == Part then
						local topleftCFrame = Part.CFrame * CFrame.new(Part.Size.X/2, Part.Size.Y/2, -Part.Size.Z/2)
						local mouseCFrame = CFrame.lookAt(result.Position, result.Position+(result.Normal or Vector3.one)) * CFrame.Angles( 0, 0, math.rad(90) )
						local RelCF = topleftCFrame:ToObjectSpace(mouseCFrame)
						local res = self.Resolution

						return RoundPoint(Vector2.new(
							math.clamp( math.abs(RelCF.X)/Part.Size.X*res.X, 0, res.X ),
							math.clamp( math.abs(RelCF.Y)/Part.Size.Y*res.Y, 0, res.Y )
						))
					end

					--[[ old method
					if Result then
						local Normal = Result.Normal
						local IntersectionPos = Result.Position

						if VectorFuncs.normalVectorToFace(Part, Normal) ~= SurfaceGui.Face then
							return
						end

						-- Credits to @Krystaltinan for some of this code
						local hitCF = CFrame.lookAt(IntersectionPos, IntersectionPos + Normal)

						local topLeftCorners = VectorFuncs.getTopLeftCorners(Part)
						local topLeftCFrame = topLeftCorners[SurfaceGui.Face]

						local hitOffset = topLeftCFrame:ToObjectSpace(hitCF)

						local ScreenPos = Vector2.new(
							math.abs(hitOffset.X), 
							math.abs(hitOffset.Y)
						)

						-- Ensure the calculations work for all faces
						if SurfaceGui.Face == Enum.NormalId.Front or SurfaceGui.Face == Enum.NormalId.Back then
							ScreenPos -= Vector2.new(Part.Size.X / 2, Part.Size.Y / 2)
							ScreenPos /= Vector2.new(Part.Size.X, Part.Size.Y)
						else
							return -- Other faces don't seem to work for now
						end

						local PositionalOffset
						local AspectRatioDifference = FastCanvasFrameSize / CanvasFrameSize
						local SurfaceGuiSizeDifference = SurfaceGui.AbsoluteSize / CanvasFrameSize

						--print(SurfaceGuiSizeDifference)

						local PosFixed = ScreenPos + Vector2.new(0.5, 0.5) -- Move origin to top left

						ScreenPos = PosFixed * SurfaceGui.AbsoluteSize -- Convert to SurfaceGui space

						ScreenPos -= CanvasPosition

						local TransformedPoint = (ScreenPos / FastCanvasFrameSize) -- Normalised

						TransformedPoint *= self.Resolution -- Canvas space
						TransformedPoint += Vector2.new(0.5, 0.5)

						-- Make sure everything is aligned when the canvas is at different aspect ratios
						local RatioDifference = Vector2New(CanvasFrameSize.X / FastCanvasFrameSize.X, CanvasFrameSize.Y / FastCanvasFrameSize.Y) - Vector2New(1, 1)
						TransformedPoint -= (RatioDifference / 2) * self.Resolution
						TransformedPoint = RoundPoint(TransformedPoint)

						-- If the point is within the canvas, return it.
						if TransformedPoint.X > 0 and TransformedPoint.Y > 0 and TransformedPoint.X <= self.CurrentResX and TransformedPoint.Y <= self.CurrentResY then
							return TransformedPoint
						end

						return TransformedPoint
					end
					]]
				end	
			end
		else
			OutputWarn("Failed to get point from mouse (you cannot use this function on the server. Please call this function from a client script).")
		end
	end

Here is the testing place (118.6 KB)

So yea I hope my contribution was helpful for those with or without the issue.
If there is anything you need to know feel free to ask

1 Like

Your use case of having a SurfaceGui in a BillboardGui is a really rare. And your fix is almost perfect, but you forgot to take into account the frame’s possible size and offset in the SurfaceGUI as shown below:

This probably explains why my version was so lengthy, but if you could add this into your code I will be sure to implement this into the next version and mention you!

Also thanks for that detailed explanation! The effort you put into this is nice, highly appreciate it!

I completely agree that the use case here is pretty rare, I was confused when I did not happen to have the issue when trying to replicate it on a testing place. It took me a few hours determining the issue but I got it.

Thank you for your response, I’ll see how I can implement the offset checks without breaking the function again

1 Like

Alright awesome! If you manage to keep it quite a bit shorter than the original one even with those extra checks, ill definitely add that contribution. Also a hint to help you with that:

AbsoluteSize, AbsolutePosition, Percentages of 0 to 1


Shouldn’t be too hard to implement, I’d probably figure out how to it myself, but I just don’t have the time today. Thanks again for your help!

1 Like

You’re welcome, so I have tried making sure that it takes offsets in consideration but with no success. I only managed to make sure that there is no weird offset if the scale is 1,0,1,0 or not but if the position is not 0,0,0,0 then there is a huge mouse offset which I tried to get fixed but I can’t figure out how:

	function Canvas:GetMousePoint(): Vector2?
		if RunService:IsClient() then
			local MouseLocation = UserInputService:GetMouseLocation()
			local GuiInset = game.GuiService:GetGuiInset()

			local CanvasFrameSize = self.CurrentCanvasFrame.AbsoluteSize
			local FastCanvasFrameSize = self.CurrentCanvasFrame.FastCanvas.AbsoluteSize
			local CanvasPosition = self.CurrentCanvasFrame.AbsolutePosition

			local SurfaceGui = Frame:FindFirstAncestorOfClass("SurfaceGui")

			MouseLocation -= GuiInset

			if not SurfaceGui then
				-- Gui
				local MousePoint = MouseLocation - CanvasPosition

				local TransformedPoint = (MousePoint / FastCanvasFrameSize) -- Normalised

				TransformedPoint *= self.Resolution -- Canvas space

				-- Make sure everything is aligned when the canvas is at different aspect ratios
				local RatioDifference = Vector2New(CanvasFrameSize.X / FastCanvasFrameSize.X, CanvasFrameSize.Y / FastCanvasFrameSize.Y) - Vector2New(1, 1)
				TransformedPoint -= (RatioDifference / 2) * self.Resolution
				
				local UnroundedVec = TransformedPoint
				local RoundX = math.ceil(TransformedPoint.X)
				local RoundY = math.ceil(TransformedPoint.Y)
				
				TransformedPoint = Vector2.new(RoundX, RoundY)
				
				-- If the point is within the canvas, return it.
				--if TransformedPoint.X > 0 and TransformedPoint.Y > 0 and TransformedPoint.X <= self.CurrentResX and TransformedPoint.Y <= self.CurrentResY then
					return TransformedPoint
				--end
			else
				-- SurfaceGui
				local Part = SurfaceGui.Adornee or SurfaceGui:FindFirstAncestorWhichIsA("BasePart") 
				local Camera = workspace.CurrentCamera

				local FastCanvasFrame = Frame:FindFirstChild("FastCanvas")

				if Part and FastCanvasFrame then
					local mouse = UserInputService:GetMouseLocation()
					local params = RaycastParams.new()
					params.FilterDescendantsInstances = {Part}
					params.FilterType = Enum.RaycastFilterType.Include

					local unitRay = Camera:ViewportPointToRay(mouse.X, mouse.Y)
					unitRay = Ray.new(unitRay.Origin, unitRay.Direction*1000)

					local result = workspace:Raycast(unitRay.Origin, unitRay.Direction, params)

					if result and result.Instance == Part then
						local mouseCFrame = CFrame.lookAt(result.Position, result.Position + (result.Normal or Vector3.one)) * CFrame.Angles(0, 0, math.rad(90))
						local RelCF = (Part.CFrame * CFrame.new(Part.Size.X / 2, Part.Size.Y / 2, -Part.Size.Z / 2)):ToObjectSpace(mouseCFrame)

						local surfaceGuiSize = SurfaceGui.AbsoluteSize
						local sizeRatio = Vector2.new(
							surfaceGuiSize.X / FastCanvasFrameSize.X,
							surfaceGuiSize.Y / FastCanvasFrameSize.Y
						)

						local correctedX = math.clamp(math.abs(RelCF.X) / Part.Size.X * surfaceGuiSize.X, 0, surfaceGuiSize.X)
						local correctedY = math.clamp(math.abs(RelCF.Y) / Part.Size.Y * surfaceGuiSize.Y, 0, surfaceGuiSize.Y)

						local finalX = correctedX * sizeRatio.X
						local finalY = correctedY * sizeRatio.Y

						local additionalOffsetX = (surfaceGuiSize.X - FastCanvasFrameSize.X) / 2
						local finalPoint = Vector2.new(finalX - additionalOffsetX, finalY)
						
						if finalPoint.X > 0 and finalPoint.Y > 0 and finalPoint.X <= self.CurrentResX and finalPoint.Y <= self.CurrentResY then
							return RoundPoint(finalPoint)
						else
							return nil
						end
					end
				end	
			end
		else
			OutputWarn("Failed to get point from mouse (you cannot use this function on the server. Please call this function from a client script).")
		end
	end

Thats how far I have gotten, also after some more digging around I found out that the reason for this huge offset in the default version (if the surfaceGui is inside a ScreenGui) is because of a bug from roblox. For no reason what so ever, roblox’s engine decides to add the Gui inset to 2d-instances that are inside a surface gui (if the surfaceGui is in a screenGui) no matter if the screenGui has “IgnoreGuiInset” toggled or not. I will probably file a bug report regarding that.

1 Like

CanvasDraw Patch - v4.2.1.b

  • Fixed a the outline versions of DrawCircle and DrawCircleXY not drawing and clipping correctly

WOW THIS IS REALLY COOL!!! how long to render this?

1 Like

Finally ran into a case to use this module! (Thank you good man)

1 Like

I wanted to make a horror game with the same graphics as you showed in the beginning

1 Like

Any ideas why the script errors when I call canvas.new? Thank you so much!

You need the editable image beta feature enabled in your settings in studio.

EditableImage isn’t released fully

That video is showcasing a raycaster engine I made. There are plenty of online tutorials that could be easily done in luau with CanvasDraw

1 Like

Module and Plugin Update v4.3.0

Hey all, been a bit since an update has happened. I’ve added some small but very useful features to both the module and the image tools plugin!


  • Added Canvas:SetClearRGBA() to change the clearing colour and transparency

  • Added two new optional parameters to CanvasDraw.GetImageDataFromTextureId() to limit the image size while maintaining the aspect ratio: MaxWidth and MaxHeight

  • Added SetRGBA() and GetRGBA() to ‘ImageData’

  • Added two new filters to the CanvasDraw tools image editor plugin; Adjust Brightness and Colour Tint to help adjust colours and light levels.

2 Likes

It’s definitely possible to make a renderer within Roblox. I recently decided to make one, which offers good performance


The only thing I wonder, is how I can do z buffers or some culling occlusion method.

1 Like

I didn’t say it wasn’t possible. This graphics library can do real-time renderers really efficiently. Here’s two very powerful renderers I made with CanvasDraw:

Raycaster engine (runs above I think 160 FPS at 240x240)

Real-time raytracing with textures and unique rendering optimisation tricks to get above 120 FPS at 100x100

You should probably get back-face culling working first, as I dont think you’re doing that.

This video covers culling very well, and there’s a part 3 which covers clipping of triangles, which is kind of occlusion culling in a way, but it means you can have the camera close to the triangles without them going huge or running into divide by 0 issues.


Here’s a 3D OBJ renderer I made around a year ago that uses these implementations:

This was made before EditableImage

1 Like

Module and Plugin Patch v4.3.1

Just fixed an error on the plugin and fixed and fixed an where the Canvas.FpsLimit property does not function quite right


Canvas Changes

  • Canvas.FpsLimit is now write safe!

  • Deprecated Canvas:SetFPSLimit() in favour of Canvas.FpsLimit no longer being read-only

Plugin Changes

  • Increased the speed of mass importing SaveObjects

  • Fixed a rare occasion where importing a specific formatted PNG may break the plugin causing the gui to get stuck on the screen.

Hello! Did one of these updates effect 3.0? In Draw & Donate the canvases are showing strange blurry lines I’ve never seen before, I didn’t change anything in the game they randomly just showed up. Also accept my DC friend request if you’re able to, I have a few more questions about this module! I love this module and would like to learn more about it to improve my game. Thank you!

Hello. This is a roblox bug

CanvasDraw 3.0 hasn’t been updated since November 2023


I sure hope this gets fixed soon

1 Like

This issue appears to be fixed!

1 Like