├── .gitignore ├── lua ├── paint │ ├── text_cl.lua │ ├── downsampling_cl.lua │ ├── lines_cl.lua │ ├── blur_cl.lua │ ├── svg_cl.lua │ ├── batch_cl.lua │ ├── rects_cl.lua │ ├── main_cl.lua │ ├── outlines_cl.lua │ ├── circles_cl.lua │ └── rounded_boxes_cl.lua └── autorun │ └── !!!paint_autorun_sh.lua ├── minifier.py ├── LICENSE └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .luarc.json 2 | .vscode/** 3 | -------------------------------------------------------------------------------- /lua/paint/text_cl.lua: -------------------------------------------------------------------------------- 1 | ---@class paint.text 2 | local text = {} 3 | 4 | ---@class paint.text.fonts 5 | ---@field [string] paint.text.font 6 | local fonts = {} 7 | 8 | do 9 | ---@class paint.text.font 10 | ---@field name string 11 | ---@field atlasName string 12 | ---@field glyphs {[string] : string} # table containing glyph (utf8 character) as a key and cell id as a value 13 | ---@field texts {[string] : IMesh} # table containing text unique id, and a IMesh 14 | end 15 | 16 | do 17 | ---surface.DrawText generation 18 | local function generateEngineGlyphs(characterList, fontName) 19 | end 20 | end 21 | -------------------------------------------------------------------------------- /minifier.py: -------------------------------------------------------------------------------- 1 | files = [ 2 | 'main', 3 | 'batch', 4 | 'lines', 5 | 'rects', 6 | 'rounded_boxes', 7 | 'outlines', 8 | 'blur', 9 | 'circles', 10 | 'svg', 11 | 'downsampling' 12 | ] 13 | 14 | wrapper = """ 15 | if SERVER then return AddCSLuaFile() end 16 | local paint = {} 17 | local MINIFIED = true 18 | """ # do at the end is because of 'end do'.join() 19 | 20 | contents = [] 21 | for i in files: 22 | with open(f'lua/paint/{i}_cl.lua', 'r') as f: 23 | contents.append(f.read()) 24 | 25 | version = 1.12 26 | totalText = f'{wrapper} do {" end do ".join(contents)} end return paint' 27 | 28 | with open('paint_minified_source.lua', 'w') as f: 29 | f.write(totalText) 30 | -------------------------------------------------------------------------------- /lua/autorun/!!!paint_autorun_sh.lua: -------------------------------------------------------------------------------- 1 | local function load(path) 2 | AddCSLuaFile(path) 3 | if CLIENT then 4 | include(path) 5 | end 6 | end 7 | 8 | load('paint/main_cl.lua') 9 | load('paint/batch_cl.lua') 10 | load('paint/lines_cl.lua') 11 | load('paint/rects_cl.lua') 12 | load('paint/rounded_boxes_cl.lua') 13 | load('paint/outlines_cl.lua') 14 | load('paint/blur_cl.lua') 15 | load('paint/circles_cl.lua') 16 | load('paint/svg_cl.lua') 17 | load('paint/downsampling_cl.lua') 18 | 19 | --#endregion Load Examples 20 | 21 | local VERSION = 1.12 22 | local SHOW_MSG = true 23 | 24 | if SHOW_MSG then 25 | local function coloredMsgC(text) 26 | local prevRandom 27 | for i = 1, #text do 28 | local nowRandom = math.random(0, 360) 29 | nowRandom = (nowRandom + (prevRandom or nowRandom)) / 2 30 | prevRandom = nowRandom 31 | MsgC(HSVToColor(nowRandom, 0.5, 1), string.sub(text, i, i)) 32 | end 33 | MsgC('\n') 34 | end 35 | coloredMsgC('paint library has been loaded. Version is: ' .. VERSION) 36 | coloredMsgC('copyright @jaffies, aka @mikhail_svetov') 37 | end 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Wanou 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Paint Library for Garry's Mod 2 | The best painting library, that replaces surface library! 3 | 4 | ## What you can have with this library? 5 | 6 | ### Blur 7 | ![1](https://media.discordapp.net/attachments/682720535234609160/1261103048718159943/147ab44163426bd0.png?ex=669265fc&is=6691147c&hm=ed03434684f530ff617ada6e45daa5d1cb2fbced975ea072def82beb80c567b6&=&format=webp&quality=lossless&width=630&height=444) 8 | ![2](https://media.discordapp.net/attachments/682720535234609160/1261103049070608434/3f5d0bb65e62a366.png?ex=669265fd&is=6691147d&hm=dff264e966f28cfe81f27be70103e29553e25c8703fb9ee69f82c02924237a5f&=&format=webp&quality=lossless&width=791&height=444) 9 | ![3](https://media.discordapp.net/attachments/682720535234609160/1261103049519136899/1f0d08c134c69142.png?ex=669265fd&is=6691147d&hm=344991f1c794614ea81843cdcfcf336b4937ed5474b43339e675206f971e1555&=&format=webp&quality=lossless&width=780&height=444) 10 | 11 | ### Gradients 12 | ![Video 1](https://github.com/user-attachments/assets/6d8730be-8c57-4c73-8123-06ecc50f9e84) 13 | ![Video 2](https://github.com/user-attachments/assets/ef57ba80-403b-451a-8ded-442b81a3a3bf) 14 | ![Video 4](https://github.com/user-attachments/assets/da65e394-2f1d-4d22-8efe-57c74757681e) 15 | ![Video 4](https://github.com/user-attachments/assets/b6f17992-ac35-46b4-a044-3c46ec13a79c) 16 | ### Textures ;) 17 | ![8](https://github.com/user-attachments/assets/adbb8ed7-10a3-40d6-bd4f-86cc717260b4) 18 | 19 | Updates from me are currently in this [TG channel](https://t.me/+8WHlsebY9Q5lYzUy) 20 | -------------------------------------------------------------------------------- /lua/paint/downsampling_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | ---Simple library that provides a way to downsample your shapes. 3 | ---In other words: It provides you a way to make SSAA (super sampling anti aliasing) with 2D/3D meshes. 4 | ---@class paint.downsampling 5 | local downspampling = {} 6 | local paint = paint 7 | 8 | do 9 | local w, h = ScrW(), ScrH() 10 | 11 | local rt = GetRenderTargetEx('paint.downsampleRT', w * 2, h * 2, RT_SIZE_LITERAL, MATERIAL_RT_DEPTH_SEPARATE, 2 + 256, 12 | 0, IMAGE_FORMAT_BGRA8888) 13 | local material = CreateMaterial('paint.downsampleMat', 'UnlitGeneric', { 14 | ['$basetexture'] = rt:GetName(), 15 | ['$translucent'] = '1' 16 | }) 17 | 18 | ---@type IMesh? 19 | local rectMesh 20 | 21 | local function createRectMesh() 22 | if rectMesh then 23 | rectMesh:Destroy() 24 | end 25 | 26 | rectMesh = Mesh(material) 27 | 28 | local color = Color(255, 255, 255) 29 | ---@diagnostic disable-next-line: invisible 30 | paint.rects.generateRectMesh(rectMesh, 0, 0, w, h, { color, color, color, color }, 0, 0, 1, 1) 31 | end 32 | 33 | createRectMesh() 34 | ---@cast rectMesh -? 35 | 36 | hook.Add('OnScreenSizeChanged', 'paint.downsampling' .. SysTime(), function(_, _, newW, newH) 37 | w, h = newW, newH 38 | 39 | rt:Download() -- I vaguely remember it being used to reset rt params. Might not work btw.. 40 | rt = GetRenderTargetEx('paint.downsampleRT', w * 2, h * 2, RT_SIZE_LITERAL, MATERIAL_RT_DEPTH_SEPARATE, 2 + 256, 41 | 0, IMAGE_FORMAT_BGRA8888) 42 | createRectMesh() 43 | end) 44 | 45 | local pushRenderTarget = render.PushRenderTarget 46 | local popRenderTarget = render.PopRenderTarget 47 | local setMaterial = render.SetMaterial 48 | local clear = render.Clear 49 | 50 | local drawMesh = rectMesh.Draw 51 | 52 | local pushModelMatrix = cam.PushModelMatrix 53 | local popModelMatrix = cam.PopModelMatrix 54 | 55 | local start2D = cam.Start2D 56 | local end2D = cam.End2D 57 | 58 | local pushFilterMin = render.PushFilterMin 59 | local popFilterMin = render.PopFilterMin 60 | local pushFilterMag = render.PushFilterMag 61 | local popFilterMag = render.PopFilterMag 62 | 63 | local matrix = Matrix() 64 | matrix:SetScale(Vector(2, 2)) 65 | 66 | local nullMatrix = Matrix() 67 | 68 | function downspampling.start(stopMultiply) 69 | pushRenderTarget(rt) 70 | clear(0, 0, 0, 0, true, true) 71 | start2D() 72 | pushModelMatrix(stopMultiply and nullMatrix or matrix, true) 73 | end 74 | 75 | function downspampling.stop() 76 | end2D() 77 | popModelMatrix() 78 | popRenderTarget() 79 | 80 | pushFilterMin(2) 81 | pushFilterMag(2) 82 | setMaterial(material) 83 | drawMesh(rectMesh) 84 | popFilterMin() 85 | popFilterMag() 86 | end 87 | end 88 | 89 | paint.downsampling = downspampling 90 | -------------------------------------------------------------------------------- /lua/paint/lines_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | local paint = paint --[[@as paint]] 3 | 4 | --- Lines. Why they are good? 5 | --- 1) They support gradients. It means that you do not need to make a lot of lines to make 6 | --- color grading smooth between start of segment and the end of it. 7 | --- 8 | --- 2) They support batching. It means that you can make a lot of lines without any performance costs 9 | --- Examples of paint.lines 10 | --- 11 | --- Simple line example: 12 | --- 13 | --- Drawing lines with a gradient of different colors. 14 | ---```lua 15 | --- paint.lines.drawLine( 10, 20, 34, 55, Color( 0, 255, 0 ), Color( 255, 0, 255 ) ) 16 | --- paint.lines.drawLine( 40, 10, 70, 40, Color( 255, 255, 0 ) ) 17 | ---``` 18 | ---Batched Lines Example: 19 | --- 20 | ---Drawing 50 lines with improved performance by using batching. 21 | ---```lua 22 | ---paint.lines.startBatching() 23 | --- for i = 1, 50 do 24 | --- paint.lines.drawLine( i * 10, 10, i * 10 + 5, 55, Color( 0, i * 255 / 50, 0 ), Color( 255, 0, 255 ) ) 25 | --- end 26 | ---paint.lines.stopBatching() 27 | ---``` 28 | ---@class paint.lines 29 | local lines = {} 30 | 31 | --- batch table 32 | local batch = { [0] = 0 } 33 | 34 | local PRIMITIVE_LINES = MATERIAL_LINES 35 | local PRIMITIVE_LINE_STRIP = MATERIAL_LINE_STRIP 36 | local PRIMITIVE_LINE_LOOP = MATERIAL_LINE_LOOP 37 | 38 | do 39 | -- define drawing functions 40 | local meshBegin = mesh.Begin 41 | local meshEnd = mesh.End 42 | local meshPosition = mesh.Position 43 | local meshColor = mesh.Color 44 | local meshAdvanceVertex = mesh.AdvanceVertex 45 | 46 | local renderSetColorMaterialIgnoreZ = render.SetColorMaterialIgnoreZ 47 | 48 | -- single line 49 | -- It is used when there is no any batching. 50 | 51 | ---Draws single unbatched line. Used internally 52 | ---@param startX number 53 | ---@param startY number 54 | ---@param endX number 55 | ---@param endY number 56 | ---@param startColor Color 57 | ---@param endColor? Color 58 | ---@private Internal variable. Not meant to use outside 59 | function lines.drawSingleLine(startX, startY, endX, endY, startColor, endColor) 60 | if endColor == nil then 61 | endColor = startColor 62 | end 63 | 64 | renderSetColorMaterialIgnoreZ() 65 | 66 | meshBegin(PRIMITIVE_LINES, 1) 67 | meshColor(startColor.r, startColor.g, startColor.b, startColor.a) 68 | 69 | meshPosition(startX, startY, 0) 70 | 71 | meshAdvanceVertex() 72 | 73 | meshColor(endColor.r, endColor.g, endColor.b, endColor.a) 74 | 75 | meshPosition(endX, endY, 0) 76 | 77 | meshAdvanceVertex() 78 | meshEnd() 79 | end 80 | end 81 | 82 | do -- drawing 83 | local drawSingleLine = lines.drawSingleLine 84 | 85 | --- Draws a line with the specified parameters. 86 | ---@param startX number # The X position of the start of the line 87 | ---@param startY number # The Y position of the start of the line 88 | ---@param endX number # The X position of the end of the line 89 | ---@param endY number # The Y position of the end of the line 90 | ---@param startColor Color # The color of the start of the line 91 | ---@param endColor? Color # The color of the end of the line. Default: startColor 92 | function lines.drawLine(startX, startY, endX, endY, startColor, endColor) 93 | drawSingleLine(startX, startY, endX, endY, startColor, endColor) 94 | end 95 | end 96 | 97 | paint.lines = lines 98 | -------------------------------------------------------------------------------- /lua/paint/blur_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | ---The paint library has a built-in blur effect! 3 | --- 4 | ---This works by taking a copy of the screen, lowering its resolution, blurring it, then returning that as a material. 5 | --- 6 | ---You can then use that material with any of the paint functions to draw a blurred shape. 7 | --- 8 | ---It's a simple, cheap, and cool effect! 9 | --- 10 | ---Simple example: 11 | ---```lua 12 | ---local x, y = panel:LocalToScreen( 0, 0 ) -- getting absolute position 13 | ---local scrW, scrH = ScrW(), ScrH() -- it will be used to get UV coordinates 14 | ---local mat = paint.blur.getBlurMaterial() 15 | ---paint.rects.drawRect( 0, 0, 100, 64, color_white, mat, x / scrW, y / scrH, (x + 100) / scrW, (y + 64) / scrH ) 16 | ---paint.roundedBoxes.roundedBox( 32, 120, 0, 120, 64, color_white, mat, (x + 120) / scrW, y / scrH, (x + 240) / scrW, (y + 64) / scrH ) 17 | ---``` 18 | 19 | ---@class paint.blur 20 | local blur = {} 21 | local paint = paint 22 | 23 | --[[ 24 | Library that gets blured frame texture. 25 | It doesn't ocupy smalltex1 now. Use it freely) 26 | ]] 27 | 28 | local RT_SIZE = 256 29 | 30 | local BLUR = 10 31 | local BLUR_PASSES = 1 32 | local BLUR_TIME = 1 / 30 33 | 34 | local BLUR_EXPENSIVE = true -- This is set to true because default gmodscreenspace shader actually sucks and makes noise on some devices 35 | 36 | local RT_FLAGS = 2 + 256 + 32768 37 | local TEXTURE_PREFIX = 'paint_library_rt_' 38 | local MATERIAL_PREFIX = 'paint_library_material_' 39 | 40 | ---@type {[string] : ITexture} 41 | local textures = { 42 | default = GetRenderTargetEx(TEXTURE_PREFIX .. 'default', RT_SIZE, RT_SIZE, 1, 2, RT_FLAGS, 0, 3) 43 | } 44 | 45 | ---@type {[string] : number} 46 | local textureTimes = { 47 | default = 0 48 | } 49 | 50 | ---@type {[string] : IMaterial} 51 | local textureMaterials = { 52 | default = CreateMaterial(MATERIAL_PREFIX .. 'default', 'UnlitGeneric', { 53 | ['$basetexture'] = TEXTURE_PREFIX .. 'default', 54 | ['$vertexalpha'] = 1, 55 | ['$vertexcolor'] = 1, 56 | }) 57 | } 58 | 59 | 60 | do 61 | local copyRTToTex = render.CopyRenderTargetToTexture 62 | 63 | local pushRenderTarget = render.PushRenderTarget 64 | local popRenderTarget = render.PopRenderTarget 65 | 66 | local start2D = cam.Start2D 67 | local end2D = cam.End2D 68 | 69 | local overrideColorWriteEnable = render.OverrideColorWriteEnable 70 | local overrideAlphaWriteEnable = render.OverrideAlphaWriteEnable 71 | local drawScreenQuad = render.DrawScreenQuad 72 | local updateScreenEffectTexture = render.UpdateScreenEffectTexture 73 | local setMaterial = render.SetMaterial 74 | 75 | local blurMaterial = Material('pp/blurscreen') 76 | 77 | local setTexture = blurMaterial.SetTexture 78 | local setFloat = blurMaterial.SetFloat 79 | local recompute = blurMaterial.Recompute 80 | 81 | local screenEffectTexture = render.GetScreenEffectTexture() 82 | local whiteMaterial = Material('vgui/white') 83 | 84 | local blurRTExpensive = render.BlurRenderTarget 85 | 86 | ---@param rt ITexture 87 | ---@param _ number 88 | ---@param blurStrength number 89 | ---@param passes number 90 | local function blurRTCheap(rt, _, blurStrength, passes) 91 | setMaterial(blurMaterial) 92 | setTexture(blurMaterial, '$basetexture', rt) 93 | 94 | for i = 1, passes do 95 | setFloat(blurMaterial, '$blur', (i / passes) * blurStrength) 96 | recompute(blurMaterial) 97 | 98 | -- if you don't update screenEffect texture 99 | -- Then for whatever reason gmodscreenspace 100 | -- shader won't update it's $basetexture 101 | -- resulting in broken passes 102 | -- and picture like it was only single pass instead of multiple. 103 | 104 | --ScreenEffect texutre is not used by blur at all. 105 | --Like literally, i have to update it only for gmodscreenspace shader to work. 106 | --That's tottally retarded. 107 | updateScreenEffectTexture() 108 | drawScreenQuad() 109 | end 110 | 111 | --Reseting it's basetexture to default one 112 | setTexture(blurMaterial, '$basetexture', screenEffectTexture) 113 | end 114 | 115 | 116 | ---Blurs texture with specified parameters 117 | ---@param blurStrength number? How much blur strength the result texture will have. Overrides BLUR 118 | ---@param passes number? How much bluring passes texture will have. More passes will result in better bluring quality, but worse performace. Affects performance a lot. 119 | ---@param expensive boolean? If set to true, it will try to blur texture with defualt Source Engine shaders called BlurX, BlurY. They are expensive. If unset or false, it will try to blur stuff with gmodscreenspace shader. 120 | function blur.generateBlur(id, blurStrength, passes, expensive) -- used right before drawing 2D shit 121 | local texToBlur = textures[id or 'default'] 122 | 123 | blurStrength = blurStrength or BLUR 124 | passes = passes or BLUR_PASSES 125 | expensive = expensive or BLUR_EXPENSIVE 126 | 127 | copyRTToTex(texToBlur) 128 | 129 | pushRenderTarget(texToBlur) 130 | start2D() 131 | ---@type fun(texture: ITexture, blurX: number, blurY: number, passes: number) 132 | local blurRT = expensive and blurRTExpensive or blurRTCheap 133 | blurRT(texToBlur, blurStrength, blurStrength, passes) 134 | 135 | overrideAlphaWriteEnable(true, true) 136 | overrideColorWriteEnable(true, false) 137 | 138 | setMaterial(whiteMaterial) 139 | drawScreenQuad() 140 | 141 | overrideAlphaWriteEnable(false, true) 142 | overrideColorWriteEnable(false, true) 143 | end2D() 144 | popRenderTarget() 145 | 146 | -- Even if this RT doesn't use alpha channel (IMAGE_FORMAT), it stil somehow uses alpha... BAD! 147 | -- At least no clearDepth 148 | end 149 | end 150 | 151 | do 152 | local clock = os.clock 153 | local generateBlur = blur.generateBlur 154 | 155 | ---Tries to blur texture with specified id and parameters according to it's last time being blurred 156 | ---@param id string Identifier of blur texture. If set to nil or 'default', then default blur texture will be asked to be blurred with legacy logic 157 | ---If it is set, and not set to 'default', then it tries to blur texture if needs to and enables other arguments as well. Use with caution! 158 | ---@param time number? How much time needs to be passed for next texture's bluring? You usually want it to set to ``1 / blurFPS``. Overrides BLUR_FPS. Affects performance a lot. 159 | ---@param blurStrength number? How much blur strength the result texture will have. Overrides BLUR 160 | ---@param passes number? How much bluring passes texture will have. More passes will result in better bluring quality, but worse performace. Affects performance a lot. 161 | ---@param expensive boolean? If set to true, it will try to blur texture with defualt Source Engine shaders called BlurX, BlurY. They are expensive. If unset or false, it will try to blur stuff with gmodscreenspace shader. 162 | ---@overload fun(id : 'default'?): IMaterial 163 | function blur.requestBlur(id, time, blurStrength, passes, expensive) 164 | id = id or 'default' 165 | time = time or BLUR_TIME 166 | 167 | if textureTimes[id] == nil then 168 | textureTimes[id] = clock() + time 169 | return 170 | end 171 | 172 | if id ~= 'default' and textureTimes[id] < clock() then 173 | generateBlur(id, blurStrength, passes, expensive) 174 | 175 | if time > 0 then 176 | textureTimes[id] = nil 177 | else 178 | textureTimes[id] = 0 179 | end 180 | end 181 | end 182 | 183 | hook.Add('RenderScreenspaceEffects', 'paint.blur' .. SysTime(), function() 184 | local time = textureTimes['default'] 185 | if time == nil then return end 186 | 187 | if time < clock() then 188 | generateBlur() 189 | textureTimes['default'] = nil 190 | end 191 | end) 192 | end 193 | 194 | do 195 | local requestBlur = blur.requestBlur 196 | local getRenderTargetEx = GetRenderTargetEx 197 | 198 | local createMaterial = CreateMaterial 199 | 200 | local pushRenderTarget = render.PushRenderTarget 201 | local popRenderTarget = render.PopRenderTarget 202 | local clear = render.Clear 203 | 204 | ---Returns a Texture with the blurred image from the screen. 205 | ---@param id string Identifier of blur texture. If set to nil or 'default', then default blur texture will be returned with legacy logic 206 | ---If it is set, and not set to 'default', then it tries to blur texture if needs to and enables other arguments as well. Use with caution! 207 | ---@param time number? How much time needs to be passed for next texture's bluring? You usually want it to set to ``1 / blurFPS``. Overrides BLUR_FPS. Affects performance a lot. 208 | ---@param blurStrength number? How much blur strength the result texture will have. Overrides BLUR 209 | ---@param passes number? How much bluring passes texture will have. More passes will result in better bluring quality, but worse performace. Affects performance a lot. 210 | ---@param expensive boolean? If set to true, it will try to blur texture with defualt Source Engine shaders called BlurX, BlurY. They are expensive. If unset or false, it will try to blur stuff with gmodscreenspace shader. 211 | ---@nodiscard 212 | ---@overload fun(id : 'default'?): ITexture 213 | ---@return ITexture 214 | function blur.getBlurTexture(id, time, blurStrength, passes, expensive) 215 | id = id or 'default' 216 | 217 | if textures[id] == nil then 218 | local tex = getRenderTargetEx(TEXTURE_PREFIX .. id, RT_SIZE, RT_SIZE, 1, 2, RT_FLAGS, 0, 3) 219 | textures[id] = tex 220 | textureTimes[id] = 0 221 | 222 | pushRenderTarget(tex) 223 | clear(0, 0, 0, 255) 224 | popRenderTarget() 225 | end 226 | 227 | requestBlur(id, time, blurStrength, passes, expensive) 228 | 229 | return textures[id] 230 | end 231 | 232 | local getBlurTexture = blur.getBlurTexture 233 | 234 | ---Returns a Material with the blurred image from the screen. 235 | ---@param id string Identifier of blur material. If set to nil or 'default', then default blur material will be returned with legacy logic 236 | ---If it is set, and not set to 'default', then it tries to blur material if needs to and enables other arguments as well. Use with caution! 237 | ---@param time number? How much time needs to be passed for next material's bluring? You usually want it to set to ``1 / blurFPS``. Overrides BLUR_FPS. Affects performance a lot. 238 | ---@param blurStrength number? How much blur strength the result material will have. Overrides BLUR 239 | ---@param passes number? How much bluring passes material will have. More passes will result in better bluring quality, but worse performace. Affects performance a lot. 240 | ---@param expensive boolean? If set to true, it will try to blur material with defualt Source Engine shaders called BlurX, BlurY. They are expensive. If unset or false, it will try to blur stuff with gmodscreenspace shader. 241 | ---@nodiscard 242 | ---@overload fun(id : 'default'?): IMaterial 243 | ---@return IMaterial # Blurred screen image 244 | function blur.getBlurMaterial(id, time, blurStrength, passes, expensive) 245 | id = id or 'default' 246 | local mat = textureMaterials[id] 247 | 248 | if mat == nil then 249 | mat = createMaterial(MATERIAL_PREFIX .. id, 'UnlitGeneric', { 250 | ['$basetexture'] = getBlurTexture(id, time, blurStrength, passes, expensive):GetName(), 251 | ['$vertexalpha'] = 1, 252 | ['$vertexcolor'] = 1, 253 | ['$model'] = 1, 254 | ['$translucent'] = 1, 255 | }) 256 | textureMaterials[id] = mat 257 | 258 | return mat -- requestBlur is arleady done. 259 | end 260 | 261 | requestBlur(id, time, blurStrength, passes, expensive) 262 | 263 | return mat 264 | end 265 | end 266 | 267 | paint.blur = blur 268 | -------------------------------------------------------------------------------- /lua/paint/svg_cl.lua: -------------------------------------------------------------------------------- 1 | --- Currently WIP. 2 | --- Planned features are (strong means completed): 3 | --- 2. ** Support for rect ** 4 | --- 3. ** Support for circle ** 5 | --- 1. Support for defs and g 6 | --- 4. Support for path 7 | --- 5. Support for fill 8 | --- 6. Support for fill='linear-gradient' 9 | --- 7. Support for ellipses in path 10 | ---@class paint.svg 11 | local svg = {} 12 | local paint = paint 13 | 14 | paint.svg = svg 15 | 16 | do -- CSS Colors 17 | local sub = string.sub 18 | local color = Color 19 | 20 | ---An olde function of mine from simple UI library. Could GAMMA un-correct color too. 21 | ---@param hex string 22 | ---@return Color 23 | local function fromHex(hex) 24 | local ind = sub(hex, 1, 1) == '#' and 1 or 0 25 | local r, g, b, a = hex:sub(1 + ind, 2 + ind), hex:sub(3 + ind, 4 + ind), hex:sub(5 + ind, 6 + ind), 26 | hex:sub(7 + ind, 8 + ind) 27 | 28 | ---@diagnostic disable-next-line: cast-local-type 29 | r, g, b, a = tonumber(r, 16) or 255, tonumber(g, 16) or 255, tonumber(b, 16) or 255, 30 | a ~= '' and tonumber(a, 16) or 255 31 | 32 | return color(r, g, b, a) 33 | end 34 | 35 | ---https://developer.mozilla.org/en-US/docs/Web/CSS/named-color 36 | ---@enum paint.svg.colors 37 | local colors = { 38 | ['black'] = color(0, 0, 0), 39 | ['silver'] = fromHex('#c0c0c0'), 40 | ['gray'] = fromHex('#808080'), 41 | ['white'] = color(255, 255, 255), 42 | ['maroon'] = fromHex('#800000'), 43 | ['red'] = color(255, 0, 0), 44 | ['purple'] = fromHex('#800080'), 45 | ['fuchsia'] = fromHex('#ff00ff'), 46 | ['green'] = color(0, 255, 0), 47 | ['lime'] = fromHex('#00ff00'), 48 | ['olive'] = fromHex('#808000'), 49 | ['yellow'] = fromHex('#ffff00'), 50 | ['navy'] = fromHex('#000080'), 51 | ['blue'] = color(0, 0, 255), 52 | ['teal'] = fromHex('#008080'), 53 | ['aqua'] = fromHex('#00ffff'), 54 | --- Below are additional colors. They will be added here when i wont be lazy... 55 | ['aliceblue'] = fromHex('#F0F8FF'), 56 | ['pink'] = fromHex('#FFC0CB'), 57 | } 58 | 59 | ---@diagnostic disable-next-line: inject-field 60 | colors.fromHex = fromHex 61 | svg.colors = colors 62 | end 63 | 64 | ---Parsing 65 | do 66 | ---@class paint.svg.parserObj 67 | ---@field [0] string type 68 | 69 | ---@type {[string] : fun(tag : string) : paint.svg.parserObj} 70 | local parsers = {} 71 | 72 | ---@param tag string 73 | ---@param parser fun(tag : string) : paint.svg.parserObj 74 | local function addParser(tag, parser) 75 | parsers[tag] = parser 76 | end 77 | 78 | local match = string.match 79 | local sub = string.sub 80 | 81 | local parseColor 82 | do 83 | local colors = svg.colors 84 | local black = Color(0, 0, 0) 85 | 86 | ---@param color string 87 | ---@return gradients 88 | local function parseColorValue(color) 89 | if colors[color:lower()] then 90 | return colors[color:lower()] 91 | elseif sub(color, 1, 1) == '#' then 92 | return colors.fromHex(color) 93 | else 94 | return black 95 | end 96 | end 97 | 98 | function parseColor(tag, name) 99 | return parseColorValue(match(tag, name .. ' ?=["\']([%w%d#().]+)["\']') or '') 100 | end 101 | end 102 | 103 | local parseSize 104 | do 105 | local tonumber = tonumber 106 | ---@return fun(height : number) : number 107 | local function createPercentFunc(val) 108 | local percent = tonumber(sub(val, 1, -2)) / 100 109 | return function(height) 110 | return height * percent 111 | end 112 | end 113 | 114 | ---@param val integer? 115 | ---@return fun() : number 116 | local function createAbsoluteFunc(val) 117 | val = tonumber(val) or 0 118 | return function() 119 | return val 120 | end 121 | end 122 | 123 | local patternSafe = string.PatternSafe 124 | 125 | ---@param tag string 126 | ---@param name string 127 | ---@param name2 string 128 | ---@return fun(w : number, h : number) : number, number 129 | ---@overload fun(tag: string, name : string) : fun(w : number, h: number) : number 130 | function parseSize(tag, name, name2) 131 | name = patternSafe(name) 132 | 133 | if name2 then 134 | name2 = patternSafe(name2) 135 | end 136 | 137 | do -- Positioning 138 | local x = match(tag, name .. ' ?=["\'](.-)["\']') 139 | local y = match(tag, (name2 or name) .. ' ?=["\'](.-)["\']') 140 | 141 | local xFunc, yFunc 142 | if x and sub(x, -1, -1) == '%' then 143 | xFunc = createPercentFunc(x) 144 | else 145 | xFunc = createAbsoluteFunc(x) 146 | end 147 | 148 | if y and sub(y, -1, -1) == '%' then 149 | yFunc = createPercentFunc(y) 150 | else 151 | yFunc = createAbsoluteFunc(y) 152 | end 153 | 154 | if name2 then 155 | return function(w, h) 156 | return xFunc(w), yFunc(h) 157 | end 158 | end 159 | 160 | return function(w, h) 161 | ---@diagnostic disable-next-line: missing-return-value 162 | return xFunc((w + h) / 2) 163 | end 164 | end 165 | end 166 | end 167 | 168 | ---@class paint.svg.parserObj.rect : paint.svg.parserObj 169 | ---@field pos fun(w, h) : number, number # This function returns position of rect 170 | ---@field size fun(w, h) : number, number # This function returns size of rect. 171 | ---@field fill gradients # returns colors of filling 172 | ---@field stroke Color? # return stroke color. Can be nil if stroke is unset. 173 | ---@field radius number # returns (rx+ry) / 2 - average radius of horizontal/vertical radiuses of roundings. 174 | ---@field strokeWidth fun(w, h) : number # 175 | 176 | 177 | ---@param tag string 178 | ---@return paint.svg.parserObj.rect 179 | addParser('rect', function(tag) 180 | local tab = {} 181 | 182 | tab.pos = parseSize(tag, 'x', 'y') 183 | tab.size = parseSize(tag, 'width', 'height') 184 | 185 | local radiusSize = parseSize(tag, 'rx', 'ry') 186 | 187 | ---@type fun(w : number, h : number) : number 188 | tab.radius = function(w, h) 189 | local rx, ry = radiusSize(w, h) 190 | 191 | return (rx + ry) / 2 192 | end 193 | 194 | tab.strokeWidth = parseSize(tag, 'stroke-width') 195 | 196 | do 197 | tab.fill = parseColor(tag, 'fill') 198 | tab.stroke = parseColor(tag, 'stroke') 199 | end 200 | 201 | ---@cast tab paint.svg.parserObj.rect 202 | return tab 203 | end) 204 | 205 | ---@class paint.svg.parserObj.circle : paint.svg.parserObj 206 | ---@field pos fun(w, h) : number, number # This function returns position of rect 207 | ---@field radius fun(w, h) : number 208 | ---@field fill Color # returns colors of filling 209 | ---@field strokeWidth number 210 | ---@field stroke Color 211 | 212 | ---@param tag string 213 | ---@return paint.svg.parserObj.circle 214 | addParser('circle', function(tag) 215 | local tab = {} 216 | 217 | tab.pos = parseSize(tag, 'cx', 'cy') 218 | tab.radius = parseSize(tag, 'r') 219 | 220 | do 221 | tab.fill = parseColor(tag, 'fill') 222 | tab.stroke = parseColor(tag, 'stroke') 223 | end 224 | 225 | tab.strokeWidth = parseSize(tag, 'stroke-width') 226 | 227 | return tab 228 | end) 229 | 230 | ---@class paint.svg.parserObj.svg : paint.svg.parserObj 231 | ---@field width number? 232 | ---@field height number? 233 | ---@field x number? 234 | ---@field y number? 235 | 236 | ---@param tag string 237 | ---@return paint.svg.parserObj.svg 238 | addParser('svg', function(tag) 239 | local tab = {} 240 | 241 | local x, y, w, h = match(tag, 'viewBox ?=["\'] ?(%d+) (%d+) (%d+) (%d+) ?["\']') 242 | 243 | w = w or match(tag, 'width ?=["\'](%d+)["\']') 244 | h = h or match(tag, 'height ?=["\'](%d+)["\']') 245 | 246 | x = x or match(tag, 'x ?=["\'](%d+)["\']') 247 | y = y or match(tag, 'y ?=["\'](%d+)["\']') 248 | 249 | tab.x = tonumber(x) 250 | tab.y = tonumber(y) 251 | tab.width = tonumber(w) 252 | tab.height = tonumber(h) 253 | 254 | ---@cast tab paint.svg.parserObj.svg 255 | return tab 256 | end) 257 | 258 | svg.parsers = parsers 259 | end 260 | 261 | ---Construction 262 | do 263 | ---@class paint.svg.settings 264 | ---@field w number 265 | ---@field h number 266 | ---@field x number 267 | ---@field y number 268 | ---@field circleVertexCount number? 269 | 270 | ---@type {[string] : fun(obj : paint.svg.parserObj, settings : paint.svg.settings)} 271 | local constructors = {} 272 | 273 | ---@param tag string 274 | ---@param constructor fun(obj : paint.svg.parserObj, settings : paint.svg.settings) 275 | local function addConstructor(tag, constructor) 276 | constructors[tag] = constructor 277 | end 278 | 279 | do 280 | local min = math.min 281 | local roundedBox = paint.roundedBoxes.roundedBox 282 | local drawOutline = paint.outlines.drawOutline 283 | ---@param obj paint.svg.parserObj.rect 284 | ---@param settings paint.svg.settings 285 | addConstructor('rect', function(obj, settings) 286 | local x, y = obj.pos(settings.w, settings.h) 287 | local w, h = obj.size(settings.w, settings.h) 288 | 289 | local strokeWidth = obj.strokeWidth(settings.w, settings.h) 290 | 291 | local radiusVal = min(w, h) / 2 292 | local radius = obj.radius(radiusVal, radiusVal) 293 | 294 | local fill = obj.fill 295 | local stroke = obj.stroke 296 | 297 | roundedBox(radius, x + settings.x, y + settings.y, w, h, fill) 298 | 299 | if strokeWidth > 0 then 300 | ---@cast stroke -? 301 | drawOutline(radius, x + settings.x, y + settings.y, w, h, stroke, nil, strokeWidth) 302 | end 303 | end) 304 | end 305 | 306 | do 307 | local drawCircle = paint.circles.drawCircle 308 | local drawOutlinedCircle = paint.circles.drawOutline 309 | ---@param obj paint.svg.parserObj.circle 310 | ---@param settings paint.svg.settings 311 | addConstructor('circle', function(obj, settings) 312 | local x, y = obj.pos(settings.w, settings.h) 313 | local radius = obj.radius(settings.w, settings.h) 314 | 315 | local fill = obj.fill 316 | local stroke = obj.stroke 317 | 318 | drawCircle(x + settings.x, y + settings.y, radius, radius, fill, settings.circleVertexCount) 319 | 320 | local strokeWidth = obj.strokeWidth(radius, radius) 321 | 322 | if strokeWidth > 0 then 323 | drawOutlinedCircle(x + settings.x, y + settings.y, radius, radius, stroke, strokeWidth, 324 | settings.circleVertexCount) 325 | end 326 | end) 327 | end 328 | 329 | ---@param obj paint.svg.parserObj.svg 330 | ---@param settings paint.svg.settings 331 | addConstructor('svg', function(obj, settings) 332 | settings.w = obj.width or settings.w 333 | settings.h = obj.height or settings.h 334 | 335 | settings.x = obj.x or settings.x 336 | settings.y = obj.y or settings.y 337 | end) 338 | 339 | svg.constructors = constructors 340 | end 341 | 342 | ---API 343 | do 344 | local gmatch = string.gmatch 345 | local match = string.match 346 | 347 | local parsers = svg.parsers 348 | 349 | ---@param svgText string 350 | ---@return {[integer] : paint.svg.parserObj} 351 | function svg.parseSVG(svgText) 352 | local parsingTab = {} 353 | local index = 0 354 | 355 | for tag in gmatch(svgText, '<(.-)>') do 356 | ---@type string 357 | local nameOfTag = match(tag, '^(%w+)') or 'undefined' 358 | if parsers[nameOfTag] then 359 | local tab = parsers[nameOfTag](tag) 360 | tab[0] = nameOfTag 361 | 362 | index = index + 1 363 | parsingTab[index] = tab 364 | elseif nameOfTag ~= 'undefined' then 365 | print('[paint] Cant parse <' .. nameOfTag .. '> tag. Not implemented yet') 366 | end 367 | end 368 | 369 | return parsingTab 370 | end 371 | 372 | local startBatching = paint.batch.startBatching 373 | local stopBatching = paint.batch.stopBatching 374 | 375 | local constructors = svg.constructors 376 | 377 | ---@param tab {[integer] : paint.svg.parserObj} 378 | ---@param settings paint.svg.settings? 379 | ---@return IMesh 380 | function svg.generateIMesh(tab, settings) 381 | settings = settings or { w = 0, h = 0, x = 0, y = 0 } 382 | ---@cast settings -? 383 | 384 | startBatching() 385 | 386 | for k, v in ipairs(tab) do 387 | local type = v[0] 388 | 389 | if constructors[type] then 390 | constructors[type](v, settings) 391 | else 392 | print('[paint] Cant construct IMesh from ' .. type .. ' type. Not implemented yet.') 393 | end 394 | end 395 | 396 | return stopBatching() 397 | end 398 | end 399 | -------------------------------------------------------------------------------- /lua/paint/batch_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | local paint = paint --[[@as paint]] 3 | ---# Batching library of paint lib 4 | ---This is a really hard to explain thing, and made for experienced lua coders 5 | --- 6 | ---This library allows you to generate IMeshes on the fly, by using default 7 | ---paint library draw functions 8 | --- 9 | ---In order to cache resulted IMesh of course! 10 | --- 11 | ---That allows you to batch your multiple shape in 1 single mesh in order to save draw calls 12 | ---@class paint.batch 13 | local batch = {} 14 | 15 | do 16 | ---@class paint.batch.atlasData 17 | ---@field size integer 18 | ---@field cellSize integer 19 | ---@field cells { [string] : integer, [0] : integer } table containing string id of cells and it's int position 20 | 21 | local getRenderTarget = GetRenderTarget 22 | 23 | ---@type {[string] : paint.batch.atlasData} 24 | local atlasData = {} 25 | ---@rtoe {[string] : ITexture} 26 | local atlasTextures = {} 27 | 28 | ---@param name string 29 | ---@param atlasSize integer 30 | ---@param cellSize integer # max value is ``atlasSize / 2`` 31 | function batch.createAtlas(name, atlasSize, cellSize) 32 | ---@type paint.batch.atlasData 33 | local data = { size = atlasSize, cellSize = cellSize, cells = { [0] = 0 } } 34 | 35 | atlasData[name] = data 36 | atlasTextures[name] = getRenderTarget('paintlib.' .. name, atlasSize, atlasSize) 37 | 38 | return atlasData[name], atlasTextures[name] 39 | end 40 | 41 | ---@param name string 42 | function batch.getAtlas(name) 43 | return atlasData[name], atlasTextures[name] 44 | end 45 | 46 | ---@param name string 47 | ---@param id string 48 | function batch.createAtlasCell(name, id) 49 | local cells = atlasData[name].cells 50 | local position = cells[0] + 1 51 | 52 | cells[id] = position 53 | cells[0] = position 54 | end 55 | 56 | local floor = math.floor 57 | 58 | ---@param name string 59 | ---@return number uStart 60 | ---@return number vStart 61 | ---@return number uEnd 62 | ---@return number vEnd 63 | function batch.getAtlasCellPosition(name, id) 64 | local data = atlasData[name] 65 | 66 | if not data then 67 | return 0, 0, 1, 1 68 | end 69 | 70 | local position = data.cells[id] - 1 71 | 72 | local atlasSize = data.size 73 | local cellSize = data.cellSize 74 | local cellsPerRow = atlasSize / data.cellSize 75 | 76 | local x = position % cellsPerRow 77 | local y = floor(position / cellsPerRow) 78 | 79 | local xUV, yUV = (cellSize * x) / atlasSize, cellSize * y / atlasSize 80 | return xUV, yUV, xUV + cellSize / atlasSize, yUV + cellSize / atlasSize 81 | end 82 | 83 | ---@type string|nil 84 | local currentAtlas 85 | ---@type string? 86 | local currentCell 87 | 88 | ---@param name string? # can be nil to unset atlas 89 | function batch.setDrawAtlas(name) 90 | currentAtlas = name 91 | end 92 | 93 | ---@return string|nil returns nil if no atlas was set 94 | function batch.getDrawAtlas() 95 | return currentAtlas 96 | end 97 | 98 | ---@param id string? 99 | function batch.setDrawCell(id) 100 | currentCell = id 101 | end 102 | 103 | function batch.getDrawCell() 104 | return currentCell 105 | end 106 | 107 | ---@param atlasName string 108 | ---@param cellId string 109 | ---@param material IMaterial 110 | function batch.copyMaterialToAtlas(atlasName, cellId, material) 111 | local data, texture = batch.getAtlas(atlasName) 112 | 113 | local uStart, vStart, uEnd, vEnd = batch.getAtlasCellPosition(atlasName, cellId) 114 | local size, cellSize = data.size, data.cellSize 115 | 116 | render.PushRenderTarget(texture) 117 | cam.Start2D() 118 | surface.SetDrawColor(255, 255, 255) 119 | surface.SetMaterial(material) 120 | surface.DrawTexturedRect(uStart * size, vStart * size, cellSize, cellSize) 121 | cam.End2D() 122 | render.PopRenderTarget() 123 | end 124 | 125 | batch.offsetX = 0 126 | batch.offsetY = 0 127 | 128 | ---@offsets x, y positions for batching. 129 | ---@param x number 130 | ---@param y number 131 | function batch.setRelativePosition(x, y) 132 | batch.offsetX = x 133 | batch.offsetY = y 134 | end 135 | end 136 | 137 | batch.batching = false 138 | 139 | ---@type table # current batching table 140 | local batchTable = { 141 | [0] = 0 142 | } 143 | 144 | --- Resets batching queue 145 | function batch.reset() 146 | batchTable = { 147 | [0] = 0 148 | } 149 | 150 | ---@type table # current batching table 151 | batch.batchTable = batchTable 152 | end 153 | 154 | --- Starts batching queue 155 | function batch.startBatching() 156 | batch.batching = true 157 | batch.reset() 158 | end 159 | 160 | --[[ 161 | I guess this function will get JIT compiled 162 | ]] 163 | 164 | --- Internal function 165 | ---@param tab table 166 | ---@param i integer offset 167 | ---@return number x 168 | ---@return number y 169 | ---@return number z 170 | ---@return Color color 171 | ---@return number x1 172 | ---@return number y1 173 | ---@return Color color1 174 | ---@return number x2 175 | ---@return number y2 176 | ---@return Color color2 177 | ---@return number u1 178 | ---@return number v1 179 | ---@return number u2 180 | ---@return number v2 181 | ---@return number u3 182 | ---@return number v3 183 | ---@return integer cellPosition 184 | local function getVariables(tab, i) 185 | return tab[i], tab[i + 1], tab[i + 2], tab[i + 3], tab[i + 4], tab[i + 5], tab[i + 6], tab[i + 7], tab[i + 8], 186 | tab[i + 9], tab[i + 10], tab[i + 11], tab[i + 12], tab[i + 13], tab[i + 14], tab[i + 15], tab[i + 16] 187 | end 188 | 189 | do 190 | local meshBegin = mesh.Begin 191 | local meshEnd = mesh.End 192 | local meshPosition = mesh.Position 193 | local meshTexCoord = mesh.TexCoord 194 | local meshColor = mesh.Color 195 | local meshAdvanceVertex = mesh.AdvanceVertex 196 | 197 | local meshConstructor = Mesh 198 | local PRIMITIVE_TRIANGLES = MATERIAL_TRIANGLES 199 | 200 | local remap = math.Remap 201 | 202 | --- Stops batching queue and returns builded mesh. 203 | ---@return IMesh batchedMesh #batched mesh 204 | ---@nodiscard 205 | function batch.stopBatching() 206 | local tab = batch.batchTable 207 | 208 | local iMesh = meshConstructor() 209 | 210 | meshBegin(iMesh, PRIMITIVE_TRIANGLES, tab[0] * 0.3) 211 | local res, reason = pcall(function() 212 | local atlas = batch.getDrawAtlas() 213 | 214 | local offsetX, offsetY = batch.offsetX, batch.offsetY 215 | for i = 1, tab[0], 17 do 216 | local x, y, z, color, x1, y1, color1, x2, y2, color2, u1, v1, u2, v2, u3, v3, cell = getVariables( 217 | tab, i) 218 | 219 | local cellu1, cellv1, cellu2, cellv2 = 0, 0, 1, 1 220 | if atlas and cell then 221 | cellu1, cellv1, cellu2, cellv2 = batch.getAtlasCellPosition(atlas, cell) 222 | end 223 | 224 | u1, v1, u2, v2, u3, v3 = u1 or 0, v1 or 1, u2 or 1, v2 or 0, u3 or 1, v3 or 1 225 | 226 | x = x + offsetX 227 | y = y + offsetY 228 | 229 | meshPosition(x, y, z) 230 | meshColor(color.r, color.g, color.b, color.a) 231 | meshTexCoord(0, remap(u1, 0, 1, cellu1, cellu2), remap(v1, 0, 1, cellv1, cellv2)) 232 | meshAdvanceVertex() 233 | 234 | meshPosition(x1, y1, z) 235 | meshColor(color1.r, color1.g, color1.b, color1.a) 236 | meshTexCoord(0, remap(u2, 0, 1, cellu1, cellu2), remap(v2, 0, 1, cellv1, cellv2)) 237 | meshAdvanceVertex() 238 | 239 | meshPosition(x2, y2, z) 240 | meshColor(color2.r, color2.g, color2.b, color2.a) 241 | meshTexCoord(0, remap(u3, 0, 1, cellu1, cellu2), remap(v3, 0, 1, cellv1, cellv2)) 242 | meshAdvanceVertex() 243 | end 244 | end) 245 | meshEnd() 246 | 247 | if not res then 248 | print('[paint] batching error', reason) 249 | end 250 | 251 | batch.reset() 252 | batch.batching = false 253 | 254 | return iMesh 255 | end 256 | end 257 | 258 | do 259 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 260 | local meshDestroy = FindMetaTable('IMesh') --[[@as IMesh]].Destroy 261 | local resetZ = paint.resetZ 262 | 263 | local setMaterial = render.SetMaterial 264 | 265 | local startBatching = batch.startBatching 266 | local stopBatching = batch.stopBatching 267 | 268 | local startVGUI, endVGUI = paint.startVGUI, paint.endVGUI 269 | 270 | ---@param self paint.injectedPanel 271 | ---@param x number 272 | ---@param y number 273 | local panelPaint = function(self, x, y) 274 | do 275 | local beforePaint = self.BeforePaint 276 | if beforePaint then 277 | beforePaint(self, x, y) 278 | end 279 | end 280 | 281 | local iMesh = self.iMesh 282 | if not iMesh then return end 283 | setMaterial(paint.defaultMaterial) 284 | 285 | startVGUI() 286 | meshDraw(iMesh) 287 | endVGUI() 288 | end 289 | 290 | ---@param self paint.injectedPanel 291 | ---@param x number 292 | ---@param y number 293 | local panelRebuildMesh = function(self, x, y) 294 | resetZ() 295 | local iMesh = self.iMesh 296 | if iMesh then 297 | meshDestroy(iMesh) 298 | end 299 | 300 | local drawFunc = self.PaintMesh 301 | 302 | if drawFunc then 303 | startBatching() 304 | drawFunc(self, x, y) 305 | self.iMesh = stopBatching() 306 | end 307 | resetZ() 308 | end 309 | 310 | ---@param self paint.injectedPanel 311 | ---@param x number 312 | ---@param y number 313 | local panelOnSizeChanged = function(self, x, y) 314 | local rebuildMesh = self.RebuildMesh 315 | 316 | if rebuildMesh then 317 | rebuildMesh(self, x, y) 318 | end 319 | 320 | local oldOnSizeChanged = self.OldOnSizeChanged 321 | 322 | if oldOnSizeChanged then 323 | oldOnSizeChanged(self, x, y) 324 | end 325 | end 326 | 327 | ---@param self paint.injectedPanel 328 | local panelRemove = function(self) 329 | if IsValid(self.iMesh) then 330 | self.iMesh:Destroy() 331 | end 332 | end 333 | 334 | ---@class paint.injectedPanel : Panel # The injected panel is a supporting class that actually behaves as a wrapped pannel. Made for people who like 335 | ---type checking, and lsp things. Used internally only. 336 | ---@field Paint function 337 | ---@field OnSizeChanged function 338 | ---@field OnOldRemove function? 339 | ---@field OldOnSizeChanged function? 340 | ---@field RebuildMesh function 341 | ---@field BeforePaint function? 342 | ---@field PaintMesh function? 343 | ---@field OnRemove function 344 | ---@field iMesh IMesh? 345 | 346 | ---Wraps panel with some hacky functions that overrides paint function and OnChangeSize 347 | ---That is made for panel to use Panel:PaintMesh() when panel is updated (size updated/etc) 348 | ---@param panel Panel 349 | function batch.wrapPanel(panel) 350 | ---@cast panel paint.injectedPanel 351 | panel.Paint = panelPaint 352 | panel.OldOnSizeChanged = panel.OnSizeChanged 353 | panel.OnSizeChanged = panelOnSizeChanged 354 | panel.RebuildMesh = panelRebuildMesh 355 | panel.OnOldRemove = panel.OnRemove 356 | panel.OnRemove = panelRemove 357 | end 358 | end 359 | 360 | do 361 | ---Adds triangle to batching queue. If you want to manually add some figures to paint batching, then you can use this. 362 | ---@param z number Z position of next triangle. You want to use paint.incrementZ for that 363 | ---@param x1 number 364 | ---@param y1 number 365 | ---@param color1 Color color of first vertex 366 | ---@param x2 number 367 | ---@param y2 number 368 | ---@param color2 Color color of second vertex 369 | ---@param x3 number 370 | ---@param y3 number 371 | ---@param color3 Color color of third vertex 372 | function batch.addTriangle(z, x1, y1, color1, x2, y2, color2, x3, y3, color3) 373 | local len = batchTable[0] 374 | 375 | batchTable[len + 1] = x1 376 | batchTable[len + 2] = y1 377 | batchTable[len + 3] = z 378 | ---@diagnostic disable-next-line: assign-type-mismatch 379 | batchTable[len + 4] = color1 380 | 381 | batchTable[len + 5] = x2 382 | batchTable[len + 6] = y2 383 | ---@diagnostic disable-next-line: assign-type-mismatch 384 | batchTable[len + 7] = color2 385 | 386 | batchTable[len + 8] = x3 387 | batchTable[len + 9] = y3 388 | ---@diagnostic disable-next-line: assign-type-mismatch 389 | batchTable[len + 10] = color3 390 | 391 | batchTable[0] = len + 10 392 | end 393 | end 394 | 395 | ---@type table current batching table 396 | batch.batchTable = batchTable 397 | 398 | --- Batch library for paint lib 399 | paint.batch = batch 400 | -------------------------------------------------------------------------------- /lua/paint/rects_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | local paint = paint --[[@as paint]] 3 | 4 | --- What makes paint rectangles different from surface and draw rectangles? 5 | --- 1) Support for linear, per-corner gradients! 6 | --- 2) Vastly improved performance when drawing multiple rectangles, thanks to batching! 7 | --- 8 | --- Examples! 9 | --- 10 | --- Simple Example: 11 | --- 12 | ---Drawing an uncolored rectangle with a material, a rectangle with a material and per-corner colors, and a rectangle with just per-color corners. 13 | ---```lua 14 | --- local mat = Material( "icon16/application_xp.png" ) 15 | --- paint.rects.drawRect( 0, 0, 64, 64, color_white, mat, 0.5, 0, 1, 0.75 ) 16 | --- paint.rects.drawRect( 64, 0, 64, 64, { Color(255, 0, 0 ), Color( 0, 255, 0 ), Color( 0, 0, 255 ), color_white }, mat ) 17 | --- paint.rects.drawRect( 128, 0, 64, 64, { Color(255, 0, 0 ), Color( 0, 255, 0 ), Color( 0, 0, 255 ), color_white } ) 18 | ---``` 19 | ---Batched Example 20 | --- 21 | ---Drawing 25 rectangles with improved performance by using batching. 22 | ---```lua 23 | ---paint.rects.startBatching() 24 | --- for i = 1, 25 do 25 | --- paint.rects.drawRect( i * 15, 0, 15, 50, { COLOR_WHITE, COLOR_BLACK, COLOR_BLACK, COLOR_WHITE } ) 26 | --- end 27 | ---paint.rects.stopBatching() 28 | ---``` 29 | ---@class paint.rects 30 | local rects = {} 31 | 32 | do 33 | --[[ 34 | Purpose: makes a table, containing Rectangular mesh. 35 | Same params as drawSingleRect, except: 36 | w, h are replaced to endX, endY. 37 | They are end coordinates, not width, or height. 38 | It means they are calculated as startX + w and startY + h in drawSingleRect 39 | colors can accept only table of colors. 40 | And there's no material parameter 41 | ]] 42 | 43 | --[[ function rects.generateRectMesh(startX, startY, endX, endY, colors, u1, v1, u2, v2) 44 | 45 | local leftBottom = { pos = vector(startX, endY), color = colors[4], u = u1, v = v2 } 46 | local rightTop = { pos = vector(endX, startY), color = colors[2], u = u2, v = v1 } 47 | 48 | return { 49 | leftBottom, -- first triangle 50 | { pos = vector(startX, startY), color = colors[1], u = u1, v = v1 }, 51 | rightTop, 52 | 53 | leftBottom, -- second one 54 | rightTop, 55 | { pos = vector(endX, endY), color = colors[3], u = u2, v = v2 } 56 | } 57 | end--]] 58 | 59 | local meshBegin = mesh.Begin 60 | local meshEnd = mesh.End 61 | local meshPosition = mesh.Position 62 | local meshColor = mesh.Color 63 | local meshTexCoord = mesh.TexCoord 64 | local meshAdvanceVertex = mesh.AdvanceVertex 65 | 66 | local PRIMITIVE_QUADS = MATERIAL_QUADS 67 | 68 | --- Helper function to unpack color 69 | ---@param color Color 70 | ---@return integer r 71 | ---@return integer g 72 | ---@return integer b 73 | ---@return integer a 74 | local function unpackColor(color) return color.r, color.g, color.b, color.a end -- FindMetaTable still works shitty. 75 | 76 | --- generates quad onto IMesh 77 | ---@param mesh IMesh 78 | ---@param startX number 79 | ---@param startY number 80 | ---@param endX number 81 | ---@param endY number 82 | ---@param colors gradients # Color or colors used by gradient. Can be a single color, or a table of colors. 83 | ---@param u1 number 84 | ---@param v1 number 85 | ---@param u2 number 86 | ---@param v2 number 87 | ---@param skew number? sets skew for top side of rect. 88 | ---@param topSize number? overrides size for top side of rect 89 | ---@private Internal variable. Not meant to use outside 90 | function rects.generateRectMesh(mesh, startX, startY, endX, endY, colors, u1, v1, u2, v2, skew, topSize) 91 | local startTopX = startX + (skew or 0) 92 | local endTopX = topSize and topSize > 0 and startTopX + topSize or endX + (skew or 0) 93 | 94 | meshBegin(mesh, PRIMITIVE_QUADS, 1) 95 | meshPosition(startX, endY, 0) 96 | meshColor(unpackColor(colors[4])) 97 | meshTexCoord(0, u1, v2) 98 | meshAdvanceVertex() 99 | 100 | meshPosition(startTopX, startY, 0) 101 | meshColor(unpackColor(colors[1])) 102 | meshTexCoord(0, u1, v1) 103 | meshAdvanceVertex() 104 | 105 | meshPosition(endTopX, startY, 0) 106 | meshColor(unpackColor(colors[2])) 107 | meshTexCoord(0, u2, v1) 108 | meshAdvanceVertex() 109 | 110 | meshPosition(endX, endY, 0) 111 | meshColor(unpackColor(colors[3])) 112 | meshTexCoord(0, u2, v2) 113 | meshAdvanceVertex() 114 | meshEnd() 115 | end 116 | end 117 | 118 | do 119 | -- purpose: draws batched rectangle. 120 | local incrementZ = paint.incrementZ 121 | local batch = paint.batch 122 | 123 | --- Adds rect to triangle batch queue 124 | ---@private Internal variable. Not meant to use outside 125 | ---@param startX number 126 | ---@param startY number 127 | ---@param endX number 128 | ---@param endY number 129 | ---@param colors gradients # Color or colors used by gradient. Can be a single color, or a table of colors 130 | function rects.drawBatchedRect(startX, startY, endX, endY, colors, u1, v1, u2, v2, skew, topSize) 131 | local tab = batch.batchTable 132 | local len = tab[0] 133 | local z = incrementZ() 134 | 135 | local startTopX = startX + (skew or 0) 136 | local endTopX = topSize and topSize > 0 and startTopX + topSize or endX + (skew or 0) 137 | 138 | tab[len + 1] = startX 139 | tab[len + 2] = endY 140 | tab[len + 3] = z 141 | tab[len + 4] = colors[4] 142 | 143 | tab[len + 5] = startTopX 144 | tab[len + 6] = startY 145 | tab[len + 7] = colors[1] 146 | 147 | tab[len + 8] = endTopX 148 | tab[len + 9] = startY 149 | tab[len + 10] = colors[2] 150 | 151 | tab[len + 11] = u1 152 | tab[len + 12] = v2 153 | tab[len + 13] = u1 154 | tab[len + 14] = v1 155 | tab[len + 15] = u2 156 | tab[len + 16] = v1 157 | tab[len + 17] = batch.getDrawCell() 158 | 159 | 160 | tab[len + 18] = startX 161 | tab[len + 19] = endY 162 | tab[len + 20] = z 163 | tab[len + 21] = colors[4] 164 | 165 | tab[len + 22] = endX 166 | tab[len + 23] = startY 167 | tab[len + 24] = colors[2] 168 | 169 | tab[len + 25] = endX 170 | tab[len + 26] = endY 171 | tab[len + 27] = colors[3] 172 | 173 | tab[len + 28] = u1 174 | tab[len + 29] = v2 175 | tab[len + 30] = u2 176 | tab[len + 31] = v1 177 | tab[len + 32] = u2 178 | tab[len + 33] = v2 179 | tab[len + 34] = batch.getDrawCell() 180 | 181 | tab[0] = len + 34 182 | end 183 | end 184 | 185 | do 186 | ---@type {[string] : IMesh} 187 | local cachedRectMeshes = {} 188 | local defaultMat = paint.defaultMaterial or Material('vgui/white') 189 | 190 | --[[ 191 | Purpose: draws Rectangle on screen. 192 | Params: 193 | x - startX (absolute screen position) 194 | y - startY (too) 195 | w - width 196 | h - height 197 | colors - color table (or just color). 198 | if table of colors is supplied, then it will be gradient one 199 | Basically, color per corner. order is: left top, right top, right bottom, left bottom 200 | if single color supplied, then will be solid color. 201 | u1, v1, u2, v2 - UV's 202 | -- ]] 203 | 204 | local format = string.format 205 | 206 | local meshConstructor = Mesh 207 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 208 | 209 | local renderSetMaterial = render.SetMaterial 210 | 211 | local generateRectMesh = rects.generateRectMesh 212 | 213 | -- why is it a standalone function? 214 | -- This GETS JIT compiled as it does not contain any C API code and it does not have %s in them 215 | -- It means string.format gets compiled as native code, and speed of that will be 100 faster than default 216 | -- Mastermind tricks? 217 | 218 | --- Function used to get id of rect's IMesh. Used as tricky optimisation to make it JIT compiled 219 | ---@param x number 220 | ---@param y number 221 | ---@param w number 222 | ---@param h number 223 | ---@param color1 Color 224 | ---@param color2 Color 225 | ---@param color3 Color 226 | ---@param color4 Color 227 | ---@param u1 number 228 | ---@param v1 number 229 | ---@param u2 number 230 | ---@param v2 number 231 | ---@param skew number sets elevation for top side of rect. 232 | ---@param topSize number overrides size for top side of rect 233 | ---@return string 234 | local function getId(x, y, w, h, color1, color2, color3, color4, u1, v1, u2, v2, skew, topSize) 235 | return format('%f;%f;%f;%f;%x%x%x%x;%x%x%x%x;%x%x%x%x;%x%x%x%x;%f;%f;%f;%f;%f;%f', 236 | x, y, w, h, 237 | color1.r, color1.g, color1.b, color1.a, 238 | color2.r, color2.g, color2.b, color2.a, 239 | color3.r, color3.g, color3.b, color3.a, 240 | color4.r, color4.g, color4.b, color4.a, 241 | u1, v1, u2, v2, skew, topSize 242 | ) 243 | end 244 | 245 | --- Draws single rect (quad) 246 | ---@private Internal variable. Not meant to use outside 247 | ---@param x number 248 | ---@param y number 249 | ---@param w number 250 | ---@param h number 251 | ---@param colors gradients # Color or colors used by gradient. Can be a single color, or a table of colors 252 | ---@param material? IMaterial 253 | ---@param u1 number 254 | ---@param v1 number 255 | ---@param u2 number 256 | ---@param v2 number 257 | ---@param skew number sets elevation for top side of rect. 258 | ---@param topSize number overrides size for top side of rect 259 | ---@overload fun(x : number, y : number, w : number, h : number, colors: gradients, material?: Material) 260 | function rects.drawSingleRect(x, y, w, h, colors, material, u1, v1, u2, v2, skew, topSize) 261 | local id = getId(x, y, w, h, colors[1], colors[2], colors[3], colors[4], u1, v1, u2, v2, skew, topSize) 262 | 263 | local mesh = cachedRectMeshes[id] 264 | if mesh == nil then 265 | mesh = meshConstructor() 266 | 267 | generateRectMesh(mesh, x, y, x + w, y + h, colors, u1, v1, u2, v2, skew, topSize) 268 | 269 | cachedRectMeshes[id] = mesh 270 | end 271 | 272 | renderSetMaterial(material or defaultMat) 273 | meshDraw(mesh) 274 | end 275 | 276 | timer.Create('paint.rectMeshGarbageCollector' .. SysTime(), 60, 0, function() 277 | for k, v in pairs(cachedRectMeshes) do 278 | cachedRectMeshes[k] = nil 279 | v:Destroy() 280 | end 281 | end) 282 | end 283 | 284 | do 285 | -- batching doesn't support materials at all! 286 | local drawSingleRect = rects.drawSingleRect 287 | local drawBatchedRect = rects.drawBatchedRect 288 | 289 | local batch = paint.batch 290 | 291 | local getColorTable = paint.getColorTable 292 | 293 | --- Main function to draw rects 294 | ---@param x number # start X position of the rectangle 295 | ---@param y number # start Y position of the rectangle 296 | ---@param w number # width of the rectangle 297 | ---@param h number # height of the rectangle 298 | ---@param colors gradients # Either a table of Colors, or a single Color. 299 | --- If it is a table, it must have 4 elements, one for each corner. 300 | --- 301 | --- The order of the corners is: 302 | --- 1. Top-Left 303 | --- 2. Top-Right 304 | --- 3. Bottom-Right 305 | --- 4. Bottom-Left 306 | ---@param material? IMaterial # Either a Material, or nil. Default: vgui/white 307 | ---@param u1 number # The texture U coordinate of the Top-Left corner of the rectangle. Default : 0 308 | ---@param v1 number # The texture V coordinate of the Top-Left corner of the rectangle. Default : 0 309 | ---@param u2 number # The texture U coordinate of the Bottom-Right corner of the rectangle. Default : 1 310 | ---@param v2 number # The texture V coordinate of the Bottom-Right corner of the rectangle. Default : 1 311 | ---@param skew number? sets elevation for top side of rect. 312 | ---@param topSize number? overrides size for top side of rect 313 | ---@overload fun(x : number, y : number, w : number, h : number, colors: gradients, material? : IMaterial) # Overloaded variant without UV's. They are set to 0, 0, 1, 1 314 | function rects.drawRect(x, y, w, h, colors, material, u1, v1, u2, v2, skew, topSize) 315 | if colors[4] == nil then 316 | ---@cast colors Color 317 | ---@diagnostic disable-next-line: cast-local-type 318 | colors = getColorTable(4, colors) 319 | end 320 | ---@cast colors paint.gradientsTable 321 | 322 | if u1 == nil then 323 | u1, v1 = 0, 0 324 | u2, v2 = 1, 1 325 | end 326 | 327 | skew, topSize = skew or 0, topSize or 0 328 | 329 | if batch.batching then 330 | drawBatchedRect(x, y, x + w, y + h, colors, u1, v1, u2, v2, skew, topSize) 331 | else 332 | drawSingleRect(x, y, w, h, colors, material, u1, v1, u2, v2, skew, topSize) 333 | end 334 | end 335 | end 336 | 337 | paint.rects = rects 338 | -------------------------------------------------------------------------------- /lua/paint/main_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | ---Paint library for GMod! 3 | --- 4 | ---Purpose: drop in replacement to all surface/draw functions. Now there's no need to use them 5 | --- 6 | --- Features: 7 | --- 8 | --- 1) Enchanced lines, with support of linear gradients. 9 | --- 10 | --- 2) Enchanced rounded boxes. They support stencils, materials and outlines. 11 | --- 12 | --- 3) Circles. Super fast. 13 | --- 14 | --- 4) Batching. Everything here can be batched to save draw calls. Saves a lot of performance. 15 | --- 16 | --- 5) This library is SUPER fast. Some functions here are faster than default ones. 17 | --- 18 | --- 6) Rectangle support, with support of per-corner gradienting 19 | --- 20 | --- 7) Coordinates do not end up being rounded. Good for markers and other stuff. 21 | --- 22 | --- Coded by [@jaffies](https://github.com/jaffies), aka [@mikhail_svetov](https://github.com/jaffies) (formely @michael_svetov) in discord. 23 | --- Thanks to [A1steaksa](https://github.com/Jaffies/paint/pull/1), PhoenixF, [Riddle](https://github.com/Jaffies/paint/pull/2) and other people in gmod discord for various help 24 | --- 25 | --- Please, keep in mind that this library is still in development. 26 | --- You can help the project by contributing to it at [github repository](https://github.com/jaffies/paint) 27 | ---@class paint # paint library. Provides ability to draw shapes with mesh power 28 | ---@field lines paint.lines # lines module of paint library. Can make batched and gradient lines out of the box 29 | ---@field roundedBoxes paint.roundedBoxes # roundedBoxes provide better rounded boxes drawing because it makes them via meshes/polygons you name it. 30 | ---@field rects paint.rects # Rect module, gives rects with ability to batch and gradient per corner support 31 | ---@field outlines paint.outlines # outline module, gives you ability to create hollow outlines with 32 | ---@field batch paint.batch Unfinished module of batching. Provides a way to create IMeshes 33 | ---@field blur paint.blur blur library, provides a nice way to retrieve a cheap blur textures/materials 34 | ---@field circles paint.circles Circles! killer. 35 | ---@field svg paint.svg 36 | ---@field downsampling paint.downsampling 37 | local paint = paint or {} 38 | 39 | ---@alias gradients Color | paint.gradientsTable 40 | ---@alias linearGradient Color | paint.linearGradientsTable 41 | 42 | ---@class paint.gradientsTable 43 | ---@field [1] Color # top left 44 | ---@field [2] Color # top right 45 | ---@field [3] Color # bottom right 46 | ---@field [4] Color # bottom left 47 | ---@field [5] Color? # center (rounded boxes only) 48 | 49 | ---@class paint.linearGradientsTable 50 | ---@field [1] Color # inner 51 | ---@field [2] Color # outer 52 | 53 | 54 | do 55 | -- this fixes rendering issues with batching 56 | 57 | ---Internal variable made for batching to store Z pos meshes won't overlap each other 58 | ---@private Internal variable. Not meant to use outside 59 | paint.Z = 0 60 | 61 | ---resets paint.Z to 0 62 | function paint.resetZ() 63 | paint.Z = 0 64 | end 65 | 66 | --- Increments Z, meaning that next draw operation will be on top of others while batching (because of it's Z position heh) 67 | ---@return number Z # current Z position 68 | function paint.incrementZ() 69 | paint.Z = paint.Z + 1 70 | 71 | if paint.Z > 16384 then 72 | paint.resetZ() 73 | end 74 | 75 | return paint.getZ() 76 | end 77 | 78 | --- Calculates Z position, depending of paint.Z value. Made for batching 79 | ---@return number z # calculated Z position. Is not equal to paint.Z 80 | function paint.getZ() 81 | return -1 + paint.Z / 8192 82 | end 83 | end 84 | 85 | do -- Additional stuff to scissor rect. 86 | -- needed for panels, i.e. multiple DScrollPanels clipping. 87 | local tab = {} 88 | local len = 0 89 | 90 | local setScissorRect = render.SetScissorRect 91 | local max = math.max 92 | local min = math.min 93 | 94 | --- Pushes new scissor rect boundaries to stack. Simmilar to Push ModelMatrix/RenderTarget/Filter(Mag/Min) 95 | ---@see render.PushRenderTarget # A simmilar approach to render targets. 96 | ---@param x number # start x position 97 | ---@param y number # start y position 98 | ---@param endX number # end x position. Must be bigger than x 99 | ---@param endY number # end y position. Must be bigger than y 100 | function paint.pushScissorRect(x, y, endX, endY) 101 | local prev = tab[len] 102 | 103 | if prev then 104 | x = max(prev[1], x) 105 | y = max(prev[2], y) 106 | endX = min(prev[3], endX) 107 | endY = min(prev[4], endY) 108 | end 109 | 110 | len = len + 1 111 | 112 | tab[len] = { x, y, endX, endY } 113 | setScissorRect(x, y, endX, endY, true) 114 | end 115 | 116 | --- Pops last scissor rect's boundaries from the stack. Simmilar to Pop ModelMatrix/RenderTarget/Filter(Mag/Min) 117 | ---@see paint.pushScissorRect 118 | function paint.popScissorRect() 119 | tab[len] = nil 120 | len = max(0, len - 1) 121 | 122 | local newTab = tab[len] 123 | 124 | if newTab then 125 | setScissorRect(newTab[1], newTab[2], newTab[3], newTab[4], true) 126 | else 127 | setScissorRect(0, 0, 0, 0, false) 128 | end 129 | end 130 | end 131 | 132 | do 133 | local vector = Vector() 134 | local paintColoredMaterial = CreateMaterial("testMaterial" .. SysTime(), "UnlitGeneric", { 135 | ["$basetexture"] = "color/white", 136 | ["$model"] = 1, 137 | ["$translucent"] = 1, 138 | ["$vertexalpha"] = 1, 139 | ["$vertexcolor"] = 1 140 | }) 141 | 142 | local recompute = paintColoredMaterial.Recompute 143 | local setVector = paintColoredMaterial.SetVector 144 | local setUnpacked = vector.SetUnpacked 145 | 146 | local currentR, currentG, currentB, currentA = 255, 255, 255, 255 147 | 148 | ---This function provides you a material with solid color, allowing you to replicate ``render.SetColorModulation``/``surface.SetDrawColor`` 149 | --- 150 | ---Meant to be used to have paint's shapes have animated colors without rebuilding mesh every time color changes 151 | --- 152 | ---It will tint every color of shape, but not override it. Meaning that yellow color wont be overriden to blue. 153 | --- 154 | ---instead it will be black because red/green components will be multiplied to 0, and blue component (which is 0, because its yellow) will be mutliplied by 1. Which equeals 0 155 | --- 156 | ---**Note:** You will have to call this function every time the color of coloredMaterial changes, because it uses 1 material and sets its color to what you want 157 | ---Example: 158 | ---```lua 159 | ---paint.outlines.drawOutline(32, 100, 100, 256, 256, {color_white, color_transparent}, paint.getColoredMaterial( HSVToColor(RealTime() * 100, 1, 1) ), 16 ) 160 | -----[[It will make halo/shadow with animated color]] 161 | ---``` 162 | ---@param color Color color that material will have 163 | ---@return IMaterial coloredMaterial 164 | function paint.getColoredMaterial(color) 165 | local r, g, b, a = color.r, color.g, color.b, color.a 166 | 167 | if currentR ~= r or currentG ~= g or currentB ~= b or currentA ~= a then 168 | currentR, currentG, currentB, currentA = r, g, b, a 169 | setUnpacked(vector, r / 255, g / 255, b / 255) 170 | setVector(paintColoredMaterial, '$color', vector) 171 | end 172 | 173 | return paintColoredMaterial 174 | end 175 | 176 | ---@param color Color 177 | ---@param mat IMaterial 178 | function paint.colorMaterial(color, mat) 179 | local r, g, b, a = color.r, color.g, color.b, color.a 180 | 181 | setUnpacked(vector, r / 255, g / 255, b / 255) 182 | setVector(mat, '$color', vector) 183 | end 184 | end 185 | 186 | do 187 | -- Helper functions 188 | -- startPanel - pops model matrix and pushes 189 | 190 | local matrix = Matrix() 191 | local setField = matrix.SetField 192 | 193 | local pushModelMatrix = cam.PushModelMatrix 194 | local popModelMatrix = cam.PopModelMatrix 195 | 196 | local panelTab = FindMetaTable('Panel') 197 | ---@cast panelTab Panel 198 | 199 | local localToScreen = panelTab.LocalToScreen 200 | local getSize = panelTab.GetSize 201 | 202 | local pushScissorRect = paint.pushScissorRect 203 | local popScissorRect = paint.popScissorRect 204 | 205 | local setScissorRect = render.SetScissorRect 206 | 207 | --- 208 | ---Unfortunately, the paint library cannot integrate seamlessly with VGUI and Derma in the way that the surface and draw libraries do. 209 | ---This is because Meshes, which are used by the paint library, can only use absolute screen coordinates whereas the surface and draw libraries are automatically provided with panel-relative coordinates by the VGUI system. 210 | --- 211 | ---In addition, meshes cannot be clipped with the default VGUI clipping system and will behave as though it is disabled. 212 | --- 213 | ---To work around these limitations, you can use this function. 214 | ---@param panel Panel # The panel to draw on. 215 | ---@param pos? boolean # Set to true to autoamtically adjust all future paint operations to be relative to the panel. Default: true 216 | ---@param boundaries? boolean # Set to true to enable ScissorRect to the size of the panel. Default: false 217 | function paint.startPanel(panel, pos, boundaries, multiply) 218 | local x, y = localToScreen(panel, 0, 0) 219 | 220 | if pos or pos == nil then 221 | setField(matrix, 1, 4, x) 222 | setField(matrix, 2, 4, y) 223 | 224 | pushModelMatrix(matrix, multiply) 225 | end 226 | 227 | if boundaries then 228 | local w, h = getSize(panel) 229 | 230 | pushScissorRect(x, y, x + w, y + h) 231 | end 232 | end 233 | 234 | ---@see paint.startPanel # Note: You need to have same arguments for position and boundaries between start and end panel functions. 235 | ---@param pos? boolean # Set to true to autoamtically adjust all future paint operations to be relative to the panel. Default: true 236 | ---@param boundaries? boolean # Set to true to enable ScissorRect to the size of the panel. Default: false 237 | function paint.endPanel(pos, boundaries) 238 | if pos or pos == nil then 239 | popModelMatrix() 240 | end 241 | 242 | if boundaries then 243 | popScissorRect() 244 | end 245 | end 246 | 247 | do -- since startPanel and endPanel sound stupid and i figured it out only now, i'll make an aliases for them 248 | paint.beginPanel = paint.startPanel 249 | paint.stopPanel = paint.endPanel 250 | 251 | -- paint.beginPanel -> paint.endPanel (like in Pascal language, or mesh.Begin -> mesh.End) 252 | -- paint.startPanel -> paint.stopPanel (start/stop sound cool in pairs) 253 | end 254 | 255 | local getPanelPaintState = surface.GetPanelPaintState 256 | 257 | ---# Starts new VGUI context 258 | ---A modern alternative to paint.startPanel without the need to pass a reference of panel 259 | ---and without the need to manually clip ``DScrollPanel``s. 260 | ---## Example: 261 | ---```lua 262 | ---function PANEL:Paint(w, h) 263 | --- paint.startVGUI() 264 | --- paint.roundedBoxes.roundedBox(32, 0, 0, w, h, color_white) 265 | --- --Any other stuff here. Note that surface.* (or surface.* derived) functions will double it's positioning. 266 | --- --This is all because of surface internal positioning + matrix translation. 267 | --- --You will need to use only paint.* functions inside paint.start/endVGUI() block. 268 | --- paint.endVGUI() 269 | ---end 270 | ---``` 271 | ---@see paint.startPanel 272 | function paint.startVGUI() 273 | local state = getPanelPaintState() 274 | 275 | setField(matrix, 1, 4, state.translate_x) 276 | setField(matrix, 2, 4, state.translate_y) 277 | pushModelMatrix(matrix, true) 278 | 279 | if state.scissor_enabled then 280 | setScissorRect(state.scissor_left, state.scissor_top, state.scissor_right, state.scissor_bottom, true) 281 | end 282 | end 283 | 284 | ---# Ends new VGUI context 285 | ---A modern alternative to paint.startPanel without the need to pass a reference of panel 286 | ---and without the need to manually clip ``DScrollPanel``s. 287 | ---@see paint.startPanel 288 | function paint.endVGUI() 289 | popModelMatrix() 290 | setScissorRect(0, 0, 0, 0, false) 291 | end 292 | 293 | paint.beginVGUI = paint.startVGUI 294 | paint.stopVGUI = paint.endVGUI 295 | 296 | --- Simple helper function which makes bilinear interpolation 297 | ---@private Internal variable. Not meant to use outside 298 | ---@param x number # x is fraction between 0 and 1. 0 - left side, 1 - right side 299 | ---@param y number # y is fraction between 0 and 1. 0 - top side, 1 - bottom side 300 | ---@param leftTop integer 301 | ---@param rightTop integer 302 | ---@param rightBottom integer 303 | ---@param leftBottom integer 304 | ---@return number result # result of bilinear interpolation 305 | function paint.bilinearInterpolation(x, y, leftTop, rightTop, rightBottom, leftBottom) 306 | if leftTop == rightTop and leftTop == rightBottom and leftTop == leftBottom then return leftTop end -- Fix (sometimes 255 alpha could get 254, probably double prescision isn't enought or smth like that) 307 | local top = leftTop == rightTop and leftTop or ((1 - x) * leftTop + x * rightTop) 308 | local bottom = leftBottom == rightBottom and leftBottom or 309 | ((1 - x) * leftBottom + x * rightBottom) -- more precise checking 310 | return (1 - y) * top + y * bottom 311 | end 312 | end 313 | 314 | do 315 | local tab = {} 316 | 317 | -- When designing paint library i forgot that some third party libraries could use colors in pretty the same hacky way as i was 318 | -- This func will move all color refs to outer table instead of mofifying color itself 319 | ---@param len 2 320 | ---@param color Color 321 | ---@return paint.linearGradientsTable 322 | ---@overload fun(len : 4, color : Color) : paint.gradientsTable 323 | ---@overload fun(len : 5, color : Color) : paint.gradientsTable 324 | function paint.getColorTable(len, color) 325 | for i = 1, len do 326 | tab[i] = color 327 | end 328 | 329 | return tab 330 | end 331 | end 332 | 333 | do 334 | if not file.Exists('shaders/fxc/paintlib_shader_ps30.vcs', 'MOD') then -- Shadered white material with dithering, inline gma injection is used. 335 | ---Used to increase perf + disable gamma correction + dithering (to remove gradient banding) 336 | local gma = [[R01BRAMAAAAAAAAAAGuaJGgAAAAAAHBhaW50bGliIHNoYWRlcgB7CgkiZGVzY3JpcHRpb24iOiAi 337 | RGVzY3JpcHRpb24iLAoJInR5cGUiOiAic2VydmVyY29udGVudCIsCgkidGFncyI6IFsKCQkiYnVp 338 | bGQiLAoJCSJmdW4iCgldCn0AQXV0aG9yIE5hbWUAAQAAAAEAAABtYXRlcmlhbHMvcGFpbnRsaWIu 339 | dm10ABgCAAAAAAAAmVZRJgIAAABzaGFkZXJzL2Z4Yy9wYWludGxpYl9zaGFkZXJfcHMzMC52Y3MA 340 | cQEAAAAAAAB+v8m+AwAAAHNoYWRlcnMvZnhjL3BhaW50bGliX3NoYWRlcl92czMwLnZjcwClAQAA 341 | AAAAAGsVTFQAAAAAc2NyZWVuc3BhY2VfZ2VuZXJhbAp7CgkkcGl4c2hhZGVyICJwYWludGxpYl9z 342 | aGFkZXJfcHMzMCIKICAgICR2ZXJ0ZXhzaGFkZXIgInBhaW50bGliX3NoYWRlcl92czMwIgogICAg 343 | JGlnbm9yZXogICAgICAgICAgICAxCgoJJGJhc2V0ZXh0dXJlICIiCgkkdGV4dHVyZTEgICAgIiIK 344 | CSR0ZXh0dXJlMiAgICAiIgoJJHRleHR1cmUzICAgICIiCgogICAgJHZlcnRleGNvbG9yIDEKICAg 345 | ICR2ZXJ0ZXh0cmFuc2Zvcm0gMQoKICAgICRjMF94IDEuMAogICAgJGMwX3kgMS4wCiAgICAkYzBf 346 | eiAxLjAKICAgICRjMF93IDEuMAoKCSRjb3B5YWxwaGEgICAgICAgICAgICAgICAgIDAKCSRhbHBo 347 | YV9ibGVuZF9jb2xvcl9vdmVybGF5IDAKCSRhbHBoYV9ibGVuZCAgICAgICAgICAgICAgIDEKCSRs 348 | aW5lYXJ3cml0ZSAgICAgICAgICAgICAgIDEKCSRsaW5lYXJyZWFkX2Jhc2V0ZXh0dXJlICAgIDAK 349 | CSRsaW5lYXJyZWFkX3RleHR1cmUxICAgICAgIDAKCSRsaW5lYXJyZWFkX3RleHR1cmUyICAgICAg 350 | IDAKCSRsaW5lYXJyZWFkX3RleHR1cmUzICAgICAgIDAKfQoGAAAAAQAAAAEAAAAAAAAAAAAAAAIA 351 | AABL7ioNAAAAADAAAAD/////cQEAAAAAAAA5AQBATFpNQXgCAAAoAQAAXQAAAAEAAGiaXeCGv+yp 352 | J8XEIyBnOYzWiWMQpnqHj+igQa0vpL1baF70QH9iOW9HonOew/fDtQ0kPKqJWJbqVX90l4oT5560 353 | 0ucYC+vkzTC+MAXy1dyXh/fDaWhxohmNcgEj6UoQYiP5PlkNWxnnBwvDjJkoe4IlhB14JNr6I2DJ 354 | fJnrlUAXLK1UVCUTv9yXgEycLLBVpo2f8RCB1k9fwkm3xOaV/r5gZIl8pt/ZRYG94yfIc8zYXJTb 355 | NYzKkhaX4aHOeA5Wo2bsB6n70OgUIcGXnLC2p8E/9aC+Eq42+5igB8u9fSu/S3hQ/+cgSOJbV/Sy 356 | WbCBuMvAObVGerLYONnieZ9oUMzgpBVf9Drs+pm89xpKBKb3qyRL9WZVxCXcgSOvGvkeAty1k03x 357 | AP////8GAAAAAQAAAAEAAAAAAAAAAAAAAAIAAAB3Q0KZAAAAADAAAAD/////pQEAAAAAAABtAQBA 358 | TFpNQWQEAABcAQAAXQAAAAEAAGiVXjSFv+xjGapeOKoS4OTw07jJnFNIBxQKhv/vJQgnEuk6S8i9 359 | iIOZ/cNnl7/kpbIBgyPggGrn4SUaCO4wMM2uaBqNFB4WPClr3W2FicbhjyonQ67Q6oYaEEu4Tu17 360 | xE7pKjTBMD1skFqcZFlmquDhVT5cVKqouodVdly2UrW6k5isS7ZWYZJdaQfqVAWOF8Vq5R385tAH 361 | sLGH5M+N+ECZpJv5SpYxCk1MLxSB0nv3/3Mz+jlCz3a8CEU/IWSl6lt4AVLtEofUP6eCTvQF2s2F 362 | XRrtEpQHI9BAuEQBqct+RuetelPCxZdaqLxgeV6qhbshamEvUWNu1lDz4CfLHGsjA5VhQgXVBZdG 363 | tTWxTpUQEvdF3wqhlSE5ESWuHP/KIIxPwCKCLSuH8E952Pdo4C+DRuaDvGtr1zHfgc1bFSBgKXLv 364 | DUKxOcxozUD1ymvSW1FbAH+qNahLhAD/////PyQ5WA==]] 365 | local gmaName = 'eventsui.gma' 366 | file.Write(gmaName, util.Base64Decode(gma)) 367 | game.MountGMA('data/' .. gmaName) 368 | end 369 | 370 | paint.defaultMaterial = Material('paintlib') 371 | 372 | local testRT = GetRenderTarget('paintlib_test_rt', 8, 8) 373 | 374 | render.PushRenderTarget(testRT) 375 | 376 | render.Clear(0, 0, 0, 0, true, true) 377 | 378 | cam.Start2D() 379 | surface.SetDrawColor(255, 255, 255) 380 | surface.SetMaterial(paint.defaultMaterial) 381 | surface.DrawTexturedRect(0, 0, 8, 8) 382 | cam.End2D() 383 | 384 | render.CapturePixels() 385 | local r, g, b = render.ReadPixel(1, 1) 386 | 387 | render.PopRenderTarget() 388 | 389 | 390 | if r == 0 or g == 0 or b == 0 then 391 | ---Fallback for cases if custom shader does not work properly 392 | paint.defaultMaterial = CreateMaterial('paintlib_no_shader', 'UnlitGeneric', { 393 | ['$basetexture'] = 'color/white', 394 | ['$model'] = 1, 395 | ['$translucent'] = 1, 396 | ['$vertexalpha'] = 1, 397 | ['$vertexcolor'] = 1, 398 | ['$gammacolorread'] = 1, 399 | ['$linearwrite'] = 1 400 | }) 401 | 402 | if not IsValid(paint.defaultMaterial) then 403 | paint.defaultMaterial = Material('vgui/white') 404 | end 405 | end 406 | end 407 | 408 | ---@diagnostic disable-next-line: undefined-global 409 | if not MINIFIED then 410 | _G.paint --[[@as paint]] = paint 411 | end 412 | -------------------------------------------------------------------------------- /lua/paint/outlines_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | local paint = paint --[[@as paint]] 3 | 4 | --``` 5 | --What makes paint outlines better than stencils: 6 | --1) Support for materials! 7 | --2) Support for gradients within the outline! 8 | --3) Curviness! 9 | --``` 10 | --# Simple example: 11 | --- 12 | --Drawing outlines with different thicknesses on each side. 13 | --```lua 14 | --paint.outlines.drawOutline( 32, 16, 10, 64, 64, { COLOR_WHITE, COLOR_BLACK }, nil, 8 ) 15 | --paint.outlines.drawOutline( 32, 102, 10, 64, 64, { COLOR_WHITE, color_transparent }, nil, 8 ) 16 | --paint.outlines.drawOutline( 32, 192, 10, 64, 64, { COLOR_BLACK, ColorAlpha( COLOR_BLACK, 0 ) }, nil, 8 ) 17 | ---``` 18 | ---# Asymmetrical Example 19 | --- 20 | ---Drawing outlines with a different inner and outer color. 21 | ---```lua 22 | -- paint.outlines.drawOutline( 32, 16, 10, 64, 64, { COLOR_WHITE, COLOR_BLACK }, nil, 8 ) 23 | -- paint.outlines.drawOutline( 32, 102, 10, 64, 64, { COLOR_WHITE, color_transparent }, nil, 8 ) 24 | -- paint.outlines.drawOutline( 32, 192, 10, 64, 64, { COLOR_BLACK, ColorAlpha( COLOR_BLACK, 0 ) }, nil, 8 ) 25 | ---``` 26 | ---# Draw Outline Animated Gradient Example 27 | --- 28 | ---Drawing an animated, colorful outline with a gradient. 29 | ---```lua 30 | -- local color1, color2 = HSVToColor( RealTime() * 120, 1, 1 ), HSVToColor( RealTime() * 120 + 30, 1, 1 ) 31 | -- paint.outlines.drawOutline( 32, 32, 18, 64, 64, { color1, color2 }, nil, 16 ) 32 | ---``` 33 | ---@class paint.outlines 34 | local outlines = {} 35 | 36 | do 37 | local meshPosition = mesh.Position 38 | local meshColor = mesh.Color 39 | local meshTexCoord = mesh.TexCoord 40 | local meshAdvanceVertex = mesh.AdvanceVertex 41 | 42 | ---@type boolean 43 | local isFirst = true 44 | ---@type number? 45 | local prevU 46 | ---@type boolean? 47 | local isInside 48 | 49 | ---@type number? 50 | local cornerness = 1 51 | 52 | ---@type number 53 | local outlineLeft = 0 54 | ---@type number 55 | local outlineRight = 0 56 | ---@type number 57 | local outlineTop = 0 58 | ---@type number 59 | local outlineBottom = 0 60 | 61 | local atan2 = math.atan2 62 | 63 | ---@type paint.createVertexFunc 64 | local function createVertex(x, y, u, v, colors) 65 | if isFirst then 66 | isFirst = false 67 | return 68 | end 69 | 70 | local texU = 1 - (atan2((1 - v) - 0.5, u - 0.5) / (2 * math.pi) + 0.5) 71 | 72 | if prevU and prevU > texU then 73 | texU = texU + 1 74 | else 75 | prevU = texU 76 | end 77 | 78 | local newX, newY 79 | 80 | if u < 0.5 then 81 | newX = x - outlineLeft * (((1 - u) - 0.5) * 2) ^ cornerness 82 | elseif u ~= 0.5 then 83 | newX = x + outlineRight * ((u - 0.5) * 2) ^ cornerness 84 | else 85 | newX = x 86 | end 87 | 88 | if v < 0.5 then 89 | newY = y - outlineTop * (((1 - v) - 0.5) * 2) ^ cornerness 90 | elseif v ~= 0.5 then 91 | newY = y + outlineBottom * ((v - 0.5) * 2) ^ cornerness 92 | else 93 | newY = y 94 | end 95 | 96 | if isInside then 97 | meshPosition(newX, newY, 0) 98 | meshColor(colors[2].r, colors[2].g, colors[2].b, colors[2].a) 99 | meshTexCoord(0, texU, 0.02) 100 | meshAdvanceVertex() 101 | 102 | meshPosition(x, y, 0) 103 | meshColor(colors[1].r, colors[1].g, colors[1].b, colors[1].a) 104 | meshTexCoord(0, texU, 1) 105 | meshAdvanceVertex() 106 | else 107 | meshPosition(x, y, 0) 108 | meshColor(colors[1].r, colors[1].g, colors[1].b, colors[1].a) 109 | meshTexCoord(0, texU, 1) 110 | meshAdvanceVertex() 111 | 112 | meshPosition(newX, newY, 0) 113 | meshColor(colors[2].r, colors[2].g, colors[2].b, colors[2].a) 114 | meshTexCoord(0, texU, 0.02) 115 | meshAdvanceVertex() 116 | end 117 | end 118 | 119 | ---@diagnostic disable-next-line: invisible 120 | local generateSingleMesh = paint.roundedBoxes.generateSingleMesh 121 | 122 | local meshBegin = mesh.Begin 123 | local meshEnd = mesh.End 124 | 125 | 126 | local PRIMITIVE_TRIANGLE_STRIP = MATERIAL_TRIANGLE_STRIP 127 | 128 | local getMeshVertexCount = paint.roundedBoxes.getMeshVertexCount 129 | --- draw single outline 130 | 131 | --- Generates outline mesh 132 | ---@param mesh IMesh 133 | ---@param radius number 134 | ---@param x number 135 | ---@param y number 136 | ---@param w number 137 | ---@param h number 138 | ---@param leftTop? boolean 139 | ---@param rightTop? boolean 140 | ---@param rightBottom? boolean 141 | ---@param leftBottom? boolean 142 | ---@param colors {[1]: Color, [2]: Color} 143 | ---@param l number 144 | ---@param t number 145 | ---@param r number 146 | ---@param b number 147 | ---@param curviness number? 148 | ---@param inside boolean? 149 | ---@param cornernessArg number? 150 | ---@private Internal variable, not meant to be used outside. 151 | function outlines.generateOutlineSingle(mesh, radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, 152 | l, t, r, b, curviness, inside, cornernessArg) 153 | isInside = inside or false 154 | outlineTop, outlineRight, outlineBottom, outlineLeft = t or 0, r or 0, b or 0, l or 0 155 | curviness = curviness or 2 156 | cornerness = cornernessArg or 1 157 | 158 | isFirst = true 159 | prevU = nil 160 | 161 | meshBegin(mesh, PRIMITIVE_TRIANGLE_STRIP, 162 | getMeshVertexCount(radius, rightTop, rightBottom, leftBottom, leftTop) * 2) 163 | generateSingleMesh(createVertex, nil, radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, 0, 164 | 0, 1, 1, curviness) 165 | meshEnd() 166 | end 167 | end 168 | 169 | do 170 | ---@type {[string]: IMesh} 171 | local cachedOutlinedMeshes = {} 172 | 173 | local format = string.format 174 | --- Helper function to get id 175 | ---@param radius number 176 | ---@param w number 177 | ---@param h number 178 | ---@param corners number 179 | ---@param color1 Color 180 | ---@param color2 Color 181 | ---@param l number 182 | ---@param t number 183 | ---@param r number 184 | ---@param b number 185 | ---@param curviness number? 186 | ---@param inside boolean? 187 | ---@return string id 188 | local function getId(radius, w, h, corners, color1, color2, l, t, r, b, curviness, inside, cornerness) 189 | return format('%f;%f;%f;%u;%x%x%x%x;%x%x%x%x;%f;%f;%f;%f;%f;%u;%f', 190 | radius, w, h, corners, 191 | color1.r, color1.g, color1.b, color1.a, 192 | color2.r, color2.g, color2.b, color2.a, 193 | l, t, r, b, curviness or 2, inside and 1 or 0, cornerness 194 | ) 195 | end 196 | 197 | local pushModelMatrix = cam.PushModelMatrix 198 | local popModelMatrix = cam.PopModelMatrix 199 | 200 | local meshConstructor = Mesh 201 | local generateOutlineSingle = outlines.generateOutlineSingle 202 | 203 | local matrix = Matrix() 204 | local setField = matrix.SetField 205 | 206 | local setMaterial = render.SetMaterial 207 | 208 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 209 | 210 | ---Draws outline. Unbatched 211 | ---@param radius number 212 | ---@param x number 213 | ---@param y number 214 | ---@param w number 215 | ---@param h number 216 | ---@param leftTop? boolean 217 | ---@param rightTop? boolean 218 | ---@param rightBottom? boolean 219 | ---@param leftBottom? boolean 220 | ---@param colors {[1]: Color, [2]: Color} 221 | ---@param material? IMaterial # Default material is vgui/white 222 | ---@param l number 223 | ---@param t number 224 | ---@param r number 225 | ---@param b number 226 | ---@param curviness number? 227 | ---@param inside boolean 228 | ---@param cornerness number? Number in which corner fraction (value between 0 and 1) will be powered to. Default is 1 229 | ---@private Internal variable, not meant to be used outside. 230 | function outlines.drawOutlineSingle(radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, material, 231 | l, t, r, b, curviness, inside, cornerness) 232 | curviness = curviness or 2 233 | inside = inside or false 234 | cornerness = cornerness or 1 235 | 236 | 237 | local id = getId(radius, w, h, 238 | (leftTop and 8 or 0) + (rightTop and 4 or 0) + (rightBottom and 2 or 0) + (leftBottom and 1 or 0), colors[1], 239 | colors[2], l, t, r, b, curviness, inside, cornerness) 240 | 241 | local meshObj = cachedOutlinedMeshes[id] 242 | 243 | if meshObj == nil then 244 | meshObj = meshConstructor() 245 | generateOutlineSingle(meshObj, radius, 0, 0, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, l, t, 246 | r, b, curviness, inside, cornerness) 247 | 248 | cachedOutlinedMeshes[id] = meshObj 249 | end 250 | 251 | setField(matrix, 1, 4, x) 252 | setField(matrix, 2, 4, y) 253 | 254 | pushModelMatrix(matrix, true) 255 | setMaterial(material or paint.defaultMaterial) 256 | meshDraw(meshObj) 257 | popModelMatrix() 258 | end 259 | 260 | timer.Create('paint.outlinesGarbageCollector' .. SysTime(), 15, 0, function() 261 | for k, v in pairs(cachedOutlinedMeshes) do 262 | v:Destroy() 263 | cachedOutlinedMeshes[k] = nil 264 | end 265 | end) 266 | end 267 | 268 | do 269 | ---@diagnostic disable-next-line: invisible 270 | local generateSingleMesh = paint.roundedBoxes.generateSingleMesh 271 | 272 | ---@type number?, number?, number?, number? 273 | local outlineL, outlineT, outlineR, outlineB -- use it to get outline widths per side 274 | ---@type boolean? 275 | local first -- to skip first vertex since it is center of rounded box 276 | ---@type number?, number?, number?, number? 277 | local prevX, prevY, prevU, prevV 278 | ---@type number? 279 | local z 280 | 281 | local atan2 = math.atan2 282 | local batch = paint.batch 283 | 284 | ---@param x number 285 | ---@param y number 286 | ---@param u number 287 | ---@param v number 288 | ---@param colors {[1] : Color, [2]: Color} 289 | local function createVertex(x, y, u, v, colors) 290 | if first then 291 | first = false 292 | return 293 | elseif first == false then 294 | prevX, prevY, prevU, prevV = x, y, u, v 295 | first = nil 296 | return 297 | end 298 | 299 | local batchTable = batch.batchTable 300 | local len = batchTable[0] 301 | 302 | local color1, color2 = colors[1], colors[2] 303 | local cell = batch.getDrawCell() 304 | 305 | batchTable[len + 1] = prevX 306 | batchTable[len + 2] = prevY 307 | batchTable[len + 3] = z 308 | batchTable[len + 4] = color1 309 | 310 | do -- make some calculations to get outer border 311 | if prevU < 0.5 then 312 | prevX = prevX - outlineL * ((1 - prevU) - 0.5) * 2 313 | elseif prevU ~= 0.5 then 314 | prevX = prevX + outlineR * (prevU - 0.5) * 2 315 | end 316 | 317 | if prevV < 0.5 then 318 | prevY = prevY - outlineT * ((1 - prevV) - 0.5) * 2 319 | elseif prevV ~= 0.5 then 320 | prevY = prevY + outlineB * (prevV - 0.5) * 2 321 | end 322 | end 323 | 324 | batchTable[len + 5] = prevX 325 | batchTable[len + 6] = prevY 326 | batchTable[len + 7] = color2 327 | 328 | batchTable[len + 8] = x 329 | batchTable[len + 9] = y 330 | batchTable[len + 10] = color1 331 | 332 | local texPrevU = 1 - (atan2((1 - prevV) - 0.5, prevU - 0.5) / (2 * math.pi) + 0.5) 333 | local texU = 1 - (atan2((1 - v) - 0.5, u - 0.5) / (2 * math.pi) + 0.5) 334 | 335 | if texPrevU and texPrevU > texU then 336 | texU = texU + 1 337 | else 338 | texPrevU = texU 339 | end 340 | 341 | batchTable[len + 11] = texPrevU 342 | batchTable[len + 12] = 0.02 343 | batchTable[len + 13] = texPrevU 344 | batchTable[len + 14] = 1 345 | batchTable[len + 15] = texU 346 | batchTable[len + 16] = 0.02 347 | batchTable[len + 17] = cell 348 | 349 | 350 | batchTable[len + 18] = x 351 | batchTable[len + 19] = y 352 | batchTable[len + 20] = z 353 | batchTable[len + 21] = color1 354 | 355 | batchTable[len + 22] = prevX 356 | batchTable[len + 23] = prevY 357 | batchTable[len + 24] = color2 358 | 359 | prevX, prevY, prevU, prevV = x, y, u, v 360 | do 361 | if u < 0.5 then 362 | x = x - outlineL * (((1 - u) - 0.5) * 2) 363 | elseif u ~= 0.5 then 364 | x = x + outlineR * (u - 0.5) * 2 365 | end 366 | 367 | if v < 0.5 then 368 | y = y - outlineT * ((1 - v) - 0.5) * 2 369 | elseif v ~= 0.5 then 370 | y = y + outlineB * (v - 0.5) * 2 371 | end 372 | end 373 | 374 | batchTable[len + 25] = x 375 | batchTable[len + 26] = y 376 | batchTable[len + 27] = color2 377 | 378 | batchTable[len + 28] = texU 379 | batchTable[len + 29] = 0.02 380 | batchTable[len + 30] = texPrevU 381 | batchTable[len + 31] = 1 382 | batchTable[len + 32] = texU 383 | batchTable[len + 33] = 1 384 | batchTable[len + 34] = cell 385 | 386 | batchTable[0] = len + 34 387 | end 388 | 389 | local incrementZ = paint.incrementZ 390 | 391 | ---Draws outline. Batched 392 | ---@param radius number 393 | ---@param x number 394 | ---@param y number 395 | ---@param w number 396 | ---@param h number 397 | ---@param leftTop? boolean 398 | ---@param rightTop? boolean 399 | ---@param rightBottom? boolean 400 | ---@param leftBottom? boolean 401 | ---@param colors {[1]: Color, [2]: Color} 402 | ---@param l number 403 | ---@param t number 404 | ---@param r number 405 | ---@param b number 406 | ---@param curviness number? 407 | ---@private Internal variable, not meant to be used outside. 408 | function outlines.drawOutlineBatched(radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, _, l, t, 409 | r, b, curviness) 410 | outlineL, outlineT, outlineR, outlineB = l, t, r, b 411 | first = true 412 | curviness = curviness or 2 413 | 414 | z = incrementZ() 415 | generateSingleMesh(createVertex, nil, radius, x, y, x + w, y + h, leftTop, rightTop, rightBottom, leftBottom, 416 | colors, 0, 0, 1, 1, curviness) 417 | end 418 | end 419 | 420 | do 421 | local batch = paint.batch 422 | local drawOutlineSingle = outlines.drawOutlineSingle 423 | local drawOutlineBatched = outlines.drawOutlineBatched 424 | 425 | local getColorTable = paint.getColorTable 426 | 427 | ---Identical to drawOutline other than that it allows you to specify specific corners to be rounded. 428 | ---@param radius number 429 | ---@param x number start X position of outline 430 | ---@param y number start Y position of outline 431 | ---@param w number width of outline 432 | ---@param h number height of outline 433 | ---@param colors linearGradient Colors of outline. Either a color, or table with 2 colors inside. 434 | ---@param material? IMaterial # Default material is vgui/white 435 | ---@param leftTop? boolean 436 | ---@param rightTop? boolean 437 | ---@param rightBottom? boolean 438 | ---@param leftBottom? boolean 439 | ---@param l number Left outline width 440 | ---@param t number Top outline width 441 | ---@param r number Right outline width 442 | ---@param b number Botton outline width 443 | ---@param curviness number? Curviness of rounded box. Default is 2. Makes rounded box behave as with formula ``x^curviness+y^curviness=radius^curviness`` (this is circle formula btw. Rounded boxes are superellipses) 444 | ---@param inside boolean? Revert vertex order to make outlines visible only on inside (when outline thickness is below 0.). Default - false 445 | ---@param cornerness number? Value, by which corner fraction (which value is between 0 and 1) will be powered to. Default - 1. 446 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, leftTop? : boolean, rightTop? : boolean, rightBottom? : boolean, leftBottom? : boolean, colors: Color[], material?: IMaterial, outlineThickness: number) 447 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, leftTop? : boolean, rightTop? : boolean, rightBottom? : boolean, leftBottom? : boolean, colors: Color[], material?: IMaterial, outlineWidth: number, outlineHeight: number) 448 | function outlines.drawOutlineEx(radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, material, l, 449 | t, r, b, curviness, inside, cornerness) 450 | if colors[2] == nil then 451 | ---@cast colors Color 452 | ---@diagnostic disable-next-line: cast-local-type 453 | colors = getColorTable(2, colors) 454 | end 455 | 456 | if radius == 0 then 457 | leftTop, rightTop, rightBottom, leftBottom = false, false, false, false 458 | end 459 | 460 | if t == nil then 461 | t, r, b = l, l, l 462 | elseif r == nil then 463 | r, b = l, t 464 | end 465 | 466 | inside = inside or false 467 | curviness = curviness or 2 468 | cornerness = cornerness or 1 469 | 470 | if batch.batching then 471 | drawOutlineBatched(radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, material, l, t, r, 472 | b, curviness) 473 | else 474 | drawOutlineSingle(radius, x, y, w, h, leftTop, rightTop, rightBottom, leftBottom, colors, material, l, t, r, 475 | b, curviness, inside, cornerness) 476 | end 477 | end 478 | 479 | local drawOutlineEx = outlines.drawOutlineEx 480 | 481 | ---Draws an outline with the specified parameters. Bases on rounded box, but makes outline of them. 482 | ---@param radius number radius of roundedBox the outline will 'outline' 483 | ---@param x number start X position of outline 484 | ---@param y number start Y position of outline 485 | ---@param w number width of outline 486 | ---@param h number height of outline 487 | ---@param colors linearGradient Colors of outline. Either a color, or table with 2 colors inside. 488 | ---@param material? IMaterial # Default material is vgui/white 489 | ---@param l number Left outline width 490 | ---@param t number Top outline width 491 | ---@param r number Right outline width 492 | ---@param b number Botton outline width 493 | ---@param curviness number? Curviness of rounded box. Default is 2. Makes rounded box behave as with formula ``x^curviness+y^curviness=radius^curviness`` (this is circle formula btw. Rounded boxes are superellipses) 494 | ---@param inside boolean? 495 | ---@param cornerness number? Value, by which corner fraction (which value is between 0 and 1) will be powered to. Default - 1. 496 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors: gradients, material?: IMaterial, outlineThickness: number, _ : nil, _ : nil, _ : nil, curviness: number?, inside : boolean?, cornerness: number?) 497 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors: gradients, material?: IMaterial, outlineWidth: number, outlineHeight: number, _ : nil, _ : nil, curviness: number?, inside : boolean?, cornerness: number?) 498 | function outlines.drawOutline(radius, x, y, w, h, colors, material, l, t, r, b, curviness, inside, cornerness) 499 | drawOutlineEx(radius, x, y, w, h, true, true, true, true, colors, material, l, t, r, b, curviness, inside, 500 | cornerness) 501 | end 502 | end 503 | 504 | paint.outlines = outlines 505 | -------------------------------------------------------------------------------- /lua/paint/circles_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | ---# paint.circles! 3 | ---### Forget about Circles! from sneakysquid 4 | ---he's a f***** btw ;) 5 | --- 6 | ---This library allows you to create and draw circles and ellipses 7 | ---``` 8 | ---But with a twist: 9 | ---1) They have gradients of course 10 | ---2) They can be sliced 11 | ---3) They support stencils 12 | ---4) They can have various curviness (squircles/SwiftUI/IOS rounded square ) 13 | ---@class paint.circles 14 | local circles = {} 15 | local paint = paint 16 | 17 | ---@param num number 18 | ---@param power number 19 | ---@return number 20 | local function fpow(num, power) 21 | if num > 0 then 22 | return num ^ power 23 | else 24 | return -((-num) ^ power) 25 | end 26 | end 27 | 28 | do 29 | local meshConstructor = Mesh 30 | 31 | local meshBegin = mesh.Begin 32 | local meshEnd = mesh.End 33 | local meshPosition = mesh.Position 34 | local meshColor = mesh.Color 35 | local meshTexCoord = mesh.TexCoord 36 | local meshAdvanceVertex = mesh.AdvanceVertex 37 | 38 | local PRIMITIVE_POLYGON = MATERIAL_POLYGON 39 | 40 | local originVector = Vector(0, 0, 0) 41 | 42 | local sin, cos = math.sin, math.cos 43 | 44 | 45 | ---Generates single circle mesh, unbatched 46 | ---@param vertexCount integer 47 | ---@param startAngle number 48 | ---@param endAngle number 49 | ---@param colors {[1]: Color, [2]: Color} 50 | ---@param curviness number 51 | ---@param rotation number 52 | ---@private Internal variable, not meant to be used outside. 53 | ---@return IMesh 54 | function circles.generateSingleMesh(vertexCount, startAngle, endAngle, colors, rotation, curviness) 55 | local meshObj = meshConstructor() 56 | 57 | local r, g, b, a = colors[2].r, colors[2].g, colors[2].b, colors[2].a 58 | local deltaAngle = endAngle - startAngle 59 | 60 | meshBegin(meshObj, PRIMITIVE_POLYGON, vertexCount + 2) -- vertexcount + center vertex 61 | meshPosition(originVector) 62 | meshColor(colors[1].r, colors[1].g, colors[1].b, colors[1].a) 63 | meshTexCoord(0, 0.5, 0.5) 64 | meshAdvanceVertex() 65 | 66 | for i = 0, vertexCount do 67 | local angle = startAngle + deltaAngle * i / vertexCount 68 | 69 | meshPosition(fpow(cos(angle), curviness), fpow(sin(angle), curviness), 0) 70 | meshColor(r, g, b, a) 71 | meshTexCoord(0, fpow(sin(angle + rotation), curviness) / 2 + 0.5, 72 | fpow(cos(angle + rotation), curviness) / 2 + 0.5) 73 | meshAdvanceVertex() 74 | end 75 | meshEnd() 76 | 77 | return meshObj 78 | end 79 | end 80 | 81 | do 82 | local batch = paint.batch 83 | local incrementZ = paint.incrementZ 84 | 85 | local sin, cos = math.sin, math.cos 86 | 87 | ---Generates circle mesh with batching being used. Since it's batched, we can't use matrices, so there are also x, y, and radius arguments 88 | ---@param x number 89 | ---@param y number 90 | ---@param vertexCount integer 91 | ---@param startAngle number 92 | ---@param endAngle number 93 | ---@param colors {[1]: Color, [2]: Color} 94 | ---@param curviness number 95 | ---@param rotation number 96 | ---@private Internal variable, not meant to be used outside. 97 | function circles.generateMeshBatched(x, y, w, h, vertexCount, startAngle, endAngle, colors, rotation, curviness) 98 | local startColor, endColor = colors[1], colors[2] 99 | 100 | local batchTable = batch.batchTable 101 | local len = batchTable[0] 102 | 103 | local z = incrementZ() 104 | 105 | local deltaAngle = endAngle - startAngle 106 | 107 | local cell = batch.getDrawCell() 108 | for i = 0, vertexCount - 1 do -- we make a triangle each time, we need to get next point, so yeah... 109 | local indexI = i * 17 110 | 111 | do -- 1st vertex (middle) 112 | batchTable[len + 1 + indexI] = x 113 | batchTable[len + 2 + indexI] = y 114 | batchTable[len + 3 + indexI] = z 115 | batchTable[len + 4 + indexI] = startColor 116 | end 117 | 118 | do -- 2nd vertex (current point) 119 | local angle = startAngle + deltaAngle * i / vertexCount 120 | 121 | batchTable[len + 5 + indexI] = x + fpow(cos(angle), curviness) * w -- second vertex 122 | batchTable[len + 6 + indexI] = y + fpow(sin(angle), curviness) * h 123 | batchTable[len + 7 + indexI] = endColor 124 | end 125 | 126 | do -- 3rd vertex (next point) 127 | local angle = startAngle + deltaAngle * (i + 1) / vertexCount 128 | 129 | batchTable[len + 8 + indexI] = x + fpow(cos(angle), curviness) * w -- second vertex 130 | batchTable[len + 9 + indexI] = y + fpow(sin(angle), curviness) * h 131 | batchTable[len + 10 + indexI] = endColor 132 | end 133 | --UV 134 | do 135 | batchTable[len + 11 + indexI] = 0.5 136 | batchTable[len + 12 + indexI] = 0.5 137 | --fpow(sin(angle + rotation), curviness) / 2 + 0.5, 138 | -- fpow(cos(angle + rotation), curviness) / 2 + 0.5 139 | batchTable[len + 13 + indexI] = fpow(sin(startAngle + deltaAngle * i / vertexCount + rotation), curviness) / 140 | 2 + 0.5 141 | batchTable[len + 14 + indexI] = fpow(cos(startAngle + deltaAngle * i / vertexCount + rotation), curviness) / 142 | 2 + 0.5 143 | 144 | batchTable[len + 15 + indexI] = fpow(sin(startAngle + deltaAngle * (i + 1) / vertexCount + rotation), 145 | curviness) / 2 + 0.5 146 | batchTable[len + 16 + indexI] = fpow(cos(startAngle + deltaAngle * (i + 1) / vertexCount + rotation), 147 | curviness) / 2 + 0.5 148 | 149 | batchTable[len + 17 + indexI] = cell 150 | end 151 | end 152 | 153 | batchTable[0] = len + 17 * vertexCount 154 | end 155 | end 156 | 157 | do 158 | local angleConverter = math.pi / 180 159 | 160 | local batch = paint.batch 161 | 162 | local matrix = Matrix() 163 | local setUnpacked = matrix.SetUnpacked 164 | 165 | local pushModelMatrix = cam.PushModelMatrix 166 | local popModelMatrix = cam.PopModelMatrix 167 | 168 | ---@type {[string] : IMesh} 169 | local cachedCircleMeshes = {} 170 | 171 | local format = string.format 172 | 173 | ---@param color1 Color 174 | ---@param color2 Color 175 | ---@param vertexCount integer 176 | ---@param startAngle number 177 | ---@param curviness number 178 | ---@return string id 179 | local function getId(color1, color2, vertexCount, startAngle, endAngle, rotation, curviness) 180 | return format('%x%x%x%x;%x%x%x%x;%u;%f;%f;%f;%f', 181 | color1.r, color1.g, color1.b, color1.a, 182 | color2.r, color2.g, color2.b, color2.a, 183 | vertexCount, startAngle, endAngle, rotation, curviness 184 | ) 185 | end 186 | 187 | local renderSetMaterial = render.SetMaterial 188 | 189 | local generateSingleMesh = circles.generateSingleMesh 190 | local generateMeshBatched = circles.generateMeshBatched 191 | 192 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 193 | 194 | local getColorTable = paint.getColorTable 195 | 196 | ---@param x number # CENTER X coordinate of circle 197 | ---@param y number # CENTER Y coordinate of circle 198 | ---@param w number x xradius # Width/X radius of circle 199 | ---@param h number y radius # Height/Y radius of circle 200 | ---@param vertexCount integer? Vertex count that circle will have 201 | ---@param startAngle number? Starting angle of sliced circle. Default is 0. MUST BE LOWER THAN END ANGLE 202 | ---@param endAngle number? Ending angle of sliced circle. Default is 360. MUST BE HIGHER THAN START ANGLE 203 | ---@param colors Color | {[1]: Color, [2]: Color} Color of circle. Can be a Color, or table with 2 colors inside. 204 | ---@param curviness number? Curviness ratio of circle. Think of circle defined as a formula like ``x^2+y^2=1``. But replace 2 with curviness. 205 | ---For squircle like in IOS, curviness is 4, resulting in ``x^4+y^4=1`` 206 | function circles.drawCircle(x, y, w, h, colors, vertexCount, startAngle, endAngle, material, rotation, curviness) 207 | if colors[2] == nil then 208 | colors = getColorTable(2, colors) 209 | end 210 | 211 | curviness = 2 / (curviness or 2) 212 | 213 | if vertexCount == nil then 214 | vertexCount = 24 215 | end 216 | 217 | if startAngle == nil then 218 | startAngle = 0 219 | endAngle = 360 220 | end 221 | 222 | if rotation == nil then 223 | rotation = 0 224 | end 225 | 226 | rotation = rotation * angleConverter 227 | startAngle = startAngle * angleConverter 228 | endAngle = endAngle * angleConverter 229 | 230 | if batch.batching then 231 | generateMeshBatched(x, y, w, h, vertexCount, startAngle, endAngle, colors, rotation, curviness) 232 | else 233 | local id = getId(colors[1], colors[2], vertexCount, startAngle, endAngle, rotation, curviness) 234 | 235 | local meshObj = cachedCircleMeshes[id] 236 | 237 | if meshObj == nil then 238 | meshObj = generateSingleMesh(vertexCount, startAngle, endAngle, colors, rotation, curviness) 239 | cachedCircleMeshes[id] = meshObj 240 | end 241 | 242 | material = material or paint.defaultMaterial 243 | 244 | setUnpacked(matrix, 245 | w, 0, 0, x, 246 | 0, h, 0, y, 247 | 0, 0, 1, 0, 248 | 0, 0, 0, 1 249 | ) 250 | 251 | renderSetMaterial(material) 252 | 253 | pushModelMatrix(matrix, true) 254 | meshDraw(meshObj) 255 | popModelMatrix() 256 | end 257 | end 258 | 259 | timer.Create('paint.circlesGarbageCollector' .. SysTime(), 60, 0, function() 260 | for k, v in pairs(cachedCircleMeshes) do 261 | v:Destroy() 262 | cachedCircleMeshes[k] = nil 263 | end 264 | end) 265 | end 266 | 267 | -- Now circled outlines! 268 | 269 | do 270 | local meshConstructor = Mesh 271 | 272 | local meshBegin = mesh.Begin 273 | local meshEnd = mesh.End 274 | local meshPosition = mesh.Position 275 | local meshColor = mesh.Color 276 | local meshTexCoord = mesh.TexCoord 277 | local meshAdvanceVertex = mesh.AdvanceVertex 278 | 279 | local PRIMITIVE_TRIANGLE_STRIP = MATERIAL_TRIANGLE_STRIP 280 | local sin, cos = math.sin, math.cos 281 | 282 | ---Generates single circle mesh, unbatched 283 | ---@param vertexCount integer 284 | ---@param startAngle number 285 | ---@param endAngle number 286 | ---@param colors {[1]: Color, [2]: Color} 287 | ---@param startU number 288 | ---@param endU number 289 | ---@param outlineWidth number # note, that this outlineWidth is between 0-1, cuz it's basically a percentage of radius 290 | ---@param curviness number 291 | ---@private Internal variable, not meant to be used outside . 292 | ---@return IMesh 293 | function circles.generateOutlineMeshSingle(vertexCount, startAngle, endAngle, colors, startU, endU, outlineWidth, 294 | curviness) 295 | local meshObj = meshConstructor() 296 | 297 | local startR, startG, startB, startA = colors[1].r, colors[1].g, colors[1].b, colors[1].a 298 | local endR, endG, endB, endA = colors[2].r, colors[2].g, colors[2].b, colors[2].a 299 | 300 | local deltaAngle = endAngle - startAngle 301 | 302 | local startRadius = 1 - outlineWidth 303 | meshBegin(meshObj, PRIMITIVE_TRIANGLE_STRIP, vertexCount * 2) -- result vertexcount = innerVertexes + outerVertexes. Count of inner veretxes = count of outer veretxes 304 | for i = 0, vertexCount do 305 | local percent = i / vertexCount 306 | local angle = startAngle + deltaAngle * percent 307 | local sinn, coss = fpow(sin(angle), curviness), fpow(cos(angle), curviness) 308 | 309 | local u = startU + percent * (endU - startU) 310 | 311 | meshPosition(coss * startRadius, sinn * startRadius, 0) 312 | meshColor(startR, startG, startB, startA) 313 | meshTexCoord(0, u, 0) 314 | meshAdvanceVertex() 315 | 316 | meshPosition(coss, sinn, 0) 317 | meshColor(endR, endG, endB, endA) 318 | meshTexCoord(0, u, 1) 319 | meshAdvanceVertex() 320 | end 321 | meshEnd() 322 | 323 | return meshObj 324 | end 325 | end 326 | 327 | do 328 | local batch = paint.batch 329 | local incrementZ = paint.incrementZ 330 | 331 | local sin, cos = math.sin, math.cos 332 | 333 | ---@param vertexCount integer 334 | ---@param startAngle number 335 | ---@param endAngle number 336 | ---@param colors {[1]: Color, [2]: Color} 337 | ---@param x number 338 | ---@param y number 339 | ---@param radiusW number 340 | ---@param radiusH number 341 | ---@param startU number 342 | ---@param endU number 343 | ---@param outlineWidth number 344 | ---@param curviness number 345 | function circles.generateOutlineMeshBatched(vertexCount, startAngle, endAngle, colors, x, y, radiusW, radiusH, 346 | startU, endU, outlineWidth, curviness) 347 | local startColor, endColor = colors[1], colors[2] 348 | 349 | local batchTable = batch.batchTable 350 | local len = batchTable[0] 351 | 352 | local z = incrementZ() 353 | 354 | local deltaAngle = endAngle - startAngle 355 | local cell = batch.getDrawCell() 356 | for i = 0, vertexCount - 1 do 357 | local indexI = i * 34 358 | 359 | local angle = startAngle + deltaAngle * i / vertexCount 360 | local u = startU + i / vertexCount * (endU - startU) 361 | batchTable[len + 1 + indexI] = x + fpow(cos(angle), curviness) * radiusW 362 | batchTable[len + 2 + indexI] = y + fpow(sin(angle), curviness) * radiusH 363 | batchTable[len + 3 + indexI] = z 364 | batchTable[len + 4 + indexI] = startColor 365 | 366 | batchTable[len + 5 + indexI] = x + fpow(cos(angle), curviness) * (radiusW + outlineWidth) 367 | batchTable[len + 6 + indexI] = y + fpow(sin(angle), curviness) * (radiusH + outlineWidth) 368 | batchTable[len + 7 + indexI] = endColor 369 | 370 | local angle2 = startAngle + deltaAngle * (i + 1) / vertexCount 371 | local u2 = startU + (i + 1) / vertexCount * (endU - startU) 372 | batchTable[len + 8 + indexI] = x + fpow(cos(angle2), curviness) * radiusW 373 | batchTable[len + 9 + indexI] = y + fpow(sin(angle2), curviness) * radiusH 374 | batchTable[len + 10 + indexI] = startColor 375 | 376 | batchTable[len + 11 + indexI] = u 377 | batchTable[len + 12 + indexI] = 0 378 | batchTable[len + 13 + indexI] = u 379 | batchTable[len + 14 + indexI] = 1 380 | batchTable[len + 15 + indexI] = u2 381 | batchTable[len + 16 + indexI] = 0 382 | 383 | batchTable[len + 17 + indexI] = cell 384 | 385 | 386 | batchTable[len + 18 + indexI] = x + fpow(cos(angle2), curviness) * radiusW 387 | batchTable[len + 19 + indexI] = y + fpow(sin(angle2), curviness) * radiusH 388 | batchTable[len + 20 + indexI] = z 389 | batchTable[len + 21 + indexI] = startColor 390 | 391 | batchTable[len + 22 + indexI] = x + fpow(cos(angle), curviness) * (radiusW + outlineWidth) 392 | batchTable[len + 23 + indexI] = y + fpow(sin(angle), curviness) * (radiusH + outlineWidth) 393 | batchTable[len + 24 + indexI] = endColor 394 | 395 | batchTable[len + 25 + indexI] = x + fpow(cos(angle2), curviness) * (radiusW + outlineWidth) 396 | batchTable[len + 26 + indexI] = y + fpow(sin(angle2), curviness) * (radiusH + outlineWidth) 397 | batchTable[len + 27 + indexI] = endColor 398 | 399 | batchTable[len + 28 + indexI] = u2 400 | batchTable[len + 29 + indexI] = 0 401 | batchTable[len + 30 + indexI] = u 402 | batchTable[len + 31 + indexI] = 1 403 | batchTable[len + 32 + indexI] = u2 404 | batchTable[len + 33 + indexI] = 1 405 | 406 | batchTable[len + 34 + indexI] = cell 407 | end 408 | 409 | batchTable[0] = len + 34 * vertexCount 410 | end 411 | end 412 | 413 | do 414 | local format = string.format 415 | 416 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 417 | local pushModelMatrix = cam.PushModelMatrix 418 | local popModelMatrix = cam.PopModelMatrix 419 | 420 | local generateOutlineMeshSingle = circles.generateOutlineMeshSingle 421 | 422 | local matrix = Matrix() 423 | local setUnpacked = matrix.SetUnpacked 424 | 425 | local renderSetMaterial = render.SetMaterial 426 | 427 | local cachedCircleOutlineMeshes = {} 428 | 429 | ---@param vertexCount integer 430 | ---@param startAngle number 431 | ---@param endAngle number 432 | ---@param startU number 433 | ---@param endU number 434 | ---@param outlineWidth number 435 | ---@param curviness number 436 | ---@return string id 437 | local function getId(color1, color2, vertexCount, startAngle, endAngle, startU, endU, outlineWidth, curviness) 438 | return format('%x%x%x%x;%x%x%x%x;%u;%f;%f;%f;%f;%f;%f', color1.r, color1.g, color1.b, color1.a, color2.r, 439 | color2.g, color2.b, color2.a, vertexCount, startAngle, endAngle, startU, endU, outlineWidth, curviness) 440 | end 441 | 442 | ---@param x number 443 | ---@param y number 444 | ---@param w number 445 | ---@param h number 446 | ---@param vertexCount integer 447 | ---@param startAngle number 448 | ---@param endAngle number 449 | ---@param colors {[1]: Color, [2]: Color} 450 | ---@param startU number 451 | ---@param endU number 452 | ---@param curviness number 453 | ---@param outlineWidth number # note, that this outlineWidth is between 0-1, cuz it's basically a percentage of radius 454 | ---@private Internal variable, not meant to be used outside. 455 | function circles.drawOutlineSingle(x, y, w, h, colors, vertexCount, startAngle, endAngle, material, startU, endU, 456 | outlineWidth, curviness) 457 | local id = getId(colors[1], colors[2], vertexCount, startAngle, endAngle, startU, endU, outlineWidth, curviness) 458 | 459 | local meshObj = cachedCircleOutlineMeshes[id] 460 | 461 | if meshObj == nil then 462 | meshObj = generateOutlineMeshSingle(vertexCount, startAngle, endAngle, colors, startU, endU, outlineWidth, 463 | curviness) 464 | cachedCircleOutlineMeshes[id] = meshObj 465 | end 466 | 467 | setUnpacked(matrix, 468 | w, 0, 0, x, 469 | 0, h, 0, y, 470 | 0, 0, 1, 0, 471 | 0, 0, 0, 1 472 | ) 473 | 474 | renderSetMaterial(material) 475 | 476 | pushModelMatrix(matrix, true) 477 | meshDraw(meshObj) 478 | popModelMatrix() 479 | end 480 | 481 | timer.Create('paint.circleOutlinesGarbageCollector' .. SysTime(), 60, 0, function() 482 | for k, v in pairs(cachedCircleOutlineMeshes) do 483 | v:Destroy() 484 | cachedCircleOutlineMeshes[k] = nil 485 | end 486 | end) 487 | end 488 | 489 | do 490 | local angleConverter = math.pi / 180 491 | 492 | local drawOutlineSingle = circles.drawOutlineSingle 493 | local max = math.max 494 | 495 | local batch = paint.batch 496 | 497 | local getColorTable = paint.getColorTable 498 | 499 | local generateOutlineMeshBatched = circles.generateOutlineMeshBatched 500 | ---Draws circled outline. UNBATCHED ONLY. 501 | ---@param x number # CENTER X coordinate of circled outline 502 | ---@param y number # CENTER Y coordinate of circled outline 503 | ---@param w number x xradius # Width/X radius of circled outline 504 | ---@param h number y radius # Height/Y radius of circled outline 505 | ---@param colors Color | {[1]: Color, [2]: Color} Color of circledOutline. Can be a Color, or table with 2 colors inside. 506 | ---@param outlineWidth number 507 | ---@param vertexCount integer? Vertex count that circled outline will have 508 | ---@param startAngle number? Starting angle of sliced circled outline. Default is 0. MUST BE LOWER THAN END ANGLE 509 | ---@param endAngle number? Ending angle of sliced circled outline. Default is 360. MUST BE HIGHER THAN START ANGLE 510 | ---@param startU? number 511 | ---@param endU? number 512 | ---@param curviness number? Curviness ratio of circledOutline. Think of circledOutline defined as a formula like ``outlineRatio^2<=x^2+y^2<=1``. But replace 2 with curviness. 513 | ---For squircle like in IOS, curviness is 4, resulting in ``outlineRatio^4<=x^4+y^4<=1`` 514 | function circles.drawOutline(x, y, w, h, colors, outlineWidth, vertexCount, startAngle, endAngle, material, startU, 515 | endU, curviness) 516 | if colors[2] == nil then 517 | colors = getColorTable(2, colors) 518 | end 519 | 520 | if vertexCount == nil then 521 | vertexCount = 24 522 | end 523 | 524 | curviness = 2 / (curviness or 2) 525 | 526 | if startAngle == nil then 527 | startAngle = 0 528 | endAngle = 360 529 | end 530 | 531 | if startU == nil then 532 | startU = 0 533 | endU = 1 534 | end 535 | ---@cast endU -? 536 | 537 | material = material or paint.defaultMaterial 538 | 539 | startAngle = startAngle * angleConverter 540 | endAngle = endAngle * angleConverter 541 | 542 | 543 | if batch.batching then 544 | generateOutlineMeshBatched(vertexCount, startAngle, endAngle, colors, x, y, w, h, startU, endU, outlineWidth, 545 | curviness) 546 | else 547 | outlineWidth = outlineWidth / max(w, h) 548 | ---@diagnostic disable-next-line: param-type-mismatch 549 | drawOutlineSingle(x, y, w, h, colors, vertexCount, startAngle, endAngle, material, startU, endU, outlineWidth, 550 | curviness) 551 | end 552 | end 553 | end 554 | 555 | paint.circles = circles 556 | -------------------------------------------------------------------------------- /lua/paint/rounded_boxes_cl.lua: -------------------------------------------------------------------------------- 1 | ---@diagnostic disable: deprecated 2 | local paint = paint --[[@as paint]] 3 | 4 | --What makes paint rounded boxes better than the draw library's rounded boxes? 5 | --1) Support for per-corner gradients! 6 | --2) Improved performance when drawing multiple rounded boxes, thanks to batching! 7 | --3) Stencil support! 8 | --4) Material support! 9 | --5) Curviness support (squircles/superellipses support) 10 | -- 11 | --Simple Example 12 | --Drawing rounded boxes with different corner radius and colors. 13 | --```lua 14 | -- -- A colorful rounded box 15 | -- paint.roundedBoxes.roundedBox( 20, 5, 5, 64, 64, { 16 | -- Color( 255, 0, 0 ), -- Top Left 17 | -- Color( 0, 255, 0 ), -- Top Right 18 | -- Color( 0, 0, 255 ), -- Bottom Right 19 | -- color_white, -- Bottom Left 20 | -- color_black -- Center 21 | -- } ) 22 | -- -- An icon with rounded corners 23 | -- paint.roundedBoxes.roundedBox( 32, 72, 5, 64, 64, COLOR_WHITE, ( Material( "icon16/application_xp.png" ) ) ) 24 | --``` 25 | -- 26 | --Asymmetrical Example 27 | --Drawing a rounded box with only the top-right and bottom-left corners rounded. 28 | --```lua 29 | --paint.roundedBoxes.roundedBoxEx( 16, 10, 10, 64, 64, COLOR_WHITE, false, true, false, true ) 30 | --``` 31 | -- 32 | --Stencil Masked Example 33 | --```lua 34 | -- local function mask(drawMask, draw) 35 | -- render.ClearStencil() 36 | -- render.SetStencilEnable(true) 37 | -- 38 | -- render.SetStencilWriteMask(1) 39 | -- render.SetStencilTestMask(1) 40 | -- 41 | -- render.SetStencilFailOperation(STENCIL_REPLACE) 42 | -- render.SetStencilPassOperation( STENCIL_REPLACE) 43 | -- render.SetStencilZFailOperation(STENCIL_KEEP) 44 | -- render.SetStencilCompareFunction(STENCIL_ALWAYS) 45 | -- render.SetStencilReferenceValue(1) 46 | -- 47 | -- drawMask() 48 | -- 49 | -- render.SetStencilFailOperation(STENCIL_KEEP) 50 | -- render.SetStencilPassOperation(STENCIL_REPLACE) 51 | -- render.SetStencilZFailOperation(STENCIL_KEEP) 52 | -- render.SetStencilCompareFunction(STENCIL_EQUAL) 53 | -- render.SetStencilReferenceValue(1) 54 | -- 55 | -- draw() 56 | -- 57 | -- render.SetStencilEnable(false) 58 | -- render.ClearStencil() 59 | -- end 60 | -- 61 | -- local RIPPLE_DIE_TIME = 1 62 | -- local RIPPLE_START_ALPHA = 50 63 | -- 64 | -- function button:Paint(w, h) 65 | -- paint.startPanel(self) 66 | -- mask(function() 67 | -- paint.roundedBoxes.roundedBox( 32, 0, 0, w, h, COLOR_RED ) 68 | -- end, 69 | -- function() 70 | -- local ripple = self.rippleEffect 71 | -- 72 | -- if ripple == nil then return end 73 | -- 74 | -- local rippleX, rippleY, rippleStartTime = ripple[1], ripple[2], ripple[3] 75 | -- 76 | -- local percent = (RealTime() - rippleStartTime) / RIPPLE_DIE_TIME 77 | -- 78 | -- if percent >= 1 then 79 | -- self.rippleEffect = nil 80 | -- else 81 | -- local alpha = RIPPLE_START_ALPHA * (1 - percent) 82 | -- local radius = math.max(w, h) * percent * math.sqrt(2) 83 | -- 84 | -- paint.roundedBoxes.roundedBox(radius, rippleX - radius, rippleY - radius, radius * 2, radius * 2, ColorAlpha(COLOR_WHITE, alpha)) 85 | -- end 86 | -- end) 87 | -- paint.endPanel() 88 | -- end 89 | --``` 90 | -- 91 | --Animated Rainbow Colors Example 92 | --Drawing a rounded box with a rainbow gradient. 93 | --```lua 94 | -- local time1, time2 = RealTime() * 100, RealTime() * 100 + 30 95 | -- local time3 = (time1 + time2) / 2 96 | -- 97 | -- local color1, color2, color3 = HSVToColor(time1, 1, 1), HSVToColor(time2, 1, 1), HSVToColor(time3, 1, 1) 98 | -- 99 | -- paint.roundedBoxes.roundedBox(32, 10, 10, 300, 128, {color1, color3, color2, color3}) 100 | -- -- Center is color3 not nil because interpolating between colors and between HSV is different 101 | --``` 102 | ---@class paint.roundedBoxes 103 | local roundedBoxes = {} 104 | 105 | ---@alias paint.createVertexFunc fun(x : number, y : number, u : number, v: number, colors : Color[], u1 : number, v1 : number, u2 : number, v2 : number) 106 | 107 | do 108 | -- NOTE: it's likely implied that radius cant be 0, and can't be higher than width / 2 or height / 2 109 | local meshBegin = mesh.Begin 110 | local meshEnd = mesh.End 111 | 112 | local PRIMITIVE_POLYGON = MATERIAL_POLYGON 113 | local clamp = math.Clamp 114 | local halfPi = math.pi / 2 115 | 116 | local sin = math.sin 117 | local cos = math.cos 118 | 119 | ---@param num number 120 | ---@param power number 121 | ---@return number 122 | local function fpow(num, power) 123 | if num > 0 then 124 | return num ^ power 125 | else 126 | return -((-num) ^ power) 127 | end 128 | end 129 | 130 | ---@param radius number 131 | ---@param rightTop boolean? 132 | ---@param rightBottom boolean? 133 | ---@param leftBottom boolean? 134 | ---@param leftTop boolean? 135 | ---@return integer vertex count 136 | function roundedBoxes.getMeshVertexCount(radius, rightTop, rightBottom, leftBottom, leftTop) 137 | if radius > 3 then 138 | local vertsPerEdge = clamp(radius / 2, 3, 24) 139 | return 10 140 | + (rightTop and vertsPerEdge or 0) 141 | + (rightBottom and vertsPerEdge or 0) 142 | + (leftBottom and vertsPerEdge or 0) 143 | + (leftTop and vertsPerEdge or 0) 144 | else 145 | return 10 146 | + (rightTop and 1 or 0) 147 | + (rightBottom and 1 or 0) 148 | + (leftBottom and 1 or 0) 149 | + (leftTop and 1 or 0) 150 | end 151 | end 152 | 153 | local getMeshVertexCount = roundedBoxes.getMeshVertexCount 154 | 155 | ---@type Color[] 156 | local centreTab = {} 157 | --- Generates roundedBox mesh, used by outlines, 158 | ---@param createVertex paint.createVertexFunc # function used to create vertex. 159 | ---@param mesh? IMesh 160 | ---@param radius number 161 | ---@param x number 162 | ---@param y number 163 | ---@param endX number 164 | ---@param endY number 165 | ---@param leftTop? boolean 166 | ---@param rightTop? boolean 167 | ---@param rightBottom? boolean 168 | ---@param leftBottom? boolean 169 | ---@param colors Color[] 170 | ---@param u1 number 171 | ---@param v1 number 172 | ---@param u2 number 173 | ---@param v2 number 174 | ---@param curviness number? 175 | ---@private Internal variable. Not meant to use outside 176 | function roundedBoxes.generateSingleMesh(createVertex, mesh, radius, x, y, endX, endY, leftTop, rightTop, rightBottom, 177 | leftBottom, colors, u1, v1, u2, v2, curviness) 178 | local vertsPerEdge = clamp(radius / 2, 3, 24) 179 | 180 | local isRadiusBig = radius > 3 181 | 182 | curviness = 2 / (curviness or 2) 183 | 184 | local w, h = endX - x, endY - y 185 | 186 | if mesh then 187 | meshBegin(mesh, PRIMITIVE_POLYGON, getMeshVertexCount(radius, rightTop, rightBottom, leftBottom, leftTop)) 188 | end 189 | 190 | local fifthColor = colors[5] 191 | if fifthColor == nil then 192 | createVertex((x + endX) * 0.5, (y + endY) * 0.5, 0.5, 0.5, colors, u1, v1, u2, v2) 193 | else 194 | centreTab[1], centreTab[2], centreTab[3], centreTab[4] = fifthColor, fifthColor, fifthColor, fifthColor 195 | createVertex((x + endX) * 0.5, (y + endY) * 0.5, 0.5, 0.5, centreTab, u1, v1, u2, v2) 196 | end 197 | 198 | createVertex(x + (leftTop and radius or 0), y, (leftTop and radius or 0) / w, 0, colors, u1, v1, u2, v2) 199 | 200 | createVertex((x + endX) * 0.5, y, 0.5, 0, colors, u1, v1, u2, v2) 201 | 202 | createVertex(endX - (rightTop and radius or 0), y, 1 - (rightTop and radius or 0) / w, 0, colors, u1, v1, u2, v2) 203 | -- 3 vertices 204 | 205 | if rightTop then 206 | if isRadiusBig then 207 | local deltaX = endX - radius 208 | local deltaY = y + radius 209 | 210 | for i = 1, vertsPerEdge - 1 do 211 | local angle = halfPi * (i / vertsPerEdge) 212 | 213 | local sinn, coss = fpow(sin(angle), curviness), fpow(cos(angle), curviness) 214 | 215 | local newX, newY = deltaX + sinn * radius, deltaY - coss * radius 216 | 217 | createVertex(newX, newY, 1 - (1 - sinn) * radius / w, (1 - coss) * radius / h, colors, u1, v1, u2, v2) 218 | end 219 | end 220 | 221 | createVertex(endX, y + radius, 1, radius / h, colors, u1, v1, u2, v2) 222 | end 223 | 224 | createVertex(endX, (y + endY) * 0.5, 1, 0.5, colors, u1, v1, u2, v2) 225 | 226 | createVertex(endX, endY - (rightBottom and radius or 0), 1, 1 - (rightBottom and radius or 0) / h, colors, u1, v1, 227 | u2, v2) 228 | 229 | if rightBottom then 230 | if isRadiusBig then 231 | local deltaX = endX - radius 232 | local deltaY = endY - radius 233 | 234 | for i = 1, vertsPerEdge - 1 do 235 | local angle = halfPi * (i / vertsPerEdge) 236 | 237 | local sinn, coss = fpow(sin(angle), curviness), fpow(cos(angle), curviness) 238 | 239 | local newX, newY = deltaX + coss * radius, deltaY + sinn * radius 240 | 241 | createVertex(newX, newY, 1 - ((1 - coss) * radius) / w, 1 - ((1 - sinn) * radius) / h, colors, u1, v1, 242 | u2, v2) 243 | end 244 | end 245 | 246 | createVertex(endX - radius, endY, 1 - radius / w, 1, colors, u1, v1, u2, v2) 247 | end 248 | 249 | createVertex((x + endX) * 0.5, endY, 0.5, 1, colors, u1, v1, u2, v2) 250 | 251 | createVertex(x + (leftBottom and radius or 0), endY, (leftBottom and radius or 0) / w, 1, colors, u1, v1, u2, v2) 252 | 253 | if leftBottom then 254 | if isRadiusBig then 255 | local deltaX = x + radius 256 | local deltaY = endY - radius 257 | 258 | for i = 1, vertsPerEdge - 1 do 259 | local angle = halfPi * (i / vertsPerEdge) 260 | 261 | local sinn, coss = fpow(sin(angle), curviness), fpow(cos(angle), curviness) 262 | 263 | local newX, newY = deltaX - sinn * radius, deltaY + coss * radius 264 | 265 | createVertex(newX, newY, (1 - sinn) * radius / w, 1 - (1 - coss) * radius / h, colors, u1, v1, u2, v2) 266 | end 267 | end 268 | 269 | createVertex(x, endY - radius, 0, 1 - radius / h, colors, u1, v1, u2, v2) 270 | end 271 | 272 | createVertex(x, (y + endY) * 0.5, 0, 0.5, colors, u1, v1, u2, v2) 273 | 274 | createVertex(x, y + (leftTop and radius or 0), 0, (leftTop and radius or 0) / h, colors, u1, v1, u2, v2) 275 | 276 | if leftTop then 277 | if isRadiusBig then 278 | local deltaX = x + radius 279 | local deltaY = y + radius 280 | 281 | for i = 1, vertsPerEdge - 1 do 282 | local angle = halfPi * (i / vertsPerEdge) 283 | 284 | local sinn, coss = fpow(sin(angle), curviness), fpow(cos(angle), curviness) 285 | 286 | local newX, newY = deltaX - coss * radius, deltaY - sinn * radius 287 | 288 | createVertex(newX, newY, (1 - coss) * (radius / w), (1 - sinn) * (radius / h), colors, u1, v1, u2, v2) 289 | end 290 | end 291 | 292 | createVertex(x + radius, y, radius / w, 0, colors, u1, v1, u2, v2) 293 | end 294 | 295 | if mesh then 296 | meshEnd() 297 | end 298 | end 299 | end 300 | 301 | do 302 | local meshPosition = mesh.Position 303 | local meshColor = mesh.Color 304 | local meshAdvanceVertex = mesh.AdvanceVertex 305 | local meshTexCoord = mesh.TexCoord 306 | 307 | ---@diagnostic disable-next-line: invisible 308 | local bilinearInterpolation = paint.bilinearInterpolation 309 | 310 | ---Internal function used in pair with mesh.Begin(PRIMITIVE_POLYGON). Used for single batched rounded boxes. 311 | ---@type paint.createVertexFunc 312 | local function createVertex(x, y, u, v, colors, u1, v1, u2, v2) 313 | local leftTop, rightTop, rightBottom, leftBottom = colors[1], colors[2], colors[3], colors[4] 314 | meshPosition(x, y, 0) 315 | meshTexCoord(0, u * (u2 - u1) + u1, v * (v2 - v1) + v1) 316 | meshColor( 317 | bilinearInterpolation(u, v, leftTop.r, rightTop.r, rightBottom.r, leftBottom.r), 318 | bilinearInterpolation(u, v, leftTop.g, rightTop.g, rightBottom.g, leftBottom.g), 319 | bilinearInterpolation(u, v, leftTop.b, rightTop.b, rightBottom.b, leftBottom.b), 320 | bilinearInterpolation(u, v, leftTop.a, rightTop.a, rightBottom.a, leftBottom.a) 321 | ) 322 | 323 | meshAdvanceVertex() 324 | end 325 | 326 | 327 | local meshConstructor = Mesh 328 | local meshDraw = FindMetaTable('IMesh') --[[@as IMesh]].Draw 329 | 330 | local format = string.format 331 | 332 | local setMaterial = render.SetMaterial 333 | 334 | local matrix = Matrix() 335 | local setField = matrix.SetField 336 | 337 | local pushModelMatrix = cam.PushModelMatrix 338 | local popModelMatrix = cam.PopModelMatrix 339 | 340 | local generateSingleMesh = roundedBoxes.generateSingleMesh 341 | 342 | --- Helper function to get ID 343 | ---@param radius number 344 | ---@param w number 345 | ---@param h number 346 | ---@param corners number 347 | ---@param colors Color[] 348 | ---@param u1 number 349 | ---@param v1 number 350 | ---@param u2 number 351 | ---@param v2 number 352 | ---@param curviness number 353 | ---@return string id 354 | local function getId(radius, w, h, corners, colors, u1, v1, u2, v2, curviness) 355 | local color1, color2, color3, color4, color5 = colors[1], colors[2], colors[3], colors[4], colors[5] 356 | 357 | if color5 == nil then 358 | return format('%f;%f;%f;%u;%x%x%x%x;%x%x%x%x;%x%x%x%x;%x%x%x%x;%f;%f;%f;%f;%f', 359 | radius, w, h, corners, 360 | color1.r, color1.g, color1.b, color1.a, 361 | color2.r, color2.g, color2.b, color2.a, 362 | color3.r, color3.g, color3.b, color3.a, 363 | color4.r, color4.g, color4.b, color4.a, 364 | u1, v1, u2, v2, curviness 365 | ) 366 | else 367 | return format('%f;%f;%f;%u;%x%x%x%x;%x%x%x%x;%x%x%x%x;%x%x%x%x;%x%x%x%x;%f;%f;%f;%f;%f', 368 | radius, w, h, corners, 369 | color1.r, color1.g, color1.b, color1.a, 370 | color2.r, color2.g, color2.b, color2.a, 371 | color3.r, color3.g, color3.b, color3.a, 372 | color4.r, color4.g, color4.b, color4.a, 373 | color5.r, color5.g, color5.b, color5.a, 374 | u1, v1, u2, v2, curviness 375 | ) 376 | end 377 | end 378 | 379 | ---@type table 380 | local cachedRoundedBoxMeshes = {} 381 | 382 | --- Draws single unbached rounded box 383 | ---@param radius number 384 | ---@param x number 385 | ---@param y number 386 | ---@param w number 387 | ---@param h number 388 | ---@param colors Color[] 389 | ---@param leftTop? boolean 390 | ---@param rightTop? boolean 391 | ---@param rightBottom? boolean 392 | ---@param leftBottom? boolean 393 | ---@param material IMaterial 394 | ---@param u1 number 395 | ---@param v1 number 396 | ---@param u2 number 397 | ---@param v2 number 398 | ---@param curviness number? 399 | ---@private Internal variable. Not meant to use outside 400 | function roundedBoxes.roundedBoxExSingle(radius, x, y, w, h, colors, leftTop, rightTop, rightBottom, leftBottom, 401 | material, u1, v1, u2, v2, curviness) 402 | curviness = curviness or 2 403 | local id = getId(radius, w, h, 404 | (leftTop and 8 or 0) + (rightTop and 4 or 0) + (rightBottom and 2 or 0) + (leftBottom and 1 or 0), colors, u1, 405 | v1, u2, v2, curviness) 406 | 407 | local meshObj = cachedRoundedBoxMeshes[id] 408 | 409 | if meshObj == nil then 410 | meshObj = meshConstructor() 411 | generateSingleMesh(createVertex, meshObj, radius, 0, 0, w, h, leftTop, rightTop, rightBottom, leftBottom, 412 | colors, u1, v1, u2, v2, curviness) 413 | 414 | cachedRoundedBoxMeshes[id] = meshObj 415 | end 416 | 417 | setField(matrix, 1, 4, x) 418 | setField(matrix, 2, 4, y) 419 | 420 | pushModelMatrix(matrix, true) 421 | setMaterial(material) 422 | meshDraw(meshObj) 423 | popModelMatrix() 424 | end 425 | 426 | timer.Create('paint.roundedBoxesGarbageCollector' .. SysTime(), 30, 0, function() 427 | for k, v in pairs(cachedRoundedBoxMeshes) do 428 | v:Destroy() 429 | cachedRoundedBoxMeshes[k] = nil 430 | end 431 | end) 432 | end 433 | 434 | do 435 | ---@type {[1] : number, [2]:number, [3]: Color, [4] : number} | nil 436 | local prev1 437 | ---@type {[1] : number, [2] : number, [3] : Color} | nil 438 | local prev2 = {} 439 | 440 | local batch = paint.batch 441 | local incrementZ = paint.incrementZ 442 | 443 | local color = Color 444 | ---@diagnostic disable-next-line: invisible 445 | local bilinearInterpolation = paint.bilinearInterpolation 446 | 447 | ---@type paint.createVertexFunc 448 | local function createVertex(x, y, u, v, colors, u1, v1, u2, v2) 449 | local texU, texV = u * (u2 - u1) + u1, v * (v2 - v1) + v1 450 | 451 | if prev1 == nil then 452 | local z = incrementZ() 453 | local blendedColor = color( 454 | (colors[1].r + colors[2].r + colors[3].r + colors[4].r) / 4, 455 | (colors[1].g + colors[2].g + colors[3].g + colors[4].g) / 4, 456 | (colors[1].b + colors[2].b + colors[3].b + colors[4].b) / 4, 457 | (colors[1].a + colors[2].a + colors[3].a + colors[4].a) / 4 458 | ) 459 | 460 | prev1 = { x, y, blendedColor, z, texU, texV } 461 | return 462 | end 463 | 464 | ---@type Color 465 | local prefferedColor = color( 466 | bilinearInterpolation(u, v, colors[1].r, colors[2].r, colors[3].r, colors[4].r), 467 | bilinearInterpolation(u, v, colors[1].g, colors[2].g, colors[3].g, colors[4].g), 468 | bilinearInterpolation(u, v, colors[1].b, colors[2].b, colors[3].b, colors[4].b), 469 | bilinearInterpolation(u, v, colors[1].a, colors[2].a, colors[3].a, colors[4].a) 470 | ) 471 | if prev2 == nil then 472 | prev2 = { x, y, prefferedColor, texU, texV } 473 | return 474 | end 475 | 476 | ---@type table 477 | local batchTable = batch.batchTable 478 | 479 | local len = batchTable[0] 480 | batchTable[len + 1] = prev1[1] 481 | batchTable[len + 2] = prev1[2] 482 | batchTable[len + 3] = prev1[4] 483 | batchTable[len + 4] = prev1[3] 484 | 485 | batchTable[len + 5] = prev2[1] 486 | batchTable[len + 6] = prev2[2] 487 | batchTable[len + 7] = prev2[3] 488 | 489 | batchTable[len + 8] = x 490 | batchTable[len + 9] = y 491 | batchTable[len + 10] = prefferedColor 492 | 493 | batchTable[len + 11] = prev1[5] 494 | batchTable[len + 12] = prev1[6] 495 | batchTable[len + 13] = prev2[4] 496 | batchTable[len + 14] = prev2[5] 497 | batchTable[len + 15] = texU 498 | batchTable[len + 16] = texV 499 | 500 | batchTable[len + 17] = batch.getDrawCell() 501 | 502 | batchTable[0] = len + 17 503 | 504 | prev2[1] = x 505 | prev2[2] = y 506 | prev2[3] = prefferedColor 507 | prev2[4] = texU 508 | prev2[5] = texV 509 | end 510 | 511 | local generateSingleMesh = roundedBoxes.generateSingleMesh 512 | 513 | --- Adds rounded box to batched queue 514 | ---@param radius number 515 | ---@param x number 516 | ---@param y number 517 | ---@param w number 518 | ---@param colors Color[] 519 | ---@param leftTop? boolean 520 | ---@param rightTop? boolean 521 | ---@param rightBottom? boolean 522 | ---@param leftBottom? boolean 523 | ---@param u1 number 524 | ---@param v1 number 525 | ---@param u2 number 526 | ---@param v2 number 527 | ---@param curviness number? 528 | ---@private Internal variable. Not meant to use outside 529 | function roundedBoxes.roundedBoxExBatched(radius, x, y, w, h, colors, leftTop, rightTop, rightBottom, leftBottom, 530 | u1, v1, u2, v2, curviness) 531 | prev1 = nil 532 | prev2 = nil 533 | generateSingleMesh(createVertex, nil, radius, x, y, x + w, y + h, leftTop, rightTop, rightBottom, leftBottom, 534 | colors, u1, v1, u2, v2, curviness) 535 | end 536 | end 537 | 538 | do 539 | local roundedBoxExSingle = roundedBoxes.roundedBoxExSingle 540 | local roundedBoxExBatched = roundedBoxes.roundedBoxExBatched 541 | 542 | local batch = paint.batch 543 | local getColorTable = paint.getColorTable 544 | 545 | -- Identical to roundedBox other than that it allows you to specify specific corners to be rounded. 546 | -- For brevity, arguments duplicated from roundedBox are not repeated here. 547 | ---@param radius number # radius of the rounded corners 548 | ---@param x number #start X position of rounded box (upper left corner) 549 | ---@param y number #start X position of rounded box (upper left corner) 550 | ---@param w number #width of rounded box 551 | ---@param h number #height of rounded box 552 | ---@param colors gradients #colors of rounded box. Either a table of Colors, or a single Color. 553 | ---@param material? IMaterial #Either a Material, or nil. Default: vgui/white 554 | ---@param u1 number #The texture U coordinate of the Top-Left corner of the rounded box. 555 | ---@param v1 number #The texture V coordinate of the Top-Left corner of the rounded box. 556 | ---@param u2 number #The texture U coordinate of the Bottom-Right corner of the rounded box. 557 | ---@param v2 number #The texture V coordinate of the Bottom-Right corner of the rounded box. 558 | ---@param curviness number? Curviness of rounded box. Default is 2. Makes rounded box behave as with formula ``x^curviness+y^curviness=radius^curviness`` (this is circle formula btw. Rounded boxes are superellipses) 559 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors : gradients, material? : IMaterial) 560 | ---@param leftTop? boolean 561 | ---@param rightTop? boolean 562 | ---@param rightBottom? boolean 563 | ---@param leftBottom? boolean 564 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors : gradients, leftTop? : boolean, rightTop? : boolean, rightBottom? : boolean, leftBottom? : boolean, material? : IMaterial) 565 | function roundedBoxes.roundedBoxEx(radius, x, y, w, h, colors, leftTop, rightTop, rightBottom, leftBottom, material, 566 | u1, v1, u2, v2, curviness) 567 | if colors[4] == nil then 568 | ---@cast colors Color 569 | ---@diagnostic disable-next-line: cast-local-type 570 | colors = getColorTable(4, colors) 571 | end 572 | 573 | if u1 == nil then 574 | u1, v1, u2, v2 = 0, 0, 1, 1 575 | end 576 | 577 | curviness = curviness or 2 578 | 579 | if radius == 0 then 580 | leftTop, rightTop, rightBottom, leftBottom = false, false, false, false 581 | end 582 | 583 | material = material or paint.defaultMaterial 584 | 585 | if batch.batching then 586 | roundedBoxExBatched(radius, x, y, w, h, colors, leftTop, rightTop, rightBottom, leftBottom, u1, v1, u2, v2, 587 | curviness) 588 | else 589 | roundedBoxExSingle(radius, x, y, w, h, colors, leftTop, rightTop, rightBottom, leftBottom, material, u1, v1, 590 | u2, v2, curviness) 591 | end 592 | end 593 | 594 | local roundedBoxEx = roundedBoxes.roundedBoxEx 595 | 596 | ---Draws a rounded box with the specified parameters. 597 | ---@param radius number # radius of the rounded corners 598 | ---@param x number #start X position of rounded box (upper left corner) 599 | ---@param y number #start X position of rounded box (upper left corner) 600 | ---@param w number #width of rounded box 601 | ---@param h number #height of rounded box 602 | ---@param colors gradients #colors of rounded box. Either a table of Colors, or a single Color. 603 | ---@param material? IMaterial #Either a Material, or nil. Default: vgui/white 604 | ---@param u1 number #The texture U coordinate of the Top-Left corner of the rounded box. 605 | ---@param v1 number #The texture V coordinate of the Top-Left corner of the rounded box. 606 | ---@param u2 number #The texture U coordinate of the Bottom-Right corner of the rounded box. 607 | ---@param v2 number #The texture V coordinate of the Bottom-Right corner of the rounded box. 608 | ---@param curviness number? Curviness of rounded box. Default is 2. Makes rounded box behave as with formula ``x^curviness+y^curviness=radius^curviness`` (this is circle formula btw. Rounded boxes are superellipses) 609 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors : gradients, material? : IMaterial) 610 | ---@overload fun(radius : number, x : number, y : number, w : number, h : number, colors : gradients, material? : IMaterial, _ : nil, _ : nil, _: nil, _: nil, curviness : number) 611 | function roundedBoxes.roundedBox(radius, x, y, w, h, colors, material, u1, v1, u2, v2, curviness) 612 | roundedBoxEx(radius, x, y, w, h, colors, true, true, true, true, material, u1, v1, u2, v2, curviness) 613 | end 614 | 615 | roundedBoxes.drawRoundedBox = roundedBoxes.roundedBox 616 | roundedBoxes.drawRoundedBoxEx = roundedBoxes.roundedBoxEx 617 | end 618 | 619 | paint.roundedBoxes = roundedBoxes 620 | --------------------------------------------------------------------------------