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

i figured i could make a procedural fire system by manually manipulating the pixels so i need the background to be invisible

As of right now, you can simulate transparency using alpha blending.

Here are some (admittedly rather crude) examples of what that might look like:

local function drawAlphaBlendedPixel(canvas: any, position: Vector2, sourceColor: Color3, sourceAlpha: number)
	sourceAlpha = math.clamp(sourceAlpha, 0, 1)
	
	--Get the color of the pixel at our destination
	local destColor = canvas:GetPixel(position)
	
	--Destination alpha is inverse of source alpha
	local destAlpha = 1 - math.clamp(sourceAlpha, 0, 1)
	
	--Perform weighted averages of each color value
	local blendR = (sourceColor.R * sourceAlpha) + (destColor.R * destAlpha)
	local blendG = (sourceColor.G * sourceAlpha) + (destColor.G * destAlpha)
	local blendB = (sourceColor.B * sourceAlpha) + (destColor.B * destAlpha)
	
	--Draw pixel using averaged color values
	canvas:SetPixel(position.X, position.Y, Color3.new(blendR, blendG, blendB))
end

local function getAlphaBlendedColorAtPosition(canvas: any, position: Vector2, sourceColor: Color3, sourceAlpha: number)
	sourceAlpha = math.clamp(sourceAlpha, 0, 1)
	
	--Get the color of the pixel at our destination
	local destColor = canvas:GetPixel(position)
	
	--Destination alpha is inverse of source alpha
	local destAlpha = 1 - math.clamp(sourceAlpha, 0, 1)
	
	--Perform weighted averages of each color value
	local blendR = (sourceColor.R * sourceAlpha) + (destColor.R * destAlpha)
	local blendG = (sourceColor.G * sourceAlpha) + (destColor.G * destAlpha)
	local blendB = (sourceColor.B * sourceAlpha) + (destColor.B * destAlpha)
	
	--Get new color
	return Color3.new(blendR, blendG, blendB)
end

I might write a more detailed tutorial on this and other types of color blending later.

1 Like

but how would this makes the pixel transparent? i am not very smart and i just see some color manipulation. i mean like in a 3d environment where i wanna make a billboard fire

Unfortunately, it doesn’t. It just creates the appearance of transparency.

The only other option I can think of for your situation is either manipulating the Transparency property of individual gradients, or something involving raycasting/tracing.

2 Likes

How did you manage it to run at 256x256? If I try anything more than 150x82 it runs at 4 FPS, and every GUI (Including the one from CanvasDraw) starts to flash, pretty seizure incurring.

Also is there any way to draw more efficiently?

Lets say I have this code:

function renderImage()
	local currentTime = (tick() - math.floor(tick())) * 255
	local color_y = currentTime

	if lastCurrentTime > currentTime then
		iteration += 1
	end

	if iteration % 2 == 0  then
		color_y = 255 - currentTime
	end
	
	lastCurrentTime = currentTime

	for x = 1, resolution.X do
		for y = 1, resolution.Y do
			local color_x = math.floor((256 / resolution.X) * x) - 1
			local color_z = math.floor((256 / resolution.Y) * y) - 1

			Canvas:SetPixel(x, y, Color3.fromRGB(color_x, color_y, color_z))
		end
	end
end

