How do I a multiselect programatically in fabric.js? - fabricjs

I'm trying to select a few fabric.js objects so I can manipulate them together.
First I load an SVG file as a fabric group.
Second I click on the button to filter objects and "select" objects that I want to make the 'active selection out of.
In this example, you can see the bounding box is not where you'd expect it to be. This is causing some issues in my app.
I've seen in the 'gotchas' about setCoords() and I'm not sure if this is related to that or how it would apply here since I haven't moved anything in the group x,y.
This example codepen an attempt to show a simple example.
https://codepen.io/bencbaumann/pen/PodoeXq?editors=1010
html
<button id="multiselect">Multi Select</button>
<canvas id="canvas">
js
const canvas = new fabric.Canvas("canvas", {
height: 500,
width: 500
});
var url = "https://assets.codepen.io/496640/dots3.svg";
fabric.loadSVGFromURL(url, function (objects, options) {
const svgObj = fabric.util.groupSVGElements(objects, options);
canvas.add(svgObj);
canvas.renderAll();
});
function selectObjectsByIds({ canvas, ids }) {
console.log("ids", ids); // show [ids] that we are trying to select
const objects = canvas.getObjects(); // get objects in canvas
console.log("objects.length:", objects.length);
console.log("objs:", objects);
const objectsWithId = objects.filter((o) => o.id);
const objectsWithChildren = objects.filter((o) => o._objects);
const childrenWithId = objectsWithChildren
.map((o) => o._objects)
.flat()
.filter((o) => o.id);
const selected_ids = [...objectsWithId, ...childrenWithId];
console.log(selected_ids)
const selectedObjects = selected_ids.filter((o) => ids.includes(o.id));
canvas.discardActiveObject();
console.log('selectedObjects: ', selectedObjects)
var sel = new fabric.ActiveSelection(selectedObjects, {
canvas: canvas,
});
canvas.setActiveObject(sel);
canvas.requestRenderAll();
}
var $ = function(id){return document.getElementById(id)};
var multiselect = $('multiselect')
multiselect.onclick = () => selectObjectsByIds({canvas, ids: ['dot']})
the bounding box is not where it should be. I expect the bounding box to be around the objects in my selection.
The 3 green dots have the id of 'dot'

Related

Fixed SVG Layer Position on Open Layers Map

