FabricJS - Group selection brings objects to front while selected - javascript

When I select a group of objects all them are automatically rendered on top of everything. This is a annoying behaviour that makes hard to select other elements that were on top of the selected elements.
There is an example of that happening here.
function newRect(index) {
return new fabric.Rect({
width: 100,
height: 100,
top: index * 30,
left: index * 30,
fill: '#' + (0x1000000 + (Math.random()) * 0xffffff).toString(16).substr(1, 6),
});
}
var canvas = new fabric.Canvas('canvas');
var rect0 = newRect(0);
canvas.add(rect0);
var rect1 = newRect(1);
canvas.add(rect1);
var rect2 = newRect(2);
canvas.add(rect2);
var group = new fabric.Group([rect0, rect1]);
canvas.setActiveGroup(group).renderAll();
setTimeout(function() {
canvas.discardActiveGroup().renderAll();
}, 5000);
When the two elements that are behind another are selected, they are rendered on top of the third one. But when they are deselected, (wait 5 seconds until the group is discarded) they are rendered properly behind the element on top.
Is there anyway to disable this behaviour? I would that the selected elements maintain there z order while selected and do not be on top of everything.

I think I've found the solution.
I need to put the add preserveObjectStacking: true option while initializing the canvas.
var canvas = new fabric.Canvas('canvas', {
preserveObjectStacking: true,
});
https://jsfiddle.net/fbgu7697/3/
It's working as I expected but it seems to have a weird bug. I'll open it on github.
UPDATE
The issue was already fixed in 1.6.1 version.
https://jsfiddle.net/1vawp9gu/

Related

pts.js: Speed up animation of grid cells?

I'm using Pts.js to create a grid of cells and then color these cells depending on their distance to the mouse pointer. My code is largely based on a demo from the official Pts.js Website.
Pts.quickStart("#pt", "#123");
//
let pts = [];
var follower = new Pt();
space.add({
start: (bound) => {
pts = Create.gridCells(space.innerBound, 40, 20);
follower = space.center;
},
//
animate: (time, ftime) => {
follower = follower.add(space.pointer.$subtract(follower).divide(20));
form.stroke("#123");
//
let rects = pts.map((p) => {
let color;
let mag = follower.$subtract(Rectangle.center(p)).magnitude();
let r = Rectangle.fromCenter(Rectangle.center(p), Rectangle.size(p));
//
if (mag >= 100) {
color = "#000"
} else {
color = "#f00"
}
//
form.fill(color).rect(r);
})
//
form.fillOnly("#fff").point(space.pointer, 10, "circle");
}
})
//
space.bindMouse().bindTouch().play();
#pt {
width: 800px;
height: 600px;
margin: 30px auto 0;
}
<script src="https://unpkg.com/pts#0.9.4/dist/pts.min.js"></script>
<div id="pt"></div>
The implementation works absolutely fine. But I'd like to increase the speed with which the »coloring« of the cells »follows« the cursor, i.e. reduce the delay with which the red space around the cursor is animated. Ideally, I'd like to have no delay.
I'm new to Pts.js, so still wrapping my head around the docs, and I can't find an option or explanation for how to control the animation's speed. If anyone could point me to what I need to do here, that'd be great.
It seems that this line is what controls the behavior of the red grid area:
follower = follower.add(space.pointer.$subtract(follower).divide(20));
The value supplied to .divide() controls the speed at which the red area follows the cursor. Changing its argument from 20 to 1 (or even removing .divide(20) entirely) causes the "following" behavior to be immediate.
(Though, if you intend to remove the capability for that behavior, I suspect that entire line could be simplified.)

How to implement a vertical scrollbar?

