Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Questions about the LÖVE API, installing LÖVE and other support related questions go here.
Forum rules
Before you make a thread asking for help, read this.
Post Reply
azoyan
Prole
Posts: 25
Joined: Wed Dec 27, 2017 5:19 pm

Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by azoyan »

Code: Select all

function CreateAutoSizedFont(width, height, str, fontName, baseSize, mode)
    mode = "fast"
    local k = 1.5

    local font = love.graphics.newFont(fontName, baseSize)
    local fontCache = {}
    fontCache[baseSize] = font
    local textHeight = font:getHeight()
    local textWidth, wrappedtext = font:getWrap(str, width)

    local needIncrease = #wrappedtext == 1 and (textHeight < height / 1.5 or textWidth < width / 1.5)
    local needDecrease = textHeight > height or #wrappedtext > 1

    while needIncrease and not needDecrease do
        baseSize = mode == "fast" and baseSize * k or baseSize + 1
        baseSize = math.floor(baseSize)
        if fontCache[baseSize] == nil then
            font = love.graphics.newFont(fontName, baseSize)
            fontCache[baseSize] = font
        else
            font = fontCache[baseSize]
        end
        textHeight = font:getHeight()
        textWidth, wrappedtext = font:getWrap(str, width)
        needIncrease = #wrappedtext == 1 and (textHeight < height / 1.5 or textWidth < width / 1.5)
        needDecrease = textHeight > height or #wrappedtext > 1
    end

    while needDecrease do
        baseSize = mode == "fast" and baseSize / k or baseSize - 1
        baseSize = math.ceil(baseSize)
        if fontCache[baseSize] == nil then
            font = love.graphics.newFont(fontName, baseSize)
            fontCache[baseSize] = font
        else
            font = fontCache[baseSize]
        end
        textHeight = font:getHeight()
        textWidth, wrappedtext = font:getWrap(str, width)
        needDecrease = textHeight > height or #wrappedtext > 1
    end

    return font
end
https://gist.github.com/azoyan/4653f8ff ... 1c9600c24a

I need to increase or decrease font depending on the specified dimensions.

The main problem is that the font is re-created every time it needs to be increased / decreased, because the slow operation love.graphics.newFont() is called. Is there a way to find size of text without recreating font?

In this version I use the local cache to avoid creating the font if the font with the given size has already been created

Of course, I understand that this operation is performed once per game and the font will be globally saved. But I hope we have an alternative way or a better algorithm.
What about the best practices for this?
Last edited by azoyan on Fri Jun 04, 2021 1:24 pm, edited 1 time in total.
User avatar
pgimeno
Party member
Posts: 3673
Joined: Sun Oct 18, 2015 2:58 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by pgimeno »

azoyan wrote: Fri Jun 04, 2021 8:49 am The main problem is that the font is re-created every time it needs to be increased / decreased, because the slow operation love.graphics.newFont() is called. Is there a way to find size of text without recreating font?
No. Theoretically the best algorithm is an exponential search. You're doing one in the "fast" variant for increasing, but with base 1.5 instead of 2, and not using binary search in the decreasing side. See https://en.wikipedia.org/wiki/Exponential_search

Memoizing (caching) the height per font could also help.

Edit: Maybe you can approximate the final size by assuming that font size is proportional to font height. That will save you steps.
azoyan
Prole
Posts: 25
Joined: Wed Dec 27, 2017 5:19 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by azoyan »

Thank you for your answer!
No
Correctly I understand that you said that we have no other ways, except through the re-creation of the font (love.graphcis.newFont)?
You're doing one in the "fast" variant for increasing, but with base 1.5 instead of 2, and not using binary search in the decreasing side
Yes, I've tried resizing by multiplying / dividing by 2, and it really reduces the number of operations. But the result is less accurate - size of resulting font is still "visually asking" to change its own size to fit dimensions.
Memoizing (caching) the height per font could also help.

