Quantcast
Channel: David Kelley » CodeDavid Kelley
Viewing all articles
Browse latest Browse all 7

Creating an Image Collage

$
0
0

This post will take a look at how to create a randomized collage of images, including some of the more complicated aspects of the overall algorithm. This script will handle the positioning, sizing and resizing/cropping of images within the grid, resulting in a collage similar to the one depicted in this post.

It is worthy to note that this script does not use any additional Javascript libraries, such as jQuery.

Image collages are great for displaying your favorite images and can make some really great backgrounds on your favorite devices. The image displayed below works perfectly as an iPhone4 wallpaper. The collage of images below was created by taking advantage of the Piccsy API, in-order to retrieve my favorite images from the site. The script we will look at creating, will be able to collage any array of images into a great looking grid.

Image Collage

Lets start the script by wrapping the function in-order to prevent global namespace pollution. For this post, the window object will be passed in to the function. However, the script itself can be attached to any arbitrary object, particularly useful for modular implementations.

(function(target, name) {

    //attach to target or fallback to window
    target = target !== void 0 ? target : window;

    //create the body of the script
    target[name] = (function() {
        "use strict";

        function F(images, width, height, columns, rows) {
            //Function body...
        };

        return F;
    })();
})(window, "Collage");

Now that the frame for the script has been constructed, we can begin to construct the body of the script, including performing some initial logic on the arguments that have been passed. Normally, you would want to do some presence and validation checking here, but I have omitted it to reduce the overall complexity of the script for the purposes of this post.

//create canvas element
var canvas = document.createElement('canvas');

//set canvas width & height to required width and height
canvas.width = width;
canvas.height = height;

//get context within closure
var ctx = canvas.getContext('2d');

//determine value in pixels of a singular column and row unit
var column_unit = Math.floor(width/columns);
var row_unit = Math.floor(height/rows);

//determine margin between images (deducted from bottom right of each image
var margin = (Math.floor(width/(columns*2)) / 10) - (Math.floor(width/(columns*2)) / 10)/columns;

//create grid array to store offsets
var grid = new Array(rows);
for (var r = 0; r < grid.length; r++) {
  grid[r] = new Array(columns);
}

//store current image
var img = 0;

That is quite a lot of logic to cover, so I’ll break the slightly more complicated parts down. In addition to creating the canvas element, setting its dimensions and grabbing its context, we are calculating the values for a singular column and row, as well as the gutter/margin between each image.

//determine value in pixels of a singular column and row unit
var column_unit = Math.floor(width/columns);
var row_unit = Math.floor(height/rows);

The math behind determining the width of a single column and the height of a single row is fairly straightforward, we simply divide the overall width by the amount of columns; performing a similar operation for finding the row height.

//determine margin between images (deducted from bottom right of each image
var margin = (Math.floor(width/(columns*2)) / 10) - (Math.floor(width/(columns*2)) / 10)/columns;

The math behind determining the margin value is slightly more complex, we will use a bottom-right-deductible, to subtract the margin value from each individual image itself. Therefore, given an images width and height of 300 x 200 respectively, subtracting a margin of 20 would result in an overall width and height of 280 x 180 for each image.

The final lines of code handle creating the 2-dimensional grid array, this grid is a representation of each block within the collage (rows x columns). This array will enable the main loop to determine whether or not this particular block has been consumed, we will use a simple binary value to determines this (0|1).

You can take a look at the script so far, here. Note that it is not yet complete.

The main body of the script iterates through the entire grid, rotating through the array of images, sizing and positioning them onto the main canvas. If the array of images provided to the script is too small (ie. only two images), the script will infinitely iterate through this array until all blocks on the grid have been occupied.

//loop through each row
for (var r = 0; r < grid.length; r++) {
    //loop through each column within the current row
    for (var c = 0; c < grid[r].length; c++) {

        //skip block if it is occupied
        if (grid[r][c] !== 1) {
            //rotate through array of images, pick next image
            var src = images[img++%images.length];

            //create a new image element
            var el = new Image();

            //get the amount of columns and rows for this block
            var columns = this.getColumns(grid,r,c);
            var rows = this.getRows(grid,r,c);

            //push the images x & y values
            el.dataset.x = c * column_unit;
            el.dataset.y = r * row_unit;

            //set the images width and height, subtract margin
            el.dataset.w = column_unit * columns - margin;
            el.dataset.h = row_unit * rows - margin;

            el.src = src;

            //upon image onload
            //.....

            //loop through all blocks this image occupies and set to 1
            for (var or = r; or < r+rows; or++) {
                for (var oc = c; oc < c+columns; oc++) {
                    grid[or][oc] = 1;
                }
            }
        }
    }
}

The main loop is nested, looping through each column within each row. If the block that it encounters is unoccupied, it will proceed to pick an image for that block within the grid. We create a new HTML Image element, in-order to only draw it to the canvas after the image has successfully been downloaded by the browser.

There are two functions in the code above, that are not currently implemented; getColumns() and getRows() determine if this blocks dimensions will randomly occupy more than one column or row, respectively. In addition to the image onload function, we will implement these later in the post. For now, lets take a look at the main loop.

//rotate through array of images, pick next image
var src = images[img++%images.length];

//create a new image element
var el = new Image();

