17

I am using the HTML5 canvas API to display some string (canvas.fillText), and I was wondering whether text-decoration (like underline, strikethrough, etc.) was something possible with the canvas API. Unfortunately, I found nothing about this.

The only solution I found was to manually do the decoration using the canvas drawing API (I mean, explicitly drawing a horizontal line, for example, to mimic the 'underline' decoration).

Is this possible using the canvas text API?

Melanie Palen
  • 2,645
  • 6
  • 31
  • 50
Patrick Ruzand
  • 465
  • 1
  • 3
  • 10

5 Answers5

9

It won't work with a built-in method, but here is a simplified function I used successfully based on reading, "HTML5 Canvas: Text underline workaround" on the ScriptStock website.

var underline = function(ctx, text, x, y, size, color, thickness ,offset){
  var width = ctx.measureText(text).width;

  switch(ctx.textAlign){
    case "center":
    x -= (width/2); break;
    case "right":
    x -= width; break;
  }

  y += size+offset;
  
  ctx.beginPath();
  ctx.strokeStyle = color;
  ctx.lineWidth = thickness;
  ctx.moveTo(x,y);
  ctx.lineTo(x+width,y);
  ctx.stroke();

}
Melanie Palen
  • 2,645
  • 6
  • 31
  • 50
Mulhoon
  • 1,852
  • 21
  • 26
6

You can do this by using measureText and fillRect like so:

ctx.fillText(text, xPos, yPos);
let { width } = ctx.measureText("Hello World");
ctx.fillRect(xPos, yPos, width, 2);

The only difficult part about this approach is there is no way to obtain the height use measureText. Otherwise, you could use that as your Y coordinate when drawing your fillRect.

Your Y position will only depend on the height of your text and how close you'd like the underline.

Demo in Stack Snippets

// get canvas / context
var can = document.getElementById('my-canvas');
var ctx = can.getContext('2d')

let xPos=10, yPos=15;
let text = "Hello World"

ctx.fillText(text, xPos, yPos);
let { width } = ctx.measureText("Hello World");
ctx.fillRect(xPos, yPos, width, 2);
<canvas id="my-canvas" width="250" height="150"></canvas>
KyleMit
  • 30,350
  • 66
  • 462
  • 664
Garebear
  • 61
  • 1
  • 6
3

I created an alternative version of Mulhoon's code. I also take into account the text baseline.

const underline = (ctx, text, x, y) => {
  let metrics = measureText(ctx, text)
  let fontSize = Math.floor(metrics.actualHeight * 1.4) // 140% the height 
  switch (ctx.textAlign) {
    case "center" : x -= (metrics.width / 2) ; break
    case "right"  : x -= metrics.width       ; break
  }
  switch (ctx.textBaseline) {
    case "top"    : y += (fontSize)     ; break
    case "middle" : y += (fontSize / 2) ; break
  }
  ctx.save()
  ctx.beginPath()
  ctx.strokeStyle = ctx.fillStyle
  ctx.lineWidth = Math.ceil(fontSize * 0.08)
  ctx.moveTo(x, y)
  ctx.lineTo(x + metrics.width, y)
  ctx.stroke()
  ctx.restore()
}

Full Example

const triggerEvent = (el, eventName) => {
  var event = document.createEvent('HTMLEvents')
  event.initEvent(eventName, true, false)
  el.dispatchEvent(event)
}

const measureText = (ctx, text) => {
  let metrics = ctx.measureText(text)
  return {
    width: Math.floor(metrics.width),
    height: Math.floor(metrics.fontBoundingBoxAscent + metrics.fontBoundingBoxDescent),
    actualHeight: Math.floor(metrics.actualBoundingBoxAscent + metrics.actualBoundingBoxDescent)
  }
}

