react-moveable
Version:
A React Component that create Moveable, Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable.
530 lines (486 loc) • 15.9 kB
text/typescript
import {
SnapInfo, SnappableProps, SnappableState,
SnapGuideline, ResizableProps, ScalableProps,
SnapOffsetInfo, MoveableManagerInterface, SnapDirectionPoses, SnapDirectionInfo,
} from "../../types";
import {
selectValue, getTinyDist, abs,
} from "../../utils";
import { getPosByDirection, getPosesByDirection } from "../../gesto/GestoUtils";
import { TINY_NUM } from "../../consts";
import { minus } from "@scena/matrix";
import { splitSnapDirectionPoses } from "./utils";
import { NAME_snapHorizontalThreshold, NAME_snapVerticalThreshold } from "./names";
export function checkMoveableSnapPoses(
moveable: MoveableManagerInterface<SnappableProps, SnappableState>,
posesX: number[],
posesY: number[],
dirXs: string[] = [],
dirYs: string[] = [],
customSnapVerticalThreshold: number | undefined,
customSnapHorizontalThreshold: number | undefined,
) {
const props = moveable.props;
const snapThresholdMultiples = moveable.state.snapThresholdInfo?.multiples || [1, 1];
const snapHorizontalThreshold = selectValue<number>(
customSnapHorizontalThreshold,
props[NAME_snapHorizontalThreshold],
5,
);
const snapVerticalThreshold = selectValue<number>(
customSnapVerticalThreshold,
props[NAME_snapVerticalThreshold],
5,
);
return checkSnapPoses(
moveable.state.guidelines,
posesX,
posesY,
dirXs,
dirYs,
snapHorizontalThreshold,
snapVerticalThreshold,
snapThresholdMultiples,
);
}
export function checkSnapPoses(
guidelines: SnapGuideline[],
posesX: number[],
posesY: number[],
dirXs: string[],
dirYs: string[],
snapHorizontalThreshold: number,
snapVerticalThreshold: number,
multiples: number[],
) {
return {
vertical: checkSnap(guidelines, "vertical", posesX, snapVerticalThreshold * multiples[0], dirXs),
horizontal: checkSnap(guidelines, "horizontal", posesY, snapHorizontalThreshold * multiples[1], dirYs),
};
}
export function checkSnapKeepRatio(
moveable: MoveableManagerInterface<SnappableProps, SnappableState>,
startPos: number[],
endPos: number[],
): { vertical: SnapOffsetInfo, horizontal: SnapOffsetInfo } {
const [endX, endY] = endPos;
const [startX, startY] = startPos;
let [dx, dy] = minus(endPos, startPos);
const isBottom = dy > 0;
const isRight = dx > 0;
dx = getTinyDist(dx);
dy = getTinyDist(dy);
const verticalInfo: SnapOffsetInfo = {
isSnap: false,
offset: 0,
pos: 0,
};
const horizontalInfo: SnapOffsetInfo = {
isSnap: false,
offset: 0,
pos: 0,
};
if (dx === 0 && dy === 0) {
return {
vertical: verticalInfo,
horizontal: horizontalInfo,
};
}
const {
vertical: verticalSnapInfo,
horizontal: horizontalSnapInfo,
} = checkMoveableSnapPoses(
moveable,
dx ? [endX] : [],
dy ? [endY] : [],
[],
[],
undefined,
undefined,
);
verticalSnapInfo.posInfos.filter(({ pos }) => {
return isRight ? pos >= startX : pos <= startX;
});
horizontalSnapInfo.posInfos.filter(({ pos }) => {
return isBottom ? pos >= startY : pos <= startY;
});
verticalSnapInfo.isSnap = verticalSnapInfo.posInfos.length > 0;
horizontalSnapInfo.isSnap = horizontalSnapInfo.posInfos.length > 0;
const {
isSnap: isVerticalSnap,
guideline: verticalGuideline,
} = getNearestSnapGuidelineInfo(verticalSnapInfo);
const {
isSnap: isHorizontalSnap,
guideline: horizontalGuideline,
} = getNearestSnapGuidelineInfo(horizontalSnapInfo);
const horizontalPos = isHorizontalSnap ? horizontalGuideline!.pos[1] : 0;
const verticalPos = isVerticalSnap ? verticalGuideline!.pos[0] : 0;
if (dx === 0) {
if (isHorizontalSnap) {
horizontalInfo.isSnap = true;
horizontalInfo.pos = horizontalGuideline!.pos[1];
horizontalInfo.offset = endY - horizontalInfo.pos;
}
} else if (dy === 0) {
if (isVerticalSnap) {
verticalInfo.isSnap = true;
verticalInfo.pos = verticalPos;
verticalInfo.offset = endX - verticalPos;
}
} else {
// y - y1 = a * (x - x1)
const a = dy / dx;
const b = endPos[1] - a * endX;
let y = 0;
let x = 0;
let isSnap = false;
if (isVerticalSnap) {
x = verticalPos;
y = a * x + b;
isSnap = true;
} else if (isHorizontalSnap) {
y = horizontalPos;
x = (y - b) / a;
isSnap = true;
}
if (isSnap) {
verticalInfo.isSnap = true;
verticalInfo.pos = x;
verticalInfo.offset = endX - x;
horizontalInfo.isSnap = true;
horizontalInfo.pos = y;
horizontalInfo.offset = endY - y;
}
}
return {
vertical: verticalInfo,
horizontal: horizontalInfo,
};
}
function getStringDirection(dir: number | string) {
let stringDirection = "";
if (dir === -1 || dir === "top" || dir === "left") {
stringDirection = "start";
} else if (dir === 0 || dir === "center" || dir === "middle") {
stringDirection = "center";
} else if (dir === 1 || dir === "right" || dir === "bottom") {
stringDirection = "end";
}
return stringDirection;
}
export function checkSnaps(
moveable: MoveableManagerInterface<SnappableProps, SnappableState>,
rect: SnapDirectionPoses,
customSnapVerticalThreshold: number | undefined,
customSnapHorizontalThreshold: number | undefined,
): { vertical: SnapDirectionInfo; horizontal: SnapDirectionInfo } {
const poses = splitSnapDirectionPoses(moveable.props.snapDirections, rect);
const result = checkMoveableSnapPoses(
moveable,
poses.vertical,
poses.horizontal,
poses.verticalNames.map(name => getStringDirection(name)),
poses.horizontalNames.map(name => getStringDirection(name)),
customSnapVerticalThreshold,
customSnapHorizontalThreshold,
);
const horizontalDirection = getStringDirection(poses.horizontalNames[result.horizontal.index]);
const verticalDirection = getStringDirection(poses.verticalNames[result.vertical.index]);
return {
vertical: {
...result.vertical,
direction: verticalDirection,
},
horizontal: {
...result.horizontal,
direction: horizontalDirection,
},
};
}
export function getNearestSnapGuidelineInfo(
snapInfo: SnapInfo,
) {
const isSnap = snapInfo.isSnap;
if (!isSnap) {
return {
isSnap: false,
offset: 0,
dist: -1,
pos: 0,
guideline: null,
};
}
const posInfo = snapInfo.posInfos[0];
const guidelineInfo = posInfo!.guidelineInfos[0];
const offset = guidelineInfo!.offset;
const dist = guidelineInfo!.dist;
const guideline = guidelineInfo!.guideline;
return {
isSnap,
offset,
dist,
pos: posInfo!.pos,
guideline,
};
}
function checkSnap(
guidelines: SnapGuideline[],
targetType: "horizontal" | "vertical",
targetPoses: number[],
snapThreshold: number,
dirs: string[] = [],
): SnapInfo {
if (!guidelines || !guidelines.length) {
return {
isSnap: false,
index: -1,
direction: "",
posInfos: [],
};
}
const isVertical = targetType === "vertical";
const posType = isVertical ? 0 : 1;
const snapPosInfos = targetPoses.map((targetPos, index) => {
const direction = dirs[index] || "";
const guidelineInfos = guidelines.map(guideline => {
const { pos } = guideline;
const offset = targetPos - pos[posType];
return {
offset,
dist: abs(offset),
guideline,
direction,
};
}).filter(({ guideline, dist }) => {
const { type } = guideline;
if (
type !== targetType
|| dist > snapThreshold
) {
return false;
}
return true;
}).sort(
(a, b) => a.dist - b.dist,
);
return {
pos: targetPos,
index,
guidelineInfos,
direction,
};
}).filter(snapPosInfo => {
return snapPosInfo.guidelineInfos.length > 0;
}).sort((a, b) => {
return a.guidelineInfos[0].dist - b.guidelineInfos[0].dist;
});
const isSnap = snapPosInfos.length > 0;
return {
isSnap,
index: isSnap ? snapPosInfos[0].index : -1,
direction: snapPosInfos[0]?.direction ?? "",
posInfos: snapPosInfos,
};
}
export function getSnapInfosByDirection(
moveable: MoveableManagerInterface<SnappableProps & (ResizableProps | ScalableProps), SnappableState>,
// pos1 pos2 pos3 pos4
poses: number[][],
snapDirection: number[],
customSnapVerticalThreshold: number | undefined,
customSnapHorizontalThreshold: number | undefined,
): { vertical: SnapDirectionInfo; horizontal: SnapDirectionInfo } {
let dirs: number[][] = [];
if (snapDirection[0] && snapDirection[1]) {
dirs = [
snapDirection,
[-snapDirection[0], snapDirection[1]],
[snapDirection[0], -snapDirection[1]],
];
} else if (!snapDirection[0] && !snapDirection[1]) {
[
[-1, -1],
[1, -1],
[1, 1],
[-1, 1],
].forEach((dir, i, arr) => {
const nextDir = (arr[i + 1] || arr[0]);
dirs.push(dir);
dirs.push([
(dir[0] + nextDir[0]) / 2,
(dir[1] + nextDir[1]) / 2,
]);
});
} else {
if (moveable.props.keepRatio) {
dirs.push(
[-1, -1],
[-1, 1],
[1, -1],
[1, 1],
snapDirection,
);
} else {
dirs.push(...getPosesByDirection([
[-1, -1],
[1, -1],
[-1, -1],
[1, 1],
], snapDirection));
if (dirs.length > 1) {
dirs.push([
(dirs[0][0] + dirs[1][0]) / 2,
(dirs[0][1] + dirs[1][1]) / 2,
]);
}
}
}
const nextPoses = dirs.map(dir => getPosByDirection(poses, dir));
const xs = nextPoses.map(pos => pos[0]);
const ys = nextPoses.map(pos => pos[1]);
const result = checkMoveableSnapPoses(
moveable,
xs, ys,
dirs.map(dir => getStringDirection(dir[0])),
dirs.map(dir => getStringDirection(dir[1])),
customSnapVerticalThreshold,
customSnapHorizontalThreshold,
);
const verticalDirection = getStringDirection(dirs.map(dir => dir[0])[result.vertical.index]);
const horizontalDirection = getStringDirection(dirs.map(dir => dir[1])[result.horizontal.index]);
return {
vertical: {
...result.vertical,
direction: verticalDirection,
},
horizontal: {
...result.horizontal,
direction: horizontalDirection,
},
};
}
export function checkSnapBoundPriority(
a: { isBound: boolean, isSnap: boolean, offset: number },
b: { isBound: boolean, isSnap: boolean, offset: number },
) {
const aDist = abs(a.offset);
const bDist = abs(b.offset);
if (a.isBound && b.isBound) {
return bDist - aDist;
} else if (a.isBound) {
return -1;
} else if (b.isBound) {
return 1;
} else if (a.isSnap && b.isSnap) {
return bDist - aDist;
} else if (a.isSnap) {
return -1;
} else if (b.isSnap) {
return 1;
} else if (aDist < TINY_NUM) {
return 1;
} else if (bDist < TINY_NUM) {
return -1;
}
return aDist - bDist;
}
export function getNearOffsetInfo<T extends { offset: number[], isBound: boolean, isSnap: boolean, sign: number[] }>(
offsets: T[],
index: number,
) {
return offsets.slice().sort((a, b) => {
const aSign = a.sign[index];
const bSign = b.sign[index];
const aOffset = a.offset[index];
const bOffset = b.offset[index];
// -1 The positions of a and b do not change.
// 1 The positions of a and b are reversed.
if (!aSign) {
return 1;
} else if (!bSign) {
return -1;
}
return checkSnapBoundPriority(
{ isBound: a.isBound, isSnap: a.isSnap, offset: aOffset },
{ isBound: b.isBound, isSnap: b.isSnap, offset: bOffset },
);
})[0];
}
export function getCheckSnapDirections(
direction: number[],
fixedDirection: number[],
keepRatio: boolean
) {
const directions: number[][][] = [];
// const fixedDirection = [-direction[0], -direction[1]];
if (keepRatio) {
if (abs(fixedDirection[0]) !== 1 || abs(fixedDirection[1]) !== 1) {
directions.push(
[fixedDirection, [-1, -1]],
[fixedDirection, [-1, 1]],
[fixedDirection, [1, -1]],
[fixedDirection, [1, 1]],
);
} else {
directions.push(
[fixedDirection, [direction[0], -direction[1]]],
[fixedDirection, [-direction[0], direction[1]]],
);
}
directions.push([fixedDirection, direction]);
} else {
if ((direction[0] && direction[1]) || (!direction[0] && !direction[1])) {
const endDirection = direction[0] ? direction : [1, 1];
[1, -1].forEach(signX => {
[1, -1].forEach(signY => {
const nextDirection = [signX * endDirection[0], signY * endDirection[1]];
if (
fixedDirection[0] === nextDirection[0]
&& fixedDirection[1] === nextDirection[1]
) {
return;
}
directions.push([fixedDirection, nextDirection]);
});
});
} else if (direction[0]) {
const signs = abs(fixedDirection[0]) === 1 ? [1] : [1, -1];
signs.forEach(sign => {
directions.push(
[
[fixedDirection[0], -1],
[sign * direction[0], -1],
],
[
[fixedDirection[0], 0],
[sign * direction[0], 0],
],
[
[fixedDirection[0], 1],
[sign * direction[0], 1],
]
);
});
} else if (direction[1]) {
const signs = abs(fixedDirection[1]) === 1 ? [1] : [1, -1];
signs.forEach(sign => {
directions.push(
[
[-1, fixedDirection[1]],
[-1, sign * direction[1]],
],
[
[0, fixedDirection[1]],
[0, sign * direction[1]],
],
[
[1, fixedDirection[1]],
[1, sign * direction[1]],
]
);
});
}
}
return directions;
}