Set parent-child object drag limit in fabric.js - fabricjs

How can I set drag limit relative to either parent or child object ? In my current implementation, dragging is not smooth.
When you try to drag close to the edges, it breaks and stops moving. Instead, it should keep dragging on the available space.
You can see demo here: https://jsfiddle.net/xorbmoon/3ruaL9gs/114/
I have tried this, but it is not an optimal solution.
const loadImageFromURL = (src) => {
return new Promise((resolve) => {
const image = new Image();
image.src = src;
image.crossOrigin = "Anonymous";
image.onload = () => {
resolve(image);
};
});
}
const IMAGE = "https://images.pexels.com/photos/670741/pexels-photo-670741.jpeg?auto=compress&cs=tinysrgb&dpr=2&h=650&w=940";
const canvas = new fabric.Canvas("canvas", {
width: 1200,
height: 800,
preserveObjectStacking: true
})
const init = async () => {
const imageElement = await loadImageFromURL(IMAGE);
const image = new fabric.Image(imageElement, {
scaleX: 0.25,
scaleY: 0.25,
angle: 80,
left: 400
})
const cropper = new fabric.Rect({
id: "cropper",
absolutePositioned: true,
lockScalingFlip: true,
lockMovementX: true,
lockMovementY: true,
selectable: false,
evented:false,
top: 40,
left: 200,
fill:"red",
height: 100,
width: 300,
angle: 80
})
canvas.add( image, cropper)
const updatePosition = () => {
image.setCoords()
cropper.setCoords();
const isIn = cropper.isContainedWithinObject(image);
if(!isIn){
if(image._stateProperties){
image.left = image._stateProperties.left;
image.top = image._stateProperties.top;
}
} else {
image.setCoords();
image.saveState();
}
}
image.on("moving", updatePosition)
}
init()

Related

Problem subclassing fabricjs Image and restoring it

I am trying to subclass a fabricjs Image for a barcode field.
import {fabric} from 'fabric';
import bwipjs from 'bwip-js';
class Barcode extends fabric.Image {
constructor(options) {
console.log("constructor", options);
var wrkOptions = {
width: options.position.width,
height: options.position.height,
left: options.position.left,
top: options.position.top,
angle: options.direction || 0,
form: options.form || {}
};
super(wrkOptions);
this.type = 'barcode';
let canvas1 = document.createElement('canvas');
canvas1.width = options.position.width;
canvas1.height = options.position.height;
var bcOptions = { bcid: 'code128', // Barcode type
text: '0123456789', // Text to encode
scale: 3, // 3x scaling factor
height: 10, // Bar height, in millimeters
includetext: true, // Show human-readable text
textxalign: 'center', // Always good to set this
};
bwipjs.toCanvas(canvas1, bcOptions);
var dataUrl = canvas1.toDataURL("image/png");
this.setSrc(dataUrl, () => {
if (!options.restore) {
curCanvas.add(this);
this.setOptions(options);
this.setCoords();
}
},
wrkOptions);
}
toObject = () =>{
var ret = {
type: this.type,
position: {top: this.top, left: this.left, width: this.width * this.scaleX, height: this.height * this.scaleY},
color: this.fill,
direction: this.angle,
form: this.form
};
console.log(ret);
return ret;
}
}
fabric.Barcode = Barcode;
fabric.Barcode.fromObject = function (options, callback) {
var wrkOptions = {restore: true, ...options};
var bc = new Barcode(wrkOptions);
callback(bc);
};
Creating the barcode field works:
onAddBarcode = () => {
var color = Math.floor(Math.random() * 256 * 256 *256).toString(16);
var options = {
color: "#" + color,
position: {
top: Math.floor(Math.random() * 500),
left: Math.floor(Math.random() * 500),
width: Math.floor(Math.random() * 100),
height: Math.floor(Math.random() * 100)
}
};
return ret;
...this.getRandom()
};
var barcode = new Barcode(options);
}
But serializing and restoring the canvas does not work:
onReload = () => {
var json = JSON.stringify(this.canvas, null, 2);
this.canvas.clear();
this.canvas.loadFromJSON(json, this.canvas.renderAll.bind(this.canvas));
}
It does restore a barcode image, but it is positioned wrongly outside of the controls.
What's wrong?
I got it positioned correctly (after restoring) by resetting the position attributes in the setSrc callback:
....
this.setSrv(dataUrl,
() => {
this.left = options.position.left;
this.top = options.position.top;
this.angle = options.direction || 0;
this.width = options.position.width;
this.height = options.position.height;
this.setCoords();
callback(this);
});

