需求:图片比例自适应屏幕,放大缩小,马赛克,画笔粗细可调整;
具体实现如下图:
图中可看出马赛克画笔粗细的不同,打码的大小也不同, 第四个按钮是全图重置,点击橡皮按住A/t键和鼠标左键可局部擦除已打码的部分。
我们项目用的是若依ruoyi的框架,vue+elementUI,所以代码中含有一些框架自带的API,若和你的项目不同,可以忽略,只看js或者vue部分。
话不多说上代码:
先是页面的一些基础功能,图片比例自适应屏幕,放大缩小,图片保存等。
<template><div class="app-container image_mosaic" v-loading="loading"><el-card class="box-card"><div slot="header" class="clearfix"><el-popoverplacement="bottom"style="margin-right:10px;"trigger="hover"><div><div @click="selectType(1)" class="mosaic_type"><div class="pen_type" style="height:2px;"></div></div><div @click="selectType(2)" class="mosaic_type"><div class="pen_type" style="height:4px;"></div></div><div @click="selectType(3)" class="mosaic_type"><div class="pen_type" style="height:6px;"></div></div><div @click="selectType(4)" class="mosaic_type"><div class="pen_type" style="height:8px;"></div></div></div><el-button el-button slot="reference" @click="visible = !visible" title="马赛克" type="primary" :plain="isEditImg" id="drawSwitch"><img class="btn_icons" src="@/assets/icons/edit.png"/></el-button></el-popover><el-button title="橡皮擦" type="primary" :plain="isClearImg" id='clearSwitch'><img class="btn_icons" src="@/assets/icons/eraser.png"/></el-button><el-button title="全图马赛克" type="primary" id='drawAll'><img class="btn_icons" src="@/assets/icons/mosaicFull.png"/></el-button><el-button title="重置" type="primary" id='clearAll'><img class="btn_icons" src="@/assets/icons/back.png"/></el-button><el-button title="下载" type="primary" @click="download"><img class="btn_icons" src="@/assets/icons/download.png"/></el-button><el-button title="1:1" v-if="nowRate==100" type="primary" id='resizeImg' @click="resizeImg" ><img class="btn_icons" src="@/assets/icons/resize-01.png"/></el-button><el-button title="1:1" v-else type="primary" id='resizeImg' @click="resizeImg" ><img class="btn_icons" src="@/assets/icons/resize-02.png"/></el-button><el-button title="放大" type="primary" id='zoomIn' @click="zoomIn" ><img class="btn_icons" src="@/assets/icons/zoomIn.png"/></el-button><el-button title="缩小" type="primary" id='zoomOut' @click="zoomOut"><img class="btn_icons" src="@/assets/icons/zoomOut.png"/></el-button><el-button title="保存" :disabled="isSave" style="float: right;display: flex;align-items: center;" type="primary" @click="dialogUpload" ><img class="btn_icons" style="margin-top:-3px" src="@/assets/icons/save.png"/> 保存</el-button></div><div class="out_box"><div id="canvasBox"><canvas id="canvas" v-drag="{isEditImg,isClearImg}" class="drag_box"></canvas></div><div v-show="showNowRate" class="rate_box">{{nowRate}}%</div></div></el-card></div></template><script>import Mosaic from './mosaic'import{ fileExport } from "@/api/project/newProject";import { updateFeedback } from "@/api/inforDelivery/questionManage.js";let globalMosaic = {}let MouseEvents = {}export default {name: 'editPhoto',data(){return{visible: false,showAlert: false,loading: false,isEditImg: false,isClearImg: false,isSave: false,id:'',url:'',penNum:2,queriesId:'',feedback:'',flagId:'',fileUrl:'',sort:0,sizeRate: 0.1,imgUrl:'',nowRate:100,showNowRate: false,timer:'',that:this,hasRefresh:false}},directives:{drag:{update:function(el,binding){let dragBox = elif(binding.value.isEditImg || binding.value.isClearImg){dragBox.onmousedown = nulldragBox.style.cursor = 'default'}else{dragBox.style.cursor = 'move'dragBox.onmousedown = (e)=>{let boxX = e.clientX - dragBox.offsetLeftlet boxY = e.clientY - dragBox.offsetTopdocument.onmousemove = (e) =>{let left = e.clientX - boxXlet top = e.clientY - boxYdragBox.style.left = `${left}px`dragBox.style.top = `${top}px`}document.onmouseup = (e) =>{document.onmousemove = nulldocument.onmouseup = null}}}}}},activated(){if(!this.$store.state.questionPreview.isEditPhoto){this.init()this.$mit('SET_IS_EDIT_PHOTO', true)}},deactivated(){var tempImg = document.getElementById('canvas').toDataURL('image/jpeg')this.$mit('SET_EDIT_FORMDATA_URL', tempImg)},computed:{savePhoto(){return this.$store.state.questionPreview.savePhoto},visitedViews() {return this.$store.state.tagsView.visitedViews},},watch:{savePhoto:function(newVal,oldVal){if(newVal==true){this.loading = truevar tempImg = document.getElementById('canvas').toDataURL('image/jpeg')let file = this.base64ImgtoFile(tempImg,'测试图片名称')// 得到File对象let formData = new FormData() //FormData对象,添加参数只能通过append('key', value)的形式添加this.feedback = this.$route.query.feedbackif(Array.isArray(this.feedback)){this.feedback = this.feedback.map(item=>{return item}).join("|");}else{this.feedback = this.feedback}this.queriesId = this.$route.query.queriesIdthis.flagId = this.$route.query.flagIdthis.fileUrl = this.$route.query.fileUrlthis.sort = this.$route.query.sort||''formData.append('file', file) //添加文件对象formData.append('queriesId', this.queriesId) //添加文件对象formData.append('feedback', this.feedback) //添加文件对象formData.append('flagId', this.flagId) //添加文件对象formData.append('fileUrl', this.fileUrl) //添加文件对象formData.append('sort', this.sort) //添加文件对象this.isSave = trueupdateFeedback(formData).then(res=>{this.loading = falsethis.isSave = falseif(res.responseCode==0){this.$message.success(res.responseMsg || '修改成功')let url = res.responseBody.fileUrllet feedback = res.responseBody.fileUrlList.join('|')this.$store.dispatch("tagsView/delView", this.$route);this.$router.push({path:`/inforDelivery/imagePreview/${this.id}`,query:{url,queriesId:this.queriesId,feedback,flagId:this.flagId,fileUrl:this.feedback}});setTimeout(()=>{this.$tab.refreshPage();},500)this.$mit('SET_SAVE_PHOTO', false)}else{this.$message.error('修改失败')}}).catch(res=>{this.loading = falsethis.isSave = falsethis.$message.error('修改失败')})}}},methods:{init(){globalMosaic = nullMouseEvents = nullthis.$mit('SET_EDIT_FORMDATA', "")this.$mit('SET_EDIT_FORMDATA_URL', {})this.showAlert= falsethis.loading= falsethis.isEditImg= falsethis.isClearImg= falsethis.isSave= falsethis.showNowRate= falsethis.nowRate = 100this.timer = ''this.id = this.$route.query.idthis.url = this.$route.query.urlthis.queriesId = this.$route.query.queriesIdthis.feedback = this.$route.query.feedbackif(Array.isArray(this.feedback)){this.feedback = this.feedback.map(item=>{return item}).join("|");}else{this.feedback = this.feedback}this.flagId = this.$route.query.flagIdthis.fileUrl = this.$route.query.fileUrlthis.sort = this.$route.query.sort||''let editFormData = {id:this.id,queriesId:this.queriesId,feedback:this.feedback,flagId:this.flagId,fileUrl:this.fileUrl,sort:this.sort ||'',}this.$mit('SET_EDIT_FORMDATA', editFormData)this.$mit('SET_IS_EDIT_PHOTO', false)this.getFiles(this.url)},selectType(e){ //选择画笔类型this.penNum = e*2let nowPenDiv = document.getElementsByClassName('mosaic_type')let nowPen = document.getElementsByClassName('pen_type')console.log(nowPenDiv[e-1].style,'nowPenDiv[e-1]');nowPenDiv.forEach((item,index)=>{item.style.backgroundColor = '#fff'item.onmouseover = function(){item.style.border='1px dashed #1890ff'}item.onmouseleave = function(){item.style.border='none'}})nowPen.forEach((item,index)=>{nowPenDiv[e-1].style.backgroundColor = '#1890ff'nowPen[e-1].style.backgroundColor = '#fff'},zoomIn(){ //图片放大let that = thisif(this.sizeRate<2){clearTimeout(this.timer)let canvas = document.getElementById('canvasBox')this.sizeRate += .1this.nowRate += 10this.showNowRate = truecanvas.style.transform=`scale(${this.sizeRate})`this.timer = setTimeout(()=>{that.showNowRate = false},1500)}else{this.showNowRate = trueclearTimeout(that.timer)that.timer = setTimeout(()=>{that.showNowRate = false},1500)return}},zoomOut(){ //图片缩小let that = thisclearTimeout(that.timer)if(this.sizeRate-.1>0.3){this.sizeRate -= .1this.nowRate -= 10this.showNowRate = truelet canvas = document.getElementById('canvasBox')canvas.style.transform=`scale(${this.sizeRate})`this.timer = setTimeout(()=>{that.showNowRate = false},1500)}else{this.showNowRate = truethis.timer = setTimeout(()=>{that.showNowRate = false},1500)return}},resizeImg(){let canvasBox = document.getElementById('canvasBox')let canvas = document.getElementById('canvas')canvasBox.style.transform='scale(1)'this.sizeRate = 1this.nowRate = 100canvas.style.left = ''canvas.style.top = ''},initImage(){ //初始化图片大小————1:1自适应屏幕let out_box = document.getElementsByClassName('out_box')let box_width = out_box[0].offsetWidthlet box_height = out_box[0].offsetHeightthis.nowRate = 100this.showNowRate = truethis.initMosaic(this.imgUrl, box_width, box_height)},drawImageToCanvas(url, width, height) { //canvas画图片let that = thislet canvas = document.getElementById('canvas')let ctx = canvas.getContext('2d')let rate = 0return new Promise((resolve, reject) => {const image = new Image()image.crossOrigin = 'Annoymous'image.src = ''image.onload = function () {if(image.width<image.height){ //若图片 高 大于 宽 H>Wcanvas.width = widthrate = width/image.widthcanvas.height = image.height*rateif(canvas.height>height){canvas.height = heightrate = height/image.heightcanvas.width = image.width*rate}}else {canvas.height = heightrate = height/image.heightcanvas.width = image.width*rateif(canvas.width>width){canvas.width = widthrate = width/image.widthcanvas.height = image.height*rate}}if(image.width<=width && image.height<=height){rate = 1}canvas.width = image.widthcanvas.height = image.heightctx.drawImage( this, 0, 0, canvas.width, canvas.height)canvas.style.transform=`scale(${rate})`that.sizeRate = 1that.loading = falsethat.timer = setTimeout(()=>{that.showNowRate = false},1500)resolve(ctx)ctx = nullcanvas = nullthat.resizeImg()}image.src = url})},initMosaic (url, width, height) { //初始化马赛克let that = thisthis.drawImageToCanvas(url, width, height).then(ctx => {this.isEditImg = falsethis.isClearImg = falseglobalMosaic = new Mosaic(ctx, {tileWidth: 10*this.penNum, //此处考虑到马赛克一般由四个正方形组成,所以宽高都一致,且动态获取画笔粗细 this.penNumbrushSize: 2,})MouseEvents = {init () {globalMosaic.context.canvas.addEventListener('mousedown', MouseEvents.mousedown)},mousedown () {globalMosaic.context.canvas.addEventListener('mousemove', MouseEvents.mousemove)document.addEventListener('mouseup', MouseEvents.mouseup)},mousemove (e) {let X = e.offsetXlet Y = e.offsetYif (that.isClearImg) {globalMosaic.eraseTileByPoint(X, Y,parseInt(10*that.penNum),2)}if(that.isEditImg){that.$mit('SET_IS_EDIT_PHOTO', true)globalMosaic.drawTileByPoint(X, Y,parseInt(10*that.penNum),2)}},mouseup () {globalMosaic.context.canvas.removeEventListener('mousemove', MouseEvents.mousemove)document.removeEventListener('mouseup', MouseEvents.mouseup)},close () {globalMosaic.context.canvas.removeEventListener('mousedown', MouseEvents.mousedown)globalMosaic.context.canvas.removeEventListener('mousemove', MouseEvents.mousemove)}}MouseEvents.init()document.querySelector('#drawSwitch').addEventListener('click', () => { //点击编辑按钮this.$nextTick(()=>{this.isEditImg = !this.isEditImgthis.isClearImg = false})console.log(this.isEditImg,'this.isEditImg');if(!this.isEditImg){MouseEvents.init()}else{MouseEvents.close()}})document.querySelector('#clearSwitch').addEventListener('click', () => { //点击编辑按钮 this.$nextTick(()=>{this.isClearImg = !this.isClearImgthis.isEditImg = false})if(!this.isClearImg){MouseEvents.init()}else{MouseEvents.close()}})document.querySelector('#drawAll').addEventListener('click', () => { //点击全图打码按钮this.$mit('SET_IS_EDIT_PHOTO', true)this.isEditImg = falsethis.isClearImg = falseglobalMosaic.drawAllTiles(parseInt(10*that.penNum))})document.querySelector('#clearAll').addEventListener('click', () => { //点击重置图片按钮// this.$mit('SET_IS_EDIT_PHOTO', false)this.isEditImg = falsethis.isClearImg = falseglobalMosaic.eraseAllTiles(parseInt(10*that.penNum))})})},// 保存dialogUpload () {console.log(this.$route.query,'this.$route.query');this.loading = truevar tempImg = document.getElementById('canvas').toDataURL('image/jpeg')let file = this.base64ImgtoFile(tempImg,'测试图片名称')// 得到File对象let formData = new FormData() //FormData对象,添加参数只能通过append('key', value)的形式添加let params = this.$store.state.questionPreview.editFormDatathis.feedback = params.feedbackthis.queriesId = params.queriesIdthis.flagId = params.flagIdthis.fileUrl = params.fileUrlthis.sort = params.sort||''this.id = params.idformData.append('file', file) //添加文件对象formData.append('queriesId', this.queriesId) //添加文件对象formData.append('feedback', this.feedback) //添加文件对象formData.append('flagId', this.flagId) //添加文件对象formData.append('fileUrl', this.fileUrl) //添加文件对象formData.append('sort', this.sort) //添加文件对象this.isSave = trueupdateFeedback(formData).then(res=>{this.loading = falsethis.isSave = falseif(res.responseCode==0){// console.log(res.responseBody);this.$message.success(res.responseMsg || '修改成功')let url = res.responseBody.fileUrllet feedback = res.responseBody.fileUrlList.join('|')// console.log(params);this.$store.dispatch("tagsView/delView", this.$route);this.$router.push({path:`/inforDelivery/imagePreview/${params.id}`,query:{url,queriesId:params.queriesId,feedback,flagId:params.flagId,fileUrl:params.feedback}});setTimeout(()=>{this.$tab.refreshPage();},500)this.$mit('SET_SAVE_PHOTO', false)}else{this.$message.error('修改失败')}}).catch(res=>{this.loading = falsethis.isSave = falsethis.$message.error('修改失败')})},// // 图片转base64base64ImgtoFile (dataurl, filename = 'file') {let arr = dataurl.split(',')const mime = arr[0].match(/:(.*?);/)[1]const suffix = mime.split('/')[1]const bstr = atob(arr[1])let n = bstr.lengtharr = nullconst u8arr = new Uint8Array(n)while (n--) {u8arr[n] = bstr.charCodeAt(n)}return new File([u8arr], `${filename}.${suffix}`, {type: mime})},getFiles(fileUrl) {let params = {fileUrl}this.loading = truefileExport(params).then(res=>{this.loading = falselet imgUrl = window.URL.createObjectURL(new Blob([res],{type: "image/jpeg"}));res = nullthis.imgUrl = imgUrlthis.initImage()imgUrl = null}).catch(res=>{this.loading = false// this.$message.error('图片加载失败')})},download(){ //下载已打码的图片var tempImg = document.getElementById('canvas').toDataURL('image/jpeg')let file = this.base64ImgtoFile(tempImg, this.$route.query.url)// 得到File对象let formData = new FormData() //FormData对象,添加参数只能通过append('key', value)的形式添加 formData.append('file', file) //添加文件对象let imgUrl = window.webkitURL.createObjectURL(file) || window.URL.createObjectURL(file) // imgUrl图片网络路径let a = document.createElement('a')a.href = imgUrla.target="_blank"a.download = this.$route.query.url// 下载文件的名字a.click()file = nulla.remove()}},beforeDestroy(){this.$mit('SET_IS_EDIT_PHOTO', false) //在vuex中修改是否已遍及过图片的判断,来展示组织弹出昂let canvas = document.getElementById('canvas')canvas.remove()globalMosaic = nullMouseEvents = null}}</script><style>.out_box {height:75vh;overflow: hidden;}#canvasBox{width:90%;overflow-y: hidden;margin:0 auto;display: flex;justify-content: center;align-items: center;height: 75vh;overflow: hidden;}.btn_group{text-align:center;margin-top: 1%;}.btn_icons{display: inline-block;width: 18px;height: 18px;}.rate_box {position:absolute;right: 47.5%;top:50%;color: #fff;font-size: 20px;width: 120px;height: 40px;line-height: 40px;text-align: center;background-color: #5C5B5B;border-radius: 25px;}.drag_box {position: absolute;}.mosaic_type {height: 30px;margin: 5px auto;display: flex;align-items: center;justify-content: center;}.pen_type{background-color: #46a6ff;width: 90px;}.mosaic_type:hover {border:1px dashed #1890ff;}.mosaic_type .el-radio-button__inner {border: none !important;border-radius: 3px !important;}</style>
下面是马赛克纯js内容,参考了一些大神们的代码,还有公司巨佬idea的支持,本来是用了git上的一个插件做马赛克这部分功能,但是由于插件不能满足调整画笔大小的功能,又在插件的基础上自行封装了;
/*** {* context,* imageData,* width,* height,* tileWidth,* tileHeight,* tileRowSize,* tileColumnSize,* tiles: [{*row,*column,*pixelWidth,*pixelHeight,*data,*color,*isFilled,* }, ...]* }*/class Mosaic {constructor(context, { tileWidth = 10, brushSize = 3 } = {}) {const { canvas } = context;this.context = context;this.brushSize = brushSize;this.width = canvas.width;this.height = canvas.height;this.tileWidth = tileWidth;// this.tileHeight = tileHeight;const { width, height } = this;this.imageData = context.getImageData(0, 0, width, height).data;this.tileRowSize = Math.ceil(height / this.tileWidth);this.tileColumnSize = Math.ceil(width / this.tileWidth);this.tiles = []; // All image tiles.// Set tiles.for (let i = 0; i < this.tileRowSize; i++) {for (let j = 0; j < this.tileColumnSize; j++) {const tile = {row: i,column: j,pixelWidth: this.tileWidth,// pixelHeight: tileHeight,pixelHeight: this.tileWidth,};if (j === this.column - 1) { // Last columntile.pixelWidth = width - (j * this.tileWidth);}if (i === this.row - 1) { // Last row// tile.pixelHeight = height - (i * tileHeight);tile.pixelHeight = height - (i * this.tileWidth);}// Set tile data;const data = [];// const pixelPosition = this.width * 4 * this.tileHeight * tile.row + tile.column * this.tileWidth * 4;const pixelPosition = this.width * 4 * this.tileWidth * tile.row + tile.column * this.tileWidth * 4;for (let i = 0, j = tile.pixelHeight; i < j; i++) {const position = pixelPosition + this.width * 4 * i;data.push.apply(data, this.imageData.slice(position, position + tile.pixelWidth * 4));};tile.data = data;this.tiles.push(tile);}}}drawTile(tiles,tileWidth) {tiles = [].concat(tiles);tiles.forEach((tile,index) => {if (tile.isFilled) {return false; // Already filled.}if (!tile.color) {let dataLen = tile.data.length;let r = 0, g = 0, b = 0, a = 0;for (let i = 0; i < dataLen; i += 4) {r += tile.data[i];g += tile.data[i + 1];b += tile.data[i + 2];a += tile.data[i + 3];}// Set tile color.let pixelLen = dataLen / 4;tile.color = {r: parseInt(r / pixelLen, 10),g: parseInt(g / pixelLen, 10),b: parseInt(b / pixelLen, 10),a: parseInt(a / pixelLen, 10),};}const color = tile.color;this.context.fillStyle=`rgba(${color.r}, ${color.g}, ${color.b}, ${color.a /255})`;let x,y;if(index==0 || index == 2){x = tile.column * this.tileWidth;}else{x = (tile.column * this.tileWidth) +(tileWidth-this.tileWidth);}if(index==2 || index == 3){y = (tile.row * this.tileWidth) +(tileWidth-this.tileWidth);}else{y = tile.row * this.tileWidth}const w = tileWidth;const h = tileWidth;console.log(x, y, w, h,'x, y, w, h',index,'index');this.context.clearRect(x, y, w, h); // Clear.this.context.fillRect(x, y, w, h); // Draw.tile.isFilled = true;});}drawTileByPoint(x, y, tileWidth, brushSize, isBrushSize = true) {const tile = this.getTilesByPoint(x, y, brushSize, isBrushSize);this.drawTile(tile,tileWidth);}getTilesByPoint(x, y, brushSize, isBrushSize = true) {const tiles = [];if (isBrushSize) {brushSize=brushSize||this.brushSize;let startRow = Math.max(0, Math.floor(y / this.tileWidth ) - Math.floor(brushSize / 2));let startColumn = Math.max(0, Math.floor(x / this.tileWidth ) - Math.floor(brushSize / 2));// let startRow = Math.max(0, Math.floor(y / this.tileHeight) - Math.floor(brushSize / 2));// let startColumn = Math.max(0, Math.floor(x / this.tileWidth) - Math.floor(brushSize / 2));let endRow = Math.min(this.tileRowSize, startRow + brushSize);let endColumn = Math.min(this.tileColumnSize, startColumn + brushSize);// Get tiles.while (startRow < endRow) {let column = startColumn;while (column < endColumn) {tiles.push(this.tiles[startRow * this.tileColumnSize + column]);column += 1;}startRow += 1;}}return tiles;}drawAllTiles(tileWidth) {this.drawTile(this.tiles,tileWidth);}eraseTile(tiles,tileWidth) {[].concat(tiles).forEach((tile) => {const x = tile.column * this.tileWidth;const y = tile.row * this.tileWidth; const w = tile.pixelWidth;const h = tile.pixelHeight;var imgData = this.context.createImageData(w, h);tile.data.forEach((val, i) => {imgData.data[i] = val;})this.context.clearRect(x, y, w, h); // Clear.this.context.putImageData(imgData, x, y); // Draw.tile.isFilled = false;});}eraseTileByPoint(x, y, tileWidth, brushSize,isBrushSize = true) {const tile = this.getTilesByPoint(x, y, brushSize, isBrushSize);this.eraseTile(tile,tileWidth);}eraseAllTiles(tileWidth) {this.eraseTile(this.tiles,tileWidth);}}export default Mosaic;
当然,代码中有很多不足的部分,欢迎大佬指出,小白退下啦。
如果觉得《前端图片放大缩小/比例自适应/打码/码大小可调整》对你有帮助,请点赞、收藏,并留下你的观点哦!