It barely runs at 30FPS (on 150x82 and calling renderImage 10 times a second.
Is there maybe a image object, so I can make a top variable, and only change the pixels that need changing (Since Im not changing them) and apply the image after I set some pixels, at the end of the function?

Also it seems that Update is slower than normal.

If I do it without Update (And let Canvas.AutoUpdate to true) it runs at 30FPS
If I set Canvas.AutoUpdate to false and call Canvas:Update() every time the functions runs it only manages 20FPS

If anyone wants to see what the function does

ignore the weird background sounds

Time to go through the painful time of having to learn this resource to build a video player, but it will be worth it, I will finally be able to play 144p videos of stupid memes.

This formula is possibly slow.

Might wanna optimize this too.

The first thing I immediately noticed is that your not taking one of the major limitations into account

You are using way too many colours in that demo.

The reason why this is an issue, is because CanvasDraw uses UIGradients to merge same coloured pixels, meaning way less calculations are done, and since you are rendering a lossless gradient, this will cause lag as we are doing waaaay more per-pixel calculations.

to solve this, just reduce the amount of colours allowed through each of the RBG values. The less colours, the more FPS you’ll get.

color_z = math.floor(color_z / compressionAmount) * compressionAmount

This will reduce the possible amount of colours from 16,000,000, to something like 1,000, depending on how much your compress it.

I also tested this with your code, and now I get almost 60 FPS in real time with the compressionAmount variable set to 5

Here’s your updated code:

function renderImage()
	local currentTime = (tick() - math.floor(tick())) * 255
	local color_y = currentTime

	if lastCurrentTime > currentTime then
		iteration += 1
	end

	if iteration % 2 == 0  then
		color_y = 255 - currentTime
	end
	
	color_y = math.floor(color_y / compressionAmount) * compressionAmount

	lastCurrentTime = currentTime

	for x = 1, resolution.X do
        -- I moved this so we aren't doing as much calculations
		local color_x = math.floor((256 / resolution.X) * x) - 1
		color_x = math.floor(color_x / compressionAmount) * compressionAmount
		
		for y = 1, resolution.Y do
			local color_z = math.floor((256 / resolution.Y) * y) - 1
			color_z = math.floor(color_z / compressionAmount) * compressionAmount

			Canvas:SetPixel(x, y, Color3.fromRGB(color_x, color_y, color_z))
		end
	end
end

-- This is how I tested the rendering, I used the updated event (which is just RunService.Heartbeat) 
Canvas.Updated:Connect(function()
	renderImage()
	Canvas:Update()
end)

That formula is just (x - floor(x)) * 255. A CPU does billions of calculations every frame, there is no reason it slows by a few thousand percent from 3 simple calculations every 1/10 seconds.

Outside of moving color_x up a row there is no possible optimisation. Also removing Canvas:SetPixels shows that the code only uses 0.1% of script usage, so its pretty light.

Now it looks horrible, at this point, I could just decrease the pixel ratio. Is there a reason you removed color blending a few updates before? It would help very much.

Ah, the BlurEnabled parameter. Well, main reason why I removed it is because almost no one uses it as it doesn’t look that good at all, and it only works in the one direction. It especially looks bad if you have textures in your project too as 10 pixels in a column, now can only hold 2 colours.

Removing this parameter also meant that i can slightly improve performance on the per-pixel calculations.

I completely forgot that Gradients are 1D and not 2D, my bad. Maybe using Parts instead of gradients works better?

UIGradients are by far the best way to have minimal instances being updated as you can store up to 10 pixels in a UIGradient frame, and with parts or frames, you can only store the one colour which heavily increases the amount of instances being updated

insane amount of effort when to this. honestly amazed.

1 Like

any updates on transparencies for each pixels?

When you create a image using CanvasDraw.GetImageDataFromSaveObject, the ImageData doesn’t come with the GetPixel function. That happens because in the line 1355 of Canvas Draw, when creating the image data, the module doesn’t put the GetPixel function in the table

-- Convert the SaveObject into image data
	local ImageData = {ImageColours = PixelColours, ImageAlphas = PixelAlphas, ImageResolution = SaveDataImageResolution}

I think it should be like:

-- Convert the SaveObject into image data
local ImageData = {
	ImageColours = PixelColours,
	ImageAlphas = PixelAlphas,
	ImageResolution = SaveDataImageResolution,
    GetPixel = CanvasDrawModule.GetPixelFromImage,
    GetPixelXY = CanvasDrawModule.GetPixelFromImageXY
}
1 Like

I released an update yesterday which fixes that issue. Check out the new module and api documentation as you can now call ImageData:GetPixel()

I forgot to actually announce the update before haha

Small ImageData Update - v3.2.0

Hey all. I have made some small needed changes to how we work and use ImageData with CanvasDraw.

For those who don’t know, ImageData is an object type containing cached pixel data from a PNG image file created from a SaveObject instance (Which can be imported by using this plugin)


Here’s a complete list of the changes:

  • ImageData is no longer treated as a table, it is now treated as an object (like the Canvas object) and can now hold methods.

  • ImageData now has two new usable methods to sample pixel data:

    • ImageData:GetPixel() and ImageData:GetPixelXY()

    • These methods replace the CanvasDraw.GetPixelFromImage() and CanvasDraw.GetPixelFromImageXY() functions as they cause some mild problems and are less practical as far as modern image manipulation standards goes. These functions have now been marked as deprecated.

  • The CanvasDraw functions GetPixelFromImage() and GetPixelFromImageXY() have now been marked as deprecated and is no longer listed in the API documentation.
    Please use the new ImageData methods instead for new work as these functions will no longer be maintained.

  • Renamed the function CanvasDraw.GetImageDataFromSaveObject() to CanvasDraw.GetImageData(). This was needed as the function name was far too long, and there currently isn’t any other ways to obtain ImageData.

  • Slight performance increase with pixel sampling (Applies to all pixel-based fetch methods)

1 Like

sooo, any update on transparency?

Transparency is probably not going to be added anytime soon unfortunately, as it will impact on performance too much.

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