webbist
webbist

Reputation: 456

Drawing text on canvas by the letter to control the alpha of individual letters while being aligned to centre

I am trying to draw some letters to a canvas in a very specific way - able to target individual letters and apply an alpha. These words need to be centered on the baseline and aligned center in the canvas and filled with strokeText rather than fill style.

The text also needs to be line broken resulting in eg;

enter image description here

Now, I have tried several ways of getting this out - it works fine (without the fade) when writing out the words (as full words) - however when I attempt to write them out as individual letters I cannot center them correctly. My code is below omitting the alpha on the specific letters, which once I can center things correctly shouldn't be an issue!

I realize the issue is I am trying to draw each letter separately centered at 0 on the canvas and adding letter spacing for each letter, but given the different size of the middle line I cannot figure a way to have them centered!

var can = document.querySelector('canvas'),
    ctx = can.getContext('2d');
    
 function drawStroked(text, fontSize, color, offsetX, offsetY) {
        let line = text.split('\n');
        this.ctx.font = fontSize + 'px ' + 'TimesNewRoman';
        this.ctx.strokeStyle = color;
        this.ctx.lineWidth = 2;
        this.ctx.textBaseline = 'middle';
        this.ctx.textAlign = 'center';
        let positionX = this.ctx.canvas.width/3;
        let positionY = this.ctx.canvas.height/4;

        if(offsetX !== 0) {
            positionX += offsetX;
        }

        if(offsetY !== 0) {
            positionY += offsetY;
        }
        for (var i = 0; i < line.length; i++) {
            for (var j = 0; j < line[i].length; j++) {
                let letterSpacing = 0;
                let lineHeight = positionY;

                if(line[i][j] === line[i].length) {
                    lineHeight = lineHeight * i;
                }
            this.ctx.strokeText(line[i][j], positionX + (letterSpacing + (j*130)), positionY + (i*fontSize));
            }
        }
    }
    
drawStroked('THIS\nIS THE\nTEXT', 100, '#000', 0, 0);
<canvas width="1000" height="1000"></canvas>


Resulting output & Finished code thanks to Blindman67!

enter image description here

const Hero = class {
    constructor(pos, canvas) {
        this.position = document.getElementById(pos);
        this.canvas = document.getElementById(canvas);
        this.height = document.getElementsByClassName('home')[0].clientHeight;
        this.width = this.position.offsetWidth;
        this.ctx = this.canvas.getContext('2d');
        this.title = 'THIS\nIS THE\nTEXT';
        this.canvas.width = this.width;
        this.canvas.height = this.height;

        this._init_ui();
    }

    // Draw text to text canvas
    _init_ui() {
        // BLUE
        this.drawStroked(300, '#1816ff', -3, 2, 0.95, [1, 5, 9, 12]);
        // GREEN
        this.drawStroked(300, '#1bff32', 0, 0, 0.95, [1, 5, 9, 12]);
        // RED
        this.drawStroked(300, '#ff162f', 3, -2, 0.95, [1, 5, 9, 12]);
    }

    drawStroked(fontSize, color, offsetX, offsetY, textVertSpacing, fade) {
        // Random Char's to scramble through --- to do
        // let chars = '!<>-_\\/[]{}—=+*^?#________';
        // The words
        let line = this.title.split('\n');
        // Set the font + size
        this.ctx.font = fontSize + 'px ' + 'Kommissar';
        // Set the colour - NEED TO ADD ALPHA LOGIC
        this.ctx.strokeStyle = color;
        // Set the stroke width
        this.ctx.lineWidth = 1;
        // Set the baseline
        this.ctx.textBaseline = 'middle';
        // Set the align
        this.ctx.textAlign = 'center';

        let positionX = this.width/2;
        let positionY = this.height/4;
        positionX += offsetX;
        positionY += offsetY;

        let charIndex = 0;

        for (var i = 0; i < line.length; i++) {
            // get the width of the whole line
            let width = this.ctx.measureText(line[i]).width;
            console.log(width);
            // use the width to find start
            var textPosX = positionX - width / 2;

            for (let j = 0; j < line[i].length; j++) {
                // get char
                let char = line[i][j];
                // get its width
                let cWidth = this.ctx.measureText(char).width;
                // check if char needs to fade
                if (fade.indexOf(charIndex) > -1) {
                    this.ctx.globalAlpha = 0.2;
                } else {
                    this.ctx.globalAlpha = 1;
                }
                // draw the char offset by half its width (center)
                this.ctx.strokeText(char, textPosX + cWidth / 2, positionY);
                // move too the next pos
                textPosX += cWidth;
                // count the char
                charIndex += 1;
            }
            // move down one line
            positionY += fontSize * textVertSpacing;
        }
    }
};

