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

It’s just never been suggested or contributed by anyone before. You can make a canvas transparent like so:

Canvas:SetClearRGBA(0, 0, 0, 0)
Canvas:Clear()

As for drawing things on a transparent canvas, there currently isn’t any good automatic support for that yet, ill be sure to see if i can add better support for that soon.

In the mean time, you can interate through specific pixels and use Canvas:SetAlpha() to set the transparency accordingly

1 Like

To be honest, if I made something as good as this my ego would be also as high.

1 Like

people might see this as useless but i think it’s very useful if you wanna make a game from scratch while still using luau

1 Like

I tried using those two functions before but all it did was make the canvas completely invisible with the Save image loaded in.

But I managed to get something by using the Canvas:FloodFillXY() While still keeping the Save image visible. Minus the remains of the color from the canvas that’s surrounding the image (The red crust)

hey is it optional to not use editableimages and use the pixel render instead? i prefer the pixel render where it created pixel frames.

Yeah, you can go to the CanvasDraw archive here:

Not really sure what you mean here. There are many ways to update pixels in bulk.

Some examples;


-- Fill a certain region of pixels
for X = 50, 100 do
    for Y = 50, 100 do
         Canvas:SetRGB(X, Y, R, G, B) 
    end
end

-- Draw an image
Canvas:DrawImage(ImageData)

-- Draw using an array of pixels
Canvas:SetGrid(PixelArray)

-- Draw using a buffer
Canvas:SetBuffer(PixelBuffer)

This is pretty common practice in most rendering APIs and game engines

local ImageData = CanvasDraw.GetImageData(Textures["BrickWall1.png"])
-- OR
local ImageData = CanvasDraw.GetImageDataFromTextureId("rbxassetid://12345678")

local R, G, B = ImageData:GetRGB(X, Y)

It was originally based off of a simple C++ pixel engine. It’s intended to be very simple to understand and use and it just consists of using X and Y values.

the API is intended to as fast as possible as well, and this was designed with that in mind.

2 Likes

Should be basically 1:1 for Canvas:SetBuffer()

One thing to note, is that drawing or setting the grid, doesn’t directly apply it to the EditableImage immediately.

For an example, using :SetRGB, or :DrawCircle, etc, will simply just draw to an internal buffer, by default, for every heartbeat, the pixels are then rendered to the EditableImage.

Depending on what you use the canvas for, you may only want to update the EditableImage only when necessary;

Canvas.AutoRender = false -- Disable auto rendering

-- Drawing stuffs:
Canvas:DrawCircle(...) -- Only set's the pixels nessecary.

 -- Manually render the canvas 
-- (which internally calls EditableImage:WritePixelBuffer)
Canvas:Render()

From my testing, the memory usage was fine, at least for lower resolutions. You can convert the numbers to buffers, which is much more memory efficient and then you could easily store like 50 or more snapshots of a high resolution just fine, I don’t think 50 frames will go over 200 MB if you use unsigned integer values. Simpler drawings will be much lower however, as low as 20 MB even. It all depends on image detail as roblox buffers are already compressed.

I could provide an example for you if you wish!

From personal testing, i have never had a performance issue with drawing a circle or other shapes. The code is quite well optimised. Optimsing has been something I have been doing for more than 2 years on this project, so it should be almost just as fast!

The main reason why this doesn’t use the EditableImage drawing functions is simply because it isn’t directly compatible with this workflow. I can’t store a drawn circle in memory before manually rendering it to the canvas.

Another big reason, is control. With CanvasDraw, you get way more control of how you draw things.

For an example, with a circle, you can either draw a filled circle, or a circle outline, which EditableImage cannot do.

I don’t even think EditableImage can do lines with thicknesses.

Also another thing to note, is that this module is 3 years old. It was designed before EditableImage came out, so that’s another reason why you don’t see use of EditableImage shapes or drawing methods.

1 Like

