Skip to content Skip to sidebar Skip to footer

How Can I Colour Different Words In The Same Line With HTML5 Canvas?

Consider this loop that runs through key value pairs in an object: Object.keys(categories).forEach(categoryKey => { ctx.fillText(`${categoryKey}: ${categories[categoryKey]}`,

Solution 1:

There are many times I need to format text for canvas rendering (particularly for math examples) so below is a modification of the function I use to modify the rendered text using a nested formatting guide.

Demo

A modified copy from my own library that formats text according to nested style rules.

The simpleTextStyler works by first getting the size of each character in the font (use simpleTextStyler.setFont() to set the current context font or it will not work) and then looking at each character in the string separating out groups of characters with the same styles and rendering them one at a time.

It uses a stack to hold the current style pushing a style onto the stack if a new one is found "{" and popping the style off the stack at each closing "}"

It will add a new line if "\n" is found. Move to the next tab stop if "\t" and use various nested style inside "{" and "}" where the first character after the "{" denotes the function.

  • s sub script
  • S super script
  • + bigger text
  • - smaller text
  • # colour must be followed by 6 char hex colour eg red {#FF0000This text is red}

NOTE: Must use only 7 char CSS hex colour. eg "Change colour to {#FF0000Red}"

Text styles can be nested eg "text{Ssuper{ssub{Ssuper}}{Ssuper super}}" is text

The demo renders the string "Testing\nnewline\n\tTab\n\t\tTab\n\t\t\tTab\nSub{sScript} Super{SScript} Size {+Big {+Bigger}} Normal {-Small {-Smaller}}\nAnd now colours \n{#FF0000Red} {#00FF00Green} {#0000FFBlue}"

For your need

simpleTextStyler.setFont(); // only needs to be set for the font family
                            // sizing text is in the last argument of the next call

simpleTextStyler.drawText(ctx,
    `{#FF0000${categoryKey}}: ${categories[categoryKey]}`, 40, (30 * i) + 160,fontSize);

var ctx = canvas.getContext("2d");
ctx.font = "18px arial";
setTimeout(drawExamples,0);
function drawExamples(){
  simpleTextStyler.setFont(); // set the current font
  simpleTextStyler.drawText(ctx,
  "Testing simple Canvas2D text styler...\nnewline\n\tTab\n\t\tTab\n\t\t\tTab\nSub{sScript} Super{SScript} Size {+Big {+Bigger}} Normal {-Small {-Smaller}}\nAnd now colours \n{#FF0000Red} {#00FF00Green} {#0000FFBlue}",
      10,20,18)
}
      

const simpleTextStyler = (function(){
    const simpleTextStyler = {
        sizes: [],
        baseSize: undefined,
        font: undefined,
        controlChars: "{}\n\t",
        spaceSize: 0,
        tabSize: 8, // in spaceSize units
        tabs: (function() {var t = []; for(var i=0; i < 100; i += 8){t.push(i);}; return t;})(),
        getNextTab: function(x) {
            var i = 0;
            while (i < this.tabs.length) {
                if (x < this.tabs[i] * this.tabSize * this.spaceSize) {
                    return this.tabs[i] * this.tabSize * this.spaceSize;
                }
                i++;
            }
            return this.tabs[i-1] * this.tabSize * this.spaceSize;
        },
        getFontSize: function(font){
            var numFind = /[0-9]+/;
            var number = numFind.exec(font)[0];
            if (isNaN(number)) {
                throw Error("SimpleTextStyler Cant find font size");
            }
            return Number(number);
        },
        setFont: function(font = ctx.font) {
            this.font = ctx.font = font;
            this.baseSize = this.getFontSize(font);
            for (var i = 32; i < 256; i ++) {
                this.sizes[i - 32] = ctx.measureText(String.fromCharCode(i), 0, 0).width/this.baseSize;
            }
            this.spaceSize = this.sizes[0];
        },
        drawText: function(context, text, x, y, size) {
            var i, len, subText;
            var w, scale;
            var xx, yy, ctx;
            var state = [];
            if(text === undefined){ return }
            xx = x;
            yy = y;
            if (!context.setTransform) { // simple test if this is a 2D context
                if (context.ctx) { ctx = context.ctx } // may be a image with attached ctx?
                else{ return }
            } else { ctx = context }

            function renderText(text) {
                ctx.save();
                ctx.fillStyle = colour;
                ctx.translate(x, y)
                ctx.scale(scale, scale)
                ctx.fillText(text, 0, 0);
                ctx.restore();
            }
            var colour = ctx.fillStyle;
            ctx.font = this.font;
            len = text.length;
            subText = "";
            w = 0;
            i = 0;
            scale = size / this.baseSize;
            while (i < len) {
                const c = text[i];
                const cc = text.charCodeAt(i);
                if (cc < 256) { // only ascii
                    if (this.controlChars.indexOf(c) > -1) {
                        if (subText !== "") {
                            scale = size / this.baseSize;
                            renderText(subText);
                            x += w;
                            w = 0;
                            subText = "";                        
                        }
                        if (c === "\n") {  // return move to new line
                            x = xx;
                            y += size;
                        } else if (c === "\t") { // tab move to next tab
                            x = this.getNextTab(x - xx) + xx;
                        } else if (c === "{") {   // Text format delimiter                       
                            state.push({size, colour, x, y})
                            i += 1;
                            const t = text[i];
                            if (t === "+") {  // Increase size
                                size *= 1/(3/4);
                            } else if (t === "-") {  // decrease size
                                size *= 3/4;
                            } else if (t === "s") { // sub script
                                y += size * (1/3);
                                size  *= (2/3);
                            } else if (t === "S") { // super script
                                y -= size * (1/3);
                                size  *= (2/3);
                            } else if (t === "#") {
                                colour = text.substr(i,7);
                                i+= 6;
                            }
                        } else if (c  === "}"){
                            const s = state.pop();
                            y = s.y;
                            size = s.size;
                            colour = s.colour;
                            scale = size / this.baseSize;
                        }
                    } else {
                        subText += c;
                        w += this.sizes[cc-32] * size;
                    }
                 }
                 i += 1;
            }
            if (subText !== "") { renderText(subText) }
        },
    }
    return simpleTextStyler;
})();
canvas {
    border : 2px solid black;
}
<canvas id="canvas" width=512 height=160></canvas>

Solution 2:

Credit to @Blindman67 for suggesting the use of measureText().

Here's how I did it in the end (though some of the code in this context wont make sense to you probably as it's part of bigger thing). Hopefully you should be able to see the key parts:

let i = 0;

Object.keys(categories).forEach(categoryKey => {
  const category = paramKeyToCategoryName(categoryKey);
  const offsetTop = 190;
  const offsetLeft = 40;
  const spacing = 30;

  if (category.length) {
    // Category text.
    ctx.fillStyle = '#f13f3d';
    ctx.fillText(`${category.toUpperCase()}:`, offsetLeft, (spacing * i) + offsetTop);

    // Measure category text length.
    const measure = ctx.measureText(category.toUpperCase());
    const measureWidth = measure.width + 12;

    // Selected artist text.
    ctx.fillStyle = '#fff';
    ctx.fillText(`${categories[categoryKey].toUpperCase()}`, (offsetLeft + measureWidth), (spacing * i) + offsetTop);

    i++;
  }
});

Hope this helps someone.


Post a Comment for "How Can I Colour Different Words In The Same Line With HTML5 Canvas?"