Skip to content

SignaturePad 100% Pure Beef TypeScript Implementation

Victor Tomaili edited this page May 3, 2021 · 1 revision

How to add a 100% Pure Beef TypeScript implementation of SignaturePad.

(by: Ken Schnell)


I had wanted to implement a SignaturePad for quite some time and came across Wesley Huang's solution- which by the way was terrific. This typescript version here does not have the red tilde ~ that his code had and seems to work quite well so far.

I modified his SignaturePadEditor code as necessary to get rid of the javascript dependency and rely solely on TypeScript. I grabbed Signature Pad Typescript library. made a few modifications but not many and I have included all of them here (there are 4 files in the set, 1 large and 3 small files).

I hate having to have TS code mapper files and the java files for the same functionality this makes no "coding" sense to me and makes the code wet; like that goes against DRY.

In the code below I have made a few modifications, adding options for a border and handling the Image to be the same size as the canvas, if you don't do this your image on update will add a smaller image on top of the existing.

In my effort to be DRYer and also with a BIG SHOUT OUT of Thanks to @VolkanCeylan for this framework and many others for their code sharing I am posting the entire implementation here.


There are a total of 5 Files that you will need to have.

  1. SignatureEditor.ts
  2. Bezier.ts
  3. Pad.ts
  4. Point.ts
  5. Throttle.ts

Each of these files is separated by a remarks that looks like

Filename.ts


//=================================================================================================== // // FILE: SignatureEditor.ts // // Code mostly from @Wezmeg // Modified for pure Typescript by Ken Schnell //===================================================================================================

namespace MyProjects.MyModule {
    @Serenity.Decorators.element('<div />')
    @Serenity.Decorators.registerEditor([Serenity.IGetEditValue, Serenity.ISetEditValue])
    export class SignatureEditor extends Serenity.Widget<SignatureEditorOptions>
        implements Serenity.IGetEditValue, Serenity.ISetEditValue {

        private signaturePad: any;
        private clearDiv: string = "<div class='s-pad' height='30px' align='right' style='padding-left: 20px;' >";

        constructor(div: JQuery, options: SignatureEditorOptions) {
            super(div, options);

            var CanvasId = 'sign_' + this.uniqueName;

            let canvas = $('<canvas />').attr('id', CanvasId);

            this.element.append(canvas);

            this.signaturePad = new SignaturePad.Pad(document.querySelector('canvas'));

            let clearButton = $('<button />').attr('id', 'clearsign_' + this.uniqueName).html('<i class="fa fa-eraser"></i>');
            clearButton.on('click', (e) => {
                e.preventDefault();
                this.signaturePad.clear();
            });

            this.element.append(this.clearDiv);
            this.element.append(clearButton);
            this.element.append("</div>");

        }

        public get value(): string {
            return this.get_value();
        }

        protected get_value(): string {
            return this.signaturePad.toDataURL();
        }

        public set value(value: string) {
            this.set_value(value);
        }

        protected set_value(value: string): void {
            this.signaturePad.fromDataURL(value);
        }

        setEditValue(source: any, property: Serenity.PropertyItem): void {
            this.value = source[property.name];
        }

        getEditValue(property: Serenity.PropertyItem, target: any): void {
            target[property.name] = this.value;
        }

    }

    export interface SignatureEditorOptions {
    }

}

// Code Below comes from // https://github.com/szimek/signature_pad // //
// Options for Border , and Image Create (matched canvas size) added by Ken Schnell. //

//=================================================================================================== // // FILE: Bezier.ts // //===================================================================================================

namespace SignaturePad {
    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: IBasicPoint,
            s2: IBasicPoint,
            s3: IBasicPoint,
        ): {
                c1: IBasicPoint,
                c2: IBasicPoint,
            } {
            const dx1 = s1.x - s2.x;
            const dy1 = s1.y - s2.y;
            const dx2 = s2.x - s3.x;
            const dy2 = s2.y - s3.y;

            const m1 = { x: (s1.x + s2.x) / 2.0, y: (s1.y + s2.y) / 2.0 };
            const m2 = { x: (s2.x + s3.x) / 2.0, y: (s2.y + s3.y) / 2.0 };

            const l1 = Math.sqrt((dx1 * dx1) + (dy1 * dy1));
            const l2 = Math.sqrt((dx2 * dx2) + (dy2 * dy2));

            const dxm = (m1.x - m2.x);
            const dym = (m1.y - m2.y);

            const k = l2 / (l1 + l2);
            const cm = { x: m2.x + (dxm * k), y: m2.y + (dym * k) };

            const tx = s2.x - cm.x;
            const ty = s2.y - cm.y;

            return {
                c1: new Point(m1.x + tx, m1.y + ty),
                c2: new Point(m2.x + tx, m2.y + ty),
            };
        }

        constructor(
            public startPoint: Point,
            public control2: IBasicPoint,
            public control1: IBasicPoint,
            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 {
            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);
        }
    }
}

