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

Sorry if this might be a bother, but I’m still very confused on how to make a working videoplayer without causing a huge amount of lag everytime it loads an image, my code is currently a sample, however everytime I load the image, it causes alot of lag, I can’t imagine loading those images every 0.1 seconds. Is there anyway to make it so it doesn’t create a whole bunch of lag when loading in the pixels? I’m not really familiar with the module as I just started using it a couple days ago. (Also yes im aware this code will only load one image.)

heres the code:

local Gui = script.Parent
local Frame = Gui.Frame

local CanvasDraw = require(Gui.CanvasDraw)

local Canvas = CanvasDraw.new(Frame)

-- Load our image
local ImageData = CanvasDraw.GetImageDataFromSaveObject(script.Parent.ImageTest) -- Our Image

local Canvas = CanvasDraw.new(Frame, ImageData.ImageResolution)

-- Main

local CompressionAmount = 10 -- From 0 to 256 (The lower, more colours, the higher, less colours)

local NewColours = {}

for i, OldColour in pairs(ImageData.ImageColours) do -- Loop through the raw image Color3 values

	-- Compress the RGB channels
	local CompressedR = math.floor((OldColour.R * 255) / CompressionAmount) * CompressionAmount
	local CompressedG = math.floor((OldColour.G * 255) / CompressionAmount) * CompressionAmount
	local CompressedB = math.floor((OldColour.B * 255) / CompressionAmount) * CompressionAmount

	CompressedColour = Color3.fromRGB(CompressedR, CompressedG, CompressedB)
	NewColours[i] = CompressedColour
end

ImageData.ImageColours = NewColours

Canvas:DrawImageXY(ImageData, 1, 1)
1 Like

What you’re meant to be doing is slowly loading all the images once, and then store them in a table, and then play the video based on that large table.

Once you have a table full of your loaded imageData, loop through that table and call DrawImageXY() for each object in that table

2 Likes

I have tried using imageData and its very slow at loading + I am not getting stable FPS its very jumpy. Any recommendations?

I am using the code by PixelatedCherry

Im new to using CanvasDraw and want to get smooth video playing. Thanks!

Heres my current code:

local Gui = game.Players.LocalPlayer.PlayerGui:WaitForChild("ScreenGui", 3)
local Frame = Gui.Frame

local CanvasDraw = require(script:WaitForChild("CanvasDraw", 3))

-- Load our image

local Canvas = CanvasDraw.new(Frame, Vector2.new(256, 256))

-- Main

local CompressionAmount = 10 -- From 0 to 256 (The lower, more colours, the higher, less colours)

local NewColours = {}

local Frames = {}

for i, v in pairs(game.Workspace.Files:GetChildren()) do
	local ImageData = CanvasDraw.GetImageData(v)

	for i, OldColour in pairs(ImageData.ImageColours) do -- Loop through the raw image Color3 values
	
		-- Compress the RGB channels
		local CompressedR = math.floor((OldColour.R * 255) / CompressionAmount) * CompressionAmount
		local CompressedG = math.floor((OldColour.G * 255) / CompressionAmount) * CompressionAmount
		local CompressedB = math.floor((OldColour.B * 255) / CompressionAmount) * CompressionAmount
	
		CompressedColour = Color3.fromRGB(CompressedR, CompressedG, CompressedB)
		NewColours[i] = CompressedColour
	end
	
	print("Loaded frame number: " .. i)
	
	ImageData.ImageColours = NewColours
	
	table.insert(Frames, ImageData)
	task.wait()
end

for i, iFrame in pairs(Frames) do
	Canvas:DrawImageXY(iFrame, 1, 1)
	task.wait()
end

print("done")

The reason for the jumpy framerate is due to different colour positions and quantities.

This is pretty bad, you are loading and rendering large 256x256 images every frame (or heartbeat idk).

I don’t reccomend trying to do real-time videos at 256x256, and especially trying to do it on every task.wait(). it is just way too computational for roblox on the CPU to run smoothly.

The highest resolution I ever went for a GIF player of mine was 256x150 at 30FPS, which i’d say is a nice hard limit for CanvasDraw videos.

Try rendering the video at 30 or 25 FPS, at a lower resolution. it should be a more consistent and put way less stress on roblox.


Another thing too, is that you can crash or slow down the roblox client if you load up way too many high-resolution ImageData frames, for example, if you were to do a 256x256 video with 1000 frames, you would end up storing around 65 million Color3 values in tables, which will take up A LOT of memory.

The best way around this is just to stick to a lower resolution and framerate.

