CanvasDraw Legacy

Run Settings
LanguageLua
Language Version
Run Command
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
Editor Settings
Theme
Key bindings
Full width
Lines