--[[ | This file was obtained through the combined efforts | of Madbluntz & Plymouth Antiquarian Society. | | Credits: lifestorm, Gregory Wayne Rossel JR., | Maloy, DrPepper10 @ RIP, Atle! | | Visit for more: https://plymouth.thetwilightzone.ru/ --]] if SERVER then return false end local _R = debug.getregistry() if _R.Circles then return _R.Circles end local CIRCLE = {} CIRCLE.__index = CIRCLE CIRCLE_FILLED = 0 CIRCLE_OUTLINED = 1 CIRCLE_BLURRED = 2 local New do local err_number = "bad argument #%i to 'New' (number expected, got %s)" function New(t, r, x, y, ...) assert(isnumber(t), string.format(err_number, 1, type(t))) assert(isnumber(r), string.format(err_number, 2, type(r))) assert(isnumber(x), string.format(err_number, 3, type(x))) assert(isnumber(y), string.format(err_number, 4, type(y))) local circle = setmetatable({}, CIRCLE) circle:SetType(t) circle:SetRadius(r) circle:SetX(x) circle:SetY(y) circle:SetVertices({Count = 0}) if t == CIRCLE_OUTLINED then local outline_width = ... assert(outline_width == nil or isnumber(outline_width), string.format(err_number, 5, type(outline_width))) circle:SetOutlineWidth(outline_width) elseif t == CIRCLE_BLURRED then local blur_layers, blur_density = ... assert(blur_layers == nil or isnumber(blur_layers), string.format(err_number, 5, type(blur_layers))) assert(blur_density == nil or isnumber(blur_density), string.format(err_number, 6, type(blur_density))) circle:SetBlurLayers(blur_layers) circle:SetBlurDensity(blur_density) end return circle end end local RotateVertices do local err_table = "bad argument #1 to 'RotateVertices' (table expected, got %s)" local err_number = "bad argument #%i to 'RotateVertices' (number expected, got %s)" function RotateVertices(vertices, ox, oy, rotation, rotate_uv) assert(istable(vertices), string.format(err_table, type(vertices))) assert(isnumber(ox), string.format(err_number, 2, type(ox))) assert(isnumber(oy), string.format(err_number, 3, type(oy))) assert(isnumber(rotation), string.format(err_number, 4, type(rotation))) local rotation = math.rad(rotation) local c = math.cos(rotation) local s = math.sin(rotation) for i = 1, vertices.Count or #vertices do local vertex = vertices[i] local vx, vy = vertex.x, vertex.y vx = vx - ox vy = vy - oy vertex.x = ox + (vx * c - vy * s) vertex.y = oy + (vx * s + vy * c) if rotate_uv == false then local u, v = vertex.u, vertex.v u, v = u - 0.5, v - 0.5 vertex.u = 0.5 + (u * c - v * s) vertex.v = 0.5 + (u * s + v * c) end end end end local CalculateVertices do local err_number = "bad argument #%i to 'CalculateVertices' (number expected, got %s)" function CalculateVertices(x, y, radius, rotation, start_angle, end_angle, distance, rotate_uv) assert(isnumber(x), string.format(err_number, 1, type(x))) assert(isnumber(y), string.format(err_number, 2, type(y))) assert(isnumber(radius), string.format(err_number, 3, type(radius))) assert(isnumber(rotation), string.format(err_number, 4, type(rotation))) assert(isnumber(start_angle), string.format(err_number, 5, type(start_angle))) assert(isnumber(end_angle), string.format(err_number, 6, type(end_angle))) assert(isnumber(distance), string.format(err_number, 7, type(distance))) local vertices = {Count = 0} local step = distance / radius local rad_start_angle = math.rad(start_angle) local rad_end_angle = math.rad(end_angle) local rad_rotation = math.rad(rotation) for a = rad_start_angle, rad_end_angle + step, step do a = math.min(a, rad_end_angle) local c = math.cos(a + rad_rotation) local s = math.sin(a + rad_rotation) local vertex = { x = x + c * radius, y = y + s * radius, } if rotate_uv == false then vertex.u = 0.5 + math.cos(a) / 2 vertex.v = 0.5 + math.sin(a) / 2 else vertex.u = 0.5 + c / 2 vertex.v = 0.5 + s / 2 end vertices.Count = vertices.Count + 1 vertices[vertices.Count] = vertex end if end_angle - start_angle ~= 360 then table.insert(vertices, 1, { x = x, y = y, u = 0.5, v = 0.5, }) vertices.Count = vertices.Count + 1 else table.remove(vertices) vertices.Count = vertices.Count - 1 end return vertices end end function CIRCLE:__tostring() return string.format("Circle: %p", self) end function CIRCLE:Copy() return table.Copy(self) end function CIRCLE:IsValid() return ( not self.m_Dirty and self.m_Vertices.Count >= 3 and self.m_Radius >= 1 and self.m_Distance >= 1 ) end function CIRCLE:Calculate() local rotate_uv = self.m_RotateMaterial local radius = self.m_Radius local x, y = self.m_X, self.m_Y local rotation = self.m_Rotation local start_angle = self.m_StartAngle local end_angle = self.m_EndAngle local distance = self.m_Distance assert(radius >= 1, string.format("circle radius should be >= 1 (%.4f)", radius)) assert(distance >= 1, string.format("circle distance should be >= 1 (%.4f)", distance)) self:SetVertices(CalculateVertices(x, y, radius, rotation, start_angle, end_angle, distance, rotate_uv)) if self.m_Type == CIRCLE_OUTLINED then local inner = self.m_ChildCircle or self:Copy() local inner_r = radius - self.m_OutlineWidth inner:SetType(CIRCLE_FILLED) inner:SetPos(x, y) inner:SetRadius(inner_r) inner:SetRotation(rotation) inner:SetAngles(start_angle, end_angle) inner:SetDistance(distance) inner:SetColor(false) inner:SetMaterial(false) inner:SetShouldRender(inner_r >= 1) inner:SetDirty(inner.m_ShouldRender) self:SetShouldRender(inner_r < radius) self:SetChildCircle(inner) elseif self.m_ChildCircle then self.m_ChildCircle = nil end self:SetDirty(false) return self end do local blur = Material("pp/blurscreen") function CIRCLE:__call() if self.m_Dirty then self:Calculate() end if not self:IsValid() then return false end if not self.m_ShouldRender then return false end do local col, mat = self.m_Color, self.m_Material if IsColor(col) then if col.a <= 0 then return end surface.SetDrawColor(col.r, col.g, col.b, col.a) end if mat == true then draw.NoTexture() elseif TypeID(mat) == TYPE_MATERIAL then surface.SetMaterial(mat) end end if self.m_Type == CIRCLE_OUTLINED then render.ClearStencil() render.SetStencilEnable(true) render.SetStencilTestMask(0xFF) render.SetStencilWriteMask(0xFF) render.SetStencilReferenceValue(0x01) render.SetStencilCompareFunction(STENCIL_NEVER) render.SetStencilFailOperation(STENCIL_REPLACE) render.SetStencilZFailOperation(STENCIL_REPLACE) self.m_ChildCircle() render.SetStencilCompareFunction(STENCIL_GREATER) render.SetStencilFailOperation(STENCIL_KEEP) render.SetStencilZFailOperation(STENCIL_KEEP) surface.DrawPoly(self.m_Vertices) render.SetStencilEnable(false) elseif self.m_Type == CIRCLE_BLURRED then render.ClearStencil() render.SetStencilEnable(true) render.SetStencilTestMask(0xFF) render.SetStencilWriteMask(0xFF) render.SetStencilReferenceValue(0x01) render.SetStencilCompareFunction(STENCIL_NEVER) render.SetStencilFailOperation(STENCIL_REPLACE) render.SetStencilZFailOperation(STENCIL_REPLACE) surface.DrawPoly(self.m_Vertices) render.SetStencilCompareFunction(STENCIL_LESSEQUAL) render.SetStencilFailOperation(STENCIL_KEEP) render.SetStencilZFailOperation(STENCIL_KEEP) surface.SetMaterial(blur) local sw, sh = ScrW(), ScrH() for i = 1, self.m_BlurLayers do blur:SetFloat("$blur", (i / self.m_BlurLayers) * self.m_BlurDensity) blur:Recompute() render.UpdateScreenEffectTexture() surface.DrawTexturedRect(0, 0, sw, sh) end render.SetStencilEnable(false) else surface.DrawPoly(self.m_Vertices) end return true end CIRCLE.Draw = CIRCLE.__call end do local err_number = "bad argument #%i to 'Translate' (number expected, got %s)" function CIRCLE:Translate(x, y) assert(isnumber(x), string.format(err_number, 1, type(x))) assert(isnumber(y), string.format(err_number, 2, type(y))) if x ~= 0 or y ~= 0 then self.m_X = self.m_X + x self.m_Y = self.m_Y + y if self:IsValid() then for i = 1, self.m_Vertices.Count do local vertex = self.m_Vertices[i] vertex.x = vertex.x + x vertex.y = vertex.y + y end if self.m_Type == CIRCLE_OUTLINED and self.m_ChildCircle then self.m_ChildCircle:Translate(x, y) end end end return self end end do local err_number = "bad argument #1 to 'Scale' (number expected, got %s)" function CIRCLE:Scale(scale) assert(isnumber(scale), string.format(err_number, type(scale))) if scale ~= 1 then self.m_Radius = self.m_Radius * scale if self:IsValid() then local x, y = self.m_X, self.m_Y for i = 1, self.m_Vertices.Count do local vertex = self.m_Vertices[i] vertex.x = x + (vertex.x - x) * scale vertex.y = y + (vertex.y - y) * scale end if self.m_Type == CIRCLE_OUTLINED and self.m_ChildCircle then self.m_ChildCircle:Scale(scale) end end end return self end end do local err_number = "bad argument #1 to 'Rotate' (number expected, got %s)" function CIRCLE:Rotate(rotation) assert(isnumber(rotation), string.format(err_number, type(rotation))) if rotation ~= 0 then self.m_Rotation = self.m_Rotation + rotation if self:IsValid() then local x, y = self.m_X, self.m_Y local vertices = self.m_Vertices local rotate_uv = self.m_RotateMaterial RotateVertices(vertices, x, y, rotation, rotate_uv) if self.m_Type == CIRCLE_OUTLINED and self.m_ChildCircle then self.m_ChildCircle:Rotate(rotation) end end end return self end end do local function AccessorFunc(name, default, dirty, callback) local varname = "m_" .. name CIRCLE["Get" .. name] = function(self) return self[varname] end CIRCLE["Set" .. name] = function(self, value) if default ~= nil and value == nil then value = default end if self[varname] ~= value then if dirty then self[dirty] = true end if callback ~= nil then local new = callback(self, self[varname], value) value = new ~= nil and new or value end self[varname] = value end return self end CIRCLE[varname] = default end local function OffsetVerticesX(circle, old, new) circle:Translate(new - old, 0) if circle.m_Type == CIRCLE_OUTLINED and circle.m_ChildCircle then circle.m_ChildCircle:Translate(new - old, 0) end end local function OffsetVerticesY(circle, old, new) circle:Translate(0, new - old) if circle.m_Type == CIRCLE_OUTLINED and circle.m_ChildCircle then circle.m_ChildCircle:Translate(0, new - old) end end local function UpdateRotation(circle, old, new) circle:Rotate(new - old) if circle.m_Type == CIRCLE_OUTLINED and circle.m_ChildCircle then circle.m_ChildCircle:Rotate(new - old) end end -- These are set internally. Only use them if you know what you're doing. AccessorFunc("Dirty", true) AccessorFunc("Vertices", false) AccessorFunc("ChildCircle", false) AccessorFunc("ShouldRender", true) AccessorFunc("Color", false) -- The colour you want the circle to be. If set to false then surface.SetDrawColor's can be used. AccessorFunc("Material", false) -- The material you want the circle to render. If set to false then surface.SetMaterial can be used. AccessorFunc("RotateMaterial", true) -- Sets whether or not the circle's UV points should be rotated with the vertices. AccessorFunc("Type", CIRCLE_FILLED, "m_Dirty") -- The circle's type. AccessorFunc("X", 0, false, OffsetVerticesX) -- The circle's X position relative to the top left of the screen. AccessorFunc("Y", 0, false, OffsetVerticesY) -- The circle's Y position relative to the top left of the screen. AccessorFunc("Radius", 8, "m_Dirty") -- The circle's radius. AccessorFunc("Rotation", 0, false, UpdateRotation) -- The circle's rotation, measured in degrees. AccessorFunc("StartAngle", 0, "m_Dirty") -- The circle's start angle, measured in degrees. AccessorFunc("EndAngle", 360, "m_Dirty") -- The circle's end angle, measured in degrees. AccessorFunc("Distance", 10, "m_Dirty") -- The maximum distance between each of the circle's vertices. This should typically be used for large circles in 3D2D. AccessorFunc("BlurLayers", 3) -- The circle's blur layers if Type is set to CIRCLE_BLURRED. AccessorFunc("BlurDensity", 2) -- The circle's blur density if Type is set to CIRCLE_BLURRED. AccessorFunc("OutlineWidth", 10, "m_Dirty") -- The circle's outline width if Type is set to CIRCLE_OUTLINED. function CIRCLE:SetPos(x, y) x = tonumber(x) or self.m_X y = tonumber(y) or self.m_Y if self:IsValid() then self:Translate(x - self.m_X, y - self.m_Y) else self.m_X = x self.m_Y = y end return self end function CIRCLE:SetAngles(s, e) s = tonumber(s) or self.m_StartAngle e = tonumber(e) or self.m_EndAngle self:SetDirty(self.m_Dirty or s ~= self.m_StartAngle or e ~= self.m_EndAngle) self.m_StartAngle = s self.m_EndAngle = e return self end function CIRCLE:GetPos() return self.m_X, self.m_Y end function CIRCLE:GetAngles() return self.m_StartAngle, self.m_EndAngle end end _R.Circles = { _MT = CIRCLE, New = New, RotateVertices = RotateVertices, CalculateVertices = CalculateVertices, } return _R.Circles