Making a Backbone.js view to draw objects on a canvas - javascript

I am making an application where different rectangles are painted on a canvas and I am trying to do it with Backbone. I have a model called box:
Box = Backbone.Model.extend({
defaults: {
x: 0,
y: 0,
w: 1,
h: 1,
color: "#FF9000",
linewidth: 3,
id: 0,
},
drawBox: function(ctx) {
ctx.fillStyle = "#FF9000";
ctx.globalAlpha = 0.1;
ctx.fillRect(this.get("x"), this.get("y"), this.get("w"), this.get("h")); //transparent box in the back
ctx.globalAlpha = 1;
ctx.strokeStyle = this.get("color");
ctx.lineWidth = this.get("linewidth");
ctx.strokeRect(this.get("x"), this.get("y"), this.get("w"), this.get("h")); //rectangle on top
}
});
And I also have a collection of this Box model:
BoxSet = Backbone.Collection.extend({
model: Box
});
What I have in mind is to have a view where I can put every Box model in the BoxSet collection on a canvas using the drawBox method in the Box model, but so far all the tutorials and examples deal with simple text templates and I cannot figure out how to acomplish this.
Any ideas on how could this be done using Backbone views?
Thanks in advance.

I would follow the separation of models and views offered by Backbone. Keep your models as data repositories :
var Box = Backbone.Model.extend({
defaults: {
x: 0,
y: 0,
w: 1,
h: 1,
color: "#FF9000",
linewidth: 3
// don't define a default id, that leads to strange behaviors
}
});
var BoxSet = Backbone.Collection.extend({
model:Box
});
And define the views to render the different pieces on a canvas:
var BoxView = Backbone.View.extend({
render: function() {
var model = this.model, ctx = this.options.ctx;
ctx.fillStyle = "#FF9000";
ctx.globalAlpha = 0.1;
ctx.fillRect(
model.get("x"), model.get("y"),
model.get("w"), model.get("h")
);
ctx.globalAlpha = 1;
ctx.strokeStyle = model.get("color");
ctx.lineWidth = model.get("linewidth");
ctx.strokeRect(
model.get("x"), model.get("y"),
model.get("w"), model.get("h")
);
}
});
var SetView= Backbone.View.extend({
initialize: function() {
this.listenTo(this.collection, "all", this.render);
},
render: function() {
var canvas = this.el, ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, canvas.width, canvas.height);
this.collection.each(function(model) {
var view = new BoxView({ctx: ctx, model: model});
view.render();
})
}
});
And finally instantiate and render:
var c = new BoxSet();
c.add({x: 150, y: 150, w: 100, h: 100});
c.add({x: 10, y: 10, w: 100, h: 100});
var v = new SetView({
el: $("canvas"),
collection : c
});
v.render();
A Fiddle to view those two nice squares http://jsfiddle.net/JB9yg/
Another one where a change to the collection leads to re-rendering http://jsfiddle.net/JB9yg/1/
This example can probably be built upon to provide cleaner manipulations, but that should get you started.

Also you may try to use Backbone.KineticView plugin to add canvas support to Backbone.
It works via KineticJS, so may also use all power of event delegation for canvas nodes.
Example:
var MyView = Backbone.KineticView.extend({
// build Kineticjs object, then return it.
el : function(){
var rect = new Kinetic.Rect({
x : 100,
y : 100,
width : 50,
height : 50,
fill : 'green',
id : 'rect'
});
var circle = new Kinetic.Circle({
x : 200,
y : 100,
radius : 50,
fill : 'red',
name : 'circle'
});
var group = new Kinetic.Group();
group.add(rect).add(circle);
return group;
},
// setup events
events : {
'click #rect' : function(){
console.log("on rectangle clicked");
},
'mouseover .circle' : 'onMouseOverCircle'
},
onMouseOverCircle : function(){
console.log('Mouse is over circle');
},
render : function(){
// this.$el - cached kineticjs object.
this.options.layer.add(this.$el);
layer.draw();
}
});
var stage = new Kinetic.Stage({
container : 'container',
width : 300,
height : 300
});
var layer = new Kinetic.Layer();
stage.add(layer);
view = new MyView({layer:layer});
view.render();