const underline = (ctx, text, x, y) => {
  let metrics = measureText(ctx, text)
  let fontSize = Math.floor(metrics.actualHeight * 1.4) // 140% the height 
  switch (ctx.textAlign) {
    case "center" : x -= (metrics.width / 2) ; break
    case "right"  : x -= metrics.width       ; break
  }
  switch (ctx.textBaseline) {
    case "top"    : y += (fontSize)     ; break
    case "middle" : y += (fontSize / 2) ; break
  }
  ctx.save()
  ctx.beginPath()
  ctx.strokeStyle = ctx.fillStyle
  ctx.lineWidth = Math.ceil(fontSize * 0.08)
  ctx.moveTo(x, y)
  ctx.lineTo(x + metrics.width, y)
  ctx.stroke()
  ctx.restore()
}

const getOrigin = (ctx) => ({
  x : Math.floor(ctx.canvas.width / 2),
  y : Math.floor(ctx.canvas.height / 2)
})

const redraw = (ctx, sampleText, fontSize) => {
  let origin = getOrigin(ctx)
  ctx.font = fontSize + 'px Arial'
  ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height)
  renderText(ctx, sampleText, origin.x, origin.y, 'Yellow', 'left', 'top')
  renderText(ctx, sampleText, origin.x, origin.y, 'SkyBlue ', 'right', 'bottom')
  renderText(ctx, sampleText, origin.x, origin.y, 'Tomato', 'left', 'bottom')
  renderText(ctx, sampleText, origin.x, origin.y, 'Chartreuse ', 'right', 'top')
  renderText(ctx, sampleText, origin.x, origin.y, 'Black', 'center', 'middle')
}

const renderText = (ctx, text, x, y, fillStyle, textAlign, textBaseLine) => {
  ctx.fillStyle = fillStyle
  ctx.textAlign = textAlign
  ctx.textBaseline = textBaseLine
  ctx.fillText(text, x, y)
  underline(ctx, text, x, y)
}

const sampleText = 'Hello World'
const fontSizes = [ 8, 12, 16, 24, 32 ]

document.addEventListener('DOMContentLoaded', () => {
  let ctx = document.querySelector('#demo').getContext('2d')
  let sel = document.querySelector('select[name="font-size"]')
  
  fontSizes.forEach(fontSize => sel.appendChild(new Option(fontSize, fontSize)))
  sel.addEventListener('change', (e) => redraw(ctx, sampleText, sel.value))
  sel.value = fontSizes[fontSizes.length - 1]
  triggerEvent(sel, 'change')
})
canvas { border: thin solid grey }
label { font-weight: bold }
label::after { content: ": " }
<canvas id="demo" width="360" height="120"></canvas>
<form>
  <label for="font-size-select">Font Size</label>
  <select id="font-size-select" name="font-size"></select>
</form>
Mr. Polywhirl
  • 42,981
  • 12
  • 84
  • 132
3

To add an underline to your canvas text, simply add underline characters at the same (x,y) position as your text. e.g. you want to underline abc

    context.fillText("abc",x,y);
    context.fillText ("___",x,y);

Similarly for strike through, you would use the "-" character rather than underline.

Zaplady
  • 31
  • 4
  • Love the hack and it works, but if you closely inspect you would notice that there are some edges to the line. Implemented here -> https://codepen.io/stormraider2495/pen/gOgNJEL – Arunit Mazumdar May 21 '21 at 11:09
  • 1
    You are absolutely right, it was just a quick easy hack based on the old method of underlining and striking through on a typewriter that would have had a fixed width font so the lines would have been the same width as the text. – Zaplady May 22 '21 at 16:45
1

I'm sorry to say that the answer is 'no'. There are no 'text-decoration' or similar styles available in the text methods of the HTML Canvas Context.

Phrogz
  • 296,393
  • 112
  • 651
  • 745
  • 2
    Patrick, you still can use a normal div with text inside and put it over your canvas, not really answering your question, but it is easy to do and easy to manage. – Tim Jan 08 '11 at 16:34
  • @Tim Excellent point. Not only can you get the extra text decorations, but you also get [subpixel antialiasing](http://stackoverflow.com/questions/4550926/subpixel-anti-aliased-text-on-html5s-canvas-element), unlike text on the canvas. – Phrogz Jan 08 '11 at 16:40
  • 1
    @Tim. But assuming my text has a rotation transform, it would imply to use CSS3 transforms, and I have to stick with the canvas api at this moment. – Patrick Ruzand Jan 10 '11 at 15:47