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));