1

I was surprised to find out that apparently the canvas API does not allow you to apply gradients to shadows like this:

var grad = ctx.createLinearGradient(fromX, fromY, toX, toY);

grad.addColorStop(0, "red");
grad.addColorStop(1, "blue");

ctx.strokeStyle = grad;
ctx.lineWidth = 3;
ctx.shadowBlur = 10;
ctx.shadowColor = grad; // doesn't seem to work

ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.closePath();
ctx.stroke();

// linear gradient from start to end of line
var canvas = document.getElementById('mycanvas'),
  ctx = canvas.getContext('2d'),
  fromX = 3,
  fromY = 3,
  toX = 197,
  toY = 197,
  grad = ctx.createLinearGradient(fromX, fromY, toX, toY);

canvas.width = 200;
canvas.height = 200;

grad.addColorStop(0, "red");
grad.addColorStop(1, "blue");

ctx.strokeStyle = grad;
ctx.lineWidth = 3;
ctx.shadowBlur = 20;
ctx.shadowColor = grad;

ctx.beginPath();
ctx.moveTo(fromX, fromY);
ctx.lineTo(toX, toY);
ctx.closePath();
ctx.stroke();
body {
  background: black
}
<canvas id="mycanvas"></canvas>

One workaround is to simply draw the line/shape/etc. multiple times at different sizes and opacity to get a similar result:

var grad = ctx.createLinearGradient(fromX, fromY, toX, toY);

canvas.width = 200;
canvas.height = 200;

grad.addColorStop(0, "red");
grad.addColorStop(1, "blue");

ctx.strokeStyle = grad;
ctx.lineWidth = 3;
//ctx.shadowBlur = 20;
//ctx.shadowColor = grad;

for (var i = 10; i > 1; i--) {
  ctx.lineWidth = i;
  ctx.globalAlpha = 1 / i;
  ctx.beginPath();
  ctx.moveTo(fromX, fromY);
  ctx.lineTo(toX, toY);
  ctx.closePath();
  ctx.stroke();
}

// linear gradient from start to end of line
var canvas = document.getElementById('mycanvas'),
  ctx = canvas.getContext('2d'),
  fromX = 3,
  fromY = 3,
  toX = 197,
  toY = 197,
  grad = ctx.createLinearGradient(fromX, fromY, toX, toY);

canvas.width = 200;
canvas.height = 200;

grad.addColorStop(0, "red");
grad.addColorStop(1, "blue");

ctx.strokeStyle = grad;
ctx.lineWidth = 3;
//ctx.shadowBlur = 20;
//ctx.shadowColor = grad;

for (var i = 10; i > 1; i--) {
  ctx.lineWidth = i;
  ctx.globalAlpha = 1 / i;
  ctx.beginPath();
  ctx.moveTo(fromX, fromY);
  ctx.lineTo(toX, toY);
  ctx.closePath();
  ctx.stroke();
}
body {
  background: black;
  }
<canvas id="mycanvas"></canvas>

Here's the comparison. Although the change is subtle, the right image shows roughly the desired effect.

line1 line2

Is there a better way of doing this? I imagine there's a more efficient way than drawing the same thing multiple times. Does anyone know of a library that provides this kind of functionality?

Sheng Fang
  • 21
  • 3
NanoWizard
  • 2,104
  • 1
  • 21
  • 34

1 Answers1

2

Use the filter property of the canvas 2d context. MDN filter though (as usual) it does say filter is not supported on Chrome it has been from some time on the Beta version. For IE I do not know and for FF it has been supported for some time. You will have to test for it if you use it.

UPDATE

Support does not seam to be automatic. Though MDN shows support for Firefox you must set the canvas.filters.enable to true (whatever that means, I am sure firefox lovers know) and seams for chrome you must go to chrome://flags then set experimental canvas features to enabled

More I have added a fallback as there is such limited support. It uses a second canvas to blur the shadow by using the ctx.imageSmoothingEnabled=true; and rendering at a scale one half the blur amount. So if blur is 5 pixels then in background canvas must be one tenth the size. Then on the original canvas render the background canvas at full size with smoothing on.

