I'm trying to create a blurry donut shape like this on a canvas with js.
I tried
gradient.addColorStop(0, "rgba(0, 0, 0, .1)");
gradient.addColorStop(0.5, "rgba(128, 128, 128, .1)");
gradient.addColorStop(1, "rgba(0, 0, 0, .1)");
But I only get
It has a distinct defined circle at the stop radius. What I want is a smooth falloff. Something like this..
Is this possible?
It looks like you're after a Gaussian blur rather than a gradient.
You can create such a blur using the ctx.filter property and pass in a CSS filter value "blur(Npx)".
However Safari still doesn't support this property, so for this browser, we need to use a shadow as a workaround.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
if (ctx.filter === "none") {
ctx.filter = "blur(60px)";
}
else { // Safari still doesn't support ctx.filter...
ctx.shadowColor = "#34aaff";
ctx.shadowBlur = 120; // x2
ctx.shadowOffsetX = 800;
ctx.translate(-800, 0); // we draw the actual shape outside of the visible context
}
ctx.arc(400, 400, 200, 0, Math.PI*2);
ctx.lineWidth = 125;
ctx.strokeStyle = "#34aaff";
ctx.stroke();
<canvas width=800 height=800></canvas>
You can try to add one or more color stops to control the shape:
gradient.addColorStop(0, "White");
gradient.addColorStop(0.3, "rgba(128, 128, 256, .5)");
gradient.addColorStop(0.4, "rgba(128, 128, 256, .5)");
gradient.addColorStop(1, "White");
Related
I have a canvas and when i draw 2 things overlapping with low opacity, the opacity gets higher in the part where they are overlapping. Is there a way to make it all the same opacity even if 2 things are overlapping.
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
ctx.fillStyle = "rgba(0, 0, 255, 0.2)";
ctx.fillRect(10, 10, 50, 50);
ctx.fillRect(20, 20, 50, 60);
ctx.fillRect(40, 5, 50, 40)
canvas {
width: 100vw;
height: 100vh;
}
<p>all of it should be the same color but the overlapping parts are darker</p>
<canvas id="canvas"></canvas>
For simple cases the usual trick is to use a second canvas as a layer: you draw the parts that shouldn't mix with full opacity on a detached canvas, and then you draw this canvas with the intended alpha on the visible one:
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
const detached = canvas.cloneNode();
const ctx2 = detached.getContext("2d");
// draw at full opacity
ctx2.fillStyle = "rgb(0, 0, 255)";
ctx2.fillRect(10, 10, 50, 50);
ctx2.fillRect(20, 20, 50, 60);
ctx2.fillRect(40, 5, 50, 40)
// draw something in the background of the visible canvas
// where we want the blue rects to mix with
ctx.fillStyle = "green";
ctx.fillRect(50, 65, 30, 30);
// now draw the blue rects in a single pass with the expected alpha
ctx.globalAlpha = 0.2
ctx.drawImage(detached, 0, 0);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Note that this exact example could have been done with a single canvas: if all your shape do share the same color, you can make them all be part of the same sub-path and fill them all in a single call.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 65, 30, 30);
ctx.fillStyle = "rgb(0, 0, 255, 0.2)";
ctx.beginPath();
ctx.rect(10, 10, 50, 50);
ctx.rect(20, 20, 50, 60);
ctx.rect(40, 5, 50, 40);
ctx.fill();
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Now these work only when the transparency is the same for all the shapes to be drawn. In case of multiple shapes with different alpha values, they'd still get mixed.
Here is an example of such a case.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
ctx.fillStyle = "rgba(0, 0, 255, .2)";
ctx.fillRect(40, 50, 70, 60);
ctx.fillStyle = "rgba(0, 0, 255, .8)";
ctx.fillRect(10, 20, 60, 70);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
If we wanted the first blue rect to be over the second one without mixing, but still mix with the green one, we'd need to use a variation of the first solution, but use more steps:
First you calculate the alpha value of the more transparent rect relative to the more opaque one. Here we have 0.2 and 0.8, so if we want the 0.8 to be 1, 0.2 has to become 0.25.
We draw the more transparent rect at 0.25, then the more opaque one at 1 over this.
We redraw the composition with the target 0.8 alpha value.
We draw that over the green background.
However I'll take the opportunity of this new snippet to show that with some creative use of the globalCompositeOperation property we can make all this on a single canvas.
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
// To use a single canvas, we will draw the green "background" later
// target color is rgb(0, 0, 255, 0.2) & max alpha is 0.8
// 0.2 x (1 / 0.8) -> 0.25
ctx.fillStyle = "rgb(0, 0, 255, 0.25)";
ctx.fillRect(40, 50, 70, 60);
ctx.fillStyle = "rgba(0, 0, 255, 1)";
ctx.fillRect(10, 20, 60, 70);
ctx.globalAlpha = 0.8;
// gCO "copy" will clear whatever was on the context before the next paint
ctx.globalCompositeOperation = "copy";
ctx.drawImage(ctx.canvas, 0, 0);
// we could continue over like this if more such opacities were required
ctx.globalAlpha = 1;
// gCO "destination-over" will draw behind what's already painted on the context
ctx.globalCompositeOperation = "destination-over";
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
Now, it may be of interest for some, but at WHATWG/html we started a discussion about enabling a layer API for the canvas2D, and while it's far from reaching a consensus, I wrote a not so little prototype of a CanvasLayer interface that would allow us to, well, use layers in a canvas.
The idea is to create a CanvasLayer object, that does record drawings operations that will get executed on it when it's rendered on a context, using the context's current settings. Basically reproducing the detached canvas trick, but with taking care automagically of the sizing of the detached canvas, and (if implemented natively), without assigning a full bitmap buffer.
In my very biased opinion (I'm the author of both that proposal and of the prototype), this would allow for clearer code when we have to deal with multiple layers over a canvas.
/* CSS checkerboard stolen from https://drafts.csswg.org/css-images-4/#example-2de97f53 */
canvas {
background: repeating-conic-gradient(rgba(0,0,0,0.1) 0deg 25%, white 0deg 50%);
background-size: 2em 2em;
}
<canvas></canvas>
<script type="module">
import CanvasLayer from "https://cdn.jsdelivr.net/gh/Kaiido/CanvasLayer/bundles/CanvasLayer.min.mjs";
const canvas = document.querySelector("canvas");
const ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(50, 10, 50, 60);
const backLayer = new CanvasLayer();
backLayer.fillStyle = "rgba(0, 0, 255)"; // fully opaque
backLayer.fillRect(40, 50, 70, 60);
const frontLayer = new CanvasLayer();
frontLayer.fillStyle = "rgba(0, 0, 255)";
frontLayer.fillRect(10, 20, 60, 70);
frontLayer.globalAlpha = 0.2;
frontLayer.renderLayer(backLayer);
ctx.globalAlpha = 0.8;
ctx.renderLayer(frontLayer);
</script>
I'm trying to draw a transparent circle on the HTML Canvas. Here is the code:
const canvas = document.querySelector('#canvas');
let ctx = canvas.getContext('2d');
canvas.width = 700;
canvas.height = 700;
ctx.beginPath();
ctx.fillStyle = 'rgba(0, 0, 0, 0.5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-out';
ctx.clearRect(50, 50, 200, 200);
ctx.beginPath();
ctx.fillRect(50, 50, 200, 200);
ctx.moveTo(350, 150);
ctx.beginPath();
ctx.arc(350, 150, 80, 0, 2 * Math.PI, false);
ctx.closePath();
ctx.fill();
I was able to make the rectangle transparent with globalCompositeOperation set to destination-out but it failed to make the circle fully transparent.
Here is what MDN says about destination-out operation,
The existing content is kept where it doesn't overlap the new shape.
The circle has still some opaque color applied to it. How to make it fully transparent?
Here is the live demo.
Unable to understand what's wrong with the code. Any help would be appreciated.
You are using beginpath methods but the directives you use aren't paths.
The fully transparent rectangle is created by the clearRect and not by the fillRect method.
So you should set fillStyle to white and then the code works.
const canvas = document.querySelector('#canvas');
let ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 400;
ctx.fillStyle = 'rgba(0, 0, 0, .5)';
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = 'destination-out';
ctx.fillStyle = 'rgba(255, 255, 255, 1)';
ctx.fillRect(25, 25, 150, 150);
ctx.arc(275, 100, 60, 0, 2 * Math.PI, false);
ctx.fill();
body {
background-color: skyblue;
position: relative;
}
img, #canvas {
position: absolute;
}
<body>
<img src="https://images.unsplash.com/photo-1635315850978-44cd0b237006?ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&ixlib=rb-1.2.1&auto=format&fit=crop&w=1770&q=80" alt="" width=600 height=390>
<canvas id="canvas"></canvas>
</body>
I'm making a game with javascript canvas. I'm drawing all the game elements, like the player, blocks and lines, but I don't want you to see all that. Instead want the whole screen to be black, expect for in some places where there is lightsources. I don't need any shadows, just a circle on the screen that is lit up with a radial gradient. I am able to achieve this for one lightsource by adding a transparent gradient after I have drawn everything else, like this: (imagine the red rectangle to be all the things in the game)
//Drawing all the game elements
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 400, 300);
//adding the darkness and the lightsources
var grd = ctx.createRadialGradient(150, 100, 5, 150, 100, 100);
grd.addColorStop(0, "transparent");
grd.addColorStop(1, "black");
ctx.fillStyle = grd; ctx.fillRect(0, 0, 600, 400);
JSFiddle
But how can I achieve this with multiple lightsources? The technique showed won't work.
I have tried using the Illuminated.js api, but that was incredibly slow, and I don't need anything of the shadows and all that fancy stuff, just a circle where you can see the game.
Here's an example of my approach - create black&white mask and multiply the base with it:
var c = document.getElementById("myCanvas");
var ctx = c.getContext("2d");
//Drawing all the game elements
ctx.fillStyle = "red";
ctx.fillRect(100, 100, 400, 300);
//adding the darkness and the lightsources
function addlight(ctx, x, y) {
var grd = ctx.createRadialGradient(x, y, 10, x, y, 150);
grd.addColorStop(0, "white");
grd.addColorStop(1, "transparent");
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 600, 400);
}
var buffer = document.createElement('canvas');
buffer.width = 600;
buffer.height = 400;
b_ctx = buffer.getContext('2d');
b_ctx.fillStyle = "black";
b_ctx.fillRect(0, 0, 600, 400);
addlight(b_ctx, 150, 100);
addlight(b_ctx, 350, 200);
addlight(b_ctx, 450, 250);
ctx.globalCompositeOperation = "multiply";
ctx.drawImage(buffer, 0, 0);
<canvas id="myCanvas" width="600" height="400" style="border:1px solid #d3d3d3;">
I am currently coding an interactive application using Javascript and the HTML canvas element. One of the things I'm coding is an opacity gradient. My gradient is supposed to fade from transparent to partially opaque, but the entire area I defined as the place to create the gradient is only partially opaque with no fade from transparency. Here's my code:
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var grd = ctx.createLinearGradient(0, 1000, 0, 0);
grd.addColorStop(0.5, "rgba(255, 255, 255, 0.5)");
grd.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = grd;
ctx.fillRect(0, 660, 1000, 10);
}
It has something to with the dimensions you use in createLinearGradient and fillRect. Right now you are defining your gradient area as a really tall thin rectangle [(0,0) to (0,1000)]. However, then you draw your gradient rectangle as a pretty big rectangle [(0,660) to (1000,10)].
I'm not exactly sure what your desired output looks like but it seems that your ctx.fillRect line is wrong. Trying the following works for me:
function draw() {
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var grd = ctx.createLinearGradient(0, 1000, 0, 0);
grd.addColorStop(0.5, "rgba(255, 255, 255, 0.5)");
grd.addColorStop(1, "rgba(255, 255, 255, 0)");
ctx.fillStyle = grd;
ctx.fillRect(0, 10, 1000, 660);
}
And the jsFiddle if you are interested
I create a simple circle with the arc function:
/* ctx is the 2d context */
ctx.beginPath();
var gradient = ctx.createLinearGradient(0, 0, this.radius, this.radius);
gradient.addColorStop(0, '#FF0000');
gradient.addColorStop(1, '#FFFFFF');
ctx.lineWidth = 10;
ctx.arc(this.radius, this.radius, this.radius, 0, 2*Math.PI, true);
ctx.strokeStyle = gradient;
ctx.stroke();
So I want to rotate the gradient, is that possible?
I've tried with ctx.rotate(x) but that rotates the whole circle!
Yes. Your gradient is going from x1,y1 to x2,y2, which are the four last arguments of createLinearGradient
For example, to reverse your gradient do this:
var gradient = ctx.createLinearGradient(this.radius, this.radius, 0, 0);
Or change it up however you please:
var gradient = ctx.createLinearGradient(this.radius, 0, 0, 0);
And so on.