首次完整推送,

V:1.20240808.006
This commit is contained in:
fm453
2024-08-13 18:32:37 +08:00
parent 15be3e9373
commit c62d15b288
939 changed files with 111777 additions and 0 deletions

View File

@ -0,0 +1,227 @@
> 插件来源:[https://ext.dcloud.net.cn/plugin?id=3594](https://ext.dcloud.net.cn/plugin?id=3594)
##### 以下是作者写的插件介绍:
# Clipper 图片裁剪
> uniapp 图片裁剪,可用于图片头像等裁剪处理
> [查看更多](http://liangei.gitee.io/limeui/#/clipper) <br>
> Q群458377637
## 平台兼容
| H5 | 微信小程序 | 支付宝小程序 | 百度小程序 | 头条小程序 | QQ 小程序 | App |
| --- | ---------- | ------------ | ---------- | ---------- | --------- | --- |
| √ | √ | √ | 未测 | √ | √ | √ |
## 代码演示
### 基本用法
`@success` 事件点击 👉 **确定** 后会返回生成的图片信息,包含 `url``width``height`
```html
<image :src="url" v-if="url" mode="widthFix"></image>
<l-clipper v-if="show" @success="url = $event.url; show = false" @cancel="show = false" ></l-clipper>
<button @tap="show = true">裁剪</button>
```
```js
// 非uni_modules引入
import lClipper from '@/components/lime-clipper/'
// uni_modules引入
import lClipper from '@/uni_modules/lime-clipper/components/lime-clipper/'
export default {
components: {lClipper},
data() {
return {
show: false,
url: '',
}
}
}
```
### 传入图片
`image-url`可传入**相对路径**、**临时路径**、**本地路径**、**网络图片**<br>
* **当为网络地址时**
* H5👉 需要解决跨域问题。 <br>
* 小程序:👉 需要配置 downloadFile 域名 <br>
```html
<image :src="url" v-if="url" mode="widthFix"></image>
<l-clipper v-if="show" :image-url="imageUrl" @success="url = $event.url; show = false" @cancel="show = false" ></l-clipper>
<button @tap="show = true">裁剪</button>
```
```js
export default {
components: {lClipper},
data() {
return {
imageUrl: 'https://img12.360buyimg.com/pop/s1180x940_jfs/t1/97205/26/1142/87801/5dbac55aEf795d962/48a4d7a63ff80b8b.jpg',
show: false,
url: '',
}
}
}
```
### 确定按钮颜色
样式变量名:`--l-clipper-confirm-color`
可放到全局样式的 `page` 里或节点的 `style`
```html
<l-clipper class="clipper" style="--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)" ></l-clipper>
```
```css
// css 中为组件设置 CSS 变量
.clipper {
--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)
}
// 全局
page {
--l-clipper-confirm-color: linear-gradient(to right, #ff6034, #ee0a24)
}
```
### 使用插槽
共五个插槽 `cancel` 取消按钮、 `photo` 选择图片按钮、 `rotate` 旋转按钮、 `confirm` 确定按钮和默认插槽。
```html
<image :src="url" v-if="url" mode="widthFix"></image>
<l-clipper
v-if="show"
:isLockWidth="isLockWidth"
:isLockHeight="isLockHeight"
:isLockRatio="isLockRatio"
:isLimitMove="isLimitMove"
:isDisableScale="isDisableScale"
:isDisableRotate="isDisableRotate"
:isShowCancelBtn="isShowCancelBtn"
:isShowPhotoBtn="isShowPhotoBtn"
:isShowRotateBtn="isShowRotateBtn"
:isShowConfirmBtn="isShowConfirmBtn"
@success="url = $event.url; show = false"
@cancel="show = false" >
<!-- 四个基本按钮插槽 -->
<view slot="cancel">取消</view>
<view slot="photo">选择图片</view>
<view slot="rotate">旋转</view>
<view slot="confirm">确定</view>
<!-- 默认插槽 -->
<view class="tools">
<view>显示取消按钮
<switch :checked="isShowCancelBtn" @change="isShowCancelBtn = $event.target.value" ></switch>
</view>
<view>显示选择图片按钮
<switch :checked="isShowPhotoBtn" @change="isShowPhotoBtn = $event.target.value" ></switch>
</view>
<view>显示旋转按钮
<switch :checked="isShowRotateBtn" @change="isShowRotateBtn = $event.target.value" ></switch>
</view>
<view>显示确定按钮
<switch :checked="isShowConfirmBtn" @change="isShowConfirmBtn = $event.target.value" ></switch>
</view>
<view>锁定裁剪框宽度
<switch :checked="isLockWidth" @change="isLockWidth = $event.target.value" ></switch>
</view>
<view>锁定裁剪框高度
<switch :checked="isLockHeight" @change="isLockHeight = $event.target.value" ></switch>
</view>
<view>锁定裁剪框比例
<switch :checked="isLockRatio" @change="isLockRatio = $event.target.value" ></switch>
</view>
<view>限制移动范围
<switch :checked="isLimitMove" @change="isLimitMove = $event.target.value" ></switch>
</view>
<view>禁止缩放
<switch :checked="isDisableScale" @change="isDisableScale = $event.target.value" ></switch>
</view>
<view>禁止旋转
<switch :checked="isDisableRotate" @change="isDisableRotate = $event.target.value" ></switch>
</view>
</view>
</l-clipper>
<button @tap="show = true">裁剪</button>
```
```js
export default {
components: {lClipper},
data() {
return {
show: false,
url: '',
isLockWidth: false,
isLockHeight: false,
isLockRatio: true,
isLimitMove: false,
isDisableScale: false,
isDisableRotate: false,
isShowCancelBtn: true,
isShowPhotoBtn: true,
isShowRotateBtn: true,
isShowConfirmBtn: true
}
}
}
```
## API
### Props
| 参数 | 说明 | 类型 | 默认值 |
| ------------- | ------------ | ---------------- | ------------ |
| image-url | 图片路径 | <em>string</em> | |
| quality | 图片的质量,取值范围为 [0, 1]不在范围内时当作1处理 | <em>number</em> | `1` |
| source | `{album: '从相册中选择'}`key为图片来源类型value为选项说明 | <em>Object</em> | |
| width | 裁剪框宽度,单位为 `rpx` | <em>number</em> | `400` |
| height | 裁剪框高度 | <em>number</em> | `400` |
| min-width | 裁剪框最小宽度 | <em>number</em> | `200` |
| min-height |裁剪框最小高度 | <em>number</em> | `200` |
| max-width | 裁剪框最大宽度 | <em>number</em> | `600` |
| max-height | 裁剪框最大宽度 | <em>number</em> | `600` |
| min-ratio | 图片最小缩放比 | <em>number</em> | `0.5` |
| max-ratio | 图片最大缩放比 | <em>number</em> | `2` |
| rotate-angle | 旋转按钮每次旋转的角度 | <em>number</em> | `90` |
| scale-ratio | 生成图片相对于裁剪框的比例, **比例越高生成图片越清晰** | <em>number</em> | `1` |
| is-lock-width | 是否锁定裁剪框宽度 | <em>boolean</em> | `false` |
| is-lock-height | 是否锁定裁剪框高度上 | <em>boolean</em> | `false` |
| is-lock-ratio | 是否锁定裁剪框比例 | <em>boolean</em> | `true` |
| is-disable-scale | 是否禁止缩放 | <em>boolean</em> | `false` |
| is-disable-rotate | 是否禁止旋转 | <em>boolean</em> | `false` |
| is-limit-move | 是否限制移动范围 | <em>boolean</em> | `false` |
| is-show-photo-btn | 是否显示选择图片按钮 | <em>boolean</em> | `true` |
| is-show-rotate-btn | 是否显示转按钮 | <em>boolean</em> | `true` |
| is-show-confirm-btn | 是否显示确定按钮 | <em>boolean</em> | `true` |
| is-show-cancel-btn | 是否显示关闭按钮 | <em>boolean</em> | `true` |
### 事件 Events
| 事件名 | 说明 | 回调 |
| ------- | ------------ | -------------- |
| success | 生成图片成功 | {`width`, `height`, `url`} |
| fail | 生成图片失败 | `error` |
| cancel | 关闭 | `false` |
| ready | 图片加载完成 | {`width`, `height`, `path`, `orientation`, `type`} |
| change | 图片大小改变时触发 | {`width`, `height`} |
| rotate | 图片旋转时触发 | `angle` |
## 常见问题
> 1、H5端使用网络图片需要解决跨域问题。<br>
> 2、小程序使用网络图片需要去公众平台增加下载白名单二级域名也需要配<br>
> 3、H5端生成图片是base64有时显示只有一半可以使用原生标签`<IMG/>`<br>
> 4、IOS APP 请勿使用HBX2.9.3.20201014的版本!这个版本无法生成图片。<br>
> 5、APP端无成功反馈、也无失败反馈时请更新基座和HBX。<br>
## 打赏
如果你觉得本插件,解决了你的问题,赠人玫瑰,手留余香。<br>
![输入图片说明](https://images.gitee.com/uploads/images/2020/1122/222521_bb543f96_518581.jpeg "微信图片编辑_20201122220352.jpg")

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:#606060;}
.st1{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
.st2{fill:#FFFFFF;}
</style>
<g>
<path class="st2" d="M11.6,11c0.4,0.4,0.6,0.9,0.6,1.5c0,0.6-0.2,1.1-0.6,1.4c-0.4,0.4-0.9,0.6-1.5,0.6c-0.6,0-1.1-0.2-1.5-0.6
c-0.4-0.4-0.6-0.9-0.6-1.4s0.2-1.1,0.6-1.5c0.4-0.4,0.9-0.6,1.5-0.6C10.8,10.4,11.2,10.6,11.6,11z M24.6,18.4V6.7H5.4v12l1.8-1.8
c0.3-0.3,0.6-0.4,1-0.4c0.4,0,0.7,0.1,1,0.4l1.8,1.8l5.8-7c0.3-0.3,0.6-0.5,1.1-0.5c0.4,0,0.8,0.2,1.1,0.5
C18.8,11.6,24.6,18.4,24.6,18.4z M25.6,5.7C25.9,6,26,6.3,26,6.7v16.1c0,0.4-0.1,0.7-0.4,1c-0.3,0.3-0.6,0.4-1,0.4H5.4
c-0.4,0-0.7-0.1-1-0.4c-0.3-0.3-0.4-0.6-0.4-1V6.7c0-0.4,0.1-0.7,0.4-1c0.3-0.3,0.6-0.4,1-0.4h19.3C25,5.3,25.3,5.4,25.6,5.7z"/>
<path class="st1" d="M24.3,21.5H5.7c-0.2,0-0.3-0.2-0.3-0.3V7c0-0.2,0.2-0.3,0.3-0.3h18.6c0.2,0,0.3,0.2,0.3,0.3v14.2
C24.6,21.3,24.5,21.5,24.3,21.5z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="图层_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="30px" height="30px" viewBox="0 0 30 30" style="enable-background:new 0 0 30 30;" xml:space="preserve">
<style type="text/css">
.st0{fill:none;stroke:#FFFFFF;stroke-width:2.4306;stroke-miterlimit:10;}
.st1{fill:#FFFFFF;}
</style>
<g>
<path class="st0" d="M17.1,24.2h-12c-0.2,0-0.3-0.2-0.3-0.3v-9.3c0-0.2,0.2-0.3,0.3-0.3h12c0.2,0,0.3,0.2,0.3,0.3v9.3
C17.5,24.1,17.3,24.2,17.1,24.2z"/>
<path class="st0" d="M16.6,5.4c4.8,0,8.7,3.9,8.7,8.7"/>
<polyline class="st0" points="19.3,10.1 14.9,5.6 19.3,1.2 "/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 791 B

View File

@ -0,0 +1,160 @@
.flex-auto {
flex: auto;
}
.bg-transparent {
background-color: rgba(0,0,0,0.9);
transition-duration: 0.35s;
}
.l-clipper {
width: 100vw;
height: calc(100vh - var(--window-top));
background-color: rgba(0,0,0,0.9);
position: fixed;
top: var(--window-top);
left: 0;
z-index: 1;
}
.l-clipper-mask {
position: relative;
z-index: 2;
pointer-events: none;
}
.l-clipper__content {
pointer-events: none;
position: absolute;
border: 1rpx solid rgba(255,255,255,0.3);
box-sizing: border-box;
box-shadow: rgba(0,0,0,0.5) 0 0 0 80vh;
background: transparent;
}
.l-clipper__content::before,
.l-clipper__content::after {
content: '';
position: absolute;
border: 1rpx dashed rgba(255,255,255,0.3);
}
.l-clipper__content::before {
width: 100%;
top: 33.33%;
height: 33.33%;
border-left: none;
border-right: none;
}
.l-clipper__content::after {
width: 33.33%;
left: 33.33%;
height: 100%;
border-top: none;
border-bottom: none;
}
.l-clipper__edge {
position: absolute;
width: 34rpx;
height: 34rpx;
border: 6rpx solid #fff;
pointer-events: auto;
}
.l-clipper__edge::before {
content: '';
position: absolute;
width: 40rpx;
height: 40rpx;
background-color: transparent;
}
.l-clipper__edge:nth-child(1) {
left: -6rpx;
top: -6rpx;
border-bottom-width: 0 !important;
border-right-width: 0 !important;
}
.l-clipper__edge:nth-child(1):before {
top: -50%;
left: -50%;
}
.l-clipper__edge:nth-child(2) {
right: -6rpx;
top: -6rpx;
border-bottom-width: 0 !important;
border-left-width: 0 !important;
}
.l-clipper__edge:nth-child(2):before {
top: -50%;
left: 50%;
}
.l-clipper__edge:nth-child(3) {
left: -6rpx;
bottom: -6rpx;
border-top-width: 0 !important;
border-right-width: 0 !important;
}
.l-clipper__edge:nth-child(3):before {
bottom: -50%;
left: -50%;
}
.l-clipper__edge:nth-child(4) {
right: -6rpx;
bottom: -6rpx;
border-top-width: 0 !important;
border-left-width: 0 !important;
}
.l-clipper__edge:nth-child(4):before {
bottom: -50%;
left: 50%;
}
.l-clipper-image {
width: 100%;
border-style: none;
position: absolute;
top: 0;
left: 0;
z-index: 1;
-webkit-backface-visibility: hidden;
backface-visibility: hidden;
transform-origin: center;
}
.l-clipper-canvas {
position: fixed;
z-index: 10;
left: -200vw;
top: -200vw;
pointer-events: none;
}
.l-clipper-tools {
position: fixed;
left: 0;
bottom: 10px;
width: 100%;
z-index: 99;
color: #fff;
}
.l-clipper-tools__btns {
font-weight: bold;
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 20rpx 40rpx;
box-sizing: border-box;
}
.l-clipper-tools__btns .cancel {
width: 112rpx;
height: 60rpx;
text-align: center;
line-height: 60rpx;
}
.l-clipper-tools__btns .confirm {
width: 112rpx;
height: 60rpx;
line-height: 60rpx;
background-color: #07c160;
border-radius: 6rpx;
text-align: center;
}
.l-clipper-tools__btns image {
display: block;
width: 60rpx;
height: 60rpx;
}
.l-clipper-tools__btns {
flex-direction: row;
}

View File

@ -0,0 +1,820 @@
<template>
<view class="l-clipper" :class="{open: value}" disable-scroll :style="'z-index: ' + zIndex + ';' + customStyle">
<view class="l-clipper-mask" @touchstart.stop.prevent="clipTouchStart" @touchmove.stop.prevent="clipTouchMove" @touchend.stop.prevent="clipTouchEnd">
<view class="l-clipper__content" :style="clipStyle"><view class="l-clipper__edge" v-for="(item, index) in [0, 0, 0, 0]" :key="index"></view></view>
</view>
<image
class="l-clipper-image"
@error="imageLoad"
@load="imageLoad"
@touchstart.stop.prevent="imageTouchStart"
@touchmove.stop.prevent="imageTouchMove"
@touchend.stop.prevent="imageTouchEnd"
:src="image"
:mode="imageWidth == 'auto' ? 'widthFix' : ''"
v-if="image"
:style="imageStyle"
/>
<canvas
:canvas-id="canvasId"
id="l-clipper"
disable-scroll
:style="'width: ' + canvasWidth * scaleRatio + 'px; height:' + canvasHeight * scaleRatio + 'px;'"
class="l-clipper-canvas"
></canvas>
<view class="l-clipper-tools">
<view class="l-clipper-tools__btns">
<view v-if="isShowCancelBtn" @tap="cancel">
<slot name="cancel" v-if="$slots.cancel" />
<view v-else class="cancel">取消</view>
</view>
<view v-if="isShowPhotoBtn" @tap="uploadImage">
<slot name="photo" v-if="$slots.photo" />
<image v-else src="./images/photo.svg" />
</view>
<view v-if="isShowRotateBtn" @tap="rotate">
<slot name="rotate" v-if="$slots.rotate" />
<image v-else src="./images/rotate.svg" data-type="inverse" />
</view>
<view v-if="isShowConfirmBtn" @tap="confirm">
<slot name="confirm" v-if="$slots.confirm" />
<view v-else class="confirm">确定</view>
</view>
</view>
<slot></slot>
</view>
</view>
</template>
<script>
import { determineDirection, calcImageOffset, calcImageScale, calcImageSize, calcPythagoreanTheorem, clipTouchMoveOfCalculate, imageTouchMoveOfCalcOffset } from './utils';
const cache = {}
export default {
// version: '0.6.3',
name: 'l-clipper',
props: {
value: {
type: Boolean,
default: true
},
// #ifdef MP-WEIXIN
type: {
type: String,
default: '2d'
},
// #endif
customStyle: {
type: String,
},
canvasId: {
type: String,
default: 'l-clipper'
},
zIndex: {
type: Number,
default: 99
},
imageUrl: {
type: String
},
fileType: {
type: String,
default: 'png'
},
quality: {
type: Number,
default: 1
},
width: {
type: Number,
default: 400
},
height: {
type: Number,
default: 400
},
minWidth: {
type: Number,
default: 200
},
maxWidth: {
type: Number,
default: 600
},
minHeight: {
type: Number,
default: 200
},
maxHeight: {
type: Number,
default: 600
},
isLockWidth: {
type: Boolean,
default: false
},
isLockHeight: {
type: Boolean,
default: false
},
isLockRatio: {
type: Boolean,
default: true
},
scaleRatio: {
type: Number,
default: 1
},
minRatio: {
type: Number,
default: 0.5
},
maxRatio: {
type: Number,
default: 2
},
isDisableScale: {
type: Boolean,
default: false
},
isDisableRotate: {
type: Boolean,
default: false
},
isLimitMove: {
type: Boolean,
default: false
},
isShowPhotoBtn: {
type: Boolean,
default: true
},
isShowRotateBtn: {
type: Boolean,
default: true
},
isShowConfirmBtn: {
type: Boolean,
default: true
},
isShowCancelBtn: {
type: Boolean,
default: true
},
rotateAngle: {
type: Number,
default: 90
},
source: {
type: Object,
default: () => ({
album: '从相册中选择',
camera: '拍照',
// #ifdef MP-WEIXIN
message: '从微信中选择'
// #endif
})
}
},
data() {
return {
canvasWidth: 0,
canvasHeight: 0,
clipX: 0,
clipY: 0,
clipWidth: 0,
clipHeight: 0,
animation: false,
imageWidth: 0,
imageHeight: 0,
imageTop: 0,
imageLeft: 0,
scale: 1,
angle: 0,
image: this.imageUrl,
sysinfo: {},
throttleTimer: null,
throttleFlag: true,
timeClipCenter: null,
flagClipTouch: false,
flagEndTouch: false,
clipStart: {},
animationTimer: null,
touchRelative: [{x: 0,y: 0}],
hypotenuseLength: 0,
ctx: null
};
},
computed: {
clipStyle() {
const {clipWidth, clipHeight, clipY, clipX, animation} = this
return `
width: ${clipWidth}px;
height:${clipHeight}px;
transition-property: ${animation ? '' : 'background'};
left: ${clipX}px;
top: ${clipY}px
`
},
imageStyle() {
const {imageWidth, imageHeight, imageLeft, imageTop, animation, scale, angle} = this
return `
width: ${imageWidth ? imageWidth + 'px' : 'auto'};
height: ${imageHeight ? imageHeight + 'px' : 'auto'};
transform: translate3d(${imageLeft - imageWidth / 2}px, ${imageTop - imageHeight / 2}px, 0) scale(${scale}) rotate(${angle}deg);
transition-duration: ${animation ? 0.35 : 0}s
`
},
clipSize() {
const { clipWidth, clipHeight } = this;
return { clipWidth, clipHeight };
},
clipPoint() {
const { clipY, clipX } = this;
return { clipY, clipX };
}
},
watch: {
value(val) {
if(!val) {
this.animation = 0
this.angle = 0
} else {
if(this.imageUrl) {
const {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight, path} = cache?.[this.imageUrl] || {}
if(path != this.image) {
this.image = this.imageUrl;
} else {
this.setDiffData({imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight})
}
}
}
},
imageUrl(url) {
this.image = url
},
image:{
handler: async function(url) {
this.getImageInfo(url)
},
// immediate: true,
},
clipSize({ widthVal, heightVal }) {
let { minWidth, minHeight } = this;
minWidth = minWidth / 2;
minHeight = minHeight / 2;
if (widthVal < minWidth) {
this.setDiffData({clipWidth: minWidth})
}
if (heightVal < minHeight) {
this.setDiffData({clipHeight: minHeight})
}
this.calcClipSize();
},
angle(val) {
this.animation = true;
this.moveStop();
const { isLimitMove } = this;
if (isLimitMove && val % 90) {
this.setDiffData({
angle: Math.round(val / 90) * 90
})
}
this.imgMarginDetectionScale();
},
animation(val) {
clearTimeout(this.animationTimer);
if (val) {
let animationTimer = setTimeout(() => {
this.setDiffData({
animation: false
})
}, 260);
this.setDiffData({animationTimer})
this.animationTimer = animationTimer;
}
},
isLimitMove(val) {
if (val) {
if (this.angle % 90) {
this.setDiffData({
angle : Math.round(this.angle / 90) * 90
})
}
this.imgMarginDetectionScale();
}
},
clipPoint() {
this.cutDetectionPosition();
},
width(width, oWidth) {
if (width !== oWidth) {
this.setDiffData({
clipWidth: width / 2
})
}
},
height(height, oHeight) {
if (height !== oHeight) {
this.setDiffData({
clipHeight: height / 2
})
}
}
},
mounted() {
const sysinfo = uni.getSystemInfoSync();
this.sysinfo = sysinfo;
this.setClipInfo();
if(this.image) {
this.getImageInfo(this.image)
}
this.setClipCenter();
this.calcClipSize();
this.cutDetectionPosition();
},
methods: {
setDiffData(data) {
Object.keys(data).forEach(key => {
if (this[key] !== data[key]) {
this[key] = data[key];
}
});
},
getImageInfo(url) {
if (!url) return;
if(this.value) {
uni.showLoading({
title: '请稍候...',
mask: true
});
}
uni.getImageInfo({
src: url,
success: res => {
this.imgComputeSize(res.width, res.height);
this.image = res.path;
if (this.isLimitMove) {
this.imgMarginDetectionScale();
this.$emit('ready', res);
}
const {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight} = this
cache[url] = Object.assign(res, {imageWidth, imageHeight, imageLeft, imageTop, scale, clipX, clipY, clipWidth, clipHeight});
},
fail: (err) => {
this.imgComputeSize();
if (this.isLimitMove) {
this.imgMarginDetectionScale();
}
}
});
},
setClipInfo() {
const { width, height, sysinfo, canvasId } = this;
const clipWidth = width / 2;
const clipHeight = height / 2;
const clipY = (sysinfo.windowHeight - clipHeight) / 2;
const clipX = (sysinfo.windowWidth - clipWidth) / 2;
const imageLeft = sysinfo.windowWidth / 2;
const imageTop = sysinfo.windowHeight / 2;
this.ctx = uni.createCanvasContext(canvasId, this);
this.clipWidth = clipWidth;
this.clipHeight = clipHeight;
this.clipX = clipX;
this.clipY = clipY;
this.canvasHeight = clipHeight;
this.canvasWidth = clipWidth;
this.imageLeft = imageLeft;
this.imageTop = imageTop;
},
setClipCenter() {
const { sysInfo, clipHeight, clipWidth, imageTop, imageLeft } = this;
let sys = sysInfo || uni.getSystemInfoSync();
let clipY = (sys.windowHeight - clipHeight) * 0.5;
let clipX = (sys.windowWidth - clipWidth) * 0.5;
this.imageTop = imageTop - this.clipY + clipY;
this.imageLeft = imageLeft - this.clipX + clipX;
this.clipY = clipY;
this.clipX = clipX;
},
calcClipSize() {
const { clipHeight, clipWidth, sysinfo, clipX, clipY } = this;
if (clipWidth > sysinfo.windowWidth) {
this.setDiffData({
clipWidth: sysinfo.windowWidth
})
} else if (clipWidth + clipX > sysinfo.windowWidth) {
this.setDiffData({
clipX: sysinfo.windowWidth - clipX
})
}
if (clipHeight > sysinfo.windowHeight) {
this.setDiffData({
clipHeight: sysinfo.windowHeight
})
} else if (clipHeight + clipY > sysinfo.windowHeight) {
this.clipY = sysinfo.windowHeight - clipY;
this.setDiffData({
clipY: sysinfo.windowHeight - clipY
})
}
},
cutDetectionPosition() {
const { clipX, clipY, sysinfo, clipHeight, clipWidth } = this;
let cutDetectionPositionTop = () => {
if (clipY < 0) {
this.setDiffData({clipY: 0})
}
if (clipY > sysinfo.windowHeight - clipHeight) {
this.setDiffData({clipY: sysinfo.windowHeight - clipHeight})
}
},
cutDetectionPositionLeft = () => {
if (clipX < 0) {
this.setDiffData({clipX: 0})
}
if (clipX > sysinfo.windowWidth - clipWidth) {
this.setDiffData({clipX: sysinfo.windowWidth - clipWidth})
}
};
if (clipY === null && clipX === null) {
let newClipY = (sysinfo.windowHeight - clipHeight) * 0.5;
let newClipX = (sysinfo.windowWidth - clipWidth) * 0.5;
this.setDiffData({
clipX: newClipX,
clipY: newClipY
})
} else if (clipY !== null && clipX !== null) {
cutDetectionPositionTop();
cutDetectionPositionLeft();
} else if (clipY !== null && clipX === null) {
cutDetectionPositionTop();
this.setDiffData({
clipX: (sysinfo.windowWidth - clipWidth) / 2
})
} else if (clipY === null && clipX !== null) {
cutDetectionPositionLeft();
this.setDiffData({
clipY: (sysinfo.windowHeight - clipHeight) / 2
})
}
},
imgComputeSize(width, height) {
const { imageWidth, imageHeight } = calcImageSize(width, height, this);
this.imageWidth = imageWidth;
this.imageHeight = imageHeight;
},
imgMarginDetectionScale(scale) {
if (!this.isLimitMove) return;
const currentScale = calcImageScale(this, scale);
this.imgMarginDetectionPosition(currentScale);
},
imgMarginDetectionPosition(scale) {
if (!this.isLimitMove) return;
const { scale: currentScale, left, top } = calcImageOffset(this, scale);
this.setDiffData({
imageLeft: left,
imageTop: top,
scale: currentScale
})
},
throttle() {
this.setDiffData({
throttleFlag: true
})
},
moveDuring() {
clearTimeout(this.timeClipCenter);
},
moveStop() {
clearTimeout(this.timeClipCenter);
const timeClipCenter = setTimeout(() => {
if (!this.animation) {
this.setDiffData({animation: true})
}
this.setClipCenter();
}, 800);
this.setDiffData({timeClipCenter})
},
clipTouchStart(event) {
// #ifdef H5
event.preventDefault()
// #endif
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none',
duration: 3000
});
return;
}
const currentX = event.touches[0].clientX;
const currentY = event.touches[0].clientY;
const { clipX, clipY, clipWidth, clipHeight } = this;
const corner = determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY);
this.moveDuring();
if(!corner) {return}
this.clipStart = {
width: clipWidth,
height: clipHeight,
x: currentX,
y: currentY,
clipY,
clipX,
corner
};
this.flagClipTouch = true;
this.flagEndTouch = true;
},
clipTouchMove(event) {
// #ifdef H5
event.stopPropagation()
event.preventDefault()
// #endif
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none',
duration: 3000
});
return;
}
// 只针对单指点击做处理
if (event.touches.length !== 1) {
return;
}
const { flagClipTouch, throttleFlag } = this;
if (flagClipTouch && throttleFlag) {
const { isLockRatio, isLockHeight, isLockWidth } = this;
if (isLockRatio && (isLockWidth || isLockHeight)) return;
this.setDiffData({
throttleFlag: false
})
this.throttle();
const clipData = clipTouchMoveOfCalculate(this, event);
if(clipData) {
const { width, height, clipX, clipY } = clipData;
if (!isLockWidth && !isLockHeight) {
this.setDiffData({
clipWidth: width,
clipHeight: height,
clipX,
clipY
})
} else if (!isLockWidth) {
this.setDiffData({
clipWidth: width,
clipX
})
} else if (!isLockHeight) {
this.setDiffData({
clipHeight: height,
clipY
})
}
this.imgMarginDetectionScale();
}
}
},
clipTouchEnd() {
this.moveStop();
this.flagClipTouch = false;
},
imageTouchStart(e) {
// #ifdef H5
event.preventDefault()
// #endif
this.flagEndTouch = false;
const { imageLeft, imageTop } = this;
const clientXForLeft = e.touches[0].clientX;
const clientYForLeft = e.touches[0].clientY;
let touchRelative = [];
if (e.touches.length === 1) {
touchRelative[0] = {
x: clientXForLeft - imageLeft,
y: clientYForLeft - imageTop
};
this.touchRelative = touchRelative;
} else {
const clientXForRight = e.touches[1].clientX;
const clientYForRight = e.touches[1].clientY;
let width = Math.abs(clientXForLeft - clientXForRight);
let height = Math.abs(clientYForLeft - clientYForRight);
const hypotenuseLength = calcPythagoreanTheorem(width, height);
touchRelative = [
{
x: clientXForLeft - imageLeft,
y: clientYForLeft - imageTop
},
{
x: clientXForRight - imageLeft,
y: clientYForRight - imageTop
}
];
this.touchRelative = touchRelative;
this.hypotenuseLength = hypotenuseLength;
}
},
imageTouchMove(e) {
// #ifdef H5
event.preventDefault()
// #endif
const { flagEndTouch, throttleFlag } = this;
if (flagEndTouch || !throttleFlag) return;
const clientXForLeft = e.touches[0].clientX;
const clientYForLeft = e.touches[0].clientY;
this.setDiffData({throttleFlag: false})
this.throttle();
this.moveDuring();
if (e.touches.length === 1) {
const { left: imageLeft, top: imageTop} = imageTouchMoveOfCalcOffset(this, clientXForLeft, clientYForLeft);
this.setDiffData({
imageLeft,
imageTop
})
this.imgMarginDetectionPosition();
} else {
const clientXForRight = e.touches[1].clientX;
const clientYForRight = e.touches[1].clientY;
let width = Math.abs(clientXForLeft - clientXForRight),
height = Math.abs(clientYForLeft - clientYForRight),
hypotenuse = calcPythagoreanTheorem(width, height),
scale = this.scale * (hypotenuse / this.hypotenuseLength);
if (this.isDisableScale) {
scale = 1;
} else {
scale = scale <= this.minRatio ? this.minRatio : scale;
scale = scale >= this.maxRatio ? this.maxRatio : scale;
this.$emit('change', {
width: this.imageWidth * scale,
height: this.imageHeight * scale
});
}
this.imgMarginDetectionScale(scale);
this.hypotenuseLength = Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
this.scale = scale;
}
},
imageTouchEnd() {
this.setDiffData({
flagEndTouch: true
})
this.moveStop();
},
uploadImage() {
const itemList = Object.entries(this.source)
const sizeType = ['original', 'compressed']
const success = ({tempFilePaths:a, tempFiles: b}) => {
this.image = a ? a[0] : b[0].path
};
const _uploadImage = (type) => {
if(type !== 'message') {
uni.chooseImage({
count: 1,
sizeType,
sourceType: [type],
success
});
}
// #ifdef MP-WEIXIN
if(type == 'message') {
wx.chooseMessageFile({
count: 1,
type: 'image',
success
})
}
// #endif
}
if(itemList.length > 1) {
uni.showActionSheet({
itemList: itemList.map(v => v[1]),
success: ({tapIndex: i}) => {
_uploadImage(itemList[i][0])
}
})
} else {
_uploadImage(itemList[0][0])
}
},
imageReset() {
const sys = this.sysinfo || uni.getSystemInfoSync();
this.scale = 1;
this.angle = 0;
this.imageTop = sys.windowHeight / 2;
this.imageLeft = sys.windowWidth / 2;
},
imageLoad(e) {
this.imageReset();
uni.hideLoading();
this.$emit('ready', e.detail);
},
rotate(event) {
if (this.isDisableRotate) return;
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none',
duration: 3000
});
return;
}
const { rotateAngle } = this;
const originAngle = this.angle
const type = event.currentTarget.dataset.type;
if (type === 'along') {
this.angle = originAngle + rotateAngle
} else {
this.angle = originAngle - rotateAngle
}
this.$emit('rotate', this.angle);
},
confirm() {
if (!this.image) {
uni.showToast({
title: '请选择图片',
icon: 'none',
duration: 3000
});
return;
}
uni.showLoading({
title: '加载中'
});
const { canvasHeight, canvasWidth, clipHeight, clipWidth, ctx, scale, imageLeft, imageTop, clipX, clipY, angle, scaleRatio: dpr, image, quality, fileType, type: imageType, canvasId } = this;
const draw = () => {
const imageWidth = this.imageWidth * scale * dpr;
const imageHeight = this.imageHeight * scale * dpr;
const xpos = imageLeft - clipX;
const ypos = imageTop - clipY;
ctx.translate(xpos * dpr, ypos * dpr);
ctx.rotate((angle * Math.PI) / 180);
ctx.drawImage(image, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight);
ctx.draw(false, () => {
const width = clipWidth * dpr
const height = clipHeight * dpr
let params = {
x: 0,
y: 0,
width,
height,
destWidth: width,
destHeight: height,
canvasId: canvasId,
fileType,
quality,
success: (res) => {
data.url = res.tempFilePath;
uni.hideLoading();
this.$emit('success', data);
this.$emit('input', false)
},
fail: (error) => {
console.error('error', error)
this.$emit('fail', error);
this.$emit('input', false)
}
};
let data = {
url: '',
width,
height
};
uni.canvasToTempFilePath(params, this)
});
};
if (canvasWidth !== clipWidth || canvasHeight !== clipHeight) {
this.canvasWidth = clipWidth;
this.canvasHeight = clipHeight;
ctx.draw();
this.$nextTick(() => {
setTimeout(() => {
draw();
}, 100);
})
} else {
draw();
}
},
cancel() {
this.$emit('cancel', false)
this.$emit('input', false)
},
}
};
</script>
<style scoped>
@import './index'
</style>

View File

@ -0,0 +1,244 @@
/**
* 判断手指触摸位置
*/
export function determineDirection(clipX, clipY, clipWidth, clipHeight, currentX, currentY) {
/*
* (右下>>1 右上>>2 左上>>3 左下>>4)
*/
let corner;
/**
* 思路:(利用直角坐标系)
* 1.找出裁剪框中心点
* 2.如点击坐标在上方点与左方点区域内,则点击为左上角
* 3.如点击坐标在下方点与右方点区域内,则点击为右下角
* 4.其他角同理
*/
const mainPoint = [clipX + clipWidth / 2, clipY + clipHeight / 2]; // 中心点
const currentPoint = [currentX, currentY]; // 触摸点
if (currentPoint[0] <= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
corner = 3; // 左上
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] <= mainPoint[1]) {
corner = 2; // 右上
} else if (currentPoint[0] <= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
corner = 4; // 左下
} else if (currentPoint[0] >= mainPoint[0] && currentPoint[1] >= mainPoint[1]) {
corner = 1; // 右下
}
return corner;
}
/**
* 图片边缘检测检测时,计算图片偏移量
*/
export function calcImageOffset(data, scale) {
let left = data.imageLeft;
let top = data.imageTop;
scale = scale || data.scale;
let imageWidth = data.imageWidth;
let imageHeight = data.imageHeight;
if ((data.angle / 90) % 2) {
imageWidth = data.imageHeight;
imageHeight = data.imageWidth;
}
const {
clipX,
clipWidth,
clipY,
clipHeight
} = data;
// 当前图片宽度/高度
const currentImageSize = (size) => (size * scale) / 2;
const currentImageWidth = currentImageSize(imageWidth);
const currentImageHeight = currentImageSize(imageHeight);
left = clipX + currentImageWidth >= left ? left : clipX + currentImageWidth;
left = clipX + clipWidth - currentImageWidth <= left ? left : clipX + clipWidth - currentImageWidth;
top = clipY + currentImageHeight >= top ? top : clipY + currentImageHeight;
top = clipY + clipHeight - currentImageHeight <= top ? top : clipY + clipHeight - currentImageHeight;
return {
left,
top,
scale
};
}
/**
* 图片边缘检测时,计算图片缩放比例
*/
export function calcImageScale(data, scale) {
scale = scale || data.scale;
let {
imageWidth,
imageHeight,
clipWidth,
clipHeight,
angle
} = data
if ((angle / 90) % 2) {
imageWidth = imageHeight;
imageHeight = imageWidth;
}
if (imageWidth * scale < clipWidth) {
scale = clipWidth / imageWidth;
}
if (imageHeight * scale < clipHeight) {
scale = Math.max(scale, clipHeight / imageHeight);
}
return scale;
}
/**
* 计算图片尺寸
*/
export function calcImageSize(width, height, data) {
let imageWidth = width,
imageHeight = height;
let {
clipWidth,
clipHeight,
sysinfo,
width: originWidth,
height: originHeight
} = data
if (imageWidth && imageHeight) {
if (imageWidth / imageHeight > (clipWidth || originWidth) / (clipWidth || originHeight)) {
imageHeight = clipHeight || originHeight;
imageWidth = (width / height) * imageHeight;
} else {
imageWidth = clipWidth || originWidth;
imageHeight = (height / width) * imageWidth;
}
} else {
let sys = sysinfo || uni.getSystemInfoSync();
imageWidth = sys.windowWidth;
imageHeight = 0;
}
return {
imageWidth,
imageHeight
};
}
/**
* 勾股定理求斜边
*/
export function calcPythagoreanTheorem(width, height) {
return Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
}
/**
* 拖动裁剪框时计算
*/
export function clipTouchMoveOfCalculate(data, event) {
const clientX = event.touches[0].clientX;
const clientY = event.touches[0].clientY;
let {
clipWidth,
clipHeight,
clipY: oldClipY,
clipX: oldClipX,
clipStart,
isLockRatio,
maxWidth,
minWidth,
maxHeight,
minHeight
} = data;
maxWidth = maxWidth / 2;
minWidth = minWidth / 2;
minHeight = minHeight / 2;
maxHeight = maxHeight / 2;
let width = clipWidth,
height = clipHeight,
clipY = oldClipY,
clipX = oldClipX,
// 获取裁剪框实际宽度/高度
// 如果大于最大值则使用最大值
// 如果小于最小值则使用最小值
sizecorrect = () => {
width = width <= maxWidth ? (width >= minWidth ? width : minWidth) : maxWidth;
height = height <= maxHeight ? (height >= minHeight ? height : minHeight) : maxHeight;
},
sizeinspect = () => {
sizecorrect();
if ((width > maxWidth || width < minWidth || height > maxHeight || height < minHeight) && isLockRatio) {
return false;
} else {
return true;
}
};
//if (clipStart.corner) {
height = clipStart.height + (clipStart.corner > 1 && clipStart.corner < 4 ? 1 : -1) * (clipStart.y - clientY);
//}
switch (clipStart.corner) {
case 1:
width = clipStart.width - clipStart.x + clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) return;
break;
case 2:
width = clipStart.width - clipStart.x + clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipY = clipStart.clipY - (height - clipStart.height);
}
break;
case 3:
width = clipStart.width + clipStart.x - clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipY = clipStart.clipY - (height - clipStart.height);
clipX = clipStart.clipX - (width - clipStart.width);
}
break;
case 4:
width = clipStart.width + clipStart.x - clientX;
if (isLockRatio) {
height = width / (clipWidth / clipHeight);
}
if (!sizeinspect()) {
return;
} else {
clipX = clipStart.clipX - (width - clipStart.width);
}
break;
default:
break;
}
return {
width,
height,
clipX,
clipY
};
}
/**
* 单指拖动图片计算偏移
*/
export function imageTouchMoveOfCalcOffset(data, clientXForLeft, clientYForLeft) {
let left = clientXForLeft - data.touchRelative[0].x,
top = clientYForLeft - data.touchRelative[0].y;
return {
left,
top
};
}