//=================================================================================================== // // FILE: Pad.ts // //=================================================================================================== /*

*/


namespace SignaturePad {

    export interface IOptions {
        dotSize?: number | (() => number);
        minWidth?: number;
        maxWidth?: number;
        minDistance?: number;
        backgroundColor?: string;
        penColor?: string;
        throttle?: number;
        borderWidth?: number;  //lineWidth
        borderColor?: string;  // strokeStyle = "#FF0000";
        velocityFilterWeight?: number;
        onBegin?: (event: MouseEvent | Touch) => void;
        onEnd?: (event: MouseEvent | Touch) => void;
    }

    export interface IPointGroup {
        color: string;
        points: IBasicPoint[];
    }

    export interface Window {
        PointerEvent: typeof PointerEvent;
    }

    export class Pad {
        // Public stuff
        public dotSize: number | (() => number);
        public minWidth: number;
        public maxWidth: number;
        public minDistance: number;
        public backgroundColor: string;
        public penColor: string;
        public throttle: number;
        public borderWidth: number;  //lineWidth
        public borderColor: string;  // strokeStyle = "#FF0000";
        public velocityFilterWeight: number;
        public onBegin?: (event: MouseEvent | Touch) => void;
        public onEnd?: (event: MouseEvent | Touch) => void;


        // Private stuff
        /* tslint:disable: variable-name */
        private _ctx: CanvasRenderingContext2D;
        private _mouseButtonDown: boolean;
        private _isEmpty: boolean;
        private _lastPoints: Point[]; // Stores up to 4 most recent points; used to generate a new curve
        private _data: IPointGroup[]; // Stores all points in groups (one group per line or dot)
        private _lastVelocity: number;
        private _lastWidth: number;
        private _strokeMoveUpdate: (event: MouseEvent | Touch) => void;
        /* tslint:enable: variable-name */

        constructor(private canvas: HTMLCanvasElement, private options: IOptions = {}) {
            this.velocityFilterWeight = options.velocityFilterWeight || 0.7;
            this.minWidth = options.minWidth || 0.5;
            this.maxWidth = options.maxWidth || 2.5;
            this.throttle = ('throttle' in options ? options.throttle : 16) as number; // in milisecondss
            this.minDistance = ('minDistance' in options ? options.minDistance : 5) as number; // in pixels
            this.borderWidth = options.borderWidth || 0;
            this.borderColor = options.borderColor || '#FF0000';

            if (this.throttle) {
                this._strokeMoveUpdate = throttle(Pad.prototype._strokeUpdate, this.throttle);
            } else {
                this._strokeMoveUpdate = Pad.prototype._strokeUpdate;
            }
            // this: Pad
            this.dotSize = options.dotSize || function (this: Pad) // (  this: Pad) 
            {
                return (this.minWidth + this.maxWidth) / 2;
            };
            this.penColor = options.penColor || 'black';
            this.backgroundColor = options.backgroundColor || 'rgba(0,0,0,0)';
            this.onBegin = options.onBegin;
            this.onEnd = options.onEnd;

            this._ctx = canvas.getContext('2d') as CanvasRenderingContext2D;
            this.clear();

            // Enable mouse and touch event handlers
            this.on();
        }

        public clear(): void {
            const ctx = this._ctx;
            const canvas = this.canvas;

            // Clear canvas using background color
            ctx.fillStyle = this.backgroundColor;
            ctx.clearRect(0, 0, canvas.width, canvas.height);
            ctx.globalCompositeOperation = "destination-over";
            ctx.fillRect(0, 0, canvas.width, canvas.height);
            ctx.globalCompositeOperation = "source-over";
            ctx.lineWidth = this.borderWidth;
            ctx.strokeStyle = this.borderColor;
            ctx.strokeRect(0, 0, canvas.width, canvas.height);

            this._data = [];
            this._reset();
            this._isEmpty = true;
        }

        public fromDataURL(
            dataUrl: string,
            options: { ratio?: number, width?: number, height?: number } = {},
            callback?: (error?: ErrorEvent) => void,
        ): void {

            const ratio = options.ratio || window.devicePixelRatio || 1;
            const width = options.width || (this.canvas.width / ratio);
            const height = options.height || (this.canvas.height / ratio);
            const image = new Image(width, height);

            this._reset();

            image.onload = () => {
                this._ctx.drawImage(image, 0, 0, image.width, image.height, 0, 0, width, height);
                if (callback) { callback(); }
            };
            image.onerror = (error) => {
                if (callback) { callback(<ErrorEvent>error); }
            };
            image.src = dataUrl;

            this._isEmpty = false;
        }

