import { prefix, getLineStyle, getDirection, getAbsolutePosesByState, triggerEvent, fillParams, fillEndParams, getDirectionViewClassName, fillCSSObject, } from "../utils"; import { convertDimension, invert, multiply, calculate, createIdentityMatrix, ignoreDimension, minus, createWarpMatrix, plus, } from "@scena/matrix"; import { NEARBY_POS } from "../consts"; import { setDragStart, getDragDist, getPosIndexesByDirection, setDefaultTransformIndex, fillTransformStartEvent, resolveTransformEvent, convertTransformFormat, fillOriginalTransform, getTransfromMatrix, } from "../gesto/GestoUtils"; import { WarpableProps, ScalableProps, ResizableProps, Renderer, SnappableProps, SnappableState, OnWarpStart, OnWarp, OnWarpEnd, MoveableManagerInterface, } from "../types"; import { hasClass, dot, getRad } from "@daybrush/utils"; import { renderAllDirections } from "../renderDirections"; import { hasGuidelines } from "./snappable/utils"; import { checkMoveableSnapBounds } from "./snappable/snapBounds"; function getMiddleLinePos(pos1: number[], pos2: number[]) { return pos1.map((pos, i) => dot(pos, pos2[i], 1, 2)); } function getTriangleRad(pos1: number[], pos2: number[], pos3: number[]) { // pos1 Rad const rad1 = getRad(pos1, pos2); const rad2 = getRad(pos1, pos3); const rad = rad2 - rad1; return rad >= 0 ? rad : rad + 2 * Math.PI; } function isValidPos(poses1: number[][], poses2: number[][]) { const rad1 = getTriangleRad(poses1[0], poses1[1], poses1[2]); const rad2 = getTriangleRad(poses2[0], poses2[1], poses2[2]); const pi = Math.PI; if ((rad1 >= pi && rad2 <= pi) || (rad1 <= pi && rad2 >= pi)) { return false; } return true; } /** * @namespace Moveable.Warpable * @description Warpable indicates whether the target can be warped(distorted, bented). */ export default { name: "warpable", ableGroup: "size", props: [ "warpable", "renderDirections", "edge", "displayAroundControls", ] as const, events: [ "warpStart", "warp", "warpEnd", ] as const, viewClassName: getDirectionViewClassName("warpable"), render(moveable: MoveableManagerInterface, React: Renderer): any[] { const { resizable, scalable, warpable, zoom } = moveable.props; if (resizable || scalable || !warpable) { return []; } const { pos1, pos2, pos3, pos4 } = moveable.state; const linePosFrom1 = getMiddleLinePos(pos1, pos2); const linePosFrom2 = getMiddleLinePos(pos2, pos1); const linePosFrom3 = getMiddleLinePos(pos1, pos3); const linePosFrom4 = getMiddleLinePos(pos3, pos1); const linePosTo1 = getMiddleLinePos(pos3, pos4); const linePosTo2 = getMiddleLinePos(pos4, pos3); const linePosTo3 = getMiddleLinePos(pos2, pos4); const linePosTo4 = getMiddleLinePos(pos4, pos2); return [
,
,
,
, ...renderAllDirections(moveable, "warpable", React), ]; }, dragControlCondition(moveable: any, e: any) { if (e.isRequest) { return false; } const target = e.inputEvent.target; return hasClass(target, prefix("direction")) && hasClass(target, prefix("warpable")); }, dragControlStart( moveable: MoveableManagerInterface, e: any, ) { const { datas, inputEvent } = e; const { target } = moveable.props; const { target: inputTarget } = inputEvent; const direction = getDirection(inputTarget, datas); if (!direction || !target) { return false; } const state = moveable.state; const { transformOrigin, is3d, targetTransform, targetMatrix, width, height, left, top, } = state; datas.datas = {}; datas.targetTransform = targetTransform; datas.warpTargetMatrix = is3d ? targetMatrix : convertDimension(targetMatrix, 3, 4); datas.targetInverseMatrix = ignoreDimension(invert(datas.warpTargetMatrix, 4), 3, 4); datas.direction = direction; datas.left = left; datas.top = top; datas.poses = [ [0, 0], [width, 0], [0, height], [width, height], ].map(p => minus(p, transformOrigin)); datas.nextPoses = datas.poses.map(([x, y]: number[]) => calculate(datas.warpTargetMatrix, [x, y, 0, 1], 4)); datas.startValue = createIdentityMatrix(4); datas.prevMatrix = createIdentityMatrix(4); datas.absolutePoses = getAbsolutePosesByState(state); datas.posIndexes = getPosIndexesByDirection(direction); setDragStart(moveable, e); setDefaultTransformIndex(moveable, e, "matrix3d"); state.snapRenderInfo = { request: e.isRequest, direction, }; const params = fillParams(moveable, e, { set: (matrix: number[]) => { datas.startValue = matrix; }, ...fillTransformStartEvent(moveable, e), }); const result = triggerEvent(moveable, "onWarpStart", params); if (result !== false) { datas.isWarp = true; } return datas.isWarp; }, dragControl( moveable: MoveableManagerInterface, e: any, ) { const { datas, isRequest } = e; let { distX, distY } = e; const { targetInverseMatrix, prevMatrix, isWarp, startValue, poses, posIndexes, absolutePoses, } = datas; if (!isWarp) { return false; } resolveTransformEvent(moveable, e, "matrix3d"); if (hasGuidelines(moveable, "warpable")) { const selectedPoses: number[][] = posIndexes.map((index: number) => absolutePoses[index]); if (selectedPoses.length > 1) { selectedPoses.push([ (selectedPoses[0][0] + selectedPoses[1][0]) / 2, (selectedPoses[0][1] + selectedPoses[1][1]) / 2, ]); } const { horizontal: horizontalSnapInfo, vertical: verticalSnapInfo, } = checkMoveableSnapBounds( moveable, isRequest, { horizontal: selectedPoses.map(pos => pos[1] + distY), vertical: selectedPoses.map(pos => pos[0] + distX), }, ); distY -= horizontalSnapInfo.offset; distX -= verticalSnapInfo.offset; } const dist = getDragDist({ datas, distX, distY }, true); const nextPoses = datas.nextPoses.slice(); posIndexes.forEach((index: number) => { nextPoses[index] = plus(nextPoses[index], dist); }); if (!NEARBY_POS.every( nearByPoses => isValidPos(nearByPoses.map(i => poses[i]), nearByPoses.map(i => nextPoses[i])), )) { return false; } const h = createWarpMatrix( poses[0], poses[2], poses[1], poses[3], nextPoses[0], nextPoses[2], nextPoses[1], nextPoses[3], ); if (!h.length) { return false; } // B * A * M const afterMatrix = multiply(targetInverseMatrix, h, 4); // B * M * A const matrix = getTransfromMatrix(datas, afterMatrix, true); const delta = multiply(invert(prevMatrix, 4), matrix, 4); datas.prevMatrix = matrix; const totalMatrix = multiply(startValue, matrix, 4); const nextTransform = convertTransformFormat( datas, `matrix3d(${totalMatrix.join(", ")})`, `matrix3d(${matrix.join(", ")})`); fillOriginalTransform(e, nextTransform); triggerEvent(moveable, "onWarp", fillParams(moveable, e, { delta, matrix: totalMatrix, dist: matrix, multiply, transform: nextTransform, ...fillCSSObject({ transform: nextTransform, }, e), })); return true; }, dragControlEnd( moveable: MoveableManagerInterface, e: any, ) { const { datas, isDrag } = e; if (!datas.isWarp) { return false; } datas.isWarp = false; triggerEvent(moveable, "onWarpEnd", fillEndParams(moveable, e, {})); return isDrag; }, }; /** * Whether or not target can be warped. (default: false) * @name Moveable.Warpable#warpable * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body); * * moveable.warpable = true; */ /** * Set directions to show the control box. (default: ["n", "nw", "ne", "s", "se", "sw", "e", "w"]) * @name Moveable.Warpable#renderDirections * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { * warpable: true, * renderDirections: ["n", "nw", "ne", "s", "se", "sw", "e", "w"], * }); * * moveable.renderDirections = ["nw", "ne", "sw", "se"]; */ /** * When the warp starts, the warpStart event is called. * @memberof Moveable.Warpable * @event warpStart * @param {Moveable.Warpable.OnWarpStart} - Parameters for the warpStart event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { warpable: true }); * moveable.on("warpStart", ({ target }) => { * console.log(target); * }); */ /** * When warping, the warp event is called. * @memberof Moveable.Warpable * @event warp * @param {Moveable.Warpable.OnWarp} - Parameters for the warp event * @example * import Moveable from "moveable"; * let matrix = [ * 1, 0, 0, 0, * 0, 1, 0, 0, * 0, 0, 1, 0, * 0, 0, 0, 1, * ]; * const moveable = new Moveable(document.body, { warpable: true }); * moveable.on("warp", ({ target, transform, delta, multiply }) => { * // target.style.transform = transform; * matrix = multiply(matrix, delta); * target.style.transform = `matrix3d(${matrix.join(",")})`; * }); */ /** * When the warp finishes, the warpEnd event is called. * @memberof Moveable.Warpable * @event warpEnd * @param {Moveable.Warpable.OnWarpEnd} - Parameters for the warpEnd event * @example * import Moveable from "moveable"; * * const moveable = new Moveable(document.body, { warpable: true }); * moveable.on("warpEnd", ({ target, isDrag }) => { * console.log(target, isDrag); * }); */