Neue Version des SignaturePads

This commit is contained in:
2023-04-24 19:22:09 +02:00
parent 8ef5fe78a6
commit 25d64637fc
48 changed files with 3976 additions and 177 deletions

View File

@@ -243,6 +243,7 @@
<Compile Include="Util\MobileUtils.cs" />
<Compile Include="Util\ReferenceNumberWithNotice.cs" />
<Compile Include="Util\ReportUtils\ConfirmationReceiptObject.cs" />
<Compile Include="Util\ReportUtils\Quittierungsbelegsunterschriftenobjekt.cs" />
<Compile Include="Util\ReportUtils\ReportTimeFrame.cs" />
<Compile Include="Util\ServiceRecord2Validation.cs" />
<Compile Include="Util\SignatureUtils.cs" />
@@ -24907,7 +24908,6 @@
<Content Include="Scripts\popper.js" />
<Content Include="Scripts\popper.min.js" />
<Content Include="Scripts\signature.js" />
<Content Include="Scripts\signature_pad-3.2.min.js" />
<Content Include="Scripts\src-min\ace.js" />
<Content Include="Scripts\src-min\ext-beautify.js" />
<Content Include="Scripts\src-min\ext-code_lens.js" />
@@ -25471,6 +25471,7 @@
<Content Include="Views\Report\QuittierungsbelegsResultPartial.cshtml" />
<Content Include="Views\ReportViewer\ReportViewer.cshtml" />
<Content Include="Views\ReportViewer\DocumentWebViewerPartial.cshtml" />
<Content Include="Views\Report\ConfirmationReceiptSignaturePartial.cshtml" />
</ItemGroup>
<ItemGroup>
<Folder Include="App_Data\" />
@@ -25828,7 +25829,7 @@
<VisualStudio>
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<UseIIS>False</UseIIS>
<UseIIS>True</UseIIS>
<AutoAssignPort>False</AutoAssignPort>
<DevelopmentServerPort>8808</DevelopmentServerPort>
<DevelopmentServerVPath>/</DevelopmentServerVPath>

View File

@@ -8,7 +8,7 @@
<IISExpressWindowsAuthentication />
<IISExpressUseClassicPipelineMode />
<UseGlobalApplicationHostFile />
<LastActiveSolutionConfig>Release|Any CPU</LastActiveSolutionConfig>
<LastActiveSolutionConfig>Debug|Any CPU</LastActiveSolutionConfig>
<ProjectView>ShowAllFiles</ProjectView>
<WebStackScaffolding_ViewDialogWidth>600</WebStackScaffolding_ViewDialogWidth>
<Controller_SelectedScaffolderID>MvcControllerEmptyScaffolder</Controller_SelectedScaffolderID>
@@ -29,7 +29,7 @@
<FlavorProperties GUID="{349c5851-65df-11da-9384-00065b846f21}">
<WebProjectProperties>
<StartPageUrl>http://localhost/BeWoPlanerMobil</StartPageUrl>
<StartAction>URL</StartAction>
<StartAction>CurrentPage</StartAction>
<AspNetDebugging>True</AspNetDebugging>
<SilverlightDebugging>False</SilverlightDebugging>
<NativeDebugging>False</NativeDebugging>

View File

@@ -696,13 +696,13 @@ namespace BeWoPlanerMobil.Controllers
{
try
{
if (Model is null)
if(Model is null)
{
Logout();
return LeerzeichenFuerGetMethoden;
}
using (var signatureBitmap = SignatureUtils.Base64StringToBitmap(blob.Split(',')[1]))
using(var signatureBitmap = SignatureUtils.Base64StringToBitmap(blob.Split(',')[1]))
{
var isValid = SignatureUtils.ValidateSignature(signatureBitmap);
@@ -723,7 +723,7 @@ namespace BeWoPlanerMobil.Controllers
return LeerzeichenFuerGetMethoden;
}
catch (Exception e)
catch(Exception e)
{
Log.Error(e.Message, e);
return SerializeObject("Fehler 540");

View File

@@ -743,7 +743,7 @@ namespace BeWoPlanerMobil.Controllers
{
try
{
if(Model == null)
if(Model is null)
{
Logout();
return LeerzeichenFuerGetMethoden;
@@ -765,14 +765,7 @@ namespace BeWoPlanerMobil.Controllers
var signatures = Model.GetConfirmationReceitSignaturesByOids(oids).Where(signature => signature.SignatureImage != null).ToList();
var imageStrings = new List<string>();
foreach(var signature in signatures)
{
imageStrings.AddIfNotIn(MainController.ApplyWatermark(signature.SignatureImage, Server.MapPath("../Content/images/bewoplaner_by_ownsoft_logo_mok.png")));
}
return MobileUtils.SerializeObject(imageStrings);
return MobileUtils.SerializeObject(ConvertSignatureDCsToSignatureObjects(signatures));
}
return LeerzeichenFuerGetMethoden;
@@ -783,6 +776,30 @@ namespace BeWoPlanerMobil.Controllers
}
}
private List<ConfirmationReceiptSignatureObject> ConvertSignatureDCsToSignatureObjects(List<ConfirmationReceiptSignatureDC> dataContracts)
{
var result = new List<ConfirmationReceiptSignatureObject>();
if(dataContracts is null)
{
return result;
}
foreach(var signature in dataContracts)
{
var img = MainController.ApplyWatermark(signature.SignatureImage, Server.MapPath("../Content/images/bewoplaner_by_ownsoft_logo_mok.png"));
var date = signature.InsTs?.ToString("dd.MM.yyyy HH:mm") ?? "unbekannt";
var personName =
signature.SignatureType == SignatureType.Customer ?
(Model.Customers.FirstOrDefault(f => f.CustomerOid.Equals(signature.CustomerOid))?.LastNameFirstName ?? "unbekannt") :
signature.Employee.SimpleDescription;
result.AddIfNotIn(new ConfirmationReceiptSignatureObject(img, date, personName, signature.SignatureType == SignatureType.Employee, signature.ServiceRecords.Count));
}
return result;
}
/// <summary>
/// Lädt die Übersicht über bereits geleistete oder fehlende Unterschriften für den ausgewählten Zeitraum
/// </summary>
@@ -1091,5 +1108,61 @@ namespace BeWoPlanerMobil.Controllers
OperationsService.DeleteConfirmationReceiptSignatures(signatures.ToDictionary(s => s.ConfirmationReceiptSignatureOid.Value, s => s.ConfirmationReceiptSignatureVersion.Value));
}
[Authorize]
public ActionResult FetchConfirmationReceiptSignatures(string identifier, bool isEmployee)
{
if(Model is null)
{
return Logout();
}
if(!Guid.TryParse(identifier, out var resultIdentifier))
{
return PartialView("ConfirmationReceiptSignaturePartial", Model);
}
var obj = Model.ConfirmationReceiptObject.ConfirmationReceiptResultList.FirstOrDefault(f => f.Identifier.Equals(resultIdentifier));
if(obj is null)
{
return PartialView("ConfirmationReceiptSignaturePartial", Model);
}
var oids = isEmployee ? obj.EmployeeSignatureOids : obj.CustomerSignatureOids;
var signatures = Model.GetConfirmationReceitSignaturesByOids(oids);
Model.SelectedConfirmationReceiptSignatures = ConvertSignatureDCsToSignatureObjects(signatures);
var customerName = obj.CustomerName;
var employeeName = MobileSessionFacade.LoggedInCompactEmployee.SimpleDescription;
var serviceRecordCount = 0;
signatures.DoForEach(sig => serviceRecordCount += sig.ServiceRecords.Count);
var personName = isEmployee ? employeeName : customerName;
Model.Quittierungsbelegsunterschriftenobjekt = new Quittierungsbelegsunterschriftenobjekt(isEmployee, personName, Model.ConfirmationReceiptObject.TimeSpanString, customerName, serviceRecordCount);
return PartialView("ConfirmationReceiptSignaturePartial", Model);
}
}
public class ConfirmationReceiptSignatureObject
{
public string ImageStr { get; }
public string InsTsStr { get; }
public string PersonName { get; }
public bool IsEmployeeSignature { get; }
public int NumberOfServiceRecords { get; }
public ConfirmationReceiptSignatureObject(string imageStr, string insTsStr, string personName, bool isEmployeeSignature, int numberOfServiceRecords)
{
ImageStr = imageStr;
InsTsStr = insTsStr;
PersonName = personName;
IsEmployeeSignature = isEmployeeSignature;
NumberOfServiceRecords = numberOfServiceRecords;
}
}
}

View File

@@ -84,6 +84,11 @@ namespace BeWoPlanerMobil.Controllers
Model.MyTeams = EmployeeService.GetAllActiveCompactTeamsForEmployee(Model.Employee.EmployeeOid.Value);
}
if(!(Model is null))
{
Model.TeamMemberCustomerOids = EmployeeService.LoadTeamsRelatedCustomerOids(MobileSessionFacade.LoggedInCompactEmployee.EmployeeOid);
}
LoadAppointmentsForDate();
return View(Model);

View File

@@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Web.Mvc;
using BeWoPlanerMobil.Controllers;
using BeWoPlanerMobil.Util.ReportUtils;
using BS.Shared.Core;
using BS.Shared.DataContracts;
@@ -276,5 +277,9 @@ namespace BeWoPlanerMobil.Models
public bool IsForSelectedEmployeesOnly { get; set; }
public bool IsForSelectedCostbearersOnly { get; set; }
public bool IsForSelectedCategoryOnly { get; set; }
public List<ConfirmationReceiptSignatureObject> SelectedConfirmationReceiptSignatures { get; set; }
public Quittierungsbelegsunterschriftenobjekt Quittierungsbelegsunterschriftenobjekt { get; set; }
}
}

View File

