I am creating a game using the HTML5 Canvas element, and as one of the visual effects I would like to create a glow (like a light) effect. Previously for glow effects I found solutions involving creating shadows of shapes, but these require a solid shape or object to cast the shadow. What I am looking for is a way to create something like an ambient light glow with a source location but no object at the position.
Something I have thought of was to define a centerpoint x and y and create hundreds of concentric circles, each 1px larger than the last and each with a very low opacity, so that together they create a solid center and a transparent edge. However, this is very computationally heavy and does not seem elegant at all, as the resulting glow looks awkward.
While this is all that I am asking of and I would be more than happy to stop here, bonus points if your solution is A) computationally light, B) modifiable to create a focused direction of light, or even better, C) if there was a way to create an "inverted" light system in which the entire screen is darkened by a mask and the shade is lifted where there is light.
I have done several searches, but none have turned up any particularly illuminating results.
So I'm not quite sure what you want, but I hope the following snippet will help.
Instead of creating a lot of concentric circles, create one radialGradient.
Then you can combine this radial gradient with some blending, and even filters to modify the effect as you wish.
var img = new Image();
img.onload = init;
img.src = "https://dev.w3.org/SVG/tools/svgweb/samples/svg-files/car.svg";
var ctx = c.getContext('2d');
var gradCtx = c.cloneNode().getContext('2d');
var w, h;
var ratio;
function init() {
w = c.width = gradCtx.canvas.width = img.width;
h = c.height = gradCtx.canvas.height = img.height;
draw(w / 2, h / 2)
updateGradient();
c.onmousemove = throttle(handleMouseMove);
}
function updateGradient() {
var grad = gradCtx.createRadialGradient(w / 2, h / 2, w / 8, w / 2, h / 2, 0);
grad.addColorStop(0, 'transparent');
grad.addColorStop(1, 'white');
gradCtx.fillStyle = grad;
gradCtx.filter = "blur(5px)";
gradCtx.fillRect(0, 0, w, h);
}
function handleMouseMove(evt) {
var rect = c.getBoundingClientRect();
var x = evt.clientX - rect.left;
var y = evt.clientY - rect.top;
draw(x, y);
}
function draw(x, y) {
ctx.clearRect(0, 0, w, h);
ctx.globalCompositeOperation = 'source-over';
ctx.drawImage(img, 0, 0);
ctx.globalCompositeOperation = 'destination-in';
ctx.drawImage(gradCtx.canvas, x - w / 2, y - h / 2);
ctx.globalCompositeOperation = 'lighten';
ctx.fillRect(0, 0, w, h);
}
function throttle(callback) {
var active = false; // a simple flag
var evt; // to keep track of the last event
var handler = function() { // fired only when screen has refreshed
active = false; // release our flag
callback(evt);
}
return function handleEvent(e) { // the actual event handler
evt = e; // save our event at each call
if (!active) { // only if we weren't already doing it
active = true; // raise the flag
requestAnimationFrame(handler); // wait for next screen refresh
};
}
}
<canvas id="c"></canvas>
Related
So I have this piece of code that I use for erasing and restoring parts of an image with a (for example) removed background. Erasing from the main canvas is simple and the user can erase a circular shape with a line between points.
if(removeMode) {
ctxs[index].globalCompositeOperation = 'destination-out';
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, 2 * Math.PI);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
}
The problem is with the restoring. Currently I am able to copy parts of the original image to the main canvas but only in a rectangular shape using the getImageData() and putImageData() functions.
ctxs[index].globalCompositeOperation = 'source-out';
ctxs[0].putImageData(ctxs[1].getImageData(x-radius, y-radius, 2*radius, 2*radius), x-radius, y-radius);
Ideally I would like to clip a part of the original image canvas to the main canvas with a shape similar to the erasing feature. I have tried the clip() function but honestly I am not sure how to go about it. Here is what I initially tried to clip a part of a canvas.
ctxs[index].beginPath();
ctxs[index].arc(x, y, radius, 0, Math.PI * 2);
ctxs[index].fill();
ctxs[index].lineWidth = 2 * radius;
ctxs[index].beginPath();
ctxs[index].moveTo(old.x, old.y);
ctxs[index].lineTo(x, y);
ctxs[index].stroke();
ctxs[index].clip();
How do I copy a custom shape from a canvas to another canvas?
Thanks in advance,
Edit:
I have also thought of using a mask where I would create the mask as such (example using numpy in python):
Y, X = np.ogrid[:canvas_height, :canvas_width]
# Y, X are matrix values and x, y are coordinates of the cursor within the image
center_dist = np.sqrt((X - x)**2 + (Y-y)**2)
# create mask
mask = center_dist <= radius
# omit everything except circular shape from mask
circular_img = original_img.copy()
circular_img[~mask] = 0
# combine images
new_img = np.maximum(original_img, new_img)
Example of what I have now
Simpler solution
Every shape fits into a rectangle.
Proof Your canvas is a rectangle and already contains the shape.
As a result, you can determine the smallest possible rectangle that contains the full shape and store that. It will necessarily contain your shape. Upon reload you will need to know the shape's boundaries inside the copy though, so that info will also be needed.
Harder, but more precise solution
You can create a structure and store the content, point-by-point (yet, this will be not very performant):
const data = context.getImageData(0, 0, canvas.width, canvas.height).data;
let myShape = [];
for (let x = 0; x < canvas.width; x++) {
for (let y = 0; y < canvas.height; y++) {
if (inShape(x, y, canvas)) {
myShape.push({x, y, content: data});
}
}
}
The snippet above assumes that you have properly implemented inShape.
Homogeneous shape
If all the points inside the shape are similar, then you will need to only know where the boundaries of the shape were. If you have a convex polygon, for example, then you will need to know where its center is and what the boundaries are. If you have a filled circle, then you will only need its center and radius. The geometrical data you need largely depend on what shape you have.
Keep using composite operations.
"destination-out" will indeed remove the previous pixels that do overlap with the new ones.
If you use the inverse "destination-in", only the previous pixels that do overlap with the new ones are kept.
So you store your original image intact, and then use one of these modes to render it given the action you want to perform.
Here since it seems we are in a paint-like configuration, I guess it makes more sense to erase the final result and restore the original canvas. For this we need a third canvas, detached where we'll draw the "restoration" part on its own before drawing that back to the visible canvas:
(async () => {
// the main, visible canvas
const canvas = document.querySelector("canvas");
canvas.width = 500;
canvas.height = 250;
const ctx = canvas.getContext("2d");
// a detached canvas context to do the compositing
const detached = canvas.cloneNode().getContext("2d");
// the "source" canvas (here just an ImageBitmap)
const originalCanvas = await loadImage();
ctx.lineWidth = detached.lineWidth = 8;
// we store every drawing in its own Path2D object
const paths = [];
let down = false;
const checkbox = document.querySelector("input");
canvas.onmousedown = (evt) => {
down = true;
const newPath = new Path2D();
newPath.isEraser = !checkbox.checked;
paths.push(newPath);
};
canvas.onmouseup = (evt) => { down = false; };
canvas.onmousemove = (evt) => {
if (!down) { return; }
const {x, y} = parseMouseEvent(evt);
paths[paths.length - 1].lineTo(x, y);
redraw();
};
redraw();
function redraw() {
// clear the visible context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(originalCanvas, 0, 0);
paths.forEach((path) => {
if (path.isEraser) {
// erase the current content
ctx.globalCompositeOperation = "destination-out";
ctx.stroke(path);
}
else {
// to restore
// we do the compositing on the detached canvas
detached.globalCompositeOperation = "source-over";
detached.drawImage(originalCanvas, 0, 0);
detached.globalCompositeOperation = "destination-in";
detached.stroke(path);
// draw the result on the main context
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(detached.canvas, 0, 0);
}
});
}
})().catch(console.error);
async function loadImage() {
const url = "https://picsum.photos/500/250";
const req = await fetch(url);
const blob = req.ok && await req.blob();
return createImageBitmap(blob);
}
function parseMouseEvent(evt) {
const rect = evt.target.getBoundingClientRect();
return {x: evt.clientX - rect.left, y: evt.clientY - rect.top };
}
canvas { border: 1px solid; vertical-align: top }
<label>erase/restore <input type="checkbox"></label>
<canvas></canvas>
Note that here I do create new paths every time, but you could very well use the same ones for both erasing and restoring (and even any other graphic source).
I have been given the following task, but I am getting errors that can be seen when the code snippet is run. I would like some help figuring out what exactly I am doing wrong.
Basically, I need to draw a circle, make it so that it moves and changes the direction/color when touching the walls of the screen.
Task: create a Circle class with the following properties:
x - the initial value of the coordinate x
y is the initial value of the y coordinate
radius - values of width and height
color - fill color Describe the methods:
draw () - marks off on the screen an element that is described by the given properties
setColor (newColor) - Changes the fill color to newColor
move ({x = 0, y = 0}) - moves the captured object by the vector (x, y) - each time period (for example, 100 ms) changes (adds \ subtracts)
to the values x and y, respectively. When a circle collides with any
edge of the screen it is necessary to realize its mirror reflection
(change the value of the corresponding coordinate of the vector on the
opposite of the value of the sign, and call this method with the new
vector) and generate the collision event, collision, which is captured
at the document level.Hang on this event a handler that will change
the color of the pouring of the circle into another (random) value.
Movement occurs until the stop method is called.
stop () - stops the circle movement
If the Escape button on the keyboard was pressed, the movement should stop.
I created a canvas and set the frame to move. I drew a circle and tried to move it using setInterval(), but it seems like I'm losing the context.
let c = document.getElementById("mycanvas");
let ctx = c.getContext("2d");
let xinc = 1;
let yinc = 1;
class Circle {
constructor(xpos, ypos, radius, color) {
this.xpos = xpos;
this.ypos = ypos;
this.radius = radius;
this.color = color;
}
draw() {
ctx.beginPath();
ctx.arc(this.xpos, this.ypos, this.radius, 0, Math.PI * 2);
ctx.fillStyle = "red";
ctx.fill();
}
move(xpos, ypos) {
ctx.clearRect(0, 0, c.width, c.height);
ctx.beginPath();
this.draw();
xpos += xinc;
ypos += yinc;
console.log(xpos, ypos);
if ((this.xpos > c.width - this.radius) || (this.xpos < 0 + this.radius)) {
xinc = -xinc;
}
if ((this.ypos > c.height - this.radius) || (this.ypos < 0 + this.radius)) {
yinc = -yinc;
}
setInterval(this.move, 10);
//this.draw();
}
}
let circle = new Circle(200, 300, 50, "red");
circle.draw();
circle.move(200, 300);
<canvas id="mycanvas" width="1335" height="650" style="border: 1px solid"> </canvas>
I am just starting to learn events and DOMs, please help me correctly implement this task
You are passing this.move to setInterval with no context - just a function, with no this to call it in. You can pass in this.move.bind(this) to create a bound function. You can also do it once in the constructor: this.move = this.move.bind(this).
Also, the call to beginPath in move seems unnecessary.
I've been working on a game that's sort of a Worms clone. In it, the player rotates a cannon with the up up and down keys (it's a 2D game) to fire at enemies coming from above. I use the context.rotate() and context.translate() methods when drawing the cannon, then immediately context.restore() back to the default canvas.The cannon is the only thing (for now) that's rotated.
The problem is, I want to accurately show projectiles coming from the top of the cannon. For this, I need to know the top of the cannon's coordinates at all times. Normally, this is something I could easily calculate. However, because the canvas is rotated only before the cannon is drawn, it's not as simple.
Just use simple trigonometry to track the top:
var canonTopX = pivotX + Math.cos(angleInRadians) * canonLength;
var canonTopY = pivotY + Math.sin(angleInRadians) * canonLength;
You can choose to render the canon using transformations of course, or share the math.
ctx.translate(pivotX, pivotY);
ctx.rotate(angleInRadians);
//render canon from (0,0) pointing right (0°)
ctx.setTransform(1,0,0,1,0,0); // instead of save/restore
// calc canon top for projectiles here
var ctx = c.getContext("2d");
var canonLength = 70;
var angleInRadians = 0;
var angleStep = 0.03;
var pivotX = ctx.canvas.width>>1;
var pivotY = ctx.canvas.height>>1;
ctx.fillStyle = "#000";
ctx.strokeStyle = "#c00";
(function loop() {
angleInRadians += angleStep;
render();
requestAnimationFrame(loop);
})();
function render() {
ctx.clearRect(0,0,ctx.canvas.width,ctx.canvas.height);
ctx.translate(pivotX, pivotY);
ctx.rotate(angleInRadians);
ctx.fillRect(0, -5, canonLength, 10);
ctx.setTransform(1,0,0,1,0,0); // instead of save/restore
var canonTopX = pivotX + Math.cos(angleInRadians) * canonLength;
var canonTopY = pivotY + Math.sin(angleInRadians) * canonLength;
ctx.beginPath();
ctx.arc(canonTopX, canonTopY, 9, 0, 6.3);
ctx.stroke();
}
<canvas id=c width=600 height=180></canvas>
How to draw outer and inner border around any canvas shape?
I'm drawing several stroke-only shapes on an html canvas, and I would like to draw an inner and outer border around them.
draft example:
Is there a generic why to do it for any shape (assuming it's a closed stroke-only shape)?
Two methods
There is no inbuilt way to do this and there are two programmatic ways that I use. The first is complicated and involves expanding and contracting the path then drawing along that path. This works for most situations but will fail in complex situation, and the solution has many variables and options to account for these complications and how to handle them.
The better of the two
The second and easiest way that I present below is by using the ctx.globalCompositeOperation setting to mask out what you want drawn or not. As the stroke is drawn along the center and the fill fills up to the center you can draw the stroke at twice the desired width and then either mask in or mask out the inner or outer part.
This does become problematic when you start to create very complex images as the masking (Global Composite Operation) will interfere with what has already been drawn.
To simplify the process you can create a second canvas the same size as the original as a scratch space. You can then draw the shape on he scratch canvas do the masking and then draw the scratch canvas onto the working one.
Though this method is not as fast as computing the expanded or shrunk path, it does not suffer from the ambiguities faced by moving points in the path. Nor does this method create the lines with the correct line join or mitering for the inside or outside edges, for that you must use a the other method. For most purposes the masking it is a good solution.
Below is a demo of the masking method to draw an inner or outer path. If you modify the mask by including drawing a stroke along with the fill you can also set an offset so that the outline or inline will be offset by a number of pixels. I have left that for you. (hint add stroke and set the line width to twice the offset distance when drawing the mask).
var demo = function(){
/** fullScreenCanvas.js begin **/
var canvas = ( function () {
canvas = document.getElementById("canv");
if(canvas !== null){
document.body.removeChild(canvas);
}
// creates a blank image with 2d context
canvas = document.createElement("canvas");
canvas.id = "canv";
canvas.width = window.innerWidth;
canvas.height = window.innerHeight;
canvas.style.position = "absolute";
canvas.style.top = "0px";
canvas.style.left = "0px";
canvas.style.zIndex = 1000;
canvas.ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
return canvas;
})();
var ctx = canvas.ctx;
/** fullScreenCanvas.js end **/
/** CreateImage.js begin **/
// creates a blank image with 2d context
var createImage = function(w,h){
var image = document.createElement("canvas");
image.width = w;
image.height =h;
image.ctx = image.getContext("2d");
return image;
}
/** CreateImage.js end **/
// define a shape for demo
var shape = [0.1,0.1,0.9,0.1,0.5,0.5,0.8,0.9,0.1,0.9];
// draws the shape as a stroke
var strokeShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] *w, shape[1] *h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i] * w, shape[i + 1] * h);
}
ctx.closePath();
ctx.stroke();
}
// draws the shape as filled
var fillShape = function (ctx) {
var w, h, i;
w = canvas.width;
h = canvas.height;
ctx.beginPath();
ctx.moveTo(shape[0] * w,shape[1] * h)
for (i = 2; i < shape.length; i += 2) {
ctx.lineTo(shape[i]*w,shape[i+1]*h);
}
ctx.closePath();
ctx.fill();
}
var drawInOutStroke = function(width,style,where){
// clear the workspace
workCtx.ctx.globalCompositeOperation ="source-over";
workCtx.ctx.clearRect(0, 0, workCtx.width, workCtx.height);
// set the width to double
workCtx.ctx.lineWidth = width*2;
workCtx.ctx.strokeStyle = style;
// fill colour does not matter here as its not seen
workCtx.ctx.fillStyle = "white";
// can use any join type
workCtx.ctx.lineJoin = "round";
// draw the shape outline at double width
strokeShape(workCtx.ctx);
// set comp to in.
// in means leave only pixel that are both in the source and destination
if (where.toLowerCase() === "in") {
workCtx.ctx.globalCompositeOperation ="destination-in";
} else {
// out means only pixels on the destination that are not part of the source
workCtx.ctx.globalCompositeOperation ="destination-out";
}
fillShape(workCtx.ctx);
ctx.drawImage(workCtx, 0, 0);
}
// clear in case of resize
ctx.globalCompositeOperation ="source-over";
ctx.clearRect(0,0,canvas.width,canvas.height);
// create the workspace canvas
var workCtx = createImage(canvas.width, canvas.height);
// draw the outer stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "black", "out");
// draw the inner stroke
drawInOutStroke((canvas.width + canvas.height) / 45, "red", "in");
// draw the shape outline just to highlight the effect
ctx.strokeStyle = "white";
ctx.lineJoin = "round";
ctx.lineWidth = (canvas.width + canvas.height) / 140;
strokeShape(ctx);
};
// run the demo
demo();
// incase fullscreen redraw it all
window.addEventListener("resize",demo)
I`m new with canvas so thanks for your patience.
I wrote an engine that is creating 2 different layers in 2 canvas elements which are one over another. They contain some generated pictures, which aren`t important here.
I'm trying to create an effect which will display bottom layer when I move mouse over the top layer and click.
Something like this:
This is what I have tried so far:
To use transparency on canvas element and display bottom canvas (fast but not usable)
Re-create a clipping region.
Whenever I press the mouse I store current coordinates and re-render the canvas with updated clipping region
Updating clipping region is slow if I use stroke to create shadows + I`m not sure how to remove lines from it (see picture).
If I remove shadow effect, it works really fast, but I need to have it.
The only thing that comes on my mind how to speed this, is to save coordinates of every click, and then to re-calculate that into 1 shape and drop a shadow on it - I`ll still have lines, but it will be faster because there won`t be thousand of circles to draw...
Any help will be most appreciated!
You can take advantage of the browser's built in interpolation by using it as a pseudo low-pass filter, but first by painting it black:
Copy the top layer to the bottom layer
Set source-in comp. mode
Draw all black
Set source-in comp. mode
Scale down image to 25%
Scale the 25% region back up to 50% of original (or double of current)
Scale the now 50% region back up to 100% of original. It will be blurred.
Depending on how much blur you want you can add additional steps. That being said: blurred shadow is an intensive operation no matter how it is twisted and turned. One can make compromise to only render the shadow on mouse up for example (as in the demo below).
Example
Example using two layers. Top layer let you draw anything, bottom will show shadow version at the bottom later while drawing.
var ctx = document.getElementById("top").getContext("2d"),
bctx = document.getElementById("bottom").getContext("2d"),
bg = new Image(),
isDown = false;
bg.src = "http://i.imgur.com/R2naCpK.png";
ctx.fillStyle = "#27f";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.globalCompositeOperation = "destination-out"; // "eraser"
ctx.canvas.onmousedown = function(e) {isDown = true};
window.onmousemove = function(e) {
if (!isDown) return;
var pos = getPos(ctx.canvas, e);
ctx.beginPath();
ctx.moveTo(pos.x + 10, pos.y);
ctx.arc(pos.x, pos.y, 10, 0, 2*Math.PI); // erase while drawing
ctx.fill();
};
window.onmouseup = function(e) {
if (isDown) {
isDown = false;
makeShadow();
}
};
function makeShadow(){
var w = bctx.canvas.width,
h = bctx.canvas.height,
offset = 7,
alpha = 0.75;
// reset alpha
bctx.globalAlpha = 1;
// normal comp mode to clear as it is faster than using "copy"
bctx.globalCompositeOperation = "source-over";
bctx.clearRect(0, 0, w, h);
// copy top-layer to bottom-layer
bctx.drawImage(ctx.canvas, 0, 0);
// comp. mode will only draw in to non-alpha pixels next
bctx.globalCompositeOperation = "source-in";
// black overlay
bctx.fillRect(0, 0, w, h);
// copy mode so we don't need an extra canvas
bctx.globalCompositeOperation = "copy";
// step 1: reduce to 50% (quality related - create more steps to increase blur/quality)
bctx.drawImage(bctx.canvas, 0, 0, w, h, 0, 0, w * 0.5, h * 0.5);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, 0, 0, w * 0.25, h * 0.25);
bctx.drawImage(bctx.canvas, 0, 0, w * 0.25, h * 0.25, 0, 0, w * 0.5, h * 0.5);
// shadow transparency
bctx.globalAlpha = alpha;
// step 2: draw back up to 100%, draw offset
bctx.drawImage(bctx.canvas, 0, 0, w * 0.5, h * 0.5, offset, offset, w, h);
// comp in background image
bctx.globalCompositeOperation = "destination-over";
bctx.drawImage(bg, 0, 0, w, h);
}
function getPos(canvas, e) {
var r = canvas.getBoundingClientRect();
return {x: e.clientX - r.left, y: e.clientY - r.top};
}
div {position:relative;border:1px solid #000;width:500px;height:500px}
canvas {position:absolute;left:0;top:0}
#bottom {background:#eee}
<div>
<canvas id="bottom" width=500 height=500></canvas>
<canvas id="top" width=500 height=500></canvas>
</div>