Skip to content

Commit 1ab8c6e

Browse files
committed
feat: add canvas component.
1 parent 7dbee61 commit 1ab8c6e

File tree

6 files changed

+278
-0
lines changed

6 files changed

+278
-0
lines changed

core/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,33 @@ export default function App() {
3232
}
3333
```
3434
35+
## Canvas
36+
37+
**Experimental** components
38+
39+
```jsx mdx:preview
40+
import React, { useRef } from "react";
41+
import Signature from '@uiw/react-signature/canvas';
42+
43+
44+
const points = {
45+
"path-1": [[83.52734375,63.6015625],[83.22265625,64.02734375],[81.86328125,66.0390625],[78.69140625,70.90625],[72.76171875,80.44140625],[67.01171875,91.421875],[64.5390625,98.19921875],[63.83203125,101.25390625],[63.640625,102.5078125],[63.62109375,102.7109375],[63.96484375,102.22265625],[64.890625,100.87890625],[66.3671875,98.515625]],
46+
"path-2": [[116.5546875,65.8359375],[117.3125,65.8359375],[119.23046875,65.90625],[122.078125,66.39453125],[125.44140625,67.51171875],[128.33203125,69.2421875],[130.6484375,71.53515625],[131.94140625,73.6796875],[132.28125,75.65234375],[132.0625,77.5],[130.33203125,79.78125],[126.4921875,83.24609375],[120.9375,87.5234375],[114.859375,91.13671875],[108.09765625,93.66796875],[101.8359375,94.7734375],[96.26953125,94.7734375],[92.23828125,94.90625],[89.94921875,94.96484375],[88.234375,95.04296875],[88.03515625,95.08984375],[89.6015625,95.4296875],[94.75,96.640625],[107.55859375,98.640625],[123.6171875,100.09375],[135.5546875,100.734375],[141.140625,101.03515625],[142.2578125,101.08984375]]
47+
}
48+
49+
export default function App() {
50+
const $canvas = useRef(null);
51+
const handle = (evn) => $canvas.current?.clear();
52+
return (
53+
<>
54+
<Signature ref={$canvas} width="450" height="230" defaultPoints={points} />
55+
<br />
56+
<button onClick={handle}>Clear</button>
57+
</>
58+
);
59+
}
60+
```
61+
3562
## Readonly
3663
3764
```jsx mdx:preview
@@ -209,6 +236,34 @@ export type SignatureRef = {
209236
export default function Signature(props?: SignatureProps): React.JSX.Element;
210237
```
211238

239+
## Canvas Props
240+
241+
**Experimental** components props
242+
243+
```ts
244+
import React from 'react';
245+
import { type StrokeOptions } from 'perfect-freehand';
246+
import { type Dispatch } from '@uiw/react-signature/esm/store';
247+
export * from 'perfect-freehand';
248+
export * from '@uiw/react-signature/esm/utils';
249+
export * from '@uiw/react-signature/esm/options';
250+
export * from '@uiw/react-signature/esm/store';
251+
export interface SignatureProps extends React.CanvasHTMLAttributes<HTMLCanvasElement> {
252+
prefixCls?: string;
253+
options?: StrokeOptions;
254+
readonly?: boolean;
255+
defaultPoints?: Record<string, number[][]>;
256+
onPointer?: (points: number[][]) => void;
257+
}
258+
export type SignatureCanvasRef = {
259+
canvas: HTMLCanvasElement | null;
260+
dispatch: Dispatch;
261+
clear: () => void;
262+
};
263+
const Signature: React.ForwardRefExoticComponent<SignatureProps & React.RefAttributes<SignatureCanvasRef>>;
264+
export default Signature;
265+
```
266+
212267
### Options
213268
214269
The options object is optional, as are each of its properties.

core/canvas.d.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
declare module '@uiw/react-signature/canvas' {
2+
import React from 'react';
3+
import { type StrokeOptions } from 'perfect-freehand';
4+
import { type Dispatch } from '@uiw/react-signature/esm/store';
5+
export * from 'perfect-freehand';
6+
export * from '@uiw/react-signature/esm/utils';
7+
export * from '@uiw/react-signature/esm/options';
8+
export * from '@uiw/react-signature/esm/store';
9+
export type SignatureCanvasRef = {
10+
canvas: HTMLCanvasElement | null;
11+
dispatch: Dispatch;
12+
clear: () => void;
13+
};
14+
export interface SignatureProps extends React.CanvasHTMLAttributes<HTMLCanvasElement> {
15+
prefixCls?: string;
16+
options?: StrokeOptions;
17+
readonly?: boolean;
18+
defaultPoints?: Record<string, number[][]>;
19+
renderPath?: (d: string, keyName: string, point: number[][], index: number) => JSX.Element;
20+
onPointer?: (points: number[][]) => void;
21+
}
22+
const Signature: React.ForwardRefExoticComponent<SignatureProps & React.RefAttributes<SignatureCanvasRef>>;
23+
export default Signature;
24+
}

core/package.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,24 @@
1212
"license": "MIT",
1313
"main": "./cjs/index.js",
1414
"module": "./esm/index.js",
15+
"exports": {
16+
"./README.md": "./README.md",
17+
"./package.json": "./package.json",
18+
".": {
19+
"import": "./esm/index.js",
20+
"types": "./cjs/index.d.ts",
21+
"require": "./cjs/index.js"
22+
},
23+
"./canvas": {
24+
"import": "./esm/canvas/index.js",
25+
"types": "./cjs/canvas/index.d.ts",
26+
"require": "./cjs/canvas/index.js"
27+
}
28+
},
1529
"files": [
1630
"dist.css",
1731
"dist",
32+
"canvas.d.ts",
1833
"cjs",
1934
"esm",
2035
"src"

core/src/canvas/Paths.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import React, { useEffect } from 'react';
2+
import { getStroke } from 'perfect-freehand';
3+
import { useStore } from '../store';
4+
import { useOptionStore } from '../options';
5+
import { getSvgPathFromStroke } from '../utils';
6+
7+
export const Paths = () => {
8+
const data = useStore();
9+
const { container, ...option } = useOptionStore();
10+
11+
const canvas = container as unknown as HTMLCanvasElement;
12+
const ctx = canvas?.getContext('2d');
13+
useEffect(() => {
14+
if (!canvas) return;
15+
if (ctx) {
16+
ctx?.clearRect(0, 0, canvas.width || 0, canvas.height || 0);
17+
}
18+
Object.keys(data).forEach((key, index) => {
19+
const stroke = getStroke(data[key], option);
20+
const pathData = getSvgPathFromStroke(stroke);
21+
if (ctx) {
22+
const myPath = new Path2D(pathData);
23+
ctx.fillStyle = 'black';
24+
ctx.beginPath();
25+
ctx.fill(myPath);
26+
}
27+
});
28+
}, [data, canvas, option]);
29+
return null;
30+
};

core/src/canvas/Signature.tsx

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import React, { useEffect, useRef, useId, forwardRef, useImperativeHandle } from 'react';
2+
import { getBoundingClientRect, getClinetXY, useEvent } from '../utils';
3+
4+
import { SignatureCanvasRef, SignatureProps } from '.';
5+
import { useDispatch } from '../store';
6+
import { useOptionDispatch } from '../options';
7+
8+
export const defaultStyle: React.CSSProperties = {
9+
'--w-signature-background': '#fff',
10+
touchAction: 'none',
11+
position: 'relative',
12+
backgroundColor: 'var(--w-signature-background)',
13+
} as React.CSSProperties;
14+
15+
export const Signature = forwardRef<SignatureCanvasRef, SignatureProps>((props, ref) => {
16+
const {
17+
className,
18+
prefixCls = 'w-signature',
19+
style,
20+
readonly = false,
21+
onPointer,
22+
options,
23+
children,
24+
...others
25+
} = props;
26+
const cls = [className, prefixCls].filter(Boolean).join(' ');
27+
const $canvas = useRef<HTMLCanvasElement>(null);
28+
const $path = useRef<SVGPathElement>();
29+
const pointsRef = useRef<number[][]>();
30+
const pointCount = useRef<number>(0);
31+
const pointId = useId();
32+
const dispatch = useDispatch();
33+
const dispatchOption = useOptionDispatch();
34+
useImperativeHandle<SignatureCanvasRef, SignatureCanvasRef>(
35+
ref,
36+
() => ({
37+
canvas: $canvas.current,
38+
dispatch,
39+
clear: () => {
40+
dispatch({});
41+
const ctx = $canvas.current?.getContext('2d');
42+
ctx?.clearRect(0, 0, $canvas.current?.width || 0, $canvas.current?.height || 0);
43+
},
44+
}),
45+
[$canvas.current, dispatch],
46+
);
47+
48+
const handlePointerDown = useEvent((e: React.PointerEvent<HTMLCanvasElement>) => {
49+
if (readonly) return;
50+
pointCount.current += 1;
51+
const { offsetY, offsetX } = getBoundingClientRect($canvas.current);
52+
const clientX = e.clientX || e.nativeEvent.clientX;
53+
const clientY = e.clientY || e.nativeEvent.clientY;
54+
pointsRef.current = [[clientX - offsetX, clientY - offsetY]];
55+
const pathElm = document.createElementNS('http://www.w3.org/2000/svg', 'path');
56+
$path.current = pathElm;
57+
$canvas.current!.appendChild(pathElm);
58+
dispatch({
59+
[pointId + pointCount.current]: pointsRef.current,
60+
});
61+
});
62+
63+
const handlePointerMove = useEvent((e: PointerEvent) => {
64+
if ($path.current) {
65+
const { offsetY, offsetX } = getBoundingClientRect($canvas.current);
66+
const { clientX, clientY } = getClinetXY(e);
67+
pointsRef.current = [...pointsRef.current!, [clientX - offsetX, clientY - offsetY]];
68+
dispatch({
69+
[pointId + pointCount.current]: pointsRef.current,
70+
});
71+
}
72+
});
73+
74+
const handlePointerUp = useEvent(() => {
75+
let result = pointsRef.current || [];
76+
onPointer && props.onPointer!(result);
77+
$path.current = undefined;
78+
pointsRef.current = undefined;
79+
});
80+
81+
useEffect(() => {
82+
if ($canvas.current) {
83+
dispatchOption({ container: $canvas.current });
84+
}
85+
if (readonly) return;
86+
document.addEventListener('pointermove', handlePointerMove);
87+
document.addEventListener('pointerup', handlePointerUp);
88+
return () => {
89+
if (readonly) return;
90+
document.removeEventListener('pointermove', handlePointerMove);
91+
document.removeEventListener('pointerup', handlePointerUp);
92+
};
93+
}, []);
94+
return (
95+
<canvas
96+
{...others}
97+
ref={$canvas}
98+
className={cls}
99+
onPointerDown={handlePointerDown}
100+
style={{ ...defaultStyle, ...style }}
101+
>
102+
{children}
103+
</canvas>
104+
);
105+
});

core/src/canvas/index.tsx

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useReducer, forwardRef, useEffect } from 'react';
2+
import { type StrokeOptions } from 'perfect-freehand';
3+
import { PointerContext, PointerDispatchContext, reducer, type Dispatch } from '../store';
4+
import { OptionContext, OptionDispatchContext, reducerOption, defaultOptions } from '../options';
5+
import { Signature as Container } from './Signature';
6+
import { Paths } from './Paths';
7+
8+
export * from 'perfect-freehand';
9+
export * from '../utils';
10+
export * from '../options';
11+
export * from '../store';
12+
13+
export type SignatureCanvasRef = {
14+
canvas: HTMLCanvasElement | null;
15+
dispatch: Dispatch;
16+
clear: () => void;
17+
};
18+
19+
export interface SignatureProps extends React.CanvasHTMLAttributes<HTMLCanvasElement> {
20+
prefixCls?: string;
21+
options?: StrokeOptions;
22+
readonly?: boolean;
23+
defaultPoints?: Record<string, number[][]>;
24+
onPointer?: (points: number[][]) => void;
25+
}
26+
27+
const Signature = forwardRef<SignatureCanvasRef, SignatureProps>(
28+
({ children, options, defaultPoints, ...props }, ref) => {
29+
const [state, dispatch] = useReducer(reducer, Object.assign({}, defaultPoints));
30+
const [stateOption, dispatchOption] = useReducer(reducerOption, Object.assign({ ...defaultOptions }, options));
31+
useEffect(() => dispatchOption({ ...options }), [options]);
32+
return (
33+
<PointerContext.Provider value={state}>
34+
<PointerDispatchContext.Provider value={dispatch}>
35+
<OptionContext.Provider value={stateOption}>
36+
<OptionDispatchContext.Provider value={dispatchOption}>
37+
<Container {...props} ref={ref}>
38+
{children}
39+
</Container>
40+
<Paths />
41+
</OptionDispatchContext.Provider>
42+
</OptionContext.Provider>
43+
</PointerDispatchContext.Provider>
44+
</PointerContext.Provider>
45+
);
46+
},
47+
);
48+
49+
export default Signature;

0 commit comments

Comments
 (0)