Reputation: 181
For CSS box-shadow, the "spread" property is used to set the size of shadow. But the shadow API of CANVAS seems lack of the counterpart property.
I know the answer is probably NO.
But is there some way to achieve the shadow "spread" effect on CANVAS?
If the answer is still NO, will it be "YES" in the next up-coming HTML standard?
Upvotes: 3
Views: 1916
Reputation: 137006
There is no built-in way to do this.
There is a hope someday drop-shadow()
CSS function will support that parameter too, that day we will be able to use it in the filter
parameter of our 2D contexts, but for the time being the closest we can have is through SVG filters.
Here is one attempt of mine, which only approximates what box-shadow
's do. Nevertheless, it may help:
const canvas = document.getElementById( 'canvas' );
const ctx = canvas.getContext( '2d' );
const filter = spreadingBoxShadow( 5, 5, 5, 15, 'lime' );
const img = new Image();
img.onload = (e) => {
ctx.filter = filter;
ctx.drawImage( img, 50, 50, 200, 150 );
};
img.src = "https://upload.wikimedia.org/wikipedia/commons/4/47/PNG_transparency_demonstration_1.png";
function spreadingBoxShadow( offset_x, offset_y, blur_radius, spread_radius, color ) {
const _id = "spread-radius-" + Date.now();
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.classList.add( 'hidden-svg' );
svg.innerHTML = `
<filter id="${ _id }" x="-100%" y="-100%" width="300%" height="300%" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-color="${ color }" result="flood" in="SourceAlpha" />
<feComposite in2="SourceAlpha" in="flood" operator="atop" result="color" />
<feMorphology operator="dilate" radius="${ spread_radius}" result="spread" in="color"/>
<feGaussianBlur in="spread" stdDeviation="${ blur_radius }" result="shadow"/>
<feOffset dx="${ offset_x }" dy="${ offset_y }" in="shadow" result="offset"/>
<feMerge result="merge">
<feMergeNode in="offset"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
`;
document.body.append( svg );
return `url(#${ _id })`;
}
.hidden-svg {
position: absolute;
z-index: -1;
pointer-events: none;
visibility: hidden;
}
<canvas id="canvas" width="400" height="300"></canvas>
Upvotes: 0
Reputation: 625
But is there some way to achieve the shadow "spread" effect on CANVAS?
Yup :)
A way to mimic the spread radius
is to create a bigger version of the box that we will position outside the canvas & use the offset X and offset Y to get its shadow inside the canvas at the place we want. It will only remain to draw the original box after that.
To create a bigger box we will use scale(), we will need to calculate the scale factor first.
We obtain the scale factor (SF) by doing the ratio between the size of the wanted shadow and the size of the box SF = (boxW + 2 * spreadRadius) / boxW
Then we will save the entire state of the canvas with the save() method before calling the scale(), so we will be able to get back our original state after the manipulation (with the restore() method)
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
const x = 50;
let y = 20;
const spreadRadius = 10;
const boxW = 50;
const boxH = 50;
const color = 'rgba(255, 0, 0, 0.4)';
const blurRadius = 10;
const offsetX = 0;
const offsetY = 0;
ctx.fillRect(x, y, boxW, boxH);
ctx.font = "16px Arial";
ctx.fillText('Origin Box', x + 100, y + 30);
ctx.save();
y += 85;
let scaleFactor = ((2 * spreadRadius) + boxW) / boxW;
ctx.scale(scaleFactor, scaleFactor);
ctx.fillRect((x - spreadRadius) / scaleFactor, (y - spreadRadius ) / scaleFactor, boxW, boxH);
ctx.restore();
ctx.fillText('Scaled Box', x + 100, y + 30);
ctx.save();
y += 100;
scaleFactor = ((2 * spreadRadius) + boxW) / boxW;
ctx.scale(scaleFactor, scaleFactor);
ctx.shadowColor = color;
ctx.shadowBlur = blurRadius;
ctx.shadowOffsetX = offsetX;
ctx.shadowOffsetY = offsetY;
ctx.fillRect((x - spreadRadius) / scaleFactor, (y - spreadRadius) / scaleFactor, boxW, boxH);
ctx.restore();
ctx.fillText('Scaled Box with shadow', x + 100, y + 30);
ctx.save();
y += 100;
scaleFactor = ((2 * spreadRadius) + boxW) / boxW;
ctx.scale(scaleFactor, scaleFactor);
ctx.shadowColor = color;
ctx.shadowBlur = blurRadius;
ctx.shadowOffsetX = offsetX + w;
ctx.shadowOffsetY = offsetY + h;
ctx.fillRect((x - spreadRadius - w) / scaleFactor, (y - spreadRadius - h) / scaleFactor, boxW, boxH);
ctx.restore();
ctx.fillText('Same but we draw the box outside the canvas', x + 100, y + 30);
ctx.fillText('and keep its shadow by playing on the offsets', x + 100, y + 47);
ctx.save();
y += 100;
scaleFactor = ((2 * spreadRadius) + boxW) / boxW;
ctx.scale(scaleFactor, scaleFactor);
ctx.shadowColor = color;
ctx.shadowBlur = blurRadius;
ctx.shadowOffsetX = offsetX + w;
ctx.shadowOffsetY = offsetY + h;
ctx.fillRect((x - spreadRadius - w) / scaleFactor, (y - spreadRadius - h) / scaleFactor, boxW, boxH);
ctx.restore();
ctx.clearRect(x, y, boxW, boxH);
ctx.fillRect(x, y, boxW, boxH);
ctx.fillText('Origin Box + shadow with the spread radius', x + 100, y + 30);
#canvas{
border: 1px solid black;
}
<canvas id="canvas" width="500" height="500"></canvas>
The trick is to draw the scaled box outside the canvas. We're doing so by substracting the width & height of the canvas to the x & y position of the scaled box. By doing so, we are sure that it's drawn outside the canvas:
ctx.fillRect(x - w, y - h, boxW, boxH); // We substract w to the x position and h to the y position
And then, we add it to the shadow offsets to draw the shadow inside the canvas:
ctx.shadowOffsetX = offsetX + w; // We add w which is the canvas width
ctx.shadowOffsetY = offsetY + h; // We add h which is the canvas height
Only remain to draw the original box and we have our desired result.
I created a boxShadow()
function to gather all the approach in a concret example. That function work like that:
Create a shadowValues Object:
const newShadowObject = {box: {posX: x, posY: y, width: boxW, height: boxH}, offsetX, offsetY, blurRadius, spreadRadius, color}
Call the boxShadow()
function with the object as parameter:
boxShadow(newShadowObject);
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const w = canvas.width;
const h = canvas.height;
function boxShadow(shadowValues){
const {box: {posX: x, posY: y, width: boxW, height: boxH}, offsetX, offsetY, blurRadius, spreadRadius, color} = shadowValues;
// We draw the shadow
ctx.save();
const scaleFactor = ((2 * spreadRadius) + boxW) / boxW;
ctx.scale(scaleFactor, scaleFactor);
ctx.shadowColor = color;
ctx.shadowBlur = blurRadius;
ctx.shadowOffsetX = offsetX + w;
ctx.shadowOffsetY = offsetY + h;
ctx.fillRect((x - spreadRadius - w) / scaleFactor, (y - spreadRadius - h) / scaleFactor, boxW, boxH);
ctx.restore();
// We draw the original box
ctx.clearRect(x, y, boxW, boxH);
ctx.rect(x, y, boxW, boxH);
ctx.stroke();
}
const case1 = {box: {posX: 30, posY: 30, width: 50, height: 50}, offsetX: 0, offsetY: 0, blurRadius: 0, spreadRadius: 0, color: '#ff0000'};
boxShadow(case1);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 0", 30, 120);
ctx.fillText("offsetY: 0", 30, 140);
ctx.fillText("blurRadius: 0", 30, 160);
ctx.fillText("spreadRadius: 0", 30, 180);
const case2 = {box: {posX: 170, posY: 30, width: 50, height: 50}, offsetX: 0, offsetY: 0, blurRadius: 0, spreadRadius: 10, color: '#ff0000'};
boxShadow(case2);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 0", 170, 120);
ctx.fillText("offsetY: 0", 170, 140);
ctx.fillText("blurRadius: 0", 170, 160);
ctx.fillText("spreadRadius: 10", 170, 180);
const case3 = {box: {posX: 320, posY: 30, width: 50, height: 50}, offsetX: 5, offsetY: 5, blurRadius: 0, spreadRadius: 10, color: '#ff0000'};
boxShadow(case3);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 5", 320, 120);
ctx.fillText("offsetY: 5", 320, 140);
ctx.fillText("blurRadius: 0", 320, 160);
ctx.fillText("spreadRadius: 10", 320, 180);
const case4 = {box: {posX: 470, posY: 30, width: 50, height: 50}, offsetX: 5, offsetY: 5, blurRadius: 20, spreadRadius: 10, color: '#ff0000'};
boxShadow(case4);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 5", 470, 120);
ctx.fillText("offsetY: 5", 470, 140);
ctx.fillText("blurRadius: 20", 470, 160);
ctx.fillText("spreadRadius: 10", 470, 180);
const case5 = {box: {posX: 55, posY: 225, width: 50, height: 100}, offsetX: 0, offsetY: 0, blurRadius: 20, spreadRadius: 0, color: '#ff0000'};
boxShadow(case5);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 0", 30, 380);
ctx.fillText("offsetY: 0", 30, 400);
ctx.fillText("blurRadius: 20", 30, 420);
ctx.fillText("spreadRadius: 0", 30, 440);
const case6 = {box: {posX: 200, posY: 250, width: 100, height: 50}, offsetX: 10, offsetY: 5, blurRadius: 0, spreadRadius: 0, color: '#ff0000'};
boxShadow(case6);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 10", 200, 380);
ctx.fillText("offsetY: 5", 200, 400);
ctx.fillText("blurRadius: 0", 200, 420);
ctx.fillText("spreadRadius: 0", 200, 440);
const case7 = {box: {posX: 400, posY: 210, width: 50, height: 50}, offsetX: 70, offsetY: 70, blurRadius: 10, spreadRadius: 10, color: '#ff0000'};
boxShadow(case7);
ctx.font = "16px Arial";
ctx.fillText("offsetX: 70", 400, 380);
ctx.fillText("offsetY: 70", 400, 400);
ctx.fillText("blurRadius: 0", 400, 420);
ctx.fillText("spreadRadius: 10", 400, 440);
#canvas{
border: 1px solid black;
background-color: #EFEFFA;
}
<canvas id="canvas" width="620" height="500"></canvas>
Upvotes: 3