signature-pad - resize not working - signaturepad

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.

Related

How do I a multiselect programatically in fabric.js?

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'

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?

Pixi.js How should HP write?

How should HP write?
Because HP will decrease, but I found that he will deform.
Each time container.hpStatus.width- = 1; HP's icon will be distorted, especially HP = 0 is most obvious.
enter image description here
You Can Look My Codepen.
app.ticker.add((delta) => {
if (container.hpStatus.width > 0) {
container.hpStatus.width -= 1;
} else {
container.hpStatus.width = 450;
}
});
How can i make sure he doesn't deform?
The hp bar is getting distorted because you are decreasing width of "container.hpStatus" which is Geometry object which is itself a Container:
https://pixijs.download/dev/docs/PIXI.Graphics.html#Graphics
And as you see in docs of the "width" property: https://pixijs.download/dev/docs/PIXI.Container.html#width
width number
The width of the Container, setting this will actually modify the scale to achieve the value set
It means that changing "width" scales whole container ("container.hpStatus").
To draw such hp bar without "distortion" you can do it by drawing hp bar on each "tick" (each frame).
Plaese check following code - is your example but modified. Most important parts are "createHpBar" function and modified "main loop" (ticker).
(i also added some comments so you can understand better)
and here is updated codepen: https://codepen.io/domis86/pen/poJrKdq
const app = new PIXI.Application({
view: document.getElementById('main'),
width: 900,
height: 900,
antialias: true,
transparent: false,
backgroundColor: 0x00CC99,
});
// See: https://pixijs.download/dev/docs/PIXI.Ticker.html
let ticker = PIXI.Ticker.shared;
ticker.autoStart = false;
const container = new PIXI.Container();
app.stage.addChild(container);
function createHpBar(currentHp, maxHp, color) {
let hpBar = new PIXI.Graphics();
hpBar.beginFill(color);
let hpPortion = currentHp / maxHp;
hpBar.drawPolygon([
50, 80,
50 + (400 * hpPortion), 80,
32 + (400 * hpPortion), 150,
32, 150,
]);
hpBar.endFill();
return hpBar;
}
// Create black hp bar (the one behind the red one) and add it to our container:
let blackHpBar = createHpBar(450, 450, 0x000000);
container.addChild(blackHpBar);
container.x = 100;
container.y = 200;
let renderer = app.renderer;
// Starting amount of hp:
var currentHp = 450;
ticker.add((delta) => {
// create red hp bar which is a polygon with vertices calculated using "currentHp" amount:
let redHpBar = createHpBar(currentHp, 450, 0xFF0000);
// add red hp bar to container:
container.addChild(redHpBar);
// render our stage (render all objects from containers added to stage)
// see https://pixijs.download/dev/docs/PIXI.Ticker.html#.shared
renderer.render(app.stage);
// Remove red hp bar from our container:
// We do this because on next tick (aka: next frame) we will create new redHpBar with "createHpBar" (as you see above)
container.removeChild(redHpBar);
// Update current hp amount:
if (currentHp > 0) {
currentHp -= 1;
} else {
currentHp = 450;
}
});
// start rendering loop:
ticker.start();

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!

Phaser 3. Change dimensions of the game during runtime