Edit: Maybe you can approximate the final size by assuming that font size is proportional to font height. That will save you steps.
Could you please explain this tip in more detail?
User avatar
pgimeno
Party member
Posts: 3673
Joined: Sun Oct 18, 2015 2:58 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by pgimeno »

azoyan wrote: Fri Jun 04, 2021 12:09 pm Correctly I understand that you said that we have no other ways, except through the re-creation of the font (love.graphcis.newFont)?
Yes, you understand correctly :)

azoyan wrote: Fri Jun 04, 2021 12:09 pm Yes, I've tried resizing by multiplying / dividing by 2, and it really reduces the number of operations. But the result is less accurate - size of resulting font is still "visually asking" to change its own size to fit dimensions.
I don't think you're doing a binary search in the second phase. By the way, binary search is easy to get wrong, better stick to a well-proven algorithm.

azoyan wrote: Fri Jun 04, 2021 12:09 pm
Memoizing (caching) the height per font could also help.

Edit: Maybe you can approximate the final size by assuming that font size is proportional to font height. That will save you steps.
Could you please explain this tip in more detail?
a) You're caching the font, but not the font height. Caching the font height would save one call.
b) https://love2d.org/forums/viewtopic.php ... 80#p225080
azoyan
Prole
Posts: 25
Joined: Wed Dec 27, 2017 5:19 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by azoyan »

I don't think you're doing a binary search in the second phase. By the way, binary search is easy to get wrong, better stick to a well-proven algorithm.
If I change the variable K to 2, then this value is also applied to the division operation, i.e. in the second phase it is divided by 2.
a) You're caching the font, but not the font height. Caching the font height would save one call.
b) https://love2d.org/forums/viewtopic.php ... 80#p225080
Can you change my code for a better understanding, please.
User avatar
pgimeno
Party member
Posts: 3673
Joined: Sun Oct 18, 2015 2:58 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by pgimeno »

azoyan wrote: Fri Jun 04, 2021 1:17 pm If I change the variable K to 2, then this value is also applied to the division operation, i.e. in the second phase it is divided by 2.
But you're not doing it in shrinking intervals. Check the binary search algorithm, which has a maximum and a minimum.

azoyan wrote: Fri Jun 04, 2021 1:17 pm
a) You're caching the font, but not the font height. Caching the font height would save one call.
b) https://love2d.org/forums/viewtopic.php ... 80#p225080
Can you change my code for a better understanding, please.
a)

Code: Select all

    while needIncrease and not needDecrease do
        baseSize = mode == "fast" and baseSize * k or baseSize + 1
        baseSize = math.floor(baseSize)
        if fontCache[baseSize] == nil then
            fontCache[baseSize] = love.graphics.newFont(fontName, baseSize)
            heightCache[baseSize] = font:getHeight()
        end
        font = fontCache[baseSize]
        textHeight = heightCache[baseSize]
        textWidth, wrappedtext = font:getWrap(str, width)
        needIncrease = #wrappedtext == 1 and (textHeight < height / 1.5 or textWidth < width / 1.5)
        needDecrease = textHeight > height or #wrappedtext > 1
    end
b) requires a rewrite. It's more complex, and requires values precalculated for a given font in advance.
azoyan
Prole
Posts: 25
Joined: Wed Dec 27, 2017 5:19 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by azoyan »

Thank you very much!

Is font:getHeight() a slow operation? Is there really a need to cache its result?
User avatar
pgimeno
Party member
Posts: 3673
Joined: Sun Oct 18, 2015 2:58 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by pgimeno »

In some circumstances it can slow down your application, that's all.

Here's a library:

Code: Select all

-- Find font to fit text tightly to a rectangle
--
-- Copyright © 2021 Pedro Gimeno Fortea
--
-- You can do whatever you want with this software, under the sole condition
-- that this notice and any copyright notices are preserved. It is offered
-- with no warrany, not even implied.

--[[

    Usage:
        local newFitter = require("tightFitText")

    Per font:
        local myFontFitter = newFitter("myFont.ttf")

    For the default Love2D font:
        local defaultFitter = newFitter()

    To use:
        myFontFitter:setFittingFont(text, width, height)
    will set the current font to one with a size such that the text fits
    tightly in a rectangle of the given width and height.

--]]


