{"id":"biotoxin495/numberspinnerv2","name":"numberspinnerv2","scope":"biotoxin495","platform":"roblox","description":"A modern TextLabel-based animated number spinner for Roblox UI.","version":"1.2.2","latest":"1.2.2","versions":["1.1.0","1.2.0","1.2.1","1.2.2"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"6f7dbf73f1a90bf116cc934c9a3e0549e761470edb2e375d7e8e7818b13bc69a","likes":0,"downloads":0,"install":"forest install biotoxin495/numberspinnerv2","url":"https://forest.dev/p/roblox/biotoxin495/numberspinnerv2","files":"https://api.forest.dev/ai/package/roblox/biotoxin495/numberspinnerv2/files","readme":"# NumberSpinnerV2 — A modern animated number and text spinner module\r\n\r\n**NumberSpinnerV2**, a modern animated number and text spinner utility module for Roblox UI.\r\n\r\nNumberSpinnerV2 was inspired by [NumberSpinner by boatbomber](https://devforum.roblox.com/t/numberspinner-module/1105961). However, using and extending older spinner implementations often meant maintaining separate value-update logic, manually synchronizing `TextLabel` properties, and working around limited `TextScaled` support.\r\n\r\n**NumberSpinnerV2** addresses these pain points. It is built around the same general concept, but has been rewritten from scratch and expanded into a more complete, responsive, and configurable system.\r\n\r\nThe underlying spinner system has been used internally in my projects for the past two years, and I have now polished it into an open-source module for public use.\r\n\r\nCheck out the uncopylocked showcase game on Roblox:\r\n\r\n[NumberSpinnerV2 Showcase](https://www.roblox.com/games/128706398575811/NumberSpinnerV2-Showcase)\r\n\r\nVideo showcase:\r\n\r\nhttps://www.youtube.com/watch?v=_erdwv5Xg_s\r\n\r\n## Quick example\r\n\r\nNumberSpinnerV2 attaches directly to a normal `TextLabel`.\r\n\r\n```lua\r\nlocal NumberSpinnerV2 = require(ReplicatedStorage.Modules.NumberSpinnerV2)\r\n\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tSeparator = \",\",\r\n\tPrefix = \"$\",\r\n})\r\n\r\nlabel.Text = 1250\r\n```\r\n\r\nYou continue updating the original `TextLabel.Text` property. NumberSpinnerV2 handles the animation, formatting, layout, and responsive sizing.\r\n\r\n## 🚀 Features\r\n\r\n### TextLabel-driven workflow\r\n\r\nNumberSpinnerV2 attaches directly to an existing `TextLabel`.\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.Attach(myTextLabel, options)\r\n\r\nmyTextLabel.Text = 1250\r\nmyTextLabel.Text = 50000\r\nmyTextLabel.Text = 1234567\r\n```\r\n\r\nThe source `TextLabel` remains the public interface. The module hides its original rendered text and creates the animated spinner UI inside it.\r\n\r\nThis allows you to build and style your UI normally in Roblox Studio without maintaining a separate display pipeline.\r\n\r\n### TextScaled support\r\n\r\nNumberSpinnerV2 supports `TextScaled`.\r\n\r\nThe module measures and resizes its generated characters based on the source `TextLabel`, allowing the spinner to respond when the label or its container changes size.\r\n\r\nThis was one of the main reasons I created the module.\r\n\r\n### Spin modes\r\n\r\nNumberSpinnerV2 includes multiple animation modes through the `SpinMode` option.\r\n\r\n#### Full\r\n\r\n`\"Full\"` is the default mode. Each slot uses its available character pages and moves to the target character.\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tSpinMode = \"Full\",\r\n})\r\n```\r\n\r\n#### SingleStep\r\n\r\n`\"SingleStep\"` transitions directly from the currently displayed character to the next character.\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tSpinMode = \"SingleStep\",\r\n})\r\n```\r\n\r\n#### Reel\r\n\r\n`\"Reel\"` creates a slot-machine-like animation using a character reel.\r\n\r\n`CharacterReel` accepts either a string or an ordered array of characters.\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tSpinMode = \"Reel\",\r\n\tCharacterReel = \"0123456789\",\r\n\tReelStepDuration = 0.04,\r\n\tReelMaxFinishSpread = 1,\r\n})\r\n```\r\n\r\nIf no custom reel is provided, the module uses its built-in number, punctuation, and letter reel.\r\n\r\n### Number formatting\r\n\r\nNumberSpinnerV2 supports common number-formatting options:\r\n\r\n* Thousands separators\r\n* Decimal places\r\n* Forced decimals\r\n* Abbreviations\r\n* Abbreviation-specific decimal places\r\n* Custom decimal separators\r\n\r\nExample:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tSeparator = \",\",\r\n\tDecimalPlaces = 2,\r\n\tForceDecimals = true,\r\n})\r\n\r\nlabel.Text = 1250\r\n-- Displays: 1,250.00\r\n```\r\n\r\n### Abbreviations\r\n\r\nYou can define custom abbreviation thresholds:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tAbbreviations = {\r\n\t\t[1e3] = \"K\",\r\n\t\t[1e6] = \"M\",\r\n\t\t[1e9] = \"B\",\r\n\t},\r\n\tAbbreviationDecimalPlaces = 1,\r\n})\r\n```\r\n\r\nExample displays:\r\n\r\n```text\r\n950\r\n1.2K\r\n48.5K\r\n3.6M\r\n7.1B\r\n```\r\n\r\n### Prefixes, suffixes, and icon affixes\r\n\r\nNumberSpinnerV2 supports text prefixes and suffixes:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tPrefix = \"$\",\r\n\tSuffix = \" Coins\",\r\n})\r\n```\r\n\r\nIt also supports image affixes:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tIconPrefix = \"rbxassetid://0000000000\",\r\n\tSuffix = \" Coins\",\r\n\tAffixSpacing = 6,\r\n})\r\n```\r\n\r\nIf `IconPrefix` is set, it takes priority over the text `Prefix`.\r\n\r\nIf `IconSuffix` is set, it takes priority over the text `Suffix`.\r\n\r\n### Animation configuration\r\n\r\nThe main spin animation can be configured through standard Roblox easing options:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tDuration = 0.5,\r\n\tEasingStyle = Enum.EasingStyle.Quad,\r\n\tEasingDirection = Enum.EasingDirection.Out,\r\n})\r\n```\r\n\r\nThis allows the animation to be smooth, fast, bouncy, or snappy depending on the use case.\r\n\r\n### Bounce effect\r\n\r\nNumberSpinnerV2 can optionally apply a small pop animation to the entire spinner when its displayed value changes:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tBounceOnChange = true,\r\n\tBounceScale = 1.1,\r\n})\r\n```\r\n\r\nThis works well for rewards, coins, damage values, level-ups, and similar UI feedback.\r\n\r\n### UIStroke inheritance\r\n\r\nIf the source `TextLabel` contains a `UIStroke`, NumberSpinnerV2 can clone it onto the generated characters:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tApplyStrokeToChars = true,\r\n})\r\n```\r\n\r\nThis helps the spinner preserve the visual style of the original label.\r\n\r\n### Roman numeral and text modes\r\n\r\nThe default mode formats numeric input, but NumberSpinnerV2 also supports Roman numerals and general text or character spinning.\r\n\r\nRoman numeral example:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tMode = \"Roman\",\r\n\tPrefix = \"Level \",\r\n})\r\n\r\nlabel.Text = 9\r\n-- Displays: Level IX\r\n```\r\n\r\nText or rank example:\r\n\r\n```lua\r\nNumberSpinnerV2.Attach(label, {\r\n\tMode = \"Text\",\r\n\tPrefix = \"Rank \",\r\n})\r\n\r\nlabel.Text = \"S\"\r\n-- Displays: Rank S\r\n```\r\n\r\nThese modes make the module usable for levels, ranks, short labels, and other non-standard counters.\r\n\r\n## 📖 Basic usage\r\n\r\nPlace the `NumberSpinnerV2` module somewhere accessible to a client script. Require the module and attach it to a `TextLabel`:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\n\r\nlocal NumberSpinnerV2 = require(ReplicatedStorage.Modules.NumberSpinnerV2)\r\n\r\nlocal label = script.Parent.AmountLabel\r\n\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tDuration = 0.5,\r\n\tEasingStyle = Enum.EasingStyle.Quad,\r\n\tEasingDirection = Enum.EasingDirection.Out,\r\n\r\n\tSeparator = \",\",\r\n\tDecimalPlaces = 0,\r\n\tForceDecimals = false,\r\n\r\n\tPrefix = \"$\",\r\n\tSuffix = \"\",\r\n})\r\n```\r\n\r\nUpdate the original label normally:\r\n\r\n```lua\r\nlabel.Text = 0\r\nlabel.Text = 1250\r\nlabel.Text = 50000\r\n```\r\n\r\nThe module listens to `TextLabel.Text` and handles the visual update.\r\n\r\n### Example: leaderstats coin counter\r\n\r\n```lua\r\nlocal Players = game:GetService(\"Players\")\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\n\r\nlocal player = Players.LocalPlayer\r\nlocal NumberSpinnerV2 = require(ReplicatedStorage.Modules.NumberSpinnerV2)\r\n\r\nlocal label = script.Parent.CoinsLabel\r\n\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tDuration = 0.5,\r\n\tEasingStyle = Enum.EasingStyle.Quad,\r\n\tEasingDirection = Enum.EasingDirection.Out,\r\n\r\n\tSeparator = \",\",\r\n\tDecimalPlaces = 0,\r\n\r\n\tIconPrefix = \"rbxassetid://0000000000\",\r\n\tSuffix = \" Coins\",\r\n\tAffixSpacing = 6,\r\n\r\n\tApplyStrokeToChars = true,\r\n\tBounceOnChange = true,\r\n\tBounceScale = 1.08,\r\n})\r\n\r\nlocal leaderstats = player:WaitForChild(\"leaderstats\")\r\nlocal coins = leaderstats:WaitForChild(\"Coins\")\r\n\r\nlocal function updateCoins(value)\r\n\tlabel.Text = value\r\nend\r\n\r\nupdateCoins(coins.Value)\r\ncoins.Changed:Connect(updateCoins)\r\n```\r\n\r\n### Example: abbreviated large values\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tSeparator = \",\",\r\n\r\n\tAbbreviations = {\r\n\t\t[1e3] = \"K\",\r\n\t\t[1e6] = \"M\",\r\n\t\t[1e9] = \"B\",\r\n\t},\r\n\r\n\tAbbreviationDecimalPlaces = 1,\r\n})\r\n\r\nlabel.Text = 1260000\r\n-- Displays: 1.3M\r\n```\r\n\r\n### Example: currency display\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tPrefix = \"$\",\r\n\tSeparator = \",\",\r\n\tDecimalPlaces = 2,\r\n\tForceDecimals = true,\r\n})\r\n\r\nlabel.Text = 1250\r\n-- Displays: $1,250.00\r\n```\r\n\r\n### Example: Roman numeral levels\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.Attach(label, {\r\n\tMode = \"Roman\",\r\n\tPrefix = \"Level \",\r\n\tDuration = 0.35,\r\n})\r\n\r\nlabel.Text = 4\r\n-- Displays: Level IV\r\n\r\nlabel.Text = 9\r\n-- Displays: Level IX\r\n\r\nlabel.Text = 10\r\n-- Displays: Level X\r\n```\r\n\r\n## ⚙️ API\r\n\r\n### `NumberSpinnerV2.Attach(textLabel, options)`\r\n\r\nAttaches a spinner to a `TextLabel` and returns its controller.\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.Attach(label, options)\r\n```\r\n\r\n### `NumberSpinnerV2.createSpinningNumber(textLabel, options)`\r\n\r\nAn alias for `Attach`, included for compatibility with the older usage pattern.\r\n\r\n```lua\r\nlocal spinner = NumberSpinnerV2.createSpinningNumber(label, options)\r\n```\r\n\r\n### `NumberSpinnerV2.FormatValue(value, options?)`\r\n\r\nFormats a number using the module's number-formatting options without creating a spinner.\r\n\r\n```lua\r\nlocal formatted = NumberSpinnerV2.FormatValue(1260000, {\r\n\tSeparator = \",\",\r\n\tAbbreviations = {\r\n\t\t[1e3] = \"K\",\r\n\t\t[1e6] = \"M\",\r\n\t},\r\n\tAbbreviationDecimalPlaces = 1,\r\n})\r\n\r\n-- \"1.3M\"\r\n```\r\n\r\n### `NumberSpinnerV2.ToRoman(value, options?)`\r\n\r\nConverts a number into a Roman numeral without creating a spinner.\r\n\r\n```lua\r\nlocal roman = NumberSpinnerV2.ToRoman(9)\r\n\r\n-- \"IX\"\r\n```\r\n\r\n### `NumberSpinnerV2.DEFAULT_OPTIONS`\r\n\r\nA copy of the module's default option values.\r\n\r\nIt can be used as a reference or starting point. Changing this table does not alter the internal defaults used by future spinners.\r\n\r\n### `spinner:SetValue(value, animate?)`\r\n\r\nSets the source label text.\r\n\r\nIt animates by default. Pass `false` to update without animation.\r\n\r\n```lua\r\nspinner:SetValue(5000)\r\nspinner:SetValue(5000, false)\r\n```\r\n\r\nThis is equivalent to updating the source label:\r\n\r\n```lua\r\nlabel.Text = 5000\r\n```\r\n\r\n### `spinner:SetText(text, animate?)`\r\n\r\nSets the source label text using a text value.\r\n\r\nIt animates by default. Pass `false` to update without animation.\r\n\r\n```lua\r\nspinner:SetText(\"S\")\r\nspinner:SetText(\"S\", false)\r\n```\r\n\r\n### `spinner:SetOptions(options, animate?)`\r\n\r\nMerges new options into the spinner's current options and redraws it.\r\n\r\nPass `true` to animate the redraw.\r\n\r\n```lua\r\nspinner:SetOptions({\r\n\tBounceOnChange = true,\r\n\tDuration = 0.25,\r\n})\r\n```\r\n\r\n### `spinner:Refresh(animate?)`\r\n\r\nForces the spinner to re-measure and redraw.\r\n\r\nPass `true` to animate the redraw.\r\n\r\n```lua\r\nspinner:Refresh()\r\n```\r\n\r\n### `spinner:Destroy()`\r\n\r\nCleans up the generated UI and restores the original `TextLabel` text visibility.\r\n\r\n```lua\r\nspinner:Destroy()\r\n```\r\n\r\n## Complete options reference\r\n\r\nYou normally only need to provide the options you want to change. Any omitted options use the module defaults.\r\n\r\n```lua\r\n{\r\n\t-- Animation\r\n\tDuration = 0.5,\r\n\tSlotResizeDuration = 0.08,\r\n\tSpinMode = \"Full\", -- \"Full\", \"SingleStep\", or \"Reel\"\r\n\tReelStepDuration = nil,\r\n\tReelMaxFinishSpread = 1,\r\n\tEasingStyle = Enum.EasingStyle.Quad,\r\n\tEasingDirection = Enum.EasingDirection.Out,\r\n\tCircular = false,\r\n\r\n\t-- Formatting\r\n\tMode = \"Number\",\r\n\tSeparator = \",\",\r\n\tDecimalPlaces = 0,\r\n\tForceDecimals = false,\r\n\tDecimalSeparator = \".\",\r\n\r\n\tAbbreviations = {},\r\n\tAbbreviationDecimalPlaces = 1,\r\n\tForceAbbreviationDecimals = false,\r\n\r\n\t-- Roman formatting\r\n\tRomanZero = \"N\",\r\n\tRomanMax = 3999,\r\n\tRomanAllowLarge = false,\r\n\r\n\t-- Affixes\r\n\tPrefix = \"\",\r\n\tSuffix = \"\",\r\n\tIconPrefix = \"\",\r\n\tIconSuffix = \"\",\r\n\tAffixSpacing = 0,\r\n\tIconAffixSpacing = 4,\r\n\r\n\t-- Character layout\r\n\tCharacterPadding = 2,\r\n\tPunctuationPaddingScale = 1.5,\r\n\tPunctuationWidthScale = 0.45,\r\n\tLetterWidthPadding = 5,\r\n\tLetterEdgeBleed = 2,\r\n\tUniformLetterSlots = false,\r\n\tSymbolWidthPadding = 1,\r\n\tSlotAlignment = \"Auto\",\r\n\tCharacterOrder = nil,\r\n\tCharacterReel = nil,\r\n\r\n\t-- Visual styling\r\n\tApplyStrokeToChars = true,\r\n\r\n\t-- Bounce\r\n\tBounceOnChange = false,\r\n\tBounceScale = 1.08,\r\n\tBounceDuration = 0.12,\r\n\tBounceReturnDuration = 0.14,\r\n\tBounceEasingStyle = Enum.EasingStyle.Quad,\r\n\tBounceEasingDirection = Enum.EasingDirection.Out,\r\n\r\n\t-- Accessibility\r\n\tRespectReducedMotion = true,\r\n\r\n\t-- TextScaled bounds\r\n\tMinTextSize = 1,\r\n\tMaxTextSize = 100,\r\n\r\n\t-- Advanced\r\n\tFormatter = nil,\r\n}\r\n```\r\n\r\n## Display modes\r\n\r\n### `\"Number\"`\r\n\r\nThe default mode.\r\n\r\nIt attempts to parse `TextLabel.Text` as a number and applies the configured number formatting.\r\n\r\n```lua\r\nMode = \"Number\"\r\n```\r\n\r\n### `\"Roman\"`\r\n\r\nConverts numeric input into Roman numerals.\r\n\r\n```lua\r\nMode = \"Roman\"\r\n```\r\n\r\n### `\"Text\"`\r\n\r\nDisplays and spins general text or characters without numeric formatting.\r\n\r\n```lua\r\nMode = \"Text\"\r\n```\r\n\r\n### `\"Characters\"`\r\n\r\nEquivalent to `\"Text\"`.\r\n\r\nThe singular spelling `\"Character\"` is also accepted.\r\n\r\n```lua\r\nMode = \"Characters\"\r\n```\r\n\r\n## Animation options\r\n\r\n* `SpinMode = \"Full\"` uses the normal cached character pages and moves each slot to its target character. This is the default.\r\n* `SpinMode = \"SingleStep\"` transitions from the currently displayed character directly to the next character.\r\n* `SpinMode = \"Reel\"` plays each slot through `CharacterReel` before landing on its target.\r\n* `SlotResizeDuration` controls the width-change animation when a character slot changes size.\r\n* `ReelStepDuration`, when set to a value greater than zero, derives the reel animation duration from its number of steps.\r\n* `ReelMaxFinishSpread` limits the additional finish-time spread used by reel animations.\r\n* `Circular` applies to the normal `\"Full\"` spin mode. Reel and single-step animations use a non-circular sequence.\r\n\r\n## Character layout options\r\n\r\n* `CharacterPadding` adds horizontal spacing around characters.\r\n* `PunctuationPaddingScale` adjusts the spacing applied to punctuation.\r\n* `PunctuationWidthScale` adjusts the measured width of punctuation slots.\r\n* `LetterWidthPadding` adds additional width for letter slots.\r\n* `LetterEdgeBleed` helps prevent letter edges from appearing clipped.\r\n* `UniformLetterSlots` gives letter slots a stable, uniform width.\r\n* `SymbolWidthPadding` adds additional width around symbols.\r\n* `SlotAlignment` accepts `\"Auto\"`, `\"Left\"`, or `\"Right\"`.\r\n* Automatic slot alignment is right-aligned for numeric input and left-aligned for other input.\r\n* `CharacterOrder` accepts an ordered character array and controls page ordering in `\"Full\"` mode.\r\n* `CharacterReel` accepts a string or ordered character array and is used by `SpinMode = \"Reel\"`.\r\n\r\n## 📝 Notes\r\n\r\n* NumberSpinnerV2 is intended for client-side UI.\r\n* The original `TextLabel` can be designed and styled normally in Roblox Studio.\r\n* The generated spinner UI is created inside the source `TextLabel`.\r\n* Updating `TextLabel.Text` is the main usage pattern.\r\n* `TextScaled` is supported.\r\n* If you use icon affixes, make sure the supplied asset IDs refer to valid image assets.\r\n* For extremely small or responsive UI, consider adding a `UITextSizeConstraint` to the source label.\r\n* Call `spinner:Destroy()` when you no longer need the spinner and want to restore the original label rendering.\r\n\r\n## 🛠️ Installation\r\n\r\n### Wally\r\n\r\nAdd NumberSpinnerV2 to your `wally.toml` dependencies:\r\n\r\n```toml\r\n[dependencies]\r\nNumberSpinnerV2 = \"biotoxin495/numberspinnerv2@1.2.0\"\r\n```\r\n\r\nRun:\r\n\r\n```text\r\nwally install\r\n```\r\n\r\nThen require the package from the location configured by your project. Like for example:\r\n\r\n```lua\r\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\r\n\r\nlocal NumberSpinnerV2 = require(\r\n\tReplicatedStorage.Packages.NumberSpinnerV2\r\n)\r\n```\r\n\r\n### Manual installation\r\n\r\nYou can install the standalone ModuleScript manually by copying it from the GitHub repository or the uncopylocked showcase game.\r\n\r\nRecommended structure:\r\n\r\n```text\r\nReplicatedStorage\r\n└── Modules\r\n    └── NumberSpinnerV2\r\n```\r\n\r\nThen require it with:\r\n\r\n`","readmeTruncated":true,"readmeFull":"https://api.forest.dev/v1/package/biotoxin495/roblox/numberspinnerv2/1.2.2/readme"}