Clip object to path drawn using free drawing brush

Fabric.js 2.3.6
I'm trying to clip an object to a path drawn with the free drawing bush. The code fails to show the image inside the path and I'm not sure what I'm doing wrong.
Multiple objects could be clipped, so I can't apply the path to the canvas itself.
let image = new Image();
let object;
let canvas;
// canvas
canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false
});
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function(options) {
clip(options.path);
});
// clip
function clip(path) {
object.set({
clipTo: function(ctx) {
path.render(ctx);
}
});
canvas.requestRenderAll();
}
// image
image.onload = function() {
object = new fabric.Image(image, {
width: 500,
height: 500,
top: 50,
left: 50
});
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
https://jsfiddle.net/o91rv38q/7/
In order to clip on the same spot when is path has been drawn, you need to reset pathOffset for a SVG path, and setTransform to the ctx. Your clip function will look like:
function clip(path) {
path.set({pathOffset: {x: 0, y: 0}});
object.set({
clipTo: function(ctx) {
ctx.save();
ctx.setTransform(1,0,0,1,0,0);
ctx.beginPath();
path._renderPathCommands(ctx);
ctx.restore();
}
});
canvas.requestRenderAll();
}
_renderPathCommands - renders a path.
Updated fiddle
To clip multiple objects you'll need to have some sort of an array of objects and then combine then into single path:
function combinePaths (paths) {
if (!paths.length) {
return null;
}
let singlePath = paths[0].path;
for (let i = 1; i < paths.length; i++){
singlePath = [...singlePath, ...paths[i].path];
}
return new fabric.Path(singlePath, {
top: 0,
left: 0,
pathOffset: {
x: 0,
y: 0
}
});
}
Here is an example with multiple paths to clip:
// canvas
let canvas = new fabric.Canvas("canvas", {
backgroundColor: "lightgray",
width: 1280,
height: 720,
preserveObjectStacking: true,
selection: false
});
let paths = [];
canvas.isDrawingMode = true;
canvas.freeDrawingBrush.color = "black";
canvas.freeDrawingBrush.width = 2;
canvas.on("path:created", function (options) {
paths.push(options.path);
clip(combinePaths(paths));
});
function combinePaths(paths) {
if (!paths.length) {
return null;
}
let singlePath = paths[0].path;
for (let i = 1; i < paths.length; i++) {
singlePath = [...singlePath, ...paths[i].path];
}
return new fabric.Path(singlePath, {
top: 0,
left: 0,
pathOffset: {
x: 0,
y: 0
}
});
}
function clip(path) {
if (!path) {
return;
}
object.set({
clipTo: function (ctx) {
var retina = this.canvas.getRetinaScaling();
ctx.save();
ctx.setTransform(retina, 0, 0, retina, 0, 0);
ctx.beginPath();
path._renderPathCommands(ctx);
ctx.restore();
}
});
canvas.requestRenderAll();
}
// image
let image = new Image();
let object;
image.onload = function () {
object = new fabric.Image(image, {
width: 500,
height: 500,
top: 50,
left: 50
});
object.globalCompositeOperation = 'source-atop';
canvas.add(object);
};
image.src = "http://i.imgur.com/8rmMZI3.jpg";
<script type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.6/fabric.js"></script>
<canvas id="canvas" width="1280" height="720"></canvas>

File Input fabric js adds images

I have a canvas and have 2 images on it one is not evented and other we can change. Now I'm trying to change/replace image using file input, everything is fine just while changing the image it replaces the one I want but also adds one on top. I want to change the Image with id='changeimg'
//Add Image to Canvas
var imgObj = new Image();
//imgObj.id='changeimg';
imgObj.src = "blank.png";
var imgmain = imgObj.onload = function() {
var image = new fabric.Image(imgObj);
image.set({
id: 'changeimg',
angle: 0,
height: 350,
width: canvas.getWidth(),
align: 'mid', //added
originX: 'center', //added
originY: 'center', //added
});
canvas.centerObject(image);
canvas.add(image);
}
var imgObj1 = new Image();
//imgObj1.id='backgroundimg';
imgObj1.src = "template1.png";
var imgmain = imgObj1.onload = function() {
var image1 = new fabric.Image(imgObj1);
image1.set({
id: 'backgroundimg',
angle: 0,
height: canvas.getHeight(),
width: canvas.getWidth(),
evented: false,
});
//canvas.centerObject(image1);
canvas.add(image1);
}
//On Image Browse and Set on Canvas
document.getElementById('uploadedImg').onchange = function handleImage(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function(file) {
addImage(file.target.result);
}
reader.readAsDataURL(file);
}
function addImage(imgLink) {
fabric.Image.fromURL(imgLink, function(img) {
img.set({
'left': 50
});
img.set({
'top': 150
});
img.scaleToWidth(250);
img.scaleToHeight(250);
var cnt = 0;
var objs = canvas.getObjects();
//var objs = canvas.getActiveObject();
if (objs.length > 0) {
objs.forEach(function(e) {
if (e && e.type === 'image' && e.id === "changeimg") {
e._element.src = imgLink;;
canvas.renderAll();
cnt = 1;
}
});
}
});
}
Use imageEl.setSrc to change source of image element.
DEMO
var canvas = new fabric.Canvas('c');
fabric.Image.fromURL("https://picsum.photos/200/300/?random", function(img) {
img.set({
id: 'changeimg',
align: 'mid', //added
originX: 'center', //added
originY: 'center', //added,
scaleX: 200 / img.width,
scaleY: 200 / img.height,
});
canvas.centerObject(img);
canvas.add(img);
image = img;
})
fabric.Image.fromURL("https://picsum.photos/200/250/?random", function(img) {
img.set({
id: 'backgroundimg',
angle: 0,
scaleX: canvas.width / img.width,
scaleY: canvas.height / img.height,
});
//canvas.centerObject(image1);
canvas.setBackgroundImage(img, canvas.renderAll.bind(canvas));
})
//On Image Browse and Set on Canvas
document.getElementById('uploadedImg').onchange = function handleImage(e) {
var file = e.target.files[0];
var reader = new FileReader();
reader.onload = function(file) {
addImage(file.target.result);
}
reader.readAsDataURL(file);
}
function addImage(imgLink) {
fabric.Image.fromURL(imgLink, function(img) {
var objects = canvas.getObjects();
for (var i = 0, l = objects.length; i < l; i++) {
if (objects[i].id == 'changeimg') {
imageEl = objects[i];
break
}
}
if (imageEl) {
imageEl.setSrc(img.getSrc(), function() {
canvas.renderAll()
imageEl.setCoords();
},{
left: 50,
top: 150,
scaleX: 150 / img.width,
scaleY: 200 / img.height,
})
}
});
}
canvas{
border:2px solid;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/2.3.4/fabric.js"></script>
<canvas id="c" width=500 height=500></canvas><br>
<input type='file' id='uploadedImg'>

Giving timeout after evaluate in puppeteer is not working

I have a page which contains few visualizations. I have to evaluate and resize the charts. while evaluating, it takes few seconds to resize. the Previously I did this using phantom. I use to give timeout in the callback function and it worked perfectly. I'm trying the same using puppeteer which is not working. Not working in the sense, the resizing did not happen even after the timeout.
sample puppeteer code(not working)
const puppeteer = require('puppeteer');
const fs = require('fs');
function timeout(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
(async() => {
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setViewport({ width: 2048, height: 1024 });
await page.goto("http://localhost:3000", { timeout: 180000 });
await timeout(5000);
page.on('console', msg => console.log('PAGE LOG:', msg.text()));
await page.evaluate(function() {
for (var i = 0; i < $('.react-grid-item').length; i++) {
$('.react-grid-item:eq(' + i + ')').css({
position: 'absolute',
height: '300px',
width: '100%',
top: (i * 300) + 'px',
left: '0px'
})
}
})
.then(function() {
setTimeout(function() {
page.pdf({
path: 'test' + new Date() + '.pdf',
// height: 7777,
// width: 2048,
format: 'a4',
displayHeaderFooter: false,
margin: {
top: "75px",
bottom: "75px"
}
});
// console.log(sum);
}, 5000);
}).catch(function(err) {
console.error(err);
})
})();
using await/async try follow like this:
await page.evaluate((a, b) => {
return a + b
}, 1, 2).then((sum) => {
setTimeout(() => {
console.log(sum)
}, 2000)
})

Exporting custom properties from fabricjs objects to SVG

I have an image object where I have added a custom property.
When stringify the object, I can see that the property is there, but when I export the SVG by using toSVG, the custom property is not in the SVG source.
I would like the name property in my example to be exported as a custom property in the SVG image xml data (with the possibility to add multiple properties)
Is this possible?
Please se my fiddle: https://jsfiddle.net/Jonah/ujexg46s/
var svgImage = fabric.util.createClass(fabric.Image, {
initialize: function(element, options) {
this.callSuper("initialize", element, options);
options && this.set("name", options.name);
},
toObject: function() {
return fabric.util.object.extend(this.callSuper('toObject'), {
name: this.name
});
}
});
var canvas = new fabric.Canvas('container');
function loadImage() {
var img = new Image();
img.src = imageSrc;
img.onload = function() {
var image = new svgImage(img, {
name: "test",
left: 0,
top: 0
});
canvas.add(image);
}
}
document.getElementById('load-btn').onclick = function() {
loadImage();
};
document.getElementById('export-btn').onclick = function() {
canvas.deactivateAll().renderAll();
console.log(JSON.stringify(canvas));
window.open('data:image/svg+xml;utf8,' + encodeURIComponent(canvas.toSVG()));
};
You can find answer here.
fabric.util.object.extend(fabric.Image.prototype, {
toObject: function(propertiesToInclude) {
return fabric.util.object.extend(this.callSuper('toObject', propertiesToInclude), {
src: this._originalElement.src || this._originalElement._src,
filters: this.filters.map(function(filterObj) {
return filterObj && filterObj.toObject();
}),
crossOrigin: this.crossOrigin,
alignX: this.alignX,
alignY: this.alignY,
meetOrSlice: this.meetOrSlice,
name: this.name
});
},
});
https://jsfiddle.net/mullainathan/ujexg46s/1/
You can override the existing toSVG function like this.
var circle = new fabric.Circle ({
radius: 40,
left: 50,
top: 50,
fill: 'rgb(0,255,0)',
opacity: 0.5,
id: 'hello'
});
circle.toSVG = (function(toSVG) {
return function(){
var svgString = toSVG.call(this);
var domParser = new DOMParser();
var doc = domParser.parseFromString(svgString, 'image/svg+xml');
var parentG = doc.querySelector('path')
parentG.setAttribute('id', this.id);
return doc.documentElement.outerHTML;
}
})(circle.toSVG)

Resources