        public toDataURL(type = 'image/png', encoderOptions?: number) {
            switch (type) {
                case 'image/svg+xml':
                    return this._toSVG();
                default:
                    return this.canvas.toDataURL(type, encoderOptions);
            }
        }

        public on(): void {
            // Disable panning/zooming when touching canvas element
            this.canvas.style.touchAction = 'none';
            this.canvas.style.msTouchAction = 'none';
            // PointerEvent changed below 3-6-2020 Ken
            if (window.onpointerenter) {
                this._handlePointerEvents();
            } else {
                this._handleMouseEvents();

                if ('ontouchstart' in window) {
                    this._handleTouchEvents();
                }
            }
        }

        public off(): void {
            // Enable panning/zooming when touching canvas element
            this.canvas.style.touchAction = 'auto';
            this.canvas.style.msTouchAction = 'auto';

            this.canvas.removeEventListener('pointerdown', this._handleMouseDown);
            this.canvas.removeEventListener('pointermove', this._handleMouseMove);
            document.removeEventListener('pointerup', this._handleMouseUp);

            this.canvas.removeEventListener('mousedown', this._handleMouseDown);
            this.canvas.removeEventListener('mousemove', this._handleMouseMove);
            document.removeEventListener('mouseup', this._handleMouseUp);

            this.canvas.removeEventListener('touchstart', this._handleTouchStart);
            this.canvas.removeEventListener('touchmove', this._handleTouchMove);
            this.canvas.removeEventListener('touchend', this._handleTouchEnd);
        }

        public isEmpty(): boolean {
            return this._isEmpty;
        }

        public fromData(pointGroups: IPointGroup[]): void {
            this.clear();

            this._fromData(
                pointGroups,
                ({ color, curve }) => this._drawCurve({ color, curve }),
                ({ color, point }) => this._drawDot({ color, point }),
            );

            this._data = pointGroups;
        }

        public toData(): IPointGroup[] {
            return this._data;
        }

        // Event handlers
        private _handleMouseDown = (event: MouseEvent): void => {
            if (event.which === 1) {
                this._mouseButtonDown = true;
                this._strokeBegin(event);
            }
        }

        private _handleMouseMove = (event: MouseEvent): void => {
            if (this._mouseButtonDown) {
                this._strokeMoveUpdate(event);
            }
        }

        private _handleMouseUp = (event: MouseEvent): void => {
            if (event.which === 1 && this._mouseButtonDown) {
                this._mouseButtonDown = false;
                this._strokeEnd(event);
            }
        }

        private _handleTouchStart = (event: TouchEvent): void => {
            // Prevent scrolling.
            event.preventDefault();

            if (event.targetTouches.length === 1) {
                const touch = event.changedTouches[0];
                this._strokeBegin(touch);
            }
        }

        private _handleTouchMove = (event: TouchEvent): void => {
            // Prevent scrolling.
            event.preventDefault();

            const touch = event.targetTouches[0];
            this._strokeMoveUpdate(touch);
        }

        private _handleTouchEnd = (event: TouchEvent): void => {
            const wasCanvasTouched = event.target === this.canvas;
            if (wasCanvasTouched) {
                event.preventDefault();

                const touch = event.changedTouches[0];
                this._strokeEnd(touch);
            }
        }

        // Private methods
        private _strokeBegin(event: MouseEvent | Touch): void {
            const newPointGroup = {
                color: this.penColor,
                points: [],
            };

            this._data.push(newPointGroup);
            this._reset();
            this._strokeUpdate(event);

            if (typeof this.onBegin === 'function') {
                this.onBegin(event);
            }
        }

        private _strokeUpdate(event: MouseEvent | Touch): void {
            const x = event.clientX;
            const y = event.clientY;

            const point = this._createPoint(x, y);
            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 color = lastPointGroup.color;

            // Skip this point if it's too close to the previous one
            if (!lastPoint || !(lastPoint && isLastPointTooClose)) {
                const curve = this._addPoint(point);

                if (!lastPoint) {
                    this._drawDot({ color, point });
                } else if (curve) {
                    this._drawCurve({ color, curve });
                }

                lastPoints.push({
                    time: point.time,
                    x: point.x,
                    y: point.y,
                });
            }
        }

