I found a couple of minor issues about Label

Hello!
How are you doing?
I found a couple of minor issues.
Please take a look at the attached file when you have time.
If you're busy now, feel free to ignore this.

1 labelWidth creates gaps between Japanese and English (or symbols).

2 labelWidth seems to ignore spaces between Japanese characters.

3 splitWords doesn't seem to work, even with English text. (I'm not familiar with how splitWords is supposed to work, so maybe I'm wrong.)

ZIM - Code Creativity
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FIT, 1024, 1024, light, dark, ready);
    function ready() {
        new Label({
            text: "こんにちはXさんお元気ですか?こんにちはXさん お 元 気 で す か ?",
            size: 30,
            color: black,
        }).loc(W/2, 50).reg(CENTER);

        // labelWidth creates gaps between Japanese and English (or symbols).
        new Label({
            text: "こんにちはXさんお元気ですか?こんにちはXさんお元気ですか?こんにちはXさんお元気ですか?",
            maxSize: 50,
            color: black,
            labelWidth: 400,
            labelHeight: 150,
        }).loc(W/2, 200).reg(CENTER);

        // labelWidth ignores spaces of Japanese text.
        new Label({
            text: "こんにちは お 元 気 で す か こんにちは お 元 気 で す か こんにちは お 元 気 で す か",
            maxSize: 50,
            color: black,
            labelWidth: 400,
            labelHeight: 150,
        }).loc(W / 2, 400).reg(CENTER);

        // splitWords doesn't seem to work, even with English text.
        new Label({
            text: "Good morning. ZIMJS is a fantastic framework. Good morning. ZIMJS is a fantastic framework. Good morning. ZIMJS is a fantastic framework. ZIMJS is a fantastic framework.",
            maxSize: 50,
            color: black,
            labelWidth: 400,
            labelHeight: 150,
            splitWords: true,
        }).loc(W / 2, 600).reg(CENTER);
    }

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />

I found an article about lineWidth in CreateJS. Not sure if it's helpful though.

The code that we got from here createjs TextField CJK text wrapping support | about my life of multimedia had the spaces taken out so we just assumed.

Yes - we added a space between Japanese and English words because the spaces are taken out, we decided to put them in between Japanese and English otherwise there would be no space. How it was coded, there could either always be a space or always be no space and we chose space.

So... should there also be spaces between Japanese words? If so, we can probably do an adjustment.

We will take a look perhaps tomorrow due to Christmas here. Thanks for telling us and we look forward to getting it perfect.

1 Like

Thank you for your reply! And sorry to interrupt you.
Gemini created an original labelWidth + labelHeight system.
It works perfectly for me.
How about this?
Have a nice Christmas!

ZIM - Code Creativity
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FIT, 1024, 1024, light, dark, ready);
    function ready() {
        // Configuration Settings
        const rectWidth = 400;
        const rectHeight = 200;
        const maxFontSize = 80; // Start fitting from this size
        const minFontSize = 10;  // Do not go smaller than this size
        const fontFamily = "Arial";

        // Sample text (English version for demonstration)
        const sourceText = "こんにちはaこんにちはaこんにちはaこんにちは?こんにちはaこんにちはaこんにちはaこんにちは?こんにちはaこんにちは?こんにちはaこんにちは?";

        // Text object for display
        const text = new createjs.Text("", "", "#ffffff");
        text.x = 100;
        text.y = 100;

        // Debug box (Red border for visualization)
        const box = new createjs.Shape();
        box.graphics.beginStroke("red").drawRect(0, 0, rectWidth, rectHeight);
        box.x = text.x;
        box.y = text.y;
        S.addChild(box); // Assuming 'stage' is your Stage object (formerly 'S')


        // ---------------------------------------------------------
        // Main Function: Adjusts size and handles wrapping simultaneously
        // ---------------------------------------------------------
        function autoFitText(targetText, rawString, maxWidth, maxHeight, maxFont, minFont) {

            // Hidden text object used for measurement
            const measureTool = new createjs.Text("", "", targetText.color);

            // Loop to try sizes from max down to min
            for (let size = maxFont; size >= minFont; size--) {

                // Current font settings being tested
                const currentFont = `${size}px ${fontFamily}`;
                const currentLineHeight = size * 1.4; // Set line height to 1.4x the font size

                measureTool.font = currentFont;

                // Run calculation logic to get "height" and "wrapped text"
                const calculated = calculateWrap(measureTool, rawString, maxWidth, currentLineHeight);

                // ★ Check: Does the calculated height fit within the max height?
                if (calculated.totalHeight <= maxHeight) {

                    // If it fits, apply settings and exit loop
                    targetText.font = currentFont;
                    targetText.text = calculated.wrappedString;
                    targetText.lineHeight = currentLineHeight;

                    console.log(`Fit successful: Font size ${size}px`);
                    return;
                }
            }

            // If loop finishes without fitting (text too long even at min size)
            // Fallback to minimum size (content may overflow)
            targetText.font = `${minFont}px ${fontFamily}`;
            const fallback = calculateWrap(measureTool, rawString, maxWidth, minFont * 1.4);
            targetText.text = fallback.wrappedString;
            console.log("Could not fit text even at minimum font size.");
        }

        // ---------------------------------------------------------
        // Sub-function: Calculates text wrapping for a specific font size
        // ---------------------------------------------------------
        function calculateWrap(measureObj, rawString, maxWidth, lineHeight) {
            let resultString = "";
            let currentLine = "";
            let lineCount = 1;

            for (let i = 0; i < rawString.length; i++) {
                const char = rawString[i];

                // Measure width of current line + next char
                measureObj.text = currentLine + char;
                const width = measureObj.getMeasuredWidth();

                if (width > maxWidth) {
                    // Width exceeded: Insert newline
                    resultString += currentLine + "\n";
                    currentLine = char;
                    lineCount++;
                } else {
                    // Width okay: Add character to current line
                    currentLine += char;
                }
            }
            // Append the final line
            resultString += currentLine;

            // Calculate total height
            const totalHeight = lineCount * lineHeight;

            return {
                wrappedString: resultString,
                totalHeight: totalHeight
            };
        }

        // Execute the function
        autoFitText(text, sourceText, rectWidth, rectHeight, maxFontSize, minFontSize);

        S.addChild(text);
        S.update();
    }

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />
1 Like

