react-moveable
Version:
A React Component that create Moveable, Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable.
608 lines (555 loc) • 18.2 kB
text/typescript
import { average, getRad, throttle } from "@daybrush/utils";
import { rotate } from "@scena/matrix";
import { maxOffset, getDistSize, getTinyDist, calculatePoses, abs } from "../../utils";
import { SnappableProps, DraggableProps, RotatableProps, MoveableManagerInterface } from "../../types";
import { getDragDist, getPosByDirection, getInverseDragDist } from "../../gesto/GestoUtils";
import { getNearOffsetInfo } from "./snap";
import { TINY_NUM } from "../../consts";
import { getInitialBounds, solveLineConstants } from "./utils";
function isStartLine(dot: number[], line: number[][]) {
// l o => true
// o l => false
const cx = average([line[0][0], line[1][0]]);
const cy = average([line[0][1], line[1][1]]);
return {
vertical: cx <= dot[0],
horizontal: cy <= dot[1],
};
}
function hitTestLine(
dot: number[],
[pos1, pos2]: number[][],
) {
let dx = pos2[0] - pos1[0];
let dy = pos2[1] - pos1[1];
if (abs(dx) < TINY_NUM) {
dx = 0;
}
if (abs(dy) < TINY_NUM) {
dy = 0;
}
let test1: number;
let test2: number;
if (!dx) {
test1 = pos1[0];
test2 = dot[0];
} else if (!dy) {
test1 = pos1[1];
test2 = dot[1];
} else {
const a = dy / dx;
// y = a * (x - pos1) + pos1
test1 = a * (dot[0] - pos1[0]) + pos1[1];
test2 = dot[1];
}
return test1 - test2;
}
function isSameStartLine(dots: number[][], line: number[][], centerSign: boolean, error: number = TINY_NUM) {
return dots.every(dot => {
const value = hitTestLine(dot, line);
const sign = value <= 0;
return sign === centerSign || abs(value) <= error;
});
}
function checkInnerBoundDot(
pos: number,
start: number,
end: number,
isStart: boolean,
threshold = 0,
) {
if (
(isStart && start - threshold <= pos)
|| (!isStart && pos <= end + threshold)
) {
// false 402 565 602 => 37 ([0, 37])
// true 400 524.9712603540036 600 => 124 ([124, 0])
// true 400 410 600 => 10 ([10, 0])
return {
isBound: true,
offset: isStart ? start - pos : end - pos,
};
}
return {
isBound: false,
offset: 0,
};
}
function checkInnerBound(
moveable: MoveableManagerInterface<SnappableProps>,
{ line, centerSign, verticalSign, horizontalSign, lineConstants }: InnerBoundLineInfo,
) {
const bounds = moveable.props.innerBounds;
if (!bounds) {
return {
isAllBound: false,
isBound: false,
isVerticalBound: false,
isHorizontalBound: false,
offset: [0, 0],
};
}
const { left, top, width, height } = bounds;
const leftLine = [[left, top], [left, top + height]];
const topLine = [[left, top], [left + width, top]];
const rightLine = [[left + width, top], [left + width, top + height]];
const bottomLine = [[left, top + height], [left + width, top + height]];
if (isSameStartLine([
[left, top],
[left + width, top],
[left, top + height],
[left + width, top + height],
], line, centerSign)) {
return {
isAllBound: false,
isBound: false,
isVerticalBound: false,
isHorizontalBound: false,
offset: [0, 0],
};
}
// test vertical
const topBoundInfo = checkLineBoundCollision(line, lineConstants, topLine, verticalSign);
const bottomBoundInfo = checkLineBoundCollision(line, lineConstants, bottomLine, verticalSign);
// test horizontal
const leftBoundInfo = checkLineBoundCollision(line, lineConstants, leftLine, horizontalSign);
const rightBoundInfo = checkLineBoundCollision(line, lineConstants, rightLine, horizontalSign);
const isAllVerticalBound = topBoundInfo.isBound && bottomBoundInfo.isBound;
const isVerticalBound = topBoundInfo.isBound || bottomBoundInfo.isBound;
const isAllHorizontalBound = leftBoundInfo.isBound && rightBoundInfo.isBound;
const isHorizontalBound = leftBoundInfo.isBound || rightBoundInfo.isBound;
const verticalOffset = maxOffset(topBoundInfo.offset, bottomBoundInfo.offset);
const horizontalOffset = maxOffset(leftBoundInfo.offset, rightBoundInfo.offset);
let offset = [0, 0];
let isBound = false;
let isAllBound = false;
if (abs(horizontalOffset) < abs(verticalOffset)) {
offset = [verticalOffset, 0];
isBound = isVerticalBound;
isAllBound = isAllVerticalBound;
} else {
offset = [0, horizontalOffset];
isBound = isHorizontalBound;
isAllBound = isAllHorizontalBound;
}
return {
isAllBound,
isVerticalBound,
isHorizontalBound,
isBound,
offset,
};
}
function checkLineBoundCollision(
line: number[][],
[a, b]: [number, number, number],
boundLine: number[][],
isStart: boolean,
threshold?: number,
isRender?: boolean,
) {
const dot1 = line[0];
// const dot2 = line[1];
const boundDot1 = boundLine[0];
const boundDot2 = boundLine[1];
// const dy1 = getTinyDist(dot2[1] - dot1[1]);
// const dx1 = getTinyDist(dot2[0] - dot1[0]);
const dy2 = getTinyDist(boundDot2[1] - boundDot1[1]);
const dx2 = getTinyDist(boundDot2[0] - boundDot1[0]);
const hasDx = b;
const hasDy = a;
const slope = - a / b;
// lineConstants
// ax + by + c = 0
// dx2 or dy2 is zero
if (!dx2) {
// vertical
// by + c = 0
if (isRender && !hasDy) {
// 90deg
return {
isBound: false,
offset: 0,
};
} else if (hasDx) {
// ax + by + c = 0
// const y = dy1 ? dy1 / dx1 * (boundDot1[0] - dot1[0]) + dot1[1] : dot1[1];
const y = slope * (boundDot1[0] - dot1[0]) + dot1[1];
// boundDot1[1] <= y <= boundDot2[1]
return checkInnerBoundDot(y, boundDot1[1], boundDot2[1], isStart, threshold);
} else {
// ax + c = 0
const offset = boundDot1[0] - dot1[0];
const isBound = abs(offset) <= (threshold || 0);
return {
isBound,
offset: isBound ? offset : 0,
};
}
} else if (!dy2) {
// horizontal
if (isRender && !hasDx) {
// 90deg
return {
isBound: false,
offset: 0,
};
} else if (hasDy) {
// y = a * (x - x1) + y1
// x = (y - y1) / a + x1
// const a = dy1 / dx1;
// const x = dx1 ? (boundDot1[1] - dot1[1]) / a + dot1[0] : dot1[0];
const x = (boundDot1[1] - dot1[1]) / slope + dot1[0];
// boundDot1[0] <= x && x <= boundDot2[0]
return checkInnerBoundDot(x, boundDot1[0], boundDot2[0], isStart, threshold);
} else {
const offset = boundDot1[1] - dot1[1];
const isBound = abs(offset) <= (threshold || 0);
return {
isBound,
offset: isBound ? offset : 0,
};
}
}
return {
isBound: false,
offset: 0,
};
}
export function getInnerBoundInfo(
moveable: MoveableManagerInterface<SnappableProps>,
lineInfos: InnerBoundLineInfo[],
datas: any,
) {
return lineInfos.map(info => {
const {
isBound,
offset,
isVerticalBound,
isHorizontalBound,
} = checkInnerBound(moveable, info);
const multiple = info.multiple;
const sizeOffset = getDragDist({
datas,
distX: offset[0],
distY: offset[1],
}).map((size, i) => size * (multiple[i] ? 2 / multiple[i] : 0));
return {
sign: multiple,
isBound,
isVerticalBound,
isHorizontalBound,
isSnap: false,
offset: sizeOffset,
};
});
}
export function getInnerBoundDragInfo(
moveable: MoveableManagerInterface<SnappableProps & DraggableProps, any>,
poses: number[][],
datas: any,
) {
const lines = getCheckInnerBoundLineInfos(moveable, poses, [0, 0], false).map(info => {
return {
...info,
multiple: info.multiple.map(dir => abs(dir) * 2),
};
});
const innerBoundInfo = getInnerBoundInfo(moveable, lines, datas);
const widthOffsetInfo = getNearOffsetInfo(innerBoundInfo, 0);
const heightOffsetInfo = getNearOffsetInfo(innerBoundInfo, 1);
let verticalOffset = 0;
let horizontalOffset = 0;
const isVerticalBound = widthOffsetInfo.isVerticalBound || heightOffsetInfo.isVerticalBound;
const isHorizontalBound = widthOffsetInfo.isHorizontalBound || heightOffsetInfo.isHorizontalBound;
if (isVerticalBound || isHorizontalBound) {
[verticalOffset, horizontalOffset] = getInverseDragDist({
datas,
distX: -widthOffsetInfo.offset[0],
distY: -heightOffsetInfo.offset[1],
});
}
return {
vertical: {
isBound: isVerticalBound,
offset: verticalOffset,
},
horizontal: {
isBound: isHorizontalBound,
offset: horizontalOffset,
},
};
}
export function getCheckSnapLineDirections(
direction: number[],
keepRatio: boolean,
) {
const lineDirections: number[][][] = [];
const x = direction[0];
const y = direction[1];
if (x && y) {
lineDirections.push(
[[0, y * 2], direction, [-x, y]],
[[x * 2, 0], direction, [x, -y]],
);
} else if (x) {
// vertcal
lineDirections.push(
[[x * 2, 0], [x, 1], [x, -1]],
);
if (keepRatio) {
lineDirections.push(
[[0, -1], [x, -1], [-x, -1]],
[[0, 1], [x, 1], [-x, 1]],
);
}
} else if (y) {
// horizontal
lineDirections.push(
[[0, y * 2], [1, y], [-1, y]],
);
if (keepRatio) {
lineDirections.push(
[[-1, 0], [-1, y], [-1, -y]],
[[1, 0], [1, y], [1, -y]],
);
}
} else {
// [0, 0] to all direction
lineDirections.push(
[[-1, 0], [-1, -1], [-1, 1]],
[[1, 0], [1, -1], [1, 1]],
[[0, -1], [-1, -1], [1, -1]],
[[0, 1], [-1, 1], [1, 1]],
);
}
return lineDirections;
}
export interface InnerBoundLineInfo {
line: number[][];
multiple: number[];
horizontalSign: boolean;
verticalSign: boolean;
centerSign: boolean;
lineConstants: [number, number, number];
}
export function getCheckInnerBoundLineInfos(
moveable: MoveableManagerInterface<SnappableProps>,
poses: number[][],
direction: number[],
keepRatio: boolean,
): InnerBoundLineInfo[] {
const {
allMatrix,
is3d,
} = moveable.state;
const virtualPoses = calculatePoses(allMatrix, 100, 100, is3d ? 4 : 3);
const center = getPosByDirection(virtualPoses, [0, 0]);
return getCheckSnapLineDirections(direction, keepRatio).map(([multiple, dir1, dir2]) => {
const virtualLine = [
getPosByDirection(virtualPoses, dir1),
getPosByDirection(virtualPoses, dir2),
];
const lineConstants = solveLineConstants(virtualLine);
const {
vertical: verticalSign,
horizontal: horizontalSign,
} = isStartLine(center, virtualLine);
const centerSign = hitTestLine(center, virtualLine) <= 0;
return {
multiple,
centerSign,
verticalSign,
horizontalSign,
lineConstants,
line: [
getPosByDirection(poses, dir1),
getPosByDirection(poses, dir2),
],
};
});
}
function isBoundRotate(
relativePoses: number[][],
boundDots: number[][],
center: number[],
rad: number,
) {
const nextPoses = rad ? relativePoses.map(pos => rotate(pos, rad)) : relativePoses;
return [
[nextPoses[0], nextPoses[1]],
[nextPoses[1], nextPoses[3]],
[nextPoses[3], nextPoses[2]],
[nextPoses[2], nextPoses[0]],
].some(line => {
const centerSign = hitTestLine(center, line) <= 0;
return !isSameStartLine(boundDots, line, centerSign);
});
}
function getDistPointLine([pos1, pos2]: number[][]) {
// x = 0, y = 0
// d = (ax + by + c) / root(a2 + b2)
const dx = pos2[0] - pos1[0];
const dy = pos2[1] - pos1[1];
if (!dx) {
return abs(pos1[0]);
}
if (!dy) {
return abs(pos1[1]);
}
// y - y1 = a(x - x1)
// 0 = ax -y + -a * x1 + y1
const a = dy / dx;
return abs((-a * pos1[0] + pos1[1]) / Math.sqrt(Math.pow(a, 2) + 1));
}
function solveReverseLine([pos1, pos2]: number[][]) {
const dx = pos2[0] - pos1[0];
const dy = pos2[1] - pos1[1];
if (!dx) {
return [pos1[0], 0];
}
if (!dy) {
return [0, pos1[1]];
}
const a = dy / dx;
// y - y1 = a (x - x1)
// y = ax - a * x1 + y1
const b = -a * pos1[0] + pos1[1];
// y = ax + b = -1/a x
// x = -b / (a + 1 / a)
// y = b / (1 + 1 / a^2)
return [
-b / (a + 1 / a),
b / ((a * a) + 1),
];
}
export function checkRotateInnerBounds(
moveable: MoveableManagerInterface<SnappableProps & RotatableProps, any>,
prevPoses: number[][],
nextPoses: number[][],
origin: number[],
rotation: number,
) {
const bounds = moveable.props.innerBounds;
const rad = rotation * Math.PI / 180;
if (!bounds) {
return [];
}
const {
left,
top,
width,
height,
} = bounds;
const relativeLeft = left - origin[0];
const relativeRight = left + width - origin[0];
const relativeTop = top - origin[1];
const relativeBottom = top + height - origin[1];
const dots = [
[relativeLeft, relativeTop],
[relativeRight, relativeTop],
[relativeLeft, relativeBottom],
[relativeRight, relativeBottom],
];
const center = getPosByDirection(nextPoses, [0, 0]);
if (!isBoundRotate(nextPoses, dots, center, 0)) {
return [];
}
const result: number[] = [];
const dotInfos = dots.map(dot => [
getDistSize(dot),
getRad([0, 0], dot),
]);
[
[nextPoses[0], nextPoses[1]],
[nextPoses[1], nextPoses[3]],
[nextPoses[3], nextPoses[2]],
[nextPoses[2], nextPoses[0]],
].forEach(line => {
const lineRad = getRad([0, 0], solveReverseLine(line));
const lineDist = getDistPointLine(line);
result.push(...dotInfos
.filter(([dotDist]) => {
return dotDist && lineDist <= dotDist;
})
.map(([dotDist, dotRad]) => {
const distRad = Math.acos(dotDist ? lineDist / dotDist : 0);
const nextRad1 = dotRad + distRad;
const nextRad2 = dotRad - distRad;
return [
rad + nextRad1 - lineRad,
rad + nextRad2 - lineRad,
];
})
.reduce((prev, cur) => {
prev.push(...cur);
return prev;
}, [])
.filter(nextRad => !isBoundRotate(prevPoses, dots, center, nextRad))
.map(nextRad => throttle(nextRad * 180 / Math.PI, TINY_NUM)));
});
return result;
}
export function checkInnerBoundPoses(
moveable: MoveableManagerInterface<SnappableProps>,
) {
const innerBounds = moveable.props.innerBounds;
const boundMap = getInitialBounds();
if (!innerBounds) {
return {
boundMap,
vertical: [],
horizontal: [],
};
}
const {
pos1,
pos2,
pos3,
pos4,
} = moveable.getRect();
const poses = [pos1, pos2, pos3, pos4];
const center = getPosByDirection(poses, [0, 0]);
const { left, top, width, height } = innerBounds;
const leftLine = [[left, top], [left, top + height]];
const topLine = [[left, top], [left + width, top]];
const rightLine = [[left + width, top], [left + width, top + height]];
const bottomLine = [[left, top + height], [left + width, top + height]];
const lineInfos = getCheckInnerBoundLineInfos(moveable, poses, [0, 0], false);
const horizontalPoses: number[] = [];
const verticalPoses: number[] = [];
lineInfos.forEach(lineInfo => {
const { line, lineConstants } = lineInfo;
const {
horizontal: isHorizontalStart,
vertical: isVerticalStart,
} = isStartLine(center, line);
// test vertical
const topBoundInfo = checkLineBoundCollision(line, lineConstants, topLine, isVerticalStart, 1, true);
const bottomBoundInfo = checkLineBoundCollision(line, lineConstants, bottomLine, isVerticalStart, 1, true);
// test horizontal
const leftBoundInfo = checkLineBoundCollision(line, lineConstants, leftLine, isHorizontalStart, 1, true);
const rightBoundInfo = checkLineBoundCollision(line, lineConstants, rightLine, isHorizontalStart, 1, true);
if (topBoundInfo.isBound && !boundMap.top) {
horizontalPoses.push(top);
boundMap.top = true;
}
if (bottomBoundInfo.isBound && !boundMap.bottom) {
horizontalPoses.push(top + height);
boundMap.bottom = true;
}
if (leftBoundInfo.isBound && !boundMap.left) {
verticalPoses.push(left);
boundMap.left = true;
}
if (rightBoundInfo.isBound && !boundMap.right) {
verticalPoses.push(left + width);
boundMap.right = true;
}
});
return {
boundMap,
horizontal: horizontalPoses,
vertical: verticalPoses,
};
}