├── .gitignore ├── README.md ├── _config.yml ├── autopush.bat ├── babel.config.js ├── docs ├── css │ ├── app.6838266e.css │ └── chunk-vendors.2ac5db4b.css ├── favicon.ico ├── fonts │ ├── element-icons.535877f5.woff │ └── element-icons.732389de.ttf ├── index.html └── js │ ├── app.20cd3089.js │ ├── app.20cd3089.js.map │ ├── chunk-vendors.2c95c892.js │ └── chunk-vendors.2c95c892.js.map ├── package-lock.json ├── package.json ├── public ├── favicon.ico └── index.html ├── src ├── App.vue ├── assets │ ├── ali.jpg │ ├── demo.png │ ├── icon │ │ ├── iconfont.js │ │ └── iconfont.svg │ ├── logo.png │ └── wx.jpg ├── components │ └── imgDraw │ │ ├── cursor.js │ │ ├── img.js │ │ └── imgDraw.vue ├── main.js └── plugins │ └── element.js └── vue.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | # /dist 4 | deploy.sh 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 | pnpm-debug.log* 14 | 15 | # Editor directories and files 16 | .idea 17 | .vscode 18 | *.suo 19 | *.ntvs* 20 | *.njsproj 21 | *.sln 22 | *.sw? 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Vue image-painter 2 | 3 | 图片涂鸦、绘制、标注 4 | 5 | [在线预览(GitHub)](https://kevin123x.github.io/Vue-ImagePainter) 6 | 7 | [在线预览(Gitee)](http://keivn.vip.com.gitee.io/vue-imagepainter) 8 | 9 | ### 支持功能 10 | 11 | 1.画笔颜色 🎨 12 | 13 | 2.画笔粗细 ✏️ 14 | 15 | 3.画布放大 🔍 16 | 17 | 4.画布缩小 🔍 18 | 19 | 5.清除画布 🧹 20 | 21 | 6.保存图片 🏔 22 | 23 | 7.回退一步 ↩️ 24 | 25 | 8.前进一步 ↪️ 26 | 27 | ### 画笔工具类型 28 | 29 | 1.画笔 ✏️ 30 | 31 | 2.直线 📏 32 | 33 | 3.圆形 ⚪️ 34 | 35 | 4.矩形 🔲 36 | 37 | 5.橡皮 🧽 38 | 39 | 6.文字 📝 40 | 41 | ## 实现思路 42 | 43 | 创建三个不同作用的画布 44 | 45 | ``` 46 | // 用于绘制的画板 47 | this.canvas_front = document.getElementById("ctx_front"); 48 | 49 | // 用于生成绘制后效果图的画板 50 | this.canvas_back = document.getElementById("ctx_back"); 51 | 52 | // 底图画板,橡皮擦除时获取像素放到绘制画板中,达到不擦出底图的效果 53 | this.canvas_base = document.getElementById("ctx_base"); 54 | ``` 55 | 56 | 鼠标按下判断工具类型,记录初始位置 57 | 58 | ``` 59 | let mousedown = (e) => { 60 | this.ctx_front.strokeStyle = this.defaultColor; 61 | this.ctx_front.lineWidth = this.slide; 62 | e = e || window.event; 63 | sx = e.clientX - this.canvas_front.offsetLeft; 64 | sy = e.clientY - this.canvas_front.offsetTop; 65 | 66 | this.ctx_front.moveTo(sx, sy); 67 | this.canDraw = true; 68 | ... 69 | ``` 70 | 71 | 若是输入文字则定位输入框到画布中,失焦或回车文字绘制到画布中 72 | 73 | ``` 74 | this.handleTextBlur(); 75 | this.text = ""; 76 | text.style.fontSize = 14 + this.slide * 10 + "px"; 77 | text.style.color = this.defaultColor; 78 | text.style.left = 79 | e.offsetX + this.canvas_front.offsetLeft - 20 + "px"; 80 | text.style.top = 81 | e.offsetY + this.canvas_front.offsetTop - 10 + "px"; 82 | text.style.zIndex = 10; 83 | text.style.display = "block"; 84 | this.tl = e.offsetX - 20; 85 | this.tt = e.offsetY + 10; 86 | break; 87 | 88 | ... 89 | 90 | handleTextBlur() { 91 | let text = document.getElementById("text"); 92 | this.ctx_front.font = `300 ${text.style.fontSize} sans-serif`; 93 | this.ctx_front.fillStyle = this.defaultColor; 94 | this.ctx_front.fillText(this.text, this.tl, this.tt); 95 | text.style.display = "none"; 96 | this.handleSaveCanvasStore(); 97 | } 98 | ``` 99 | 100 | 鼠标移动,根据工具类型绘制不同路径 101 | 102 | ``` 103 | let mousemove = (e) => { 104 | e = e || window.event; 105 | mx = e.clientX - this.canvas_front.offsetLeft; 106 | my = e.clientY - this.canvas_front.offsetTop; 107 | const cbx = this.ctx_base.getImageData( 108 | e.offsetX - this.slide / 2, 109 | e.offsetY - this.slide / 2, 110 | this.slide * 2, 111 | this.slide * 2 112 | ); 113 | ... 114 | ``` 115 | 116 | 若工具类型是橡皮,则从 canvas_base 中获取底图像素放回 canvas_front 中 117 | 118 | ``` 119 | const cbx = this.ctx_base.getImageData( 120 | e.offsetX - this.slide / 2, 121 | e.offsetY - this.slide / 2, 122 | this.slide * 2, 123 | this.slide * 2 124 | ); 125 | 126 | ... 127 | 128 | this.ctx_front.putImageData( 129 | cbx, 130 | e.offsetX - this.slide / 2, 131 | e.offsetY - this.slide / 2 132 | ); 133 | ... 134 | ``` 135 | 136 | 鼠标抬起,从绘制画布 canvas_front 生成绘制图并绘制到 canvas_back 中,然后再重绘 canvas_front 137 | 138 | ``` 139 | handleSaveCanvasStore() { 140 | let url = this.canvas_front.toDataURL(); 141 | let image = new Image(); 142 | image.src = url; 143 | image.onload = () => { 144 | this.ctx_front.clearRect( 145 | 0, 146 | 0, 147 | this.canvas_front.width, 148 | this.canvas_front.height 149 | ); 150 | this.ctx_front.drawImage(image, 0, 0, image.width, image.height); 151 | this.ctx_back.drawImage(image, 0, 0, image.width, image.height); 152 | const url2 = this.canvas_back.toDataURL(); 153 | this.currentImg.url = url2; 154 | this.currentImg.index += 1; 155 | this.canvasStore.push(url2); 156 | this.prevDis = false; 157 | console.log(this.canvasStore); 158 | }; 159 | }, 160 | 161 | ``` 162 | 最终保存图片 163 | ``` 164 | let canvas = document.getElementById("ctx_back"); 165 | this.$refs.download.href = canvas.toDataURL(); 166 | this.$refs.download.click(); 167 | ``` 168 | ### Project setup 169 | 170 | ``` 171 | npm install 172 | ``` 173 | 174 | ### Compiles and hot-reloads for development 175 | 176 | ``` 177 | npm run serve 178 | ``` 179 | 180 | ### Compiles and minifies for production 181 | 182 | ``` 183 | npm run build 184 | ``` 185 | 186 | ### Lints and fixes files 187 | 188 | ``` 189 | npm run lint 190 | ``` 191 | 192 | ### 感谢支持 193 | 194 | > 开发不易,且用且珍惜 ,随手给个星,开心到起飞 195 | -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-merlot -------------------------------------------------------------------------------- /autopush.bat: -------------------------------------------------------------------------------- 1 | git add . 2 | set yyyy=%date:~,4% 3 | set mm=%date:~5,2% 4 | set day=%date:~8,2% 5 | set "YYYYmmdd=%yyyy%%mm%%day%" 6 | set "YYYYmmdd=%YYYYmmdd: =0%" 7 | echo "YYYYmmdd%YYYYmmdd%YYYYmmdd" 8 | set hh=%time:~0,2% 9 | set mi=%time:~3,2% 10 | set ss=%time:~6,2% 11 | set "hhmiss=%hh%%mi%%ss%" 12 | set "hhmiss=%hhmiss: =0%" 13 | echo "hhmiss%Time%hhmiss" 14 | echo %hhmiss% 15 | set "hhmiss=%hhmiss::=0%" 16 | set "hhmiss=%hhmiss: =0%" 17 | set "timestamp=%YYYYmmdd%%hhmiss%" 18 | set /p input=更新: 19 | git commit -m "%timeStamp%%input%" 20 | git push 21 | 22 | goto exit -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /docs/css/app.6838266e.css: -------------------------------------------------------------------------------- 1 | [data-v-34058e9c]{margin:0;padding:0;box-sizing:border-box}.icon[data-v-34058e9c]{width:1em;height:1em;vertical-align:-.15em;fill:currentColor;overflow:hidden}[data-v-34058e9c] .el-dialog{display:flex;flex-direction:column;align-items:center}[data-v-34058e9c] .el-dialog>div{width:100%;box-sizing:border-box}[data-v-34058e9c] .el-dialog .el-dialog__header{padding:0 20px}[data-v-34058e9c] .el-dialog .el-dialog__header p{padding:20px 0;border-bottom:1px solid #bdbdbd}[data-v-34058e9c] .el-dialog .el-dialog__body{padding:10px 20px;flex:1;height:0;padding-top:0;position:relative;overflow:hidden}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body{height:100%;overflow-y:auto;padding-top:20px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board{position:relative;min-height:100%}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board canvas{position:absolute;margin:0 auto;left:0;right:0;top:0}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board #ctx_front{z-index:5}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board #ctx_back{z-index:3}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board #ctx_base{z-index:1}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .board #text{position:absolute;z-index:-1;resize:none;outline:none;border:1px dashed #eee;overflow:hidden;background:transparent;line-height:30px;display:none}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .tools{width:100%;position:absolute;display:flex;z-index:5;background:#fff;transition:all .2s ease-in-out}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings{top:0;left:50%;transform:translateX(-50%);z-index:10;padding:5px 10px;border-bottom-left-radius:4px;border-bottom-right-radius:4px;border:1px solid #eee;border-top:0;width:auto}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .tool_item{display:flex;align-items:center;flex-wrap:nowrap}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .tool_item:not(:last-of-type){margin-right:25px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .go_up{margin-right:0!important}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings button{display:flex;align-items:center;justify-content:center;padding:5px 10px;background:#fff;border:1px solid #eee;outline:none;cursor:pointer;position:relative;flex-wrap:nowrap}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings button svg{color:#333;font-size:18px;margin-right:5px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings button span{white-space:nowrap}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings button /deep/ .el-color-picker{position:absolute;left:0;top:0;width:100%;height:100%}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings button /deep/ .el-color-picker .el-color-picker__trigger{width:100%;height:100%;opacity:0;filter:alpha(opacity=0)}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .slide{width:150px;display:flex;flex-direction:row;flex-wrap:nowrap;align-items:center}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .slide svg{font-size:18px;margin-right:5px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .slide /deep/ .el-slider{flex:1;width:0;margin-left:10px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .slide /deep/ .el-slider .el-slider__button-wrapper .el-slider__button{width:12px;height:12px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .pull{position:absolute;right:20px;bottom:-45px;display:flex;flex-direction:column;align-items:center;justify-content:flex-start;margin-right:0}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .pull .line{width:2px;height:30px;background:#1e90ff}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .settings .pull .round{width:15px;height:15px;border-radius:50%;background:#1e90ff;cursor:pointer}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars{top:100px;right:30px;z-index:10;padding:15px;border-top-left-radius:4px;border-bottom-left-radius:4px;border:1px solid #eee;width:auto;display:flex;flex-direction:column}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item{cursor:pointer}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item:not(:last-of-type){margin-bottom:15px;border-bottom:1px solid #ddd;padding-bottom:10px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item svg{font-size:24px;margin-right:8px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item span{font-size:18px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item:hover span,[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .tool_item:hover svg{color:#1e90ff}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .activeTool{border-color:#1e90ff!important}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .activeTool span,[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .activeTool svg{color:#1e90ff}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .arrow{width:20px;height:20px;text-align:center;line-height:20px;border-radius:50%;border:1px solid #606266;color:#606266;position:absolute;left:-10px;background:#fff;top:50%;transform:translateY(-50%);cursor:pointer}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .el-icon-s-tools{left:-30px;width:30px;height:30px;line-height:30px;font-size:20px;color:#1e90ff;border-color:#1e90ff}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .bars .el-icon-arrow-right{left:calc(100% - 10px)}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .hideTools{right:-100px}[data-v-34058e9c] .el-dialog .el-dialog__body .d_body .noExpand{top:-50px}[data-v-34058e9c] .el-dialog .el-dialog__footer{text-align:center!important}[data-v-34058e9c] .el-dialog .el-dialog__footer span button{padding-left:40px;padding-right:40px}[data-v-34058e9c] .el-dialog .el-dialog__footer span button:first-of-type{margin-right:50px}[data-v-34058e9c] .el-dialog .el-dialog__footer span button:last-of-type{margin-left:50px}#app{font-family:Avenir,Helvetica,Arial,sans-serif;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale;text-align:center;color:#2c3e50;margin-top:60px} -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevin123X/Vue-ImagePainter/b8b5ea4e04d4f20d663b4c47e756fdade06d0252/docs/favicon.ico -------------------------------------------------------------------------------- /docs/fonts/element-icons.535877f5.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevin123X/Vue-ImagePainter/b8b5ea4e04d4f20d663b4c47e756fdade06d0252/docs/fonts/element-icons.535877f5.woff -------------------------------------------------------------------------------- /docs/fonts/element-icons.732389de.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kevin123X/Vue-ImagePainter/b8b5ea4e04d4f20d663b4c47e756fdade06d0252/docs/fonts/element-icons.732389de.ttf -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 |