Related
So at the moment i'm working on a small project that renders an isometric tile space using diamonds that are at a height : width ratio of 1:2.
I plan on making a "shield" effect above them by basically putting more and more blue closer to the edges with the inner circle being transparent to give the illusion of a semi-transparent sphere being above it.
To do this i am using radial gradient.
Currently my canvas when rendered looks like this:
And my code looks like this:
var gradient = ctx.createRadialGradient(450, tileheight * 5 / 2, 100, 450, tileheight * 5 / 2, 200);
gradient.addColorStop(0, "rgba(0, 0, 55, 0.5)");
gradient.addColorStop(1, "rgba(10, 10, 55, 0.8)");
drawdiamond(gradient, 450, 0, tilewidth * 5, tileheight * 5);
Where drawdiamond is a function that draws the tiles, this function will draw a diamond the size of the map with the gradient specified by the variable "gradient".
And tilewidth and tileheight are the width and height at the longest parts of each tile, they are 100 and 50 respectively.
If you look at the image, there is a problem, since i'm drawing my tiles to be wider than they are taller, i need to draw an oval where the height is half the radius of the width in order for the effect to be convincing.
There is ctx.setPattern where i can make an image that has the gradient i want, but if i want to make the map bigger during run time, i would need to create multiple images for each size, which is not ideal.
Is there a way to transform the image gradient so it draws an oval instead of a circle?
(Sorry if this has already been posted).
Matt posted an article that suggested making a shape that is larger than it should be height wise, they use ctx.transform to set the height scaling to 0.5, which will scale the gradient with it as it's part of what's drawn.
I played around with it and came up with this, thanks for the help!
var gradient = ctx.createRadialGradient(450, tilewidth * mapsize / 2, 100, 450, tilewidth * mapsize / 2, 200);
gradient.addColorStop(0, "rgba(0, 0, 55, 0.5)");
gradient.addColorStop(1, "rgba(10, 10, 55, 0.8)");
ctx.setTransform(1, 0, 0, 0.5, 0, 0);
drawdiamond(gradient, 450, 0, tilewidth * mapsize, tilewidth * mapsize);
ctx.setTransform(1, 0, 0, 1, 0, 0);
This function draws the diamond with the shield effect at double it's intended height, then scales it down when it is drawn, thus causing a scaled gradient.
Map size is the maps length in tiles on both length and height sides.
This will work for any size map.
I'm trying to achieve the following:
A number of concentric circles (or rings) are drawn on a canvas. Each circle has a "hole" in it, so the smaller circles, drawn behind it are partially visible. Each frame (we're using window.requestAnimationFrame to render) the radius of each circle/shape/ring is slightly increased.
A scenario with two rings is depicted in the image here.
The code:
function draw() {
drawBgr();
for (var i = 0, len = rings.length; i < len; i++) {
rings[i].draw();
}
}
function drawBgr() {
context.globalCompositeOperation = "source-over";
context.clearRect(0, 0, WIDTH, HEIGHT);
context.rect(0, 0, WIDTH, HEIGHT);
context.fillStyle = '#FFFFFF';
context.fill();
}
function squareRing(ring) { //called by rings[i].draw();
context.globalCompositeOperation = "source-over";
context.fillRect(ring.centerX - ring.radius / 2, ring.centerY - ring.radius / 2, ring.radius, ring.radius);
context.globalCompositeOperation = "source-out";
context.beginPath();
context.arc(CENTER_X, CENTER_Y, ring.radius, 0, 2 * Math.PI, false);
//context.lineWidth = RING_MAX_LINE_WIDTH * (ring.radius / MAX_SIDE);
context.fillStyle = '#000000';
context.fill();
context.globalCompositeOperation = "source-over";
}
What exactly is the problem here? I'm calling clearRect before the circles are drawn. See "What I'm actually getting" image. This is the result of a SINGLE RING being drawn over a number of frames. I shouldn't be getting anything different than a black circle with a hollow square in the middle. (Note that radius is increasing each frame.)
I do realize switching globalCompositeOperation might not suffice for the effect I desire. How can I draw a "hole" in an object drawn on the canvas without erasing everything in the "hole" underneath the object I'm trying to modify?
This is the tutorial I used as a reference for the globalCompositeOperation values.
I'm using Firefox 28.0.
I would not try to use globalCompositeOperation, since i find it hard to figure out what will happen after several iterations, and even harder if the canvas was not cleared before.
I prefer to use clipping, which gets me to that :
http://jsbin.com/guzubeze/1/edit?js,output
So, to build a 'hole' in a draw, how to use clipping ?
-->> Define a positive clipping sub-path, and within this area, cut off a negative part, using this time a clockwise sub-path :
Clipping must be done with one single path, so rect() cannot be used : it does begin a path each time, and does not allow to choose clockwisity (:-)), so you have to define those two functions which will just create the desired sub-paths :
// clockwise sub-path of a rect
function rectPath(x,y,w,h) {
ctx.moveTo(x,y);
ctx.lineTo(x+w,y);
ctx.lineTo(x+w,y+h);
ctx.lineTo(x,y+h);
}
// counter-clockwise sub-path of a rect
function revRectPath(x,y,w,h) {
ctx.moveTo(x,y);
ctx.lineTo(x,y+h);
ctx.lineTo(x+w,y+h);
ctx.lineTo(x+w,y);
}
then you can write your drawing code :
function drawShape(cx, cy, d, scale, rotation) {
ctx.save();
ctx.translate(cx,cy);
scale = scale || 1;
if (scale !=1) ctx.scale(scale, scale);
rotation = rotation || 0;
if (rotation) ctx.rotate(rotation);
// clip with rectangular hole
ctx.beginPath();
var r=d/2;
rectPath(-r,-r, d, d);
revRectPath(-0.25*r,-0.8*r, 0.5*r, 1.6*r);
ctx.closePath();
ctx.clip();
ctx.beginPath();
// we're clipped !
ctx.arc(0,0, r, 0, 2*Math.PI);
ctx.closePath();
ctx.fill();
ctx.restore();
}
Edit :
For the record, there is a simpler way to draw the asked scheme : just draw a circle, then draw counter clockwise a rect within. What you fill will be the part inside the circle that is outside the rect, which is what you want :
function drawTheThing(x,y,r) {
ctx.beginPath();
ctx.arc(x ,y, r, 0, 2*Math.PI);
revRectPath(x-0.25*r, y-0.8*r, 0.5*r, 1.6*r);
ctx.fill();
ctx.closePath();
}
(i do not post image : it is the same).
Depending on your need if you change the draw or if you want to introduce some kind of genericity, use first or second one.
If you do not change the scheme later, the second solution is simpler => better.
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
In attempting to get a drawing of a canvas and redrawing it onto the same canvas later is giving unexpected behavior. See Jsfiddle for example.
The logic is fairly straight forward:
Given an image on the canvas, and translation to the center (I'm using a rectangle to demonstrate).
Clip out a section of the image. I'm simply copying the entire "image" in the example.
Possibly do manipulation.
Paste back to the same canvas after clearing it. The point being to clip a section of the original image at a given point with a width / height, and paste it later with modifications.
var c=document.getElementById("cvs1"),
ctx = c.getContext("2d"),
bufferCvs = document.createElement('canvas'),
bufferCtx = bufferCvs.getContext('2d');
bufferCvs.width = c.width;
bufferCvs.height = c.height;
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx.fillRect( -75, -50 ,150,100);
//Image data experiment
//Set the buffer width / height same size as rectangle
bufferCvs.width = 150;
bufferCvs.height = 100;
//Draw the "image" from the first context to the buffer by clipping the first, and pasting to 0,0 width the same "image" dimensions.
bufferCtx.drawImage( c, -75, -50, 150, 100, 0, 0, 150, 100 );
//clear out old canvas drawing
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
ctx.drawImage( bufferCvs, -75, -50, 150, 100 );
Since I'm keeping the exact same coordinates / dimensions, the expected output would be what was originally on canvas to begin with. However, only part of the upper left corner is drawn (see fiddle). I'm using drawImage for efficiency reasons, but I've used get/putImageData with the same results. Both width and height are defined as mentioned to fix other strange behaviors.
How would you go about making sure everything stored in the buffer canvas is drawn, instead of just the top corner?
Edit:
To help with my question and what behavior I believe is going on I'll post some screens.
Step 1:
Translate context 1 to the center, and draw a rectangle to represent an image.
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx.fillRect( -75, -50 ,150,100);
Step 2:
Use drawImage to "clip" from the -75, -50 point and cut out just the rectangle using the width 150, 100. This should be drawn onto the canvas buffer, but it is not
bufferCvs.width = 150;
bufferCvs.height = 100;
//Draw the "image" from the first context to the buffer by clipping the first at -75, -50 (the start of the image), and pasting to 0,0 width the same "image" dimensions.
bufferCtx.drawImage( c, -75, -50, 150, 100, 0, 0, 150, 100 );
I would expect the buffer canvas to look like this (It is not):
However, if I change the drawImage to
bufferCtx.drawImage( c, 0, 0, 150, 100, 0, 0, 150, 100 );
I get an expected amount of white space on the buffer (and the last drawImage back to the context without issue)
Step 3: Clear out the old "image" from the first context. This shouldn't change the translation since I restore the context state after performing the clear. (This works as expected)
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
Step 4: Simply take what is in the buffer canvas and draw it onto the original context where it started to return to this:
The idea is to more "clip" a region of the original, clear the old image, and paste the new clipped region centered back into the original context. I have looked at MDN's example, but the clipping portion of drawImage isn't performing as I expect.
The main issue is that the first canvas drawn over the bufferCvs is not drawn on the place where you expect. The easiest way to prove this is by adding bufferCvs to the DOM tree: http://jsfiddle.net/CdWn6/4/
I guess this is what you're looking for: http://jsfiddle.net/CdWn6/6/
var c=document.getElementById("cvs1"),
c2 = document.getElementById("cvs2"),
bufferCvs = document.createElement('canvas'),
bufferCtx = bufferCvs.getContext('2d'),
ctx = c.getContext("2d"),
ctx2 = c2.getContext("2d");
bufferCvs.width = c.width;
bufferCvs.height = c.height;
ctx.translate( c.width/2, c.height/2 );
ctx.fillStyle="#FF0000";
ctx2.translate( c.width/2, c.height/2 );
ctx2.fillStyle="#FF0000";
bufferCtx.translate( c.width/2, c.height/2 );
ctx.fillRect( -75, -50 ,150,100);
ctx2.fillRect( -75, -50 ,150,100);
//Image data experiment
bufferCtx.drawImage( c, -135, -110, 270, 220);
ctx.save()
ctx.setTransform(1,0,0,1,0,0,1,0,0);
ctx.clearRect(0, 0, c.width, c.height);
ctx.restore();
//Draw background to demonstrate coordinates still work
ctx.fillStyle="#00FF00";
//ctx.fillRect( -75, -50 ,150,100);
ctx.drawImage( bufferCvs, -75, -50, 150, 100 );
So i'm trying to map color space in HTML and my knowledge is pretty much limited to CSS HTML and Javascript. I am looking for a way to construct a 2 dimensional gradient, with 2 variable along 2 vectors. My research has indicated that CSS and SVG tech only has capacity for single dimension grdaients. Or rather Linear Grads can only have a single vector. So to make up for this limitation I am using JS to iterate over the 256 changes I need so that I can get a gradient on 2 RGB color channels. So picture if you will an x-axis that is relative to for example purposes - Red and Grads from 0 to 255 and y-axis that is likewise relative - Green and Grads from 0 to 255 but with a JS iteration instead of a CSS linear-grad.
What I end up with is a beautiful representation of RGB color space !BUT! changes to the z-axis -blue channel in this example- means that I have to call on a JS function that iterates through 256 loops updating the background of 256 DOM elements with new CSS linear grads.
I am making this web-app because of the limitations that I see in current web-based color pickers a 256 step loop for each change of the Z-axis will place an unacceptable amount of computation overhead into the program.
Any Idea's for a better way to make a dual vector gradient? Perhaps I could make an app specific library for the HTML 5 canvas element??? Where I would be operating on a bitmap instead of DOM elements maybe significantly lower the processor cost-per-call?
You can use the canvas element for that. Here are some examples of colorpickers.
Basically you want to create two linear gradients, one horizontal one vertical, moving from transparent to whatever rgba colors you want. Then draw one gradient over the other on the canvas. There's kind of a catch though, I've found that canvas doesn't make very clean rgba gradients, but you can uses half transparent colors, draw the first one once, the second one twice, then the first one again and it seems to give pretty good results. You can play with it though, here's some code to work off of.
var Draw = function(clr1, clr2){
clr1 = clr1 || 'rgba(255, 0, 0, 0.5)';
clr2 = clr2 || 'rgba(0, 0, 255, 0.5)';
var bg1 = document.getElementById('canvas').getContext('2d'),
grad1 = bg1.createLinearGradient(0, 128, 256, 128),
grad2 = bg1.createLinearGradient(128, 0, 128, 256);
grad1.addColorStop(0, 'rgba(255, 0, 0, 0)');
grad1.addColorStop(1, clr1);
grad2.addColorStop(0, 'rgba(0, 0, 255, 0)');
grad2.addColorStop(1, clr2);
bg1.fillStyle = grad1;
bg1.fillRect(0, 0, 256, 256);
bg1.fillStyle = grad2;
bg1.fillRect(0, 0, 256, 256);
bg1.fillRect(0, 0, 256, 256);
bg1.fillStyle = grad1;
bg1.fillRect(0, 0, 256, 256);
}
Here's a simple example showing how to create an arbitrary gradient on a canvas, with per-pixel control: http://jsfiddle.net/j85FQ/3/
colorField( myCanvas, 500, 500, pretty );
function colorField(canvas,width,height,colorLookup){
var w = width-1, h = height-1;
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d'),
idata = ctx.getImageData(0,0,width,height),
data = idata.data;
for (var x=0;x<width;++x){
for (var y=0;y<height;++y){
var rgba = colorLookup(x/w,y/h);
var o = (width*y+x)*4;
for (var i=0;i<4;++i) data[o+i] = rgba[i]*255;
}
}
ctx.putImageData(idata,0,0);
}
function pretty(xPct,yPct){
return [ xPct, yPct, xPct*(1-yPct), 1];
}
Thanks guys I was able to work it out with the canvas element. I used a bucket fill for the z channel value and horizontal & vertical linear gradients from 0 to 255 for x and y channels. Setting context.globalCompositeOperation = "lighter" was the key I was missing. That was the simple additive mode I needed much easier then trying to find a suitable alpha compositing method. The following is the canvas init function I wrote.
function init() {
var c = document.getElementById('myCanvas');
var ctx = c.getContext('2d');
ctx.globalCompositeOperation = "lighter";
var grd = ctx.createLinearGradient(0, 0, 512, 0);
grd.addColorStop(0, "#000000");
grd.addColorStop(1, "#FF0000");
var grd2 = ctx.createLinearGradient(0, 0, 0, 512);
grd2.addColorStop(0, "#000000");
grd2.addColorStop(1, "#00FF00");
ctx.fillStyle = "#0000FF";
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = grd;
ctx.fillRect(0, 0, 512, 512);
ctx.fillStyle = grd2
ctx.fillRect(0, 0, 512, 512)
}