# Shadows

# Sticker effect using shadows

This code adds outwardly increasing shadows to an image to create a "sticker" version of the image.

Notes:

  • In addition to being an ImageObject, the "img" argument can also be a Canvas element. This allows you to stickerize your own custom drawings. If you draw text on the Canvas argument, you can also stickerize that text.
  • Fully opaque images will have no sticker effect because the effect is drawn around clusters of opaque pixels that are bordered by transparent pixels.

enter image description here (opens new window)

var canvas=document.createElement("canvas");
var ctx=canvas.getContext("2d");
document.body.appendChild(canvas);
canvas.style.background='navy';
canvas.style.border='1px solid red;';

// Always(!) wait for your images to fully load before trying to drawImage them!
var img=new Image();
img.onload=start;
// put your img.src here...
img.src='http://i.stack.imgur.com/bXaB6.png';
function start(){
    ctx.drawImage(img,20,20);
    var sticker=stickerEffect(img,5);
    ctx.drawImage(sticker, 150,20);
}

function stickerEffect(img,grow){
    var canvas1=document.createElement("canvas");
    var ctx1=canvas1.getContext("2d");
    var canvas2=document.createElement("canvas");
    var ctx2=canvas2.getContext("2d");
    canvas1.width=canvas2.width=img.width+grow*2;
    canvas1.height=canvas2.height=img.height+grow*2;
    ctx1.drawImage(img,grow,grow);
    ctx2.shadowColor='white';
    ctx2.shadowBlur=2;
    for(var i=0;i<grow;i++){
        ctx2.drawImage(canvas1,0,0);
        ctx1.drawImage(canvas2,0,0);
    }
    ctx2.shadowColor='rgba(0,0,0,0)';   
    ctx2.drawImage(img,grow,grow);
    return(canvas2);
}

# How to stop further shadowing

Once shadowing is turned on, every new drawing to the canvas will be shadowed.

Turn off further shadowing by setting context.shadowColor to a transparent color.

// start shadowing
context.shadowColor='black';

... render some shadowed drawings ...

// turn off shadowing.
context.shadowColor='rgba(0,0,0,0)';

# Shadowing is computationally expensive -- Cache that shadow!

Warning! Apply shadows sparingly!

Applying shadowing is expensive and is multiplicatively expensive if you apply shadowing inside an animation loop.

