UNPKG

react-moveable

Version:

A React Component that create Moveable, Draggable, Resizable, Scalable, Rotatable, Warpable, Pinchable, Groupable.

608 lines (555 loc) 18.2 kB
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, }; }