Related

How can I reload custom methods to a de-serialized FabricJS object?

I've been getting familiar w/ FabricJS on a project.
I have a custom class that extends fabric.Polyline. This custom class has many custom methods. My problem is : When I serialize the whole canvas and later on load it again with loadFromJSON(), the custom methods are not there anymore...
I noticed that the object before/after serialization are not exactly the same. Indeed, we can see that after serialization the method myCustomClassMethod() is not in the object's __proto__ anymore :
I know that loadFromJSON() has a reviver() method for re-adding event listeners, but how can I re-add methods? The tutorials say that I need to add a fromObject() method to save/restore objects, I tried playing with that but no luck.
It might also be that my de-serialized object is a "regular" object, and not a fabricjs object instance. But I haven't seen anywhere how to make it a fabric object again.
MRE down below. As you can see, method works if called before canvas reloading, but not if called afterwards.
// create a wrapper around native canvas element (with id="treeCanvas")
var canvas = new fabric.Canvas('treeCanvas', {
allowTouchScrolling: true,
});
// un-comment log below to inspect objects
canvas.on('mouse:up', function (e) {
if (e.target != null) {
//console.log(e.target);
}
});
// My TreeNode custom class
var TreeNode = fabric.util.createClass(fabric.Polyline, {
initialize: function (X, Y, armsArray, options) {
options || (options = {});
this.callSuper('initialize', options);
this.X = X;
this.Y = Y;
this.armsArray = armsArray;
this.set({ width: 160, height: 50, originX: 'center', originY: 'center' });
this.set({ left: this.X, top: this.Y, fill: 'rgba(255, 255, 255, 0)', stroke: 'black', selectable: true });
this.setCoords();
this.set({ pathOffset: { x: 0, y: 25 } });
for (point of this.armsArray) {
this.points.push({ x: point[0], y: point[1] });
this.points.push({ x: 0, y: 0 });
}
},
_render: function (ctx) {
this.callSuper('_render', ctx);
},
// this is the method that disappears after serializing
myCustomClassMethod: function () {
this.set({ stroke: 'green' });
canvas.renderAll();
}
});
// Adding an instance of my custom class to canvas
var node1 = new TreeNode(100, 100, [[-80, 50], [80, 50]], []);
canvas.add(node1);
canvas.renderAll();
// node1.myCustomClassMethod(); // method works before serializing + deserializing
var stringifiedCanvas = JSON.stringify(canvas);
canvas.loadFromJSON(stringifiedCanvas, canvas.renderAll.bind(canvas));
node1.myCustomClassMethod(); // method DOES NOT WORK anymore after deserializing
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.3.1/fabric.min.js"></script>
<div class="container">
<canvas id="treeCanvas" width="500" height="300"></canvas>
</div>
<script src="canvas.js"></script>
I am very far from being a fabricjs expert and I do things somewhat differently but you were missing a few important parts (marked with "** CHANGE" in the code below)
Hope this is of some help!
// create a wrapper around native canvas element (with id="treeCanvas")
var canvas = new fabric.Canvas('treeCanvas', {
allowTouchScrolling: true,
});
// un-comment log below to inspect objects
canvas.on('mouse:up', function(e) {
if (e.target != null) {
//console.log(e.target);
}
});
// ** CHANGE: define Treenode on the fabric instance
fabric.TreeNode = fabric.util.createClass(fabric.Polyline, {
// ** CHANGE: define the type
type: 'treeNode',
initialize: function(X, Y, armsArray, options) {
options || (options = {});
this.callSuper('initialize', options);
this.X = X;
this.Y = Y;
this.armsArray = armsArray;
this.set({
width: 160,
height: 50,
originX: 'center',
originY: 'center'
});
this.set({
left: this.X,
top: this.Y,
fill: null,
stroke: 'black',
selectable: true
});
this.setCoords();
this.set({
pathOffset: {
x: 0,
y: 25
}
});
for (point of this.armsArray) {
this.points.push({
x: point[0],
y: point[1]
});
this.points.push({
x: 0,
y: 0
});
}
},
_render: function(ctx) {
this.callSuper('_render', ctx);
},
// ** CHANGE: export the custom method when serializing
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
myCustomClassMethod: this.myCustomClassMethod
});
},
myCustomClassMethod: function(x, y, color) {
// console.log("myCustomClassMethod")
// console.log(this.myCustomClassMethod)
this.set({
left: x,
top: y,
stroke: color,
strokeWidth: 10
});
canvas.renderAll();
}
});
fabric.TreeNode.fromObject = function(object, callback) {
// console.log(object)
};
var node1 = new fabric.TreeNode(100, 100, [
[-80, 50],
[80, 50]
], []);
canvas.add(node1);
canvas.renderAll();
// DESERIALIZE
var stringifiedCanvas = JSON.stringify(canvas);
canvas.loadFromJSON(stringifiedCanvas, canvas.renderAll.bind(canvas));
// ** CHANGE: need to get a reference to the restored object
var restored = canvas.getObjects()[0]
///console.log(restored)
restored.myCustomClassMethod(200, 50, "red");
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/4.3.1/fabric.min.js"></script>
<div class="container">
<canvas id="treeCanvas" width="500" height="300"></canvas>
</div>
<script src="canvas.js"></script>

