├── .browserslistrc ├── .gitignore ├── .npmrc ├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── babel.config.js ├── build └── rollup.config.js ├── dev ├── serve.ts └── serve.vue ├── package-lock.json ├── package.json ├── shims-vue.d.ts ├── src ├── VueDrawingCanvas.ts └── entry.ts └── tsconfig.json /.browserslistrc: -------------------------------------------------------------------------------- 1 | current node 2 | last 2 versions and > 2% 3 | ie > 10 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | dist/ 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | scripts-prepend-node-path=true -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [v1.0.14](https://github.com/razztyfication/vue-drawing-canvas) 2 | 3 | - Bug fix background color not working 4 | 5 |
6 | 7 | # [v1.0.13](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.13) 8 | 9 | - Bug fix missing line guide when background image exist 10 | 11 |
12 | 13 | # [v1.0.12](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.12) 14 | 15 | - Bug fix canvas break when draw circle with no coordinates 16 | 17 |
18 | 19 | # [v1.0.11](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.11) 20 | 21 | - Added new prop **outputWidth** and **outputHeight** 22 | - Bug fixes 23 | 24 |
25 | 26 | # [v1.0.10](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.10) 27 | 28 | - Eraser no longer erase background image. 29 | - Added new prop **lineCap** and **lineJoin** 30 | - Added new value `"line"` on **strokeStyle** to draw straight line 31 | 32 |
33 | 34 | # [v1.0.9](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.9) 35 | 36 | - BUG FIX redraw function with wrong stroke type 37 | - BUG FIX typescript type declaration not compatible with `noImplicitAny` 38 | - Added new prop **additionalImages** will accept Array of `watermark` Object to draw either text or insert multiple image on canvas 39 | - Remove build for `ssr` and `unpkg` 40 | 41 |
42 | 43 | # [v1.0.8](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.8) 44 | 45 | - BUG FIX not working on drawing tablet and stylus 46 | 47 |
48 | 49 | # [v1.0.7](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.7) 50 | 51 | - Added new method **getAllStrokes()** to get all strokes and shapes from canvas. 52 | - Added new prop **initialImage** to draw all strokes and shapes from previously worked canvas. 53 | 54 | See [demo](https://codesandbox.io/s/vue-drawing-canvas-107-rc1-dcoiy) to see it in action. 55 | 56 |
57 | 58 | # [v1.0.6](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.6) 59 | 60 | - Added new props **canvasId** to allow multiple canvas on one page. Thank to [mortegro](https://github.com/mortegro) 61 | 62 |
63 | 64 | # [v1.0.5](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.5) 65 | 66 | - BUG FIX missing side when drawing shapes 67 | - BUG FIX add missing return on null background image 68 | 69 |
70 | 71 | # [v1.0.4](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.4) 72 | 73 | - Reworked multiline of text for watermark 74 | 75 | example: 76 | 77 | ```js 78 | export default { 79 | ... 80 | data() { 81 | return { 82 | ... 83 | watermark: { 84 | type: "Text", 85 | source: `This is\nWatermark 86 | TEXT`, 87 | x: 200, 88 | y: 180, 89 | fontStyle: { 90 | width: 200, 91 | lineHeight: 48, 92 | color: '#FF0000', 93 | font: 'bold 48px roboto', 94 | drawType: 'fill', 95 | textAlign: 'left', 96 | textBaseline: 'top', 97 | rotate: 0 98 | } 99 | } 100 | } 101 | } 102 | ``` 103 | 104 |
105 | 106 | # [v1.0.3](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.3) 107 | 108 | - Wrap watermark text to multiline. Thanks to [mishahobanov](https://github.com/mishahobanov) 109 | 110 |
111 | 112 | # [v1.0.2](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.2) 113 | 114 | - Bug Fix Background Image not update after redraw() 115 | 116 |
117 | 118 | # [v1.0.1](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.1) 119 | 120 | - Rename file from .vue to .ts 121 | - Update [README.md](https://github.com/razztyfication/vue-drawing-canvas/blob/master/README.md) 122 | - Added default value on [Watermark Object](https://github.com/razztyfication/vue-drawing-canvas/blob/master/README.md#watermark-object) 123 | - Bug Fix Redo with Background Color instead of white 124 | - Added Props `saveAs`, `strokeType` and `fillShape` 125 | 126 |
127 | 128 | # [v1.0.0](https://github.com/razztyfication/vue-drawing-canvas/tree/v1.0.0) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Toni Oktoro & vue-drawing-canvas contributors 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

vue-drawing-canvas

2 | 3 | VueJS Component for drawing on canvas. 4 | 5 | Support for both Vue 3 and Vue 2 + [Composition API](https://github.com/vuejs/composition-api) 6 | 7 |

8 | 9 |

Demo

10 | 11 | ### Vue 3 12 | 13 | [![Vue 3 Drawing Canvas Demo](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-3-drawing-canvas-demo-ihmmz) 14 | 15 | ### Vue 2 16 | 17 | > Deployed on a nuxt container which have access to terminal 18 | 19 |
20 | 21 | [![vue-drawing-canvas](https://codesandbox.io/static/img/play-codesandbox.svg)](https://codesandbox.io/s/vue-drawing-canvas-p4slb) 22 | 23 | > Note: 24 | > If you're using nuxt.js and receive error `Object(...) is not a function` please refer to this [issue](https://github.com/razztyfication/vue-drawing-canvas/issues/13) 25 | 26 |

27 | 28 |

Table of Contents

29 | 30 | - [Installation](#installation) 31 | - [Usage](#usage) 32 | - [Props](#props) 33 | - [Watermark Object](#watermark-object) 34 | - [Methods](#methods) 35 | - [Changelog](#changelog) 36 | - [License](#license) 37 | 38 |

39 | 40 | # Installation 41 | 42 | Install using package manager: 43 | 44 | ```bash 45 | npm install --save-dev vue-drawing-canvas 46 | 47 | # or with Vue 2 48 | 49 | npm install --save-dev vue-drawing-canvas @vue/composition-api 50 | ``` 51 | 52 | Then add it to your component files 53 | 54 | ```html 55 | 56 | 57 | 60 | 61 | 71 | ``` 72 | 73 |

74 | 75 | # Usage 76 | 77 | ## Props 78 | 79 |
80 | 81 | | Name | Type | Default Value | Description | 82 | | ---------------- | :-------------------: | :----------------: | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | 83 | | canvasId | String | `VueDrawingCanvas` | Specifies your canvas id | 84 | | width | String, Number | `600` | Specifies canvas width | 85 | | height | String, Number | `400` | Specifies canvas height | 86 | | image | String | | Your v-model to get canvas output to an base64 image | 87 | | strokeType | String | `"dash"` | Specifies stroke type to draw on canvas.

Accepted value `"dash"`, `"line"`, `"circle"`, `"square"`, `"triangle"`, `"half_triangle"` | 88 | | fillShape | Boolean | `false` | Specifies if the shape must be filled with the current fill style | 89 | | eraser | Boolean | `false` | Props to change state from drawing to erasing | 90 | | color | String | `"#000000"` | Specifies the color, gradient, or pattern to use for the strokes | 91 | | lineWidth | Number | `5` | Sets the thickness of line | 92 | | lineCap | String | `"round"` | Determines the shape used to draw the end points of line.

Accepted value `"round"`, `"square"`, `"butt"`.
Refer to [this site](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineCap) for more information. | 93 | | lineJoin | String | `"miter"` | determines the shape used to join two line segments where they meet.

Accepted value `"round"`, `"miter"`, `"square"`.
Refer to [this site](https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/lineJoin) for more information. | 94 | | lock | Boolean | `false` | Lock canvas for drawing | 95 | | backgroundColor | String | `"#FFFFFF"` | Set background color on your canvas | 96 | | backgroundImage | String | | Set background image on your canvas

**_Be carefull for performance issue when using this props !!_** | 97 | | initialImage | Array | `[]` | Draw strokes and shapes from canvas you've worked before. [Demo](https://codesandbox.io/s/vue-drawing-canvas-107-rc1-dcoiy) | 98 | | additionalImages | Array | `[]` | Accept Array of [watermark](#watermark-object) Object to draw either text or insert multiple image on canvas

**_Be carefull for performance issue when using this props !!_** | 99 | | classes | Array, String, Object | | Specifies your own classes to canvas | 100 | | styles | Array, String, Object | | Specifies your own styles to canvas | 101 | | watermark | Object | | Put watermark text/image on your image output

(see details in the next section below) | 102 | | saveAs | String | `"png"` | Specifies output type. This props accept either `"png"` or `"jpeg"` | 103 | | outputWidth | Number | `this.width` | Specifies image output width, if `undefined` then canvas width will be used | 104 | | outputHeight | Number | `this.height` | Specifies image output height, if `undefined` then canvas height will be used | 105 | 106 |
107 | 108 | ### Watermark Object 109 | 110 |
111 | 112 | ```js 113 | { 114 | // Specifies your watermark type. Type can be either "Text" or "Image" 115 | // 116 | // type: String 117 | // required: true 118 | // validator: (value) => { return ["Text", "Image"].includes(value) } 119 | type: "Text", 120 | // Specifies your watermark source 121 | // If type is "Text" enter your watermark text here 122 | // if type if "Image" enter your uploaded file createObjectURL(event.target.files[0]) Work best with .png file 123 | // 124 | // type: String 125 | // required: true 126 | source: "Watermark", 127 | // The x-axis coordinate of the point at which to begin drawing the watermark, 128 | // in pixels 129 | // 130 | // type: Number 131 | // required: true 132 | x: 200, 133 | // The y-axis coordinate of the point at which to begin drawing the watermark, 134 | // in pixels 135 | // 136 | // type: Number 137 | // required: true 138 | y: 180, 139 | // Specifies width and height for your watermark image 140 | // 141 | // type: Object 142 | // required: false 143 | imageStyle: { 144 | // The width to draw the image in the canvas 145 | // 146 | // type: Number 147 | // required: false 148 | // default: () => this.width 149 | width: 600, 150 | // The height to draw the image in the canvas 151 | // 152 | // type: Number 153 | // required: false 154 | // default: () => this.height 155 | height: 400 156 | }, 157 | // Specifies text style for your watermark 158 | // 159 | // type: Object 160 | // required: false 161 | fontStyle: { 162 | // The maximum number of pixels wide the text may be once rendered. 163 | // If not specified, there is no limit to the width of the text. 164 | // 165 | // type: Number 166 | // required: false 167 | width: 200, 168 | // Sets the height of text in pixels. Usually this value has same value with font 169 | // 170 | // type: Number 171 | // required: false 172 | // default: () => 20 173 | lineHeight: 48, 174 | // Specifies the color, gradient, or pattern to use for the text 175 | // 176 | // type: String 177 | // required: false 178 | // default: () => '#000000' 179 | color: '#FF0000', 180 | // Specifies the current text style to use when drawing text. 181 | // font: '{fontWeight} {fontSize} {fontFamily}' 182 | // 183 | // type: String 184 | // required: false 185 | // default: () => '20px serif' 186 | font: 'bold 48px serif', 187 | // Specifies drawing type to use when drawing text. 188 | // 189 | // type: String 190 | // required: false 191 | // validator: (value) => { return ["fill", "stroke"].includes(value) } 192 | // default: () => 'fill' 193 | drawType: 'fill', 194 | // Specifies the current text alignment used when drawing text 195 | // The alignment is relative to the x value 196 | // 197 | // type: String 198 | // required: false 199 | // validator: (value) => { return ["left", "right", "center", "start", "end"].includes(value) } 200 | // default: () => 'start' 201 | textAlign: 'left', 202 | // Specifies the current text baseline used when drawing text 203 | // 204 | // type: String 205 | // required: false 206 | // validator: (value) => { return ["top", "hanging", "middle", "alphabetic", "ideographic", "bottom"].includes(value) } 207 | // default: () => 'alphabetic' 208 | textBaseline: 'top', 209 | // The rotation angle, clockwise in radians 210 | // 211 | // type: Number 212 | // required: false 213 | rotate: 45 214 | } 215 | } 216 | ``` 217 | 218 |

219 | 220 | ## Methods 221 | 222 |
223 | 224 | | Method Name | Return Value | Description | 225 | | --------------------- | :-----------------------------: | ----------------------------------------------------- | 226 | | getCoordinates(event) | `{ x: 0, y: 0 }` | Get x-axis and y-axis coordinates from current canvas | 227 | | reset() | | Reset current canvas to new state | 228 | | undo() | | Remove last drawing stroke on current canvas | 229 | | redo() | | Re-draw last removed stroke on current canvas | 230 | | redraw() | | Redraw all strokes on current canvas | 231 | | isEmpty() | `true` or `false` | Get current canvas empty state | 232 | | getAllStrokes() | _`Array of strokes and shapes`_ | Get all strokes and shapes from canvas | 233 | 234 |

235 | 236 | # Changelog 237 | 238 | [Read here](https://github.com/razztyfication/vue-drawing-canvas/blob/master/CHANGELOG.md) 239 | 240 |

241 | 242 | # License 243 | 244 | [MIT](https://github.com/razztyfication/vue-drawing-canvas/blob/master/LICENSE.md) 245 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | const devPresets = ['@vue/babel-preset-app']; 2 | const buildPresets = [ 3 | [ 4 | '@babel/preset-env', 5 | // Config for @babel/preset-env 6 | { 7 | // Example: Always transpile optional chaining/nullish coalescing 8 | // include: [ 9 | // /(optional-chaining|nullish-coalescing)/ 10 | // ], 11 | }, 12 | ], 13 | '@babel/preset-typescript', 14 | ]; 15 | module.exports = { 16 | presets: (process.env.NODE_ENV === 'development' ? devPresets : buildPresets), 17 | }; 18 | -------------------------------------------------------------------------------- /build/rollup.config.js: -------------------------------------------------------------------------------- 1 | // rollup.config.js 2 | import fs from 'fs'; 3 | import path from 'path'; 4 | import vue from 'rollup-plugin-vue'; 5 | import alias from '@rollup/plugin-alias'; 6 | import commonjs from '@rollup/plugin-commonjs'; 7 | import resolve from '@rollup/plugin-node-resolve'; 8 | import replace from '@rollup/plugin-replace'; 9 | import babel from '@rollup/plugin-babel'; 10 | import PostCSS from 'rollup-plugin-postcss'; 11 | import { terser } from 'rollup-plugin-terser'; 12 | import ttypescript from 'ttypescript'; 13 | import typescript from 'rollup-plugin-typescript2'; 14 | import minimist from 'minimist'; 15 | 16 | // Get browserslist config and remove ie from es build targets 17 | const esbrowserslist = fs.readFileSync('./.browserslistrc') 18 | .toString() 19 | .split('\n') 20 | .filter((entry) => entry && entry.substring(0, 2) !== 'ie'); 21 | 22 | // Extract babel preset-env config, to combine with esbrowserslist 23 | const babelPresetEnvConfig = require('../babel.config') 24 | .presets.filter((entry) => entry[0] === '@babel/preset-env')[0][1]; 25 | 26 | const argv = minimist(process.argv.slice(2)); 27 | 28 | const projectRoot = path.resolve(__dirname, '..'); 29 | 30 | const baseConfig = { 31 | input: 'src/entry.ts', 32 | plugins: { 33 | preVue: [ 34 | alias({ 35 | entries: [ 36 | { 37 | find: '@', 38 | replacement: `${path.resolve(projectRoot, 'src')}`, 39 | }, 40 | ], 41 | }), 42 | ], 43 | replace: { 44 | 'process.env.NODE_ENV': JSON.stringify('production'), 45 | }, 46 | vue: { 47 | }, 48 | postVue: [ 49 | resolve({ 50 | extensions: ['.js', '.jsx', '.ts', '.tsx', '.vue'], 51 | }), 52 | // Process only ` -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vue-drawing-canvas", 3 | "version": "1.0.14", 4 | "author": { 5 | "name": "Toni Oktoro", 6 | "email": "tonioktoro@gmail.com" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/razztyfication/vue-drawing-canvas.git" 11 | }, 12 | "homepage": "https://github.com/razztyfication/vue-drawing-canvas", 13 | "private": false, 14 | "description": "VueJS Component for drawing on canvas.", 15 | "main": "dist/vue-drawing-canvas.esm.js", 16 | "module": "dist/vue-drawing-canvas.esm.js", 17 | "types": "dist/types/src/VueDrawingCanvas.d.ts", 18 | "files": [ 19 | "dist/*" 20 | ], 21 | "sideEffects": false, 22 | "scripts": { 23 | "serve": "vue-cli-service serve dev/serve.ts", 24 | "prebuild": "rimraf ./dist", 25 | "build": "cross-env NODE_ENV=production rollup --config build/rollup.config.js --format es", 26 | "postbuild": "rimraf ./dist/types/dev ./dist/types/src/entry.d.ts" 27 | }, 28 | "dependencies": { 29 | "vue-demi": "latest" 30 | }, 31 | "devDependencies": { 32 | "@babel/core": "^7.16.5", 33 | "@babel/preset-env": "^7.16.5", 34 | "@babel/preset-typescript": "^7.16.5", 35 | "@rollup/plugin-alias": "^3.1.8", 36 | "@rollup/plugin-babel": "^5.3.0", 37 | "@rollup/plugin-commonjs": "^14.0.0", 38 | "@rollup/plugin-node-resolve": "^9.0.0", 39 | "@rollup/plugin-replace": "^2.4.2", 40 | "@vue/cli-plugin-babel": "^4.5.15", 41 | "@vue/cli-plugin-typescript": "^4.5.15", 42 | "@vue/cli-service": "^4.5.15", 43 | "@vue/compiler-sfc": "^3.2.26", 44 | "@zerollup/ts-transform-paths": "^1.7.18", 45 | "cross-env": "^7.0.3", 46 | "minimist": "^1.2.5", 47 | "postcss": "^8.4.5", 48 | "rimraf": "^3.0.2", 49 | "rollup": "^2.61.1", 50 | "rollup-plugin-postcss": "^4.0.2", 51 | "rollup-plugin-terser": "^7.0.2", 52 | "rollup-plugin-typescript2": "^0.30.0", 53 | "rollup-plugin-vue": "^6.0.0", 54 | "ttypescript": "^1.5.13", 55 | "typescript": "^4.5.4", 56 | "vue": "^3.2.26", 57 | "vue2": "npm:vue@2" 58 | }, 59 | "peerDependencies": { 60 | "@vue/composition-api": "^1.0.0-rc.1", 61 | "vue": "^2.0.0 || ^3.0.5" 62 | }, 63 | "peerDependenciesMeta": { 64 | "@vue/composition-api": { 65 | "optional": true 66 | } 67 | }, 68 | "license": "MIT", 69 | "keywords": [ 70 | "vue", 71 | "vuejs", 72 | "vue2", 73 | "vue3", 74 | "canvas", 75 | "signature", 76 | "draw", 77 | "drawing" 78 | ], 79 | "engines": { 80 | "node": ">=12" 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue'; 3 | 4 | const Component: DefineComponent<{}, {}, any>; 5 | export default Component; 6 | } 7 | -------------------------------------------------------------------------------- /src/VueDrawingCanvas.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-debugger, no-console */ 2 | 3 | import { defineComponent, h, isVue2 } from 'vue-demi'; 4 | 5 | interface WatermarkImageStyle { 6 | width: number, 7 | height: number 8 | } 9 | 10 | interface WatermarkFontStyle { 11 | width: number, 12 | lineHeight: number, 13 | color: string, 14 | font: string, 15 | drawType: string, 16 | textAlign: string, 17 | textBaseline: string, 18 | rotate: number 19 | } 20 | 21 | interface WatermarkData { 22 | type: string, 23 | source: string, 24 | x: number, 25 | y: number, 26 | imageStyle?: WatermarkImageStyle, 27 | fontStyle?: WatermarkFontStyle 28 | } 29 | 30 | interface DataInit { 31 | loadedImage: any; 32 | drawing: boolean; 33 | context: any; 34 | images: any; 35 | strokes: any; 36 | guides: any; 37 | trash: any; 38 | } 39 | 40 | export default /*#__PURE__*/defineComponent({ 41 | name: 'VueDrawingCanvas', 42 | props: { 43 | strokeType: { 44 | type: String, 45 | validator: (value: string): boolean => { 46 | return ['dash', 'line', 'square', 'circle', 'triangle', 'half_triangle'].indexOf(value) !== -1 47 | }, 48 | default: () => 'dash' 49 | }, 50 | fillShape: { 51 | type: Boolean, 52 | default: () => false 53 | }, 54 | width: { 55 | type: [String, Number], 56 | default: () => 600 57 | }, 58 | height: { 59 | type: [String, Number], 60 | default: () => 400 61 | }, 62 | image: { 63 | type: String, 64 | default: () => '' 65 | }, 66 | eraser: { 67 | type: Boolean, 68 | default: () => false 69 | }, 70 | color: { 71 | type: String, 72 | default: () => '#000000' 73 | }, 74 | lineWidth: { 75 | type: Number, 76 | default: () => 5 77 | }, 78 | lineCap: { 79 | type: String, 80 | validator: (value: string): boolean => { 81 | return ['round', 'square', 'butt'].indexOf(value) !== -1 82 | }, 83 | default: () => 'round' 84 | }, 85 | lineJoin: { 86 | type: String, 87 | validator: (value: string): boolean => { 88 | return ['miter', 'round', 'bevel'].indexOf(value) !== -1 89 | }, 90 | default: () => 'miter' 91 | }, 92 | lock: { 93 | type: Boolean, 94 | default: () => false 95 | }, 96 | styles: { 97 | type: [Array, String, Object], 98 | }, 99 | classes: { 100 | type: [Array, String, Object], 101 | }, 102 | backgroundColor: { 103 | type: String, 104 | default: () => '#FFFFFF' 105 | }, 106 | backgroundImage: { 107 | type: String, 108 | default: (): null | string => null 109 | }, 110 | watermark: { 111 | type: Object, 112 | default: (): null | WatermarkData => null 113 | }, 114 | saveAs: { 115 | type: String, 116 | validator: (value: string) => { 117 | return ['jpeg', 'png'].indexOf(value) !== -1 118 | }, 119 | default: () => 'png' 120 | }, 121 | canvasId: { 122 | type: String, 123 | default: () => 'VueDrawingCanvas' 124 | }, 125 | initialImage: { 126 | type: Array, 127 | default: (): any => [] 128 | }, 129 | additionalImages: { 130 | type: Array, 131 | default: (): any => [] 132 | }, 133 | outputWidth: { 134 | type: Number 135 | }, 136 | outputHeight: { 137 | type: Number 138 | } 139 | }, 140 | data(): DataInit { 141 | return { 142 | loadedImage: null, 143 | drawing: false, 144 | context: null, 145 | images: [], 146 | strokes: { 147 | type: '', 148 | from: { x: 0, y: 0 }, 149 | coordinates: [], 150 | color: '', 151 | width: '', 152 | fill: false, 153 | lineCap: '', 154 | lineJoin: '' 155 | }, 156 | guides: [], 157 | trash: [] 158 | }; 159 | }, 160 | mounted() { 161 | this.setContext(); 162 | this.$nextTick(() => { 163 | this.drawInitialImage() 164 | this.drawAdditionalImages() 165 | }) 166 | }, 167 | watch: { 168 | backgroundImage: function () { 169 | this.loadedImage = null 170 | } 171 | }, 172 | methods: { 173 | async setContext() { 174 | let canvas: HTMLCanvasElement = document.querySelector('#'+this.canvasId); 175 | this.context = this.context ? this.context : canvas.getContext('2d'); 176 | 177 | await this.setBackground(); 178 | }, 179 | drawInitialImage() { 180 | if (this.initialImage && this.initialImage.length > 0) { 181 | // @ts-ignore 182 | this.images = [].concat(this.images, this.initialImage) 183 | this.redraw(true) 184 | } 185 | }, 186 | drawAdditionalImages() { 187 | if (this.additionalImages && this.additionalImages.length > 0) { 188 | let canvas: HTMLCanvasElement = document.querySelector('#'+this.canvasId); 189 | this.additionalImages.forEach((watermarkObject: any) => { 190 | this.drawWatermark(canvas, this.context, watermarkObject) 191 | }); 192 | } 193 | }, 194 | clear() { 195 | this.context.clearRect(0, 0, Number(this.width), Number(this.height)); 196 | }, 197 | async setBackground() { 198 | this.clear(); 199 | this.context.fillStyle = this.backgroundColor; 200 | this.context.fillRect(0, 0, Number(this.width), Number(this.height)) 201 | 202 | await this.$nextTick(async () => { 203 | await this.drawBackgroundImage() 204 | }) 205 | this.save(); 206 | }, 207 | async drawBackgroundImage() { 208 | if (!this.loadedImage) { 209 | return new Promise((resolve) => { 210 | if (!this.backgroundImage) { 211 | resolve() 212 | return; 213 | } 214 | const image = new Image(); 215 | image.src = this.backgroundImage; 216 | image.onload = () => { 217 | this.context.drawImage(image, 0, 0, Number(this.width), Number(this.height)); 218 | this.loadedImage = image 219 | resolve(); 220 | } 221 | }) 222 | } else { 223 | this.context.drawImage(this.loadedImage, 0, 0, Number(this.width), Number(this.height)); 224 | } 225 | }, 226 | getCoordinates(event: Event) { 227 | let x, y; 228 | if ((event).touches && (event).touches.length > 0) { 229 | let canvas: HTMLCanvasElement = document.querySelector('#'+this.canvasId); 230 | let rect = canvas.getBoundingClientRect(); 231 | x = ((event).touches[0].clientX - rect.left); 232 | y = ((event).touches[0].clientY - rect.top); 233 | } else { 234 | x = (event).offsetX; 235 | y = (event).offsetY; 236 | } 237 | return { 238 | x: x, 239 | y: y 240 | } 241 | }, 242 | startDraw(event: Event) { 243 | if (!this.lock) { 244 | this.drawing = true; 245 | 246 | let coordinate = this.getCoordinates(event); 247 | this.strokes = { 248 | type: this.eraser ? 'eraser' : this.strokeType, 249 | from: coordinate, 250 | coordinates: [], 251 | color: this.eraser ? this.backgroundColor : this.color, 252 | width: this.lineWidth, 253 | fill: this.eraser || this.strokeType === 'dash' || this.strokeType === 'line' ? false : this.fillShape, 254 | lineCap: this.lineCap, 255 | lineJoin: this.lineJoin 256 | }; 257 | this.guides = []; 258 | } 259 | }, 260 | draw(event: Event) { 261 | if (this.drawing) { 262 | if (!this.context) { 263 | this.setContext(); 264 | } 265 | let coordinate = this.getCoordinates(event); 266 | if (this.eraser || this.strokeType === 'dash') { 267 | this.strokes.coordinates.push(coordinate); 268 | this.drawShape(this.context, this.strokes, false); 269 | } else { 270 | switch (this.strokeType) { 271 | case 'line': 272 | this.guides = [ 273 | { x: coordinate.x, y: coordinate.y } 274 | ]; 275 | break; 276 | case 'square': 277 | this.guides = [ 278 | { x: coordinate.x, y: this.strokes.from.y }, 279 | { x: coordinate.x, y: coordinate.y }, 280 | { x: this.strokes.from.x, y: coordinate.y }, 281 | { x: this.strokes.from.x, y: this.strokes.from.y } 282 | ]; 283 | break; 284 | case 'triangle': 285 | let center = Math.floor((coordinate.x - this.strokes.from.x) / 2) < 0 ? Math.floor((coordinate.x - this.strokes.from.x) / 2) * -1 : Math.floor((coordinate.x - this.strokes.from.x) / 2); 286 | let width = this.strokes.from.x < coordinate.x ? this.strokes.from.x + center : this.strokes.from.x - center; 287 | this.guides = [ 288 | { x: coordinate.x, y: this.strokes.from.y }, 289 | { x: width, y: coordinate.y }, 290 | { x: this.strokes.from.x, y: this.strokes.from.y } 291 | ]; 292 | break; 293 | case 'half_triangle': 294 | this.guides = [ 295 | { x: coordinate.x, y: this.strokes.from.y }, 296 | { x: this.strokes.from.x, y: coordinate.y }, 297 | { x: this.strokes.from.x, y: this.strokes.from.y } 298 | ]; 299 | break; 300 | case 'circle': 301 | let radiusX = this.strokes.from.x - coordinate.x < 0 ? (this.strokes.from.x - coordinate.x) * -1 : this.strokes.from.x - coordinate.x; 302 | this.guides = [ 303 | { x: this.strokes.from.x > coordinate.x ? this.strokes.from.x - radiusX : this.strokes.from.x + radiusX, y: this.strokes.from.y }, 304 | { x: radiusX, y: radiusX } 305 | ]; 306 | break; 307 | } 308 | this.drawGuide(true); 309 | } 310 | } 311 | }, 312 | drawGuide(closingPath: boolean) { 313 | this.redraw(true); 314 | this.$nextTick(() => { 315 | this.context.strokeStyle = this.color; 316 | this.context.lineWidth = 1; 317 | this.context.lineJoin = this.lineJoin; 318 | this.context.lineCap = this.lineCap; 319 | 320 | this.context.beginPath(); 321 | this.context.setLineDash([15, 15]); 322 | if (this.strokes.type === 'circle') { 323 | this.context.ellipse(this.guides[0].x, this.guides[0].y, this.guides[1].x, this.guides[1].y, 0, 0, Math.PI * 2); 324 | } else { 325 | this.context.moveTo(this.strokes.from.x, this.strokes.from.y); 326 | this.guides.forEach((coordinate: {x: number, y: number}) => { 327 | this.context.lineTo(coordinate.x, coordinate.y); 328 | }); 329 | if (closingPath) { 330 | this.context.closePath(); 331 | } 332 | } 333 | this.context.stroke(); 334 | }) 335 | }, 336 | drawShape(context: CanvasRenderingContext2D, strokes: any, closingPath: boolean) { 337 | context.strokeStyle = strokes.color; 338 | context.fillStyle = strokes.color; 339 | context.lineWidth = strokes.width; 340 | context.lineJoin = strokes.lineJoin === undefined ? this.lineJoin : strokes.lineJoin; 341 | context.lineCap = strokes.lineCap === undefined ? this.lineCap : strokes.lineCap; 342 | context.beginPath(); 343 | context.setLineDash([]); 344 | if (strokes.type === 'circle') { 345 | context.ellipse(strokes.coordinates[0].x, strokes.coordinates[0].y, strokes.coordinates[1].x, strokes.coordinates[1].y, 0, 0, Math.PI * 2); 346 | } else { 347 | context.moveTo(strokes.from.x, strokes.from.y); 348 | strokes.coordinates.forEach((stroke: { x: number, y: number}) => { 349 | context.lineTo(stroke.x, stroke.y); 350 | }); 351 | if (closingPath) { 352 | context.closePath(); 353 | } 354 | } 355 | if (strokes.fill) { 356 | context.fill(); 357 | } else { 358 | context.stroke(); 359 | } 360 | }, 361 | stopDraw() { 362 | if (this.drawing) { 363 | this.strokes.coordinates = this.guides.length > 0 ? this.guides : this.strokes.coordinates; 364 | this.images.push(this.strokes); 365 | this.redraw(true); 366 | this.drawing = false; 367 | this.trash = []; 368 | } 369 | }, 370 | reset() { 371 | if (!this.lock) { 372 | this.images = []; 373 | this.strokes = { 374 | type: '', 375 | coordinates: [], 376 | color: '', 377 | width: '', 378 | fill: false, 379 | lineCap: '', 380 | lineJoin: '' 381 | }; 382 | this.guides = []; 383 | this.trash = []; 384 | this.redraw(true); 385 | } 386 | }, 387 | undo() { 388 | if (!this.lock) { 389 | let strokes = this.images.pop(); 390 | if (strokes) { 391 | this.trash.push(strokes); 392 | this.redraw(true); 393 | } 394 | } 395 | }, 396 | redo() { 397 | if (!this.lock) { 398 | let strokes = this.trash.pop(); 399 | if (strokes) { 400 | this.images.push(strokes); 401 | this.redraw(true); 402 | } 403 | } 404 | }, 405 | async redraw(output: boolean) { 406 | output = typeof output !== 'undefined' ? output : true; 407 | await this.setBackground() 408 | .then(() => { 409 | this.drawAdditionalImages() 410 | }) 411 | .then(() => { 412 | let baseCanvas: HTMLCanvasElement = document.createElement('canvas') 413 | let baseCanvasContext: CanvasRenderingContext2D | null = baseCanvas.getContext('2d') 414 | baseCanvas.width = Number(this.width) 415 | baseCanvas.height = Number(this.height) 416 | 417 | if (baseCanvasContext) { 418 | this.images.forEach((stroke: any) => { 419 | if (baseCanvasContext) { 420 | baseCanvasContext.globalCompositeOperation = stroke.type === 'eraser' ? 'destination-out' : 'source-over' 421 | if (stroke.type !== 'circle' || (stroke.type === 'circle' && stroke.coordinates.length > 0)) { 422 | this.drawShape(baseCanvasContext, stroke, (stroke.type === 'eraser' || stroke.type === 'dash' || stroke.type === 'line' ? false : true)) 423 | } 424 | } 425 | }) 426 | this.context.drawImage(baseCanvas, 0, 0, Number(this.width), Number(this.height)) 427 | } 428 | }) 429 | .then(() => { 430 | if (output) { 431 | this.save(); 432 | } 433 | }); 434 | }, 435 | wrapText(context: CanvasRenderingContext2D, text: string, x: number, y: number, maxWidth : number, lineHeight: number) { 436 | const newLineRegex = /(\r\n|\n\r|\n|\r)+/g 437 | const whitespaceRegex = /\s+/g 438 | var lines = text.split(newLineRegex).filter(word => word.length > 0) 439 | for (let lineNumber = 0; lineNumber < lines.length; lineNumber++) { 440 | var words = lines[lineNumber].split(whitespaceRegex).filter(word => word.length > 0); 441 | var line = ''; 442 | 443 | for(var n = 0; n < words.length; n++) { 444 | var testLine = line + words[n] + ' '; 445 | var metrics = context.measureText(testLine); 446 | var testWidth = metrics.width; 447 | if (testWidth > maxWidth && n > 0) { 448 | if(this.watermark && (this.watermark.fontStyle && this.watermark.fontStyle.drawType && this.watermark.fontStyle.drawType === 'stroke') ) 449 | { 450 | context.strokeText(line, x, y); 451 | } 452 | else{ 453 | context.fillText(line, x, y); 454 | } 455 | line = words[n] + ' '; 456 | y += lineHeight; 457 | } 458 | else { 459 | line = testLine; 460 | } 461 | } 462 | if(this.watermark && (this.watermark.fontStyle && this.watermark.fontStyle.drawType && this.watermark.fontStyle.drawType === 'stroke') ) 463 | { 464 | context.strokeText(line, x, y); 465 | } 466 | else{ 467 | context.fillText(line, x, y); 468 | } 469 | y += words.length > 0 ? lineHeight : 0; 470 | } 471 | }, 472 | save() { 473 | let canvas: HTMLCanvasElement = document.querySelector('#'+this.canvasId); 474 | if (this.watermark) { 475 | let temp = document.createElement('canvas'); 476 | let ctx: CanvasRenderingContext2D | null = temp.getContext('2d') 477 | 478 | if (ctx) { 479 | temp.width = Number(this.width); 480 | temp.height = Number(this.height); 481 | ctx.drawImage(canvas, 0, 0, Number(this.width), Number(this.height)); 482 | 483 | this.drawWatermark(temp, ctx, this.watermark) 484 | } 485 | } else { 486 | let temp = document.createElement('canvas'); 487 | let tempCtx: CanvasRenderingContext2D | null = temp.getContext('2d') 488 | let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth 489 | let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight 490 | temp.width = Number(tempWidth) 491 | temp.height = Number(tempHeight) 492 | 493 | if (tempCtx) { 494 | tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); 495 | this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); 496 | return temp.toDataURL('image/' + this.saveAs, 1); 497 | } 498 | } 499 | }, 500 | drawWatermark(canvas: HTMLCanvasElement, ctx: CanvasRenderingContext2D, watermark: WatermarkData) { 501 | if (watermark.type === 'Image') { 502 | let imageWidth = watermark.imageStyle ? (watermark.imageStyle.width ? watermark.imageStyle.width : Number(this.width)) : Number(this.width); 503 | let imageHeight = watermark.imageStyle ? (watermark.imageStyle.height ? watermark.imageStyle.height : Number(this.height)) : Number(this.height); 504 | 505 | const image = new Image(); 506 | image.src = watermark.source; 507 | image.onload = () => { 508 | if (watermark && ctx) { 509 | ctx.drawImage(image, watermark.x, watermark.y, Number(imageWidth), Number(imageHeight)); 510 | } 511 | 512 | let temp = document.createElement('canvas'); 513 | let tempCtx: CanvasRenderingContext2D | null = temp.getContext('2d') 514 | let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth 515 | let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight 516 | temp.width = Number(tempWidth) 517 | temp.height = Number(tempHeight) 518 | 519 | if (tempCtx) { 520 | tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); 521 | this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); 522 | return temp.toDataURL('image/' + this.saveAs, 1); 523 | } 524 | } 525 | } else if (watermark.type === 'Text') { 526 | let font = watermark.fontStyle ? (watermark.fontStyle.font ? watermark.fontStyle.font : '20px serif') : '20px serif'; 527 | let align = watermark.fontStyle ? (watermark.fontStyle.textAlign ? watermark.fontStyle.textAlign : 'start') : 'start'; 528 | let baseline = watermark.fontStyle ? (watermark.fontStyle.textBaseline ? watermark.fontStyle.textBaseline : 'alphabetic') : 'alphabetic'; 529 | let color = watermark.fontStyle ? (watermark.fontStyle.color ? watermark.fontStyle.color : '#000000') : '#000000'; 530 | 531 | ctx.font = font; 532 | ctx.textAlign = align as CanvasTextAlign; 533 | ctx.textBaseline = baseline as CanvasTextBaseline; 534 | 535 | if (watermark.fontStyle && watermark.fontStyle.rotate) { 536 | let centerX, centerY; 537 | if (watermark.fontStyle && watermark.fontStyle.width) { 538 | centerX = watermark.x + Math.floor(watermark.fontStyle.width / 2); 539 | } else { 540 | centerX = watermark.x; 541 | } 542 | if (watermark.fontStyle && watermark.fontStyle.lineHeight) { 543 | centerY = watermark.y + Math.floor(watermark.fontStyle.lineHeight / 2); 544 | } else { 545 | centerY = watermark.y; 546 | } 547 | 548 | ctx.translate(centerX, centerY); 549 | ctx.rotate(watermark.fontStyle.rotate * Math.PI / 180); 550 | ctx.translate(centerX * -1, centerY * -1); 551 | } 552 | 553 | if (watermark.fontStyle && watermark.fontStyle.drawType && watermark.fontStyle.drawType === 'stroke') { 554 | ctx.strokeStyle = watermark.fontStyle.color; 555 | if (watermark.fontStyle && watermark.fontStyle.width) { 556 | this.wrapText(ctx, watermark.source, watermark.x, watermark.y, watermark.fontStyle.width, watermark.fontStyle.lineHeight); 557 | } else { 558 | ctx.strokeText(watermark.source, watermark.x, watermark.y); 559 | } 560 | } else { 561 | ctx.fillStyle = color; 562 | if (watermark.fontStyle && watermark.fontStyle.width) { 563 | this.wrapText(ctx, watermark.source, watermark.x, watermark.y, watermark.fontStyle.width, watermark.fontStyle.lineHeight); 564 | } else { 565 | ctx.fillText(watermark.source, watermark.x, watermark.y); 566 | } 567 | } 568 | 569 | let temp = document.createElement('canvas'); 570 | let tempCtx: CanvasRenderingContext2D | null = temp.getContext('2d') 571 | let tempWidth = this.outputWidth === undefined ? this.width : this.outputWidth 572 | let tempHeight = this.outputHeight === undefined ? this.height : this.outputHeight 573 | temp.width = Number(tempWidth) 574 | temp.height = Number(tempHeight) 575 | 576 | if (tempCtx) { 577 | tempCtx.drawImage(canvas, 0, 0, Number(tempWidth), Number(tempHeight)); 578 | this.$emit('update:image', temp.toDataURL('image/' + this.saveAs, 1)); 579 | return temp.toDataURL('image/' + this.saveAs, 1); 580 | } 581 | } 582 | }, 583 | isEmpty() { 584 | return this.images.length > 0 ? false : true; 585 | }, 586 | getAllStrokes() { 587 | return this.images; 588 | } 589 | }, 590 | render() { 591 | if (isVue2) { 592 | return h('canvas', { 593 | attrs: { 594 | id: this.canvasId, 595 | width: Number(this.width), 596 | height: Number(this.height) 597 | }, 598 | style: { 599 | 'touchAction': 'none', 600 | // @ts-ignore 601 | ...this.styles 602 | }, 603 | class: this.classes, 604 | on: { 605 | mousedown: (event: Event) => this.startDraw(event), 606 | mousemove: (event: Event) => this.draw(event), 607 | mouseup: () => this.stopDraw(), 608 | mouseleave: () => this.stopDraw(), 609 | touchstart: (event: Event) => this.startDraw(event), 610 | touchmove: (event: Event) => this.draw(event), 611 | touchend: () => this.stopDraw(), 612 | touchleave: () => this.stopDraw(), 613 | touchcancel: () => this.stopDraw(), 614 | pointerdown: (event: Event) => this.startDraw(event), 615 | pointermove: (event: Event) => this.draw(event), 616 | pointerup: () => this.stopDraw(), 617 | pointerleave: () => this.stopDraw(), 618 | pointercancel: () => this.stopDraw(), 619 | }, 620 | ...this.$props 621 | }); 622 | } 623 | return h('canvas', { 624 | id: this.canvasId, 625 | height: Number(this.height), 626 | width: Number(this.width), 627 | style: { 628 | 'touchAction': 'none', 629 | // @ts-ignore 630 | ...this.styles 631 | }, 632 | class: this.classes, 633 | onMousedown: ($event: Event) => this.startDraw($event), 634 | onMousemove: ($event: Event) => this.draw($event), 635 | onMouseup: () => this.stopDraw(), 636 | onMouseleave: () => this.stopDraw(), 637 | onTouchstart: ($event: Event) => this.startDraw($event), 638 | onTouchmove: ($event: Event) => this.draw($event), 639 | onTouchend: () => this.stopDraw(), 640 | onTouchleave: () => this.stopDraw(), 641 | onTouchcancel: () => this.stopDraw(), 642 | onPointerdown: ($event: Event) => this.startDraw($event), 643 | onPointermove: ($event: Event) => this.draw($event), 644 | onPointerup: () => this.stopDraw(), 645 | onPointerleave: () => this.stopDraw(), 646 | onPointercancel: () => this.stopDraw() 647 | }); 648 | } 649 | }); 650 | -------------------------------------------------------------------------------- /src/entry.ts: -------------------------------------------------------------------------------- 1 | import VueDrawingCanvas from "./VueDrawingCanvas" 2 | export default VueDrawingCanvas -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "esnext", 5 | "strict": true, 6 | "noImplicitAny": true, 7 | "declaration": true, 8 | "declarationDir": "dist/types", 9 | "noUnusedLocals": true, 10 | "noUnusedParameters": true, 11 | "importHelpers": true, 12 | "moduleResolution": "node", 13 | "experimentalDecorators": true, 14 | "esModuleInterop": true, 15 | "allowSyntheticDefaultImports": true, 16 | "sourceMap": true, 17 | "baseUrl": ".", 18 | "newLine": "lf", 19 | "types": [ 20 | "node", 21 | "vue" 22 | ], 23 | "paths": { 24 | "@/*": [ 25 | "src/*" 26 | ] 27 | }, 28 | "plugins": [ 29 | { 30 | "transform":"@zerollup/ts-transform-paths", 31 | "exclude": ["*"] 32 | } 33 | ], 34 | "lib": [ 35 | "esnext", 36 | "dom", 37 | "dom.iterable", 38 | "scripthost" 39 | ] 40 | }, 41 | "exclude": [ 42 | "node_modules", 43 | "dist" 44 | ] 45 | } 46 | --------------------------------------------------------------------------------