:GetPixels() is mainly just a leftover from the early 2023 version, it really isn’t that performant and shouldn’t really be used. I might either rewrite it or deprecate it. It’s mostly just there for backwards compatibility.

I can understand that, to be honest around 20% of the content on this module is here mainly for backwards compatibility with older projects using CanvasDraw. There are games, even a couple games with 500+ players that rely on the older CanvasDraw API that was made before EditableImage.

Well, despite the differences between how other people code, this still runs really great, and I haven’t seen any other roblox graphics library attempts do better. Here is an example of a 3D raycaster engine running around up to 128 fps at 256x256

implementation for what sorry? is there a particular method you’d like? I can try my best to help out. I’m also open for changes, ideas and additions to help improve the module in any way.

5 Likes

Here is an optimised version of my undo code example. Works completely lag free at 1024x1024.

Memory usage only went up as high as 0.29 GB on my machine for 50 saved frames using buffers, which is quite impressive. You could improve upon memory usage even further with a different approach, such as saving the strokes rather than the whole canvas, but then you’ll have to account for other possible advanced brush tools, such as shapes, flood fill, etc. This is the simplest example which works well.

This newer and more optimized example avoids iteration;

FPS seems to be unaffected as well.

local Players = game:GetService("Players")
local LocalPlr = Players.LocalPlayer

local Gui = script.Parent
local Frame = Gui:WaitForChild("CanvasFrame")

local CanvasDraw = require(Gui:WaitForChild("CanvasDraw"))

local Mouse = LocalPlr:GetMouse()

local DrawColour = Color3.new(0, 0, 0)
local Radius = 3

-- Main

local MouseDown = false
local LastMousePoint

local Resolution = Vector2.new(1024, 1024)
local ResX, ResY = Resolution.X, Resolution.Y

local Canvas = CanvasDraw.new(Frame, Resolution)
local Canvas2 = CanvasDraw.new(Gui:WaitForChild("CanvasFrame2"), Resolution)
Canvas2:SetClearRGBA(1, 1, 1, 0)

local UndoFrames = {}
local MaxUndoFrames = 50

local function AddToUndoHistory()
	-- Get all current pixels on the canvas
	local FramePixels = Canvas:GetBuffer()
	
	if #UndoFrames >= MaxUndoFrames then
		table.remove(UndoFrames, 1)
	end

	table.insert(UndoFrames, FramePixels)
end

