507 lines
16 KiB
Plaintext
507 lines
16 KiB
Plaintext
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
|
||
]);
|
||
}
|
||
|
||
|