I try to clip an canvas using to different shape : a polygon (saying a star) and a blured circle (its edges fade-out from transparent to partially opaque).
Here is a sample of the code to make this clip but I'm totally unable to create the "blur effect" around the circle.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const img = new Image();
img.src = "https://picsum.photos/800/600";
img.onload = function () {
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
const radius = 70;
function draw() {
ctx.save();
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
const starPoints = createStar(5, mouseX, mouseY, 100, 50);
ctx.beginPath();
ctx.moveTo(starPoints[0].x, starPoints[0].y);
for (let i = 1; i < starPoints.length; i++) {
ctx.lineTo(starPoints[i].x, starPoints[i].y);
}
ctx.closePath();
ctx.clip();
const gradient = ctx.createRadialGradient(
mouseX,
mouseY,
radius * 0.8,
mouseX,
mouseY,
radius
);
gradient.addColorStop(0, "rgba(0, 0, 0, 0)");
gradient.addColorStop(1, "rgba(0, 0, 0, 0.7)");
ctx.fillStyle = gradient;
ctx.beginPath();
ctx.arc(mouseX, mouseY, radius, 0, Math.PI * 2);
ctx.fill();
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
ctx.restore();
}
function createStar(points, cx, cy, outerRadius, innerRadius) {
const result = [];
const angleStep = Math.PI / points;
for (let i = 0; i < 2 * points; i++) {
const angle = i * angleStep - Math.PI / 2;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
result.push({
x: cx + radius * Math.cos(angle),
y: cy + radius * Math.sin(angle),
});
}
return result;
}
canvas.addEventListener("mousemove", function (event) {
mouseX = event.clientX;
mouseY = event.clientY;
draw();
});
draw();
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>
Use compositing instead of clipping, the latter uses a path and disregards transparency, it thus can't do antialiasing, let alone blur. The former on the other hand does work directly with transparency.
But, since you are using some kind of semi-transparency covering to create a fade effect, you'll need another layer where you'll do the compositing first.
For this, you can simply use another <canvas>
, or an OffscreenCanvas
.
And since we're at using layers, we can even create one for the blurred star, avoiding tracing a new one + blur at every frame.
// Prepare the mask:
const mask = new OffscreenCanvas(200, 200);
{
const maskCtx = mask.getContext("2d");
// Edited so that it returns an SVGPath string
const createStar = (points, cx, cy, outerRadius, innerRadius) => {
const result = [];
const angleStep = Math.PI / points;
for (let i = 0; i < 2 * points; i++) {
const angle = i * angleStep - Math.PI / 2;
const radius = i % 2 === 0 ? outerRadius : innerRadius;
const x = cx + radius * Math.cos(angle);
const y = cy + radius * Math.sin(angle);
result.push(`${x},${y}`);
}
return `M${result.join("L")}`;
}
const starPath = new Path2D(createStar(5, 100, 100, 100, 50));
maskCtx.fill(starPath);
// We'll make the blur through a filter,
// you can keep your gradient if you prefer
maskCtx.filter = "blur(5px)";
maskCtx.arc(100, 100, 80, 0, Math.PI * 2);
maskCtx.globalCompositeOperation = "destination-in";
maskCtx.fill();
}
// Where we'll apply the mask over the image
const clippingCanvas = new OffscreenCanvas(innerWidth, innerHeight);
const updateClip = (mouseX, mouseY) => {
const ctx = clippingCanvas.getContext("2d");
if ( // same size as target canvas
clippingCanvas.width !== canvas.width ||
clippingCanvas.height !== canvas.height) {
clippingCanvas.width = canvas.width;
clippingCanvas.height = canvas.height;
}
else {
ctx.globalCompositeOperation = "source-over";
ctx.clearRect(0, 0, canvas.width, canvas.height);
}
ctx.drawImage(mask, mouseX - mask.width / 2, mouseY - mask.height / 2);
ctx.globalCompositeOperation = "source-in";
ctx.drawImage(img, 0, 0, canvas.width, canvas.height);
}
// Now the visible canvas
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
const img = new Image();
img.src = "https://picsum.photos/800/600";
img.onload = function () {
let mouseX = canvas.width / 2;
let mouseY = canvas.height / 2;
const radius = 70;
function draw() {
// fade
ctx.fillStyle = "rgba(0, 0, 0, 0.7)";
ctx.fillRect(0, 0, canvas.width, canvas.height);
updateClip(mouseX, mouseY);
ctx.drawImage(clippingCanvas, 0, 0);
}
canvas.addEventListener("mousemove", function (event) {
mouseX = event.clientX;
mouseY = event.clientY;
draw();
});
draw();
};
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<style>
body {
margin: 0;
overflow: hidden;
}
canvas {
display: block;
}
</style>
</head>
<body>
<canvas id="canvas"></canvas>
</body>
</html>