--!optimize 2
--!native

-- Services.
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")

-- Attempt to find the plugin object.
local plugin = script:FindFirstAncestorOfClass("Plugin")

-- Network packets.
local packets
if not plugin then -- Game context.
	packets = CollectionService:GetTagged("Packets")[1]
	if packets then packets = require(packets) end
end

-- Signal library.
local Signal
if plugin then
	for _, instance in plugin:GetDescendants() do
		if instance:HasTag("Signal") then
			Signal = require(instance)
			if type(Signal) == "table" and Signal.new then Signal = Signal.new end
			break
		end
	end
else -- Game context.
	Signal = CollectionService:GetTagged("Signal")[1]
	if Signal then
		Signal = require(Signal)
		if type(Signal) == "table" and Signal.new then Signal = Signal.new end
	end
end

-- Time function.
local getTime = if plugin then os.clock else time

-- Directories.
local root = script.Parent
local data = root.Data
local utilities = root.Utilities

-- Easing functions.
local easingFunctions = require(utilities.EasingFunctions)

-- Update functions constructor.
local createUpdateFunctions = require(utilities.UpdateFunctions)

-- Options defaults.
local defaults = require(data.Defaults)

-- Types.
local types = require(data.Types)
type InternalTween = types.InternalTween

-- Active tweens collection.
local tweens: {InternalTween} = table.create(script.Parent:GetAttribute("TweenPreallocations"))
local tweensAmount = 0

-- Tween functions.
local function stop(tween: InternalTween)
	-- Set playing state.
	tween.Playing = nil

	-- Tween removal.
	local index = tween.Index
	if index == tweensAmount then
		tweens[tweensAmount] = nil
	else
		local last = tweens[tweensAmount]
		last.Index = index
		tweens[index] = last
		tweens[tweensAmount] = nil
	end
	tweensAmount -= 1

	-- Set time markers.
	tween.StopTime = getTime()
end
local function destroy(tween: InternalTween)
	-- Ensure no playback.
	if tween.Playing then
		-- Tween removal.
		local index = tween.Index
		if index == tweensAmount then
			tweens[tweensAmount] = nil
		else
			local last = tweens[tweensAmount]
			last.Index = index
			tweens[index] = last
			tweens[tweensAmount] = nil
		end
		tweensAmount -= 1

		-- Fire stopped signal.
		if Signal then tween.Stopped:Fire() end
	end

	-- Destroy signals.
	if Signal then
		tween.Updated:Destroy()
		tween.Started:Destroy()
		tween.Stopped:Destroy()
		tween.Completed:Destroy()
	end
	-- Unlink methods.
	setmetatable(tween, nil)
end

local function start(tween: InternalTween, startOverride)
	-- Set playing state.
	tween.Playing = true

	-- Handle cross tween conflicts.
	local instance = tween.Instance
	local values = tween.Values

	for _, otherTween in tweens do
		if otherTween.Instance == instance then
			local otherValues = otherTween.Values
			for value in values do
				if otherValues[value] ~= nil then
					if otherTween.Playing then
						stop(otherTween)
						if otherTween.Stopped then otherTween.Stopped:Fire() end
					end
					break
				end
			end
		end
	end

	-- Set time markers.
	local currentTime = getTime()
	local stopDuration = currentTime - tween.StopTime
	if not startOverride then tween.StartTime += stopDuration end
	if tween.Interval then tween.LastUpdate = currentTime end
	if tween.WaitTime then tween.WaitTime += stopDuration end
	tween.StopTime = nil

	-- Add tween.
	tweensAmount += 1
	tweens[tweensAmount] = tween
	tween.Index = tweensAmount
end

-- Setup tween processing.
do
	local connection = (if plugin then RunService.Heartbeat else RunService.PreSimulation):Connect(function()
		if tweensAmount == 0 then return end

		local currentTime = getTime()
		for _, tween in tweens do
			-- Ensure desired interval (specified in FPS).
			if tween.Interval then
				if currentTime - tween.LastUpdate < tween.Interval then continue end
				tween.LastUpdate = currentTime
			end

			-- Account for wait.
			if tween.WaitTime then
				if currentTime - tween.WaitTime < tween.DelayTime then continue end
				tween.WaitTime = nil
				tween.StartTime = currentTime
			end

			-- Calculate progress and update values.
			local alpha = (currentTime - tween.StartTime)*tween.InverseTweenTime

			local phaseCompleted = alpha >= 1

			if phaseCompleted then
				if tween.Reverse then
					alpha = 0
					for _, update in tween.UpdateFunctions do update(0) end
				else
					alpha = 1
					for _, update in tween.UpdateFunctions do update(1) end
				end
			else
				if tween.Reverse then alpha = 1 - alpha end -- Reverse alpha to match if going in reverse.

				local easedAlpha = tween.Ease(alpha)
				for _, update in tween.UpdateFunctions do update(easedAlpha) end
			end

			-- Save alpha.
			tween.Alpha = alpha

			-- Fire update signal.
			local updateSignal = tween.Updated
			if updateSignal then updateSignal:Fire() end

			-- Phase completion stuff.
			if phaseCompleted then
				if tween.Reverse then -- Already going in reverse. Means reversing is enabled, so no need to check for that.
					tween.Repetitions += 1
					if tween.RepeatCount == -1 or tween.Repetitions < tween.RepeatCount then
						if tween.DelayTime then tween.WaitTime = currentTime end
						tween.StartTime = currentTime
						tween.Reverse = nil
					else
						stop(tween)
						if updateSignal then -- If there's one signal, we know there are all of them.
							tween.Stopped:Fire()
							tween.Completed:Fire()
						end
					end
				else -- Not going in reverse.
					tween.Repetitions += 1
					if tween.Reverses then
						-- Reversing enabled.
						if tween.DelayTime then tween.WaitTime = currentTime end
						tween.Reverse = true
						tween.StartTime = currentTime
					else
						-- Reversing disabled.
						if tween.RepeatCount == -1 or tween.Repetitions < tween.RepeatCount then
							if tween.DelayTime then tween.WaitTime = currentTime end
							tween.StartTime = currentTime
						else
							stop(tween)
							if updateSignal then -- If there's one signal, we know there are all of them.
								tween.Stopped:Fire()
								tween.Completed:Fire()
							end
						end
					end
				end
			end
		end
	end)

	if plugin then
		-- On plugin unload, connections stay, unlike in games.
		-- Therefore, we manually erase connections.
		plugin.Unloading:Connect(function()
			connection:Disconnect()
		end)
	end
end

-- Tween class.
local Tween = {}
Tween.__index = Tween

Tween.Start = function(tween: InternalTween)
	if not tween.Playing then
		start(tween)
		if Signal then tween.Started:Fire() end
	end
	return tween
end
Tween.Stop = function(tween: InternalTween)
	if tween.Playing then
		stop(tween)
		if Signal then tween.Stopped:Fire() end
	end
	return tween
end
Tween.Reset = function(tween: InternalTween)
	if tween.Playing then
		stop(tween)
		if Signal then tween.Stopped:Fire() end
	end
	tween.WaitTime = nil
	tween.StopTime = 0
	tween.StartTime = 0
	tween.Repetitions = 0
	tween.Reverse = nil
	for _, update in tween.UpdateFunctions do update(0) end
	return tween
end
Tween.Destroy = destroy

-- Global tweens setup.
if packets then
	local requiredPackets = {
		"CreateTween",
		"StartTween",
		"StopTween",
		"ResetTween",
		"DestroyTween"
	}
	local tweenPackets = 0
	for _, name in requiredPackets do
		if packets[name] then tweenPackets += 1 end
	end
	if tweenPackets == #requiredPackets then
		requiredPackets, tweenPackets = nil -- Erase unnecessary data.

		local globalTweens = table.create(256) -- Preallocate the maximum amount of global tweens.

		local function serverDestroy(tween: InternalTween)
			-- Remove from collection.
			globalTweens[tween.Identifier] = nil

			-- Ensure no playback.
			if tween.Playing then
				-- Tween removal.
				local index = tween.Index
				if index == tweensAmount then
					tweens[tweensAmount] = nil
				else
					local last = tweens[tweensAmount]
					last.Index = index
					tweens[index] = last
					tweens[tweensAmount] = nil
				end
				tweensAmount -= 1
			end

			-- Unlink methods.
			setmetatable(tween, nil)
		end

		packets.CreateTween.OnClientEvent:Connect(function(identifier, instance, values, options)
			-- Argument validation.
			if not instance then error("Could not find instance on client.") end

			-- Handle values.
			local updateFunctions = createUpdateFunctions(instance, values)

			-- Handle options.
			local easingFunction = easingFunctions[options[2] or defaults.EasingStyle]
			if type(easingFunction) == "table" then
				easingFunction = easingFunction[options[3] or defaults.EasingDirection]
			end

			-- Create tween.
			local tween = {
				-- Identifier.
				Identifier = identifier,

				-- Main arguments.
				Instance = instance,
				Values = values,

				-- Value functions.
				UpdateFunctions = updateFunctions,

				-- Options.
				InverseTweenTime = options[1] or defaults.InverseTime,
				Ease = easingFunction,
				DelayTime = options[4] or defaults.DelayTime,
				Reverses = options[5] or defaults.Reverses,
				RepeatCount = options[6] or defaults.RepeatCount,
				Interval = options[7] or defaults.Interval,

				-- Time markers.
				StartTime = 0,
				StopTime = 0,

				-- Trackers.
				Repetitions = 0
			}

			-- Store the tween with its given identifier.
			globalTweens[identifier] = tween

			-- Handle instance destroy.
			instance.Destroying:Once(function()
				serverDestroy(tween)
			end)
		end)

		packets.StartTween.OnClientEvent:Connect(function(identifier, fireTime, runTime, repetitions)
			local tween = globalTweens[identifier]
			if not tween then return end

			if repetitions then
				tween.Repetitions = repetitions
				if tween.Reverses then
					tween.Reverse = if repetitions%2 == 1 then true else nil
				end
			end
			if tween.StopTime then
				if runTime then
					tween.StartTime = getTime() - runTime - (workspace:GetServerTimeNow() - fireTime)
					start(tween, true)
				else
					start(tween)
				end
			end
		end)
		packets.StopTween.OnClientEvent:Connect(function(identifier)
			local tween = globalTweens[identifier]
			if not tween then return end

			if tween.Playing then stop(tween) end
		end)
		packets.ResetTween.OnClientEvent:Connect(function(identifier)
			local tween = globalTweens[identifier]
			if not tween then return end

			if tween.Playing then stop(tween) end
			for _, update in tween.UpdateFunctions do update(0) end
		end)
		packets.DestroyTween.OnClientEvent:Connect(function(identifier)
			local tween = globalTweens[identifier]
			if not tween then return end

			serverDestroy(tween)
		end)
	end
end

-- Local tween creation.
return function(instance, values, options)
	-- Argument validation.
	if typeof(instance) ~= "Instance" then error("Invalid instance.", 2) end
	if type(values) ~= "table" then error("Invalid values.", 2) end

	-- Handle values.
	local updateFunctions = createUpdateFunctions(instance, values)

	-- Handle options.
	local tweenTime = options.Time
	if type(tweenTime) ~= "number" then
		tweenTime = defaults.InverseTime
	elseif tweenTime <= 0 then
		tweenTime = 0
	else
		tweenTime = 1/tweenTime
	end

	local easingFunction = easingFunctions[options.EasingStyle] or easingFunctions[defaults.EasingStyle]
	if type(easingFunction) == "table" then
		easingFunction = easingFunction[options.EasingDirection] or easingFunction[defaults.EasingDirection]
	end

	local delayTime = options.DelayTime
	if type(delayTime) ~= "number" or delayTime <= 0 then
		delayTime = defaults.DelayTime
	end

	local reverses = options.Reverses
	if reverses == false then
		reverses = nil
	elseif reverses ~= true then
		reverses = defaults.Reverses
	end

	local repeatCount = options.RepeatCount
	if type(repeatCount) ~= "number" then
		repeatCount = defaults.RepeatCount
	elseif repeatCount < 0 then
		repeatCount = -1
	end

	local interval = options.FPS
	if type(interval) == "number" then
		interval = 1/interval
	end

	-- Create tween.
	local tween = setmetatable({
		-- Main arguments.
		Instance = instance,
		Values = values,

		-- Value functions.
		UpdateFunctions = updateFunctions,

		-- Options.
		InverseTweenTime = tweenTime,
		Ease = easingFunction,
		DelayTime = delayTime,
		Reverses = reverses,
		RepeatCount = repeatCount,
		Interval = interval,

		-- Time markers.
		StartTime = 0,
		StopTime = 0,

		-- Trackers.
		Repetitions = 0,
		Alpha = 0
	}, Tween)

	-- Signals.
	if Signal then
		tween.Updated = Signal()
		tween.Started = Signal()
		tween.Stopped = Signal()
		tween.Completed = Signal()
	end

	-- Handle instance destroy.
	instance.Destroying:Once(function()
		destroy(tween)
	end)

	-- Return tween object.
	return tween
end


--!optimize 2

-- Services.
local CollectionService = game:GetService("CollectionService")

-- Attempt to find the plugin object.
local plugin = script:FindFirstAncestorOfClass("Plugin")

-- Default defaults.
local defaults = {
	Time = 1,

	EasingStyle = "Linear",
	EasingDirection = "In",

	DelayTime = 0,
	Reverses = false,
	RepeatCount = 0,

	FPS = nil,

	Replicate = false
}

