27

I'm currently using HTML5's canvas to render a number of strings using the fillText method. This works fine, but I'd also like to give each string a 1px black outer stroke. Unfortunately the strokeText function seems to apply an inner stroke. To counter this, I've written a drawStrokedText function that achieves the effect I'm after. Unfortunately it's horrible slow (for obvious reasons).

Is there a fast, cross-browser way of achieving a 1px outer stroke using native canvas functionality?

drawStrokedText = function(context, text, x, y)
{
    context.fillStyle = "rgb(0,0,0)";
    context.fillText(text, x-1, y-1);
    context.fillText(text, x+1, y-1);
    context.fillText(text, x-1, y);
    context.fillText(text, x+1, y);
    context.fillText(text, x-1, y+1);
    context.fillText(text, x+1, y+1);

    context.fillStyle = "rgb(255,255,255)";
    context.fillText(text, x, y);
};

Here's an example of the effect at work:

enter image description here

ndg
  • 2,585
  • 2
  • 33
  • 58
  • How about rendering the text with `strokeText`, but with a slightly larger font to account for the inner stroke? Also, on that `drawStrokedText` method you could probably skip the horizontal / vertical shifts. (You seem to be missing vertical already, any way) – Cerbrus Nov 29 '12 at 13:45

4 Answers4

75

What's wrong with stroke? Since half the stroke will be outside of the shape, you can always draw the stroke first with a line width of double what you want. So if you wanted a 4px outer stroke you could do:

function drawStroked(text, x, y) {
    ctx.font = '80px Sans-serif';
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 8;
    ctx.strokeText(text, x, y);
    ctx.fillStyle = 'white';
    ctx.fillText(text, x, y);
}


drawStroked("37°", 50, 150);

Which makes:

enter image description here

live fiddle here: http://jsfiddle.net/vNWn6/


