# .css-1ydg16i{color:#000;-webkit-text-decoration:none;text-decoration:none;}UNPKG

 1 ```/** ``` 2 ``` * The main idea and some parts of the code (e.g. drawing variable width Bézier curve) are taken from: ``` 3 ``` * http://corner.squareup.com/2012/07/smoother-signatures.html ``` 4 ``` * ``` 5 ``` * Implementation of interpolation using cubic Bézier curves is taken from: ``` 6 ``` * https://web.archive.org/web/20160323213433/http://www.benknowscode.com/2012/09/path-interpolation-using-cubic-bezier_9742.html ``` 7 ``` * ``` 8 ``` * Algorithm for approximated length of a Bézier curve is taken from: ``` 9 ``` * http://www.lemoda.net/maths/bezier-length/index.html ``` 10 ``` */ ``` 11 ``` ``` 12 ```import { Bezier } from './bezier'; ``` 13 ```import { BasicPoint, Point } from './point'; ``` 14 ```import { SignatureEventTarget } from './signature_event_target'; ``` 15 ```import { throttle } from './throttle'; ``` 16 ``` ``` 17 ```export interface SignatureEvent { ``` 18 ``` event: MouseEvent | TouchEvent | PointerEvent; ``` 19 ``` type: string; ``` 20 ``` x: number; ``` 21 ``` y: number; ``` 22 ``` pressure: number; ``` 23 ```} ``` 24 ``` ``` 25 ```export interface FromDataOptions { ``` 26 ``` clear?: boolean; ``` 27 ```} ``` 28 ``` ``` 29 ```export interface ToSVGOptions { ``` 30 ``` includeBackgroundColor?: boolean; ``` 31 ```} ``` 32 ``` ``` 33 ```export interface PointGroupOptions { ``` 34 ``` dotSize: number; ``` 35 ``` minWidth: number; ``` 36 ``` maxWidth: number; ``` 37 ``` penColor: string; ``` 38 ``` velocityFilterWeight: number; ``` 39 ``` /** ``` 40 ``` * This is the globalCompositeOperation for the line. ``` 41 ``` * *default: 'source-over'* ``` 42 ``` * @see https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/globalCompositeOperation ``` 43 ``` */ ``` 44 ``` compositeOperation: GlobalCompositeOperation; ``` 45 ```} ``` 46 ``` ``` 47 ```export interface Options extends Partial { ``` 48 ``` minDistance?: number; ``` 49 ``` backgroundColor?: string; ``` 50 ``` throttle?: number; ``` 51 ``` canvasContextOptions?: CanvasRenderingContext2DSettings; ``` 52 ```} ``` 53 ``` ``` 54 ```export interface PointGroup extends PointGroupOptions { ``` 55 ``` points: BasicPoint[]; ``` 56 ```} ``` 57 ``` ``` 58 ```export default class SignaturePad extends SignatureEventTarget { ``` 59 ``` // Public stuff ``` 60 ``` public dotSize: number; ``` 61 ``` public minWidth: number; ``` 62 ``` public maxWidth: number; ``` 63 ``` public penColor: string; ``` 64 ``` public minDistance: number; ``` 65 ``` public velocityFilterWeight: number; ``` 66 ``` public compositeOperation: GlobalCompositeOperation; ``` 67 ``` public backgroundColor: string; ``` 68 ``` public throttle: number; ``` 69 ``` public canvasContextOptions: CanvasRenderingContext2DSettings; ``` 70 ``` ``` 71 ``` // Private stuff ``` 72 ``` /* tslint:disable: variable-name */ ``` 73 ``` private _ctx: CanvasRenderingContext2D; ``` 74 ``` private _drawingStroke = false; ``` 75 ``` private _isEmpty = true; ``` 76 ``` private _lastPoints: Point[] = []; // Stores up to 4 most recent points; used to generate a new curve ``` 77 ``` private _data: PointGroup[] = []; // Stores all points in groups (one group per line or dot) ``` 78 ``` private _lastVelocity = 0; ``` 79 ``` private _lastWidth = 0; ``` 80 ``` private _strokeMoveUpdate: (event: SignatureEvent) => void; ``` 81 ``` /* tslint:enable: variable-name */ ``` 82 ``` ``` 83 ``` constructor( ``` 84 ``` private canvas: HTMLCanvasElement, ``` 85 ``` options: Options = {}, ``` 86 ``` ) { ``` 87 ``` super(); ``` 88 ``` this.velocityFilterWeight = options.velocityFilterWeight || 0.7; ``` 89 ``` this.minWidth = options.minWidth || 0.5; ``` 90 ``` this.maxWidth = options.maxWidth || 2.5; ``` 91 ``` ``` 92 ``` // We need to handle 0 value, so use `??` instead of `||` ``` 93 ``` this.throttle = options.throttle ?? 16; // in milliseconds ``` 94 ``` this.minDistance = options.minDistance ?? 5; // in pixels ``` 95 ``` this.dotSize = options.dotSize || 0; ``` 96 ``` this.penColor = options.penColor || 'black'; ``` 97 ``` this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)'; ``` 98 ``` this.compositeOperation = options.compositeOperation || 'source-over'; ``` 99 ``` this.canvasContextOptions = options.canvasContextOptions ?? {}; ``` 100 ``` ``` 101 ``` this._strokeMoveUpdate = this.throttle ``` 102 ``` ? throttle(SignaturePad.prototype._strokeUpdate, this.throttle) ``` 103 ``` : SignaturePad.prototype._strokeUpdate; ``` 104 ``` this._ctx = canvas.getContext( ``` 105 ``` '2d', ``` 106 ``` this.canvasContextOptions, ``` 107 ``` ) as CanvasRenderingContext2D; ``` 108 ``` ``` 109 ``` this.clear(); ``` 110 ``` ``` 111 ``` // Enable mouse and touch event handlers ``` 112 ``` this.on(); ``` 113 ``` } ``` 114 ``` ``` 115 ``` public clear(): void { ``` 116 ``` const { _ctx: ctx, canvas } = this; ``` 117 ``` ``` 118 ``` // Clear canvas using background color ``` 119 ``` ctx.fillStyle = this.backgroundColor; ``` 120 ``` ctx.clearRect(0, 0, canvas.width, canvas.height); ``` 121 ``` ctx.fillRect(0, 0, canvas.width, canvas.height); ``` 122 ``` ``` 123 ``` this._data = []; ``` 124 ``` this._reset(this._getPointGroupOptions()); ``` 125 ``` this._isEmpty = true; ``` 126 ``` } ``` 127 ``` ``` 128 ``` public fromDataURL( ``` 129 ``` dataUrl: string, ``` 130 ``` options: { ``` 131 ``` ratio?: number; ``` 132 ``` width?: number; ``` 133 ``` height?: number; ``` 134 ``` xOffset?: number; ``` 135 ``` yOffset?: number; ``` 136 ``` } = {}, ``` 137 ``` ): Promise { ``` 138 ``` return new Promise((resolve, reject) => { ``` 139 ``` const image = new Image(); ``` 140 ``` const ratio = options.ratio || window.devicePixelRatio || 1; ``` 141 ``` const width = options.width || this.canvas.width / ratio; ``` 142 ``` const height = options.height || this.canvas.height / ratio; ``` 143 ``` const xOffset = options.xOffset || 0; ``` 144 ``` const yOffset = options.yOffset || 0; ``` 145 ``` ``` 146 ``` this._reset(this._getPointGroupOptions()); ``` 147 ``` ``` 148 ``` image.onload = (): void => { ``` 149 ``` this._ctx.drawImage(image, xOffset, yOffset, width, height); ``` 150 ``` resolve(); ``` 151 ``` }; ``` 152 ``` image.onerror = (error): void => { ``` 153 ``` reject(error); ``` 154 ``` }; ``` 155 ``` image.crossOrigin = 'anonymous'; ``` 156 ``` image.src = dataUrl; ``` 157 ``` ``` 158 ``` this._isEmpty = false; ``` 159 ``` }); ``` 160 ``` } ``` 161 ``` ``` 162 ``` public toDataURL( ``` 163 ``` type: 'image/svg+xml', ``` 164 ``` encoderOptions?: ToSVGOptions, ``` 165 ``` ): string; ``` 166 ``` public toDataURL(type?: string, encoderOptions?: number): string; ``` 167 ``` public toDataURL( ``` 168 ``` type = 'image/png', ``` 169 ``` encoderOptions?: number | ToSVGOptions | undefined, ``` 170 ``` ): string { ``` 171 ``` switch (type) { ``` 172 ``` case 'image/svg+xml': ``` 173 ``` if (typeof encoderOptions !== 'object') { ``` 174 ``` encoderOptions = undefined; ``` 175 ``` } ``` 176 ``` return `data:image/svg+xml;base64,\${btoa( ``` 177 ``` this.toSVG(encoderOptions as ToSVGOptions), ``` 178 ``` )}`; ``` 179 ``` default: ``` 180 ``` if (typeof encoderOptions !== 'number') { ``` 181 ``` encoderOptions = undefined; ``` 182 ``` } ``` 183 ``` return this.canvas.toDataURL(type, encoderOptions); ``` 184 ``` } ``` 185 ``` } ``` 186 ``` ``` 187 ``` public on(): void { ``` 188 ``` // Disable panning/zooming when touching canvas element ``` 189 ``` this.canvas.style.touchAction = 'none'; ``` 190 ``` (this.canvas.style as CSSStyleDeclaration & { msTouchAction: string | null }).msTouchAction = 'none'; ``` 191 ``` this.canvas.style.userSelect = 'none'; ``` 192 ``` ``` 193 ``` const isIOS = ``` 194 ``` /Macintosh/.test(navigator.userAgent) && 'ontouchstart' in document; ``` 195 ``` ``` 196 ``` // The "Scribble" feature of iOS intercepts point events. So that we can ``` 197 ``` // lose some of them when tapping rapidly. Use touch events for iOS ``` 198 ``` // platforms to prevent it. See ``` 199 ``` // https://developer.apple.com/forums/thread/664108 for more information. ``` 200 ``` if (window.PointerEvent && !isIOS) { ``` 201 ``` this._handlePointerEvents(); ``` 202 ``` } else { ``` 203 ``` this._handleMouseEvents(); ``` 204 ``` ``` 205 ``` if ('ontouchstart' in window) { ``` 206 ``` this._handleTouchEvents(); ``` 207 ``` } ``` 208 ``` } ``` 209 ``` } ``` 210 ``` ``` 211 ``` public off(): void { ``` 212 ``` // Enable panning/zooming when touching canvas element ``` 213 ``` this.canvas.style.touchAction = 'auto'; ``` 214 ``` (this.canvas.style as CSSStyleDeclaration & { msTouchAction: string | null }).msTouchAction = 'auto'; ``` 215 ``` this.canvas.style.userSelect = 'auto'; ``` 216 ``` ``` 217 ``` this.canvas.removeEventListener('pointerdown', this._handlePointerDown); ``` 218 ``` this.canvas.removeEventListener('mousedown', this._handleMouseDown); ``` 219 ``` this.canvas.removeEventListener('touchstart', this._handleTouchStart); ``` 220 ``` ``` 221 ``` this._removeMoveUpEventListeners(); ``` 222 ``` } ``` 223 ``` ``` 224 ``` private _getListenerFunctions() { ``` 225 ``` const canvasWindow = ``` 226 ``` window.document === this.canvas.ownerDocument ``` 227 ``` ? window ``` 228 ``` : this.canvas.ownerDocument.defaultView ?? this.canvas.ownerDocument; ``` 229 ``` ``` 230 ``` return { ``` 231 ``` addEventListener: canvasWindow.addEventListener.bind( ``` 232 ``` canvasWindow, ``` 233 ``` ) as typeof window.addEventListener, ``` 234 ``` removeEventListener: canvasWindow.removeEventListener.bind( ``` 235 ``` canvasWindow, ``` 236 ``` ) as typeof window.removeEventListener, ``` 237 ``` }; ``` 238 ``` } ``` 239 ``` ``` 240 ``` private _removeMoveUpEventListeners(): void { ``` 241 ``` const { removeEventListener } = this._getListenerFunctions(); ``` 242 ``` removeEventListener('pointermove', this._handlePointerMove); ``` 243 ``` removeEventListener('pointerup', this._handlePointerUp); ``` 244 ``` ``` 245 ``` removeEventListener('mousemove', this._handleMouseMove); ``` 246 ``` removeEventListener('mouseup', this._handleMouseUp); ``` 247 ``` ``` 248 ``` removeEventListener('touchmove', this._handleTouchMove); ``` 249 ``` removeEventListener('touchend', this._handleTouchEnd); ``` 250 ``` } ``` 251 ``` ``` 252 ``` public isEmpty(): boolean { ``` 253 ``` return this._isEmpty; ``` 254 ``` } ``` 255 ``` ``` 256 ``` public fromData( ``` 257 ``` pointGroups: PointGroup[], ``` 258 ``` { clear = true }: FromDataOptions = {}, ``` 259 ``` ): void { ``` 260 ``` if (clear) { ``` 261 ``` this.clear(); ``` 262 ``` } ``` 263 ``` ``` 264 ``` this._fromData( ``` 265 ``` pointGroups, ``` 266 ``` this._drawCurve.bind(this), ``` 267 ``` this._drawDot.bind(this), ``` 268 ``` ); ``` 269 ``` ``` 270 ``` this._data = this._data.concat(pointGroups); ``` 271 ``` } ``` 272 ``` ``` 273 ``` public toData(): PointGroup[] { ``` 274 ``` return this._data; ``` 275 ``` } ``` 276 ``` ``` 277 ``` public _isLeftButtonPressed(event: MouseEvent, only?: boolean): boolean { ``` 278 ``` if (only) { ``` 279 ``` return event.buttons === 1; ``` 280 ``` } ``` 281 ``` ``` 282 ``` return (event.buttons & 1) === 1; ``` 283 ``` } ``` 284 ``` private _pointerEventToSignatureEvent( ``` 285 ``` event: MouseEvent | PointerEvent, ``` 286 ``` ): SignatureEvent { ``` 287 ``` return { ``` 288 ``` event: event, ``` 289 ``` type: event.type, ``` 290 ``` x: event.clientX, ``` 291 ``` y: event.clientY, ``` 292 ``` pressure: 'pressure' in event ? event.pressure : 0, ``` 293 ``` }; ``` 294 ``` } ``` 295 ``` ``` 296 ``` private _touchEventToSignatureEvent(event: TouchEvent): SignatureEvent { ``` 297 ``` const touch = event.changedTouches[0]; ``` 298 ``` return { ``` 299 ``` event: event, ``` 300 ``` type: event.type, ``` 301 ``` x: touch.clientX, ``` 302 ``` y: touch.clientY, ``` 303 ``` pressure: touch.force, ``` 304 ``` }; ``` 305 ``` } ``` 306 ``` ``` 307 ``` // Event handlers ``` 308 ``` private _handleMouseDown = (event: MouseEvent): void => { ``` 309 ``` if (!this._isLeftButtonPressed(event, true) || this._drawingStroke) { ``` 310 ``` return; ``` 311 ``` } ``` 312 ``` this._strokeBegin(this._pointerEventToSignatureEvent(event)); ``` 313 ``` }; ``` 314 ``` ``` 315 ``` private _handleMouseMove = (event: MouseEvent): void => { ``` 316 ``` if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) { ``` 317 ``` // Stop when not pressing primary button or pressing multiple buttons ``` 318 ``` this._strokeEnd(this._pointerEventToSignatureEvent(event), false); ``` 319 ``` return; ``` 320 ``` } ``` 321 ``` ``` 322 ``` this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event)); ``` 323 ``` }; ``` 324 ``` ``` 325 ``` private _handleMouseUp = (event: MouseEvent): void => { ``` 326 ``` if (this._isLeftButtonPressed(event)) { ``` 327 ``` return; ``` 328 ``` } ``` 329 ``` ``` 330 ``` this._strokeEnd(this._pointerEventToSignatureEvent(event)); ``` 331 ``` }; ``` 332 ``` ``` 333 ``` private _handleTouchStart = (event: TouchEvent): void => { ``` 334 ``` if (event.targetTouches.length !== 1 || this._drawingStroke) { ``` 335 ``` return; ``` 336 ``` } ``` 337 ``` ``` 338 ``` // Prevent scrolling. ``` 339 ``` if (event.cancelable) { ``` 340 ``` event.preventDefault(); ``` 341 ``` } ``` 342 ``` ``` 343 ``` this._strokeBegin(this._touchEventToSignatureEvent(event)); ``` 344 ``` }; ``` 345 ``` ``` 346 ``` private _handleTouchMove = (event: TouchEvent): void => { ``` 347 ``` if (event.targetTouches.length !== 1) { ``` 348 ``` return; ``` 349 ``` } ``` 350 ``` ``` 351 ``` // Prevent scrolling. ``` 352 ``` if (event.cancelable) { ``` 353 ``` event.preventDefault(); ``` 354 ``` } ``` 355 ``` ``` 356 ``` if (!this._drawingStroke) { ``` 357 ``` this._strokeEnd(this._touchEventToSignatureEvent(event), false); ``` 358 ``` return; ``` 359 ``` } ``` 360 ``` ``` 361 ``` this._strokeMoveUpdate(this._touchEventToSignatureEvent(event)); ``` 362 ``` }; ``` 363 ``` ``` 364 ``` private _handleTouchEnd = (event: TouchEvent): void => { ``` 365 ``` if (event.targetTouches.length !== 0) { ``` 366 ``` return; ``` 367 ``` } ``` 368 ``` ``` 369 ``` if (event.cancelable) { ``` 370 ``` event.preventDefault(); ``` 371 ``` } ``` 372 ``` ``` 373 ``` this.canvas.removeEventListener('touchmove', this._handleTouchMove); ``` 374 ``` ``` 375 ``` this._strokeEnd(this._touchEventToSignatureEvent(event)); ``` 376 ``` }; ``` 377 ``` ``` 378 ``` private _handlePointerDown = (event: PointerEvent): void => { ``` 379 ``` if (!this._isLeftButtonPressed(event) || this._drawingStroke) { ``` 380 ``` return; ``` 381 ``` } ``` 382 ``` ``` 383 ``` event.preventDefault(); ``` 384 ``` ``` 385 ``` this._strokeBegin(this._pointerEventToSignatureEvent(event)); ``` 386 ``` }; ``` 387 ``` ``` 388 ``` private _handlePointerMove = (event: PointerEvent): void => { ``` 389 ``` if (!this._isLeftButtonPressed(event, true) || !this._drawingStroke) { ``` 390 ``` // Stop when primary button not pressed or multiple buttons pressed ``` 391 ``` this._strokeEnd(this._pointerEventToSignatureEvent(event), false); ``` 392 ``` return; ``` 393 ``` } ``` 394 ``` ``` 395 ``` event.preventDefault(); ``` 396 ``` this._strokeMoveUpdate(this._pointerEventToSignatureEvent(event)); ``` 397 ``` }; ``` 398 ``` ``` 399 ``` private _handlePointerUp = (event: PointerEvent): void => { ``` 400 ``` if (this._isLeftButtonPressed(event)) { ``` 401 ``` return; ``` 402 ``` } ``` 403 ``` ``` 404 ``` event.preventDefault(); ``` 405 ``` this._strokeEnd(this._pointerEventToSignatureEvent(event)); ``` 406 ``` }; ``` 407 ``` ``` 408 ``` private _getPointGroupOptions(group?: PointGroup): PointGroupOptions { ``` 409 ``` return { ``` 410 ``` penColor: group && 'penColor' in group ? group.penColor : this.penColor, ``` 411 ``` dotSize: group && 'dotSize' in group ? group.dotSize : this.dotSize, ``` 412 ``` minWidth: group && 'minWidth' in group ? group.minWidth : this.minWidth, ``` 413 ``` maxWidth: group && 'maxWidth' in group ? group.maxWidth : this.maxWidth, ``` 414 ``` velocityFilterWeight: ``` 415 ``` group && 'velocityFilterWeight' in group ``` 416 ``` ? group.velocityFilterWeight ``` 417 ``` : this.velocityFilterWeight, ``` 418 ``` compositeOperation: ``` 419 ``` group && 'compositeOperation' in group ``` 420 ``` ? group.compositeOperation ``` 421 ``` : this.compositeOperation, ``` 422 ``` }; ``` 423 ``` } ``` 424 ``` ``` 425 ``` // Private methods ``` 426 ``` private _strokeBegin(event: SignatureEvent): void { ``` 427 ``` const cancelled = !this.dispatchEvent( ``` 428 ``` new CustomEvent('beginStroke', { detail: event, cancelable: true }), ``` 429 ``` ); ``` 430 ``` if (cancelled) { ``` 431 ``` return; ``` 432 ``` } ``` 433 ``` ``` 434 ``` const { addEventListener } = this._getListenerFunctions(); ``` 435 ``` switch (event.event.type) { ``` 436 ``` case 'mousedown': ``` 437 ``` addEventListener('mousemove', this._handleMouseMove); ``` 438 ``` addEventListener('mouseup', this._handleMouseUp); ``` 439 ``` break; ``` 440 ``` case 'touchstart': ``` 441 ``` addEventListener('touchmove', this._handleTouchMove); ``` 442 ``` addEventListener('touchend', this._handleTouchEnd); ``` 443 ``` break; ``` 444 ``` case 'pointerdown': ``` 445 ``` addEventListener('pointermove', this._handlePointerMove); ``` 446 ``` addEventListener('pointerup', this._handlePointerUp); ``` 447 ``` break; ``` 448 ``` default: ``` 449 ``` // do nothing ``` 450 ``` } ``` 451 ``` ``` 452 ``` this._drawingStroke = true; ``` 453 ``` ``` 454 ``` const pointGroupOptions = this._getPointGroupOptions(); ``` 455 ``` ``` 456 ``` const newPointGroup: PointGroup = { ``` 457 ``` ...pointGroupOptions, ``` 458 ``` points: [], ``` 459 ``` }; ``` 460 ``` ``` 461 ``` this._data.push(newPointGroup); ``` 462 ``` this._reset(pointGroupOptions); ``` 463 ``` this._strokeUpdate(event); ``` 464 ``` } ``` 465 ``` ``` 466 ``` private _strokeUpdate(event: SignatureEvent): void { ``` 467 ``` if (!this._drawingStroke) { ``` 468 ``` return; ``` 469 ``` } ``` 470 ``` ``` 471 ``` if (this._data.length === 0) { ``` 472 ``` // This can happen if clear() was called while a signature is still in progress, ``` 473 ``` // or if there is a race condition between start/update events. ``` 474 ``` this._strokeBegin(event); ``` 475 ``` return; ``` 476 ``` } ``` 477 ``` ``` 478 ``` this.dispatchEvent( ``` 479 ``` new CustomEvent('beforeUpdateStroke', { detail: event }), ``` 480 ``` ); ``` 481 ``` ``` 482 ``` const point = this._createPoint(event.x, event.y, event.pressure); ``` 483 ``` const lastPointGroup = this._data[this._data.length - 1]; ``` 484 ``` const lastPoints = lastPointGroup.points; ``` 485 ``` const lastPoint = ``` 486 ``` lastPoints.length > 0 && lastPoints[lastPoints.length - 1]; ``` 487 ``` const isLastPointTooClose = lastPoint ``` 488 ``` ? point.distanceTo(lastPoint) <= this.minDistance ``` 489 ``` : false; ``` 490 ``` const pointGroupOptions = this._getPointGroupOptions(lastPointGroup); ``` 491 ``` ``` 492 ``` // Skip this point if it's too close to the previous one ``` 493 ``` if (!lastPoint || !(lastPoint && isLastPointTooClose)) { ``` 494 ``` const curve = this._addPoint(point, pointGroupOptions); ``` 495 ``` ``` 496 ``` if (!lastPoint) { ``` 497 ``` this._drawDot(point, pointGroupOptions); ``` 498 ``` } else if (curve) { ``` 499 ``` this._drawCurve(curve, pointGroupOptions); ``` 500 ``` } ``` 501 ``` ``` 502 ``` lastPoints.push({ ``` 503 ``` time: point.time, ``` 504 ``` x: point.x, ``` 505 ``` y: point.y, ``` 506 ``` pressure: point.pressure, ``` 507 ``` }); ``` 508 ``` } ``` 509 ``` ``` 510 ``` this.dispatchEvent(new CustomEvent('afterUpdateStroke', { detail: event })); ``` 511 ``` } ``` 512 ``` ``` 513 ``` private _strokeEnd(event: SignatureEvent, shouldUpdate = true): void { ``` 514 ``` this._removeMoveUpEventListeners(); ``` 515 ``` ``` 516 ``` if (!this._drawingStroke) { ``` 517 ``` return; ``` 518 ``` } ``` 519 ``` ``` 520 ``` if (shouldUpdate) { ``` 521 ``` this._strokeUpdate(event); ``` 522 ``` } ``` 523 ``` ``` 524 ``` this._drawingStroke = false; ``` 525 ``` this.dispatchEvent(new CustomEvent('endStroke', { detail: event })); ``` 526 ``` } ``` 527 ``` ``` 528 ``` private _handlePointerEvents(): void { ``` 529 ``` this._drawingStroke = false; ``` 530 ``` ``` 531 ``` this.canvas.addEventListener('pointerdown', this._handlePointerDown); ``` 532 ``` } ``` 533 ``` ``` 534 ``` private _handleMouseEvents(): void { ``` 535 ``` this._drawingStroke = false; ``` 536 ``` ``` 537 ``` this.canvas.addEventListener('mousedown', this._handleMouseDown); ``` 538 ``` } ``` 539 ``` ``` 540 ``` private _handleTouchEvents(): void { ``` 541 ``` this.canvas.addEventListener('touchstart', this._handleTouchStart); ``` 542 ``` } ``` 543 ``` ``` 544 ``` // Called when a new line is started ``` 545 ``` private _reset(options: PointGroupOptions): void { ``` 546 ``` this._lastPoints = []; ``` 547 ``` this._lastVelocity = 0; ``` 548 ``` this._lastWidth = (options.minWidth + options.maxWidth) / 2; ``` 549 ``` this._ctx.fillStyle = options.penColor; ``` 550 ``` this._ctx.globalCompositeOperation = options.compositeOperation; ``` 551 ``` } ``` 552 ``` ``` 553 ``` private _createPoint(x: number, y: number, pressure: number): Point { ``` 554 ``` const rect = this.canvas.getBoundingClientRect(); ``` 555 ``` ``` 556 ``` return new Point( ``` 557 ``` x - rect.left, ``` 558 ``` y - rect.top, ``` 559 ``` pressure, ``` 560 ``` new Date().getTime(), ``` 561 ``` ); ``` 562 ``` } ``` 563 ``` ``` 564 ``` // Add point to _lastPoints array and generate a new curve if there are enough points (i.e. 3) ``` 565 ``` private _addPoint(point: Point, options: PointGroupOptions): Bezier | null { ``` 566 ``` const { _lastPoints } = this; ``` 567 ``` ``` 568 ``` _lastPoints.push(point); ``` 569 ``` ``` 570 ``` if (_lastPoints.length > 2) { ``` 571 ``` // To reduce the initial lag make it work with 3 points ``` 572 ``` // by copying the first point to the beginning. ``` 573 ``` if (_lastPoints.length === 3) { ``` 574 ``` _lastPoints.unshift(_lastPoints[0]); ``` 575 ``` } ``` 576 ``` ``` 577 ``` // _points array will always have 4 points here. ``` 578 ``` const widths = this._calculateCurveWidths( ``` 579 ``` _lastPoints[1], ``` 580 ``` _lastPoints[2], ``` 581 ``` options, ``` 582 ``` ); ``` 583 ``` const curve = Bezier.fromPoints(_lastPoints, widths); ``` 584 ``` ``` 585 ``` // Remove the first element from the list, so that there are no more than 4 points at any time. ``` 586 ``` _lastPoints.shift(); ``` 587 ``` ``` 588 ``` return curve; ``` 589 ``` } ``` 590 ``` ``` 591 ``` return null; ``` 592 ``` } ``` 593 ``` ``` 594 ``` private _calculateCurveWidths( ``` 595 ``` startPoint: Point, ``` 596 ``` endPoint: Point, ``` 597 ``` options: PointGroupOptions, ``` 598 ``` ): { start: number; end: number } { ``` 599 ``` const velocity = ``` 600 ``` options.velocityFilterWeight * endPoint.velocityFrom(startPoint) + ``` 601 ``` (1 - options.velocityFilterWeight) * this._lastVelocity; ``` 602 ``` ``` 603 ``` const newWidth = this._strokeWidth(velocity, options); ``` 604 ``` ``` 605 ``` const widths = { ``` 606 ``` end: newWidth, ``` 607 ``` start: this._lastWidth, ``` 608 ``` }; ``` 609 ``` ``` 610 ``` this._lastVelocity = velocity; ``` 611 ``` this._lastWidth = newWidth; ``` 612 ``` ``` 613 ``` return widths; ``` 614 ``` } ``` 615 ``` ``` 616 ``` private _strokeWidth(velocity: number, options: PointGroupOptions): number { ``` 617 ``` return Math.max(options.maxWidth / (velocity + 1), options.minWidth); ``` 618 ``` } ``` 619 ``` ``` 620 ``` private _drawCurveSegment(x: number, y: number, width: number): void { ``` 621 ``` const ctx = this._ctx; ``` 622 ``` ``` 623 ``` ctx.moveTo(x, y); ``` 624 ``` ctx.arc(x, y, width, 0, 2 * Math.PI, false); ``` 625 ``` this._isEmpty = false; ``` 626 ``` } ``` 627 ``` ``` 628 ``` private _drawCurve(curve: Bezier, options: PointGroupOptions): void { ``` 629 ``` const ctx = this._ctx; ``` 630 ``` const widthDelta = curve.endWidth - curve.startWidth; ``` 631 ``` // '2' is just an arbitrary number here. If only length is used, then ``` 632 ``` // there are gaps between curve segments :/ ``` 633 ``` const drawSteps = Math.ceil(curve.length()) * 2; ``` 634 ``` ``` 635 ``` ctx.beginPath(); ``` 636 ``` ctx.fillStyle = options.penColor; ``` 637 ``` ``` 638 ``` for (let i = 0; i < drawSteps; i += 1) { ``` 639 ``` // Calculate the Bezier (x, y) coordinate for this step. ``` 640 ``` const t = i / drawSteps; ``` 641 ``` const tt = t * t; ``` 642 ``` const ttt = tt * t; ``` 643 ``` const u = 1 - t; ``` 644 ``` const uu = u * u; ``` 645 ``` const uuu = uu * u; ``` 646 ``` ``` 647 ``` let x = uuu * curve.startPoint.x; ``` 648 ``` x += 3 * uu * t * curve.control1.x; ``` 649 ``` x += 3 * u * tt * curve.control2.x; ``` 650 ``` x += ttt * curve.endPoint.x; ``` 651 ``` ``` 652 ``` let y = uuu * curve.startPoint.y; ``` 653 ``` y += 3 * uu * t * curve.control1.y; ``` 654 ``` y += 3 * u * tt * curve.control2.y; ``` 655 ``` y += ttt * curve.endPoint.y; ``` 656 ``` ``` 657 ``` const width = Math.min( ``` 658 ``` curve.startWidth + ttt * widthDelta, ``` 659 ``` options.maxWidth, ``` 660 ``` ); ``` 661 ``` this._drawCurveSegment(x, y, width); ``` 662 ``` } ``` 663 ``` ``` 664 ``` ctx.closePath(); ``` 665 ``` ctx.fill(); ``` 666 ``` } ``` 667 ``` ``` 668 ``` private _drawDot(point: BasicPoint, options: PointGroupOptions): void { ``` 669 ``` const ctx = this._ctx; ``` 670 ``` const width = ``` 671 ``` options.dotSize > 0 ``` 672 ``` ? options.dotSize ``` 673 ``` : (options.minWidth + options.maxWidth) / 2; ``` 674 ``` ``` 675 ``` ctx.beginPath(); ``` 676 ``` this._drawCurveSegment(point.x, point.y, width); ``` 677 ``` ctx.closePath(); ``` 678 ``` ctx.fillStyle = options.penColor; ``` 679 ``` ctx.fill(); ``` 680 ``` } ``` 681 ``` ``` 682 ``` private _fromData( ``` 683 ``` pointGroups: PointGroup[], ``` 684 ``` drawCurve: SignaturePad['_drawCurve'], ``` 685 ``` drawDot: SignaturePad['_drawDot'], ``` 686 ``` ): void { ``` 687 ``` for (const group of pointGroups) { ``` 688 ``` const { points } = group; ``` 689 ``` const pointGroupOptions = this._getPointGroupOptions(group); ``` 690 ``` ``` 691 ``` if (points.length > 1) { ``` 692 ``` for (let j = 0; j < points.length; j += 1) { ``` 693 ``` const basicPoint = points[j]; ``` 694 ``` const point = new Point( ``` 695 ``` basicPoint.x, ``` 696 ``` basicPoint.y, ``` 697 ``` basicPoint.pressure, ``` 698 ``` basicPoint.time, ``` 699 ``` ); ``` 700 ``` ``` 701 ``` if (j === 0) { ``` 702 ``` this._reset(pointGroupOptions); ``` 703 ``` } ``` 704 ``` ``` 705 ``` const curve = this._addPoint(point, pointGroupOptions); ``` 706 ``` ``` 707 ``` if (curve) { ``` 708 ``` drawCurve(curve, pointGroupOptions); ``` 709 ``` } ``` 710 ``` } ``` 711 ``` } else { ``` 712 ``` this._reset(pointGroupOptions); ``` 713 ``` ``` 714 ``` drawDot(points[0], pointGroupOptions); ``` 715 ``` } ``` 716 ``` } ``` 717 ``` } ``` 718 ``` ``` 719 ``` public toSVG({ includeBackgroundColor = false }: ToSVGOptions = {}): string { ``` 720 ``` const pointGroups = this._data; ``` 721 ``` const ratio = Math.max(window.devicePixelRatio || 1, 1); ``` 722 ``` const minX = 0; ``` 723 ``` const minY = 0; ``` 724 ``` const maxX = this.canvas.width / ratio; ``` 725 ``` const maxY = this.canvas.height / ratio; ``` 726 ``` const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); ``` 727 ``` ``` 728 ``` svg.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); ``` 729 ``` svg.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink'); ``` 730 ``` svg.setAttribute('viewBox', `\${minX} \${minY} \${maxX} \${maxY}`); ``` 731 ``` svg.setAttribute('width', maxX.toString()); ``` 732 ``` svg.setAttribute('height', maxY.toString()); ``` 733 ``` ``` 734 ``` if (includeBackgroundColor && this.backgroundColor) { ``` 735 ``` const rect = document.createElement('rect'); ``` 736 ``` rect.setAttribute('width', '100%'); ``` 737 ``` rect.setAttribute('height', '100%'); ``` 738 ``` rect.setAttribute('fill', this.backgroundColor); ``` 739 ``` ``` 740 ``` svg.appendChild(rect); ``` 741 ``` } ``` 742 ``` ``` 743 ``` this._fromData( ``` 744 ``` pointGroups, ``` 745 ``` ``` 746 ``` (curve, { penColor }) => { ``` 747 ``` const path = document.createElement('path'); ``` 748 ``` ``` 749 ``` // Need to check curve for NaN values, these pop up when drawing ``` 750 ``` // lines on the canvas that are not continuous. E.g. Sharp corners ``` 751 ``` // or stopping mid-stroke and than continuing without lifting mouse. ``` 752 ``` /* eslint-disable no-restricted-globals */ ``` 753 ``` if ( ``` 754 ``` !isNaN(curve.control1.x) && ``` 755 ``` !isNaN(curve.control1.y) && ``` 756 ``` !isNaN(curve.control2.x) && ``` 757 ``` !isNaN(curve.control2.y) ``` 758 ``` ) { ``` 759 ``` const attr = ``` 760 ``` `M \${curve.startPoint.x.toFixed(3)},\${curve.startPoint.y.toFixed( ``` 761 ``` 3, ``` 762 ``` )} ` + ``` 763 ``` `C \${curve.control1.x.toFixed(3)},\${curve.control1.y.toFixed(3)} ` + ``` 764 ``` `\${curve.control2.x.toFixed(3)},\${curve.control2.y.toFixed(3)} ` + ``` 765 ``` `\${curve.endPoint.x.toFixed(3)},\${curve.endPoint.y.toFixed(3)}`; ``` 766 ``` path.setAttribute('d', attr); ``` 767 ``` path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3)); ``` 768 ``` path.setAttribute('stroke', penColor); ``` 769 ``` path.setAttribute('fill', 'none'); ``` 770 ``` path.setAttribute('stroke-linecap', 'round'); ``` 771 ``` ``` 772 ``` svg.appendChild(path); ``` 773 ``` } ``` 774 ``` /* eslint-enable no-restricted-globals */ ``` 775 ``` }, ``` 776 ``` ``` 777 ``` (point, { penColor, dotSize, minWidth, maxWidth }) => { ``` 778 ``` const circle = document.createElement('circle'); ``` 779 ``` const size = dotSize > 0 ? dotSize : (minWidth + maxWidth) / 2; ``` 780 ``` circle.setAttribute('r', size.toString()); ``` 781 ``` circle.setAttribute('cx', point.x.toString()); ``` 782 ``` circle.setAttribute('cy', point.y.toString()); ``` 783 ``` circle.setAttribute('fill', penColor); ``` 784 ``` ``` 785 ``` svg.appendChild(circle); ``` 786 ``` }, ``` 787 ``` ); ``` 788 ``` ``` 789 ``` return svg.outerHTML; ``` 790 ``` } ``` 791 ```} ```