Dan
Dan

Reputation: 597

How to have a circular inner shadow for canvas shape?

In my canvas, I have a circle shape that moves with cursor. I am trying to give this a little inner shadow, but it does not work.

Here is the code and demo : JSFIDDLE

function writeMessage(canvas, message, x, y) {
    var context = canvas.getContext('2d');
    context.clearRect(0, 0, canvas.width, canvas.height);

    var pattern = context.createPattern(imageResized, 'no-repeat'); //Use imageResized, not imageObj.
    context.fillStyle = pattern;
    context.fill();

    context.font = '28pt Calibri';
    context.fillStyle = 'white';
    //context.fillText(message, x, y);
    context.beginPath();
    context.arc(x, y, 50, 0, 2 * Math.PI);
    context.stroke();


    context.beginPath();
    context.lineWidth = 5;
    context.shadowColor = 'black';
    context.strokeStyle = "rgba(0,0,0,1)";
    context.shadowBlur = 15;
    context.shadowOffsetX = 0;
    context.shadowOffsetY = 0;
    context.arc(x, y, 50, 0, 2 * Math.PI, false);
    context.stroke();
    context.restore();

}

I notice this settings would work if I add context.clip(); but then it would make the whole canvas crash.

Any idea to have an inner shadow for this circle ?

Upvotes: 2

Views: 8068

Answers (3)

Pat Lillis
Pat Lillis

Reputation: 1180

I've just created Inset.js for this exact situation!

Inset.js

Only requires setting ctx.shadowInset = true;

For example: http://codepen.io/patlillis/pen/RpEoKE

var ctx = canvas.getContext("2d");
var w = canvas.width = window.innerWidth;
var h = canvas.height = window.innerHeight;

// Set up circle styles.
ctx.shadowInset = true;
ctx.shadowBlur = 15;
ctx.shadowColor = 'black';
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.fillStyle = 'red';

// Set up mouse listener.
document.addEventListener('mousemove', function(e) {
    ctx.clearRect(0, 0, canvas.width, canvas.height);
    ctx.beginPath();
    ctx.arc(e.clientX, e.clientY, 30, 0, 2 * Math.PI);
    ctx.fill();
});

Inset circle demo

Upvotes: 0

markE
markE

Reputation: 105035

enter image description here

If doing .clip within an busy handler like mousemove is crashing your browser, you can pre-build your circle with inner shadow once at the beginning of your app and then reuse that pre-built circle with little impact on performance.

Here's how to create an in-memory canvas containing your circle with inner shadow. Yes, it uses .clip, but it only uses it once at the beginning of the app.

var PI2=Math.PI*2;
var cut=document.createElement('canvas');
var cutCtx=cut.getContext('2d');
cut.width=100;
cut.height=100;
cutCtx.arc(50,50,50,0,Math.PI*2);
cutCtx.closePath();
cutCtx.clip();
cutCtx.shadowColor='black'
cutCtx.shadowBlur=15;
for(var i=0;i<15;i++){
    cutCtx.stroke();
}

Then it's not very expensive to draw that pre-built circle inside the mousemove event.

Here's example code and a Demo:

var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var $canvas=$("#canvas");
var canvasOffset=$canvas.offset();
var offsetX=canvasOffset.left;
var offsetY=canvasOffset.top;
var scrollX=$canvas.scrollLeft();
var scrollY=$canvas.scrollTop();

var PI2=Math.PI*2;
var cut=document.createElement('canvas');
var cutCtx=cut.getContext('2d');
cut.width=100;
cut.height=100;
cutCtx.arc(50,50,50,0,Math.PI*2);
cutCtx.closePath();
cutCtx.clip();
cutCtx.shadowColor='black'
cutCtx.shadowBlur=15;
for(var i=0;i<15;i++){
  cutCtx.stroke();
}

ctx.fillStyle='white';
ctx.fillRect(0,0,cw,ch);

$("#canvas").mousemove(function(e){handleMouseMove(e);});


function applyCut(mx,my){
  // hide the background image by whiteing-out the canvas
  ctx.fillRect(0,0,cw,ch);

  // use compositing to "erase" a circle under the mouse
  ctx.globalCompositeOperation='destination-out';
  ctx.beginPath();
  ctx.arc(mx,my,50,0,PI2);
  ctx.closePath();
  ctx.fill();

  // draw the pre-built circle shadow under the mouse
  ctx.globalCompositeOperation='source-over';
  // Hint: the in-memory canvas can be an image source for
  // your on-screen canvas
  ctx.drawImage(cut,mx-50,my-50);
}


function handleMouseMove(e){
  // tell the browser we're handling this event
  e.preventDefault();
  e.stopPropagation();

  mouseX=parseInt(e.clientX-offsetX);
  mouseY=parseInt(e.clientY-offsetY);

  applyCut(mouseX,mouseY);

}
body{ background-color: ivory; }
#wrapper{position:relative;}
#bk,#canvas{position:absolute;}
#canvas{border:1px solid red;}
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<h4>Move the mouse to reveal the image underneath</h4>
<div id=wrapper>
  <img id=bk src=https://dl.dropboxusercontent.com/u/139992952/stackoverflow/tiger.png />
  <canvas id="canvas" width=512 height=512></canvas>
</div>

Upvotes: 2

Michael Cresta
Michael Cresta

Reputation: 337

Try drawing the arc that is to supply the shadow before the arc that supplies the frame. Save the context state before the beginPath and restore it after the stoke of the shadow arc. You will need to make the shadow arc radius smaller than the frame arc so that the outer edge of the shadow is covered by the stroke of the frameing arc. The outside shadow that is outside of the shadow arc is still drawn, but covered over by the framing arc.

Example:

context.save();
context.beginPath();
context.lineWidth = 6;
context.shadowColor = 'black';
context.strokeStyle = "rgba(0,0,0,1)";
context.shadowBlur = 15;
context.shadowOffsetX = 0;
context.shadowOffsetY = 0;
context.arc(x, y, 47, 0, 2 * Math.PI, false);
context.stroke();
context.restore();

context.save();
context.beginPath();
context.lineWidth = 6;
context.arc(x, y, 50, 0, 2 * Math.PI);
context.stroke();
context.restore();

You will need to adjust the size, color, and line width of the arcs to get the effect you want.

Upvotes: 3

Related Questions