932 lines
26 KiB
Vue
932 lines
26 KiB
Vue
<template>
|
||
<view class="image-cropper" :style="{ zIndex }" @wheel="cropper.mousewheel">
|
||
<canvas v-if="use2d" type="2d" id="imgCanvas" class="img-canvas" :style="{
|
||
width: `${canvansWidth}px`,
|
||
height: `${canvansHeight}px`
|
||
}"></canvas>
|
||
<canvas v-else id="imgCanvas" canvas-id="imgCanvas" class="img-canvas" :style="{
|
||
width: `${canvansWidth}px`,
|
||
height: `${canvansHeight}px`
|
||
}"></canvas>
|
||
<view class="whitefont" >
|
||
<view class="action-bar-special">
|
||
<view class="rotate-icon" @click=""></view>
|
||
</view>
|
||
<view class="">
|
||
请调整图片尺寸,放入取景框内
|
||
</view>
|
||
|
||
</view>
|
||
<view id="pic-preview" class="pic-preview" :change:init="cropper.initObserver" :init="initData"
|
||
@touchstart="cropper.touchstart" @touchmove="cropper.touchmove" @touchend="cropper.touchend">
|
||
<image v-if="imgSrc" id="crop-image" class="crop-image" :style="cropper.imageStyles" :src="imgSrc" webp>
|
||
</image>
|
||
<view v-for="(item, index) in maskList" :key="item.id" :id="item.id" class="crop-mask-block"
|
||
:style="cropper.maskStylesList[index]"></view>
|
||
<view v-if="showBorder" id="crop-border" class="crop-border">
|
||
<view class="border-son"></view>
|
||
</view>
|
||
<view v-if="radius > 0" id="crop-circle-box" class="crop-circle-box" :style="cropper.circleBoxStyles">
|
||
<view class="crop-circle" id="crop-circle" :style="cropper.circleStyles"></view>
|
||
</view>
|
||
<block v-if="showGrid">
|
||
<view v-for="(item, index) in gridList" :key="item.id" :id="item.id" class="crop-grid"
|
||
:style="cropper.gridStylesList[index]"></view>
|
||
</block>
|
||
<!-- <block v-if="showAngle">
|
||
<view v-for="(item, index) in angleList" :key="item.id" :id="item.id" class="crop-angle"
|
||
:style="cropper.angleStylesList[index]">
|
||
<view :style="[{
|
||
width: `${angleSize}px`,
|
||
height: `${angleSize}px`
|
||
}]"></view>
|
||
</view>
|
||
</block> -->
|
||
</view>
|
||
<slot />
|
||
<view class="fixed-bottom safe-area-inset-bottom" :style="{ zIndex: initData.area.zIndex + 99 }">
|
||
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar">
|
||
<view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view>
|
||
<view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view>
|
||
</view>
|
||
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar-anther">
|
||
<view v-if="reverseRotatable" class="rotate-icon" @click="chooseImage"></view>
|
||
<view v-if="rotatable" class="rotate-icon is-reverse" @click="chooseImage"></view>
|
||
</view>
|
||
<view v-if="(rotatable || reverseRotatable) && !!imgSrc" class="action-bar-right" @click="cropClick">
|
||
确认
|
||
<!-- <view v-if="reverseRotatable" class="rotate-icon" @click="cropper.rotateImage270"></view> -->
|
||
<!-- <view v-if="rotatable" class="rotate-icon is-reverse" @click="cropper.rotateImage90"></view> -->
|
||
</view>
|
||
<!-- <view v-if="!choosable" class="choose-btn" @click="cropClick">确定</view> -->
|
||
<!-- <block v-else-if="!!imgSrc">
|
||
<view class="rechoose" @click="chooseImage">重选</view>
|
||
<button class="button" size="mini" @click="cropClick">确定</button>
|
||
</block> -->
|
||
<!-- <view v-else class="choose-btn" @click="chooseImage">选择图片</view> -->
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<!-- #ifdef APP-VUE -->
|
||
<script module="cropper" lang="renderjs">
|
||
import cropper from './qf-image-cropper.render.js';
|
||
// vue3 app renderjs中条件编译无效
|
||
cropper.setPlatform('APP');
|
||
export default {
|
||
mixins: [cropper]
|
||
}
|
||
</script>
|
||
<!-- #endif -->
|
||
<!-- #ifdef H5 -->
|
||
<script module="cropper" lang="renderjs">
|
||
import cropper from './qf-image-cropper.render.js';
|
||
export default {
|
||
mixins: [cropper]
|
||
}
|
||
</script>
|
||
<!-- #endif -->
|
||
<!-- #ifdef MP-WEIXIN || MP-QQ -->
|
||
<script module="cropper" lang="wxs" src="./qf-image-cropper.wxs"></script>
|
||
<!-- #endif -->
|
||
<script>
|
||
/** 裁剪区域最大宽高所占屏幕宽度百分比 */
|
||
const AREA_SIZE = 75;
|
||
/** 图片默认宽高 */
|
||
const IMG_SIZE = 300;
|
||
|
||
export default {
|
||
name: "qf-image-cropper",
|
||
// #ifdef MP-WEIXIN
|
||
options: {
|
||
// 表示启用样式隔离,在自定义组件内外,使用 class 指定的样式将不会相互影响
|
||
styleIsolation: "isolated"
|
||
},
|
||
// #endif
|
||
props: {
|
||
/** 图片资源地址 */
|
||
src: {
|
||
type: String,
|
||
default: ''
|
||
},
|
||
/** 裁剪宽度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||
width: {
|
||
type: Number,
|
||
default: IMG_SIZE
|
||
},
|
||
/** 裁剪高度,有些平台或设备对于canvas的尺寸有限制,过大可能会导致无法正常绘制 */
|
||
height: {
|
||
type: Number,
|
||
default: IMG_SIZE
|
||
},
|
||
/** 是否绘制裁剪区域边框 */
|
||
showBorder: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 是否绘制裁剪区域网格参考线 */
|
||
showGrid: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 是否展示四个支持伸缩的角 */
|
||
showAngle: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 裁剪区域最小缩放倍数 */
|
||
areaScale: {
|
||
type: Number,
|
||
default: 0.3
|
||
},
|
||
/** 图片最小缩放倍数 */
|
||
minScale: {
|
||
type: Number,
|
||
default: 0.3
|
||
},
|
||
/** 图片最大缩放倍数 */
|
||
maxScale: {
|
||
type: Number,
|
||
default: 2
|
||
},
|
||
/** 检查图片位置是否超出裁剪边界,如果超出则会矫正位置 */
|
||
checkRange: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
/** 生成图片背景色:如果裁剪区域没有完全包含在图片中时,不设置该属性生成图片存在一定的透明块 */
|
||
backgroundColor: {
|
||
type: String
|
||
},
|
||
/** 是否有回弹效果:当 checkRange 为 true 时有效,拖动时可以拖出边界,释放时会弹回边界 */
|
||
bounce: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 是否支持翻转 */
|
||
rotatable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 是否支持逆向翻转 */
|
||
reverseRotatable: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
/** 是否支持从本地选择素材 */
|
||
choosable: {
|
||
type: Boolean,
|
||
default: true
|
||
},
|
||
/** 是否开启硬件加速,图片缩放过程中如果出现元素的“留影”或“重影”效果,可通过该方式解决或减轻这一问题 */
|
||
gpu: {
|
||
type: Boolean,
|
||
default: false
|
||
},
|
||
/** 四个角尺寸,单位px */
|
||
angleSize: {
|
||
type: Number,
|
||
default: 20
|
||
},
|
||
/** 四个角边框宽度,单位px */
|
||
angleBorderWidth: {
|
||
type: Number,
|
||
default: 2
|
||
},
|
||
zIndex: {
|
||
type: [Number, String]
|
||
},
|
||
/** 裁剪图片圆角半径,单位px */
|
||
radius: {
|
||
type: Number,
|
||
default: 60
|
||
},
|
||
/** 生成文件的类型,只支持 'jpg' 或 'png'。默认为 'png' */
|
||
fileType: {
|
||
type: String,
|
||
default: 'png'
|
||
},
|
||
/**
|
||
* 图片从绘制到生成所需时间,单位ms
|
||
* 微信小程序平台使用 `Canvas 2D` 绘制时有效
|
||
* 如绘制大图或出现裁剪图片空白等情况应适当调大该值,因 `Canvas 2d` 采用同步绘制,需自己把控绘制完成时间
|
||
*/
|
||
delay: {
|
||
type: Number,
|
||
default: 1000
|
||
},
|
||
// #ifdef H5
|
||
/**
|
||
* 页面是否是原生标题栏
|
||
* H5平台当 showAngle 为 true 时,使用插件的页面在 `page.json` 中配置了 "navigationStyle": "custom" 时,必须将此值设为 false ,否则四个可拉伸角的触发位置会有偏差。
|
||
* 注:因H5平台的窗口高度是包含标题栏的,而屏幕触摸点的坐标是不包含的
|
||
*/
|
||
navigation: {
|
||
type: Boolean,
|
||
default: true
|
||
}
|
||
// #endif
|
||
},
|
||
emits: ["crop"],
|
||
data() {
|
||
return {
|
||
// 用不同 id 使 v-for key 不重复
|
||
maskList: [{
|
||
id: 'crop-mask-block-1'
|
||
},
|
||
{
|
||
id: 'crop-mask-block-2'
|
||
},
|
||
{
|
||
id: 'crop-mask-block-3'
|
||
},
|
||
{
|
||
id: 'crop-mask-block-4'
|
||
},
|
||
],
|
||
gridList: [{
|
||
id: 'crop-grid-1'
|
||
},
|
||
{
|
||
id: 'crop-grid-2'
|
||
},
|
||
{
|
||
id: 'crop-grid-3'
|
||
},
|
||
{
|
||
id: 'crop-grid-4'
|
||
},
|
||
],
|
||
angleList: [{
|
||
id: 'crop-angle-1'
|
||
},
|
||
{
|
||
id: 'crop-angle-2'
|
||
},
|
||
{
|
||
id: 'crop-angle-3'
|
||
},
|
||
{
|
||
id: 'crop-angle-4'
|
||
},
|
||
],
|
||
/** 本地缓存的图片路径 */
|
||
imgSrc: '',
|
||
/** 图片的裁剪宽度 */
|
||
imgWidth: IMG_SIZE,
|
||
/** 图片的裁剪高度 */
|
||
imgHeight: IMG_SIZE,
|
||
/** 裁剪区域最大宽度所占屏幕宽度百分比 */
|
||
widthPercent: AREA_SIZE,
|
||
/** 裁剪区域最大高度所占屏幕宽度百分比 */
|
||
heightPercent: AREA_SIZE,
|
||
/** 裁剪区域布局信息 */
|
||
area: {},
|
||
/** 未被缩放过的图片宽 */
|
||
oldWidth: 0,
|
||
/** 未被缩放过的图片高 */
|
||
oldHeight: 0,
|
||
/** 系统信息 */
|
||
sys: uni.getSystemInfoSync(),
|
||
scaleWidth: 0,
|
||
scaleHeight: 0,
|
||
rotate: 0,
|
||
offsetX: 0,
|
||
offsetY: 0,
|
||
use2d: false,
|
||
canvansWidth: 0,
|
||
canvansHeight: 0,
|
||
// imageStyles: {},
|
||
// maskStylesList: [{}, {}, {}, {}],
|
||
// borderStyles: {},
|
||
// gridStylesList: [{}, {}, {}, {}],
|
||
// angleStylesList: [{}, {}, {}, {}],
|
||
// circleBoxStyles: {},
|
||
// circleStyles: {},
|
||
}
|
||
},
|
||
computed: {
|
||
initData() {
|
||
// console.log('initData')
|
||
return {
|
||
timestamp: new Date().getTime(),
|
||
area: {
|
||
...this.area,
|
||
bounce: this.bounce,
|
||
showBorder: this.showBorder,
|
||
showGrid: this.showGrid,
|
||
showAngle: this.showAngle,
|
||
angleSize: this.angleSize,
|
||
angleBorderWidth: this.angleBorderWidth,
|
||
minScale: this.areaScale,
|
||
widthPercent: this.widthPercent,
|
||
heightPercent: this.heightPercent,
|
||
radius: this.radius,
|
||
checkRange: this.checkRange,
|
||
zIndex: +this.zIndex || 0,
|
||
},
|
||
sys: this.sys,
|
||
img: {
|
||
minScale: this.minScale,
|
||
maxScale: this.maxScale,
|
||
src: this.imgSrc,
|
||
width: this.oldWidth,
|
||
height: this.oldHeight,
|
||
oldWidth: this.oldWidth,
|
||
oldHeight: this.oldHeight,
|
||
gpu: this.gpu,
|
||
}
|
||
}
|
||
},
|
||
imgProps() {
|
||
return {
|
||
width: this.width,
|
||
height: this.height,
|
||
src: this.src,
|
||
}
|
||
}
|
||
},
|
||
watch: {
|
||
imgProps: {
|
||
handler(val, oldVal) {
|
||
// 自定义裁剪尺,示例如下:
|
||
this.imgWidth = Number(val.width) || IMG_SIZE;
|
||
this.imgHeight = Number(val.height) || IMG_SIZE;
|
||
let use2d = true;
|
||
// #ifndef MP-WEIXIN
|
||
use2d = false;
|
||
// #endif
|
||
// if(use2d && (this.imgWidth > 1365 || this.imgHeight > 1365)) {
|
||
// use2d = false;
|
||
// }
|
||
let canvansWidth = this.imgWidth;
|
||
let canvansHeight = this.imgHeight;
|
||
let size = Math.max(canvansWidth, canvansHeight)
|
||
let scalc = 1;
|
||
if (size > 1365) {
|
||
scalc = 1365 / size;
|
||
}
|
||
this.canvansWidth = canvansWidth * scalc;
|
||
this.canvansHeight = canvansHeight * scalc;
|
||
this.use2d = use2d;
|
||
this.initArea();
|
||
const src = val.src || this.imgSrc;
|
||
src && this.initImage(src, oldVal === undefined);
|
||
},
|
||
immediate: true
|
||
},
|
||
},
|
||
methods: {
|
||
/** 提供给wxs调用,用来接收图片变更数据 */
|
||
dataChange(e) {
|
||
// console.log('dataChange', e)
|
||
this.scaleWidth = e.width;
|
||
this.scaleHeight = e.height;
|
||
this.rotate = e.rotate;
|
||
this.offsetX = e.x;
|
||
this.offsetY = e.y;
|
||
},
|
||
/** 初始化裁剪区域布局信息 */
|
||
initArea() {
|
||
// 底部操作栏高度 = 底部底部操作栏内容高度 + 设备底部安全区域高度
|
||
this.sys.offsetBottom = uni.upx2px(100) + this.sys.safeAreaInsets.bottom;
|
||
// #ifndef H5
|
||
this.sys.windowTop = 0;
|
||
this.sys.navigation = true;
|
||
// #endif
|
||
// #ifdef H5
|
||
// h5平台的窗口高度是包含标题栏的
|
||
this.sys.windowTop = this.sys.windowTop || 44;
|
||
this.sys.navigation = this.navigation;
|
||
// #endif
|
||
let wp = this.widthPercent;
|
||
let hp = this.heightPercent;
|
||
if (this.imgWidth > this.imgHeight) {
|
||
hp = hp * this.imgHeight / this.imgWidth;
|
||
} else if (this.imgWidth < this.imgHeight) {
|
||
wp = wp * this.imgWidth / this.imgHeight;
|
||
}
|
||
const size = this.sys.windowWidth > this.sys.windowHeight ? this.sys.windowHeight : this.sys.windowWidth;
|
||
const width = size * wp / 100;
|
||
const height = size * hp / 100;
|
||
const left = (this.sys.windowWidth - width) / 2;
|
||
const right = left + width;
|
||
const top = (this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - height) / 2;
|
||
const bottom = this.sys.windowHeight + this.sys.windowTop - this.sys.offsetBottom - top;
|
||
this.area = {
|
||
width,
|
||
height,
|
||
left,
|
||
right,
|
||
top,
|
||
bottom
|
||
};
|
||
this.scaleWidth = width;
|
||
this.scaleHeight = height;
|
||
},
|
||
/** 从本地选取图片 */
|
||
chooseImage(options) {
|
||
// #ifdef MP-WEIXIN || MP-JD
|
||
if (uni.chooseMedia) {
|
||
uni.chooseMedia({
|
||
...options,
|
||
count: 1,
|
||
mediaType: ['image'],
|
||
success: (res) => {
|
||
this.resetData();
|
||
this.initImage(res.tempFiles[0].tempFilePath);
|
||
}
|
||
});
|
||
return;
|
||
}
|
||
// #endif
|
||
uni.chooseImage({
|
||
...options,
|
||
count: 1,
|
||
success: (res) => {
|
||
this.resetData();
|
||
this.initImage(res.tempFiles[0].path);
|
||
}
|
||
});
|
||
},
|
||
/** 重置数据 */
|
||
resetData() {
|
||
this.imgSrc = '';
|
||
this.rotate = 0;
|
||
this.offsetX = 0;
|
||
this.offsetY = 0;
|
||
this.initArea();
|
||
},
|
||
/**
|
||
* 初始化图片信息
|
||
* @param {String} url 图片链接
|
||
*/
|
||
initImage(url, isFirst) {
|
||
uni.getImageInfo({
|
||
src: url,
|
||
success: async (res) => {
|
||
if (isFirst && this.src === url) await (new Promise((resolve) => setTimeout(resolve,
|
||
50)));
|
||
this.imgSrc = res.path;
|
||
let scale = res.width / res.height;
|
||
let areaScale = this.area.width / this.area.height;
|
||
if (scale > 1) { // 横向图片
|
||
if (scale >= areaScale) { // 图片宽不小于目标宽,则高固定,宽自适应
|
||
this.scaleWidth = (this.scaleHeight / res.height) * this.scaleWidth * (res
|
||
.width / this.scaleWidth);
|
||
} else { // 否则宽固定、高自适应
|
||
this.scaleHeight = res.height * this.scaleWidth / res.width;
|
||
}
|
||
} else { // 纵向图片
|
||
if (scale <= areaScale) { // 图片高不小于目标高,宽固定,高自适应
|
||
this.scaleHeight = (this.scaleWidth / res.width) * this.scaleHeight / (this
|
||
.scaleHeight / res.height);
|
||
} else { // 否则高固定,宽自适应
|
||
this.scaleWidth = res.width * this.scaleHeight / res.height;
|
||
}
|
||
}
|
||
// 记录原始宽高,为缩放比列做限制
|
||
this.oldWidth = +this.scaleWidth.toFixed(2);
|
||
this.oldHeight = +this.scaleHeight.toFixed(2);
|
||
},
|
||
fail: (err) => {
|
||
console.error(err)
|
||
}
|
||
});
|
||
},
|
||
/**
|
||
* 剪切图片圆角
|
||
* @param {Object} ctx canvas 的绘图上下文对象
|
||
* @param {Number} radius 圆角半径
|
||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||
* @param {Function} drawImage 执行剪切时所调用的绘图方法,入参为是否执行了剪切
|
||
*/
|
||
drawClipImage(ctx, radius, scale, drawImage) {
|
||
if (radius > 0) {
|
||
ctx.save();
|
||
ctx.beginPath();
|
||
const w = this.canvansWidth;
|
||
const h = this.canvansHeight;
|
||
if (w === h && radius >= w / 2) { // 圆形
|
||
ctx.arc(w / 2, h / 2, w / 2, 0, 2 * Math.PI);
|
||
} else { // 圆角矩形
|
||
if (w !== h) { // 限制圆角半径不能超过短边的一半
|
||
radius = Math.min(w / 2, h / 2, radius);
|
||
// radius = Math.min(Math.max(w, h) / 2, radius);
|
||
}
|
||
ctx.moveTo(radius, 0);
|
||
ctx.arcTo(w, 0, w, h, radius);
|
||
ctx.arcTo(w, h, 0, h, radius);
|
||
ctx.arcTo(0, h, 0, 0, radius);
|
||
ctx.arcTo(0, 0, w, 0, radius);
|
||
ctx.closePath();
|
||
}
|
||
ctx.clip();
|
||
drawImage && drawImage(true);
|
||
ctx.restore();
|
||
} else {
|
||
drawImage && drawImage(false);
|
||
}
|
||
},
|
||
/**
|
||
* 旋转图片
|
||
* @param {Object} ctx canvas 的绘图上下文对象
|
||
* @param {Number} rotate 旋转角度
|
||
* @param {Number} scale 生成图片的实际尺寸与截取区域比
|
||
*/
|
||
drawRotateImage(ctx, rotate, scale) {
|
||
if (rotate !== 0) {
|
||
// 1. 以图片中心点为旋转中心点
|
||
const x = this.scaleWidth * scale / 2;
|
||
const y = this.scaleHeight * scale / 2;
|
||
ctx.translate(x, y);
|
||
// 2. 旋转画布
|
||
ctx.rotate(rotate * Math.PI / 180);
|
||
// 3. 旋转完画布后恢复设置旋转中心时所做的偏移
|
||
ctx.translate(-x, -y);
|
||
}
|
||
},
|
||
drawImage(ctx, image, callback) {
|
||
// 生成图片的实际尺寸与截取区域比
|
||
const scale = this.canvansWidth / this.area.width;
|
||
if (this.backgroundColor) {
|
||
if (ctx.setFillStyle) ctx.setFillStyle(this.backgroundColor);
|
||
else ctx.fillStyle = this.backgroundColor;
|
||
ctx.fillRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||
}
|
||
this.drawClipImage(ctx, this.radius, scale, () => {
|
||
this.drawRotateImage(ctx, this.rotate, scale);
|
||
const r = this.rotate / 90;
|
||
ctx.drawImage(
|
||
image,
|
||
[
|
||
(this.offsetX - this.area.left),
|
||
(this.offsetY - this.area.top),
|
||
-(this.offsetX - this.area.left),
|
||
-(this.offsetY - this.area.top)
|
||
][r] * scale,
|
||
[
|
||
(this.offsetY - this.area.top),
|
||
-(this.offsetX - this.area.left),
|
||
-(this.offsetY - this.area.top),
|
||
(this.offsetX - this.area.left)
|
||
][r] * scale,
|
||
this.scaleWidth * scale,
|
||
this.scaleHeight * scale
|
||
);
|
||
});
|
||
},
|
||
/**
|
||
* 绘图
|
||
* @param {Object} canvas
|
||
* @param {Object} ctx canvas 的绘图上下文对象
|
||
* @param {String} src 图片路径
|
||
* @param {Function} callback 开始绘制时回调
|
||
*/
|
||
draw2DImage(canvas, ctx, src, callback) {
|
||
// console.log('draw2DImage', canvas, ctx, src, callback)
|
||
if (canvas) {
|
||
const image = canvas.createImage();
|
||
image.onload = () => {
|
||
this.drawImage(ctx, image);
|
||
// 如果觉得`生成时间过长`或`出现生成图片空白`可尝试调整延迟时间
|
||
callback && setTimeout(callback, this.delay);
|
||
};
|
||
image.onerror = (err) => {
|
||
console.error(err)
|
||
uni.hideLoading();
|
||
};
|
||
image.src = src;
|
||
} else {
|
||
this.drawImage(ctx, src);
|
||
setTimeout(() => {
|
||
ctx.draw(false, callback);
|
||
}, 200);
|
||
}
|
||
},
|
||
/**
|
||
* 画布转图片到本地缓存
|
||
* @param {Object} canvas
|
||
* @param {String} canvasId
|
||
*/
|
||
canvasToTempFilePath(canvas, canvasId) {
|
||
// console.log('canvasToTempFilePath', canvas, canvasId)
|
||
uni.canvasToTempFilePath({
|
||
canvas,
|
||
canvasId,
|
||
x: 0,
|
||
y: 0,
|
||
width: this.canvansWidth,
|
||
height: this.canvansHeight,
|
||
destWidth: this.imgWidth, // 必要,保证生成图片宽度不受设备分辨率影响
|
||
destHeight: this.imgHeight, // 必要,保证生成图片高度不受设备分辨率影响
|
||
fileType: this.fileType, // 目标文件的类型,默认png
|
||
success: (res) => {
|
||
// 生成的图片临时文件路径
|
||
this.handleImage(res.tempFilePath);
|
||
},
|
||
fail: (err) => {
|
||
uni.hideLoading();
|
||
uni.showToast({
|
||
title: '裁剪失败,生成图片异常!',
|
||
icon: 'none'
|
||
});
|
||
}
|
||
}, this);
|
||
},
|
||
/** 确认裁剪 */
|
||
cropClick() {
|
||
uni.showLoading({
|
||
title: '裁剪中...',
|
||
mask: true
|
||
});
|
||
if (!this.use2d) {
|
||
const ctx = uni.createCanvasContext('imgCanvas', this);
|
||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||
this.draw2DImage(null, ctx, this.imgSrc, () => {
|
||
this.canvasToTempFilePath(null, 'imgCanvas');
|
||
});
|
||
return;
|
||
}
|
||
// #ifdef MP-WEIXIN
|
||
const query = uni.createSelectorQuery().in(this);
|
||
query.select('#imgCanvas')
|
||
.fields({
|
||
node: true,
|
||
size: true
|
||
})
|
||
.exec((res) => {
|
||
const canvas = res[0].node;
|
||
|
||
const dpr = uni.getSystemInfoSync().pixelRatio;
|
||
canvas.width = res[0].width * dpr;
|
||
canvas.height = res[0].height * dpr;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.scale(dpr, dpr);
|
||
ctx.clearRect(0, 0, this.canvansWidth, this.canvansHeight);
|
||
|
||
this.draw2DImage(canvas, ctx, this.imgSrc, () => {
|
||
this.canvasToTempFilePath(canvas);
|
||
});
|
||
});
|
||
// #endif
|
||
},
|
||
handleImage(tempFilePath) {
|
||
// 在H5平台下,tempFilePath 为 base64
|
||
// console.log(tempFilePath)
|
||
uni.hideLoading();
|
||
this.$emit('crop', {
|
||
tempFilePath
|
||
});
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.image-cropper {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
top: 0;
|
||
bottom: 0;
|
||
overflow: hidden;
|
||
display: flex;
|
||
flex-direction: column;
|
||
background-color: #000;
|
||
|
||
.img-canvas {
|
||
position: absolute !important;
|
||
transform: translateX(-100%);
|
||
}
|
||
|
||
.pic-preview {
|
||
width: 100%;
|
||
flex: 1;
|
||
position: relative;
|
||
|
||
.crop-mask-block {
|
||
background-color: rgba(51, 51, 51, 0.8);
|
||
z-index: 2;
|
||
position: fixed;
|
||
box-sizing: border-box;
|
||
pointer-events: none;
|
||
}
|
||
|
||
.crop-circle-box {
|
||
position: fixed;
|
||
box-sizing: border-box;
|
||
z-index: 2;
|
||
pointer-events: none;
|
||
overflow: hidden;
|
||
|
||
.crop-circle {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.crop-image {
|
||
padding: 0 !important;
|
||
margin: 0 !important;
|
||
border-radius: 0 !important;
|
||
display: block !important;
|
||
backface-visibility: hidden;
|
||
}
|
||
|
||
.crop-border {
|
||
position: fixed;
|
||
// border: 1px solid #fff;
|
||
box-sizing: border-box;
|
||
z-index: 3;
|
||
pointer-events: none;
|
||
// border-radius: 50rpx;
|
||
// margin-left: 200rpx;
|
||
|
||
// padding: 20rpx;
|
||
.border-son{
|
||
width: 625rpx;
|
||
height: 440rpx;
|
||
margin-top: -33rpx;
|
||
margin-left: -34rpx;
|
||
// border: 1px solid #fff;
|
||
border-radius: 30rpx;
|
||
background-image: url('https://www.focusnu.com/media/directive/login/border.png');
|
||
background-size: 100% 100%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
// width: 65rpx;
|
||
// height: 65rpx;
|
||
}
|
||
}
|
||
|
||
.crop-grid {
|
||
position: fixed;
|
||
z-index: 3;
|
||
border-style: dashed;
|
||
border-color: #fff;
|
||
pointer-events: none;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.crop-angle {
|
||
position: fixed;
|
||
z-index: 3;
|
||
border-style: solid;
|
||
border-color: #fff;
|
||
pointer-events: none;
|
||
}
|
||
}
|
||
|
||
.fixed-bottom {
|
||
position: fixed;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
z-index: 99;
|
||
display: flex;
|
||
flex-direction: row;
|
||
// background-color: $uni-bg-color-grey;
|
||
// border-top-right-radius: 30rpx;
|
||
// border-top-left-radius: 30rpx;
|
||
|
||
.action-bar-anther {
|
||
position: absolute;
|
||
top: -110rpx;
|
||
left: 170rpx;
|
||
display: flex;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: rgb(63, 63, 63);
|
||
border-radius: 50%;
|
||
|
||
.rotate-icon {
|
||
background-image: url('https://www.focusnu.com/media/directive/login/smallicon.png');
|
||
background-size: 60% 60%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
width: 65rpx;
|
||
height: 65rpx;
|
||
|
||
&.is-reverse {
|
||
transform: rotateY(0deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-bar {
|
||
position: absolute;
|
||
top: -110rpx;
|
||
left: 40rpx;
|
||
display: flex;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: rgb(63, 63, 63);
|
||
border-radius: 50%;
|
||
|
||
.rotate-icon {
|
||
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABCFJREFUaEPtml3IpVMUx3//ko/ChTIyiGFSMyhllI8bc4F85yuNC2FCqLmQC1+FZORiEkUMNW7UjKjJULgxV+NzSkxDhEkZgwsyigv119J63p7zvOc8z37OmXdOb51dz82711r7/99r7bXXXucVi3xokeNnRqCvB20fDmwAlgK/5bcD+FTSr33tHXQP2H4MeHQE0A+B5yRtLiUyDQJrgVc6AAaBpyV93kXkoBMIQLbfBS5NcK8BRwDXNcD+AdwnaVMbiWkRCPBBohpxHuK7M7865sclRdgNHVMhkF6IMIpwirFEUhzo8M7lwIvASTXEqyVtH8ZgagQSbOzsDknv18HZXpHn5IL8+94IOUm7miSmSqAttjPdbgGuTrnNktYsGgLpoYuAD2qg1zRTbG8P2D4SOC6/Q7vSHPALsE/S7wWy80RsPw/ckxMfSTq/LtRJwPbxwF3ASiCUTxwHCPAnEBfVF8AWSTtL7Ng+LfWOTfmlkn6udFsJ5K15R6a4kvX6yGyUFBvTOWzHXXFzCt4g6c1OArYj9iIGh43YgR+BvztXh1PSa4cMkd0jaVmXDduPAE+k3HpJD7cSGFKvfAc8FQUX8IOk/V2L1udtB/hTgdOBW4Aba/M7Ja1qs2f7euCNlHlZUlx4/495IWQ7Jl+qGbxX0gt9AHfJ2o6zFBVoNVrDKe+F3Sm8VdK1bQQ+A85JgXckXdkFaJx527cC9TpnVdvBtl3h2iapuhsGPdBw1b9xnUvaNw7AEh3bnwDnpuwGSfeP0rN9NvAMELXRXFkxEEK2nwQeSiOtRVQJwC4Z29cAW1Nuu6TVXTrN+SaBt4ErUug2Sa/2NdhH3vZy4NvU2S/p6D768w5xI3WOrAD7LtISFpGdIhVXKfaYvjd20wP13L9M0p4DBbaFRKToSLExVkr6qs+aIwlI6iwz+izUQqC+ab29PiMwqRcmPXczD8w8MFj1zg7xXEqbpdHCw7FgWSjafZL+KcQxtpjteCeflwYulFR/J3TabSslVkj6utPChAK2f6q9uZdLitKieLQRuExSvX9ZbLRUMFs09efpUZL+KtUfVo1GW/umNHC3pOhRLtiwfSbwZS6wV9IJfRdreuBBYH0a2STp9r4G+8jbXgc8mzoDT8VSO00ClwDv1ZR7XyylC4ec7ejaLUmdsV6Aw7oSbwFXpdFdks7qA6pU1na0aR6owgeIR/1cx63UzjAC0YXYVjMQHlkn6ZtSo21ytuPZGKFagQ/xsXZ/3iGuFrYdjafXG0DiQMeBi47c9/GV3BO247UV38n5o0UAP6xmu7jFOGxjRr66On5NPBDOCBsDTapxjHY1dyOcolNXnYlx1himE53p2PmNkxosevfavhg4Izt2k7TXPwZ2S6p6QZPin/2rwcQ7OKmBohCadJGF1P8PG6aaQBKVX/8AAAAASUVORK5CYII=');
|
||
background-size: 60% 60%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
width: 70rpx;
|
||
height: 70rpx;
|
||
|
||
&.is-reverse {
|
||
transform: rotateY(180deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.action-bar-right {
|
||
position: absolute;
|
||
top: -110rpx;
|
||
right: 50rpx;
|
||
display: flex;
|
||
width: 240rpx;
|
||
height: 100rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: rgb(63, 63, 63);
|
||
border-radius: 50rpx;
|
||
color: #fff;
|
||
font-size: 30rpx;
|
||
}
|
||
|
||
.rechoose {
|
||
color: rgb(51, 51, 51);
|
||
padding: 0 $uni-spacing-row-lg;
|
||
line-height: 100rpx;
|
||
}
|
||
|
||
.choose-btn {
|
||
color: $uni-color-primary;
|
||
text-align: center;
|
||
line-height: 100rpx;
|
||
flex: 1;
|
||
}
|
||
|
||
.button {
|
||
margin: auto $uni-spacing-row-lg auto auto;
|
||
// background-color: $uni-color-primary;
|
||
background: linear-gradient(to right, #00C9FF, #0076FF);
|
||
// background: ;
|
||
color: #fff;
|
||
border-radius: 20rpx;
|
||
margin-top: 30rpx;
|
||
}
|
||
}
|
||
|
||
.safe-area-inset-bottom {
|
||
padding-bottom: 0;
|
||
padding-bottom: constant(safe-area-inset-bottom); // 兼容 IOS<11.2
|
||
padding-bottom: env(safe-area-inset-bottom); // 兼容 IOS>=11.2
|
||
}
|
||
|
||
}
|
||
|
||
.action-bar-special {
|
||
display: flex;
|
||
width: 100rpx;
|
||
height: 100rpx;
|
||
justify-content: center;
|
||
align-items: center;
|
||
background-color: rgb(63, 63, 63);
|
||
border-radius: 50%;
|
||
z-index: 3;
|
||
z-index: 999;
|
||
margin-top: 0rpx;
|
||
margin-bottom: 35rpx;
|
||
.rotate-icon {
|
||
background-image: url('https://www.focusnu.com/media/directive/login/jietu.png');
|
||
background-size: 60% 60%;
|
||
background-repeat: no-repeat;
|
||
background-position: center;
|
||
width: 65rpx;
|
||
height: 65rpx;
|
||
|
||
&.is-reverse {
|
||
transform: rotateY(0deg);
|
||
}
|
||
}
|
||
}
|
||
|
||
.whitefont {
|
||
font-size: 25rpx;
|
||
color: #fff;
|
||
width: 100%;
|
||
z-index: 3;
|
||
// z-index: 998;
|
||
display: flex;
|
||
align-items: center;
|
||
position: absolute;
|
||
top: 72%;
|
||
left: 50%;
|
||
transform: translate(-50%, -72%);
|
||
flex-direction: column;
|
||
}
|
||
</style> |