Create Photo Collage of Google Images

This time we'll use Fabric.js and the Google Picker API

canvas,fabricjs,google picker

Note: This is a continuation of the tutorial on the Google Picker API which can be found here.

In this part of the tutorial, we'll be taking the content from Google Picker, and placing it on a canvas, allowing users to rotate, size, and scale their content. This is all made possible through the Fabric.js library for canvas. The first thing to do is head over to fabric and grab the javascript file, and include it in your code. I'm going to set my HTML file like this:


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>COLLAGE</title>
    <link rel="stylesheet" href="css/main.css">
</head>

<body>
    <canvas id="canvas"></canvas>

    <div id="tools">
        <button type="button" class="picker-btn" data-type="photo">Add Photo</button>
        <button type="button" class="picker-btn" data-type="image">Add Image</button>
        <input type="text" placeholder="Type your text" id="input-text" />
        <button type="button" class="picker-btn" data-type="text">Add Text</button>
        <button type="button" class="picker-btn" data-type="draw">Add Drawing</button>
        <button type="button" class="picker-btn" data-type="save">Save</button>
    </div>

    <script src="http://code.jquery.com/jquery-latest.min.js" type="text/javascript"></script>
    <script src="js/libs/fabric.min.js"></script>

    <script src="js/canvas.js"></script>
    <script src="js/picker.js"></script>
    <script src="js/main.js"></script>
    <script>

    window.onload = function(){
        $.getScript( "https://apis.google.com/js/api.js?onload=onGoogleApiLoad"); //load google apis
    }

    function onGoogleApiLoad() { //initialize site
        mainModule.init();
    }

    </script>
</body>
</html>

Here I'm adding a stylesheet, including jQuery because I am lazy, followed by fabric. Then I'm going to create three custom javascript files so that I can separate out all my tasks. One for the google picker stuff, one for the canvas stuff, and one main one to handle anything else I haven't thought of. You don't have to structure your code this way, but it's good practice. Below that, I have a script tag with a window.onload call - that will make sure that nothing happens until everything has been fully loaded, again - not mandatory. Inside here, I'm calling jQuery's getScript() function, which is just a quick way to do an ajax call that dynamically inserts a script tag into your html file. I'm calling the google picker api, and passing it the name of a function to run once it's loaded - in this case onGoogleApiLoad. Below this is that function, which tells something called mainModule to init.

That mainModule is a class I've defined in main.js, so let's look at that next:


var mainModule = (function ($,window) {

  init = function() {
      canvasModule.init();
      pickerModule.init(); //https://developers.google.com/maps/documentation/javascript/examples/map-simple-async
  };

  return {
    init: init
  };

})(jQuery,window);

This javascript is written in the module pattern. Any functions which are listed in the "return" section, are publicly available. In my return object, I've listed the init function. If we look at the init function, we can see all that is happening here is that we tell the canvas module and picker module to initialize. These are defined in the canvas.js and picker.js files, respectively. We'll start by looking at picker, since we'll be picking our content before placing it on our canvas.


var pickerModule = (function($, window) {

    var developerKey = '';
    var clientId = '';
    var scope = ['https://www.googleapis.com/auth/photos'];
    var pickerApiLoaded = false;
    var oauthToken;

    init = function() {
        initPicker();
    };

    initPicker = function() { //https://developers.google.com/picker/docs/index
        gapi.load('auth', {
            'callback': pickerModule.onAuthApiLoad
        });
        gapi.load('picker', {
            'callback': pickerModule.onPickerApiLoad
        });
    };

    function onAuthApiLoad() {
        console.log('onAuthApiLoad');
        window.gapi.auth.authorize({
                'client_id': clientId,
                'scope': scope,
                'immediate': false
            },
            handleAuthResult);
    }

    function onPickerApiLoad() {
        console.log('onPickerApiLoad');
        pickerApiLoaded = true;
        createPicker();
    }

    function handleAuthResult(authResult) {
        console.log('handleAuthResult');
        if (authResult && !authResult.error) {
            oauthToken = authResult.access_token;
            createPicker();
        }
    }

    function createPicker() {
        console.log('createPicker');
        if (pickerApiLoaded && oauthToken) {
            addEventListeners();
        }
    }

    function addEventListeners() {
        $('.picker-btn').on('click', function() {
            onPickerButtonClick($(this));
        })
    }

    function onPickerButtonClick($btn) {

        if (pickerApiLoaded && oauthToken) {

            var pickerType = $btn.attr('data-type');
            var callbackFunction, viewId;

            canvasModule.disableDrawingMode();

            switch (pickerType) {
                case 'photo':
                    callbackFunction = addPickerPhoto;
                    viewId = google.picker.ViewId.PHOTOS;
                    break;
                case 'image':
                    callbackFunction = addPickerPhoto;
                    viewId = google.picker.ViewId.IMAGE_SEARCH;
                    break;
                case 'text':
                    canvasModule.addText();
                    break;
                case 'draw':
                    canvasModule.addDrawing();
                    break;
                case 'save':
                    canvasModule.saveCanvas();
                    break;
            }

            var picker = new google.picker.PickerBuilder().
            addView(viewId).
            setOAuthToken(oauthToken).
            setDeveloperKey(developerKey).
            setCallback(callbackFunction).
            build();
            picker.setVisible(true);
            console.log('picker created');

        }

    }

    function addPickerPhoto(data) {
        if (data[google.picker.Response.ACTION] == google.picker.Action.PICKED) {
            var doc = data[google.picker.Response.DOCUMENTS][0];
            var thumbs = data.docs[0].thumbnails;
            var imageURL = thumbs[thumbs.length - 1].url; //select the largest image returned
            canvasModule.addImage(imageURL);
        }
    }

    return {
        init: init,
        onAuthApiLoad: onAuthApiLoad,
        onPickerApiLoad: onPickerApiLoad
    };

})(jQuery, window);

