I am currently working with the awesome library Fabric.js which has thus far met all of my needs until now. I am attempting to create a layering system by adding new instances of fabric.Canvas each with their own objects/properties of course.
I got this idea from Kangax:
https://twitter.com/fabricjs/status/344822078228283392
I am having trouble switching between these layers (canvas instances).
For example, how would I bring one to the front?
// Create two instances of fabric.Cavnas
var canvas1 = new fabric.Canvas('canvas');
var canvas2 = new fabric.Canvas('canvas');
// On button click, bring canvas 2 to the front
$( "#switchbutton" ).click(function() {
// Attempts:
//fabric.Canvas.bringToFront(canvas2);
//fabric.Object.opacity(0);
fabric.Object.bringToFront(canvas2);
//canvas1.bringToFront(canvas2);
});
Method 1
As I understand you can't do that. You are trying to bring a object to front in canvas. You need to change z-index of canvas to bring it to front
Basically You need to have many different canvas elements and initialize fabricjs as many layers u have.
So for each new layer your create new canvas element and initialize fabricjs
var canvas2 = new fabric.Canvas('canvas2');
var canvas3 = new fabric.Canvas('canvas3');
And then u will have too keep track of your canvas layers and adjust z-index of your layer canvas and upper canvas
Method 1 (preferable)
Another option would be to keep one canvas and add custom attribute to your obejects "layerId".
So when u create a object :
canvas.add(new fabric.Circle({ layerId: activeLayerId, radius: 30, fill: '#f55', top: 100, left: 100 }));
And when you switch layers u just disable other layer objects
canvas.forEachObject(function(obj) {
obj.selectable = obj.layerId == activeLayerId ? true : false;
});
Or if u want to bring to front layer objects
canvas.forEachObject(function(obj) {
if(obj.layerId == activeLayerId) {
obj.bringForward();
}
});
Related
I'm building a new version of my online t-shirt designer using FabricJS to replace an existing Flash one that I created several years ago.
I've run into a problem that I need help with. All images that I work with are raster images. When I scale an image or make changes to it such as recoloring it, I make a call to the server to create the new image and then I need to be able to replace the existing image on the canvas.
The only way I've been able to figure out how to do it is by using the canvas method getActiveObject(). This works fine, but if you have multiple objects selected, you don't know which one to update.
When the image is originally added to the canvas, I'm attaching an onModified event handler to it. Only when the scale of the image changes, do I call the server side script to generate the new image. The code I'm currently using is listed below. Any help would be greatly appreciated.
Thanks
fabric.Image.fromURL(previewPath, function (img) {
var sprite = img.set({ left: leftX, top: topY, angle: angle, borderColor: 'black', cornerColor: 'red', cornerSize: 6, transparentCorners: false });
$scope.canvas.add(sprite);
$scope.canvas.renderAll();
sprite.on('modified', function () {
// Modified event is only executed for scaling
if (1 != sprite.scaleX.toString() || 1 != sprite.scaleY.toString()) {
fabric.Image.fromURL(previewPath, function (img) {
var newSprite = img.set({ left: leftX, top: topY, angle: angle, width: width, height: height });
// Replaces visible object on canvas
// Since new image is replacing old one, need to set the scale back to 1
sprite.scale(1);
$scope.canvas.renderAll();
});
}
});
Not sure if I've read it correctly, but wouldn't you be able to add an extra property to the fabric object when it is created indicating what it is and then use that to figure out which object to remove?
e.g. in the above, you'd do sprite.objectName = "tshirtSleeves", then when you get new t-shirt sleeves, get the fabric object with objectName = "tshirtSleeves" and remove that.
You could loop through all of the objects on the canvas, check that it's an image, and then make your image changes there as needed.
This option of course has it's possible efficiency issues if you have a lot of non-image objects on the canvas. But it's an option.
You do need to do a renderAll after this of course.
var objects = document.getElementById('canvasId').fabric._objects;
jQuery.each( objects, function( key, eachObject ) {
if( eachObject.type == "image" ) {
//Perform scale and image replacement as needed here
}
});
$scope.canvas.renderAll();
Actually turned out to be pretty simple. Just pass the new image url to the setSrc method of the image object as below.
sprite.setSrc(previewPath, function (img) {
sprite.scale(1);
$scope.canvas.renderAll();
});
Currently I create a small modeling tool using Snap SVG.
var p = Snap.path(pathString).attr({fill:'none', stroke:'black', strokeWidth:1});
creates connections between other elements.
Every element, including the paths are clickable. Since it is hard to click exactly the path I tried to create an invisible outline around the path using Snap.filter.shadow so that a click event could be triggered for the path:
var filter = this.paper.filter(Snap.filter.shadow(2,2,1));
filter.click(function() {
do_as_if_i_clicked_the_path();
});
p.attr('filter', filter);
But adding a click event to the filter doesn't work.
Is there any way to create an invisible outline that is connected to the path so I can add events on it?
One possibility that works quite well, is to clone the object you want to have an event on, and either scale it up, or make the strokeWidth larger, and then have a very low opacity (you can increase this just for testing and then reduce it to almost invisible).
If you add it to a group like this example, you can just put a drag on the group, so no need to remove it or anything (unless you need the clone not to be there sometimes, which makes sense).
jsfiddle
var s = Snap("#svg");
var myLine = s.path('M0,0L100,100').attr({ stroke: 'black', strokeWidth: 1 });
var myClone = myLine.clone().attr({ strokeWidth: 50, opacity: 0.01 });
var myGroup = s.g( myLine, myClone );
myGroup.drag();
Consider the following code:
heatmap = new OpenLayers.Layer.Heatmap( "Heatmap Layer", map, osm,
{visible: true, radius: radiusForScale[zoom], legend: {position: 'br',title: 'title'}},
{isBaseLayer: false, opacity: 0.3, projection: new OpenLayers.Projection("EPSG:4326")}
);
Background: heatmap.js is great for displaying population densities and such... however, I do not want heatmap.js to control the radius of my points. Instead, I wish to dynamically updated the radius based on a custom zoom function I have created, without having to destroy the heatmap and recreate it every time the zoom changes. Now, this is possible if you add the heatmap to a div as follows:
var config = {
element: document.getElementById("heatmapArea"),
radius: 30,
opacity: 50
};
//creates and initializes the heatmap
var heatmap = h337.create(config);
In this example, it's as simple as updating the JSON object and updating the display. However, this in only a static display assigned to a div, and therefore renders the vector layer useless. Has anyone had any success with this in OpenLayers??
On a side note: I've browsed through all the keys in the heatmap JSON string and there doesn't seem to be a way to change the zoom variable.
Figured it out... this isn't advertised in the documentation or anywhere on the internet that I could find.. however, here's how you control the radius in OpenLayers for those of you who need it. Let's keep it simple..
// create our heatmap layer
var heatmap = new OpenLayers.Layer.Heatmap(
"Heatmap Layer", map, osm, {visible: true, radius:50},
{isBaseLayer: false, opacity: 0.3, projection: new OpenLayers.Projection("EPSG:4326")}
);
map.addLayers([heatmap]);
Now add the data:
heatmap.setDataSet(data);
Here's the key. You can do pretty much anything with this list of commands:
this.set("radius",value); //****this one solved my problem
this.set("element",value);
this.set("visible",value); //deceiving! You have to access the canvas subclass to get it to work
this.set("max",value);
this.set("gradient",value);
this.set("opacity",value);
this.set("width",value);
this.set("height",value);
this.set("debug",value);
So, for example, create a zoom event so the radius changes based on your radius function. For me, it was as simple as scaling the radius based on the ratio screen width(pixels)/screen width(meters). So now in your click event:
on zoom change: //pseudocode
canvas = heatmap.heatmap.get('canvas'); //this is the subclass I spoke of above
if the zoom is above a certain value //pseudocode
then
canvas.style.display = 'block'; //show the heatmap... you can also use heatmap.toggle() but this gives you more control
heatmap.heatmap.set('radius',zoomfunction[map.zoom]); //this dynamically updates the radius!!
heatmap.updateLayer();
else
canvas.style.display = 'none';
heatmap.updateLayer();
I hope this helps someone 'cause it drove me crazy!
I'm trying to build a web app using KineticJS, I basically have a function which runs on load which adds an image to a canvas (stage in KineticJS). Each time the function runs though, it is adding the image again underneath the existing one, where as I need it to replace the one that is currently on the stage. I know it's something to do with me creating a new image object each time the function is loaded, but I'm confused as to why it's positioning is different (underneath). I have simulated the same event here: http://jsfiddle.net/UCvbe/ - If you click the button several times, the image duplicates below. The final function will be ran when a user selects a knocker, therefore the final function must accept a parameter eg 'silver' so that it knows to draw the silver knocker etc.
Any help will be greatly appreciated!
Your code looks like this:
$('#testBtn').click(function() {
var stageKnocker = new Kinetic.Stage({
container: "canvas-overlay",
width: 402,
height: 470
});
var layerKnocker = new Kinetic.Layer();
var imageObj = new Image();
imageObj.onload = function(){
var image = new Kinetic.Image({
x: 193,
y: 110,
image: imageObj,
width: 25,
height: 50,
draggable: true
});
layerKnocker.add(image);
stageKnocker.add(layerKnocker);
};
imageObj.src = "http://shop.windowrepairshop.co.uk/WebRoot/Store3/Shops/es115683_shop/49E6/ECF2/0C60/3750/10B1/50ED/8970/710D/V6GP_0020_gold_0020_plate_0020_vic_0020_urn_0020_LArge.jpg";
})
The first problem is that you are creating a new Stage each time a user clicks the button. Don't do that--you only want one stage.
You're also creating a new layer each time a user clicks the button. Don't do that. You only need & want a single layer.
Finally, you are creating a new Kinetic.Image each time a user clicks the button and adding it to the layer. That's fine! Keep doing that. But you also want to remove the old Kinetic.Image. If you don't, you will just keep adding images, not replacing them.
So before adding the Kinetic.Image to the layer, you want to remove any existing images from the layer: layerKnocker.removeChildren() will remove everything in the layer.
When you're done, you will have a single stage, a single layer, and at any point in time a single image.
I've switched over to kineticJS and I'm trying to have a background image repeat itself. Here is the code I am using:
var background_image = new Image();
background_image.onload = function() {
var image = new Kinetic.Image({
image: background_image,
width: this.width,
height: this.height
});
mainLayer.add(image);
stage.add(mainLayer); // now mainLayer is available
};
Now what I'd like to do is essentially what this tutorial does:
http://www.html5canvastutorials.com/tutorials/html5-canvas-patterns-tutorial/ :
That tutorial makes use of the canvas/context objects to repeat the image. I couldn't find an image repeat in the docs, so I was wondering if maybe I could access the main context element of my stage (or layer?) and then use that similar to the tutorial.
Try using a Rect with an image fill. Image fills are repeated by default. Here's an example:
http://www.html5canvastutorials.com/kineticjs/html5-canvas-set-fill-with-kineticjs/
You can also change the pattern repeat type, use offsets, and scale the pattern image
Cheers!