Split HTML Canvas into rows with JavaScript

I'm trying to create an animated glitch effect using JavaScript and HTML Canvas. I have managed to draw my image onto a canvas and want to write a separate function that later splits this image into rows so they can be animated individually.
I call my function to create the canvas...
var logo_text = "logo_text.png";
drawOnCanvas(logo_text, mc_logo_text, 0, 0, 245, 60);
And here's what the function looks like...
function drawOnCanvas(image, divID, posX, posY, wid, hei) {
//////////// CREATE CANVAS /////////////
var canvas = document.createElement("CANVAS"),
ctx = canvas.getContext("2d");
canvas.width = wid;
canvas.height = hei;
canvas.id = divID.id+"_canvas";
divID.appendChild(canvas);
//////////// DRAW IMAGE /////////////
var img = new Image();
img.src = image;
img.onload = function () {
ctx.drawImage(img, posX, posY, wid, hei);
} }
This function works fine and draws the image where I need it. I just can't figure out the best way to split this into rows.
Try this!
You just need to edit the settings in each object of _config array.
drawOnCanvas() will do same times equal the object's number in _config.
By the way, the scripts need to import below the DIVs , so document.getElementById could find them.
<body>
<div id="mc_logo_text1"></div>
<div id="mc_logo_text2"></div>
<div id="mc_logo_text3"></div>
<script>
// Simply edit all settings here
var _config = [
{
src: 'logo_text.png',
domDiv: 'mc_logo_text1',
position: {x: 0, y: 0},
size: {w: 245, h: 60}
},
{
src: 'logo_text.png',
domDiv: 'mc_logo_text2',
position: {x: 0, y: 0},
size: {w: 245, h: 60}
},
{
src: 'logo_text.png',
domDiv: 'mc_logo_text3',
position: {x: 0, y: 0},
size: {w: 245, h: 60}
}
]
/////// No Need to modify ////////
var drawOnCanvas = function(config) {
var canvas = document.createElement("CANVAS");
var ctx = canvas.getContext("2d");
//Define which DIV to include new Canvas
var domTaget = document.getElementById(config.domDiv);
// Create Canvas & give new ID name
function _canvasInit() {
canvas.width = config.size.w;
canvas.height = config.size.h;
canvas.id = domTaget.id + "_canvas";
domTaget.appendChild(canvas);
}
// draw images into canvas
function _drawImgs() {
var img = new Image();
img.src = config.src;
img.onload = function() {
ctx.drawImage(img, config.position.x, config.position.y, canvas.width, canvas.height);
}
}
// Do all functions
_canvasInit();
_drawImgs();
}
// Each object in _config array do drawOnCanvas function,
for (var i = 0; i < _config.length; i = i + 1) {
drawOnCanvas(_config[i]);
}
/////// No Need to modify ////////
</script>
</body>