Yeah that does make sense. The video was already at 20 FPS and 7s long as a test.

Did you do anything special to get 256x150 at 30 FPS or is just using roughly the same code here?

Also are there any good resources for getting the wait times for 25 - 30 FPS?

nope. Didn’t even apply colour compression to it.
Actually that’s a lie, the GIF I used just naturally is compressed lol

Just the lower resolution makes it load and run much faster.

And yeah, my code was pretty similar:

while true do
	for i, ImageDataToLoad in ipairs(OrderedFramesData) do
		task.wait(1 / FrameRate)
		Canvas:DrawImageXY(ImageDataToLoad, 1, 1)
	end
end
local FPS = 30
task.wait(1 / FPS)

This isn’t the best and most certainly not that accurate, but it’s good enough to where you can just adjust the FPS value to get it at the correct speed.

Trying to get code to run at a perfect fixed framerate is actually a bit more difficult than I thought it would have been. So, i’d just stick to using task.wait and adjusting the value until it looks right.

Ah, Thank you.

It seems it doesnt want to actually switch frames in Studio it just gets stuck on one of them. Is this just a studio issue?

I have no idea, that sounds very odd. Oh and btw, I forgot to mention this, if you are rendering your video at a fixed framerate, be sure to disable AutoUpdate on the canvas and then manually update the canvas every time you draw the image.

local Canvas = CanvasDraw.new(stuff...)
Canvas.AutoUpdate = false

while true do
	for i, ImageDataToLoad in ipairs(OrderedFramesData) do
		task.wait(1 / FrameRate)
		Canvas:DrawImageXY(ImageDataToLoad, 1, 1)
        Canvas:Update() -- Manually render the image
	end
end

So rather than rendering the canvas every 60 frames a second, you can do it at like 20 or 30 times a second.

I just added this and its still stuck on 1 frame. The function still runs as it does print “done” on completion. Could it have something to do with my LocalScript being in StarterCharacterScripts?

1 Like

I honestly don’t really know, message me and send a place file and I can have a look to see if it is a CanvasDraw issue or not if you want

1 Like

How does FloodFill work exactly? I’m assuming that given any Vector2 point, pixels will fill the empty space of its neighbors. StartPoint is a valid Vector2 and I’ve even tried hardcoding a random Vector2 within the canvas space and it’s still not working, this includes the documentation’s example code.

It all produces the same error and I have not modified the CanvasDraw module in any way.

local function MouseButtonDown()
	ButtonDown = true

	if CurrentTool == "Line" then
		PreviousMousePoint = Canvas:GetMousePoint()
	else
		StartPoint = Canvas:GetMousePoint()
	end
end
elseif CurrentTool == "FloodFill" then
	Canvas:FloodFill(StartPoint, Color3.new(1, 0.5, 0)) -- Using a sample color (orange) for this example.
end

Doc’s Test Code:

-- Divide the canvas into two parts with a line (Assuming your canvas is at a resolution of 100 x 100)
local LinePointA = Vector2.new(1, 40)
local LinePointA = Vector2.new(100, 60)

Canvas:DrawLine(LinePointA, LinePointB, Color3.new(0, 0, 0))

-- Fill the bottom half of the canvas red
Canvas:FloodFill(Vector2.new(50, 75), Color3.new(1, 0, 0))

Error Stack:

Players.Arevoir.PlayerGui.testUI.CanvasDraw.FastCanvas:126: attempt to index nil with nil  -  Client - FastCanvas:126
  18:02:14.978  Stack Begin  -  Studio
  18:02:14.978  Script 'Players.Arevoir.PlayerGui.testUI.CanvasDraw.FastCanvas', Line 126 - function GetPixel  -  Studio - FastCanvas:126
  18:02:14.978  Script 'Players.Arevoir.PlayerGui.testUI.CanvasDraw', Line 643 - function CheckPixel  -  Studio - CanvasDraw:643
  18:02:14.978  Script 'Players.Arevoir.PlayerGui.testUI.CanvasDraw', Line 658 - function CheckNeighbours  -  Studio - CanvasDraw:658
  18:02:14.978  Script 'Players.Arevoir.PlayerGui.testUI.CanvasDraw', Line 670 - function FloodFill  -  Studio - CanvasDraw:670
  18:02:14.978  Script 'Players.Arevoir.PlayerGui.testUI.LocalScript', Line 60 - function MouseButtonUp  -  Studio - LocalScript:60
  18:02:14.979  Stack End  -  Studio

oh man, I completely forgot FloodFill was still in CanvasDraw.
I don’t think that function has been updated since v1.0.

