优化组件/更新
This commit is contained in:
@@ -1,8 +1,21 @@
|
||||
$clipper-edge-border-width: 6rpx !default;
|
||||
$clipper-confirm-color: #07c160 !default;
|
||||
@import '~@/uni_modules/lime-style/index.scss';
|
||||
$prefix: l !default;
|
||||
$clipper: #{$prefix}-clipper;
|
||||
|
||||
@font-face {
|
||||
font-family: clipper-icon;
|
||||
src: url('https://at.alicdn.com/t/c/font_4769200_ijsa6pjss7d.ttf?t=1733274494453')
|
||||
// src: url('data:application/octet-stream;base64,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');
|
||||
}
|
||||
|
||||
|
||||
$clipper-edge-border-width: create-var(clipper-edge-border-width, 6rpx); //6rpx !default;
|
||||
$clipper-confirm-color: create-var(clipper-confirm-color, #07c160); //#07c160 !default;
|
||||
$clipper-z-index: create-var(clipper-z-index, 99); //99 !default;
|
||||
$clipper-mask-color: create-var(clipper-mask-color, rgba(0, 0, 0, 0.5)); //99 !default;
|
||||
|
||||
.flex-auto {
|
||||
flex:auto
|
||||
flex: auto
|
||||
}
|
||||
|
||||
.bg-transparent {
|
||||
@@ -11,30 +24,48 @@ $clipper-confirm-color: #07c160 !default;
|
||||
}
|
||||
|
||||
.lime-clipper {
|
||||
width: 100vw;
|
||||
height: calc( 100vh - var(--window-top));
|
||||
width: 100%;
|
||||
// height: calc(100vh - var(--window-top));
|
||||
bottom: 0;
|
||||
/* #ifdef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
top: 0;
|
||||
/* #endif */
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
top: var(--window-top);
|
||||
/* #endif */
|
||||
|
||||
background-color: rgba(0, 0, 0, 0.9);
|
||||
position: fixed;
|
||||
top: var(--window-top);
|
||||
left: 300vw;
|
||||
z-index: 1;
|
||||
|
||||
left: 3000%;
|
||||
z-index: $clipper-z-index;
|
||||
|
||||
&.open {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
&-mask {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
overflow: visible;
|
||||
flex: 1;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
pointer-events: none;
|
||||
/* #endif */
|
||||
}
|
||||
&__content {
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
border: 1rpx solid rgba(255,255,255,.3);
|
||||
|
||||
box-sizing: border-box;
|
||||
box-shadow: rgba(0, 0, 0, 0.5) 0 0 0 80vh;
|
||||
background: transparent;
|
||||
// box-shadow: $clipper-mask-color 0 0 0 80rpx;
|
||||
box-shadow: 0 0 0 800rpx $clipper-mask-color ;
|
||||
background: transparent;
|
||||
overflow: visible;
|
||||
// transition-duration 0.35s
|
||||
// transition-property left,top
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
border: 1rpx solid rgba(255,255,255,.3);
|
||||
&::before,&::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -56,16 +87,20 @@ $clipper-confirm-color: #07c160 !default;
|
||||
border-top:none;
|
||||
border-bottom: none;
|
||||
}
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
&__edge {
|
||||
overflow: visible;
|
||||
position: absolute;
|
||||
// left 6rpx
|
||||
width: 34rpx;
|
||||
height: 34rpx;
|
||||
// background: red;
|
||||
border: $clipper-edge-border-width solid #ffffff;
|
||||
pointer-events: auto;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
@@ -73,10 +108,9 @@ $clipper-confirm-color: #07c160 !default;
|
||||
height: 40rpx;
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
left: - $clipper-edge-border-width;
|
||||
top: - $clipper-edge-border-width;
|
||||
left: calc(#{$clipper-edge-border-width} * -1);
|
||||
top: calc(#{$clipper-edge-border-width} * -1);
|
||||
border-bottom-width: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
&:before {
|
||||
@@ -86,8 +120,8 @@ $clipper-confirm-color: #07c160 !default;
|
||||
}
|
||||
|
||||
&:nth-child(2) {
|
||||
right: - $clipper-edge-border-width;
|
||||
top: - $clipper-edge-border-width;
|
||||
right: calc(#{$clipper-edge-border-width} * -1);
|
||||
top: calc(#{$clipper-edge-border-width} * -1);
|
||||
border-bottom-width: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
&:before {
|
||||
@@ -98,8 +132,8 @@ $clipper-confirm-color: #07c160 !default;
|
||||
}
|
||||
|
||||
&:nth-child(3) {
|
||||
left: - $clipper-edge-border-width;
|
||||
bottom: - $clipper-edge-border-width;
|
||||
left: calc(#{$clipper-edge-border-width} * -1);
|
||||
bottom: calc(#{$clipper-edge-border-width} * -1);
|
||||
border-top-width: 0 !important;
|
||||
border-right-width: 0 !important;
|
||||
&:before {
|
||||
@@ -109,8 +143,8 @@ $clipper-confirm-color: #07c160 !default;
|
||||
}
|
||||
|
||||
&:nth-child(4) {
|
||||
right: - $clipper-edge-border-width;
|
||||
bottom: - $clipper-edge-border-width;
|
||||
right: calc(#{$clipper-edge-border-width} * -1);
|
||||
bottom: calc(#{$clipper-edge-border-width} * -1);
|
||||
border-top-width: 0 !important;
|
||||
border-left-width: 0 !important;
|
||||
&:before {
|
||||
@@ -118,28 +152,34 @@ $clipper-confirm-color: #07c160 !default;
|
||||
left: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/* #endif */
|
||||
&-image {
|
||||
width: 100%;
|
||||
max-width: inherit;
|
||||
border-style: none;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
transform-origin: center;
|
||||
/* #ifndef APP-ANDROID || APP-IOS || APP-HARMONY */
|
||||
max-width: inherit;
|
||||
-webkit-backface-visibility: hidden;
|
||||
backface-visibility: hidden;
|
||||
transform-origin: center;
|
||||
/* #endif */
|
||||
|
||||
}
|
||||
|
||||
&-canvas {
|
||||
position: fixed;
|
||||
z-index: 10;
|
||||
left: -200vw;
|
||||
top: -200vw;
|
||||
z-index: 100;
|
||||
left: -200%;
|
||||
top: -200%;
|
||||
pointer-events: none;
|
||||
|
||||
// left:0;
|
||||
// top:50%;
|
||||
// background-color: red;
|
||||
}
|
||||
|
||||
&-tools {
|
||||
@@ -148,16 +188,23 @@ $clipper-confirm-color: #07c160 !default;
|
||||
bottom: 10px;
|
||||
width: 100%;
|
||||
z-index: 99;
|
||||
color: #fff;
|
||||
&__btns {
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 20rpx 40rpx;
|
||||
box-sizing: border-box;
|
||||
.text {
|
||||
color: #fff;
|
||||
min-width: 60rpx;
|
||||
// #ifndef UNI-APP-X
|
||||
display: block;
|
||||
// #endif
|
||||
}
|
||||
.cancel {
|
||||
font-weight: bold;
|
||||
width: 112rpx;
|
||||
height: 60rpx;
|
||||
text-align: center;
|
||||
@@ -165,19 +212,23 @@ $clipper-confirm-color: #07c160 !default;
|
||||
}
|
||||
|
||||
.confirm {
|
||||
font-weight: bold;
|
||||
width: 112rpx;
|
||||
height: 60rpx;
|
||||
line-height: 60rpx;
|
||||
background: var(--lime-clipper-confirm-color, $clipper-confirm-color);
|
||||
background: $clipper-confirm-color;
|
||||
border-radius: 6rpx;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
image {
|
||||
display: block;
|
||||
width: 60rpx;
|
||||
height: 60rpx;
|
||||
}
|
||||
.rotate,.photo {
|
||||
font-family: clipper-icon;
|
||||
font-size: 60rpx;
|
||||
}
|
||||
// image {
|
||||
// // display: block;
|
||||
// width: 60rpx;
|
||||
// height: 60rpx;
|
||||
// }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
986
uni_modules/lime-clipper/components/l-clipper/l-clipper.uvue
Normal file
986
uni_modules/lime-clipper/components/l-clipper/l-clipper.uvue
Normal file
@@ -0,0 +1,986 @@
|
||||
<template>
|
||||
<view class="lime-clipper open">
|
||||
<view class="lime-clipper-mask"
|
||||
ref="clipperMaskRef"
|
||||
@touchstart="clipTouchStart"
|
||||
@touchmove="clipTouchMove"
|
||||
@touchend="clipTouchEnd">
|
||||
<!-- #ifndef APP -->
|
||||
<view class="lime-clipper__content" ref="clipperRef" :style="clipStyle">
|
||||
<view class="lime-clipper__edge" :class="'nth-child-' + (index + 1)" v-for="(item, index) in [0, 0, 0, 0]" :key="index"></view>
|
||||
</view>
|
||||
<!-- #endif -->
|
||||
</view>
|
||||
<image
|
||||
ref="imageRef"
|
||||
class="lime-clipper-image"
|
||||
@error="imageError"
|
||||
@load="imageLoad"
|
||||
v-if="state.image != null"
|
||||
:src="state.image"
|
||||
:style="imageStyle"
|
||||
@touchstart="imageTouchStart"
|
||||
@touchmove="imageTouchMove"
|
||||
@touchend="imageTouchEnd"/>
|
||||
<canvas
|
||||
ref="canvasRef"
|
||||
canvas-id="lime-clipper"
|
||||
id="lime-clipper"
|
||||
disable-scroll
|
||||
:style="{
|
||||
width: state.canvasWidth + 'px',
|
||||
height: state.canvasHeight + 'px',
|
||||
}"
|
||||
class="lime-clipper-canvas">
|
||||
</canvas>
|
||||
<view class="lime-clipper-tools" v-if="(isShowCancelBtn || isShowPhotoBtn || isShowRotateBtn || isShowConfirmBtn)">
|
||||
<view class="lime-clipper-tools__btns" v-if="(isShowCancelBtn || isShowPhotoBtn || isShowRotateBtn || isShowConfirmBtn)">
|
||||
<view v-if="isShowCancelBtn" @tap="cancel">
|
||||
<slot name="cancel">
|
||||
<text class="text cancel">取消</text>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-if="isShowPhotoBtn" @tap="uploadImage">
|
||||
<slot name="photo">
|
||||
<text class="text icon photo"></text>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-if="isShowRotateBtn" @tap="rotate">
|
||||
<slot name="rotate">
|
||||
<text class="text icon rotate"></text>
|
||||
</slot>
|
||||
</view>
|
||||
<view v-if="isShowConfirmBtn" @tap="confirm" >
|
||||
<slot name="confirm">
|
||||
<text class="text confirm" :style="[confirmBgColor != null ? {background: confirmBgColor}: {}]">确定</text>
|
||||
</slot>
|
||||
</view>
|
||||
</view>
|
||||
<slot></slot>
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
<script setup lang="uts">
|
||||
/**
|
||||
* Clipper 图片裁剪组件
|
||||
* @description 用于实现图片裁剪功能的交互式组件,支持缩放、旋转、比例锁定等高级功能
|
||||
* <br> 插件类型:LClipperComponentPublicInstance
|
||||
* @tutorial https://ext.dcloud.net.cn/plugin?name=lime-clipper
|
||||
*
|
||||
* @property {string} customStyle 自定义容器样式(支持CSS字符串)
|
||||
* @property {number} zIndex 画布层级(默认:999)
|
||||
* @property {string} imageUrl 源图片地址(支持本地/网络路径)
|
||||
* @property {'jpg' | 'png'} fileType 输出图片格式(默认:jpg)
|
||||
* @property {number} quality 图片压缩质量(0-1,默认0.8)
|
||||
* @property {number} fixedBoxWidth 固定裁剪区域宽度(单位rpx)
|
||||
* @property {number} width 裁剪区域默认宽度(单位rpx)
|
||||
* @property {number} height 裁剪区域默认高度(单位rpx)
|
||||
* @property {number} minWidth 最小裁剪宽度(默认:50px)
|
||||
* @property {number} maxWidth 最大裁剪宽度(默认:屏幕宽度)
|
||||
* @property {number} destWidth 输出图片宽度(默认按裁剪尺寸)
|
||||
* @property {number} destHeight 输出图片高度(默认按裁剪尺寸)
|
||||
* @property {number} minHeight 最小裁剪高度(默认:50px)
|
||||
* @property {number} maxHeight 最大裁剪高度(默认:屏幕高度)
|
||||
* @property {boolean} isLockWidth 锁定裁剪宽度(禁止调整)
|
||||
* @property {boolean} isLockHeight 锁定裁剪高度(禁止调整)
|
||||
* @property {boolean} isLockRatio 锁定宽高比(默认false)
|
||||
* @property {number} scaleRatio 初始缩放比例(默认1.0)
|
||||
* @property {number} minRatio 最小缩放比例(默认0.5)
|
||||
* @property {number} maxRatio 最大缩放比例(默认3.0)
|
||||
* @property {boolean} isDisableScale 禁用缩放功能
|
||||
* @property {boolean} isDisableRotate 禁用旋转功能
|
||||
* @property {boolean} isLimitMove 限制移动不超出图片范围
|
||||
* @property {boolean} isShowPhotoBtn 显示相册选择按钮
|
||||
* @property {boolean} isShowRotateBtn 显示旋转按钮
|
||||
* @property {boolean} isShowConfirmBtn 显示确认按钮
|
||||
* @property {boolean} isShowCancelBtn 显示取消按钮
|
||||
* @property {number} rotateAngle 初始旋转角度(单位度)
|
||||
* @property {any} source 自定义图片源(预留扩展)
|
||||
* @property {string} confirmBgColor 确认按钮背景色
|
||||
* @property {string} canvasId 画布唯一标识(多实例时必填)
|
||||
* @event {Function} ready 准完成触发
|
||||
* @event {Function} change 变化时触发
|
||||
* @event {Function} rotate 旋转时触发
|
||||
* @event {Function} cancel 取消时触发
|
||||
* @event {Function} success 点击确定时成功生成图片后触发
|
||||
*/
|
||||
|
||||
import { ClipperProps, ClipperState, ClipBoxSizes, Point, ClipperclipStart, Rectangle } from './type';
|
||||
import {
|
||||
getPointPositionInRectangle,
|
||||
isPointInRotatedRectangle,
|
||||
calcImageSize,
|
||||
calcImageScale,
|
||||
calcImageOffset,
|
||||
calcPythagoreanTheorem,
|
||||
imageTouchMoveOfCalcOffset,
|
||||
clamp,
|
||||
determineDirection,
|
||||
clipTouchMoveOfCalculate } from './utils.uts';
|
||||
|
||||
const emit = defineEmits(['ready', 'change', 'rotate', 'cancel', 'input', 'success'])
|
||||
const props = withDefaults(defineProps<ClipperProps>(), {
|
||||
fileType: 'png',
|
||||
quality: 1,
|
||||
// fixedBoxWidth: 400,
|
||||
width: 400,
|
||||
height: 400,
|
||||
minWidth: 200,
|
||||
minHeight: 200,
|
||||
maxWidth: 600,
|
||||
maxHeight: 600,
|
||||
isLockWidth: false,
|
||||
isLockHeight: false,
|
||||
isLockRatio: true,
|
||||
scaleRatio: 1,
|
||||
minRatio: 0.5,
|
||||
maxRatio: 2,
|
||||
isDisableScale: false,
|
||||
isDisableRotate: false,
|
||||
isLimitMove: false,
|
||||
isShowPhotoBtn: true,
|
||||
isShowRotateBtn: true,
|
||||
isShowConfirmBtn: true,
|
||||
isShowCancelBtn: true,
|
||||
rotateAngle: 90,
|
||||
// source: ():UTSJSONObject => ({})
|
||||
source: {
|
||||
album: '从相册中选择',
|
||||
camera: '拍照',
|
||||
// #ifdef MP-WEIXIN
|
||||
message: '从微信中选择'
|
||||
// #endif
|
||||
} //as UTSJSONObject
|
||||
})
|
||||
|
||||
const state = reactive<ClipperState>({
|
||||
canvasWidth: 0,
|
||||
canvasHeight: 0,
|
||||
clipX: -1,
|
||||
clipY: -1,
|
||||
clipWidth: 0,
|
||||
clipHeight: 0,
|
||||
animation: false,
|
||||
imageWidth: 0,
|
||||
imageHeight: 0,
|
||||
imageTop: 0,
|
||||
imageLeft: 0,
|
||||
scale: 1,
|
||||
angle: 0,
|
||||
image: '',
|
||||
imageInit: false,
|
||||
|
||||
originY: -1,
|
||||
originX: -1,
|
||||
// flagClipTouch: false,
|
||||
})
|
||||
|
||||
let touchRelative:Point[] = [];
|
||||
let clipStart:ClipperclipStart = {
|
||||
width: 0,
|
||||
height: 0,
|
||||
x: 0,
|
||||
y: 0,
|
||||
clipY: 0,
|
||||
clipX: 0,
|
||||
corner: 0
|
||||
}
|
||||
let hypotenuseLength = 0;
|
||||
let throttleTimer = -1;
|
||||
let timeClipCenter = -1;
|
||||
let animationTimer = -1;
|
||||
let flagEndTouch = false;
|
||||
let flagClipTouch = false;
|
||||
let throttleFlag = true;
|
||||
let _image = '';
|
||||
|
||||
let ctx : CanvasRenderingContext2D | null = null;
|
||||
let canvas : UniCanvasElement | null = null;
|
||||
let canvasContext : CanvasContext | null = null;
|
||||
const instance = getCurrentInstance()!
|
||||
const canvasId = 'lime-clipper'
|
||||
const canvasRef = ref<UniCanvasElement | null>(null)
|
||||
|
||||
|
||||
let { windowHeight, windowWidth, pixelRatio} = uni.getWindowInfo();
|
||||
const clipBoxSizes = computed(():ClipBoxSizes=>{
|
||||
|
||||
const width = uni.rpx2px(props.width);
|
||||
const height = uni.rpx2px(props.height);
|
||||
const minWidth = uni.rpx2px(props.minWidth);
|
||||
const minHeight = uni.rpx2px(props.minHeight);
|
||||
const maxWidth = uni.rpx2px(props.maxWidth);
|
||||
const maxHeight = uni.rpx2px(props.maxHeight);
|
||||
const fixedBoxWidth = uni.rpx2px(props.fixedBoxWidth ?? 0);
|
||||
// #ifdef APP || WEB
|
||||
// #endif
|
||||
|
||||
// #ifndef APP || WEB
|
||||
// const width = uni.upx2px(props.width);
|
||||
// const height = uni.upx2px(props.height);
|
||||
// const minWidth = uni.upx2px(props.minWidth);
|
||||
// const minHeight = uni.upx2px(props.minHeight);
|
||||
// const maxWidth = uni.upx2px(props.maxWidth);
|
||||
// const maxHeight = uni.upx2px(props.maxHeight);
|
||||
// #endif
|
||||
return {
|
||||
fixedBoxWidth,
|
||||
width,
|
||||
height,
|
||||
minWidth,
|
||||
minHeight,
|
||||
maxWidth,
|
||||
maxHeight
|
||||
} as ClipBoxSizes
|
||||
})
|
||||
|
||||
const clipStyle = computed(():Map<string, any>=>{
|
||||
const style = new Map<string, any>();
|
||||
// #ifndef APP
|
||||
style.set('width', state.clipWidth + 'px')
|
||||
style.set('height', state.clipHeight + 'px')
|
||||
style.set('transition-property', state.animation ? '': 'background')
|
||||
style.set('left', state.clipX + 'px')
|
||||
style.set('top', state.clipY + 'px')
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
const imageStyle = computed(():Map<string, any>=>{
|
||||
const style = new Map<string, any>();
|
||||
// #ifndef APP
|
||||
|
||||
style.set('width', state.imageWidth != 0 ? state.imageWidth + 'px': 'auto')
|
||||
style.set('height', state.imageHeight != 0 ? state.imageHeight + 'px': 'auto')
|
||||
|
||||
style.set('transform', `translateX(${state.imageLeft - state.imageWidth / 2}px) translateY(${state.imageTop - state.imageHeight / 2}px) scale(${state.scale}) rotate(${state.angle}deg)`)
|
||||
style.set('transition-duration', state.animation ? '0.35s': '0s')
|
||||
// #endif
|
||||
return style
|
||||
})
|
||||
|
||||
const hidpi = (width: number, height: number) => {
|
||||
if(canvas == null) return
|
||||
// 处理高清屏逻辑
|
||||
const dpr = pixelRatio;
|
||||
canvas!.width = width * dpr;
|
||||
canvas!.height = height * dpr;
|
||||
ctx!.scale(dpr, dpr); // 仅需调用一次,当调用 reset 方法后需要再次 scale
|
||||
}
|
||||
const setClipInfo = () => {
|
||||
let clipWidth = clipBoxSizes.value.width;
|
||||
let clipHeight = clipBoxSizes.value.height;
|
||||
|
||||
if(props.fixedBoxWidth != null) {
|
||||
clipWidth = clipBoxSizes.value.fixedBoxWidth
|
||||
clipHeight = clipBoxSizes.value.width / clipHeight * clipWidth
|
||||
}
|
||||
|
||||
const clipY = (windowHeight - clipHeight) * 0.5;
|
||||
const clipX = (windowWidth - clipWidth) * 0.5;
|
||||
|
||||
const imageLeft = windowWidth * 0.5;
|
||||
const imageTop = windowHeight * 0.5;
|
||||
|
||||
state.clipWidth = clipWidth;
|
||||
state.clipHeight = clipHeight;
|
||||
state.clipX = clipX;
|
||||
state.clipY = clipY;
|
||||
state.canvasHeight = clipHeight;
|
||||
state.canvasWidth = clipWidth;
|
||||
state.imageLeft = imageLeft;
|
||||
state.imageTop = imageTop;
|
||||
|
||||
if (canvasRef.value == null) return
|
||||
// 异步调用方式, 跨平台写法
|
||||
nextTick(()=>{
|
||||
uni.createCanvasContextAsync({
|
||||
id: canvasId,
|
||||
component: instance.proxy!,
|
||||
success: (context : CanvasContext) => {
|
||||
canvasContext = context;
|
||||
ctx = context.getContext('2d')!;
|
||||
canvas = ctx!.canvas;
|
||||
hidpi(clipWidth, clipHeight)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
const setClipCenter = () => {
|
||||
let clipY = (windowHeight - state.clipHeight) * 0.5;
|
||||
let clipX = (windowWidth - state.clipWidth) * 0.5;
|
||||
state.imageTop = state.imageTop - state.clipY + clipY;
|
||||
state.imageLeft = state.imageLeft - state.clipX + clipX;
|
||||
state.clipY = clipY;
|
||||
state.clipX = clipX;
|
||||
|
||||
}
|
||||
const calcClipSize = () => {
|
||||
if(state.clipWidth > windowWidth) {
|
||||
state.clipWidth = windowWidth
|
||||
} else if(state.clipWidth + state.clipX > windowWidth) {
|
||||
state.clipX = windowWidth - state.clipX
|
||||
}
|
||||
|
||||
if(state.clipHeight > windowHeight) {
|
||||
state.clipHeight = windowHeight
|
||||
} else if(state.clipHeight + state.clipY > windowHeight) {
|
||||
state.clipY = windowHeight - state.clipY
|
||||
}
|
||||
}
|
||||
const cutDetectionPosition = () => {
|
||||
// 辅助函数,用于确保裁剪区域在窗口内
|
||||
const ensureWithinBounds = (value: number, maxValue: number, size: number):number => {
|
||||
if (value < 0) {
|
||||
return 0;
|
||||
} else if (value > maxValue - size) {
|
||||
return maxValue - size;
|
||||
}
|
||||
return value;
|
||||
};
|
||||
// 计算中心位置
|
||||
const centerPosition = (maxValue: number, size: number):number => (maxValue - size) * 0.5;
|
||||
const newClipY = state.clipY == -1 ? centerPosition(windowHeight, state.clipHeight) : ensureWithinBounds(state.clipY, windowHeight, state.clipHeight);
|
||||
const newClipX = state.clipX == -1 ? centerPosition(windowWidth, state.clipWidth) : ensureWithinBounds(state.clipX, windowWidth, state.clipWidth);
|
||||
state.clipY = newClipY
|
||||
state.clipX = newClipX
|
||||
}
|
||||
const imgMarginDetectionPosition = (scale: number) => {
|
||||
if (!props.isLimitMove) return;
|
||||
const [left, top, currentScale] = calcImageOffset(
|
||||
state.imageLeft,
|
||||
state.imageTop,
|
||||
state.imageWidth,
|
||||
state.imageHeight,
|
||||
state.clipX,
|
||||
state.clipY,
|
||||
state.clipWidth,
|
||||
state.clipHeight,
|
||||
state.angle,
|
||||
scale);
|
||||
state.imageLeft = left
|
||||
state.imageTop = top
|
||||
state.scale = currentScale
|
||||
|
||||
}
|
||||
|
||||
const imgMarginDetectionScale = (scale: number) => {
|
||||
if (!props.isLimitMove) return;
|
||||
const currentScale = calcImageScale(
|
||||
state.imageWidth,
|
||||
state.imageHeight,
|
||||
state.clipWidth,
|
||||
state.clipHeight,
|
||||
state.angle,
|
||||
scale);
|
||||
imgMarginDetectionPosition(currentScale)
|
||||
}
|
||||
const imgComputeSize = (width: number, height: number) => {
|
||||
const [imageWidth, imageHeight] = calcImageSize(
|
||||
width, height,
|
||||
clipBoxSizes.value.width, clipBoxSizes.value.height,
|
||||
state.clipWidth, state.clipHeight);
|
||||
|
||||
state.imageWidth = imageWidth;
|
||||
state.imageHeight = imageHeight;
|
||||
state.originX = imageWidth / 2;
|
||||
state.originY = imageHeight / 2;
|
||||
}
|
||||
const imageReset = () => {
|
||||
state.scale = 1;
|
||||
state.angle = 0;
|
||||
|
||||
state.imageTop = windowHeight * 0.5;
|
||||
state.imageLeft = windowWidth * 0.5;
|
||||
}
|
||||
const moveStop = () => {
|
||||
clearTimeout(timeClipCenter);
|
||||
timeClipCenter = setTimeout(() => {
|
||||
if (!state.animation) {
|
||||
state.animation = true
|
||||
state.imageInit = true
|
||||
}
|
||||
nextTick(setClipCenter)
|
||||
}, 800);
|
||||
}
|
||||
let touchTarget:'image' | 'clip' | null = null
|
||||
const restoreToFixedWidth = ()=> {
|
||||
const fixedWidth = uni.rpx2px(props.fixedBoxWidth??0)
|
||||
// 1. 计算宽度变化比例
|
||||
const scaleRatio = fixedWidth / state.clipWidth
|
||||
|
||||
// 2. 保存当前裁剪框中心点
|
||||
const oldImageCenterX = state.imageLeft;
|
||||
const oldImageCenterY = state.imageTop;
|
||||
const oldClipCenterX = state.clipX + state.clipWidth / 2;
|
||||
const oldClipCenterY = state.clipY + state.clipHeight / 2;
|
||||
|
||||
// 3. 计算图片中心点相对于裁剪框中心点的偏移向量
|
||||
const imageOffsetX = oldImageCenterX - oldClipCenterX;
|
||||
const imageOffsetY = oldImageCenterY - oldClipCenterY;
|
||||
// 4. 计算当前裁剪框的宽高比例
|
||||
const aspectRatio = state.clipHeight / state.clipWidth
|
||||
// 应用动画效果
|
||||
state.animation = true
|
||||
// 5. 更新裁剪框尺寸(宽度固定,高度保持比例)
|
||||
state.clipWidth = fixedWidth
|
||||
state.clipHeight = fixedWidth * aspectRatio
|
||||
|
||||
|
||||
// 6. 更新裁剪框位置(保持中心点不变)
|
||||
const newClipX = (windowWidth - fixedWidth) / 2;
|
||||
const newClipY = (windowHeight - state.clipHeight) / 2;
|
||||
// 7. 新裁剪框的中心点
|
||||
const newClipCenterX = newClipX + fixedWidth / 2;
|
||||
const newClipCenterY = newClipY + state.clipHeight / 2;
|
||||
const currentRatio = state.scale * scaleRatio
|
||||
// 8. 计算图片缩放比例变化量
|
||||
state.scale = Math.min(currentRatio, props.maxRatio);
|
||||
|
||||
// 9. 计算图片位置的变化:
|
||||
// a. 缩放导致的偏移(缩放中心是裁剪框中心)
|
||||
// b. 裁剪框位置变化带来的偏移
|
||||
state.imageTop = currentRatio > props.maxRatio ? oldImageCenterY : newClipCenterY + imageOffsetY * scaleRatio;
|
||||
state.imageLeft = currentRatio > props.maxRatio ? oldImageCenterX :newClipCenterX + imageOffsetX * scaleRatio;
|
||||
|
||||
// 10. 更新裁剪框位置
|
||||
state.clipX = newClipX;
|
||||
state.clipY = newClipY;
|
||||
|
||||
|
||||
// 12. 更新canvas尺寸
|
||||
// state.canvasWidth = fixedWidth;
|
||||
// state.canvasHeight = state.clipHeight;
|
||||
|
||||
}
|
||||
const imageTouchStart = (e: UniTouchEvent) => {
|
||||
e.preventDefault();
|
||||
flagEndTouch = false;
|
||||
state.animation = false;
|
||||
const { imageLeft, imageTop } = state;
|
||||
const [touch0] = e.touches;
|
||||
// 计算触摸点相对于图片的相对位置
|
||||
const getTouchRelative = (touch: UniTouch):Point => (
|
||||
{
|
||||
x: touch.clientX - imageLeft,
|
||||
y: touch.clientY - imageTop
|
||||
} as Point)
|
||||
// 存储触摸点的相对位置
|
||||
touchRelative = [getTouchRelative(touch0)];
|
||||
|
||||
if(e.touches.length > 1) {
|
||||
const touch1 = e.touches[1];
|
||||
const touch1Relative = getTouchRelative(touch1);
|
||||
|
||||
// 计算两点之间的距离
|
||||
const width = Math.abs(touch0.clientX - touch1.clientX);
|
||||
const height = Math.abs(touch0.clientY - touch1.clientY);
|
||||
hypotenuseLength = Math.hypot(width, height);
|
||||
touchRelative.push(touch1Relative);
|
||||
}
|
||||
}
|
||||
const imageTouchMove = (e: UniTouchEvent) => {
|
||||
e.preventDefault();
|
||||
if (flagEndTouch || !throttleFlag) return;
|
||||
throttleFlag = true;
|
||||
clearTimeout(timeClipCenter);
|
||||
const [touch0] = e.touches;
|
||||
const { clientX: clientXForLeft, clientY: clientYForLeft } = touch0;
|
||||
if(e.touches.length == 1) {
|
||||
const [imageLeft, imageTop] = imageTouchMoveOfCalcOffset(touchRelative[0], clientXForLeft, clientYForLeft);
|
||||
state.imageLeft = imageLeft;
|
||||
state.imageTop = imageTop;
|
||||
imgMarginDetectionPosition(state.scale)
|
||||
} else if(e.touches.length > 1) {
|
||||
|
||||
const touch1 = e.touches[1];
|
||||
const { clientX: clientXForRight, clientY: clientYForRight } = touch1;
|
||||
const width = Math.abs(clientXForLeft - clientXForRight);
|
||||
const height = Math.abs(clientYForLeft - clientYForRight);
|
||||
const hypotenuse = Math.hypot(width, height);
|
||||
let scale = state.scale * (hypotenuse / hypotenuseLength);
|
||||
|
||||
if(props.isDisableScale) {
|
||||
scale = 1;
|
||||
} else {
|
||||
scale = clamp(scale, props.minRatio, props.maxRatio);
|
||||
emit('change', { width: state.imageWidth * scale, height: state.imageHeight * scale });
|
||||
}
|
||||
if(state.scale == scale) return
|
||||
imgMarginDetectionScale(scale);
|
||||
hypotenuseLength = hypotenuse;
|
||||
state.scale = scale;
|
||||
}
|
||||
}
|
||||
const imageTouchEnd = (e: UniTouchEvent) => {
|
||||
flagEndTouch = true;
|
||||
|
||||
moveStop()
|
||||
// flagClipTouch = false
|
||||
// touchTarget = null
|
||||
}
|
||||
const clipTouchStart = (e: UniTouchEvent) => {
|
||||
e.preventDefault();
|
||||
if (state.image == null) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
const clientX = e.touches[0].clientX;
|
||||
const clientY = e.touches[0].clientY;
|
||||
|
||||
// #ifdef APP
|
||||
const point:Point = {x: clientX, y: clientY}
|
||||
const clipRectangle: Rectangle = {x: state.clipX, y: state.clipY, width: state.clipWidth, height: state.clipHeight}
|
||||
|
||||
const corner = getPointPositionInRectangle(
|
||||
point,
|
||||
clipRectangle
|
||||
)
|
||||
if(corner != null) {
|
||||
touchTarget = 'clip'
|
||||
} else {
|
||||
const imageRectangle: Rectangle = {
|
||||
x: state.imageLeft - state.imageWidth / 2,
|
||||
y: state.imageTop - state.imageHeight / 2,
|
||||
width: state.imageWidth,
|
||||
height: state.imageHeight}
|
||||
const isImage = isPointInRotatedRectangle(point, imageRectangle, state.scale, state.angle)
|
||||
if(isImage) {
|
||||
touchTarget = 'image'
|
||||
imageTouchStart(e)
|
||||
}
|
||||
}
|
||||
if(corner == null) return;
|
||||
// #endif
|
||||
// #ifndef APP
|
||||
const corner = determineDirection(state.clipX, state.clipY, state.clipWidth, state.clipHeight, clientX, clientY);
|
||||
if(corner == -1) return;
|
||||
// #endif
|
||||
clearTimeout(timeClipCenter);
|
||||
|
||||
clipStart = {
|
||||
width: state.clipWidth,
|
||||
height: state.clipHeight,
|
||||
x: clientX,
|
||||
y: clientY,
|
||||
clipX : state.clipX,
|
||||
clipY : state.clipY,
|
||||
corner
|
||||
} as ClipperclipStart
|
||||
flagClipTouch = true;
|
||||
flagEndTouch = true;
|
||||
}
|
||||
const clipTouchMove = (e: UniTouchEvent) => {
|
||||
// #ifdef APP
|
||||
if(touchTarget == 'image') {
|
||||
imageTouchMove(e)
|
||||
return
|
||||
}
|
||||
if(touchTarget != 'clip') return
|
||||
// #endif
|
||||
e.stopPropagation()
|
||||
e.preventDefault()
|
||||
if (state.image == null) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// 只针对单指点击做处理
|
||||
if (e.touches.length != 1) return;
|
||||
if(flagClipTouch && throttleFlag) {
|
||||
const [touch] = e.touches
|
||||
const { isLockRatio, isLockHeight, isLockWidth } = props;
|
||||
if (isLockRatio && (isLockWidth || isLockHeight)) return;
|
||||
// throttleFlag = false;
|
||||
throttleFlag = true;
|
||||
const clipData = clipTouchMoveOfCalculate(
|
||||
state.clipWidth,
|
||||
state.clipHeight,
|
||||
state.clipX,
|
||||
state.clipY,
|
||||
clipBoxSizes.value.minWidth,
|
||||
clipBoxSizes.value.maxWidth,
|
||||
clipBoxSizes.value.minHeight,
|
||||
clipBoxSizes.value.maxHeight,
|
||||
clipStart,
|
||||
props.isLockRatio,
|
||||
touch);
|
||||
|
||||
if(clipData == null) return
|
||||
const [width, height, clipX, clipY] = clipData;
|
||||
if(!isLockWidth && !isLockHeight) {
|
||||
state.clipWidth = width
|
||||
state.clipHeight = height
|
||||
state.clipX = clipX
|
||||
state.clipY = clipY
|
||||
} else if(!isLockWidth) {
|
||||
state.clipWidth = width
|
||||
state.clipX = clipX
|
||||
} else if(!isLockHeight) {
|
||||
state.clipHeight = height
|
||||
state.clipY = clipY
|
||||
}
|
||||
imgMarginDetectionScale(state.scale)
|
||||
}
|
||||
}
|
||||
const clipTouchEnd = (e: UniTouchEvent) => {
|
||||
// #ifdef APP
|
||||
if(touchTarget == 'image') {
|
||||
imageTouchEnd(e)
|
||||
return
|
||||
}
|
||||
if(touchTarget != 'clip') return
|
||||
// #endif
|
||||
if (props.fixedBoxWidth != null && flagClipTouch) {
|
||||
restoreToFixedWidth()
|
||||
}
|
||||
moveStop()
|
||||
flagClipTouch = false
|
||||
touchTarget = null
|
||||
}
|
||||
|
||||
const imageLoad = (e: UniImageLoadEvent) => {
|
||||
imageReset()
|
||||
uni.hideLoading();
|
||||
emit('ready', e);
|
||||
}
|
||||
const imageError = (e: UniImageErrorEvent) => {
|
||||
imageReset()
|
||||
uni.hideLoading();
|
||||
}
|
||||
|
||||
const uploadImage = (e:UniPointerEvent) => {
|
||||
uni.chooseImage({
|
||||
count: 1,
|
||||
sizeType: ['original', 'compressed'],
|
||||
sourceType: ['album','camera'],
|
||||
success(res) {
|
||||
state.image = res.tempFilePaths[0]
|
||||
}
|
||||
})
|
||||
}
|
||||
const rotate = (e:UniPointerEvent) => {
|
||||
if (props.isDisableRotate) return;
|
||||
if (state.image == null) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
// const { rotateAngle } = props;
|
||||
// const originAngle = state.angle
|
||||
// const type = event.currentTarget.dataset.type;
|
||||
// if (type === 'along') {
|
||||
// this.angle = originAngle + rotateAngle
|
||||
// } else {
|
||||
if(!state.animation) {
|
||||
state.animation = true;
|
||||
nextTick(()=>{
|
||||
state.angle = state.angle - props.rotateAngle
|
||||
})
|
||||
} else {
|
||||
state.angle = state.angle - props.rotateAngle
|
||||
}
|
||||
|
||||
// }
|
||||
emit('rotate', state.angle);
|
||||
}
|
||||
const cancel = (e:UniPointerEvent) => {
|
||||
emit('cancel', false)
|
||||
emit('input', false)
|
||||
uni.hideLoading()
|
||||
}
|
||||
const confirm = (e:UniPointerEvent) => {
|
||||
if (state.image == null) {
|
||||
uni.showToast({
|
||||
title: '请选择图片',
|
||||
icon: 'none'
|
||||
});
|
||||
return;
|
||||
}
|
||||
uni.showLoading({
|
||||
title: '加载中'
|
||||
});
|
||||
const {
|
||||
canvasHeight,
|
||||
canvasWidth,
|
||||
clipHeight,
|
||||
clipWidth,
|
||||
scale,
|
||||
imageLeft,
|
||||
imageTop,
|
||||
clipX,
|
||||
clipY,
|
||||
angle,
|
||||
} = state;
|
||||
const dpr = props.scaleRatio
|
||||
const draw = () => {
|
||||
if(ctx == null || canvas == null) return
|
||||
const img = canvasContext!.createImage()
|
||||
// #ifdef WEB
|
||||
// @ts-ignore
|
||||
img.crossOrigin = 'Anonymous';
|
||||
// #endif
|
||||
// @ts-ignore
|
||||
img.onload = () => {
|
||||
const imageWidth = state.imageWidth * scale * dpr;
|
||||
const imageHeight = state.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(img, -imageWidth / 2, -imageHeight / 2, imageWidth, imageHeight);
|
||||
// 鸿蒙next 渲染需要时间,故延长一下
|
||||
setTimeout(()=>{
|
||||
const url = canvas!.toDataURL();
|
||||
uni.hideLoading()
|
||||
emit('success', {
|
||||
url,
|
||||
width: clipWidth * dpr,
|
||||
height: clipHeight * dpr
|
||||
});
|
||||
emit('input', false)
|
||||
},1000)
|
||||
}
|
||||
// @ts-ignore
|
||||
img.onerror = () => {
|
||||
uni.hideLoading()
|
||||
}
|
||||
// #ifdef MP-WEIXIN
|
||||
if(_image.startsWith('static')) {
|
||||
_image = `/${_image}`
|
||||
}
|
||||
// #endif
|
||||
img.src = _image
|
||||
}
|
||||
if(canvasWidth != clipWidth || canvasHeight != clipHeight) {
|
||||
state.canvasWidth = clipWidth
|
||||
state.canvasHeight = clipHeight
|
||||
nextTick(()=>{
|
||||
hidpi(clipWidth, clipHeight)
|
||||
nextTick(draw)
|
||||
})
|
||||
} else {
|
||||
nextTick(draw)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// #ifdef APP
|
||||
const imageRef = ref<UniImageElement | null>(null)
|
||||
// const imageRef = ref<UniElement | null>(null)
|
||||
watchEffect(()=>{
|
||||
if(imageRef.value == null) return;
|
||||
|
||||
imageRef.value!.style.setProperty('width', state.imageWidth != 0 ? state.imageWidth + 'px': 'auto')
|
||||
imageRef.value!.style.setProperty('height', state.imageHeight != 0 ? state.imageHeight + 'px': 'auto')
|
||||
imageRef.value!.style.setProperty('transform', `translateX(${state.imageLeft - state.imageWidth / 2}px) translateY(${state.imageTop - state.imageHeight / 2}px) scale(${state.scale}) rotate(${state.angle}deg)`)
|
||||
imageRef.value!.style.setProperty('transition-duration', state.animation ? '0.35s': '0s')
|
||||
})
|
||||
const clipperRef = ref<UniElement|null>(null)
|
||||
const clipperMaskRef = ref<UniElement|null>(null)
|
||||
let maskCtx:DrawableContext|null = null;
|
||||
const drawMaskClip = () => {
|
||||
if(clipperMaskRef.value == null) return
|
||||
if(maskCtx == null) {
|
||||
maskCtx = clipperMaskRef.value!.getDrawableContext()
|
||||
}
|
||||
const _maskCtx = maskCtx!
|
||||
const rect = clipperMaskRef.value!.getBoundingClientRect()
|
||||
_maskCtx.reset()
|
||||
// 遮罩
|
||||
_maskCtx.fillStyle = 'rgba(0, 0, 0, 0.5)';
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(0, 0)
|
||||
_maskCtx.lineTo(rect.width, 0)
|
||||
_maskCtx.lineTo(rect.width, rect.height)
|
||||
_maskCtx.lineTo(0, rect.height)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(0, rect.height)
|
||||
_maskCtx.lineTo(0, 0)
|
||||
_maskCtx.closePath()
|
||||
_maskCtx.fill();
|
||||
// 描边
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.setLineDash([4, 4])
|
||||
_maskCtx.lineWidth = 1
|
||||
_maskCtx.strokeStyle = 'rgba(255,255,255,.3)'
|
||||
_maskCtx.moveTo(state.clipX, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY + state.clipHeight)
|
||||
_maskCtx.stroke()
|
||||
// y虚线
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX, state.clipY + state.clipHeight / 3)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight / 3)
|
||||
|
||||
_maskCtx.stroke()
|
||||
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX, state.clipY + state.clipHeight / 3 * 2)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight / 3 * 2)
|
||||
_maskCtx.stroke()
|
||||
// x虚线
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX + state.clipWidth / 3, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth / 3, state.clipY + state.clipHeight)
|
||||
_maskCtx.stroke()
|
||||
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX + state.clipWidth / 3 * 2, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth / 3 * 2, state.clipY + state.clipHeight)
|
||||
_maskCtx.stroke()
|
||||
|
||||
// 左上角
|
||||
const edgeLength = 20
|
||||
const edgeWidth = 3
|
||||
_maskCtx.lineWidth = 3
|
||||
_maskCtx.strokeStyle = 'rgba(255,255,255,1)'
|
||||
// #ifdef APP-IOS
|
||||
_maskCtx.setLineDash([0, 0.0001])//ios 不能为0
|
||||
// #endif
|
||||
// #ifdef APP-ANDROID
|
||||
_maskCtx.setLineDash([0, 0])
|
||||
// #endif
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX, state.clipY + edgeLength)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX + edgeLength, state.clipY)
|
||||
_maskCtx.stroke()
|
||||
// 右上角
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX + state.clipWidth - edgeLength, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + edgeLength)
|
||||
_maskCtx.stroke()
|
||||
// 右下角
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight - edgeLength)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX + state.clipWidth - edgeLength, state.clipY + state.clipHeight)
|
||||
_maskCtx.stroke()
|
||||
// 左下角
|
||||
_maskCtx.beginPath()
|
||||
_maskCtx.moveTo(state.clipX + edgeLength, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY + state.clipHeight)
|
||||
_maskCtx.lineTo(state.clipX, state.clipY + state.clipHeight - edgeLength)
|
||||
_maskCtx.stroke()
|
||||
|
||||
_maskCtx.update()
|
||||
}
|
||||
|
||||
watchEffect(()=>{
|
||||
if(clipperMaskRef.value == null) return;
|
||||
if(maskCtx == null) {
|
||||
setTimeout(()=>{
|
||||
drawMaskClip()
|
||||
},100)
|
||||
}
|
||||
drawMaskClip()
|
||||
|
||||
// clipperRef.value!.style.setProperty('width', state.clipWidth + 'px')
|
||||
// clipperRef.value!.style.setProperty('height', state.clipHeight + 'px')
|
||||
// clipperRef.value!.style.setProperty('transition-property', state.animation ? '': 'background')
|
||||
// clipperRef.value!.style.setProperty('left', state.clipX + 'px')
|
||||
// clipperRef.value!.style.setProperty('top', state.clipY + 'px')
|
||||
// clipperRef.value!.style.setProperty('z-index', 10)
|
||||
})
|
||||
// #endif
|
||||
|
||||
|
||||
watch(():string|null => state.image, (src: string|null)=>{
|
||||
if(src == null) return
|
||||
state.imageInit = false
|
||||
uni.showLoading({
|
||||
title: '请稍候...',
|
||||
mask: true
|
||||
});
|
||||
uni.getImageInfo({
|
||||
src,
|
||||
success(res) {
|
||||
uni.hideLoading()
|
||||
if(['right', 'left'].includes(res.orientation ?? '')){
|
||||
imgComputeSize(res.height, res.width)
|
||||
} else {
|
||||
imgComputeSize(res.width, res.height)
|
||||
}
|
||||
_image = res.path;
|
||||
if(props.isLimitMove) {
|
||||
imgMarginDetectionScale(state.scale)
|
||||
}
|
||||
},
|
||||
fail(err) {
|
||||
uni.hideLoading()
|
||||
}
|
||||
})
|
||||
}, {immediate: true})
|
||||
|
||||
// watch(():boolean => state.animation ,(value: boolean) => {
|
||||
// clearTimeout(animationTimer);
|
||||
// if(value) {
|
||||
// animationTimer = setTimeout(() => {
|
||||
// state.animation = false;
|
||||
// }, 260);
|
||||
// }
|
||||
// })
|
||||
watch(clipBoxSizes, (_clipBoxSizes: ClipBoxSizes)=> {
|
||||
state.clipWidth = clamp(clipBoxSizes.value.width, clipBoxSizes.value.minWidth, clipBoxSizes.value.maxWidth)
|
||||
state.clipHeight = clamp(clipBoxSizes.value.height, clipBoxSizes.value.minHeight, clipBoxSizes.value.maxHeight)
|
||||
calcClipSize()
|
||||
})
|
||||
watch(():number=> state.angle, (angle: number)=>{
|
||||
state.animation = true//state.imageInit;
|
||||
moveStop()
|
||||
if(props.isLimitMove && angle % 90 != 0) {
|
||||
state.angle = Math.round(angle / 90) * 90
|
||||
}
|
||||
imgMarginDetectionScale(state.scale)
|
||||
})
|
||||
watch(():boolean => props.isLimitMove, (limit: boolean)=>{
|
||||
state.animation = true//state.imageInit;
|
||||
moveStop()
|
||||
if(limit && state.angle % 90 != 0) {
|
||||
state.angle = Math.round(state.angle / 90) * 90
|
||||
}
|
||||
imgMarginDetectionScale(state.scale)
|
||||
})
|
||||
watch(():number[] => [state.clipX, state.clipY], (_:number[])=> {
|
||||
cutDetectionPosition()
|
||||
})
|
||||
|
||||
|
||||
onMounted(() => {
|
||||
nextTick(()=>{
|
||||
let res = uni.getWindowInfo();
|
||||
windowHeight = res.windowHeight
|
||||
windowWidth = res.windowWidth
|
||||
pixelRatio = res.pixelRatio;
|
||||
// state.image = props.imageUrl
|
||||
setClipInfo()
|
||||
setClipCenter()
|
||||
calcClipSize()
|
||||
cutDetectionPosition()
|
||||
|
||||
watchEffect(()=>{
|
||||
state.image = props.imageUrl;
|
||||
})
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@import './index';
|
||||
</style>
|
||||
File diff suppressed because it is too large
Load Diff
BIN
uni_modules/lime-clipper/components/l-clipper/type.ts
Normal file
BIN
uni_modules/lime-clipper/components/l-clipper/type.ts
Normal file
Binary file not shown.
Binary file not shown.
BIN
uni_modules/lime-clipper/components/l-clipper/utils.uts
Normal file
BIN
uni_modules/lime-clipper/components/l-clipper/utils.uts
Normal file
Binary file not shown.
@@ -0,0 +1,186 @@
|
||||
<template>
|
||||
<view class="demo-block">
|
||||
<text class="demo-block__title-text ultra">图片裁剪</text>
|
||||
<text class="demo-block__desc-text" style="display: flex;">可用于图片头像等裁剪处理 。</text>
|
||||
<view class="demo-block__body">
|
||||
<view class="demo-block card">
|
||||
<text class="demo-block__title-text large">基本用法</text>
|
||||
<view class="demo-block__body row">
|
||||
<image :src="url" v-if="url !=''" mode="widthFix"></image>
|
||||
<l-clipper
|
||||
v-if="show"
|
||||
:fixedBoxWidth="600"
|
||||
|
||||
:image-url="imageUrl"
|
||||
@success="success"
|
||||
@cancel="show = false"
|
||||
/>
|
||||
<!-- <l-clipper v-if="show" :image-url="imageUrl" @success="success" @cancel="show = false"/> -->
|
||||
<button @tap="onClick">裁剪图片</button>
|
||||
</view>
|
||||
</view>
|
||||
<!-- <view class="demo-block card">
|
||||
<text class="demo-block__title-text large">插槽</text>
|
||||
<view class="demo-block__body row">
|
||||
<image :src="url1" v-if="url1 !=''" mode="widthFix"></image>
|
||||
<l-clipper
|
||||
v-if="show1"
|
||||
:isLockWidth="isLockWidth"
|
||||
:isLockHeight="isLockHeight"
|
||||
:isLockRatio="isLockRatio"
|
||||
:isLimitMove="isLimitMove"
|
||||
:isDisableScale="isDisableScale"
|
||||
:isDisableRotate="isDisableRotate"
|
||||
:isShowCancelBtn="isShowCancelBtn"
|
||||
:isShowPhotoBtn="isShowPhotoBtn"
|
||||
:isShowRotateBtn="isShowRotateBtn"
|
||||
:isShowConfirmBtn="isShowConfirmBtn"
|
||||
@success="handleClipperSuccess"
|
||||
@cancel="show = false" >
|
||||
<view slot="cancel">取消</view>
|
||||
<view slot="photo">选择图片</view>
|
||||
<view slot="rotate">旋转</view>
|
||||
<view slot="confirm">确定</view>
|
||||
<view class="tools" style="flex-direction: row; flex-wrap: wrap;">
|
||||
<view>
|
||||
<text style="color: white;">显示取消按钮{{isShowCancelBtn}}</text>
|
||||
<switch :checked="isShowCancelBtn" @change="($event: UniSwitchChangeEvent) => {isShowCancelBtn = $event.detail.value}"/>
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">显示选择图片按钮</text>
|
||||
<switch :checked="isShowPhotoBtn" @change="($event: UniSwitchChangeEvent) => {isShowPhotoBtn = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">显示旋转按钮</text>
|
||||
<switch :checked="isShowRotateBtn" @change="($event: UniSwitchChangeEvent) => {isShowRotateBtn = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">显示确定按钮</text>
|
||||
<switch :checked="isShowConfirmBtn" @change="($event: UniSwitchChangeEvent) => {isShowConfirmBtn = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">锁定裁剪框宽度</text>
|
||||
<switch :checked="isLockWidth" @change="($event: UniSwitchChangeEvent) => {isLockWidth = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">锁定裁剪框高度</text>
|
||||
<switch :checked="isLockHeight" @change="($event: UniSwitchChangeEvent) => {isLockHeight = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">锁定裁剪框比例</text>
|
||||
<switch :checked="isLockRatio" @change="($event: UniSwitchChangeEvent) => {isLockRatio = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">限制移动范围</text>
|
||||
<switch :checked="isLimitMove" @change="($event: UniSwitchChangeEvent) => {isLimitMove = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">禁止缩放</text>
|
||||
<switch :checked="isDisableScale" @change="($event: UniSwitchChangeEvent) => {isDisableScale = $event.detail.value}" />
|
||||
</view>
|
||||
<view>
|
||||
<text style="color: white;">禁止旋转</text>
|
||||
<switch :checked="isDisableRotate" @change="($event: UniSwitchChangeEvent) => {isDisableRotate = $event.detail.value}" />
|
||||
</view>
|
||||
</view>
|
||||
</l-clipper>
|
||||
<button @tap="onClick2">裁剪图片</button>
|
||||
</view>
|
||||
</view> -->
|
||||
</view>
|
||||
</view>
|
||||
</template>
|
||||
|
||||
|
||||
|
||||
<script setup lang="uts">
|
||||
// const imageUrl = 'https://img12.360buyimg.com/pop/s1180x940_jfs/t1/97205/26/1142/87801/5dbac55aEf795d962/48a4d7a63ff80b8b.jpg';
|
||||
const imageUrl = '/static/mv2.jpg';
|
||||
const url = ref('')
|
||||
const url1 = ref('')
|
||||
const show = ref(false)
|
||||
const show1 = ref(false)
|
||||
|
||||
const isLockWidth = ref(false)
|
||||
const isLockHeight = ref(false)
|
||||
const isLockRatio = ref(true)
|
||||
const isLimitMove = ref(false)
|
||||
const isDisableScale = ref(false)
|
||||
const isDisableRotate = ref(false)
|
||||
const isShowCancelBtn = ref(true)
|
||||
const isShowPhotoBtn = ref(true)
|
||||
const isShowRotateBtn = ref(true)
|
||||
const isShowConfirmBtn = ref(true)
|
||||
|
||||
const onClick = ()=>{
|
||||
show.value = true;
|
||||
}
|
||||
const onClick2 = ()=>{
|
||||
show1.value = true;
|
||||
}
|
||||
const success = (res: UTSJSONObject) => {
|
||||
url.value = `${res['url'] ?? ''}`
|
||||
show.value = false
|
||||
}
|
||||
|
||||
const handleClipperSuccess = (res: UTSJSONObject) =>{
|
||||
url1.value = `${res['url'] ?? ''}`
|
||||
show1.value = false
|
||||
}
|
||||
|
||||
</script>
|
||||
<style lang="scss">
|
||||
.demo-block {
|
||||
margin: 32px 10px 0;
|
||||
|
||||
// overflow: visible;
|
||||
&.card {
|
||||
background-color: white;
|
||||
padding: 30rpx;
|
||||
margin-bottom: 20rpx !important;
|
||||
}
|
||||
|
||||
&__title {
|
||||
margin: 0;
|
||||
margin-top: 8px;
|
||||
|
||||
&-text {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
font-weight: 400;
|
||||
font-size: 14px;
|
||||
line-height: 16px;
|
||||
|
||||
&.large {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
font-size: 18px;
|
||||
font-weight: 700;
|
||||
line-height: 26px;
|
||||
}
|
||||
|
||||
&.ultra {
|
||||
color: rgba(0, 0, 0, 0.9);
|
||||
font-size: 24px;
|
||||
font-weight: 700;
|
||||
line-height: 32px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&__desc-text {
|
||||
color: rgba(0, 0, 0, 0.6);
|
||||
margin: 8px 16px 0 0;
|
||||
font-size: 14px;
|
||||
line-height: 22px;
|
||||
}
|
||||
|
||||
&__body {
|
||||
margin: 16px 0;
|
||||
overflow: visible;
|
||||
|
||||
.demo-block {
|
||||
// margin-top: 0px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -10,6 +10,7 @@
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
imageUrl: 'https://img12.360buyimg.com/pop/s1180x940_jfs/t1/97205/26/1142/87801/5dbac55aEf795d962/48a4d7a63ff80b8b.jpg',
|
||||
url: '',
|
||||
show: false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user