需求:图片比例自适应屏幕,放大缩小,马赛克,画笔粗细可调整;
具体实现如下图:
图中可看出马赛克画笔粗细的不同,打码的大小也不同, 第四个按钮是全图重置,点击橡皮按住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-popover
placement="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 = el
if(binding.value.isEditImg || binding.value.isClearImg){
dragBox.onmousedown = null
dragBox.style.cursor = 'default'
}else{
dragBox.style.cursor = 'move'
dragBox.onmousedown = (e)=>{
let boxX = e.clientX - dragBox.offsetLeft
let boxY = e.clientY - dragBox.offsetTop
document.onmousemove = (e) =>{
let left = e.clientX - boxX
let top = e.clientY - boxY
dragBox.style.left = `${left}px`
dragBox.style.top = `${top}px`
}
document.onmouseup = (e) =>{
document.onmousemove = null
document.onmouseup = null
}
}
}
}
}
},
activated(){
if(!this.$store.state.questionPreview.isEditPhoto){
this.init()
this.$store.commit('SET_IS_EDIT_PHOTO', true)
}
},
deactivated(){
var tempImg = document.getElementById('canvas').toDataURL('image/jpeg')
this.$store.commit('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 = true
var 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.feedback
if(Array.isArray(this.feedback)){
this.feedback = this.feedback.map(item=>{
return item
}).join("|");
}else{
this.feedback = this.feedback
}
this.queriesId = this.$route.query.queriesId
this.flagId = this.$route.query.flagId
this.fileUrl = this.$route.query.fileUrl
this.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 = true
updateFeedback(formData).then(res=>{
this.loading = false
this.isSave = false
if(res.responseCode==0){
this.$message.success(res.responseMsg || '修改成功')
let url = res.responseBody.fileUrl
let 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.$store.commit('SET_SAVE_PHOTO', false)
}else{
this.$message.error('修改失败')
}
}).catch(res=>{
this.loading = false
this.isSave = false
this.$message.error('修改失败')
})
}
}
},
methods:{
init(){
globalMosaic = null
MouseEvents = null
this.$store.commit('SET_EDIT_FORMDATA', "")
this.$store.commit('SET_EDIT_FORMDATA_URL', {})
this.showAlert= false
this.loading= false
this.isEditImg= false
this.isClearImg= false
this.isSave= false
this.showNowRate= false
this.nowRate = 100
this.timer = ''
this.id = this.$route.query.id
this.url = this.$route.query.url
this.queriesId = this.$route.query.queriesId
this.feedback = this.$route.query.feedback
if(Array.isArray(this.feedback)){
this.feedback = this.feedback.map(item=>{
return item
}).join("|");
}else{
this.feedback = this.feedback
}
this.flagId = this.$route.query.flagId
this.fileUrl = this.$route.query.fileUrl
this.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.$store.commit('SET_EDIT_FORMDATA', editFormData)
this.$store.commit('SET_IS_EDIT_PHOTO', false)
this.getFiles(this.url)
},
selectType(e){ //选择画笔类型
this.penNum = e*2
let 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 = this
if(this.sizeRate<2){
clearTimeout(this.timer)
let canvas = document.getElementById('canvasBox')
this.sizeRate += .1
this.nowRate += 10
this.showNowRate = true
canvas.style.transform=`scale(${this.sizeRate})`
this.timer = setTimeout(()=>{
that.showNowRate = false
},1500)
}else{
this.showNowRate = true
clearTimeout(that.timer)
that.timer = setTimeout(()=>{
that.showNowRate = false
},1500)
return
}
},
zoomOut(){ //图片缩小
let that = this
clearTimeout(that.timer)
if(this.sizeRate-.1>0.3){
this.sizeRate -= .1
this.nowRate -= 10
this.showNowRate = true
let canvas = document.getElementById('canvasBox')
canvas.style.transform=`scale(${this.sizeRate})`
this.timer = setTimeout(()=>{
that.showNowRate = false
},1500)
}else{
this.showNowRate = true
this.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 = 1
this.nowRate = 100
canvas.style.left = ''
canvas.style.top = ''
},
initImage(){ //初始化图片大小————1:1自适应屏幕
let out_box = document.getElementsByClassName('out_box')
let box_width = out_box[0].offsetWidth
let box_height = out_box[0].offsetHeight
this.nowRate = 100
this.showNowRate = true
this.initMosaic(this.imgUrl, box_width, box_height)
},
drawImageToCanvas(url, width, height) { //canvas画图片
let that = this
let canvas = document.getElementById('canvas')
let ctx = canvas.getContext('2d')
let rate = 0
return new Promise((resolve, reject) => {
const image = new Image()
image.crossOrigin = 'Annoymous'
image.src = ''
image.onload = function () {
if(image.width<image.height){ //若图片 高 大于 宽 H>W
canvas.width = width
rate = width/image.width
canvas.height = image.height*rate
if(canvas.height>height){
canvas.height = height
rate = height/image.height
canvas.width = image.width*rate
}
}else {
canvas.height = height
rate = height/image.height
canvas.width = image.width*rate
if(canvas.width>width){
canvas.width = width
rate = width/image.width
canvas.height = image.height*rate
}
}
if(image.width<=width && image.height<=height){
rate = 1
}
canvas.width = image.width
canvas.height = image.height
ctx.drawImage( this, 0, 0, canvas.width, canvas.height)
canvas.style.transform=`scale(${rate})`
that.sizeRate = 1
that.loading = false
that.timer = setTimeout(()=>{
that.showNowRate = false
},1500)
resolve(ctx)
ctx = null
canvas = null
that.resizeImg()
}
image.src = url
})
},
initMosaic (url, width, height) { //初始化马赛克
let that = this
this.drawImageToCanvas(url, width, height).then(ctx => {
this.isEditImg = false
this.isClearImg = false
globalMosaic = new Mosaic(ctx, {
tileWidth: 10*this.penNum, //此处考虑到马赛克一般由四个正方形组成,所以宽高都一致,且动态获取画笔粗细 this.penNum
brushSize: 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.offsetX
let Y = e.offsetY
if (that.isClearImg) {
globalMosaic.eraseTileByPoint(X, Y,parseInt(10*that.penNum),2)
}
if(that.isEditImg){
that.$store.commit('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.isEditImg
this.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.isClearImg
this.isEditImg = false
})
if(!this.isClearImg){
MouseEvents.init()
}else{
MouseEvents.close()
}
})
document.querySelector('#drawAll').addEventListener('click', () => { //点击全图打码按钮
this.$store.commit('SET_IS_EDIT_PHOTO', true)
this.isEditImg = false
this.isClearImg = false
globalMosaic.drawAllTiles(parseInt(10*that.penNum))
})
document.querySelector('#clearAll').addEventListener('click', () => { //点击重置图片按钮
// this.$store.commit('SET_IS_EDIT_PHOTO', false)
this.isEditImg = false
this.isClearImg = false
globalMosaic.eraseAllTiles(parseInt(10*that.penNum))
})
})
},
// 保存
dialogUpload () {
console.log(this.$route.query,'this.$route.query');
this.loading = true
var 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.editFormData
this.feedback = params.feedback
this.queriesId = params.queriesId
this.flagId = params.flagId
this.fileUrl = params.fileUrl
this.sort = params.sort||''
this.id = params.id
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 = true
updateFeedback(formData).then(res=>{
this.loading = false
this.isSave = false
if(res.responseCode==0){
// console.log(res.responseBody);
this.$message.success(res.responseMsg || '修改成功')
let url = res.responseBody.fileUrl
let 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.$store.commit('SET_SAVE_PHOTO', false)
}else{
this.$message.error('修改失败')
}
}).catch(res=>{
this.loading = false
this.isSave = false
this.$message.error('修改失败')
})
},
// // 图片转base64
base64ImgtoFile (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.length
arr = null
const 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 = true
fileExport(params).then(res=>{
this.loading = false
let imgUrl = window.URL.createObjectURL(new Blob([res],{type: "image/jpeg"}));
res = null
this.imgUrl = imgUrl
this.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 = imgUrl
a.target="_blank"
a.download = this.$route.query.url// 下载文件的名字
a.click()
file = null
a.remove()
}
},
beforeDestroy(){
this.$store.commit('SET_IS_EDIT_PHOTO', false) //在vuex中修改是否已遍及过图片的判断,来展示组织弹出昂
let canvas = document.getElementById('canvas')
canvas.remove()
globalMosaic = null
MouseEvents = 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 column
tile.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;
当然,代码中有很多不足的部分,欢迎大佬指出,小白退下啦。