Does anyone know how to implement a vertical scrollbar for a vis.js timeline? I have read the visjs.org documentation, other threads here on stack overflow and on GitHub, but I still can't implement the scrollbar.
Should it be enough to write verticalScroll: true in the configuration for a vis.js timeline? This is what I have as configuration at the moment. Do I need to write it in some other way? Or do I need to implement the vertical scroll in a totally different way?
// Configuration for the Timeline
var options = {
width: '100%',
height: '100%',
minHeight: '300px',
padding: '0px',
orientation: 'top',
max: futureDate,
min: pastDate,
groupOrder: 'start',
zoomMin: '86400000',
margin: {item: {horizontal: 0, vertical: 5}, axis: 5},
verticalScroll: true,
zoomKey: 'ctrlKey'
};
A priori the options taken are good, It would be enough to reduce the height of the timeline directly in the options rather than to use "minHeight" in this case. Normally, this should bring up the scroll bar.
To check this, reduce the timeline height to 150 px in the options (e.g)
You can also generate a large number of groups so that they exceed the vertical left pannel capacity of timeline so that the vertical scrollbar displays.
UPDATED with minimal example adapted from "vis.js examples"
See also the timeline documentation on website for configuring options...
<html>
<head>
<title>Timeline | Vertical Scroll Option</title>
<!-- note: moment.js must be loaded before vis.js, else vis.js uses its embedded version of moment.js -->
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.js"></script>
<link rel="stylesheet" type="text/css" href="https://cdnjs.cloudflare.com/ajax/libs/vis/4.21.0/vis.min.css" />
</head>
<body>
<h1>Timeline vertical scroll option</h1>
<h2>With <code>
verticalScroll: true,
zoomKey: 'ctrlKey'</code>
</h2>
<div id="mytimeline1"></div>
<script>
// create groups
var numberOfGroups = 25;
var groups = new vis.DataSet()
for (var i = 0; i < numberOfGroups; i++) {
groups.add({
id: i,
content: 'Truck ' + i
})
}
// create items
var numberOfItems = 1000;
var items = new vis.DataSet();
var itemsPerGroup = Math.round(numberOfItems/numberOfGroups);
for (var truck = 0; truck < numberOfGroups; truck++) {
var date = new Date();
for (var order = 0; order < itemsPerGroup; order++) {
date.setHours(date.getHours() + 4 * (Math.random() < 0.2));
var start = new Date(date);
date.setHours(date.getHours() + 2 + Math.floor(Math.random()*4));
var end = new Date(date);
var orderIndex = order + itemsPerGroup * truck
items.add({
id: orderIndex,
group: truck,
start: start,
end: end,
content: 'Order ' + orderIndex
});
}
}
// specify options
var options = {
stack: true,
verticalScroll: true,
zoomKey: 'ctrlKey',
height: 200, // you can use also "300px"
start: new Date(),
end: new Date(1000*60*60*24 + (new Date()).valueOf()),
};
// create a Timeline
var container1 = document.getElementById('mytimeline1');
timeline1 = new vis.Timeline(container1, items, groups, options);
</script>
</body>
</html>
It sounds like you can get a scrollbar using overflow-y: scroll. Also, height: 100% will most likely never cause this to kick in (unless this element is contained within another element that has a set height) as the element that you are editing will keep growing in height rather than staying a certain height and have a scrollbar. So I would recommend removing height: 100% and using max-height instead (if your element isn't contained within another element), so if the element content grows to something larger than your max-height, it will begin to scroll. If you're looking to style that scrollbar, that's a whole different story.
https://www.w3schools.com/cssref/css3_pr_overflow-y.asp

How to determine the height and width of HTML user input and tell gridster to fit accordingly?

I'm currently using Gridster.js (http://gridster.net/) in combination with CKEditor.
Once the user saves their content with CKEditor, this content is put into the widget. However the widgets do not automatically resize themselves to fit the content, and while the user is able to resize it themselves, it would be more convienient for the userbase to have it be done for them the moment they press save.
I have tried a few things, but none to any avail. I'm having trouble getting the size of the current content, and then resizing the gridster respectively.
In my code, I have two values to work with. the gridster element (widget), and the value that will be put into it (contents). I have to determine the height of the contents. Once this is done successfully, I will be able to determine if my code for getting the x and y values work.
My current code looks like this:
// Initialization of the gridster element.
// The base dimensions are relevant to understand how we
// calculate the multipliers, later on.
gridster = $(".gridster > ul").gridster({
widget_margins: [10, 10],
widget_base_dimensions: [100, 50],
draggable: {
handle: 'header'
},
resize: {
enabled: true,
max_size: [20, 10],
min_size: [2, 1]
}
}).data('gridster');
And (the relevant bits of) my JavaScript class that handles saving and resizing:
// Saves the content from CKEditor to the gridster widget
this.save = function (data) {
var lastContents = this.default_html + data + '</div>';
$(this.editor).removeClass('gs-w-new');
this.resize_widget(this.editor, lastContents);
$(this.editor).html(lastContents);
this.modal.modal('hide');
};
/* #TODO: resize_widget function */
// if the new content from ckeditor is larger than the
// original size of the widget, we need to make it larger.
this.resize_widget = function(widgetId, contents) {
var element = $('<div>')
.addClass('fake-div-gs-w-resize')
/*
.fake-div-gs-w-resize {
position: absolute;
display: none;
height: auto;
width: auto;
white-space: nowrap;
}
*/
.css('display', 'block')
.html(contents);
var widget = $(widgetId);
var elementWidth = $(element).width(), // I am expecting this to return the width of the content, but it returns 0.
elementHeight = $(element).height(), // As you might imagine, this also returns 0.
width = widget.width(),
height = widget.height();
$(element).css('display', 'none');
console.log(widgetId, widget, width, height, elementWidth, elementHeight);
// this code never gets past here, because element{Height,Width} returns 0.
if (elementHeight > height || elementWidth > width) {
var width_multiplier = 100, // data-x = 1 === width_multiplier px
height_multiplier = 50; // from "widget_base_dimensions: [100, 50],"
var x = Math.round(width / width_multiplier),
y = Math.round(height / height_multiplier),
eX = Math.ceil(elementWidth / width_multiplier),
eY = Math.ceil(elementHeight / height_multiplier);
console.log("setting to x:" + eX + ", y:" + eY + " with width:" + width + ", height:" + height);
if (eX >= x && eY >= y)
gridster.resize_widget(widget, eX, eY);
}
};
Whilst I am not completely confident in my logic for determining the sizes; the main focus of this question is with determining the size of the HTML contents, as what I gathered from other SO posts did not seem to help in my case.
You need to actually add the element to the DOM for the width() and height() functions to work. In your example, the element is not added to the document.
See this JS Fiddle as an example https://jsfiddle.net/y1yf1zzp/10/
I had the same challenge, i.e. dynamic content appearing inside the new tile caused an overflow and appeared outside the tile boundaries. We used the '.scrollHeight' of the tile contents in combination with Zartus' code:
var contentHeight = $widgit.firstChild.scrollHeight;
var tileHeight = $widgit.firstChild.clientHeight;

How to select the objects by only clicking the actual contents by Fabric.js

I want to select the objects by clicking the actual contents. So I set the perPixelTargetFind=true and following the paragraph in the introduction:
"By default, all Fabric objects on canvas can be dragged by the bounding box. However, if you want different behavior — clicking/dragging objects only by its actual contents, you can use "perPixelTargetFind" property on an object. Just set it to true to get the desired behavior."
By doing this, I can drag the objects by the actual contents, but the objects can still be selected by clicking the bounding area.
Is it possible to clicking objects only by its actual contents?
Is this what you wanted?
for (var i = 0, len = 15; i < len; i++) {
fabric.Image.fromURL('../assets/ladybug.png', function(img) {
img.set({
left: fabric.util.getRandomInt(0, 600),
top: fabric.util.getRandomInt(0, 500),
angle: fabric.util.getRandomInt(0, 90)
});
img.perPixelTargetFind = true;
img.targetFindTolerance = 4;
img.hasControls = img.hasBorders = false;
img.scale(fabric.util.getRandomInt(50, 100) / 100);
canvas.add(img);
});
}
Basically, you assign perPixelTargetFind is true, and (hasControls & hasBorders) is false.

Fabric.js Ungroup items change their positions

Hi I'm trying to ungroup items using fabric.js, but everytime I try to ungroup them their position changes on the canvas.
I tried many technique and even tried to manually recalculate the positions but I was always off.
What seams to be the best technique is listed on this post
https://gist.github.com/msievers/6069778
Since the _restoreObjectsState is called by the group.destroy(), I used this instead (both gave me the same results)
I have a jsfiddle that shows that shows the weird behavior.
http://jsfiddle.net/vq3Lj0th/
var canvas = new fabric.Canvas('viewport');
var text1 = new fabric.IText('Text1', {
left: 0,
top: 0
});
var text2 = new fabric.IText('Text2', {
left: 0,
top: 125
});
var group = new fabric.Group([text1, text2], {
left: 150,
top: 100
});
canvas.add(group);
document.getElementById('ungroup').addEventListener('click' ,function ungroup() {
var destroyedGroup = group.destroy();
var items = destroyedGroup.getObjects();
items.forEach(function (item) {
canvas.add(item);
});
canvas.remove(group);
});
as a reference, what i'm trying to do is to ungroup the items and send them to another canvas to simulate group edition mode. i'm almost there but I can't figure how to ungroup things without losing their position.
Thanks for your help.
I found a way to make it work:
The bleeding edge version of fabric resolve this issue:
https://github.com/kangax/fabric.js/issues/1798

Categories