Auto sizing a TTF label to fix a box

Hi all,

2 years ago, user dmitry asked this very same question without any answer (http://www.cocos2d-x.org/forums/6/topics/6890).

I’m currently in the same position he was, trying to create a TTF label (cocos2d-x v3) using Label::createWithTTF() function, and I was wondering if there is an efficient way to create my label so it fits a boundingbox (automatically adjusting the size of the font, based on the string of the label).

This, or any other way to make sure (and not using a BMfont) my label fits a defined size (so I can create my label in all languages at runtime).

Any suggestion?
Thanks,
Kiki

Anyone?

is it possible with current base class, or is there a way to achieve this otherwise?

I noticed there is no setFontSize for the cocos2d::Label class, so I can’t even iterate and try to find a fitting font without recreating the object everytime.

Here is the code I use (cocos2d-x v2.2, you can probably achieve the same in 3.0).

delegate is the actual CCLabelTTF
realDimensions is the size you want your label to fit in
fitType is an enum with NoResize (do nothing, especially for large labels which can go over the texture limit size), CutEnd (to cut the end of the label to fit in without changing the font size) and ResizeFont (that’s probably what you are looking for)

I didn’t touch that code in over 6 months, feel free to ask questions if something non-obvious isn’t explained by the comments (I did a binary search for cutEnd because I runned into performance issues. Note that I use a modified v2.2 version for Android, with speed optimizations in the native CCLabelTTF generation).

if(realDimensions.width != 0 && realDimensions.height != 0 && fitType != NoResize)
{
bool wasChanged = false;
float scale = this->getScale();
CCSize size = delegate->getContentSize();
//Add a 5% margin for fitInside comparison since the algorithm underneath is not exact …
bool fitInside = size.height * scale <= realDimensions.height * 1.05 && size.width * scale <= realDimensions.width * 1.05;

   //Used by CutEnd to perform a binary search (optimization because CCLabelTTF::updateTexture is slow on Android)
   //If you run into performance issues, you should also cache the CutEnd results
   std::string original = delegate->getString();
   int end = utf8_len(original);
   int start = fitInside || fitType != CutEnd ? end : 0; //If it already fit, bypass the while
   
   while((fitType != CutEnd && !fitInside)
         || end - start > 1) //There is one character precision (it may cut one more character than necessary)
   {
       wasChanged = true;
        if(fitType == ResizeFont)
        {
            scale *= 0.9;
            this->setScale(scale);
            CCSize dimensions = CCSizeMake(realDimensions.width / scale, 0);
            delegate->setDimensions(dimensions);
            size = delegate->getContentSize();
            fitInside = size.height * scale <= realDimensions.height * 1.05 && size.width * scale <= realDimensions.width * 1.05;
        }
        else if(fitType == CutEnd)
        {
            std::string value = delegate->getString();
            int middle = start + ((end - start) / 2);
            value = utf8_substr(original, 0, middle);
            CCAssert(value.length() != 0, "Invalid UTF8 string");
            delegate->setString(value.c_str());
            size = delegate->getContentSize();
            fitInside = size.height * scale <= realDimensions.height * 1.05 && size.width * scale <= realDimensions.width * 1.05;
            if(fitInside)
                start = middle;
            else
                end = middle;
        }
        else
        {
            CCLOG("Warning, unsupported fit type, won't cut");
            return;
        }
    }
    if(wasChanged && fitType == CutEnd)
    {
        //TODO : refactor to compute the size with 3 '.' instead of just replacing the 3 last characters
        std::string value = delegate->getString();
        value = utf8_substr(value, 0, utf8_len(value) - 3).append("...");
        delegate->setString(value.c_str());
    }
}

P.S. : sorry for the formatting. I can’t find out how to format code properly in cocos2d-x forum anymore.

1 Like

Hi,

I thought about using a Node->setScale() call to adjust the size of the label to fit inside my box, but I feared it might not be visualy good. I’ll try your solution (realy faster than having to recreate a texture with the new font size), even if I prefer not having to scale the label.

Thanks anyway!
Kiki

std::string LabelBreak::getShowString(const char *string, const char *fontName, float fontSize, Size labelTTFSize){
std::string showString = std::string(string);
bool isHaveFish = true;
int nDeleteLen = 2;
while (isHaveFish) {
int nStrLen = showString.length();
nDeleteLen++;

    while(0x80 == (0xC0 & showString.at(nStrLen - nDeleteLen)))
    {
        ++nDeleteLen;
    }
    std::string textString(showString.c_str(), nStrLen - nDeleteLen);
    textString += "...";
    CCLabelTTF *label = CCLabelTTF::create(textString.c_str(), fontName, fontSize);
    Size textureSize = label->getContentSize();
    if (textureSize.width<labelTTFSize.width){
        return textString;
    }
}
return "";

}

but this will be slow on android.

1 Like

Here’s my solution, I’m using cocos2d-x 3.4. basically I start with a big font size and iterate by dividing the font size by two each time until it fits the box, then sizing it up to fit.

Unfortunately, there’s no setFontsize method for the Label class, so I need to recreate a label for the iterations, so there could be performance improvement.

Label* LabelUtils::createLabelToFitWidth(std::string text, int maxFontSize, Size boxSize, std::string fontName) {
    auto label = Label::createWithSystemFont(text, fontName, maxFontSize, Size(boxSize.width, 0));
    float height = label->getContentSize().height;
    while (height > boxSize.height) {
        maxFontSize /= 2;
        label = Label::createWithSystemFont(text, fontName, maxFontSize, Size(boxSize.width, 0));
        height = label->getContentSize().height;
    }
    while (height < boxSize.height) {
        maxFontSize += 2;
        label = Label::createWithSystemFont(text, fontName, maxFontSize, Size(boxSize.width, 0));
        height = label->getContentSize().height;
    }
    return Label::createWithSystemFont(text, fontName, maxFontSize - 2, Size(boxSize.width, 0));
}
3 Likes