FabricJS optical look of lines

I am working at a project with fabric js. I tried to minimize my problem, so I'm hoping that the code isn't too messed up.
I am creating some Objects which are linked with each other:
A Line, which contains a Start and an Endpoint
A Circle, which is StartPoint of 1 line and Endpoint of another line
with this combination i can create different shapes(like a polygon) and modify my move-functions for them too.
When a Circle is dragged, the related Lines are scaling and moving too. (in my code you can move the lines too and the shape is resized after that, but i didnt put it into this example, bc this short extract should be enough to show what my problem is.)
I got a little example in jsfiddle: https://jsfiddle.net/bxgox7cr/
When you look at the ends of the lines, you can clearly see a cut, so the eye soon recognize, that this is not a connected shape but rather some lines which are close to each other. Is there a way to modify the look of the lines, that the shape looks "closed"?
Here is my code, i tried to put some comments, that it is easy to read:
var canvas = new fabric.Canvas('canvas');
fabric.Object.prototype.originX = fabric.Object.prototype.originY = 'center';
document.getElementById("canvas").tabIndex = 1000;
/** ------------creating a Line Object, which contains a start and an endpoint ------------**/
fabric.LineWithPoints = fabric.util.createClass(fabric.Line, {
initialize: function(points, options) {
options || (options = {});
this.callSuper('initialize', points, options);
options &&
this.set('type', options.type),
this.set('name', options.name),
this.set('start_point', options.start_point),
this.set('end_point', options.end_point),
this.set('current_x', options.current_x),
this.set('current_y', options.current_y)
},
setStartPointAndEndPoint: function(start_point, end_point) {
this.set({
start_point: start_point,
end_point: end_point
});
},
setValues: function(new_x1, new_y1, new_x2, new_y2) {
// console.log(this);
this.set({
x1: new_x1,
x2: new_x2,
y1: new_y1,
y2: new_y2
});
this.setCoords();
}
});
/**--- modifie the circle element, adding new functions for the movement of the object-------*/
fabric.LinePoint = fabric.util.createClass(fabric.Circle, {
initialize: function(options) {
options || (options = {});
this.callSuper('initialize', options);
options &&
this.set('subtype', 'line_point'),
this.set('x', this.left),
this.set('y', this.top)
},
setPointCoordinates: function(new_left, new_top) {
this.set({
x: new_left,
y: new_top,
left: new_left,
top: new_top
});
this.setCoords();
},
move: function(new_left, new_top) {
var wall_1 = this.line1;
var wall_2 = this.line2;
this.setPointCoordinates(new_left, new_top);
wall_1.setValues(wall_1.x1, wall_1.y1, this.getLeft(), this.getTop());
wall_2.setValues(this.getLeft(), this.getTop(), wall_2.x2, wall_2.y2);
canvas.renderAll();
},
});
/**------------------- Moving Function------------------------------------------------- */
canvas.on('object:moving', function(event) {
var object = event.target;
if (object.subtype == "line_point") {
object.move(object.getLeft(), object.getTop());
}
});
/**------------------------------ create functions for the objects -----------------------*/
function newCircleObject(left, top, wall_1, wall_2) {
var circle = new fabric.LinePoint({
left: left,
top: top,
strokeWidth: 2,
radius: 15,
fill: 'grey',
stroke: 'black',
opacity: 0.1,
perPixelTargetFind: true,
subtype: 'line_point',
includeDefaultValues: false
});
circle.hasControls = false;
circle.hasBorders = false;
circle.line1 = wall_1;
circle.line2 = wall_2;
return circle;
}
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
/**------------------------------ adding the shapes--------------------------------*/
var wall_1 = newWallObject([100, 100, 100, 500]);
var wall_2 = newWallObject([100, 500, 500, 500]);
var wall_3 = newWallObject([500, 500, 500, 100]);
var wall_4 = newWallObject([500, 100, 100, 100]);
var end_point_1 = newCircleObject(wall_1.x1, wall_1.y1, wall_4, wall_1);
var end_point_2 = newCircleObject(wall_2.x1, wall_2.y1, wall_1, wall_2);
var end_point_3 = newCircleObject(wall_3.x1, wall_3.y1, wall_2, wall_3);
var end_point_4 = newCircleObject(wall_4.x1, wall_4.y1, wall_3, wall_4);
wall_1.setStartPointAndEndPoint(end_point_1.name, end_point_2.name);
wall_2.setStartPointAndEndPoint(end_point_2.name, end_point_3.name);
wall_3.setStartPointAndEndPoint(end_point_3.name, end_point_4.name);
wall_4.setStartPointAndEndPoint(end_point_4.name, end_point_1.name);
canvas.add(wall_1, wall_2, wall_3, wall_4, end_point_1, end_point_2, end_point_3, end_point_4);
Add strokeLineCap: 'round',:
function newWallObject(coords) {
var wall = new fabric.LineWithPoints(coords, {
stroke: 'black',
strokeWidth: 6,
lockScalingX: true,
lockScalingY: true,
perPixelTargetFind: true,
strokeLineCap: 'round',
subtype: 'line',
type: 'line',
padding: 10,
includeDefaultValues: false
});
wall.hasControls = false;
wall.hasBorders = false;
return wall;
}
I looked up: http://fabricjs.com/docs/fabric.Object.html#strokeLineCap

