Use globalCompositeOperation with elements outside of canvas element - javascript

I'm trying to use globalCompositeOperation on an object within a <canvas> element but my goal is to blend with an object outside of the canvas - a plain html markup element like a paragraph.
My end goal will be inverting the content on the page using difference like so
My existing code is below. Is this even possible?
var canvas = document.getElementById('canvas');
window.onresize=function(){
"use strict";
var winMin = Math.min(window.innerWidth,window.innerHeight);
canvas.width = winMin;
canvas.height = winMin;
var w = winMin / 3;
var ctx = canvas.getContext('2d');
ctx.globalCompositeOperation = 'multiply';
ctx.globalAlpha = .5;
//magenta
ctx.fillStyle = 'rgb(255,0,255)';
ctx.beginPath();
ctx.arc(w, w, w, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
//cyan
ctx.fillStyle = 'rgb(0,255,255)';
ctx.beginPath();
ctx.arc(w*2, w, w, 0, Math.PI*2, true);
ctx.closePath();
ctx.fill();
};
window.onresize();
Codepen: http://codepen.io/jeremypbeasley/pen/NqwGoO

The globalCompositeOperation blending operations define how pixels backed by the canvas element blend with fragments to be written to that backing. That has nothing to do with pixels that live in some other dimension of the web page, like the DOM. Total rasterization of the canvas occurs and some other graphics system composites the pixels of the canvas onto the pixels of the rest of the web page. Reflow of the web page could happen at any time, but that does not mean that the canvas would be re-rasterized, just re-composited, in which case the globalCompositeOperations would have no effect and you wouldn't see the photo negative effect you desire.

Related

Clearing canvas "layers" separately?

I've been battling with <canvas> for a while. I want to create an animation/game with lots of different units on different layers.
Due to <canvas> limitation to just one context my approach is as follows:
have one <canvas> on the page,
create multiple "layers" using document.createElement("canvas"),
animate/rerender "layers" separately.
But this approach does not seem to work properly due to one quirk - in order to stack "layers" on my main canvas I'm doing realCanvas.drawImage(layerCanvas, 0, 0);. Otherwise the layers are not being rendered.
The issue here is ultimately it does not change a thing as everything is in being drawn on my main <canvas> and when I do clearRect on one of my layers it does nothing as the pixels are also drawn on the main canvas in addition to given layer. If I run clearRect on main canvas then the layers are useless as every layer is on main canvas so I'm back to starting point as I'm clearing the whole canvas and layers are not separated at all.
Is there a way to fix it easily? I feel like I'm missing something obvious.
Here's an example, how do I clear blue ball trail without touching background rectangles here? There should be only one blue ball under your cursor. Note it's a very simplified example, I'll have multiple blue balls and multiple other layers. I just want to know how the heck do I clear only one layer in canvas. Note I don't want to use multiple <canvas> elements and don't want to use any libs/engines as I'm trying to learn canvas by this. I know many apps use just one canvas html element, many layers and somehow animate them separately.
Source: https://jsfiddle.net/rpmf4tsb/
Try adding canvas2ctx.clearRect(0,0, canvas.width, canvas.height); under ctx.clearRect(0,0, canvas.width, canvas.height); and it works as supposed but all the layers are being cleared, not only the one with the ball...
If you look at things from a performance point-of-view, things are better if you use a single visible <canvas> element for your visual output.
Nothing is stopping you from doing things on seperate canvases you stack on top of each other though. Maybe there's just a basic misunderstanding here.
You say:
and when I do clearRect on one of my layers it does nothing as the
pixels are also drawn on the main canvas in addition to given layer
Well that's not true. If you draw the contents of a freshly cleared canvas onto another canvas it won't overwrite the target canvas with 'nothing'.
Take a look at this example:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Our main canvas contains a green background with a black circle and we're utilizing the drawImage() method to draw a dynamically created, freshly cleared canvas onto, which results in a green background with a black circle as the new canvas element did not contain any data to draw. It did not erase the main canvas.
If we change the example a bit, so the second canvas contains a rectangle things will work as expected:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
ctx.fillStyle = "green";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.lineWidth = 10;
ctx.arc(canvas.width / 2, canvas.height / 2, 50, 0, 2 * Math.PI);
ctx.stroke();
let tempCanvas = document.createElement("canvas");
let tempContext = tempCanvas.getContext("2d");
tempContext.clearRect(0, 0, tempCanvas.width, tempCanvas.height);
tempContext.strokeRect(tempCanvas.width / 2 - 60, tempCanvas.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvas, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
Now if we assume the green background with the circle (tempCanvasA) and the rectangle (tempCanvasB) are two separate canvases we ultimately want to draw to a main canvas it will bring up an important point: the order of drawing.
So this will work:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
while this fails:
let canvas = document.getElementById("canvas")
let ctx = canvas.getContext("2d");
let tempCanvasA = document.createElement("canvas");
let tempContextA = tempCanvasA.getContext("2d");
tempContextA.fillStyle = "green";
tempContextA.fillRect(0, 0, tempCanvasA.width, tempCanvasA.height);
tempContextA.beginPath();
tempContextA.lineWidth = 10;
tempContextA.arc(tempCanvasA.width / 2, tempCanvasA.height / 2, 50, 0, 2 * Math.PI);
tempContextA.stroke();
let tempCanvasB = document.createElement("canvas");
let tempContextB = tempCanvasB.getContext("2d");
tempContextB.strokeRect(tempCanvasB.width / 2 - 60, tempCanvasB.height / 2 - 60, 120, 120);
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
<canvas id="canvas"></canvas>
The rectangle is missing! Why does it fail? Because we changed the order we draw the canvases onto the main canvas. In the latter example:
ctx.drawImage(tempCanvasB, 0, 0, canvas.width, canvas.height);
ctx.drawImage(tempCanvasA, 0, 0, canvas.width, canvas.height);
We first draw tempCanvasB which contains a transparent background & the rectangle and afterwards tempCanvasA with the solid green background - which covers the entire canvas - and the circle. As there are no transparent pixels it will overwrite the rectangle which we've drawn first.
To get to your example with the ball. The problem is that you're drawing the ball to the wrong canvas. Inside your draw function you're doing this:
ctx.clearRect(0, 0, canvas.width, canvas.height);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
ctx.drawImage(canvas2, 0, 0);
So first you clear ctx, afterwards call ball's draw method which draws onto canvas2ctx and finally drawImage onto ctx with the contents of canvas2ctx.
Instead draw the ball onto the main ctx after using drawImage()
e.g.
// helper functions
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1) + min)
}
// canvas
let firstRender = true;
var canvas = document.getElementById('canvas');
canvas.width = window.innerWidth - 50;
canvas.height = window.innerHeight - 50;
let ctx = canvas.getContext('2d');
// virtual canvas for rectangles layer
let canvas2 = document.createElement("canvas");
canvas2.width = window.innerWidth - 50;
canvas2.height = window.innerHeight - 5;
let canvas2ctx = canvas2.getContext("2d");
let ball = {
x: 100,
y: 100,
vx: 5,
vy: 2,
radius: 25,
color: 'blue',
draw: function() {
ctx.beginPath();
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2, true);
ctx.closePath();
ctx.fillStyle = this.color;
ctx.fill();
}
};
function draw(e) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.drawImage(canvas2, 0, 0);
ball.draw();
ball.x = e.clientX;
ball.y = e.clientY;
if (firstRender) {
drawRandomRectangles()
firstRender = false;
}
}
function drawRandomRectangles() {
for (i = 0; i < 50; i++) {
canvas2ctx.beginPath();
canvas2ctx.rect(randomInt(0, window.innerWidth - 50), randomInt(0, window.innerWidth - 50), randomInt(5, 20), randomInt(5, 20));
canvas2ctx.stroke();
}
}
canvas.addEventListener('mousemove', function(e) {
draw(e);
});
ball.draw();
<canvas id="canvas"></canvas>
Thinking about your approach of multiple canvas stacking above each other sounds like an interesting approach to get things done. I would not recommend doing this in that way and therefore handle multiple layers through JavaScript and then still render every time everything new. Especially if you will use animations, then I believe that multiple not synchronized canvases will give you another sort of headache.
Then you would do the following:
Clear your canvas with clearRect.
Draw in an iteration each layer above each other
I hope this theoretical explanation helps.
Now to your code: At the end of the day your ctx and canvas2ctx are in the very same context, because they are from the same canvas. That makes anyway not much sense.