-- Merge user defaults.
local userDefaults
if plugin then
	for _, instance in plugin:GetDescendants() do
		if instance:HasTag("TweenDefaults") then
			userDefaults = require(instance)
			break
		end
	end
else
	userDefaults = CollectionService:GetTagged("TweenDefaults")[1]
	if userDefaults then userDefaults = require(userDefaults) end
end
if userDefaults and type(userDefaults) == "table" then
	local optionsList = {
		Time = true,

		EasingStyle = true,
		EasingDirection = true,

		DelayTime = true,
		Reverses = true,
		RepeatCount = true,

		FPS = true,

		Replicate = true
	}
	for key in userDefaults do
		if optionsList[key] then defaults[key] = userDefaults[key] end
	end
end

-- Remove false booleans.
for key, value in defaults do
	if value == false then defaults[key] = nil end
end

-- Remove DelayTime if 0.
if defaults.DelayTime == 0 then defaults.DelayTime = nil end

-- Invert time.
defaults.InverseTime = 1/defaults.Time
defaults.Time = nil

-- Convert FPS to interval.
local fps = defaults.FPS
if fps then
	defaults.Interval = 1/fps
	defaults.FPS = nil
end

-- Return final defaults.
return defaults


-- Signal types from Signal+.
export type Connection = {
	Connected: boolean,
	Disconnect: typeof(
		-- Removes the connection from the signal.
		-- The connection’s data remains.
		function(connection: Connection) end
	)
}
export type Signal<Parameters...> = {
	Connect: typeof(
		-- Connects a function.
		function(signal: Signal<Parameters...>, callback: (Parameters...) -> ()): Connection end
	),
	Once: typeof(
		-- Connects a function, then auto-disconnects after the first call.
		function(signal: Signal<Parameters...>, callback: (Parameters...) -> ()): Connection end
	),
	Wait: typeof(
		-- Yields the calling thread until the next fire.
		function(signal: Signal<Parameters...>): Parameters... end
	),

	Fire: typeof(
		-- Runs all connected functions, and resumes all waiting threads.
		function(signal: Signal<Parameters...>, ...: Parameters...) end
	),

	DisconnectAll: typeof(
		-- Erases all connections.
		-- Much faster than calling Disconnect on each.
		function(signal: Signal<Parameters...>) end
	),
	Destroy: typeof(
		-- Erases all connections and methods.
		-- To fully erase, also remove all references to the signal.
		function(signal: Signal<Parameters...>) end
	)
}

-- Internal server tween base.
export type InternalTween = {
	Index: number?,

	Playing: boolean,

	Updated: Signal?,
	Started: Signal?,
	Stopped: Signal?,
	Completed: Signal?,

	Instance: Instance,
	Values: Values,

	UpdateFunctions: {(alpha: number) -> ()},
	ResetFunctions: {() -> ()},

	InverseTweenTime: number,
	Ease: (alpha: number) -> (),
	RepeatCount: number,
	Reverses: boolean,
	Interval: number?,
	Table: {},

	StartTime: number,
	StopTime: number?,
	WaitTime: number?,
	LastUpdate: number?,

	Repetitions: number,
	Reverse: boolean,
	Alpha: number
}

-- Obligatory module return.
return nil


--!optimize 2
--!native

-- Services.
local CollectionService = game:GetService("CollectionService")
local RunService = game:GetService("RunService")
local Players = game:GetService("Players")

-- Get and verify network packets.
local packets = CollectionService:GetTagged("Packets")[1]
if packets then
	packets = require(packets)

	local requiredPackets = {
		"CreateTween",
		"StartTween",
		"StopTween",
		"ResetTween",
		"DestroyTween"
	}
	local missingPackets = {}
	for _, name in requiredPackets do
		if not packets[name] then table.insert(missingPackets, name) end
	end
	if #missingPackets > 0 then
		packets = nil
		if #missingPackets ~= #requiredPackets then
			for _, name in missingPackets do
				warn("Packet '"..name.."' is missing for global tweening to function.")
			end
		end
	end
end

-- Signal library.
local Signal = CollectionService:GetTagged("Signal")[1]
if Signal then
	Signal = require(Signal)
	if type(Signal) == "table" and Signal.new then Signal = Signal.new end
end

-- Directories.
local root = script.Parent
local data = root.Data
local utilities = root.Utilities

-- Easing functions.
local easingFunctions = require(utilities.EasingFunctions)

-- Updation functions constructor.
local createUpdateFunctions = require(utilities.UpdateFunctions)

-- Option defaults.
local defaults = require(data.Defaults)

-- Types.
local types = require(data.Types)

type Signal = types.Signal
type InternalTween = types.InternalTween & {
	Identifier: number,

	PlayerConnection: Signal
}

-- Localize packets.
if packets then
	createTweenPacket = packets.CreateTween
	startTweenPacket = packets.StartTween
	stopTweenPacket = packets.StopTween
	resetTweenPacket = packets.ResetTween
	destroyTweenPacket = packets.DestroyTween
end

-- Store all possible global tween identifiers.
local globalIdentifiers = table.create(256)
for identifier = 0, 255 do
	globalIdentifiers[identifier] = true
end

-- Active tweens collection.
local tweens: {InternalTween} = table.create(script.Parent:GetAttribute("TweenPreallocations"))
local tweensAmount = 0

-- Tween functions.
local function stop(tween: InternalTween)
	-- Set playing state.
	tween.Playing = nil

	-- Update instance values.
	local alpha = tween.Alpha
	if alpha ~= 1 and alpha ~= 0 then alpha = tween.Ease(alpha) end
	for _, update in tween.UpdateFunctions do update(alpha) end

	-- Tween removal.
	local index = tween.Index
	if index == tweensAmount then
		tweens[tweensAmount] = nil
	else
		local last = tweens[tweensAmount]
		last.Index = index
		tweens[index] = last
		tweens[tweensAmount] = nil
	end
	tweensAmount -= 1

	-- Set time markers.
	tween.StopTime = time()

	-- Fire stopped signal.
	if Signal then tween.Stopped:Fire() end
end
local function destroy(tween: InternalTween)
	-- Disconnect new player connection.
	local playerConnection = tween.PlayerConnection
	if playerConnection then playerConnection:Disconnect() end
	-- Disconnect destroy connection.
	tween.DestroyConnection:Disconnect()

	-- Free up identifier.
	globalIdentifiers[tween.Identifier] = true

	-- Ensure no playback.
	if tween.Playing then
		-- Update instance values.
		local alpha = tween.Alpha
		if alpha ~= 1 and alpha ~= 0 then alpha = tween.Ease(alpha) end
		for _, update in tween.UpdateFunctions do
			update(alpha)
		end

		-- Tween removal.
		local index = tween.Index
		if index == tweensAmount then
			tweens[tweensAmount] = nil
		else
			local last = tweens[tweensAmount]
			last.Index = index
			tweens[index] = last
			tweens[tweensAmount] = nil
		end
		tweensAmount -= 1

		-- Fire stopped signal.
		if Signal then tween.Stopped:Fire() end
	end

	-- Destroy signals.
	if Signal then
		tween.Updated:Destroy()
		tween.Started:Destroy()
		tween.Stopped:Destroy()
		tween.Completed:Destroy()
	end
	-- Unlink methods.
	setmetatable(tween, nil)
end

-- Setup tween processing.
RunService.PreSimulation:Connect(function()
	if tweensAmount == 0 then return end

	local currentTime = time()
	for _, tween in tweens do
		-- Ensure desired interval (specified in FPS).
		if tween.Interval then
			if currentTime - tween.LastUpdate < tween.Interval then continue end
			tween.LastUpdate = currentTime
		end

		-- Account for wait.
		if tween.WaitTime then
			if currentTime - tween.WaitTime < tween.DelayTime then continue end
			tween.WaitTime = nil
			tween.StartTime = currentTime
		end

		-- Calculate progress and update values.
		local alpha = (currentTime - tween.StartTime)*tween.InverseTweenTime
		local phaseCompleted = alpha >= 1

		if tween.Identifier then
			if phaseCompleted then
				alpha = if tween.Reverse then 0 else 1 -- Force the value to ensure it doesn't exceed the range 0-1.
			else
				if tween.Reverse then alpha = 1 - alpha end -- Reverse alpha to match if going in reverse.
			end
		else
			if phaseCompleted then
				if tween.Reverse then
					alpha = 0
					for _, update in tween.UpdateFunctions do update(0) end
				else
					alpha = 1
					for _, update in tween.UpdateFunctions do update(1) end
				end
			else
				if tween.Reverse then alpha = 1 - alpha end -- Reverse alpha to match if going in reverse.

				local easedAlpha = tween.Ease(alpha)
				for _, update in tween.UpdateFunctions do update(easedAlpha) end
			end
		end

		-- Save alpha.
		tween.Alpha = alpha

		-- Fire update signal.
		if Signal then
			tween.Alpha = alpha
			tween.Updated:Fire()
		end

		-- Phase completion stuff.
		if phaseCompleted then
			if tween.Reverse then -- Already going in reverse. Means reversing is enabled, so no need to check for that.
				tween.Repetitions += 1
				if tween.RepeatCount == -1 or tween.Repetitions < tween.RepeatCount then
					if tween.DelayTime then tween.WaitTime = currentTime end
					tween.StartTime = currentTime
					tween.Reverse = nil
				else
					stop(tween)
					if Signal then
						tween.Stopped:Fire()
						tween.Completed:Fire()
					end
				end
			else -- Not going in reverse.
				tween.Repetitions += 1
				if tween.Reverses then
					-- Reversing enabled.
					if tween.DelayTime then tween.WaitTime = currentTime end
					tween.Reverse = true
					tween.StartTime = currentTime
				else
					-- Reversing disabled.
					if tween.RepeatCount == -1 or tween.Repetitions < tween.RepeatCount then
						if tween.DelayTime then tween.WaitTime = currentTime end
						tween.StartTime = currentTime
					else
						stop(tween)
						if Signal then
							tween.Stopped:Fire()
							tween.Completed:Fire()
						end
					end
				end
			end
		end
	end
end)

-- Tween class.
local Tween = {}
Tween.__index = Tween

Tween.Start = function(tween: InternalTween)
	-- Verify playback state.
	if tween.Playing then return tween end

	-- Notify clients.
	for _, player in Players:GetPlayers() do
		startTweenPacket:FireClient(player, tween.Identifier)
	end

	-- Set playing state.
	tween.Playing = true

	-- Handle cross tween conflicts.
	local instance = tween.Instance
	local values = tween.Values

	for _, otherTween in tweens do
		if otherTween.Instance == instance then
			local otherValues = otherTween.Values
			for value in values do
				if otherValues[value] ~= nil then
					if otherTween.Playing then stop(otherTween) end
					break
				end
			end
		end
	end

	-- Set time markers.
	local currentTime = time()
	local stopDuration = currentTime - tween.StopTime
	tween.StartTime += stopDuration
	if tween.Interval then tween.LastUpdate = currentTime end
	if tween.WaitTime then tween.WaitTime += stopDuration end
	tween.StopTime = nil

	-- Add tween.
	tweensAmount += 1
	tweens[tweensAmount] = tween
	tween.Index = tweensAmount

	-- Fire started signal.
	if Signal then tween.Started:Fire() end

	-- Return self.
	return tween
end
Tween.Stop = function(tween: InternalTween)
	if tween.Playing then
		for _, player in Players:GetPlayers() do
			stopTweenPacket:FireClient(player, tween.Identifier)
		end

		stop(tween)
	end
	return tween
end
Tween.Reset = function(tween: InternalTween)
	for _, player in Players:GetPlayers() do
		resetTweenPacket:FireClient(player, tween.Identifier)
	end

	if tween.Playing then stop(tween) end
	tween.WaitTime = nil
	tween.StopTime = 0
	tween.StartTime = 0
	tween.Repetitions = 0
	tween.Reverse = nil
	for _, update in tween.UpdateFunctions do update(0) end

	return tween
end
Tween.Destroy = function(tween: InternalTween)
	for _, player in Players:GetPlayers() do
		destroyTweenPacket:FireClient(player, tween.Identifier)
	end

	destroy(tween)
end

