Skip to content
Snippets Groups Projects
qfq.fabric.js 22.31 KiB
/**
 * @author Benjamin Baer <benjamin.baer@math.uzh.ch>
 *
 * qfq.fabric.js: Integrates a callable fabric.js html5 canvas.
 * Currently in development for a special case, may be expanded
 * and integrated into qfq.
 *
 * Buttons and color switches are generated on basis of a json.
 * May be expanded to make it easy to create new brushes and stuff.
 *
 * Probably will soon open up "easy" ways for you to create plugins
 * that integrate into the qfq js client.
 *
 */

var QfqNS = QfqNS || {};

$(function (n) {
    n.Fabric = function() {
        this.parentElement = {};
        this.controlElement = {};
        this.emojiContainer = {};
        this.eventEmitter = new EventEmitter();
        this.textContainer = {};
        this.userTextInput = {};
        this.outputField = {};
        this.canvas = {};
        this.activeColor = {red: 0, green: 68, blue: 255, opacity: 1};
        this.brushSize = 2;
        this.borderSize = 5;
        this.textSize = 16;
        this.panning = false;
        this.moveMode = false;
        this.isZoomMode = false;
        this.userText = "";
        this.drawRectangleMode = false;
        this.drawTextBoxMode = false;
        this.emojiMode = false;
        this.isHighlightMode = false;
        this.isDrawingMode = true;
        this.isDown = false;
        this.origX = 0;
        this.origY = 0;
        this.userChangePossible = false;

        // Handles button states and generation of said buttons. Should be renamed.
        function ModeSettings() {
            this.qFabric = {};
            this.myButtons = [];
            this.myColors = [];
            this.myModes = {modes: [], currentMode: "", colors: []};
        }

        this.modeSettings = new ModeSettings();

        ModeSettings.prototype.initialize = function(qfqFabric, uri) {
            this.qFabric = qfqFabric;
            this.getMyModes(uri);
        };

        ModeSettings.prototype.getMyModes = function (uri) {
            var that = this;
            $.getJSON(uri, function(data) {
                that.setMyModes(data);
            });
        };

        ModeSettings.prototype.setUpButtons = function() {
            var $controlWrapper = this.qFabric.controlElement;
            var $buttonGroup = $("<div>", {class: "btn-group"});
            var that = this;
            this.myModes.modes.forEach(function(o) {
                var $button = $("<button>", {
                    type: 'button',
                    id: o.selector,
                    class: 'btn btn-default'
                });
                var $symbol = $("<span>", {
                    class: 'glyphicon ' + o.icon
                });
                $button.append($symbol);
                if (o.name === that.myModes.currentMode) {
                    $button.removeClass("btn-default");
                    $button.addClass("btn-primary");
                }
                that.myButtons.push($button);
                $buttonGroup.append($button);
                var modePressed = o.name;
                $button.on("click", function() {
                    that.qFabric.buttonPress(modePressed, $button)
                });
            });
            $controlWrapper.append($buttonGroup);


            var $colorSelector = $("<div>", {class: "color-picker"});
            $controlWrapper.append($colorSelector);
            this.myModes.colors.forEach(function (o) {
                var $button = $("<button>", {
                    type: 'button',
                    id: o.selector,
                    class: "text-bg-toggle",
                    style: "background-color: rgba(" + o.red +"," + o.green + "," + o.blue + ",1)"
                });
                $colorSelector.append($button);
                that.myColors.push($button);
                $button.on("click", function() {
                    that.qFabric.setColor(o, $button);
                })
            });
        };

        ModeSettings.prototype.setMyModes = function (data) {
            this.myModes = data;
            this.setUpButtons();
        };

        ModeSettings.prototype.activateMode = function (modeName, $button) {
            this.myModes.currentMode = modeName;
            var that = this;
            $.each(this.myModes.modes, function(i, o) {
                if (o.name == that.myModes.currentMode) {
                    console.log(o.requiresDrawing);
                    if (o.requiresDrawing) {
                        that.qFabric.canvas.isDrawingMode = true;
                        console.log(that.qFabric);
                    } else {
                        that.qFabric.canvas.isDrawingMode = false;
                    }

                    if (o.requiresSelection) {
                        that.qFabric.canvas.selection = true;
                    } else {
                        that.qFabric.canvas.selection = false;
                    }
                    if (o.isToggle) {
                        $button.removeClass("btn-default");
                        $button.addClass("btn-primary");
                        that.qFabric[o.toggle] = true;
                    }
                    if (o.hasToggleElement) {
                        console.log(o.toggleElement);
                        that.qFabric[o.toggleElement].slideToggle();
                    }
                } else {
                    that.deactivateMode(o);
                }
            });
            this.qFabric.canvas.renderAll();
        };

        ModeSettings.prototype.deactivateMode = function (o) {
            if (o.isToggle) {
                var $button = this.getButtonById(o.selector);
                $button.removeClass("btn-primary");
                $button.addClass("btn-default");
                this.qFabric[o.toggle] = false;
            }
            if (o.hasToggleElement) {
                this.qFabric[o.toggleElement].slideUp();
            }
        };

        ModeSettings.prototype.getButtonById = function (needle) {
            var needleInHaystack = {};
            this.myButtons.forEach(function (haystack) {
                if (haystack[0].id === needle) {
                    needleInHaystack = haystack;
                }
            });
            if (needleInHaystack === {}) {
                console.error("Button not found, id: " + string);
            } else {
                return needleInHaystack;
            }
        };

        ModeSettings.prototype.getModeByName = function (string) {
            $.each(this.myModes.modes, function(i, o) {
                if (o.name == string) {
                    return o;
                }
            });
            console.error("Couldn't find mode with name: " + string);
        };

        function Emojis() {
            this.qFabric = {};
            this.emojis = [];
        }

        this.emojiHandler = new Emojis();

        Emojis.prototype.initialize = function(qFabric, uri) {
            this.qFabric = qFabric;
            this.getFromJSON(uri);
        };

        Emojis.prototype.getFromJSON = function(uri) {
            var that = this;
            $.getJSON(uri, function(data) {
                that.setData(data);
            });
        };

        Emojis.prototype.setData = function(data) {
            if (data.emojis instanceof Array) {
                this.emojis = data.emojis;
                this.buildList();
            } else {
                console.error("Couldn't load emojis: data.emojis not an array");
            }
        };

        Emojis.prototype.buildList = function() {
            var that = this;
            var $container = this.qFabric.emojiContainer;
            var $emojiField = $("<div>");
            $container.append($emojiField);
            this.emojis.forEach(function (o) {
                $img = $("<img>", {
                    class: o.class,
                    src: o.url
                });
                $emojiField.append($img);
                $img.on("click", function() {
                    that.qFabric.emojiAdd(o.url);
                    console.log(o.name + " clicked");
                });
            })
        };


        fabric.Object.prototype.transparentCorners = false;
    };

    n.Fabric.prototype.initialize = function($fabricElement) {
        var jsonButtons = $fabricElement.data('buttons');
        var jsonEmojis = $fabricElement.data('emojis');
        var inputField = $fabricElement.data('control-name');
        this.parentElement = $fabricElement;
        this.outputField = $("#"+inputField);
        this.modeSettings.initialize(this, jsonButtons);
        this.emojiHandler.initialize(this, jsonEmojis);
        this.generateCanvas();
        if (this.outputField.val()) {
            this.canvas.loadFromJSON(this.outputField.val());
        }
        this.setBackground();
        this.setBrush();
        var that = this;
        setTimeout(function() {
           that.canvas.renderAll();
           that.userChangePossible = true;
        }, 1000);
    };

    n.Fabric.prototype.generateCanvas = function() {
        var canvas = document.createElement('canvas');
        var controlElement = $('<div>', {
            id: 'qfq-fabric-control'
        });
        var emojiContainer = $('<div>', {
            style: 'display: none;'
        });
        var textContainer = $('<div>', {
            style: 'display: none;'
        });
        var textArea = $('<textarea>', {
            class: 'fabric-text',
            placeholder: 'Write a text and then draw a textbox over the image'
        });
        var that = this;
        canvas.id = "c1";
        var $img = $(".qfq-fabric-image");
        var ratio = $img.height() / $img.width();
        canvas.width = this.parentElement.innerWidth();
        canvas.height = canvas.width * ratio;
        textContainer.append(textArea);
        this.parentElement.append(controlElement);
        this.parentElement.append(emojiContainer);
        this.parentElement.append(textContainer);
        this.controlElement = controlElement;
        this.emojiContainer = emojiContainer;
        this.textContainer = textContainer;
        this.userTextInput = textArea;
        this.parentElement.append(canvas);
        this.canvas = this.__canvas = new fabric.Canvas(canvas, {
            isDrawingMode: true,
            stateful: true,
            enableRetinaScaling: true
        });
        this.canvas.on('mouse:up', function (e) { that.defaultMouseUpEvent(e) });
        this.canvas.on('mouse:down', function (e) { that.defaultMouseDownEvent(e) });
        this.canvas.on('mouse:move', function (e) { that.defaultMouseMoveEvent(e) });
        this.canvas.on('mouse:out', function (e) { that.defaultMouseOutEvent(e) });
        this.canvas.on('after:render', function(e){ that.defaultChangeHandler(e) });
        $('.canvas').on('contextmenu', function(e) { that.defaultRightClickEvent(e) });
        $(window).resize(function() { that.resizeCanvas(); });
    };

    n.Fabric.prototype.resizeCanvas = function () {
        var newWidth = this.parentElement.innerWidth();
        if (newWidth != this.canvas.width) {
            var scaleMultiplier = newWidth / this.canvas.width;
            this.canvas.setWidth(this.parentElement.innerWidth());
            this.canvas.setHeight(this.canvas.getHeight() * scaleMultiplier);
            var objects = this.canvas.getObjects();
            for (var i in objects) {
                objects[i].scaleX = objects[i].scaleX * scaleMultiplier;
                objects[i].scaleY = objects[i].scaleY * scaleMultiplier;
                objects[i].left = objects[i].left * scaleMultiplier;
                objects[i].top = objects[i].top * scaleMultiplier;
                objects[i].setCoords();
            }
            this.setBackground();
            this.canvas.renderAll();
        }
    };

    n.Fabric.prototype.zoomCanvas = function(o, zoomCalc) {
        var zoom = this.canvas.getZoom() + zoomCalc;
        var zoomPoint = this.canvas.getPointer(o.e);
        this.canvas.zoomToPoint(zoomPoint, zoom);
        this.canvas.renderAll();
    };

    n.Fabric.prototype.setBackground = function (imageSelector) {
        /* Old code to load image From URL, stays here for reference
         var that = this;
        fabric.Image.fromURL(imagePath, function(img) {
            img.set({
                width: that.canvas.width,
                height: that.canvas.height,
                originX: 'left',
                originY: 'top',
                lockUniScaling: true,
                centeredScaling: true
            });
            that.canvas.setBackgroundImage(img, that.canvas.renderAll.bind(that.canvas));
        });*/
        //var getJSON = $('#fabric').data('images');
        //console.log(getJSON.images[0].selector);
        //var $image = document.getElementById(getJSON.images[0].selector);
        var $image = document.getElementsByClassName("qfq-fabric-image")[0];
        var img = new fabric.Image($image, {
            width: this.canvas.width,
            height: this.canvas.height,
            originX: 'left',
            originY: 'top',
            lockUniScaling: true,
            centeredScaling: true
        });
        this.canvas.setBackgroundImage(0, this.canvas.renderAll.bind(this.canvas));
        this.canvas.setBackgroundImage(img, this.canvas.renderAll.bind(this.canvas));
    };

    n.Fabric.prototype.deactivatePanning = function () {
        this.panning = false;
    };

    n.Fabric.prototype.emojiAdd = function(uri) {
        var that = this;
        fabric.Image.fromURL(uri, function(oImg) {
            oImg.set({
                left: 30,
                top: 30,
                height: 32,
                width: 32
            });
            that.canvas.add(oImg);
        });
    };

    n.Fabric.prototype.deactivateRectangleDrawing = function() {
        //this.drawRectangleMode = false;
        this.drawTextBoxMode = false;
        this.isDown = false;
        var rect = this.canvas.getActiveObject();
        rect.hasControls = true;
        this.canvas.selection = true;
        this.canvas.discardActiveObject();
        this.canvas.remove(rect);
        this.canvas.add(rect);
        this.canvas.renderAll();

    };

    n.Fabric.prototype.panImage = function(e) {
        if (this.panning && e && e.e) {
            var delta = new fabric.Point(e.e.movementX, e.e.movementY);
            this.canvas.relativePan(delta);
        }
    };

    n.Fabric.prototype.getActiveRGBA = function(changedOpacity) {
        var opacity = this.activeColor.opacity;
        if (changedOpacity) {
            opacity = changedOpacity;
        }
        return "rgba(" + this.activeColor.red + ","
            + this.activeColor.green + ","
            + this.activeColor.blue + ","
            + opacity + ")";
    };

    // Has to be changed for Fabric 2.0! Group selection are deprecated then, still needed
    // for Fabric 1.x and current Fabric 2.0 beta has a major drawing bug.
    n.Fabric.prototype.deleteActiveGroup = function() {
        var that = this;
        if (this.canvas.getActiveGroup()) {
            this.canvas.getActiveGroup().forEachObject(function(o) { that.canvas.remove(o) });
            this.canvas.discardActiveGroup().renderAll();
        } else {
            this.canvas.remove(this.canvas.getActiveObject());
        }
    };

    n.Fabric.prototype.freeDrawRectangleStart = function(o) {
        this.isDown = true;
        var that = this;
        var pointer = this.canvas.getPointer(o.e);
        this.origX = pointer.x;
        this.origY = pointer.y;
        var colorFill = this.getActiveRGBA(0.4);
        var colorBorder = this.getActiveRGBA(1);
        this.pointer = this.canvas.getPointer(o.e);
        var rect = new fabric.Rect({
            left: that.origX,
            top: that.origY,
            originX: 'left',
            originY: 'top',
            width: pointer.x - that.origX,
            height: pointer.y - that.origY,
            angle: 0,
            fill: colorFill,
            strokeWidth: this.borderSize,
            stroke: colorBorder,
            selectable: true,
            borderScaleFactor: 0,
            hasControls: false
        });
        this.canvas.add(rect);
        this.canvas.setActiveObject(rect);

        this.canvas.selection = false;
    };

    n.Fabric.prototype.freeDrawTextBoxStart = function(o) {
        this.isDown = true;
        var that = this;
        var pointer = this.canvas.getPointer(o.e);
        this.origX = pointer.x;
        this.origY = pointer.y;
        var colorFill = this.getActiveRGBA(1);
        pointer = this.canvas.getPointer(o.e);
        var textBox = new fabric.Textbox(this.userText, {
            left: that.origX,
            top: that.origY,
            originX: 'left',
            originY: 'top',
            width: pointer.x - that.origX,
            height: pointer.y - that.origY,
            angle: 0,
            backgroundColor: colorFill,
            padding: 5,
            editable: true
        });
        this.canvas.add(textBox);
        this.canvas.setActiveObject(textBox);
        this.canvas.selection = false;
    };

    // Used by both textbox and rectangle sizing. Maybe fusing those function later, since
    // they reuse code and have significant overlap.
    n.Fabric.prototype.resizeRectangle = function(o) {
        if (!this.isDown) return;
        var rect = this.canvas.getActiveObject();
        var pointer = this.canvas.getPointer(o.e);

        if(this.origX > pointer.x){
            rect.set({
                left: Math.abs(pointer.x)
            });
        }
        if(this.origY > pointer.y){
            rect.set({
                top: Math.abs(pointer.y)
            });
        }

        rect.set({
            width: Math.abs(this.origX - pointer.x)
        });
        rect.set({
            height: Math.abs(this.origY - pointer.y)
        });

        this.canvas.renderAll();
    };

    n.Fabric.prototype.setBrush = function() {
        var color = this.getActiveRGBA();
        this.canvas.freeDrawingBrush.color = color;
        this.canvas.freeDrawingBrush.width = this.brushSize;
    };

    // Default Canvas mouse events are currently strangely implemented.
    n.Fabric.prototype.defaultMouseUpEvent = function(e) {
        if (this.moveMode) {
            this.deactivatePanning();
        }

        if (this.drawRectangleMode || this.drawTextBoxMode) {
            this.deactivateRectangleDrawing();
        }

        if (this.isZoomMode) {
            this.zoomCanvas(e, 0.1);
        }
    };

    n.Fabric.prototype.defaultMouseOutEvent = function(e) {
        if (this.moveMode) {
            this.deactivatePanning();
        }
    };

    n.Fabric.prototype.defaultMouseDownEvent = function(e) {
        if (this.moveMode) {
            this.panning = true;
        }
        if (this.drawRectangleMode) {
            this.freeDrawRectangleStart(e);
        }
        if (this.drawTextBoxMode) {
            this.freeDrawTextBoxStart(e);
        }
    };

    n.Fabric.prototype.defaultMouseMoveEvent = function(e) {
        if (this.moveMode) {
            this.panImage(e);
        }
        if (this.drawRectangleMode || this.drawTextBoxMode) {
            this.resizeRectangle(e);
        }
    };

    n.Fabric.prototype.defaultRightClickEvent = function(e) {
        console.log("Text");
        if (this.isZoomMode) {
            this.zoomCanvas(e, -0.1);
            return false;
        }
    };

    // Calls additional functions on button press, could eventually be integrated to
    // the button/mode json. Talk about strange integration.
    n.Fabric.prototype.buttonPress = function(string, $button) {
        this.modeSettings.activateMode(string, $button);
        switch(string) {
            case "draw":
                this.draw();
                break;
            case "highlight":
                this.highlight();
                break;
            case "write":
            case "rectangle":
            case "move":
                break;
            case "delete":
                this.delete();
                break;
            default:
                console.error("unrecognized mode");
        }
    };

    n.Fabric.prototype.delete = function() {
       this.deleteActiveGroup();
    };

    n.Fabric.prototype.draw = function() {
        this.activeColor.opacity = 0.8;
        this.brushSize = 2;
        this.setBrush();
    };

    n.Fabric.prototype.highlight = function() {
        this.activeColor.opacity = 0.4;
        this.brushSize = 14;
        this.setBrush();
    };

    n.Fabric.prototype.rectangle = function() {
        this.drawRectangleMode = true;
    };

    n.Fabric.prototype.setColor = function(color, $button) {
        this.activeColor.red = color.red;
        this.activeColor.blue = color.blue;
        this.activeColor.green = color.green;
        this.setBrush();
    };

    n.Fabric.prototype.defaultChangeHandler = function (e) {
        var that = this;
        this.canvas.calcOffset();
        console.log("Changehandler called.");
        /* Important! Changes only possible after initialisation */
        if (this.userChangePossible) {
            console.log("User Change detected");
            this.outputField.val(JSON.stringify(that.canvas.toJSON()));
            var $saveButton = $("#save-button");
            $saveButton.removeClass("disabled");
            $saveButton.addClass($saveButton.data('class-on-change') || 'alert-warning');
            $saveButton.removeAttr("disabled");
        }
    };

    /*
    later
    $("#text-bg-submit").on("click", function() {
        n.fabric.userText = $("#text-user-value").val();
        n.fabric.drawTextBoxMode = true;
        $("#text-user-value").val('');
    });
    To be integrated, save to localstorage & at the and as ajax to server.
    Export to image is a maybe. Mainly here for future reference.

    $("#save1").on("click", function() {
        if (!n.fabric.saveOne) {
            n.fabric.saveOne = n.fabric.canvas.toJSON();
        } else {
            n.fabric.canvas.loadFromJSON(saveOne);
        }
    });

    $("#save2").on("click", function() {
        if (!n.fabric.saveTwo) {
            n.fabric.saveTwo = n.fabric.canvas.toJSON();
        } else {
            n.fabric.canvas.loadFromJSON(saveTwo);
        }
    });

    $("#save3").on("click", function() {
        if (!saveThree) {
            n.fabric.saveThree = canvas.toJSON();
        } else {
            n.fabric.canvas.loadFromJSON(saveThree);
        }
    });

    $("#export-svg").on("click", function() {
        var svg = n.fabric.canvas.toSVG();
        var png = n.fabric.canvas.toDataURL('png');
        $("#target-svg").html(svg);
        $("#target-png").attr("src", png);

    });

    */

}(QfqNS));