How to Style Images in a canvas?

I'm using node-canvas and I was wonder how style an imported image in canvas similar to how you would an image in CSS.
For example, how would I crop a square image in canvas to a circle. In CSS, all you need to do is set border radius to 50%.
Well obviously you cannot use CSS in this case since CSS is applied to the DOM and not the the pixel based content of a Canvas element.
However the Canvas element has its own set of draw functions which allow to you replicate or at least approximate CSS rules.
Since you mentioned cropping an image to a circle I'll focus on this example. To achieve this effect you want to specify a clipping region before drawing the image. Every pixel outside of the clipped region will not be drawn. Effectively this will crop the image to the clipped region.
In code:
// Retrieve canvas and get context
var canvas = document.getElementById("canvas");
var context = canvas.getContext("2d");
// Save the context so we can undo the clipping region at a later time
context.save();
// Define the clipping region as an 360 degrees arc at point x and y
context.beginPath();
context.arc(x, y, radius, 0, 2 * Math.PI, false);
// Clip!
context.clip();
// Draw the image at imageX, imageY.
context.drawImage(image, imageX, imageY);
// Restore context to undo the clipping
context.restore();
I'd advice taking a look at this page to give you an idea of what you can do with the Canvas element and the 2D rendering context.
I don't know if this would work in node, However you can do this with canvas;
The simplest way of doing it is using, as you intended, border-radius:
canvas{border-radius:50%;}
An other way of doing it is by using the ctx.clip() method.
let canvas = document.querySelector("canvas");
let ctx = canvas.getContext("2d");
ctx.beginPath();
ctx.arc(125,120,100,0,2*Math.PI);
// you clip the context
ctx.clip();
let img = document.querySelector("#testImg");
ctx.drawImage(img, 0, 20);
<canvas width="250" height="240" >
<img id="testImg" src="theImage.jpg">
</canvas>
Yet an other way of doing it is by using ctx.globalCompositeOperation = "destination-atop"in this way:
const canvas = document.getElementById("canvas");
const ctx = canvas.getContext("2d");
let cw = canvas.width = 400,
cx = cw / 2;
let ch = canvas.height = 400,
cy = ch / 2;
ctx.globalCompositeOperation = "destination-atop";
let img = document.querySelector("#testImg");
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.fillStyle = "#f00";
ctx.arc(cx, cx, 100, 0, 2 * Math.PI);
ctx.fill();

