react-native-signature-touch
Version:
A lightweight signature capture library for React Native apps
218 lines (217 loc) • 6.51 kB
JavaScript
;
import { useCallback, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react';
import { View, StyleSheet, PanResponder } from 'react-native';
import Svg, { Path, Rect } from 'react-native-svg';
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
function dist(a, b) {
const dx = a.x - b.x;
const dy = a.y - b.y;
return Math.sqrt(dx * dx + dy * dy);
}
export function pointsToPath(points) {
const [first, ...rest] = points;
if (!first) return '';
if (rest.length === 0) {
return `M ${first.x} ${first.y} L ${first.x + 0.01} ${first.y + 0.01}`;
}
let d = `M ${first.x} ${first.y}`;
for (let i = 0; i < rest.length - 1; i++) {
const curr = rest[i];
const next = rest[i + 1];
const midX = (curr.x + next.x) / 2;
const midY = (curr.y + next.y) / 2;
d += ` Q ${curr.x} ${curr.y} ${midX} ${midY}`;
}
const last = rest[rest.length - 1] ?? first;
d += ` L ${last.x} ${last.y}`;
return d;
}
export const Signature = /*#__PURE__*/forwardRef(function Signature({
strokeColor = '#000',
strokeWidth = 3,
backgroundColor = 'transparent',
minDistance = 2,
onBegin,
onEnd,
onChange,
style
}, ref) {
const svgRef = useRef(null);
const [size, setSize] = useState({
width: 0,
height: 0
});
const [paths, setPaths] = useState([]);
const redoStack = useRef([]);
const currentPoints = useRef([]);
const [_, setTick] = useState(0);
const handleLayout = e => {
const {
width,
height
} = e.nativeEvent.layout;
setSize({
width,
height
});
};
const startStroke = useCallback((x, y) => {
currentPoints.current = [{
x,
y
}];
redoStack.current = [];
onBegin?.();
setTick(t => t + 1);
}, [onBegin]);
const addPoint = useCallback((x, y) => {
const pts = currentPoints.current;
const p = {
x,
y
};
const prev = pts[pts.length - 1];
if (!prev || dist(prev, p) >= minDistance) {
pts.push(p);
setTick(t => t + 1);
}
}, [minDistance]);
const endStroke = useCallback(() => {
if (currentPoints.current.length > 0) {
const d = pointsToPath(currentPoints.current);
setPaths(prev => {
const next = [...prev, d];
onChange?.(next.length > 0);
return next;
});
currentPoints.current = [];
onEnd?.(paths);
}
setTick(t => t + 1);
}, [onEnd, onChange, paths]);
const panResponder = useMemo(() => PanResponder.create({
onStartShouldSetPanResponder: () => true,
onMoveShouldSetPanResponder: () => true,
onPanResponderGrant: evt => {
const {
locationX,
locationY
} = evt.nativeEvent;
startStroke(locationX, locationY);
},
onPanResponderMove: (evt, _gs) => {
const {
locationX,
locationY
} = evt.nativeEvent;
addPoint(locationX, locationY);
},
onPanResponderRelease: endStroke,
onPanResponderTerminate: endStroke
}), [startStroke, addPoint, endStroke]);
const clear = useCallback(() => {
setPaths([]);
redoStack.current = [];
currentPoints.current = [];
onChange?.(false);
setTick(t => t + 1);
}, [onChange]);
const undo = useCallback(() => {
setPaths(prev => {
if (prev.length === 0) return prev;
const copy = [...prev];
const last = copy.pop();
redoStack.current.push(last);
onChange?.(copy.length > 0);
return copy;
});
}, [onChange]);
const redo = useCallback(() => {
if (redoStack.current.length === 0) return;
const last = redoStack.current.pop();
setPaths(prev => {
const next = [...prev, last];
onChange?.(next.length > 0);
return next;
});
}, [onChange]);
const isEmpty = useCallback(() => paths.length === 0 && currentPoints.current.length === 0, [paths.length]);
useImperativeHandle(ref, () => ({
clear,
undo,
redo,
isEmpty,
getSvg: () => {
const pathEls = paths.map(d => `<path d="${d}" stroke="${strokeColor}" stroke-width="${strokeWidth}" fill="none" stroke-linecap="round" stroke-linejoin="round" />`).join('');
const bg = backgroundColor !== 'transparent' ? `<rect width="100%" height="100%" fill="${backgroundColor}" />` : '';
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="${size.width}" height="${size.height}" viewBox="0 0 ${size.width} ${size.height}">
${bg}${pathEls}
</svg>`;
return {
svg,
width: size.width,
height: size.height
};
},
getImage: opts => new Promise((resolve, reject) => {
const {
mimeType = 'image/png',
quality = 1,
scale = 1
} = opts || {};
const target = svgRef.current;
if (!target || !target.toDataURL) {
reject(new Error('toDataURL not supported. Ensure react-native-svg >= 13.'));
return;
}
const exportW = Math.max(1, Math.round(size.width * scale));
const exportH = Math.max(1, Math.round(size.height * scale));
target.toDataURL(data => {
resolve(data);
}, {
width: exportW,
height: exportH,
quality,
format: mimeType === 'image/jpeg' ? 'jpg' : 'png',
backgroundColor: backgroundColor === 'transparent' ? undefined : backgroundColor
});
})
}), [paths, size, strokeColor, strokeWidth, backgroundColor, clear, undo, redo, isEmpty]);
return /*#__PURE__*/_jsx(View, {
style: [styles.container, style],
onLayout: handleLayout,
...panResponder.panHandlers,
children: /*#__PURE__*/_jsxs(Svg, {
ref: svgRef,
width: size.width,
height: size.height,
children: [backgroundColor !== 'transparent' && /*#__PURE__*/_jsx(Rect, {
x: 0,
y: 0,
width: size.width,
height: size.height,
fill: backgroundColor
}), currentPoints.current.length > 0 && /*#__PURE__*/_jsx(Path, {
d: pointsToPath(currentPoints.current),
stroke: strokeColor,
strokeWidth: strokeWidth,
fill: "none",
strokeLinecap: "round",
strokeLinejoin: "round"
}), paths.map((d, i) => /*#__PURE__*/_jsx(Path, {
d: d,
stroke: strokeColor,
strokeWidth: strokeWidth,
fill: "none",
strokeLinecap: "round",
strokeLinejoin: "round"
}, i))]
})
});
});
const styles = StyleSheet.create({
container: {
flex: 1
}
});
//# sourceMappingURL=Signature.js.map