{"id":"ocauapaz/rbx-blocktext","name":"rbx-blocktext","scope":"ocauapaz","platform":"roblox","description":"Floating 3D text made of springy cubes for Roblox: gradients, rebuild transitions, camera billboarding and touch interaction.","version":"0.1.0","latest":"0.1.0","versions":["0.1.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{},"integrity":"0714c54d6442d7dcea5591d0b5c3a8c437e5f842d415e719f1c20f9d4d9057ad","likes":0,"downloads":0,"install":"forest install ocauapaz/rbx-blocktext","url":"https://forest.dev/p/roblox/ocauapaz/rbx-blocktext","files":"https://api.forest.dev/ai/package/roblox/ocauapaz/rbx-blocktext/files","readme":"[Português](README.pt-br.md)\n\n# BlockText\n\nBlockText renders a string in the Roblox 3D world as a grid of small cubes, where every single cube is driven\nby an analytic spring for BOTH its world position and its scale -- there is no TweenService anywhere in it.\nBecause the follow happens in **world space**, a moving anchor drags the letters along: they trail behind, lean\ninto the turn and then settle, instead of snapping to a rigid offset. The very same spring is what morphs one\nword into the next, so `SetText` is a transition rather than a cut. It is client-side presentation only -- run\nit on the client, nothing here is replicated or server-authoritative.\n\n## Features\n\n- **Fluid follow** -- every cube springs toward its world slot; `MoveFrequency` / `MoveDamping` tune how floaty\n  or how snappy that is.\n- **Morphing text** -- `SetText` relocates the existing cubes to their new slots, so a word melts into the next\n  one. Six transition styles to pick from.\n- **Per-letter float** -- each letter bobs and tumbles on its own phase, so the word ripples instead of sliding\n  around as one rigid slab.\n- **Camera billboarding** -- keep the word readable from any angle with `FollowCamera`.\n- **Face target** -- aim the word's local +X at a world point every frame; combine it with `Font.ARROW` for a\n  waypoint arrow that always points at its destination.\n- **Gradients, rainbow and tone** -- the color lerps across the word at any angle, optionally scrolling or as an\n  animated hue rainbow, with a soft directional sheen so the block reads as lit.\n- **Touch interaction** -- cubes near a character are pushed away and squashed, then spring back.\n- **Custom cubes** -- bring your own Model for outlined, textured or oddly-shaped letters.\n- **Reduced motion** -- one global hook drops all the motion while keeping the text fully readable.\n- **Pure-Luau font** -- the glyph table and the layout math have no Roblox instances, and are unit-tested\n  outside Studio.\n\n## Install\n\n### Wally\n\nAdd the dependency to your `wally.toml`:\n\n```toml\n[dependencies]\nBlockText = \"ocauapaz/rbx-blocktext@0.1.0\"\n```\n\nThen run:\n\n```sh\nwally install\n```\n\nAnd require it:\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal BlockText = require(ReplicatedStorage.Packages.BlockText)\n```\n\n### Manual\n\nCopy `src/` into your place as a ModuleScript named `BlockText` (its children `Font`, `Spring` and\n`CubeTemplate` come along with it), then require it from wherever you put it.\n\n## Quick start\n\n```lua\nlocal ReplicatedStorage = game:GetService(\"ReplicatedStorage\")\nlocal BlockText = require(ReplicatedStorage.Packages.BlockText)\n\nlocal attachment = workspace.Sign.Attachment\n\n-- Anchored to an Attachment: move the part and the letters trail after it.\nlocal handle = BlockText.Create(attachment, \"HELLO\", {\n\tPreset = \"Hologram\",\n\tCubeSize = 0.5,\n})\n\n-- The same spring that follows the anchor also morphs the word.\ntask.wait(3)\nhandle:SetText(\"WORLD\", \"Scatter\")\n\ntask.wait(3)\nhandle:Destroy()\n```\n\nText is uppercase-only (lowercase folds to uppercase), `\\n` starts a new line, and any character without a\nglyph renders as a blank gap.\n\n## Presets\n\nReady-made option bundles. Use `{ Preset = \"Hologram\" }` in your options -- every other field you pass wins over\nthe preset -- or build a tweaked copy with `BlockText.Preset(\"Hologram\", { CubeSize = 0.3 })`.\n\n| Preset | What it looks like |\n| --- | --- |\n| `Hologram` | Cyan-to-blue sci-fi label that always faces the camera. No outline (the neon glow carries it), a heavier float and a slower, floatier glide. |\n| `Arcade` | Punchy magenta-to-cyan that types itself in letter by letter and reacts to the player walking through it. |\n| `Toon` | Solid golden-yellow cartoon text with the template's outline shell kept. Pair it with a template that actually has an `Outer` shell. |\n| `Ghost` | Soft, pale blue-white text with a heavy slow float and a very loose, drifting follow. No outline. |\n| `Rainbow` | Continuous hue rainbow scrolling across the text, camera-facing, with a light tonal sheen. |\n| `Confetti` | Four-color gradient with a high tonal spread; the cubes explode outward and reassemble on every text change, and react to touch. |\n\n## Rebuild styles\n\nHow a `SetText` transition looks. Set the default for a word with the `RebuildStyle` option, or override it per\ncall: `handle:SetText(\"WORLD\", \"Scatter\")`.\n\n| Style | Transition |\n| --- | --- |\n| `\"Relocate\"` | Reuses the cubes already on screen and glides each one over to its new slot. The default. |\n| `\"Shuffle\"` | Reuses them as well, but pairs each cube with a scrambled slot, so the letters cross over each other on the way. |\n| `\"Rebuild\"` | Shrinks the old word out and grows the new one in, in place. |\n| `\"Scatter\"` | The old cubes explode outward and the new ones fly back in, from 10 studs out. |\n| `\"Rise\"` | The old cubes fall out downward and the new ones rise back up, over 5 studs. |\n| `\"Typewriter\"` | Clears the word, then pops the letters in left to right, 0.07 s apart. |\n\nWhen the new text is shorter than the old one, the surplus cubes shrink out and are cleaned up on their own.\n\n## Custom cubes\n\nA template is a **Model** with:\n\n- `Inner` -- a BasePart, the colored body. BlockText tints this per cube and resizes it every frame.\n- `Outer` -- an OPTIONAL BasePart, the outline shell drawn around the body.\n\n```\nCube (Model)\n |- Inner (Part,     Material = Neon)           -- tinted per cube\n |- Outer (MeshPart, inverted-hull cube, black) -- the outline\n```\n\n```lua\nlocal handle = BlockText.Create(attachment, \"OUTLINED\", {\n\tTemplate = ReplicatedStorage.Assets.Cube,\n\tOutlineColor = Color3.new(0, 0, 0),\n})\n```\n\nBlockText reads the `Inner`/`Outer` size ratio straight off your template, so the outline keeps exactly the\nthickness it was authored with at every `CubeSize`. Cloned parts are stripped of collision, queries, touch\nevents, mass and shadows.\n\n**Why the built-in template ships without an outline:** a solid outline shell cannot be built from a plain Part.\nAn opaque box around the body would simply hide it. A real outline needs an *inverted-hull* mesh -- a cube\nMeshPart with its normals flipped, so the camera sees its far faces and the body shows through -- and that is\nauthored art, not something a library can conjure at runtime. So the built-in template is just `Inner`, a neon\ncube, and the body IS the full cube. Author the outlined Model once in Studio and pass it as `Template` to get\noutlined letters.\n\n## Reduced motion\n\nWire the global check to your accessibility setting once at startup:\n\n```lua\nBlockText.SetReducedMotion(function()\n\treturn Settings.Get(\"ReduceMotion\") == true\nend)\n```\n\nIt is polled every frame, so installing or clearing it takes effect immediately. While it returns `true`, every\nword drops the float, the wobble, the touch interaction and the transition animation, and snaps straight to its\nrest pose -- the text stays exactly where it should be and stays fully readable, it just stops moving. Animated\ncolors freeze as well.\n\nA single word can override the global check with its own `ReducedMotion` option, which is used *instead of* the\nglobal one for that word:\n\n```lua\n-- This word keeps moving even when the global check says otherwise.\nBlockText.Create(attachment, \"ALWAYS\", { ReducedMotion = function() return false end })\n```\n\nPass `nil` to `SetReducedMotion` to clear the global check.\n\n## Performance\n\n- **One connection, one engine call.** Every live word shares a single `Heartbeat` connection. Each word's\n  stepper computes its cubes' new CFrames into a shared batch and the frame ends with a single\n  `Workspace:BulkMoveTo`, so a screenful of words costs one engine round trip instead of hundreds of individual\n  `.CFrame` writes. `Enum.BulkMoveMode.FireCFrameChanged` skips the Position/Orientation changed-signal fanout,\n  since nothing ever listens to these cubes.\n- **Allocation-free hot loop.** The word's frame is unpacked once per frame into scalars, so each cube's world\n  target is plain scalar math -- no per-cube `Vector3` or `CFrame` temporaries for the GC to churn through.\n- **Colors only when they move.** A static word writes each cube's color exactly once, when the cube takes its\n  slot. Only a rainbow or a scrolling gradient refreshes per frame.\n- **Trig memoized per letter.** The per-letter float offset and the wobble rotation are computed once per letter\n  per frame, not once per cube -- a letter is roughly fifteen cubes that would otherwise repeat the same sines\n  and the same CFrame construction.\n- **Resize only on change.** Parts are resized only when the scale actually changed by a meaningful amount.\n- **Nothing when idle.** The shared `Heartbeat` disconnects itself when the last word is destroyed.\n- The system reports itself to the MicroProfiler under a `BlockText` label instead of being lumped into the\n  engine's aggregated event bucket.\n\n## API\n\nFull reference: [docs/API.md](docs/API.md).\n\n## Development\n\nRun the font tests (pure Luau, no Studio needed):\n\n```sh\nlune run tests/font\n```\n\nFormat and lint:\n\n```sh\nstylua src tests\nselene src tests\n```\n\nType-check:\n\n```sh\nrojo sourcemap default.project.json -o sourcemap.json\nluau-lsp analyze --sourcemap=sourcemap.json src tests\n```\n\nToolchain versions are pinned in `rokit.toml`.\n\n## License\n\nMIT. See [LICENSE](LICENSE).\n","readmeTruncated":false}