Renders text as justified paragraphs. REQUIRES the example Justified text

Example render

Top paragraph has setting.compact = true and bottom false and line spacing is 1.2 rather than the default 1.5. Rendered by code usage example bottom of this example.

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/d748a033-2216-4327-8f4d-49940f60b66a/Untitled.png


Example code

// Requires justified text extensions 
(function(){
   // code point A
   if(typeof CanvasRenderingContext2D.prototype.fillJustifyText !== "function"){
       throw new ReferenceError("Justified Paragraph extension missing requiered CanvasRenderingContext2D justified text extension");
   }
   var maxSpaceSize = 3; // Multiplier for max space size. If greater then no justificatoin applied
   var minSpaceSize = 0.5; // Multiplier for minimum space size   
   var compact = true; // if true then try and fit as many words as possible. If false then try to get the spacing as close as possible to normal
   var lineSpacing = 1.5; // space between lines
   const noJustifySetting = {  // This setting forces justified text off. Used to render last line of paragraph.
       minSpaceSize : 1,
       maxSpaceSize : 1,
   }

   // Parse vet and set settings object.
   var justifiedTextSettings = function(settings){
       var min, max;
       var vetNumber = (num, defaultNum) => {
           num = num !== null && num !== null && !isNaN(num) ? num : defaultNum;
           return num < 0 ? defaultNum : num;
       }
       if(settings === undefined || settings === null){ return; }
       compact = settings.compact === true ? true : settings.compact === false ? false : compact;
       max = vetNumber(settings.maxSpaceSize, maxSpaceSize);
       min = vetNumber(settings.minSpaceSize, minSpaceSize);
       lineSpacing = vetNumber(settings.lineSpacing, lineSpacing);
       if(min > max){ return; }
       minSpaceSize = min;
       maxSpaceSize = max;
   }        
   var getFontSize = function(font){  // get the font size. 
       var numFind = /[0-9]+/;
       var number = numFind.exec(font)[0];
       if(isNaN(number)){
           throw new ReferenceError("justifiedPar Cant find font size");
       }
       return Number(number);
   }
   function justifiedPar(ctx, text, x, y, width, settings, stroke){
       var spaceWidth, minS, maxS, words, count, lines, lineWidth, lastLineWidth, lastSize, i, renderer, fontSize, adjSpace, spaces, word, lineWords, lineFound;
       spaceWidth = ctx.measureText(" ").width;
       minS = spaceWidth * minSpaceSize;
       maxS = spaceWidth * maxSpaceSize;
       words = text.split(" ").map(word => {  // measure all words.
           var w = ctx.measureText(word).width;                
           return {
               width : w,
               word : word,
           };
       });
       // count = num words, spaces = number spaces, spaceWidth normal space size
       // adjSpace new space size >= min size. useSize Resulting space size used to render
       count = 0;
       lines = [];
       // create lines by shifting words from the words array until the spacing is optimal. If compact
       // true then will true and fit as many words as possible. Else it will try and get the spacing as
       // close as possible to the normal spacing
       while(words.length > 0){
           lastLineWidth = 0;
           lastSize = -1;
           lineFound = false;
           // each line must have at least one word.
           word = words.shift();
           lineWidth = word.width;
           lineWords = [word.word];
           count = 0;
           while(lineWidth < width && words.length > 0){ // Add words to line
               word = words.shift();
               lineWidth += word.width;
               lineWords.push(word.word);
               count += 1;
               spaces = count - 1;
               adjSpace =  (width - lineWidth) / spaces;
               if(minS > adjSpace){  // if spacing less than min remove last word and finish line
                   lineFound = true;
                   words.unshift(word);
                   lineWords.pop();
               }else{
                   if(!compact){ // if compact mode 
                       if(adjSpace < spaceWidth){ // if less than normal space width
                           if(lastSize === -1){
                               lastSize = adjSpace;
                           }
                           // check if with last word on if its closer to space width
                           if(Math.abs(spaceWidth - adjSpace) < Math.abs(spaceWidth - lastSize)){
                               lineFound = true; // yes keep it
                           }else{
                               words.unshift(word);  // no better fit if last word removes
                               lineWords.pop();
                               lineFound = true;
                           }
                       }
                   }
               }
               lastSize = adjSpace; // remember spacing 
           }
           lines.push(lineWords.join(" ")); // and the line
       }
       // lines have been worked out get font size, render, and render all the lines. last
       // line may need to be rendered as normal so it is outside the loop.
       fontSize = getFontSize(ctx.font);
       renderer = stroke === true ? ctx.strokeJustifyText.bind(ctx) : ctx.fillJustifyText.bind(ctx);
       for(i = 0; i < lines.length - 1; i ++){
           renderer(lines[i], x, y, width, settings);
           y += lineSpacing * fontSize;
       }
       if(lines.length > 0){ // last line if left or start aligned for no justify
           if(ctx.textAlign === "left" || ctx.textAlign === "start"){
               renderer(lines[lines.length - 1], x, y, width, noJustifySetting);
               ctx.measureJustifiedText("", width, settings);
           }else{
               renderer(lines[lines.length - 1], x, y, width);
           }
       }
       // return details about the paragraph.
       y += lineSpacing * fontSize;
       return {
           nextLine : y,
           fontSize : fontSize,
           lineHeight : lineSpacing * fontSize,
       };
   }
   // define fill
   var fillParagraphText = function(text, x, y, width, settings){
       justifiedTextSettings(settings);
       settings = {
           minSpaceSize : minSpaceSize,
           maxSpaceSize : maxSpaceSize,
       };
       return justifiedPar(this, text, x, y, width, settings);
   }
   // define stroke
   var strokeParagraphText = function(text, x, y, width, settings){
       justifiedTextSettings(settings);
       settings = {
           minSpaceSize : minSpaceSize,
           maxSpaceSize : maxSpaceSize,
       };
       return justifiedPar(this, text, x, y, width, settings,true);
   }
   CanvasRenderingContext2D.prototype.fillParaText = fillParagraphText;
   CanvasRenderingContext2D.prototype.strokeParaText = strokeParagraphText;
})();

NOTE this extends the CanvasRenderingContext2D prototype. If you do not wish this to happen use the example Justified text to work out how to change this example to be part of the global namespace.

NOTE Will throw a ReferenceError if this example can not find the function CanvasRenderingContext2D.prototype.fillJustifyText

How to use

ctx.fillParaText(text, x, y, width, [settings]);
ctx.strokeParaText(text, x, y, width, [settings]);

See Justified text for details on arguments. Arguments between \\[ and \\] are optional.

The settings argument has two additional properties.

Properties missing from the settings object will default to their default values or to the last valid values. The properties will only be changed if the new values are valid. For compact valid values are only booleans true or false Truthy values are not considered valid.

Return object

The two functions return an object containing information to help you place the next paragraph. The object contains the following properties.