-- Table of precalculated size-to-pixels ratio.
-- Precalculate with a program which just does this:
-- print(love.graphics.newFont("name", 1000):getHeight()/1000)
local sizeRatios = {
  [false] = 1.164;
  ["LiberationMono-Regular.ttf"] = 1.133;
  ["LiberationSans-Regular.ttf"] = 1.150;
}

local newFont = love.graphics.newFont
local newFonts = 0

local function getFontAndHeight(self, size, name)
  local sizeName = size .. (name and "\0" .. name or "")
  if self.cacheFont[sizeName] == nil then
    if name then
      self.cacheFont[sizeName] = newFont(name, size)
    else
      self.cacheFont[sizeName] = newFont(size)
    end
    self.cacheHeight[sizeName] = self.cacheFont[sizeName]:getHeight()
  end
  return self.cacheFont[sizeName], self.cacheHeight[sizeName]
end

local function setFittingFont(self, text, width, height)
  local widthRef = self.fontRef:getWidth(text)
  if widthRef == 0 then return end
  local estimatedSize = math.ceil(math.min(self.sizeRef / widthRef * width,
    self.sizeRef / self.heightRef * height))
  local font, bestHeight = self:getFontAndHeight(estimatedSize, self.fontName)
  local bestWidth = font:getWidth(text)
  if bestWidth < width and bestHeight < height then
    -- Might still be too short; increase until found
    local oldFont
    repeat
      oldFont = font
      estimatedSize = estimatedSize + 1
      font, bestHeight = self:getFontAndHeight(estimatedSize, self.fontName)
      bestWidth = font:getWidth(text)
    until bestWidth > width or bestHeight > height
    estimatedSize = estimatedSize - 1
    font = oldFont
  elseif bestWidth > width or bestHeight > height then
    -- Decrease until found
    repeat
      estimatedSize = estimatedSize - 1
      font, bestHeight = self:getFontAndHeight(estimatedSize, self.fontName)
      bestWidth = font:getWidth(text)
    until bestWidth <= width and bestHeight <= height
  end
  love.graphics.setFont(font)
end

local function objNew(fontName, sizeRef)
  local self = {
    fontName = fontName;
    ratio = sizeRatios[fontName or false];
    cacheFont = {};
    cacheHeight = {};
    sizeRef = sizeRef or 100;
    setFittingFont = setFittingFont;
    getFontAndHeight = getFontAndHeight;
  }
  self.heightRef = self.sizeRef * self.ratio
  self.fontRef = self:getFontAndHeight(self.sizeRef, fontName)
  return self
end

return objNew
Example attached.
Attachments
tightFitText-demo.love
(158.46 KiB) Downloaded 292 times
monolifed
Party member
Posts: 188
Joined: Sat Feb 06, 2016 9:42 pm

Re: Create Font that fit on dimensions (...or hot to compute Font size). Best Practices

Post by monolifed »

Using a precomputed ratio seems to be a good way

Code: Select all

local  ver = love.getVersion()
if ver <= 11 then FONTSCALE = 1.16364 -- love v11.3, vera
else FONTSCALE = 1.36233 end -- love v12, noto

-- get pixel size from point size. This works without error for almost all cases except for a few with +-1 pixel error
getFontHeight = function(pt)
	return math.floor(FONTSCALE * pt + 0.5)
end
-- get point size from pixel height. This works without error for the actual font heights.
-- Note: different px values can give same pt values
getFontPoint = function(px)
	return math.floor(px / FONTSCALE + 0.5)
end
I computed the ratio FONTSCALE with this code:

Code: Select all

r = 0
for pt = 8, 144 do
  local font = love.graphics.newFont(pt)
  r = r + font:getHeight() / pt
end
print(r / (144 - 8 + 1))
Post Reply

Who is online

Users browsing this forum: No registered users and 5 guests