Why is the HTML5 canvas not clearing in my code?

I am just getting started with Canvas programming and trying to build a small game. Below is a sample code that I am trying out. My intention is to:
Create a canvas.
Fill it with some background color.
Draw a circle.
Clear the canvas.
Draw another circle in different location.
Here's the code:
var canvas = document.createElement('canvas');
canvas.width= 400;
canvas.height = 400;
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
// 2. Fill background
ctx.fillStyle = 'rgb(30,0,0)';
ctx.fillRect(0,0,400,400);
// 3. Draw circle
ctx.save();
ctx.fillStyle = 'rgba(256,30,30,.8)';
ctx.arc(50,50, 20, 0, Math.PI*2, true);
ctx.fill();
ctx.restore();
// 4. Clear Canvas
ctx.save();
ctx.setTransform(1, 0, 0, 1, 0, 0);
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.restore();
// 5. Draw another circle
ctx.save();
ctx.fillStyle = 'rgba(256,30,30,.8)';
ctx.arc(150,150, 20, 0, Math.PI*2, true);
ctx.fill();
ctx.restore();
But as you can see, only the background color gets cleared and the first circle remains as it is.
Why is the above code fails to clear the canvas completely before drawing second circle?
If you don't use beginPath before starting a new path, all draw command keeps stacking in the current path.
What's happening here is that when you fill() the second time, the first circle is still in the current path, so even if the screen was in deed cleared, there are two circles drawn with this single fill() command.
==>> use beginPath() before starting a new path.