export default Hero;

Upvotes: 0

Views: 2230

Answers (2)

webbist
webbist

Reputation: 456

Using this in a webpack setup so sorry it doesnt run!

// Code in UTIL
getRandomInt(max) {
  return Math.floor(Math.random() * (max - 0 + 1)) + 0;
};

const $window = $(window);

let running = false;

const Hero = {
    init() {
        this.home          = $('#home');
        this.position      = $('#hero');
        this.canvas        = $('#title');
        this.ctx           = this.canvas[0].getContext('2d');
        this.width         = this.position.width();
        this.height        = this.home.height();
        this.ctx.lineWidth = 1.5;
        this.fontSize      = null;
        this.letterSpacing = null;

        if(this.position.lenth === 0) {
            return;
        }

        if(running) {
            return;
        }

        // Set hero opacity to 0 for animation
        // $('#hero').css('opacity', 0);

        this.size();

        $window.on('resize', () => {
            clearTimeout(this.debounce);
            this.debounce = setTimeout( () => {
                this.height = this.home.height();
                this.width = this.position.width();

                this.size();
            }, 50);
        });
    },

    size() {
        running = true;

        this.canvas[0].width = this.width;
        this.canvas[0].height = this.height;

        if(this.width < 1000) {
            this.fontSize = 150;
            this.letterSpacing = 5;
        } else {
            this.fontSize = 300;
            this.letterSpacing = 30;
        }
    },

    animate(frames) {
        var frameCount = frames || 0;
        const flickerRate = 4;
        const fade = [Utils.getRandomInt(13), Utils.getRandomInt(13)];

       if((frameCount % flickerRate) === 0){
            this.ctx.clearRect(0, 0, this.width, this.height);
            // Blue
            this.drawStroked(this.fontSize, '#0426ff', -2, 2, true, fade);
            // Green
            this.drawStroked(this.fontSize, '#04ffae', 1, 2, true, fade);
            // Pink
            this.drawStroked(this.fontSize, '#ff29ad', 0, 0, true, fade);
            // White
            this.drawStroked(this.fontSize, '#fff', 0, 0, true, fade);
        }

        frameCount ++;
        console.log(frameCount);
        // requestAnimationFrame(this.animate);
        setTimeout(() => {
            this.animate(frameCount);
        }, 0.5);
    },

    drawStroked(fontSize, color, offsetX, offsetY, flicker, fade) {
        let line  = 'CODE\nIN THE\nDARK'.split('\n'),
            chars = line.join('');
        // Set the font + size
        this.ctx.font = fontSize + 'px ' + 'Kommissar';
        // Set the colour
        this.ctx.strokeStyle = color;
        // Set the baseline
        this.ctx.textBaseline = 'middle';
        // Set the align
        this.ctx.textAlign = 'center';

        let letterSpacing = this.letterSpacing,
            positionX = (this.width/2 + letterSpacing) + offsetX,
            positionY = (this.height/4) + offsetY,
            charIndex = 0;

        for (var i = 0; i < line.length; i++) {
            // get the width of the whole line
            let width = this.ctx.measureText(line[i]).width;
            // use the width to find start
            var textPosX = positionX - width / 2;

            for (let j = 0; j < line[i].length; j++) {
                // get char
                let char = line[i][j];
                // get its width
                let cWidth = this.ctx.measureText(char).width;
                // check if char needs to fade
                if(flicker) {
                    this.ctx.globalAlpha = fade.indexOf(charIndex) > -1 ? Math.random() * 0.5 + 0.25 : 0;
                } else {
                    this.ctx.globalAlpha = 1;
                }
                // draw the char offset by half its width (center)
                this.ctx.shadowColor = color;
                this.ctx.shadowBlur = 15;
                this.ctx.strokeText(char, textPosX + cWidth / 2, positionY);
                // move too the next pos
                textPosX += cWidth;
                // count the char
                charIndex += 1;
            }
            // move down one line
            positionY += fontSize * 1.05;
        }
    }
};

export default Hero;
#home {
   width: 100%;

  #hero {
      position: absolute;
      z-index: 5;
      top: 0;
      left: 0;
      width: 100%;
      padding: 30px 0;

      > canvas {
          margin: 0 auto;
          display: block;
      }
  }
}
<div id="home">
    <div id="hero">
      <canvas id="title"></canvas>
    </div>
</div>

Upvotes: 0

Blindman67
Blindman67

Reputation: 54026

Use ctx.measureText

You need to use ctx.measureText and get the the width of each character, then you can space them correctly.

Correct char spacing

