├── .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 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](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 | [![Buy me a coffee](https://img.shields.io/badge/Buy%20me%20a%20coffee-048754?logo=buymeacoffee)](https://jaywcjlove.github.io/#/sponsor) 5 | [![Build & Deploy](https://github.com/uiwjs/react-signature/actions/workflows/ci.yml/badge.svg)](https://github.com/uiwjs/react-signature/actions/workflows/ci.yml) 6 | [![Coverage Status](https://uiwjs.github.io/react-signature/badges.svg)](https://uiwjs.github.io/react-signature/lcov-report/) 7 | [![npm version](https://img.shields.io/npm/v/@uiw/react-signature.svg)](https://www.npmjs.com/package/@uiw/react-signature) 8 | 9 | A signature board component for react. 10 | 11 | [![react-signature](https://github.com/uiwjs/react-signature/assets/1680273/85bdb3d3-dfe5-4dc5-a95b-72cde1a9a10c)](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 | 71 | {children} 72 | 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 | 102 | {children} 103 | 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 |
182 | 197 | 213 | 229 | 245 |
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 | --------------------------------------------------------------------------------