@@ -15,6 +15,8 @@ namespace BeWoPlanerMobil.Models
{
public class SchedulerModel : AbstractModel
{
public List<long> TeamMemberCustomerOids { get; set; }
public DateTime SelectedDate { get; set; } = DateTime.Today;
public SchedulerAppointmentDC SelectedAppointment { get; set; }
@@ -55,45 +57,7 @@ namespace BeWoPlanerMobil.Models
{
var appointment = AppointmentListItems.FirstOrDefault(app => app.Identifier.Equals(identifier))?.SchedulerAppointment; //Appointments.FirstOrDefault(app => app.SchedulerAppointmentOid.HasValue && app.SchedulerAppointmentOid.Value == appointmentOid);
if(appointment == null)
{
return false;
}
var hasCustomers = appointment.CustomerList.Any();
var hasEmployees = appointment.EmployeeList.Any();
var hasResources = appointment.ResourceList.Any();
var result = true;
if(hasEmployees)
{
result = hasResources ?
MobileSessionFacade.CheckForUserRight(UserRightType.KalenderRessourcentermineAndererAendern) :
MobileSessionFacade.CheckForUserRight(UserRightType.KalenderMitarbeitertermineAendern);
if (appointment.CustomerList.Any())
{
result = MobileSessionFacade.CheckForUserRight(UserRightType.KalenderKliententermineAendern) && result;
}
}
else
{
if(hasCustomers && hasResources)
{
result = MobileSessionFacade.CheckForUserRight(UserRightType.KalenderKliententermineAendern) && MobileSessionFacade.CheckForUserRight(UserRightType.KalenderRessourcentermineAendern);
}
else if(hasCustomers)
{
result = MobileSessionFacade.CheckForUserRight(UserRightType.KalenderKliententermineAendern);
}
else if(hasResources)
{
result = MobileSessionFacade.CheckForUserRight(UserRightType.KalenderRessourcentermineAendern);
}
}
return result;
return appointment != null && Utils.CheckSchedulerRights(appointment.CustomerList, appointment.ResourceList, appointment.EmployeeList, appointment.Originator, appointment.SchedulerAppointmentOid is null, SchedulerRightsCheckType.Edit, MobileSessionFacade.LoggedInUserDC, TeamMemberCustomerOids ?? new List<long>());
}
public string DescriptionToEdit => SelectedAppointment?.Description ?? string.Empty;

File diff suppressed because one or more lines are too long

View File

@@ -138,6 +138,7 @@ function onCustomerSelectionChange() {
// Lädt die bisher geleisteten Unterschriften
function getSignatures(signatureOidStrings, containerId) {
logInfo3("Lade vorhandene Unterschriften für Container '" + containerId + "' mit veralteter Methode");
try {
showSpinner();
@@ -148,16 +149,9 @@ function getSignatures(signatureOidStrings, containerId) {
if(isUndefinedOrNull(json) || !checkJson(json)) {
if(!checkJson(json)) {
hideSpinner();
logError("Ungültiges JSON in loadConfirmationReceiptSignatures. JSON: <" + json + ">");
}
if(isUndefinedOrNull(json)) {
logError("JSON ist undefined oder null in loadConfirmationReceiptSignatures");
}
}
logDebug("Gültiges JSON in loadConfirmationReceiptSignatures");
if(isEmptyOrSpaces(json)) {
hideSpinner();
return;
@@ -168,10 +162,12 @@ function getSignatures(signatureOidStrings, containerId) {
var newHtml = "";
$.each(signatureList, function(index, signature) {
newHtml += '<div class="row justify-content-md-center mt-2">' +
'<div class="col">' +
'<img src="' + signature + '" class="img-fluid mx-auto d-block"/>' +
"</div>" +
newHtml +=
'<div class="row justify-content-md-center mt-2">' +
'<div class="col">' +
"<div>Unterschrift vom " + signature.InsTsStr + "</div>" +
'<img src="' + signature.ImageStr + '" class="img-fluid mx-auto d-block"/>' +
"</div>" +
"</div>";
});
@@ -183,20 +179,43 @@ function getSignatures(signatureOidStrings, containerId) {
}
}
function cancelSignature(canSign, signaturePad) {
function cancelSignature(canSign, signaturePad, canvasId, signatureContainer, overridingAlertId) {
try {
if (canSign === false) {
hideSignatureContainer(canSign, signaturePad);
hideSignatureContainer(signatureContainer, overridingAlertId);
signaturePad.clear();
return;
}
var canvas = document.getElementById(canvasId);
var twoDContext = canvas.getContext("2d");
var width = canvas.getBoundingClientRect().width;
var height = canvas.getBoundingClientRect().height;
if (width === 0 || height === 0) {
hideSignatureContainer(signatureContainer, overridingAlertId);
signaturePad.clear();
return;
}
var isEmpty = signaturePad.isEmpty();
logInfo3("Canvas ist leer: " + isEmpty);
if(isEmpty === true) {
hideSignatureContainer(signatureContainer, overridingAlertId);
signaturePad.clear();
return;
}
showMessagePopupWithCallback("Unterschrift",
"Sind Sie sicher, dass Sie die Unterschrift nicht speichern möchten?",
function() {
hideSignatureContainer(canSign, signaturePad);
hideSignatureContainer(signatureContainer, overridingAlertId);
signaturePad.clear();
},
@@ -206,9 +225,8 @@ function cancelSignature(canSign, signaturePad) {
}
}
function hideSignatureContainer() {
$("#report-signature-container-div").removeClass("d-block");
$("#overwriting-signature-alert").removeClass("d-block");
function hideSignatureContainer(signatureContainer, overridingAlertId) {
$("#" + signatureContainer + ", #" + overridingAlertId).removeClass("d-block");
$("#report-form-container").show();
}
@@ -251,11 +269,11 @@ function showSignaturePad(overwrite, signatureOidsString, canSign, cardBodyId, c
var clearButton = document.getElementById("clear-report-canvas-button");
cancelButton.addEventListener("click", function() {
cancelSignature(canSign, signaturePad);
cancelSignature(canSign, signaturePad, "report-sketch-pad-canvas", "report-signature-container-div", "overwriting-signature-alert");
});
saveButton.addEventListener("click", function() {
saveSignature(customerOid, isEmployeeSignature, overwrite, signaturePad);
saveSignature(customerOid, isEmployeeSignature, overwrite, signaturePad, "report-sketch-pad-canvas");
});
clearButton.addEventListener("click", function() {
@@ -266,16 +284,21 @@ function showSignaturePad(overwrite, signatureOidsString, canSign, cardBodyId, c
}
}
function saveSignature(customerOid, isEmployeeSignature, overwrite, signaturePad) {
function saveSignature(customerOid, isEmployeeSignature, overwrite, signaturePad, canvasId) {
try {
var clonedCanvas = trimCanvas(cloneCanvas(document.getElementById("report-sketch-pad-canvas")));
var canvas = document.getElementById(canvasId);
var clonedCanvas = cloneCanvas(canvas);
var trimmedCanvas = trimCanvas(clonedCanvas);
var context = clonedCanvas.getContext("2d");
var context = trimmedCanvas.getContext("2d");
context.globalCompositeOperation = "destination-over";
context.fillStyle = "white";
context.fillRect(0, 0, clonedCanvas.width, clonedCanvas.height);
var dataUrl = clonedCanvas.toDataURL("image/png");
logInfo3(trimmedCanvas.width + " x " + trimmedCanvas.height);
context.fillRect(0, 0, trimmedCanvas.width, trimmedCanvas.height);
var dataUrl = trimmedCanvas.toDataURL("image/png");
showMessagePopupWithCallback(
"Unterschrift speichern",

View File

@@ -1,6 +1,5 @@
using System;
using BeWo.Report.ReportObjects;
using BS.Shared.DataContracts.Compact;
namespace BeWoPlanerMobil.Util
{

View File

@@ -10,6 +10,7 @@ namespace BeWoPlanerMobil.Util
public string CustomerName { get; }
// Die Liste mit den Zeiterfassungseinträgen
public List<QuittierungsbelegItem> Items { get; }
public long CustomerOid { get; }

View File

@@ -0,0 +1,20 @@
namespace BeWoPlanerMobil.Util.ReportUtils
{
public class Quittierungsbelegsunterschriftenobjekt
{
public bool IsEmployeeSignature { get; }
public string PersonName { get; }
public string TimeSpanInfo { get; }
public string CustomerName { get; }
public int ServiceRecordCount { get; }
public Quittierungsbelegsunterschriftenobjekt(bool isEmployeeSignature, string personName, string timeSpanInfo, string customerName, int serviceRecordCount)
{
IsEmployeeSignature = isEmployeeSignature;
PersonName = personName;
TimeSpanInfo = timeSpanInfo;
CustomerName = customerName;
ServiceRecordCount = serviceRecordCount;
}
}
}

View File

@@ -1,9 +1,7 @@
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Linq;
namespace BeWoPlanerMobil.Util
{
@@ -11,7 +9,7 @@ namespace BeWoPlanerMobil.Util
{
public static bool ValidateSignature(Bitmap signatureBitmap)
{
if(signatureBitmap == null)
if(signatureBitmap is null)
{
return false;
}
@@ -41,37 +39,37 @@ namespace BeWoPlanerMobil.Util
public static bool HasTransparentBackground(string base64Image)
{
if (string.IsNullOrWhiteSpace(base64Image))
if(string.IsNullOrWhiteSpace(base64Image))
{
return true;
}
if (base64Image.Contains(","))
if(base64Image.Contains(","))
{
base64Image = base64Image.Split(',')[1];
}
var whitePixelCount = 0;
using (var bitmap = Base64StringToBitmap(base64Image))
using(var bitmap = Base64StringToBitmap(base64Image))
{
if (bitmap.Width == 0 || bitmap.Height == 0)
if(bitmap.Width == 0 || bitmap.Height == 0)
{
return true;
}
for (var x = 0; x < bitmap.Width; x++)
for(var x = 0; x < bitmap.Width; x++)
{
for (var y = 0; y < bitmap.Height; y++)
for(var y = 0; y < bitmap.Height; y++)
{
var pixel = bitmap.GetPixel(x, y);
if (pixel.R == 255 && pixel.G == 255 && pixel.B == 255)
if(pixel.R == 255 && pixel.G == 255 && pixel.B == 255)
{
whitePixelCount++;
}
if (whitePixelCount == 20)
if(whitePixelCount == 20)
{
return false;
}

View File

@@ -0,0 +1,102 @@
@model BeWoPlanerMobil.Models.ReportModel
@{
var plural = Model.ConfirmationReceiptObject.NumberOfServiceRecords > 1 ? "en" : string.Empty;
var leistungsplurarl = Model.ConfirmationReceiptObject.NumberOfServiceRecords > 1 ? "en" : string.Empty;
var sigInfo = $"Unterschrift{plural} von {Model.Quittierungsbelegsunterschriftenobjekt.PersonName} für {Model.Quittierungsbelegsunterschriftenobjekt.ServiceRecordCount} erbrachte Leistung{leistungsplurarl} von {Model.Quittierungsbelegsunterschriftenobjekt.CustomerName} ({Model.Quittierungsbelegsunterschriftenobjekt.TimeSpanInfo})";
}
<div class="container-fluid">
<div class="row">
<div class="col">
<h4 class="d-inline" id="qb-sig-info-col">@sigInfo</h4>
<button type="button" class="btn btn-primary float-right" id="qb-sig-cancel-btn">
<span class="fas fa-times-circle"></span>
</button>
</div>
</div>
<div id="qb-sig-container" class="container mt-4 w-100">
@*
ToDo: Hier die bereits geleisteten Unterschriften anzeigen
xs: < 576px
sm: >= 576px
md: >= 768px
lg: >= 992px
xl: >= 1200px
xs: maximal 1
sm: maximal 1
md: maximal 2
lg: maximal 3
xl: maximal 4
*@
<div class="row">
@{
var count = Model.SelectedConfirmationReceiptSignatures.Count;
var colClass = $"col-sm-12 col-md-";
}
@foreach(var signature in Model.SelectedConfirmationReceiptSignatures)
{
var blubb = $"{(signature.NumberOfServiceRecords == 1 ? "eine" : signature.NumberOfServiceRecords.ToString())} Leistung{(signature.NumberOfServiceRecords != 1 ? "en" : string.Empty)}";
<div class="col-sm-12 col-md-6 col-lg-4 col-xl-3 mx-0 ">
<div class="card">
<img src="@signature.ImageStr" class="card-img-top" alt="Unterschrift von @signature.PersonName geleistet am @signature.InsTsStr"/>
<div class="card-body">
<div class="row">
<div class="col-5">
Unterschrift von:
</div>
<div class="col-7">
@signature.PersonName
</div>
</div>
<div class="row">
<div class="col-5">
Geleistet am:
</div>
<div class="col-7">
@signature.InsTsStr
</div>
</div>
<div class="row">
<div class="col-5">
Geleistet für:
</div>
<div class="col-7">
@blubb
</div>
</div>
</div>
</div>
</div>
}
</div>
</div>
<div class="row mt-3 signingContent">
<div class="col">
<div class="alert alert-primary d-none" role="alert" id="qb-sig-override-alert">
Achtung, Sie sind dabei eine vorhandene Unterschrift zu ersetzen!
</div>
</div>
</div>
<div class="row mt-3 signingContent" id="qb-sig-canvas-row">
<div class="canvas-container" id="qb-sig-canvas-container">
<canvas id="qb-sig-sketch-pad" width="500" heigh="200" style="border: 1px solid #343A40" class="bg-white rounded"></canvas>
</div>
</div>
<div class="row signingContent">
<div class="col-12">
<button type="button" class="btn btn-primary float-right" id="qb-sig-save-btn">Speichern</button>
<button type="button" class="btn btn-secondary float-right mr-3" id="qb-sig-clear-canvas-btn">Neu</button>
</div>
</div>
</div>

View File

@@ -28,6 +28,50 @@
showErrorPopup(error);
}
}
function loadConfirmationReceiptSignatures(cardBodyId, identifier, isEmployee, isOverriding, canSign, customerOid) {
cardHeaderButtonClick(cardBodyId);
$("#report-form-container").hide();
$("#report-signature-container").addClass("d-block");
$("#report-signature-container").load("@Url.Action("FetchConfirmationReceiptSignatures", "Report")", { identifier: identifier, isEmployee: isEmployee }, function() {
var overrideAlert = $("#qb-sig-override-alert");
if (isOverriding === true) {
overrideAlert.show();
} else {
overrideAlert.hide();
}
if (canSign) {
$("#qb-sig-canvas-row, #qb-sig-canvas-container, #qb-sig-sketch-pad").show();
} else {
$("#qb-sig-canvas-row, #qb-sig-canvas-container, #qb-sig-sketch-pad, #qb-sig-save-btn, #qb-sig-clear-canvas-btn").hide();
}
resizeCanvas("#qb-sig-sketch-pad", "#qb-sig-canvas-row");
var canvas = document.getElementById("qb-sig-sketch-pad");
var signaturePad = new SignaturePad(canvas, { backgroundColor: "rgba(255, 255, 255, 0)" });
var cancelButton = document.getElementById("qb-sig-cancel-btn");
var saveButton = document.getElementById("qb-sig-save-btn");
var clearButton = document.getElementById("qb-sig-clear-canvas-btn");
cancelButton.addEventListener("click", function () {
cancelSignature(canSign, signaturePad, "qb-sig-sketch-pad", "report-signature-container", "qb-sig-override-alert");
});
saveButton.addEventListener("click", function () {
saveSignature(customerOid, isEmployee, isOverriding, signaturePad, "qb-sig-sketch-pad");
});
clearButton.addEventListener("click", function () {
signaturePad.clear();
});
});
}
</script>
<div class="mt-3">
@@ -166,8 +210,18 @@
</button>
}
<button class="btn btn-bewo-customers" type="button" onclick="@methodName">
<span class="fas fa-signature"></span>
<span class="fas fa-@customerIcon" style="color: @customerBtnColor !important"></span>
<span class="fas fa-signature mr-0 pr-0"></span>
@if(customerSignatureState != SignatureState.All)
{
<span class="fa-stack align-middle bg-dark" style="height: auto !important">
<i class="fas fa-check fa-stack-1x" style="color: yellow !important"></i>
<i class="fas fa-slash fa-stack-1x text-danger"></i>
</span>
}
else
{
<span class="fas fa-check-double" style="color: yellowgreen !important"></span>
}
K
</button>
</div>
@@ -179,9 +233,19 @@
</button>
}
<button class="btn btn-bewo-employee" type="button" onclick="@employeeMethodName">
<span class="fas fa-signature"></span>
<span class="fas fa-@employeeIcon" style="color: @employeeBtnColor !important"></span>
<button class="btn btn-bewo-employee" type="button" onclick="loadConfirmationReceiptSignatures('@cardBodyId_ssp', '@result.Identifier', true, @overwrite_ssp, @canEmployeeSign.ToString().ToLower(), @result.CustomerOid)">
<span class="fas fa-signature mr-0 pr-0"></span>
@if(employeeSignatureState != SignatureState.All)
{
<span class="fa-stack align-middle" style="height: auto !important">
<i class="fas fa-check fa-stack-1x" style="color: yellow !important"></i>
<i class="fas fa-slash fa-stack-1x text-danger"></i>
</span>
}
else
{
<span class="fas fa-check-double" style="color: yellowgreen !important"></span>
}
M
</button>
</div>

View File

@@ -1,4 +1,5 @@
@model BeWoPlanerMobil.Models.ReportModel
@using BeWoPlanerMobil.Util
@model BeWoPlanerMobil.Models.ReportModel
@{
ViewBag.Title = "Quittierungsbeleg";
@@ -6,7 +7,7 @@
<style type="text/css">
.btn-group {
width: 123px;
width: 135px;
}
</style>
@@ -65,7 +66,13 @@
function createServiceOverview() {
try {
$("#report-signatures-container").load("@Url.Action("LoadServiceOverview")");
showSpinner();
$("#report-signatures-container").load("@Url.Action("LoadServiceOverview")",
function () {
hideSpinner();
}
);
} catch (error) {
showErrorPopup(error);
}
@@ -306,14 +313,16 @@
</div>
</div>
</div>
<div class="clearfix mt-3">
@if(Html.IsInDebugMode())
{
<button type="button" class="btn btn-bewo-dev float-left" data-toggle="modal" data-target="#qb-info-popup">
<span class="fas fa-info"></span>
</button>
}
<div class="d-flex bd-highlight mt-3">
<div class="mr-auto bd-highlight">
<!-- Abstandshalter -->
</div>
<div class="ml-3 bd-highlight">
<button type="button" class="btn btn-primary" id="create-button" onclick="createServiceOverview()">Erstellen</button>
</div>
<button type="button" class="btn btn-primary float-right" id="create-button" onclick="createServiceOverview()">Erstellen</button>
</div>
</div>
<div class="col col-12" id="report-signatures-container">
@@ -324,40 +333,9 @@
<!-- Container für die Mitarbeiterunterschrift -->
<div class="container-fluid d-none my-3" id="report-signature-container">
<div class="container-fluid">
<div class="row">
<div class="col">
<h4 class="d-inline" id="report-info-column">Unterschrift</h4>
<button type="button" class="btn btn-primary float-right" id="cancel-report-signature-btn">
<span class="fas fa-times-circle"></span>
</button>
</div>
</div>
<div id="employee-signature-container" class="mt-4">
</div>
<div class="row mt-3 signingContent">
<div class="col">
<div class="alert alert-primary d-none" role="alert" id="overwriting-employee-signature-alert">
Achtung, Sie sind dabei eine vorhandene Unterschrift zu ersetzen!
</div>
</div>
</div>
<div class="row mt-3 signingContent" id="report-canvas-row">
<div class="canvas-container" id="canvas-container">
<canvas id="report-sketch-pad" width="500" heigh="200" style="border: 1px solid #343A40" class="bg-white rounded"></canvas>
</div>
</div>
<div class="row signingContent">
<div class="col-12">
<button type="button" class="btn btn-primary float-right" id="save-report-signature-btn">Speichern</button>
<button type="button" class="btn btn-secondary float-right mr-3" id="clear-report-canvas-btn">Neu</button>
</div>
</div>
</div>
</div>
<!-- /Container für die Mitarbeiterunterschrift -->
<!-- Container für die Unterschrift -->
<div class="container-fluid d-none my-3" id="report-signature-container-div">
<div class="container-fluid">
@@ -392,4 +370,58 @@
</div>
</div>
</div>
<!-- /Container für die Unterschrift -->
<!-- /Container für die Unterschrift -->
<!-- Popup der Legende -->
<div class="modal" tabindex="-1" role="dialog" id="qb-info-popup">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title text-danger">Legende</h5>
<button class="close" type="button" data-dismiss="modal" aria-label="close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body" style="background-color: #F7F7F7 !important">
<button type="button" class="btn btn-bewo-customers d-inline">
<span class="fas fa-signature"></span>
<span class="fas fa-check" style="color: yellow !important"></span>
K
</button>
/
<button type="button" class="btn btn-bewo-employee d-inline">
<span class="fas fa-signature"></span>
<span class="fas fa-check" style="color: yellow !important"></span>
M
</button>
- Nicht alle Zeiterfassungseinträge haben eine Monatsunterschrift; Die Unterschrift wird nicht auf dem Quittierungsbeleg angezeigt.
<br />
<br />
<button type="button" class="btn btn-bewo-customers d-inline">
<span class="fas fa-signature"></span>
<span class="fas fa-check-double" style="color: greenyellow !important"></span>
K
</button>
/
<button type="button" class="btn btn-bewo-employee d-inline">
<span class="fas fa-signature"></span>
<span class="fas fa-check-double" style="color: greenyellow !important"></span>
M
</button>
- Alle Zeiterfassungseinträge haben eine Monatsunterschrift; Die Unterschrift wird auf dem Quittierungsbeleg angezeigt.
<br />
<br />
<p><span class="fas fa-check text-bewo-customers"></span> - Dieser Zeiterfassungseintrag hat vom Klienten eine Monatsunterschrift.</p>
<p><span class="fas fa-check text-bewo-employee"></span> - Dieser Zeiterfassungseintrag hat vom Mitarbeiter eine Monatsunterschrift.</p>
</div>
<div class="modal-footer">
<div class="clearfix">
<button class="btn btn-primary float-right" type="button" data-dismiss="modal">Schlie&szlig;en</button>
</div>
</div>
</div>
</div>
</div>
<!-- /Popup der Legende -->

View File

@@ -255,20 +255,20 @@
@if(Model.HasRightToEditAppointment(appointment.Identifier) && !appointment.IsTask && appointment.CanBeEdited)
{
<div class="row mb-3">
<div class="row mb-3">
@if(appointment.Oid.HasValue)
{
using(Html.BeginForm("SelectAppointmentToEdit", "Scheduler", FormMethod.Post))
{
<div class="col-auto">
<button type="submit" class="btn btn-primary" onclick="showSpinner()">
<span class="fas fa-edit"></span>
</button>
<input type="hidden" name="scheduler-oid-holder" value="@appointment.Oid" />
</div>
}
}
@if(appointment.Oid.HasValue)
{
using(Html.BeginForm("SelectAppointmentToEdit", "Scheduler", FormMethod.Post))
{
<div class="col-auto">
<button type="submit" class="btn btn-primary" onclick="showSpinner()">
<span class="fas fa-edit"></span>
</button>
<input type="hidden" name="scheduler-oid-holder" value="@appointment.Oid" />
</div>
}
}
<div class="col-auto">
<button type="button" class="btn btn-primary" onclick="deleteAppointment(@(appointment.Oid ?? 0), '@appointment.Identifier', @appointment.IsException.ToString().ToLower(), '@appointment.Subject')">

View File

@@ -46,29 +46,33 @@
<script src="@Url.Content("~/node_modules/popper.js/dist/umd/popper.js")"></script>
<script src="@Url.Content("~/node_modules/bootstrap/dist/js/bootstrap.js")"></script>
<link rel="stylesheet" href="~/Content/style.css?v=1.19">
<script src="~/Scripts/mobileUtils.js?v=1.18"></script>
<script src="~/Scripts/moment.min.js"></script>
<script src="~/Scripts/locale/de.js?v=1.18"></script>
<script src="~/Scripts/group-booking.js?v=1.18"></script>
@*<script src="~/Scripts/view-scripts/main.js?v=1.19"></script>*@
<link rel="stylesheet" href="@Url.Content("~/Content/style.css")" />
<link rel="stylesheet" href="@Url.Content("~/Content/tempusdominus-bootstrap-4.min.css")" />
<script src="@Url.Content("~/Scripts/moment.min.js")"></script>
<script src="@Url.Content("~/Scripts/locale/de.js")"></script>
<script src="@Url.Content("~/Scripts/tempusdominus-boostrap-4.min.js")"></script>
<script src="@Url.Content("~/Scripts/mobileUtils.js")"></script>
<script src="@Url.Content("~/Scripts/group-booking.js")"></script>
<script src="@Url.Content("~/Scripts/view-scripts/main.js")"></script>
<script src="~/Scripts/signature.js?v=1.18"></script>
<script src="~/Scripts/tempusdominus-boostrap-4.min.js"></script>
<link rel="stylesheet" href="~/Content/tempusdominus-bootstrap-4.min.css" />
<script src="~/Scripts/Chart.min.js?v=1.18"></script>
<script src="~/Scripts/support-concept-statistics.js?v=1.18"></script>
<script src="~/Scripts/view-scripts/customer.js?v=1.18"></script>
<script src="@Url.Content("~/Scripts/signature.js")"></script>
<script src="@Url.Content("~/Scripts/Chart.min.js")"></script>
<script src="@Url.Content("~/Scripts/support-concept-statistics.js")"></script>
<script src="@Url.Content("~/Scripts/view-scripts/customer.js")"></script>
<script src="@Url.Content("~/Scripts/view-scripts/scheduler.js")"></script>
<script src="~/Scripts/utils/dateUtils.js?v=1.18"></script>
<script src="~/Scripts/view-scripts/report.js?v=1.18"></script>
<script src="~/Scripts/signature_pad-3.2.min.js?v=1.18"></script>
<script src="~/Scripts/utils/trimCanvas.js?v=1.18"></script>
<script src="~/Scripts/utils/resources-list-utils.js?v=1.18"></script>
<script src="~/Scripts/utils/list-utils.js?v=1.18"></script>
<script src="~/Scripts/utils/service-record-time-calc.js?v=1.18"></script>
<script src="~/Scripts/utils/js-helper.js?v=1.18"></script>
<script src="~/Scripts/utils/logging.js?v=1.18"></script>
<script src="@Url.Content("~/Scripts/utils/dateUtils.js")"></script>
<script src="@Url.Content("~/Scripts/view-scripts/report.js")"></script>
<script src="@Url.Content("~/node_modules/signature_pad/dist/signature_pad.umd.min.js")"></script>
<script src="@Url.Content("~/Scripts/utils/trimCanvas.js")"></script>
<script src="@Url.Content("~/Scripts/utils/resources-list-utils.js")"></script>
<script src="@Url.Content("~/Scripts/utils/list-utils.js")"></script>
<script src="@Url.Content("~/Scripts/utils/service-record-time-calc.js")"></script>
<script src="@Url.Content("~/Scripts/utils/js-helper.js")"></script>
<script src="@Url.Content("~/Scripts/utils/logging.js")"></script>
@Html.DevExpress().GetStyleSheets(
new StyleSheet { ExtensionSuite = ExtensionSuite.Report }
@@ -435,7 +439,7 @@
<hr/>
<div class="form-group">
<label for="fehlerdetail-textarea">Fehlerdetails:</label>
<textarea class="form-control font-italic text-monospace" style="height: 20em;" id="fehlerdetail-textarea"></textarea>
<textarea class="form-control font-italic text-monospace" style="height: 20em; max-height: 20em; overflow-y: scroll;" id="fehlerdetail-textarea"></textarea>
</div>
</div>
<div class="modal-footer">

View File

@@ -5896,6 +5896,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signature_pad": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-4.1.5.tgz",
"integrity": "sha512-VOE846UbQMeLBbcR08KwjwE1wNLgp3gqC7yr/AELkgSMs/BdRpxIZna6K5XyZJpA7IWq9GiInw1C8PLm57VO6Q=="
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",

21
BeWoPlanerMobil/node_modules/signature_pad/LICENSE generated vendored Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2018 Szymon Nowak
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

231
BeWoPlanerMobil/node_modules/signature_pad/README.md generated vendored Normal file
View File

@@ -0,0 +1,231 @@
# Signature Pad [![npm](https://badge.fury.io/js/signature_pad.svg)](https://www.npmjs.com/package/signature_pad) [![tests](https://github.com/szimek/signature_pad/actions/workflows/test.yml/badge.svg)](https://github.com/szimek/signature_pad/actions/workflows/test.yml) [![Code Climate](https://codeclimate.com/github/szimek/signature_pad.png)](https://codeclimate.com/github/szimek/signature_pad) [![](https://data.jsdelivr.com/v1/package/npm/signature_pad/badge?style=rounded)](https://www.jsdelivr.com/package/npm/signature_pad)
Signature Pad is a JavaScript library for drawing smooth signatures. It's HTML5 canvas based and uses variable width Bézier curve interpolation based on [Smoother Signatures](https://developer.squareup.com/blog/smoother-signatures/) post by [Square](https://squareup.com).
It works in all modern desktop and mobile browsers and doesn't depend on any external libraries.
![Example](https://f.cloud.github.com/assets/9873/268046/9ced3454-8efc-11e2-816e-a9b170a51004.png)
## Demo
[Demo](http://szimek.github.io/signature_pad) works in desktop and mobile browsers. You can check out its [source code](https://github.com/szimek/signature_pad/blob/gh-pages/js/app.js) for some tips on how to handle window resize and high DPI screens. You can also find more about the latter in [HTML5 Rocks tutorial](http://www.html5rocks.com/en/tutorials/canvas/hidpi).
### Other demos
- Erase feature: <https://jsfiddle.net/szimek/jq9cyzuc/>
- Undo feature: <https://jsfiddle.net/szimek/osenxvjc/>
## Installation
You can install the latest release using npm:
```bash
npm install --save signature_pad
```
or Yarn:
```bash
yarn add signature_pad
```
You can also add it directly to your page using `<script>` tag:
```html
<script src="https://cdn.jsdelivr.net/npm/signature_pad@4.0.0/dist/signature_pad.umd.min.js"></script>
```
You can select a different version at [https://www.jsdelivr.com/package/npm/signature_pad](https://www.jsdelivr.com/package/npm/signature_pad).
This library is provided as UMD (Universal Module Definition) and ES6 module.
## Usage
### API
```javascript
const canvas = document.querySelector("canvas");
const signaturePad = new SignaturePad(canvas);
// Returns signature image as data URL (see https://mdn.io/todataurl for the list of possible parameters)
signaturePad.toDataURL(); // save image as PNG
signaturePad.toDataURL("image/jpeg"); // save image as JPEG
signaturePad.toDataURL("image/jpeg", 0.5); // save image as JPEG with 0.5 image quality
signaturePad.toDataURL("image/svg+xml"); // save image as SVG data url
// Return svg string without converting to base64
signaturePad.toSVG(); // "<svg...</svg>"
signaturePad.toSVG({includeBackgroundColor: true}); // add background color to svg output
// Draws signature image from data URL (mostly uses https://mdn.io/drawImage under-the-hood)
// NOTE: This method does not populate internal data structure that represents drawn signature. Thus, after using #fromDataURL, #toData won't work properly.
signaturePad.fromDataURL("data:image/png;base64,iVBORw0K...");
// Draws signature image from data URL and alters it with the given options
signaturePad.fromDataURL("data:image/png;base64,iVBORw0K...", { ratio: 1, width: 400, height: 200, xOffset: 100, yOffset: 50 });
// Returns signature image as an array of point groups
const data = signaturePad.toData();
// Draws signature image from an array of point groups
signaturePad.fromData(data);
// Draws signature image from an array of point groups, without clearing your existing image (clear defaults to true if not provided)
signaturePad.fromData(data, { clear: false });
// Clears the canvas
signaturePad.clear();
// Returns true if canvas is empty, otherwise returns false
signaturePad.isEmpty();
// Unbinds all event handlers
signaturePad.off();
// Rebinds all event handlers
signaturePad.on();
```
### Options
<dl>
<dt>dotSize</dt>
<dd>(float or function) Radius of a single dot. Also the width of the start of a mark.</dd>
<dt>minWidth</dt>
<dd>(float) Minimum width of a line. Defaults to <code>0.5</code>.</dd>
<dt>maxWidth</dt>
<dd>(float) Maximum width of a line. Defaults to <code>2.5</code>.</dd>
<dt>throttle</dt>
<dd>(integer) Draw the next point at most once per every <code>x</code> milliseconds. Set it to <code>0</code> to turn off throttling. Defaults to <code>16</code>.</dd>
<dt>minDistance</dt>
<dd>(integer) Add the next point only if the previous one is farther than <code>x</code> pixels. Defaults to <code>5</code>.
<dt>backgroundColor</dt>
<dd>(string) Color used to clear the background. Can be any color format accepted by <code>context.fillStyle</code>. Defaults to <code>"rgba(0,0,0,0)"</code> (transparent black). Use a non-transparent color e.g. <code>"rgb(255,255,255)"</code> (opaque white) if you'd like to save signatures as JPEG images.</dd>
<dt>penColor</dt>
<dd>(string) Color used to draw the lines. Can be any color format accepted by <code>context.fillStyle</code>. Defaults to <code>"black"</code>.</dd>
<dt>velocityFilterWeight</dt>
<dd>(float) Weight used to modify new velocity based on the previous velocity. Defaults to <code>0.7</code>.</dd>
</dl>
You can set options during initialization:
```javascript
const signaturePad = new SignaturePad(canvas, {
minWidth: 5,
maxWidth: 10,
penColor: "rgb(66, 133, 244)"
});
```
or during runtime:
```javascript
const signaturePad = new SignaturePad(canvas);
signaturePad.minWidth = 5;
signaturePad.maxWidth = 10;
signaturePad.penColor = "rgb(66, 133, 244)";
```
### Events
<dl>
<dt>beginStroke</dt>
<dd>Triggered before stroke begins.</dd>
<dt>endStroke</dt>
<dd>Triggered after stroke ends.</dd>
<dt>beforeUpdateStroke</dt>
<dd>Triggered before stroke update.</dd>
<dt>afterUpdateStroke</dt>
<dd>Triggered after stroke update.</dd>
</dl>
You can add listeners to events with [`.addEventListener`](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener):
```javascript
const signaturePad = new SignaturePad(canvas);
signaturePad.addEventListener("beginStroke", () => {
console.log("Signature started");
}, { once: true });
```
### Tips and tricks
#### Handling high DPI screens
To correctly handle canvas on low and high DPI screens one has to take `devicePixelRatio` into account and scale the canvas accordingly. This scaling is also necessary to properly display signatures loaded via `SignaturePad#fromDataURL`. Here's an example how it can be done:
```javascript
function resizeCanvas() {
const ratio = Math.max(window.devicePixelRatio || 1, 1);
canvas.width = canvas.offsetWidth * ratio;
canvas.height = canvas.offsetHeight * ratio;
canvas.getContext("2d").scale(ratio, ratio);
signaturePad.clear(); // otherwise isEmpty() might return incorrect value
}
window.addEventListener("resize", resizeCanvas);
resizeCanvas();
```
Instead of `resize` event you can listen to screen orientation change, if you're using this library only on mobile devices. You can also throttle the `resize` event - you can find some examples on [this MDN page](https://developer.mozilla.org/en-US/docs/Web/Events/resize).
#### Handling canvas resize
When you modify width or height of a canvas, it will be automatically cleared by the browser. SignaturePad doesn't know about it by itself, so you can call `signaturePad.fromData(signaturePad.toData())` to reset the drawing, or `signaturePad.clear()` to make sure that `signaturePad.isEmpty()` returns correct value in this case.
This clearing of the canvas by the browser can be annoying, especially on mobile devices e.g. when screen orientation is changed. There are a few workarounds though, e.g. you can [lock screen orientation](https://developer.mozilla.org/en-US/docs/Web/API/Screen/lockOrientation), or read an image from the canvas before resizing it and write the image back after.
#### Handling data URI encoded images on the server side
If you are not familiar with data URI scheme, you can read more about it on [Wikipedia](http://en.wikipedia.org/wiki/Data_URI_scheme).
There are 2 ways you can handle data URI encoded images.
You could simply store it in your database as a string and display it in HTML like this:
```html
<img src="data:image/png;base64,iVBORw0K..." />
```
but this way has many disadvantages - it's not easy to get image dimensions, you can't manipulate it e.g. to create a thumbnail and it also [has some performance issues on mobile devices](http://www.mobify.com/blog/data-uris-are-slow-on-mobile/).
Thus, more common way is to decode it and store as a file. Here's an example in Ruby:
```ruby
require "base64"
data_uri = "data:image/png;base64,iVBORw0K..."
encoded_image = data_uri.split(",")[1]
decoded_image = Base64.decode64(encoded_image)
File.open("signature.png", "wb") { |f| f.write(decoded_image) }
```
Here's an example in PHP:
```php
$data_uri = "data:image/png;base64,iVBORw0K...";
$encoded_image = explode(",", $data_uri)[1];
$decoded_image = base64_decode($encoded_image);
file_put_contents("signature.png", $decoded_image);
```
Here's an example in C# for ASP.NET:
```csharp
var dataUri = "data:image/png;base64,iVBORw0K...";
var encodedImage = dataUri.Split(',')[1];
var decodedImage = Convert.FromBase64String(encodedImage);
System.IO.File.WriteAllBytes("signature.png", decodedImage);
```
#### Removing empty space around a signature
If you'd like to remove (trim) empty space around a signature, you can do it on the server side or the client side. On the server side you can use e.g. ImageMagic and its `trim` option: `convert -trim input.jpg output.jpg`. If you don't have access to the server, or just want to trim the image before submitting it to the server, you can do it on the client side as well. There are a few examples how to do it, e.g. [here](https://github.com/szimek/signature_pad/issues/49#issue-29108215) or [here](https://github.com/szimek/signature_pad/issues/49#issuecomment-260976909) and there's also a tiny library [trim-canvas](https://github.com/agilgur5/trim-canvas) that provides this functionality.
#### Drawing over an image
Demo: <https://jsfiddle.net/szimek/d6a78gwq/>
## License
Released under the [MIT License](http://www.opensource.org/licenses/MIT).

View File

@@ -0,0 +1,559 @@
/*!
* Signature Pad v4.1.5 | https://github.com/szimek/signature_pad
* (c) 2023 Szymon Nowak | Released under the MIT license
*/
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
super();
this.canvas = canvas;
this._drawningStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (event.buttons === 1) {
this._drawningStroke = true;
this._strokeBegin(event);
}
};
this._handleMouseMove = (event) => {
if (this._drawningStroke) {
this._strokeMoveUpdate(event);
}
};
this._handleMouseUp = (event) => {
if (event.buttons === 1 && this._drawningStroke) {
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this._handleTouchStart = (event) => {
if (event.cancelable) {
event.preventDefault();
}
if (event.targetTouches.length === 1) {
const touch = event.changedTouches[0];
this._strokeBegin(touch);
}
};
this._handleTouchMove = (event) => {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.targetTouches[0];
this._strokeMoveUpdate(touch);
};
this._handleTouchEnd = (event) => {
const wasCanvasTouched = event.target === this.canvas;
if (wasCanvasTouched) {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.changedTouches[0];
this._strokeEnd(touch);
}
};
this._handlePointerStart = (event) => {
this._drawningStroke = true;
event.preventDefault();
this._strokeBegin(event);
};
this._handlePointerMove = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._strokeMoveUpdate(event);
}
};
this._handlePointerEnd = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = ('throttle' in options ? options.throttle : 16);
this.minDistance = ('minDistance' in options ? options.minDistance : 5);
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d');
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
this.canvas.removeEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.removeEventListener('pointerup', this._handlePointerEnd);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.removeEventListener('mouseup', this._handleMouseUp);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this.canvas.removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
};
}
_strokeBegin(event) {
this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const x = event.clientX;
const y = event.clientY;
const pressure = event.pressure !== undefined
? event.pressure
: event.force !== undefined
? event.force
: 0;
const point = this._createPoint(x, y, pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event) {
this._strokeUpdate(event);
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerStart);
this.canvas.addEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.addEventListener('pointerup', this._handlePointerEnd);
}
_handleMouseEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
this.canvas.addEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.addEventListener('mouseup', this._handleMouseUp);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
this.canvas.addEventListener('touchmove', this._handleTouchMove);
this.canvas.addEventListener('touchend', this._handleTouchEnd);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
export { SignaturePad as default };
//# sourceMappingURL=signature_pad.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,567 @@
/*!
* Signature Pad v4.1.5 | https://github.com/szimek/signature_pad
* (c) 2023 Szymon Nowak | Released under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SignaturePad = factory());
})(this, (function () { 'use strict';
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
super();
this.canvas = canvas;
this._drawningStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (event.buttons === 1) {
this._drawningStroke = true;
this._strokeBegin(event);
}
};
this._handleMouseMove = (event) => {
if (this._drawningStroke) {
this._strokeMoveUpdate(event);
}
};
this._handleMouseUp = (event) => {
if (event.buttons === 1 && this._drawningStroke) {
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this._handleTouchStart = (event) => {
if (event.cancelable) {
event.preventDefault();
}
if (event.targetTouches.length === 1) {
const touch = event.changedTouches[0];
this._strokeBegin(touch);
}
};
this._handleTouchMove = (event) => {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.targetTouches[0];
this._strokeMoveUpdate(touch);
};
this._handleTouchEnd = (event) => {
const wasCanvasTouched = event.target === this.canvas;
if (wasCanvasTouched) {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.changedTouches[0];
this._strokeEnd(touch);
}
};
this._handlePointerStart = (event) => {
this._drawningStroke = true;
event.preventDefault();
this._strokeBegin(event);
};
this._handlePointerMove = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._strokeMoveUpdate(event);
}
};
this._handlePointerEnd = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = ('throttle' in options ? options.throttle : 16);
this.minDistance = ('minDistance' in options ? options.minDistance : 5);
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d');
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
this.canvas.removeEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.removeEventListener('pointerup', this._handlePointerEnd);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.removeEventListener('mouseup', this._handleMouseUp);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this.canvas.removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
};
}
_strokeBegin(event) {
this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const x = event.clientX;
const y = event.clientY;
const pressure = event.pressure !== undefined
? event.pressure
: event.force !== undefined
? event.force
: 0;
const point = this._createPoint(x, y, pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event) {
this._strokeUpdate(event);
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerStart);
this.canvas.addEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.addEventListener('pointerup', this._handlePointerEnd);
}
_handleMouseEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
this.canvas.addEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.addEventListener('mouseup', this._handleMouseUp);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
this.canvas.addEventListener('touchmove', this._handleTouchMove);
this.canvas.addEventListener('touchend', this._handleTouchEnd);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
return SignaturePad;
}));
//# sourceMappingURL=signature_pad.umd.js.map

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,17 @@
import { BasicPoint, Point } from './point';
export declare class Bezier {
startPoint: Point;
control2: BasicPoint;
control1: BasicPoint;
endPoint: Point;
startWidth: number;
endWidth: number;
static fromPoints(points: Point[], widths: {
start: number;
end: number;
}): Bezier;
private static calculateControlPoints;
constructor(startPoint: Point, control2: BasicPoint, control1: BasicPoint, endPoint: Point, startWidth: number, endWidth: number);
length(): number;
private point;
}

View File

@@ -0,0 +1,16 @@
export interface BasicPoint {
x: number;
y: number;
pressure: number;
time: number;
}
export declare class Point implements BasicPoint {
x: number;
y: number;
pressure: number;
time: number;
constructor(x: number, y: number, pressure?: number, time?: number);
distanceTo(start: BasicPoint): number;
equals(other: BasicPoint): boolean;
velocityFrom(start: BasicPoint): number;
}

View File

@@ -0,0 +1,7 @@
export declare class SignatureEventTarget {
private _et;
constructor();
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
dispatchEvent(event: Event): boolean;
removeEventListener(type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | EventListenerOptions): void;
}

View File

@@ -0,0 +1,100 @@
/**
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* https://web.archive.org/web/20160323213433/http://www.benknowscode.com/2012/09/path-interpolation-using-cubic-bezier_9742.html
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*/
import { BasicPoint } from './point';
import { SignatureEventTarget } from './signature_event_target';
declare global {
interface CSSStyleDeclaration {
msTouchAction: string | null;
}
}
export declare type SignatureEvent = MouseEvent | Touch | PointerEvent;
export interface FromDataOptions {
clear?: boolean;
}
export interface ToSVGOptions {
includeBackgroundColor?: boolean;
}
export interface PointGroupOptions {
dotSize: number;
minWidth: number;
maxWidth: number;
penColor: string;
velocityFilterWeight: number;
}
export interface Options extends Partial<PointGroupOptions> {
minDistance?: number;
backgroundColor?: string;
throttle?: number;
}
export interface PointGroup extends PointGroupOptions {
points: BasicPoint[];
}
export default class SignaturePad extends SignatureEventTarget {
private canvas;
dotSize: number;
minWidth: number;
maxWidth: number;
penColor: string;
minDistance: number;
velocityFilterWeight: number;
backgroundColor: string;
throttle: number;
private _ctx;
private _drawningStroke;
private _isEmpty;
private _lastPoints;
private _data;
private _lastVelocity;
private _lastWidth;
private _strokeMoveUpdate;
constructor(canvas: HTMLCanvasElement, options?: Options);
clear(): void;
fromDataURL(dataUrl: string, options?: {
ratio?: number;
width?: number;
height?: number;
xOffset?: number;
yOffset?: number;
}): Promise<void>;
toDataURL(type: 'image/svg+xml', encoderOptions?: ToSVGOptions): string;
toDataURL(type?: string, encoderOptions?: number): string;
on(): void;
off(): void;
isEmpty(): boolean;
fromData(pointGroups: PointGroup[], { clear }?: FromDataOptions): void;
toData(): PointGroup[];
private _handleMouseDown;
private _handleMouseMove;
private _handleMouseUp;
private _handleTouchStart;
private _handleTouchMove;
private _handleTouchEnd;
private _handlePointerStart;
private _handlePointerMove;
private _handlePointerEnd;
private _getPointGroupOptions;
private _strokeBegin;
private _strokeUpdate;
private _strokeEnd;
private _handlePointerEvents;
private _handleMouseEvents;
private _handleTouchEvents;
private _reset;
private _createPoint;
private _addPoint;
private _calculateCurveWidths;
private _strokeWidth;
private _drawCurveSegment;
private _drawCurve;
private _drawDot;
private _fromData;
toSVG({ includeBackgroundColor }?: ToSVGOptions): string;
}

View File

@@ -0,0 +1 @@
export declare function throttle(fn: (...args: any[]) => any, wait?: number): (this: any, ...args: any[]) => any;

View File

@@ -0,0 +1,5 @@
module.exports = {
globals: {
SignaturePad: false
}
};

View File

@@ -0,0 +1,20 @@
.signature-pad {
margin: auto;
height: auto;
}
.signature-pad--body {
min-height: 360px;
}
.signature-pad--actions {
overflow: hidden;
}
.signature-pad--actions > div:first-child {
float: left;
}
.signature-pad--actions > div:last-child {
float: right;
}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,61 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Signature Pad demo</title>
<meta name="description" content="Signature Pad - HTML5 canvas based smooth signature drawing using variable width spline interpolation.">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1, user-scalable=no">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black">
<link rel="stylesheet" href="css/signature-pad.css">
<script type="text/javascript">
var _gaq = _gaq || [];
_gaq.push(['_setAccount', 'UA-39365077-1']);
_gaq.push(['_trackPageview']);
(function() {
var ga = document.createElement('script'); ga.type = 'text/javascript'; ga.async = true;
ga.src = ('https:' == document.location.protocol ? 'https://ssl' : 'http://www') + '.google-analytics.com/ga.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(ga, s);
})();
</script>
</head>
<body onselectstart="return false">
<a id="github" style="position: absolute; top: 0; right: 0; border: 0" href="https://github.com/szimek/signature_pad">
<img src="https://s3.amazonaws.com/github/ribbons/forkme_right_gray_6d6d6d.png" alt="Fork me on GitHub">
</a>
<div id="signature-pad" class="signature-pad">
<div class="signature-pad--body">
<canvas></canvas>
</div>
<div class="signature-pad--footer">
<div class="description">Sign above</div>
<div class="signature-pad--actions">
<div class="column">
<button type="button" class="button clear" data-action="clear">Clear</button>
<button type="button" class="button" data-action="change-background-color">Change background color</button>
<button type="button" class="button" data-action="change-color">Change color</button>
<button type="button" class="button" data-action="change-width">Change width</button>
<button type="button" class="button" data-action="undo">Undo</button>
</div>
<div class="column">
<button type="button" class="button save" data-action="save-png">Save as PNG</button>
<button type="button" class="button save" data-action="save-jpg">Save as JPG</button>
<button type="button" class="button save" data-action="save-svg">Save as SVG</button>
<button type="button" class="button save" data-action="save-svg-with-background">Save as SVG with background</button>
</div>
</div>
</div>
</div>
<script src="js/signature_pad.umd.js"></script>
<script src="js/app.js"></script>
</body>
</html>

View File

@@ -0,0 +1,156 @@
const wrapper = document.getElementById("signature-pad");
const clearButton = wrapper.querySelector("[data-action=clear]");
const changeBackgroundColorButton = wrapper.querySelector("[data-action=change-background-color]");
const changeColorButton = wrapper.querySelector("[data-action=change-color]");
const changeWidthButton = wrapper.querySelector("[data-action=change-width]");
const undoButton = wrapper.querySelector("[data-action=undo]");
const savePNGButton = wrapper.querySelector("[data-action=save-png]");
const saveJPGButton = wrapper.querySelector("[data-action=save-jpg]");
const saveSVGButton = wrapper.querySelector("[data-action=save-svg]");
const saveSVGWithBackgroundButton = wrapper.querySelector("[data-action=save-svg-with-background]");
const canvas = wrapper.querySelector("canvas");
const signaturePad = new SignaturePad(canvas, {
// It's Necessary to use an opaque color when saving image as JPEG;
// this option can be omitted if only saving as PNG or SVG
backgroundColor: 'rgb(255, 255, 255)'
});
// Adjust canvas coordinate space taking into account pixel ratio,
// to make it look crisp on mobile devices.
// This also causes canvas to be cleared.
function resizeCanvas() {
// 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.
const 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);
// This library does not listen for canvas changes, so after the canvas is automatically
// cleared by the browser, SignaturePad#isEmpty might still return false, even though the
// canvas looks empty, because the internal data of this library wasn't cleared. To make sure
// that the state of this library is consistent with visual state of the canvas, you
// have to clear it manually.
//signaturePad.clear();
// If you want to keep the drawing on resize instead of clearing it you can reset the data.
signaturePad.fromData(signaturePad.toData());
}
// On mobile devices it might make more sense to listen to orientation change,
// rather than window resize events.
window.onresize = resizeCanvas;
resizeCanvas();
function download(dataURL, filename) {
const blob = dataURLToBlob(dataURL);
const url = window.URL.createObjectURL(blob);
const a = document.createElement("a");
a.style = "display: none";
a.href = url;
a.download = filename;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}
// One could simply use Canvas#toBlob method instead, but it's just to show
// that it can be done using result of SignaturePad#toDataURL.
function dataURLToBlob(dataURL) {
// Code taken from https://github.com/ebidel/filer.js
const parts = dataURL.split(';base64,');
const contentType = parts[0].split(":")[1];
const raw = window.atob(parts[1]);
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i < rawLength; ++i) {
uInt8Array[i] = raw.charCodeAt(i);
}
return new Blob([uInt8Array], { type: contentType });
}
clearButton.addEventListener("click", () => {
signaturePad.clear();
});
undoButton.addEventListener("click", () => {
const data = signaturePad.toData();
if (data) {
data.pop(); // remove the last dot or line
signaturePad.fromData(data);
}
});
changeBackgroundColorButton.addEventListener("click", () => {
const r = Math.round(Math.random() * 255);
const g = Math.round(Math.random() * 255);
const b = Math.round(Math.random() * 255);
const color = "rgb(" + r + "," + g + "," + b +")";
signaturePad.backgroundColor = color;
const data = signaturePad.toData();
signaturePad.clear();
signaturePad.fromData(data);
});
changeColorButton.addEventListener("click", () => {
const r = Math.round(Math.random() * 255);
const g = Math.round(Math.random() * 255);
const b = Math.round(Math.random() * 255);
const color = "rgb(" + r + "," + g + "," + b +")";
signaturePad.penColor = color;
});
changeWidthButton.addEventListener("click", () => {
const min = Math.round(Math.random() * 100) / 10;
const max = Math.round(Math.random() * 100) / 10;
signaturePad.minWidth = Math.min(min, max);
signaturePad.maxWidth = Math.max(min, max);
});
savePNGButton.addEventListener("click", () => {
if (signaturePad.isEmpty()) {
alert("Please provide a signature first.");
} else {
const dataURL = signaturePad.toDataURL();
download(dataURL, "signature.png");
}
});
saveJPGButton.addEventListener("click", () => {
if (signaturePad.isEmpty()) {
alert("Please provide a signature first.");
} else {
const dataURL = signaturePad.toDataURL("image/jpeg");
download(dataURL, "signature.jpg");
}
});
saveSVGButton.addEventListener("click", () => {
if (signaturePad.isEmpty()) {
alert("Please provide a signature first.");
} else {
const dataURL = signaturePad.toDataURL('image/svg+xml');
download(dataURL, "signature.svg");
}
});
saveSVGWithBackgroundButton.addEventListener("click", () => {
if (signaturePad.isEmpty()) {
alert("Please provide a signature first.");
} else {
const dataURL = signaturePad.toDataURL('image/svg+xml', {includeBackgroundColor: true});
download(dataURL, "signature.svg");
}
});

View File

@@ -0,0 +1,567 @@
/*!
* Signature Pad v4.1.5 | https://github.com/szimek/signature_pad
* (c) 2023 Szymon Nowak | Released under the MIT license
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.SignaturePad = factory());
})(this, (function () { 'use strict';
class Point {
constructor(x, y, pressure, time) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
distanceTo(start) {
return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
}
equals(other) {
return (this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time);
}
velocityFrom(start) {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}
class Bezier {
constructor(startPoint, control2, control1, endPoint, startWidth, endWidth) {
this.startPoint = startPoint;
this.control2 = control2;
this.control1 = control1;
this.endPoint = endPoint;
this.startWidth = startWidth;
this.endWidth = endWidth;
}
static fromPoints(points, widths) {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
static calculateControlPoints(s1, s2, s3) {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
length() {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(t, this.startPoint.x, this.control1.x, this.control2.x, this.endPoint.x);
const cy = this.point(t, this.startPoint.y, this.control1.y, this.control2.y, this.endPoint.y);
if (i > 0) {
const xdiff = cx - px;
const ydiff = cy - py;
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
point(t, start, c1, c2, end) {
return (start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ (end * t * t * t);
}
}
class SignatureEventTarget {
constructor() {
try {
this._et = new EventTarget();
}
catch (error) {
this._et = document;
}
}
addEventListener(type, listener, options) {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event) {
return this._et.dispatchEvent(event);
}
removeEventListener(type, callback, options) {
this._et.removeEventListener(type, callback, options);
}
}
function throttle(fn, wait = 250) {
let previous = 0;
let timeout = null;
let result;
let storedContext;
let storedArgs;
const later = () => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(...args) {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
}
else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}
class SignaturePad extends SignatureEventTarget {
constructor(canvas, options = {}) {
super();
this.canvas = canvas;
this._drawningStroke = false;
this._isEmpty = true;
this._lastPoints = [];
this._data = [];
this._lastVelocity = 0;
this._lastWidth = 0;
this._handleMouseDown = (event) => {
if (event.buttons === 1) {
this._drawningStroke = true;
this._strokeBegin(event);
}
};
this._handleMouseMove = (event) => {
if (this._drawningStroke) {
this._strokeMoveUpdate(event);
}
};
this._handleMouseUp = (event) => {
if (event.buttons === 1 && this._drawningStroke) {
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this._handleTouchStart = (event) => {
if (event.cancelable) {
event.preventDefault();
}
if (event.targetTouches.length === 1) {
const touch = event.changedTouches[0];
this._strokeBegin(touch);
}
};
this._handleTouchMove = (event) => {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.targetTouches[0];
this._strokeMoveUpdate(touch);
};
this._handleTouchEnd = (event) => {
const wasCanvasTouched = event.target === this.canvas;
if (wasCanvasTouched) {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.changedTouches[0];
this._strokeEnd(touch);
}
};
this._handlePointerStart = (event) => {
this._drawningStroke = true;
event.preventDefault();
this._strokeBegin(event);
};
this._handlePointerMove = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._strokeMoveUpdate(event);
}
};
this._handlePointerEnd = (event) => {
if (this._drawningStroke) {
event.preventDefault();
this._drawningStroke = false;
this._strokeEnd(event);
}
};
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = ('throttle' in options ? options.throttle : 16);
this.minDistance = ('minDistance' in options ? options.minDistance : 5);
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d');
this.clear();
this.on();
}
clear() {
const { _ctx: ctx, canvas } = this;
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
fromDataURL(dataUrl, options = {}) {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = () => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error) => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
toDataURL(type = 'image/png', encoderOptions) {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(this.toSVG(encoderOptions))}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
on() {
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS = /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
}
else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
off() {
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
this.canvas.removeEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.removeEventListener('pointerup', this._handlePointerEnd);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.removeEventListener('mouseup', this._handleMouseUp);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this.canvas.removeEventListener('touchend', this._handleTouchEnd);
}
isEmpty() {
return this._isEmpty;
}
fromData(pointGroups, { clear = true } = {}) {
if (clear) {
this.clear();
}
this._fromData(pointGroups, this._drawCurve.bind(this), this._drawDot.bind(this));
this._data = this._data.concat(pointGroups);
}
toData() {
return this._data;
}
_getPointGroupOptions(group) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight: group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
};
}
_strokeBegin(event) {
this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup = Object.assign(Object.assign({}, pointGroupOptions), { points: [] });
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
_strokeUpdate(event) {
if (this._data.length === 0) {
this._strokeBegin(event);
return;
}
this.dispatchEvent(new CustomEvent('beforeUpdateStroke', { detail: event }));
const x = event.clientX;
const y = event.clientY;
const pressure = event.pressure !== undefined
? event.pressure
: event.force !== undefined
? event.force
: 0;
const point = this._createPoint(x, y, pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint = lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
}
else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
_strokeEnd(event) {
this._strokeUpdate(event);
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
_handlePointerEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerStart);
this.canvas.addEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.addEventListener('pointerup', this._handlePointerEnd);
}
_handleMouseEvents() {
this._drawningStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
this.canvas.addEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.addEventListener('mouseup', this._handleMouseUp);
}
_handleTouchEvents() {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
this.canvas.addEventListener('touchmove', this._handleTouchMove);
this.canvas.addEventListener('touchend', this._handleTouchEnd);
}
_reset(options) {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
}
_createPoint(x, y, pressure) {
const rect = this.canvas.getBoundingClientRect();
return new Point(x - rect.left, y - rect.top, pressure, new Date().getTime());
}
_addPoint(point, options) {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
const widths = this._calculateCurveWidths(_lastPoints[1], _lastPoints[2], options);
const curve = Bezier.fromPoints(_lastPoints, widths);
_lastPoints.shift();
return curve;
}
return null;
}
_calculateCurveWidths(startPoint, endPoint, options) {
const velocity = options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
_strokeWidth(velocity, options) {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
_drawCurveSegment(x, y, width) {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
_drawCurve(curve, options) {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(curve.startWidth + ttt * widthDelta, options.maxWidth);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
_drawDot(point, options) {
const ctx = this._ctx;
const width = options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
_fromData(pointGroups, drawCurve, drawDot) {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(basicPoint.x, basicPoint.y, basicPoint.pressure, basicPoint.time);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
}
else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
toSVG({ includeBackgroundColor = false } = {}) {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(pointGroups, (curve, { penColor }) => {
const path = document.createElement('path');
if (!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)) {
const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
}, (point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
});
return svg.outerHTML;
}
}
return SignaturePad;
}));
//# sourceMappingURL=signature_pad.umd.js.map

103
BeWoPlanerMobil/node_modules/signature_pad/package.json generated vendored Normal file
View File

@@ -0,0 +1,103 @@
{
"name": "signature_pad",
"description": "Library for drawing smooth signatures.",
"version": "4.1.5",
"homepage": "https://github.com/szimek/signature_pad",
"author": {
"name": "Szymon Nowak",
"email": "szimek@gmail.com",
"url": "https://github.com/szimek"
},
"license": "MIT",
"source": "src/signature_pad.ts",
"main": "dist/signature_pad.umd.js",
"module": "dist/signature_pad.js",
"types": "dist/types/signature_pad.d.ts",
"scripts": {
"build": "yarn run lint && yarn run clean && rollup --config && yarn run emit-types && yarn run update-docs",
"clean": "yarn run del dist",
"emit-types": "yarn run del dist/types && yarn run tsc src/signature_pad.ts --lib DOM,ES2015 --declaration --declarationDir dist/types --emitDeclarationOnly",
"format": "prettier --write {src,tests}/**/*.{js,ts}",
"lint": "eslint {src,tests}/**/*.ts",
"prepublishOnly": "yarn run build",
"serve": "serve -l 9000 docs",
"start": "yarn run build && yarn run serve",
"test": "jest --coverage",
"update-docs": "yarn run cp-cli dist/signature_pad.umd.js docs/js/signature_pad.umd.js",
"prepare": "husky install"
},
"repository": {
"type": "git",
"url": "https://github.com/szimek/signature_pad.git"
},
"files": [
"src",
"dist",
"docs"
],
"devDependencies": {
"@rollup/plugin-typescript": "^9.0.1",
"@semantic-release/changelog": "^6.0.1",
"@semantic-release/commit-analyzer": "^9.0.2",
"@semantic-release/git": "^10.0.1",
"@semantic-release/github": "^8.0.6",
"@semantic-release/npm": "^9.0.1",
"@semantic-release/release-notes-generator": "^10.0.3",
"@types/jest": "^29.1.2",
"@types/node": "^18.8.4",
"@typescript-eslint/eslint-plugin": "^5.40.0",
"@typescript-eslint/parser": "^5.40.0",
"cp-cli": "^2.0.0",
"del": "^7.0.0",
"del-cli": "^5.0.0",
"eslint": "^8.25.0",
"eslint-config-prettier": "^8.5.0",
"husky": "^8.0.1",
"jest": "^29.1.2",
"jest-canvas-mock": "^2.4.0",
"jest-environment-jsdom": "^29.1.2",
"lint-staged": "^13.0.3",
"prettier": "^2.7.1",
"rollup": "^3.0.0",
"rollup-plugin-terser": "^7.0.2",
"semantic-release": "^19.0.5",
"serve": "^14.0.1",
"ts-jest": "^29.0.3",
"tslib": "^2.4.0",
"typescript": "~4.8.4"
},
"lint-staged": {
"*.ts": "prettier --write"
},
"jest": {
"moduleFileExtensions": [
"ts",
"js"
],
"testEnvironment": "jsdom",
"testEnvironmentOptions": {
"resources": "usable",
"url": "http://localhost:3000/"
},
"testMatch": [
"<rootDir>/tests/**/*.test.ts"
],
"transform": {
"^.+\\.tsx?$": "ts-jest"
},
"setupFiles": [
"jest-canvas-mock"
]
},
"release": {
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github",
"@semantic-release/git"
]
},
"packageManager": "yarn@3.2.1"
}

View File

@@ -0,0 +1,109 @@
import { BasicPoint, Point } from './point';
export class Bezier {
public static fromPoints(
points: Point[],
widths: { start: number; end: number },
): Bezier {
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2;
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1;
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end);
}
private static calculateControlPoints(
s1: BasicPoint,
s2: BasicPoint,
s3: BasicPoint,
): {
c1: BasicPoint;
c2: BasicPoint;
} {
const dx1 = s1.x - s2.x;
const dy1 = s1.y - s2.y;
const dx2 = s2.x - s3.x;
const dy2 = s2.y - s3.y;
const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };
const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2);
const dxm = m1.x - m2.x;
const dym = m1.y - m2.y;
const k = l2 / (l1 + l2);
const cm = { x: m2.x + dxm * k, y: m2.y + dym * k };
const tx = s2.x - cm.x;
const ty = s2.y - cm.y;
return {
c1: new Point(m1.x + tx, m1.y + ty),
c2: new Point(m2.x + tx, m2.y + ty),
};
}
constructor(
public startPoint: Point,
public control2: BasicPoint,
public control1: BasicPoint,
public endPoint: Point,
public startWidth: number,
public endWidth: number,
) {}
// Returns approximated length. Code taken from https://www.lemoda.net/maths/bezier-length/index.html.
public length(): number {
const steps = 10;
let length = 0;
let px;
let py;
for (let i = 0; i <= steps; i += 1) {
const t = i / steps;
const cx = this.point(
t,
this.startPoint.x,
this.control1.x,
this.control2.x,
this.endPoint.x,
);
const cy = this.point(
t,
this.startPoint.y,
this.control1.y,
this.control2.y,
this.endPoint.y,
);
if (i > 0) {
const xdiff = cx - (px as number);
const ydiff = cy - (py as number);
length += Math.sqrt(xdiff * xdiff + ydiff * ydiff);
}
px = cx;
py = cy;
}
return length;
}
// Calculate parametric value of x or y given t and the four point coordinates of a cubic bezier curve.
private point(
t: number,
start: number,
c1: number,
c2: number,
end: number,
): number {
// prettier-ignore
return ( start * (1.0 - t) * (1.0 - t) * (1.0 - t))
+ (3.0 * c1 * (1.0 - t) * (1.0 - t) * t)
+ (3.0 * c2 * (1.0 - t) * t * t)
+ ( end * t * t * t);
}
}

View File

@@ -0,0 +1,45 @@
// Interface for point data structure used e.g. in SignaturePad#fromData method
export interface BasicPoint {
x: number;
y: number;
pressure: number;
time: number;
}
export class Point implements BasicPoint {
public x: number;
public y: number;
public pressure: number;
public time: number;
constructor(x: number, y: number, pressure?: number, time?: number) {
if (isNaN(x) || isNaN(y)) {
throw new Error(`Point is invalid: (${x}, ${y})`);
}
this.x = +x;
this.y = +y;
this.pressure = pressure || 0;
this.time = time || Date.now();
}
public distanceTo(start: BasicPoint): number {
return Math.sqrt(
Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2),
);
}
public equals(other: BasicPoint): boolean {
return (
this.x === other.x &&
this.y === other.y &&
this.pressure === other.pressure &&
this.time === other.time
);
}
public velocityFrom(start: BasicPoint): number {
return this.time !== start.time
? this.distanceTo(start) / (this.time - start.time)
: 0;
}
}

View File

@@ -0,0 +1,35 @@
export class SignatureEventTarget {
/* tslint:disable: variable-name */
private _et: EventTarget;
/* tslint:enable: variable-name */
constructor() {
try {
this._et = new EventTarget();
} catch (error) {
// Using document as EventTarget to support iOS 13 and older.
// Because EventTarget constructor just exists at iOS 14 and later.
this._et = document;
}
}
addEventListener(
type: string,
listener: EventListenerOrEventListenerObject | null,
options?: boolean | AddEventListenerOptions,
): void {
this._et.addEventListener(type, listener, options);
}
dispatchEvent(event: Event): boolean {
return this._et.dispatchEvent(event);
}
removeEventListener(
type: string,
callback: EventListenerOrEventListenerObject | null,
options?: boolean | EventListenerOptions,
): void {
this._et.removeEventListener(type, callback, options);
}
}

View File

@@ -0,0 +1,675 @@
/**
* The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from:
* http://corner.squareup.com/2012/07/smoother-signatures.html
*
* Implementation of interpolation using cubic Bézier curves is taken from:
* https://web.archive.org/web/20160323213433/http://www.benknowscode.com/2012/09/path-interpolation-using-cubic-bezier_9742.html
*
* Algorithm for approximated length of a Bézier curve is taken from:
* http://www.lemoda.net/maths/bezier-length/index.html
*/
import { Bezier } from './bezier';
import { BasicPoint, Point } from './point';
import { SignatureEventTarget } from './signature_event_target';
import { throttle } from './throttle';
declare global {
interface CSSStyleDeclaration {
msTouchAction: string | null;
}
}
export type SignatureEvent = MouseEvent | Touch | PointerEvent;
export interface FromDataOptions {
clear?: boolean;
}
export interface ToSVGOptions {
includeBackgroundColor?: boolean;
}
export interface PointGroupOptions {
dotSize: number;
minWidth: number;
maxWidth: number;
penColor: string;
velocityFilterWeight: number;
}
export interface Options extends Partial<PointGroupOptions> {
minDistance?: number;
backgroundColor?: string;
throttle?: number;
}
export interface PointGroup extends PointGroupOptions {
points: BasicPoint[];
}
export default class SignaturePad extends SignatureEventTarget {
// Public stuff
public dotSize: number;
public minWidth: number;
public maxWidth: number;
public penColor: string;
public minDistance: number;
public velocityFilterWeight: number;
public backgroundColor: string;
public throttle: number;
// Private stuff
/* tslint:disable: variable-name */
private _ctx: CanvasRenderingContext2D;
private _drawningStroke = false;
private _isEmpty = true;
private _lastPoints: Point[] = []; // Stores up to 4 most recent points; used to generate a new curve
private _data: PointGroup[] = []; // Stores all points in groups (one group per line or dot)
private _lastVelocity = 0;
private _lastWidth = 0;
private _strokeMoveUpdate: (event: SignatureEvent) => void;
/* tslint:enable: variable-name */
constructor(private canvas: HTMLCanvasElement, options: Options = {}) {
super();
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
this.minWidth = options.minWidth || 0.5;
this.maxWidth = options.maxWidth || 2.5;
this.throttle = ('throttle' in options ? options.throttle : 16) as number; // in milisecondss
this.minDistance = (
'minDistance' in options ? options.minDistance : 5
) as number; // in pixels
this.dotSize = options.dotSize || 0;
this.penColor = options.penColor || 'black';
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
this._strokeMoveUpdate = this.throttle
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
: SignaturePad.prototype._strokeUpdate;
this._ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
this.clear();
// Enable mouse and touch event handlers
this.on();
}
public clear(): void {
const { _ctx: ctx, canvas } = this;
// Clear canvas using background color
ctx.fillStyle = this.backgroundColor;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.fillRect(0, 0, canvas.width, canvas.height);
this._data = [];
this._reset(this._getPointGroupOptions());
this._isEmpty = true;
}
public fromDataURL(
dataUrl: string,
options: {
ratio?: number;
width?: number;
height?: number;
xOffset?: number;
yOffset?: number;
} = {},
): Promise<void> {
return new Promise((resolve, reject) => {
const image = new Image();
const ratio = options.ratio || window.devicePixelRatio || 1;
const width = options.width || this.canvas.width / ratio;
const height = options.height || this.canvas.height / ratio;
const xOffset = options.xOffset || 0;
const yOffset = options.yOffset || 0;
this._reset(this._getPointGroupOptions());
image.onload = (): void => {
this._ctx.drawImage(image, xOffset, yOffset, width, height);
resolve();
};
image.onerror = (error): void => {
reject(error);
};
image.crossOrigin = 'anonymous';
image.src = dataUrl;
this._isEmpty = false;
});
}
public toDataURL(
type: 'image/svg+xml',
encoderOptions?: ToSVGOptions,
): string;
public toDataURL(type?: string, encoderOptions?: number): string;
public toDataURL(
type = 'image/png',
encoderOptions?: number | ToSVGOptions | undefined,
): string {
switch (type) {
case 'image/svg+xml':
if (typeof encoderOptions !== 'object') {
encoderOptions = undefined;
}
return `data:image/svg+xml;base64,${btoa(
this.toSVG(encoderOptions as ToSVGOptions),
)}`;
default:
if (typeof encoderOptions !== 'number') {
encoderOptions = undefined;
}
return this.canvas.toDataURL(type, encoderOptions);
}
}
public on(): void {
// Disable panning/zooming when touching canvas element
this.canvas.style.touchAction = 'none';
this.canvas.style.msTouchAction = 'none';
this.canvas.style.userSelect = 'none';
const isIOS =
/Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document;
// The "Scribble" feature of iOS intercepts point events. So that we can lose some of them when tapping rapidly.
// Use touch events for iOS platforms to prevent it. See https://developer.apple.com/forums/thread/664108 for more information.
if (window.PointerEvent && !isIOS) {
this._handlePointerEvents();
} else {
this._handleMouseEvents();
if ('ontouchstart' in window) {
this._handleTouchEvents();
}
}
}
public off(): void {
// Enable panning/zooming when touching canvas element
this.canvas.style.touchAction = 'auto';
this.canvas.style.msTouchAction = 'auto';
this.canvas.style.userSelect = 'auto';
this.canvas.removeEventListener('pointerdown', this._handlePointerStart);
this.canvas.removeEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.removeEventListener(
'pointerup',
this._handlePointerEnd,
);
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
this.canvas.removeEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.removeEventListener(
'mouseup',
this._handleMouseUp,
);
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
this.canvas.removeEventListener('touchmove', this._handleTouchMove);
this.canvas.removeEventListener('touchend', this._handleTouchEnd);
}
public isEmpty(): boolean {
return this._isEmpty;
}
public fromData(
pointGroups: PointGroup[],
{ clear = true }: FromDataOptions = {},
): void {
if (clear) {
this.clear();
}
this._fromData(
pointGroups,
this._drawCurve.bind(this),
this._drawDot.bind(this),
);
this._data = this._data.concat(pointGroups);
}
public toData(): PointGroup[] {
return this._data;
}
// Event handlers
private _handleMouseDown = (event: MouseEvent): void => {
if (event.buttons === 1) {
this._drawningStroke = true;
this._strokeBegin(event);
}
};
private _handleMouseMove = (event: MouseEvent): void => {
if (this._drawningStroke) {
this._strokeMoveUpdate(event);
}
};
private _handleMouseUp = (event: MouseEvent): void => {
if (event.buttons === 1 && this._drawningStroke) {
this._drawningStroke = false;
this._strokeEnd(event);
}
};
private _handleTouchStart = (event: TouchEvent): void => {
// Prevent scrolling.
if (event.cancelable) {
event.preventDefault();
}
if (event.targetTouches.length === 1) {
const touch = event.changedTouches[0];
this._strokeBegin(touch);
}
};
private _handleTouchMove = (event: TouchEvent): void => {
// Prevent scrolling.
if (event.cancelable) {
event.preventDefault();
}
const touch = event.targetTouches[0];
this._strokeMoveUpdate(touch);
};
private _handleTouchEnd = (event: TouchEvent): void => {
const wasCanvasTouched = event.target === this.canvas;
if (wasCanvasTouched) {
if (event.cancelable) {
event.preventDefault();
}
const touch = event.changedTouches[0];
this._strokeEnd(touch);
}
};
private _handlePointerStart = (event: PointerEvent): void => {
this._drawningStroke = true;
event.preventDefault();
this._strokeBegin(event);
};
private _handlePointerMove = (event: PointerEvent): void => {
if (this._drawningStroke) {
event.preventDefault();
this._strokeMoveUpdate(event);
}
};
private _handlePointerEnd = (event: PointerEvent): void => {
if (this._drawningStroke) {
event.preventDefault();
this._drawningStroke = false;
this._strokeEnd(event);
}
};
private _getPointGroupOptions(group?: PointGroup) {
return {
penColor: group && 'penColor' in group ? group.penColor : this.penColor,
dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize,
minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth,
maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth,
velocityFilterWeight:
group && 'velocityFilterWeight' in group
? group.velocityFilterWeight
: this.velocityFilterWeight,
};
}
// Private methods
private _strokeBegin(event: SignatureEvent): void {
this.dispatchEvent(new CustomEvent('beginStroke', { detail: event }));
const pointGroupOptions = this._getPointGroupOptions();
const newPointGroup: PointGroup = {
...pointGroupOptions,
points: [],
};
this._data.push(newPointGroup);
this._reset(pointGroupOptions);
this._strokeUpdate(event);
}
private _strokeUpdate(event: SignatureEvent): void {
if (this._data.length === 0) {
// This can happen if clear() was called while a signature is still in progress,
// or if there is a race condition between start/update events.
this._strokeBegin(event);
return;
}
this.dispatchEvent(
new CustomEvent('beforeUpdateStroke', { detail: event }),
);
const x = event.clientX;
const y = event.clientY;
const pressure =
(event as PointerEvent).pressure !== undefined
? (event as PointerEvent).pressure
: (event as Touch).force !== undefined
? (event as Touch).force
: 0;
const point = this._createPoint(x, y, pressure);
const lastPointGroup = this._data[this._data.length - 1];
const lastPoints = lastPointGroup.points;
const lastPoint =
lastPoints.length > 0 && lastPoints[lastPoints.length - 1];
const isLastPointTooClose = lastPoint
? point.distanceTo(lastPoint) <= this.minDistance
: false;
const pointGroupOptions = this._getPointGroupOptions(lastPointGroup);
// Skip this point if it's too close to the previous one
if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
const curve = this._addPoint(point, pointGroupOptions);
if (!lastPoint) {
this._drawDot(point, pointGroupOptions);
} else if (curve) {
this._drawCurve(curve, pointGroupOptions);
}
lastPoints.push({
time: point.time,
x: point.x,
y: point.y,
pressure: point.pressure,
});
}
this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event }));
}
private _strokeEnd(event: SignatureEvent): void {
this._strokeUpdate(event);
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
}
private _handlePointerEvents(): void {
this._drawningStroke = false;
this.canvas.addEventListener('pointerdown', this._handlePointerStart);
this.canvas.addEventListener('pointermove', this._handlePointerMove);
this.canvas.ownerDocument.addEventListener(
'pointerup',
this._handlePointerEnd,
);
}
private _handleMouseEvents(): void {
this._drawningStroke = false;
this.canvas.addEventListener('mousedown', this._handleMouseDown);
this.canvas.addEventListener('mousemove', this._handleMouseMove);
this.canvas.ownerDocument.addEventListener('mouseup', this._handleMouseUp);
}
private _handleTouchEvents(): void {
this.canvas.addEventListener('touchstart', this._handleTouchStart);
this.canvas.addEventListener('touchmove', this._handleTouchMove);
this.canvas.addEventListener('touchend', this._handleTouchEnd);
}
// Called when a new line is started
private _reset(options: PointGroupOptions): void {
this._lastPoints = [];
this._lastVelocity = 0;
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
this._ctx.fillStyle = options.penColor;
}
private _createPoint(x: number, y: number, pressure: number): Point {
const rect = this.canvas.getBoundingClientRect();
return new Point(
x - rect.left,
y - rect.top,
pressure,
new Date().getTime(),
);
}
// Add point to _lastPoints array and generate a new curve if there are enough points (i.e. 3)
private _addPoint(point: Point, options: PointGroupOptions): Bezier | null {
const { _lastPoints } = this;
_lastPoints.push(point);
if (_lastPoints.length > 2) {
// To reduce the initial lag make it work with 3 points
// by copying the first point to the beginning.
if (_lastPoints.length === 3) {
_lastPoints.unshift(_lastPoints[0]);
}
// _points array will always have 4 points here.
const widths = this._calculateCurveWidths(
_lastPoints[1],
_lastPoints[2],
options,
);
const curve = Bezier.fromPoints(_lastPoints, widths);
// Remove the first element from the list, so that there are no more than 4 points at any time.
_lastPoints.shift();
return curve;
}
return null;
}
private _calculateCurveWidths(
startPoint: Point,
endPoint: Point,
options: PointGroupOptions,
): { start: number; end: number } {
const velocity =
options.velocityFilterWeight * endPoint.velocityFrom(startPoint) +
(1 - options.velocityFilterWeight) * this._lastVelocity;
const newWidth = this._strokeWidth(velocity, options);
const widths = {
end: newWidth,
start: this._lastWidth,
};
this._lastVelocity = velocity;
this._lastWidth = newWidth;
return widths;
}
private _strokeWidth(velocity: number, options: PointGroupOptions): number {
return Math.max(options.maxWidth / (velocity + 1), options.minWidth);
}
private _drawCurveSegment(x: number, y: number, width: number): void {
const ctx = this._ctx;
ctx.moveTo(x, y);
ctx.arc(x, y, width, 0, 2 * Math.PI, false);
this._isEmpty = false;
}
private _drawCurve(curve: Bezier, options: PointGroupOptions): void {
const ctx = this._ctx;
const widthDelta = curve.endWidth - curve.startWidth;
// '2' is just an arbitrary number here. If only length is used, then
// there are gaps between curve segments :/
const drawSteps = Math.ceil(curve.length()) * 2;
ctx.beginPath();
ctx.fillStyle = options.penColor;
for (let i = 0; i < drawSteps; i += 1) {
// Calculate the Bezier (x, y) coordinate for this step.
const t = i / drawSteps;
const tt = t * t;
const ttt = tt * t;
const u = 1 - t;
const uu = u * u;
const uuu = uu * u;
let x = uuu * curve.startPoint.x;
x += 3 * uu * t * curve.control1.x;
x += 3 * u * tt * curve.control2.x;
x += ttt * curve.endPoint.x;
let y = uuu * curve.startPoint.y;
y += 3 * uu * t * curve.control1.y;
y += 3 * u * tt * curve.control2.y;
y += ttt * curve.endPoint.y;
const width = Math.min(
curve.startWidth + ttt * widthDelta,
options.maxWidth,
);
this._drawCurveSegment(x, y, width);
}
ctx.closePath();
ctx.fill();
}
private _drawDot(point: BasicPoint, options: PointGroupOptions): void {
const ctx = this._ctx;
const width =
options.dotSize > 0
? options.dotSize
: (options.minWidth + options.maxWidth) / 2;
ctx.beginPath();
this._drawCurveSegment(point.x, point.y, width);
ctx.closePath();
ctx.fillStyle = options.penColor;
ctx.fill();
}
private _fromData(
pointGroups: PointGroup[],
drawCurve: SignaturePad['_drawCurve'],
drawDot: SignaturePad['_drawDot'],
): void {
for (const group of pointGroups) {
const { points } = group;
const pointGroupOptions = this._getPointGroupOptions(group);
if (points.length > 1) {
for (let j = 0; j < points.length; j += 1) {
const basicPoint = points[j];
const point = new Point(
basicPoint.x,
basicPoint.y,
basicPoint.pressure,
basicPoint.time,
);
if (j === 0) {
this._reset(pointGroupOptions);
}
const curve = this._addPoint(point, pointGroupOptions);
if (curve) {
drawCurve(curve, pointGroupOptions);
}
}
} else {
this._reset(pointGroupOptions);
drawDot(points[0], pointGroupOptions);
}
}
}
public toSVG({ includeBackgroundColor = false }: ToSVGOptions = {}): string {
const pointGroups = this._data;
const ratio = Math.max(window.devicePixelRatio || 1, 1);
const minX = 0;
const minY = 0;
const maxX = this.canvas.width / ratio;
const maxY = this.canvas.height / ratio;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
svg.setAttribute('viewBox', `${minX} ${minY} ${maxX} ${maxY}`);
svg.setAttribute('width', maxX.toString());
svg.setAttribute('height', maxY.toString());
if (includeBackgroundColor && this.backgroundColor) {
const rect = document.createElement('rect');
rect.setAttribute('width', '100%');
rect.setAttribute('height', '100%');
rect.setAttribute('fill', this.backgroundColor);
svg.appendChild(rect);
}
this._fromData(
pointGroups,
(curve, { penColor }) => {
const path = document.createElement('path');
// Need to check curve for NaN values, these pop up when drawing
// lines on the canvas that are not continuous. E.g. Sharp corners
// or stopping mid-stroke and than continuing without lifting mouse.
/* eslint-disable no-restricted-globals */
if (
!isNaN(curve.control1.x) &&
!isNaN(curve.control1.y) &&
!isNaN(curve.control2.x) &&
!isNaN(curve.control2.y)
) {
const attr =
`M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(
3,
)} ` +
`C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} ` +
`${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} ` +
`${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
path.setAttribute('d', attr);
path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
path.setAttribute('stroke', penColor);
path.setAttribute('fill', 'none');
path.setAttribute('stroke-linecap', 'round');
svg.appendChild(path);
}
/* eslint-enable no-restricted-globals */
},
(point, { penColor, dotSize, minWidth, maxWidth }) => {
const circle = document.createElement('circle');
const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2;
circle.setAttribute('r', size.toString());
circle.setAttribute('cx', point.x.toString());
circle.setAttribute('cy', point.y.toString());
circle.setAttribute('fill', penColor);
svg.appendChild(circle);
},
);
return svg.outerHTML;
}
}

View File

@@ -0,0 +1,51 @@
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-this-alias */
// Slightly simplified version of http://stackoverflow.com/a/27078401/815507
export function throttle(
fn: (...args: any[]) => any,
wait = 250,
): (this: any, ...args: any[]) => any {
let previous = 0;
let timeout: number | null = null;
let result: any;
let storedContext: any;
let storedArgs: any[];
const later = (): void => {
previous = Date.now();
timeout = null;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
};
return function wrapper(this: any, ...args: any[]): any {
const now = Date.now();
const remaining = wait - (now - previous);
storedContext = this;
storedArgs = args;
if (remaining <= 0 || remaining > wait) {
if (timeout) {
clearTimeout(timeout);
timeout = null;
}
previous = now;
result = fn.apply(storedContext, storedArgs);
if (!timeout) {
storedContext = null;
storedArgs = [];
}
} else if (!timeout) {
timeout = window.setTimeout(later, remaining);
}
return result;
};
}

View File

@@ -17,7 +17,8 @@
"jquery-ui": "^1.13.2",
"jquery-validation": "^1.19.5",
"knockout": "^3.3.0",
"popper": "^1.0.1"
"popper": "^1.0.1",
"signature_pad": "^4.1.5"
},
"devDependencies": {
"@types/node": "^18.15.1"
@@ -5967,6 +5968,11 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/signature_pad": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/signature_pad/-/signature_pad-4.1.5.tgz",
"integrity": "sha512-VOE846UbQMeLBbcR08KwjwE1wNLgp3gqC7yr/AELkgSMs/BdRpxIZna6K5XyZJpA7IWq9GiInw1C8PLm57VO6Q=="
},
"node_modules/simple-concat": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",

View File

@@ -17,7 +17,8 @@
"jquery-ui": "^1.13.2",
"jquery-validation": "^1.19.5",
"knockout": "^3.3.0",
"popper": "^1.0.1"
"popper": "^1.0.1",
"signature_pad": "^4.1.5"
},
"description": "",
"devDependencies": {