This is the zimjs version with the splitword option made by GPT.
I hope you find it useful!

index.zip (2.6 KB)

import zim from "https://zimjs.org/cdn/019/zim";

new Frame(FIT, 1024, 1024, light, dark, ready);

function ready() {
    // --------------------------------------------------------------------
    // Goal:
    // Auto-fit a ZIM Label into a fixed rectangle (width/height),
    // by dynamically:
    //  1) choosing a font size that fits the height, and
    //  2) inserting manual line breaks to fit the width,
    // while keeping the text visually centered in the box.
    //
    // Note:
    // We intentionally do NOT use ZIM's labelWidth/labelHeight/lineWidth
    // (or splitWords), because those properties can be unreliable/buggy
    // in some cases. Instead we measure text with createjs.Text.
    // --------------------------------------------------------------------

    const rectWidth = 700;
    const rectHeight = 300;
    const maxFontSize = 80;
    const minFontSize = 10;
    const fontFamily = "Arial";

    const sourceText =
        "Hello world this is a long sentence to test wrapping. Hello world this is a long sentence to test wrapping. Hello world this is a long sentence to test wrapping.";

    // splitword:
    //  true  -> allow breaking anywhere (character-based wrap)
    //  false -> prefer breaking on spaces (word-based wrap).
    //          If a single word is longer than maxWidth, we fall back
    //          to character-based splitting for that word only.
    const splitword = false;

    const boxX = 0;
    const boxY = 0;

    // Visible text is a ZIM Label, but we will control its internal
    // CreateJS text object directly (label.label) for font/lineHeight.
    const label = new Label({
        text: "",
        size: maxFontSize,
        font: fontFamily,
        color: "#ffffff"
    });

    // Access the internal CreateJS Text instance used by ZIM Label.
    // This lets us set alignment without relying on Label sizing APIs.
    const txt = label.label;
    txt.textAlign = "center";
    txt.textBaseline = "top";

    // --------------------------------------------------------------------
    // Main routine:
    // Try font sizes from max -> min until the wrapped text totalHeight
    // fits inside rectHeight. Then center it in the target rectangle.
    // --------------------------------------------------------------------
    function autoFitLabelCentered(targetLabel, rawString, maxWidth, maxHeight, maxFont, minFont, splitword) {

        // Measure with a separate CreateJS Text instance (not displayed).
        const measureTool = new createjs.Text("", "", "#ffffff");

        for (let size = maxFont; size >= minFont; size--) {
            const fontStr = `${size}px ${fontFamily}`;
            const lineHeight = size * 1.4;

            measureTool.font = fontStr;

            const calculated = calculateWrap(measureTool, rawString, maxWidth, lineHeight, splitword);

            // If height fits, apply font + wrapped text and center.
            if (calculated.totalHeight <= maxHeight) {
                targetLabel.label.font = fontStr;
                targetLabel.label.lineHeight = lineHeight;
                targetLabel.text = calculated.wrappedString;

                // Horizontal centering via x=center + textAlign="center"
                targetLabel.x = boxX + maxWidth / 2;

                // Vertical centering by shifting down by remaining gap/2
                targetLabel.y = boxY + (maxHeight - calculated.totalHeight) / 2;
                return;
            }
        }

        // Fallback if nothing fits: use min font size without wrapping logic changes.
        targetLabel.label.font = `${minFont}px ${fontFamily}`;
        targetLabel.label.lineHeight = minFont * 1.4;
        targetLabel.text = rawString;
        targetLabel.x = boxX + maxWidth / 2;
        targetLabel.y = boxY;
    }

    // --------------------------------------------------------------------
    // Character-based wrapping:
    // Break lines at any character boundary when maxWidth is exceeded.
    // Useful for CJK text or when splitword=true.
    // --------------------------------------------------------------------
    function wrapByChar(measureObj, rawString, maxWidth) {
        const lines = [];
        let currentLine = "";

        for (let i = 0; i < rawString.length; i++) {
            const ch = rawString[i];
            measureObj.text = currentLine + ch;

            if (currentLine !== "" && measureObj.getMeasuredWidth() > maxWidth) {
                lines.push(currentLine);
                currentLine = ch;
            } else {
                currentLine += ch;
            }
        }
        if (currentLine !== "") lines.push(currentLine);
        return lines;
    }

    // --------------------------------------------------------------------
    // Word-based wrapping (splitword=false):
    // Prefer wrapping at spaces so English words are not split mid-word.
    //
    // Implementation detail:
    // We tokenize as "word + trailing spaces" to preserve multiple spaces.
    // Then we build lines until adding the next token exceeds maxWidth.
    // If a single token (word) is too long for maxWidth, we fall back
    // to character splitting for that token only.
    // --------------------------------------------------------------------
    function wrapByWord(measureObj, rawString, maxWidth) {
        // Example:
        // "Hello  world " -> ["Hello  ", "world "]
        const tokens = rawString.match(/\S+\s*/g) || [""];

        const lines = [];
        let currentLine = "";

        for (const tokenRaw of tokens) {
            const token = tokenRaw; // token includes trailing whitespace

            measureObj.text = currentLine + token;

            // If adding the token exceeds width, push current line and start a new one.
            if (currentLine !== "" && measureObj.getMeasuredWidth() > maxWidth) {
                // Remove trailing whitespace at end of line.
                lines.push(currentLine.replace(/\s+$/g, ""));

                // Prevent leading spaces on the next line.
                let next = token.replace(/^\s+/g, "");

                // If the next token is still too wide by itself, split it by character.
                measureObj.text = next;
                if (next !== "" && measureObj.getMeasuredWidth() > maxWidth) {
                    const charLines = wrapByChar(measureObj, next, maxWidth);
                    lines.push(...charLines.slice(0, -1));
                    currentLine = charLines[charLines.length - 1] || "";
                } else {
                    currentLine = next;
                }
            } else {
                currentLine += token;
            }
        }

        if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
        return lines;
    }

    // --------------------------------------------------------------------
    // Wrap dispatcher:
    // Returns the wrapped string with \n line breaks and a computed totalHeight.
    // --------------------------------------------------------------------
    function calculateWrap(measureObj, rawString, maxWidth, lineHeight, splitword) {
        const lines = splitword
            ? wrapByChar(measureObj, rawString, maxWidth)
            : wrapByWord(measureObj, rawString, maxWidth);

        return {
            wrappedString: lines.join("\n"),
            totalHeight: lines.length * lineHeight
        };
    }

    // Run and display
    autoFitLabelCentered(label, sourceText, rectWidth, rectHeight, maxFontSize, minFontSize, splitword);
    label.center();
    S.addChild(label);
}

