Search code examples
javascripthtmlcanvasrotationword-wrap

html5 canvas: auto font size for drawn wrapped rotated text


suppose that there is a text to be drawn inside a rotated bounding rectangle (not aligned to normal axes x-y), and that text can be also rotated, given the max width of the bounding box, how to select the best font size to use to draw a wrapped text inside that bounding box in html5 canvas and javascript?

I know that method: measureText() can measure dimensions of give font size, but I need the inverse of that: using a known width to get the problem font size.

thanks


Solution

  • You do not have to find the font point size to make it fit. The font will smoothly scale up and down according to the current transformation scale.

    All you do is measureText to find its textWidth, get the pointSize from the context.font attribute then if you have the width and height of the box you need to fit then find the minimum of the width / textWidth and height / pointSize and you have the scale that you need to render the font at.

    As a function

    var scale2FitCurrentFont = function(ctx, text, width, height){
        var points, fontWidth;
        points = Number(ctx.font.split("px")[0]); // get current point size
        points += points * 0.2; // As point size does not include hanging tails and
                                // other top and bottom extras add 20% to the height
                                // to accommodate the extra bits  
        var fontWidth = ctx.measureText(text).width;
        // get the max scale that will allow the text to fi the current font
        return Math.min(width / fontWidth, height / points);
    }
    

    The arguments are

    • ctx is current context to draw to
    • text the text to draw
    • width the width to fit the text to
    • height the height to fit the text to

    Returns the scale to fit the text within the width and height.

    The demo has it all integrated and it draws random boxes and fills with random text from your question. It keeps the font selection and point size separate from the font scaling so you can see it will work for any font and any point size.

    var demo = function(){
        
        /** fullScreenCanvas.js begin **/
        var canvas = (function(){
            var canvas = document.getElementById("canv");
            if(canvas !== null){
                document.body.removeChild(canvas);
            }
            // creates a blank image with 2d context
            canvas = document.createElement("canvas"); 
            canvas.id = "canv";    
            canvas.width = window.innerWidth;
            canvas.height = window.innerHeight; 
            canvas.style.position = "absolute";
            canvas.style.top = "0px";
            canvas.style.left = "0px";
            canvas.style.zIndex = 1000;
            canvas.ctx = canvas.getContext("2d"); 
            document.body.appendChild(canvas);
            return canvas;
        })();
        var ctx = canvas.ctx;
        
        /** fullScreenCanvas.js end **/
        /** FrameUpdate.js begin **/
        var w = canvas.width;
        var h = canvas.height;
        var cw = w / 2;
        var ch = h / 2;
        
        var PI2 = Math.PI * 2; // 360 to save typing 
        var PIh = Math.PI / 2; // 90 
        
        
        
        // draws a rounded rectangle path
        function roundedRect(ctx,x, y, w, h, r){
    
            ctx.beginPath(); 
            ctx.arc(x + r, y + r, r, PIh * 2, PIh * 3);  
            ctx.arc(x + w - r, y + r, r, PIh * 3, PI2);
            ctx.arc(x + w - r, y + h - r, r, 0, PIh);  
            ctx.arc(x + r, y + h - r, r, PIh, PIh * 2);  
            ctx.closePath(); 
        }
        
        // random words
        var question = "Suppose that there is a text to be drawn inside a rotated bounding rectangle (not aligned to normal axes x-y), and that text can be also rotated, given the max width of the bounding box, how to select the best font size to use to draw a wrapped text inside that bounding box in html5 canvas and javascript? I know that method: measureText() can measure dimensions of give font size, but I need the inverse of that: using a known width to get the problem font size. thanks.";
        question = question.split(" ");
        var getRandomWords= function(){
            var wordCount, firstWord, s, i, text;
            wordCount = Math.floor(rand(4)+1);
            firstWord = Math.floor(rand(question.length - wordCount));
            text = "";
            s = "";
            for(i = 0; i < wordCount; i++){
                text += s + question[i + firstWord];
                s = " ";  
            }
            return text;
        }
        
    
        // fonts to use?? Not sure if these are all safe for all OS's
        var fonts = "Arial,Arial Black,Verdanna,Comic Sans MS,Courier New,Lucida Console,Times New Roman".split(",");
        // creates a random font with random points size in pixels
        var setRandomFont = function(ctx){
            var size, font;
            size = Math.floor(rand(10, 40));
            font = fonts[Math.floor(rand(fonts.length))];
            ctx.font = size + "px " + font;
        }
        var scale2FitCurrentFont = function(ctx, text, width, height){
            var points, fontWidth;
            var points = Number(ctx.font.split("px")[0]); // get current point size
            points += points * 0.2;
            var fontWidth = ctx.measureText(text).width;
            // get the max scale that will allow the text to fi the current font
            return Math.min(width / fontWidth, height / points);
        }
    
    
        var rand = function(min, max){
            if(max === undefined){
                max = min;
                min = 0;
            }
            return Math.random() * (max - min)+min;
        }
        var randomBox = function(ctx){
            "use strict";
            var width, height, rot, dist, x, y, xx, yy,cx, cy, text, fontScale;
            // get random box
            width = rand(40, 400);
            height = rand(10, width * 0.4);
            rot = rand(-PIh,PIh);
            dist = Math.sqrt(width * width + height * height)
            x = rand(0, ctx.canvas.width - dist);
            y = rand(0, ctx.canvas.height - dist);
            xx = Math.cos(rot);
            yy = Math.sin(rot);
            ctx.fillStyle = "white";
            ctx.strokeStyle = "black";
            ctx.lineWidth = 2;
            // rotate the box
            ctx.setTransform(xx, yy, -yy, xx, x, y);
            // draw the box
            roundedRect(ctx, 0, 0, width, height, Math.min(width / 3, height / 3));
            ctx.fill();
            ctx.stroke();
            
            // get some random text
            text = getRandomWords();
            // get the scale that will fit the font
            fontScale = scale2FitCurrentFont(ctx, text, width - textMarginLeftRigth * 2, height - textMarginTopBottom * 2);
            // get center of rotated box
            cx = x + width / 2 * xx + height / 2 * -yy;
            cy = y + width / 2 * yy + height / 2 * xx;
            // scale the transform
            xx *= fontScale;
            yy *= fontScale;
            // set the font transformation to fit the box
            ctx.setTransform(xx, yy, -yy, xx, cx, cy);
            // set up the font render
            ctx.fillStyle = "Black";
            ctx.textAlign = "center";
            ctx.textBaseline = "middle"
            // draw the text to fit the box
            ctx.fillText(text, 0, 0);
        }
        var textMarginLeftRigth = 8; // margin for fitted text in pixels
        var textMarginTopBottom = 4; // margin for fitted text in pixels
        var drawBoxEveryFrame = 60; // frames between drawing new box
        var countDown = 1;
        // update function will try 60fps but setting will slow this down.    
        function update(){
            // restore transform
            ctx.setTransform(1, 0, 0, 1, 0, 0);
          
            // fade clears the screen 
            ctx.fillStyle = "white"
            ctx.globalAlpha = 1/ (drawBoxEveryFrame * 1.5);
            ctx.fillRect(0, 0, w, h);
          
            // reset the alpha
            ctx.globalAlpha = 1;
            
            // count frames
            countDown -= 1;
            if(countDown <= 0){ // if frame count 0 the draw another text box
                countDown = drawBoxEveryFrame;
                setRandomFont(ctx);
                randomBox(ctx);
            }
            
            if(!STOP){ // do until told to stop.
                requestAnimationFrame(update);
            }else{
                STOP = false;
                
            }
        }
        update();
    }
    
    // demo code to restart on resize
    var STOP = false;  // flag to tell demo app to stop 
    function resizeEvent(){
        var waitForStopped = function(){
            if(!STOP){  // wait for stop to return to false
                demo();
                return;
            }
            setTimeout(waitForStopped,200);
        }
        STOP = true;
        setTimeout(waitForStopped,100);
    }
    window.addEventListener("resize",resizeEvent);
    demo();
    /** FrameUpdate.js end **/