Mastering images with HTML5 canvas – part 2
Clipping, transparency and brightness effects
In the first part we learned the basics of the image processing with canvas, implemented grayscale and colored photo filters. In this part we'll clip images in different shapes, deeper understand the sense of transparency and brightness, and make some cool graphical effects that can be achieved by them.
Clipping the images
First of all, we should create a clipping path. Canvas path is a series of points connected by lines or curves dependent on the drawing instructions. We start the path using the method beginPath and set its first point using the method moveTo. Then we construct multiple subpaths using the lineTo, arcTo, quadraticCurveTo and bezierCurveTo methods, while the ending point of each new subpath becomes the new context point. Finally we use the closePath method to return to the starting point and so make out path closed. Rather than drawing the path point by point, we can also use the whole shape drawing methods rect and arc. When the path is ready, we can fill it, stroke it, or use it as a clipping region.
The clipping in HTML5 canvas is simple and straightforward. Since we use the clip method, all future drawing will be limited to the clipping region. So, if we draw the image after the clip method then only the part of the image inside the path will be actually drawn and the rest of the canvas area will stay unchanged.
Let's draw the below picture (I took it in the Schonbrunn Gardens, Vienna; just an amazing place!) into the canvas that we'll clip with a smaller rectangle at the center.
Below you can see the original image and the clipping result.
Clipping image in a polygon
To clip image in a shape we should fulfill the following sequence of operations: create a canvas and get the canvas context, make the desired shape, call the 'clip' method, draw the image into canvas and finally update the source image pixels. To clip an image in a polygon, for example, we should prepare the polygon inscribed in the ellipse inscribed in the image bounds as it's shown in the below picture:
So, the X and Y coordinates of the n-th point of the N-side polygon inscribed in the picture of W x H size are:
Using this formula we can write down the function making the polygon path:
- prepare the array of pointsvar Points = new Array(numSides);for (var i = 0; i < numSides; i++){var p = new Point();p.x = width / 2 + width / 2 * Math.cos(i * 2 * Math.PI / numSides);p.y = height / 2 + height / 2 * Math.sin(i * 2 * Math.PI / numSides);Points[i] = p;
- make the pathctx.beginPath();ctx.moveTo(Points[0].x, Points[0].y);for (var i = 0; i < numSides; i++){
- ctx.lineTo(Points[i].x, Points[i].y);
ctx.closePath();
and use it for the image clipping:
- var canvas = document.createElement("canvas");canvas.width = img.width;canvas.height = img.height;var ctx = canvas.getContext("2d");pathPolygon(ctx, numSides, img.width, img.height);ctx.clip();ctx.drawImage(img, 0, 0);img.src = canvas.toDataURL("image/png");
Pay attention to the last string. In the first article part examples we placed canvas as an additional element on top of the source image to allow transitions between the two. In case of clipping, however, it's much more logical to change the source image itself. Luckily, HTML5 canvas provides a special method for this operation called toDataURL. This method returns a data URI containing a representation of the image in the format specified by the type parameter, so we can just assign the image element's src property to it.
Now we can play with the number of the polygon's sides and see the result:
Clipping image in a star
To make an N-ray star path we need two sets of points. The first set is the ray vertexes points which are identical to the points of the N-side polygon. The second is the set of ray base points that are placed on a smaller size concentric ellipse as it's shown in the below picture:
K is a coefficient defining the ratio between the kernel ellipse for the ray base points and the main ellipse for the ray vertex points. So, the X and Y coordinates of the base points are:
The following code makes the star path:
- this.rx; vertex xthis.ry; vertex ythis.bx; base xthis.by; base y
- prepare the array of pointsvar rayPoints = new Array(numRays);for (var i = 0; i < numRays; i++){
- var r = new Ray();r.rx = width / 2 * (1 + Math.cos(i * 2 * Math.PI / numRays));r.ry = height / 2 * (1 + Math.sin(i * 2 * Math.PI / numRays));r.bx = width / 2 * (1 + Kernel *
- Math.cos((Math.PI + i * 2 * Math.PI) / numRays));
r.by = height / 2 * (1 + Kernel *- Math.sin((Math.PI + i * 2 * Math.PI) / numRays));
rayPoints[i] = p;
}make the pathctx.beginPath();ctx.moveTo(rayPoints[0].rx, rayPoints[0].ry);for (var i = 0; i < numRays; i++){- ctx.lineTo(rayPoints[i].rx, rayPoints[i].ry);ctx.lineTo(rayPoints[i].bx, rayPoints[i].by);
}ctx.closePath();
Having replaced the function pathPolygon to pathStar in the previous sample we got the image clipped in a star and can play with the number of rays and the kernel coefficient:
Clipping image in an ellipse
Nobody can draw an ellipse, because ellipse is a mathematical abstraction. However, everyone can draw an oval approximating a corresponding ellipse, and so we'll do. The simplest way to draw an ellipse would be to draw a circle and then transform it to fit the image bounds, but the transformation would affect our image and so this way is not acceptable. Instead, to make the elliptic path we'll use the Bezier curves as following:
- var kappa = 0.5522848,
- ox = (w / 2) * kappa, control point offset horizontaloy = (h / 2) * kappa, control point offset verticalxe = x + w, x-endye = y + h, y-endxm = x + w / 2, x-middleym = y + h / 2; y-middle
ctx.beginPath();ctx.moveTo(x, ym);ctx.bezierCurveTo(x, ym - oy, xm - ox, y, xm, y);ctx.bezierCurveTo(xm + ox, y, xe, ym - oy, xe, ym);ctx.bezierCurveTo(xe, ym + oy, xm + ox, ye, xm, ye);ctx.bezierCurveTo(xm - ox, ye, x, ym + oy, x, ym);ctx.closePath();
Now we can use this function to make a clipping path and get the following result:
Elliptic image with variable transparency
Just clipping image is cool, but we can do something even more interesting. Now we'll take an image clipped in an ellipse and using variable transparency make “an image growing out of the background” effect. The central part of the image will be fully opaque, while on the sides, for the space of 25 percent of the total image size, it will become semi-transparent until the full transparency on the bounds.
First of all, let's prepare the transparency mask. We'll create an additional off-screen canvas, and using the function pathEllipse written in the previous example stroke in it the set of concentric ellipses with variable transparency. The central ellipse making 75 percent of the image size we'll fill with the full opacity. The color doesn't matter here because in the end we only need the alpha component.
- create a canvas for the mask preparationvar tCanvas = document.createElement("canvas");tCanvas.width = width;tCanvas.height = height;var tCtx = tCanvas.getContext("2d");stroke the set of concentric ellipses with variable alphavar ratio = height / width;for (var i = 0; i <= width / 8; i++){
- var opacity = 1.0 * i / (width / 8);tCtx.beginPath();drawEllipse(tCtx, i, i * ratio, width - 2 * i,
- height - 2 * i * ratio);
tCtx.strokeStyle() = 'rgba(0,255,0,' + opacity + ')';tCtx.lineWidth = 2;tCtx.stroke();
}fill the opaque central parttCtx.beginPath();drawEllipse(tCtx, width / 8, height / 8, width * 3 / 4, height * 3 / 4);tCtx.fillStyle = '#00ff00';tCtx.fill();return the image datareturn tCtx.getImageData(0, 0, width, height);
Therefore, we have the transparency mask and can combine it with the source image pixels:
- prepare the transparancy maskvar transpMask = getTransparencyMask(width, height);var transpData = transpMask.data;get the mask data and apply it to the main canvas pixelsvar data = imageData.data;for (var i = 0; i < data.length; i += 4)
- data[i + 3] = transpData[i + 3];
update the image datactx.putImageData(imageData, 0, 0);
Finally we use the above function together with clipping:
- create canvas and get its contextvar canvas = document.createElement("canvas");canvas.width = img.width;canvas.height = img.height;var ctx = canvas.getContext("2d");draw the image clipped in ellipsectx.beginPath();drawEllipse(ctx, 1, 1, img.width - 2, img.height - 2);ctx.clip();ctx.drawImage(img, 0, 0);make the transparency effectvar imageData = ctx.getImageData(0, 0, img.width, img.height);setTransparency(ctx, imageData, img.width, img.height);update the source image pixels$(img).get(0).src = canvas.toDataURL("image/png");
Now we can see how the image “grows out” of the background:
Elliptic image with variable brightness
To finish this part of the article with something nice, we'll use the previous sample approach for a different effect. The same way as we made the variable transparency, now we'll make a variable brightness to simulate the central lighting effect and give our image a semi-3D view.
From the first article part we already know how to compute the luma component, but this time we need to make a full computation of YUV from RGB and vice versa. At first we should compute the YUV from RGB using the formula:
Then we reduce the luma component (Y) proportionally using the transparency mask and then compute back the RGB using the formula:
The following code implements this idea:
- get transparency maskvar transpMask = getTransparencyMask(width, height);var transpData = transpMask.data;var data = imageData.data;process image pixelsfor (var i = 0; i < data.length; i += 4){
- if ((transpData[i + 3] > 0) && (transpData[i + 3] < 255)){
- compute the intensity coefficientvar intenDvsr = 1.0 + (255 - transpData[i + 3]) / 100;compute YUVvar Y = 0.299 * data[i] + 0.587 * data[i + 1] +
- 0.114 * data[i + 2];
var U = -0.147 * data[i] - 0.289 * data[i + 1] +- 0.436 * data[i + 2];
var V = 0.615 * data[i] - 0.515 * data[i + 1] -- 0.100 * data[i + 2];
reduce the luma componentY = Y / intenDvsr;compute RGBdata[i] = Math.round(Y + 1.140 * V);data[i + 1] = Math.round(Y - 0.395 * U - 0.581 * V);data[i + 2] = Math.round(Y + 2.032 * U);
}
}return image datactx.putImageData(imageData, 0, 0);
Again, we use this function to apply the effect to our image:
- create canvas and get its contextvar canvas = document.createElement("canvas");canvas.width = img.width;canvas.height = img.height;var ctx = canvas.getContext("2d");draw the image clipped in ellipsectx.beginPath();drawEllipse(ctx, 1, 1, img.width - 2, img.height - 2);ctx.clip();ctx.drawImage(img, 0, 0);make the lighting effectvar imageData = ctx.getImageData(0, 0, img.width, img.height);setLighting(ctx, imageData, img.width, img.height);update the source image pixels$(img).get(0).src = canvas.toDataURL("image/png");
and see the result below:
Conclusion
In this part we have learned how to clip images and implement the clipping in different shapes like N-side polygon, N-rays star and ellipse. We have also seen how clipping can be combined with variable transparency and brightness to get some additional cool effects.
The next part will deal with image blurring and more…