<template>
<div class="crop-image" :style="wrapStyle">
<img :src="url" alt="" crossOrigin="anonymous" :style="imgStyle" v-if="url">
<canvas ref="canvas" @mousedown="onmousedown" @mousemove="onmousemove" @mousewheel="onmousewheel" ></canvas>
</div>
</template>
<script>
export default {
name:"crop-image",
props:{
opt:{
type:Object,
default(){
return {
width:400,
height:400,
addScale:0.06 // 缩放的速度
};
}
}
},
data(){
return {
img:null,
url:null,
imgInfo: null, // 压缩前的信息
imgCInfo:null, // 压缩后的信息
clipInfo:null, // 压缩前的信息
clipCinfo:null, // 压缩后的信息
ctx:null, // 画板
pos:{x:0,y:0},
deg:0
}
},
computed:{
wrapStyle(){
return {
width:`${this.opt.width}px`,
height:`${this.opt.height}px`
}
},
imgStyle(){
let imgCInfo = this.imgCInfo;
if(imgCInfo){
return {
width: `${imgCInfo.w}px`,
height: `${imgCInfo.h}px`,
left:`${this.pos.x}px`,
top:`${this.pos.y}px`,
transform: `rotateZ(${this.deg}deg)`,
transformOrigin: "center"
}
}
return {height:"0px",height:"0px",display:"none"};
}
},
mounted(){
let canvas = this.$refs.canvas;
canvas.width = this.opt.width;
canvas.height = this.opt.height;
this.ctx = canvas.getContext("2d");
window.addEventListener("mouseup",this.onmouseup);
},
methods:{
init(src,bound){
this.setSrc(src)
this.setClip(bound);
}, loadImage(url,attrs){ let img = new Image();
img.src = url;
attrs = attrs || {};
for(let i in attrs){
img[i] = attrs[i];
}
return new Promise((resolve,reject)=>{
img.onload = function(){
resolve(img);
};
img.onerror = reject;
});
},
compress(target){
// 压缩比例
let opt = this.opt;
let wRate = opt.width/opt.height;
let tRate = target.width/target.height;
let w,h,scale;
if(wRate > tRate){
h = opt.height;
scale = h/target.height;
w = scale * target.width;
}else{
w = opt.width;
scale = w / target.width;
h = scale * target.height;
}
return {
w,h,scale
}
},
// 外部调用
async setSrc(src){
let img = await this.loadImage(src,{crossOrigin:"anonymous"});
this.url = src;
this.img = img;
this.setImg(img);
},
// 外部调用
setClip(clipInfo){
if(clipInfo){
this.clipInfo = {
w:clipInfo.width,
h:clipInfo.height
};
this.clipCinfo = this.compress(clipInfo);
this.clipCinfo.x = (this.opt.width - this.clipCinfo.w)/2;
this.clipCinfo.y = (this.opt.height - this.clipCinfo.h)/2;
this.fill();
}
},
async exportBase(){
// 导出图片 base64
let deg = this.deg;
let pos = this.pos;
let clipCinfo = this.clipCinfo; let imgCInfo = this.imgCInfo; // 压缩后的宽高
let imgInfo = this.imgInfo; // 原始的宽高 let scale = imgCInfo.scale;
let sx = clipCinfo.x - pos.x;
let sy = clipCinfo.y - pos.y;
let canvas = document.createElement("canvas");
canvas.width = parseInt(clipCinfo.w / scale);
canvas.height = parseInt(clipCinfo.h / scale);
let swidth = imgInfo.w;
let sheight = imgInfo.h;
if(swidth < canvas.width){
swidth = canvas.width
}
if(sheight < canvas.height){
sheight = canvas.height
}
let ctx = canvas.getContext("2d");
let img = this.img;
if(deg != 0){
img = await this.loadImage(this.getRotateImg());
if(deg == 90 || deg == 270){
// 竖着的,需要坐标转换
let x = pos.x + (imgCInfo.w -imgCInfo.h)/2;
let y = pos.y + (imgCInfo.h -imgCInfo.w)/2;
sx = clipCinfo.x - x;
sy = clipCinfo.y - y;
}
}
sx /= scale;
sy /= scale;
ctx.drawImage(img,sx,sy,swidth,sheight,0,0,swidth,sheight);
return canvas.toDataURL("image/png");
},
rotate(flag){
// 旋转
let deg = this.deg;
if(flag){
deg += 90;
}else{
deg -= 90;
}
if(deg >= 360){
deg -= 360;
}
if(deg <= 0){
deg += 360;
}
this.deg = deg;
},
getRotateImg(){
// 获取旋转后的图片
let deg = this.deg;
let imgInfo = this.imgInfo;
let canvas = document.createElement("canvas");
let w,h;
if(deg == 90 || deg == 270){
// 垂直
w = imgInfo.h;
h = imgInfo.w;
}else{
// 横着的
w = imgInfo.w;
h = imgInfo.h;
}
canvas.width = w;
canvas.height = h;
let ctx = canvas.getContext("2d");
ctx.translate(w/2,h/2);
ctx.rotate(deg * Math.PI/180 );
ctx.drawImage(this.img,0,0,imgInfo.w,imgInfo.h,-imgInfo.w/2,-imgInfo.h/2,imgInfo.w,imgInfo.h);
return canvas.toDataURL("image/png");
},
setImg(img){
this.imgInfo = {
w:img.width,
h:img.height
};
this.imgCInfo = this.compress(img);
let x = (this.opt.width - this.imgCInfo.w)/2;
let y = (this.opt.height - this.imgCInfo.h)/2;
this.pos = {
x,y
};
}, onmouseup(){
this.mouse = null;
},
onmousedown(e){
this.mouse = {
x:e.offsetX,
y:e.offsetY
}
},
onmousemove(e){
if(this.mouse){
this.pos.x += e.offsetX - this.mouse.x ;
this.pos.y += e.offsetY - this.mouse.y;
this.mouse = {
x:e.offsetX,
y:e.offsetY
};
}
},
onmousewheel(event){
event.preventDefault();
const direction = (event.wheelDelta || -event.detail) > 0 ? 1 : 0;
let addScale = this.opt.addScale || 0.06;
let w,h,scale;
if(direction){
// 放大
w = this.imgCInfo.w * (1 + addScale);
}else{
// 缩小
w = this.imgCInfo.w * (1 - addScale);
}
if(w < 10){
w = 10;
}
scale = w/this.imgInfo.w;
h = this.imgInfo.h * scale;
let x = this.pos.x - (w - this.imgCInfo.w)/2;
let y = this.pos.y - (h - this.imgCInfo.h)/2;
this.pos = {
x,y
};
this.imgCInfo = {
scale,
w,
h
}
},
fill(){
let {w,h,x,y} = this.clipCinfo;
let clipctx = this.ctx;
let opt = this.opt;
clipctx.clearRect(0, 0, opt.width, opt.height);
clipctx.beginPath();
clipctx.fillStyle = 'rgba(0,0,0,0.6)';
clipctx.strokeStyle = "green";
//遮罩层
clipctx.globalCompositeOperation = "source-over";
clipctx.fillRect(0, 0, opt.width, opt.height);
//画框
clipctx.globalCompositeOperation = 'destination-out';
clipctx.fillStyle = 'rgba(0,0,0,1)';
clipctx.fillRect(x, y, w, h);
//描边
clipctx.globalCompositeOperation = "source-over";
clipctx.moveTo(x, y);
clipctx.lineTo(x + w, y);
clipctx.lineTo(x + w, y + h);
clipctx.lineTo(x, y + h);
clipctx.lineTo(x, y);
clipctx.stroke();
clipctx.closePath();
}
}
}
</script>
<style lang="scss" scoped>
.crop-image{
border: 1px solid rgb(35, 184, 255);
margin: 20px auto;
position: relative;
overflow: hidden;
background: url(data:image/png;base64,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) center repeat;
canvas{
width: 100%;
height: 100%;
position: relative;
z-index: 2;
cursor: move;
}
img{
position: absolute;
z-index: 1;
}
}
</style>

  