Probably the ? on the next line is not best?

image

And the Gemini one works with Japanese but not with English.

image

The ZIM splitWords works - it is only used if a single word is longer than the width. Not if a word is about to go longer than the width - in that case we wrap.

We are close with the CreateJS version - it had just been coded to remove spaces in the CJK letters. We will work on it so it does not remove spaces.

Okay - try a refresh now and let us know how it goes.

image

Thank you for your support!
Unfortunately, It still doesn't seem to work...

Here is the function code that I’m using.
I think it works with English too. (You need to set splitByCharacter false.)

ZIM - Code Creativity
<!-- zimjs.com - JavaScript Canvas Framework -->
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FIT, 1024, 1024, light, dark, ready);

    function ready() {
        new Label ({
            text: "こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?",
            labelWidth:400,
            labelHeight:200,
            align:"center",
            splitWords:true,
        }).center(S).mov(0, -300)

        generateText({
            text: "こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?",
            maxWidth: 400,
            maxHeight: 200,
            lineHeightRatio:1.1,
            textAlign:"center",
        }).center(S).mov(0, 0)

        generateText({
            text: "Happy new year! Happy new Safawoiefawigkso. Happy new year! Happy new Safawoiefawigkso. Happy new year! Happy new Safawoiefawigkso.",
            maxWidth: 400,
            maxHeight: 200,
            lineHeightRatio: 1.1,
            textAlign: "center",
            splitByCharacter: false,
        }).center(S).mov(0, 300)


        function generateText({
            text = "Text",
            maxWidth = 300,
            maxHeight = 300,
            fontName,
            color,
            maxFontSize = 80,
            lineHeightRatio = 1.4,
            textAlign = "center",
            splitByCharacter = true,
            minFontSize = 10
        }) {
            // Create the ZIM Label
            const label = new Label({
                text: "",
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const internalText = label.label;
            internalText.textAlign = textAlign;
            internalText.textBaseline = "middle";

            // Create a dummy Text object for measuring width/height
            const measureObj = new createjs.Text("", "", color);

            // Function: Wrap text by character (splits character by character)
            function wrapByCharacter(measurer, rawString, limitWidth) {
                const lines = [];
                let currentLine = "";

                for (let i = 0; i < rawString.length; i++) {
                    const char = rawString[i];
                    measurer.text = currentLine + char;

                    // Check if adding the character exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        lines.push(currentLine);
                        currentLine = char;
                    } else {
                        currentLine += char;
                    }
                }
                if (currentLine !== "") lines.push(currentLine);
                return lines;
            }

            // Function: Wrap text by word (keeps words intact unless too long)
            function wrapByWord(measurer, rawString, limitWidth) {
                // Split by whitespace but keep the token logic
                const tokens = rawString.match(/\S+\s*/g) || [""];
                const lines = [];
                let currentLine = "";

                for (const rawToken of tokens) {
                    const token = rawToken;
                    measurer.text = currentLine + token;

                    // Check if adding the token exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        // Push the current line (trim trailing spaces)
                        lines.push(currentLine.replace(/\s+$/g, ""));

                        // Handle the next token (remove leading spaces)
                        let nextToken = token.replace(/^\s+/g, "");
                        measurer.text = nextToken;

                        // If the single token itself is wider than the limit, force character split
                        if (nextToken !== "" && measurer.getMeasuredWidth() > limitWidth) {
                            const charLines = wrapByCharacter(measurer, nextToken, limitWidth);
                            if (charLines.length > 1) {
                                lines.push(...charLines.slice(0, -1));
                                currentLine = charLines[charLines.length - 1] || "";
                            } else {
                                currentLine = charLines[0] || "";
                            }
                        } else {
                            currentLine = nextToken;
                        }
                    } else {
                        currentLine += token;
                    }
                }

                if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
                return lines;
            }

            // Function: Calculate line breaks and total height
            function calculateLayout(measurer, rawString, limitWidth, lineHeight, isCharSplit) {
                const lines = isCharSplit
                    ? wrapByCharacter(measurer, rawString, limitWidth)
                    : wrapByWord(measurer, rawString, limitWidth);

                return {
                    wrappedText: lines.join("\n"),
                    totalHeight: lines.length * lineHeight
                };
            }

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                const currentLineHeight = size * lineHeightRatio;

                measureObj.font = fontString;

                const result = calculateLayout(measureObj, text, maxWidth, currentLineHeight, splitByCharacter);

                if (result.totalHeight <= maxHeight) {
                    // Apply successful settings
                    internalText.font = fontString;
                    internalText.lineHeight = currentLineHeight;
                    label.text = result.wrappedText;

                    // Horizontal Alignment
                    if (textAlign === "center") {
                        label.x = maxWidth / 2;
                    } else if (textAlign === "right") {
                        label.x = maxWidth;
                    } else {
                        label.x = 0;
                    }

                    // Vertical Alignment (Center)
                    label.y = (maxHeight - result.totalHeight) / 2;

                    return label;
                }
            }

            // If text doesn't fit even at min size, enforce minimum settings
            internalText.font = `${minFontSize}px ${fontName}`;
            internalText.lineHeight = minFontSize * lineHeightRatio;
            label.text = text;

            if (textAlign === "center") {
                label.x = maxWidth / 2;
            } else if (textAlign === "right") {
                label.x = maxWidth;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }

    } // end ready

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />

Oh sorry!!! I forgot that we changed the module version to just CreateJS 1.5 - we had updated 1.5.1. So can you please check again now with a refresh.

Here is what we see:

image

Your test with normal lineHeight looks like this:

image

1 Like

It works! :+1:
Thank you very much!
You're the best!

If I had one suggestion, it would be nice if there was another lineHeight feature.
If you have free time, check this code and watch the video clip attached!

<!-- zimjs.com - JavaScript Canvas Framework -->
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FULL, null, null, light, dark, ready);

    function ready() {
        let A; let B; let C;

        function test (){
            if (A != null) {
                A.removeFrom();
                A = null;
            }
            if (B != null) {
                B.removeFrom();
                B = null;
            }
            if (C != null) {
                C.removeFrom();
                C = null;
            }

            A = new Label({
                text: "こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?",
                labelWidth: W * 0.7,
                labelHeight: 150,
                lineHeight: 35,
                align: "center",
                splitWords: true,
            }).center(S).mov(0, -300)

            B = generateText({
                text: "こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?こんに ちは。最近aどうですか?",
                maxWidth: W * 0.7,
                maxHeight: 150,
                lineHeightRatio: 1.4,
                textAlign: "center",
            }).center(S).mov(0, 0)

            C = generateText({
                text: "Happy new year! Happy new Safawoiefawigkso. Happy new year! Happy new Safawoiefawigkso. Happy new year! Happy new Safawoiefawigkso.",
                maxWidth: W * 0.7,
                maxHeight: 150,
                lineHeightRatio: 1.4,
                textAlign: "center",
                splitByCharacter: false,
            }).center(S).mov(0, 300)
        }

        
        test()

        F.on("resize", () => {
            test()
        });


        function generateText({
            text = "Text",
            maxWidth = 300,
            maxHeight = 300,
            fontName,
            color,
            maxFontSize = 80,
            lineHeightRatio = 1.4,
            textAlign = "center",
            splitByCharacter = true,
            minFontSize = 10
        }) {
            // Create the ZIM Label
            const label = new Label({
                text: "",
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const internalText = label.label;
            internalText.textAlign = textAlign;
            internalText.textBaseline = "middle";

            // Create a dummy Text object for measuring width/height
            const measureObj = new createjs.Text("", "", color);

            // Function: Wrap text by character (splits character by character)
            function wrapByCharacter(measurer, rawString, limitWidth) {
                const lines = [];
                let currentLine = "";

                for (let i = 0; i < rawString.length; i++) {
                    const char = rawString[i];
                    measurer.text = currentLine + char;

                    // Check if adding the character exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        lines.push(currentLine);
                        currentLine = char;
                    } else {
                        currentLine += char;
                    }
                }
                if (currentLine !== "") lines.push(currentLine);
                return lines;
            }

            // Function: Wrap text by word (keeps words intact unless too long)
            function wrapByWord(measurer, rawString, limitWidth) {
                // Split by whitespace but keep the token logic
                const tokens = rawString.match(/\S+\s*/g) || [""];
                const lines = [];
                let currentLine = "";

                for (const rawToken of tokens) {
                    const token = rawToken;
                    measurer.text = currentLine + token;

                    // Check if adding the token exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        // Push the current line (trim trailing spaces)
                        lines.push(currentLine.replace(/\s+$/g, ""));

                        // Handle the next token (remove leading spaces)
                        let nextToken = token.replace(/^\s+/g, "");
                        measurer.text = nextToken;

                        // If the single token itself is wider than the limit, force character split
                        if (nextToken !== "" && measurer.getMeasuredWidth() > limitWidth) {
                            const charLines = wrapByCharacter(measurer, nextToken, limitWidth);
                            if (charLines.length > 1) {
                                lines.push(...charLines.slice(0, -1));
                                currentLine = charLines[charLines.length - 1] || "";
                            } else {
                                currentLine = charLines[0] || "";
                            }
                        } else {
                            currentLine = nextToken;
                        }
                    } else {
                        currentLine += token;
                    }
                }

                if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
                return lines;
            }

            // Function: Calculate line breaks and total height
            function calculateLayout(measurer, rawString, limitWidth, lineHeight, isCharSplit) {
                const lines = isCharSplit
                    ? wrapByCharacter(measurer, rawString, limitWidth)
                    : wrapByWord(measurer, rawString, limitWidth);

                return {
                    wrappedText: lines.join("\n"),
                    totalHeight: lines.length * lineHeight
                };
            }

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                const currentLineHeight = size * lineHeightRatio;

                measureObj.font = fontString;

                const result = calculateLayout(measureObj, text, maxWidth, currentLineHeight, splitByCharacter);

                if (result.totalHeight <= maxHeight) {
                    // Apply successful settings
                    internalText.font = fontString;
                    internalText.lineHeight = currentLineHeight;
                    label.text = result.wrappedText;

                    // Horizontal Alignment
                    if (textAlign === "center") {
                        label.x = maxWidth / 2;
                    } else if (textAlign === "right") {
                        label.x = maxWidth;
                    } else {
                        label.x = 0;
                    }

                    // Vertical Alignment (Center)
                    label.y = (maxHeight - result.totalHeight) / 2;

                    return label;
                }
            }

            // If text doesn't fit even at min size, enforce minimum settings
            internalText.font = `${minFontSize}px ${fontName}`;
            internalText.lineHeight = minFontSize * lineHeightRatio;
            label.text = text;

            if (textAlign === "center") {
                label.x = maxWidth / 2;
            } else if (textAlign === "right") {
                label.x = maxWidth;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }

    } // end ready

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />

And here is the generateSingleLineText function made by AI!
This is useful when you want to change the font size without wrapping words.
Sorry if it is already implemented. (I couldn't find it.)

<!-- zimjs.com - JavaScript Canvas Framework -->
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FIT, 1000, 1000, light, dark, ready);

    function ready() {
        let A; let B; let C;

        function test (){
            if (A != null) {
                A.removeFrom();
                A = null;
            }
            if (B != null) {
                B.removeFrom();
                B = null;
            }
            if (C != null) {
                C.removeFrom();
                C = null;
            }


            A = new Label({
                text: "Happy new year! Happy new year! Happy new year!",
                lineWidth: 500,
                align: "center",
            }).center(S).mov(0, -300)

             B = generateSingleLineText({
                text: "Happy new year! Happy new year! Happy new year!",
                width: 500,
                color: dark,
                align: "center"
            }).center(S).mov(0, 0)
        }
        
        test()

        function generateSingleLineText({
            text = "Text",
            width = 600,
            fontName,
            color,
            maxFontSize = 80,
            align = "center",
            minFontSize = 10
        }) {
            // Create ZIM Label
            const label = new Label({
                text: text,
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const innerText = label.label;
            innerText.textAlign = align;
            innerText.textBaseline = "middle";

            // CreateJS Text for measurement
            const measureText = new createjs.Text(text, "", color);

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                measureText.font = fontString;

                const measuredWidth = measureText.getMeasuredWidth();

                if (measuredWidth <= width) {
                    innerText.font = fontString;
                    label.text = text;

                    // Horizontal alignment
                    if (align === "center") {
                        label.x = width / 2;
                    } else if (align === "right") {
                        label.x = width;
                    } else {
                        label.x = 0;
                    }

                    label.y = 0;

                    return label;
                }
            }

            // If no size fits, display at minimum size
            innerText.font = `${minFontSize}px ${fontName}`;
            label.text = text;

            if (align === "center") {
                label.x = width / 2;
            } else if (align === "right") {
                label.x = width;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }
        
    } // end ready

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />

There is a lineHeight parameter ;-). It is not a factor but just play with the numbers. When it is scaling to a labelHeight then it does not make much sense numerically but it will change things.

I meant to say it would be useful to be able to adjust the lineHeight value depending on the font size even if I set labellHeight.
When you set the lineHeightRatio in this custom code, the lineHeight seems to become the font size * lineHeightRatio.
I think it is useful.
But this is just a request. Feel free to prioritize your main work!

function generateText({
            text = "Text",
            maxWidth = 300,
            maxHeight = 300,
            fontName,
            color,
            maxFontSize = 80,
            lineHeightRatio = 1.4,
            textAlign = "center",
            splitByCharacter = true,
            minFontSize = 10
        }) {
            // Create the ZIM Label
            const label = new Label({
                text: "",
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const internalText = label.label;
            internalText.textAlign = textAlign;
            internalText.textBaseline = "middle";

            // Create a dummy Text object for measuring width/height
            const measureObj = new createjs.Text("", "", color);

            // Function: Wrap text by character (splits character by character)
            function wrapByCharacter(measurer, rawString, limitWidth) {
                const lines = [];
                let currentLine = "";

                for (let i = 0; i < rawString.length; i++) {
                    const char = rawString[i];
                    measurer.text = currentLine + char;

                    // Check if adding the character exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        lines.push(currentLine);
                        currentLine = char;
                    } else {
                        currentLine += char;
                    }
                }
                if (currentLine !== "") lines.push(currentLine);
                return lines;
            }

            // Function: Wrap text by word (keeps words intact unless too long)
            function wrapByWord(measurer, rawString, limitWidth) {
                // Split by whitespace but keep the token logic
                const tokens = rawString.match(/\S+\s*/g) || [""];
                const lines = [];
                let currentLine = "";

                for (const rawToken of tokens) {
                    const token = rawToken;
                    measurer.text = currentLine + token;

                    // Check if adding the token exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        // Push the current line (trim trailing spaces)
                        lines.push(currentLine.replace(/\s+$/g, ""));

                        // Handle the next token (remove leading spaces)
                        let nextToken = token.replace(/^\s+/g, "");
                        measurer.text = nextToken;

                        // If the single token itself is wider than the limit, force character split
                        if (nextToken !== "" && measurer.getMeasuredWidth() > limitWidth) {
                            const charLines = wrapByCharacter(measurer, nextToken, limitWidth);
                            if (charLines.length > 1) {
                                lines.push(...charLines.slice(0, -1));
                                currentLine = charLines[charLines.length - 1] || "";
                            } else {
                                currentLine = charLines[0] || "";
                            }
                        } else {
                            currentLine = nextToken;
                        }
                    } else {
                        currentLine += token;
                    }
                }

                if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
                return lines;
            }

            // Function: Calculate line breaks and total height
            function calculateLayout(measurer, rawString, limitWidth, lineHeight, isCharSplit) {
                const lines = isCharSplit
                    ? wrapByCharacter(measurer, rawString, limitWidth)
                    : wrapByWord(measurer, rawString, limitWidth);

                return {
                    wrappedText: lines.join("\n"),
                    totalHeight: lines.length * lineHeight
                };
            }

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                const currentLineHeight = size * lineHeightRatio;

                measureObj.font = fontString;

                const result = calculateLayout(measureObj, text, maxWidth, currentLineHeight, splitByCharacter);

                if (result.totalHeight <= maxHeight) {
                    // Apply successful settings
                    internalText.font = fontString;
                    internalText.lineHeight = currentLineHeight;
                    label.text = result.wrappedText;

                    // Horizontal Alignment
                    if (textAlign === "center") {
                        label.x = maxWidth / 2;
                    } else if (textAlign === "right") {
                        label.x = maxWidth;
                    } else {
                        label.x = 0;
                    }

                    // Vertical Alignment (Center)
                    label.y = (maxHeight - result.totalHeight) / 2;

                    return label;
                }
            }

            // If text doesn't fit even at min size, enforce minimum settings
            internalText.font = `${minFontSize}px ${fontName}`;
            internalText.lineHeight = minFontSize * lineHeightRatio;
            label.text = text;

            if (textAlign === "center") {
                label.x = maxWidth / 2;
            } else if (textAlign === "right") {
                label.x = maxWidth;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }

The lineHeight does the line height and not the font size. It may appear to do the font size if the labelHeight is set as if you increase the lineHeight, there is less room height-wise so the whole text is made smaller.

1 Like

And thank you for all the support code and care and patience. Let us know how it goes - cheers.

1 Like

Thank you for your support. When I set lineHeight after adjusting font size with labelHeight, the letters cross the lineHeight value because the font size doesn't change. When I set lineHeight first, the actual spacing changes depending on the font size. So it's difficult to use both of lineHeight and labelHeight at the same time.

<!-- zimjs.com - JavaScript Canvas Framework -->
<script type="module">

    import zim from "https://zimjs.org/cdn/019/zim";

    new Frame(FIT, 1000, 1000, light, dark, ready);

    function ready() {
        let A; let B; let C;

        function test (){
            if (A != null) {
                A.removeFrom();
                A = null;
            }
            if (B != null) {
                B.removeFrom();
                B = null;
            }

            A = new Label({
                text: "Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year!",
                labelWidth: 500,
                labelHeight: 250,
                align: "center",
                splitWords: true,
            }).center(S).mov(0, -200).outline();

            zog(A.size)

            A.label.lineHeight = A.size * 1.5

            B = generateText({
                text: "Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year! Happy new year!",
                maxWidth: 500,
                maxHeight: 250,
                lineHeightRatio: 2,
                textAlign: "center",
            }).center(S).mov(0, 200).outline();
        }

        test();

        function generateText({
            text = "Text",
            maxWidth = 300,
            maxHeight = 300,
            fontName,
            color,
            maxFontSize = 80,
            lineHeightRatio = 1.4,
            textAlign = "center",
            splitByCharacter = true,
            minFontSize = 10
        }) {
            // Create the ZIM Label
            const label = new Label({
                text: "",
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const internalText = label.label;
            internalText.textAlign = textAlign;
            internalText.textBaseline = "middle";

            // Create a dummy Text object for measuring width/height
            const measureObj = new createjs.Text("", "", color);

            // Function: Wrap text by character (splits character by character)
            function wrapByCharacter(measurer, rawString, limitWidth) {
                const lines = [];
                let currentLine = "";

                for (let i = 0; i < rawString.length; i++) {
                    const char = rawString[i];
                    measurer.text = currentLine + char;

                    // Check if adding the character exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        lines.push(currentLine);
                        currentLine = char;
                    } else {
                        currentLine += char;
                    }
                }
                if (currentLine !== "") lines.push(currentLine);
                return lines;
            }

            // Function: Wrap text by word (keeps words intact unless too long)
            function wrapByWord(measurer, rawString, limitWidth) {
                // Split by whitespace but keep the token logic
                const tokens = rawString.match(/\S+\s*/g) || [""];
                const lines = [];
                let currentLine = "";

                for (const rawToken of tokens) {
                    const token = rawToken;
                    measurer.text = currentLine + token;

                    // Check if adding the token exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        // Push the current line (trim trailing spaces)
                        lines.push(currentLine.replace(/\s+$/g, ""));

                        // Handle the next token (remove leading spaces)
                        let nextToken = token.replace(/^\s+/g, "");
                        measurer.text = nextToken;

                        // If the single token itself is wider than the limit, force character split
                        if (nextToken !== "" && measurer.getMeasuredWidth() > limitWidth) {
                            const charLines = wrapByCharacter(measurer, nextToken, limitWidth);
                            if (charLines.length > 1) {
                                lines.push(...charLines.slice(0, -1));
                                currentLine = charLines[charLines.length - 1] || "";
                            } else {
                                currentLine = charLines[0] || "";
                            }
                        } else {
                            currentLine = nextToken;
                        }
                    } else {
                        currentLine += token;
                    }
                }

                if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
                return lines;
            }

            // Function: Calculate line breaks and total height
            function calculateLayout(measurer, rawString, limitWidth, lineHeight, isCharSplit) {
                const lines = isCharSplit
                    ? wrapByCharacter(measurer, rawString, limitWidth)
                    : wrapByWord(measurer, rawString, limitWidth);

                return {
                    wrappedText: lines.join("\n"),
                    totalHeight: lines.length * lineHeight
                };
            }

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                const currentLineHeight = size * lineHeightRatio;

                measureObj.font = fontString;

                const result = calculateLayout(measureObj, text, maxWidth, currentLineHeight, splitByCharacter);

                if (result.totalHeight <= maxHeight) {
                    // Apply successful settings
                    internalText.font = fontString;
                    internalText.lineHeight = currentLineHeight;
                    label.text = result.wrappedText;

                    // Horizontal Alignment
                    if (textAlign === "center") {
                        label.x = maxWidth / 2;
                    } else if (textAlign === "right") {
                        label.x = maxWidth;
                    } else {
                        label.x = 0;
                    }

                    // Vertical Alignment (Center)
                    label.y = (maxHeight - result.totalHeight) / 2;

                    return label;
                }
            }

            // If text doesn't fit even at min size, enforce minimum settings
            internalText.font = `${minFontSize}px ${fontName}`;
            internalText.lineHeight = minFontSize * lineHeightRatio;
            label.text = text;

            if (textAlign === "center") {
                label.x = maxWidth / 2;
            } else if (textAlign === "right") {
                label.x = maxWidth;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }

    } // end ready

</script>
<meta name="viewport" content="width=device-width, user-scalable=no" />
1 Like

There is no lineHeight property on the ZIM Label. If you use the CreateJS lineHeight property afterwards, yes - it will not be recorded as setting the size within the labelHeight.

I think we would need to add a labelHeight property to the ZIM Label and make it work to rescale, etc. For much of ZIM, we did not even know CreateJS had a lineHeight. So we must have missed at that time, adding a lineHeight property to the Label.

We will have a look. Have added it to requests.

1 Like

Thank you very much!
This code seems to multiply lineHeightRatio and font size before processing.
Anyway, have a happy New Year!

function generateText({
            text = "Text",
            maxWidth = 300,
            maxHeight = 300,
            fontName,
            color,
            maxFontSize = 80,
            lineHeightRatio = 1.4,
            textAlign = "center",
            splitByCharacter = true,
            minFontSize = 10
        }) {
            // Create the ZIM Label
            const label = new Label({
                text: "",
                size: maxFontSize,
                font: fontName,
                color: color
            });

            // Access the internal CreateJS Text object
            const internalText = label.label;
            internalText.textAlign = textAlign;
            internalText.textBaseline = "middle";

            // Create a dummy Text object for measuring width/height
            const measureObj = new createjs.Text("", "", color);

            // Function: Wrap text by character (splits character by character)
            function wrapByCharacter(measurer, rawString, limitWidth) {
                const lines = [];
                let currentLine = "";

                for (let i = 0; i < rawString.length; i++) {
                    const char = rawString[i];
                    measurer.text = currentLine + char;

                    // Check if adding the character exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        lines.push(currentLine);
                        currentLine = char;
                    } else {
                        currentLine += char;
                    }
                }
                if (currentLine !== "") lines.push(currentLine);
                return lines;
            }

            // Function: Wrap text by word (keeps words intact unless too long)
            function wrapByWord(measurer, rawString, limitWidth) {
                // Split by whitespace but keep the token logic
                const tokens = rawString.match(/\S+\s*/g) || [""];
                const lines = [];
                let currentLine = "";

                for (const rawToken of tokens) {
                    const token = rawToken;
                    measurer.text = currentLine + token;

                    // Check if adding the token exceeds the width
                    if (currentLine !== "" && measurer.getMeasuredWidth() > limitWidth) {
                        // Push the current line (trim trailing spaces)
                        lines.push(currentLine.replace(/\s+$/g, ""));

                        // Handle the next token (remove leading spaces)
                        let nextToken = token.replace(/^\s+/g, "");
                        measurer.text = nextToken;

                        // If the single token itself is wider than the limit, force character split
                        if (nextToken !== "" && measurer.getMeasuredWidth() > limitWidth) {
                            const charLines = wrapByCharacter(measurer, nextToken, limitWidth);
                            if (charLines.length > 1) {
                                lines.push(...charLines.slice(0, -1));
                                currentLine = charLines[charLines.length - 1] || "";
                            } else {
                                currentLine = charLines[0] || "";
                            }
                        } else {
                            currentLine = nextToken;
                        }
                    } else {
                        currentLine += token;
                    }
                }

                if (currentLine !== "") lines.push(currentLine.replace(/\s+$/g, ""));
                return lines;
            }

            // Function: Calculate line breaks and total height
            function calculateLayout(measurer, rawString, limitWidth, lineHeight, isCharSplit) {
                const lines = isCharSplit
                    ? wrapByCharacter(measurer, rawString, limitWidth)
                    : wrapByWord(measurer, rawString, limitWidth);

                return {
                    wrappedText: lines.join("\n"),
                    totalHeight: lines.length * lineHeight
                };
            }

            // Try font sizes from largest to smallest
            for (let size = maxFontSize; size >= minFontSize; size--) {
                const fontString = `${size}px ${fontName}`;
                const currentLineHeight = size * lineHeightRatio;

                measureObj.font = fontString;

                const result = calculateLayout(measureObj, text, maxWidth, currentLineHeight, splitByCharacter);

                if (result.totalHeight <= maxHeight) {
                    // Apply successful settings
                    internalText.font = fontString;
                    internalText.lineHeight = currentLineHeight;
                    label.text = result.wrappedText;

                    // Horizontal Alignment
                    if (textAlign === "center") {
                        label.x = maxWidth / 2;
                    } else if (textAlign === "right") {
                        label.x = maxWidth;
                    } else {
                        label.x = 0;
                    }

                    // Vertical Alignment (Center)
                    label.y = (maxHeight - result.totalHeight) / 2;

                    return label;
                }
            }

            // If text doesn't fit even at min size, enforce minimum settings
            internalText.font = `${minFontSize}px ${fontName}`;
            internalText.lineHeight = minFontSize * lineHeightRatio;
            label.text = text;

            if (textAlign === "center") {
                label.x = maxWidth / 2;
            } else if (textAlign === "right") {
                label.x = maxWidth;
            } else {
                label.x = 0;
            }
            label.y = 0;

            return label;
        }

    }
1 Like