Canvas clipping with "feather" edges effect

I'm currently drawing an image to an HTML5 Canvas and masking it with an arc, calling clip() before I draw the image so that only the portion that's in the arc is shown. How can I feather the edges of this arc? I know from googling around that there is no simple way to simply apply a "feather" to a shape drawn with canvas. What abut going in on the pixel data for the image where its edges touch the arc? Thanks for any help.
Here is the relevant portion of my code:
ctx.arc(canvas.width/2, canvas.height/2, 250, 0, 6.28, false);//draw the circle
ctx.restore();
ctx.save();
ctx.drawImage(background, 0, 0,
background.width * scale, background.height * scale);
ctx.clip();//call the clip method so the next render is clipped in last path
ctx.drawImage(img, 0, 0,
img.width * scale, img.height * scale);
ctx.closePath();
ctx.restore();
UPDATE
Thanks for the thorough answer and very helpful code/comments Ken!! I spent a few hours last night trying to work this solution in my particular use case and I'm having trouble. It seems that if I clip an image with the second-canvas technique you describe I can't redraw it on transforms the same way that I can with an arc() and clip() routine. Here's a JS Fiddle of what I'm trying to accomplis, minus the feathering on the arc, notice the click and drag events on the two layered images.
http://jsfiddle.net/g3WkN/
I tried replacing the arc() with your method, but I'm having a hard time getting that to be responsive to the transforms that happen on mouse events.
Update 2017/7
Since this answer was given there are now a new option available in newer browsers, the filter property on the context. Just note that not all browsers currently supports it.
For browsers which do we can cut down the code as well as remove temporary canvas like this:
var ctx = demo.getContext('2d');
ctx.fillStyle = '#f90';
ctx.fillRect(0, 0, demo.width, demo.height);
clipArc(ctx, 200, 200, 150, 40);
function clipArc(ctx, x, y, r, f) {
ctx.globalCompositeOperation = 'destination-out';
ctx.filter = "blur(25px)"; // "feather"
ctx.beginPath();
ctx.arc(x, y, r, 0, 2 * Math.PI);
ctx.fill();
// reset comp. mode and filter
ctx.globalCompositeOperation = 'destination-out';
ctx.filter = "none";
}
body {background:#07c}
<canvas id="demo" width=400 height=400></canvas>
Old answer
Technique
You can achieve this by combining the following steps:
Use off-screen canvas
Use the shadow feature (the secret ingredient)
Use composite modes
The concept is based on having the browser make the feather internally by utilizing the blurred shadow. This is much faster than blurring in JavaScript. As we can make shadow for any object you can make complex feathered masks.
The off-screen canvas is used to draw the shadow only. We achieve this by moving the actual shape outside the canvas and then offset the shadow accordingly. The result is that shadow is drawn on the off-screen canvas while the actual shape is "invisible".
Now that we have a feathered version of our shape we can use that as a mask for composite mode. We choose destination-out to cleat where the shadow is drawn, or destination-in to invert the mask.
Example
Lets create a wrapper function that do all the steps for us
ONLINE DEMO HERE
function clipArc(ctx, x, y, r, f) { /// context, x, y, radius, feather size
/// create off-screen temporary canvas where we draw in the shadow
var temp = document.createElement('canvas'),
tx = temp.getContext('2d');
temp.width = ctx.canvas.width;
temp.height = ctx.canvas.height;
/// offset the context so shape itself is drawn outside canvas
tx.translate(-temp.width, 0);
/// offset the shadow to compensate, draws shadow only on canvas
tx.shadowOffsetX = temp.width;
tx.shadowOffsetY = 0;
/// black so alpha gets solid
tx.shadowColor = '#000';
/// "feather"
tx.shadowBlur = f;
/// draw the arc, only the shadow will be inside the context
tx.beginPath();
tx.arc(x, y, r, 0, 2 * Math.PI);
tx.closePath();
tx.fill();
/// now punch a hole in main canvas with the blurred shadow
ctx.save();
ctx.globalCompositeOperation = 'destination-out';
ctx.drawImage(temp, 0, 0);
ctx.restore();
}
That's all there is to it.
USAGE
clipArc(context, centerX, centerY, radius, featherSize);
With demo background (see fiddle):
ctx.fillStyle = '#ffa';
ctx.fillRect(0, 0, demo.width, demo.height);
clipArc(ctx, 200, 200, 150, 40);
Result:
If you want to keep center intact just replace composite mode with destination-in.
Demo for inverted feathered mask

HTML5 Canvas - How to fill() a specific context

I'm trying to make a website where an image is drawn on Canvas, then later the user is able to press a button to ctx.fill() certain parts of it with color. I'm running into issues where I can only ctx.fill() the most recently created shape which often isn't the shape I want.
Here's an example. In this code (live at http://build.rivingtondesignhouse.com/piol/test/) I'm trying to draw the first rectangle, then save() it on the stack, then draw the second rectangle (and don't save it), then when my fill() function is called I want to restore() the first rectangle and ctx.fill() it with a different pattern. It doesn't work!
In practice, I'm actually trying to fill the gray part of this complex shape with any color the user chooses AFTER the image has been drawn, but I think the technique is the same. (http://build.rivingtondesignhouse.com/piol/test/justTop.html)
Thanks in advance for any help!!!
Here's the code:
<script type="text/javascript">
var canvas;
var ctx;
function init() {
canvas = document.getElementById("canvas");
ctx = canvas.getContext("2d");
draw();
}
function draw() {
ctx.fillStyle = '#FA6900';
ctx.shadowOffsetX = 5;
ctx.shadowOffsetY = 5;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(0,0,15,150);
ctx.save();
ctx.fillStyle = '#E0E4CD';
ctx.shadowOffsetX = 10;
ctx.shadowOffsetY = 10;
ctx.shadowBlur = 4;
ctx.shadowColor = 'rgba(204, 204, 204, 0.5)';
ctx.fillRect(30,0,30,150);
}
function fill(){
var image = new Image();
image.src = "http://www.html5canvastutorials.com/demos/assets/wood-pattern.png";
image.onload = drawPattern;
function drawPattern() {
ctx.restore();
ctx.fillStyle = ctx.createPattern(image, "repeat");
ctx.fill();
}
}
init();
There are a few misunderstands that we need to clear up before I can answer the question.
save() and restore() do not save and restore the canvas bitmap. Instead they save and restore all properties that are set on the canvas context and that's all!
For example
ctx.fillStyle = 'red';
ctx.save(); // save the fact that the fillstyle is red
ctx.fillStyle = 'blue'; // change the fillstyle
ctx.fillRect(0,0,5,5); // draws a blue rectangle
ctx.restore(); // restores the old context state, so the fillStyle is back to red
ctx.fillRect(10,0,5,5); // draws a red rectangle // draws a red rectangle
See that code live here.
So you aren't saving a rectangle (or anything drawn) by calling save(). The only way you you can save the bitmap is by drawing it (or part of it) to another canvas (using anotherCanvasContext.drawImage(canvasIWantToSave, 0, 0)) or by saving enough information that you can redraw the entire scene with the appropriate changes.
Here is an example of one way you could re-structure your code so that it does what you want: http://jsfiddle.net/xwqXb/

Categories