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

Could you ask me how to load image data into canvas? I keep getting errors.

if savedData and next(savedData) then
	print("loading...")
	local decompressedImageData = CanvasDraw.DecompressImageData(savedData)
	CanvasDraw:GetImageData(decompressedImageData)
end

You should probably read the API documentation in cases like these as you are using GetImageData incorrectly

CanvasDraw.GetImageData is used for loading ImageData from save object instances. However the use of it is still useless here as you already have image data (decompressedImageData)

What you want is Canvas:DrawImage(decompressedImageData)

Let me know if any other issues arise

1 Like

I’m sorry. I’ll read it properly again

This is awesome! Has anyone made a Code page 437 port to the TextCharacters module?

1 Like

I don’t think so, but I am working on adding actual font support for CanvasDraw v4.5!

One of these fonts are the Code page 437 font :slight_smile:

Canvas:DrawTextXY('Welcome to CanvasDraw v4.5.0!!!', X, Y, Color3.new(), "Codepage8x10")
4 Likes

Module Update - v4.5.0

Hey all. It’s been a bit since an update.

CanavsDraw now supports multiple fonts for the :DrawText() & :DrawTextXY() canvas methods!

Before, the font used for :DrawText() was a small 3x5 upper only case font that I created. But this has since changed!

We have 7 new built-in bitmap fonts that can be used:

Fonts can now created with a bitmap font image that I import via a script of a selected bitmap font sheet image.

For an example, here’s the code page 437 font sheet I used:
Codepage9x16.png

NOTE: Currently, there isn’t a way for other users to create custom fonts. However, if there is enough demand, I will create a font creator for the CanvasDraw Tools plugin that will allow you to create custom fonts for CanvasDraw via PNG images of bitmap font sheets.


This update adds a new font parameter to the DrawText and DrawTextXY methods:

-- Draw black 'Hello, World!' text at point 20x20 with the Codepage font
local Text = "Hello, World!"
Canvas:DrawTextXY(Text, 20, 20, Color3.new(0, 0, 0), "Codepage")


All useable fonts can be found under the CanvasDraw module in a folder called Fonts:

image


Here’s the full list of changes:

  • Added FontName parameter to Canvas:DrawText() and Canvas:DrawTextXY()

  • Removed Canvas.FpsLimit as it had some serious performance issues and never seemed to perform reliably. You will have to come up with your own way to limit auto render rates


CanvasDraw4.5.0.b.rbxm (57.2 KB)

8 Likes

Part 2!!

I have created a fully functional Mandelbrot zoom-in renderer in Roblox using my beloved CanvasDraw!

MandelbrotSetViewer.rbxm (66.7 KB)

It uses a modified CanvasDraw because it doesn’t provide everything I needed. Here’s the things I added for my version:

  • Canvas:GetGrid(): {number}
  • Canvas:SetGrid(grid: {number})
  • Canvas:IsDestroyed()

I need to get and set the raw grid to quickly save the result of each render. Using GetImageData() and :GetPixels() failed because of how long it took, or because it couldn’t be reused without other modifications.

The main problem with this is the memory usage. My memory fills up rapidly at the maximum 1024x1024 quality, so I made it so you could split the recording up. It took 26 videos and a few hours of time to create this video!

5 Likes

This is amazing :open_mouth:

I love it!

Will definitely add Get and Set grid methods to CanvasDraw soon as they do seem useful.

However, what is your use case for :IsDestroyed??

1 Like

While I search for a good spot to zoom in to, it shows a much lower quality version.

This is a different canvas, because you cannot dynamically change the resolution using a single canvas instance! When one is not visible, it is destroyed. While I’m rendering, if the canvas is destroyed, it will fail when trying to set pixels. Since there was no way to tell if it was destroyed so I could cancel the render, I had to add that method. It simply returns not Grid, because Grid gets set to nil when destroyed.

3 Likes

