Last active
November 16, 2021 07:41
-
-
Save msvargas/3de4f80e57521cab416c818c294e9003 to your computer and use it in GitHub Desktop.
Signature Pad component with react-native-svg
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='; | |
const Base64 = { | |
btoa: (input: string = '') => { | |
let str = input; | |
let output = ''; | |
for ( | |
let block = 0, charCode, i = 0, map = chars; | |
str.charAt(i | 0) || ((map = '='), i % 1); | |
output += map.charAt(63 & (block >> (8 - (i % 1) * 8))) | |
) { | |
charCode = str.charCodeAt((i += 3 / 4)); | |
if (charCode > 0xff) { | |
throw new Error("'btoa' failed: The string to be encoded contains characters outside of the Latin1 range."); | |
} | |
block = (block << 8) | charCode; | |
} | |
return output; | |
}, | |
atob: (input: string = '') => { | |
let str = input.replace(/=+$/, ''); | |
let output = ''; | |
if (str.length % 4 == 1) { | |
throw new Error("'atob' failed: The string to be decoded is not correctly encoded."); | |
} | |
for ( | |
let bc = 0, bs = 0, buffer, i = 0; | |
(buffer = str.charAt(i++)); | |
~buffer && ((bs = bc % 4 ? bs * 64 + buffer : buffer), bc++ % 4) | |
? (output += String.fromCharCode(255 & (bs >> ((-2 * bc) & 6)))) | |
: 0 | |
) { | |
buffer = chars.indexOf(buffer); | |
} | |
return output; | |
}, | |
}; | |
export default Base64; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { IBasicPoint, Point } from './Point'; | |
export class Bezier { | |
public static fromPoints(points: Point[], widths: { start: number; end: number }): Bezier { | |
const c2 = this.calculateControlPoints(points[0], points[1], points[2]).c2; | |
const c3 = this.calculateControlPoints(points[1], points[2], points[3]).c1; | |
return new Bezier(points[1], c2, c3, points[2], widths.start, widths.end); | |
} | |
private static calculateControlPoints( | |
s1: 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 { | |
// 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); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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 { | |
const dx = this.x - start.x; | |
const dy = this.y - start.y; | |
return Math.sqrt(dx * dx + dy * dy); | |
} | |
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; | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as React from 'react'; | |
import { View, StyleSheet, ViewStyle, Animated, Platform, PixelRatio } from 'react-native'; | |
import { | |
PanGestureHandler, | |
TapGestureHandler, | |
PanGestureHandlerGestureEvent, | |
PanGestureHandlerStateChangeEvent, | |
TapGestureHandlerStateChangeEvent, | |
State, | |
} from 'react-native-gesture-handler'; | |
import Svg, { G, Path } from 'react-native-svg'; | |
import { captureRef as takeSnapshotAsync, CaptureOptions } from 'react-native-view-shot'; | |
import { Bezier } from './utils/bezier'; | |
import { IBasicPoint, Point } from './utils/point'; | |
import { serializeSvg, enableXML } from './utils/serialize'; | |
import { Base64 } from './utils/base64'; | |
export interface IOptions { | |
dotSize?: number | (() => number); | |
minStrokeWidth?: number; | |
maxStrokeWidth?: number; | |
minDistance?: number; | |
strokeColor?: string; | |
velocityFilterWeight?: number; | |
onBegin?: (e: PanGestureHandlerStateChangeEvent) => void; | |
onEnd?: (e: PanGestureHandlerStateChangeEvent) => void; | |
} | |
interface IFormatOutputOptions { | |
/** | |
* | |
* @description either png or jpg or webm (Android). Defaults to png. raw is a ARGB array of image pixels. | |
*/ | |
format?: CaptureOptions['format'] | 'svg'; | |
/** | |
* Output type when onFingerUp in function | |
* @format xml only working with "svg" to get output in string xml or (html) format | |
* @platform "zip-base64" (Android) | |
* @default "paths" | |
* @see the method you want to use to save the snapshot, one of: " - tmpfile" (default): save to a temporary file (that will only exist for as long as the app is running). " - base64": encode as base64 and returns the raw string. Use only with small images as this may result of lags (the string is sent over the bridge). N.B. This is not a data uri, use data-uri instead. " - data-uri": same as base64 but also includes the Data URI scheme header. " - zip-base64: compress data with zip deflate algorithm and than convert to base64 and return as a raw string." | |
*/ | |
output?: CaptureOptions['result'] | 'xml'; | |
} | |
interface SaveOptions extends IFormatOutputOptions, Omit<CaptureOptions, 'format' | 'output'> { | |
viewRef?: 'view' | 'svg'; | |
} | |
export interface SignatureCapturePanelProps extends IOptions, IFormatOutputOptions { | |
/** | |
* @description height when autoSave or save fire be default its de same size of signature | |
*/ | |
width?: number; | |
/** | |
* @description height when autoSave or save fire be default its de same size of signature | |
*/ | |
height?: number; | |
/** | |
* @description Custom style or override height and width, change backgroundColor ...etc | |
*/ | |
style?: ViewStyle; | |
/** | |
* autoSave when user end signature after autoSaveTimeout= 1000 | |
*/ | |
onAutoSave?: (result: string) => void; | |
/** | |
* timeout to run autoSave | |
*/ | |
autoSaveTimeout?: number; | |
/** | |
* @description called when save callback produce an error | |
*/ | |
onAutoSaveError?: (error: Error) => void; | |
/** | |
* @description imageimageOutputSize | |
* @default 480 | |
*/ | |
imageOutputSize?: number; | |
/** | |
* @default 1.0 | |
* @description the quality. 0.0 - 1.0 (default). (only available on lossy formats like jpg) | |
*/ | |
quality?: CaptureOptions['quality']; | |
/** | |
* @description enable create output with react-native-view-shot and put output after 1s when call onFingerUp | |
* @default false | |
*/ | |
autoSave?: boolean; | |
/** | |
* @description PanResponder determine if cancel draw signature when outside | |
*/ | |
shouldCancelWhenOutside?: boolean; | |
/** | |
* @description | |
*/ | |
initialPaths?: { [strokeWidth: string]: string }; | |
/** | |
* @description select if capture view shot to view container or svg container | |
* @default view | |
*/ | |
viewRef?: 'view' | 'svg'; | |
} | |
//type PathElement = React.ReactElement<React.ComponentPropsWithoutRef<typeof Path>>; | |
export type IPointGroup = Array<IBasicPoint>; | |
export interface IBasicPath { | |
[strokeWidth: string]: string; | |
} | |
export interface SignatureCapturePanelState { | |
paths: IBasicPath; | |
} | |
const isAndroid = Platform.OS === 'android'; | |
class SignatureCapturePanel extends React.PureComponent<SignatureCapturePanelProps, SignatureCapturePanelState> { | |
public static defaultProps: SignatureCapturePanelProps = { | |
format: 'png', | |
quality: 1.0, | |
output: 'data-uri', | |
autoSave: true, | |
strokeColor: 'black', | |
viewRef: 'view', | |
autoSaveTimeout: 1000, | |
}; | |
// Public stuff | |
public dotSize: number | (() => number); | |
public minStrokeWidth: number; | |
public maxStrokeWidth: number; | |
public minDistance: number; | |
public velocityFilterWeight: number; | |
// Private stuff | |
private timer: any = null; | |
private signatureContainerRef = React.createRef<any>(); | |
private svgRef = React.createRef<any>(); | |
private panRef = React.createRef<PanGestureHandler>(); | |
private tapRef = React.createRef<TapGestureHandler>(); | |
/* tslint:disable: variable-name */ | |
private _isEmpty: boolean = true; | |
private _lastPoints!: Point[]; // Stores up to 4 most recent points; used to generate a new curve | |
// Stores up to 4 most recent points; used to generate a new curve | |
private _data: Array<IPointGroup> = []; // Stores all points in groups (one group per line or dot) | |
// Stores all points in groups (one group per line or dot) | |
private _lastVelocity!: number; | |
private _lastWidth!: number; | |
private _strokeMoveUpdate!: (event: PanGestureHandlerGestureEvent) => void; | |
/* tslint:enable: variable-name */ | |
private _signatureElement: React.ReactNode; | |
private _isTap: boolean | undefined = false; | |
constructor(props: SignatureCapturePanelProps) { | |
super(props); | |
this.state = { | |
paths: props.initialPaths || {}, | |
}; | |
this.velocityFilterWeight = props.velocityFilterWeight || StyleSheet.hairlineWidth * 0.01; | |
this.minStrokeWidth = props.minStrokeWidth || StyleSheet.hairlineWidth * 0.25; | |
this.maxStrokeWidth = props.maxStrokeWidth || PixelRatio.get(); | |
this.minDistance = ('minDistance' in props ? props.minDistance : PixelRatio.get() * 0.5) as number; // in pixels | |
this._strokeMoveUpdate = this._strokeUpdate.bind(this); | |
this.dotSize = | |
props.dotSize || | |
function dotSize(this: SignatureCapturePanel) { | |
return (this.minStrokeWidth + this.maxStrokeWidth) * 0.5; | |
}; | |
this._reset(); | |
} | |
public componentDidMount() { | |
console.log(StyleSheet.hairlineWidth, PixelRatio.get()); | |
// automatic enable XML if outputType if svg | |
if (this.props.format === 'svg') enableXML(); | |
} | |
public componentWillUnmount() { | |
// clear timer on unmount signature panel | |
this.clearTimeoutAutoSave(); | |
this._signatureElement = undefined; | |
} | |
public render() { | |
const { style, shouldCancelWhenOutside } = this.props; | |
this._signatureElement = this.renderSvg(); | |
return ( | |
<TapGestureHandler | |
ref={this.tapRef} | |
numberOfTaps={1} | |
waitFor={this.panRef} | |
onHandlerStateChange={this.handlerTapStateChange} | |
> | |
<PanGestureHandler | |
ref={this.panRef} | |
maxPointers={1} | |
minPointers={1} | |
avgTouches={true} | |
shouldCancelWhenOutside={shouldCancelWhenOutside} | |
onHandlerStateChange={this.handlerPanStateChange} | |
onGestureEvent={this.handlerGestureEvent} | |
> | |
<Animated.View | |
//@ts-ignore | |
ref={this.signatureContainerRef} | |
style={[styles.container, style]} | |
> | |
{this._signatureElement} | |
</Animated.View> | |
</PanGestureHandler> | |
</TapGestureHandler> | |
); | |
} | |
/** | |
* @description get SVG paths | |
*/ | |
public get paths() { | |
return this.state.paths; | |
} | |
/** | |
* @description get svg tags of signature from SVG paths | |
*/ | |
public get svg(): string { | |
return serializeSvg(this._signatureElement) as string; | |
} | |
/** | |
* @description | |
*/ | |
/** | |
* @description clear signature | |
*/ | |
public clear() { | |
this.clearTimeoutAutoSave(); | |
this._reset(); | |
this._data = []; | |
this._isEmpty = true; | |
this.setState({ | |
paths: {}, | |
}); | |
} | |
/** | |
* @description check if signature is empty | |
*/ | |
public isEmpty(): boolean { | |
return this._isEmpty; | |
} | |
/** | |
* @description save signature | |
*/ | |
public async save(options?: SaveOptions) { | |
const { viewRef, ..._options } = Object.assign( | |
{ | |
viewRef: this.props.viewRef, | |
format: this.props.format, | |
quality: this.props.quality, | |
result: this.props.output as any, | |
}, | |
options, | |
); | |
const output = _options.output; | |
if (_options.format === 'svg') { | |
const xml = this.svg; | |
return output !== 'base64' && output !== 'data-uri' | |
? xml | |
: (output === 'data-uri' ? 'data:image/svg+xml;base64,' : '') + Base64.btoa(xml as string); | |
} | |
const { width, height } = this.props; | |
if (typeof width === 'number') _options.width = width; | |
if (typeof height === 'number') _options.height = height; | |
return await takeSnapshotAsync(viewRef === 'svg' ? this.svgRef : this.signatureContainerRef, _options as any); | |
} | |
/** | |
* @description set custom paths to draw signature | |
* @param paths | |
*/ | |
public setPaths(paths: IBasicPath) { | |
this.setState({ paths }); | |
} | |
/** | |
* @description call toDataURL inner of Svg component | |
* @callback (PNG : base64string) | |
*/ | |
public toDataURL(callback: (base64: string) => void, options?: { height?: number; width?: number }) { | |
return this.svgRef.current?.toDataURL(callback, options); | |
} | |
/** | |
* @description clear timer of autosave | |
*/ | |
public clearTimeoutAutoSave() { | |
if (this.timer) { | |
clearTimeout(this.timer); | |
this.timer = undefined; | |
} | |
} | |
/** | |
* @description autoSave function when touchEnd | |
*/ | |
public async autoSave() { | |
try { | |
const file = await this.save(); | |
this.props?.onAutoSave?.(file); | |
} catch (error) { | |
this.props.onAutoSaveError?.(error); | |
} finally { | |
this.timer = undefined; | |
} | |
} | |
/** | |
* @description append child path and group by strokeWidth to avoid create many views | |
* @param strokeWidth | |
* @param path | |
*/ | |
private appendChild(strokeWidth: string, path: string) { | |
const paths = { ...this.state.paths }; | |
if (!paths[strokeWidth]) paths[strokeWidth] = path; | |
else paths[strokeWidth] += path; | |
this.setState({ paths }); | |
} | |
/** | |
* @description Detect handle pan responder state to detect end touch | |
*/ | |
private handlerPanStateChange = (e: PanGestureHandlerStateChangeEvent) => { | |
const { oldState, state } = e.nativeEvent; | |
if (state === State.ACTIVE) { | |
this._strokeBegin(e); | |
} else if (oldState === State.ACTIVE && (state === State.END || state === State.CANCELLED)) { | |
this._strokeEnd(e); | |
} | |
}; | |
/** | |
* @description Detect signature dots to draw circles | |
*/ | |
private handlerTapStateChange = (e: TapGestureHandlerStateChangeEvent) => { | |
const { state, oldState } = e.nativeEvent; | |
if (oldState === State.ACTIVE && state === State.END) { | |
this._isTap = true; | |
this.clearTimeoutAutoSave(); | |
this._reset(); | |
this._strokeEnd(e as any); | |
} | |
}; | |
/** | |
* Detect the touch start and move events on the signature pad | |
* @param {GestureResponderEvent} e Event | |
* @private | |
*/ | |
private handlerGestureEvent = (e: PanGestureHandlerGestureEvent) => { | |
this._strokeMoveUpdate(e); | |
}; | |
/** | |
* Takes the points and forms an SVG from them | |
* @param {Array} paths | |
* @return {Element}* | |
* @private | |
*/ | |
private renderSvg = () => { | |
return ( | |
<Svg ref={this.svgRef} style={styles.pad} width="100%" height="100%"> | |
<G> | |
{Object.entries(this.state.paths).map(([strokeWidth, value]) => ( | |
<Path | |
key={strokeWidth} | |
d={value} | |
stroke={this.props.strokeColor} | |
strokeWidth={strokeWidth} | |
strokeLinecap="round" | |
strokeLinejoin="round" | |
fill={value.indexOf('a') > -1 ? 'black' : 'none'} | |
/> | |
))} | |
</G> | |
</Svg> | |
); | |
}; | |
/** | |
* @description returnSignature check if autoSave if enable | |
*/ | |
public returnSignature = () => { | |
const { autoSave } = this.props; | |
if (autoSave) this.timer = setTimeout(() => this.autoSave(), this.props.autoSaveTimeout || 1000); | |
}; | |
/** | |
* @description reset lastPoints and timeout | |
*/ | |
private _reset(): void { | |
this._lastPoints = []; | |
this._lastVelocity = 0; | |
this._lastWidth = (this.minStrokeWidth + this.maxStrokeWidth) / 2; | |
} | |
private _createPoint(x: number, y: number): Point { | |
return new Point(x, y); | |
} | |
// 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.maxStrokeWidth / (velocity + 1), this.minStrokeWidth); | |
} | |
// Private methods | |
private _strokeBegin(event: PanGestureHandlerStateChangeEvent): void { | |
this.clearTimeoutAutoSave(); | |
this._reset(); | |
this._data.push([]); | |
this._strokeUpdate(event); | |
this.props.onBegin?.(event); | |
} | |
private _strokeUpdate(event: PanGestureHandlerGestureEvent) { | |
const x = event.nativeEvent.x; | |
const y = event.nativeEvent.y; | |
const point = this._createPoint(x, y); | |
const lastPointGroup = this._data[this._data.length - 1]; | |
const lastPoints = lastPointGroup; | |
const lastPoint = lastPoints?.length > 0 && lastPoints[lastPoints?.length - 1]; | |
const isLastPointTooClose = lastPoint ? point.distanceTo(lastPoint) <= this.minDistance : false; | |
// Skip this point if it's too close to the previous one | |
if (!lastPoint || !(lastPoint && isLastPointTooClose)) { | |
const curve = this._addPoint(point); | |
if (this._isTap) { | |
this._drawDot(point); | |
this._isTap = false; | |
} else if (curve) { | |
this._drawCurve(curve); | |
} | |
lastPoints?.push?.({ | |
time: point.time, | |
x: point.x, | |
y: point.y, | |
}); | |
} | |
} | |
private _strokeEnd(e: PanGestureHandlerStateChangeEvent): void { | |
this.returnSignature(); | |
this._strokeUpdate(e); | |
this.props.onEnd?.(e); | |
} | |
private _drawCurve(curve: Bezier): void { | |
// 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 path = | |
`M ${curve.startPoint.x.toFixed(2)},${curve.startPoint.y.toFixed(2)} ` + | |
`C ${curve.control1.x.toFixed(2)},${curve.control1.y.toFixed(2)} ` + | |
`${curve.control2.x.toFixed(2)},${curve.control2.y.toFixed(2)} ` + | |
`${curve.endPoint.x.toFixed(2)},${curve.endPoint.y.toFixed(2)}`; | |
const strokeWidth = Math.round(curve.endWidth * 2.5).toString(); | |
this.appendChild(strokeWidth, path); | |
} | |
/* eslint-enable no-restricted-globals */ | |
} | |
/** | |
* Draw point | |
* @param point | |
*/ | |
private _drawDot(point: IBasicPoint) { | |
const R = Math.round(typeof this.dotSize === 'function' ? this.dotSize() : this.dotSize); | |
//circle as path | |
const path = `M ${point.x - R},${point.y} a ${R},${R} 0 1,0 ${R * 2},0 a ${1},${1} 0 1,0 -${R * 2},0`; | |
this.appendChild(R.toString(), path); | |
} | |
} | |
const styles = StyleSheet.create({ | |
container: { | |
backgroundColor: 'white', | |
alignSelf: 'stretch', | |
}, | |
pad: { | |
backgroundColor: 'transparent', | |
}, | |
}); | |
export default SignatureCapturePanel; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Slightly simplified version of http://stackoverflow.com/a/27078401/815507 | |
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 wrapper(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 = setTimeout(later, remaining) as any; | |
} | |
return result; | |
}; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment