TextSize Approximation with TextScaled Enabled

Hello everyone!

Allow me to get straight to the point.

  • With TextLabels, there is currently no property or API to get the actual “TextSize” of a TextLabel when the property TextScaled is enabled.

Why is this an issue?

  • The TextWrapped property is force enabled with TextScaled, so now instead of just having 1 row of text (A simple TextLabel.TextBounds.Y), you could have multiple rows!!

Okay but does it look good?

  • The popping out is the viewport updating the TextBounds before I manually update TextSize

This code block provided below is a quick way to get an “approximated” TextSize. From my testing it is nearly perfect, only occasionally being a pixel off.

local Params = Instance.new("GetTextBoundsParams")

function getTextBoundParams(TextLabel, TextSize)
    Params.Size = TextSize
    Params.Font = TextLabel.FontFace
    Params.Width = math.huge
    Params.Text = TextLabel.Text
    Params.RichText = TextLabel.RichText
    return Params
end

function GetTextSize(TextLabel)
    if TextLabel.TextScaled then
        local smallSample, bigSample = 1, 100

        local lower = TextService:GetTextBoundsAsync(getTextBoundParams(smallSample))
        local upper = TextService:GetTextBoundsAsync(getTextBoundParams(bigSample))
        local verticalBound = math.floor(TextLabel.TextBounds.Y)

        local volume = math.floor(TextLabel.TextBounds.X) * verticalBound
        local lowerVolume = (lower.X * lower.Y) / volume -- Scalar to compare TextSize 1 TextBounds vs the actual TextBounds of the TextLabel
        local upperVolume = (upper.X * upper.Y) / volume -- Same as lowerVolme but TextSize 100

        local s1 = math.sqrt(lowerVolume)
        local s2 = math.sqrt(upperVolume)

        -- Linearly interpolate the volumes by the slope to find the "perfect" volume along the slope.
        local textSizeApproximation = smallSample + (1 - s1) * (bigSample - smallSample) / (s2 - s1)
        local rows = math.round(verticalBound / textSizeApproximation)

        return math.floor(verticalBound / rows)
    end
    return TextLabel.TextSize
end

Let me know if this has helped you at all and feel free to pitch in!

7 Likes

Hi, MasterDevelopments here, so I wanted to make an honest answer., I feel like this is very useful in MY opinion. The code is great, nothing there I don’t like or think needs major changing, but some suggestions.

  1. Make it a Module. A script is fine, but a module is reusable
  2. I find this useful, but please, and by please, I mean please recycle your params. its unoptimized
  3. Don’t use self unless it’s an OOP or metatable, either then that, please do noit’sits annoying.
1 Like

Thanks for the reply!

My response regarding your suggestion:

  1. I made this so anyone could easily read it & refactor it into their own systems
  2. (Same as #1, not entirely sure what you mean though since the same Instance is used?)
  3. That was an oversight, I had refactored this code from my own system that used OOP, it is now fixed.

Oh, my bad on number 2, In the video you kept creating params when you were changing your viewport size, but silly me, anyways, I refactored your code, here you are:

local TextService = game:GetService("TextService")

local TextSizeEstimator = {}
TextSizeEstimator.__index = TextSizeEstimator

function TextSizeEstimator.new(label)
    assert(label:IsA("TextLabel"), "TextSizeEstimator expects a TextLabel")
    local self = setmetatable({}, TextSizeEstimator)
    self.Label = label
    self.Params = Instance.new("GetTextBoundsParams")
    self.Params.Font = label.FontFace
    self.Params.Width = math.huge
    self.Params.Text = label.Text
    self.Params.RichText = label.RichText
    return self
end

function TextSizeEstimator:GetBounds(size)
    self.Params.Size = size
    return TextService:GetTextBoundsAsync(self.Params)
end

function TextSizeEstimator:Estimate()
    if not self.Label.TextScaled then
        return self.Label.TextSize
    end

    if not self.Label.Text or self.Label.Text == "" then
        return 0
    end

    local smallSize, largeSize = 1, 100
    local boundsSmall = self:GetBounds(smallSize)
    local boundsLarge = self:GetBounds(largeSize)

    local actualBounds = self.Label.TextBounds
    local volume = actualBounds.X * actualBounds.Y

    local volumeSmall = boundsSmall.X * boundsSmall.Y
    local volumeLarge = boundsLarge.X * boundsLarge.Y

    local scaleSmall = math.sqrt(volumeSmall / volume)
    local scaleLarge = math.sqrt(volumeLarge / volume)

    local estimatedSize = smallSize + (1 - scaleSmall) * (largeSize - smallSize) / (scaleLarge - scaleSmall)
    local rows = math.max(1, math.round(actualBounds.Y / estimatedSize))

    return math.floor(actualBounds.Y / rows)
end

return TextSizeEstimator

also, I made a little helper for input Tired of Rewriting Keybind Logic? InputManager Handles It All—Context, Debounce, Gamepad, Touch - Resources / Community Resources - Developer Forum | Roblox