        private _strokeEnd(event: MouseEvent | Touch): void {
            this._strokeUpdate(event);

            if (typeof this.onEnd === 'function') {
                this.onEnd(event);
            }
        }

        private _handlePointerEvents(): void {
            this._mouseButtonDown = false;

            this.canvas.addEventListener('pointerdown', this._handleMouseDown);
            this.canvas.addEventListener('pointermove', this._handleMouseMove);
            document.addEventListener('pointerup', this._handleMouseUp);
        }

        private _handleMouseEvents(): void {
            this._mouseButtonDown = false;

            this.canvas.addEventListener('mousedown', this._handleMouseDown);
            this.canvas.addEventListener('mousemove', this._handleMouseMove);
            document.addEventListener('mouseup', this._handleMouseUp);
        }

        private _handleTouchEvents(): void {
            this.canvas.addEventListener('touchstart', this._handleTouchStart);
            this.canvas.addEventListener('touchmove', this._handleTouchMove);
            this.canvas.addEventListener('touchend', this._handleTouchEnd);
        }

        // Called when a new line is started
        private _reset(): void {
            this._lastPoints = [];
            this._lastVelocity = 0;
            this._lastWidth = (this.minWidth + this.maxWidth) / 2;

            this._ctx.fillStyle = this.penColor;
        }

        private _createPoint(x: number, y: number): Point {
            const rect = this.canvas.getBoundingClientRect();

            return new Point(
                x - rect.left,
                y - rect.top,
                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): 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]);
                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): { start: number, end: number } {
            const velocity = (this.velocityFilterWeight * endPoint.velocityFrom(startPoint))
                + ((1 - this.velocityFilterWeight) * this._lastVelocity);

            const newWidth = this._strokeWidth(velocity);

            const widths = {
                end: newWidth,
                start: this._lastWidth,
            };

            this._lastVelocity = velocity;
            this._lastWidth = newWidth;

            return widths;
        }

        private _strokeWidth(velocity: number): number {
            return Math.max(this.maxWidth / (velocity + 1), this.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({ color, curve }: { color: string, curve: Bezier }): void {
            const ctx = this._ctx;
            const widthDelta = curve.endWidth - curve.startWidth;
            // '2' is just an arbitrary number here. If only lenght is used, then
            // there are gaps between curve segments :/
            const drawSteps = Math.floor(curve.length()) * 2;

            ctx.beginPath();
            ctx.fillStyle = color;

            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 = curve.startWidth + (ttt * widthDelta);
                this._drawCurveSegment(x, y, width);
            }

            ctx.closePath();
            ctx.fill();
        }

        private _drawDot({ color, point }: { color: string, point: IBasicPoint }): void {
            const ctx = this._ctx;
            const width = typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize;

            ctx.beginPath();
            this._drawCurveSegment(point.x, point.y, width);
            ctx.closePath();
            ctx.fillStyle = color;
            ctx.fill();
        }

        private _fromData(
            pointGroups: IPointGroup[],
            drawCurve: Pad['_drawCurve'],
            drawDot: Pad['_drawDot'],
        ): void {
            for (const group of pointGroups) {
                const { color, points } = 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.time);

                        // All points in the group have the same color, so it's enough to set
                        // penColor just at the beginning.
                        this.penColor = color;

                        if (j === 0) {
                            this._reset();
                        }

                        const curve = this._addPoint(point);

                        if (curve) {
                            drawCurve({ color, curve });
                        }
                    }
                } else {
                    this._reset();

                    drawDot({
                        color,
                        point: points[0],
                    });
                }
            }
        }

        private _toSVG(): 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('width', this.canvas.width.toString());
            svg.setAttribute('height', this.canvas.height.toString());

            this._fromData(
                pointGroups,

                ({ color, curve }: { color: string, curve: Bezier }) => {
                    const path = document.createElement('path');

                    // Need to check curve for NaN values, these pop up when drawing
                    // lines on the canvas that are not continuous. E.g. Sharp corners
                    // or stopping mid-stroke and than continuing without lifting mouse.
                    /* eslint-disable no-restricted-globals */
                    if (!isNaN(curve.control1.x) &&
                        !isNaN(curve.control1.y) &&
                        !isNaN(curve.control2.x) &&
                        !isNaN(curve.control2.y)) {
                        const attr = `M ${curve.startPoint.x.toFixed(3)},${curve.startPoint.y.toFixed(3)} `
                            + `C ${curve.control1.x.toFixed(3)},${curve.control1.y.toFixed(3)} `
                            + `${curve.control2.x.toFixed(3)},${curve.control2.y.toFixed(3)} `
                            + `${curve.endPoint.x.toFixed(3)},${curve.endPoint.y.toFixed(3)}`;
                        path.setAttribute('d', attr);
                        path.setAttribute('stroke-width', (curve.endWidth * 2.25).toFixed(3));
                        path.setAttribute('stroke', color);
                        path.setAttribute('fill', 'none');
                        path.setAttribute('stroke-linecap', 'round');

                        svg.appendChild(path);
                    }
                    /* eslint-enable no-restricted-globals */
                },

                ({ color, point }: { color: string, point: IBasicPoint }) => {
                    const circle = document.createElement('circle');
                    const dotSize = typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize;
                    circle.setAttribute('r', dotSize.toString());
                    circle.setAttribute('cx', point.x.toString());
                    circle.setAttribute('cy', point.y.toString());
                    circle.setAttribute('fill', color);

                    svg.appendChild(circle);
                },
            );

            const prefix = 'data:image/svg+xml;base64,';
            const header = '<svg'
                + ' xmlns="http://www.w3.org/2000/svg"'
                + ' xmlns:xlink="http://www.w3.org/1999/xlink"'
                + ` viewBox="${minX} ${minY} ${maxX} ${maxY}"`
                + ` width="${maxX}"`
                + ` height="${maxY}"`
                + '>';
            let body = svg.innerHTML;

            // IE hack for missing innerHTML property on SVGElement
            if (body === undefined) {
                const dummy = document.createElement('dummy');
                const nodes = svg.childNodes;
                dummy.innerHTML = '';

                // tslint:disable-next-line: prefer-for-of
                for (let i = 0; i < nodes.length; i += 1) {
                    dummy.appendChild(nodes[i].cloneNode(true));
                }

                body = dummy.innerHTML;
            }

            const footer = '</svg>';
            const data = header + body + footer;

            return prefix + btoa(data);
        }
    }
}