Hi please help me to find out how trully responsive game can be created with Phaser3.
Respnsiveness is critical because game (representation layer of Blockly workspace) should be able to be exapanded to larger portion on screen and shrinked back many times during the session.
The question is How I can change dimentions of the game in runtime?
--- edited ---
It turns out there is pure css solution, canvas can be ajusted with css zoom property. In browser works well (no noticeable effect on performance), in cordova app (android) works too.
Here is Richard Davey's answer if css zoom can break things:
I've never actually tried it to be honest. Give it a go and see what
happens! It might break input, or it may carry on working. That (and
possibly the Scale Manager) are the only things it would break,
though, nothing else is likely to care much.
// is size of html element size that needed to fit
let props = { w: 1195, h: 612, elementId: 'myGame' };
// need to know game fixed size
let gameW = 1000, gameH = 750;
// detect zoom ratio from width or height
let isScaleWidth = gameW / gameH > props.w / props.h ? true : false;
// find zoom ratio
let zoom = isScaleWidth ? props.w / gameW : props.h / gameH;
// get DOM element, props.elementId is parent prop from Phaser game config
let el = document.getElementById(props.elementId);
// set css zoom of canvas element
el.style.zoom = zoom;
Resize the renderer as you're doing, but you also need to update the world bounds, as well as possibly the camera's bounds.
// the x,y, width and height of the games world
// the true, true, true, true, is setting which side of the world bounding box
// to check for collisions
this.physics.world.setBounds(x, y, width, height, true, true, true, true);
// setting the camera bound will set where the camera can scroll to
// I use the 'main' camera here fro simplicity, use whichever camera you use
this.cameras.main.setBounds(0, 0, width, height);
and that's how you can set the world boundary dynamically.
window.addEventListener('resize', () => {
game.resize(window.innerWidth, window.innerHeight);
});
There is some builtin support for resizing that can be configured in the Game Config. Check out the ScaleManager options. You have a number of options you can specify in the scale property, based on your needs.
I ended up using the following:
var config = {
type: Phaser.AUTO,
parent: 'game',
width: 1280, // initial width that determines the scaled size
height: 690,
scale: {
mode: Phaser.Scale.WIDTH_CONTROLS_HEIGHT ,
autoCenter: Phaser.Scale.CENTER_BOTH
},
physics: {
default: 'arcade',
arcade: {
gravity: {y: 0, x: 0},
debug: true
}
},
scene: {
key: 'main',
preload: preload,
create: this.create,
update: this.update
},
banner: false,
};
Just in case anybody else still has this problem, I found that just resizing the game didn't work for me and physics didn't do anything in my case.
To get it to work I needed to resize the game and also the scene's viewport (you can get the scene via the scenemanager which is a property of the game):
game.resize(width,height)
scene.cameras.main.setViewport(0,0,width,height)
For Phaser 3 the resize of the game now live inside the scale like this:
window.addEventListener('resize', () => {
game.scale.resize(window.innerWidth, window.innerHeight);
});
But if you need the entire game scale up and down only need this config:
const gameConfig: Phaser.Types.Core.GameConfig = {
...
scale: {
mode: Phaser.Scale.WIDTH_CONTROLS_HEIGHT,
},
...
};
export const game = new Phaser.Game(gameConfig);
Take a look at this article.
It explains how to dynamically resize the canvas while maintaining game ratio.
Note: All the code below is from the link above. I did not right any of this, it is sourced from the article linked above, but I am posting it here in case the link breaks in the future.
It uses CSS to center the canvas:
canvas{
display:block;
margin: 0;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
It listens to the window 'resize' event and calls a function to resize the game.
Listening to the event:
window.onload = function(){
var gameConfig = {
//config here
};
var game = new Phaser.Game(gameConfig);
resize();
window.addEventListener("resize", resize, false);
}
Resizing the game:
function resize() {
var canvas = document.querySelector("canvas");
var windowWidth = window.innerWidth;
var windowHeight = window.innerHeight;
var windowRatio = windowWidth / windowHeight;
var gameRatio = game.config.width / game.config.height;
if(windowRatio < gameRatio){
canvas.style.width = windowWidth + "px";
canvas.style.height = (windowWidth / gameRatio) + "px";
}
else{
canvas.style.width = (windowHeight * gameRatio) + "px";
canvas.style.height = windowHeight + "px";
}
}
I have used and modified this code in my phaser 3 project and it works great.
If you want to see other ways to resize dynamically check here
Put this in your create function:
const resize = ()=>{
game.scale.resize(window.innerWidth, window.innerHeight)
}
window.addEventListener("resize", resize, false);
Important! Make sure you have phaser 3.16+
Or else this won't work!!!
Use the game.resize function:
// when the page is loaded, create our game instance
window.onload = () => {
var game = new Game(config);
game.resize(400,700);
};
Note this only changes the canvas size, nothing else.

Resources