toSVG method not working on extended class with Fabric.js

I'm working on a project with Fabric.js.
Now I need to create a custom class and to export it to SVG.
I'm using Fabric.js tutorial to begin:
http://www.sitepoint.com/fabric-js-advanced/
Here is the javascript code:
var LabeledRect = fabric.util.createClass(fabric.Rect, {
type: 'labeledRect',
initialize: function(options) {
options || (options = { });
this.callSuper('initialize', options);
this.set('label', options.label || '');
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
label: this.get('label')
});
},
_render: function(ctx) {
this.callSuper('_render', ctx);
ctx.font = '20px Helvetica';
ctx.fillStyle = '#333';
ctx.fillText(this.label, -this.width/2, -this.height/2 + 20);
}
});
var canvas = new fabric.Canvas('container');
var labeledRect = new LabeledRect({
width: 100,
height: 50,
left: 100,
top: 100,
label: 'test',
fill: '#faa'
});
canvas.add(labeledRect);
document.getElementById('export-btn').onclick = function() {
canvas.deactivateAll().renderAll();
window.open('data:image/svg+xml;utf8,' + encodeURIComponent(canvas.toSVG()));
};
Here the HTML:
<canvas id="container" width="780" height="500"></canvas>
Export SVG
Here is my jsfiddle..
http://jsfiddle.net/QPDy5/
What I'm doing wrong?
Thanks in advance.
Every "class" in Fabric (rectangle, circle, image, path, etc.) knows how to output its SVG markup. The method responsible for it is toSVG. You can see toSVG of fabric.Rect, for example, or fabric.Text one.
Since you created subclass of fabric.Rect here and didn't specify toSVG, toSVG of the next object in prototype chain is used. The next "class" in the inheritance chain happens to be fabric.Rect, so you're seeing a result of fabric.Rect.prototype.toSVG.
The solution is simple: specify toSVG in your subclass. Theoretically, you would need to combine the code from fabric.Rect#toSVG and fabric.Text#toSVG, but to avoid repetition and keep things maintainable, we can utilize a bit of a hack:
toSVG: function() {
var label = new fabric.Text(this.label, {
originX: 'left',
originY: 'top',
left: this.left - this.width / 2,
top: this.top - this.height / 2,
fontSize: 20,
fill: '#333',
fontFamily: 'Helvetica'
});
return this.callSuper('toSVG') + label.toSVG();
}
The "fontSize", "fill", and "fontFamily" should ideally be moved to an instance-level property of course, to avoid repeating them there.
Here's a modified test — http://jsfiddle.net/fabricjs/QPDy5/1/
What #Sergiu Paraschiv suggested with a group is another just-as-viable solution.
Problem is there's no SVG equivalent to fillText so FabricJS seems to ignore it.
My workaround is to use a fabric.Group and build an inner Rect and Text
var LabeledRect = function(options) {
var rect = new fabric.Rect({
width: options.width,
height: options.height,
fill: options.fill
});
var label = new fabric.Text(options.label);
var group = new fabric.Group([rect, label]);
group.left = options.left;
group.top = options.top;
group.toSVG = function() {
var e=[];
for(var t = 0; t < this._objects.length; t++)
e.push(this._objects[t].toSVG());
return'<g transform="'+this.getSvgTransform()+'">'+e.join("")+"</g>"
};
return group;
};
The reason I overwrote toSVG is because the default implementation cycles through the children in reverse order (for(var t=this._objects.length;t--;)). I have no clue why it does that, but the text renders underneath the rectangle.

