Now I run into the issue where it doesn’t seem to fill the cursor boundary. Have you got any ideas on how I might fix this?
Nevermind fixed it by just adding in the radius // 10. Idk why that works but it does.
ignore ^
function Canvas:FillCircle(circle:Frame, Colour:Color3)
local Position = circle.AbsolutePosition --Top left of circle
local CenterX = Position.X + (circle.AbsoluteSize.X / 2)
local CenterY = Position.Y + (circle.AbsoluteSize.Y / 2)
local RelativePosition = Vector2New(CenterX, CenterY)
RelativePosition = Canvas:GetPointFromPosition(RelativePosition,false) --Convert it into pixel space. Not clamped
local StartX = Canvas:GetPointFromPosition(Position,false) --Find the top left in pixel space
local Radius = RelativePosition.X - StartX.X --Minus to get the radius in pixels
return Canvas:DrawCircle(RelativePosition,Radius,Colour,true) --Return drawing the circle
end
I came up a script which basically finds the pixel size of the radius of the pointer itself and then slightly tweaked the GetPointFromPosition to not clamp the values.
Thanks so much for you previous I probably wouldn’t of thought of this without that!
Hi! I come here with two feature requests that would be helpful for folks trying to use the DrawImageRect function for 2D projects:
-
Transparent Color Filter
An image should have an Optional Color3 variable that if set should make it so any pixel similar to that color (FuzzyEq Vector3) would be completely invisible (return R,G,B,0) -
Alpha Blending/Transparency
The cool thing about the Image Rect function is that you can easily manipulate it to also make animated sprite sheet characters. Unfortunately I have stumbled upon a limitation where any pixels with alpha would be either drawn as 0 or 1 with no in-betweens. It would be cool if there was an optional boolean argument that would blend the color that was already there for a simple sort of “transparency” effect.
I’ve added it myself cuz i was bored and found something similar in a different function.
This adds Transparency as an optional argument for DrawTexturedTriangleXY and DrawImageRectXY.
The catch is that since this implementation adds a new argument to texturetriXY, any older project converting to the new one would have to remember to add that extra argument. so it’s really up to you on adding it (I.e break backwards compatibility)
but regardless, it’s a small edit but it serves my usecase and maybe it would for others.
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?, TransparenyEnabled: boolean?
)
local TexResX, TexResY = ImageData.Width, ImageData.Height
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
Brightness = Brightness or 1
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
local ScanlineLength = bx - ax
if ScanlineLength == 0 then return end -- Avoid divide-by-zero
-- Calculate UV increments per pixel
local Step = 1 / ScanlineLength
local du = (tex_eu - tex_su) * Step
local dv = (tex_ev - tex_sv) * Step
-- Clip X right
if bx > self.CurrentResX then
ScanlineLength = self.CurrentResX - ax
end
-- Clip X left
local StartOffsetX = 0
local t = 0
if ax < 1 then
StartOffsetX = -(ax - 1)
t = Step * StartOffsetX
end
-- Initialize UV coordinates with offset
TexU = tex_su + t * (tex_eu - tex_su)
TexV = tex_sv + t * (tex_ev - tex_sv)
TexU = TexU * TexResX + 1
TexV = TexV * TexResY + 1
du *= TexResX
dv *= TexResY
-- Main loop to draw pixels across the scanline
if TransparenyEnabled then
-- Perform alpha blending
for j = StartOffsetX, ScanlineLength do
local SampleX = ClampN(FloorN(TexU), 1, TexResX)
local SampleY = ClampN(FloorN(TexV), 1, TexResY)
local ImgR, ImgG, ImgB, ImgA = ImageData:GetRGBA(SampleX, SampleY)
if ImgA > 0 then -- No need to do any calculations for completely transparent pixels
local BgR, BgG, BgB = InternalCanvas:GetRGB(ax + j, Y)
if ImgA < 1 then
ImgR = Lerp(BgR, ImgR, ImgA)
ImgG = Lerp(BgG, ImgG, ImgA)
ImgB = Lerp(BgB, ImgB, ImgA)
end
if Brightness < 1 then
ImgR *= Brightness
ImgG *= Brightness
ImgB *= Brightness
end
InternalCanvas:SetRGB(ax + j, Y, ImgR, ImgG, ImgB)
end
-- Increment UV values
TexU += du
TexV += dv
end
else
-- Normal render loop
for j = StartOffsetX, ScanlineLength do
local SampleX = ClampN(FloorN(TexU), 1, TexResX)
local SampleY = ClampN(FloorN(TexV), 1, TexResY)
local R, G, B, A = ImageData:GetRGBA(SampleX, SampleY)
if Brightness < 1 then
R *= Brightness
G *= Brightness
B *= Brightness
end
InternalCanvas:SetRGB(ax + j, Y, R, G, B)
-- Increment UV values
TexU += du
TexV += dv
end
end
end
-- Clip Y top
local YStart = 1
if Y1 < 1 then
YStart = 1 - Y1
end
-- Clip Y top
local TopYDist = math.min(Y2 - Y1, self.CurrentResY - Y1)
-- Draw top triangle
for i = YStart, TopYDist do
--task.wait(1)
local ax = RoundN(X1 + i * dax_step)
local bx = RoundN(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 - 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 = RoundN(X2 + (i - Y2) * dax_step)
local bx = RoundN(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:DrawImageRectXY(ImageData: {}, X: number, Y: number,
RectOffsetX: number, RectOffsetY: number, RectSizeX: number, RectSizeY: number,
ScaleX: number?, ScaleY: number?, Angle: number?, TransparencyEnabled: boolean?
) -- Contributed by @DukeAunarky
ScaleX = ScaleX or 1
ScaleY = ScaleY or 1
Angle = Angle or 0
local PivotX, PivotY = X, Y
local ImageSizeX, ImageSizeY = ImageData.Width, ImageData.Height
local ImageScaledSizeX, ImageScaledSizeY = ImageSizeX * ScaleX, ImageSizeY * ScaleY
local CosTheta, SinTheta = math.cos(Angle), math.sin(Angle)
local function RotatePoint(X, Y)
-- Rotation maths
local RotX = (CosTheta * (X - PivotX) - SinTheta * (Y - PivotY) + PivotX)
local RotY = (SinTheta * (X - PivotX) + CosTheta * (Y - PivotY) + PivotY)
return math.floor(RotX), math.floor(RotY)
end
local X1, Y1 = RotatePoint(X, Y)
local X2, Y2 = RotatePoint(X + ImageScaledSizeX - 1, Y)
local X3, Y3 = RotatePoint(X + ImageScaledSizeX - 1, Y + ImageScaledSizeY - 1)
local X4, Y4 = RotatePoint(X, Y + ImageScaledSizeY - 1)
local Padding = 0.001 -- Small padding to prevent bleeding
local U1, V1 = (RectOffsetX + Padding) / ImageSizeX, (RectOffsetY + Padding) / ImageSizeY
local U2, V2 = (RectOffsetX + RectSizeX - Padding) / ImageSizeX, (RectOffsetY + Padding) / ImageSizeY
local U3, V3 = (RectOffsetX + RectSizeX - Padding) / ImageSizeX, (RectOffsetY + RectSizeY - Padding) / ImageSizeY
local U4, V4 = (RectOffsetX + Padding) / ImageSizeX, (RectOffsetY + RectSizeY - Padding) / ImageSizeY
Canvas:DrawTexturedTriangleXY(
X1, Y1, X2, Y2, X3, Y3,
U1, V1, U2, V2, U3, V3,
ImageData, 1, TransparencyEnabled
)
Canvas:DrawTexturedTriangleXY(
X1, Y1, X4, Y4, X3, Y3,
U1, V1, U4, V4, U3, V3,
ImageData, 1, TransparencyEnabled
)
end
Also upon testing it out, there’s this very weird seam looking thing, and I’m gonna assume those are the two triangles overlapping ever so slightly from DrawTriangle
Alpha blending is definitely something I planned for the other textured methods, I just never really added it due to performance reasons before CanvasDraw v4.0.
But we’ve gotten to a point where CanvasDraw is so fast, I can add in more complex stuff such as alpha blending without having much of a performance impact at all.
As for the transparent color filter, i’m not quite sure what you’d use that for? It’s a really specific suggestion. I’ll keep a note for it however.
This is the main reason why I never added alpha blending, as you will always have a seam. Not just caused from alpha blending a 4-point image, but from normal use in a 3D engine via DrawTexturedTriangle.
I’m not quite sure how one would go about fixing this
Primary use-case is spritesheets with a background color that are programmed to be ignored when being drawn, a very common practice (surprisingly) when devs make spritesheets, and many programs (eg: Tiled) supports a color being ignored.
then not a bug then, its a feature
My main motive for adding alpha blending was because pixel art could have “smoothing” on the edges with slightly transparent colors, but rather than being rendered with alpha it’s binary [0-1 transparency] thus resulting in something like this happening:
Adding it, while may be never perfect, was indeed the solution
Ah, yeah that makes sense. You could just modify your image to have transparent pixels instead. My main concern with implementing something like this is the immediate extra per pixel computation. I imagine FuzzyEq Vector3 would not be suitable for sampling every pixel in the image at a high res.
So for now, it’s just an idea i’ll note down
I think I could make alpha blending work under very specific scenarios. Specifically for DrawDistortedImage and DrawImageRect. I’ll keep you up to date, i’ll test out some ideas
I’m not sure if this is even fixable but I might report it to see if this is doable:
When resizing the sprites (even when just multiplying with base numbers like x2, x3, x4, etc) there tends to be artifacting/“missing pixels” or “extra pixels” when being drawn at resolutions not original to its source
Multiplied by x2:
It should look like this:
That’s odd… I never had that issue. Base numbers shouldn’t be affected by something like that. I’ll take a look at it later
your triangle rasterizer is not supposed to be drawing pixels that are on the bottom-right edge, which is why its overdrawing on that image
Yeah, i’d have to make a special case for my 4 point image method
this is for any given triangle, so you need to directly add it to your draw triangle functions
Oh? How do I do that without having an issue with triangle corners not exactly meeting the given 3 points? This was one of the reasons I kind of left :DrawTexturedTriangle as is
(Apologies if this was already answered)
Will there be Wally support for this in the future?
As currently get image data from texture is limited to v 4.0 I made a rudimentary version which uses v 3. It’s quite laggy and you need the actual url of the image not the assetid.
Proxy Code: Glitch :・゚✧
(do not use this proxy in the example I will be password protecting at some point)
Example code for getting all of a users friends
local HttpService = game:GetService('HttpService')
local RunService = game:GetService('RunService')
local CanvasDraw = require(game.ReplicatedStorage.Modules.CanvasDraw)
local EndPoint = "https://thumbnails.roproxy.com/v1/users/avatar-headshot?userIds={userId}&size=180x180&format=Png&isCircular=false"
local function GetAllFriendHeadshots(id)
local success, AllFriends = pcall(function() return game.Players:GetFriendsAsync(id) end)
if not success then
warn(AllFriends)
return nil
end
local Ids = ""
repeat
for i,v in pairs(AllFriends:GetCurrentPage()) do
Ids = Ids..v.Id..","
end
if not AllFriends.IsFinished then
AllFriends:AdvanceToNextPageAsync()
end
until AllFriends.IsFinished == true
local Url = EndPoint:gsub("{userId}",Ids)
local success,res = pcall(function()
return HttpService:JSONDecode(HttpService:GetAsync(Url))
end)
if not success then
warn(res)
return nil
else
local Stuff = {}
for i,v in pairs(res.data) do
table.insert(Stuff,v.imageUrl)
end
return Stuff
end
end
local function FastWait(Count) -- Avoid lag spikes
local FastWaitCount = 0
if FastWaitCount >= Count then
FastWaitCount = 0
RunService.Heartbeat:Wait()
else
FastWaitCount += 1
end
end
local function Simplify(data)
local Newpixels = {}
local NewAlphas = {}
for i,v in pairs(data.colours) do
pcall(function()
--print(Color3.fromRGB(table.unpack(v)))
table.insert(Newpixels,Color3.fromRGB(table.unpack(v)))
end)
FastWait(4000)
end
for i,v in pairs(data.alphas) do
pcall(function()
--print(v / 255)
table.insert(NewAlphas,v)
end)
FastWait(4000)
end
return Newpixels,NewAlphas
end
local function GetImageData(link)
local url = "https://image-to-pixels-proxy.glitch.me/image-to-pixel?url="..link
local success,res = pcall(function()
return HttpService:JSONDecode(HttpService:GetAsync(url))
end)
if not success then
warn(res)
return nil
else
local Pixels,Alphas = Simplify(res)
local CanvasData = {
ImageColours = Pixels,
ImageAlphas = Alphas,
ImageResolution = Vector2.one * 180
}
local obj = CanvasDraw.CreateSaveObject(CanvasData,true)
obj.Parent = game.Workspace.Saves
obj.Name = link
return res
end
end
local Main = {}
function Main.CreateSaveOfFriends(userId)
local SaveObject = {}
local AllFriends = GetAllFriendHeadshots(userId)
for i,v in pairs(AllFriends) do
if not v then continue end
coroutine.wrap(GetImageData)(v)
FastWait(500)
end
end
return Main
you can use an png image decoder to get it without needing the proxy