I do believe this is a genuine bug. I’ll make a quick fix soon

1 Like

thank you! I thought I was going crazy for a second there

Update | Line Thickness! - v3.3.0

Hey all. This update gives the DrawLine and DrawLineXY methods a new optional thickness parameter!

When you set the parameter of the line thickness, it will draw a new type of line that is incredibly fast and can be as thick as you want!


The above example is drawing a nice 15 wide line at 256x256 running 60 FPS


As for the rest of this update, I just fixed a couple of things:

  • Fixed DrawLine and DrawLineXY methods missing a pixel from the starting point

  • Fixed the FloodFill method being completely broken.
    This method hasn’t gotten any updates since version 1.0 and I plan to rewrite this method to make it far more faster as it is currently super laggy and unpractical.

3 Likes

When drawing a rectangle, it creates the shape as expected when the first point is in the upper-left hand corner and the last point is south & east of the first point, creating what would be the lower-right hand corner of the polygon. This is not the case when these points are inverted, as shown. The expected behavior would be to re-orient the shape, just how drawTriangle does already.

Canvas:DrawRectangle(StartPoint, EndPoint, CurrentColor, false)

-- As expected:
-- Start Point: 38, 40 
-- End Point: 105, 116
Canvas:DrawRectangle(StartPoint, EndPoint, CurrentColor, false)

-- Unexpected shape/behaviour:
-- Start Point: 208, 119
-- End Point: 144, 38 

A thickness for polygons would be amazing too! This is currently how I manage shape thickness.

local function CreateShape(shape, PointA, PointB, Colour, Fill, thickness)
	local offsets = {
		Vector2.new(1, 0),
		Vector2.new(-1, 0),
		Vector2.new(0, 1),
		Vector2.new(0, -1)
	}

	if shape == "Rectangle" then
		if Fill then
			Canvas:DrawRectangle(PointA, PointB, Colour, true)
		else

			Canvas:DrawRectangle(PointA, PointB, Colour, false)

			for i = 1, thickness do
				for _, offset in ipairs(offsets) do
					local newPointA = PointA + offset * I
					local newPointB = PointB + offset * I
					Canvas:DrawRectangle(newPointA, newPointB, Colour, false)
				end
			end
		end
	elseif shape == "Triangle" then
		if Fill then
			Canvas:DrawTriangle(PointA, PointB, Vector2.new(PointA.X, PointB.Y), Colour, true)
		else
			Canvas:DrawTriangle(PointA, PointB, Vector2.new(PointA.X, PointB.Y), Colour, false)

			for i = 1, thickness do
				for _, offset in ipairs(offsets) do
					local newPointA = PointA + offset * I
					local newPointB = PointB + offset * I
					local newPointC = Vector2.new(PointA.X, PointB.Y) + offset * I
					Canvas:DrawTriangle(newPointA, newPointB, newPointC, Colour, false)
				end
			end
		end
	elseif shape == "Circle" then
		local radius = (PointB - PointA).Magnitude
		if Fill then
			Canvas:DrawCircle(PointA, radius, Colour, true)
		else
			Canvas:DrawCircle(PointA, radius, Colour, false)

			for i = 1, thickness do
				Canvas:DrawCircle(PointA, radius + i, Colour, false)
				Canvas:DrawCircle(PointA, radius - i, Colour, false)
			end
		end
	end
end
1 Like

Oh, thank you very much for pointing that out. I completely forgot about sorting the coordinates for the rectangle when drawing.

This will fixed in the version of CanvasDraw!

2 Likes

A more optimized version of flood fill, incase it’s useful to anyone in the short term.

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

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

		local queue = { Point }

		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 <= self.CurrentResX and currentPointY <= self.CurrentResY then
				local key = currentPointX .. "," .. currentPointY

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

						seen[key] = true

						table.insert(queue, currentPoint + Vector2New(0, -1))
						table.insert(queue, currentPoint + Vector2New(0, 1))
						table.insert(queue, currentPoint + Vector2New(-1, 0))
						table.insert(queue, currentPoint + Vector2New(1, 0))
					end
				end
			end
		end

		return ReturnPointsArray
	end
1 Like

Oh wow, this is faaaar faster than my old attempt at this.

This is actually good enough to use on a 256x256 canvas wow.

Do you mind implement this code to the next version of CanvasDraw? I’ll count this as a contribution if you wish!

2 Likes

Absolutely! I love this project so to be able to contribute in some way is awesome.

2 Likes

Any plans to update to use DynamicImage?

2 Likes