This is more or less the same as the last tutorial. I've left out the api key and clientID in the code, so make sure you obtain those for you application, and input them in the developerKey and clientId variables at the top of the page. Below this we see the init function which has been called from main.js. This initializes the picker, waits for your authentication and login to google, and then adds listeners to the buttons on screen which allow you to choose what type of content you'll be picking. When one of those buttons is clicked, we detect what type it was in the onPickerButtonClick function based on its data-type attribute, then it triggers a picker window. The switch statement in onPickerButtonClick defines which picker method we'll be using (in this case PHOTOS or IMAGE_SEARCH, as well as a callback function to call once the user has picked something.

You'll notice that both the photo and image cases have the same callback function, addPickerPhoto. That's because whether we're doing an image search or searching our own photos on google+, the end result will still be the same, an image that gets placed on the canvas.

You'll also notice that the text and draw methods don't have callback functions or viewIds - that's because we won't be using picker for those, but rather just drawing things in fabric.js straight to canvas. In these cases, we're just calling functions within canvasModule, which is in canvas.js.

If you look at the addPickerPhoto, we're simply taking the data returned from the API, finding the largest thumbnail, and calling a method within canvas.js which will take care of adding it to the screen.

Now we're ready to look at new stuff - so here's canvas.js:


var canvasModule = (function($, window) {

    var canvas;

    init = function() {
        initCanvas();
    };

    initCanvas = function() {
        canvas = new fabric.Canvas('canvas');
        var windowDimensions = commonModule.getWindowDimensions();
        canvas.setWidth(windowDimensions.width);
        canvas.setHeight(windowDimensions.height);
    };

    addImage = function(url) {
        fabric.Image.fromURL(url, function(img) {
            img.set({
                left: 50,
                top: 100,
            });
            canvas.add(img).renderAll();
            canvas.setActiveObject(img);
        });
    };

    addText = function(){ //http://fabricjs.com/fabric-intro-part-2/#text
        var text = $('#input-text').val();
        var fabricText = new fabric.Text(text,
            { left: 100, top: 100, fontFamily: 'Times New Roman', textBackgroundColor: 'rgb(0,200,0)' });
        canvas.add(fabricText);
    };

    disableDrawingMode = function(){
        canvas.isDrawingMode = false;
    }

    addDrawing = function(){ //http://fabricjs.com/fabric-intro-part-4/#free_drawing
        canvas.isDrawingMode = true;
    };

    saveCanvas = function(){ //http://fabricjs.com/fabric-intro-part-3/#serialization

        var canvasSVG = canvas.toSVG();
        $('.canvas-container').replaceWith(canvasSVG);
        $('#tools').hide();

    };

    getWindowDimensions = function(){
      return { width: window.innerWidth, height: window.innerHeight }
    };

    return {
        canvas: canvas,
        init: init,
        addImage : addImage,
        addText : addText,
        addDrawing: addDrawing,
        saveCanvas: saveCanvas,
        disableDrawingMode : disableDrawingMode
    };

})(jQuery, window);

The setup for this file is the same - we're calling init, which calls initCanvas, which calls the constructor function for fabricjs - new fabric.canvas('canvas'). The parameter canvas in parenthesis, is the id of the canvas element in our code. We add the canvas, and make it the width and height of our screen. That's the only thing that gets called by default in this file. Then we wait for the user to pick something, at which point addImage, addText, or addDrawing get called.

Add image is fairly straightforward. We pass it the url for the thumbnail of the photo or image the user selected. Then we use fabric's image function to build a canvas representation of that image. Then we set its left and top position. I'm using the arbitrary values of 100 and 150 for left and top here, because I know I'm going to allow the users to drag these items wherever they want on screen anyway. Then I call the renderAll() function to make it show up on screen. canvas.setActiveObject tells fabric to make this item the active selection on screen. When you do this, you'll see that fabric puts a bunch of handles on the image. By dragging these you can scale or rotate the photo automatically, without having to write any code! If you want to move the image, just click in the middle of it and drag.

addTextgets the text input from the text field at the bottom of the page. It then places it on screen with the styling we've given it (Times New Roman, with a green background). Again, we can manipulate the text just like the photos.

addDrawing sets one simple variable - canvas.isDrawingMode = true. This just signifies that we can drag on blank sections of the canvas, and it will draw lines. If I wanted to change how those lines looked (add color, set stroke width, etc,) I could do so here, or I could do it up front within the canvas constructor function by passing a list of parameters.

After that, all that's left is to save our canvas. The saveCanvas function calls Fabric's canvas.toSVG() function, which turns everything on screen into a single SVG file. Then we replace the canvas itself with the SVG, and hide the tools, since we no longer have a canvas to draw on. You could save that SVG file to your hard drive, or add some functionality to trigger a prompt for the user to save it to their hard drive.

Lastly, I'll give you the CSS I'm using, so your buttons show up in the right place. Very simple:



body {
    margin: 0;
    padding: 0;
    width: 100%;
    height: 100%;
}

#canvas {
    width: 100%;
    height: 100%;
}

#tools {
    position: fixed;
    bottom: 0;
    height: 50px;
    width: 100%;
}

That's all there is to it! If you look at my repo - you'll find a slightly more involved example here in this git repo where you can also pick videos or record yourself, then add special event listeners to launch those videos or recordings when they're clicked on screen.