qujiancesu/uni_modules/lime-clipper/components/l-clipper/utils.uts

507 lines
16 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { Point, ClipperclipStart, Rectangle } from './type';
const sysinfo = uni.getWindowInfo();
/**
* 将给定的值限制在指定的最小值和最大值之间
* @param {number} value - 要限制的值。
* @param {number} min - 允许的最小值。
* @param {number} max - 允许的最大值。
* @returns {number} 返回限制后的值。
*/
export function clamp(value : number, min : number, max : number) : number {
return Math.min(Math.max(value, min), max)
};
/**
* 计算图像尺寸
* @param {number} width - 图像原始宽度
* @param {number} height - 图像原始高度
* @param {number} [clipWidth] - 图像剪辑宽度
* @param {number} [clipHeight] - 图像剪辑高度
* @param {number} [originWidth] - 图像原始宽度(备用)
* @param {number} [originHeight] - 图像原始高度(备用)
* @returns {{imageWidth: number, imageHeight: number}} - 计算后的图像尺寸
*/
export function calcImageSize(
width : number,
height : number,
originWidth : number,
originHeight : number,
clipWidth ?: number,
clipHeight ?: number) : number[] {
// 如果原始宽度和高度都为0则将图像宽度设置为系统窗口宽度高度设置为0
if (width == 0 && height == 0) {
return [sysinfo.windowWidth, 0]
}
// 使用剪辑宽度和高度,如果未提供则使用原始宽度和高度
const finalWidth = clipWidth ?? originWidth;
const finalHeight = clipHeight ?? originHeight;
// 根据宽高比计算最终的图像尺寸
if (width / height > finalWidth / finalHeight) {
return [
(width / height) * finalHeight,
finalHeight
]
} else {
return [
finalWidth,
(height / width) * finalWidth
]
}
}
/**
* 计算图片缩放比例
* @param {number} imageWidth 图片原始宽度
* @param {number} imageHeight 图片原始高度
* @param {number} clipWidth 裁剪区域宽度
* @param {number} clipHeight 裁剪区域高度
* @param {number} angle 图片旋转角度
* @param {number=} scale 图片缩放比例可选如果未提供则使用originScale
* @returns {number} 计算后的图片缩放比例
*/
export function calcImageScale(
imageWidth : number,
imageHeight : number,
clipWidth : number,
clipHeight : number,
angle : number,
scale : number
) : number {
let _scale = scale
let _imageWidth = imageWidth
let _imageHeight = imageHeight
// 旋转角度导致宽高互换的情况
if ((angle / 90) % 2 != 0) {
_imageWidth = imageHeight
_imageHeight = imageWidth
}
// 计算缩放比例
const scaleX = clipWidth / _imageWidth;
const scaleY = clipHeight / _imageHeight;
// 如果旋转角度不为0且当前缩放比例等于最大缩放比例则调整缩放比例
if (angle != 0 && scale == Math.max(clipWidth / _imageHeight, clipHeight / _imageWidth)) {
return Math.max(scaleX, scaleY);
}
// 根据裁剪区域调整缩放比例
if (_imageWidth * scale < clipWidth) {
_scale = scaleX;
}
if (_imageHeight * scale < clipHeight) {
_scale = Math.max(scale, scaleY);
}
return _scale
}
/**
* 计算图片在裁剪区域内的偏移量
* @param {number} imageLeft 图片左边缘的初始偏移量。
* @param {number} imageTop 图片顶边缘的初始偏移量。
* @param {number} imageWidth 图片的原始宽度。
* @param {number} imageHeight 图片的原始高度。
* @param {number} clipX 裁剪区域的左边缘位置。
* @param {number} clipY 裁剪区域的顶边缘位置。
* @param {number} clipWidth 裁剪区域的宽度。
* @param {number} clipHeight 裁剪区域的高度。
* @param {number} angle 图片的旋转角度。
* @param {number} scale 图片的缩放比例。
* @returns {OffsetAndScale} 包含计算后的偏移量left、top和缩放比例scale的对象。
*/
export function calcImageOffset(
imageLeft : number,
imageTop : number,
imageWidth : number,
imageHeight : number,
clipX : number,
clipY : number,
clipWidth : number,
clipHeight : number,
angle : number,
scale : number) : number[] {
let left = imageLeft;
let top = imageTop;
let _imageWidth = imageWidth
let _imageHeight = imageHeight
// 旋转角度导致宽高互换的情况
if ((angle / 90) % 2 != 0) {
_imageWidth = imageHeight
_imageHeight = imageWidth
}
// 计算当前图片尺寸
const currentImageWidth = _imageWidth * scale / 2;
const currentImageHeight = _imageHeight * scale / 2;
// 限制图片偏移量,确保图片在裁剪内
left = Math.min(left, clipX + currentImageWidth);
left = Math.max(left, clipX + clipWidth - currentImageWidth);
top = Math.min(top, clipY + currentImageHeight);
top = Math.max(top, clipY + clipHeight - currentImageHeight);
// return {
// left,
// top,
// scale
// }
return [left, top, scale]
}
/**
* 根据给定的宽度和高度计算直角三角形的斜边长度(勾股定理)
* @param {number} width - 直角三角形的宽度。
* @param {number} height - 直角三角形的高度。
* @returns {number} 返回直角三角形的斜边长度。
*/
export function calcPythagoreanTheorem(width : number, height : number) {
return Math.sqrt(Math.pow(width, 2) + Math.pow(height, 2));
}
/**
* 计算触摸点相对于给定点的偏移量
* @param {Point} point - 给定的点对象,包含 x 和 y 坐标。
* @param {number} clientX - 触摸点的客户端 x 坐标。
* @param {number} clientY - 触摸点的客户端 y 坐制。
* @returns {number[]} 返回一个包含两个数字的数组,分别表示触摸点相对于给定点的 x 和 y 偏移量。
*/
export function imageTouchMoveOfCalcOffset(point : Point, clientX : number, clientY : number) : number[] {
return [
clientX - point.x,
clientY - point.y,
]
}
/**
* 根据触摸点的位置确定点击的是裁剪框的哪个角
* @param {number} clipX - 裁剪框的 x 坐标。
* @param {number} clipY - 裁剪框的 y 坐标。
* @param {number} clipWidth - 裁剪框的宽度。
* @param {number} clipHeight - 裁剪框的高度。
* @param {number} currentX - 触摸点的 x 坐标。
* @param {number} currentY - 触摸点的 y 坐标。
* @returns {number} 返回点击的角1 表示右下角2 表示右上角3 表示左上角4 表示左下角;如果没有点击在角上,则返回 -1。
*/
export function determineDirection(clipX : number, clipY : number, clipWidth : number, clipHeight : number, currentX : number, currentY : number) : number {
/*
* (右下>>1 右上>>2 左上>>3 左下>>4)
*/
let corner : number = -1;
/**
* 思路:(利用直角坐标系)
* 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;
}
/**
* 根据触摸事件更新裁剪框的尺寸和位置,并保持比例(如果需要)
* @param {number} clipWidth - 当前裁剪框的宽度。
* @param {number} clipHeight - 当前裁剪框的高度。
* @param {number} oldClipX - 当前裁剪框的 x 坐标。
* @param {number} oldClipY - 当前裁剪框的 y 坐标。
* @param {number} minWidth - 裁剪框允许的最小宽度。
* @param {number} maxWidth - 裁剪框允许的最大宽度。
* @param {number} minHeight - 裁剪框允许的最小高度。
* @param {number} maxHeight - 裁剪框允许的最大高度。
* @param {ClipperclipStart} clipStart - 包含裁剪框起始位置和拖动角的信息。
* @param {boolean} isLockRatio - 是否保持裁剪框的宽高比。
* @param {UniTouch} touch - 触摸事件对象,包含 clientX 和 clientY 属性。
* @returns {number[] | null} 如果裁剪框尺寸或位置发生变化,则返回一个包含新宽度、新高度、新 x 坐标和新 y 坐标的数组;否则返回 null。
*/
export function clipTouchMoveOfCalculate(
clipWidth : number,
clipHeight : number,
oldClipX : number,
oldClipY : number,
minWidth : number,
maxWidth : number,
minHeight : number,
maxHeight : number,
clipStart : ClipperclipStart,
isLockRatio : boolean,
touch : UniTouch) : number[] | null {
const clientX = touch.clientX;
// const clientY = touch.clientY;
// 获取裁剪框新尺寸
let width = clipWidth;
let height = clipHeight;
let clipX = oldClipX;
let clipY = oldClipY;
// 更新尺寸并保持比例
const updateSizeWithRatio = (newWidth : number) => {
width = newWidth;
height = isLockRatio ? width / (clipWidth / clipHeight) : height;
};
// 检查并修正尺寸
const checkAndCorrectSize = () : boolean => {
width = clamp(width, minWidth, maxWidth);
height = clamp(height, minHeight, maxHeight);
return width != clipWidth || height != clipHeight;
};
// 根据拖动位置更新尺寸和位置
switch (clipStart.corner) {
case 1:
updateSizeWithRatio(clipStart.width - clipStart.x + clientX);
if (!checkAndCorrectSize()) return null;
break;
case 2:
updateSizeWithRatio(clipStart.width - clipStart.x + clientX);
if (!checkAndCorrectSize()) return null;
clipY = clipStart.clipY - (height - clipStart.height);
break;
case 3:
updateSizeWithRatio(clipStart.x - clientX + clipStart.width);
if (!checkAndCorrectSize()) return null;
clipY = clipStart.clipY - (height - clipStart.height);
clipX = clipStart.clipX - (width - clipStart.width);
break;
case 4:
updateSizeWithRatio(clipStart.width + clipStart.x - clientX);
if (!checkAndCorrectSize()) return null;
clipX = clipStart.clipX - (width - clipStart.width);
break;
default:
return null;
}
return [
width,
height,
clipX,
clipY
]
}
/**
* 判断一个点是否在圆内(包括圆上)
* @param {Point} point - 要检查的点对象,包含 x 和 y 坐标。
* @param {Point} center - 圆心的点对象,包含 x 和 y 坐标。
* @param {number} [radius=10] - 圆的半径,默认值为 10。
* @returns {boolean} 如果点在圆内(包括圆上),则返回 true否则返回 false。
*/
export function isPointInCircle(point : Point, center : Point, radius : number = 10) : boolean {
const dx = point.x - center.x;
const dy = point.y - center.y;
return Math.sqrt(dx * dx + dy * dy) <= radius;
}
/**
* 获取坐标在矩形的哪个顶点
* @param point 点的坐标 Point
* @param rectangle 多边形的
* @param radius 顶点的半径
* @returns 如果点在多边形顶点返回顶点下标
*/
export function getPointPositionInRectangle(
point : Point,
rectangle : Rectangle,
radius : number = 30
) : number | null {
const topLeft : Point = { x: rectangle.x, y: rectangle.y };
const topRight : Point = { x: rectangle.x + rectangle.width, y: rectangle.y };
const bottomLeft : Point = { x: rectangle.x, y: rectangle.y + rectangle.height };
const bottomRight : Point = { x: rectangle.x + rectangle.width, y: rectangle.y + rectangle.height };
const corners = [
bottomRight,
topRight,
topLeft,
bottomLeft,
];
for (let i = 0; i < corners.length; i++) {
if (isPointInCircle(point, corners[i], radius)) {
return i + 1;
}
}
return null;
}
/**
* 判断点是否在多边形内(使用射线法)
* @param point 点的坐标 Point
* @param vs 多边形的顶点数组,每个顶点是一个 Point 数组
* @param start 起始索引可选默认为0
* @param end 结束索引可选默认为vs.length
* @returns 如果点在多边形内返回 true否则返回 false
*/
function pointInPolygonNested(point: Point, vs: Point[], start?: number, end?: number): boolean {
const {x, y} = point;
let inside = false;
start = start ?? 0;
end = end ?? vs.length
const len = end - start;
for (let i = 0, j = len - 1; i < len; j = i++) {
const xi = vs[i + start].x;
const yi = vs[i + start].y;
const xj = vs[j + start].x;
const yj = vs[j + start].y;
const intersect = ((yi > y) !== (yj > y))
&& (x < (xj - xi) * (y - yi) / (yj - yi) + xi);
if (intersect) inside = !inside;
}
return inside;
}
/**
* 判断一个点是否在旋转后的矩形内(包括边界)
* @param {Point} point - 要检查的点对象,包含 x 和 y 坐标。
* @param {Rectangle} rect - 矩形对象,包含 x、y、width 和 height 属性。
* @param {number} scale - 矩形的缩放比例。
* @param {number} angle - 矩形旋转的角度(角度制)。
* @returns {boolean} 如果点在旋转后的矩形内(包括边界),则返回 true否则返回 false。
*/
export function isPointInRotatedRectangle(
point : Point,
rect : Rectangle,
scale : number,
angle : number // 角度制
) : boolean {
// 将角度转换为弧度
const radians = (angle * Math.PI) / 180;
// 计算矩形的中心点
const rectCenterX = rect.x + rect.width / 2;
const rectCenterY = rect.y + rect.height / 2;
// 缩放矩形的宽高
const scaledWidth = rect.width * scale;
const scaledHeight = rect.height * scale;
// 旋转点坐标,基于矩形的中心点
const cosAngle = Math.cos(-radians);
const sinAngle = Math.sin(-radians);
// const rotatedPoint : Point = {
// x:
// cosAngle * (point.x - rectCenterX) -
// sinAngle * (point.y - rectCenterY) +
// rectCenterX,
// y:
// sinAngle * (point.x - rectCenterX) +
// cosAngle * (point.y - rectCenterY) +
// rectCenterY
// };
// 检查旋转后的点是否在未旋转的矩形内
// 这里我们需要计算旋转后的矩形的四个顶点,然后判断点是否在这个四边形内
const halfScaledWidth = scaledWidth / 2;
const halfScaledHeight = scaledHeight / 2;
// 计算旋转后的矩形的四个顶点
const topLeft : Point = {
x: rectCenterX - halfScaledWidth * cosAngle + halfScaledHeight * sinAngle,
y: rectCenterY - halfScaledWidth * sinAngle - halfScaledHeight * cosAngle
};
const topRight : Point = {
x: rectCenterX + halfScaledWidth * cosAngle + halfScaledHeight * sinAngle,
y: rectCenterY + halfScaledWidth * sinAngle - halfScaledHeight * cosAngle
};
const bottomRight : Point = {
x: rectCenterX + halfScaledWidth * cosAngle - halfScaledHeight * sinAngle,
y: rectCenterY + halfScaledWidth * sinAngle + halfScaledHeight * cosAngle
};
const bottomLeft : Point = {
x: rectCenterX - halfScaledWidth * cosAngle - halfScaledHeight * sinAngle,
y: rectCenterY - halfScaledWidth * sinAngle + halfScaledHeight * cosAngle
};
// 计算向量叉乘
function crossMul(v1 : Point, v2 : Point) : number {
return v1.x * v2.y - v1.y * v2.x;
}
// 判断两条线段是否相交
function checkCross(p1 : Point, p2 : Point, p3 : Point, p4 : Point) : boolean {
const v1 : Point = { x: p1.x - p3.x, y: p1.y - p3.y };
const v2 : Point = { x: p2.x - p3.x, y: p2.y - p3.y };
const v3 : Point = { x: p4.x - p3.x, y: p4.y - p3.y };
const v : number = crossMul(v1, v3) * crossMul(v2, v3);
const v1_2 : Point = { x: p3.x - p1.x, y: p3.y - p1.y };
const v2_2 : Point = { x: p4.x - p1.x, y: p4.y - p1.y };
const v3_2 : Point = { x: p2.x - p1.x, y: p2.y - p1.y };
return (v <= 0 && crossMul(v1_2, v3_2) * crossMul(v2_2, v3_2) <= 0);
}
// 使用射线法判断点是否在四边形内
function isPointInPolygon(point : Point, polygon : Point[]) : boolean {
// const p1 : Point = point;
// const p2 : Point = { x: 1000000000000, y: point.y };
// let count = 0;
// // 对每条边都和射线作对比
// for (let i = 0; i < polygon.length - 1; i++) {
// const p3 : Point = polygon[i];
// const p4 : Point = polygon[i + 1];
// if (checkCross(p1, p2, p3, p4)) {
// count++;
// }
// }
// console.log('count', count)
// const p3 : Point = polygon[polygon.length - 1];
// const p4 : Point = polygon[0];
// if (checkCross(p1, p2, p3, p4)) {
// count++;
// }
// return count % 2 == 0 ? false : true;
let isInside = false;
for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
if (
(polygon[i].y > point.y) != (polygon[j].y > point.y) &&
point.x <
((polygon[j].x - polygon[i].x) * (point.y - polygon[i].y)) /
(polygon[j].y - polygon[i].y) +
polygon[i].x
) {
isInside = !isInside;
}
}
return isInside;
}
return isPointInPolygon(point, [
topLeft,
topRight,
bottomRight,
bottomLeft
]);
}