Interactive drawing with kineticjs

I'd like to draw a rectangle with click and drag. How can I do this ? Where do I have to put my click event listener ? On the stage or on the layer ? I have the following code but it doesn't work :
stage = new Kinetic.Stage({...})
layer = new Kinetic.Layer({...})
stage.add(layer)
stage.on('click', function() {
var pos = stage.getMousePosition();
var rect = new Kinetic.Rect({
x: pos.x,
y: pos.y,
width: 10,
height: 10,
});
layer.add(rect);
layer.draw();
})
Thanks.
As far as i know there is no "click" event on stage in kineticjs. You should use something like this:
stage.getContainer().addEventListener('mousedown', function(evt) {});
Link to a fiddle that shows what I've been working on:
http://jsfiddle.net/robtown/SGQq7/22/
It's a set of drawing tools using KineticJS and Sketch.js
You need to select "make sketch" to draw freehand and then "copy sketch to Kinetic" to copy your sketch into the kinetic stage. Select "Make rectangle" make a rectangle.
I need to include code to post this so here's the code for when you select the "Make Rectangle" button:
$('#makeRect').click(function (e) {
followRect = new Kinetic.Rect({
width: 120,
height: 40,
x: -200,
y:-200,
stroke: 'red',
strokeWidth: 3
});
drawLayer.setVisible(true);
drawLayer.add(followRect);
drawLayer.draw();
makeRect = true;
drawLayer.on("mousemove", function (e) {
if (makeRect) {
followRect.setX(e.x+5);
followRect.setY(e.y+5);
drawLayer.draw();
}
});
This creates a rectangle that follows the mouse until you click on the canvas, then it drops the rectangle into the Redlines layer of the stage:
drawLayer.on("mousedown", function (e) {
//for (var f = 0 ; f < 1; f++) {
//alert(e.length);
if (makeRect) {
addToRedlineLayer(e.x, e.y);
}
//}
followRect.setX(-200);
drawLayer.setVisible(false);
return;
});
I had the exact same problem, and indeed the method of Guilherme works greatly.
But there's a simple alternative: create a transparent Rect (Kinetic rectangle) the same size as the canvas:
<script type='text/javascript'>//<![CDATA[
window.onload=function(){
function writeMessage(messageLayer, message) {
var context = messageLayer.getContext();
messageLayer.clear();
context.font = '18pt Calibri';
context.fillStyle = 'black';
context.fillText(message, 10, 25);
}
var stage = new Kinetic.Stage({
container: 'container',
width: 578,
height: 200
});
var shapesLayer = new Kinetic.Layer();
var messageLayer = new Kinetic.Layer();
var rect = new Kinetic.Rect({
x:0,
y:0,
width:stage.getWidth(),
height:stage.getHeight(),
stroke:0
});
rect.on('mousemove', function() {
var mousePos = stage.getMousePosition();
var x = mousePos.x;
var y = mousePos.y;
writeMessage(messageLayer, 'x: ' + x + ', y: ' + y);
});
stage.getContainer().addEventListener('mouseout', function(evt) {
writeMessage(messageLayer, '');
});
shapesLayer.add(rect);
stage.add(shapesLayer);
stage.add(messageLayer);
}//]]>
</script>
The above code will print the x and y position of the mouse when you hover it over the canvas (a div with id "container"). You of course need to load the KineticJS library before using this code.

Categories

Resources