Quick actions

cmd+k|ctrl+k

Navigation

Languages

CanvasDraw Legacy

Snippet info

Language

Lua

Visibility

public

Author

thosewhoknow

Created

2024-01-11T19:21:54.920154Z

Updated

2024-01-11T19:21:54.920154Z

local GuiPool do
	local module = {}

	local OFF_SCREEN = UDim2.fromOffset(0, math.huge)

	local TableInsert = table.insert
	local TableRemove = table.remove

	function module.new(original: GuiObject, initSize: number?)
		initSize = initSize or 50

		local Pool = {
			_Available = table.create(initSize),
			_Source = original:Clone(),
			_Index = initSize,
		}

		for i = 1, initSize do
			Pool._Available[i] = Pool._Source:Clone()
		end

		function Pool:Get()
			local Index = self._Index

			if Index > 0 then
				local object = self._Available[Index]
				TableRemove(self._Available, Index)
				self._Index -= 1
				return object
			end

			return self._Source:Clone()
		end

		function Pool:Return(object: GuiObject)
			object.Position = OFF_SCREEN
			TableInsert(self._Available, object)
			self._Index += 1
		end

		return Pool
	end
	GuiPool = module
end

--[[

FastCanvas is a modified version of Greedy/GradientCanvas by BoatBomber. 
This version has the ability to remove blur artefacts from the original module and improve performance. 
There are also some other minor additions to fit CanvasDraw's needs.

Original module: https://github.com/boatbomber/GradientCanvas

]]

