├── .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 | [](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 | [](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 |
58 |
59 |
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 |
--------------------------------------------------------------------------------