local M={}local B=game:GetService("\084\101\120\116\083\101\114\118\105\099\101")local S=game:GetService("\082\117\110\083\101\114\118\105\099\101")local q=0 function getLayerCollector(M)if not M then return nil elseif M:IsA("\076\097\121\101\114\067\111\108\108\101\099\116\111\114")then return M elseif M and M.Parent then return getLayerCollector(M.Parent)else return nil end end function shallowCopy(M)local B={}for M,S in pairs(M)do B[M]=S end return B end function getColorFromString(M)if colors[M]then return colors[M]else local B,S,q=M:match("\040\037\100\043\041\044\040\037\100\043\041\044\040\037\100\043\041")return Color3.new(B/255,S/255,q/255)end end function getVector2FromString(M)local B,S=M:match("\040\037\100\043\041\044\040\037\100\043\041")return Vector2.new(B,S)end function M.New(T,M,Q,u)for M,B in pairs(M:GetChildren())do B:Destroy()end local x={}local D={}local I={}local j={}local W={}local R=0 local c=Instance.new("\084\101\120\116\076\097\098\101\108")local G=Instance.new("\073\109\097\103\101\076\097\098\101\108")local Y=getLayerCollector(M)local y,E,A,s,i,H function E(M,B)M=propertyShortcuts[M]or M if B=="\047"then if D[M]then B=D[M]else warn("\065\116\116\101\109\112\116 \116\111 \100\101\102\097\117\108\116 \060"..(M.."\062 \116\111 \118\097\108\117\101 \119\105\116\104 \110\111 \100\101\102\097\117\108\116"))end end if tonumber(B)then B=tonumber(B)elseif B=="\102\097\108\115\101"or B=="\116\114\117\101"then B=B=="\116\114\117\101"end x[M]=B if y(M,B)then elseif defaults[M]then elseif M=="\073\109\103"then i(B)else return false end return true end function y(M,B,S)local q local Q=false for S,u in pairs(S and{S}or{c;G})do local T=pcall(function()q=typeof(u[M])end)if T then if q=="\067\111\108\111\114\051"then u[M]=getColorFromString(B)elseif q=="\086\101\099\116\111\114\050"then u[M]=getVector2FromString(B)else u[M]=B end Q=true end end return Q end for M,B in pairs(defaults)do E(M,B)D[propertyShortcuts[M]or M]=x[propertyShortcuts[M]or M]end for M,B in pairs(u or{})do E(M,B)D[propertyShortcuts[M]or M]=x[propertyShortcuts[M]or M]end local function X()local B=Instance.new("\070\114\097\109\101")B.Name=string.format("\076\105\110\101\037\048\051\100",#I+1)B.Size=UDim2.new(0,0,0,0)B.BackgroundTransparency=1 local S=Instance.new("\070\114\097\109\101",B)S.Name="\067\111\110\116\097\105\110\101\114"S.Size=UDim2.new(0,0,0,0)S.BackgroundTransparency=1 if x.ContainerHorizontalAlignment=="\076\101\102\116"then S.AnchorPoint=Vector2.new(0,0)S.Position=UDim2.new(0,0,0,0)elseif x.ContainerHorizontalAlignment=="\067\101\110\116\101\114"then S.AnchorPoint=Vector2.new(.5,0)S.Position=UDim2.new(.5,0,0,0)elseif x.ContainerHorizontalAlignment=="\082\105\103\104\116"then S.AnchorPoint=Vector2.new(1,0)S.Position=UDim2.new(1,0,0,0)end B.Parent=M table.insert(I,B)j[#I]={}R=0 end X()local function d()if x.TextScaled==true then local B if x.TextScaleRelativeTo=="\083\099\114\101\101\110"then B=Y.AbsoluteSize.Y elseif x.TextScaleRelativeTo=="\070\114\097\109\101"then B=M.AbsoluteSize.Y end return math.min(x.TextScale*B,100)else return x.TextSize end end local function z(M)W[M]=shallowCopy(x)W[M].InitialSize=M.Size W[M].InitialPosition=M.Position W[M].InitialAnchorPoint=M.AnchorPoint end function A(B,S,q,Q)local u=I[#I]local T=tostring(x.TextYAlignment)if T=="\084\111\112"then B.Position=UDim2.new(0,R,0,0)B.AnchorPoint=Vector2.new(0,0)elseif T=="\067\101\110\116\101\114"then B.Position=UDim2.new(0,R,.5,0)B.AnchorPoint=Vector2.new(0,.5)elseif T=="\066\111\116\116\111\109"then B.Position=UDim2.new(0,R,1,0)B.AnchorPoint=Vector2.new(0,1)end R=R+q if R>M.AbsoluteSize.X and not(R==q)then B:Destroy()local M=j[#I][#j[#I]]if M:IsA("\084\101\120\116\076\097\098\101\108")and M.Text==" "then u.Container.Size=UDim2.new(0,(R-q)-M.Size.X.Offset,1,0)M:Destroy()table.remove(j[#I])end X()Q()else B.Size=UDim2.new(0,q,0,S)u.Container.Size=UDim2.new(0,R,1,0)u.Size=UDim2.new(1,0,0,math.max(u.Size.Y.Offset,S))B.Name=string.format("\071\114\111\117\112\037\048\051\100",#j[#I]+1)B.Parent=u.Container table.insert(j[#I],B)z(B)x.AnimateYield=0 end end function s(M)if M=="\n"then X()return elseif M==" "and R==0 then return end local S=d()local q=(B:GetTextSize(M,S,c.Font,Vector2.new(Y.AbsoluteSize.X,S))).X local Q=c:Clone()Q.TextScaled=false Q.TextSize=S Q.Text=M Q.TextTransparency=1 Q.TextStrokeTransparency=1 local u=0 for q=1,utf8.len(M),1 do local T=string.sub(M,q,q)local x=(B:GetTextSize(T,S,c.Font,Vector2.new(Y.AbsoluteSize.X,S))).X local D=c:Clone()D.Text=T D.TextScaled=false D.TextSize=S D.Position=UDim2.new(0,u,0,0)D.Size=UDim2.new(0,x,0,S)D.Name=string.format("\067\104\097\114\037\048\051\100",q)D.Parent=Q D.Visible=false z(D)u=u+x end A(Q,S,q,function()s(M)end)end function i(M)local B=d()local S=B local q=G:Clone()q.Image="\114\098\120\097\115\115\101\116\105\100\058\047\047"..(images[M]or M)q.Size=UDim2.new(0,B,0,S)q.Visible=false A(q,B,S,function()i(M)end)end function H(M)for M,B in pairs(M)do local S,q=string.match(B,"\060\040\046\043\041\061\040\046\043\041\062")if S and q then if not E(S,q)then warn("\067\111\117\108\100 \110\111\116 \097\112\112\108\121 \109\097\114\107\117\112\058 ",B)end else s(B)end end end local L=1 local e=utf8.len(Q)local V={}while L and L<=e do local M,B=string.find(Q,"\060\046-\062",L)local S,q=string.find(Q,"\091 \t\n\093",L)local u,T,x if M and(B and(not S or M<S))then u,T=M,B else u,T=S or e+1,q or e+1 x=true end local D=u>L and string.sub(Q,L,u-1)or nil local I=u<=e and string.sub(Q,u,T)or nil table.insert(V,D)if x then H(V)H({I})V={}else table.insert(V,I)end L=T+1 end H(V)local k=Instance.new("\085\073\076\105\115\116\076\097\121\111\117\116")k.HorizontalAlignment=x.ContainerHorizontalAlignment k.VerticalAlignment=x.ContainerVerticalAlignment k.Parent=M local o=0 local Z=M.AbsoluteSize.X local F=0 for M,B in pairs(I)do o=o+B.Size.Y.Offset local S=B.Container local q,Q if S.AnchorPoint.X==0 then q=S.Position.X.Offset Q=S.Size.X.Offset elseif S.AnchorPoint.X==.5 then q=B.AbsoluteSize.X/2-S.Size.X.Offset/2 Q=B.AbsoluteSize.X/2+S.Size.X.Offset/2 elseif S.AnchorPoint.X==1 then q=B.AbsoluteSize.X-S.Size.X.Offset Q=B.AbsoluteSize.X end Z=math.min(Z,q)F=math.max(F,Q)end q=q+1 local J=false local l="\084\101\120\116\065\110\105\109\097\116\105\111\110"..q local N={}local function U()if J then S:UnbindFromRenderStep(l)N={}return end local M=tick()for B=#N,1,-1 do local S=N[B]local q=S.Settings local Q=animationStyles[q.AnimateStyle]if not Q then warn("\078\111 \097\110\105\109\097\116\105\111\110 \115\116\121\108\101 \102\111\117\110\100 \102\111\114\058 ",q.AnimateStyle,"\044 \100\101\102\097\117\108\116\105\110\103 \116\111 \065\112\112\101\097\114")Q=animationStyles.Appear end local u=math.min((M-S.Start)/q.AnimateStyleTime,1)Q(S.Char,u,q)if u>=1 then table.remove(N,B)end end end local function b(M)M.Position=W[M].InitialPosition M.Size=W[M].InitialSize M.AnchorPoint=W[M].InitialAnchorPoint for B,S in pairs(W[M])do y(B,S,M)end end local function a(M,B)M.Visible=B for M,S in pairs(M:GetChildren())do S.Visible=B if B then b(S)end end if B and M:IsA("\073\109\097\103\101\076\097\098\101\108")then b(M)end end local function w()J=false S:BindToRenderStep(l,Enum.RenderPriority.Last.Value,U)local M local B local q local Q for M,B in pairs(j)do for M,B in pairs(B)do a(B,false)end end local function u(M,B)table.insert(N,{Char=M,Settings=B,Start=tick()})end local function T()if Q%q==0 and B>=0 then wait(B>0 and B or nil)end end for S,x in pairs(j)do for S,x in pairs(x)do local D=W[x]if not(D.AnimateStepGrouping==M)or not(D.AnimateStepFrequency==q)then Q=0 end M=D.AnimateStepGrouping B=D.AnimateStepTime q=D.AnimateStepFrequency if D.AnimateYield>0 then wait(D.AnimateYield)end if M=="\087\111\114\100"or M=="\065\108\108"then if x:IsA("\084\101\120\116\076\097\098\101\108")then x.Visible=true for M,B in pairs(x:GetChildren())do u(B,W[B])end else u(x,D)end if M=="\087\111\114\100"then Q=Q+1 T()end elseif M=="\076\101\116\116\101\114"then if x:IsA("\084\101\120\116\076\097\098\101\108")then x.Visible=true local M=x.Text for M=1,utf8.len(M),1 do local B=x[string.format("\067\104\097\114\037\048\051\100",M)]u(B,W[B])Q=Q+1 T()if J then return end end else u(x,D)Q=Q+1 T()end else warn("\073\110\118\097\108\105\100 \115\116\101\112 \103\114\111\117\112\105\110\103\058 ",M)end if J then return end end end while#N>0 do S.RenderStepped:Wait()end J=true end local t={}t.ContentSize=Vector2.new(F-Z,o)function t.Animate(B,M)if M then w()else(coroutine.wrap(w))()end end function t.Show(M)J=true for M,B in pairs(j)do for M,B in pairs(B)do a(B,true)end end end function t.Hide(M)J=true for M,B in pairs(j)do for M,B in pairs(B)do a(B,false)end end end return t end return M


local richText = {}
local textService = game:GetService("TextService")
local runService = game:GetService("RunService")
local animationCount = 0

function getLayerCollector(frame)
	if not frame then
		return nil
	elseif frame:IsA("LayerCollector") then
		return frame
	elseif frame and frame.Parent then
		return getLayerCollector(frame.Parent)
	else
		return nil
	end
end

function shallowCopy(tab)
	local ret = {}
	for key, value in pairs(tab) do
		ret[key] = value
	end
	return ret
end

function getColorFromString(value)
	if colors[value] then
		 return colors[value]
	else
		local r, g, b = value:match("(%d+),(%d+),(%d+)")
		return Color3.new(r / 255, g / 255, b / 255)
	end
end

function getVector2FromString(value)
	local x, y = value:match("(%d+),(%d+)")
	return Vector2.new(x, y)
end

function richText:New(frame, text, startingProperties)
	for _, v in pairs(frame:GetChildren()) do
		v:Destroy()
	end
	
	local properties = {}
	local defaultProperties = {}
	
	local lineFrames = {}
	local textFrames = {}
	local frameProperties = {}
	local linePosition = 0
	local textLabel = Instance.new("TextLabel")
	local imageLabel = Instance.new("ImageLabel")
	local layerCollector = getLayerCollector(frame)
	
	local applyProperty, applyMarkup, formatLabel, printText, printImage, printSeries	
	
	----- Apply properties / markups -----
	function applyMarkup(key, value)
		key = propertyShortcuts[key] or key
		if value == "/" then
			if defaultProperties[key] then
				value = defaultProperties[key]
			else
				warn("Attempt to default <"..key.."> to value with no default")
			end
		end
		if tonumber(value) then
			value = tonumber(value)
		elseif value == "false" or value == "true" then
			value = value == "true"
		end
		properties[key] = value
		
		if applyProperty(key, value) then
			-- Ok
		elseif defaults[key] then
			-- Ok
		elseif key == "Img" then
			printImage(value)
		else
			-- Unknown value
			return false	
		end
		return true
	end
	
	function applyProperty(name, value, frame)
		local propertyType
		local ret = false
		for _, label in pairs(frame and {frame} or {textLabel, imageLabel}) do
			local isProperty = pcall(function() propertyType = typeof(label[name]) end) -- is there a better way to check if it's a property?
			if isProperty then
				if propertyType == "Color3" then
					label[name] = getColorFromString(value)
				elseif propertyType == "Vector2" then
					label[name] = getVector2FromString(value)
				else
					label[name] = value	
				end
				ret = true	
			end
		end
		return ret
	end
	
	----- Set up default properties -----
	for name, value in pairs(defaults) do
		applyMarkup(name, value)
		defaultProperties[propertyShortcuts[name] or name] = properties[propertyShortcuts[name] or name]
	end
	for name, value in pairs(startingProperties or {}) do
		applyMarkup(name, value)
		defaultProperties[propertyShortcuts[name] or name] = properties[propertyShortcuts[name] or name]
	end
	
	----- Lines -----
	local function newLine()
		local lineFrame = Instance.new("Frame")
		lineFrame.Name = string.format("Line%03d", #lineFrames + 1)
		lineFrame.Size = UDim2.new(0, 0, 0, 0)
		lineFrame.BackgroundTransparency = 1		
		local textContainer = Instance.new("Frame", lineFrame)
		textContainer.Name = "Container"
		textContainer.Size = UDim2.new(0, 0, 0, 0)
		textContainer.BackgroundTransparency = 1
		if properties.ContainerHorizontalAlignment == "Left" then
			textContainer.AnchorPoint = Vector2.new(0, 0)
			textContainer.Position = UDim2.new(0, 0, 0, 0)
		elseif properties.ContainerHorizontalAlignment == "Center" then
			textContainer.AnchorPoint = Vector2.new(0.5, 0)
			textContainer.Position = UDim2.new(0.5, 0, 0, 0)
		elseif properties.ContainerHorizontalAlignment == "Right" then
			textContainer.AnchorPoint = Vector2.new(1, 0)
			textContainer.Position = UDim2.new(1, 0, 0, 0)
		end
		
		lineFrame.Parent = frame
		table.insert(lineFrames, lineFrame)
		textFrames[#lineFrames] = {}
		linePosition = 0
	end
	newLine()
	
	----- Get vertical size -----
	local function getTextSize()
		if properties.TextScaled == true then
			local relativeHeight
			if properties.TextScaleRelativeTo == "Screen" then
				relativeHeight = layerCollector.AbsoluteSize.Y
			elseif properties.TextScaleRelativeTo == "Frame" then
				relativeHeight = frame.AbsoluteSize.Y
			end
			return math.min(properties.TextScale * relativeHeight, 100)
		else
			return properties.TextSize
		end
	end
	
	----- Label printing -----
	local function addFrameProperties(frame)
		frameProperties[frame] = shallowCopy(properties)
		frameProperties[frame].InitialSize = frame.Size
		frameProperties[frame].InitialPosition = frame.Position
		frameProperties[frame].InitialAnchorPoint = frame.AnchorPoint
	end
	
	function formatLabel(newLabel, labelHeight, labelWidth, endOfLineCallback)
		local lineFrame = lineFrames[#lineFrames]
		
		local verticalAlignment = tostring(properties.TextYAlignment)
		if verticalAlignment  == "Top" then
			newLabel.Position = UDim2.new(0, linePosition, 0, 0)
			newLabel.AnchorPoint = Vector2.new(0, 0)
		elseif verticalAlignment  == "Center" then
			newLabel.Position = UDim2.new(0, linePosition, 0.5, 0)
			newLabel.AnchorPoint = Vector2.new(0, 0.5)
		elseif verticalAlignment  == "Bottom" then
			newLabel.Position = UDim2.new(0, linePosition, 1, 0)
			newLabel.AnchorPoint = Vector2.new(0, 1)
		end
		
		linePosition = linePosition + labelWidth
		if linePosition > frame.AbsoluteSize.X and not (linePosition == labelWidth) then
			-- Newline, get rid of label and redo on next line
			newLabel:Destroy()
			local lastLabel = textFrames[#lineFrames][#textFrames[#lineFrames]]
			if lastLabel:IsA("TextLabel") and lastLabel.Text == " " then -- get rid of trailing space
				lineFrame.Container.Size = UDim2.new(0, linePosition - labelWidth - lastLabel.Size.X.Offset, 1, 0)
				lastLabel:Destroy()
				table.remove(textFrames[#lineFrames])
			end
			newLine()
			endOfLineCallback()
		else
			-- Label is ok
			newLabel.Size = UDim2.new(0, labelWidth, 0, labelHeight)
			lineFrame.Container.Size = UDim2.new(0, linePosition, 1, 0)
			lineFrame.Size = UDim2.new(1, 0, 0, math.max(lineFrame.Size.Y.Offset, labelHeight))
			newLabel.Name = string.format("Group%03d", #textFrames[#lineFrames] + 1)
			newLabel.Parent = lineFrame.Container
			table.insert(textFrames[#lineFrames], newLabel)
			addFrameProperties(newLabel)
			properties.AnimateYield = 0
		end
	end
	
	function printText(text)
		if text == "\n" then
			newLine()
			return
		elseif text == " " and linePosition == 0 then
			return
		end
		
		local textSize = getTextSize()
		local textWidth = textService:GetTextSize(text, textSize, textLabel.Font, Vector2.new(layerCollector.AbsoluteSize.X, textSize)).X
		
		local newTextLabel = textLabel:Clone()
		newTextLabel.TextScaled = false
		newTextLabel.TextSize = textSize
		newTextLabel.Text = text -- This text is never actually displayed. We just use it as a reference for knowing what the group string is.
		newTextLabel.TextTransparency = 1
		newTextLabel.TextStrokeTransparency = 1
		
		-- Keep the real text in individual frames per character:
		local charPos = 0
		for i = 1, utf8.len(text), 1 do
			local character = string.sub(text, i, i)
			local characterWidth = textService:GetTextSize(character, textSize, textLabel.Font, Vector2.new(layerCollector.AbsoluteSize.X, textSize)).X
			local characterLabel = textLabel:Clone()
			characterLabel.Text = character
			characterLabel.TextScaled = false
			characterLabel.TextSize = textSize
			characterLabel.Position = UDim2.new(0, charPos, 0, 0)
			characterLabel.Size = UDim2.new(0, characterWidth, 0, textSize)
			characterLabel.Name = string.format("Char%03d", i)
			characterLabel.Parent = newTextLabel
			characterLabel.Visible = false
			addFrameProperties(characterLabel)
			charPos = charPos + characterWidth
		end

		formatLabel(newTextLabel, textSize, textWidth, function() printText(text) end)
	end
	
	function printImage(imageId)
		local imageHeight = getTextSize()
		local imageWidth = imageHeight -- Would be nice if we could get aspect ratio of image to get width properly.
		
		local newImageLabel = imageLabel:Clone()
		newImageLabel.Image = "rbxassetid://"..(images[imageId] or imageId)
		newImageLabel.Size = UDim2.new(0, imageHeight, 0, imageWidth)
		newImageLabel.Visible = false

		formatLabel(newImageLabel, imageHeight, imageWidth, function() printImage(imageId) end)
	end
	
	function printSeries(labelSeries)	
		for _, t in pairs(labelSeries) do
			local markupKey, markupValue = string.match(t, "<(.+)=(.+)>")
			if markupKey and markupValue then
				if not applyMarkup(markupKey, markupValue) then
					warn("Could not apply markup: ", t)
				end			
			else
				printText(t)
			end
		end	
	end
	
	----- Text traversal + parsing -----
	local textPos = 1
	local textLength = utf8.len(text)
	local labelSeries = {}
	
	while textPos and textPos <= textLength do
		local nextMarkupStart, nextMarkupEnd = string.find(text, "<.->", textPos)
		local nextSpaceStart, nextSpaceEnd = string.find(text, "[ \t\n]", textPos)
		
		local nextBreakStart, nextBreakEnd, breakIsWhitespace
		if nextMarkupStart and nextMarkupEnd and (not nextSpaceStart or nextMarkupStart < nextSpaceStart) then
			nextBreakStart, nextBreakEnd = nextMarkupStart, nextMarkupEnd
		else
			nextBreakStart, nextBreakEnd = nextSpaceStart or textLength + 1, nextSpaceEnd or textLength + 1
			breakIsWhitespace = true
		end
		
		local nextWord = nextBreakStart > textPos and string.sub(text, textPos, nextBreakStart - 1) or nil
		local nextBreak = nextBreakStart <= textLength and string.sub(text, nextBreakStart, nextBreakEnd) or nil
		table.insert(labelSeries, nextWord)
		
		if breakIsWhitespace then
			printSeries(labelSeries)
			printSeries({nextBreak})
			labelSeries = {}
		else
			table.insert(labelSeries, nextBreak)
		end
		
		textPos = nextBreakEnd + 1
		--textPos = utf8.offset(text, 2, nextBreakEnd)
	end
	printSeries(labelSeries)

	----- Alignment layout -----
	local listLayout = Instance.new("UIListLayout")
	listLayout.HorizontalAlignment = properties.ContainerHorizontalAlignment
	listLayout.VerticalAlignment = properties.ContainerVerticalAlignment
	listLayout.Parent = frame
	
	----- Calculate content size -----
	local contentHeight = 0
	local contentLeft = frame.AbsoluteSize.X
	local contentRight = 0
	for _, lineFrame in pairs(lineFrames) do
		contentHeight = contentHeight + lineFrame.Size.Y.Offset
		local container = lineFrame.Container
		local left, right
		if container.AnchorPoint.X == 0 then
			left = container.Position.X.Offset
			right = container.Size.X.Offset
		elseif container.AnchorPoint.X == 0.5 then
			left = lineFrame.AbsoluteSize.X / 2 - container.Size.X.Offset / 2
			right = lineFrame.AbsoluteSize.X / 2 + container.Size.X.Offset / 2
		elseif container.AnchorPoint.X == 1 then
			left = lineFrame.AbsoluteSize.X - container.Size.X.Offset
			right = lineFrame.AbsoluteSize.X
		end
		contentLeft = math.min(contentLeft, left)
		contentRight = math.max(contentRight, right)
	end
	
	----- Animation -----
	animationCount = animationCount + 1
	local animationDone = false
	local animationRenderstepBinding = "TextAnimation"..animationCount
	local animateQueue = {}
	
	local function updateAnimations()
		if animationDone then
			runService:UnbindFromRenderStep(animationRenderstepBinding)
			animateQueue = {}
			return
		end
		local t = tick()
		for i = #animateQueue, 1, -1 do
			local set = animateQueue[i]
			local properties = set.Settings
			local animateStyle = animationStyles[properties.AnimateStyle]
			if not animateStyle then
				warn("No animation style found for: ", properties.AnimateStyle, ", defaulting to Appear")
				animateStyle = animationStyles.Appear
			end
			local animateAlpha = math.min((t - set.Start) / properties.AnimateStyleTime, 1)
			animateStyle(set.Char, animateAlpha, properties) 
			if animateAlpha >= 1 then
				table.remove(animateQueue, i)
			end
		end
	end
	
	local function setFrameToDefault(frame)
		frame.Position = frameProperties[frame].InitialPosition
		frame.Size = frameProperties[frame].InitialSize
		frame.AnchorPoint = frameProperties[frame].InitialAnchorPoint
		for name, value in pairs(frameProperties[frame]) do
			applyProperty(name, value, frame)
		end
	end

	local function setGroupVisible(frame, visible)
		frame.Visible = visible
		for _, v in pairs(frame:GetChildren()) do
			v.Visible = visible
			if visible then
				setFrameToDefault(v)
			end
		end
		if visible and frame:IsA("ImageLabel") then
			setFrameToDefault(frame)
		end
	end

	local function animate()
		animationDone = false
		runService:BindToRenderStep(animationRenderstepBinding, Enum.RenderPriority.Last.Value, updateAnimations)

		local stepGrouping
		local stepTime
		local stepFrequency
		local numAnimated
		
		-- Make everything invisible to start
		for lineNum, list in pairs(textFrames) do
			for _, frame in pairs(list) do
				setGroupVisible(frame, false)
			end
		end
	
		local function animateCharacter(char, properties)
			table.insert(animateQueue, {Char = char, Settings = properties, Start = tick()})
		end
		
		local function yield()
			if numAnimated % stepFrequency == 0 and stepTime >= 0 then
				wait(stepTime > 0 and stepTime or nil)
			end
		end
		
		for lineNum, list in pairs(textFrames) do
			for _, frame in pairs(list) do
				local properties = frameProperties[frame]
				if not (properties.AnimateStepGrouping  == stepGrouping) or not (properties.AnimateStepFrequency == stepFrequency) then
					numAnimated = 0
				end
				stepGrouping = properties.AnimateStepGrouping
				stepTime = properties.AnimateStepTime
				stepFrequency = properties.AnimateStepFrequency
			
				if properties.AnimateYield > 0 then
					wait(properties.AnimateYield)
				end

				if stepGrouping == "Word" or stepGrouping == "All" then
					--if not (frame:IsA("TextLabel") and (frame.Text == " ")) then
						if frame:IsA("TextLabel") then
							frame.Visible = true
							for _, v in pairs(frame:GetChildren()) do
								animateCharacter(v, frameProperties[v])
							end
						else
							animateCharacter(frame, properties)
						end
						if stepGrouping == "Word" then
							numAnimated = numAnimated + 1
							yield()
						end
					--end
				elseif stepGrouping == "Letter" then
					if frame:IsA("TextLabel") --[[and not (frame.Text == " ") ]]then
						frame.Visible = true
						local text = frame.Text
						for i = 1, utf8.len(text), 1 do
							local v = frame[string.format("Char%03d", i)]
							animateCharacter(v, frameProperties[v])
							numAnimated = numAnimated + 1
							yield()
							if animationDone then
								return
							end
						end
					else
						animateCharacter(frame, properties)
						numAnimated = numAnimated + 1
						yield()
					end
				else
					warn("Invalid step grouping: ", stepGrouping)
				end
				
				if animationDone then
					return
				end
			end
		end
			
		while #animateQueue > 0 do
			runService.RenderStepped:Wait()
		end
		
		animationDone = true
	end
	

	----- Return object API -----
	local textObject = {}
	
	textObject.ContentSize = Vector2.new(contentRight - contentLeft, contentHeight)
	
	function textObject:Animate(yield)
		if yield then
			animate()
		else
			coroutine.wrap(animate)()
		end
	end
	
	function textObject:Show()
		animationDone = true
		for lineNum, list in pairs(textFrames) do
			for _, frame in pairs(list) do
				setGroupVisible(frame, true)
			end
		end
	end
	
	function textObject:Hide()
		animationDone = true
		for lineNum, list in pairs(textFrames) do
			for _, frame in pairs(list) do
				setGroupVisible(frame, false)
			end
		end
	end
	
	return textObject
end


return richText