No the best result and will no be good for lines, but its fast and can be played around with to optimise results.

Snippet to show how to detect support and use.

var canvas = document.getElementById("canV");
var ctx = canvas.getContext("2d");



var g = ctx.createLinearGradient(10,10,100,100);
for(var i = 0; i <= 1; i+= 0.05){
   g.addColorStop(i,"hsl("+Math.floor(i*360)+",100%,50%)");
}
var gDark = ctx.createLinearGradient(20,20,100,100);
for(var i = 0; i <= 1; i+= 0.05){
   gDark.addColorStop(i,"hsl("+Math.floor(i*360)+",100%,30%)");
}
ctx.font = "16px Arial";  
ctx.textAlign = "center";
ctx.textBaseline = "hanging";
if(ctx.filter !== undefined){
    ctx.fillText("Using filter.",65,125);
    ctx.fillStyle = gDark;
    ctx.filter = "blur(5px)"; // set the blur
    ctx.fillRect(20,20,100,100);  // draw the shadow
    ctx.fillStyle = g;   // set the lighter gradoent
    ctx.filter = "blur(0px)";  // remove the blur
    ctx.lineWidth = 2;  
    ctx.strokeStyle = "black"
    ctx.fillRect(10,10,100,100); // draw the box
    ctx.strokeRect(10,10,100,100); // with line to look nice.
  
}else{
     // fallback method 
    ctx.fillText("Using Fallback.",60,125);
    var can = document.createElement("canvas"); // create a second canvas
    can.width = Math.floor(canvas.width/10);  // size to make one pixel the 
    can.height =Math.floor(canvas.height/10);  // size of the blur
    var ctxS = can.getContext("2d");
    ctxS.setTransform(1/10,0,0,1/10,0,0);  // set scale so can work in same coords
    ctxS.fillStyle = gDark;
    ctxS.fillRect(20,20,100,100);  // draw the shadow
    ctx.imageSmoothingEnabled=true;
    ctx.drawImage(can,0,0,canvas.width,canvas.height);
}
ctx.fillStyle = g;   // set the lighter gradoent
ctx.lineWidth = 2;  
ctx.strokeStyle = "black"
ctx.fillRect(10,10,100,100); // draw the box
ctx.strokeRect(10,10,100,100); // with line to look nice.
#canV {
  width:200px;
  height:200px;
}
<canvas id="canV" width = 200 height =200></canvas>
Blindman67
  • 51,134
  • 11
  • 73
  • 136
  • Running your snippet on the latest browsers I have (Chrome 46.0.2490.86, Firefox 42.0, and IE 11.0.22), shows "Filter not supported" in *all* of them. This looks like a promising addition to the canvas API though. – NanoWizard Nov 25 '15 at 20:26
  • Sorry just read the FF need to have it activated. I have added a fix to the snippet and it should work on Firefox if I have done it correctly, as it has support on FF since V35. I am on Chrome 47.0.2526.69 beta-m but I may have experimental canvas features turned on. I think they are all afraid of it because it is very slow to use filters, not something to use in animations that's for sure. – Blindman67 Nov 25 '15 at 20:40
  • My bad.. Must be a browser flag for FF `canvas.filters.enabled = true`. MDN clear as mud. – Blindman67 Nov 25 '15 at 20:44
  • I'm still seeing the same behavior, even after your `canvas.filters.enabled = true` addition. Might need those experimental canvas features enabled in the browser settings or something. Regardless, I personally need something that will work across all modern browsers anyway. – NanoWizard Nov 25 '15 at 20:57
  • How you have done is the one way. Another is to render on a canvas that is half, quarter, 1/8th the size depending on the amount of blur (smaller more blur) and then render that back at full size with `ctx.imageSmoothingEnabled=true;` its a lot faster (once you have a new canvas in memory for use) and does a reasonable job but not perfect. – Blindman67 Nov 25 '15 at 21:02
  • That sounds like a decent solution. I'll have to try it out when I get a chance. :) – NanoWizard Nov 25 '15 at 21:09
  • Adding it to the snippet as a fallback. – Blindman67 Nov 25 '15 at 21:11