init.lua 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880
  1. --[[
  2. Rich text markup with support for modifying any TextLabel property per character + inline images + entrance animations
  3. Written by Defaultio ~ August 30 2017
  4. Changes:
  5. October 21 2017 - Unicode support added thanks to Tiffany Bennett - https://gist.github.com/tiffany352/ccb3559738f4e8d4152d940126998c41
  6. January 29 2017 - Added finishAnimate parameter to RichTextObject:Show() function
  7. - Fixed bug on iOS devices causing some characters to fail to appear. Using TextWrapped = false on character labels fixed this, thanks Buildthomas.
  8. TODO:
  9. - exit animations
  10. - emphasis animations
  11. - support for inline buttons
  12. - sounds for each text step
  13. - markup events that will fire a callback provided in the constructor when the text animation is reached, use for character animations, etc
  14. Let me know if these features will be useful and I'll add them more quickly.
  15. ___________________________________________________________________________________________________________________
  16. API:
  17. Constructor:
  18. RichText:New(GuiObject frame, String text, Dictionary startingProperties = {}, Boolean allowOverflow = true)
  19. frame: the parent frame which will be populated with text
  20. text: self explanitory
  21. startingProperties: a dictionary of what the default text properties should be
  22. allowOverflow: if false, text will stop rendering when it fills the vertical height of the frame. To continue the text in another frame, see RichText:ContinueOverflow() below...
  23. returns: richText object
  24. RichText:ContinueOverflow(GuiObject nextFrame, RichTextObject previousRichTextObject)
  25. nextFrame: the parent frame that text will continue into
  26. previousRichTextObject: the previous RichTextObject that is being overflown from.
  27. returns: richTextObject
  28. RichText Object: (returned by constructor)
  29. RichTextObject:Animate(doYield = false)
  30. Will run the animation. If doYield is true, the thread will yield until the animation is complete. Else, it will wrap the animation function in a coroutine
  31. RichTextObject:Show(finishAnimation = false)
  32. Shows the entirety of the text body. Will interrupt and stop the animation if it's running. if finishAnimation is true, the remaining text will animate in instead of appearing instantly.
  33. RichTextObject:Hide()
  34. Hides the text body. Will interrupt and stop the animation if it's running. The animation can be replayed after hiding.
  35. Vector2 RichTextObject.ContentSize
  36. Content size in pixels
  37. Boolean RichTextObject.Overflown
  38. If allowOverflow was false, this value shows if the text is overflown or if it fit in the frame. If overflown, use RichText:ContinueOverflow to continue into a new frame.
  39. ___________________________________________________________________________________________________________________
  40. USAGE:
  41. The text supplied in the constructor can be any text string. Insert a markup modifier by including <MarkupKey=MarkupValue>. No spaces.
  42. Examples of what this looks like include:
  43. <Font=ArialBold> --Set the font to ArialBold
  44. <Img=639588687> -- Insert an inline image with this ID
  45. <AnimateStepTime=0.4> -- Set the animate step time to 0.4 seconds.
  46. <AnimateYield=1> -- Yield for one second at this point in the animation
  47. <TextColor3=1,0,0> -- Set text color to red
  48. <Color=Red> -- Equivalent to above. The shortcut for the property name is defined in the propertyShortcuts table, and the color shortcut is defined in the colors table.
  49. After you set any markup value, you can revert it back to default later by setting it to "/". For example:
  50. <Font=/>
  51. <AnimateStepTime=/>
  52. <Color=/>
  53. Default values are defined by values in the "default" table below, or by values supplied in the startingProperties dictionary when the object is constructed.
  54. Currently does not support escapement characters for < and >, so you can't use these characters in the text string.
  55. To when using RichText:ContinueOverflow, calling Animate(), Show(), or Hide() on the initial RichText object will pass this call onto subsequent overflown rich text objects, so
  56. only a call to the first object is neccessary. See example.
  57. ___________________________________________________________________________________________________________________
  58. Example code:
  59. local richText = require(richTextModule)
  60. local text = "Hello world!\nLine two! <AnimateDelay=1><Img=Thinking>"
  61. local textObject = richText:New(frame, text)
  62. textObject:Animate(true)
  63. print("Animation done!")
  64. Example string 1: Basic
  65. local text = "<Font=SourceSansBold><TextScale=0.3>Oh!<TextScale=/><Font=/><AnimateYield=1> I didn't see you there<AnimateStepFrequency=1><AnimateStepTime=0.4> . . .<AnimateStepFrequency=/><AnimateStepTime=/>\n I wasn't expecting <Color=255,0,0>you<Color=/>. Please forgive the state of my room.<AnimateYield=1><Img=639588687>"
  66. --This yields this result: https://twitter.com/Defaultio/status/903094769617747968
  67. Example string 2: Wind Waker
  68. Insert the WindWakerExample ScreenGui in this module into StarterGui.
  69. Insert this module into WindWakerExample.
  70. Ensure WindWakerExample.LocalText is not Disabled.
  71. -- This yields this result: https://twitter.com/Defaultio/status/903138250054709248
  72. Example string 3: Text-in animations
  73. local text = "This text is about to be <Color=Green><AnimateStyle=Wiggle><AnimateStepFrequency=1><AnimateStyleTime=2>wiggly<AnimateStyle=/><AnimateStepFrequency=/><AnimateStyleTime=/><Color=/>!<AnimateYield=1.5>\nIt can also be <Color=Red><AnimateStyle=Fade><AnimateStepFrequency=1><AnimateStyleTime=0.5>fadey fadey<AnimateStyle=/><AnimateStepFrequency=/><AnimateStyleTime=/><Color=/>!<AnimateYield=1>\n<AnimateStyle=Rainbow><AnimateStyleTime=2>Or rainbow!!! :O<AnimateStyle=/><AnimateStyleTime=/><AnimateYield=1>\n<AnimateStyle=Swing><AnimateStyleTime=3>Make custom animations!"
  74. -- This yields this result: https://twitter.com/Defaultio/status/903346975688425472
  75. Example string 4: Variable text justification per line
  76. local text = "Have you ever <Color=Red>thought<Color=/><AnimateStepFrequency=1><AnimateStepTime=0.4> . . .<AnimateStepFrequency=/><AnimateStepTime=/><AnimateYield=1><ContainerHorizontalAlignment=Center>\n<TextScale=0.5><AnimateStyle=Rainbow><AnimateStyleTime=2.5><Img=Thinking><AnimateStyle=/><TextScale=/><AnimateYield=3><ContainerHorizontalAlignment=Right>\n<Color=Green><AnimateStyle=Spin><AnimateStyleTime=1.5>Wow<AnimateStyle=/><Color=/>!"
  77. -- This yields this result: https://twitter.com/Defaultio/status/903381787467956224
  78. Example 5: Overflowing
  79. Insert the OverflowingExample ScreenGui in this module into StarterGui.
  80. Inert this module into the OverflowingExample ScreenGui.
  81. Ensure OverflowingExample.LocalText is not disabled.
  82. -- This yields this result: https://twitter.com/Defaultio/status/918619989107621888
  83. ___________________________________________________________________________________________________________________
  84. --]]
  85. local richText = {}
  86. --------- SHORTCUTS ---------
  87. -- Color shortcuts: you can use these strings instead of full property names
  88. local propertyShortcuts = {}
  89. propertyShortcuts.Color = "TextColor3"
  90. propertyShortcuts.StrokeColor = "TextStrokeColor3"
  91. propertyShortcuts.ImageColor = "ImageColor3"
  92. -- Color shortcuts: you can use these strings instead of defining exact color values
  93. richText.ColorShortcuts = {}
  94. richText.ColorShortcuts.White = Color3.new(1, 1, 1)
  95. richText.ColorShortcuts.Black = Color3.new(0, 0, 0)
  96. richText.ColorShortcuts.Red = Color3.new(1, 0.3, 0.4)
  97. richText.ColorShortcuts.Green = Color3.new(0.4, 1, 0.4)
  98. richText.ColorShortcuts.Blue = Color3.new(0.4, 0.4, 1)
  99. richText.ColorShortcuts.Cyan = Color3.new(0.4, 0.85, 1)
  100. richText.ColorShortcuts.Orange = Color3.new(1, 0.5, 0.2)
  101. richText.ColorShortcuts.Yellow = Color3.new(1, 0.9, 0.2)
  102. -- Image shortcuts: you can use these string instead of using image ids
  103. richText.ImageShortcuts = {}
  104. richText.ImageShortcuts.Eggplant = 639588687
  105. richText.ImageShortcuts.Thinking = 955646496
  106. richText.ImageShortcuts.Sad = 947900188
  107. richText.ImageShortcuts.Happy = 414889555
  108. richText.ImageShortcuts.Despicable = 711674643
  109. --------- DEFAULTS ---------
  110. local defaults = {}
  111. --Text alignment default properties
  112. defaults.ContainerHorizontalAlignment = "Left" -- Align,ent of text within frame container
  113. defaults.ContainerVerticalAlignment = "Top"
  114. defaults.TextYAlignment = "Bottom" -- Alignment of the text on the line, only makes a difference if the line has variable text sizes
  115. -- Text size default properties
  116. defaults.TextScaled = true
  117. defaults.TextScaleRelativeTo = "Frame" -- "Frame" or "Screen" If Frame, will scale relative to vertical size of the parent frame. If Screen, will scale relative to vertical size of the ScreenGui.
  118. defaults.TextScale = 1 -- If you want the frame to have a nominal count of n lines of text, make this value 1 / n. For four lines, 1 / 4 = 0.25.
  119. defaults.TextSize = 20 -- Only applicable if TextScaled = false
  120. -- TextLabel default properties
  121. defaults.Font = "SourceSans"
  122. defaults.TextColor3 = "White"
  123. defaults.TextStrokeColor3 = "Black"
  124. defaults.TextTransparency = 0
  125. defaults.TextStrokeTransparency = 1
  126. defaults.BackgroundTransparency = 1
  127. defaults.BorderSizePixel = 0
  128. -- Image label default properties
  129. defaults.ImageColor3 = "White"
  130. defaults.ImageTransparency = 0
  131. defaults.ImageRectOffset = "0,0"
  132. defaults.ImageRectSize = "0,0"
  133. -- Text animation default properties
  134. -- character appearance timing:
  135. defaults.AnimateStepTime = 0 -- Seconds between newframes
  136. defaults.AnimateStepGrouping = "Letter" -- "Word" or "Letter" or "All"
  137. defaults.AnimateStepFrequency = 3 -- How often to step, 1 is all, 2 is step in pairs, 3 is every three, etc.
  138. -- yielding:
  139. defaults.AnimateYield = 0 -- Set this markup to yield
  140. -- entrance style parameters:
  141. defaults.AnimateStyle = "Fade"
  142. defaults.AnimateStyleTime = 0.1 -- How long it takes for an entrance style to fully execute
  143. defaults.AnimateStyleNumPeriods = 3 -- Used differently for each entrance style
  144. defaults.AnimateStyleAmplitude = 0.5 -- Used differently for each entrance style
  145. --------- ENTRANCE ANIMATION FUNCTIONS ---------
  146. -- These are functions responsible for animating how text enters. The functions are passed:
  147. -- characters: A list of the characters to be animated.
  148. -- animateAlpha: A value of 0 - 1 that represents the lifetime of the animation
  149. -- properties: A dictionary of all the properties at that character, including InitialSize and InitialPosition
  150. local animationStyles = {}
  151. function animationStyles.Appear(character)
  152. character.Visible = true
  153. end
  154. function animationStyles.Fade(character, animateAlpha, properties)
  155. character.Visible = true
  156. if character:IsA("TextLabel") then
  157. character.TextTransparency = 1 - (animateAlpha * (1 - properties.TextTransparency))
  158. elseif character:IsA("ImageLabel") then
  159. character.ImageTransparency = 1 - (animateAlpha * (1 - properties.ImageTransparency))
  160. end
  161. end
  162. function animationStyles.Wiggle(character, animateAlpha, properties)
  163. character.Visible = true
  164. local amplitude = properties.InitialSize.Y.Offset * (1 - animateAlpha) * properties.AnimateStyleAmplitude
  165. character.Position = properties.InitialPosition + UDim2.new(0, 0, 0, math.sin(animateAlpha * math.pi * 2 * properties.AnimateStyleNumPeriods) * amplitude / 2)
  166. end
  167. function animationStyles.Swing(character, animateAlpha, properties)
  168. character.Visible = true
  169. local amplitude = 90 * (1 - animateAlpha) * properties.AnimateStyleAmplitude
  170. character.Rotation = math.sin(animateAlpha * math.pi * 2 * properties.AnimateStyleNumPeriods) * amplitude
  171. end
  172. function animationStyles.Spin(character, animateAlpha, properties)
  173. character.Visible = true
  174. character.Position = properties.InitialPosition + UDim2.new(0, properties.InitialSize.X.Offset / 2, 0, properties.InitialSize.Y.Offset / 2)
  175. character.AnchorPoint = Vector2.new(0.5, 0.5)
  176. character.Rotation = animateAlpha * properties.AnimateStyleNumPeriods * 360
  177. end
  178. function animationStyles.Rainbow(character, animateAlpha, properties)
  179. character.Visible = true
  180. local rainbowColor = Color3.fromHSV(animateAlpha * properties.AnimateStyleNumPeriods % 1, 1, 1)
  181. if character:IsA("TextLabel") then
  182. local initialColor = getColorFromString(properties.TextColor3)
  183. character.TextColor3 = Color3.new(rainbowColor.r + animateAlpha * (initialColor.r - rainbowColor.r), rainbowColor.g + animateAlpha * (initialColor.g - rainbowColor.g), rainbowColor.b + animateAlpha * (initialColor.b - rainbowColor.b))
  184. else
  185. local initialColor = getColorFromString(properties.ImageColor3)
  186. character.ImageColor3 = Color3.new(rainbowColor.r + animateAlpha * (initialColor.r - rainbowColor.r), rainbowColor.g + animateAlpha * (initialColor.g - rainbowColor.g), rainbowColor.b + animateAlpha * (initialColor.b - rainbowColor.b))
  187. end
  188. end
  189. --------- MODULE BEGIN ---------
  190. local textService = game:GetService("TextService")
  191. local runService = game:GetService("RunService")
  192. local animationCount = 0
  193. function getLayerCollector(frame)
  194. if not frame then
  195. return nil
  196. elseif frame:IsA("LayerCollector") then
  197. return frame
  198. elseif frame and frame.Parent then
  199. return getLayerCollector(frame.Parent)
  200. else
  201. return nil
  202. end
  203. end
  204. function shallowCopy(tab)
  205. local ret = {}
  206. for key, value in pairs(tab) do
  207. ret[key] = value
  208. end
  209. return ret
  210. end
  211. function getColorFromString(value)
  212. if richText.ColorShortcuts[value] then
  213. return richText.ColorShortcuts[value]
  214. else
  215. local r, g, b = value:match("(%d+),(%d+),(%d+)")
  216. return Color3.new(r / 255, g / 255, b / 255)
  217. end
  218. end
  219. function getVector2FromString(value)
  220. local x, y = value:match("(%d+),(%d+)")
  221. return Vector2.new(x, y)
  222. end
  223. function setHorizontalAlignment(frame, alignment)
  224. if alignment == "Left" then
  225. frame.AnchorPoint = Vector2.new(0, 0)
  226. frame.Position = UDim2.new(0, 0, 0, 0)
  227. elseif alignment == "Center" then
  228. frame.AnchorPoint = Vector2.new(0.5, 0)
  229. frame.Position = UDim2.new(0.5, 0, 0, 0)
  230. elseif alignment == "Right" then
  231. frame.AnchorPoint = Vector2.new(1, 0)
  232. frame.Position = UDim2.new(1, 0, 0, 0)
  233. end
  234. end
  235. function richText:New(frame, text, startingProperties, allowOverflow, prevTextObject)
  236. for _, v in pairs(frame:GetChildren()) do
  237. v:Destroy()
  238. end
  239. if allowOverflow == nil then
  240. allowOverflow = true
  241. end
  242. local properties = {}
  243. local defaultProperties = {}
  244. if prevTextObject then
  245. text = prevTextObject.Text
  246. startingProperties = prevTextObject.StartingProperties
  247. end
  248. local lineFrames = {}
  249. local textFrames = {}
  250. local frameProperties = {}
  251. local linePosition = 0
  252. local overflown = false
  253. local textLabel = Instance.new("TextLabel")
  254. local imageLabel = Instance.new("ImageLabel")
  255. local layerCollector = getLayerCollector(frame)
  256. local applyProperty, applyMarkup, formatLabel, printText, printImage, printSeries
  257. ----- Apply properties / markups -----
  258. function applyMarkup(key, value)
  259. key = propertyShortcuts[key] or key
  260. if value == "/" then
  261. if defaultProperties[key] then
  262. value = defaultProperties[key]
  263. else
  264. warn("Attempt to default <"..key.."> to value with no default")
  265. end
  266. end
  267. if tonumber(value) then
  268. value = tonumber(value)
  269. elseif value == "false" or value == "true" then
  270. value = value == "true"
  271. end
  272. properties[key] = value
  273. if applyProperty(key, value) then
  274. -- Ok
  275. elseif key == "ContainerHorizontalAlignment" and lineFrames[#lineFrames] then
  276. setHorizontalAlignment(lineFrames[#lineFrames].Container, value)
  277. elseif defaults[key] then
  278. -- Ok
  279. elseif key == "Img" then
  280. printImage(value)
  281. else
  282. -- Unknown value
  283. return false
  284. end
  285. return true
  286. end
  287. function applyProperty(name, value, frame)
  288. local propertyType
  289. local ret = false
  290. for _, label in pairs(frame and {frame} or {textLabel, imageLabel}) do
  291. local isProperty = pcall(function() propertyType = typeof(label[name]) end) -- is there a better way to check if it's a property?
  292. if isProperty then
  293. if propertyType == "Color3" then
  294. label[name] = getColorFromString(value)
  295. elseif propertyType == "Vector2" then
  296. label[name] = getVector2FromString(value)
  297. else
  298. label[name] = value
  299. end
  300. ret = true
  301. end
  302. end
  303. return ret
  304. end
  305. ----- Set up default properties -----
  306. for name, value in pairs(defaults) do
  307. applyMarkup(name, value)
  308. defaultProperties[propertyShortcuts[name] or name] = properties[propertyShortcuts[name] or name]
  309. end
  310. for name, value in pairs(startingProperties or {}) do
  311. applyMarkup(name, value)
  312. defaultProperties[propertyShortcuts[name] or name] = properties[propertyShortcuts[name] or name]
  313. end
  314. if prevTextObject then
  315. properties = prevTextObject.OverflowPickupProperties
  316. for name, value in pairs(properties) do
  317. applyMarkup(name, value)
  318. end
  319. end
  320. ----- Get vertical size -----
  321. local function getTextSize()
  322. if properties.TextScaled == true then
  323. local relativeHeight
  324. if properties.TextScaleRelativeTo == "Screen" then
  325. relativeHeight = layerCollector.AbsoluteSize.Y
  326. elseif properties.TextScaleRelativeTo == "Frame" then
  327. relativeHeight = frame.AbsoluteSize.Y
  328. end
  329. return math.min(properties.TextScale * relativeHeight, 100)
  330. else
  331. return properties.TextSize
  332. end
  333. end
  334. ----- Lines -----
  335. local contentHeight = 0
  336. local function newLine()
  337. local lastLineFrame = lineFrames[#lineFrames]
  338. if lastLineFrame then
  339. contentHeight = contentHeight + lastLineFrame.Size.Y.Offset
  340. if not allowOverflow and contentHeight + getTextSize() > frame.AbsoluteSize.Y then
  341. overflown = true
  342. return
  343. end
  344. end
  345. local lineFrame = Instance.new("Frame")
  346. lineFrame.Name = string.format("Line%03d", #lineFrames + 1)
  347. lineFrame.Size = UDim2.new(0, 0, 0, 0)
  348. lineFrame.BackgroundTransparency = 1
  349. local textContainer = Instance.new("Frame", lineFrame)
  350. textContainer.Name = "Container"
  351. textContainer.Size = UDim2.new(0, 0, 0, 0)
  352. textContainer.BackgroundTransparency = 1
  353. setHorizontalAlignment(textContainer, properties.ContainerHorizontalAlignment)
  354. lineFrame.Parent = frame
  355. table.insert(lineFrames, lineFrame)
  356. textFrames[#lineFrames] = {}
  357. linePosition = 0
  358. end
  359. newLine()
  360. ----- Label printing -----
  361. local function addFrameProperties(frame)
  362. frameProperties[frame] = shallowCopy(properties)
  363. frameProperties[frame].InitialSize = frame.Size
  364. frameProperties[frame].InitialPosition = frame.Position
  365. frameProperties[frame].InitialAnchorPoint = frame.AnchorPoint
  366. end
  367. function formatLabel(newLabel, labelHeight, labelWidth, endOfLineCallback)
  368. local lineFrame = lineFrames[#lineFrames]
  369. local verticalAlignment = tostring(properties.TextYAlignment)
  370. if verticalAlignment == "Top" then
  371. newLabel.Position = UDim2.new(0, linePosition, 0, 0)
  372. newLabel.AnchorPoint = Vector2.new(0, 0)
  373. elseif verticalAlignment == "Center" then
  374. newLabel.Position = UDim2.new(0, linePosition, 0.5, 0)
  375. newLabel.AnchorPoint = Vector2.new(0, 0.5)
  376. elseif verticalAlignment == "Bottom" then
  377. newLabel.Position = UDim2.new(0, linePosition, 1, 0)
  378. newLabel.AnchorPoint = Vector2.new(0, 1)
  379. end
  380. linePosition = linePosition + labelWidth
  381. if linePosition > frame.AbsoluteSize.X and not (linePosition == labelWidth) then
  382. -- Newline, get rid of label and retry it on the next line
  383. newLabel:Destroy()
  384. local lastLabel = textFrames[#lineFrames][#textFrames[#lineFrames]]
  385. if lastLabel:IsA("TextLabel") and lastLabel.Text == " " then -- get rid of trailing space
  386. lineFrame.Container.Size = UDim2.new(0, linePosition - labelWidth - lastLabel.Size.X.Offset, 1, 0)
  387. lastLabel:Destroy()
  388. table.remove(textFrames[#lineFrames])
  389. end
  390. newLine()
  391. endOfLineCallback()
  392. else
  393. -- Label is ok
  394. newLabel.Size = UDim2.new(0, labelWidth, 0, labelHeight)
  395. lineFrame.Container.Size = UDim2.new(0, linePosition, 1, 0)
  396. lineFrame.Size = UDim2.new(1, 0, 0, math.max(lineFrame.Size.Y.Offset, labelHeight))
  397. newLabel.Name = string.format("Group%03d", #textFrames[#lineFrames] + 1)
  398. newLabel.Parent = lineFrame.Container
  399. table.insert(textFrames[#lineFrames], newLabel)
  400. addFrameProperties(newLabel)
  401. properties.AnimateYield = 0
  402. end
  403. end
  404. function printText(text)
  405. if text == "\n" then
  406. newLine()
  407. return
  408. elseif text == " " and linePosition == 0 then
  409. return -- no leading spaces
  410. end
  411. local textSize = getTextSize()
  412. local textWidth = textService:GetTextSize(text, textSize, textLabel.Font, Vector2.new(layerCollector.AbsoluteSize.X, textSize)).X
  413. local newTextLabel = textLabel:Clone()
  414. newTextLabel.TextScaled = false
  415. newTextLabel.TextSize = textSize
  416. newTextLabel.Text = text -- This text is never actually displayed. We just use it as a reference for knowing what the group string is.
  417. newTextLabel.TextTransparency = 1
  418. newTextLabel.TextStrokeTransparency = 1
  419. newTextLabel.TextWrapped = false
  420. -- Keep the real text in individual frames per character:
  421. local charPos = 0
  422. local i = 1
  423. for first, last in utf8.graphemes(text) do
  424. local character = string.sub(text, first, last)
  425. local characterWidth = textService:GetTextSize(character, textSize, textLabel.Font, Vector2.new(layerCollector.AbsoluteSize.X, textSize)).X
  426. local characterLabel = textLabel:Clone()
  427. characterLabel.Text = character
  428. characterLabel.TextScaled = false
  429. characterLabel.TextSize = textSize
  430. characterLabel.Position = UDim2.new(0, charPos, 0, 0)
  431. characterLabel.Size = UDim2.new(0, characterWidth, 0, textSize)
  432. characterLabel.Name = string.format("Char%03d", i)
  433. characterLabel.Parent = newTextLabel
  434. characterLabel.Visible = false
  435. addFrameProperties(characterLabel)
  436. charPos = charPos + characterWidth
  437. i = i + 1
  438. end
  439. formatLabel(newTextLabel, textSize, textWidth, function() if not overflown then printText(text) end end)
  440. end
  441. function printImage(imageId)
  442. local imageHeight = getTextSize()
  443. local imageWidth = imageHeight -- Would be nice if we could get aspect ratio of image to get width properly.
  444. local newImageLabel = imageLabel:Clone()
  445. if richText.ImageShortcuts[imageId] then
  446. newImageLabel.Image = typeof(richText.ImageShortcuts[imageId]) == "number" and "rbxassetid://"..richText.ImageShortcuts[imageId] or richText.ImageShortcuts[imageId]
  447. else
  448. newImageLabel.Image = "rbxassetid://"..imageId
  449. end
  450. newImageLabel.Size = UDim2.new(0, imageHeight, 0, imageWidth)
  451. newImageLabel.Visible = false
  452. formatLabel(newImageLabel, imageHeight, imageWidth, function() if not overflown then printImage(imageId) end end)
  453. end
  454. function printSeries(labelSeries)
  455. for _, t in pairs(labelSeries) do
  456. local markupKey, markupValue = string.match(t, "<(.+)=(.+)>")
  457. if markupKey and markupValue then
  458. if not applyMarkup(markupKey, markupValue) then
  459. warn("Could not apply markup: ", t)
  460. end
  461. else
  462. printText(t)
  463. end
  464. end
  465. end
  466. ----- Text traversal + parsing -----
  467. local overflowText
  468. local textPos = 1
  469. local textLength = #text
  470. local labelSeries = {}
  471. if prevTextObject then
  472. textPos = prevTextObject.OverflowPickupIndex
  473. end
  474. while textPos and textPos <= textLength do
  475. local nextMarkupStart, nextMarkupEnd = string.find(text, "<.->", textPos)
  476. local nextSpaceStart, nextSpaceEnd = string.find(text, "[ \t\n]", textPos)
  477. local nextBreakStart, nextBreakEnd, breakIsWhitespace
  478. if nextMarkupStart and nextMarkupEnd and (not nextSpaceStart or nextMarkupStart < nextSpaceStart) then
  479. nextBreakStart, nextBreakEnd = nextMarkupStart, nextMarkupEnd
  480. else
  481. nextBreakStart, nextBreakEnd = nextSpaceStart or textLength + 1, nextSpaceEnd or textLength + 1
  482. breakIsWhitespace = true
  483. end
  484. local nextWord = nextBreakStart > textPos and string.sub(text, textPos, nextBreakStart - 1) or nil
  485. local nextBreak = nextBreakStart <= textLength and string.sub(text, nextBreakStart, nextBreakEnd) or nil
  486. table.insert(labelSeries, nextWord)
  487. if breakIsWhitespace then
  488. printSeries(labelSeries)
  489. if overflown then
  490. break
  491. end
  492. printSeries({nextBreak})
  493. if overflown then
  494. textPos = nextBreakStart
  495. break
  496. end
  497. labelSeries = {}
  498. else
  499. table.insert(labelSeries, nextBreak)
  500. end
  501. textPos = nextBreakEnd + 1
  502. --textPos = utf8.offset(text, 2, nextBreakEnd)
  503. end
  504. if not overflown then
  505. printSeries(labelSeries)
  506. end
  507. ----- Alignment layout -----
  508. local listLayout = Instance.new("UIListLayout")
  509. listLayout.HorizontalAlignment = properties.ContainerHorizontalAlignment
  510. listLayout.VerticalAlignment = properties.ContainerVerticalAlignment
  511. listLayout.Parent = frame
  512. ----- Calculate content size -----
  513. local contentHeight = 0
  514. local contentLeft = frame.AbsoluteSize.X
  515. local contentRight = 0
  516. for _, lineFrame in pairs(lineFrames) do
  517. contentHeight = contentHeight + lineFrame.Size.Y.Offset
  518. local container = lineFrame.Container
  519. local left, right
  520. if container.AnchorPoint.X == 0 then
  521. left = container.Position.X.Offset
  522. right = container.Size.X.Offset
  523. elseif container.AnchorPoint.X == 0.5 then
  524. left = lineFrame.AbsoluteSize.X / 2 - container.Size.X.Offset / 2
  525. right = lineFrame.AbsoluteSize.X / 2 + container.Size.X.Offset / 2
  526. elseif container.AnchorPoint.X == 1 then
  527. left = lineFrame.AbsoluteSize.X - container.Size.X.Offset
  528. right = lineFrame.AbsoluteSize.X
  529. end
  530. contentLeft = math.min(contentLeft, left)
  531. contentRight = math.max(contentRight, right)
  532. end
  533. ----- Animation -----
  534. animationCount = animationCount + 1
  535. local animationDone = false
  536. local allTextReached = false
  537. local overrideYield = false
  538. local animationRenderstepBinding = "TextAnimation"..animationCount
  539. local animateQueue = {}
  540. local function updateAnimations()
  541. if allTextReached and #animateQueue == 0 or animationDone then
  542. animationDone = true
  543. runService:UnbindFromRenderStep(animationRenderstepBinding)
  544. animateQueue = {}
  545. return
  546. end
  547. local t = tick()
  548. for i = #animateQueue, 1, -1 do
  549. local set = animateQueue[i]
  550. local properties = set.Settings
  551. local animateStyle = animationStyles[properties.AnimateStyle]
  552. if not animateStyle then
  553. warn("No animation style found for: ", properties.AnimateStyle, ", defaulting to Appear")
  554. animateStyle = animationStyles.Appear
  555. end
  556. local animateAlpha = math.min((t - set.Start) / properties.AnimateStyleTime, 1)
  557. animateStyle(set.Char, animateAlpha, properties)
  558. if animateAlpha >= 1 then
  559. table.remove(animateQueue, i)
  560. end
  561. end
  562. end
  563. local function setFrameToDefault(frame)
  564. frame.Position = frameProperties[frame].InitialPosition
  565. frame.Size = frameProperties[frame].InitialSize
  566. frame.AnchorPoint = frameProperties[frame].InitialAnchorPoint
  567. for name, value in pairs(frameProperties[frame]) do
  568. applyProperty(name, value, frame)
  569. end
  570. end
  571. local function setGroupVisible(frame, visible)
  572. frame.Visible = visible
  573. for _, v in pairs(frame:GetChildren()) do
  574. v.Visible = visible
  575. if visible then
  576. setFrameToDefault(v)
  577. end
  578. end
  579. if visible and frame:IsA("ImageLabel") then
  580. setFrameToDefault(frame)
  581. end
  582. end
  583. local function animate(waitForAnimationToFinish)
  584. animationDone = false
  585. runService:BindToRenderStep(animationRenderstepBinding, Enum.RenderPriority.Last.Value, updateAnimations)
  586. local stepGrouping
  587. local stepTime
  588. local stepFrequency
  589. local numAnimated
  590. -- Make everything invisible to start
  591. for lineNum, list in pairs(textFrames) do
  592. for _, frame in pairs(list) do
  593. setGroupVisible(frame, false)
  594. end
  595. end
  596. local function animateCharacter(char, properties)
  597. table.insert(animateQueue, {Char = char, Settings = properties, Start = tick()})
  598. end
  599. local function yield()
  600. if not overrideYield and numAnimated % stepFrequency == 0 and stepTime >= 0 then
  601. local yieldTime = stepTime > 0 and stepTime or nil
  602. wait(yieldTime)
  603. end
  604. end
  605. for lineNum, list in pairs(textFrames) do
  606. for _, frame in pairs(list) do
  607. local properties = frameProperties[frame]
  608. if not (properties.AnimateStepGrouping == stepGrouping) or not (properties.AnimateStepFrequency == stepFrequency) then
  609. numAnimated = 0
  610. end
  611. stepGrouping = properties.AnimateStepGrouping
  612. stepTime = properties.AnimateStepTime
  613. stepFrequency = properties.AnimateStepFrequency
  614. if properties.AnimateYield > 0 then
  615. wait(properties.AnimateYield)
  616. end
  617. if stepGrouping == "Word" or stepGrouping == "All" then
  618. --if not (frame:IsA("TextLabel") and (frame.Text == " ")) then
  619. if frame:IsA("TextLabel") then
  620. frame.Visible = true
  621. for _, v in pairs(frame:GetChildren()) do
  622. animateCharacter(v, frameProperties[v])
  623. end
  624. else
  625. animateCharacter(frame, properties)
  626. end
  627. if stepGrouping == "Word" then
  628. numAnimated = numAnimated + 1
  629. yield()
  630. end
  631. --end
  632. elseif stepGrouping == "Letter" then
  633. if frame:IsA("TextLabel") --[[and not (frame.Text == " ") ]]then
  634. frame.Visible = true
  635. local text = frame.Text
  636. local i = 1
  637. while true do
  638. local v = frame:FindFirstChild(string.format("Char%03d", i))
  639. if not v then
  640. break
  641. end
  642. animateCharacter(v, frameProperties[v])
  643. numAnimated = numAnimated + 1
  644. yield()
  645. if animationDone then
  646. return
  647. end
  648. i = i + 1
  649. end
  650. else
  651. animateCharacter(frame, properties)
  652. numAnimated = numAnimated + 1
  653. yield()
  654. end
  655. else
  656. warn("Invalid step grouping: ", stepGrouping)
  657. end
  658. if animationDone then
  659. return
  660. end
  661. end
  662. end
  663. allTextReached = true
  664. if waitForAnimationToFinish then
  665. while #animateQueue > 0 do
  666. runService.RenderStepped:Wait()
  667. end
  668. end
  669. end
  670. local textObject = {}
  671. ----- Overflowing -----
  672. textObject.Overflown = overflown
  673. textObject.OverflowPickupIndex = textPos
  674. textObject.StartingProperties = startingProperties
  675. textObject.OverflowPickupProperties = properties
  676. textObject.Text = text
  677. if prevTextObject then
  678. prevTextObject.NextTextObject = textObject
  679. end
  680. -- to overflow: check if textObject.Overflown, then use richText:ContinueOverflow(newFrame, textObject) to continue to another frame.
  681. ----- Return object API -----
  682. textObject.ContentSize = Vector2.new(contentRight - contentLeft, contentHeight)
  683. function textObject:Animate(yield)
  684. if yield then
  685. animate()
  686. else
  687. coroutine.wrap(animate)()
  688. end
  689. if self.NextTextObject then
  690. self.NextTextObject:Animate(yield)
  691. end
  692. end
  693. function textObject:Show(finishAnimation)
  694. if finishAnimation then
  695. overrideYield = true
  696. else
  697. animationDone = true
  698. for lineNum, list in pairs(textFrames) do
  699. for _, frame in pairs(list) do
  700. setGroupVisible(frame, true)
  701. end
  702. end
  703. end
  704. if self.NextTextObject then
  705. self.NextTextObject:Show(finishAnimation)
  706. end
  707. end
  708. function textObject:Hide()
  709. animationDone = true
  710. for lineNum, list in pairs(textFrames) do
  711. for _, frame in pairs(list) do
  712. setGroupVisible(frame, false)
  713. end
  714. end
  715. if self.NextTextObject then
  716. self.NextTextObject:Hide()
  717. end
  718. end
  719. return textObject
  720. end
  721. function richText:ContinueOverflow(newFrame, prevTextObject)
  722. return richText:New(newFrame, nil, nil, false, prevTextObject)
  723. end
  724. return richText