Is there a way to clip an image to an SVG shape with a CanvasRenderingContext2D?
I'm trying to use different SVG shapes to display various parts of an image on demand.
For example – an SVG with an <ellipse> element (or an equivalent <path>) would allow me to show a circular portion of the image.
CanvasRenderingContext2D.clip() seems to be the close to what I need, but I can't find any information about how to use it with an SVG, or alternatively how to draw an SVG as a path.
Another direction I'm thinking about is saving the clipping area as a <path> element and manually transforming it to CanvasRenderingContext2D equivalent methods such as lineTo and arc.
Three steps:
Draw the SVG to your canvas.
Set context.globalCompositeOperation = 'source-in';
Draw the image.
The image will only be drawn where the SVG has already filled in pixels with colour, effectively clipping the image to whatever has already been drawn.
You can either set the globalCompositeOperation back to 'source-over' (the default), or use context.save() before and context.restore() after to put the canvas back into a "normal" drawing mode.
#Kaiido has some good suggestions for you (see his comment to the question).
Draw the SVG to canvas & use globalCompositeOperation instead of clipping. But Firefox has a bug in applying gCO directly to an svg image, which force you to first draw your svg on a second canvas ; and that IE prior to Edge will taint the canvas when an svg is drawn to it.
Parse your svg first and then use the canvas API to draw those shapes (path commands are quite similar so it's not so hard and library like fabricjs can even handle it in a nice way for you)
Another option is to convert your SVG drawings to .png format and use that image + globalCompositeOperation to clip your image inside the .png shape. This avoids the cross-browser problems with SVG.
But, if your clipping shapes are just simple SVG paths (ovals, etc), then you might forget SVG and draw your path using canvas path commands.
I'll repost a previous SO Q&A to illustrate clipping inside a canvas path:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var img=new Image();
img.onload=start;
img.src="https://dl.dropboxusercontent.com/u/139992952/multple/kidwallpaper.jpg";
function start(){
// resize the canvas to equal the image size
var iw=img.width;
var ih=img.height;
cw=canvas.width=iw;
ch=canvas.height=ih;
// calculate the scaling needed to max the display of the image
// inside the oval
if(iw>ih){
var scaleX=iw/ih
var scaleY=1;
var r=ih/2;
}else{
var scaleX=1;
var scaleY=ih/iw;
var r=iw/2;
}
// scale so the circle (arc) becomes an oval
ctx.scale(scaleX,scaleY);
ctx.arc(cw/scaleX/2,ch/scaleY/2,r,0,Math.PI*2);
ctx.fill();
// undo the scaling
ctx.scale(1/scaleX,1/scaleY);
// draw the image centered inside the oval using compositing
ctx.globalCompositeOperation='source-atop';
ctx.drawImage(img,cw/2-img.width/2,ch/2-img.height/2);
ctx.globalCompositeOperation='source-atop';
}
body{ background-color: black; }
#canvas{border:1px solid red; margin:0 auto; }
<canvas id="canvas" width=300 height=300></canvas>
You cannot do this directly. You can:
Draw the SVG into a canvas and use that for compositing to clip. Drawback is that some browsers has limited support due to security (external links, tainting the canvas and so on).
Parse the SVG manually
Here is an example of how you can build a simple parser for SVG. It cannot be used for generic use but assumes you know the SVG in question. You can build on top of this to support different units, transform lists etc.
The path that is extracted can be stored on a Path2D object instead of directly as shown below (Path2D may need poly-fill in some browsers), or store custom objects/arrays etc. This is entirely up to you.
The example is more a proof-of-concept.
Example SVG parser to canvas Path2D
var ctx = c.getContext("2d"),
mask = document.getElementById("mask"); // get a SVG element
// some random graphics
for(var i=30,r=Math.random;i--;) {
ctx.fillStyle = "hsl(" + (360*r()) + ",50%,50%)";ctx.fillRect(280*r(),120*r(),50,50)}
// parse SVG element
if (mask.localName) {
switch(mask.localName) {
case "rect":
ctx.rect(v("x"), v("y"), v("width"), v("height"));
break;
case "ellipse":
// need polyfill in some browsers
ctx.ellipse(v("cx"), v("cy"), v("rx"), v("ry"), 0, 6.28);
break;
// more cases here
}
// use path from SVG to clip
ctx.globalCompositeOperation = "destination-in";
ctx.fill();
}
// helper - obtains a numeric value for SVG element property
function v(name) {return mask[name].baseVal.value}
#c, svg {border:1px solid #777}
<h4>SVG (showing mask)</h4>
<svg xmlns="http://www.w3.org/2000/svg"
width="300" height="150">
<rect id="mask" x="50" y="30" width="200" height="100" />
</svg>
<h4>Canvas (mask applied from SVG)</h4>
<canvas id=c></canvas>
Related
The following creates an unexpected clip path. Is it because of the
movement from the end of one arc to the start of the next? The circles turned out to be ellipses, which was also unexpected.
var canvas3=document.getElementById("drawing3");
var ct=canvas3.getContext("2d");
ct.beginPath();
ct.arc(50,80,30,0,Math.PI*2,false);
ct.arc(120,40,60,0,Math.PI*2,true);
ct.arc(180,40,30,0,Math.PI*2,true);
ct.arc(240,40,30,0,Math.PI*2,true);
ct.clip();
ct.fillStyle="yellow";
ct.fillRect(10,10,400,200);
ct.fillStyle="magenta";
ct.font = "40px sans-serif";
ct.fillText("what's this?",30,70);
By the way, does a clip area always have to start with beginPath()?
Why ellipses instead of arcs
Although you don't include your CSS, canvas drawings become deformed when the canvas element is resized with CSS. The default size of html5 canvas is 300x150 so adjusting its size with CSS will stretch or squish subsequent drawings unless the CSS resizes the canvas exactly in proportion to its original 2:1 proporitions.
So, if you want to resize the canvas do the resizing by changing the element's size directly:
var canvas3=document.getElementById("drawing3");
canvas3.width=400;
canvas3.height=300;
Why beginPath is useful (essential!)
A context.clip always acts upon the last set of path commands and a set of path commands should be started with context.beginPath. So yes, a clip should always start with context.beginPath.
Why is beginPath useful (always use it!): If you issue multiple sets of path commands without starting each set with beginPath, then each new set will also execute all previous sets. Your path will just become one long drawing rather than having distinct drawings.
Fixing the inter-connected circles
context.arc is a canvas path drawing command.
Path commands will always connect (with a line) all drawings that occur between context.beginPath and context.stroke or context.fill.
But you can command the path to "pick up the pencil and move it to the next circle center" using context.moveTo. This will prevent your circles from becoming inter-connected.
// pick up the pencil and move it to the next arc's centerpoint
ct.moveTo(x,y);
// and draw the arc around the centerpoint
ct.arc(x,y,r,0,Math.PI*2,false);
Here's your code refactored to "pick up the pencil" between arcs:
var canvas3=document.getElementById("drawing3");
var ct=canvas3.getContext("2d");
ct.beginPath();
arcAtXY(50,80,30);
arcAtXY(120,40,60);
arcAtXY(180,40,30);
arcAtXY(240,40,30);
ct.clip();
ct.fillStyle="yellow";
ct.fillRect(10,10,400,200);
ct.fillStyle="magenta";
ct.font = "40px sans-serif";
ct.fillText("what's this?",30,70);
function arcAtXY(x,y,r){
ct.moveTo(x,y);
ct.arc(x,y,r,0,Math.PI*2,false);
}
canvas{border:1px solid red;}
<canvas id="drawing3" width=300 height=300></canvas>
An additional thought not directly related to your design
If you want to stroke the circles instead of filling them, you would move to the next circle's perimeter rather than its centerpoint. Moving to the centerpoint would cause an automatic line to draw from the centerpoint to the perimeter.
function arcAtXY(x,y,r){
ct.moveTo(x+r,y);
ct.arc(x,y,r,0,Math.PI*2,false);
}
I have overlay image with rectangle transparent hole. Beneath is scalable and draggable image. How to cut only visible part of image ?
How to determine size and position of transparent rectangle ? Is it possible to do pixel by pixel alpha channel search only on overlay image ?
Any other ideas ?
EDIT:
Solution to another quoted problem is usefull, although it works only on whole canvas not on individual items like background, overlay or added images or shapes. Is it possible to read pixel values on individual fabric elements ?
I use overlay image as external png files.
The FabricJS API does not include a method to fetch the pixel values of an image.
You will have to draw your overlay image onto an html5 canvas and use getImageData to fetch the pixel data from that canvas.
getImageData().data contains the red, green, blue & alpha information for each pixel on the canvas.
You can test each pixel's alpha value and determine the minimum & maximum boundary of the transparent rectangle.
Here's example code and a Demo:
var canvas=document.getElementById("canvas");
var ctx=canvas.getContext("2d");
var cw=canvas.width;
var ch=canvas.height;
var img=new Image();
img.crossOrigin='anonymous';
img.onload=start;
img.src="http://masterblocks.co.in/static/img/logo.png";
function start(){
cw=canvas.width=img.width;
ch=canvas.height=img.height
ctx.drawImage(img,0,0);
// clear a test rectangle
ctx.clearRect(100,100,100,75);
// get clear rect bounds
var bounds=findCutoutBounds();
// test: stroke the bounds
ctx.lineWidth=2;
ctx.strokeStyle='red';
ctx.strokeRect(bounds.x,bounds.y,bounds.width,bounds.height);
}
// Get the imageData of that canvas
function findCutoutBounds(){
var minX=1000000;
var minY=1000000;
var maxX=-1;
var maxY=-1;
var data=ctx.getImageData(0,0,canvas.width,canvas.height).data;
for(var y=0;y<ch;y++){
for(var x=0;x<cw;x++){
var n=(y*cw+x)*4;
if(data[n+3]<5){
if(y<minY){minY=y;}
if(y>maxY){maxY=y;}
if(x<minX){minX=x;}
if(x>maxX){maxX=x;}
}
}}
return({x:minX,y:minY,width:maxX-minX,height:maxY-minY});
}
body{ background-color: ivory; }
#canvas{border:1px solid red;}
<h4>The bounding box of the transparent rectangle is stroked in red</h4>
<canvas id="canvas" width=300 height=300></canvas>
I'm having a bit of trouble here to develop this functionality since it must work on IE9+ so css clip-path is not an option ( http://caniuse.com/#feat=css-clip-path ).
The issue:
I need to create a grid composed of 6 elements.
Each element is an image.
The images can be different according to user answers before getting to the grid page.
Eeach element / image must be clicable and will acquire a "selected" class that will overlay div with text and background image.
image:
What is the best way to achieve this?
One way to do this could be to save out each combination of the six images you require into one big image. Then, depending on the user's answer combination, you insert the corresponding image as a background-image of a div. You then overlay click-able hotspots within the same div that roughly correlate to the dividing edges.
This may however not be the most practical solution and largely depends on how many answers/images you are dealing with.
Alternatively you could draw SVG shapes and set their fills to the images you require.
I can recommend Raphael.js as a starting point. You should be able to find what you need in the documentation
Another option would be to use HTML5 canvas:
http://jsfiddle.net/julienbidoret/GKP7X/1/
(credit goes to julienbidoret for the jsfiddle)
Javascript:
var canvas = document.getElementById('c');
var ctx = canvas.getContext('2d');
var img = document.createElement('IMG');
img.onload = function () {
ctx.save();
ctx.beginPath();
ctx.moveTo(20, 0);
ctx.lineTo(240, 0);
ctx.lineTo(220, 240);
ctx.lineTo(0, 240);
ctx.closePath();
ctx.clip();
ctx.drawImage(img, 0, 0);
ctx.restore();
}
img.src = "http://upload.wikimedia.org/wikipedia/commons/2/2b/Clouds.JPG";
HTML:
<canvas id="c" width="300" height="300" ></canvas>
Both SVG and canvas are supported in IE9.
Is it possible to give a glow effect to an image automatically, say using canvas?
jsfiddle
The canvas tag would have to omit the transparent
and make it have an outter glow?
<canvas id="canvas" width=960 height=960></canvas>
Make a canvas path glow by applying a series of overlapping shadows with increasing blur
A Demo: http://jsfiddle.net/m1erickson/Z3Lx2/
You can change the styling of the glow by varying the number of overlays and the blur size.
Example code for a glow effect:
var canvas = document.getElementById("canvas");
var ctx = canvas.getContext("2d");
// glow
var glowColor="blue";
ctx.save();
ctx.strokeStyle = glowColor;
ctx.shadowColor = glowColor;
ctx.shadowOffsetX=300;
for (var i = 0; i < 10; i++) {
ctx.shadowBlur = i * 2;
ctx.strokeRect(-270, 30, 75, 150);
}
ctx.restore();
To get the outline path of your phone image, you can use the "marching ants" algorithm.
This algorithm will create a path that outlines an image.
In your case you would define the image as all pixels that are not transparent.
Here's a very good implementation of "marching ants" that is used in the excellent d3 library:
https://github.com/d3/d3-plugins/blob/master/geom/contour/contour.js
It's used like this:
DrawImage your phone on the canvas.
// draw the image
// (this time to grab the image's pixel data
ctx.drawImage(img,0,0);
Get the pixel color array from the canvas using ctx.getImageData
// grab the image's pixel data
imgData=ctx.getImageData(0,0,canvas.width,canvas.height);
data=imgData.data;
Define a function that checks the pixel array for non-transparent pixels at any x,y on the canvas.
// This is used by the marching ants algorithm
// to determine the outline of the non-transparent
// pixels on the image
var defineNonTransparent=function(x,y){
var a=data[(y*cw+x)*4+3];
return(a>20);
}
Call the contour function:
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
var points=geom.contour(defineNonTransparent);
Here's an example result:
the glow is automatically generated using overlapping shadows
the outline path of the phone is calculated using the marching ants algorithm
Is there a way to use an image as a clipping mask instead of creating a shape like this:
var c=document.getElementById("myCanvas");
var ctx=c.getContext("2d");
// Clip a rectangular area
ctx.rect(50,20,200,120);
ctx.stroke();
ctx.clip();
I have tried to context.drawImage('myimg.png') on top of the context and clip but that did not work.
You can only directly clip using a path.
If you have an image that you wish to clip by, you can probably achieve this by drawing your content in another canvas, and then using globalCompositeOperation combined with drawImage (with the mask) to remove the bits you don't want.
You would then then use .drawImage again (possibly with a different globalCompositeOperation) to merge that clipped image with your original content.
See for example http://www.html5canvastutorials.com/advanced/html5-canvas-global-composite-operations-tutorial/