Canvas Shadows

Canvas Shadows
Nikhil
Published on 2021-07-05 10:09:04

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 sticker your own custom drawings. If you draw text on the Canvas argument, you can also sticker 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.

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 to shadow is expensive and is multiplicatively expensive if you apply to shadow inside an animation loop.

Instead, cache a shadowed version of your image (or another 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)

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

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.

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.

  • 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.
  • Set compositing to destination-in which keeps the existing stroked shadow only where it is overlapped by any new drawings.
  • 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


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.

  • 4. Set compositing to destination-over which causes the fill to be drawn under the existing inner-shadow.
  • 5. Turn off shadowing by setting context.shadowColor to a transparent color.
  • 6. 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();
}

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();
}

ATutorialHub Related Guide

Comments (8)

Leave a Comment

Your email address will not be published. Required fields are marked*

User Comments

html tutorial comments

panduranga gupta

2021-07-05 07:03:13

good website for learning and help me a lot

html tutorial comments

raju

2021-09-25 14:58:47

The awsome website i am looking like for a long time, good work atutorialhub team keep doing

html tutorial comments

Shivani

2021-09-01 15:03:56

Learning a lot from the courses present on atutorialhub. The courses are very well explained. Great experience

html tutorial comments

Harshitha

2021-09-10 15:05:45

It is very helpful to students and easy to learn the concepts

html tutorial comments

Sowmya

2021-09-14 15:06:41

Great job Tutorials are easy to understand Please make use of it

html tutorial comments

Zain Khan

2021-09-18 15:07:23

Great content and customized courses.

html tutorial comments

Rudrakshi Bhatt

2021-09-09 15:08:10

Well structured coursed and explained really well!

html tutorial comments

Pavana Somashekar

2021-09-11 15:09:08

Good platform for beginners and learn a lot on this website