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<ResizableProps & ScalableProps & WarpableProps>, 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 [
<div className={prefix("line")}
key="middeLine1" style={getLineStyle(linePosFrom1, linePosTo1, zoom)}></div>,
<div className={prefix("line")}
key="middeLine2" style={getLineStyle(linePosFrom2, linePosTo2, zoom)}></div>,
<div className={prefix("line")}
key="middeLine3" style={getLineStyle(linePosFrom3, linePosTo3, zoom)}></div>,
<div className={prefix("line")}
key="middeLine4" style={getLineStyle(linePosFrom4, linePosTo4, zoom)}></div>,
...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<WarpableProps, SnappableState>,
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<OnWarpStart>(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<WarpableProps & SnappableProps, SnappableState>,
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<OnWarp>(moveable, e, {
delta,
matrix: totalMatrix,
dist: matrix,
multiply,
transform: nextTransform,
...fillCSSObject({
transform: nextTransform,
}, e),
}));
return true;
},
dragControlEnd(
moveable: MoveableManagerInterface<WarpableProps>,
e: any,
) {
const { datas, isDrag } = e;
if (!datas.isWarp) {
return false;
}
datas.isWarp = false;
triggerEvent(moveable, "onWarpEnd", fillEndParams<OnWarpEnd>(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);
* });
*/