Instead, cache a shadowed version of your image (or other drawing):

  • At the start of your app, create a shadowed version of your image in a second in-memory-only Canvas: `var memoryCanvas = document.createElement('canvas') ...`
  • Whenever you need the shadowed version, draw that pre-shadowed image from the in-memory canvas to the visible canvas: `context.drawImage(memoryCanvas,x,y)`
  • enter image description here (opens new window)

    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    var cw=canvas.width;
    var ch=canvas.height;
    canvas.style.border='1px solid red;';
    document.body.appendChild(canvas);
    
    // Always(!) use "img.onload" to give your image time to 
    //     fully load before you try drawing it to the Canvas!
    var img=new Image();
    img.onload=start;
    // Put your own img.src here
    img.src="http://i.stack.imgur.com/hYFNe.png";
    function start(){
        ctx.drawImage(img,0,20);
        var cached=cacheShadowedImage(img,'black',5,3,3);
        for(var i=0;i<5;i++){ 
            ctx.drawImage(cached,i*(img.width+10),80);
        }
    }
    
    function cacheShadowedImage(img,shadowcolor,blur){
        var c=document.createElement('canvas');
        var cctx=c.getContext('2d');
        c.width=img.width+blur*2+2;
        c.height=img.height+blur*2+2;
        cctx.shadowColor=shadowcolor;
        cctx.shadowBlur=blur;
        cctx.drawImage(img,blur+1,blur+1);
        return(c);
    }
    
    

    # Add visual depth with shadows

    The traditional use of shadowing is to give 2-dimensional drawings the illusion of 3D depth.

    This example shows the same "button" with and without shadowing

    enter image description here (opens new window)

    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    ctx.fillStyle='skyblue';
    ctx.strokeStyle='lightgray';
    ctx.lineWidth=5;
    
    // without shadow
    ctx.beginPath();
    ctx.arc(60,60,30,0,Math.PI*2);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    
    // with shadow
    ctx.shadowColor='black';
    ctx.shadowBlur=4;
    ctx.shadowOffsetY=3;
    ctx.beginPath();
    ctx.arc(175,60,30,0,Math.PI*2);
    ctx.closePath();
    ctx.fill();
    ctx.stroke();
    // stop the shadowing
    ctx.shadowColor='rgba(0,0,0,0)';
    
    

    # Inner shadows

    Canvas does not have CSS's inner-shadow.

    • Canvas will shadow the outside of a filled shape.
    • Canvas will shadow both inside and outside a stroked shape.

    But it's easy to create inner-shadows using compositing.

    # Strokes with an inner-shadow

    enter image description here (opens new window)

    To create strokes with an inner-shadow, use destination-in compositing which causes existing content to remain only where existing content is overlapped by new content. Existing content that is not overlapped by new content is erased.

    1. Stroke a shape with a shadow. The shadow will extend both outward and inward from the stroke. We must get rid of the outer-shadow -- leaving just the desired inner-shadow.
    2. Set compositing to destination-in which keeps the existing stroked shadow only where it is overlapped by any new drawings.
    3. Fill the shape. This causes the stroke and inner-shadow to remain while the outer shadow is erased. Well, not exactly! Since a stroke is half-inside and half-outside the filled shape, the outside half of the stroke will be erased also. The fix is to double the context.lineWidth so half of the double-sized stroke is still inside the filled shape.
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    // draw an opaque shape -- here we use a rounded rectangle
    defineRoundedRect(30,30,100,75,10);
    
    // set shadowing
    ctx.shadowColor='black';
    ctx.shadowBlur=10;
    
    // stroke the shadowed rounded rectangle
    ctx.lineWidth=4;
    ctx.stroke();
    
    // set compositing to erase everything outside the stroke
    ctx.globalCompositeOperation='destination-in';
    ctx.fill();
    
    // always clean up -- set compsiting back to default
    ctx.globalCompositeOperation='source-over';   
    
    
    function defineRoundedRect(x,y,width,height,radius) {
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + width - radius, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        ctx.lineTo(x + radius, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
    }
    
    

    # Stroked Fills with an inner-shadow

    enter image description here (opens new window)

    To create fills with an inner-shadow, follow steps #1-3 above but further use destination-over compositing which causes new content to be drawn under existing content.

    1. Set compositing to destination-over which causes the fill to be drawn under the existing inner-shadow.
    2. Turn off shadowing by setting context.shadowColor to a transparent color.
    3. Fill the shape with the desired color. The shape will be filled underneath the existing inner-shadow.
    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    // draw an opaque shape -- here we use a rounded rectangle
    defineRoundedRect(30,30,100,75,10);
    
    // set shadowing
    ctx.shadowColor='black';
    ctx.shadowBlur=10;
    
    // stroke the shadowed rounded rectangle
    ctx.lineWidth=4;
    ctx.stroke();
    
    // stop shadowing
    ctx.shadowColor='rgba(0,0,0,0)';
    
    // set compositing to erase everything outside the stroke
    ctx.globalCompositeOperation='destination-in';
    ctx.fill();
    
    // set compositing to erase everything outside the stroke
    ctx.globalCompositeOperation='destination-over';
    ctx.fillStyle='gold';
    ctx.fill();
    
    // always clean up -- set compsiting back to default
    ctx.globalCompositeOperation='source-over';   
    
    function defineRoundedRect(x,y,width,height,radius) {
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + width - radius, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        ctx.lineTo(x + radius, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
    }
    
    

    # Non-stroked Fills with an inner-shadow

    enter image description here (opens new window)

    To draw a filled shape with an inner-shadow, but with no stroke, you can draw the stroke off-canvas and use shadowOffsetX to push the shadow back onto the canvas.

    var canvas=document.createElement("canvas");
    var ctx=canvas.getContext("2d");
    document.body.appendChild(canvas);
    
    // define an opaque shape -- here we use a rounded rectangle
    defineRoundedRect(30-500,30,100,75,10);
    
    // set shadowing
    ctx.shadowColor='black';
    ctx.shadowBlur=10;
    ctx.shadowOffsetX=500;
    
    // stroke the shadowed rounded rectangle
    ctx.lineWidth=4;
    ctx.stroke();
    
    // stop shadowing
    ctx.shadowColor='rgba(0,0,0,0)';
    
    // redefine an opaque shape -- here we use a rounded rectangle
    defineRoundedRect(30,30,100,75,10);
    
    // set compositing to erase everything outside the stroke
    ctx.globalCompositeOperation='destination-in';
    ctx.fill();
    
    // set compositing to erase everything outside the stroke
    ctx.globalCompositeOperation='destination-over';
    ctx.fillStyle='gold';
    ctx.fill();
    
    // always clean up -- set compsiting back to default
    ctx.globalCompositeOperation='source-over';   
    
    function defineRoundedRect(x,y,width,height,radius) {
        ctx.beginPath();
        ctx.moveTo(x + radius, y);
        ctx.lineTo(x + width - radius, y);
        ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
        ctx.lineTo(x + width, y + height - radius);
        ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
        ctx.lineTo(x + radius, y + height);
        ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
        ctx.lineTo(x, y + radius);
        ctx.quadraticCurveTo(x, y, x + radius, y);
        ctx.closePath();
    }