Because you have alignment center, you have to move the character half its width, then draw it and then move half the width again. The spacing between character's centers is half the width of each added. So if a "I" is 20 pixels wide and a "W" is 60 then the space between them is 10 + 30 = 40;

Fade characters

To do the fade I passed an array with the index of the characters to fade. Each Time I draw a character I count it. To check if a character should fade I check the index array for the character count. If they match then fade that character.

See the example for more information

Simple example...

...of what I think you want. I added two red lines to make sure the alignment was correct.

const ctx = canvas.getContext('2d');
ctx.fillStyle = "#FDD"; // mark the center
ctx.fillRect(canvas.width / 2 | 0, 0, 1, canvas.height);
ctx.fillRect(0, canvas.height / 2 | 0, canvas.width, 1);
ctx.fillStyle = "black";

// textVertSpacing is fraction of FontSize
// fade is the index of characters to fade, including spaces
// centerX and y is center of all text
function drawStroked(text, fontSize, color, centerX, centerY, textVertSpacing, fade) {
  let line = text.split('\n');
  ctx.font = fontSize + 'px ' + 'TimesNewRoman';
  ctx.strokeStyle = color;
  ctx.lineWidth = 2;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  // to count each character 
  var charIndex = 0;
  // find the top ypos and then move down half a char space
  var yPos = centerY - fontSize * line.length * 0.5 * textVertSpacing + fontSize * textVertSpacing / 2;
  
  for (var i = 0; i < line.length; i++) {
    // get the width of the whole line
    var width = ctx.measureText(line[i]).width;
    // use the width to find start
    var textPosX = centerX - width / 2;
    for (var j = 0; j < line[i].length; j++) {
      // get char
      var char = line[i][j];
      // get its width
      var cWidth = ctx.measureText(char).width;
      // check if char needs to fade
      if (fade.indexOf(charIndex) > -1) {
        ctx.globalAlpha = 0.5;
      } else {
        ctx.globalAlpha = 1;
      }
      // draw the char offset by half its width (center)
      ctx.fillText(char, textPosX + cWidth / 2, yPos);
      // move too the next pos
      textPosX += cWidth;
      // count the char
      charIndex += 1
    }
    // move down one line
    yPos += fontSize * textVertSpacing;
  }
}

drawStroked('THIS\nIS THE\nTEXT', 60, '#000', canvas.width / 2, canvas.height / 2, 0.9, [2, 4, 8, 12]);
<canvas id="canvas" width="500" height="200"></canvas>

Update

Added some flicker to text by adding an animation loop and calling the text rendering function every few frames. The flicker is done by randomizing the alpha. See snippet below for more info.

requestAnimationFrame(animLoop);
const flickerRate = 4; // change alpha every 4 frames
var frameCount = 0;
const ctx = canvas.getContext('2d');
ctx.fillStyle = "#FDD"; // mark the center
ctx.fillRect(canvas.width / 2 | 0, 0, 1, canvas.height);
ctx.fillRect(0, canvas.height / 2 | 0, canvas.width, 1);
ctx.fillStyle = "black";


function drawStroked(text, fontSize, color, centerX, centerY, textVertSpacing, fade) {
  let line = text.split('\n');
  ctx.font = fontSize + 'px ' + 'TimesNewRoman';
  ctx.strokeStyle = color;
  ctx.lineWidth = 2;
  ctx.textBaseline = 'middle';
  ctx.textAlign = 'center';
  var charIndex = 0;
  var yPos = centerY - fontSize * line.length * 0.5 * textVertSpacing + fontSize * textVertSpacing / 2;
  
  for (var i = 0; i < line.length; i++) {
    var width = ctx.measureText(line[i]).width;
    var textPosX = centerX - width / 2;
    for (var j = 0; j < line[i].length; j++) {
      var char = line[i][j];
      var cWidth = ctx.measureText(char).width;
      ctx.globalAlpha = fade.indexOf(charIndex) > -1 ? Math.random()* 0.5+0.25 : 1;
      ctx.fillText(char, textPosX + cWidth / 2, yPos);
      textPosX += cWidth;
      charIndex += 1
    }
    yPos += fontSize * textVertSpacing;
  }
}
function animLoop(){
  if((frameCount % flickerRate) === 0){
    ctx.clearRect(0,0,canvas.width,canvas.height);

    drawStroked('THIS\nIS THE\nTEXT', 60, '#000', canvas.width / 2, canvas.height / 2, 0.9, [2, 4, 8, 12]);
  }
  frameCount ++;
  requestAnimationFrame(animLoop);
}
<canvas id="canvas" width="500" height="200"></canvas>

Upvotes: 1

Related Questions