初始化:

this.$refs.cropImage.init("图片路径",{width:80,height:120});  

旋转:

this.$refs.cropImage.rotate()

  

导出等比例的图片:

this.src = await this.$refs.cropImage.exportBase();

  

vue 等比例截图组件,支持缩放和旋转的更多相关文章

  1. 小程序canvas截图组件

    最近做一个小程序的过程中,需要用到截图功能,网上搜了一下,发现没有符合要求的,就自己搞了个组件,方便复用. 目前功能很简单,传入宽高和图片路径即可,宽高是为了计算截图的比例,只支持缩放和移动. 实现思 ...

  2. 【Java实例】使用Thumbnailator生成缩略图(缩放、旋转、裁剪、水印)

    1 需求 表哥需要给儿子报名考试,系统要求上传不超过30KB的图片,而现在的手机随手一拍就是几MB的,怎么弄一个才30KB的图片呢? 一个简单的办法是在电脑上把图片缩小,然后截屏小图片,但现在的电脑屏 ...

  3. 整理目前支持 Vue 3 的 UI 组件库 (2021 年)

    最近,让前端圈子振奋的消息莫过于 Vue 3.0 的发布,一个无论是性能还是 API 设计都有了重大升级的新版本.距离 Vue 3.0 正式版发布已经有一段时间了,相信相关生态周边库也正在适配新版本中 ...

  4. 基于Vue.js PC桌面端弹出框组件|vue自定义弹层组件|vue模态框

    vue.js构建的轻量级PC网页端交互式弹层组件VLayer. 前段时间有分享过一个vue移动端弹窗组件,今天给大家分享一个最近开发的vue pc端弹出层组件. VLayer 一款集Alert.Dia ...

  5. Vue.js——60分钟组件快速入门(下篇)

    概述 上一篇我们重点介绍了组件的创建.注册和使用,熟练这几个步骤将有助于深入组件的开发.另外,在子组件中定义props,可以让父组件的数据传递下来,这就好比子组件告诉父组件:"嘿,老哥,我开 ...

  6. Android实现支持缩放平移图片

    本文主要用到了以下知识点 Matrix GestureDetector 能够捕捉到长按.双击 ScaleGestureDetector 用于检测缩放的手势 自由的缩放 需求:当图片加载时,将图片在屏幕 ...

  7. Vue.js学习 Item11 – 组件与组件间的通信

    什么是组件? 组件(Component)是 Vue.js 最强大的功能之一.组件可以扩展 HTML 元素,封装可重用的代码.在较高层面上,组件是自定义元素,Vue.js 的编译器为它添加特殊功能.在有 ...

  8. vue实现简单表格组件

    本来想这一周做一个关于vuex的总结的,但是由于朋友反应说还不知道如何用vue去写一个组件,所以在此写写一篇文章来说明下如何去写vue页面或者组件.vue的核心思想就是组件,什么是组件呢?按照我的理解 ...

  9. 一个 Vue 的滑动按钮组件

    git 地址:https://github.com/SyMind/vue-sliding-button vue-better-slider 一个 Vue 的滑动按钮组件,有关滑动方面的处理借鉴 bet ...