local FastCanvas do
	local module = {}

	--local Util = require(script.Util)

	-- Micro optimisations
	local TableInsert = table.insert
	local TableClear = table.clear
	local TableCreate = table.create

	local Mclamp = math.clamp

	local UDim2FromScale = UDim2.fromScale
	local ColorSeqKeyPNew = ColorSequenceKeypoint.new
	local ColorSeqNew = ColorSequence.new

	function module.new(ResX: number, ResY: number, BlurEnabled: boolean)
		local Canvas = {
			_Active = 0,
			_ColumnFrames = {},
			_UpdatedColumns = {},

			--Threshold = 2,
			--LossyThreshold = 4,
		}

		local invX, invY = 1 / ResX, 1 / ResY
		--local dist = ResY * 0.03

		-- Generate initial grid of color data
		local Grid = TableCreate(ResX)
		for x = 1, ResX do
			Grid[x] = TableCreate(ResY, Color3.new(0.98, 1, 1))
		end
		Canvas._Grid = Grid

		-- Create a pool of Frame instances with Gradients
		do
			local Pixel = Instance.new("Frame")
			Pixel.BackgroundColor3 = Color3.new(1, 1, 1)
			Pixel.BorderSizePixel = 0
			Pixel.Name = "Pixel"
			local Gradient = Instance.new("UIGradient")
			Gradient.Name = "Gradient"
			Gradient.Rotation = 90
			Gradient.Parent = Pixel

			Canvas._Pool = GuiPool.new(Pixel, ResX)
			Pixel:Destroy()
		end

		-- Create GUIs
		local Gui = Instance.new("Frame")
		Gui.Name = "GradientCanvas"
		Gui.BackgroundTransparency = 1
		Gui.ClipsDescendants = true
		Gui.Size = UDim2.fromScale(1, 1)
		Gui.Position = UDim2.fromScale(0.5, 0.5)
		Gui.AnchorPoint = Vector2.new(0.5, 0.5)

		local AspectRatio = Instance.new("UIAspectRatioConstraint")
		AspectRatio.AspectRatio = ResX / ResY
		AspectRatio.Parent = Gui

		local Container = Instance.new("Folder")
		Container.Name = "FrameContainer"
		Container.Parent = Gui

		-- Define API
		local function createGradient(colorData, x, pixelStart, pixelCount)
			local Sequence = TableCreate(#colorData)
			for i, data in colorData do
				Sequence[i] = ColorSeqKeyPNew(Mclamp(data.p / pixelCount, 0, 1), data.c)
			end

			local Frame = Canvas._Pool:Get()
			Frame.Position = UDim2FromScale(invX * (x - 1), pixelStart * invY)
			Frame.Size = UDim2FromScale(invX, invY * pixelCount)
			Frame.Gradient.Color = ColorSeqNew(Sequence)
			Frame.Parent = Container

			if Canvas._ColumnFrames[x] == nil then
				Canvas._ColumnFrames[x] = { Frame }
			else
				TableInsert(Canvas._ColumnFrames[x], Frame)
			end

			Canvas._Active += 1
		end

		function Canvas:Destroy()
			TableClear(Canvas._Grid)
			TableClear(Canvas)
			Gui:Destroy()
		end

		function Canvas:SetParent(parent: Instance)
			Gui.Parent = parent
		end

		function Canvas:SetPixel(x: number, y: number, color: Color3)
			local Col = self._Grid[x]

			if Col[y] ~= color then
				Col[y] = color
				self._UpdatedColumns[x] = Col
			end
		end

		function Canvas:GetPixel(x: number, y: number)
			--local Col = self._Grid[x]
			--if not Col then
			--	return
			--end

			return self._Grid[x][y]
		end

		function Canvas:Clear(x: number?)
			if x then
				local column = self._ColumnFrames[x]
				if column == nil then return end

				for _, object in column do
					self._Pool:Return(object)
					self._Active -= 1
				end
				TableClear(column)
			else
				for _, object in Container:GetChildren() do
					self._Pool:Return(object)
				end
				self._Active = 0
				TableClear(self._ColumnFrames)
			end
		end

		function Canvas:Render()
			for x, column in self._UpdatedColumns do
				self:Clear(x)

				local colorCount, colorData = 1, {
					{ p = 0, c = column[1] },
				}

				local pixelStart, pixelCount = 0, 0
				--local pixelStart, lastPixel, pixelCount = 0, 0, 0

				local lastColor = column[1]

				-- Compress into gradients
				for y, color in column do
					pixelCount += 1

					-- Early exit to avoid the delta check on direct equality
					if lastColor == color then
						continue
					end

					--local delta = Util.DeltaRGB(lastColor, color)
					--if delta > self.Threshold then
					local offset = y - pixelStart - 1

					--if (delta > self.LossyThreshold) or (y-lastPixel > dist) then

					TableInsert(colorData, { p = offset - 0.08, c = lastColor })
					--colorCount += 1

					TableInsert(colorData, { p = offset, c = color })
					colorCount += 2

					lastColor = color
					--lastPixel = y

					if colorCount > 18 then
						TableInsert(colorData, { p = pixelCount, c = color })
						createGradient(colorData, x, pixelStart, pixelCount)

						pixelStart = y - 1
						pixelCount = 0
						colorCount = 1
						TableClear(colorData)
						colorData[1] = { p = 0, c = color }
					end
					--end
				end

				if pixelCount + pixelStart ~= ResY then
					pixelCount += 1
				end
				TableInsert(colorData, { p = pixelCount, c = lastColor })
				createGradient(colorData, x, pixelStart, pixelCount)
			end

			TableClear(self._UpdatedColumns)
		end

		return Canvas
	end

	FastCanvas = module
end

local StringCompressor do
	-- Module by 1waffle1, optimized by boatbomber
	-- https://devforum.roblox.com/t/text-compression/163637

	local dictionary = {}
	do -- populate dictionary
		local length = 0
		for i = 32, 127 do
			if i ~= 34 and i ~= 92 then
				local c = string.char(i)
				dictionary[c], dictionary[length] = length, c
				length = length + 1
			end
		end
	end

	local escapemap = {}
	do -- Populate escape map
		for i = 1, 34 do
			i = ({ 34, 92, 127 })[i - 31] or i
			local c, e = string.char(i), string.char(i + 31)
			escapemap[c], escapemap[e] = e, c
		end
	end

	local function escape(s)
		return string.gsub(s, '[%c"\\]', function(c)
			return "\127" .. escapemap[c]
		end)
	end
	local function unescape(s)
		return string.gsub(s, "\127(.)", function(c)
			return escapemap[c]
		end)
	end

	local function copy(t)
		local new = {}
		for k, v in pairs(t) do
			new[k] = v
		end
		return new
	end

	local b93Cache = {}
	local function tobase93(n)
		local value = b93Cache[n]
		if value then
			return value
		end

		value = ""
		repeat
			local remainder = n % 93
			value = dictionary[remainder] .. value
			n = (n - remainder) / 93
		until n == 0

		b93Cache[n] = value
		return value
	end

	local b10Cache = {}
	local function tobase10(value)
		local n = b10Cache[value]
		if n then
			return n
		end

		n = 0
		for i = 1, #value do
			n = n + 93 ^ (i - 1) * dictionary[string.sub(value, -i, -i)]
		end

		b10Cache[value] = n
		return n
	end

	local function compress(text)
		local dictionaryCopy = copy(dictionary)
		local key, sequence, size = "", {}, #dictionaryCopy
		local width, spans, span = 1, {}, 0
		local function listkey(k)
			local value = tobase93(dictionaryCopy[k])
			local valueLength = #value
			if valueLength > width then
				width, span, spans[width] = valueLength, 0, span
			end
			table.insert(sequence, string.rep(" ", width - valueLength) .. value)
			span += 1
		end
		text = escape(text)
		for i = 1, #text do
			local c = string.sub(text, i, i)
			local new = key .. c
			if dictionaryCopy[new] then
				key = new
			else
				listkey(key)
				key = c
				size += 1
				dictionaryCopy[new], dictionaryCopy[size] = size, new
			end
		end
		listkey(key)
		spans[width] = span
		return table.concat(spans, ",") .. "|" .. table.concat(sequence)
	end

	local function decompress(text)
		local dictionaryCopy = copy(dictionary)
		local sequence, spans, content = {}, string.match(text, "(.-)|(.*)")
		local groups, start = {}, 1
		for span in string.gmatch(spans, "%d+") do
			local width = #groups + 1
			groups[width] = string.sub(content, start, start + span * width - 1)
			start = start + span * width
		end
		local previous

		for width, group in ipairs(groups) do
			for value in string.gmatch(group, string.rep(".", width)) do
				local entry = dictionaryCopy[tobase10(value)]
				if previous then
					if entry then
						table.insert(dictionaryCopy, previous .. string.sub(entry, 1, 1))
					else
						entry = previous .. string.sub(previous, 1, 1)
						table.insert(dictionaryCopy, entry)
					end
					table.insert(sequence, entry)
				else
					sequence[1] = entry
				end
				previous = entry
			end
		end
		return unescape(table.concat(sequence))
	end

	StringCompressor =  { Compress = compress, Decompress = decompress }
end

local TextCharacters do
	local Characters = {
		-- Numbers
		["0"] = {
			{1,1,1},
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,1,1},
		},
		["1"] = {
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,1,0},
		},
		["2"] = {
			{1,1,1},
			{0,0,1},
			{1,1,1},
			{1,0,0},
			{1,1,1},
		},
		["3"] = {
			{1,1,1},
			{0,0,1},
			{1,1,1},
			{0,0,1},
			{1,1,1},
		},
		["4"] = {
			{1,0,1},
			{1,0,1},
			{1,1,1},
			{0,0,1},
			{0,0,1},
		},
		["5"] = {
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{0,0,1},
			{1,1,1},
		},
		["6"] = {
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{1,0,1},
			{1,1,1},
		},
		["7"] = {
			{1,1,1},
			{0,0,1},
			{0,0,1},
			{0,0,1},
			{0,0,1},
		},
		["8"] = {
			{1,1,1},
			{1,0,1},
			{1,1,1},
			{1,0,1},
			{1,1,1},
		},
		["9"] = {
			{1,1,1},
			{1,0,1},
			{1,1,1},
			{0,0,1},
			{1,1,1},
		},

		-- Letters
		["a"] = {
			{1,1,1},
			{1,0,1},
			{1,1,1},
			{1,0,1},
			{1,0,1},
		},
		["b"] = {
			{1,1,0},
			{1,0,1},
			{1,1,1},
			{1,0,1},
			{1,1,1},
		},
		["c"] = {
			{1,1,1},
			{1,0,0},
			{1,0,0},
			{1,0,0},
			{1,1,1},
		},
		["d"] = {
			{1,1,0},
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,1,1},
		},
		["e"] = {
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{1,0,0},
			{1,1,1},
		},
		["f"] = {
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{1,0,0},
			{1,0,0},
		},
		["g"] = {
			{1,1,1},
			{1,0,0},
			{1,0,1},
			{1,0,1},
			{1,1,1},
		},
		["h"] = {
			{1,0,1},
			{1,0,1},
			{1,1,1},
			{1,0,1},
			{1,0,1},
		},
		["i"] = {
			{1,1,1},
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{1,1,1},
		},
		["j"] = {
			{0,0,1},
			{0,0,1},
			{0,0,1},
			{1,0,1},
			{1,1,1},
		},
		["k"] = {
			{1,0,1},
			{1,0,1},
			{1,1,0},
			{1,0,1},
			{1,0,1},
		},
		["l"] = {
			{1,0,0},
			{1,0,0},
			{1,0,0},
			{1,0,0},
			{1,1,1},
		},
		["m"] = {
			{1,0,1},
			{1,1,1},
			{1,1,1},
			{1,0,1},
			{1,0,1},
		},
		["n"] = {
			{1,1,1},
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,0,1},
		},
		["o"] = {
			{1,1,1},
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,1,1},
		},
		["p"] = {
			{1,1,1},
			{1,0,1},
			{1,1,1},
			{1,0,0},
			{1,0,0},
		},
		["q"] = {
			{1,1,1},
			{1,0,1},
			{1,0,1},
			{1,1,1},
			{0,1,0},
		},
		["r"] = {
			{1,1,1},
			{1,0,1},
			{1,1,0},
			{1,0,1},
			{1,0,1},
		},
		["s"] = {
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{0,0,1},
			{1,1,1},
		},
		["t"] = {
			{1,1,1},
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,1,0},
		},
		["u"] = {
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{1,1,1},
		},
		["v"] = {
			{1,0,1},
			{1,0,1},
			{1,0,1},
			{0,1,0},
			{0,1,0},
		},
		["w"] = {
			{1,0,1},
			{1,0,1},
			{1,1,1},
			{1,1,1},
			{1,0,1},
		},
		["x"] = {
			{1,0,1},
			{1,0,1},
			{0,1,0},
			{1,0,1},
			{1,0,1},
		},
		["y"] = {
			{1,0,1},
			{1,0,1},
			{1,1,1},
			{0,1,0},
			{0,1,0},
		},
		["z"] = {
			{1,1,1},
			{0,0,1},
			{0,1,0},
			{1,0,0},
			{1,1,1},
		},

		-- Symbols
		["!"] = {
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,0,0},
			{0,1,0},
		},
		["@"] = {
			{1,1,0},
			{0,0,1},
			{1,1,1},
			{1,0,1},
			{1,1,0},
		},
		["#"] = {
			{1,0,1},
			{1,1,1},
			{1,0,1},
			{1,1,1},
			{1,0,1},
		},
		["$"] = {
			{0,1,0},
			{1,1,1},
			{1,0,0},
			{1,1,1},
			{0,1,0},
		},
		["%"] = {
			{1,0,1},
			{0,0,1},
			{0,1,0},
			{1,0,0},
			{1,0,1},
		},
		["^"] = {
			{0,1,0},
			{1,0,1},
			{0,0,0},
			{0,0,0},
			{0,0,0},
		},
		["&"] = {
			{0,1,0},
			{1,0,1},
			{0,1,0},
			{1,0,1},
			{1,1,1},
		},
		["*"] = {
			{1,0,1},
			{0,1,0},
			{1,0,1},
			{0,0,0},
			{0,0,0},
		},
		["("] = {
			{0,1,1},
			{1,0,0},
			{1,0,0},
			{1,0,0},
			{0,1,1},
		},
		[")"] = {
			{1,1,0},
			{0,0,1},
			{0,0,1},
			{0,0,1},
			{1,1,0},
		},
		["["] = {
			{1,1,1},
			{1,0,0},
			{1,0,0},
			{1,0,0},
			{1,1,1},
		},
		["]"] = {
			{1,1,1},
			{0,0,1},
			{0,0,1},
			{0,0,1},
			{1,1,1},
		},
		["{"] = {
			{0,1,1},
			{0,1,0},
			{1,1,0},
			{0,1,0},
			{0,1,1},
		},
		["}"] = {
			{1,1,0},
			{0,1,0},
			{0,1,1},
			{0,1,0},
			{1,1,0},
		},
		["-"] = {
			{0,0,0},
			{0,0,0},
			{1,1,1},
			{0,0,0},
			{0,0,0},
		},
		["_"] = {
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{1,1,1},
		},
		["+"] = {
			{0,0,0},
			{0,1,0},
			{1,1,1},
			{0,1,0},
			{0,0,0},
		},
		["="] = {
			{0,0,0},
			{1,1,1},
			{0,0,0},
			{1,1,1},
			{0,0,0},
		},
		["<"] = {
			{0,0,1},
			{0,1,0},
			{1,0,0},
			{0,1,0},
			{0,0,1},
		},
		[">"] = {
			{1,0,0},
			{0,1,0},
			{0,0,1},
			{0,1,0},
			{1,0,0},
		},
		["?"] = {
			{1,1,0},
			{0,0,1},
			{0,1,0},
			{0,0,0},
			{0,1,0},
		},
		["."] = {
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,1,0},
		},
		[","] = {
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,1,0},
			{0,1,0},
		},
		["/"] = {
			{0,0,1},
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{1,0,0},
		},
		["|"] = {
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,1,0},
			{0,1,0},
		},
		[":"] = {
			{1,0,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{1,0,0},
		},
		[";"] = {
			{1,0,0},
			{0,0,0},
			{0,0,0},
			{1,0,0},
			{1,0,0},
		},
		['"'] = {
			{1,0,1},
			{1,0,1},
			{0,0,0},
			{0,0,0},
			{0,0,0},
		},
		["'"] = {
			{0,1,0},
			{0,1,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
		},
		["`"] = {
			{1,0,0},
			{0,1,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
		},
		["~"] = {
			{0,0,0},
			{1,1,0},
			{0,1,1},
			{0,0,0},
			{0,0,0},
		},
		[" "] = {
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
			{0,0,0},
		},
	}

	TextCharacters = Characters

end

local VectorFuncs do
	local Abs = math.abs

	function getNormalFromPartFace(part, normalId)
		return part.CFrame:VectorToWorldSpace(Vector3.FromNormalId(normalId))
	end

	function normalVectorToFace(part, normalVector)
		local normalIDs = {
			Enum.NormalId.Front,
			Enum.NormalId.Back,
			Enum.NormalId.Bottom,
			Enum.NormalId.Top,
			Enum.NormalId.Left,
			Enum.NormalId.Right
		}  

		for _, normalId in ipairs(normalIDs) do
			if getNormalFromPartFace(part, normalId):Dot(normalVector) > 0.999 then
				return normalId
			end
		end

		return nil -- None found within tolerance.
	end

	function getTopLeftCorners(part)
		local size = part.Size
		return {
			[Enum.NormalId.Front] = part.CFrame * CFrame.new(size.X/2, size.Y/2, -size.Z/2),
			[Enum.NormalId.Back] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, size.Z/2),
			[Enum.NormalId.Right] = part.CFrame * CFrame.new(size.X/2, size.Y/2, size.Z/2),
			[Enum.NormalId.Left] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, -size.Z/2),
			[Enum.NormalId.Bottom] = part.CFrame * CFrame.new(size.X/2, -size.Y/2, size.Z/2),
			[Enum.NormalId.Top] = part.CFrame * CFrame.new(-size.X/2, size.Y/2, size.Z/2)
		}
	end

	function getRotationComponents(offset) 
		local components = {offset:GetComponents()}
		table.remove(components, 1)
		table.remove(components, 2)
		table.remove(components, 3)

		return components
	end

	VectorFuncs = {
		["normalVectorToFace"] = normalVectorToFace,
		["getTopLeftCorners"] = getTopLeftCorners
	}
