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

Hey man, first of all thank you very much for creating this. You saved me from endless hours of working on my own drawing system inside roblox, but I have a question:

I am using the legacy module for my drawing system and it runs very smoothly, but how would I set the canvas alpha to be transparent during creation? Like when I create a canva, the pixels are not transparent by default but I need them to be transparent. Thank you for any responses before hand.

1 Like

Sorry for the late reply, but the legacy version doesn’t really have transparent support, but what you could do is modify the module so upon creation of the frames with the UIGradients, you can set the transparency of the gradients

The UIGradient stuff can be found in the explorer > CanvasDraw > FastCanvas, and then open that module and go to around line 53 and set the transparency of the gradient:

image

image

1 Like

Thank you very much, I appreciate your response. I will use the Studio only version now because there seems no point for me in adding alpha support into the legacy version when there is a version which has that already. But I have a small problem, I am unable to draw on surfaces/parts, it for some reason allows me to draw on half of the surface rather than the entire surface and after a lot of digging it seems like the problem is due to the way it converts the mouse point position from 3d to 2d. I’ll try to fix this, but if you know what’s the cause it would be nice if you’d fix this.

Edit:
I fixed the issue myself, I edited the module/the function GetMousePoint, I only had to change the lines which affected the way it would get the mouse position on surface gui’s. If you want the fixed code I can give it for sure, I would love to contribute to this huge module.
Sincerely coolco

1 Like

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