效果图:
代码
html
<template>
<div class="n-upload">
<input type="file" style="display: none" @change="nUpload" ref="nUpload" />
<n-clip
v-show="clipConf.src"
:clipW="200"
:clipH="300"
:src="clipConf.src"
ref="nClip"
>
</n-clip>
<div v-show="!clipConf.src" @click="uploadClick()" class="n-upload-btn">
<i class="el-icon-plus"></i>
<p>点击上传</p>
</div>
<el-button type="primary" @click="clipImg">确定</el-button>
<img :src="img" />
</div>
</template>
<script>
export default {
data() {
return {
clipConf: {
show: true,
src: "",
},
img: "",
activeName: 0, //判断图片上传,视频上传
};
},
components: {
"n-clip": () => import("@/accountnum/components/clip"),
},
watch: {
"clipConf.src"(v) {
if (!v) return;
setTimeout(() => this.$refs.nClip.resize(), 300);
this.formdata = v;
},
},
methods: {
clipImg() {
let src;
if (this.activeName == 0)
src = this.clipConf.outSrc = this.$refs.nClip.out();
else src = this.clipConf.outSrc = this.$refs.videoClip.out();
console.log(src);
this.img = src;
},
async nUpload({ target: tg }) {
const file = tg.files[0];
this.noneForm = file;
tg.value = "";
if (!file) return;
const rd = new FileReader();
rd.readAsDataURL(file);
rd.onload = () => (this.clipConf.src = rd.result);
},
uploadClick() {
this.$refs.nUpload.click();
},
},
};
</script>
clip.vue
<style lang="scss" scoped>
.n-clip {
width: 100%;
height: 300px;
}
</style>
<template>
<canvas v-resize="resize" @dragstart.prevent="" :style="{ cursor: hover ? 'move' : 'auto' }"
@mousewheel.prevent.stop="onWheel" @DOMMouseScroll.prevent.stop="onWheel"
@mousemove="(e) => img && (hover = isImgRange(...getCanvasXy(e)))" @mousedown="onDown" ref="c"
class="n-clip"></canvas>
</template>
<script>
const resizes = new Set();
const resizeLoop = () => {
for (const el of resizes) {
if (el._oWidth === el.clientWidth && el._oHeight === el.clientHeight) continue;
el._oWidth = el.clientWidth;
el._oHeight = el.clientHeight;
el._resizeFunc(el)
}
window.requestAnimationFrame(resizeLoop)
}
window.requestAnimationFrame(resizeLoop);
export default {
props: {
src: String,
bg: {
type: String,
default: "#000",
},
scaleRatio: {
// scale调整一次的比例
type: Number,
default: 0.05,
},
clipW: {
type: Number,
default: 100,
},
clipH: {
type: Number,
default: 100,
},
},
watch: {
clipW() {
this.minSize();
},
clipH() {
this.minSize();
},
src: {
immediate: true,
async handler(v1, v2) {
console.log(5666)
if (v1 === v2) return;
if (!v1) return (this.img = null);
const img = (this.img = new Image());
img.src = v1;
const res = await new Promise(
(r) => ((img.onload = () => r(1)), (img.onerror = () => r(0)))
);
res && this.initImgParams();
},
},
scale(v1, v2) {
if (v1 === v2 || this.skipScaleCb) return (this.skipScaleCb = false);
const {
img,
imgW,
imgH,
imgNW,
imgNH
} = this;
if (v1 !== v1 || !img) return (this.scale = 1);
this.minSize();
this.imgW = imgNW * this.scale;
this.imgH = imgNH * this.scale;
this.boundary();
},
},
data() {
return {
// 界面参数
w: 100,
h: 100,
// 鼠标参数
mouseX: 0,
mouseY: 0,
isMove: false,
hover: false,
// 图片参数
imgX: 0,
imgY: 0,
imgW: 0,
imgH: 0,
imgNW: 0,
imgNH: 0,
skipScaleCb: false,
scale: 1,
c: null,
ctx: null,
};
},
directives: {
'resize': {
bind(el, binding, vNode) {
el._oWidth = el.clientWidth;
el._oHeight = el.clientHeight;
el._resizeFunc = binding.value;
el._resizeVNode = vNode;
el._resizeId = new Date().getTime();
resizes.add(el)
},
unbind(el) {
resizes.delete(el);
}
}
},
mounted() {
this.c = this.$refs.c;
const ctx = (this.ctx = this.c.getContext("2d"));
this.resize();
this.loop();
document.addEventListener("mousemove", this.onMove);
document.addEventListener("mouseup", this.onUp);
},
beforeDestroy() {
document.removeEventListener("mousemove", this.onMove);
document.removeEventListener("mouseup", this.onUp);
},
methods: {
resize() {
// console.log(123);
if (!this._isMounted || this._isDestroyed) return;
const {
c,
clipW,
clipH
} = this;
this.w = c.width = c.clientWidth;
this.h = c.height = c.clientHeight;
this.boundary();
},
// 初始化图片比例
initImgParams() {
const {
img,
w,
h
} = this;
if (!img) return;
const {
naturalWidth: nw,
naturalHeight: nh
} = img;
this.imgNW = nw;
this.imgNH = nh;
if (nw === 0 || nh === 0) return (this.scale = 1);
const [r1, r2] = [(w / nw) * 0.8, (h / nh) * 0.8];
this.scale = r1 < r2 ? r1 : r2;
this.minSize();
this.imgW = nw * this.scale;
this.imgH = nh * this.scale;
// 置于中间
this.imgX = (w - this.imgW) / 2;
this.imgY = (h - this.imgH) / 2;
this.boundary();
this.skipScaleCb = true;
},
minSize() {
const {
img,
imgNW,
imgNH,
clipW,
clipH,
scale
} = this;
if (!img) return;
// 计算最小值不超出裁剪框
const [r1, r2] = [clipW / imgNW, clipH / imgNH];
const min = r1 > r2 ? r1 : r2;
return scale < min ? ((this.scale = min), true) : false;
},
// 边界
boundary() {
let {
w,
h,
imgW,
imgH,
imgX,
imgY,
clipW,
clipH
} = this;
let [clipX, clipY] = [(w - clipW) / 2, (h - clipH) / 2];
if (imgX > clipX) imgX = clipX;
if (imgX + imgW < clipX + clipW) imgX = clipX + clipW - imgW;
if (imgY > clipY) imgY = clipY;
if (imgY + imgH < clipY + clipH) imgY = clipY + clipH - imgH;
this.imgX = imgX;
this.imgY = imgY;
},
isImgRange(x, y) {
let {
w,
h,
imgW,
imgH,
imgX,
imgY,
clipW,
clipH,
img
} = this;
let [clipX, clipY] = [(w - clipW) / 2, (h - clipH) / 2];
if (!img) return false;
return x >= imgX && x <= imgX + imgW && y >= imgY && y <= imgY + imgH;
},
getCanvasXy(e) {
let {
clientX,
clientY
} = e.targetTouches ? e.targetTouches[0] : e;
const {
x: boxX,
y: boxY
} = this.c.getBoundingClientRect();
return [clientX - boxX, clientY - boxY];
},
onDown(e) {
if (!this.img) return;
const [x, y] = this.getCanvasXy(e);
if (!this.isImgRange(x, y)) return;
this.mouseX = x;
this.mouseY = y;
this.isMove = true;
},
onMove(e) {
const {
mouseX,
mouseY,
isMove
} = this;
if (!isMove) return;
const [x, y] = this.getCanvasXy(e);
this.mouseX = x;
this.mouseY = y;
this.imgX += x - mouseX;
this.imgY += y - mouseY;
this.boundary();
},
onUp(e) {
this.isMove = false;
},
onWheel({
wheelDelta,
detail
}) {
const {
img,
imgW,
imgH,
imgNW,
imgNH
} = this;
if (!img) return;
this.scale +=
((wheelDelta || -detail) > 0 ? 1 : -1) * (this.scale * this.scaleRatio);
this.minSize();
this.imgW = imgNW * this.scale;
this.imgH = imgNH * this.scale;
// 变换原点
this.imgX += (imgW - this.imgW) / 2;
this.imgY += (imgH - this.imgH) / 2;
this.boundary();
this.skipScaleCb = true;
},
// 输出裁剪后的图片
out(q = 1) {
const {
ctx,
w,
h,
clipW,
clipH
} = this;
const c2 = document.createElement("canvas");
const ctx2 = c2.getContext("2d");
c2.width = clipW;
c2.height = clipH;
ctx2.putImageData(
ctx.getImageData((w - clipW) / 2, (h - clipH) / 2, clipW, clipH),
0,
0
);
return c2.toDataURL("image/png", q);
},
draw() {
let {
ctx,
w,
h,
img,
clipW,
clipH,
bg,
imgX,
imgY,
imgW,
imgH
} = this;
// 背景
ctx.beginPath();
ctx.clearRect(0, 0, w, h);
ctx.rect(0, 0, w, h);
if (bg)(ctx.fillStyle = bg), ctx.fill();
// 图片
if (!img) return;
ctx.save();
ctx.translate(imgX + imgW * 0.5, imgY + imgH * 0.5);
ctx.drawImage(img, imgW * -0.5, imgH * -0.5, imgW, imgH);
ctx.restore();
ctx.fillStyle = "rgba(0,0,0,0.4)";
ctx.fill();
// 裁剪框
const [clipX, clipY] = [(w - clipW) / 2, (h - clipH) / 2];
ctx.beginPath();
ctx.rect(clipX, clipY, clipW, clipH);
ctx.save();
ctx.clip();
ctx.translate(imgX + imgW * 0.5, imgY + imgH * 0.5);
ctx.drawImage(img, imgW * -0.5, imgH * -0.5, imgW, imgH);
ctx.restore();
ctx.beginPath();
ctx.lineWidth = 1.5;
ctx.strokeStyle = "#3da7b4";
ctx.strokeRect(clipX - 1.5, clipY - 1.5, clipW + 3, clipH + 3);
},
loop() {
if (this._isDestroyed) return;
this.draw();
window.requestAnimationFrame(this.loop.bind(this));
},
},
};
</script>