end

--[[
	================== CanvasDraw ===================
	
	Created by: Ethanthegrand (@Ethanthegrand14)
	
	Last updated: 6/11/2023
	Version: 3.4.1
	
	Learn how to use the module here: https://devforum.roblox.com/t/1624633
	Detailed API Documentation: https://devforum.roblox.com/t/2017699
	
	Copyright © 2022 - 2023 | CanvasDraw
]]
--[[
	============== QUICK API REFERENCE ==============

	CanvasDraw Functions:
	
	   - CanvasDraw.new() : Canvas
	      * Constructs and returns a canvas class/object
	
	   - CanvasDraw.GetImageData() : ImageData
	      * Reads the selected SaveObject's compressed ImageData and returns a readable ImageData class
	      
	   - CanvasDraw.CreateSaveObject() : Instance
	      * Returns a physical save object (a folder instance) containing compressed ImageData.
	      * This instance can be stored anywhere in your place and can be loaded into CanvasDraw.
		  * When 'InstantCreate' is set to false, CanvasDraw will slowly create this SaveObject to 
		    avoid lag (Doing this is recommended for large images).
		  * The CanvasDraw image importer plugin uses this internally.
	
	Canvas Properties:
		
	   - OutputWarnings : boolean
	      * Determines whether any warning messages will appear in the output if something is out of place 
	        or not working correctly according to the module.
	   
	   - AutoUpdate : boolean
	      * Determines whether the canvas will automatically update and render the pixels on the canvas every heartbeat.
	      * Set this property to false and call the Update() method to manually update and render the canvas.
	   
	   - Canvas.CanvasColour : Color3 [READ ONLY]
	      * The default background colour of the generated canvas.
	   
	   - Canvas.Resolution : Vector2 [READ ONLY]
	      * The current resolution of the canvas.
	   
	Canvas Drawing Methods:
		
	   - Canvas:FillCanvas()
	      * Replaces every pixel on the canvas with a colour
	   
	   - Canvas:ClearCanvas()
	      * Replaces every current pixel on the canvas with the canvas colour
	
	   - Canvas:FloodFill() : {...}
	   - Canvas:FloodFillXY()
	      * This function will fill an area of pixels on the canvas of the specific colour that your point is on.
	      * An array will also be returned containing all pixel points that were used to fill with.
	      * NOTE: This function is not very fast! Do not use for real-time engines
	     
	   - Canvas:DrawPixel() : Vector2
	   - Canvas:SetPixel()
	      * Places a pixel on the canvas
	  
	   - Canvas:DrawCircle() : {...}
	   - Canvas:DrawCircleXY()
	      * Draws a circle at a desired point with a set radius and colour.
	  
	   - Canvas:DrawRectangle() : {...}
	   - Canvas:DrawRectangleXY()
	      * Draws a simple rectangle shape from point A (top left) to point B (bottom right).
	  
	   - Canvas:DrawTriangle() : {...}
	   - Canvas:DrawTriangleXY()
	      * Draws a plain triangle from three points on the canvas.
	  
	   - Canvas:DrawLine() : {...}
	   - Canvas:DrawLineXY()
	      * Draws a simple pixel line from two points on the canvas.
	  
	   - Canvas:DrawImage()
	   - Canvas:DrawImageXY()
	      * Draws an image to the canvas from ImageData with optional scaling.
	      * Supports alpha blending when the 'TransparencyEnabled' parameter is set to true.
	     
	   - Canvas:DrawTexturedTriangle()
	   - Canvas:DrawTexturedTriangleXY()
	      * Draws a textured triangle at three points from a given ImageData and UV coordinates.
	      * UV coordinates range from a scale of 0 to 1 for each axis. 
	        (0, 0 is top left, and 1, 1 is bottom right)
	      * Intended for 3D rendering or 2D textured polygons
	      * Supports transparency, but not alpha blending
	      * Has automatic 2D clipping
	      
	   - Canvas:DrawInterpolatedTriangle()
	   - Canvas:DrawInterpolatedTriangleXY()
	      * Draws a gradient-based triangle at three points with three colours.
	      * The triangle will interpolate between your three colours at the three points.
	      * Intended for 3D rendering
	      * Has automatic 2D clipping
	     
	   - Canvas:DrawDistortedImage()
	   - Canvas:DrawDistortedImageXY()
	      * Draws a four point textured quad/plane which can be scaled dynamically
	      * This can be used for 3D rendering or rotating, stretching, skewing or warping 2D images.
	      * Supports transparency, but not alpha blending
	     
	   - Canvas:DrawText()
	   - Canvas:DrawTextXY()
	      * Draw simple pixel text to the canvas. Great for debugging.
	
	 
	Canvas Fetch Methods:
	
	   - Canvas:GetPixel() : Color3
	   - Canvas:GetPixelXY() : Color3
	      * Returns the chosen pixel's colour (Color3)
	   
	   - Canvas:GetPixels(PointA: Vector2?, PointB: Vector2?) : {...}
	      * Returns all pixels ranging from PointA to PointB
	   
	   - Canvas:GetMousePoint() : Vector2? [CLIENT ONLY]
	      * If the client's mouse is within the canvas, a canvas point (Vector2) will be returned
	        Otherwise, nothing will be returned (nil)
	      * This function is compatible with Guis and SurfaceGuis
	   
	   - Canvas:CreateImageData() : ImageData
	      * Returns an ImageData class/table from the canvas pixels from PointA to PointB or the whole canvas.
	        
	        
	ImageData Methods:
	
	   - ImageData:GetPixel() : Color3, number
	   - ImageData:GetPixelXY() : Color3, number
	      * Returns a tuple in order; the pixel's Color3 value and the pixel's alpha/transparency value (from 0 - 255)
	   
	   - ImageData:Tint()
	      * Interpolates the image's original pixels with a set colour
	   
	   - ImageData:SetPixel()
	      * Sets a specified pixel on the image to a given colour and alpha value
	   
	Other Canvas Methods:
	
	   - Canvas:DestroyCanvas()
	      * Destroys the canvas and all data related
	      
	   - Canvas:Update()
	      * Manually update/render the canvas (if Canvas.AutoUpdate is set to 'false')
	
	Canvas Events:
	
	   - Canvas.Updated(DeltaTime)
	      * The same as RunService.Heartbeat.
	      * This event is what CanvasDraw uses to update the canvas every frame 
	        when the AutoUpdate property is set to true.
	      

]]

local RunService = game:GetService("RunService")
local UserInputService = game:GetService("UserInputService")

-- Modules
local GradientCanvas = FastCanvas
local PixelTextCharacters = TextCharacters

local CanvasDraw = {}

-- These variables are only accessed by this module
local SaveObjectResolutionLimit = Vector2.new(256, 256) -- [DO NOT EDIT!] Roblox string value character limits
local CanvasResolutionLimit = Vector2.new(256, 256) -- Too many frames can cause rendering issues for roblox. So I think having this limit will help solve this problem for now.

-- Micro optimisations
local TableInsert = table.insert
local TableFind = table.find
local RoundN = math.round
local Vector2New = Vector2.new
local CeilN = math.ceil

--== BUILT-IN FUNCTIONS ==--

local function Swap(A, B)
	return B, A
end

local function GetRange(A, B)
	if A > B then
		return RoundN(A - B), -1
	else
		return RoundN(B - A), 1
	end
end

local function RoundPoint(Point)
	local X = RoundN(Point.X)
	local Y = RoundN(Point.Y)
	return Vector2New(X, Y)
end

local function PointToPixelIndex(Point, Resolution)
	return RoundN(Point.X) + (RoundN(Point.Y) - 1) * Resolution.X
end

local function XYToPixelIndex(X, Y, ResolutionX)
	return X + (Y - 1) * ResolutionX
end

local function Lerp(A, B, T)
	return A + (B - A) * T
end

local function LerpF(A, B, T) -- Assumes values range from 0 to 1
	return A + (B - A) * T
end

--== MODULE FUCNTIONS ==--

-- Canvas functions