Your work is awesome! Also, why not store the fonts in a “proper” bitmap format (using lua binary numerals?

Your format:

How I store characters:


Preferably, each character could be indexed automatically with it’s ASCII code

Now, each font could have it’s own bitmap data array (in ascii order), and return other functions along with that can return/find the relative character data, etc.

2 Likes

Well, the main reason why i did this is so i didn’t have to rewrite my DrawText() function when I added the fonts.

The other reason is simplicity. This is easy to modify and can be arranged in a way so you can somewhat visualise each character’s pixels too.

Also I didn’t really see how binary numerals can give me an advantage, so I tend to not use them often. I’d love to know why this might be better though!

How would one achieve this? Because I have realised that some characters cannot be used to index my character sheet, meaning i cant use most special characters

This might be a stupid question, I’m trying to make a drawing system with this module but I’m quite confused on how to make an Undo and Redo function? I appreciate if you could help me.

2 Likes

You could probably store the last 10 frames or so, and then revert to the last one, and store a new one every time you draw/erase.

And then for redo, when undoing still store the frames you were undoing, and then remove them the next time you draw/erase.

Somewhat how I would do it. Hope you figure it out! :+1:

(Sorry if it’s a little hard to follow. Tried my best lol)

Yeah exactly what i was trying to do but i don’t know how to get the last stroke of frames the player did with their mouse, I looked over the module to get the frames but still couldn’t find anything

1 Like

Using binary numerals would be as intuitive as using nested tables, but more efficient:
Instead of having {{0,0,1,0,1},{0,1,0,0,1}}, you’d have {0b00101, 0b01001}

You can then get the pixel value by using bitwise functions (I’d me more than glad to help over discord, but here’s how I did it on a 5x8 20x4 LCD with the original hitachi bitmap: Dot Matrix LCD in Roblox! (with hd44780 bitmaps))

Creating an undo system is relatively easy to do. One way to do this is to store multiple copies of the canvas as you draw your image (i.e, upon every mouse button click, save the current state of our image)

You can do this by using a table

local UndoFrames = {}
local MaxUndoFrames = 32

local function AddToUndoHistory()
	-- Get all current pixels on the canvas
	local Pixels = {}
	
	for Y = 1, Resolution.Y do
		for X = 1, Resolution.X do
			table.insert(Pixels, Canvas:GetPixelXY(X, Y))
		end
	end
	
    -- Avoid excessive memory consumption
	if #UndoFrames >= MaxUndoFrames then
		table.remove(UndoFrames, 1)
	end

	table.insert(UndoFrames, Pixels)
end

local function Undo()
    -- Draw and remove the previous state of the canvas
	local LastFrame = UndoFrames[#UndoFrames]

	if LastFrame then
		for X = 1, Resolution.X do
			for Y = 1, Resolution.Y do
				Canvas:SetPixel(X, Y, LastFrame[X + (Y - 1) * Resolution.X]) -- Formula for getting a 1D position from 2D coordinates
			end
		end

		table.remove(UndoFrames, #UndoFrames)
	end
end

In this example, I am saving to an array of a size of 32 images max every time I press the mouse button, and pressing Z to undo:

Full code
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 = 1

-- Main

local MouseDown = false
local LastMousePoint

local Resolution = Vector2.new(158, 128)

local Canvas = CanvasDraw.new(Frame, Resolution)

local UndoFrames = {}
local MaxUndoFrames = 32

local function AddToUndoHistory()
	-- Get all current pixels on the canvas
	local Pixels = {}
	
	for Y = 1, Resolution.Y do
		for X = 1, Resolution.X do
			table.insert(Pixels, Canvas:GetPixelXY(X, Y))
		end
	end
	
	if #UndoFrames >= MaxUndoFrames then
		table.remove(UndoFrames, 1)
	end

	table.insert(UndoFrames, Pixels)
end

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

	if LastFrame then
		for X = 1, Resolution.X do
			for Y = 1, Resolution.Y do
				Canvas:SetPixel(X, Y, LastFrame[X + (Y - 1) * Resolution.X])
			end
		end

		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 
		--MousePoint += Vector2.new(25, 25)
		
		-- 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
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()

	-- For those who like dots
	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)

AddToUndoHistory() -- Initalise
3 Likes

Thank you very much didn’t know it was that simple.

2 Likes

Thanks! I think this might be more efficient, I’ll give it a try!

1 Like

Module Update v4.6.0

Hey all. We have some new and very useful additions that a couple of you guys have suggested in the past.


Canvas:Resize()

Yes, that’s right! We can now dynamically resize the canvas without having to destroy and create a new one.

NOTE: Currently, contents of the canvas do not rescale, and instead clears.
You will have to redraw and render the image after clearing for a clean transition.


Canvas:SetGrid() / Canvas:GetGrid()

These new methods were honestly quite overdue. These are very useful for when you want to do per pixel processing or copying/pasting of pixel data


Full list of changes:

  • Added Canvas:SetGrid()

  • Added Canvas:GetGrid()

  • Added Canvas:Resize()

  • Optimised font storage and rendering for Canvas:DrawText() and Canvas:DrawTextXY()

  • Fixed Canvas:GetPixels() not working correctly on aspect ratios that aren’t 1:1


Special thanks to: @bluebxrrybot and @Lxi099 for contributing some of these ideas


CanvasDraw4.6.0.b.rbxm (50.9 KB)

7 Likes

Here’s a port of the ModernDOS (GitHub - susam/pcface: Bitmap arrays for rendering CP437 glyphs using IBM PC OEM fonts) font to your CanvasDraw Bitmap format:


ModernDOS.lua (27.5 KB)

Demo string containing all characters:

☺☻♥♦♣♠•◘○◙♂♀♪♫☼►◄↕‼¶§▬↨↑↓→←∟↔▲▼ !"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`abcdefghijklmnopqrstuvwxyz{|}~⌂ÇüéâäàåçêëèïîìÄÅÉæÆôöòûùÿÖÜ¢£¥₧ƒáíóúñѪº¿⌐¬½¼¡«»░▒▓│┤╡╢╖╕╣║╗╝╜╛┐└┴┬├─┼╞╟╚╔╩╦╠═╬╧╨╤╥╙╘╒╓╫╪┘┌█▄▌▐▀αßΓπΣσµτΦΘΩδ∞φε∩≡±≥≤⌠⌡÷≈°∙·√ⁿ²■ 

EDIT:
I noticed most of the characters weren’t being displayed. This is due to the string.split(TextLine, "") in the Characters definition on the DrawTextXY script. You have to replace Characters with this; which adds support for multi-byte utf8 graphemes (like the smiley faces, greek letters and blocks)

local Characters = {}
for _, c in utf8.codes(TextLine) do
	table.insert(Characters, utf8.char(c))
end
2 Likes