-- Tween creation.
return function(instance, values, options)
	-- Argument validation.
	if typeof(instance) ~= "Instance" then error("Invalid instance.", 2) end
	if type(values) ~= "table" then error("Invalid values.", 2) end

	-- Handle networking stuff.
	local replicate = options.Replicate
	if replicate ~= true and replicate ~= false then replicate = defaults.Replicate end

	local globalIdentifier
	if packets then -- Valid networking setup.
		if replicate then
			globalIdentifier = next(globalIdentifiers)
			if not globalIdentifier then error("Maximum global tweens (256) exceeded. Destroy tweens to free up space.", 2) end
		end
	else -- Invalid networking setup.
		if replicate then
			error("Global tweening not available. Please set up necessary networking or disable replicate.")
		end
	end

	-- Handle values.
	local updateFunctions = createUpdateFunctions(instance, values)

	-- Set up tween.
	local tween

	if replicate then -- Global tween.
		-- Handle options.
		local optionsForClients = {}

		local tweenTime = options.Time
		if type(tweenTime) ~= "number" then
			tweenTime = defaults.InverseTime
		else
			if tweenTime <= 0 then
				tweenTime = 0
			else
				tweenTime = 1/tweenTime
			end
			if tweenTime ~= defaults.Time then optionsForClients[1] = tweenTime end
		end

		local easingFunction
		do
			local easingStyle = options.EasingStyle
			local easing = easingFunctions[easingStyle]
			if easing then
				if easingStyle ~= defaults.EasingStyle then optionsForClients[2] = easingStyle end
			else
				easing = easingFunctions[defaults.EasingStyle]
			end

			if type(easing) == "table" then
				local easingDirection = options.EasingDirection
				easingFunction = easing[easingDirection]
				if easingFunction then
					if easingDirection ~= defaults.EasingDirection then optionsForClients[3] = easingDirection end
				else
					easingFunction = easing[defaults.EasingDirection]
				end
			end
		end

		local delayTime = options.DelayTime
		if type(delayTime) ~= "number" or delayTime <= 0 then
			delayTime = defaults.DelayTime
		end
		if delayTime ~= defaults.DelayTime then optionsForClients[4] = delayTime end

		local reverses = options.Reverses
		if reverses == false then
			reverses = nil
			if defaults.Reverses then optionsForClients[5] = false end
		elseif reverses == true then
			if not defaults.Reverses then optionsForClients[5] = true end
		else
			reverses = defaults.Reverses
		end

		local repeatCount = options.RepeatCount
		if type(repeatCount) ~= "number" then
			repeatCount = defaults.RepeatCount
		else
			if repeatCount < 0 then repeatCount = -1 end
			if repeatCount ~= defaults.RepeatCount then optionsForClients[6] = repeatCount end
		end

		local interval = options.FPS
		if type(interval) == "number" then
			interval = 1/interval
			if interval ~= defaults.Interval then optionsForClients[7] = interval end
		else
			interval = defaults.Interval
		end

		-- Notify clients.
		for _, player in Players:GetPlayers() do
			createTweenPacket:FireClient(player, globalIdentifier, instance, values, optionsForClients)
		end

		-- Create tween.
		tween = setmetatable({
			-- Identifier.
			Identifier = globalIdentifier,

			-- New player handling.
			PlayerConnection = Players.PlayerAdded:Connect(function(player)
				createTweenPacket:FireClient(player, globalIdentifier, instance, values, optionsForClients)

				if tween.Playing then
					startTweenPacket:FireClient(
						player,
						globalIdentifier,

						workspace:GetServerTimeNow(),
						time() - tween.StartTime,
						tween.Repetitions
					)
				end
			end),

			-- Main arguments.
			Instance = instance,
			Values = values,

			-- Value functions.
			UpdateFunctions = updateFunctions,

			-- Options.
			InverseTweenTime = tweenTime,
			Ease = easingFunction,
			DelayTime = delayTime,
			Reverses = reverses,
			RepeatCount = repeatCount,
			Interval = interval,

			-- Time markers.
			StartTime = 0,
			StopTime = 0,

			-- Trackers.
			Repetitions = 0,
			Alpha = 0
		}, Tween)
	else -- Local tween.
		-- Handle options.
		local tweenTime = options.Time
		if type(tweenTime) ~= "number" then
			tweenTime = defaults.InverseTime
		elseif tweenTime <= 0 then
			tweenTime = 0
		else
			tweenTime = 1/tweenTime
		end

		local easingFunction = easingFunctions[options.EasingStyle] or easingFunctions[defaults.EasingStyle]
		if type(easingFunction) == "table" then
			easingFunction = easingFunction[options.EasingDirection] or easingFunction[defaults.EasingDirection]
		end

		local delayTime = options.DelayTime
		if type(delayTime) ~= "number" or delayTime <= 0 then
			delayTime = defaults.DelayTime
		end

		local reverses = options.Reverses
		if reverses == false then
			reverses = nil
		elseif reverses ~= true then
			reverses = defaults.Reverses
		end

		local repeatCount = options.RepeatCount
		if type(repeatCount) ~= "number" then
			repeatCount = defaults.RepeatCount
		elseif repeatCount < 0 then
			repeatCount = -1
		end

		local interval = options.FPS
		if type(interval) == "number" then
			interval = 1/interval
		end

		-- Create tween.
		tween = setmetatable({
			-- Main arguments.
			Instance = instance,
			Values = values,

			-- Value functions.
			UpdateFunctions = updateFunctions,

			-- Options.
			InverseTweenTime = tweenTime,
			Ease = easingFunction,
			DelayTime = delayTime,
			Reverses = reverses,
			RepeatCount = repeatCount,
			Interval = interval,

			-- Time markers.
			StartTime = 0,
			StopTime = 0,

			-- Trackers.
			Repetitions = 0,
			Alpha = 0
		}, Tween)
	end

	-- Signals.
	if Signal then
		tween.Updated = Signal()
		tween.Started = Signal()
		tween.Stopped = Signal()
		tween.Completed = Signal()
	end

	-- Handle instance destroy.
	instance.Destroying:Once(function()
		destroy(tween)
	end)

	-- Return tween object.
	return tween
end


--!optimize 2
--!native

-- Services.
local CollectionService = game:GetService("CollectionService")

-- Elastic constants.
local elasticInP = 0.3
local elasticInA = 1.0
local elasticInS = elasticInP*0.25
local elasticOutP = 0.3
local elasticOutA = 1.0
local elasticOutS = elasticOutP*0.25
local elasticInOutP = 0.45
local elasticInOutA = 1.0
local elasticInOutS = elasticInOutP*0.25

-- Back constants.
local backS = 1.70158
local backInOutS = backS*1.525

-- Linear easing.
local function linear(alpha)
	return alpha
end

-- Quad easing.
local function inQuad(alpha)
	return alpha*alpha
end
local function outQuad(alpha)
	return alpha*(2 - alpha)
