Search code examples
javascripthtmlshadow

Creating a Gradient Path Fill JavaScript


I've been recently adding shadows to a project. I've ended up with something that I like, but the shadows are a solid transparent color throughout. I would prefer them to be a fading gradient as they go further.


What I currently have:
Example 1

What I'd like to achieve:
Example 2


Right now I'm using paths to draw my shadows on a 2D Canvas. The code that is currently in place is the following:

// Check if edge is invisible from the perspective of origin
var a = points[points.length - 1];
for (var i = 0; i < points.length; ++i, a = b)
{
    var b = points[i];

    var originToA = _vec2(origin, a);
    var normalAtoB = _normal(a, b);
    var normalDotOriginToA = _dot(normalAtoB, originToA);

    // If the edge is invisible from the perspective of origin it casts
    // a shadow.
    if (normalDotOriginToA < 0)
    {
        // dot(a, b) == cos(phi) * |a| * |b|
        // thus, dot(a, b) < 0 => cos(phi) < 0 => 90° < phi < 270°

        var originToB = _vec2(origin, b);

        ctx.beginPath();
        ctx.moveTo(a.x, a.y);
        ctx.lineTo(a.x + scale * originToA.x,
                   a.y + scale * originToA.y);
        ctx.lineTo(b.x + scale * originToB.x,
                   b.y + scale * originToB.y);
        ctx.lineTo(b.x, b.y);
        ctx.closePath();

        ctx.globalAlpha = _shadowIntensity / 2;
        ctx.fillStyle = 'black';
        ctx.fillRect(_innerX, _innerY, _innerWidth, _innerHeight);
        ctx.globalAlpha = _shadowIntensity;
        ctx.fill();
        ctx.globalAlpha = 1;

    }
}

Suggestions on how I could go about achieving this? Any and all help is highly appreciated.


Solution

  • You can use composition + the new filter property on the context which takes CSS filters, in this case blur.

    You will have to do it in several steps - normally this falls under the 3D domain, but we can "fake" it in 2D as well by rendering a shadow-map.

    Here we render a circle shape along a line represented by length and angle, number of iterations, where each iteration increasing the blur radius. The strength of the shadow is defined by its color and opacity.

    If the filter property is not available in the browser it can be replaced by a manual blur (there are many out there such as StackBoxBlur and my own rtblur), or simply use a radial gradient.

    For multiple use and speed increase, "cache" or render to an off-screen canvas and when done composite back to the main canvas. This will require you to calculate the size based on max blur radius as well as initial radius, then render it centered at angle 0°. To draw use drawImage() with a local transform transformed based on start of shadow, then rotate and scale (not shown below as being a bit too broad).

    In the example below it is assumed that the main object is drawn on top after the shadow has been rendered.

    The main function takes the following arguments:

    renderShadow(ctx, x, y, radius, angle, length, blur, iterations)
    
    // ctx - context to use
    // x/y - start of shadow
    // radius - shadow radius (assuming circle shaped)
    // angle - angle in radians. 0° = right
    // length - core-length in pixels (radius/blur adds to real length)
    // blur - blur radius in pixels. End blur is radius * iterations
    // iterations - line "resolution"/quality, also affects total end blur
    

    Play around with shape, shadow color, blur radius etc. to find the optimal result for your scene.

    Demo

    Result if browser supports filter:

    result

    var ctx = c.getContext("2d");
    
    // render shadow
    renderShadow(ctx, 30, 30, 30, Math.PI*0.25, 300, 2.5, 20);
    
    // show main shape
    ctx.beginPath();
    ctx.moveTo(60, 30);
    ctx.arc(30, 30, 30, 0, 6.28);
    ctx.fillStyle = "rgb(0,140,200)";
    ctx.fill();
    
    function renderShadow(ctx, x, y, radius, angle, length, blur, iterations) {
    
      var step = length / iterations,         // calc number of steps
          stepX = step * Math.cos(angle),     // calc angle step for x based on steps
          stepY = step * Math.sin(angle);     // calc angle step for y based on steps
      
      for(var i = iterations; i > 0; i--) {   // run number of iterations
        ctx.beginPath();                      // create some shape, here circle
        ctx.moveTo(x + radius + i * stepX, y + i * stepY); // move to x/y based on step*ite.
        ctx.arc(x + i * stepX, y + i * stepY, radius, 0, 6.28);
     
        ctx.filter = "blur(" + (blur * i) + "px)"; // set filter property
        ctx.fillStyle = "rgba(0,0,0,0.5)";    // shadow color
        ctx.fill();
      }
      
      ctx.filter = "none";                    // reset filter
    }
    <canvas id=c width=450 height=350></canvas>