local function Undo()
	local LastFrame = UndoFrames[#UndoFrames]

	if LastFrame then
		Canvas:SetBuffer(LastFrame)
		table.remove(UndoFrames, #UndoFrames)
	end
end

Mouse.Move:Connect(function()
	local MousePoint = Canvas:GetMousePoint()
	
	-- Check if we have a valid point on the canvas and the mouse button is being held down
	if MousePoint and MouseDown then 
		
		-- Draw a line between the last mouse point and the current mouse point to avoid gaps from dragging the mouse too quickly
		if LastMousePoint then
			Canvas:DrawLine(LastMousePoint, MousePoint, DrawColour, Radius)
		end
		
		LastMousePoint = MousePoint
	end
	
    -- Draw cursor
	if MousePoint then
		Canvas2:Clear()
		Canvas2:SetAlpha(MousePoint.X, MousePoint.Y, 1)
		Canvas2:SetRGB(MousePoint.X, MousePoint.Y, 1, 0, 0)
	end
end)

Mouse.Button1Down:Connect(function()
	MouseDown = true
	
	AddToUndoHistory()
	
	local MousePoint = Canvas:GetMousePoint()
	
	-- For those who like dots
	if MousePoint then
		LastMousePoint = MousePoint
		Canvas:DrawCircle(MousePoint, Radius, DrawColour)
	end
end)

Mouse.Button2Down:Connect(function()
	AddToUndoHistory()
	
	local MousePoint = Canvas:GetMousePoint()

	-- Le flood fill
	if MousePoint then
		Canvas:FloodFillXY(MousePoint.X, MousePoint.Y, DrawColour)
	end
end)


Mouse.Button1Up:Connect(function()
	LastMousePoint = nil
	MouseDown = false
end)

-- Undo
game.UserInputService.InputBegan:Connect(function(Inp)
	if Inp.KeyCode == Enum.KeyCode.Z then
		Undo()
	end
end)
3 Likes

Module and Plugin Update/Patch - v4.11.0


Module changes:

  • Added error handling for situations where an EditableImage may fail to create incase of a memory limit being hit

  • Optimised Canvas:FloodFill()

  • Fixed a possible memory leak issue from destroying, resizing and recreating canvases

  • Deprecated the following methods:

    • Canvas:GetPixels()
    • Canvas:ClearPixels()
    • Canvas:FillPixels()

Plugin changes:

  • Added an Import from TextureId option for the Image Editor
4 Likes

got inspired by bluebxrrybot’s Mandelbrot Set and tried to recreate Julia Set with canvas draw

2 Likes

I have a question. i send the image data to the server:

local ImageData = Canvas:CreateImageDataFromCanvas(Vector2.new(0,0), Vector2.new(100, 100))
local SaveObject = CanvasDraw.CreateSaveObject(ImageData)
SaveObject.Name = "MyDrawing"
SaveObject.Parent = script.Folder
		
local canvasData = script.Folder.MyDrawing:FindFirstChild("Chunk1").Value

Then the server collects all the players created drawings and then i wanna load all the drawings that all the players have created for a draw game to see who made the best drawing:

GetDrawData.OnClientEvent:Connect(function(canvasData, WorstOrBest)
	for each, canvasDataNew in pairs(canvasData) do		
		local clonedCanvas = script.Canvas:Clone()
		clonedCanvas.Parent = game.Players.LocalPlayer.PlayerGui.GUI.ImageFrames
		clonedCanvas.Name = canvasDataNew.Player.Name

		if canvasDataNew.Player.Name == game.Players.LocalPlayer.Name then
			clonedCanvas.Visible = false
		end

		-- Initialize the canvas (if necessary)
		local CanvasInstance = CanvasDrawModule.new(clonedCanvas.Canvas)

		local MyDrawing = Instance.new("Folder", script.Folder)
		MyDrawing.Name = "MyDrawing"
		MyDrawing:SetAttribute("Resolution", Vector2.new(100, 100))

		local LoadData = Instance.new("StringValue", MyDrawing)
		LoadData.Value = canvasDataNew.CanvasData
		LoadData.Name = "Chunk1"

		print(LoadData.Value)

		local ImageData = CanvasDrawModule.GetImageData(MyDrawing)
		CanvasInstance:DrawImage(ImageData, Vector2.new(0, 0))

		MyDrawing:Destroy()
	end
end)

But for some reason. the drawings only load 75% of the time. Does anyone know the cause of this?

Not really sure why, it could due to your method of storing the data and relying on re-creating the save instances. How come you aren’t using something more suitable such as CanvasDraw.CompressImageData() for storing the drawings?

All though I don’t see why your current method wouldn’t work

I have found a bug.

at line 18, it does :GetChildren() to iterate over the StringValues. However, There’s a chance where :GetChildren() returns {Chunk2, Chunk1} instead of the ordered pair. The fix would be to either sort the Children using table.sort, or (more efficiently) use #StringValues inside an for i loop

2 Likes

Oh, thank you so much for pointing that out. I’ll be sure to fix that. Im not sure why I never ordered it

2 Likes

Ah didn’t notice that one. But now i’ve added that one in my code but i keep getting the error: Buffer expected, got table.

How i get the the compression:

local function GenerateImageData(Canvas)
	local topLeft = Vector2.new(0, 0) -- Start position on the canvas
	local bottomRight = ResolutionSize -- Full canvas size
	local ImageData = Canvas:CreateImageDataFromCanvas(topLeft, bottomRight) -- Adjust if needed
	return ImageData
end

local ImageData = GenerateImageData(Canvas)
	if ImageData then
		-- Compress the ImageData
		local CompressedData = CanvasDraw.CompressImageData(ImageData)

		-- Send the compressed data to the server
		game.ReplicatedStorage.Remotes.CollectDrawings:FireServer(CompressedData)
	else
		warn("Failed to generate ImageData.")
	end

Server:

local CollectedPaintingData = {}

repStorage.Remotes.CollectDrawings.OnServerEvent:Connect(function(player, canvasData)
	table.insert(CollectedPaintingData, {
		Player = player,        -- Store the player
		CanvasData = canvasData -- Store the canvas data
	})
end)

And then send that back to client:

GetDrawData.OnClientEvent:Connect(function(canvasData, WorstOrBest)
for each, canvasDataNew in pairs(canvasData) do	
    local CanvasInstance = CanvasDrawModule.new(clonedCanvas.Canvas)
	
    -- Prepare data for decompression
    local ImageData = CanvasDrawModule.DecompressImageData(canvasDataNew.CanvasData)
    CanvasInstance:DrawImage(ImageData)
end

I checked everything but i am not sure what else to do.

I just have to say, whatever game you are making right there looks AWESOME!!

3 Likes

Large Update - v4.12.0


Hey all. Lot’s of big changes this update! Let’s start with the biggest:

Canvas Alpha Blending Modes

Yes, that’s right! You can now finally control how draw methods alpha blend with the canvas!

This means you can finally do stuff such as rendering shapes or images onto a completely transparent canvas by changing the blending mode!

This can be done like so;

-- Ignores the destination pixels and doesn't perform any blending
Canvas.AlphaBlendingMode = CanvasDraw.BlendingModes.Replace

-- Current default blending behaviour
Canvas.AlphaBlendingMode = CanvasDraw.BlendingModes.Normal

-- Or alternatively;

Canvas.AlphaBlendingMode = 1 -- Replace
Canvas.AlphaBlendingMode = 0 -- Normal [Default]

Currently, there are only 2 blending modes

  • Normal (Default)
  • Replace

Here’s an example of differences between the two modes.

This is a 200x200 transparent canvas with red triangle, an image with transparency, and text in that order:


Canvas.AlphaBlendingMode = CanvasDraw.BlendingModes.Normal


Canvas.AlphaBlendingMode = CanvasDraw.BlendingModes.Replace

As you can see, with the ‘Replace’ mode, you can draw on Transparent canvases! This is also very useful for masking effects with images or shapes.

This new canvas property affects the following draw methods
  • Canvas:DrawCircle()
  • Canvas:DrawRectangle()
  • Canvas:DrawTriangle()
  • Canvas:DrawLine()
  • Canvas:DrawText()
  • Canvas:DrawImage()
  • Canvas:DrawTexturedTriangle()
  • Canvas:DrawDistortedImage()
  • Canvas:DrawImageRect()
  • Canvas:DrawRotatedImage()
  • Canvas:FloodFill()

along with the XY equivalents


Optional Alpha Parameter for Shapes

This is one that many people have suggested in the past, and I am happy to announce that we finally have a new alpha parameter on the following draw methods along with their XY equivalents:

  • Canvas:DrawCircle()
  • Canvas:DrawRectangle()
  • Canvas:DrawTriangle()
  • Canvas:FloodFill()
  • Canvas:Fill()

Currently, Canvas:DrawLine() does not have this parameter due to the complexity and overdraw that occurs when rendering a line with thickness. This may change in the future however!


Replacement for Returning Point Arrays from Shapes Methods

This change was overdue as using the non XY equivelant methods for drawing is really slow as Vector and table construction would occur for every drawn pixel.

This has been removed from those draw methods and replaced with new Canvas:Get<Shape>Point methods!

local LinePoints = Canvas:GetLinePoints(PointA, PointB, Thickness, RoundedCaps)

local CirclePoints = Canvas:GetCirclePoints(Point, Radius, Fill)

local RectanglePoints = Canvas:GetRectanglePoints(PointA, PointB, Fill)

local TrianglePoints = Canvas:GetTrianglePoints(PointA, PointB, PointC, Fill)

Migrating any older projects that may rely on the previous behavior is quite simple!

local TriPoints = Canvas:DrawTriangle(PointA, PointB, PointC, Colour, Fill)

-- REPLACE WITH

local TriPoints = Canvas:GetTrianglePoints(PointA, PointB, PointC, Fill)
Canvas:DrawTriangle(PointA, PointB, PointC, Colour, Fill) -- Only do this if you have to draw the triangle as well

The rest of the changes can be seen in the following patch notes here:

  • Added Canvas:SetRGBA() and Canvas:GetRGBA()

  • Added Canvas:SetBlur()

  • Added canvas alpha blending options to allow for shapes and certain draw methods to blend with canvas transparency differently.
    This property will affect the following methods:

    • Canvas:DrawCircle()
    • Canvas:DrawRectangle()
    • Canvas:DrawTriangle()
    • Canvas:DrawLine()
    • Canvas:DrawText()
    • Canvas:DrawImage()
    • Canvas:DrawTexturedTriangle()
    • Canvas:DrawDistortedImage()
    • Canvas:DrawImageRect()
    • Canvas:DrawRotatedImage()
    • Canvas:FloodFill()
  • Added Canvas:FillRGBA()

  • Added Canvas:SetBufferFromImage(). A super fast alternative to Canvas:DrawImage()

  • Added a new optional HorizontalAlignment parameter to Canvas:DrawText()

  • Added an optional alpha parameter to the following draw methods:

    • Canvas:DrawCircle()
    • Canvas:DrawRectangle()
    • Canvas:DrawTriangle()
    • Canvas:FloodFill()
    • Canvas:Fill()
  • Added shape pixel point fetch methods:

    • Canvas:GetCirclePoints()
    • Canvas:GetLinePoints()
    • Canvas:GetRectanglePoints()
    • Canvas:GetTrianglePoints()
  • Added an optional canvas framerate limit property for auto rendering (Canvas.AutoRenderFpsLimit)

  • Rewrote filled circle algorithm to avoid overdraw

  • Rewrote filled triangle algorithm to avoid overdraw

  • Improved accuracy for thick lines with round end caps

  • Optimised CanvasDraw.CreateBlankImageData()

  • Optimised Canvas:Fill()

  • Optimised Canvas:SetClearRGBA()

  • Fixed a chunk issue when reading SaveObjects

  • Canvas:DrawImage() will now have it’s optional TransparencyEnabled paramter as true by default if omitted or set to nil.

  • Canvas:DrawCircle(), Canvas:DrawRectangle(), Canvas:DrawTriangle(), Canvas:DrawLine() and Canvas:FloodFill() no longer return an array of Vectors.
    Use the new Canvas:Get<Shape>Points for new work

  • Removed the old deprecated method Canvas:SetFPSLimit()

  • Removed the old deprecated property Canvas.AutoUpdate

  • Deprecated Canvas:DrawPixel(). Use Canvas:SetRGB() for new work!


Other changes:

  • The main post has been updated in the Limitations section to help users address EditableImage limits

  • For those who want to use CanvasDraw v2.0.0 to v3.4.1, a CanvasDraw Legacy Image Importer plugin has been made as the modern importer has a different format that is not compatible with older CanvasDraw versions.


:birthday: CanvasDraw has also recently just turned 3 years old! :birthday:


Special thanks to @EatSleepCodeRepeat for contributing some of these ideas!

CanvasDraw4.12.0.rbxm (72.5 KB)

6 Likes

Another phenomenal update Ethan!

1 Like