end
local function inOutQuad(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*alpha*alpha
		else
		1 - 0.5*(2 - alpha)^2
end
local function outInQuad(alpha)
	return if alpha < 0.5 then
		0.5*outQuad(alpha*2)
		else
		0.5*inQuad((alpha - 0.5)*2) + 0.5
end

-- Cubic easing.
local function inCubic(alpha)
	return alpha*alpha*alpha
end
local function outCubic(alpha)
	return 1 + (alpha - 1)^3
end
local function inOutCubic(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*alpha*alpha*alpha
		else
		0.5*((alpha - 2)^3 + 2)
end
local function outInCubic(alpha)
	return if alpha < 0.5 then
		0.5*((2*alpha - 1)^3 + 1)
		else
		0.5*(2*alpha - 1)^3 + 0.5
end

-- Quart easing.
local function inQuart(alpha)
	return alpha*alpha*alpha*alpha
end
local function outQuart(alpha)
	return 1 - (alpha - 1)^4
end
local function inOutQuart(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*alpha*alpha*alpha*alpha
		else
		-0.5*((alpha - 2)^4 - 2)
end
local function outInQuart(alpha)
	return if alpha < 0.5 then
		0.5*(1 - (alpha*2 - 1)^4)
		else
		0.5*((2*alpha - 1)^4) + 0.5
end

-- Quint easing.
local function inQuint(alpha)
	return alpha*alpha*alpha*alpha*alpha
end
local function outQuint(alpha)
	return 1 + (alpha - 1)^5
end
local function inOutQuint(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*alpha*alpha*alpha*alpha*alpha
		else
		0.5*((alpha - 2)^5 + 2)
end
local function outInQuint(alpha)
	return if alpha < 0.5 then
		0.5*((2*alpha - 1)^5 + 1)
		else
		0.5*(2*alpha - 1)^5 + 0.5
end

-- Sine easing.
local function inSine(alpha)
	return 1 - math.cos(alpha*math.pi*0.5)
end
local function outSine(alpha)
	return math.sin(alpha*math.pi*0.5)
end
local function inOutSine(alpha)
	return 0.5*(1 - math.cos(math.pi*alpha))
end
local function outInSine(alpha)
	return if alpha < 0.5 then
		0.5*math.sin(alpha*math.pi)
		else
		0.5*(1 - math.cos((2*alpha - 1)*math.pi*0.5)) + 0.5
end

-- Exponential easing.
local function inExponential(alpha)
	return 2^(10*(alpha - 1))
end
local function outExponential(alpha)
	return if alpha == 1 then
		1
		else
		1 - 2^(-10*alpha)
end
local function inOutExponential(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*2^(10*(alpha - 1))
		else
		0.5*(2 - 2^(-10*(alpha - 1)))
end
local function outInExponential(alpha)
	return if alpha < 0.5 then
		0.5*outExponential(alpha*2)
		else
		0.5*(inExponential(2*alpha - 1) + 1)
end

-- Circular easing.
local function inCircular(alpha)
	return 1 - math.sqrt(1 - alpha*alpha)
end
local function outCircular(alpha)
	return math.sqrt(1 - (alpha - 1)^2)
end
local function inOutCircular(alpha)
	alpha *= 2
	return if alpha < 1 then
		0.5*(1 - math.sqrt(1 - alpha*alpha))
		else
		0.5*(math.sqrt(1 - (alpha - 2)^2) + 1)
end
local function outInCircular(alpha)
	return if alpha < 0.5 then
		0.5*outCircular(alpha*2)
		else
		0.5*(inCircular(2*alpha - 1) + 1)
end

-- Elastic easing.
local function inElastic(alpha)
	local s = if elasticInA < 1 then
		elasticInP*0.25
	else
		elasticInP/(2*math.pi)*math.asin(1/elasticInA)
	local t = alpha - 1
	return -elasticInA*2^(10*t)*math.sin((t - s)*(2*math.pi/elasticInP))
end
local function outElastic(alpha)
	local s = if elasticOutA < 1 then
		elasticOutP*0.25
	else
		elasticOutP/(2*math.pi)*math.asin(1/elasticOutA)
	return elasticOutA*2^(-10*alpha)*math.sin((alpha - s)*(2*math.pi/elasticOutP)) + 1
end
local function inOutElastic(alpha)
	local s = if elasticInOutA < 1 then
		elasticInOutP*0.25
	else
		elasticInOutP/(2*math.pi)*math.asin(1/elasticInOutA)
	alpha *= 2
	local t = alpha - 1
	return if alpha < 1 then
		-0.5*elasticInOutA*2^(10*t)*math.sin((t - s)*(2*math.pi/elasticInOutP))
		else
		0.5*elasticInOutA*2^(-10*t)*math.sin((t - s)*(2*math.pi/elasticInOutP)) + 1
end
local function outInElastic(alpha)
	return if alpha < 0.5 then
		0.5*outElastic(alpha*2)
		else
		0.5*(inElastic(2*alpha - 1) + 1)
end

-- Back easing.
local function inBack(alpha)
	return alpha*alpha*((backS + 1)*alpha - backS)
end
local function outBack(alpha)
	local a = alpha - 1
	return a*a*((backS + 1)*a + backS) + 1
end
local function inOutBack(alpha)
	alpha *= 2
	if alpha < 1 then
		return 0.5*alpha*alpha*((backInOutS + 1)*alpha - backInOutS)
	else
		alpha -= 2
		return 0.5*(alpha*alpha*((backInOutS + 1)*alpha + backInOutS) + 2)
	end
end
local function outInBack(alpha)
	return if alpha < 0.5 then
		0.5*outBack(alpha*2)
		else
		0.5*(inBack(2*alpha - 1) + 1)
end

-- Bounce easing.
local function outBounce(alpha)
	return if alpha < 1/2.75 then
		7.5625*alpha*alpha
		elseif alpha < 2/2.75 then
		7.5625*(alpha - 1.5/2.75)^2 + 0.75
		elseif alpha < 2.5/2.75 then
		7.5625*(alpha - 2.25/2.75)^2 + 0.9375
		else
		7.5625*(alpha - 2.625/2.75)^2 + 0.984375
end
local function inBounce(alpha)
	return 1 - outBounce(1 - alpha)
end
local function inOutBounce(alpha)
	return if alpha < 0.5 then
		0.5*inBounce(2*alpha)
		else
		0.5*outBounce(2*alpha - 1) + 0.5
end
local function outInBounce(alpha)
	return if alpha < 0.5 then
		0.5*outBounce(2*alpha)
		else
		0.5*inBounce(2*alpha - 1) + 0.5
end

-- Map built-in functions.
local easingFunctions = {
	Linear = linear,
	Quad = {
		In = inQuad,
		Out = outQuad,
		InOut = inOutQuad,
		OutIn = outInQuad,
	},
	Cubic = {
		In = inCubic,
		Out = outCubic,
		InOut = inOutCubic,
		OutIn = outInCubic,
	},
	Quart = {
		In = inQuart,
		Out = outQuart,
		InOut = inOutQuart,
		OutIn = outInQuart,
	},
	Quint = {
		In = inQuint,
		Out = outQuint,
		InOut = inOutQuint,
		OutIn = outInQuint,
	},
	Sine = {
		In = inSine,
		Out = outSine,
		InOut = inOutSine,
		OutIn = outInSine,
	},
	Exponential = {
		In = inExponential,
		Out = outExponential,
		InOut = inOutExponential,
		OutIn = outInExponential,
	},
	Circular = {
		In = inCircular,
		Out = outCircular,
		InOut = inOutCircular,
		OutIn = outInCircular,
	},
	Elastic = {
		In = inElastic,
		Out = outElastic,
		InOut = inOutElastic,
		OutIn = outInElastic,
	},
	Back = {
		In = inBack,
		Out = outBack,
		InOut = inOutBack,
		OutIn = outInBack,
	},
	Bounce = {
		In = inBounce,
		Out = outBounce,
		InOut = inOutBounce,
		OutIn = outInBounce,
	}
}

-- Handle user functions.
local userFunctions = CollectionService:GetTagged("EasingFunctions")[1]
if userFunctions then
	userFunctions = require(userFunctions)

	local directions = {
		In = true,
		Out = true,
		InOut = true,
		OutIn = true
	}

	for key, value in userFunctions do
		if type(key) ~= "string" then warn("Invalid easing function name:", key) continue end
		if easingFunctions[key] then warn("Easing function name '"..key.."' already used.") continue end

		local valueType = type(value)
		if valueType ~= "function" and valueType ~= "table" then warn("Missing function for '"..key.."' easing definition.") continue end

		if valueType == "table" then
			local missing
			for _, direction in directions do
				if type(value[direction]) ~= "function" then
					missing = true
					warn("Missing "..direction.." direction function for '"..key.."' easing.")
				end
			end
			if missing then continue end
		end

		easingFunctions[key] = value
	end
end

-- Return all functions.
return easingFunctions


--!optimize 2

-- Value functions.
local valueFunctions = require(script.Parent.ValueFunctions)

local normalValueFunctions = valueFunctions.Normal
local advancedValueFunctions = valueFunctions.Advanced

-- Property validation function.
local function checkProperty(instance, property)
	instance[property] = instance[property]
end

-- Update functions function.
return function(instance, values)
	-- Verify values and add update functions for each.
	local updateFunctions = table.create(#values)

	local index = 1
	for name, destination in values do
		if type(name) ~= "string" then error("Invalid value name of type '"..typeof(name).."'. It should be a string.", 3) end

		if name:sub(1, 1) == "@" then
			-- Attribute.
			local attributeName = name:sub(2)

			local original = instance:GetAttribute(attributeName)
			if original == nil then error("'"..attributeName.."' is not a valid attribute of '"..instance:GetFullName().."'.", 3) end

			local value = normalValueFunctions[typeof(destination)]
			if not value then error("'"..typeof(destination).."' data type is not supported.", 3) end
			value = value(original, destination)

			updateFunctions[index] = function(alpha)
				instance:SetAttribute(attributeName, value(alpha))
			end
		else
			-- Not attribute.
			if pcall(checkProperty, instance, name) then
				-- Validate.
				local value = instance[name]
				local dataType = if value == nil then "Instance" else typeof(value)
				value = normalValueFunctions[dataType]
				if not value then error("'"..dataType.."' data type is not supported.", 3) end

				-- Set up.
				local original = instance[name]
				value = value(original, destination)

				updateFunctions[index] = function(alpha)
					instance[name] = value(alpha)
				end
			else
				local value = advancedValueFunctions[name]
				if value then
					-- Hidden property.
					if not instance:IsA(value.Target) then error("'"..instance:GetFullName().."' doesn't support "..name..".", 3) end

					local original = value.Get(instance)
					local setValue = value.Set(instance, original, destination)
					updateFunctions[index] = function(alpha)
						setValue(alpha)
					end
				else
					error("'"..name.."' is not a valid property of '"..instance:GetFullName().."'.", 3)
				end
			end
		end

		index += 1
	end

	-- Return update functions.
	return updateFunctions
end


--!optimize 2
--!native

local function numberLerper(a, b)
	local difference = b - a

	return function(alpha)
		return a + difference*alpha
	end
end

local function normalize(x, y, z, w)
	local m = math.sqrt(x*x + y*y + z*z + w*w)
	if m == 0 then return 0, 0, 0, 1 end
	m = 1/m
	return x*m, y*m, z*m, w*m
end
local function normalizedQuaternionFromCFrame(cframe)
	local rightVector = cframe.RightVector
	local upVector = cframe.UpVector
	local lookVector = cframe.LookVector

	local rightX, rightY, rightZ = rightVector.X, rightVector.Y, rightVector.Z
	local upX, upY, upZ = upVector.X, upVector.Y, upVector.Z
	local leftX, leftY, leftZ = -lookVector.X, -lookVector.Y, -lookVector.Z

	local trace = rightX + upY + leftZ
	if trace > 0 then
		local s = math.sqrt(trace + 1)*2
		return normalize((upZ - leftY)/s, (leftX - rightZ)/s, (rightY - upX)/s, 0.25*s)
	end

	if rightX > upY and rightX > leftZ then
		local s = math.sqrt(1 + rightX - upY - leftZ)*2
		return normalize(0.25*s, (upX + rightY)/s, (leftX + rightZ)/s, (upZ - leftY)/s)
	elseif upY > leftZ then
		local s = math.sqrt(1 + upY - rightX - leftZ)*2
		return normalize((upX + rightY)/s, 0.25*s, (upZ + leftY)/s, (leftX - rightZ)/s)
	else
		local s = math.sqrt(1 + leftZ - rightX - upY)*2
		return normalize((leftX + rightZ)/s, (upZ + leftY)/s, 0.25*s, (rightY - upX)/s)
	end
end
local function cframeLerper(a, b)
	-- Position stuff.
	local ax, ay, az = a.X, a.Y, a.Z
	local bx, by, bz = b.X, b.Y, b.Z
	local dx, dy, dz = bx - ax, by - ay, bz - az

	-- Get normalized quaternions.
	local aqx, aqy, aqz, aqw = normalizedQuaternionFromCFrame(a)
	local bqx, bqy, bqz, bqw = normalizedQuaternionFromCFrame(b)

	-- Dot, and shortest path.
	local dot = aqx*bqx + aqy*bqy + aqz*bqz + aqw*bqw
	if dot < 0 then
		dot = -dot
		bqx, bqy, bqz, bqw = -bqx, -bqy, -bqz, -bqw
	end

	-- Slerp precompute.
	local dotSquared = dot*dot
	local A = 1.0904 - 3.2452*dot + 3.55645*dotSquared - 1.43519*dotSquared*dot
	local B = 0.848013 - 1.06021*dot + 0.215638*dotSquared

	-- Interpolation function.
	return function(alpha)
		if alpha == 0 then return a end
		if alpha == 1 then return b end

		-- Quaternion lerp.
		local shiftedAlpha = alpha - 0.5
		local s1 = (A*shiftedAlpha*shiftedAlpha + B)*(alpha*shiftedAlpha*(alpha - 1)) + alpha
		local s0 = 1 - s1

		local rx = s0*aqx + s1*bqx
		local ry = s0*aqy + s1*bqy
		local rz = s0*aqz + s1*bqz
		local rw = s0*aqw + s1*bqw

		local m = math.sqrt(rx*rx + ry*ry + rz*rz + rw*rw)
		if m ~= 0 then
			m = 1/m
			rx, ry, rz, rw = rx*m, ry*m, rz*m, rw*m
		end

		-- Build CFrame.
		return CFrame.new(
			-- Position.
			ax + dx*alpha,
			ay + dy*alpha,
			az + dz*alpha,
			-- Matrix.
			1 - 2*(ry*ry + rz*rz), 2*(rx*ry - rw*rz), 2*(rx*rz + rw*ry),
			2*(rx*ry + rw*rz), 1 - 2*(rx*rx + rz*rz), 2*(ry*rz - rw*rx),
			2*(rx*rz - rw*ry), 2*(ry*rz + rw*rx), 1 - 2*(rx*rx + ry*ry)
		)
	end
end

local function rgbToOklab(r, g, b)
	-- Gamma expansion.
	r = if r > 0.04045 then ((r + 0.055)/1.055)^2.4 else r/12.92
	g = if g > 0.04045 then ((g + 0.055)/1.055)^2.4 else g/12.92
	b = if b > 0.04045 then ((b + 0.055)/1.055)^2.4 else b/12.92

	-- LMS conversion.
	local l = 0.4122214708*r + 0.5363325363*g + 0.0514459929*b
	local m = 0.2119034982*r + 0.6806995451*g + 0.1073969566*b
	local s = 0.0883024619*r + 0.2817188376*g + 0.6299787005*b

	-- Cube roots.
	l = l^(1/3)
	m = m^(1/3)
	s = s^(1/3)

	-- Return l, a, b.
	return
		0.2104542553*l + 0.7936177850*m - 0.0040720468*s,
		1.9779984951*l - 2.4285922050*m + 0.4505937099*s,
		0.0259040371*l + 0.7827717662*m - 0.8086757660*s
end
local function oklabToRGB(l, a, b)
	-- Matrix transformation.
	local m = l - 0.1055613458*a - 0.0638541728*b
	local s = l - 0.0894841775*a - 1.2914855480*b
	local l = l + 0.3963377774*a + 0.2158037573*b

	-- Cube values.
	l = l*l*l
	m = m*m*m
	s = s*s*s

	-- RGB conversion.
	local r = 4.0767416621*l - 3.3077115913*m + 0.2309699292*s
	local g = -1.2684380046*l + 2.6097574011*m - 0.3413193965*s
	b = -0.0041960863*l - 0.7034186147*m + 1.7076147010*s

	-- Gamma compression.
	if r > 0.0031308 then
		r = 1.055*(r^0.4166666667) - 0.055
	else
		r = r*12.92
	end
	if r < 0 then r = 0 elseif r > 1 then r = 1 end

	if g > 0.0031308 then
		g = 1.055*(g^0.4166666667) - 0.055
	else
		g = g*12.92
	end
	if g < 0 then g = 0 elseif g > 1 then g = 1 end

	if b > 0.0031308 then
		b = 1.055*(b^0.4166666667) - 0.055
	else
		b = b*12.92
	end
	if b < 0 then b = 0 elseif b > 1 then b = 1 end

	-- Return r, g, b.
	return r, g, b
end
local function colorLerper(a, b)
	local l1, a1, b1 = rgbToOklab(a.R, a.G, a.B)
	local l2, a2, b2 = rgbToOklab(b.R, b.G, b.B)
	return function(alpha)
		if alpha == 0 then return a end
		if alpha == 1 then return b end

		return Color3.new(
			oklabToRGB(
				l1 + (l2 - l1)*alpha,
				a1 + (a2 - a1)*alpha,
				b1 + (b2 - b1)*alpha
			)
		)
	end
end

local function findKeypoints(keypoints, targetTime)
	-- Binary search for a specific time.
	local low = 1
	local high = #keypoints
	while low <= high do
		local mid = math.floor((low + high)*0.5)
		local midTime = keypoints[mid].Time
		if midTime < targetTime then
			low = mid + 1
		elseif midTime > targetTime then
			high = mid - 1
		else
			mid = keypoints[mid]
			return mid, mid
		end
	end
	return keypoints[high], keypoints[low]
end
local function getNumberKeypoint(sequence, time)
	local keypoints = sequence.Keypoints
	local a, b = findKeypoints(keypoints, time)

	local aTime = a.Time

	if aTime == b.Time then return a.Value, a.Envelope end

	local aValue = a.Value
	local aEnvelope = a.Envelope

	local alpha = (time - aTime)/(b.Time - aTime)
	return
		aValue + (b.Value - aValue)*alpha,
		aEnvelope + (b.Envelope - aEnvelope)*alpha
end
local function getColorKeypoint(sequence, time)
	local keypoints = sequence.Keypoints
	local a, b = findKeypoints(keypoints, time)

	local aTime = a.Time

	if aTime == b.Time then return a.Value end

	local alpha = (time - aTime)/(b.Time - aTime)
	return a.Value:Lerp(b.Value, alpha)
end

local function commonPrefix(a, b)
	local aLength = utf8.len(a)
	local bLength = utf8.len(b)
	local minLength = if aLength < bLength then aLength else bLength

	for index = 1, minLength, 1 do
		local aPosition = utf8.offset(a, index)
		local bPosition = utf8.offset(b, index)

		if utf8.codepoint(a, aPosition) ~= utf8.codepoint(b, bPosition) then
			return index - 1
		end
	end
	return minLength
end
local function commonSuffix(a, b, prefixLen)
	local aLength = utf8.len(a)
	local bLength = utf8.len(b)
	local maxLength = if aLength < bLength then aLength - prefixLen else bLength - prefixLen

	for index = 1, maxLength, 1 do
		local aPosition = utf8.offset(a, -index)
		local bPosition = utf8.offset(b, -index)

		if utf8.codepoint(a, aPosition) ~= utf8.codepoint(b, bPosition) then
			return index - 1
		end
	end
	return maxLength
end

return {
	Normal = {
		number = numberLerper,

		Vector3 = function(a, b)
			local aX = a.X
			local aY = a.Y
			local aZ = a.Z
			local xDifference = b.X - aX
			local yDifference = b.Y - aY
			local zDifference = b.Z - aZ

			return function(alpha)
				return vector.create(
					aX + xDifference*alpha,
					aY + yDifference*alpha,
					aZ + zDifference*alpha
				)
			end
		end,

		UDim2 = function(a, b)
			local aX = a.X
			local bX = b.X
			local aXScale = aX.Scale
			local aXOffset = aX.Offset
			local xScaleDifference = bX.Scale - aXScale
			local xOffsetDifference = bX.Offset - aXOffset

			local aY = a.Y
			local bY = b.Y
			local aYScale = aY.Scale
			local aYOffset = aY.Offset
			local yScaleDifference = bY.Scale - aYScale
			local yOffsetDifference = bY.Offset - aYOffset

			return function(alpha)
				return UDim2.new(
					aXScale + xScaleDifference*alpha,
					aXOffset + xOffsetDifference*alpha,

					aYScale + yScaleDifference*alpha,
					aYOffset + yOffsetDifference*alpha
				)
			end
		end,

		Vector2 = function(a, b)
			local aX = a.X
			local aY = a.Y
			local xDifference = b.X - aX
			local yDifference = b.Y - aY

			return function(alpha)
				return Vector2.new(
					aX + xDifference*alpha,
					aY + yDifference*alpha
				)
			end
		end,

		CFrame = cframeLerper,

		UDim = function(a, b)
			local aScale = a.Scale
			local aOffset = a.Offset
			local scaleDifference = b.Scale - aScale
			local offsetDifference = b.Offset - aOffset

			return function(alpha)
				return UDim.new(
					aScale + scaleDifference*alpha,
					aOffset + offsetDifference*alpha
				)
			end
		end,

		boolean = function(a, b)
			return function(alpha)
				return if alpha < 0.5 then a else b
			end
		end,

		string = function(a, b)
			local prefixLength = commonPrefix(a, b)
			local suffixLength = commonSuffix(a, b, prefixLength)

			local aMiddleStart = prefixLength + 1
			local aMiddleEnd = #a - suffixLength
			local bMiddleStart = prefixLength + 1
			local bMiddleEnd = #b - suffixLength

			local aMiddle = if aMiddleStart <= aMiddleEnd then a:sub(aMiddleStart, aMiddleEnd) else ""
			local bMiddle = if bMiddleStart <= bMiddleEnd then b:sub(bMiddleStart, bMiddleEnd) else ""

			local prefix = a:sub(1, prefixLength)
			local suffix = if suffixLength > 0 then a:sub(-suffixLength) else ""

			return function(alpha)
				if alpha == 0 then return a end
				if alpha == 1 then return b end

				local aVisible = math.floor(#aMiddle*(1 - alpha) + 0.5)
				local bVisible = math.floor(#bMiddle*alpha + 0.5)

				aVisible = math.min(#aMiddle, aVisible)
				bVisible = math.min(#bMiddle, bVisible)

				local result = prefix
				if aVisible > 0 then
					result ..= aMiddle:sub(1, aVisible)
				end
				if bVisible > 0 then
					result ..= bMiddle:sub(1, bVisible)
				end
				if suffixLength > 0 then
					result ..= suffix
				end
				return result
			end
		end,

		Color3 = colorLerper,

		NumberRange = function(a, b)
			local aMin = a.Min
			local aMax = a.Max
			local minimumDifference = b.Min - aMin
			local maximumDifference = b.Max - aMax

			return function(alpha)
				return NumberRange.new(
					aMin + minimumDifference*alpha,
					aMax + maximumDifference*alpha
				)
			end
		end,

		NumberSequence = function(a, b)
			-- Gather unique times.
			local uniqueTimes = {}
			for index, keypoint in a.Keypoints do
				uniqueTimes[index] = keypoint.Time
			end
			local previousAmount = #uniqueTimes
			for index, keypoint in b.Keypoints do
				uniqueTimes[index + previousAmount] = keypoint.Time
			end
			table.sort(uniqueTimes)

			-- Create and store lerpers.
			local valueLerpers = {}
			local envelopeLerpers = {}
			for index, time in uniqueTimes do
				local aValue, aEnvelope = getNumberKeypoint(a, time)
				local bValue, bEnvelope = getNumberKeypoint(b, time)
				valueLerpers[index] = numberLerper(aValue, bValue)
				envelopeLerpers[index] = numberLerper(aEnvelope, bEnvelope)
			end

			-- Final interpolator.
			return function(alpha)
				if alpha == 0 then return a end
				if alpha == 1 then return b end

				local keypoints = {}
				for index, time in uniqueTimes do
					keypoints[index] = NumberSequenceKeypoint.new(
						time,
						valueLerpers[index](alpha),
						envelopeLerpers[index](alpha)
					)
				end
				return NumberSequence.new(keypoints)
			end
		end,

		ColorSequence = function(a, b)
			-- Gather unique times.
			local uniqueTimes = {}
			for index, keypoint in a.Keypoints do
				uniqueTimes[index] = keypoint.Time
			end
			local previousAmount = #uniqueTimes
			for index, keypoint in b.Keypoints do
				uniqueTimes[index + previousAmount] = keypoint.Time
			end
			table.sort(uniqueTimes)

			-- Create and store lerpers.
			local lerpers = {}
			for index, time in uniqueTimes do
				lerpers[index] = colorLerper(getColorKeypoint(a, time), getColorKeypoint(b, time))
			end

			-- Final interpolator.
			return function(alpha)
				if alpha == 0 then return a end
				if alpha == 1 then return b end

				local keypoints = {}
				for index, time in uniqueTimes do
					keypoints[index] = ColorSequenceKeypoint.new(
						time,
						lerpers[index](alpha)
					)
				end
				return ColorSequence.new(keypoints)
			end
		end
	} :: {
		(a: any, b: any) -> ((alpha: number) -> any)
	},

	Advanced = {
		Pivot = {
			Target = "PVInstance",
			Set = function(instance, a, b)
				local lerp = cframeLerper(a, b)

				return function(alpha)
					instance:PivotTo(lerp(alpha))
				end
			end,
			Get = function(instance)
				return instance:GetPivot()
			end
		},

		Scale = {
			Target = "Model",
			Set = function(instance, a, b)
				local difference = b - a

				return function(alpha)
					instance:ScaleTo(a + difference*alpha)
				end
			end,
			Get = function(instance)
				return instance:GetScale()
			end
		}
	} :: {
		{
			Set: (instance: Instance, a: any, b: any) -> ((alpha: number) -> any),
			Get: (instance: Instance) -> any
		}
	}
}


--!strict

local Internal = require("./internal")

local AssetService = game:GetService("AssetService")

local MAX_ATLAS_SIZE = 1024

export type CreateAtlasParams = {
	CornerRadius: number?,
	CornerSmoothing: number?,
	TopLeftRadius: number?,
	TopRightRadius: number?,
	BottomRightRadius: number?,
	BottomLeftRadius: number?,
}

export type AtlasParams = {
	Start: CreateAtlasParams,
	End: CreateAtlasParams,
	Keypoints: number,
	PreserveSmoothing: boolean?,
	AntiAlias: boolean?,
	Stroke: number?,
}

type KeypointData = {
	ImageRectOffset: Vector2,
	ImageRectSize: Vector2,
	SliceCenter: Rect,
	SliceScale: number,
}

export type SupercornerAtlas = {
	GetContent: (self: SupercornerAtlas) -> Content,
	GetKeypoint: (self: SupercornerAtlas, index: number) -> KeypointData,
	GetKeypointFromAlpha: (self: SupercornerAtlas, alpha: number) -> KeypointData,
	GetKeypointCount: (self: SupercornerAtlas) -> number,
	Destroy: (self: SupercornerAtlas) -> (),
}

type AtlasCacheEntry = {
	eImage: EditableImage,
	keypointData: { KeypointData },
	refCount: number,
	key: string,
}

-- Cache key: Start (6 * f64) + End (6 * f64) + Keypoints (u32) + PreserveSmoothing (u8) + AntiAlias (u8) + Stroke (f64) = 110 bytes
local CACHE_KEY_SIZE = 110
local CacheKeyBuf = buffer.create(CACHE_KEY_SIZE)

local AtlasCache: { [string]: AtlasCacheEntry } = {}
local atlasCacheCount = 0

local function WriteAtlasParamsToKey(buf: buffer, offset: number, params: CreateAtlasParams)
	buffer.writef64(buf, offset, params.CornerRadius or 0)
	buffer.writef64(buf, offset + 8, params.CornerSmoothing or 1)
	buffer.writef64(buf, offset + 16, if params.TopLeftRadius ~= nil then params.TopLeftRadius else -1)
	buffer.writef64(buf, offset + 24, if params.TopRightRadius ~= nil then params.TopRightRadius else -1)
	buffer.writef64(buf, offset + 32, if params.BottomRightRadius ~= nil then params.BottomRightRadius else -1)
	buffer.writef64(buf, offset + 40, if params.BottomLeftRadius ~= nil then params.BottomLeftRadius else -1)
end

local function GetAtlasCacheKey(params: AtlasParams): string
	WriteAtlasParamsToKey(CacheKeyBuf, 0, params.Start)
	WriteAtlasParamsToKey(CacheKeyBuf, 48, params.End)
	buffer.writeu32(CacheKeyBuf, 96, params.Keypoints)
	buffer.writeu8(CacheKeyBuf, 100, if params.PreserveSmoothing then 1 else 0)
	buffer.writeu8(CacheKeyBuf, 101, if params.AntiAlias == false then 0 else 1)
	buffer.writef64(CacheKeyBuf, 102, if params.Stroke ~= nil then params.Stroke else -1)
	return buffer.tostring(CacheKeyBuf)
end

local function Lerp(a: number, b: number, t: number): number
	return a + (b - a) * t
end

local function ResolveAtlasParams(params: CreateAtlasParams): (number, number, number, number, number, number)
	local cornerRadius = params.CornerRadius or 0
	local cornerSmoothing = params.CornerSmoothing or 1
	local tl = params.TopLeftRadius or cornerRadius
	local tr = params.TopRightRadius or cornerRadius
	local br = params.BottomRightRadius or cornerRadius
	local bl = params.BottomLeftRadius or cornerRadius
	return cornerSmoothing, cornerRadius, tl, tr, br, bl
end

local function InterpolateParams(
	startParams: CreateAtlasParams,
	endParams: CreateAtlasParams,
	alpha: number,
	atlasParams: AtlasParams
): Internal.RedrawParams
	local startSmoothing, startDefault, startTL, startTR, startBR, startBL = ResolveAtlasParams(startParams)
	local endSmoothing, endDefault, endTL, endTR, endBR, endBL = ResolveAtlasParams(endParams)

	return {
		CornerRadius = Lerp(startDefault, endDefault, alpha),
		CornerSmoothing = Lerp(startSmoothing, endSmoothing, alpha),
		TopLeftRadius = Lerp(startTL, endTL, alpha),
		TopRightRadius = Lerp(startTR, endTR, alpha),
		BottomRightRadius = Lerp(startBR, endBR, alpha),
		BottomLeftRadius = Lerp(startBL, endBL, alpha),
		PreserveSmoothing = atlasParams.PreserveSmoothing,
		AntiAlias = atlasParams.AntiAlias,
		Stroke = atlasParams.Stroke,
	}
end

type KeypointLayout = {
	width: number,
	height: number,
	xOffset: number,
	yOffset: number,
	params: Internal.RedrawParams,
}

local function Atlas(params: AtlasParams): SupercornerAtlas
	assert(params.Keypoints >= 1, "[Supercorner] Atlas requires at least 1 keypoint")

	local cacheKey = GetAtlasCacheKey(params)
	local existing = AtlasCache[cacheKey]

	if existing then
		existing.refCount += 1

		local keypointData = existing.keypointData
		local keypointCount = #keypointData
		local destroyed = false

		return {
			GetContent = function(self: SupercornerAtlas): Content
				return Content.fromObject(existing.eImage)
			end,

			GetKeypoint = function(self: SupercornerAtlas, index: number): KeypointData
				assert(index >= 1 and index <= keypointCount, `[Supercorner] Keypoint index {index} out of range [1, {keypointCount}]`)
				return keypointData[index]
			end,

			GetKeypointFromAlpha = function(self: SupercornerAtlas, alpha: number): KeypointData
				local index = math.clamp(math.round(alpha * (keypointCount - 1)) + 1, 1, keypointCount)
				return keypointData[index]
			end,

			GetKeypointCount = function(self: SupercornerAtlas): number
				return keypointCount
			end,

			Destroy = function(self: SupercornerAtlas)
				if destroyed then return end
				destroyed = true

				existing.refCount -= 1

				if existing.refCount == 0 then
					AtlasCache[existing.key] = nil
					atlasCacheCount -= 1
					existing.eImage:Destroy()
				end
			end,
		}
	end

	-- Pass 1: compute layout
	local keypointCount = params.Keypoints

	local function computeLayouts(count: number): { KeypointLayout }
		local result = table.create(count) :: { KeypointLayout }

		for idx = 1, count do
			local alpha = if count > 1 then (idx - 1) / (count - 1) else 0
			local interpolated = InterpolateParams(params.Start, params.End, alpha, params)

			local width, height = Internal.CalcSize(
				interpolated.TopLeftRadius or interpolated.CornerRadius or 0,
				interpolated.TopRightRadius or interpolated.CornerRadius or 0,
				interpolated.BottomRightRadius or interpolated.CornerRadius or 0,
				interpolated.BottomLeftRadius or interpolated.CornerRadius or 0,
				params.Stroke
			)

			result[idx] = {
				width = width,
				height = height,
				xOffset = 0,
				yOffset = 0,
				params = interpolated,
			}
		end

		return result
	end

	local function packLayouts(layouts: { KeypointLayout }, count: number): (number, number, number)
		local atlasW = 0
		local rowX = 0
		local rowY = 0
		local rowH = 0

		for idx = 1, count do
			local layout = layouts[idx]

			if rowX > 0 and rowX + layout.width > MAX_ATLAS_SIZE then
				rowY += rowH

				if rowY + layout.height > MAX_ATLAS_SIZE then
					return atlasW, rowY, idx - 1
				end

				atlasW = math.max(atlasW, rowX)
				rowX = 0
				rowH = 0
			end

			if rowY + layout.height > MAX_ATLAS_SIZE then
				return atlasW, rowY, idx - 1
			end

			layout.xOffset = rowX
			layout.yOffset = rowY
			rowX += layout.width
			rowH = math.max(rowH, layout.height)
		end

		atlasW = math.max(atlasW, rowX)
		return atlasW, rowY + rowH, count
	end

	local layouts = computeLayouts(keypointCount)
	local atlasWidth, atlasHeight, fitCount = packLayouts(layouts, keypointCount)

	while fitCount < keypointCount do
		keypointCount = fitCount
		layouts = computeLayouts(keypointCount)
		atlasWidth, atlasHeight, fitCount = packLayouts(layouts, keypointCount)
	end

	if keypointCount < params.Keypoints then
		warn(`[Supercorner] Atlas keypoints clamped from {params.Keypoints} to {keypointCount} to fit within {MAX_ATLAS_SIZE}x{MAX_ATLAS_SIZE} limit.`)
	end

	-- Pass 2: render directly into atlas buffer
	local atlasRowStride = atlasWidth * 4
	local atlasBuf = buffer.create(atlasWidth * atlasHeight * 4)
	local eImage = AssetService:CreateEditableImage({ Size = Vector2.new(atlasWidth, atlasHeight) })

	local keypointData = table.create(keypointCount) :: { KeypointData }
	local prevLayout: KeypointLayout? = nil

	for idx = 1, keypointCount do
		local layout = layouts[idx]
		local w, h = layout.width, layout.height
		local xOff = layout.xOffset
		local yOff = layout.yOffset

		-- Deduplicate: if this keypoint has identical params and size to the
		-- previous one, copy from the already-rendered region instead of
		-- re-running the full render pipeline.
		local duplicated = false

		if prevLayout and w == prevLayout.width and h == prevLayout.height then
			local prev = prevLayout.params
			local cur = layout.params

			if prev.CornerRadius == cur.CornerRadius
				and prev.CornerSmoothing == cur.CornerSmoothing
				and prev.TopLeftRadius == cur.TopLeftRadius
				and prev.TopRightRadius == cur.TopRightRadius
				and prev.BottomRightRadius == cur.BottomRightRadius
				and prev.BottomLeftRadius == cur.BottomLeftRadius
				and prev.Stroke == cur.Stroke
			then
				for row = 0, h - 1 do
					local srcOffset = (prevLayout.yOffset + row) * atlasRowStride + prevLayout.xOffset * 4
					local dstOffset = (yOff + row) * atlasRowStride + xOff * 4
					buffer.copy(atlasBuf, dstOffset, atlasBuf, srcOffset, w * 4)
				end

				duplicated = true
			end
		end

		if not duplicated then
			Internal.RenderToBuffer(atlasBuf, w, h, layout.params, xOff, yOff, atlasRowStride)
		end

		keypointData[idx] = {
			ImageRectOffset = Vector2.new(xOff, yOff),
			ImageRectSize = Vector2.new(w, h),
			SliceCenter = if w <= 2 or h <= 2 then Rect.new(0, 0, 0, 0) else Internal.CalcSliceCenter(w, h, layout.params),
			SliceScale = 0.5,
		}

		prevLayout = layout
	end

	eImage:WritePixelsBuffer(
		Vector2.new(0, 0),
		Vector2.new(atlasWidth, atlasHeight),
		atlasBuf
	)

	-- Cache the result
	local entry: AtlasCacheEntry = {
		eImage = eImage,
		keypointData = keypointData,
		refCount = 1,
		key = cacheKey,
	}

	AtlasCache[cacheKey] = entry
	atlasCacheCount += 1

	local destroyed = false

	return {
		GetContent = function(self: SupercornerAtlas): Content
			return Content.fromObject(eImage)
		end,

		GetKeypoint = function(self: SupercornerAtlas, index: number): KeypointData
			assert(index >= 1 and index <= keypointCount, `[Supercorner] Keypoint index {index} out of range [1, {keypointCount}]`)
			return keypointData[index]
		end,

		GetKeypointFromAlpha = function(self: SupercornerAtlas, alpha: number): KeypointData
			local index = math.clamp(math.round(alpha * (keypointCount - 1)) + 1, 1, keypointCount)
			return keypointData[index]
		end,

		GetKeypointCount = function(self: SupercornerAtlas): number
			return keypointCount
		end,

		Destroy = function(self: SupercornerAtlas)
			if destroyed then return end
			destroyed = true

			entry.refCount -= 1

			if entry.refCount == 0 then
				AtlasCache[entry.key] = nil
				atlasCacheCount -= 1
				eImage:Destroy()
			end
		end,
	}
end

local function GetAtlasCacheSize(): number
	return atlasCacheCount
end

return {
	atlas = Atlas,
	GetAtlasCacheSize = GetAtlasCacheSize,
}


--!strict

local types = require("./types")

type NormalizedCorners = types.NormalizedCorners

-- Corner indices: 1=TL, 2=TR, 3=BR, 4=BL
-- Adjacency (hardcoded):
--   TL: TR via top(width),  BL via left(height)
--   TR: TL via top(width),  BR via right(height)
--   BR: BL via bottom(width), TR via right(height)
--   BL: BR via bottom(width), TL via left(height)

local function distribute_and_normalize(
	width: number,
	height: number,
	top_left_radius: number,
	top_right_radius: number,
	bottom_right_radius: number,
	bottom_left_radius: number
): NormalizedCorners
	local r = { top_left_radius, top_right_radius, bottom_right_radius, bottom_left_radius }
	local b = { -1.0, -1.0, -1.0, -1.0 }

	-- Sort indices by radius descending
	local order = { 1, 2, 3, 4 }
	table.sort(order, function(a, b) return r[a] > r[b] end)

	for _, ci in order do
		local radius = r[ci]

		-- Look up two adjacent corners and their shared side lengths
		local a1, s1, a2, s2
		if ci == 1 then     a1, s1, a2, s2 = 2, width, 4, height     -- TL
		elseif ci == 2 then a1, s1, a2, s2 = 1, width, 3, height     -- TR
		elseif ci == 3 then a1, s1, a2, s2 = 4, width, 2, height     -- BR
		else                a1, s1, a2, s2 = 3, width, 1, height end  -- BL

		local adj1_r, adj1_b = r[a1], b[a1]
		local budget1 = if radius == 0 and adj1_r == 0 then 0
			elseif adj1_b >= 0 then s1 - adj1_b
			else (radius / (radius + adj1_r)) * s1

		local adj2_r, adj2_b = r[a2], b[a2]
		local budget2 = if radius == 0 and adj2_r == 0 then 0
			elseif adj2_b >= 0 then s2 - adj2_b
			else (radius / (radius + adj2_r)) * s2

		local min_budget = math.min(budget1, budget2)
		b[ci] = min_budget

		if radius > min_budget then
			r[ci] = min_budget
		end
	end

	return {
		topLeft = {
			radius = r[1],
			roundingAndSmoothingBudget = b[1],
		},

		topRight = {
			radius = r[2],
			roundingAndSmoothingBudget = b[2],
		},

		bottomRight = {
			radius = r[3],
			roundingAndSmoothingBudget = b[3],
		},

		bottomLeft = {
			radius = r[4],
			roundingAndSmoothingBudget = b[4],
		},
	}
end

return {
	distribute_and_normalize = distribute_and_normalize,
}


--!strict

const types = require("./types")

type CornerPathParams = types.CornerPathParams
type SvgOp = types.SvgOp

type SvgPathInput = {
	width: number,
	height: number,
	top_left_path_params: CornerPathParams,
	top_right_path_params: CornerPathParams,
	bottom_right_path_params: CornerPathParams,
	bottom_left_path_params: CornerPathParams,
}

const function get_path_params_for_corner(
	corner_radius: number,
	corner_smoothing: number,
	preserve_smoothing: boolean,
	rounding_and_smoothing_budget: number
): CornerPathParams
	local smoothing = corner_smoothing

	local p = (1.0 + smoothing) * corner_radius

	if not preserve_smoothing then
		const max_corner_smoothing = rounding_and_smoothing_budget / corner_radius - 1.0
		smoothing = math.min(smoothing, max_corner_smoothing)
		p = math.min(p, rounding_and_smoothing_budget)
	end

	const arc_measure = 90.0 * (1.0 - smoothing)
	const arcMeasureHalfRad = math.rad(arc_measure / 2.0)
	const arc_section_length = math.sin(arcMeasureHalfRad) * corner_radius * math.sqrt(2)

	const angle_alpha = (90.0 - arc_measure) / 2.0
	const angleAlphaHalfRad = math.rad(angle_alpha / 2.0)
	const p3_to_p4_distance = corner_radius * math.tan(angleAlphaHalfRad)
	const angleBetaRad = math.rad(45.0 * smoothing)
	const c = p3_to_p4_distance * math.cos(angleBetaRad)
	const d = c * math.tan(angleBetaRad)

	local b = (p - arc_section_length - c - d) / 3.0
	local a = 2.0 * b

	if preserve_smoothing and p > rounding_and_smoothing_budget then
		const p1_to_p3_max_distance = rounding_and_smoothing_budget - d - arc_section_length - c
		const min_a = p1_to_p3_max_distance / 6.0
		const max_b = p1_to_p3_max_distance - min_a
		b = math.min(b, max_b)
		a = p1_to_p3_max_distance - b
		p = math.min(p, rounding_and_smoothing_budget)
	end

	return {
		a = a,
		b = b,
		c = c,
		d = d,
		p = p,
		cornerRadius = corner_radius,
		arcSectionLength = arc_section_length,
	}
end

-- Inserts the 3 corner ops (cubic, arc, cubic) directly into the ops array.
-- Each corner has a unique sign pattern applied to (a, b, c, d, arcSectionLength).
local function insert_corner_ops(ops: { SvgOp }, pp: CornerPathParams, corner: number)
	if pp.cornerRadius == 0 then
		return
	end

	local a, b, c, d = pp.a, pp.b, pp.c, pp.d
	local arc = pp.arcSectionLength
	local r = pp.cornerRadius
	local ab = a + b
	local abc = ab + c
	local cb = c + b
	local cba = cb + a

	if corner == 1 then -- top right
		table.insert(ops, { type = "c" :: "c", dx1 = a, dy1 = 0, dx2 = ab, dy2 = 0, dx = abc, dy = d })
		table.insert(ops, { type = "a" :: "a", rx = r, ry = r, rotation = 0, largeArc = 0, sweep = 1, dx = arc, dy = arc } :: SvgOp)
		table.insert(ops, { type = "c" :: "c", dx1 = d, dy1 = c, dx2 = d, dy2 = cb, dx = d, dy = cba })

	elseif corner == 2 then -- bottom right
		table.insert(ops, { type = "c" :: "c", dx1 = 0, dy1 = a, dx2 = 0, dy2 = ab, dx = -d, dy = abc })
		table.insert(ops, { type = "a" :: "a", rx = r, ry = r, rotation = 0, largeArc = 0, sweep = 1, dx = -arc, dy = arc } :: SvgOp)
		table.insert(ops, { type = "c" :: "c", dx1 = -c, dy1 = d, dx2 = -cb, dy2 = d, dx = -cba, dy = d })

	elseif corner == 3 then -- bottom left
		table.insert(ops, { type = "c" :: "c", dx1 = -a, dy1 = 0, dx2 = -ab, dy2 = 0, dx = -abc, dy = -d })
		table.insert(ops, { type = "a" :: "a", rx = r, ry = r, rotation = 0, largeArc = 0, sweep = 1, dx = -arc, dy = -arc } :: SvgOp)
		table.insert(ops, { type = "c" :: "c", dx1 = -d, dy1 = -c, dx2 = -d, dy2 = -cb, dx = -d, dy = -cba })

	else -- top left
		table.insert(ops, { type = "c" :: "c", dx1 = 0, dy1 = -a, dx2 = 0, dy2 = -ab, dx = d, dy = -abc })
		table.insert(ops, { type = "a" :: "a", rx = r, ry = r, rotation = 0, largeArc = 0, sweep = 1, dx = arc, dy = -arc } :: SvgOp)
		table.insert(ops, { type = "c" :: "c", dx1 = c, dy1 = -d, dx2 = cb, dy2 = -d, dx = cba, dy = -d })
	end
end

const function get_svg_ops(input: SvgPathInput): { SvgOp }
	const width = input.width
	const height = input.height
	const tl = input.top_left_path_params
	const tr = input.top_right_path_params
	const br = input.bottom_right_path_params
	const bl = input.bottom_left_path_params

	local ops: { SvgOp } = table.create(21) :: { SvgOp }

	table.insert(ops, { type = "M" :: "M", x = tl.p, y = 0 })
	table.insert(ops, { type = "L" :: "L", x = width - tr.p, y = 0 })
	insert_corner_ops(ops, tr, 1)

	table.insert(ops, { type = "L" :: "L", x = width, y = height - br.p })
	insert_corner_ops(ops, br, 2)

	table.insert(ops, { type = "L" :: "L", x = bl.p, y = height })
	insert_corner_ops(ops, bl, 3)

	table.insert(ops, { type = "L" :: "L", x = 0, y = tl.p })
	insert_corner_ops(ops, tl, 4)

	table.insert(ops, { type = "Z" :: "Z" })

	return ops
end

return {
	get_path_params_for_corner = get_path_params_for_corner,
	get_svg_ops = get_svg_ops,
}


--!strict

const types = require("./figma_squircle_luau/types")
const distribute = require("./figma_squircle_luau/distribute")
const draw = require("./figma_squircle_luau/draw")

export type FigmaSquircleParams = types.FigmaSquircleParams
export type SvgOp = types.SvgOp

const function get_svg_path(params: FigmaSquircleParams): { SvgOp }
	const width = params.width
	const height = params.height
	const corner_smoothing = params.cornerSmoothing
	const preserve_smoothing = if params.preserveSmoothing then params.preserveSmoothing else false
	const default_radius = params.cornerRadius or 0

	const tl_radius = params.topLeftCornerRadius or default_radius
	const tr_radius = params.topRightCornerRadius or default_radius
	const br_radius = params.bottomRightCornerRadius or default_radius
	const bl_radius = params.bottomLeftCornerRadius or default_radius

	const all_equal = tl_radius == tr_radius and tr_radius == br_radius and br_radius == bl_radius

	if all_equal then
		const budget = math.min(width, height) / 2
		const radius = math.min(tl_radius, budget)

		const path_params = draw.get_path_params_for_corner(
			radius, corner_smoothing, preserve_smoothing, budget
		)

		return draw.get_svg_ops({
			width = width,
			height = height,
			top_left_path_params = path_params,
			top_right_path_params = path_params,
			bottom_right_path_params = path_params,
			bottom_left_path_params = path_params,
		})
	end

	const normalized = distribute.distribute_and_normalize(
		width, height, tl_radius, tr_radius, br_radius, bl_radius
	)

	const tl_path_params = draw.get_path_params_for_corner(
		normalized.topLeft.radius, corner_smoothing, preserve_smoothing,
		normalized.topLeft.roundingAndSmoothingBudget
	)

	const tr_path_params = draw.get_path_params_for_corner(
		normalized.topRight.radius, corner_smoothing, preserve_smoothing,
		normalized.topRight.roundingAndSmoothingBudget
	)

	const br_path_params = draw.get_path_params_for_corner(
		normalized.bottomRight.radius, corner_smoothing, preserve_smoothing,
		normalized.bottomRight.roundingAndSmoothingBudget
	)

	const bl_path_params = draw.get_path_params_for_corner(
		normalized.bottomLeft.radius, corner_smoothing, preserve_smoothing,
		normalized.bottomLeft.roundingAndSmoothingBudget
	)

	return draw.get_svg_ops({
		width = width,
		height = height,
		top_left_path_params = tl_path_params,
		top_right_path_params = tr_path_params,
		bottom_right_path_params = br_path_params,
		bottom_left_path_params = bl_path_params,
	})
end

return {
	get_svg_path = get_svg_path,
}


--!strict

export type MoveToOp = {
	type: "M",
	x: number,
	y: number,
}

export type LineToOp = {
	type: "L",
	x: number,
	y: number,
}

export type CubicBezierOp = {
	type: "c",
	dx1: number,
	dy1: number,
	dx2: number,
	dy2: number,
	dx: number,
	dy: number,
}

export type ArcOp = {
	type: "a",
	rx: number,
	ry: number,
	rotation: number,
	largeArc: number,
	sweep: number,
	dx: number,
	dy: number,
}

export type ClosePathOp = {
	type: "Z",
}

export type SvgOp = MoveToOp | LineToOp | CubicBezierOp | ArcOp | ClosePathOp

export type FigmaSquircleParams = {
	width: number,
	height: number,
	cornerRadius: number?,
	topLeftCornerRadius: number?,
	topRightCornerRadius: number?,
	bottomRightCornerRadius: number?,
	bottomLeftCornerRadius: number?,
	cornerSmoothing: number,
	preserveSmoothing: boolean?,
}

export type CornerPathParams = {
	a: number,
	b: number,
	c: number,
	d: number,
	p: number,
	cornerRadius: number,
	arcSectionLength: number,
}

export type NormalizedCorner = {
	radius: number,
	roundingAndSmoothingBudget: number,
}

export type NormalizedCorners = {
	topLeft: NormalizedCorner,
	topRight: NormalizedCorner,
	bottomLeft: NormalizedCorner,
	bottomRight: NormalizedCorner,
}

return nil


--!strict

const squircleTypes = require(script.Parent.figma_squircle_luau)

type SvgOp = squircleTypes.SvgOp

const VERTEX_STRIDE = 8
const INITIAL_CAPACITY = 128

-- Module-level scratch buffer for De Casteljau subdivision stack, reused across calls.
const MAX_DEPTH = 16
const STACK_STRIDE = 36
const stackBuf = buffer.create(MAX_DEPTH * 2 * STACK_STRIDE)

const function createVertexBuf(capacity: number): buffer
	return buffer.create(capacity * VERTEX_STRIDE)
end

const function flatten(ops: { SvgOp }, tolerance: number): (buffer, number)
	-- Estimate vertex count from op types to avoid dynamic resizing.
	-- Conservative upper bounds: cubics subdivide to at most 2^MAX_DEPTH segments,
	-- but typical squircle cubics produce ~8-16 vertices; arcs produce ~4-12.
	local estimate = 0

	for _, op in ops do
		const t = op.type

		if t == "c" then
			estimate += 10

		elseif t == "a" then
			estimate += 8

		else
			estimate += 1
		end
	end

	local capacity = math.max(INITIAL_CAPACITY, estimate)
	local buf = createVertexBuf(capacity)
	local count = 0

	local curX = 0.0
	local curY = 0.0
	local startX = 0.0
	local startY = 0.0

	const function emit(x: number, y: number)
		if count >= capacity then
			const oldBuf = buf
			capacity *= 2
			buf = createVertexBuf(capacity)
			buffer.copy(buf, 0, oldBuf, 0, count * VERTEX_STRIDE)
		end

		const offset = count * VERTEX_STRIDE
		buffer.writef32(buf, offset, x)
		buffer.writef32(buf, offset + 4, y)
		count += 1
		curX = x
		curY = y
	end

	const tolSq = tolerance * tolerance

	const function flattenCubic(
		sx0: number, sy0: number,
		sx1: number, sy1: number,
		sx2: number, sy2: number,
		sx3: number, sy3: number
	)
		local sp = 0
		buffer.writef32(stackBuf, 0, sx0)
		buffer.writef32(stackBuf, 4, sy0)
		buffer.writef32(stackBuf, 8, sx1)
		buffer.writef32(stackBuf, 12, sy1)
		buffer.writef32(stackBuf, 16, sx2)
		buffer.writef32(stackBuf, 20, sy2)
		buffer.writef32(stackBuf, 24, sx3)
		buffer.writef32(stackBuf, 28, sy3)
		buffer.writef32(stackBuf, 32, 0)
		sp = 1

		while sp > 0 do
			sp -= 1
			const base = sp * STACK_STRIDE
			const x0 = buffer.readf32(stackBuf, base)
			const y0 = buffer.readf32(stackBuf, base + 4)
			const x1 = buffer.readf32(stackBuf, base + 8)
			const y1 = buffer.readf32(stackBuf, base + 12)
			const x2 = buffer.readf32(stackBuf, base + 16)
			const y2 = buffer.readf32(stackBuf, base + 20)
			const x3 = buffer.readf32(stackBuf, base + 24)
			const y3 = buffer.readf32(stackBuf, base + 28)
			const depth = buffer.readf32(stackBuf, base + 32)

			const dx = x3 - x0
			const dy = y3 - y0
			const dSq = dx * dx + dy * dy

			if dSq < 1e-10 then
				emit(x3, y3)
				continue
			end

			-- Flatness test: max distance from control points to the chord
			const invDSq = 1.0 / dSq

			const t1 = ((x1 - x0) * dx + (y1 - y0) * dy) * invDSq
			const px1 = x0 + t1 * dx - x1
			const py1 = y0 + t1 * dy - y1
			const d1Sq = px1 * px1 + py1 * py1

			const t2 = ((x2 - x0) * dx + (y2 - y0) * dy) * invDSq
			const px2 = x0 + t2 * dx - x2
			const py2 = y0 + t2 * dy - y2
			const d2Sq = px2 * px2 + py2 * py2

			if (d1Sq <= tolSq and d2Sq <= tolSq) or depth >= MAX_DEPTH then
				emit(x3, y3)
				continue
			end

			-- De Casteljau midpoint split
			const m01x = (x0 + x1) * 0.5
			const m01y = (y0 + y1) * 0.5
			const m12x = (x1 + x2) * 0.5
			const m12y = (y1 + y2) * 0.5
			const m23x = (x2 + x3) * 0.5
			const m23y = (y2 + y3) * 0.5

			const m012x = (m01x + m12x) * 0.5
			const m012y = (m01y + m12y) * 0.5
			const m123x = (m12x + m23x) * 0.5
			const m123y = (m12y + m23y) * 0.5

			const mx = (m012x + m123x) * 0.5
			const my = (m012y + m123y) * 0.5

			-- Push right half first so left half is popped first (preserves vertex order)
			const rBase = sp * STACK_STRIDE
			buffer.writef32(stackBuf, rBase, mx)
			buffer.writef32(stackBuf, rBase + 4, my)
			buffer.writef32(stackBuf, rBase + 8, m123x)
			buffer.writef32(stackBuf, rBase + 12, m123y)
			buffer.writef32(stackBuf, rBase + 16, m23x)
			buffer.writef32(stackBuf, rBase + 20, m23y)
			buffer.writef32(stackBuf, rBase + 24, x3)
			buffer.writef32(stackBuf, rBase + 28, y3)
			buffer.writef32(stackBuf, rBase + 32, depth + 1)
			sp += 1

			const lBase = sp * STACK_STRIDE
			buffer.writef32(stackBuf, lBase, x0)
			buffer.writef32(stackBuf, lBase + 4, y0)
			buffer.writef32(stackBuf, lBase + 8, m01x)
			buffer.writef32(stackBuf, lBase + 12, m01y)
			buffer.writef32(stackBuf, lBase + 16, m012x)
			buffer.writef32(stackBuf, lBase + 20, m012y)
			buffer.writef32(stackBuf, lBase + 24, mx)
			buffer.writef32(stackBuf, lBase + 28, my)
			buffer.writef32(stackBuf, lBase + 32, depth + 1)
			sp += 1
		end
	end

	-- SVG arc: endpoint-to-center parameterization (SVG spec F.6.5–F.6.6),
	-- then incremental rotation to avoid per-step trig calls
	const function flattenArc(
		cx: number, cy: number,
		rx: number, ry: number,
		rotation: number,
		largeArc: number, sweep: number,
		dx: number, dy: number
	)
		const x1 = cx
		const y1 = cy
		const x2 = cx + dx
		const y2 = cy + dy

		if dx == 0 and dy == 0 then
			return
		end

		rx = math.abs(rx)
		ry = math.abs(ry)

		if rx < 1e-10 or ry < 1e-10 then
			emit(x2, y2)
			return
		end

		const rotRad = math.rad(rotation)
		const cosPhi = math.cos(rotRad)
		const sinPhi = math.sin(rotRad)

		const hdx = (x1 - x2) * 0.5
		const hdy = (y1 - y2) * 0.5
		const x1p = cosPhi * hdx + sinPhi * hdy
		const y1p = -sinPhi * hdx + cosPhi * hdy

		const x1pSq = x1p * x1p
		const y1pSq = y1p * y1p
		local rxSq = rx * rx
		local rySq = ry * ry

		-- Scale radii if the ellipse is too small to reach both endpoints
		const lambda = x1pSq / rxSq + y1pSq / rySq
		if lambda > 1.0 then
			const scale = math.sqrt(lambda)
			rx *= scale
			ry *= scale
			rxSq = rx * rx
			rySq = ry * ry
		end

		const num = math.max(0, rxSq * rySq - rxSq * y1pSq - rySq * x1pSq)
		const den = rxSq * y1pSq + rySq * x1pSq

		local sq = if den > 1e-10 then math.sqrt(num / den) else 0.0
		sq = if largeArc == sweep then -sq else sq

		const cxp = sq * rx * y1p / ry
		const cyp = -sq * ry * x1p / rx

		const centerX = cosPhi * cxp - sinPhi * cyp + (x1 + x2) * 0.5
		const centerY = sinPhi * cxp + cosPhi * cyp + (y1 + y2) * 0.5

		local theta1 = math.atan2((y1p - cyp) / ry, (x1p - cxp) / rx)
		local dtheta = math.atan2((-y1p - cyp) / ry, (-x1p - cxp) / rx) - theta1

		if sweep == 1 and dtheta < 0 then
			dtheta += 2 * math.pi

		elseif sweep == 0 and dtheta > 0 then
			dtheta -= 2 * math.pi
		end

		-- Step count from chord deviation: r·θ²/8 ≤ tolerance → θ ≤ sqrt(8·tol/r)
		const maxR = math.max(rx, ry)
		const maxStep = math.sqrt(8 * tolerance / maxR)
		const numSteps = math.max(4, math.ceil(math.abs(dtheta) / maxStep))
		const step = dtheta / numSteps

		-- Incremental rotation via angle-addition identity (avoids per-step cos/sin)
		const cosStep = math.cos(step)
		const sinStep = math.sin(step)

		const crx = cosPhi * rx
		const sry = sinPhi * ry
		const srx = sinPhi * rx
		const cry = cosPhi * ry

		local cosA = math.cos(theta1)
		local sinA = math.sin(theta1)

		for i = 1, numSteps do
			const nextCos = cosA * cosStep - sinA * sinStep
			const nextSin = sinA * cosStep + cosA * sinStep
			cosA = nextCos
			sinA = nextSin

			emit(crx * cosA - sry * sinA + centerX, srx * cosA + cry * sinA + centerY)
		end
	end

	for _, op in ops do
		if op.type == "M" then
			startX = op.x
			startY = op.y
			emit(op.x, op.y)

		elseif op.type == "L" then
			emit(op.x, op.y)

		elseif op.type == "c" then
			const x0 = curX
			const y0 = curY
			flattenCubic(
				x0, y0,
				x0 + op.dx1, y0 + op.dy1,
				x0 + op.dx2, y0 + op.dy2,
				x0 + op.dx, y0 + op.dy
			)

		elseif op.type == "a" then
			flattenArc(
				curX, curY,
				op.rx, op.ry,
				op.rotation,
				op.largeArc, op.sweep,
				op.dx, op.dy
			)

		elseif op.type == "Z" then
			if curX ~= startX or curY ~= startY then
				emit(startX, startY)
			end
		end
	end

	return buf, count
end

return flatten


--!strict
--!nolint LocalShadow

const Internal = require("@self/internal")
const Atlas = require("@self/atlas")

const AssetService = game:GetService("AssetService")

export type CreateParams = {
	CornerRadius: number?,
	CornerSmoothing: number?,
	TopLeftRadius: number?,
	TopRightRadius: number?,
	BottomRightRadius: number?,
	BottomLeftRadius: number?,
	MaxCornerRadius: number?,
	MaxTopLeftRadius: number?,
	MaxTopRightRadius: number?,
	MaxBottomRightRadius: number?,
	MaxBottomLeftRadius: number?,
	PreserveSmoothing: boolean?,
	AntiAlias: boolean?,
	Redrawable: boolean?,
	Stroke: number?,
}

export type RedrawParams = {
	CornerRadius: number?,
	CornerSmoothing: number?,
	TopLeftRadius: number?,
	TopRightRadius: number?,
	BottomRightRadius: number?,
	BottomLeftRadius: number?,
	PreserveSmoothing: boolean?,
	AntiAlias: boolean?,
	Stroke: number?,
}

export type CreateAtlasParams = Atlas.CreateAtlasParams
export type AtlasParams = Atlas.AtlasParams
export type SupercornerAtlas = Atlas.SupercornerAtlas

export type Supercorner = {
	Redraw: (self: Supercorner, params: RedrawParams) -> (),
	GetContent: (self: Supercorner) -> Content,
	GetSliceCenter: (self: Supercorner) -> Rect,
	GetSliceScale: (self: Supercorner) -> number,
	Destroy: (self: Supercorner) -> (),
}

type SupercornerState = {
	EImage: EditableImage,
	PixelBuf: buffer,
	Width: number,
	Height: number,
}

type CacheEntry = {
	state: SupercornerState,
	refCount: number,
	key: string,
}

type StaticCacheEntry = {
	content: Content,
	refCount: number,
	key: string,
}

type CreateDataModelContentAsync =
    (self: AssetService, content: Content, options: { [string]: any }?)
        -> (Enum.CreateContentResult, Content)

const CAN_CREATE_STATIC_CONTENT = (function()
    const ok, data = pcall(
        AssetService.CreateDataModelContentAsync, AssetService, Content.none, nil
    )

    return if ok then ok else data ~= "CreateDataModelContentAsync is not available yet"
end)()

const Cache: { [string]: CacheEntry } = {}
const StaticCache: { [string]: StaticCacheEntry } = {}
local cacheCount = 0
local staticCacheCount = 0
const CacheKeyBuf = buffer.create(74)

const function GetCacheKey(width: number, height: number, params: RedrawParams): string
	buffer.writef64(CacheKeyBuf, 0, width)
	buffer.writef64(CacheKeyBuf, 8, height)
	buffer.writef64(CacheKeyBuf, 16, params.CornerRadius or 0)
	buffer.writef64(CacheKeyBuf, 24, params.CornerSmoothing or 1)
	buffer.writef64(CacheKeyBuf, 32, if params.TopLeftRadius ~= nil then params.TopLeftRadius else -1)
	buffer.writef64(CacheKeyBuf, 40, if params.TopRightRadius ~= nil then params.TopRightRadius else -1)
	buffer.writef64(CacheKeyBuf, 48, if params.BottomRightRadius ~= nil then params.BottomRightRadius else -1)
	buffer.writef64(CacheKeyBuf, 56, if params.BottomLeftRadius ~= nil then params.BottomLeftRadius else -1)
	buffer.writeu8(CacheKeyBuf, 64, if params.PreserveSmoothing then 1 else 0)
	buffer.writeu8(CacheKeyBuf, 65, if params.AntiAlias == false then 0 else 1)
	buffer.writef64(CacheKeyBuf, 66, if params.Stroke ~= nil then params.Stroke else -1)
	return buffer.tostring(CacheKeyBuf)
end

const function Render(state: SupercornerState, params: RedrawParams)
	Internal.RenderToBuffer(state.PixelBuf, state.Width, state.Height, params)

	state.EImage:WritePixelsBuffer(
		Vector2.new(0, 0),
		Vector2.new(state.Width, state.Height),
		state.PixelBuf
	)
end

const function ToStaticContent(eImage: EditableImage): Content
	local result, content = (AssetService.CreateDataModelContentAsync :: CreateDataModelContentAsync)(AssetService, Content.fromObject(eImage))
	assert(result == Enum.CreateContentResult.Success, "Failed to create static content")
	return content
end

const function ClampToMaxRadii(
	params: RedrawParams,
	maxTopLeft: number,
	maxTopRight: number,
	maxBottomRight: number,
	maxBottomLeft: number
): RedrawParams
	local defaultRadius = params.CornerRadius or 0
	local topLeft = params.TopLeftRadius or defaultRadius
	local topRight = params.TopRightRadius or defaultRadius
	local bottomRight = params.BottomRightRadius or defaultRadius
	local bottomLeft = params.BottomLeftRadius or defaultRadius

	local clamped = false

	if topLeft > maxTopLeft then
		clamped = true
		warn(`[Supercorner] TopLeft corner radius {topLeft} exceeds max {maxTopLeft}. Clamping!`)
		topLeft = maxTopLeft
	end

	if topRight > maxTopRight then
		clamped = true
		warn(`[Supercorner] TopRight corner radius {topRight} exceeds max {maxTopRight}. Clamping!`)
		topRight = maxTopRight
	end

	if bottomRight > maxBottomRight then
		clamped = true
		warn(`[Supercorner] BottomRight corner radius {bottomRight} exceeds max {maxBottomRight}. Clamping!`)
		bottomRight = maxBottomRight
	end

	if bottomLeft > maxBottomLeft then
		clamped = true
		warn(`[Supercorner] BottomLeft corner radius {bottomLeft} exceeds max {maxBottomLeft}. Clamping!`)
		bottomLeft = maxBottomLeft
	end

	if not clamped then
		return params
	end

	return {
		CornerRadius = math.min(defaultRadius, maxTopLeft, maxTopRight, maxBottomRight, maxBottomLeft),
		CornerSmoothing = params.CornerSmoothing,
		TopLeftRadius = topLeft,
		TopRightRadius = topRight,
		BottomRightRadius = bottomRight,
		BottomLeftRadius = bottomLeft,
		PreserveSmoothing = params.PreserveSmoothing,
		AntiAlias = params.AntiAlias,
		Stroke = params.Stroke,
	}
end

const CalcSliceCenter = Internal.CalcSliceCenter
const CalcSize = Internal.CalcSize

const function NewRedrawable(
	width: number,
	height: number,
	params: RedrawParams,
	maxTopLeft: number,
	maxTopRight: number,
	maxBottomRight: number,
	maxBottomLeft: number
): Supercorner
	local cacheKey = GetCacheKey(width, height, params)
	local entry: CacheEntry? = Cache[cacheKey]

	if entry == nil then
		local state: SupercornerState = {
			EImage = AssetService:CreateEditableImage({ Size = Vector2.new(width, height) }),
			PixelBuf = buffer.create(width * height * 4),
			Width = width,
			Height = height,
		}

		Render(state, params)

		entry = {
			state = state,
			refCount = 0,
			key = cacheKey,
		}

		Cache[cacheKey] = entry
		cacheCount += 1
	end

	local state: SupercornerState = (entry :: CacheEntry).state
	local destroyed = false
	local sliceCenter = CalcSliceCenter(width, height, params);

	(entry :: CacheEntry).refCount += 1

	return {
		Redraw = function(self: Supercorner, newParams: RedrawParams)
			if destroyed then return end

			newParams = ClampToMaxRadii(newParams, maxTopLeft, maxTopRight, maxBottomRight, maxBottomLeft)
			local newKey = GetCacheKey(width, height, newParams)

			if entry ~= nil and entry.key == newKey then return end

			sliceCenter = CalcSliceCenter(width, height, newParams)

			local newEntry = Cache[newKey]

			if newEntry ~= nil then
				local oldEImage = state.EImage
				local ownsOldState = entry == nil or entry.refCount == 1

				if entry ~= nil then
					entry.refCount -= 1

					if entry.refCount == 0 then
						Cache[entry.key] = nil
						cacheCount -= 1
					end
				end

				local newEntry = newEntry :: CacheEntry
				newEntry.refCount += 1
				state = newEntry.state

				if ownsOldState and oldEImage ~= state.EImage then
					oldEImage:Destroy()
				end

				return
			end

			-- No cache hit — reuse our state if we're the sole owner, otherwise clone
			if entry ~= nil then
				entry.refCount -= 1

				if entry.refCount == 0 then
					Cache[entry.key] = nil
					cacheCount -= 1

				else
					state = {
						EImage = AssetService:CreateEditableImage({ Size = Vector2.new(width, height) }),
						PixelBuf = buffer.create(width * height * 4),
						Width = width,
						Height = height,
					}
				end
			end

			Render(state, newParams)
			entry = { state = state, refCount = 1, key = newKey }
			Cache[newKey] = entry
			cacheCount += 1
		end,

		GetContent = function(self: Supercorner): Content
			return Content.fromObject(state.EImage)
		end,

		GetSliceCenter = function(self: Supercorner): Rect
			return sliceCenter
		end,

		GetSliceScale = function(self: Supercorner): number
			return 0.5
		end,

		Destroy = function(self: Supercorner)
			if destroyed then return end
			destroyed = true

			if entry ~= nil then
				entry.refCount -= 1

				if entry.refCount == 0 then
					Cache[entry.key] = nil
					cacheCount -= 1
					state.EImage:Destroy()
				end

				entry = nil

			else
				state.EImage:Destroy()
			end
		end,
	}
end

const function NewStatic(width: number, height: number, params: RedrawParams): Supercorner
	local cacheKey = GetCacheKey(width, height, params)
	local staticEntry: StaticCacheEntry? = StaticCache[cacheKey]
	local destroyed = false
	local staticContent: Content

	if staticEntry ~= nil then
		(staticEntry :: StaticCacheEntry).refCount += 1
		staticContent = (staticEntry :: StaticCacheEntry).content

	else
		local eImage = AssetService:CreateEditableImage({ Size = Vector2.new(width, height) })

		local tempState: SupercornerState = {
			EImage = eImage,
			PixelBuf = buffer.create(width * height * 4),
			Width = width,
			Height = height,
		}

		Render(tempState, params)
		staticContent = ToStaticContent(eImage)
		eImage:Destroy()

		staticEntry = {
			content = staticContent,
			refCount = 1,
			key = cacheKey,
		}

		StaticCache[cacheKey] = staticEntry :: StaticCacheEntry
		staticCacheCount += 1
	end

	local sliceCenter = CalcSliceCenter(width, height, params)

	return {
		Redraw = function(self: Supercorner, newParams: RedrawParams)
			error(
				"Cannot redraw a non-redrawable Supercorner. Pass `Redrawable = true` in the"
				.. " constructor params to enable redrawing. You can also call `:SetRedrawable(true)`"
				.. " on an existing instance, however the former is strongly preferred."
			)
		end,

		GetContent = function(self: Supercorner): Content
			return staticContent
		end,

		GetSliceCenter = function(self: Supercorner): Rect
			return sliceCenter
		end,

		GetSliceScale = function(self: Supercorner): number
			return 0.5
		end,

		Destroy = function(self: Supercorner)
			if destroyed then return end
			destroyed = true

			if staticEntry == nil then return end

			local staticEntry = staticEntry :: StaticCacheEntry
			staticEntry.refCount -= 1

			if staticEntry.refCount == 0 then
				StaticCache[staticEntry.key] = nil
				staticCacheCount -= 1
			end
		end,
	}
end

const function New(params: CreateParams): Supercorner
	local defaultRadius: number = params.CornerRadius or 0
	local topLeft: number = params.TopLeftRadius or defaultRadius
	local topRight: number = params.TopRightRadius or defaultRadius
	local bottomRight: number = params.BottomRightRadius or defaultRadius
	local bottomLeft: number = params.BottomLeftRadius or defaultRadius

	if params.Redrawable or not CAN_CREATE_STATIC_CONTENT then
		local maxDefault: number = params.MaxCornerRadius or defaultRadius
		local maxTopLeft: number = params.MaxTopLeftRadius or math.max(maxDefault, topLeft)
		local maxTopRight: number = params.MaxTopRightRadius or math.max(maxDefault, topRight)
		local maxBottomRight: number = params.MaxBottomRightRadius or math.max(maxDefault, bottomRight)
		local maxBottomLeft: number = params.MaxBottomLeftRadius or math.max(maxDefault, bottomLeft)

		local width, height = CalcSize(maxTopLeft, maxTopRight, maxBottomRight, maxBottomLeft, params.Stroke)
		local clampedParams = ClampToMaxRadii(params, maxTopLeft, maxTopRight, maxBottomRight, maxBottomLeft)

		return NewRedrawable(width, height, clampedParams, maxTopLeft, maxTopRight, maxBottomRight, maxBottomLeft)
	end

	local width, height = CalcSize(topLeft, topRight, bottomRight, bottomLeft, params.Stroke)

	return NewStatic(width, height, params)
end

local function GetCacheSize(): number
	return cacheCount
end

local function GetStaticCacheSize(): number
	return staticCacheCount
end

return {
	new = New,
	atlas = Atlas.atlas,
	GetCacheSize = GetCacheSize,
	GetStaticCacheSize = GetStaticCacheSize,
	GetAtlasCacheSize = Atlas.GetAtlasCacheSize,
}