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.


ctx.beginPath();
ctx.rect(img.width / 4, img.height / 4, img.width / 2, img.height / 2);
ctx.clip();
ctx.drawImage(img, 0, 0);

Below you can see the original image and the clipping result.

Clipping canvas sample image

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:

Inscribed n-side polygon

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:

Inscribed polygon point coordinates

Using this formula we can write down the function making the polygon path:


function pathPolygon(ctx, numSides, width, height){
  • prepare the array of points
    var 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 path
    ctx.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:


function clipImageInPolygon(img, numSides){
  • 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:

Inscribed n-rays star

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:

Inscribed star point coordinates

The following code makes the star path:


star ray object
function Ray(){
  • this.rx;  vertex x
    this.ry;  vertex y
    this.bx;  base x
    this.by;  base y
}
 
function pathStar(ctx, numRays, Kernel, width, height){
  • prepare the array of points
    var 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 path
    ctx.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:


function pathEllipse(ctx, x, y, w, h){
  • var kappa = 0.5522848,
    • ox = (w / 2) * kappa,  control point offset horizontal
      oy = (h / 2) * kappa,  control point offset vertical
      xe = x + w,            x-end
      ye = y + h,            y-end
      xm = x + w / 2,        x-middle
      ym = 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.


function getTransparencyMask(width, height){
  • create a canvas for the mask preparation
    var tCanvas = document.createElement("canvas");
    tCanvas.width = width;
    tCanvas.height = height;
    var tCtx = tCanvas.getContext("2d");
     
    stroke the set of concentric ellipses with variable alpha
    var 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 part
    tCtx.beginPath();
    drawEllipse(tCtx, width / 8, height / 8, width * 3 / 4, height * 3 / 4);
    tCtx.fillStyle = '#00ff00';
    tCtx.fill();
     
    return the image data
    return tCtx.getImageData(0, 0, width, height);
}

Therefore, we have the transparency mask and can combine it with the source image pixels:


function setTransparency(ctx, imageData, width, height){
  • prepare the transparancy mask
    var transpMask = getTransparencyMask(width, height);
    var transpData = transpMask.data;
     
    get the mask data and apply it to the main canvas pixels
    var data = imageData.data;
    for (var i = 0; i < data.length; i += 4)
    • data[i + 3] = transpData[i + 3];
     
    update the image data
    ctx.putImageData(imageData, 0, 0);
}

Finally we use the above function together with clipping:


function transpClipImage(img){
  • create canvas and get its context
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
     
    draw the image clipped in ellipse
    ctx.beginPath();
    drawEllipse(ctx, 1, 1, img.width - 2, img.height - 2);
    ctx.clip();
    ctx.drawImage(img, 0, 0);
     
    make the transparency effect
    var 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:

RGB to YUV computation

Then we reduce the luma component (Y) proportionally using the transparency mask and then compute back the RGB using the formula:

YUV to RGB computation

The following code implements this idea:


function setLighting(ctx, imageData, width, height){
  • get transparency mask
    var transpMask = getTransparencyMask(width, height);
    var transpData = transpMask.data;
    var data = imageData.data;
     
    process image pixels
    for (var i = 0; i < data.length; i += 4){
    • if ((transpData[i + 3] > 0) && (transpData[i + 3] < 255)){
      • compute the intensity coefficient
        var intenDvsr = 1.0 + (255 - transpData[i + 3]) / 100;
         
        compute YUV
        var 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 component
        Y = Y / intenDvsr;
         
        compute RGB
        data[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 data
    ctx.putImageData(imageData, 0, 0);
}

Again, we use this function to apply the effect to our image:


function lightingClipImage(img){
  • create canvas and get its context
    var canvas = document.createElement("canvas");
    canvas.width = img.width;
    canvas.height = img.height;
    var ctx = canvas.getContext("2d");
     
    draw the image clipped in ellipse
    ctx.beginPath();
    drawEllipse(ctx, 1, 1, img.width - 2, img.height - 2);
    ctx.clip();
    ctx.drawImage(img, 0, 0);
     
    make the lighting effect
    var 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…

Enjoyed this Article?
Recommend it to friends and colleagues!

0 Reader Comments