I have designed a ruler using fabric.js and when the user mouses over the specific part of the ruler I want to print to the screen the X-coordinate of the ruler (i.e. not the screen coordinate). Right now the ruler canvas starts at position 37 and ends at 726 relative to the ruler, but the ruler goes from 1 to 4600 (it will always start at 1 but the length of the ruler can change). How to transform the mouse coordinates to accurately reflect it's position on the ruler? Here is the code:
var canvas = new fabric.Canvas('canvas');
line_length = input_len;
adjusted_length = (line_length / 666) * 100;
canvas.add(new fabric.Line([0, 0, adjusted_length, 0], {
left: 30,
top: 0,
stroke: '#d89300',
strokeWidth: 3
}));
$('#canvas_container').css('overflow-x', 'scroll');
$('#canvas_container').css('overflow-y', 'hidden');
drawRuler();
function drawRuler() {
$("#ruler[data-items]").val(line_length / 200);
$("#ruler[data-items]").each(function () {
var ruler = $(this).empty(),
len = Number($("#ruler[data-items]").val()) || 0,
item = $(document.createElement("li")),
i;
ruler.append(item.clone().text(1));
for (i = 1; i < len; i++) {
ruler.append(item.clone().text(i * 200));
}
ruler.append(item.clone().text(i * 200));
});
}
canvas.add(new fabric.Text('X-cord', {
fontStyle: 'italic',
fontFamily: 'Hoefler Text',
fontSize: 12,
left: 0,
top: 0,
hasControls: false,
selectable: false
}));
canvas.on('mouse:move', function (options) {
getMouse(options);
});
function getMouse(options) {
canvas.getObjects('text')[0].text =
"X-coords: " + options.e.clientX ; //+ " Y: " + options.e.clientY;
canvas.renderAll();
}
Use the getPointer merthod on canvas Instance.
In your case it should be canvas.getPointer(options.e)which returns an object with x and y properties which represent pointer coordinates relative to canvas.
Related
I have a function where the rect, on scaling, cannot pass from the 20px limits on width or height, and its working fine.
document.getElementById("rectanglebtn").onclick = () => {
var rect = new fabric.Rect({
fill: fillcolor,
width: 100,
height: 100,
objectCaching: false,
stroke: "#fff",
strokeWidth: 0,
rx: 4,
ry: 4,
});
rect.minScaleLimit = 50 / rect.getScaledWidth();
rect.on('scaling', function () {
if (rect.getScaledWidth() < 20) {
rect.width = 20;
console.log("small")
}
if (rect.getScaledHeight() < 20) {
rect.height = 20;
console.log("small")
} else {}
this.set({
width: this.width * this.scaleX,
height: this.height * this.scaleY,
scaleX: 1,
scaleY: 1,
})
})
canvas.add(rect);
}
When I try to save this into a JSON, and then import it again, the previous rect with the scaling limits doesn't work like when I create a new one.
Maybe because the on scaling function is inside the rect function, and it doesn't allow to read the on scaling, but I cannot take it out because then the rect wouldn't be defined..
Maybe I should put an on scaling function on the loadfromJSON import function, but I don't really know how to do it either..
Any helps?
I am trying to create a stage using konva.js. Basically, I need to add two (at least) or more images on the stage based on some layouts.
Example:
I have a layout that splits the stage area verically into two similar groups. Each group has one image.
Example code:
var stage = new Konva.Stage({
container: 'container',
width: width, // 295px
height: height, // 600px
});
var layer = new Konva.Layer({
imageSmoothingEnabled: true
});
// add a vertical line in order to show seperated groups
var line = new Konva.Line({
points: [stage.width() / 2, 0, stage.width() / 2, stage.height()],
stroke: '#9499a3',
strokeWidth: 2,
lineCap: 'round',
lineJoin: 'round',
});
layer.add(line)
// create group #1
var group1 = new Konva.Group({
x: 0,
y: 0,
width: stage.width() / 2,
height: stage.height()
});
var image1;
var imageObj = new Image();
imageObj.onload = function () {
image1 = new Konva.Image({
x: (stage.width() / 2 - imageObj.width) - line.strokeWidth() / 2,
//y: 0,
width: imageObj.width,
height: stage.height(),
image: imageObj,
draggable: true,
});
//layer.add(image1);
group1.add(image1);
image1.on('dragstart', function () {
console.log('dragstart')
});
image1.on('dragmove', function(e){
console.log('X : ' + this.attrs.x + ', Y : ' + this.attrs.y)
});
image1.on('dragend', function () {
console.log('X : ' + this.attrs.x + ', Y : ' + this.attrs.y)
});
};
imageObj.src = 'img/1.jpg';
// create group #2
var group2 = new Konva.Group({
x: stage.width() / 2 + line.strokeWidth() / 2,
y: 0,
width: stage.width() / 2,
height: stage.height()
});
var image2;
var imageObj = new Image();
imageObj.onload = function () {
image2 = new Konva.Image({
//x: stage.width() / 2 + line.strokeWidth() / 2,
//y: stage.height() / 2 - imageObj.height / 2,
width: imageObj.width,
height: stage.height(),
image: imageObj,
draggable: true,
});
//layer.add(image2);
group2.add(image2);
};
imageObj.src = 'img/2.jpg';
layer.add(group1, group2)
stage.add(layer);
What I want to do:
Define the draggable area of each image (or group) so to not be overflowed between each other.
Check if layer is visible so re-position image to hide the layer.
So the way to achieve this is to have a rect defining the frame in which the image will be constrained. Put this into a group and set the clip region of the group to match the position and size of the frame rect. Now add the image to the group. Only the part of the image in the group will be visible.
As a bonus, if you add a dragBoundFunc to the group you can ensure that an oversized image cannot be dragged beyond frame edges.
See snippet below (best run full-screen) and editable CodePen here.
This is, of course, only a single image frame where you have described that you will have say two in your use case. I suggest that you unravel the code then make a class, then you can use as many as required, etc.
// this data gives the position and size of the frame
let data = {
frameGroup: { x: 50, y: 100, width: 800, height: 300, strokeWidth: 10, stroke: 'cyan'},
fadeImage: {opacity: 0.3}
}
// add a stage
let stage = new Konva.Stage({container: 'container', width: 1000, height: 500 }),
layer = new Konva.Layer({}), // Add a layer and group to draw on
group = new Konva.Group({clip: data.frameGroup}),
rect = new Konva.Rect(data.frameGroup),
image = new Konva.Image({draggable: true}),
fadeImage = null,
imageObj = new Image();
stage.add(layer);
layer.add(group)
group.add(rect);
rect.listening(false); // stop the frame rect intercepting events
// Use the html image object to load the image and handle when laoded.
imageObj.onload=function () {
image.image(imageObj); // set the Konva image content to the html image content
// compute image position so that it is initially centered in the frame
let imagePos = getMiddlePos(data.frameGroup, data.frameGroup, {width: imageObj.width, height: imageObj.height});
// set the Konva image attributes as needed
image.setAttrs({
x: imagePos.x, y: imagePos.y, width: imageObj.width, height: imageObj.height,
// This function ensures the oversized image cannot be dragged beyond frame edges.
// Is is firect by the drag event.
dragBoundFunc: function(pos){
var imagePos = this.getClientRect(); // get the image dimensions.
let
maxPos = { // compute max x & y position allowed for image
x: data.frameGroup.x,
y: data.frameGroup.y
},
minPos = {
x: data.frameGroup.x + data.frameGroup.width - imagePos.width,
y: data.frameGroup.y + data.frameGroup.height - imagePos.height
},
newX = (pos.x >= maxPos.x) ? maxPos.x : pos.x, // ensure left edge not within frame
newY = (pos.y >= maxPos.y) ? maxPos.y : pos.y; // ensure top edge not within frame
newX = newX < minPos.x ? minPos.x : newX; // ensure right edge not within frame
newY = newY < minPos.y ? minPos.y : newY; // ensure top edge not within frame
fadeImage.setAttrs({x: newX, y: newY}); // apply what we computed
// dragBoundFunc must return a value with x & y. Either return same value passed in
// or modify the value.
return {
x: newX,
y: newY
};
}
})
group.add(image) // add the image to the frame group
image.moveToBottom(); // ensure the frame rect is above the image in the z-index.
// make a clone of the image to be used as the fade image.
fadeImage = image.clone({draggable: false, opacity: data.fadeImage.opacity});
layer.add(fadeImage);
// ensure fade image is one step below the frame group. ! - in this simple demo Konva will raise
// a warning because group.zIndex() = 0.
fadeImage.zIndex(group.zIndex() - 1);
}
imageObj.src = "https://assets.codepen.io/255591/hubble_space_image.jpg?x=1"
// simple function to get the x & y for the image to be centered in the frame
function getMiddlePos(framePos, frameSize, imageSize){
return{
x: framePos.x + (frameSize.width - imageSize.width)/2,
y: framePos.y + (frameSize.height - imageSize.height)/2,
}
}
// Toggle use of fade image to show overflowed part of image.
$('#useFadImage').on('change', function(e){
if (fadeImage){
fadeImage.visible(!fadeImage.visible());
}
})
body {
margin: 14px;
padding: 10px;
font: 12pt Verdana, Arial, sans-serif;
}
#container {
width: 1000px;
height: 500px;
border: 1px solid red;
}
<p><h2>Constrain an image to the bounds of a frame</h2></p>
<p>1. Set the frame group clip region.</p>
<p>2. Set dragBounds function on the image so that it cannot escape the group.</p>
<p>Drag the image and note that it cannot me dragged such that white space in the frame would be visible.</p>
<p>
<input type='checkbox' id='useFadImage' checked='' /><label for="useFadImage">Use fade image - shows area of image outside the frame </label>
</p>
<div id='container' ></div>
<p>Image from NASA Image of the day.</p>
<script src='https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js'></script>
<script src="https://unpkg.com/konva#8/konva.min.js"></script>
There is a blog entry on the subject here.
I've created a fabric custom class "VectorPlaceholder" that is basically a group that contains a Rectangle and a Vector:
// Fabric.js custom vector EPS object
fabric.VectorPlaceholder = fabric.util.createClass(fabric.Group, {
async: true,
type: 'vector-placeholder',
lockUniScalingWithSkew: false,
noScaleCache: true,
initialize: function (options) {
boundsRectangle = new fabric.Rect({
strokeDashArray: [10,10],
originX: 'center',
originY: 'center',
stroke: '#000000',
strokeWidth: 1,
width: options.width || 300,
height: options.height || 300,
fill: 'rgba(0, 0, 0, 0)',
});
this.setControlsVisibility({
ml: false,
mb: false,
mr: false,
mt: false,
});
this.originX = 'center',
this.originY = 'center',
this.callSuper('initialize', [boundsRectangle], options);
},
setVector: function (vector) {
//We remove any EPS that was in that position
var EPSGroup = this;
EPSGroup.forEachObject(function (object) {
if (object && object.type != "rect") {
EPSGroup.remove(object);
}
});
var scale = 1;
var xOffset = EPSGroup.getScaledWidth() / 2;
var yOffset = EPSGroup.getScaledHeight() / 2;
if (vector.height > vector.width) {
scale = EPSGroup.getScaledHeight() / vector.height;
xOffset = xOffset - (EPSGroup.getScaledWidth() - vector.width * scale) / 2
}
else {
scale = EPSGroup.getScaledWidth() / vector.width;
yOffset = yOffset - (EPSGroup.getScaledHeight() - vector.height * scale) / 2
}
vector.left = EPSGroup.left - xOffset;
vector.top = EPSGroup.top - yOffset;
vector.set('scaleY', scale);
vector.set('scaleX', scale);
var angle = 0;
if (EPSGroup.get('angle')) {
angle = EPSGroup.get('angle');
vector.setAngle(angle);
}
EPSGroup.addWithUpdate(vector);
EPSGroup.setCoords();
},
});
The idea of this class is to have a placeholder where users can upload SVGs.
This is done by calling to fabric.loadSVGFromString and then passing the result to the function in my custom class (setVector)
fabric.loadSVGFromString(svgString, function(objects, options) {
// Group the SVG objects to make a single element
var a = fabric.util.groupSVGElements(objects, options);
var EPSGroup = new fabric.VectorPlaceholder({});
EPSGroup.setVector(a);
This works perfectly when I create my custom object and don't rotate it. As you can see the group controls are aligned with the dashed rectangle.
The problem is when I create an empty VectorPlaceholder and I rotate it manually. After the manual rotation, when setVector is called this is what happens:
I can't understand why the group controls ignore the rotation, what I'm doing wrong? How can I make the group controls render aligned with the rotated rectangle?
You need to set the angle after you make setVector method
http://jsfiddle.net/2segrwx0/1/
// Fabric.js custom vector EPS object
fabric.VectorPlaceholder = fabric.util.createClass(fabric.Group, {
async: true,
type: 'vector-placeholder',
lockUniScalingWithSkew: false,
noScaleCache: true,
initialize: function (options) {
boundsRectangle = new fabric.Rect({
strokeDashArray: [10,10],
originX: 'center',
originY: 'center',
stroke: '#000000',
strokeWidth: 1,
width: options.width || 300,
height: options.height || 300,
fill: 'rgba(0, 0, 0, 0)',
});
this.setControlsVisibility({
ml: false,
mb: false,
mr: false,
mt: false,
});
this.originX = 'center',
this.originY = 'center',
this.callSuper('initialize', [boundsRectangle], options);
},
setVector: function (vector) {
//We remove any EPS that was in that position
var EPSGroup = this;
EPSGroup.forEachObject(function (object) {
if (object && object.type != "rect") {
EPSGroup.remove(object);
}
});
var scale = 1;
var xOffset = EPSGroup.getScaledWidth() / 2;
var yOffset = EPSGroup.getScaledHeight() / 2;
if (vector.height > vector.width) {
scale = EPSGroup.getScaledHeight() / vector.height;
xOffset = xOffset - (EPSGroup.getScaledWidth() - vector.width * scale) / 2
}
else {
scale = EPSGroup.getScaledWidth() / vector.width;
yOffset = yOffset - (EPSGroup.getScaledHeight() - vector.height * scale) / 2
}
vector.left = EPSGroup.left - xOffset;
vector.top = EPSGroup.top - yOffset;
vector.set('scaleY', scale);
vector.set('scaleX', scale);
/*var angle = 0;
if (EPSGroup.get('angle')) {
angle = EPSGroup.get('angle');
vector.setAngle(angle);
} */
EPSGroup.addWithUpdate(vector);
EPSGroup.setCoords();
},
});
canvas = new fabric.Canvas('c', {
});
fabric.loadSVGFromString('<svg height="210" width="500"><polygon points="100,10 40,198 190,78 10,78 160,198" style="fill:lime;stroke:purple;stroke-width:5;fill-rule:nonzero;" /></svg>', function(objects, options) {
// Group the SVG objects to make a single element
var a = fabric.util.groupSVGElements(objects, options);
var EPSGroup = new fabric.VectorPlaceholder({});
EPSGroup.left=200;
EPSGroup.top=200;
EPSGroup.setVector(a);
EPSGroup.angle=45;
canvas.add(EPSGroup);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.4.5/fabric.js"></script>
<canvas id="c" width=500 height=300 ></canvas>
My fiddle: http://jsfiddle.net/yeftc60v/1/
So I'm trying to get the obj.left to be based on the left side(wall) of the group instead of the center. So right now, when I try to get the left of an object, if it's on the left side, it will return a negative number.
I've tried both setOriginY and setOriginX but they don't have any effect.
I'm working around this by doing some maths based on the group's width.
Right now if you hit the "clone" button, the left values read negative values (-100.5 and -0.5). Instead they should be 0 and 99.5(I think), respectively.
Unfortunately, any active group's origin x and y will always be at center. This won't take originX and originY property into consideration, hence setting setOriginX and setOriginY won't have any effect.
To get around this, you need to manually calculate the object's top and left position respective to the selected group, like so ...
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
ᴅᴇᴍᴏ
var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect({
left: 50,
top: 50,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
var rect1 = new fabric.Rect({
left: 150,
top: 150,
fill: "#FF0000",
stroke: "#000",
width: 100,
height: 100,
strokeWidth: 1,
opacity: .8
});
canvas.add(rect);
canvas.add(rect1);
function clone() {
let active = canvas.getActiveGroup();
if (active) {
active.forEachObject(obj => {
let clone = obj.clone();
// get object's left & top relative to the group
let objLeft = obj.left + (active.width / 2);
let objTop = obj.top + (active.height / 2);
clone.setLeft(objLeft + active.left);
clone.setTop(objTop + active.top);
canvas.add(clone);
});
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/1.7.11/fabric.min.js"></script>
<canvas id="c" width="300" height="300" style="border: 1px solid black;"></canvas>
<button onclick="clone()">
Clone
</button>
I am using Fabric.js with React/Redux and i am trying to achieve the following effect on canvas: (note i have 2 rulers on the top and left which shouldn't move around when panning, just horizontally and vertically).
This is a video of the effect I am trying to get: https://vid.me/qs5Q
I have the following code:
var canvas = new fabric.Canvas('c');
var topRuler = new fabric.Text('this is the top ruler', {
top: 0,
left: 60,
selectable: false
});
var leftRuler = new fabric.Text('this is the left ruler', {
top: 150,
left: -125,
selectable: false
});
var circle = new fabric.Circle({
radius: 20,
fill: 'green',
left: 180,
top: 100
});
var triangle = new fabric.Triangle({
fill: 'green',
left: 150,
top: 170
});
var panning = false;
var panningOn = false;
leftRuler.setAngle(-90).setCoords();
canvas.add(circle, triangle, topRuler, leftRuler);
canvas.renderAll();
canvas.on('mouse:up', function(e) {
if (panningOn) panning = false;
});
canvas.on('mouse:down', function(e) {
if (panningOn) panning = true;
});
canvas.on('mouse:move', function(e) {
if (panning && e && e.e) {
moveX = e.e.movementX;
moveY = e.e.movementY;
topRuler.set({
top: topRuler.top - moveY,
left: topRuler.left - moveX
});
leftRuler.set({
top: leftRuler.top - moveY,
left: leftRuler.left - moveX
});
canvas.relativePan(new fabric.Point(moveX, moveY));
}
});
$('#toggle').click(function() {
panningOn = !panningOn;
canvas.selection = !panningOn;
canvas.defaultCursor = 'default';
$('#toggle').html('Turn Panning ON');
if (panningOn) {
$('#toggle').html('Turn Panning OFF');
canvas.defaultCursor = 'move';
}
});
/* ZOOM FUNCTIONS */
$('#zoomIn').click(function(){
canvas.setZoom(canvas.getZoom() * 1.1 ) ;
});
$('#zoomOut').click(function(){
canvas.setZoom(canvas.getZoom() / 1.1 ) ;
});
This is a working JSFiddle: https://jsfiddle.net/z3p8utmc/19/
The code is not complete since I do not know exactly how to position the rulers, tried to play with viewPort, canvas size, etc. but did not manage to get the same effect.
Thank you for the help.
Here's a screen shot of the results:
Here's the two zoom functions:
/* ZOOM FUNCTIONS */
$('#zoomIn').click(function() {
rectangle.set({
width: rectangle.width * 1.1,
height: rectangle.height * 1.1
});
rectangle.setCoords();
textSample.set({
top: 60 + (textSample.top - 60) * 1.1,
left: 60 + (textSample.left - 60) * 1.1,
scaleX: textSample.scaleX * 1.1,
scaleY: textSample.scaleY * 1.1
});
textSample.setCoords();
setPins();
addLeftRuler();
addTopRuler();
resizeCanvas();
});
$('#zoomOut').click(function() {
rectangle.set({
width: rectangle.width / 1.1,
height: rectangle.height / 1.1
});
rectangle.setCoords();
textSample.set({
top: 60 + (textSample.top - 60) / 1.1,
left: 60 + (textSample.left - 60) / 1.1,
scaleX: textSample.scaleX / 1.1,
scaleY: textSample.scaleY / 1.1
})
textSample.setCoords();
setPins();
addLeftRuler();
addTopRuler();
resizeCanvas();
});
Panning is done using a 3rd-Party control, and the traditional way, since that's what the video shows (and you wanted) - top and left rulers are kept in place by updating their positions as scrolling occurs.
Zooming is done by scaling the individual objects based on point (60, 60) - as in the video. Pins are reset, rulers are rescaled, and canvas is resized - as needed.
The trick is calculating and recalculating everything as user actions are taken and events are firing.
Hopefully this gets you the canvas foundation you need to build upon.
Here's a working JSFiddle, https://jsfiddle.net/rekrah/hs6jL8d4/.