Canvas shape inner shadow + blending mode - javascript

so this has been driving me crazy for the past few days.
I'm trying to replicate Photoshop's inner shadow in JS with different blending modes like overlay.
There are easy ways to add shadows to shapes like ctx.shadowBlur or ctx.filter = 'drop-shadow(...)', but these only generate outer shadows. You can create inner shadow with some composition magic with xor, but this leaves the image with not smooth edges (I guess xor doesn't handle anti-alias really well), like in this example:
https://jsfiddle.net/89pes8ap/1/
So, I had another idea that kind of worked, because it used xor only once:
https://jsfiddle.net/3cnwtvyj/
But as you can see the overlay-ed version still doesn't have smooth edges.
So, my question is this: how can you add smooth inner shadow with different blending modes that could work with all kinds of shapes?

Composite Operations do work on the alpha channel, and hence don't go well with shadows.
Unfortunately, I think there is not much you can do to circumvent this for your shadow over transparent background, moreover for a circle.
One little thing I can see though is that you actually don't need the xor part, and can replace it with a path composed of a rect and an arc that you'd fill as 'evenodd'. This will create the hole directly, reducing a little bit antialiasing artifacts.
drawOP();
drawEvenOdd();
function drawEvenOdd() {
let canvas = document.getElementById('evenodd');
canvas.width = 150;
canvas.height = 200;
let ctx = canvas.getContext('2d')
ctx.textAlign = 'center';
ctx.shadowColor = 'black';
ctx.shadowBlur = 10 * 2;
ctx.shadowOffsetY = 5;
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.arc(100, 100, 50, 0, 2 * Math.PI);
// will draw an hole
ctx.fill('evenodd');
// remove shadow
ctx.shadowColor = 'transparent';
ctx.shadowBlur = ctx.shadowOffsetY = 0;
ctx.globalCompositeOperation = 'destination-out';
ctx.fill('evenodd');
ctx.globalCompositeOperation = 'source-over';
ctx.fillText('evenodd', 100, 180)
}
function drawOP() {
let canvas = document.getElementById('OP')
canvas.width = 150;
canvas.height = 200;
let ctx = canvas.getContext('2d')
ctx.textAlign = 'center';
ctx.fillStyle = 'black'
ctx.fillRect(0, 0, canvas.width, canvas.height)
ctx.globalCompositeOperation = 'xor'
ctx.arc(100, 100, 50, 0, 2 * Math.PI)
ctx.fill()
ctx.filter = 'drop-shadow(0 5px 10px black)'
ctx.drawImage(canvas, 0, 0)
ctx.filter = 'none';
ctx.fillText('OP', 100, 180)
}
<canvas id="OP"></canvas>
<canvas id="evenodd"></canvas>

Related

Filling in two colors while using the rect() method

I was trying to make two different shapes that are different colors but it isn't working. Both of the shapes are the same colors. Please help!(Please note that I am not the best coder in the world)
I've looked for other examples on this website, but all of them use the lineTo() method and I would like to use the rect() method just to make things easier.
//make canvas and set it up
var canvas = document.createElement('canvas');
document.body.appendChild(canvas);
var ctx = canvas.getContext('2d');
canvas.height = window.innerHeight;
canvas.width = window.innerWidth;
canvas.style.position = 'absolute';
canvas.style.left = '0px';
canvas.style.top = '0px';
canvas.style.backgroundColor = '#D0C6C6';
var cH = canvas.height;
var cW = canvas.width;
//draw paddles
//variables
var paddleLength = 120;
var redPaddleY = window.innerHeight / 2;
var bluePaddleY = window.innerHeight / 2;
var paddleWidth = 20;
//drawing starts
function drawPaddles() {
//RED PADDLE
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
//BLUE PADDLE
var bluePaddle = function(color) {
ctx.fillStyle = color;
ctx.clearRect(0, 0, cW, cH);
ctx.rect(cH / 12 * 14, bluePaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
redPaddle('red');
bluePaddle('blue');
};
var interval = setInterval(drawPaddles, 25);
Whenever you add a shape to the canvas it becomes part of the current path. The current path remains open until you tell the canvas to start a new one with beginPath(). This means that when you add your second rect() it is combined with the first and filled with the same colour.
The simplest fix would be to use the fillRect() function instead of rect which begins, closes and fills a path in one call.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.fillRect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
};
If you still want to use rect() you should tell the canvas to begin a new path for each paddle.
var redPaddle = function(color) {
ctx.fillStyle = color;
ctx.beginPath();
ctx.rect(cH / 12, redPaddleY - paddleLength / 2, paddleWidth, paddleLength);
ctx.fill();
};
I would also suggest moving the clearRect() outside of the drawing functions too. Clear once per frame and draw both paddles.
...
ctx.clearRect(0, 0, cW, cH);
redPaddle();
bluePaddle();
...
You should also investigate requestAnimationFrame() to do your animation loop as it provides many performance improvements over intervals.

Canvas polygons not touching properly

So I've been fiddling with the canvas element, and I seem to have run into a situation that is highly irritating, yet I haven't been able to find a solution.
Say that two polygons are drawn on a canvas, and that they should be touching each other. Where one polygon is drawn like this:
ctx.beginPath();
ctx.moveTo(oX,oY);
ctx.lineTo(oX=oX+k,oY=oY-h);
ctx.lineTo(oX=oX+k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY-h);
ctx.fill();
A simple version is implemented in this fiddle.
As you can probably see there is a thin line between these shapes. How can I avoid it? I have tried the solutions here, but they don't really seem to refer to this case, because I'm dealing with diagonal lines.
One solution
You could always use the stroke-line trick, but depending on your goal:
If it is to show many polygons next to each other, you could look at the polygons as simple squares.
Draw them in as such in an off-screen canvas next to each other. This will produce a result with no gaps.
Then transform the main canvas into the position you want those polygons to appear. Add rotation and/or skew depending on goal.
Finally, draw the off-screen canvas onto the main canvas as an image. Problem gone.
This will give you an accurate result with no extra steps in stroking, and the calculations for the boxes becomes very simple and fast to do (think 2d grid).
You have to use an off-screen canvas though. If you transform main canvas and draw in the shapes you will encounter the same problem as already present. This is because each point is transformed and if there is need for interpolation it will be calculated for each path shape separately. Drawing in an image will add interpolation on the whole surface, and only where there are gaps (non-opaque alpha). As we already are "gap-free" this is no longer a problem.
This will require an extra step in planning to place them correctly, but this is a simple step.
Example
Step 1 - draw boxes into an off-screen canvas:
This code draws on the off-screen canvas resulting in two boxes with no gap:
(the example uses an on-screen to show result, see next step for usage of off-screen canvas)
var ctx = document.querySelector("canvas").getContext("2d");
ctx.fillStyle = "red";
ctx.fillRect(10, 10, 50, 50);
ctx.fillRect(60, 10, 50, 50);
<canvas/>
Step 2 - transform main canvas and draw in off-screen canvas
When drawn into main canvas with transformation set, the result will be (pseudo-random transformation just to show):
var ctx = document.querySelector("canvas").getContext("2d");
// off-screen canvas
var octx = document.createElement("canvas").getContext("2d");
octx.fillStyle = "red";
octx.fillRect(10, 10, 50, 50);
octx.fillRect(60, 10, 50, 50);
// transform and draw to main
ctx.translate(80, 0);
ctx.rotate(0.5, Math.PI);
ctx.transform(1, 0, Math.tan(-0.5),1, 0,0); // skew
ctx.drawImage(octx.canvas, 0, 0);
<canvas />
Step 3 (optional) - Interaction
If you want to interact with the boxes you simply apply the same transform, then add path for a box and hit-test it against the mouse position. Redraw a single state, erase by clearing and draw back the off-screen canvas on top:
var ctx = document.querySelector("canvas").getContext("2d");
// off-screen canvas
var octx = document.createElement("canvas").getContext("2d");
octx.fillStyle = "red";
octx.fillRect(10, 10, 50, 50);
octx.fillRect(60, 10, 50, 50);
// allow us to reuse some of the steps:
function getTransforms() {
ctx.setTransform(1,0,0,1,0,0);
ctx.translate(80, 0);
ctx.rotate(0.5, Math.PI);
ctx.transform(1, 0, Math.tan(-0.5),1, 0,0); // skew
}
function clear() {
ctx.setTransform(1,0,0,1,0,0);
ctx.clearRect(0,0,300,150);
}
function redraw() {
ctx.drawImage(octx.canvas, 0, 0);
}
getTransforms();
redraw();
ctx.canvas.onmousemove = function(e) {
var r = this.getBoundingClientRect(),
x = e.clientX - r.left, y = e.clientY - r.top;
// box 1 (for many, use array)
ctx.beginPath();
ctx.rect(10, 10, 50, 50);
clear(); // these can be optimized to use state-flags
getTransforms(); // so they aren't redraw for every move...
redraw();
// just one box check here
if (ctx.isPointInPath(x, y)) {
ctx.fill();
}
};
<canvas />
Yes, it's annoying when filled polygons result in that tiny gap. It's especially common on diagonals that should theoretically meet.
A common workaround is to put a half-pixel, same-colored stroke around the polygons:
//Some basic setup ...
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var oX = 50;
var oY = 50;
var h = 33;
var k = 50;
ctx.fillStyle = 'red';
ctx.strokeStyle='red';
ctx.lineWidth=0.50;
//Draw one polygon
ctx.beginPath();
ctx.moveTo(oX,oY);
ctx.lineTo(oX=oX+k,oY=oY-h);
ctx.lineTo(oX=oX+k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY-h);
ctx.fill();
ctx.stroke();
//Draw another polygon
oX = oX+k;
oY = oY+h;
ctx.beginPath();
ctx.moveTo(oX,oY);
ctx.lineTo(oX=oX+k,oY=oY-h);
ctx.lineTo(oX=oX+k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY-h);
ctx.fill();
ctx.stroke();
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
//Some basic setup ...
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
var oX = 50;
var oY = 50;
var h = 33;
var k = 50;
ctx.fillStyle = 'red';
ctx.strokeStyle='red';
ctx.lineWidth=0.50;
//Draw one polygon
ctx.beginPath();
ctx.moveTo(oX,oY);
ctx.lineTo(oX=oX+k,oY=oY-h);
ctx.lineTo(oX=oX+k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY-h);
ctx.fill();
ctx.stroke();
//Draw another polygon
oX = oX+k;
oY = oY+h;
ctx.beginPath();
ctx.moveTo(oX,oY);
ctx.lineTo(oX=oX+k,oY=oY-h);
ctx.lineTo(oX=oX+k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY+h);
ctx.lineTo(oX=oX-k,oY=oY-h);
ctx.fill();
ctx.stroke();
#canvas{border:1px solid red;}
<canvas id="canvas" width=300 height=300></canvas>

How to multiply colors of simple shapes in an animated canvas

My canvas is used for drawing and animating some lines in different colors, which could look like this: http://jsfiddle.net/swknhg3t/
HTML:
<canvas id="myCanvas" width="400px" height="400px"></canvas>
JavaScript:
var elem = document.getElementById('myCanvas');
var c = elem.getContext('2d');
var count = 0;
function draw() {
c.save();
c.clearRect(0, 0, 400, 400);
// red
c.beginPath();
c.moveTo(20, 50);
c.lineCap = "round";
c.lineWidth = 20;
c.lineTo(380, 50);
c.strokeStyle = "#f00";
c.stroke();
// green
c.beginPath();
c.moveTo(20, 350);
c.lineCap = "round";
c.lineWidth = 20;
c.lineTo(380, 350);
c.strokeStyle = "#0f0";
c.stroke();
// blue
c.translate(200, 200);
c.rotate(count * (Math.PI / 180));
c.beginPath();
c.moveTo(0, 0);
c.lineCap = "round";
c.lineWidth = 20;
c.lineTo(180, 0);
c.strokeStyle = "#00f";
c.stroke();
c.restore();
count++;
}
setInterval(draw, 20);
When those lines intersect, I want their colors being multiplied (like the Photoshop filter).
I found a nice explanation on how to do this pixelwise, but since my canvas will hold an animation I wonder if there is a better option than iterating over every pixel and calculating the color value manually in every loop.
You shouldn't need to do it on an entire canvas per-pixel basis.
If you're only using modern browsers, you could do...
c.globalCompositeOperation = "multiply";
jsFiddle.
Otherwise, you could...
Figure out the line intersections
Use those offset to grab getImageData() (depending on grabbing smaller portions and modifying is more performant than grabbing it all and then using the offsets)
Modify the surrounding portion related to the thickness of your lines. If the colour is transparent, simply bail early when iterating.
Use putImageData() to render it back.

<canvas> clearRect() in a letter shape

I've recently started learning web techniques, so I'm still a newbie regarding pretty much everything. I've stumbled upon this portfolio site, link, which I'm trying to "recreate" as part of my learning process/practice.
Here I'm interested in the background of the page, and how to make that transparent letter on canvas. In my current work, I have a html background image set, fillRect() on a full screen with opacity of 0.9 , and now I don't know how to make use of clearRect().
The question is: Am I on the right track, and is there any way that I'm not aware of, in which I can use clearRect() to clear pixels on canvas in a letter shape? Like, without manually defining a path for clearRect(), but where I would only input a letter and clear pixels in its shape. Sorry if I posted my current code below the wrong way, it's my first time posting here.
var canvas = document.getElementById('layout');
if (canvas.getContext) {
var ctx = canvas.getContext('2d');
}
$(document).ready(function() {
window.addEventListener('resize', setCanvasSize); //false
window.addEventListener('resize', draw); //false
//set canvas size----------------------------------------
setCanvasSize();
function setCanvasSize() {
canvas.width = $(window).width();
canvas.height = $(window).height();
}
//draw---------------------------------------------------
draw();
function draw() {
var x = canvas.width;
var y = canvas.height;
ctx.fillStyle = "white";
ctx.fillRect(0, 0, x, y);
ctx.clearRect(50, 30, 110, 35);
}
});
You can achieve effects such as this using globalCompositeOperation.
Here's an example where some text is used as shape to erase drawn pixels:
var canvas = document.createElement("canvas");
canvas.width = 300;
canvas.height = 200;
var ctx = canvas.getContext("2d");
document.body.appendChild(canvas);
ctx.fillStyle = "rgba(255, 255, 255, 0.8";
ctx.fillRect(0, 0, canvas.width, canvas.height);
ctx.globalCompositeOperation = "destination-out";
ctx.font = "70pt Arial";
ctx.fillText("Text", 50, 130);
Here's the JSFiddle: http://jsfiddle.net/eSpUv/

Getting single shadow for fill and stroke on HTML canvas

I have to draw a stroked fill on canvas. For this I call ctx.fill and ctx.stroke separately. Due to this, the shadow of the stroke draws on top of the fill which I want to avoid.
Could somebody please tell if there is a way to avoid this?
Here is my code:
ctx1.fillStyle = "blue";
ctx1.strokeStyle = "black";
ctx1.shadowColor = "rgba(0,255,0, 1)";
ctx1.shadowOffsetX = 50;
ctx1.shadowOffsetY = 50;
ctx1.lineWidth = "20";
ctx.beginPath();
ctx.moveTo(300, 100);
ctx.lineTo(400, 100);
ctx.lineTo(400, 200);
ctx.lineTo(300, 200);
ctx.closePath();
ctx1.fill();
ctx1.stroke();
http://jsfiddle.net/abWYZ/3/
Every time you perform a draw action on the context the shadow is also drawn. The way canvas works is every thing that's drawn is placed on top of what was previously there. So whats happening is the fill is performed, making a shadow of it, and then the stroke is drawn, which makes a shadow on top of all previous drawn objects.
Here is one possible solution.
Live Demo
// Grab the Canvas and Drawing Context
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
ctx.save();
ctx.fillStyle = "blue";
ctx.strokeStyle = "black";
ctx.lineWidth="20";
ctx.beginPath();
ctx.moveTo(300, 100);
ctx.lineTo(400, 100);
ctx.lineTo(400, 200);
ctx.lineTo(300, 200);
ctx.closePath();
ctx.shadowColor = "rgba(0,255,0, 1)";
ctx.shadowOffsetX = 50;
ctx.shadowOffsetY = 50;
ctx.stroke();
ctx.fill();
// clear the shadow
ctx.shadowColor = 0;
ctx.shadowOffsetX = 0;
ctx.shadowOffsetY = 0;
// restroke w/o the shadow
ctx.stroke();
If you use an approach like this I suggest making a function called toggleShadow or something along those lines allowing you to control when the shadows are drawn.

Categories

Resources