├── .github
├── FUNDING.yml
└── workflows
│ └── ci.yml
├── .gitignore
├── .husky
└── pre-commit
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── core
├── .kktrc.ts
├── README.md
├── canvas.d.ts
├── package.json
├── src
│ ├── Paths.tsx
│ ├── Signature.tsx
│ ├── canvas
│ │ ├── Paths.tsx
│ │ ├── Signature.tsx
│ │ └── index.tsx
│ ├── index.tsx
│ ├── options.tsx
│ ├── store.tsx
│ └── utils.ts
└── tsconfig.json
├── lerna.json
├── package.json
├── renovate.json
├── test
├── canvas.test.tsx
└── index.test.tsx
├── tsconfig.json
└── website
├── .kktrc.ts
├── README.md
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── Example.tsx
├── index.tsx
└── react-app-env.d.ts
└── tsconfig.json
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | ko_fi: jaywcjlove
2 | buy_me_a_coffee: jaywcjlove
3 | custom: ["https://www.paypal.me/kennyiseeyou", "https://jaywcjlove.github.io/#/sponsor"]
4 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - main
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | permissions:
11 | contents: write
12 | id-token: write
13 | steps:
14 | - uses: actions/checkout@v4
15 | - uses: actions/setup-node@v4
16 | with:
17 | node-version: 20
18 | registry-url: 'https://registry.npmjs.org'
19 |
20 | - run: npm install --unsafe-perm
21 | # - run: npm run hoist
22 | - run: npm run build
23 | - run: npm run coverage
24 | - run: npm run bundle
25 | - run: npm run bundle:min
26 | - run: npm run doc
27 |
28 | - name: Generate Contributors Images
29 | uses: jaywcjlove/github-action-contributors@main
30 | with:
31 | filter-author: (renovate\[bot\]|renovate-bot|dependabot\[bot\])
32 | output: website/build/CONTRIBUTORS.svg
33 | avatarSize: 42
34 |
35 | - name: Create Coverage Badges
36 | uses: jaywcjlove/coverage-badges-cli@main
37 | with:
38 | output: website/build/badges.svg
39 |
40 | - run: cp -rp coverage/lcov-report website/build
41 | - run: cp -rp core/dist/*js website/build
42 |
43 | - name: Is a tag created auto?
44 | id: create_tag
45 | uses: jaywcjlove/create-tag-action@main
46 | with:
47 | token: ${{ secrets.GITHUB_TOKEN }}
48 | package-path: ./core/package.json
49 |
50 | - name: get tag version
51 | id: tag_version
52 | uses: jaywcjlove/changelog-generator@main
53 |
54 | - name: Deploy
55 | uses: peaceiris/actions-gh-pages@v4
56 | with:
57 | commit_message: ${{steps.tag_version.outputs.tag}} ${{ github.event.head_commit.message }}
58 | github_token: ${{ secrets.GITHUB_TOKEN }}
59 | publish_dir: ./website/build
60 |
61 | - name: Generate Changelog
62 | id: changelog
63 | uses: jaywcjlove/changelog-generator@main
64 | if: steps.create_tag.outputs.successful
65 | with:
66 | head-ref: ${{ steps.create_tag.outputs.version }}
67 | filter-author: (小弟调调™|Renovate Bot|renovate-bot)
68 | filter: '[R|r]elease[d]\s+[v|V]\d(\.\d+){0,2}'
69 |
70 | - name: Create Release
71 | uses: ncipollo/release-action@v1
72 | if: steps.create_tag.outputs.successful
73 | with:
74 | allowUpdates: true
75 | token: ${{ secrets.GITHUB_TOKEN }}
76 | name: ${{ steps.changelog.outputs.tag }}
77 | tag: ${{ steps.changelog.outputs.tag }}
78 | body: |
79 | [](https://jaywcjlove.github.io/#/sponsor)
80 |
81 | Documentation ${{ steps.changelog.outputs.tag }}: https://raw.githack.com/uiwjs/react-signature/${{ steps.changelog.outputs.gh-pages-short-hash }}/index.html
82 | Comparing Changes: ${{ steps.changelog.outputs.compareurl }}
83 |
84 | ${{ steps.changelog.outputs.changelog }}
85 |
86 | - run: npm publish --access public --provenance
87 | name: 📦 @uiw/react-signature to NPM
88 | working-directory: core
89 | continue-on-error: true
90 | env:
91 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
92 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | coverage
3 | dist
4 | build
5 | lib
6 | esm
7 | cjs
8 |
9 | dist.css
10 |
11 | # Log files
12 | npm-debug.log*
13 | yarn.lock
14 | yarn-debug.log*
15 | yarn-error.log*
16 | pnpm-debug.log*
17 | package-lock.json
18 |
19 | # local env files
20 | .env.local
21 | .env.*.local
22 |
23 | # Editor directories and files
24 | .DS_Store
25 | .idea
26 | .lerna_backup
27 | .vscode
28 | *.suo
29 | *.ntvs*
30 | *.njsproj
31 | *.sln
32 | *.sw?
33 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | npx --no-install lint-staged
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | **/*.md
2 | **/*.svg
3 | **/*.ejs
4 | **/*.yml
5 | package.json
6 | node_modules
7 | dist
8 | build
9 | lib
10 | esm
11 | cjs
12 | test
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all",
4 | "printWidth": 120,
5 | "overrides": [
6 | {
7 | "files": ".prettierrc",
8 | "options": { "parser": "json" }
9 | }
10 | ]
11 | }
12 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 uiw
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | core/README.md
--------------------------------------------------------------------------------
/core/.kktrc.ts:
--------------------------------------------------------------------------------
1 | import { LoaderConfOptions, WebpackConfiguration } from 'kkt';
2 | import lessModules from '@kkt/less-modules';
3 |
4 | export default (conf: WebpackConfiguration, env: 'development' | 'production', options: LoaderConfOptions) => {
5 | conf = lessModules(conf, env, options);
6 | if (options.bundle) {
7 | conf.output!.library = '@uiw/react-signature';
8 | conf.externals = {
9 | react: {
10 | root: 'React',
11 | commonjs2: 'react',
12 | commonjs: 'react',
13 | amd: 'react',
14 | },
15 | 'react-dom': {
16 | root: 'ReactDOM',
17 | commonjs2: 'react-dom',
18 | commonjs: 'react-dom',
19 | amd: 'react-dom',
20 | },
21 | };
22 | }
23 | return conf;
24 | };
25 |
--------------------------------------------------------------------------------
/core/README.md:
--------------------------------------------------------------------------------
1 | react-signature
2 | ===
3 |
4 | [](https://jaywcjlove.github.io/#/sponsor)
5 | [](https://github.com/uiwjs/react-signature/actions/workflows/ci.yml)
6 | [](https://uiwjs.github.io/react-signature/lcov-report/)
7 | [](https://www.npmjs.com/package/@uiw/react-signature)
8 |
9 | A signature board component for react.
10 |
11 | [](https://uiwjs.github.io/react-signature/)
12 |
13 | ## Quick Start
14 |
15 | ```bash
16 | npm install @uiw/react-signature
17 | ```
18 |
19 | ```jsx mdx:preview
20 | import React, { useRef } from "react";
21 | import Signature from '@uiw/react-signature';
22 |
23 | export default function App() {
24 | const $svg = useRef(null);
25 | const handle = (evn) => $svg.current?.clear();
26 | return (
27 | <>
28 |
29 |
30 | >
31 | );
32 | }
33 | ```
34 |
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 |
55 |
56 |
57 | >
58 | );
59 | }
60 | ```
61 |
62 | ## Readonly
63 |
64 | ```jsx mdx:preview
65 | import React, { useRef, useState, useEffect } from "react";
66 | import Signature from '@uiw/react-signature';
67 |
68 | const points = {
69 | "path-01": [[81.546875,38.87890625],[82.34375,37.55078125],[84.27734375,35.2265625],[87.73046875,33.2421875],[94.98046875,30.87890625],[106.29296875,28.6796875],[118.23828125,27.71875],[126.6953125,29.45703125],[134.9375,36.22265625],[141.8359375,47.13671875],[143.9765625,59.53515625],[141.8671875,76],[128.1484375,98.9609375],[106.203125,118.87109375],[95.04296875,125.8203125],[94.5859375,125.04296875],[94.7265625,122.04296875],[97.90234375,115.30859375],[109.28515625,102.3046875],[129.75390625,86.796875],[155.27734375,76.14453125],[184.99609375,71.8828125],[218.16015625,73.33984375],[249.26953125,77.8671875],[264.2421875,81.10546875],[266.51171875,82.35546875],[267.69921875,82.99609375],[267.64453125,83.08203125],[267.73046875,82.6875],[267.73828125,80.4921875],[267.83203125,75.4765625],[268.36328125,66.6484375],[268.84765625,56.4609375],[268.421875,46.88671875],[266.9296875,40.86328125],[264.90625,38.4765625],[262.87890625,37.30859375],[258.68359375,39.13671875],[251.265625,43.80078125],[242.21484375,51.79296875],[232.60546875,63.21484375],[222.6484375,78.53125],[213.01171875,94.58984375],[206.3203125,104.57421875],[201.64453125,109.59375],[198.61328125,111.87109375],[197.24609375,111.3671875],[195.421875,109.23828125],[193.87890625,104.91015625],[193.7265625,99.140625],[198.671875,89.87109375],[214.1015625,75.14453125],[243.25390625,54.8671875],[290.3671875,29.3515625],[329.31640625,10.6875],[338.9609375,6.70703125],[338.5234375,9.07421875],[336.8671875,15.4453125],[331.4296875,28.4140625],[323.234375,43.5234375],[312.70703125,57.9609375],[302.296875,66.97265625],[293.85546875,70.734375],[287.6484375,72.25],[283.45703125,70.9453125],[280.51171875,68.09375],[279.5390625,64.13671875],[280.828125,60.0234375],[284.98828125,56.40625],[294.63671875,54.62109375],[307.8984375,56.17578125],[316.7890625,62.65234375],[317.5859375,74.11328125],[309.1640625,92.50390625],[295.12109375,110.55859375],[282.93359375,123.125],[278.58203125,129.00390625],[279.46875,129.80859375],[285.25390625,129.26953125],[299.71484375,125.71875],[321.41015625,118.89453125],[349.296875,108.58203125],[372.53125,98.8671875],[387.57421875,91],[395.23046875,84.53125],[396.7421875,79.3046875],[395.31640625,75.45703125],[389.86328125,73.57421875],[374.875,77.3203125],[347.36328125,88.46875],[310.44140625,107.75],[273.14453125,130.30078125],[252.08984375,145.77734375],[246.2890625,152.421875],[245.4609375,153.8046875],[245.3671875,153.1640625],[245.78515625,148.46875],[247.5859375,136.29296875],[250.55078125,119.38671875],[253.1796875,107.87890625],[255.4765625,102.29296875],[257.40625,99.61328125],[260.58203125,99.8125],[273.9453125,105.66015625],[296.24609375,118.10546875],[326.40234375,138.03125],[355.63671875,158.3515625],[368.66796875,167.1796875],[373.015625,170.12109375],[374.7578125,170.8203125],[374.73046875,169.78125],[374.6953125,167.3203125],[374.765625,161.75],[375.6328125,154.09375],[377.94921875,146.08984375],[380.94921875,140.23046875],[383.6640625,136.3046875],[385.0546875,134.08203125],[385.19140625,133.45703125],[384.81640625,132.41015625]],
70 | "path-02": [[335.0078125,77.8828125],[335.31640625,77.984375],[336.16796875,78.30078125],[337.640625,79.0546875],[339.65234375,80.44140625],[342.88671875,82.921875],[347.0625,86.28125],[353.9296875,91.546875],[358.88671875,95.125],[359.7734375,95.6875],[360.48828125,96.19921875],[360.2265625,96.42578125]]
71 | }
72 |
73 | export default function App() {
74 | const [readonly, setReadonly] = useState(true)
75 | return (
76 | <>
77 |
78 |
79 | >
80 | );
81 | }
82 | ```
83 |
84 | ## Background Color & Text color
85 |
86 | Background transparency can be set with `--w-signature-background: transparent;`
87 |
88 | ```jsx mdx:preview
89 | import React, { useRef } from "react";
90 | import Signature from '@uiw/react-signature';
91 |
92 | export default function App() {
93 | const $svg = useRef(null);
94 | const handle = (evn) => $svg.current?.clear();
95 | return (
96 | <>
97 |
98 |
99 | >
100 | );
101 | }
102 | ```
103 |
104 | ## Create Points
105 |
106 | ```jsx mdx:preview
107 | import React, { useRef, useState, useCallback, useEffect } from "react";
108 | import Signature from '@uiw/react-signature';
109 |
110 | export default function App() {
111 | const $svg = useRef(null);
112 | const [points, setPoints] = useState([])
113 | const handle = (evn) => {
114 | $svg.current?.clear();
115 | setPoints([])
116 | }
117 | const handlePoints = (data) => {
118 | if (data.length > 0) {
119 | setPoints([ ...points, JSON.stringify(data) ]);
120 | }
121 | }
122 | return (
123 | <>
124 |
125 |
126 | {points.map((item, idx) => {
127 | return
{item}
128 | })}
129 | >
130 | );
131 | }
132 | ```
133 |
134 | ## Render Path
135 |
136 | ```jsx mdx:preview
137 | import React, { useRef } from "react";
138 | import Signature from '@uiw/react-signature';
139 |
140 | const points = {
141 | "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]],
142 | "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]]
143 | }
144 |
145 | export default function App() {
146 | const $svg = useRef(null);
147 | const handle = (evn) => $svg.current?.clear();
148 | return (
149 | <>
150 | {
154 | if (keyName === 'path-1' || index === 0) {
155 | return
156 | }
157 | if (keyName === 'path-2' || index === 1) {
158 | return
159 | }
160 | }}
161 | />
162 |
163 | >
164 | );
165 | }
166 | ```
167 |
168 | ## Stroke
169 |
170 | ```jsx mdx:preview
171 | import React, { useRef } from "react";
172 | import Signature from '@uiw/react-signature';
173 |
174 | const points = {
175 | "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]],
176 | "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]]
177 | }
178 |
179 | export default function App() {
180 | const $svg = useRef(null);
181 | const handle = (evn) => $svg.current?.clear();
182 | return (
183 | <>
184 | {
191 | return (
192 | <>
193 |
202 |
211 | >
212 | )
213 | }}
214 | />
215 |
216 | >
217 | );
218 | }
219 | ```
220 |
221 | ## Props
222 |
223 | ```ts
224 | import React from 'react';
225 | import { type StrokeOptions } from 'perfect-freehand';
226 | import { type Dispatch } from '@uiw/react-signature';
227 | export interface SignatureProps extends React.SVGProps {
228 | prefixCls?: string;
229 | options?: StrokeOptions;
230 | readonly?: boolean;
231 | defaultPoints?: Record;
232 | renderPath?: (d: string, keyName: string, point: number[][], index: number, container: SVGSVGElement) => JSX.Element;
233 | onPointer?: (points: number[][]) => void;
234 | }
235 | export type SignatureRef = {
236 | svg: SVGSVGElement | null;
237 | dispatch: Dispatch;
238 | clear: () => void;
239 | };
240 | export default function Signature(props?: SignatureProps): React.JSX.Element;
241 | ```
242 |
243 | ## Canvas Props
244 |
245 | **Experimental** components props
246 |
247 | ```ts
248 | import React from 'react';
249 | import { type StrokeOptions } from 'perfect-freehand';
250 | import { type Dispatch } from '@uiw/react-signature/esm/store';
251 | export * from 'perfect-freehand';
252 | export * from '@uiw/react-signature/esm/utils';
253 | export * from '@uiw/react-signature/esm/options';
254 | export * from '@uiw/react-signature/esm/store';
255 | export interface SignatureProps extends React.CanvasHTMLAttributes {
256 | prefixCls?: string;
257 | options?: StrokeOptions;
258 | readonly?: boolean;
259 | defaultPoints?: Record;
260 | onPointer?: (points: number[][]) => void;
261 | }
262 | export type SignatureCanvasRef = {
263 | canvas: HTMLCanvasElement | null;
264 | dispatch: Dispatch;
265 | clear: () => void;
266 | };
267 | const Signature: React.ForwardRefExoticComponent>;
268 | export default Signature;
269 | ```
270 |
271 | ### Options
272 |
273 | The options object is optional, as are each of its properties.
274 |
275 | | Property | Type | Default | Description |
276 | | ------------------ | -------- | ------- | ----------------------------------------------------- |
277 | | `size` | number | 8 | The base size (diameter) of the stroke. |
278 | | `thinning` | number | .5 | The effect of pressure on the stroke's size. |
279 | | `smoothing` | number | .5 | How much to soften the stroke's edges. |
280 | | `streamline` | number | .5 | How much to streamline the stroke. |
281 | | `simulatePressure` | boolean | true | Whether to simulate pressure based on velocity. |
282 | | `easing` | function | t => t | An easing function to apply to each point's pressure. |
283 | | `start` | { } | | Tapering options for the start of the line. |
284 | | `end` | { } | | Tapering options for the end of the line. |
285 | | `last` | boolean | false | Whether the stroke is complete. |
286 |
287 | **Note:** When the `last` property is `true`, the line's end will be drawn at the last input point, rather than slightly behind it.
288 |
289 | The `start` and `end` options accept an object:
290 |
291 | | Property | Type | Default | Description |
292 | | -------- | ----------------- | ------- | ---------------------------------------------------------------------------------------- |
293 | | `cap` | boolean | true | Whether to draw a cap. |
294 | | `taper` | number or boolean | 0 | The distance to taper. If set to true, the taper will be the total length of the stroke. |
295 | | `easing` | function | t => t | An easing function for the tapering effect. |
296 |
297 | **Note:** The `cap` property has no effect when `taper` is more than zero.
298 |
299 | ```js
300 | t,
307 | simulatePressure: true,
308 | last: true,
309 | start: {
310 | cap: true,
311 | taper: 0,
312 | easing: (t) => t,
313 | },
314 | end: {
315 | cap: true,
316 | taper: 0,
317 | easing: (t) => t,
318 | },
319 | }}
320 | />
321 | ```
322 |
323 | > **Tip:** To create a stroke with a steady line, set the `thinning` option to `0`.
324 |
325 | > **Tip:** To create a stroke that gets thinner with pressure instead of thicker, use a negative number for the `thinning` option.
326 |
327 | ## Development
328 |
329 | 1. Dependencies in the installation package and example
330 |
331 | ```bash
332 | npm run install
333 | ```
334 |
335 | 2. To develop, run the self-reloading build:
336 |
337 | ```bash
338 | npm run build # Compile packages 📦 @uiw/react-signature
339 | npm run watch # Real-time compilation 📦 @uiw/react-signature
340 | ```
341 |
342 | 3. Run Document Website Environment:
343 |
344 | ```bash
345 | npm run start
346 | ```
347 |
348 | 4. To contribute, please fork repos, add your patch and tests for it (in the `test/` folder) and submit a pull request.
349 |
350 | ```
351 | npm run test
352 | ```
353 |
354 | ## Contributors
355 |
356 | As always, thanks to our amazing contributors!
357 |
358 |
359 |
360 |
361 |
362 | Made with [contributors](https://github.com/jaywcjlove/github-action-contributors).
363 |
364 | ## License
365 |
366 | Licensed under the MIT License.
--------------------------------------------------------------------------------
/core/canvas.d.ts:
--------------------------------------------------------------------------------
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 {
15 | prefixCls?: string;
16 | options?: StrokeOptions;
17 | readonly?: boolean;
18 | defaultPoints?: Record;
19 | renderPath?: (d: string, keyName: string, point: number[][], index: number) => JSX.Element;
20 | onPointer?: (points: number[][]) => void;
21 | }
22 | const Signature: React.ForwardRefExoticComponent>;
23 | export default Signature;
24 | }
25 |
--------------------------------------------------------------------------------
/core/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@uiw/react-signature",
3 | "version": "1.3.2",
4 | "description": "A signature board component for react.",
5 | "author": "Kenny Wong ",
6 | "homepage": "https://uiwjs.github.io/react-signature",
7 | "funding": "https://jaywcjlove.github.io/#/sponsor",
8 | "repository": {
9 | "type": "git",
10 | "url": "https://github.com/uiwjs/react-signature.git"
11 | },
12 | "license": "MIT",
13 | "main": "./cjs/index.js",
14 | "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 | },
29 | "files": [
30 | "dist.css",
31 | "dist",
32 | "canvas.d.ts",
33 | "cjs",
34 | "esm",
35 | "src"
36 | ],
37 | "publishConfig": {
38 | "access": "public"
39 | },
40 | "keywords": [
41 | "react-signature",
42 | "react.js",
43 | "react",
44 | "signature",
45 | "monorepo",
46 | "uiw",
47 | "uiw-react",
48 | "react-component",
49 | "component",
50 | "components",
51 | "ui",
52 | "css",
53 | "uikit",
54 | "react-ui",
55 | "framework"
56 | ],
57 | "peerDependencies": {
58 | "react": ">=16.9.0",
59 | "react-dom": ">=16.9.0"
60 | },
61 | "dependencies": {
62 | "perfect-freehand": "^1.2.0"
63 | },
64 | "devDependencies": {
65 | "@babel/runtime": "^7.18.9",
66 | "@types/react": "^18.0.17",
67 | "@types/react-dom": "^18.0.6",
68 | "react": "^18.2.0",
69 | "react-dom": "^18.2.0"
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/core/src/Paths.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } 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 | return (
10 |
11 | {Object.keys(data).map((key, index) => (
12 |
13 | ))}
14 |
15 | );
16 | };
17 |
18 | type CreatePathProps = {
19 | data: number[][];
20 | keyName: string;
21 | index: number;
22 | };
23 |
24 | const CreatePath = ({ data = [], index, keyName }: CreatePathProps) => {
25 | const { renderPath, container, ...options } = useOptionStore();
26 | const stroke = getStroke(data, options);
27 | const pathData = getSvgPathFromStroke(stroke);
28 | const dom = renderPath ? renderPath(pathData, keyName, data, index, container as unknown as SVGSVGElement) : null;
29 | if (dom) return dom;
30 | return ;
31 | };
32 |
--------------------------------------------------------------------------------
/core/src/Signature.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef, useId, forwardRef, useImperativeHandle } from 'react';
2 | import { getBoundingClientRect, getClinetXY, defaultStyle, useEvent } from './utils';
3 | import { useDispatch } from './store';
4 | import { SignatureRef, SignatureProps } from './';
5 |
6 | export const Signature = forwardRef>(
7 | (props, ref) => {
8 | const { className, prefixCls = 'w-signature', style, readonly = false, onPointer, children, ...others } = props;
9 | const cls = [className, prefixCls].filter(Boolean).join(' ');
10 | const $svg = useRef(null);
11 | const $path = useRef();
12 | const pointsRef = useRef();
13 | const pointCount = useRef(0);
14 | const pointId = useId();
15 | const dispatch = useDispatch();
16 | useImperativeHandle(
17 | ref,
18 | () => ({ svg: $svg.current, dispatch, clear: () => dispatch({}) }),
19 | [$svg.current, dispatch],
20 | );
21 |
22 | const handlePointerDown = useEvent((e) => {
23 | if (readonly) return;
24 | pointCount.current += 1;
25 | const { offsetY, offsetX } = getBoundingClientRect($svg.current);
26 | const evn = e as unknown as React.PointerEvent;
27 | const clientX = evn.clientX || evn.nativeEvent.clientX;
28 | const clientY = evn.clientY || evn.nativeEvent.clientY;
29 | pointsRef.current = [[clientX - offsetX, clientY - offsetY]];
30 | const pathElm = document.createElementNS('http://www.w3.org/2000/svg', 'path');
31 | $path.current = pathElm;
32 | $svg.current!.appendChild(pathElm);
33 | dispatch({
34 | [pointId + pointCount.current]: pointsRef.current,
35 | });
36 | document.addEventListener('pointermove', handlePointerMove);
37 | });
38 |
39 | const handlePointerMove = useEvent((e: PointerEvent) => {
40 | if ($path.current) {
41 | const { offsetY, offsetX } = getBoundingClientRect($svg.current);
42 | const { clientX, clientY } = getClinetXY(e);
43 | pointsRef.current = [...pointsRef.current!, [clientX - offsetX, clientY - offsetY]];
44 | dispatch({
45 | [pointId + pointCount.current]: pointsRef.current,
46 | });
47 | }
48 | });
49 |
50 | const handlePointerUp = useEvent(() => {
51 | let result = pointsRef.current || [];
52 | onPointer && props.onPointer!(result);
53 | $path.current = undefined;
54 | pointsRef.current = undefined;
55 | document.removeEventListener('pointermove', handlePointerMove);
56 | });
57 |
58 | useEffect(() => {
59 | document.addEventListener('pointerup', handlePointerUp);
60 | $svg.current?.addEventListener('pointerdown', handlePointerDown);
61 | return () => {
62 | document.removeEventListener('pointerup', handlePointerUp);
63 | $svg.current?.removeEventListener('pointerdown', handlePointerDown);
64 | };
65 | }, []);
66 |
67 | const svgStyle: React.CSSProperties = { ...defaultStyle, ...style };
68 |
69 | return (
70 |
73 | );
74 | },
75 | );
76 |
--------------------------------------------------------------------------------
/core/src/canvas/Paths.tsx:
--------------------------------------------------------------------------------
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 | };
31 |
--------------------------------------------------------------------------------
/core/src/canvas/Signature.tsx:
--------------------------------------------------------------------------------
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((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(null);
28 | const $path = useRef();
29 | const pointsRef = useRef();
30 | const pointCount = useRef(0);
31 | const pointId = useId();
32 | const dispatch = useDispatch();
33 | const dispatchOption = useOptionDispatch();
34 | useImperativeHandle(
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) => {
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 |
104 | );
105 | });
106 |
--------------------------------------------------------------------------------
/core/src/canvas/index.tsx:
--------------------------------------------------------------------------------
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 {
20 | prefixCls?: string;
21 | options?: StrokeOptions;
22 | readonly?: boolean;
23 | defaultPoints?: Record;
24 | onPointer?: (points: number[][]) => void;
25 | }
26 |
27 | const Signature = forwardRef(
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 |
34 |
35 |
36 |
37 |
38 | {children}
39 |
40 |
41 |
42 |
43 |
44 |
45 | );
46 | },
47 | );
48 |
49 | export default Signature;
50 |
--------------------------------------------------------------------------------
/core/src/index.tsx:
--------------------------------------------------------------------------------
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 SignatureRef = {
14 | svg: SVGSVGElement | null;
15 | dispatch: Dispatch;
16 | clear: () => void;
17 | };
18 |
19 | export interface SignatureProps extends React.SVGProps {
20 | prefixCls?: string;
21 | options?: StrokeOptions;
22 | readonly?: boolean;
23 | defaultPoints?: Record;
24 | renderPath?: (d: string, keyName: string, point: number[][], index: number, container: SVGSVGElement) => JSX.Element;
25 | onPointer?: (points: number[][]) => void;
26 | }
27 |
28 | const Signature = forwardRef(
29 | ({ children, options, renderPath, defaultPoints, ...props }, ref) => {
30 | const [state, dispatch] = useReducer(reducer, Object.assign({}, defaultPoints));
31 | const [stateOption, dispatchOption] = useReducer(
32 | reducerOption,
33 | Object.assign({ ...defaultOptions, renderPath }, options),
34 | );
35 | useEffect(() => dispatchOption({ ...options, renderPath }), [options, renderPath]);
36 | return (
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 | {children}
46 |
47 |
48 |
49 | );
50 | },
51 | );
52 |
53 | export default Signature;
54 |
--------------------------------------------------------------------------------
/core/src/options.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { type StrokeOptions } from 'perfect-freehand';
3 | import { SignatureProps } from './';
4 |
5 | export const defaultOptions: InitialOptionState = {
6 | size: 6,
7 | smoothing: 0.46,
8 | thinning: 0.73,
9 | streamline: 0.5,
10 | easing: (t) => t,
11 | start: {
12 | taper: 0,
13 | easing: (t) => t,
14 | cap: true,
15 | },
16 | end: {
17 | taper: 0,
18 | easing: (t) => t,
19 | cap: true,
20 | },
21 | };
22 |
23 | export const OptionContext = createContext(defaultOptions);
24 | export const OptionDispatchContext = createContext(() => {});
25 |
26 | type Dispatch = React.Dispatch;
27 | type InitialOptionState = StrokeOptions & {
28 | renderPath?: SignatureProps['renderPath'];
29 | container?: HTMLElement;
30 | };
31 |
32 | export function reducerOption(tasks: InitialOptionState, action: InitialOptionState) {
33 | return { ...tasks, ...action };
34 | }
35 |
36 | export const useOptionStore = () => {
37 | return useContext(OptionContext);
38 | };
39 |
40 | export const useOptionDispatch = () => {
41 | return useContext(OptionDispatchContext);
42 | };
43 |
--------------------------------------------------------------------------------
/core/src/store.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 |
3 | export const initialState: InitialState = {};
4 |
5 | export const PointerContext = createContext(initialState);
6 | export const PointerDispatchContext = createContext(() => {});
7 |
8 | export type Dispatch = React.Dispatch;
9 | type InitialState = Record;
10 |
11 | export function reducer(tasks: InitialState, action: InitialState) {
12 | if (action && Object.keys(action).length === 0) return initialState;
13 | return { ...tasks, ...action };
14 | }
15 |
16 | export const useStore = () => {
17 | return useContext(PointerContext);
18 | };
19 |
20 | export const useDispatch = () => {
21 | return useContext(PointerDispatchContext);
22 | };
23 |
--------------------------------------------------------------------------------
/core/src/utils.ts:
--------------------------------------------------------------------------------
1 | import { useCallback, useEffect, useRef } from 'react';
2 |
3 | /**
4 | * Turn the points returned from perfect-freehand into SVG path data.
5 | */
6 | export function getSvgPathFromStroke(stroke: number[][]) {
7 | if (!stroke.length) return '';
8 | const d = stroke.reduce(
9 | (acc, [x0, y0], i, arr) => {
10 | const [x1, y1] = arr[(i + 1) % arr.length];
11 | acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2);
12 | return acc;
13 | },
14 | ['M', ...stroke[0], 'Q'],
15 | );
16 |
17 | d.push('Z');
18 | return d.join(' ');
19 | }
20 |
21 | export const getBoundingClientRect = (el: T | null) => {
22 | const rect = el?.getBoundingClientRect();
23 | const offsetX = rect?.left || 0;
24 | const offsetY = rect?.top || 0;
25 | return { offsetX, offsetY };
26 | };
27 |
28 | export const getClinetXY = ({ clientX, clientY }: PointerEvent) => {
29 | return { clientX, clientY };
30 | };
31 |
32 | export const defaultStyle: React.CSSProperties = {
33 | '--w-signature-background': '#fff',
34 | touchAction: 'none',
35 | position: 'relative',
36 | width: '100%',
37 | height: '100%',
38 | backgroundColor: 'var(--w-signature-background)',
39 | } as React.CSSProperties;
40 |
41 | // Saves incoming handler to the ref in order to avoid "useCallback hell"
42 | export function useEvent(handler?: (event: K) => void): (event: K) => void {
43 | const callbackRef = useRef(handler);
44 |
45 | useEffect(() => {
46 | callbackRef.current = handler;
47 | });
48 |
49 | return useCallback((event: K) => callbackRef.current && callbackRef.current(event), []);
50 | }
51 |
--------------------------------------------------------------------------------
/core/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "include": ["src"],
4 | "compilerOptions": {
5 | "outDir": "./cjs",
6 | "baseUrl": ".",
7 | "noEmit": false
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/lerna.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.3.2",
3 | "packages": ["website", "core"]
4 | }
5 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "private": true,
3 | "scripts": {
4 | "⬇️⬇️⬇️⬇️⬇️ package ⬇️⬇️⬇️⬇️⬇️": "▼▼▼▼▼ package ▼▼▼▼▼",
5 | "build": "lerna exec --scope @uiw/* -- tsbb build src/*.tsx --use-babel --cjs cjs --bail",
6 | "watch": "lerna exec \"tsbb watch src/*.tsx --use-babel --cjs cjs\" --scope @uiw/*",
7 | "bundle": "lerna exec --scope @uiw/* -- ncc build src/index.tsx --target web --filename dist",
8 | "bundle:min": "lerna exec --scope @uiw/* -- ncc build src/index.tsx --target web --filename dist --minify",
9 | "⬆️⬆️⬆️⬆️⬆️ package ⬆️⬆️⬆️⬆️⬆️": "▲▲▲▲▲ package ▲▲▲▲▲",
10 | "start": "lerna exec --scope website -- npm run start",
11 | "doc": "lerna exec --scope website -- npm run build",
12 | "bootstrap": "lerna bootstrap",
13 | "hoist": "lerna bootstrap --hoist",
14 | "test": "tsbb test",
15 | "coverage": "tsbb test --coverage --bail",
16 | "prepare": "husky install",
17 | "version": "lerna version --exact --force-publish --no-push --no-git-tag-version",
18 | "prettier": "prettier --write '**/*.{js,jsx,tsx,ts,less,md,json}'",
19 | "remove": "npm run clean && lerna exec \"rm -rf package-lock.json\" --scope @uiw/* --scope website",
20 | "clean": "lerna clean --yes"
21 | },
22 | "lint-staged": {
23 | "*.{js,jsx,ts,tsx,less,md,json}": [
24 | "prettier --write"
25 | ]
26 | },
27 | "jest": {
28 | "setupFiles": ["jest-canvas-mock"],
29 | "collectCoverageFrom": [
30 | "/core/src/**/*.{js,jsx,ts,tsx}"
31 | ],
32 | "testMatch": [
33 | "/test/*.{ts,tsx}"
34 | ],
35 | "transformIgnorePatterns": [
36 | "/node_modules/?!(.*)"
37 | ]
38 | },
39 | "workspaces": [
40 | "website",
41 | "core"
42 | ],
43 | "engines": {
44 | "node": ">=16.0.0"
45 | },
46 | "devDependencies": {
47 | "@kkt/less-modules": "^7.5.4",
48 | "@kkt/ncc": "^1.1.1",
49 | "@types/react-test-renderer": "^18.0.0",
50 | "compile-less-cli": "^1.8.13",
51 | "husky": "^8.0.1",
52 | "lerna": "^8.0.0",
53 | "lint-staged": "^15.0.0",
54 | "prettier": "^3.0.1",
55 | "react-test-renderer": "^18.2.0",
56 | "tsbb": "^4.5.1"
57 | },
58 | "dependencies": {
59 | "jest-canvas-mock": "^2.5.2"
60 | }
61 | }
62 |
--------------------------------------------------------------------------------
/renovate.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3 | "extends": ["config:base"],
4 | "packageRules": [
5 | {
6 | "matchPackagePatterns": ["*"],
7 | "rangeStrategy": "replace"
8 | }
9 | ]
10 | }
11 |
--------------------------------------------------------------------------------
/test/canvas.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React, { useRef } from 'react';
4 | import Signature from '../core/src/canvas';
5 |
6 | it('renders empty path test case', () => {
7 | render(
8 |
9 | );
10 | const svg = screen.getByTestId('keyname');
11 | expect(svg.tagName).toEqual('CANVAS');
12 | expect(svg.style).toHaveProperty('position', 'relative');
13 | expect(svg.getAttribute('width')).toEqual('450');
14 | expect(svg.getAttribute('height')).toEqual('230');
15 | expect(svg.getAttribute('class')).toEqual('w-signature');
16 |
17 | });
18 |
--------------------------------------------------------------------------------
/test/index.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, screen, fireEvent } from '@testing-library/react';
2 | import '@testing-library/jest-dom';
3 | import React, { useRef } from 'react';
4 | import Signature, { SignatureRef } from '../core/src';
5 |
6 | const points = {
7 | "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]],
8 | "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]]
9 | }
10 |
11 | test('renders learn react link', () => {
12 | render(learn react);
13 | const linkElement = screen.getByText(/learn react/i);
14 | expect(linkElement).toBeInTheDocument();
15 | });
16 |
17 | it('renders defaultPoints props test case', () => {
18 | const { debug } = render(
19 |
20 | );
21 | const copied = screen.getByTestId('keyname');
22 | expect(copied.tagName).toEqual('svg');
23 | expect(copied.style).toHaveProperty('height', '100%');
24 | expect(copied.style).toHaveProperty('width', '100%');
25 | expect(copied.style).toHaveProperty('position', 'relative');
26 | expect(copied.getAttribute('width')).toEqual('450');
27 | expect(copied.getAttribute('height')).toEqual('230');
28 | expect(copied.getAttribute('class')).toEqual('w-signature');
29 |
30 | if (copied.firstChild) {
31 | const path = copied.firstChild as SVGPathElement;
32 | expect(path.tagName).toEqual('path');
33 | const lastChild = copied.lastChild as SVGPathElement;
34 | expect(lastChild.tagName).toEqual('path');
35 | }
36 | });
37 |
38 |
39 | it('renders empty path test case', () => {
40 | render(
41 |
42 | );
43 | const svg = screen.getByTestId('keyname');
44 | expect(svg.tagName).toEqual('svg');
45 | expect(svg.style).toHaveProperty('height', '100%');
46 | expect(svg.style).toHaveProperty('width', '100%');
47 | expect(svg.style).toHaveProperty('position', 'relative');
48 | expect(svg.getAttribute('width')).toEqual('450');
49 | expect(svg.getAttribute('height')).toEqual('230');
50 | expect(svg.getAttribute('class')).toEqual('w-signature');
51 |
52 | const path = svg.firstChild as SVGPathElement;
53 | expect(path).toBeNull();
54 | });
55 |
56 |
57 | it('renders clear test case', () => {
58 | function App() {
59 | const $svg = useRef(null);
60 | const handle = (evn: React.MouseEvent) => {
61 | $svg.current?.clear()
62 | };
63 | return (
64 | <>
65 |
66 |
67 | >
68 | );
69 | }
70 | render();
71 |
72 | const svg = screen.getByTestId("signature");
73 | const clearButton = screen.getByText("Clear");
74 |
75 | // Ensure the Signature component is rendered
76 | expect(svg).toBeInTheDocument();
77 |
78 | const path = svg.firstChild as SVGPathElement;
79 | expect(path).toBeInTheDocument();
80 |
81 | // Simulate a click on the Clear button
82 | fireEvent.click(clearButton);
83 | const path2 = svg.firstChild as SVGPathElement;
84 | expect(path2).toBeNull();
85 |
86 | });
87 |
88 |
89 | it('renders clear test case', () => {
90 | const { debug } = render();
91 |
92 | const svg = screen.getByTestId("signature");
93 | // Ensure the Signature component is rendered
94 | expect(svg).toBeInTheDocument();
95 |
96 | // Create a mock event object
97 | const mockEvent = new MouseEvent('pointerup', {
98 | bubbles: true,
99 | cancelable: true,
100 | clientX: 100, // Set clientX to a desired value
101 | clientY: 200, // Set clientY to a desired value
102 | // Add other properties as needed
103 | });
104 | fireEvent(svg, mockEvent);
105 |
106 | // Create a mock event object
107 | const moveMockEvent = new MouseEvent('pointermove', {
108 | bubbles: true,
109 | cancelable: true,
110 | clientX: 150, // Set clientX to a desired value
111 | clientY: 250, // Set clientY to a desired value
112 | // Add other properties as needed
113 | });
114 |
115 | // Trigger a pointermove event with the mock event object
116 | fireEvent(svg, moveMockEvent);
117 |
118 | });
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "jsx": "react",
4 | "target": "esnext",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "resolveJsonModule": true,
15 | "isolatedModules": true,
16 | "declaration": true,
17 | "baseUrl": ".",
18 | "noFallthroughCasesInSwitch": true,
19 | "noEmit": true
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/website/.kktrc.ts:
--------------------------------------------------------------------------------
1 | import webpack from 'webpack';
2 | import { WebpackConfiguration, LoaderConfOptions } from 'kkt';
3 | import { mdCodeModulesLoader } from 'markdown-react-code-preview-loader';
4 | import pkg from './package.json';
5 |
6 | export default (conf: WebpackConfiguration, env: 'development' | 'production', options: LoaderConfOptions) => {
7 | conf = mdCodeModulesLoader(conf);
8 | conf.plugins!.push(
9 | new webpack.DefinePlugin({
10 | VERSION: JSON.stringify(pkg.version),
11 | }),
12 | );
13 |
14 | conf.module!.exprContextCritical = false;
15 | /**
16 | * fix failed to parse source map issue
17 | * https://github.com/kktjs/kkt/issues/446
18 | */
19 | conf.ignoreWarnings = [{ module: /node_modules[\\/]parse5[\\/]/ }];
20 | if (env === 'production') {
21 | conf.output = { ...conf.output, publicPath: './' };
22 | conf.optimization = {
23 | ...conf.optimization,
24 | splitChunks: {
25 | cacheGroups: {
26 | reactvendor: {
27 | test: /[\\/]node_modules[\\/](react|react-dom)[\\/]/,
28 | name: 'react-vendor',
29 | chunks: 'all',
30 | },
31 | refractor: {
32 | test: /[\\/]node_modules[\\/](refractor)[\\/]/,
33 | name: 'refractor-prismjs-vendor',
34 | chunks: 'all',
35 | },
36 | },
37 | },
38 | };
39 | }
40 |
41 | return conf;
42 | };
43 |
--------------------------------------------------------------------------------
/website/README.md:
--------------------------------------------------------------------------------
1 | Document Website
2 | ===
3 |
4 | https://uiwjs.github.io/react-signature
--------------------------------------------------------------------------------
/website/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "website",
3 | "version": "1.3.2",
4 | "private": true,
5 | "scripts": {
6 | "start": "kkt start",
7 | "build": "kkt build"
8 | },
9 | "license": "MIT",
10 | "dependencies": {
11 | "@uiw/copy-to-clipboard": "^1.0.16",
12 | "@uiw/react-markdown-preview-example": "^2.0.0",
13 | "@uiw/react-signature": "1.3.2",
14 | "react": "^18.2.0",
15 | "react-dom": "^18.2.0"
16 | },
17 | "devDependencies": {
18 | "@types/react": "^18.0.17",
19 | "@types/react-dom": "^18.0.6",
20 | "kkt": "^7.5.4",
21 | "markdown-react-code-preview-loader": "^2.1.2"
22 | },
23 | "eslintConfig": {
24 | "extends": [
25 | "react-app",
26 | "react-app/jest"
27 | ]
28 | },
29 | "browserslist": {
30 | "production": [
31 | ">0.2%",
32 | "not dead",
33 | "not op_mini all"
34 | ],
35 | "development": [
36 | "last 1 chrome version",
37 | "last 1 firefox version",
38 | "last 1 safari version"
39 | ]
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/website/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/uiwjs/react-signature/52c29637925dd5520714d756a940dff456db83da/website/public/favicon.ico
--------------------------------------------------------------------------------
/website/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | react-signature
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
24 |
25 |
--------------------------------------------------------------------------------
/website/src/Example.tsx:
--------------------------------------------------------------------------------
1 | import { useRef, useState } from 'react';
2 | import Signature, { defaultOptions, type StrokeOptions, type SignatureRef } from '@uiw/react-signature';
3 | import copyTextToClipboard from '@uiw/copy-to-clipboard';
4 | import styled from 'styled-components';
5 |
6 | const Wrapper = styled.div`
7 | border-radius: 4px;
8 | flex: 1;
9 | `;
10 |
11 | const points1 = [
12 | [79.51953125, 53.6640625],
13 | [80.234375, 53.08984375],
14 | [82.796875, 51.59375],
15 | [86.5, 50.140625],
16 | [90.828125, 49.328125],
17 | [97.3203125, 48.8671875],
18 | [104.25390625, 49.52734375],
19 | [112.109375, 52.39453125],
20 | [120.70703125, 58.33203125],
21 | [126.0234375, 66.8828125],
22 | [126.30859375, 80.859375],
23 | [121.109375, 98.12109375],
24 | [111.484375, 112.98046875],
25 | [101.30078125, 122.02734375],
26 | [91.9765625, 125.265625],
27 | [85.08203125, 125.6796875],
28 | [81.6640625, 124.00390625],
29 | [80.19921875, 120.87890625],
30 | [80.6953125, 115.671875],
31 | [84.8125, 106.984375],
32 | [92.9921875, 97.24609375],
33 | [105.1640625, 90.71875],
34 | [120.99609375, 87.29296875],
35 | [136.98046875, 87.63671875],
36 | [149.97265625, 90.87109375],
37 | [155.30078125, 94.56640625],
38 | [155.94921875, 99.23828125],
39 | [155.328125, 104.59765625],
40 | [151.8046875, 111.31640625],
41 | [146.5625, 117.6015625],
42 | [140.90234375, 121.62890625],
43 | [137.8515625, 123.453125],
44 | [137.328125, 123.40625],
45 | [136.95703125, 122.9453125],
46 | [137.13671875, 121.32421875],
47 | [140.44921875, 115.890625],
48 | [148.14453125, 106.15234375],
49 | [157.15625, 96.91796875],
50 | [173.70703125, 83.3203125],
51 | [191.55859375, 70.1015625],
52 | [199.70703125, 65.35546875],
53 | [203.46875, 63.515625],
54 | [204.6484375, 63.234375],
55 | [204.1796875, 64.6171875],
56 | [202.94921875, 67.875],
57 | [199.71875, 74.88671875],
58 | [192.64453125, 88.46875],
59 | [184.30078125, 105.28125],
60 | [178.7421875, 116.85546875],
61 | [175.46484375, 121.59765625],
62 | [173.140625, 122.93359375],
63 | [171.39453125, 122.44140625],
64 | [170.09765625, 119.9375],
65 | [169.9375, 112.65234375],
66 | [173.2265625, 99.640625],
67 | [182.1953125, 82.4453125],
68 | [193.19140625, 67.78515625],
69 | [203.48046875, 58.6953125],
70 | [212.25, 53.43359375],
71 | [219.55078125, 51.18359375],
72 | [224.8046875, 50.40234375],
73 | [227.41015625, 50.87890625],
74 | [229.0859375, 52.3984375],
75 | [230.19140625, 55.34765625],
76 | [231.05078125, 60.60546875],
77 | [231.3515625, 67.671875],
78 | [230.57421875, 73.42578125],
79 | [228.1953125, 78.00390625],
80 | [224.86328125, 80.7265625],
81 | [221.46875, 81.40234375],
82 | [218.390625, 79.51953125],
83 | [212.5859375, 72.31640625],
84 | [202.6640625, 61.04296875],
85 | [190.8125, 47.4296875],
86 | [182.90234375, 37.1953125],
87 | [180.26171875, 32.00390625],
88 | [179.12890625, 29.2421875],
89 | [179.1796875, 28.84765625],
90 | [179.921875, 29.28125],
91 | [181.41015625, 30.5234375],
92 | [185.00390625, 33.3671875],
93 | [193.6015625, 39.0859375],
94 | [213.0859375, 51.1328125],
95 | [249.11328125, 72.74609375],
96 | [296.953125, 102.0859375],
97 | [342.8203125, 130.1328125],
98 | [381.08984375, 152.51171875],
99 | [408, 167.734375],
100 | [419.70703125, 173.66796875],
101 | [422.6171875, 174.94921875],
102 | [423.01953125, 175.1484375],
103 | ];
104 |
105 | const points2 = [
106 | [277.48828125, 62.6015625],
107 | [277.4453125, 62.64453125],
108 | [276.41015625, 64.51953125],
109 | [269.54296875, 74.9921875],
110 | [253.75, 96.70703125],
111 | [232.1171875, 122.76171875],
112 | [214.55859375, 140.99609375],
113 | [206.01171875, 147.85546875],
114 | [201.65625, 150.41015625],
115 | [200.0078125, 151.3125],
116 | [200.0078125, 151.2265625],
117 | [200.15625, 151.0234375],
118 | ];
119 |
120 | const points = { points1, points2 };
121 |
122 | export const ExampleSignature = () => {
123 | const $svg = useRef(null);
124 | const [options, setOptions] = useState(defaultOptions);
125 | const handle = (evn: React.MouseEvent) => {
126 | $svg.current?.clear();
127 | };
128 |
129 | const resetOption = () => setOptions(defaultOptions);
130 | const handleCopy = () => copyTextToClipboard(JSON.stringify(options, null, 2));
131 | const handleSVGCopy = () => {
132 | const svgelm = $svg.current?.svg?.cloneNode(true) as SVGSVGElement;
133 | const clientWidth = $svg.current?.svg?.clientWidth;
134 | const clientHeight = $svg.current?.svg?.clientHeight;
135 | svgelm.removeAttribute('style');
136 | svgelm.setAttribute('width', `${clientWidth}px`);
137 | svgelm.setAttribute('height', `${clientHeight}px`);
138 | svgelm.setAttribute('viewbox', `${clientWidth} ${clientHeight}`);
139 | copyTextToClipboard(svgelm.outerHTML);
140 | };
141 |
142 | const downloadImage = () => {
143 | const svgelm = $svg.current?.svg?.cloneNode(true) as SVGSVGElement;
144 | const clientWidth = $svg.current?.svg?.clientWidth;
145 | const clientHeight = $svg.current?.svg?.clientHeight;
146 | svgelm.removeAttribute('style');
147 | svgelm.setAttribute('width', `${clientWidth}px`);
148 | svgelm.setAttribute('height', `${clientHeight}px`);
149 | svgelm.setAttribute('viewbox', `${clientWidth} ${clientHeight}`);
150 | const data = new XMLSerializer().serializeToString(svgelm);
151 | const canvas = document.createElement('canvas');
152 | const ctx = canvas.getContext('2d');
153 | const img = new Image();
154 | img.onload = () => {
155 | canvas.width = clientWidth || 0;
156 | canvas.height = clientHeight || 0;
157 | ctx?.drawImage(img, 0, 0);
158 | const a = document.createElement('a');
159 | a.download = 'signature.png';
160 | a.href = canvas.toDataURL('image/png');
161 | a.click();
162 | };
163 | img.src = `data:image/svg+xml;base64,${window.btoa(unescape(encodeURIComponent(data)))}`;
164 | };
165 |
166 | return (
167 |
168 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
246 |
247 | );
248 | };
249 |
--------------------------------------------------------------------------------
/website/src/index.tsx:
--------------------------------------------------------------------------------
1 | import { createRoot } from 'react-dom/client';
2 | import MarkdownPreviewExample from '@uiw/react-markdown-preview-example';
3 | import pkg from '@uiw/react-signature/package.json';
4 | import data from '@uiw/react-signature/README.md';
5 | import { ExampleSignature } from './Example';
6 |
7 | const Github = MarkdownPreviewExample.Github;
8 | const Example = MarkdownPreviewExample.Example;
9 |
10 | const container = document.getElementById('root');
11 | const root = createRoot(container!);
12 | root.render(
13 |
21 |
25 | Sponsor
26 | ,
27 | ]}
28 | />
29 |
30 |
31 |
32 |
33 | ,
34 | );
35 |
--------------------------------------------------------------------------------
/website/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.less' {
4 | const classes: { readonly [key: string]: string };
5 | export default classes;
6 | }
7 |
8 | declare var VERSION: string;
9 |
10 | declare module '*.md' {
11 | import { CodeBlockData } from 'markdown-react-code-preview-loader';
12 | const src: CodeBlockData;
13 | export default src;
14 | }
15 |
--------------------------------------------------------------------------------
/website/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "../tsconfig",
3 | "include": [".kktrc.ts", "src"],
4 | "compilerOptions": {
5 | "jsx": "react-jsx",
6 | "baseUrl": "./src",
7 | "noEmit": true
8 | }
9 | }
10 |
--------------------------------------------------------------------------------