随机推荐

  1. DNGuard HVM Unpacker(3.71 trial support and x64 fixed)

    DNGuard HVM Unpacker(3.71 trial support and x64 fixed) Gr8 news. Finally got the x64 crash fixed. DN ...

  2. TP-Link TL-WR941N Ver 5.1安装OPENWRT过程

    昨天为了试验下adsl多拨刷OPENWRT成砖了,硬件是WR941N Ver 5.1,用的是在原厂固件下的web界面直刷openwrt-ar71xx-tl-wr941nd-v4-squashfs-fa ...

  3. 与 ES5 相比,React 的 ES6 语法有何不同?

    以下语法是 ES5 与 ES6 中的区别: 1.require 与 import // ES5 var React = require('react'); // ES6 import React fr ...

  4. Java Hessian实践

    Hessian是基于HTTP的轻量级远程服务解决方案,Hessian向RMI一样,使用二进制进行客户端和服务端的交互.但是与其它二进制远程调用技术(例如RMI)不同的是,它的二进制消息可以移植到其它非 ...

  5. Nginx 配置操作注意事项

    Nginx reload 会中断现有连接吗? - 知乎https://www.zhihu.com/question/57096250 在NGINX上配置HTTPS---血的教训--要重启NGINX - ...

  6. Jmeter之__CSVRead随机读取变量

    背景:当你需要从一些数据中随机的取值用的话可以用这个函数来实现__CSVRead __CSVRead函数用于对脚本进行参数化,当脚本中不同变量需要不同参数值时,可以考虑__CSVRead函数. _CS ...

  7. java8 数据集过滤removeIf和filter

    对象如下,需求:只要30岁以下的人 //求职者的实体类 public class Person { private String name;//姓名 private Integer age;//年龄 ...

  8. vector 移除元素

    vector中移除“与某值相等”的第一个元素. std::vector<Elem> coll; ... //remove first element with value val std: ...

  9. bootstrap 输入框只能数字和字母等其他限制

    --输入中文.数字.英文: <input οnkeyup="value=value.replace(/[^\w\u4E00-\u9FA5]/g, '')"> --输入数 ...

  10. 判断本网段有多少可用的ip地址

    为了提高效率,使用多线程方式同时ping. 但是如果开启255个线程,又会因为网络端口太拥挤,会被判定为无法ping通.所以本例使用java自带线程池,线程池的连接数还不能太大,启动了15个线程. 等 ...