In-order to rotate through the image array, we simply post-increment the current image and apply a modular operator in-order to ensure that if the image array length is not equal to the sum of columns x rows then the images will simply repeat from the start. Additionally, we create a new HTML Image element to make use of the native onload function.

//push the images x & y values
el.dataset.x = c * column_unit;
el.dataset.y = r * row_unit;

To determine this blocks top left position within the grid, we simply multiply the determined values for both the singular column and row dimensions against the respective location of the currently considered block within the grid. Furthermore, we push these values to the images dataset object to ensure that they are readily accessible within the images onload function.

//set the images width and height, subtract margin
el.dataset.w = column_unit * columns - margin;
el.dataset.h = row_unit * rows - margin;

The resulting values for the variables columns and rows are discussed later in this post; essentially, they determine how many columns and rows that this image consumes (ie. 1, 2, 3). The current images width and height are determined by multiplying a singular column and row value against the respective columns and rows values. Finally, subtracting the determined margin value ensures that there will be a sufficient margin between this block and its neighbors.

To see the entire script up to this point, click here

The getColumns() and getRows() functions randomly determine how many columns and rows a single block will consume. The functions are relatively simplistic but form the core functionality of the algorithm. This post will only cover the implementation of double column and row images, but with a little determination and extrapolation, it won’t be too difficult to implement triple or even quadruple images.

F.prototype.getColumns = function(row, column) {
    var columns = this.grid[row].length;
    if (column + 1 < columns && !this.grid[row][column+1] && Math.random()*100 < 30) {
        return 2;
    }
    return 1;
}

F.prototype.getRows = function(row, column) {
    var rows = this.grid.length;
    if (row + 1 < rows && !this.grid[row+1][column] && Math.random()*100 < 30) {
        return 2;
    }
    return 1;
}

Determining whether an image can occupy an additional column or row, is a matter of checking to ensure that the image can expand rectangularly into adjacent grid blocks that are unoccupied. Additionally, to ensure that an image does not overflow the canvas, an additional check is performed to ensure there is enough remaining space on the grid to expand into. Finally, a random value is generated and compared to a threshold, to enforce a degree of randomness within the canvas. Each respective function returns an integer determining how many columns or rows that image will consume within the grid.

One of the final parts of this script, is defining the Image onload function. This function is responsible for resizing and cropping the image once it has loaded, as well as placing it into the main canvas at its determined and co-ordinates. Because it is possible for a portrait image to be assigned a landscape block, the script has to ensure that each image is resized appropriately, in-order to avoid any empty spaces caused by a sizing mismatch.

el.onload = function() {
    //create a canvas element
    var c = document.createElement('canvas');

    //set canvas element to width and height of image
    c.width = this.dataset.w;
    c.height = this.dataset.h;

    //grab context
    var l = c.getContext('2d');

    //grab width and height
    var w = this.dataset.w;
    var h = this.dataset.h;

    //determine aspect ratio
    var ar = this.width / this.height;

    //determine how best to render image based upon aspect ratio
    if (this.dataset.h > this.dataset.w) {
        //portrait image
        w = this.dataset.h * ar;

        if (w < this.dataset.w) {
            w = this.dataset.w;
            h = this.dataset.w / ar;
        }
    } else {
        //landscape image
        h = this.dataset.w / ar;

        if (h < this.dataset.h) {
            h = this.dataset.h;
            w = this.dataset.h * ar;
        }
    }

    //draw image to canvas
    l.drawImage(this, 0, 0, w, h);

    //push canvas image to main canvas element
    ctx.drawImage(c, this.dataset.x, this.dataset.y, this.dataset.w, this.dataset.h);

}

The function draws the image that has been loaded onto a separate canvas, so that any overflowing parts of the image are cropped, before it is placed into the main canvas. The width and height of the image, are calculated by determining the images natural aspect ratio here:

//determine aspect ratio
var ar = this.width / this.height;

//determine how best to render image based upon aspect ratio
if (this.dataset.h > this.dataset.w) {
    //portrait image
    w = this.dataset.h * ar;

    if (w < this.dataset.w) {
        w = this.dataset.w;
        h = this.dataset.w / ar;
    }
} else {
    //landscape image
    h = this.dataset.w / ar;

    if (h < this.dataset.h) {
        h = this.dataset.h;
        w = this.dataset.h * ar;
    }
}

The above code, facilitates the resizing of the image based upon the determined aspect ratio. If a portrait image is placed into a landscape block, the width attribute is chosen and the image is subsequently resized so that the images width matches that of the block. If a landscape image is chosen for a portrait image, the height attribute is subsequently chosen and the image resized accordingly.

There is also further checking going on, to ensure that any calculated values match the minimum dimensions for the block. Finally, once the images size has been determined, it is placed into the separate canvas and cropped to strip any parts of the image which overflow the blocks dimensions. Then using this canvas, the image is placed onto the main canvas and rendered.

The final script, can be viewed here: http://jsfiddle.net/davidkelley/sCdwh/13/

On a final note, whilst it is possible to render images from other domains within a canvas element, once you have done so it is no longer possible to extract the image itself for saving. There are ways to get around this, ie. using a back-end script to relay the image onto the same domain, but that is outside the scope of this post.


Viewing all articles
Browse latest Browse all 7

Latest Images

Trending Articles



Latest Images