├── .npmrc
├── vitest.setup.ts
├── pnpm-workspace.yaml
├── playground
├── src
│ ├── vite-env.d.ts
│ ├── assets
│ │ ├── logo.png
│ │ └── style.css
│ ├── main.ts
│ ├── components
│ │ └── Nav.vue
│ └── App.vue
├── .gitignore
├── public
│ └── favicon.ico
├── postcss.config.js
├── tailwind.config.js
├── tsconfig.node.json
├── vite.config.ts
├── index.html
├── package.json
├── tsconfig.json
└── README.md
├── eslint.config.js
├── src
├── index.ts
├── utils
│ ├── constants.ts
│ ├── get-svg-path-from-stroke.ts
│ └── h-demi.ts
└── components
│ └── VPerfectSignature.ts
├── tsup.config.ts
├── .gitignore
├── tsconfig.json
├── vitest.config.ts
├── LICENSE.md
├── package.json
├── test
├── VPerfectSignature.test.ts
└── mock.ts
└── README.md
/.npmrc:
--------------------------------------------------------------------------------
1 | ignore-workspace-root-check=true
2 |
--------------------------------------------------------------------------------
/vitest.setup.ts:
--------------------------------------------------------------------------------
1 | import 'vitest-canvas-mock'
2 |
--------------------------------------------------------------------------------
/pnpm-workspace.yaml:
--------------------------------------------------------------------------------
1 | packages:
2 | - playground
3 |
--------------------------------------------------------------------------------
/playground/src/vite-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/playground/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .DS_Store
3 | dist
4 | dist-ssr
5 | *.local
6 |
--------------------------------------------------------------------------------
/eslint.config.js:
--------------------------------------------------------------------------------
1 | import antfu from '@antfu/eslint-config'
2 |
3 | export default antfu()
4 |
--------------------------------------------------------------------------------
/playground/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wobsoriano/v-perfect-signature/HEAD/playground/public/favicon.ico
--------------------------------------------------------------------------------
/playground/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/wobsoriano/v-perfect-signature/HEAD/playground/src/assets/logo.png
--------------------------------------------------------------------------------
/playground/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/playground/src/assets/style.css:
--------------------------------------------------------------------------------
1 | @import 'tailwindcss/base';
2 | @import 'tailwindcss/components';
3 | @import 'tailwindcss/utilities';
--------------------------------------------------------------------------------
/playground/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp } from 'vue'
2 | import './assets/style.css'
3 |
4 | import App from './App.vue'
5 |
6 | createApp(App).mount('#app')
7 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | import VPerfectSignature from './components/VPerfectSignature'
2 |
3 | export {
4 | VPerfectSignature,
5 | }
6 |
7 | export default VPerfectSignature
8 |
--------------------------------------------------------------------------------
/playground/tailwind.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | mode: 'jit',
3 | content: ['./index.html', './src/**/*.{js,ts,vue}'],
4 | darkMode: 'class',
5 | variants: {},
6 | }
7 |
--------------------------------------------------------------------------------
/src/utils/constants.ts:
--------------------------------------------------------------------------------
1 | export const DEFAULT_BACKGROUND_COLOR = 'rgba(0,0,0,0)'
2 | export const DEFAULT_PEN_COLOR = '#000'
3 | export const DEFAULT_WIDTH = '100%'
4 | export const DEFAULT_HEIGHT = '100%'
5 | export const IMAGE_TYPES = ['image/png', 'image/jpeg', 'image/svg+xml']
6 |
--------------------------------------------------------------------------------
/playground/tsconfig.node.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "composite": true,
4 | "module": "ESNext",
5 | "moduleResolution": "bundler",
6 | "allowSyntheticDefaultImports": true,
7 | "skipLibCheck": true
8 | },
9 | "include": ["vite.config.ts"]
10 | }
11 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'tsup'
2 |
3 | export default defineConfig({
4 | entry: ['src/index.ts'],
5 | splitting: false,
6 | sourcemap: true,
7 | clean: true,
8 | format: ['cjs', 'esm', 'iife'],
9 | globalName: 'VPerfectSignature',
10 | dts: true,
11 | })
12 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /dist
4 |
5 |
6 | # local env files
7 | .env.local
8 | .env.*.local
9 |
10 | # Log files
11 | npm-debug.log*
12 | yarn-debug.log*
13 | yarn-error.log*
14 | pnpm-debug.log*
15 |
16 | # Editor directories and files
17 | .idea
18 | .vscode
19 | *.suo
20 | *.ntvs*
21 | *.njsproj
22 | *.sln
23 | *.sw?
--------------------------------------------------------------------------------
/playground/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [vue()],
7 | // eslint-disable-next-line node/prefer-global/process
8 | base: process.env.NODE_ENV === 'production' ? '/v-perfect-signature/' : './',
9 | })
10 |
--------------------------------------------------------------------------------
/playground/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | v-perfect-signature
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/utils/get-svg-path-from-stroke.ts:
--------------------------------------------------------------------------------
1 | export default function getSvgPathFromStroke(stroke: number[][]) {
2 | if (!stroke.length)
3 | return ''
4 |
5 | const d = stroke.reduce(
6 | (acc, [x0, y0], i, arr) => {
7 | const [x1, y1] = arr[(i + 1) % arr.length]
8 | acc.push(x0, y0, (x0 + x1) / 2, (y0 + y1) / 2)
9 | return acc
10 | },
11 | ['M', ...stroke[0], 'Q'],
12 | )
13 |
14 | d.push('Z')
15 | return d.join(' ')
16 | }
17 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es2020",
4 | "baseUrl": ".",
5 | "module": "esnext",
6 | "moduleResolution": "node",
7 | "resolveJsonModule": true,
8 | "types": ["vitest/globals"],
9 | "allowJs": true,
10 | "strict": true,
11 | "noImplicitAny": true,
12 | "noUnusedLocals": true,
13 | "noEmit": true,
14 | "outDir": "dist",
15 | "esModuleInterop": true,
16 | "skipLibCheck": true
17 | },
18 | "include": [
19 | "src/**/*.ts",
20 | "src/**/*.vue",
21 | "shims-vue.d.ts"
22 | ],
23 | "exclude": [
24 | "node_modules"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------
/playground/src/components/Nav.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vitest/config'
2 |
3 | export default defineConfig({
4 | test: {
5 | setupFiles: ['./vitest.setup.ts'],
6 | environment: 'jsdom',
7 | deps: {
8 | optimizer: {
9 | web: {
10 | include: ['vitest-canvas-mock'],
11 | },
12 | },
13 | },
14 | // For this config, check https://github.com/vitest-dev/vitest/issues/740
15 | poolOptions: {
16 | threads: {
17 | singleThread: true,
18 | },
19 | },
20 | environmentOptions: {
21 | jsdom: {
22 | resources: 'usable',
23 | },
24 | },
25 | },
26 | })
27 |
--------------------------------------------------------------------------------
/playground/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "playground",
3 | "type": "module",
4 | "version": "0.0.0",
5 | "scripts": {
6 | "dev": "vite",
7 | "build": "vue-tsc --noEmit && vite build",
8 | "serve": "vite preview",
9 | "deploy": "gh-pages -d dist",
10 | "clean": "rm -rf node_modules && rm -rf dist"
11 | },
12 | "dependencies": {
13 | "v-perfect-signature": "*",
14 | "vue": "^3.3.11"
15 | },
16 | "devDependencies": {
17 | "@types/node": "^20.10.4",
18 | "@vitejs/plugin-vue": "^4.5.2",
19 | "autoprefixer": "^10.4.16",
20 | "gh-pages": "^6.1.0",
21 | "postcss": "^8.4.32",
22 | "tailwindcss": "^3.3.6",
23 | "typescript": "^5.3.3",
24 | "vite": "^5.0.8",
25 | "vue-tsc": "^1.8.25"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/playground/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "ES2020",
4 | "jsx": "preserve",
5 | "lib": ["ES2020", "DOM", "DOM.Iterable"],
6 | "useDefineForClassFields": true,
7 | "module": "ESNext",
8 |
9 | /* Bundler mode */
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "allowImportingTsExtensions": true,
13 |
14 | /* Linting */
15 | "strict": true,
16 | "noFallthroughCasesInSwitch": true,
17 | "noUnusedLocals": true,
18 | "noUnusedParameters": true,
19 | "noEmit": true,
20 | "isolatedModules": true,
21 | "skipLibCheck": true
22 | },
23 | "references": [{ "path": "./tsconfig.node.json" }],
24 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
25 | }
26 |
--------------------------------------------------------------------------------
/src/utils/h-demi.ts:
--------------------------------------------------------------------------------
1 | import { h as hDemi, isVue2 } from 'vue-demi'
2 |
3 | interface Options {
4 | props?: Record
5 | domProps?: Record
6 | on?: Record
7 | }
8 |
9 | function adaptOnsV3(ons: Record) {
10 | if (!ons)
11 | return null
12 | return Object.entries(ons).reduce((ret, [key, handler]) => {
13 | key = key.charAt(0).toUpperCase() + key.slice(1)
14 | key = `on${key}`
15 | return { ...ret, [key]: handler }
16 | }, {})
17 | }
18 |
19 | function h(type: string | Record, options: Options & any = {}, children?: any) {
20 | if (isVue2)
21 | return hDemi(type, options, children)
22 |
23 | const { props, domProps, on, ...extraOptions } = options
24 |
25 | const ons = adaptOnsV3(on)
26 | const params = { ...extraOptions, ...props, ...domProps, ...ons }
27 | return hDemi(type, params, children)
28 | }
29 |
30 | export default h
31 |
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 Robert Soriano
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 |
--------------------------------------------------------------------------------
/playground/README.md:
--------------------------------------------------------------------------------
1 | # Vue 3 + Typescript + Vite
2 |
3 | This template should help get you started developing with Vue 3 and Typescript in Vite.
4 |
5 | ## Recommended IDE Setup
6 |
7 | [VSCode](https://code.visualstudio.com/) + [Vetur](https://marketplace.visualstudio.com/items?itemName=octref.vetur). Make sure to enable `vetur.experimental.templateInterpolationService` in settings!
8 |
9 | ### If Using `
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
46 |
47 |
48 |
51 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
69 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "v-perfect-signature",
3 | "type": "module",
4 | "version": "1.4.0",
5 | "description": "Perfect pressure-sensitive signature drawing for Vue 2 and 3",
6 | "license": "MIT",
7 | "homepage": "https://github.com/wobsoriano/v-perfect-signature#readme",
8 | "repository": {
9 | "type": "git",
10 | "url": "git+https://github.com/wobsoriano/v-perfect-signature.git"
11 | },
12 | "keywords": [
13 | "vue",
14 | "signature",
15 | "pad",
16 | "freehand"
17 | ],
18 | "exports": {
19 | ".": {
20 | "types": {
21 | "import": "./dist/index.d.ts",
22 | "require": "./dist/index.d.cts"
23 | },
24 | "import": "./dist/index.js",
25 | "require": "./dist/index.cjs"
26 | }
27 | },
28 | "main": "./dist/index.cjs",
29 | "module": "./dist/index.js",
30 | "unpkg": "dist/index.global.js",
31 | "jsdelivr": "dist/index.global.js",
32 | "types": "./dist/index.d.ts",
33 | "typesVersions": {
34 | "*": {
35 | "*": [
36 | "./dist/*"
37 | ]
38 | }
39 | },
40 | "files": [
41 | "dist",
42 | "nuxt.mjs"
43 | ],
44 | "scripts": {
45 | "dev": "tsup --watch --onSuccess \"pnpm --filter playground dev\"",
46 | "build": "tsup",
47 | "prepublishOnly": "pnpm run build",
48 | "test": "vitest run",
49 | "release": "bumpp && pnpm publish",
50 | "lint": "eslint .",
51 | "lint:fix": "eslint . --fix"
52 | },
53 | "peerDependencies": {
54 | "@vue/composition-api": "^1.7.1",
55 | "vue": "^2.6.14 || ^3.2.0"
56 | },
57 | "peerDependenciesMeta": {
58 | "@vue/composition-api": {
59 | "optional": true
60 | }
61 | },
62 | "dependencies": {
63 | "perfect-freehand": "^1.2.0",
64 | "vue-demi": "^0.14.6"
65 | },
66 | "devDependencies": {
67 | "@antfu/eslint-config": "^2.4.5",
68 | "@vue/test-utils": "2.4.1",
69 | "bumpp": "^9.2.1",
70 | "canvas": "^2.11.2",
71 | "eslint": "^8.55.0",
72 | "jsdom": "^23.0.1",
73 | "tslib": "^2.6.2",
74 | "tsup": "^8.0.1",
75 | "typescript": "^5.3.3",
76 | "vitest": "^1.0.4",
77 | "vitest-canvas-mock": "^0.3.3",
78 | "vue": "^3.3.11"
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/test/VPerfectSignature.test.ts:
--------------------------------------------------------------------------------
1 | import { shallowMount } from '@vue/test-utils'
2 |
3 | import { describe, expect, it, vi } from 'vitest'
4 | import VPerfectSignature from '../src'
5 | import { inputPointsMockData, mockDataURL } from './mock'
6 |
7 | describe('#props', () => {
8 | it('should receive default props', () => {
9 | const wrapper = shallowMount(VPerfectSignature)
10 |
11 | const expectedWidth = '100%'
12 | const expectedHeight = '100%'
13 | const expectedPenColor = '#000'
14 | const expectedBackgroundColor = 'rgba(0,0,0,0)'
15 | const expectedStrokeOptions = {}
16 |
17 | expect(wrapper.props().width).toBe(expectedWidth)
18 | expect(wrapper.props().height).toBe(expectedHeight)
19 | expect(wrapper.props().penColor).toBe(expectedPenColor)
20 | expect(wrapper.props().backgroundColor).toBe(expectedBackgroundColor)
21 | expect(wrapper.props().strokeOptions).toEqual(expectedStrokeOptions)
22 | })
23 | })
24 |
25 | describe('#toDataURL', () => {
26 | it('throws error if invalid type', () => {
27 | const wrapper = shallowMount(VPerfectSignature)
28 |
29 | expect(() => wrapper.vm.toDataURL('text/html')).toThrow()
30 | })
31 |
32 | it('returns undefined if pad is empty', () => {
33 | const wrapper = shallowMount(VPerfectSignature)
34 |
35 | expect(wrapper.vm.toDataURL()).toBeUndefined()
36 | })
37 |
38 | // TODO: Returns incorrect data url. Bug?
39 | it.todo('should return data uri', async () => {
40 | const wrapper = shallowMount(VPerfectSignature)
41 |
42 | await wrapper.setData({
43 | allInputPoints: inputPointsMockData,
44 | })
45 |
46 | expect(wrapper.vm.toDataURL()).toBe(mockDataURL)
47 | })
48 | })
49 |
50 | describe('#fromDataURL', () => {
51 | it('should set signature from data uri', async () => {
52 | const wrapper = shallowMount(VPerfectSignature)
53 | await expect(wrapper.vm.fromDataURL(mockDataURL)).resolves.toBe(true)
54 | })
55 |
56 | vi.spyOn(console, 'error').mockImplementation(() => {})
57 |
58 | it('fails if data uri is incorrect', async () => {
59 | const wrapper = shallowMount(VPerfectSignature)
60 |
61 | await expect(wrapper.vm.fromDataURL('random string')).rejects.toThrow(
62 | 'Incorrect data uri provided',
63 | )
64 | })
65 | })
66 |
67 | describe('#toData', () => {
68 | it('returns array of array input points', () => {
69 | const wrapper = shallowMount(VPerfectSignature)
70 |
71 | wrapper.setData({
72 | allInputPoints: inputPointsMockData,
73 | })
74 |
75 | expect(wrapper.vm.toData()).toEqual(inputPointsMockData)
76 | })
77 |
78 | it('should set signature from array of array of input points', () => {
79 | const wrapper = shallowMount(VPerfectSignature)
80 |
81 | expect(wrapper.vm.fromData(inputPointsMockData)).toBeUndefined()
82 | expect(wrapper.vm.toData()).toEqual(inputPointsMockData)
83 | })
84 | })
85 |
86 | describe('#clear', () => {
87 | it('clears data structures and pad', () => {
88 | const wrapper = shallowMount(VPerfectSignature)
89 |
90 | wrapper.setData({
91 | allInputPoints: inputPointsMockData,
92 | })
93 | wrapper.vm.clear()
94 |
95 | expect(wrapper.vm.toData()).toEqual([])
96 | })
97 | })
98 |
99 | describe('#isEmpty', () => {
100 | it('returns true if pad is empty', () => {
101 | const wrapper = shallowMount(VPerfectSignature)
102 |
103 | expect(wrapper.vm.isEmpty()).toBe(true)
104 | })
105 |
106 | it('returns false if pad is not empty', () => {
107 | const wrapper = shallowMount(VPerfectSignature)
108 |
109 | wrapper.setData({
110 | allInputPoints: inputPointsMockData,
111 | })
112 |
113 | expect(wrapper.vm.isEmpty()).toBe(false)
114 | })
115 | })
116 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # v-perfect-signature
2 |
3 | Pressure-sensitive signature drawing for Vue 2 and 3 built on top of [perfect-freehand](https://github.com/steveruizok/perfect-freehand).
4 |
5 | Demo: https://wobsoriano.github.io/v-perfect-signature
6 |
7 | ## Install
8 |
9 | ```bash
10 | pnpm add v-perfect-signature
11 | ```
12 |
13 | ## Usage
14 |
15 | ```vue
16 |
33 |
34 |
35 |
36 |
37 | ```
38 |
39 | ## Props
40 |
41 | | Name | Type | Default | Description |
42 | | ----------------- | ------ | -------------------------------------------------------------------- | ------------------------ |
43 | | `width` | String | `100%` | Set canvas width |
44 | | `height` | String | `100%` | Set canvas height |
45 | | `backgroundColor` | String | `rgba(0,0,0,0)` | Canvas background color |
46 | | `penColor` | String | `#000` | Canvas pen color |
47 | | `strokeOptions` | Object | [Reference](https://github.com/steveruizok/perfect-freehand#options) | Perfect freehand options |
48 |
49 | ## Methods
50 |
51 | | Name | Argument Type | Description |
52 | | --------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------ |
53 | | `toDataURL(type)` | [String](https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement/toDataURL) | Returns signature image as data URL |
54 | | `fromDataURL(dataUri)` | [String](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/Data_URIs) | Draws signature image from data URL |
55 | | `toData` | - | Returns signature image as an array of array of input points |
56 | | `fromData(data)` | [Array](https://github.com/wobsoriano/v-perfect-signature/blob/master/packages/lib/src/components/__tests__/mock.ts#L1) | Draws signature image from array of array of input points |
57 | | `clear()` | - | Clears the canvas |
58 | | `isEmpty()` | - | Returns true if canvas is empty |
59 | | `resizeCanvas(shouldClear)` | `Boolean` | Resizes and recalculate dimensions |
60 |
61 | Note: Like [signature_pad](https://github.com/szimek/signature_pad), `fromDataURL` does not populate internal data structure. Thus, after using `fromDataURL`, `toData` won't work properly.
62 |
63 | ## Events
64 |
65 | | Name | Type | Default | Description |
66 | | --------- | -------- | ------- | ----------------------- |
67 | | `onBegin` | Function | - | Fired when stroke begin |
68 | | `onEnd` | Function | - | Fired when stroke end |
69 |
70 | ## Nuxt
71 |
72 | ```js
73 | export default defineNuxtConfig({
74 | build: {
75 | transpile: ['v-perfect-signature']
76 | }
77 | })
78 | ```
79 |
80 | ## License
81 |
82 | MIT
83 |
--------------------------------------------------------------------------------
/src/components/VPerfectSignature.ts:
--------------------------------------------------------------------------------
1 | import type { PropType } from 'vue-demi'
2 | import { defineComponent } from 'vue-demi'
3 | import type { StrokeOptions } from 'perfect-freehand'
4 | import * as PerfectFreehand from 'perfect-freehand'
5 |
6 | import h from '../utils/h-demi'
7 | import getSvgPathFromStroke from '../utils/get-svg-path-from-stroke'
8 | import {
9 | DEFAULT_BACKGROUND_COLOR,
10 | DEFAULT_HEIGHT,
11 | DEFAULT_PEN_COLOR,
12 | DEFAULT_WIDTH,
13 | IMAGE_TYPES,
14 | } from '../utils/constants'
15 |
16 | const { getStroke } = PerfectFreehand
17 |
18 | type InputPoints = number[]
19 |
20 | export default defineComponent({
21 | props: {
22 | width: {
23 | type: String,
24 | required: false,
25 | default: DEFAULT_WIDTH,
26 | },
27 | height: {
28 | type: String,
29 | required: false,
30 | default: DEFAULT_HEIGHT,
31 | },
32 | backgroundColor: {
33 | type: String,
34 | required: false,
35 | default: DEFAULT_BACKGROUND_COLOR,
36 | },
37 | penColor: {
38 | type: String,
39 | required: false,
40 | default: DEFAULT_PEN_COLOR,
41 | },
42 | strokeOptions: {
43 | type: Object as PropType,
44 | required: false,
45 | default: () => ({}),
46 | },
47 | },
48 | emits: ['onBegin', 'onEnd'],
49 | data: () => ({
50 | allInputPoints: [] as InputPoints[][],
51 | currentInputPoints: null as InputPoints[] | null,
52 | isDrawing: false,
53 | isLocked: false,
54 | cachedImages: [] as HTMLImageElement[],
55 | ctx: null as null | CanvasRenderingContext2D,
56 | }),
57 | watch: {
58 | backgroundColor() {
59 | this.setBackgroundAndPenColor()
60 | },
61 | penColor(color: string) {
62 | const ctx = this.getCanvasContext()
63 | if (ctx)
64 | ctx.fillStyle = color
65 | },
66 | allInputPoints: {
67 | deep: true,
68 | handler() {
69 | this.inputPointsHandler()
70 | },
71 | },
72 | currentInputPoints: {
73 | deep: true,
74 | handler() {
75 | this.inputPointsHandler()
76 | },
77 | },
78 | },
79 | mounted() {
80 | this.resizeCanvas()
81 | },
82 | methods: {
83 | _drawImage(image: HTMLImageElement) {
84 | const canvas = this.getCanvasElement()
85 | const ctx = this.getCanvasContext()
86 | const dpr = window.devicePixelRatio || 1
87 |
88 | ctx?.scale(1 / dpr, 1 / dpr) // To allow proper scaling of the image on HiDPI, we need to reset the scaling before calling `drawImage`
89 | ctx?.drawImage(image, 0, 0, canvas.width, canvas.height)
90 | ctx?.scale(dpr, dpr) // Set scaling back to original value
91 | },
92 | handlePointerDown(e: PointerEvent) {
93 | if (this.isLocked)
94 | return
95 |
96 | e.preventDefault()
97 |
98 | const canvas = e.composedPath()[0] as HTMLCanvasElement
99 | const rect = canvas.getBoundingClientRect()
100 | const x = e.clientX - rect.left
101 | const y = e.clientY - rect.top
102 |
103 | this.currentInputPoints = [[x, y, e.pressure]]
104 | this.isDrawing = true
105 | this.$emit('onBegin', e)
106 | },
107 | handlePointerMove(e: PointerEvent) {
108 | if (this.isLocked)
109 | return
110 | if (!this.isDrawing)
111 | return
112 |
113 | if (e.buttons === 1) {
114 | e.preventDefault()
115 |
116 | const canvas = e.composedPath()[0] as HTMLCanvasElement
117 | const rect = canvas.getBoundingClientRect()
118 | const x = e.clientX - rect.left
119 | const y = e.clientY - rect.top
120 |
121 | this.currentInputPoints = [
122 | ...(this.currentInputPoints ?? []),
123 | [x, y, e.pressure],
124 | ]
125 | }
126 | },
127 | handlePointerUp(e: PointerEvent) {
128 | if (this.isLocked)
129 | return
130 |
131 | e.preventDefault()
132 | this.isDrawing = false
133 |
134 | if (!this.currentInputPoints)
135 | return
136 |
137 | this.allInputPoints = [...this.allInputPoints, this.currentInputPoints]
138 | this.currentInputPoints = null
139 |
140 | this.$emit('onEnd', e)
141 | },
142 | handlePointerEnter(e: PointerEvent) {
143 | if (this.isLocked)
144 | return
145 |
146 | if (e.buttons === 1)
147 | this.handlePointerDown(e)
148 | },
149 | handlePointerLeave(e: PointerEvent) {
150 | if (this.isLocked)
151 | return
152 | if (!this.isDrawing)
153 | return
154 | this.handlePointerUp(e)
155 | },
156 | isEmpty() {
157 | return !this.allInputPoints.length && !this.cachedImages.length
158 | },
159 | clear() {
160 | this.cachedImages = []
161 | this.allInputPoints = []
162 | this.currentInputPoints = null
163 | },
164 | fromData(data: InputPoints[][]) {
165 | this.allInputPoints = [...this.allInputPoints, ...data]
166 | },
167 | lock() {
168 | this.isLocked = true
169 | },
170 | unlock() {
171 | this.isLocked = false
172 | },
173 | toData() {
174 | return this.allInputPoints
175 | },
176 | fromDataURL(data: string) {
177 | return new Promise((resolve, reject) => {
178 | const image = new Image()
179 |
180 | image.onload = () => {
181 | this._drawImage(image)
182 | this.cachedImages.push(image)
183 | resolve(true)
184 | }
185 |
186 | image.onerror = () => {
187 | reject(new Error('Incorrect data uri provided'))
188 | }
189 |
190 | image.crossOrigin = 'anonymous'
191 | image.src = data
192 | })
193 | },
194 | toDataURL(type?: string) {
195 | if (type && !IMAGE_TYPES.includes(type)) {
196 | throw new Error(
197 | `Incorrect image type. Must be one of ${IMAGE_TYPES.join(', ')}.`,
198 | )
199 | }
200 |
201 | if (this.isEmpty())
202 | return
203 |
204 | const canvas = this.getCanvasElement()
205 | return canvas.toDataURL(type ?? 'image/png')
206 | },
207 | getCanvasElement() {
208 | return this.$refs.signaturePad as HTMLCanvasElement
209 | },
210 | getCanvasContext() {
211 | if (!this.ctx) {
212 | const canvas = this.getCanvasElement()
213 | this.ctx = canvas.getContext('2d')
214 | }
215 | return this.ctx
216 | },
217 | setBackgroundAndPenColor() {
218 | const canvas = this.getCanvasElement()
219 | const ctx = canvas.getContext('2d')
220 | ctx!.fillStyle = this.backgroundColor
221 | ctx?.fillRect(0, 0, canvas.width, canvas.height)
222 | ctx!.fillStyle = this.penColor
223 | },
224 | resizeCanvas(clearCanvas = true) {
225 | const canvas = this.getCanvasElement()
226 | const rect = canvas.getBoundingClientRect()
227 | const dpr = window.devicePixelRatio || 1
228 |
229 | canvas.width = rect.width * dpr
230 | canvas.height = rect.height * dpr
231 | const ctx = this.getCanvasContext()
232 | ctx?.scale(dpr, dpr)
233 |
234 | canvas.style.width = `${rect.width}px`
235 | canvas.style.height = `${rect.height}px`
236 |
237 | if (clearCanvas) {
238 | ctx?.clearRect(0, 0, canvas.width, canvas.height)
239 | this.clear()
240 | }
241 |
242 | this.setBackgroundAndPenColor()
243 | },
244 | inputPointsHandler() {
245 | const canvas = this.getCanvasElement()
246 | const ctx = this.getCanvasContext()
247 |
248 | // Makes smooth lines
249 | ctx?.clearRect(0, 0, canvas.width, canvas.height)
250 |
251 | this.cachedImages.forEach(image => this._drawImage(image))
252 | this.setBackgroundAndPenColor()
253 |
254 | this.allInputPoints.forEach((point: InputPoints[]) => {
255 | const pathData = getSvgPathFromStroke(
256 | getStroke(point, this.strokeOptions),
257 | )
258 | const myPath = new Path2D(pathData)
259 | ctx?.fill(myPath)
260 | })
261 |
262 | if (!this.currentInputPoints)
263 | return
264 | const pathData = getSvgPathFromStroke(
265 | getStroke(this.currentInputPoints!, this.strokeOptions),
266 | )
267 | const myPath = new Path2D(pathData)
268 | ctx?.fill(myPath)
269 | },
270 | },
271 | render() {
272 | const { width, height } = this
273 |
274 | const handlers = {
275 | pointerdown: this.handlePointerDown,
276 | pointerup: this.handlePointerUp,
277 | pointermove: this.handlePointerMove,
278 | pointerenter: this.handlePointerEnter,
279 | pointerleave: this.handlePointerLeave,
280 | }
281 |
282 | return h('canvas', {
283 | ref: 'signaturePad',
284 | style: {
285 | height,
286 | width,
287 | touchAction: 'none',
288 | },
289 | on: handlers,
290 | })
291 | },
292 | })
293 |
--------------------------------------------------------------------------------
/test/mock.ts:
--------------------------------------------------------------------------------
1 | export const inputPointsMockData = [
2 | [
3 | [213.0234375, 415.578125, 0.5],
4 | [213.25, 415.578125, 0.5],
5 | [213.25, 415.34765625, 0.5],
6 | [213.4765625, 415.34765625, 0.5],
7 | [215.1015625, 415.34765625, 0.5],
8 | [218.99609375, 414.234375, 0.5],
9 | [228.234375, 410.93359375, 0.5],
10 | [235.30078125, 407.07421875, 0.5],
11 | [251.10546875, 397.015625, 0.5],
12 | [272.53125, 381.5859375, 0.5],
13 | [293.80078125, 363.859375, 0.5],
14 | [299.58203125, 358.07421875, 0.5],
15 | [315.0078125, 336.64453125, 0.5],
16 | [323.46875, 320.41796875, 0.5],
17 | [329.80859375, 299.01171875, 0.5],
18 | [331.8984375, 283.6640625, 0.5],
19 | [332.59765625, 266.7890625, 0.5],
20 | [330.44140625, 248.109375, 0.5],
21 | [326.25390625, 232.76171875, 0.5],
22 | [322.25, 223.42578125, 0.5],
23 | [318.53515625, 217.85546875, 0.5],
24 | [316.4453125, 215.2421875, 0.5],
25 | [314.0625, 213.8125, 0.5],
26 | [313.1875, 213.8125, 0.5],
27 | [312.95703125, 213.8125, 0.5],
28 | [312.7265625, 214.0390625, 0.5],
29 | [311.91015625, 215.6640625, 0.5],
30 | [306.484375, 225.8359375, 0.5],
31 | [302.38671875, 238.125, 0.5],
32 | [297.62890625, 260.3203125, 0.5],
33 | [294.66796875, 294.828125, 0.5],
34 | [294.66796875, 310.93359375, 0.5],
35 | [296.31640625, 334.88671875, 0.5],
36 | [300.625, 353.5625, 0.5],
37 | [305.50390625, 368.20703125, 0.5],
38 | [308.59375, 373.7734375, 0.5],
39 | [313.15625, 381.59375, 0.5],
40 | [315.9375, 384.9296875, 0.5],
41 | [317.15625, 386.1484375, 0.5],
42 | [317.15625, 386.375, 0.5],
43 | [317.15625, 386.14453125, 0.5],
44 | [317.15625, 385.9140625, 0.5],
45 | [316.74609375, 384.28515625, 0.5],
46 | [316.74609375, 381.15234375, 0.5],
47 | [316.74609375, 374.203125, 0.5],
48 | [317.38671875, 366.48828125, 0.5],
49 | [319.95703125, 358.7734375, 0.5],
50 | [325.171875, 351.6015625, 0.5],
51 | [326.390625, 350.37890625, 0.5],
52 | [335.72265625, 345.7109375, 0.5],
53 | [342.66796875, 345.078125, 0.5],
54 | [346.5625, 346.1875, 0.5],
55 | [351.5078125, 349.8984375, 0.5],
56 | [353.41015625, 351.80078125, 0.5],
57 | [357.625, 356.015625, 0.5],
58 | [364.69140625, 359.87109375, 0.5],
59 | [377.89453125, 362.23828125, 0.5],
60 | [383.3125, 362.23828125, 0.5],
61 | [387.20703125, 361.125, 0.5],
62 | [395.02734375, 357.2109375, 0.5],
63 | [403.02734375, 351.20703125, 0.5],
64 | [408.80859375, 345.421875, 0.5],
65 | [417.73828125, 335.11328125, 0.5],
66 | [424.60546875, 323.4296875, 0.5],
67 | [427.26953125, 313.42578125, 0.5],
68 | [427.87109375, 308.00390625, 0.5],
69 | [427.2109375, 298.765625, 0.5],
70 | [425.78125, 296.859375, 0.5],
71 | [425.55078125, 296.62890625, 0.5],
72 | [425.08984375, 296.62890625, 0.5],
73 | [424.859375, 296.85546875, 0.5],
74 | [422.4765625, 298.28125, 0.5],
75 | [418.76171875, 303.2265625, 0.5],
76 | [415.046875, 308.79296875, 0.5],
77 | [411.1328125, 316.61328125, 0.5],
78 | [408.65625, 322.796875, 0.5],
79 | [405.96484375, 333.55859375, 0.5],
80 | [405.28515625, 345.0859375, 0.5],
81 | [406.5859375, 353.55859375, 0.5],
82 | [412.046875, 365.1640625, 0.5],
83 | [415.7578125, 370.109375, 0.5],
84 | [423.828125, 377.5078125, 0.5],
85 | [433.16015625, 381.5078125, 0.5],
86 | [450.03125, 383.61328125, 0.5],
87 | [456.21484375, 383.61328125, 0.5],
88 | [469.26953125, 379.48828125, 0.5],
89 | [481.765625, 371.84765625, 0.5],
90 | [491.484375, 360.734375, 0.5],
91 | [498.45703125, 346.78125, 0.5],
92 | [499.69140625, 340.59375, 0.5],
93 | [500.37890625, 326.7734375, 0.5],
94 | [497.57421875, 310.6640625, 0.5],
95 | [492.6328125, 293.02734375, 0.5],
96 | [487.1328125, 280.65625, 0.5],
97 | [485.32421875, 275.234375, 0.5],
98 | [480.54296875, 263.625, 0.5],
99 | [477.17578125, 252.859375, 0.5],
100 | [475.31640625, 246.671875, 0.5],
101 | [474.79296875, 243.5390625, 0.5],
102 | [474.79296875, 243.30859375, 0.5],
103 | [474.5625, 243.765625, 0.5],
104 | [474.5625, 244.63671875, 0.5],
105 | [474.5625, 253.875, 0.5],
106 | [474.5625, 261.5859375, 0.5],
107 | [475.265625, 279.21875, 0.5],
108 | [478.0859375, 296.8515625, 0.5],
109 | [484.42578125, 318.25390625, 0.5],
110 | [488.21484375, 324.5703125, 0.5],
111 | [496.50390625, 336.3125, 0.5],
112 | [506.12109375, 345.9296875, 0.5],
113 | [513.37890625, 351.8671875, 0.5],
114 | [522.7109375, 356.53125, 0.5],
115 | [525.83984375, 357.05078125, 0.5],
116 | [532.0234375, 357.05078125, 0.5],
117 | [539.84375, 353.13671875, 0.5],
118 | [547.9140625, 345.0625, 0.5],
119 | [555.51171875, 333.31640625, 0.5],
120 | [560.17578125, 323.98046875, 0.5],
121 | [567.30859375, 302.57421875, 0.5],
122 | [570.1171875, 285.69921875, 0.5],
123 | [571.55078125, 267.01953125, 0.5],
124 | [571.55078125, 246.578125, 0.5],
125 | [570.8671875, 234.28515625, 0.5],
126 | [569.5, 221.9921875, 0.5],
127 | [566.765625, 209.69921875, 0.5],
128 | [564.95703125, 204.27734375, 0.5],
129 | [564.546875, 202.6484375, 0.5],
130 | [564.31640625, 202.6484375, 0.5],
131 | [564.31640625, 202.875, 0.5],
132 | [563.1484375, 207.53125, 0.5],
133 | [561.7890625, 219.05859375, 0.5],
134 | [561.7890625, 235.9296875, 0.5],
135 | [562.48046875, 250.5078125, 0.5],
136 | [566.0703125, 269.18359375, 0.5],
137 | [571.09765625, 287.859375, 0.5],
138 | [576.62109375, 300.984375, 0.5],
139 | [582.1171875, 313.3515625, 0.5],
140 | [585.2734375, 319.66796875, 0.5],
141 | [590.48828125, 326.8359375, 0.5],
142 | [594.703125, 331.05078125, 0.5],
143 | [597.83203125, 332.61328125, 0.5],
144 | [598.703125, 332.90234375, 0.5],
145 | [599.57421875, 332.90234375, 0.5],
146 | [602.703125, 332.37890625, 0.5],
147 | [606.91796875, 328.76171875, 0.5],
148 | [612.05859375, 322.9765625, 0.5],
149 | [618.88671875, 312.73046875, 0.5],
150 | [625.828125, 299.53515625, 0.5],
151 | [630.74609375, 283.36328125, 0.5],
152 | [633.5234375, 268.78125, 0.5],
153 | [633.5234375, 258.015625, 0.5],
154 | [633.5234375, 250.30078125, 0.5],
155 | [632.35546875, 245.640625, 0.5],
156 | [632.125, 245.1796875, 0.5],
157 | [632.125, 245.40625, 0.5],
158 | [632.125, 245.86328125, 0.5],
159 | [631.6015625, 248.9921875, 0.5],
160 | [630.921875, 260.51953125, 0.5],
161 | [630.921875, 274.3359375, 0.5],
162 | [630.921875, 281.28125, 0.5],
163 | [634.35546875, 294.3359375, 0.5],
164 | [638.97265625, 302.9140625, 0.5],
165 | [645.69921875, 311.66015625, 0.5],
166 | [649.19140625, 315.15234375, 0.5],
167 | [659.36328125, 320.57421875, 0.5],
168 | [671.65234375, 323.3046875, 0.5],
169 | [680.890625, 323.9609375, 0.5],
170 | [692.41796875, 323.28125, 0.5],
171 | [697.07421875, 322.11328125, 0.5],
172 | [701.890625, 319.09765625, 0.5],
173 | [704.671875, 315.7578125, 0.5],
174 | [705.2890625, 309.5703125, 0.5],
175 | [704.61328125, 298.8046875, 0.5],
176 | [701.3515625, 290.98046875, 0.5],
177 | [694.51953125, 280.05078125, 0.5],
178 | [687.18359375, 272.71484375, 0.5],
179 | [676.12890625, 264.421875, 0.5],
180 | [666.79296875, 260.41796875, 0.5],
181 | [664.41015625, 259.46484375, 0.5],
182 | [659.75, 258.87890625, 0.5],
183 | [654.328125, 258.87890625, 0.5],
184 | [648.140625, 260.11328125, 0.5],
185 | [646.234375, 261.5390625, 0.5],
186 | [645.7734375, 261.99609375, 0.5],
187 | [645.48046875, 262.8671875, 0.5],
188 | ],
189 | ]
190 |
191 | export const mockDataURL
192 | = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAA1MAAAGpCAYAAABoLmzKAAAcOUlEQVR4Xu3XoREAAAjEMNh/aWagOvg3OUx3HAECBAgQIECAAAECBAi8Bfa9MCBAgAABAgQIECBAgACBEVOegAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCIipgGZCgAABAgQIECBAgAABMeUHCBAgQIAAAQIECBAgEATEVEAzIUCAAAECBAgQIECAgJjyAwQIECBAgAABAgQIEAgCYiqgmRAgQIAAAQIECBAgQEBM+QECBAgQIECAAAECBAgEATEV0EwIECBAgAABAgQIECAgpvwAAQIECBAgQIAAAQIEgoCYCmgmBAgQIECAAAECBAgQEFN+gAABAgQIECBAgAABAkFATAU0EwIECBAgQIAAAQIECIgpP0CAAAECBAgQIECAAIEgIKYCmgkBAgQIECBAgAABAgTElB8gQIAAAQIECBAgQIBAEBBTAc2EAAECBAgQIECAAAECYsoPECBAgAABAgQIECBAIAiIqYBmQoAAAQIECBAgQIAAATHlBwgQIECAAAECBAgQIBAExFRAMyFAgAABAgQIECBAgICY8gMECBAgQIAAAQIECBAIAmIqoJkQIECAAAECBAgQIEBATPkBAgQIECBAgAABAgQIBAExFdBMCBAgQIAAAQIECBAgIKb8AAECBAgQIECAAAECBIKAmApoJgQIECBAgAABAgQIEBBTfoAAAQIECBAgQIAAAQJBQEwFNBMCBAgQIECAAAECBAiIKT9AgAABAgQIECBAgACBICCmApoJAQIECBAgQIAAAQIExJQfIECAAAECBAgQIECAQBAQUwHNhAABAgQIECBAgAABAmLKDxAgQIAAAQIECBAgQCAIiKmAZkKAAAECBAgQIECAAAEx5QcIECBAgAABAgQIECAQBMRUQDMhQIAAAQIECBAgQICAmPIDBAgQIECAAAECBAgQCAJiKqCZECBAgAABAgQIECBAQEz5AQIECBAgQIAAAQIECAQBMRXQTAgQIECAAAECBAgQICCm/AABAgQIECBAgAABAgSCgJgKaCYECBAgQIAAAQIECBAQU36AAAECBAgQIECAAAECQUBMBTQTAgQIECBAgAABAgQIiCk/QIAAAQIECBAgQIAAgSAgpgKaCQECBAgQIECAAAECBMSUHyBAgAABAgQIECBAgEAQEFMBzYQAAQIECBAgQIAAAQJiyg8QIECAAAECBAgQIEAgCBx++AGq6N2iaAAAAABJRU5ErkJggg=='
193 |
--------------------------------------------------------------------------------