avances en plantillas
This commit is contained in:
parent
0f84beacf1
commit
da0530d79b
2062 changed files with 598814 additions and 22 deletions
21
storage/public/dist/libs/signature_pad/LICENSE
vendored
Normal file
21
storage/public/dist/libs/signature_pad/LICENSE
vendored
Normal 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.
|
||||
240
storage/public/dist/libs/signature_pad/README.md
vendored
Normal file
240
storage/public/dist/libs/signature_pad/README.md
vendored
Normal file
|
|
@ -0,0 +1,240 @@
|
|||
# Signature Pad [](https://www.npmjs.com/package/signature_pad) [](https://github.com/szimek/signature_pad/actions/workflows/test.yml) [](https://codeclimate.com/github/szimek/signature_pad) [](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.
|
||||
|
||||

|
||||
|
||||
## 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/master/docs/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/UziTech/xa91e4Lp/>
|
||||
- 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:
|
||||
|
||||
> [!NOTE]
|
||||
> Replace `[version]` with the version you want to use.
|
||||
|
||||
```html
|
||||
<script src="https://cdn.jsdelivr.net/npm/signature_pad@[version]/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
|
||||
signaturePad.toSVG({includeDataUrl: true}); // add data url added with fromDataUrl to svg output background
|
||||
|
||||
// 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 });
|
||||
|
||||
// Redraw the canvas
|
||||
signaturePad.redraw();
|
||||
|
||||
// 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>
|
||||
<dt>canvasContextOptions</dt>
|
||||
<dd>(CanvasRenderingContext2DSettings) part of the Canvas API, provides the 2D rendering context for the drawing surface of a <code>canvas</code> element. It is used for drawing shapes, text, images, and other objects (<a href="https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/getContextAttributes">MDN</a>).</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.<br>Can be canceled with <code>event.preventDefault()</code></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.redraw()` 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](https://web.archive.org/web/20160414182912/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).
|
||||
787
storage/public/dist/libs/signature_pad/dist/signature_pad.js
vendored
Normal file
787
storage/public/dist/libs/signature_pad/dist/signature_pad.js
vendored
Normal file
|
|
@ -0,0 +1,787 @@
|
|||
/*!
|
||||
* Signature Pad v5.1.3 | https://github.com/szimek/signature_pad
|
||||
* (c) 2025 Szymon Nowak | Released under the MIT license
|
||||
*/
|
||||
|
||||
|
||||
// src/point.ts
|
||||
var Point = class {
|
||||
x;
|
||||
y;
|
||||
pressure;
|
||||
time;
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
||||
// src/bezier.ts
|
||||
var Bezier = 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, y: (s1.y + s2.y) / 2 };
|
||||
const m2 = { x: (s2.x + s3.x) / 2, y: (s2.y + s3.y) / 2 };
|
||||
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 = l1 + l2 == 0 ? 0 : 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)
|
||||
};
|
||||
}
|
||||
// Returns approximated length. Code taken from https://www.lemoda.net/maths/bezier-length/index.html.
|
||||
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;
|
||||
}
|
||||
// Calculate parametric value of x or y given t and the four point coordinates of a cubic bezier curve.
|
||||
point(t, start, c1, c2, end) {
|
||||
return start * (1 - t) * (1 - t) * (1 - t) + 3 * c1 * (1 - t) * (1 - t) * t + 3 * c2 * (1 - t) * t * t + end * t * t * t;
|
||||
}
|
||||
};
|
||||
|
||||
// src/signature_event_target.ts
|
||||
var SignatureEventTarget = class {
|
||||
/* tslint:disable: variable-name */
|
||||
_et;
|
||||
/* tslint:enable: variable-name */
|
||||
constructor() {
|
||||
try {
|
||||
this._et = new EventTarget();
|
||||
} catch {
|
||||
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);
|
||||
}
|
||||
};
|
||||
|
||||
// src/throttle.ts
|
||||
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;
|
||||
};
|
||||
}
|
||||
|
||||
// src/signature_pad.ts
|
||||
var SignaturePad = class _SignaturePad extends SignatureEventTarget {
|
||||
/* tslint:enable: variable-name */
|
||||
constructor(canvas, options = {}) {
|
||||
super();
|
||||
this.canvas = canvas;
|
||||
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
|
||||
this.minWidth = options.minWidth || 0.5;
|
||||
this.maxWidth = options.maxWidth || 2.5;
|
||||
this.throttle = options.throttle ?? 16;
|
||||
this.minDistance = options.minDistance ?? 5;
|
||||
this.dotSize = options.dotSize || 0;
|
||||
this.penColor = options.penColor || "black";
|
||||
this.backgroundColor = options.backgroundColor || "rgba(0,0,0,0)";
|
||||
this.compositeOperation = options.compositeOperation || "source-over";
|
||||
this.canvasContextOptions = options.canvasContextOptions ?? {};
|
||||
this._strokeMoveUpdate = this.throttle ? throttle(_SignaturePad.prototype._strokeUpdate, this.throttle) : _SignaturePad.prototype._strokeUpdate;
|
||||
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||||
this._handleMouseMove = this._handleMouseMove.bind(this);
|
||||
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||||
this._handleTouchStart = this._handleTouchStart.bind(this);
|
||||
this._handleTouchMove = this._handleTouchMove.bind(this);
|
||||
this._handleTouchEnd = this._handleTouchEnd.bind(this);
|
||||
this._handlePointerDown = this._handlePointerDown.bind(this);
|
||||
this._handlePointerMove = this._handlePointerMove.bind(this);
|
||||
this._handlePointerUp = this._handlePointerUp.bind(this);
|
||||
this._handlePointerCancel = this._handlePointerCancel.bind(this);
|
||||
this._handleTouchCancel = this._handleTouchCancel.bind(this);
|
||||
this._ctx = canvas.getContext(
|
||||
"2d",
|
||||
this.canvasContextOptions
|
||||
);
|
||||
this.clear();
|
||||
this.on();
|
||||
}
|
||||
// Public stuff
|
||||
dotSize;
|
||||
minWidth;
|
||||
maxWidth;
|
||||
penColor;
|
||||
minDistance;
|
||||
velocityFilterWeight;
|
||||
compositeOperation;
|
||||
backgroundColor;
|
||||
throttle;
|
||||
canvasContextOptions;
|
||||
// Private stuff
|
||||
/* tslint:disable: variable-name */
|
||||
_ctx;
|
||||
_drawingStroke = false;
|
||||
_isEmpty = true;
|
||||
_dataUrl;
|
||||
_dataUrlOptions;
|
||||
_lastPoints = [];
|
||||
// Stores up to 4 most recent points; used to generate a new curve
|
||||
_data = [];
|
||||
// Stores all points in groups (one group per line or dot)
|
||||
_lastVelocity = 0;
|
||||
_lastWidth = 0;
|
||||
_strokeMoveUpdate;
|
||||
_strokePointerId;
|
||||
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;
|
||||
this._dataUrl = void 0;
|
||||
this._dataUrlOptions = void 0;
|
||||
this._strokePointerId = void 0;
|
||||
}
|
||||
redraw() {
|
||||
const data = this._data;
|
||||
const dataUrl = this._dataUrl;
|
||||
const dataUrlOptions = this._dataUrlOptions;
|
||||
this.clear();
|
||||
if (dataUrl) {
|
||||
this.fromDataURL(dataUrl, dataUrlOptions);
|
||||
}
|
||||
this.fromData(data, { clear: false });
|
||||
}
|
||||
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;
|
||||
this._dataUrl = dataUrl;
|
||||
this._dataUrlOptions = { ...options };
|
||||
});
|
||||
}
|
||||
toDataURL(type = "image/png", encoderOptions) {
|
||||
switch (type) {
|
||||
case "image/svg+xml":
|
||||
if (typeof encoderOptions !== "object") {
|
||||
encoderOptions = void 0;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${btoa(
|
||||
this.toSVG(encoderOptions)
|
||||
)}`;
|
||||
default:
|
||||
if (typeof encoderOptions !== "number") {
|
||||
encoderOptions = void 0;
|
||||
}
|
||||
return this.canvas.toDataURL(type, encoderOptions);
|
||||
}
|
||||
}
|
||||
on() {
|
||||
this.canvas.style.touchAction = "none";
|
||||
this.canvas.style.msTouchAction = "none";
|
||||
this.canvas.style.userSelect = "none";
|
||||
this.canvas.style.webkitUserSelect = "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.style.webkitUserSelect = "auto";
|
||||
this.canvas.removeEventListener("pointerdown", this._handlePointerDown);
|
||||
this.canvas.removeEventListener("mousedown", this._handleMouseDown);
|
||||
this.canvas.removeEventListener("touchstart", this._handleTouchStart);
|
||||
this._removeMoveUpEventListeners();
|
||||
}
|
||||
_getListenerFunctions() {
|
||||
const canvasWindow = window.document === this.canvas.ownerDocument ? window : this.canvas.ownerDocument.defaultView ?? this.canvas.ownerDocument;
|
||||
return {
|
||||
addEventListener: canvasWindow.addEventListener.bind(
|
||||
canvasWindow
|
||||
),
|
||||
removeEventListener: canvasWindow.removeEventListener.bind(
|
||||
canvasWindow
|
||||
)
|
||||
};
|
||||
}
|
||||
_removeMoveUpEventListeners() {
|
||||
const { removeEventListener } = this._getListenerFunctions();
|
||||
removeEventListener("pointermove", this._handlePointerMove);
|
||||
removeEventListener("pointerup", this._handlePointerUp);
|
||||
removeEventListener("pointercancel", this._handlePointerCancel);
|
||||
removeEventListener("mousemove", this._handleMouseMove);
|
||||
removeEventListener("mouseup", this._handleMouseUp);
|
||||
removeEventListener("touchmove", this._handleTouchMove);
|
||||
removeEventListener("touchend", this._handleTouchEnd);
|
||||
removeEventListener("touchcancel", this._handleTouchCancel);
|
||||
}
|
||||
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;
|
||||
}
|
||||
_isLeftButtonPressed(event, only) {
|
||||
if (only) {
|
||||
return event.buttons === 1;
|
||||
}
|
||||
return (event.buttons & 1) === 1;
|
||||
}
|
||||
_pointerEventToSignatureEvent(event) {
|
||||
return {
|
||||
event,
|
||||
type: event.type,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
pressure: "pressure" in event ? event.pressure : 0
|
||||
};
|
||||
}
|
||||
_touchEventToSignatureEvent(event) {
|
||||
const touch = event.changedTouches[0];
|
||||
return {
|
||||
event,
|
||||
type: event.type,
|
||||
x: touch.clientX,
|
||||
y: touch.clientY,
|
||||
pressure: touch.force
|
||||
};
|
||||
}
|
||||
// Event handlers
|
||||
_handleMouseDown(event) {
|
||||
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleMouseMove(event) {
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleMouseUp(event) {
|
||||
if (this._isLeftButtonPressed(event)) {
|
||||
return;
|
||||
}
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchStart(event) {
|
||||
if (event.targetTouches.length !== 1 || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeBegin(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchMove(event) {
|
||||
if (event.targetTouches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (!this._drawingStroke) {
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchEnd(event) {
|
||||
if (event.targetTouches.length !== 0) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerCancel(event) {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
}
|
||||
_handleTouchCancel(event) {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
}
|
||||
_getPointerId(event) {
|
||||
return event.persistentDeviceId || event.pointerId;
|
||||
}
|
||||
_allowPointerId(event, allowUndefined = false) {
|
||||
if (typeof this._strokePointerId === "undefined") {
|
||||
return allowUndefined;
|
||||
}
|
||||
return this._getPointerId(event) === this._strokePointerId;
|
||||
}
|
||||
_handlePointerDown(event) {
|
||||
if (this._drawingStroke || !this._isLeftButtonPressed(event) || !this._allowPointerId(event, true)) {
|
||||
return;
|
||||
}
|
||||
this._strokePointerId = this._getPointerId(event);
|
||||
event.preventDefault();
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerMove(event) {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerUp(event) {
|
||||
if (this._isLeftButtonPressed(event) || !this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_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,
|
||||
compositeOperation: group && "compositeOperation" in group ? group.compositeOperation : this.compositeOperation
|
||||
};
|
||||
}
|
||||
// Private methods
|
||||
_strokeBegin(event) {
|
||||
const cancelled = !this.dispatchEvent(
|
||||
new CustomEvent("beginStroke", { detail: event, cancelable: true })
|
||||
);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const { addEventListener } = this._getListenerFunctions();
|
||||
switch (event.event.type) {
|
||||
case "mousedown":
|
||||
addEventListener("mousemove", this._handleMouseMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("mouseup", this._handleMouseUp, { passive: false });
|
||||
break;
|
||||
case "touchstart":
|
||||
addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("touchend", this._handleTouchEnd, { passive: false });
|
||||
addEventListener("touchcancel", this._handleTouchCancel, { passive: false });
|
||||
break;
|
||||
case "pointerdown":
|
||||
addEventListener("pointermove", this._handlePointerMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("pointerup", this._handlePointerUp, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("pointercancel", this._handlePointerCancel, {
|
||||
passive: false
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
this._drawingStroke = true;
|
||||
const pointGroupOptions = this._getPointGroupOptions();
|
||||
const newPointGroup = {
|
||||
...pointGroupOptions,
|
||||
points: []
|
||||
};
|
||||
this._data.push(newPointGroup);
|
||||
this._reset(pointGroupOptions);
|
||||
this._strokeUpdate(event);
|
||||
}
|
||||
_strokeUpdate(event) {
|
||||
if (!this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (this._data.length === 0) {
|
||||
this._strokeBegin(event);
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("beforeUpdateStroke", { detail: event })
|
||||
);
|
||||
const point = this._createPoint(event.x, event.y, event.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, shouldUpdate = true) {
|
||||
this._removeMoveUpEventListeners();
|
||||
if (!this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
this._strokeUpdate(event);
|
||||
}
|
||||
this._drawingStroke = false;
|
||||
this._strokePointerId = void 0;
|
||||
this.dispatchEvent(new CustomEvent("endStroke", { detail: event }));
|
||||
}
|
||||
_handlePointerEvents() {
|
||||
this._drawingStroke = false;
|
||||
this.canvas.addEventListener("pointerdown", this._handlePointerDown, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
_handleMouseEvents() {
|
||||
this._drawingStroke = false;
|
||||
this.canvas.addEventListener("mousedown", this._handleMouseDown, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
_handleTouchEvents() {
|
||||
this.canvas.addEventListener("touchstart", this._handleTouchStart, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
// Called when a new line is started
|
||||
_reset(options) {
|
||||
this._lastPoints = [];
|
||||
this._lastVelocity = 0;
|
||||
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
|
||||
this._ctx.fillStyle = options.penColor;
|
||||
this._ctx.globalCompositeOperation = options.compositeOperation;
|
||||
}
|
||||
_createPoint(x, y, pressure) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return new Point(
|
||||
x - rect.left,
|
||||
y - rect.top,
|
||||
pressure,
|
||||
(/* @__PURE__ */ new Date()).getTime()
|
||||
);
|
||||
}
|
||||
// Add point to _lastPoints array and generate a new curve if there are enough points (i.e. 3)
|
||||
_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, includeDataUrl = 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);
|
||||
}
|
||||
if (includeDataUrl && this._dataUrl) {
|
||||
const ratio2 = this._dataUrlOptions?.ratio || window.devicePixelRatio || 1;
|
||||
const width = this._dataUrlOptions?.width || this.canvas.width / ratio2;
|
||||
const height = this._dataUrlOptions?.height || this.canvas.height / ratio2;
|
||||
const xOffset = this._dataUrlOptions?.xOffset || 0;
|
||||
const yOffset = this._dataUrlOptions?.yOffset || 0;
|
||||
const image = document.createElement("image");
|
||||
image.setAttribute("x", xOffset.toString());
|
||||
image.setAttribute("y", yOffset.toString());
|
||||
image.setAttribute("width", width.toString());
|
||||
image.setAttribute("height", height.toString());
|
||||
image.setAttribute("preserveAspectRatio", "none");
|
||||
image.setAttribute("href", this._dataUrl);
|
||||
svg.appendChild(image);
|
||||
}
|
||||
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
|
||||
7
storage/public/dist/libs/signature_pad/dist/signature_pad.js.map
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/signature_pad.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
7
storage/public/dist/libs/signature_pad/dist/signature_pad.min.js
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/signature_pad.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
storage/public/dist/libs/signature_pad/dist/signature_pad.min.js.map
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/signature_pad.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
848
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.js
vendored
Normal file
848
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.js
vendored
Normal file
|
|
@ -0,0 +1,848 @@
|
|||
/*!
|
||||
* Signature Pad v5.1.3 | https://github.com/szimek/signature_pad
|
||||
* (c) 2025 Szymon Nowak | Released under the MIT license
|
||||
*/
|
||||
(function(g,f){if(typeof exports=="object"&&typeof module<"u"){module.exports=f()}else if("function"==typeof define && define.amd){define("SignaturePad",f)}else {g["SignaturePad"]=f()}}(typeof globalThis < "u" ? globalThis : typeof self < "u" ? self : this,function(){var exports={};var __exports=exports;var module={exports};
|
||||
var __defProp = Object.defineProperty;
|
||||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
||||
var __getOwnPropNames = Object.getOwnPropertyNames;
|
||||
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
||||
var __esm = (fn, res) => function __init() {
|
||||
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
||||
};
|
||||
var __export = (target, all) => {
|
||||
for (var name in all)
|
||||
__defProp(target, name, { get: all[name], enumerable: true });
|
||||
};
|
||||
var __copyProps = (to, from, except, desc) => {
|
||||
if (from && typeof from === "object" || typeof from === "function") {
|
||||
for (let key of __getOwnPropNames(from))
|
||||
if (!__hasOwnProp.call(to, key) && key !== except)
|
||||
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
||||
}
|
||||
return to;
|
||||
};
|
||||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
||||
|
||||
// src/point.ts
|
||||
var Point;
|
||||
var init_point = __esm({
|
||||
"src/point.ts"() {
|
||||
"use strict";
|
||||
Point = class {
|
||||
x;
|
||||
y;
|
||||
pressure;
|
||||
time;
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// src/bezier.ts
|
||||
var Bezier;
|
||||
var init_bezier = __esm({
|
||||
"src/bezier.ts"() {
|
||||
"use strict";
|
||||
init_point();
|
||||
Bezier = 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, y: (s1.y + s2.y) / 2 };
|
||||
const m2 = { x: (s2.x + s3.x) / 2, y: (s2.y + s3.y) / 2 };
|
||||
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 = l1 + l2 == 0 ? 0 : 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)
|
||||
};
|
||||
}
|
||||
// Returns approximated length. Code taken from https://www.lemoda.net/maths/bezier-length/index.html.
|
||||
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;
|
||||
}
|
||||
// Calculate parametric value of x or y given t and the four point coordinates of a cubic bezier curve.
|
||||
point(t, start, c1, c2, end) {
|
||||
return start * (1 - t) * (1 - t) * (1 - t) + 3 * c1 * (1 - t) * (1 - t) * t + 3 * c2 * (1 - t) * t * t + end * t * t * t;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// src/signature_event_target.ts
|
||||
var SignatureEventTarget;
|
||||
var init_signature_event_target = __esm({
|
||||
"src/signature_event_target.ts"() {
|
||||
"use strict";
|
||||
SignatureEventTarget = class {
|
||||
/* tslint:disable: variable-name */
|
||||
_et;
|
||||
/* tslint:enable: variable-name */
|
||||
constructor() {
|
||||
try {
|
||||
this._et = new EventTarget();
|
||||
} catch {
|
||||
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);
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// src/throttle.ts
|
||||
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;
|
||||
};
|
||||
}
|
||||
var init_throttle = __esm({
|
||||
"src/throttle.ts"() {
|
||||
"use strict";
|
||||
}
|
||||
});
|
||||
|
||||
// src/signature_pad.ts
|
||||
var signature_pad_exports = {};
|
||||
__export(signature_pad_exports, {
|
||||
default: () => SignaturePad
|
||||
});
|
||||
var SignaturePad;
|
||||
var init_signature_pad = __esm({
|
||||
"src/signature_pad.ts"() {
|
||||
"use strict";
|
||||
init_bezier();
|
||||
init_point();
|
||||
init_signature_event_target();
|
||||
init_throttle();
|
||||
init_point();
|
||||
SignaturePad = class _SignaturePad extends SignatureEventTarget {
|
||||
/* tslint:enable: variable-name */
|
||||
constructor(canvas, options = {}) {
|
||||
super();
|
||||
this.canvas = canvas;
|
||||
this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
|
||||
this.minWidth = options.minWidth || 0.5;
|
||||
this.maxWidth = options.maxWidth || 2.5;
|
||||
this.throttle = options.throttle ?? 16;
|
||||
this.minDistance = options.minDistance ?? 5;
|
||||
this.dotSize = options.dotSize || 0;
|
||||
this.penColor = options.penColor || "black";
|
||||
this.backgroundColor = options.backgroundColor || "rgba(0,0,0,0)";
|
||||
this.compositeOperation = options.compositeOperation || "source-over";
|
||||
this.canvasContextOptions = options.canvasContextOptions ?? {};
|
||||
this._strokeMoveUpdate = this.throttle ? throttle(_SignaturePad.prototype._strokeUpdate, this.throttle) : _SignaturePad.prototype._strokeUpdate;
|
||||
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||||
this._handleMouseMove = this._handleMouseMove.bind(this);
|
||||
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||||
this._handleTouchStart = this._handleTouchStart.bind(this);
|
||||
this._handleTouchMove = this._handleTouchMove.bind(this);
|
||||
this._handleTouchEnd = this._handleTouchEnd.bind(this);
|
||||
this._handlePointerDown = this._handlePointerDown.bind(this);
|
||||
this._handlePointerMove = this._handlePointerMove.bind(this);
|
||||
this._handlePointerUp = this._handlePointerUp.bind(this);
|
||||
this._handlePointerCancel = this._handlePointerCancel.bind(this);
|
||||
this._handleTouchCancel = this._handleTouchCancel.bind(this);
|
||||
this._ctx = canvas.getContext(
|
||||
"2d",
|
||||
this.canvasContextOptions
|
||||
);
|
||||
this.clear();
|
||||
this.on();
|
||||
}
|
||||
// Public stuff
|
||||
dotSize;
|
||||
minWidth;
|
||||
maxWidth;
|
||||
penColor;
|
||||
minDistance;
|
||||
velocityFilterWeight;
|
||||
compositeOperation;
|
||||
backgroundColor;
|
||||
throttle;
|
||||
canvasContextOptions;
|
||||
// Private stuff
|
||||
/* tslint:disable: variable-name */
|
||||
_ctx;
|
||||
_drawingStroke = false;
|
||||
_isEmpty = true;
|
||||
_dataUrl;
|
||||
_dataUrlOptions;
|
||||
_lastPoints = [];
|
||||
// Stores up to 4 most recent points; used to generate a new curve
|
||||
_data = [];
|
||||
// Stores all points in groups (one group per line or dot)
|
||||
_lastVelocity = 0;
|
||||
_lastWidth = 0;
|
||||
_strokeMoveUpdate;
|
||||
_strokePointerId;
|
||||
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;
|
||||
this._dataUrl = void 0;
|
||||
this._dataUrlOptions = void 0;
|
||||
this._strokePointerId = void 0;
|
||||
}
|
||||
redraw() {
|
||||
const data = this._data;
|
||||
const dataUrl = this._dataUrl;
|
||||
const dataUrlOptions = this._dataUrlOptions;
|
||||
this.clear();
|
||||
if (dataUrl) {
|
||||
this.fromDataURL(dataUrl, dataUrlOptions);
|
||||
}
|
||||
this.fromData(data, { clear: false });
|
||||
}
|
||||
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;
|
||||
this._dataUrl = dataUrl;
|
||||
this._dataUrlOptions = { ...options };
|
||||
});
|
||||
}
|
||||
toDataURL(type = "image/png", encoderOptions) {
|
||||
switch (type) {
|
||||
case "image/svg+xml":
|
||||
if (typeof encoderOptions !== "object") {
|
||||
encoderOptions = void 0;
|
||||
}
|
||||
return `data:image/svg+xml;base64,${btoa(
|
||||
this.toSVG(encoderOptions)
|
||||
)}`;
|
||||
default:
|
||||
if (typeof encoderOptions !== "number") {
|
||||
encoderOptions = void 0;
|
||||
}
|
||||
return this.canvas.toDataURL(type, encoderOptions);
|
||||
}
|
||||
}
|
||||
on() {
|
||||
this.canvas.style.touchAction = "none";
|
||||
this.canvas.style.msTouchAction = "none";
|
||||
this.canvas.style.userSelect = "none";
|
||||
this.canvas.style.webkitUserSelect = "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.style.webkitUserSelect = "auto";
|
||||
this.canvas.removeEventListener("pointerdown", this._handlePointerDown);
|
||||
this.canvas.removeEventListener("mousedown", this._handleMouseDown);
|
||||
this.canvas.removeEventListener("touchstart", this._handleTouchStart);
|
||||
this._removeMoveUpEventListeners();
|
||||
}
|
||||
_getListenerFunctions() {
|
||||
const canvasWindow = window.document === this.canvas.ownerDocument ? window : this.canvas.ownerDocument.defaultView ?? this.canvas.ownerDocument;
|
||||
return {
|
||||
addEventListener: canvasWindow.addEventListener.bind(
|
||||
canvasWindow
|
||||
),
|
||||
removeEventListener: canvasWindow.removeEventListener.bind(
|
||||
canvasWindow
|
||||
)
|
||||
};
|
||||
}
|
||||
_removeMoveUpEventListeners() {
|
||||
const { removeEventListener } = this._getListenerFunctions();
|
||||
removeEventListener("pointermove", this._handlePointerMove);
|
||||
removeEventListener("pointerup", this._handlePointerUp);
|
||||
removeEventListener("pointercancel", this._handlePointerCancel);
|
||||
removeEventListener("mousemove", this._handleMouseMove);
|
||||
removeEventListener("mouseup", this._handleMouseUp);
|
||||
removeEventListener("touchmove", this._handleTouchMove);
|
||||
removeEventListener("touchend", this._handleTouchEnd);
|
||||
removeEventListener("touchcancel", this._handleTouchCancel);
|
||||
}
|
||||
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;
|
||||
}
|
||||
_isLeftButtonPressed(event, only) {
|
||||
if (only) {
|
||||
return event.buttons === 1;
|
||||
}
|
||||
return (event.buttons & 1) === 1;
|
||||
}
|
||||
_pointerEventToSignatureEvent(event) {
|
||||
return {
|
||||
event,
|
||||
type: event.type,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
pressure: "pressure" in event ? event.pressure : 0
|
||||
};
|
||||
}
|
||||
_touchEventToSignatureEvent(event) {
|
||||
const touch = event.changedTouches[0];
|
||||
return {
|
||||
event,
|
||||
type: event.type,
|
||||
x: touch.clientX,
|
||||
y: touch.clientY,
|
||||
pressure: touch.force
|
||||
};
|
||||
}
|
||||
// Event handlers
|
||||
_handleMouseDown(event) {
|
||||
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleMouseMove(event) {
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleMouseUp(event) {
|
||||
if (this._isLeftButtonPressed(event)) {
|
||||
return;
|
||||
}
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchStart(event) {
|
||||
if (event.targetTouches.length !== 1 || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeBegin(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchMove(event) {
|
||||
if (event.targetTouches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
if (!this._drawingStroke) {
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handleTouchEnd(event) {
|
||||
if (event.targetTouches.length !== 0) {
|
||||
return;
|
||||
}
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerCancel(event) {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
}
|
||||
_handleTouchCancel(event) {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
}
|
||||
_getPointerId(event) {
|
||||
return event.persistentDeviceId || event.pointerId;
|
||||
}
|
||||
_allowPointerId(event, allowUndefined = false) {
|
||||
if (typeof this._strokePointerId === "undefined") {
|
||||
return allowUndefined;
|
||||
}
|
||||
return this._getPointerId(event) === this._strokePointerId;
|
||||
}
|
||||
_handlePointerDown(event) {
|
||||
if (this._drawingStroke || !this._isLeftButtonPressed(event) || !this._allowPointerId(event, true)) {
|
||||
return;
|
||||
}
|
||||
this._strokePointerId = this._getPointerId(event);
|
||||
event.preventDefault();
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerMove(event) {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_handlePointerUp(event) {
|
||||
if (this._isLeftButtonPressed(event) || !this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
_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,
|
||||
compositeOperation: group && "compositeOperation" in group ? group.compositeOperation : this.compositeOperation
|
||||
};
|
||||
}
|
||||
// Private methods
|
||||
_strokeBegin(event) {
|
||||
const cancelled = !this.dispatchEvent(
|
||||
new CustomEvent("beginStroke", { detail: event, cancelable: true })
|
||||
);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
const { addEventListener } = this._getListenerFunctions();
|
||||
switch (event.event.type) {
|
||||
case "mousedown":
|
||||
addEventListener("mousemove", this._handleMouseMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("mouseup", this._handleMouseUp, { passive: false });
|
||||
break;
|
||||
case "touchstart":
|
||||
addEventListener("touchmove", this._handleTouchMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("touchend", this._handleTouchEnd, { passive: false });
|
||||
addEventListener("touchcancel", this._handleTouchCancel, { passive: false });
|
||||
break;
|
||||
case "pointerdown":
|
||||
addEventListener("pointermove", this._handlePointerMove, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("pointerup", this._handlePointerUp, {
|
||||
passive: false
|
||||
});
|
||||
addEventListener("pointercancel", this._handlePointerCancel, {
|
||||
passive: false
|
||||
});
|
||||
break;
|
||||
default:
|
||||
}
|
||||
this._drawingStroke = true;
|
||||
const pointGroupOptions = this._getPointGroupOptions();
|
||||
const newPointGroup = {
|
||||
...pointGroupOptions,
|
||||
points: []
|
||||
};
|
||||
this._data.push(newPointGroup);
|
||||
this._reset(pointGroupOptions);
|
||||
this._strokeUpdate(event);
|
||||
}
|
||||
_strokeUpdate(event) {
|
||||
if (!this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (this._data.length === 0) {
|
||||
this._strokeBegin(event);
|
||||
return;
|
||||
}
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("beforeUpdateStroke", { detail: event })
|
||||
);
|
||||
const point = this._createPoint(event.x, event.y, event.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, shouldUpdate = true) {
|
||||
this._removeMoveUpEventListeners();
|
||||
if (!this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
if (shouldUpdate) {
|
||||
this._strokeUpdate(event);
|
||||
}
|
||||
this._drawingStroke = false;
|
||||
this._strokePointerId = void 0;
|
||||
this.dispatchEvent(new CustomEvent("endStroke", { detail: event }));
|
||||
}
|
||||
_handlePointerEvents() {
|
||||
this._drawingStroke = false;
|
||||
this.canvas.addEventListener("pointerdown", this._handlePointerDown, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
_handleMouseEvents() {
|
||||
this._drawingStroke = false;
|
||||
this.canvas.addEventListener("mousedown", this._handleMouseDown, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
_handleTouchEvents() {
|
||||
this.canvas.addEventListener("touchstart", this._handleTouchStart, {
|
||||
passive: false
|
||||
});
|
||||
}
|
||||
// Called when a new line is started
|
||||
_reset(options) {
|
||||
this._lastPoints = [];
|
||||
this._lastVelocity = 0;
|
||||
this._lastWidth = (options.minWidth + options.maxWidth) / 2;
|
||||
this._ctx.fillStyle = options.penColor;
|
||||
this._ctx.globalCompositeOperation = options.compositeOperation;
|
||||
}
|
||||
_createPoint(x, y, pressure) {
|
||||
const rect = this.canvas.getBoundingClientRect();
|
||||
return new Point(
|
||||
x - rect.left,
|
||||
y - rect.top,
|
||||
pressure,
|
||||
(/* @__PURE__ */ new Date()).getTime()
|
||||
);
|
||||
}
|
||||
// Add point to _lastPoints array and generate a new curve if there are enough points (i.e. 3)
|
||||
_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, includeDataUrl = 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);
|
||||
}
|
||||
if (includeDataUrl && this._dataUrl) {
|
||||
const ratio2 = this._dataUrlOptions?.ratio || window.devicePixelRatio || 1;
|
||||
const width = this._dataUrlOptions?.width || this.canvas.width / ratio2;
|
||||
const height = this._dataUrlOptions?.height || this.canvas.height / ratio2;
|
||||
const xOffset = this._dataUrlOptions?.xOffset || 0;
|
||||
const yOffset = this._dataUrlOptions?.yOffset || 0;
|
||||
const image = document.createElement("image");
|
||||
image.setAttribute("x", xOffset.toString());
|
||||
image.setAttribute("y", yOffset.toString());
|
||||
image.setAttribute("width", width.toString());
|
||||
image.setAttribute("height", height.toString());
|
||||
image.setAttribute("preserveAspectRatio", "none");
|
||||
image.setAttribute("href", this._dataUrl);
|
||||
svg.appendChild(image);
|
||||
}
|
||||
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;
|
||||
}
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
// <stdin>
|
||||
module.exports = (init_signature_pad(), __toCommonJS(signature_pad_exports)).default;
|
||||
|
||||
if(__exports != exports)module.exports = exports;return module.exports}));
|
||||
//# sourceMappingURL=signature_pad.umd.js.map
|
||||
7
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.js.map
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
9
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.min.js
vendored
Normal file
9
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.min.js.map
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/signature_pad.umd.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
17
storage/public/dist/libs/signature_pad/dist/types/bezier.d.ts
vendored
Normal file
17
storage/public/dist/libs/signature_pad/dist/types/bezier.d.ts
vendored
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import { BasicPoint, Point } from './point.js';
|
||||
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;
|
||||
}
|
||||
16
storage/public/dist/libs/signature_pad/dist/types/point.d.ts
vendored
Normal file
16
storage/public/dist/libs/signature_pad/dist/types/point.d.ts
vendored
Normal 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;
|
||||
}
|
||||
7
storage/public/dist/libs/signature_pad/dist/types/signature_event_target.d.ts
vendored
Normal file
7
storage/public/dist/libs/signature_pad/dist/types/signature_event_target.d.ts
vendored
Normal 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;
|
||||
}
|
||||
126
storage/public/dist/libs/signature_pad/dist/types/signature_pad.d.ts
vendored
Normal file
126
storage/public/dist/libs/signature_pad/dist/types/signature_pad.d.ts
vendored
Normal file
|
|
@ -0,0 +1,126 @@
|
|||
/**
|
||||
* 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.js';
|
||||
import { SignatureEventTarget } from './signature_event_target.js';
|
||||
export { BasicPoint } from './point.js';
|
||||
export interface SignatureEvent {
|
||||
event: MouseEvent | TouchEvent | PointerEvent;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
pressure: number;
|
||||
}
|
||||
export interface FromDataOptions {
|
||||
clear?: boolean;
|
||||
}
|
||||
export interface FromDataUrlOptions {
|
||||
ratio?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
xOffset?: number;
|
||||
yOffset?: number;
|
||||
}
|
||||
export interface ToSVGOptions {
|
||||
includeBackgroundColor?: boolean;
|
||||
includeDataUrl?: boolean;
|
||||
}
|
||||
export interface PointGroupOptions {
|
||||
dotSize: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
penColor: string;
|
||||
velocityFilterWeight: number;
|
||||
/**
|
||||
* This is the globalCompositeOperation for the line.
|
||||
* *default: 'source-over'*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
|
||||
*/
|
||||
compositeOperation: GlobalCompositeOperation;
|
||||
}
|
||||
export interface Options extends Partial<PointGroupOptions> {
|
||||
minDistance?: number;
|
||||
backgroundColor?: string;
|
||||
throttle?: number;
|
||||
canvasContextOptions?: CanvasRenderingContext2DSettings;
|
||||
}
|
||||
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;
|
||||
compositeOperation: GlobalCompositeOperation;
|
||||
backgroundColor: string;
|
||||
throttle: number;
|
||||
canvasContextOptions: CanvasRenderingContext2DSettings;
|
||||
private _ctx;
|
||||
private _drawingStroke;
|
||||
private _isEmpty;
|
||||
private _dataUrl;
|
||||
private _dataUrlOptions;
|
||||
private _lastPoints;
|
||||
private _data;
|
||||
private _lastVelocity;
|
||||
private _lastWidth;
|
||||
private _strokeMoveUpdate;
|
||||
private _strokePointerId;
|
||||
constructor(canvas: HTMLCanvasElement, options?: Options);
|
||||
clear(): void;
|
||||
redraw(): void;
|
||||
fromDataURL(dataUrl: string, options?: FromDataUrlOptions): Promise<void>;
|
||||
toDataURL(type: 'image/svg+xml', encoderOptions?: ToSVGOptions): string;
|
||||
toDataURL(type?: string, encoderOptions?: number): string;
|
||||
on(): void;
|
||||
off(): void;
|
||||
private _getListenerFunctions;
|
||||
private _removeMoveUpEventListeners;
|
||||
isEmpty(): boolean;
|
||||
fromData(pointGroups: PointGroup[], { clear }?: FromDataOptions): void;
|
||||
toData(): PointGroup[];
|
||||
private _isLeftButtonPressed;
|
||||
private _pointerEventToSignatureEvent;
|
||||
private _touchEventToSignatureEvent;
|
||||
private _handleMouseDown;
|
||||
private _handleMouseMove;
|
||||
private _handleMouseUp;
|
||||
private _handleTouchStart;
|
||||
private _handleTouchMove;
|
||||
private _handleTouchEnd;
|
||||
private _handlePointerCancel;
|
||||
private _handleTouchCancel;
|
||||
private _getPointerId;
|
||||
private _allowPointerId;
|
||||
private _handlePointerDown;
|
||||
private _handlePointerMove;
|
||||
private _handlePointerUp;
|
||||
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, includeDataUrl }?: ToSVGOptions): string;
|
||||
}
|
||||
1
storage/public/dist/libs/signature_pad/dist/types/throttle.d.ts
vendored
Normal file
1
storage/public/dist/libs/signature_pad/dist/types/throttle.d.ts
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
export declare function throttle(fn: (...args: any[]) => any, wait?: number): (this: any, ...args: any[]) => any;
|
||||
5
storage/public/dist/libs/signature_pad/docs/.eslintrc.js
vendored
Normal file
5
storage/public/dist/libs/signature_pad/docs/.eslintrc.js
vendored
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
module.exports = {
|
||||
globals: {
|
||||
SignaturePad: false
|
||||
}
|
||||
};
|
||||
177
storage/public/dist/libs/signature_pad/docs/css/signature-pad.css
vendored
Normal file
177
storage/public/dist/libs/signature_pad/docs/css/signature-pad.css
vendored
Normal file
File diff suppressed because one or more lines are too long
73
storage/public/dist/libs/signature_pad/docs/index.html
vendored
Normal file
73
storage/public/dist/libs/signature_pad/docs/index.html
vendored
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
<!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">
|
||||
<span id="forkongithub">
|
||||
<a href="https://github.com/szimek/signature_pad">Fork me on GitHub</a>
|
||||
</span>
|
||||
|
||||
<div id="signature-pad" class="signature-pad">
|
||||
<div id="canvas-wrapper" 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="undo" title="Ctrl-Z">Undo</button>
|
||||
<button type="button" class="button" data-action="redo" title="Ctrl-Y">Redo</button>
|
||||
<br/>
|
||||
<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="change-background-color">Change background color</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>
|
||||
<button type="button" class="button" data-action="open-in-window">Open in Window</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/signature_pad.umd.min.js"></script>
|
||||
<script src="js/app.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
198
storage/public/dist/libs/signature_pad/docs/js/app.js
vendored
Normal file
198
storage/public/dist/libs/signature_pad/docs/js/app.js
vendored
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
const wrapper = document.getElementById("signature-pad");
|
||||
const canvasWrapper = document.getElementById("canvas-wrapper");
|
||||
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 redoButton = wrapper.querySelector("[data-action=redo]");
|
||||
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 openInWindowButton = wrapper.querySelector("[data-action=open-in-window]");
|
||||
let undoData = [];
|
||||
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)'
|
||||
});
|
||||
|
||||
function randomColor() {
|
||||
const r = Math.round(Math.random() * 255);
|
||||
const g = Math.round(Math.random() * 255);
|
||||
const b = Math.round(Math.random() * 255);
|
||||
return `rgb(${r},${g},${b})`;
|
||||
}
|
||||
|
||||
// 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.redraw();
|
||||
}
|
||||
|
||||
// On mobile devices it might make more sense to listen to orientation change,
|
||||
// rather than window resize events.
|
||||
window.onresize = resizeCanvas;
|
||||
resizeCanvas();
|
||||
|
||||
window.addEventListener("keydown", (event) => {
|
||||
switch (true) {
|
||||
case event.key === "z" && event.ctrlKey:
|
||||
undoButton.click();
|
||||
break;
|
||||
case event.key === "y" && event.ctrlKey:
|
||||
redoButton.click();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
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 });
|
||||
}
|
||||
|
||||
signaturePad.addEventListener("endStroke", () => {
|
||||
// clear undoData when new data is added
|
||||
undoData = [];
|
||||
});
|
||||
|
||||
clearButton.addEventListener("click", () => {
|
||||
signaturePad.clear();
|
||||
});
|
||||
|
||||
undoButton.addEventListener("click", () => {
|
||||
const data = signaturePad.toData();
|
||||
|
||||
if (data && data.length > 0) {
|
||||
// remove the last dot or line
|
||||
const removed = data.pop();
|
||||
undoData.push(removed);
|
||||
signaturePad.redraw();
|
||||
}
|
||||
});
|
||||
|
||||
redoButton.addEventListener("click", () => {
|
||||
if (undoData.length > 0) {
|
||||
const data = signaturePad.toData();
|
||||
data.push(undoData.pop());
|
||||
signaturePad.redraw();
|
||||
}
|
||||
});
|
||||
|
||||
changeBackgroundColorButton.addEventListener("click", () => {
|
||||
signaturePad.backgroundColor = randomColor();
|
||||
signaturePad.redraw();
|
||||
});
|
||||
|
||||
changeColorButton.addEventListener("click", () => {
|
||||
signaturePad.penColor = randomColor();
|
||||
});
|
||||
|
||||
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, includeDataUrl: true });
|
||||
download(dataURL, "signature.svg");
|
||||
}
|
||||
});
|
||||
|
||||
openInWindowButton.addEventListener("click", () => {
|
||||
var externalWin = window.open('', '', `width=${canvas.width / window.devicePixelRatio},height=${canvas.height / window.devicePixelRatio}`);
|
||||
canvas.style.width = "100%";
|
||||
canvas.style.height = "100%";
|
||||
externalWin.onresize = resizeCanvas;
|
||||
externalWin.document.body.style.margin = '0';
|
||||
externalWin.document.body.appendChild(canvas);
|
||||
canvasWrapper.classList.add("empty");
|
||||
externalWin.onbeforeunload = () => {
|
||||
canvas.style.width = "";
|
||||
canvas.style.height = "";
|
||||
canvasWrapper.classList.remove("empty");
|
||||
canvasWrapper.appendChild(canvas);
|
||||
resizeCanvas();
|
||||
};
|
||||
})
|
||||
9
storage/public/dist/libs/signature_pad/docs/js/signature_pad.umd.min.js
vendored
Normal file
9
storage/public/dist/libs/signature_pad/docs/js/signature_pad.umd.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
7
storage/public/dist/libs/signature_pad/docs/js/signature_pad.umd.min.js.map
vendored
Normal file
7
storage/public/dist/libs/signature_pad/docs/js/signature_pad.umd.min.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
110
storage/public/dist/libs/signature_pad/package.json
vendored
Normal file
110
storage/public/dist/libs/signature_pad/package.json
vendored
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
{
|
||||
"name": "signature_pad",
|
||||
"description": "Library for drawing smooth signatures.",
|
||||
"version": "5.1.3",
|
||||
"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",
|
||||
"type": "module",
|
||||
"types": "dist/types/signature_pad.d.ts",
|
||||
"exports": {
|
||||
"types": "./dist/types/signature_pad.d.ts",
|
||||
"import": "./dist/signature_pad.js",
|
||||
"require": "./dist/signature_pad.umd.js",
|
||||
"default": "./dist/signature_pad.umd.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "yarn run lint && yarn run clean && node esbuild.config.js && 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 cpy 'dist/signature_pad.umd.min.*' docs/js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/szimek/signature_pad.git"
|
||||
},
|
||||
"files": [
|
||||
"src",
|
||||
"dist",
|
||||
"docs"
|
||||
],
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.34.0",
|
||||
"@semantic-release/changelog": "^6.0.3",
|
||||
"@semantic-release/commit-analyzer": "^13.0.1",
|
||||
"@semantic-release/git": "^10.0.1",
|
||||
"@semantic-release/github": "^11.0.5",
|
||||
"@semantic-release/npm": "^12.0.2",
|
||||
"@semantic-release/release-notes-generator": "^14.0.3",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/node": "^24.3.0",
|
||||
"cpy-cli": "^6.0.0",
|
||||
"del": "^8.0.0",
|
||||
"del-cli": "^6.0.0",
|
||||
"esbuild": "^0.25.9",
|
||||
"esbuild-plugin-umd-wrapper": "^3.0.0",
|
||||
"eslint": "^9.34.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"globals": "^16.3.0",
|
||||
"jest": "^30.1.3",
|
||||
"jest-canvas-mock": "^2.5.2",
|
||||
"jest-environment-jsdom": "^30.1.2",
|
||||
"prettier": "^3.6.2",
|
||||
"semantic-release": "^24.2.7",
|
||||
"serve": "^14.2.4",
|
||||
"ts-jest": "^29.4.1",
|
||||
"tslib": "^2.8.1",
|
||||
"typescript": "~5.9.2",
|
||||
"typescript-eslint": "^8.42.0"
|
||||
},
|
||||
"jest": {
|
||||
"moduleFileExtensions": [
|
||||
"ts",
|
||||
"js"
|
||||
],
|
||||
"moduleNameMapper": {
|
||||
"^\\./bezier\\.js$": "<rootDir>/src/bezier.ts",
|
||||
"^\\./point\\.js$": "<rootDir>/src/point.ts",
|
||||
"^\\./signature_event_target\\.js$": "<rootDir>/src/signature_event_target.ts",
|
||||
"^\\./throttle\\.js$": "<rootDir>/src/throttle.ts"
|
||||
},
|
||||
"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@4.9.4"
|
||||
}
|
||||
109
storage/public/dist/libs/signature_pad/src/bezier.ts
vendored
Normal file
109
storage/public/dist/libs/signature_pad/src/bezier.ts
vendored
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import { BasicPoint, Point } from './point.js';
|
||||
|
||||
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 = l1 + l2 == 0 ? 0 : 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);
|
||||
}
|
||||
}
|
||||
45
storage/public/dist/libs/signature_pad/src/point.ts
vendored
Normal file
45
storage/public/dist/libs/signature_pad/src/point.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
35
storage/public/dist/libs/signature_pad/src/signature_event_target.ts
vendored
Normal file
35
storage/public/dist/libs/signature_pad/src/signature_event_target.ts
vendored
Normal 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 {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
918
storage/public/dist/libs/signature_pad/src/signature_pad.ts
vendored
Normal file
918
storage/public/dist/libs/signature_pad/src/signature_pad.ts
vendored
Normal file
|
|
@ -0,0 +1,918 @@
|
|||
/**
|
||||
* 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.js';
|
||||
import { BasicPoint, Point } from './point.js';
|
||||
import { SignatureEventTarget } from './signature_event_target.js';
|
||||
import { throttle } from './throttle.js';
|
||||
|
||||
export { BasicPoint } from './point.js';
|
||||
|
||||
export interface SignatureEvent {
|
||||
event: MouseEvent | TouchEvent | PointerEvent;
|
||||
type: string;
|
||||
x: number;
|
||||
y: number;
|
||||
pressure: number;
|
||||
}
|
||||
|
||||
export interface FromDataOptions {
|
||||
clear?: boolean;
|
||||
}
|
||||
|
||||
export interface FromDataUrlOptions {
|
||||
ratio?: number;
|
||||
width?: number;
|
||||
height?: number;
|
||||
xOffset?: number;
|
||||
yOffset?: number;
|
||||
}
|
||||
|
||||
export interface ToSVGOptions {
|
||||
includeBackgroundColor?: boolean;
|
||||
includeDataUrl?: boolean;
|
||||
}
|
||||
|
||||
export interface PointGroupOptions {
|
||||
dotSize: number;
|
||||
minWidth: number;
|
||||
maxWidth: number;
|
||||
penColor: string;
|
||||
velocityFilterWeight: number;
|
||||
/**
|
||||
* This is the globalCompositeOperation for the line.
|
||||
* *default: 'source-over'*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation
|
||||
*/
|
||||
compositeOperation: GlobalCompositeOperation;
|
||||
}
|
||||
|
||||
export interface Options extends Partial<PointGroupOptions> {
|
||||
minDistance?: number;
|
||||
backgroundColor?: string;
|
||||
throttle?: number;
|
||||
canvasContextOptions?: CanvasRenderingContext2DSettings;
|
||||
}
|
||||
|
||||
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 compositeOperation: GlobalCompositeOperation;
|
||||
public backgroundColor: string;
|
||||
public throttle: number;
|
||||
public canvasContextOptions: CanvasRenderingContext2DSettings;
|
||||
|
||||
// Private stuff
|
||||
/* tslint:disable: variable-name */
|
||||
private _ctx: CanvasRenderingContext2D;
|
||||
private _drawingStroke = false;
|
||||
private _isEmpty = true;
|
||||
private _dataUrl: string | undefined;
|
||||
private _dataUrlOptions: FromDataUrlOptions | undefined;
|
||||
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;
|
||||
private _strokePointerId: number | undefined;
|
||||
/* 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;
|
||||
|
||||
// We need to handle 0 value, so use `??` instead of `||`
|
||||
this.throttle = options.throttle ?? 16; // in milliseconds
|
||||
this.minDistance = options.minDistance ?? 5; // in pixels
|
||||
this.dotSize = options.dotSize || 0;
|
||||
this.penColor = options.penColor || 'black';
|
||||
this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
|
||||
this.compositeOperation = options.compositeOperation || 'source-over';
|
||||
this.canvasContextOptions = options.canvasContextOptions ?? {};
|
||||
|
||||
this._strokeMoveUpdate = this.throttle
|
||||
? throttle(SignaturePad.prototype._strokeUpdate, this.throttle)
|
||||
: SignaturePad.prototype._strokeUpdate;
|
||||
|
||||
this._handleMouseDown = this._handleMouseDown.bind(this);
|
||||
this._handleMouseMove = this._handleMouseMove.bind(this);
|
||||
this._handleMouseUp = this._handleMouseUp.bind(this);
|
||||
this._handleTouchStart = this._handleTouchStart.bind(this);
|
||||
this._handleTouchMove = this._handleTouchMove.bind(this);
|
||||
this._handleTouchEnd = this._handleTouchEnd.bind(this);
|
||||
this._handlePointerDown = this._handlePointerDown.bind(this);
|
||||
this._handlePointerMove = this._handlePointerMove.bind(this);
|
||||
this._handlePointerUp = this._handlePointerUp.bind(this);
|
||||
this._handlePointerCancel = this._handlePointerCancel.bind(this);
|
||||
this._handleTouchCancel = this._handleTouchCancel.bind(this);
|
||||
|
||||
this._ctx = canvas.getContext(
|
||||
'2d',
|
||||
this.canvasContextOptions,
|
||||
) 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;
|
||||
this._dataUrl = undefined;
|
||||
this._dataUrlOptions = undefined;
|
||||
this._strokePointerId = undefined;
|
||||
}
|
||||
|
||||
public redraw(): void {
|
||||
const data = this._data;
|
||||
const dataUrl = this._dataUrl;
|
||||
const dataUrlOptions = this._dataUrlOptions;
|
||||
|
||||
this.clear();
|
||||
if (dataUrl) {
|
||||
this.fromDataURL(dataUrl, dataUrlOptions);
|
||||
}
|
||||
this.fromData(data, { clear: false });
|
||||
}
|
||||
|
||||
public fromDataURL(
|
||||
dataUrl: string,
|
||||
options: FromDataUrlOptions = {},
|
||||
): 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;
|
||||
this._dataUrl = dataUrl;
|
||||
this._dataUrlOptions = {...options};
|
||||
});
|
||||
}
|
||||
|
||||
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 as number);
|
||||
}
|
||||
}
|
||||
|
||||
public on(): void {
|
||||
// Disable panning/zooming when touching canvas element
|
||||
this.canvas.style.touchAction = 'none';
|
||||
(
|
||||
this.canvas.style as CSSStyleDeclaration & {
|
||||
msTouchAction: string | null;
|
||||
}
|
||||
).msTouchAction = 'none';
|
||||
this.canvas.style.userSelect = 'none';
|
||||
// Safari does not support userSelect property without a prefix even as of iOS 26
|
||||
// https://caniuse.com/?search=user-select
|
||||
this.canvas.style.webkitUserSelect = '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 as CSSStyleDeclaration & {
|
||||
msTouchAction: string | null;
|
||||
}
|
||||
).msTouchAction = 'auto';
|
||||
this.canvas.style.userSelect = 'auto';
|
||||
this.canvas.style.webkitUserSelect = 'auto';
|
||||
|
||||
this.canvas.removeEventListener('pointerdown', this._handlePointerDown);
|
||||
this.canvas.removeEventListener('mousedown', this._handleMouseDown);
|
||||
this.canvas.removeEventListener('touchstart', this._handleTouchStart);
|
||||
|
||||
this._removeMoveUpEventListeners();
|
||||
}
|
||||
|
||||
private _getListenerFunctions() {
|
||||
const canvasWindow =
|
||||
window.document === this.canvas.ownerDocument
|
||||
? window
|
||||
: (this.canvas.ownerDocument.defaultView ?? this.canvas.ownerDocument);
|
||||
|
||||
return {
|
||||
addEventListener: canvasWindow.addEventListener.bind(
|
||||
canvasWindow,
|
||||
) as typeof window.addEventListener,
|
||||
removeEventListener: canvasWindow.removeEventListener.bind(
|
||||
canvasWindow,
|
||||
) as typeof window.removeEventListener,
|
||||
};
|
||||
}
|
||||
|
||||
private _removeMoveUpEventListeners(): void {
|
||||
const { removeEventListener } = this._getListenerFunctions();
|
||||
removeEventListener('pointermove', this._handlePointerMove);
|
||||
removeEventListener('pointerup', this._handlePointerUp);
|
||||
removeEventListener('pointercancel', this._handlePointerCancel);
|
||||
|
||||
removeEventListener('mousemove', this._handleMouseMove);
|
||||
removeEventListener('mouseup', this._handleMouseUp);
|
||||
|
||||
removeEventListener('touchmove', this._handleTouchMove);
|
||||
removeEventListener('touchend', this._handleTouchEnd);
|
||||
removeEventListener('touchcancel', this._handleTouchCancel);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private _isLeftButtonPressed(event: MouseEvent, only?: boolean): boolean {
|
||||
if (only) {
|
||||
return event.buttons === 1;
|
||||
}
|
||||
|
||||
return (event.buttons & 1) === 1;
|
||||
}
|
||||
private _pointerEventToSignatureEvent(
|
||||
event: MouseEvent | PointerEvent,
|
||||
): SignatureEvent {
|
||||
return {
|
||||
event: event,
|
||||
type: event.type,
|
||||
x: event.clientX,
|
||||
y: event.clientY,
|
||||
pressure: 'pressure' in event ? event.pressure : 0,
|
||||
};
|
||||
}
|
||||
|
||||
private _touchEventToSignatureEvent(event: TouchEvent): SignatureEvent {
|
||||
const touch = event.changedTouches[0];
|
||||
return {
|
||||
event: event,
|
||||
type: event.type,
|
||||
x: touch.clientX,
|
||||
y: touch.clientY,
|
||||
pressure: touch.force,
|
||||
};
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
private _handleMouseDown(event: MouseEvent): void {
|
||||
if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handleMouseMove(event: MouseEvent): void {
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
// Stop when not pressing primary button or pressing multiple buttons
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handleMouseUp(event: MouseEvent): void {
|
||||
if (this._isLeftButtonPressed(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handleTouchStart(event: TouchEvent): void {
|
||||
if (event.targetTouches.length !== 1 || this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent scrolling.
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this._strokeBegin(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handleTouchMove(event: TouchEvent): void {
|
||||
if (event.targetTouches.length !== 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Prevent scrolling.
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
if (!this._drawingStroke) {
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
|
||||
this._strokeMoveUpdate(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handleTouchEnd(event: TouchEvent): void {
|
||||
if (event.targetTouches.length !== 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handlePointerCancel(event: PointerEvent): void {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
}
|
||||
|
||||
private _handleTouchCancel(event: TouchEvent): void {
|
||||
if (event.cancelable) {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
this._strokeEnd(this._touchEventToSignatureEvent(event), false);
|
||||
}
|
||||
|
||||
private _getPointerId(event: PointerEvent) {
|
||||
// @ts-expect-error persistentDeviceId is not available yet but we want to use it when it is available
|
||||
return event.persistentDeviceId || event.pointerId;
|
||||
}
|
||||
|
||||
private _allowPointerId(
|
||||
event: PointerEvent,
|
||||
allowUndefined = false,
|
||||
): boolean {
|
||||
if (typeof this._strokePointerId === 'undefined') {
|
||||
return allowUndefined;
|
||||
}
|
||||
|
||||
return this._getPointerId(event) === this._strokePointerId;
|
||||
}
|
||||
|
||||
private _handlePointerDown(event: PointerEvent): void {
|
||||
if (
|
||||
this._drawingStroke ||
|
||||
!this._isLeftButtonPressed(event) ||
|
||||
!this._allowPointerId(event, true)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._strokePointerId = this._getPointerId(event);
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
this._strokeBegin(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handlePointerMove(event: PointerEvent): void {
|
||||
if (!this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) {
|
||||
// Stop when primary button not pressed or multiple buttons pressed
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event), false);
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _handlePointerUp(event: PointerEvent): void {
|
||||
if (this._isLeftButtonPressed(event) || !this._allowPointerId(event)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
this._strokeEnd(this._pointerEventToSignatureEvent(event));
|
||||
}
|
||||
|
||||
private _getPointGroupOptions(group?: PointGroup): PointGroupOptions {
|
||||
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,
|
||||
compositeOperation:
|
||||
group && 'compositeOperation' in group
|
||||
? group.compositeOperation
|
||||
: this.compositeOperation,
|
||||
};
|
||||
}
|
||||
|
||||
// Private methods
|
||||
private _strokeBegin(event: SignatureEvent): void {
|
||||
const cancelled = !this.dispatchEvent(
|
||||
new CustomEvent('beginStroke', { detail: event, cancelable: true }),
|
||||
);
|
||||
if (cancelled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { addEventListener } = this._getListenerFunctions();
|
||||
switch (event.event.type) {
|
||||
case 'mousedown':
|
||||
addEventListener('mousemove', this._handleMouseMove, {
|
||||
passive: false,
|
||||
});
|
||||
addEventListener('mouseup', this._handleMouseUp, { passive: false });
|
||||
break;
|
||||
case 'touchstart':
|
||||
addEventListener('touchmove', this._handleTouchMove, {
|
||||
passive: false,
|
||||
});
|
||||
addEventListener('touchend', this._handleTouchEnd, { passive: false });
|
||||
addEventListener('touchcancel', this._handleTouchCancel, { passive: false });
|
||||
break;
|
||||
case 'pointerdown':
|
||||
addEventListener('pointermove', this._handlePointerMove, {
|
||||
passive: false,
|
||||
});
|
||||
addEventListener('pointerup', this._handlePointerUp, {
|
||||
passive: false,
|
||||
});
|
||||
addEventListener('pointercancel', this._handlePointerCancel, {
|
||||
passive: false,
|
||||
});
|
||||
break;
|
||||
default:
|
||||
// do nothing
|
||||
}
|
||||
|
||||
this._drawingStroke = true;
|
||||
|
||||
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._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
|
||||
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 point = this._createPoint(event.x, event.y, event.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, shouldUpdate = true): void {
|
||||
this._removeMoveUpEventListeners();
|
||||
|
||||
if (!this._drawingStroke) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (shouldUpdate) {
|
||||
this._strokeUpdate(event);
|
||||
}
|
||||
|
||||
this._drawingStroke = false;
|
||||
this._strokePointerId = undefined;
|
||||
this.dispatchEvent(new CustomEvent('endStroke', { detail: event }));
|
||||
}
|
||||
|
||||
private _handlePointerEvents(): void {
|
||||
this._drawingStroke = false;
|
||||
|
||||
this.canvas.addEventListener('pointerdown', this._handlePointerDown, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleMouseEvents(): void {
|
||||
this._drawingStroke = false;
|
||||
|
||||
this.canvas.addEventListener('mousedown', this._handleMouseDown, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
private _handleTouchEvents(): void {
|
||||
this.canvas.addEventListener('touchstart', this._handleTouchStart, {
|
||||
passive: false,
|
||||
});
|
||||
}
|
||||
|
||||
// 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;
|
||||
this._ctx.globalCompositeOperation = options.compositeOperation;
|
||||
}
|
||||
|
||||
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, includeDataUrl = 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);
|
||||
}
|
||||
|
||||
if (includeDataUrl && this._dataUrl) {
|
||||
const ratio = this._dataUrlOptions?.ratio || window.devicePixelRatio || 1;
|
||||
const width = this._dataUrlOptions?.width || this.canvas.width / ratio;
|
||||
const height = this._dataUrlOptions?.height || this.canvas.height / ratio;
|
||||
const xOffset = this._dataUrlOptions?.xOffset || 0;
|
||||
const yOffset = this._dataUrlOptions?.yOffset || 0;
|
||||
|
||||
const image = document.createElement('image');
|
||||
image.setAttribute('x', xOffset.toString());
|
||||
image.setAttribute('y', yOffset.toString());
|
||||
image.setAttribute('width', width.toString());
|
||||
image.setAttribute('height', height.toString());
|
||||
image.setAttribute('preserveAspectRatio', 'none');
|
||||
image.setAttribute('href', this._dataUrl);
|
||||
|
||||
svg.appendChild(image);
|
||||
}
|
||||
|
||||
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.
|
||||
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;
|
||||
}
|
||||
}
|
||||
51
storage/public/dist/libs/signature_pad/src/throttle.ts
vendored
Normal file
51
storage/public/dist/libs/signature_pad/src/throttle.ts
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue