local J={boolean={springType=LinearSpring.new;toIntermediate=function(J)return{J and 1 or 0}end,fromIntermediate=function(J)return J[1]>=.5 end},number={springType=LinearSpring.new;toIntermediate=function(J)return{J}end,fromIntermediate=function(J)return J[1]end},NumberRange={springType=LinearSpring.new,toIntermediate=function(J)return{J.Min,J.Max}end;fromIntermediate=function(J)return NumberRange.new(J[1],J[2])end};UDim={springType=LinearSpring.new;toIntermediate=function(J)return{J.Scale;J.Offset}end,fromIntermediate=function(J)return UDim.new(J[1],round(J[2]))end};UDim2={springType=LinearSpring.new,toIntermediate=function(J)local v=J.X local L=J.Y return{v.Scale;v.Offset,L.Scale,L.Offset}end;fromIntermediate=function(J)return UDim2.new(J[1],round(J[2]),J[3],round(J[4]))end};Vector2={springType=LinearSpring.new;toIntermediate=function(J)return{J.X;J.Y}end;fromIntermediate=function(J)return Vector2.new(J[1],J[2])end};Vector3={springType=LinearSpring.new;toIntermediate=function(J)return{J.X;J.Y;J.Z}end;fromIntermediate=function(J)return Vector3.new(J[1],J[2],J[3])end},Color3={springType=LinearSpring.new;toIntermediate=rgbToLuv,fromIntermediate=luvToRgb};ColorSequence={springType=LinearSpring.new,toIntermediate=function(J)local v=J.Keypoints local L=rgbToLuv(v[1].Value)local Y=rgbToLuv(v[#v].Value)return{L[1],L[2],L[3];Y[1];Y[2],Y[3]}end,fromIntermediate=function(J)return ColorSequence.new(luvToRgb({J[1];J[2],J[3]}),luvToRgb({J[4];J[5];J[6]}))end}}local v={}RunService.Heartbeat:Connect(function(J)for L,Y in v do for v,b in Y do if b:canSleep()then Y[v]=nil L[v]=b.rawTarget else L[v]=b:step(J)end end if not next(Y)then v[L]=nil end end end)local function L(J,v,L,Y)if not L:find(typeof(Y))then error(("\098\097\100 \097\114\103\117\109\101\110\116 \035\037\100 \116\111 \037\115 \040\037\115 \101\120\112\101\099\116\101\100\044 \103\111\116 \037\115\041"):format(J,v,L,typeof(Y)),3)end end local Y={}function Y.target(Y,b,m,N)if STRICT_TYPES then L(1,"\115\112\114\046\116\097\114\103\101\116","\073\110\115\116\097\110\099\101",Y)L(2,"\115\112\114\046\116\097\114\103\101\116","\110\117\109\098\101\114",b)L(3,"\115\112\114\046\116\097\114\103\101\116","\110\117\109\098\101\114",m)L(4,"\115\112\114\046\116\097\114\103\101\116","\116\097\098\108\101",N)end if b~=b or b<0 then error(("\101\120\112\101\099\116\101\100 \100\097\109\112\105\110\103 \114\097\116\105\111 \062\061 \048\059 \103\111\116 \037\046\050\102"):format(b),2)end if m~=m or m<0 then error(("\101\120\112\101\099\116\101\100 \117\110\100\097\109\112\101\100 \102\114\101\113\117\101\110\099\121 \062\061 \048\059 \103\111\116 \037\046\050\102"):format(m),2)end local S=v[Y]if not S then S={}v[Y]=S end for v,L in N do local N=Y[v]if STRICT_TYPES and typeof(L)~=typeof(N)then error(("\098\097\100 \112\114\111\112\101\114\116\121 \037\115 \116\111 \115\112\114\046\116\097\114\103\101\116 \040\037\115 \101\120\112\101\099\116\101\100\044 \103\111\116 \037\115\041"):format(v,typeof(N),typeof(L)),2)end local R=S[v]if not R then local Y=J[typeof(L)]if not Y then error("\117\110\115\117\112\112\111\114\116\101\100 \116\121\112\101\058 "..typeof(L),2)end R=Y.springType(b,m,N,Y,L)S[v]=R end R.d=b R.f=m R:setGoal(L)end end function Y.stop(J,Y)if STRICT_TYPES then L(1,"\115\112\114\046\115\116\111\112","\073\110\115\116\097\110\099\101",J)L(2,"\115\112\114\046\115\116\111\112","\115\116\114\105\110\103\124\110\105\108",Y)end if Y then local L=v[J]if L then L[Y]=nil end else v[J]=nil end end return table.freeze(Y)
-- transforms Roblox types into intermediate types, converting
-- between spaces as necessary to preserve perceptual linearity
local typeMetadata = {
	boolean = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value and 1 or 0 }
		end,

		fromIntermediate = function(value)
			return value[1] >= 0.5
		end,
	},

	number = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value }
		end,

		fromIntermediate = function(value)
			return value[1]
		end,
	},

	NumberRange = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value.Min, value.Max }
		end,

		fromIntermediate = function(value)
			return NumberRange.new(value[1], value[2])
		end,
	},

	UDim = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value.Scale, value.Offset }
		end,

		fromIntermediate = function(value)
			return UDim.new(value[1], round(value[2]))
		end,
	},

	UDim2 = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			local x = value.X
			local y = value.Y
			return { x.Scale, x.Offset, y.Scale, y.Offset }
		end,

		fromIntermediate = function(value)
			return UDim2.new(value[1], round(value[2]), value[3], round(value[4]))
		end,
	},

	Vector2 = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value.X, value.Y }
		end,

		fromIntermediate = function(value)
			return Vector2.new(value[1], value[2])
		end,
	},

	Vector3 = {
		springType = LinearSpring.new,

		toIntermediate = function(value)
			return { value.X, value.Y, value.Z }
		end,

		fromIntermediate = function(value)
			return Vector3.new(value[1], value[2], value[3])
		end,
	},

	Color3 = {
		springType = LinearSpring.new,
		toIntermediate = rgbToLuv,
		fromIntermediate = luvToRgb,
	},

	-- Only takes start and end keypoints
	ColorSequence = {
		springType = LinearSpring.new,
		toIntermediate = function(value)
			local keypoints = value.Keypoints

			local luv0 = rgbToLuv(keypoints[1].Value)
			local luv1 = rgbToLuv(keypoints[#keypoints].Value)

			return {
				luv0[1],
				luv0[2],
				luv0[3],
				luv1[1],
				luv1[2],
				luv1[3],
			}
		end,

		fromIntermediate = function(value)
			return ColorSequence.new(
				luvToRgb({ value[1], value[2], value[3] }),
				luvToRgb({ value[4], value[5], value[6] })
			)
		end,
	},
}

local springStates = {} -- {[instance] = {[property] = spring}

RunService.Heartbeat:Connect(function(dt)
	for instance, state in springStates do
		for propName, spring in state do
			if spring:canSleep() then
				state[propName] = nil
				instance[propName] = spring.rawTarget
			else
				instance[propName] = spring:step(dt)
			end
		end

		if not next(state) then
			springStates[instance] = nil
		end
	end
end)

local function assertType(argNum, fnName, expectedType, value)
	if not expectedType:find(typeof(value)) then
		error(("bad argument #%d to %s (%s expected, got %s)"):format(argNum, fnName, expectedType, typeof(value)), 3)
	end
end

local spr = {}

function spr.target(instance, dampingRatio, frequency, properties)
	if STRICT_TYPES then
		assertType(1, "spr.target", "Instance", instance)
		assertType(2, "spr.target", "number", dampingRatio)
		assertType(3, "spr.target", "number", frequency)
		assertType(4, "spr.target", "table", properties)
	end

	if dampingRatio ~= dampingRatio or dampingRatio < 0 then
		error(("expected damping ratio >= 0; got %.2f"):format(dampingRatio), 2)
	end

	if frequency ~= frequency or frequency < 0 then
		error(("expected undamped frequency >= 0; got %.2f"):format(frequency), 2)
	end

	local state = springStates[instance]

	if not state then
		state = {}
		springStates[instance] = state
	end

	for propName, propTarget in properties do
		local propValue = instance[propName]

		if STRICT_TYPES and typeof(propTarget) ~= typeof(propValue) then
			error(
				("bad property %s to spr.target (%s expected, got %s)"):format(
					propName,
					typeof(propValue),
					typeof(propTarget)
				),
				2
			)
		end

		local spring = state[propName]
		if not spring then
			local md = typeMetadata[typeof(propTarget)]

			if not md then
				error("unsupported type: " .. typeof(propTarget), 2)
			end

			spring = md.springType(dampingRatio, frequency, propValue, md, propTarget)
			state[propName] = spring
		end

		spring.d = dampingRatio
		spring.f = frequency
		spring:setGoal(propTarget)
	end
end

function spr.stop(instance, property)
	if STRICT_TYPES then
		assertType(1, "spr.stop", "Instance", instance)
		assertType(2, "spr.stop", "string|nil", property)
	end

	if property then
		local state = springStates[instance]
		if state then
			state[property] = nil
		end
	else
		springStates[instance] = nil
	end
end

return table.freeze(spr)