I have a map with multiple Layers. One of them shall project an SVG on the map, indicating certain regions (if not obvious, it's the red area in the images). I got this working on the default zoom level.
But as soon as I change the zoom in any direction, the SVG layer just moves too much in a direction depending on where I initiate the zoom change.
This is the relevant code I have:
//Map init
var mapcenter = ol.proj.fromLonLat([10.615219, 51.799502]);
var layers = [];
var map = new ol.Map({
target: 'map',
layers: [
new ol.layer.Tile({
source: new ol.source.OSM()
})
],
view: new ol.View({
center: mapcenter,
zoom: 9.5
})
});
//SVG init
const svgContainer = document.createElement('div');
const xhr = new XMLHttpRequest();
xhr.open('GET', '/shared/harzregionen.svg');
xhr.addEventListener('load', function () {
const svg = xhr.responseXML.documentElement;
svgContainer.ownerDocument.importNode(svg);
svgContainer.appendChild(svg);
});
xhr.send();
const width = 350;
const height = 0;
const svgResolution = 360 / width;
svgContainer.style.width = width + 'px';
svgContainer.style.height = height + 'px';
svgContainer.className = 'svg-layer';
//SVG Layer
map.addLayer(
new ol.layer.Vector({
render: function (frameState) {
const scale = svgResolution / frameState.viewState.resolution;
const center = frameState.viewState.center;
const size = frameState.size;
const cssTransform = ol.transform.composeCssTransform(
300,
120,
480 / frameState.viewState.resolution,
480 / frameState.viewState.resolution,
frameState.viewState.rotation,
-(center[0] - mapcenter[0]) / 420, //These values were kinda
(center[1] - mapcenter[1]) / 250 //trial and error for default zoom
);
svgContainer.style.transform = cssTransform;
svgContainer.style.opacity = this.getOpacity();
return svgContainer;
}
})
);
I'm pretty sure I have to use a certain formula to calculate the values in composeCssTransform but it feels like there should be an easy way to do it instead of trial and error until it somewhat works.
So, my question is: Is there an easy way to lock an SVG layer to a set position on the world map like the layers with markers?

fabric.js - Adding custom properties to a shape

I am trying to add some custom properties for a rectangle.
I have tried setting the toObject property as per the fabric.js documentation.
let rect = new fabric.Rect();
rect.toObject = ((toObject) => {
return () => {
return fabric.util.object.extend(toObject.call(this), {
name: 'some name'
});
};
})(rect.toObject);
this.canvas.add(rect);
When I click on save of the canvas
this.canvas.toJSON();
I am getting a
ERROR TypeError: this.callSuper is not a function.
Because this value is not proper inside arrow function. Either change it to normal function, or pass rect object instead this.
const canvas = new fabric.Canvas('c')
const rect = new fabric.Rect();
rect.toObject = (function(toObject) {
return function(propertiesToInclude) {
return fabric.util.object.extend(toObject.call(this, propertiesToInclude), {
name: 'some name'
});
};
})(rect.toObject);
canvas.add(rect);
console.log(canvas.toJSON())
<script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/3.4.0/fabric.js"></script>
<canvas id='c'></canvas>

signature-pad - resize not working

i'm using the signature-pad plugin and i'm having some issues whith the resize event:
- Multiple resizes lead to a loss in quality and the signature "moves" at each resize of the browser window ending with no signature in canvas.
- In some cases, the isEmpty() function wont work and i'll be able to save the empty signature.
Optional question : how can i detect an empty signature on php side ?
Thank you :)
Below my code :
$(window).resize(function() {
resizeCanvas();
});
var wrapper1 = document.getElementById("signature-pad"),
clearButton1 = wrapper1.querySelector("[data-action=clear]"),
canvas1 = wrapper1.querySelector("canvas"),
signaturePad1;
var wrapper2 = document.getElementById("signature-pad-paraphe"),
clearButton2 = wrapper2.querySelector("[data-action=clear]"),
canvas2 = wrapper2.querySelector("canvas"),
signaturePad2;
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
signaturePad1 = new SignaturePad(canvas1);
signaturePad2 = new SignaturePad(canvas2);
function resizeCanvas() {
//Sauvegarde sig / par
var sig = signaturePad1.toDataURL();
var par = signaturePad2.toDataURL();
var ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas1.width = canvas1.offsetWidth * ratio;
canvas1.height = canvas1.offsetHeight * ratio;
canvas1.getContext("2d").scale(ratio, ratio);
canvas2.width = canvas2.offsetWidth * ratio;
canvas2.height = canvas2.offsetHeight * ratio;
canvas2.getContext("2d").scale(ratio, ratio);
// redraw
signaturePad1.fromDataURL(sig);
signaturePad2.fromDataURL(par);
}
window.onresize = resizeCanvas;
resizeCanvas();
// Init -> retourne la bonne valeur de isEmpty -> !!? Not sure if needed
signaturePad1.clear();
signaturePad2.clear();
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
clearButton1.addEventListener("click", function (event) {
signaturePad1.clear();
});
clearButton2.addEventListener("click", function (event) {
signaturePad2.clear();
});
Here is i developed a little solution;
Here are two key DOM elements:
div#id_wrapper
canvas#id
Considered it may be applied at devices with different devicePixelRatio and on screens changins theirs width (f.i.: portrait-landscape orientation).
export class FlexSignatureComponent extends React.Component {
state = {
width: 0,
lines: [],
storedValue: undefined,
validationClass: '', // toggles between 'is-invalid'/'is-valid'
validationMessage: ''
}
The lib initiation is right after the component got loaded:
componentDidMount = () => {
this.signPad = new SignaturePad(document.getElementById(this.props.htmlid), {
onEnd: this.onChangeSignaturePad,
backgroundColor: '#fff'
});
if (this.valueHolder.current.value) {
const data = JSON.parse(this.valueHolder.current.value);
this.state.lines = data.value;
this.state.width = 100;
}
//you need the next workarounds if you have other onWidnowResize handlers manarging screen width
//setTimeout-0 workaround to move windowResizeHandling at the end of v8-enging procedures queue
// otherwise omit setTimeout and envoke func as it is
setTimeout(this.handleWindowResize, 0);
window.addEventListener("resize", () => setTimeout(this.handleWindowResize, 0));
}
First handle window resize change
handleWindowResize = () => {
if (this.state.storedValue) {
const prevWrapperWidth = this.state.width;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = prevWrapperWidth / currentWrapperWidth;
this.state.width = currentWrapperWidth;
this.setRescaledSignature(this.state.lines, scale);
this.resetCanvasSize();
this.signPad.fromData(this.state.lines)
} else
this.resetCanvasSize()
}
Second rescaleSignature to another width
setRescaledSignature = (lines, scale) => {
lines.forEach(line => {
line.points.forEach(point => {
point.x /= scale;
point.y /= scale;
});
});
}
Finally updated canvas size
resetCanvasSize = () => {
const canvas = document.getElementById(this.props.htmlid);
canvas.style.width = '100%';
canvas.style.height = canvas.offsetWidth / 1.75 + "px";
canvas.width = canvas.offsetWidth * devicePixelRatio;
canvas.height = canvas.offsetHeight * devicePixelRatio;
canvas.getContext("2d").scale(devicePixelRatio, devicePixelRatio);
}
Here we on every change add new drawn line to this.state.lines
and prepare the lines to be submited as json.
But before the submission they need to create deepCopy and to be rescaled to conventional size (its width is equal 100px and DPR is 1)
onChangeSignaturePad = () => {
const value = this.signPad.toData();
this.state.lines = value;
const currentWrapperWidth = $(`#${this.props.htmlid}_wrapper`).width();
const scale = currentWrapperWidth / 100;
const ratio = 1 / devicePixelRatio;
const linesCopy = JSON.parse(JSON.stringify(value));
this.setRescaledSignature(linesCopy, scale, ratio);
const data = {
signature_configs: {
devicePixelRatio: 1,
wrapper_width: 100
},
value: linesCopy
};
this.state.storedValue = JSON.stringify(data);
this.validate()
}
One more thing is the red button to swipe the previous signatures
onClickClear = (e) => {
e.stopPropagation();
this.signPad.clear();
this.valueHolder.current.value = null;
this.validate()
}
render() {
let {label, htmlid} = this.props;
const {validationClass = ''} = this.state;
return (
<div className="form-group fs_form-signature">
<label>{Label}</label>
<div className="fs_wr-signature">
<button className={'fs_btn-clear'} onClick={this.onClickClear}>
<i className="fas fa-times"></i>
</button>
<div id={htmlid + '_wrapper'} className={`w-100 fs_form-control ${validationClass}`}>
<canvas id={htmlid}/>
</div>
</div>
<div className={' invalid-feedback fs_show-feedback ' + validationClass}>Signature is a mandatory field</div>
</div>
)
}
postWillUnmount() {
this.signPad.off();
}
the used lib signature pad by szimek
Used React and Bootstrap and some custome styles
the result would be
You didn't provide a full example, or much explanation of the code, so it's hard to tell what all is going on here, but I'll do my best to give as full an answer as I can.
Saving
First, if I understand the docs correctly, $(window).resize will be triggered at the same time as window.onresize. You use both. That might be causing some issues, maybe even the issues with saving.
The following code is run once, and I'm not sure what it's supposed to do:
var signature = $('#confirm_delete_signature').val();
if(signature){
signaturePad1.fromDataURL(signature);
}
var paraphe = $('#confirm_delete_paraphe').val();
if(paraphe){
signaturePad2.fromDataURL(paraphe);
}
It looks like it's supposed to be deleting the signature (since the selector is #confirm_delete_signature), but it instead, it's restoring a signature from some data stored in the node as a string. That might be causing issues too.
That said, I'm not sure why saving isn't working, but I can't find the code of your saving function, so it's very hard to say. Maybe I missed something.
I'm not familiar with php, sorry.
Resizing
For resizing, I think the React version that #Alexey Nikonov made might work with React (I didn't run it). You have to scale the positions of the points of the lines along with the changing size of the canvas.
I wanted a version closer to vanilla js, so I recreated it with just signature_pad v4.1.4 and jQuery at https://jsfiddle.net/j2Lurpd5/1/ (with an improvement to ratio calculation).
The code is as follows, though it doesn't have a button to clear the canvas:
<div id="wrapper">
<canvas id="pad" width="200" height="100"></canvas>
</div>
canvas {
border: red 1px solid;
}
// Inspiration: https://stackoverflow.com/a/60057521
// Version with no React
const canvas = document.querySelector('#pad');
const signPad = new SignaturePad(canvas);
// Doesn't work without the #wrapper. Probably because #pad
// needs it to be able to be 100% of it. Not sure exactly
// why that makes a difference when #wrapper doesn't have
// a width set on it. Though #pad alone does work after the
// first resize.
let prevWidth = $('#wrapper').width();
let lines = [];
setTimeout(resizeSignatureAndCanvas, 0);
window.addEventListener("resize", () => setTimeout(resizeSignatureAndCanvas, 0));
window.addEventListener("orientationchange", () => setTimeout(resizeSignatureAndCanvas, 0));
function resizeSignatureAndCanvas () {
// Get the current canvas contents
lines = signPad.toData();
// if there are no lines drawn, don't need to scale them
if ( signPad.isEmpty() ) {
// Set initial size
resizeCanvas();
} else {
// Calculate new size
let currentWidth = $('#wrapper').width();
let scale = currentWidth / prevWidth;
prevWidth = currentWidth; // Prepare for next time
// Scale the contents along with the width
setRescaledSignature(lines, scale);
// Match canvas to window size/device change
resizeCanvas();
// Load the adjusted canvas contents
signPad.fromData(lines);
}
};
// This is really the key to keeping the contents
// inside the canvas. Getting the scale right is important.
function setRescaledSignature (lines, scale) {
lines.forEach(line => {
line.points.forEach(point => {
// Same scale to avoid warping
point.x *= scale;
point.y *= scale;
});
});
};
function resizeCanvas () {
/** Have to resize manually to keep the canvas the width of the
* window without distorting the location of the "pen". */
// I'm not completely sure of everything in here
const canvas = $('#pad')[0];
// Not sure why we need both styles and props
canvas.style.width = '100%';
canvas.style.height = (canvas.offsetWidth / 1.75) + 'px';
// When zoomed out to less than 100%, for some very strange reason,
// some browsers report devicePixelRatio as less than 1
// and only part of the canvas is cleared then.
let ratio = Math.max(window.devicePixelRatio || 1, 1);
// This part causes the canvas to be cleared
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
};
As you can see from my notes, I'm not completely sure why every part works, but from what I can tell it does preserve the behavior of the version that #Alexey Nikonov made.

KonvaJS, positioning editable text inputs

I need to position text inputs at various places on a KonvaJS layer. I found the following code at https://konvajs.github.io/docs/sandbox/Editable_Text.html and I'm trying to understand the textPosition, stageBox, and areaPosition vars in this code. I want my stage centered in the browser window, but when I do that, the textarea (activated on dblclick) pops up way off to the left. I can't get a console readout of the x/y coordinates, so I can't visualize how the positioning works &, thus, how to change it. Can anyone explain, or point me in the right direction?
var text_overlay = new Konva.Layer();
stage.add(text_overlay);
var textNode = new Konva.Text({
text: 'Some text here',
x: 20,
y: 50,
fontSize: 20
});
text_overlay.add(textNode);
text_overlay.draw();
textNode.on('dblclick', () => {
// create textarea over canvas with absolute position
// first we need to find its position
var textPosition = textNode.getAbsolutePosition();
var stageBox = stage.getContainer().getBoundingClientRect();
var areaPosition = {
x: textPosition.x + stageBox.left,
y: textPosition.y + stageBox.top
};
// create textarea and style it
var textarea = document.createElement('textarea');
document.body.appendChild(textarea);
textarea.value = textNode.text();
textarea.style.position = 'absolute';
textarea.style.top = areaPosition.y + 'px';
textarea.style.left = areaPosition.x + 'px';
textarea.style.width = textNode.width();
textarea.focus();
textarea.addEventListener('keydown', function (e) {
// hide on enter
if (e.keyCode === 13) {
textNode.text(textarea.value);
text_overlay.draw();
document.body.removeChild(textarea);
}
});
})
// add the layer to the stage
stage.add(text_overlay);
UPDATE: I solved part of the problem--the textarea showing up way out of position. You need to use 2 divs in the HTML file instead of one, like so:
<div id="containerWrapper" align="center"><div id="container"></div></div>
Thanks to Frens' answer on Draw border edges of the Konvajs container Stage in html for that one!

text over imported svg with fabricjs

I'm importing several svgs that are all created from the same image and layered on top of each other. I would like to add text centered over a certain svg/textarea path. And then scale textarea to fit the text provided with a min size. And ive confused myself. I have no idea anymore? I deleted everything and started over direction would be very helpful. Heres what i got.
var canvas = new fabric.Canvas('canvas');
fabric.loadSVGFromURL('/1-man.svg', function(objects, options) {
var obj = fabric.util.groupSVGElements(objects, options);
canvas.add(obj).renderAll();
});
fabric.loadSVGFromURL('/1-man-cutout.svg', function(objects, options)
{
var obj = fabric.util.groupSVGElements(objects, options);
canvas.add(obj).renderAll();
});
fabric.loadSVGFromURL('/textarea.svg', function(objects, options) {
var obj2 = fabric.util.groupSVGElements(objects, options);
canvas.add(obj2).renderAll();
});
var text = new fabric.Text("Hello", {
fontFamily: 'ubuntu',
fontSize: 300,
textAlign: "center",
});
canvas.add(text);
Brain was dead i suppose.... took about a min this morning.. Ill keep it for a lesson on sleeping on it.
text.set('left', (objects[0].width - text.width)/2);
text.set('top', objects[0].top + 5);
canvas.add(obj2, text).renderAll();

Resources