Crop image like pie chart - javascript

I want to crop image over another image like a pie-chart to create a loading animation. I was thinking of using raphaeljs, but couldn't find any information about image cropping in pie-chart style.
Here are the sample images:
Start state:
End state:
What it should look like:

Just draw a semi-transparent filled arc on top of the image (adjust alpha value to your pleasing):
var ctx = document.querySelector("canvas").getContext("2d"),
img = new Image;
img.onload = draw;
img.src = "http://i.imgur.com/hQ5Pljv.png";
function draw(){
var cx = 157, cy = 159, r = 150,
pst = 0,
ang = Math.PI * 2 * (pst/100),
dlt = 2;
// animate the following part
(function loop() {
ctx.drawImage(img, 0, 0);
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, 0, ang);
ctx.fillStyle = "rgba(0,0,0,0.33)"; // adjust alpha here
ctx.fill();
pst += dlt;
if (pst <= 0 || pst >= 100) dlt = -dlt;
ang = Math.PI * 2 * (pst/100);
requestAnimationFrame(loop)
})()
}
<canvas width=320 height=320></canvas>
Method two - compositing
Use two steps to clip the same arc above to use images instead:
Draw arc, this will be the composite data
Change comp. mode to source-atop - next drawing replaces the drawn arc
Draw secondary image in
Change comp. mode to destination-atop - next drawing will fill all non-pixels
Draw main image in
Demo:
var ctx = document.querySelector("canvas").getContext("2d"),
img1 = new Image, img2 = new Image, cnt=2;
img1.onload = img2.onload = loader;
img1.src = "http://i.imgur.com/hQ5Pljv.png";
img2.src = "http://i.imgur.com/k70j3qp.jpg";
function loader(){if (!--cnt) draw()};
function draw(){
var cx = 157, cy = 159, r = 150,
pst = 0, ang = Math.PI * 2 * (pst/100), dlt = 2;
// animate the following part
(function loop() {
ctx.clearRect(0, 0, 320, 320); // clear canvas, or set last comp mode to "copy"
// first arc
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.arc(cx, cy, r, 0, ang);
ctx.fill(); // this will be comp. basis for the next steps
// comp mode secondary image
ctx.globalCompositeOperation = "source-atop"; // replaces filled arc
ctx.drawImage(img2, 0, 0);
// comp mode main image
ctx.globalCompositeOperation = "destination-atop"; // fills all non-pixels
ctx.drawImage(img1, 0, 0);
pst += dlt; if (pst <= 0 || pst >= 100) dlt = -dlt; ang = Math.PI * 2 * (pst/100);
ctx.globalCompositeOperation = "source-over"; // reset comp. mode
requestAnimationFrame(loop)
})()
}
<canvas width=320 height=320></canvas>

You'll want an algorithm along the lines of:
Draw image A onto canvas 1
Clear canvas 2
Draw a partial circle on canvas 2, for the current state of the spinner, filled with white
Blit image B onto canvas 2, using the multiplicative blending mode
Blit canvas 2 onto canvas 1, using standard (replace) blending
Canvas 2 should contain the second image, masked by the section you want to use. Overlaying that onto canvas 1, provided you handle transparency properly, should give the effect you want.
You can also use two SVG circles with image backgrounds and do this trivially, assuming your target browsers support SVG.

Related

HTML5 canvas fill image with a circle

I was reading this post HTML5 canvas fill circle with image and was curious about doing something a little different.
Step by step I'd like to:
1. Take this image I have of a shield.
2. Trace the outline of the shield to get the shape.
3. Use a second, larger, image to fill the outline of the shield. I don't want it to fit. I just want it to observe the boundaries set by the outline of the shield and not draw outside them.
Here's what I have so far: JSFiddle - outline shape.
var img = new Image();
img.crossOrigin = "anonymous";
img.onload = function () {
// draw the image
// (this time to grab the image's pixel data
ctx.drawImage(img, canvas.width / 2 - img.width / 2, canvas.height / 2 - img.height / 2);
// grab the image's pixel data
imgData = ctx.getImageData(0, 0, canvas.width, canvas.height);
data = imgData.data;
// call the marching ants algorithm
// to get the outline path of the image
// (outline=outside path of transparent pixels
points = geom.contour(defineNonTransparent);
ctx.strokeStyle = "red";
ctx.lineWidth = 2;
redraw();
}
img.src = "https://cdn1.iconfinder.com/data/icons/shield-4/744/1-512.png";
// redraw the canvas
// user determines if original-image or outline path or both are visible
function redraw() {
// clear the canvas
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw the image
ctx.drawImage(img, canvas.width / 2 - img.width / 2, canvas.height / 2 - img.height / 2);
// draw the path (consisting of connected points)
// draw outline path
ctx.beginPath();
ctx.moveTo(points[0][0], points[0][1]);
for (var i = 1; i < points.length; i++) {
var point = points[i];
ctx.lineTo(point[0], point[1]);
}
ctx.closePath();
ctx.stroke();
ctx.fillStyle="yellow"; // Just filling with yellow temporarily but would like to use an image
ctx.fill();
}
I'm not sure how to draw inside the outline without overwriting it.
I figured it out using context save, clip, and restore. new jsfiddle
ctx.save();
ctx.clip();
loadnewimage();
ctx.restore();