//=================================================================================================== // // FILE: Point.ts // //===================================================================================================

namespace SignaturePad {

    //Interface for point data structure used e.g. in SignaturePad#fromData method
    export interface IBasicPoint {
        x: number;
        y: number;
        time: number;
    }

    export class Point implements IBasicPoint {
        public time: number;

        constructor(public x: number, public y: number, time?: number) {
            this.time = time || Date.now();
        }

        public distanceTo(start: IBasicPoint): number {
            return Math.sqrt(Math.pow(this.x - start.x, 2) + Math.pow(this.y - start.y, 2));
        }

        public equals(other: IBasicPoint): boolean {
            return this.x === other.x && this.y === other.y && this.time === other.time;
        }

        public velocityFrom(start: IBasicPoint): number {
            return (this.time !== start.time) ? this.distanceTo(start) / (this.time - start.time) : 0;
        }
    }
}

//=================================================================================================== // // FILE: Throttle.ts // //===================================================================================================

// Slightly simplified version of http://stackoverflow.com/a/27078401/815507

namespace SignaturePad {

    export function throttle(fn: (...args: any[]) => any, wait = 250) {
        let previous = 0;
        let timeout: number | null = null;
        let result: any;
        let storedContext: any;
        let storedArgs: any[];

        const later = () => {
            previous = Date.now();
            timeout = null;
            result = fn.apply(storedContext, storedArgs);

            if (!timeout) {
                storedContext = null;
                storedArgs = [];
            }
        };

        return function (this: any, ...args: 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;
        };
    }
}


========================================================================

CREATE A COLUMN IN YOUR DATABASE XYZROW_TABLE Signature nvarchar(max)

========================================================================


==========================================================================

SERENITY FRAMEWORK IMPLEMENTATIONS: NOTICE THE EDITOR ATTRIBUTE [SignatureEditor] ABOVE THE COLUMN THE EDITOR ATTRIBUTE IS WHAT MAKES TIME TRAVEL (SIGNATURE PAD) POSSIBLE

==========================================================================


MyXYZRow.cs

        [SignatureEditor]       
        [DisplayName("Signature")]
        public String Signature
        {
            get { return Fields.Signature[this]; }
            set { Fields.Signature[this] = value; }
        }

XYZForm.cs

        [SignatureEditor]
        public String Signature { get; set; }


XYZColumn.cs

        [SignatureEditor]
        public String Signature { get; set; }

XYZDialog.ts

// I am doing this as eventually my Image will be an image in the screen instead of text.
// As soon as I implement the StreamField 
        protected getSlickOptions(): Slick.GridOptions {
            let opt = super.getSlickOptions();
            opt.rowHeight = 150;
            return opt;
        }
Clone this wiki locally