function CanvasDraw.new(Frame: GuiObject, Resolution: Vector2?, CanvasColour: Color3?)
	local Canvas = {
		-- Modifyable properties/events
		OutputWarnings = true,
		AutoUpdate = true,

		-- Read only
		Resolution = Vector2New(100, 100),

		Updated = RunService.Heartbeat -- Event
	}


	--==<< Interal Functions >>==--

	local function OutputWarn(Message)
		if Canvas.OutputWarnings then
			warn("(!) CanvasDraw Module Warning: '" .. Message .. "'")
		end
	end

	--==<< Canvas Set-up >>==--

	-- Parameter defaults
	if CanvasColour then
		Canvas.CanvasColour = CanvasColour 
	else
		Canvas.CanvasColour = Frame.BackgroundColor3
	end

	if Resolution then
		if Resolution.X > CanvasResolutionLimit.X or Resolution.Y > CanvasResolutionLimit.Y then
			OutputWarn("A canvas cannot be built with a resolution larger than " .. CanvasResolutionLimit.X .. " x " .. CanvasResolutionLimit.Y .. ".")
			Resolution = CanvasResolutionLimit
		end
		Canvas.Resolution = Resolution
		Canvas.CurrentResX = Resolution.X
		Canvas.CurrentResY = Resolution.Y
	else
		Canvas.CurrentResX = 100
		Canvas.CurrentResY = 100
	end

	-- Create the canvas
	local InternalCanvas = GradientCanvas.new(Canvas.CurrentResX, Canvas.CurrentResY)
	InternalCanvas:SetParent(Frame)

	Canvas.AutoUpdateConnection = RunService.Heartbeat:Connect(function()
		if InternalCanvas and Canvas.AutoUpdate then
			InternalCanvas:Render()
		end
	end)

	Canvas.CurrentCanvasFrame = Frame

	for Y = 1, Canvas.CurrentResY do
		for X = 1, Canvas.CurrentResX do
			InternalCanvas:SetPixel(X, Y, Canvas.CanvasColour)
		end
	end

	InternalCanvas:Render()

	Canvas.InternalCanvas = InternalCanvas



	--============================================================================================================--
	--====  <<   Canvas API   >>   ================================================================================--
	--============================================================================================================--

	--==<< Canvas Methods >>==--

	function Canvas:DestroyCanvas()
		InternalCanvas:Destroy()
		self.InternalCanvas = nil
		self.CurrentCanvasFrame = nil
		self.AutoUpdateConnection:Disconnect()
	end

	function Canvas:FillCanvas(Colour: Color3)
		for Y = 1, self.CurrentResY do
			for X = 1, self.CurrentResX do
				InternalCanvas:SetPixel(X, Y, Colour)
			end
		end
	end

	function Canvas:ClearCanvas()
		self:FillCanvas(self.CanvasColour)
	end

	function Canvas:Update()
		InternalCanvas:Render()
	end


	--==<< Fetch Methods >>==--

	function Canvas:GetPixel(Point: Vector2): Color3
		Point = RoundPoint(Point)

		local X = Point.X
		local Y = Point.Y

		if X > 0 and Y > 0 and X <= self.CurrentResX and Y <= self.CurrentResY then
			return InternalCanvas:GetPixel(X, Y)
		end
	end

	function Canvas:GetPixelXY(X: number, Y: number): Color3
		return self.InternalCanvas:GetPixel(X, Y)
	end

	function Canvas:GetPixels(PointA: Vector2, PointB: Vector2): {}
		local PixelsArray = {}

		-- Get the all pixels between PointA and PointB
		if PointA and PointB then
			local DistX, FlipMultiplierX = GetRange(PointA.X, PointB.X)
			local DistY, FlipMultiplierY = GetRange(PointA.Y, PointB.Y)

			for Y = 0, DistY do
				for X = 0, DistX do
					local Point = Vector2New(PointA.X + X * FlipMultiplierX, PointA.Y + Y * FlipMultiplierY)
					local Pixel = self:GetPixel(Point)
					if Pixel then
						TableInsert(PixelsArray, Pixel)
					end
				end
			end
		else
			-- If there isn't any points in the paramaters, then return all pixels in the canvas
			for Y = 1, self.CurrentResX do
				for X = 1, self.CurrentResY do
					local Pixel = self:GetPixelXY(X, Y)
					if Pixel then
						TableInsert(PixelsArray, Pixel)
					end
				end
			end
		end

		return PixelsArray
	end

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

			local CanvasFrameSize = self.CurrentCanvasFrame.AbsoluteSize
			local GradientCanvasFrameSize = self.CurrentCanvasFrame.GradientCanvas.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 / GradientCanvasFrameSize) -- Normalised

				TransformedPoint *= self.Resolution -- Canvas space

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

				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 GradientCanvasFrame = Frame:FindFirstChild("GradientCanvas")

				if Part and GradientCanvasFrame then
					local Params = RaycastParams.new()
					Params.FilterType = Enum.RaycastFilterType.Include
					Params.FilterDescendantsInstances = {Part}

					local UnitRay = Camera:ViewportPointToRay(MouseLocation.X, MouseLocation.Y)

					local Result = workspace:Raycast(UnitRay.Origin, UnitRay.Direction * 1000, Params)

					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 = GradientCanvasFrameSize / 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 / GradientCanvasFrameSize) -- 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 / GradientCanvasFrameSize.X, CanvasFrameSize.Y / GradientCanvasFrameSize.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 LocalScript).")
		end
	end


	--==<< Canvas Image Data Methods >>==--

	function Canvas:CreateImageDataFromCanvas(PointA: Vector2, PointB: Vector2): {}
		-- Set the default points to be the whole canvas corners
		if not PointA and not PointB then
			PointA = Vector2New(1, 1)
			PointB = self.Resolution
		end

		local ImageResolutionX = GetRange(PointA.X, PointB.X) + 1
		local ImageResolutionY = GetRange(PointA.Y, PointB.Y) + 1

		local ColoursData = self:GetPixels(PointA, PointB)
		local AlphasData = {}

		-- Canvas has no transparency. So all alpha values will be 255
		for i = 1, #ColoursData do
			TableInsert(AlphasData, 255)
		end

		return {ImageColours = ColoursData, ImageAlphas = AlphasData, ImageResolution = Vector2New(ImageResolutionX, ImageResolutionY)}
	end

	function Canvas:DrawImageXY(ImageData, X: number?, Y: number?, ScaleX: number?, ScaleY: number?, TransparencyEnabled: boolean?)
		X = X or 1
		Y = Y or 1
		ScaleX = ScaleX or 1
		ScaleY = ScaleY or 1

		local ImageResolutionX = ImageData.ImageResolution.X
		local ImageResolutionY = ImageData.ImageResolution.Y
		local ImageColours = ImageData.ImageColours
		local ImageAlphas = ImageData.ImageAlphas

		local ScaledImageResX = ImageResolutionX * ScaleX
		local ScaledImageResY = ImageResolutionY * ScaleY

		local StartX = 1
		local StartY = 1

		-- Clipping
		if X < 1 then
			StartX = -X + 2
		end
		if Y < 1 then
			StartY = -Y + 2
		end
		if X + ScaledImageResX - 1 > self.CurrentResX then
			ScaledImageResX -= (X + ScaledImageResX - 1) - self.CurrentResX
		end
		if Y + ScaledImageResY - 1 > self.CurrentResY then
			ScaledImageResY -= (Y + ScaledImageResY - 1) - self.CurrentResY
		end

		if not TransparencyEnabled then
			if ScaleX == 1 and ScaleY == 1 then
				-- Draw normal image with no transparency and no scale adjustments (most optimal)
				for ImgX = StartX, ScaledImageResX do
					local PlacementX = X + ImgX - 1

					for ImgY = StartY, ScaledImageResY do
						local PlacementY = Y + ImgY - 1

						local ImgPixelColour = ImageColours[ImgX + (ImgY - 1) * ImageResolutionX]
						InternalCanvas:SetPixel(PlacementX, PlacementY, ImgPixelColour)
					end
				end
			else
				-- Draw normal image with no transparency with scale adjustments (pretty optimal)
				for ImgX = StartX, ScaledImageResX do
					local SampleX = CeilN(ImgX / ScaleX)
					local PlacementX = X + ImgX - 1

					for ImgY = StartY, ScaledImageResY do
						local SampleY = CeilN(ImgY / ScaleY)
						local PlacementY = Y + ImgY - 1

						local ImgPixelColour = ImageColours[SampleX + (SampleY - 1) * ImageResolutionX]
						InternalCanvas:SetPixel(PlacementX, PlacementY, ImgPixelColour)
					end
				end
			end	
		else
			-- Draw image with transparency (more expensive)
			for ImgX = StartX, ScaledImageResX do
				local SampleX = CeilN(ImgX / ScaleX)
				local PlacementX = X + ImgX - 1

				for ImgY = StartY, ScaledImageResY do
					local SampleY = CeilN(ImgY / ScaleY)
					local PlacementY = Y + ImgY - 1

					local ImgPixelIndex = SampleX + (SampleY - 1) * ImageResolutionX
					local ImgPixelAlpha = ImageAlphas[ImgPixelIndex]

					if ImgPixelAlpha <= 1 then -- No need to do any calculations for completely transparent pixels
						continue
					end

					local BgColour = InternalCanvas:GetPixel(PlacementX, PlacementY)

					local ImgPixelColour = ImageColours[ImgPixelIndex]

					InternalCanvas:SetPixel(PlacementX, PlacementY, BgColour:Lerp(ImgPixelColour, ImgPixelAlpha / 255))
				end
			end
		end
	end

	function Canvas:DrawImage(ImageData, Point: Vector2?, Scale: Vector2, TransparencyEnabled: boolean?)
		Point = Point or Vector2.new(1, 1)
		Scale = Scale or Vector2.new(1, 1)

		Point = RoundPoint(Point)

		Canvas:DrawImageXY(ImageData, Point.X, Point.Y, Scale.X, Scale.Y, TransparencyEnabled)
	end


	---==<< Draw Methods >>==--

	function Canvas:ClearPixels(PixelPoints: table)
		self:FillPixels(PixelPoints, self.CanvasColour)
	end

	function Canvas:FillPixels(Points: table, Colour: Color3)
		for i, Point in pairs(Points) do
			self:DrawPixel(Point, Colour)
		end
	end

	function Canvas:FloodFill(Point: Vector2, Colour: Color3) -- Optimised by @Arevoir
		Point = RoundPoint(Point)

		local OriginColour = self:GetPixel(Point)
		local ReturnPointsArray = {}
		local seen = {} 

		local vectorUp = Vector2New(0, -1)
		local vectorRight = Vector2New(1, 0)
		local vectorDown = Vector2New(0, 1)
		local vectorLeft = Vector2New(-1, 0)

		local queue = { Point }

		local canvasWidth, canvasHeight = self.CurrentResX, self.CurrentResY

		while #queue > 0 do
			local currentPoint = table.remove(queue)

			local currentPointX = currentPoint.X
			local currentPointY = currentPoint.Y

			if currentPointX > 0 and currentPointY > 0 and currentPointX <= canvasWidth and currentPointY <= canvasHeight then
				local key = currentPointX + (currentPointY - 1) * canvasWidth --currentPointX .. "," .. currentPointY

				if not seen[key] then
					local pixelColour = self:GetPixelXY(currentPointX, currentPointY)
					if pixelColour == OriginColour then
						table.insert(ReturnPointsArray, currentPoint)
						InternalCanvas:SetPixel(currentPointX, currentPointY, Colour)

						seen[key] = true

						table.insert(queue, currentPoint + vectorUp)
						table.insert(queue, currentPoint + vectorDown)
						table.insert(queue, currentPoint + vectorLeft)
						table.insert(queue, currentPoint + vectorRight)
					end
				end
			end
		end

		return ReturnPointsArray
	end

	function Canvas:FloodFillXY(Point: Vector2, Colour: Color3)
		Point = RoundPoint(Point)

		local OriginColour = self:GetPixel(Point)
		local seen = {} 

		local vectorUp = Vector2New(0, -1)
		local vectorRight = Vector2New(1, 0)
		local vectorDown = Vector2New(0, 1)
		local vectorLeft = Vector2New(-1, 0)

		local queue = { Point }

		local canvasWidth, canvasHeight = self.CurrentResX, self.CurrentResY

		while #queue > 0 do
			local currentPoint = table.remove(queue)

			local currentPointX = currentPoint.X
			local currentPointY = currentPoint.Y

			if currentPointX > 0 and currentPointY > 0 and currentPointX <= canvasWidth and currentPointY <= canvasHeight then
				local key = currentPointX + (currentPointY - 1) * canvasWidth --currentPointX .. "," .. currentPointY

				if not seen[key] then
					local pixelColour = self:GetPixelXY(currentPointX, currentPointY)
					if pixelColour == OriginColour then
						InternalCanvas:SetPixel(currentPointX, currentPointY, Colour)

						seen[key] = true

						table.insert(queue, currentPoint + vectorUp)
						table.insert(queue, currentPoint + vectorDown)
						table.insert(queue, currentPoint + vectorLeft)
						table.insert(queue, currentPoint + vectorRight)
					end
				end
			end
		end
	end

	function Canvas:DrawPixel(Point: Vector2, Colour: Color3): Vector2
		local X = RoundN(Point.X)
		local Y = RoundN(Point.Y)

		if X > 0 and Y > 0 and X <= self.CurrentResX and Y <= self.CurrentResY then	
			InternalCanvas:SetPixel(X, Y, Colour)
			return Point	
		end
	end

	function Canvas:SetPixel(X: number, Y: number, Colour: Color3) -- A raw and performant method to draw pixels (much faster than `DrawPixel()`)
		InternalCanvas:SetPixel(X, Y, Colour)
	end

	function Canvas:DrawCircle(Point: Vector2, Radius: number, Colour: Color3, Fill: boolean): {}
		local X = RoundN(Point.X)
		local Y = RoundN(Point.Y)

		local PointsArray = {}

		-- Draw the circle
		local dx, dy, err = Radius, 0, 1 - Radius

		local function CreatePixelForCircle(DrawPoint)
			self:DrawPixel(DrawPoint, Colour)
			TableInsert(PointsArray, DrawPoint)
		end

		local function CreateLineForCircle(PointB, PointA)
			local Line = self:DrawRectangle(PointA, PointB, Colour, true)

			for i, Point in pairs(Line) do
				TableInsert(PointsArray, Point)
			end
		end

		if Fill or type(Fill) == "nil" then
			while dx >= dy do -- Filled circle
				CreateLineForCircle(Vector2New(X + dx, Y + dy), Vector2New(X - dx, Y + dy))
				CreateLineForCircle(Vector2New(X + dx, Y - dy), Vector2New(X - dx, Y - dy))
				CreateLineForCircle(Vector2New(X + dy, Y + dx), Vector2New(X - dy, Y + dx))
				CreateLineForCircle(Vector2New(X + dy, Y - dx), Vector2New(X - dy, Y - dx))

				dy = dy + 1
				if err < 0 then
					err = err + 2 * dy + 1
				else
					dx, err = dx - 1, err + 2 * (dy - dx) + 1
				end
			end
		else
			while dx >= dy do -- Circle outline
				CreatePixelForCircle(Vector2New(X + dx, Y + dy))
				CreatePixelForCircle(Vector2New(X - dx, Y + dy))
				CreatePixelForCircle(Vector2New(X + dx, Y - dy))
				CreatePixelForCircle(Vector2New(X - dx, Y - dy))
				CreatePixelForCircle(Vector2New(X + dy, Y + dx))
				CreatePixelForCircle(Vector2New(X - dy, Y + dx))
				CreatePixelForCircle(Vector2New(X + dy, Y - dx))
				CreatePixelForCircle(Vector2New(X - dy, Y - dx))

				dy = dy + 1
				if err < 0 then
					err = err + 2 * dy + 1
				else
					dx, err = dx - 1, err + 2 * (dy - dx) + 1
				end
			end
		end

		return PointsArray
	end

	function Canvas:DrawCircleXY(X: number, Y: number, Radius: number, Colour: Color3, Fill: boolean)
		if X + Radius > self.CurrentResX or Y + Radius > self.CurrentResY or X - Radius < 1 or Y - Radius < 1 then
			OutputWarn("CircleXY cannot exceed bounds! Drawing cancelled.")
			return
		end

		-- Draw the circle
		local dx, dy, err = Radius, 0, 1 - Radius

		local function CreatePixelForCircle(DrawX, DrawY)
			InternalCanvas:SetPixel(DrawX, DrawY, Colour)
		end

		local function CreateLineForCircle(EndX, StartX, Y)
			for DrawX = 0, EndX - StartX do
				InternalCanvas:SetPixel(StartX + DrawX, Y, Colour)
			end
		end

		if Fill or type(Fill) == "nil" then
			while dx >= dy do -- Filled circle
				CreateLineForCircle(X + dx, X - dx, Y + dy)
				CreateLineForCircle(X + dx, X - dx, Y - dy)
				CreateLineForCircle(X + dy, X - dy, Y + dx)
				CreateLineForCircle(X + dy, X - dy, Y - dx)

				dy = dy + 1
				if err < 0 then
					err = err + 2 * dy + 1
				else
					dx, err = dx - 1, err + 2 * (dy - dx) + 1
				end
			end
		else
			while dx >= dy do -- Circle outline
				CreatePixelForCircle(X + dx, Y + dy)
				CreatePixelForCircle(X - dx, Y + dy)
				CreatePixelForCircle(X + dx, Y - dy)
				CreatePixelForCircle(X - dx, Y - dy)
				CreatePixelForCircle(X + dy, Y + dx)
				CreatePixelForCircle(X - dy, Y + dx)
				CreatePixelForCircle(X + dy, Y - dx)
				CreatePixelForCircle(X - dy, Y - dx)

				dy = dy + 1
				if err < 0 then
					err = err + 2 * dy + 1
				else
					dx, err = dx - 1, err + 2 * (dy - dx) + 1
				end
			end
		end
	end

	function Canvas:DrawRectangle(PointA: Vector2, PointB: Vector2, Colour: Color3, Fill: boolean?)
		local ReturnPoints = {}

		PointA = RoundPoint(PointA)
		PointB = RoundPoint(PointB)

		local X1, Y1 = PointA.X, PointA.Y
		local X2, Y2 = PointB.X, PointB.Y

		if Y2 < Y1 then
			Y1, Y2 = Swap(Y1, Y2)
		end

		if X2 < X1 then
			X1, X2 = Swap(X1, X2)
		end

		-- Clipped coordinates
		local StartX = math.max(X1, 1)
		local StartY = math.max(Y1, 1)

		local RangeX = math.abs(X2 - X1) + X1
		local RangeY = math.abs(Y2 - Y1) + Y1

		RangeX = math.min(RangeX, self.CurrentResX)
		RangeY = math.min(RangeY, self.CurrentResY)

		local function InsertPoints(...)
			local PointsTable = {...}
			for i, Table in ipairs(PointsTable) do
				for i, Point in ipairs(Table) do
					table.insert(ReturnPoints, Point)
				end
			end
		end

		if Fill or type(Fill) == "nil" then
			-- Fill every pixel
			for PlotX = StartX, RangeX do
				for PlotY = StartY, RangeY do
					InternalCanvas:SetPixel(PlotX, PlotY, Colour)
					table.insert(ReturnPoints, Vector2.new(PlotX, PlotY))
				end
			end
		else
			-- Just draw the outlines (using solid rectangles)
			local TopLine = Canvas:DrawRectangle(Vector2New(X1, Y1), Vector2New(X2, Y1), Colour, true)
			local BottomLine = Canvas:DrawRectangle(Vector2New(X1, Y2), Vector2New(X2, Y2), Colour, true)

			local LeftLine = Canvas:DrawRectangle(Vector2New(X1, Y1), Vector2New(X1, Y2), Colour, true)
			local RightLine = Canvas:DrawRectangle(Vector2New(X2, Y1), Vector2New(X2, Y2), Colour, true)

			InsertPoints(TopLine, BottomLine, LeftLine, RightLine)
		end

		return ReturnPoints
	end

	function Canvas:DrawRectangleXY(X1: number, Y1: number, X2: number, Y2: number, Colour: Color3, Fill: boolean?)
		if Y2 < Y1 then
			Y1, Y2 = Swap(Y1, Y2)
		end

		if X2 < X1 then
			X1, X2 = Swap(X1, X2)
		end

		-- Clipped coordinates
		local StartX = math.max(X1, 1)
		local StartY = math.max(Y1, 1)

		local RangeX = math.abs(X2 - X1) + X1
		local RangeY = math.abs(Y2 - Y1) + Y1

		RangeX = math.min(RangeX, self.CurrentResX)
		RangeY = math.min(RangeY, self.CurrentResY)

		if Fill or type(Fill) == "nil" then
			-- Fill every pixel
			for PlotX = StartX, RangeX do
				for PlotY = StartY, RangeY do
					InternalCanvas:SetPixel(PlotX, PlotY, Colour)
				end
			end
		else
			-- Just draw the outlines (using solid rectangles)
			Canvas:DrawRectangleXY(X1, Y1, X2, Y1, Colour, true)
			Canvas:DrawRectangleXY(X1, Y2, X2, Y2, Colour, true)

			Canvas:DrawRectangleXY(X1, Y1, X1, Y2, Colour, true)
			Canvas:DrawRectangleXY(X2, Y1, X2, Y2, Colour, true)
		end
	end

	function Canvas:DrawTriangle(PointA: Vector2, PointB: Vector2, PointC: Vector2, Colour: Color3, Fill: boolean?): {}
		local ReturnPoints = {}

		if typeof(Fill) == "nil" or Fill == true then
			local X1 = PointA.X
			local X2 = PointB.X
			local X3 = PointC.X
			local Y1 = PointA.Y
			local Y2 = PointB.Y
			local Y3 = PointC.Y

			local CurrentY1 = Y1
			local CurrentY2 = Y2
			local CurrentY3 = Y3

			local CurrentX1 = X1
			local CurrentX2 = X2
			local CurrentX3 = X3

			-- Sort the vertices based on Y ascending
			if Y2 < Y1 then
				Y1, Y2 = Swap(Y1, Y2)
				X1, X2 = Swap(X1, X2)
			end

			if Y3 < Y1 then
				Y1, Y3 = Swap(Y1, Y3)
				X1, X3 = Swap(X1, X3)
			end

			if Y3 < Y2 then
				Y2, Y3 = Swap(Y2, Y3)
				X2, X3 = Swap(X2, X3)
			end

			local function PlotLine(StartX, EndX, Y)
				for X = 1, EndX - StartX do
					local Point = Vector2New(StartX + X, Y)
					self:DrawPixel(Point, Colour)

					TableInsert(ReturnPoints, Point)
				end
			end

			local function DrawBottomFlatTriangle(TriX1, TriY1, TriX2, TriY2, TriX3, TriY3) 
			--[[
				TriX1, TriY1 - Triangle top point
				TriX2, TriY2 - Triangle bottom left corner
				TriX3, TriY3 - Triangle bottom right corner
			]]
				local invslope1 = (TriX2 - TriX1) / (TriY2 - TriY1)
				local invslope2 = (TriX3 - TriX1) / (TriY3 - TriY1)

				local curx1 = TriX1
				local curx2 = TriX1

				for Y = 0, TriY3 - TriY1 do
					local DrawY = TriY1 + Y
					PlotLine(math.floor(curx1), math.floor(curx2), DrawY)
					curx1 += invslope1
					curx2 += invslope2
				end
			end

			local function DrawTopFlatTriangle(TriX1, TriY1, TriX2, TriY2, TriX3, TriY3)	
			--[[
				TriX1, TriY1 - Triangle top left corner
				TriX2, TriY2 - Triangle top right corner
				TriX3, TriY3 - Triangle bottom point
			]]
				local invslope1 = (TriX3 - TriX1) / (TriY3 - TriY1)
				local invslope2 = (TriX3 - TriX2) / (TriY3 - TriY2)

				local curx1 = TriX3
				local curx2 = TriX3

				for Y = 0, TriY3 - TriY1 do
					local DrawY = TriY3 - Y
					PlotLine(math.floor(curx1), math.floor(curx2), DrawY)
					curx1 -= invslope1
					curx2 -= invslope2
				end
			end

			local TriMidX = X1 + (Y2 - Y1) / (Y3 - Y1) * (X3 - X1)

			if TriMidX < X2 then
				DrawBottomFlatTriangle(X1, Y1, TriMidX, Y2, X2, Y2)
				DrawTopFlatTriangle(TriMidX, Y2, X2, Y2, X3, Y3)
			else
				DrawBottomFlatTriangle(X1, Y1, X2, Y2, TriMidX, Y2)
				DrawTopFlatTriangle(X2, Y2, TriMidX, Y2, X3, Y3)
			end
		end

		local LineA = self:DrawLine(PointA, PointB, Colour)
		local LineB = self:DrawLine(PointB, PointC, Colour)
		local LineC = self:DrawLine(PointC, PointA, Colour)

		for Point in pairs(LineA) do
			TableInsert(ReturnPoints, Point)
		end
		for Point in pairs(LineB) do
			TableInsert(ReturnPoints, Point)
		end
		for Point in pairs(LineC) do
			TableInsert(ReturnPoints, Point)
		end

		return ReturnPoints
	end


	function Canvas:DrawTriangleXY(X1: number, Y1: number, X2: number, Y2: number, X3: number, Y3: number, Colour: Color, Fill: boolean?)
		if Fill or typeof(Fill) == "nil" then

			local function CheckPoint(X, Y)
				if X < 1 or Y < 1 or X > self.CurrentResX or Y > self.CurrentResY then
					return true
				end
			end

			if CheckPoint(X1, Y1) or CheckPoint(X2, Y2) or CheckPoint(X3, Y3) then
				OutputWarn("DrawTriangle (XY) Error: This drawing method doesn't have clipping (Points exceed bounds) Canceling...")
				return
			end

			-- Sort the vertices based on Y ascending
			if Y2 < Y1 then
				Y1, Y2 = Swap(Y1, Y2)
				X1, X2 = Swap(X1, X2)
			end

			if Y3 < Y1 then
				Y1, Y3 = Swap(Y1, Y3)
				X1, X3 = Swap(X1, X3)
			end

			if Y3 < Y2 then
				Y2, Y3 = Swap(Y2, Y3)
				X2, X3 = Swap(X2, X3)
			end

			local function PlotLine(StartX, EndX, Y)
				for X = 1, EndX - StartX do
					InternalCanvas:SetPixel(StartX + X, Y, Colour)
				end
			end

			local function DrawBottomFlatTriangle(TriX1, TriY1, TriX2, TriY2, TriX3, TriY3) 
			--[[
				TriX1, TriY1 - Triangle top point
				TriX2, TriY2 - Triangle bottom left corner
				TriX3, TriY3 - Triangle bottom right corner
			]]
				local invslope1 = (TriX2 - TriX1) / (TriY2 - TriY1)
				local invslope2 = (TriX3 - TriX1) / (TriY3 - TriY1)

				local curx1 = TriX1
				local curx2 = TriX1

				for Y = 0, TriY3 - TriY1 do
					local DrawY = TriY1 + Y
					PlotLine(math.floor(curx1), math.floor(curx2), DrawY)
					curx1 += invslope1
					curx2 += invslope2
				end
			end

			local function DrawTopFlatTriangle(TriX1, TriY1, TriX2, TriY2, TriX3, TriY3)	
			--[[
				TriX1, TriY1 - Triangle top left corner
				TriX2, TriY2 - Triangle top right corner
				TriX3, TriY3 - Triangle bottom point
			]]
				local invslope1 = (TriX3 - TriX1) / (TriY3 - TriY1)
				local invslope2 = (TriX3 - TriX2) / (TriY3 - TriY2)

				local curx1 = TriX3
				local curx2 = TriX3

				for Y = 0, TriY3 - TriY1 do
					local DrawY = TriY3 - Y
					PlotLine(math.floor(curx1), math.floor(curx2), DrawY)
					curx1 -= invslope1
					curx2 -= invslope2
				end
			end

			local TriMidX = X1 + (Y2 - Y1) / (Y3 - Y1) * (X3 - X1)

			if TriMidX < X2 then
				DrawBottomFlatTriangle(X1, Y1, TriMidX, Y2, X2, Y2)
				DrawTopFlatTriangle(TriMidX, Y2, X2, Y2, X3, Y3)
			else
				DrawBottomFlatTriangle(X1, Y1, X2, Y2, TriMidX, Y2)
				DrawTopFlatTriangle(X2, Y2, TriMidX, Y2, X3, Y3)
			end
		end

		self:DrawLineXY(X1, Y1, X2, Y2, Colour)
		self:DrawLineXY(X2, Y2, X3, Y3, Colour)
		self:DrawLineXY(X3, Y3, X1, Y1, Colour)
	end

	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?
	)
		local TexResX, TexResY = ImageData.ImageResolution.X, ImageData.ImageResolution.Y

		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

		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

			TexU, TexV = tex_su, tex_sv

			local Step = 1 / (bx - ax)
			local t = 0

			if Step > 10000 then
				Step = 10000
			end

			local ScanlineLength = math.ceil(bx - ax)

			-- Clip X right
			if bx > self.CurrentResX then
				ScanlineLength = self.CurrentResX - ax
			end

			-- Clip X left
			local StartOffsetX = 0

			if ax < 1 then	
				StartOffsetX = -(ax - 1)
				t = Step * StartOffsetX
			end

			for j = StartOffsetX, ScanlineLength do
				TexU = Lerp(tex_su, tex_eu, t)
				TexV = Lerp(tex_sv, tex_ev, t)

				local SampleX = math.min(math.floor(TexU * TexResX + 1), TexResX)
				local SampleY = math.min(math.floor(TexV * TexResY + 1), TexResY)

				local SampleColour, SampleAlpha = ImageData:GetPixelXY(SampleX, SampleY)

				if SampleColour and SampleAlpha > 1 then
					if not Brightness or Brightness == 1 then
						InternalCanvas:SetPixel(ax + j, Y, SampleColour)
					else
						local R, G, B = SampleColour.R, SampleColour.G, SampleColour.B
						R *= Brightness
						G *= Brightness
						B *= Brightness

						InternalCanvas:SetPixel(ax + j, Y, Color3.new(R, G, B))
					end
				end

				t += Step
			end

		end

		-- Clip Y top
		local YStart = 1

		if Y1 < 1 then
			YStart = 1 - Y1
		end

		-- Clip Y bottom
		local TopYDist = math.min(Y2 - Y1 - 1, self.CurrentResY - Y1)

		-- Draw top triangle
		for i = YStart, TopYDist do
			--task.wait(1)
			local ax = math.round(X1 + i * dax_step)
			local bx = math.round(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 - 1 - 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 = math.round(X2 + (i - Y2) * dax_step)
			local bx = math.round(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:DrawTexturedTriangle(
		PointA: Vector2, PointB: Vector2, PointC: Vector2, 
		UV1: Vector2, UV2: Vector2, UV3: Vector2, 
		ImageData, Brightness: number?
	)

		-- Convert to intergers
		local X1, X2, X3 = math.ceil(PointA.X), math.ceil(PointB.X), math.ceil(PointC.X)
		local Y1, Y2, Y3 = math.ceil(PointA.Y), math.ceil(PointB.Y), math.ceil(PointC.Y)

		Canvas:DrawTexturedTriangleXY(
			X1, Y1, X2, Y2, X3, Y3,
			UV1.X, UV1.Y, UV2.X, UV2.Y, UV3.X, UV3.Y,
			ImageData, Brightness
		)
	end

	function Canvas:DrawDistortedImageXY(X1, Y1, X2, Y2, X3, Y3, X4, Y4, ImageData, Brightness: number?)
		Canvas:DrawTexturedTriangleXY(
			X1, Y1, X2, Y2, X3, Y3,
			0, 0, 0, 1, 1, 1,
			ImageData, Brightness
		)
		Canvas:DrawTexturedTriangleXY(
			X1, Y1, X4, Y4, X3, Y3,
			0, 0, 1, 0, 1, 1,
			ImageData, Brightness
		)
	end

	function Canvas:DrawDistortedImage(PointA, PointB, PointC, PointD, ImageData, Brightness: number?)
		Canvas:DrawDistortedImageXY(
			PointA.X, PointA.Y, PointB.X, PointB.Y, PointC.X, PointC.Y, PointD.X, PointD.Y,
			ImageData, Brightness
		)
	end

	function Canvas:DrawLine(PointA: Vector2, PointB: Vector2, Colour: Color3, Thickness: number?): {}
		local DrawnPointsArray = {}

		if not Thickness or Thickness < 1 then
			DrawnPointsArray = {PointA}

			local X1 = RoundN(PointA.X)
			local X2 = RoundN(PointB.X)
			local Y1 = RoundN(PointA.Y)
			local Y2 = RoundN(PointB.Y)

			local sx, sy, dx, dy

			if X1 < X2 then
				sx = 1
				dx = X2 - X1
			else
				sx = -1
				dx = X1 - X2
			end

			if Y1 < Y2 then
				sy = 1
				dy = Y2 - Y1
			else
				sy = -1
				dy = Y1 - Y2
			end

			local err, e2 = dx-dy, nil

			while not (X1 == X2 and Y1 == Y2) do
				e2 = err + err
				if e2 > -dy then
					err = err - dy
					X1  = X1 + sx
				end
				if e2 < dx then
					err = err + dx
					Y1 = Y1 + sy
				end

				local Point = Vector2New(X1, Y1)
				self:DrawPixel(Point, Colour)
				TableInsert(DrawnPointsArray, Point)
			end

			self:DrawPixel(PointA, Colour)

			return DrawnPointsArray
		else -- Custom polygon based thick line
			local X1, Y1 = PointA.X, PointA.Y
			local X2, Y2 = PointB.X, PointB.Y

			local RawRot = math.atan2(PointA.X - PointB.X, PointA.Y - PointB.Y) -- Use distances between each axis
			local Theta = RawRot

			local PiHalf = math.pi / 2

			-- Ensure we get an angle that measures up to 360 degrees (also avoids negative numbers)
			if RawRot < 0 then
				Theta = math.pi * 2 + RawRot
			end

			local Diameter = 1 + (Thickness * 2)
			local Rounder = (math.pi * 1.5) / Diameter

			Theta = math.round(Theta / Rounder) * Rounder -- Avoids strange behaviours for the triangle points

			-- Start polygon points
			local StartCornerX1 = math.floor(X1 + 0.5 + math.sin(Theta + PiHalf) * Thickness)
			local StartCornerY1 = math.floor(Y1 + 0.5 + math.cos(Theta + PiHalf) * Thickness)

			local StartCornerX2 = math.floor(X1 + 0.5 + math.sin(Theta - PiHalf) * Thickness)
			local StartCornerY2 = math.floor(Y1 + 0.5 + math.cos(Theta - PiHalf) * Thickness)

			-- End polygon points
			local EndCornerX1 = math.floor(X2 + 0.5 + math.sin(Theta + PiHalf) * Thickness)
			local EndCornerY1 = math.floor(Y2 + 0.5 + math.cos(Theta + PiHalf) * Thickness)

			local EndCornerX2 = math.floor(X2 + 0.5 + math.sin(Theta - PiHalf) * Thickness)
			local EndCornerY2 = math.floor(Y2 + 0.5 + math.cos(Theta - PiHalf) * Thickness)

			-- Draw 2 triangles at the start and end corners
			local TrianglePointsA = Canvas:DrawTriangle(Vector2New(StartCornerX1, StartCornerY1), Vector2New(StartCornerX2, StartCornerY2), Vector2New(EndCornerX1, EndCornerY1), Colour)
			local TrianglePointsB = Canvas:DrawTriangle(Vector2New(StartCornerX2, StartCornerY2), Vector2New(EndCornerX1, EndCornerY1), Vector2New(EndCornerX2, EndCornerY2), Colour)

			-- Draw rounded caps
			local CirclePointsA = Canvas:DrawCircle(PointA, Thickness, Colour)
			local CirclePointsB = Canvas:DrawCircle(PointB, Thickness, Colour)

			local function InsertContents(Table)
				for i, Item in ipairs(Table) do
					table.insert(DrawnPointsArray, Item)
				end
			end

			InsertContents(TrianglePointsA)
			InsertContents(TrianglePointsB)
			InsertContents(CirclePointsA)
			InsertContents(CirclePointsB)
		end

		return DrawnPointsArray
	end

	function Canvas:DrawLineXY(X1: number, Y1: number, X2: number, Y2: number, Colour: Color3, Thickness: number?)
		if not Thickness or Thickness < 1 then -- Bresenham line
			local sx, sy, dx, dy

			if X1 < X2 then
				sx = 1
				dx = X2 - X1
			else
				sx = -1
				dx = X1 - X2
			end

			if Y1 < Y2 then
				sy = 1
				dy = Y2 - Y1
			else
				sy = -1
				dy = Y1 - Y2
			end

			local err, e2 = dx-dy, nil

			while not(X1 == X2 and Y1 == Y2) do
				e2 = err + err
				if e2 > -dy then
					err = err - dy
					X1  = X1 + sx
				end
				if e2 < dx then
					err = err + dx
					Y1 = Y1 + sy
				end
				InternalCanvas:SetPixel(X1, Y1, Colour)
			end
		else -- Custom polygon based thick line
			local RawRot = math.atan2(X1 - X2, Y1 - Y2) -- Use distances between each axis
			local Theta = RawRot

			local PiHalf = math.pi / 2

			-- Ensure we get an angle that measures up to 360 degrees (also avoids negative numbers)
			if RawRot < 0 then
				Theta = math.pi * 2 + RawRot
			end

			local Diameter = 1 + (Thickness * 2)
			local Rounder = (math.pi * 1.5) / Diameter

			Theta = math.round(Theta / Rounder) * Rounder -- Avoids strange behaviours for the triangle points

			-- Start polygon points
			local StartCornerX1 = math.floor(X1 + 0.5 + math.sin(Theta + PiHalf) * Thickness)
			local StartCornerY1 = math.floor(Y1 + 0.5 + math.cos(Theta + PiHalf) * Thickness)

			local StartCornerX2 = math.floor(X1 + 0.5 + math.sin(Theta - PiHalf) * Thickness)
			local StartCornerY2 = math.floor(Y1 + 0.5 + math.cos(Theta - PiHalf) * Thickness)

			-- End polygon points
			local EndCornerX1 = math.floor(X2 + 0.5 + math.sin(Theta + PiHalf) * Thickness)
			local EndCornerY1 = math.floor(Y2 + 0.5 + math.cos(Theta + PiHalf) * Thickness)

			local EndCornerX2 = math.floor(X2 + 0.5 + math.sin(Theta - PiHalf) * Thickness)
			local EndCornerY2 = math.floor(Y2 + 0.5 + math.cos(Theta - PiHalf) * Thickness)

			-- Draw 2 triangles at the start and end corners
			Canvas:DrawTriangleXY(StartCornerX1, StartCornerY1, StartCornerX2, StartCornerY2, EndCornerX1, EndCornerY1, Colour)
			Canvas:DrawTriangleXY(StartCornerX2, StartCornerY2, EndCornerX1, EndCornerY1, EndCornerX2, EndCornerY2, Colour)

			-- Draw rounded caps
			Canvas:DrawCircleXY(X1, Y1, Thickness, Colour)
			Canvas:DrawCircleXY(X2, Y2, Thickness, Colour)
		end

	end

	function Canvas:DrawTextXY(Text: string, X: number, Y: number, Colour: Color3, Scale: number?, Wrap: boolean?, Spacing: number?)
		if not Spacing then
			Spacing = 1
		end

		if not Scale then
			Scale = 1
		end

		Scale = math.clamp(math.round(Scale), 1, 50)

		local CharWidth = 3 * Scale
		local CharHeight = 5 * Scale

		local TextLines = string.split(Text, "\n")

		for i, TextLine in pairs(TextLines) do
			local Characters = string.split(TextLine, "")

			local OffsetX = 0
			local OffsetY = (i - 1) * (CharHeight + Spacing)

			for i, Character in pairs(Characters) do
				local TextCharacter = PixelTextCharacters[Character:lower()]

				if TextCharacter then
					local StartOffsetX = -(math.min(1, X + OffsetX) - 1) + 1
					local StartOffsetY = -(math.min(1, Y + OffsetY) - 1) + 1

					if OffsetX + CharWidth > self.CurrentResX - X + 1 then
						if Wrap or type(Wrap) == "nil" then
							OffsetY += CharHeight + Spacing
							OffsetX = 0
						else
							break -- Don't write anymore text since it's outside the canvas
						end
					end

					for SampleY = StartOffsetY, CharHeight do
						local PlacementY = Y + SampleY - 1 + OffsetY
						SampleY = math.ceil(SampleY / Scale)

						if PlacementY - 1 >= self.CurrentResY then
							break
						end

						for SampleX = StartOffsetX, CharWidth do
							local PlacementX = X + SampleX - 1 + OffsetX

							if PlacementX > self.CurrentResX or PlacementX < 1 then
								continue
							end

							SampleX = math.ceil(SampleX / Scale)

							local Fill = TextCharacter[SampleY][SampleX]
							if Fill == 1 then
								InternalCanvas:SetPixel(PlacementX, PlacementY, Colour)
							end
						end
					end
				end

				OffsetX += CharWidth + Spacing
			end
		end
	end

	function Canvas:DrawText(Text: string, Point: Vector2, Colour: Color3, Scale: number?, Wrap: boolean?, Spacing: number?)
		Point = RoundPoint(Point)
		Canvas:DrawTextXY(Text, Point.X, Point.Y, Colour, Scale, Wrap, Spacing)
	end


	return Canvas
end


--============================================================================================================--
--====  <<   CanvasDraw Module ImageData API   >>   ===========================================================--
--============================================================================================================--

function CanvasDraw.GetImageData(SaveObject: Instance)
	local SaveDataImageColours = SaveObject:GetAttribute("ImageColours")
	local SaveDataImageAlphas = SaveObject:GetAttribute("ImageAlphas")
	local SaveDataImageResolution = SaveObject:GetAttribute("ImageResolution")

	-- Decompress the data
	local DecompressedSaveDataImageColours = StringCompressor.Decompress(SaveDataImageColours)
	local DecompressedSaveDataImageAlphas = StringCompressor.Decompress(SaveDataImageAlphas)

	-- Get a single pixel colour info form the data
	local PixelDataColoursString = string.split(DecompressedSaveDataImageColours, "S")
	local PixelDataAlphasString = string.split(DecompressedSaveDataImageAlphas, "S")

	local PixelColours = {}
	local OrigColours = {}
	local PixelAlphas = {}

	for i, PixelColourString in pairs(PixelDataColoursString) do
		local RGBValues = string.split(PixelColourString, ",")
		local PixelColour = Color3.fromRGB(table.unpack(RGBValues))

		local PixelAlpha = tonumber(PixelDataAlphasString[i])

		TableInsert(PixelColours, PixelColour)
		TableInsert(OrigColours, PixelColour)
		TableInsert(PixelAlphas, PixelAlpha)
	end

	-- Convert the SaveObject into image data
	local ImageData = {ImageColours = PixelColours, ImageAlphas = PixelAlphas, ImageResolution = SaveDataImageResolution}
	local ImageDataResX = SaveDataImageResolution.X
	local ImageDataResY = SaveDataImageResolution.Y


	--== ImageData methods ==--

	function ImageData:GetPixel(Point: Vector2): (Color3, number)
		local X, Y = math.ceil(Point.X), math.ceil(Point.Y)
		local Index = XYToPixelIndex(X, Y, ImageDataResX)

		return PixelColours[Index], PixelAlphas[Index]
	end

	function ImageData:GetPixelXY(X: number, Y: number): (Color3, number)
		local Index = XYToPixelIndex(X, Y, ImageDataResX)

		return PixelColours[Index], PixelAlphas[Index]
	end

	function ImageData:Tint(Colour: Color3, T: number)
		for i, OriginalColour in ipairs(OrigColours) do
			PixelColours[i] = OriginalColour:Lerp(Colour, T)
		end
	end

	function ImageData:SetPixel(X: number, Y: number, Colour: Color3, Alpha: number?)
		local Index = XYToPixelIndex(X, Y, ImageDataResX)

		PixelColours[Index] = Colour
		if Alpha then
			PixelAlphas[Index] = Alpha
		end
	end

	return ImageData
end

function CanvasDraw.CreateSaveObject(ImageData, InstantCreate: boolean?): Folder
	if ImageData.ImageResolution.X > SaveObjectResolutionLimit.X and ImageData.ImageResolution.Y > SaveObjectResolutionLimit.Y then
		warn([[Failed to create an image save object (ImageData too large). 
		Please try to keep the resolution of the image no higher than ']] .. SaveObjectResolutionLimit.X .. " x " .. SaveObjectResolutionLimit.Y .. "'.")
		return
	end

	local FastWaitCount = 0

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

	local function ConvertColoursToListString(Colours)
		local ColourData = {}
		local RgbStringFormat = "%d,%d,%d"

		for i, Colour in ipairs(Colours) do
			local R, G, B = RoundN(Colour.R * 255), RoundN(Colour.G * 255), RoundN(Colour.B * 255)
			TableInsert(ColourData, RgbStringFormat:format(R, G, B))

			if not InstantCreate then
				FastWait(4000)
			end
		end

		return table.concat(ColourData, "S")
	end

	local function ConvertAlphasToListString(Alphas)	
		local AlphasListString = table.concat(Alphas, "S")
		return AlphasListString
	end

	local ImageColoursString = ConvertColoursToListString(ImageData.ImageColours)
	local ImageAlphasString = ConvertAlphasToListString(ImageData.ImageAlphas)

	local CompressedImageColoursString = StringCompressor.Compress(ImageColoursString)
	local CompressedImageAlphasString = StringCompressor.Compress(ImageAlphasString)

	local NewSaveObject = Instance.new("Folder")
	NewSaveObject.Name = "NewSave"

	NewSaveObject:SetAttribute("ImageColours", CompressedImageColoursString)
	NewSaveObject:SetAttribute("ImageAlphas", CompressedImageAlphasString)
	NewSaveObject:SetAttribute("ImageResolution", ImageData.ImageResolution)

	return NewSaveObject
end


--== DEPRECATED FUNCTIONS/METHODS ==--

-- (!) Use ImageData:GetPixel() instead
function CanvasDraw.GetPixelFromImage(ImageData, Point: Vector2): (Color3, number)
	local PixelIndex = PointToPixelIndex(Point, ImageData.ImageResolution) -- Convert the point into an index for the array of colours

	local PixelColour = ImageData.ImageColours[PixelIndex]
	local PixelAlpha = ImageData.ImageAlphas[PixelIndex]

	return PixelColour, PixelAlpha
end

-- (!) Use ImageData:GetPixelXY() instead
function CanvasDraw.GetPixelFromImageXY(ImageData, X: number, Y: number): (Color3, number)
	local PixelIndex = XYToPixelIndex(X, Y, ImageData.ImageResolution.X) -- Convert the coordinates into an index for the array of colours

	local PixelColour = ImageData.ImageColours[PixelIndex]
	local PixelAlpha = ImageData.ImageAlphas[PixelIndex]

	return PixelColour, PixelAlpha
end

return CanvasDraw
INFO