best practice in drawing outline of image

I've tried 3 ways to make it, but the effect doesn't looks well.
copy and fill image then make offset. The demo is
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var dArr = [-1,-1, 0,-1, 1,-1, -1,0, 1,0, -1,1, 0,1, 1,1], // offset array
s = 20, // thickness scale
i = 0, // iterator
x = 5, // final position
y = 5;
// draw images at offsets from the array scaled by s
for(; i < dArr.length; i += 2)
ctx.drawImage(img, x + dArr[i]*s, y + dArr[i+1]*s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0,0,canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
. When the outline width is large, the outline result will be wrong.
check the edge of image base on the Marching Squares algorithm. When the image shape is circle, the outline is with sawtooth. If make the outline more smoother, it won't fit the sharp shape like star.
copy and fill the image then scale it. When a image width is not equal with height, it doesn't work.
You can try with a math approach, without the offset array
var ctx = canvas.getContext('2d'),
img = new Image;
img.onload = draw;
img.src = "http://i.stack.imgur.com/UFBxY.png";
function draw() {
var s = 20, // thickness scale
x = 5, // final position
y = 5;
for (i=0; i < 360; i++)
ctx.drawImage(img, x + Math.sin(i) * s, y + Math.cos(i) * s);
// fill with color
ctx.globalCompositeOperation = "source-in";
ctx.fillStyle = "red";
ctx.fillRect(0, 0, canvas.width, canvas.height);
// draw original image in normal mode
ctx.globalCompositeOperation = "source-over";
ctx.drawImage(img, x, y);
}
<canvas id=canvas width=500 height=500></canvas>
My idea comes from the way we draw a circle using a string:
https://www.wikihow.com/Draw-a-Perfect-Circle-Using-a-Pin
Imagine that instead of a pencil at the end of the string we just have a shape
Here is a visual comparison of my approach and yours, also I'm showing a third approach scaling the image, there is really not a best one, it's just a matter of personal preference.
You could create a hybrid mode, if the hairline is important to you, get that portion of the image scaling it, then use a different way for the rest of the body.

Rounded Edges on Image

Say I have created a polygon-shaped image by creating a shape in HTML5 canvas and then filling it with an image, e.g. as below:
Now I want to round the corners on this hexagon.
There is a lineJoin = "round" property available but this doesn't seem to work (I believe because the shape is filled and there is no outer line to round).
Does anyone have any idea how to do this with HTML5 canvas or any other means?
Here is the code used to create the image:
var ctx = document.getElementById('myCanvas').getContext('2d');
var a = ctx.canvas.width, r = a / 5;
var side = Math.sqrt((4/3) * r * r);
// Draw your image onto the canvas (here I'll just fill the
// surface with red
var img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/0/03/Mountain_Bluebird.jpg";
img.onload = function () {
var pattern = ctx.createPattern(img, "no-repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, a, a);
// Switch the blending mode
ctx.globalCompositeOperation = 'destination-in';
// Draw the hexagon shape to mask the image
ctx.beginPath();
ctx.moveTo(0, side/2);
ctx.lineTo(0, 3*side/2);
ctx.lineTo(r, 2*side);
ctx.lineTo(2*r, 3*side/2);
ctx.lineTo(2*r, side/2);
ctx.lineTo(r, 0);
ctx.closePath();
ctx.fill();
};
<canvas width="1000" height="1000" id="myCanvas"></canvas>
Just change the order you draw in and then change globalCompositeOperation to 'source-in'.
I made some adjustments because some of the corners in your code were getting clipped off but didn't adjust the image position (I hope that's easy enough to do)
Preview
You need to adjust the image position by the way like I said
Snippet
var ctx = document.getElementById('myCanvas').getContext('2d');
var a = ctx.canvas.width, r = a / 5;
var side = Math.sqrt((4/3) * r * r) - 20;
// Draw your image onto the canvas (here I'll just fill the
// surface with red
var img = new Image();
img.src = "https://upload.wikimedia.org/wikipedia/commons/0/03/Mountain_Bluebird.jpg";
img.onload = function () {
ctx.lineJoin = "round";
ctx.lineWidth = 50;
// Draw the hexagon shape to mask the image
ctx.beginPath();
ctx.moveTo(50, 50 + side/2);
ctx.lineTo(50, 50 + 3*side/2);
ctx.lineTo(50 + r, 50 + 2*side);
ctx.lineTo(50 + 2*r, 50 + 3*side/2);
ctx.lineTo(50 + 2*r, 50 + side/2);
ctx.lineTo(50 + r, 50);
ctx.closePath();
ctx.stroke();
ctx.fill();
// Switch the blending mode
ctx.globalCompositeOperation = 'source-in';
var pattern = ctx.createPattern(img, "no-repeat");
ctx.fillStyle = pattern;
ctx.fillRect(0, 0, a, a);
};
<canvas width="1000" height="1000" id="myCanvas"></canvas>
There is no automatic way of doing this. The canvas API is pretty low-level, so it doesn't know that you've drawn a shape and want to detect where all the edges are. What you can do, but it would be a bit of a hassle to do is use bezierCurveTo. This method takes size arguments and can create curves. Combine that with your lines, and you can create rounded corners.
bezierCurveTo
You could also add circles at all the corners, with the arc method.

rotate one of the 2 canvas objects placed in canvas

I'm developing web app using canvas and I made three. canvas, canvas_panorama and canvas_image.
First one is something like main canvas, conteiner for the others. canvas_panorama is a background for canvas_image.
After canvas is right clicked, I'm computing angle to rotate canvas_image:
function getAngle( e, pw /*canvas*/ ){
var offset = pw.offset();
var center_x = (offset.left) + ($(pw).width() / 2);
var center_y = (offset.top) + ($(pw).height() / 2);
var mouse_x = e.pageX;
var mouse_y = e.pageY;
var radians = Math.atan2(mouse_x - center_x, mouse_y - center_y);
angle = radians;
}
After I have an angle I'm trying to rotate canvas_image like this:
function redraw(){
var p1 = ctx.transformedPoint(0,0);
var p2 = ctx.transformedPoint(canvas.width,canvas.height);
ctx.clearRect( p1.x, p1.y, p2.x-p1.x, p2.y-p1.y );
canvas_image_ctx.drawImage(image_img, 0, 0, 150, 150);
canvas_panorama_ctx.drawImage(panorama_img, 0, 0, 600, 300);
canvas_panorama_ctx.drawImage(canvas_image, 20, 20);
// rotate panorama_img around its center
// x = x + 0.5 * width
// y = y + 0.5 * height
canvas_panorama_ctx.translate(95, 95);
canvas_panorama_ctx.rotate(angle);
// translate to back
canvas_panorama_ctx.translate(-95, -95);
ctx.drawImage(canvas_panorama, 0, 0);
}
But this rotates both canvas_image and canvas_panorama. It should only rotate canvas_image
JSFiddle to show you my problem
I think you are confusing yourself with this idea of multiple canvases.
Once in the drawImage() method, every of your canvases are just images, and could be just one or even just plain shapes.
Transformation methods do apply to the canvas' context's matrix, and will have effect only if you do some drawing operations when they are set.
Note : To reset your context matrix, you can either use save(); and restore() methods which will also save all other properties of your context, so if you only need to reset the transform, then it's preferred to simply reset the transformation matrix to its default : ctx.setTransform(1,0,0,1,0,0).
Here is a simplified example to make things clearer :
var ctx = canvas.getContext('2d');
// a single shape, with the border of the context matrix
var drawRect = function(){
ctx.beginPath();
ctx.rect(10, 10, 50, 20);
ctx.fill();
ctx.stroke();
ctx.beginPath();
ctx.rect(0, 0, canvas.width, canvas.height);
ctx.stroke();
};
// set the color of our shapes
var gradient = ctx.createLinearGradient(0,0,70,0);
gradient.addColorStop(0,"green");
gradient.addColorStop(1,"yellow");
ctx.fillStyle = gradient;
// here comes the actual drawings
//we don't have modified the transform matrix yet
ctx.strokeStyle = "green";
drawRect();
// here we translate of 100px then we do rotate the context of 45deg
ctx.translate(100, 0)
ctx.rotate(Math.PI/4)
ctx.strokeStyle = "red";
drawRect();
// reset the matrix
ctx.setTransform(1,0,0,1,0,0);
// here we move of 150px to the right and 25px to the bottom
ctx.translate(150, 25)
ctx.strokeStyle = "blue";
drawRect();
// reset the matrix
ctx.setTransform(1,0,0,1,0,0);
<canvas id="canvas" width="500" height="200"></canvas>
In your code, you are setting the transformations on the canvas that does represent your image, and you do draw every of your canvases at each call.
What you want instead, is to set the transformation on the main canvas only, and draw the non-transformed image :
var main_ctx = canvas.getContext('2d');
var img_canvas = canvas.cloneNode();
var bg_canvas = canvas.cloneNode();
var angle = 0;
// draw on the main canvas, and only on the main canvas
var drawToMain = function(){
// first clear the canvas
main_ctx.clearRect(0,0,canvas.width, canvas.height);
// draw the background image
main_ctx.drawImage(bg_canvas, 0,0);
// do the transforms
main_ctx.translate(img_canvas.width/2, img_canvas.height/2);
main_ctx.rotate(angle);
main_ctx.translate(-img_canvas.width/2, -img_canvas.height/2);
// draw the img with the transforms applied
main_ctx.drawImage(img_canvas, 0,0);
// reset the transforms
main_ctx.setTransform(1,0,0,1,0,0);
};
// I changed the event to a simple onclick
canvas.onclick = function(e){
e.preventDefault();
angle+=Math.PI/8;
drawToMain();
}
// a dirty image loader
var init = function(){
var img = (this.src.indexOf('lena')>0);
var this_canvas = img ? img_canvas : bg_canvas;
this_canvas.width = this.width;
this_canvas.height = this.height;
this_canvas.getContext('2d').drawImage(this, 0,0);
if(!--toLoad){
drawToMain();
}
};
var toLoad = 2;
var img = new Image();
img.onload = init;
img.src = "http://pgmagick.readthedocs.org/en/latest/_images/lena_scale.jpg";
var bg = new Image();
bg.onload = init;
bg.src = 'http://www.fnordware.com/superpng/pnggradHDrgba.png';
<canvas id="canvas" width="500" height="300"></canvas>

How to use canvas to change opacity of a context's arc?

I've been able to draw images using a background image source and change the opacity of the background image to 25% like so...
var context = document.getElementById('myCanvas').getContext('2d');
context.globalAlpha=.25;
var img = new Image();
img.onload = function(){
context.drawImage(img, 0, 0);
}
img.src = 'pie_crust.png';
And I've been able to draw single arcs...
var canvas = document.getElementById("myCanvas");
var context = canvas.getContext("2d");
var centerX = canvas.width / 2;
var centerY = canvas.height / 2;
var radius = 100;
var startingAngle = 30 * Math.PI/180;
var endingAngle = 60 * Math.PI/180;
var counterclockwise = true;
context.arc(centerX, centerY, radius, startingAngle, endingAngle, counterclockwise);
context.lineWidth = 20;
context.strokeStyle = "black"; // line color
context.stroke();
However I haven't been able to change the opacity of a context's arc. For example I have a pie crust (pie_crust.png).
I would like for the user to specify two sets of start and end angles. Let's say the first set is (30, 60) and the second set is (135, 180) and counterclockwise is set to true. I would like those two arcs to have an opacity of 25% and the left over pie crust to have an opacity of 0% so that the resulting canvas would look like this:
How can I use canvas to achieve this effect?
You need to just draw image using pie-formed clipping paths, like this:
context.beginPath();
context.arc(centerX, centerY, radius, Math.PI/6, Math.PI/3, true);
context.moveTo(centerX, centerY);
context.closePath();
context.clip();

Categories