IF that happens to not look as accurate at smaller text rendering scales, you can always draw it large but scale it down (in the above case you'd do ctx.scale(0.25, 0.25))

hughes
  • 5,595
  • 3
  • 39
  • 55
Simon Sarris
  • 62,212
  • 13
  • 141
  • 171
  • 1
    I added some additional code with links below to polish this a bit. Nonetheless +1 to Simon for this clever solution! – Jack Mar 07 '14 at 20:18
30

Simon's answer is a good solution, yet it may have mitering glitches in some cases, especially with capital 'M', 'V', & 'W':

drawStroked("MVW", 50, 150);

http://jsfiddle.net/hwG42/1/

In this case, it's best to utilize:

ctx.miterLimit=2;

http://jsfiddle.net/hwG42/3/

Best of luck!

Ryan M
  • 18,333
  • 31
  • 67
  • 74
Jack
  • 2,229
  • 2
  • 23
  • 37
  • 3
    This can also be achieved with `ctx.lineJoin = 'round';` It may be worth doing both miterLimit and lineJoin, to make sure things look good in all browsers. – Martin Omander Mar 06 '19 at 20:45
  • An editor [suggests](https://stackoverflow.com/review/suggested-edits/32159749) that "Changing the miterlimit to three fixes the letter K (http://jsfiddle.net/Xantholeucophore/2hm7gej5/4/)" – Ryan M Jul 07 '22 at 11:15
11

The above answers are great, using some of these solutions* and some of my own ideas, I made a quick reference and some creative alternatives in the below fiddle.

*All credits given where due in the fiddle code

drawStrokedText   ( text, x, y );
drawShadowedText  ( text, x, y, shadowBlur);
drawGlowingText   ( text, x, y, glowColorHex, glowDistance);
drawBlurredText   ( text, x, y, blurAmount);
drawReflectedText ( text, x, y, reflectionScale, reflectionOpacity);

https://jsfiddle.net/vtmnyea8/

// Author: Aaron Edmistone
// Text effects using HTML5 Canvas with 2D Context.
// https://stackoverflow.com/questions/7814398/a-glow-effect-on-html5-canvas

var canvas = document.getElementById('myCanvas');
var ctx = canvas.getContext('2d');

// prepare the presentation of the canvas
ctx.fillStyle = 'black';
ctx.fillRect(0,0,250,450);
ctx.fillStyle = 'gray';
ctx.fillRect(250,0,250,450);
ctx.fillStyle = 'white';
ctx.fillRect(500,0,250,450);
ctx.fillStyle = '#0066CC';
ctx.fillRect(750,0,250,450);

// prepare the font and fill
ctx.font = "80px Sans-serif";
ctx.fillStyle = "white";

function drawStrokedText(text, x, y)
{
        // using the solutions from @Simon Sarris and @Jackalope from
    // https://stackoverflow.com/questions/7814398/a-glow-effect-on-html5-canvas
        ctx.save();
    ctx.strokeStyle = 'black';
    ctx.lineWidth = 8;
    ctx.lineJoin="round";
      ctx.miterLimit=2;
    ctx.strokeText(text, x, y);
    ctx.fillText(text, x, y);
    ctx.restore();
}

function drawShadowedText(text, x, y, shadowBlur = 3)
{
        ctx.save();
    ctx.shadowBlur = shadowBlur;
    ctx.shadowColor = "#000000";
    ctx.shadowOffsetX = 4;
    ctx.shadowOffsetY = 4;
    ctx.fillText(text, x, y);
    ctx.restore();
}

function drawGlowingText(text, x, y, glowColorHexString, glowDistance = 10)
{
        ctx.save();
    ctx.shadowBlur = glowDistance;
    ctx.shadowColor = glowColorHexString;
    ctx.strokeText(text, x, y);
    
    for(let i = 0; i < 3; i++)
        ctx.fillText(text, x, y); //seems to be washed out without 3 fills
      
    ctx.restore();
}

function drawBlurredText(text, x, y, blur = 5)
{
    //using technique from https://www.html5rocks.com/en/tutorials/canvas/texteffects/
    ctx.save();
  let width = ctx.measureText(text).width + blur * 2;
  ctx.shadowColor = ctx.fillStyle;
  ctx.shadowOffsetX = width + x + ctx.canvas.width;
  ctx.shadowOffsetY = 0;
  ctx.shadowBlur = blur;
  ctx.fillText(text, -width + -ctx.canvas.width, y);
  ctx.restore();
}

function drawReflectedText(text, x, y, reflectionScale = 0.2, reflectionAlpha = 0.10)
{
    ctx.save();
  ctx.fillText(text, x, y);
    ctx.scale(1, -reflectionScale);
  ctx.globalAlpha = reflectionAlpha;
  ctx.shadowColor = ctx.fillStyle;
  ctx.shadowBlur = 15;
    ctx.fillText(text, x, -(y * (1 / reflectionScale)));
  ctx.restore();
}

for(let i = 0; i < 4; i++)
{
    drawStrokedText     ("MVW", 20 + i * 250, 80 * 1);
    drawShadowedText    ("MVW", 20 + i * 250, 80 * 2, 3);
  drawGlowingText       ("MVW", 20 + i * 250, 80 * 3, "#FF0000", 10);
  drawBlurredText       ("MVW", 20 + i * 250, 80 * 4, 5);
  drawReflectedText ("MVW", 20 + i * 250, 80 * 5, 0.5, 0.5);
}
<canvas id="myCanvas" width="1000" height="500"></canvas>

Output of the fiddle:

Output of the below fiddle

What it supports:

  • Outline text
  • Shadow text
  • Glowing text
  • Blurred text
  • Reflected text

Some performance metrics:

Considering using this in a game or at high frame rates? Check out this jsperf using the above methods.

https://jsperf.com/various-text-effects-html5-2d-context

aloisdg
  • 22,270
  • 6
  • 85
  • 105
aaronedmistone
  • 929
  • 10
  • 17
  • Just a point about the performances part: benchmarks will use CPU rounds to calculate the performances of the passed scripts, most Canvas2D implementations are now actually using hardware acceleration, the GPU makes most of the work. So you are only measuring a small fraction of the script, the slow part (blur, shadows text-rendering) will be done later by the GPU. – Kaiido Jul 20 '21 at 08:03
5

For a smooth shadow you can try this

ctx.beginPath();
ctx.fillStyle = 'white';
ctx.font = "bold 9pt Tahoma";
ctx.shadowBlur = 3;
ctx.textAlign = "center";
ctx.shadowColor = "#000000";
ctx.shadowOffs = 0;                 
ctx.fillText('www.ifnotpics.com', 100, 50);        
ctx.closePath();