import {
triggerEvent, multiply2,
fillParams, fillEndParams, getAbsolutePosesByState,
catchEvent, getOffsetSizeDist, getDirectionCondition,
getDirectionViewClassName, getTotalDirection, sign, countEach, abs,
} from "../utils";
import { MIN_SCALE } from "../consts";
import {
setDragStart, resolveTransformEvent,
convertTransformFormat,
getScaleDist,
fillTransformStartEvent,
fillTransformEvent,
setDefaultTransformIndex,
getTranslateFixedPosition,
} from "../gesto/GestoUtils";
import { getRenderDirections } from "../renderDirections";
import {
ScalableProps, OnScaleGroup, OnScaleGroupEnd,
OnScaleGroupStart, DraggableProps, OnDragStart,
SnappableState, GroupableProps, OnScaleStart,
OnScale, OnScaleEnd, MoveableManagerInterface, MoveableGroupInterface,
OnBeforeScaleGroup,
OnBeforeScale,
} from "../types";
import {
fillChildEvents,
startChildDist,
triggerChildAbles,
} from "../groupUtils";
import Draggable from "./Draggable";
import { calculate, createRotateMatrix, plus, minus } from "@scena/matrix";
import CustomGesto from "../gesto/CustomGesto";
import { checkSnapScale } from "./Snappable";
import {
isArray, IObject, getDist,
throttle,
calculateBoundSize,
} from "@daybrush/utils";
import { getFixedDirectionInfo } from "../utils/getFixedDirection";
const directionCondition = getDirectionCondition("scalable");
/**
* @namespace Scalable
* @memberof Moveable
* @description Scalable indicates whether the target's x and y can be scale of transform.
*/
export default {
name: "scalable",
ableGroup: "size",
canPinch: true,
props: [
"scalable",
"throttleScale",
"renderDirections",
"keepRatio",
"edge",
"displayAroundControls",
] as const,
events: [
"scaleStart",
"beforeScale",
"scale",
"scaleEnd",
"scaleGroupStart",
"beforeScaleGroup",
"scaleGroup",
"scaleGroupEnd",
] as const,
render: getRenderDirections("scalable"),
dragControlCondition: directionCondition,
viewClassName: getDirectionViewClassName("scalable"),
dragControlStart(
moveable: MoveableManagerInterface<ScalableProps & DraggableProps, SnappableState>,
e: any) {
const { datas, isPinch, inputEvent, parentDirection } = e;
const direction = getTotalDirection(
parentDirection,
isPinch,
inputEvent,
datas,
);
const {
width,
height,
targetTransform,
target,
pos1,
pos2,
pos4,
} = moveable.state;
if (!direction || !target) {
return false;
}
if (!isPinch) {
setDragStart(moveable, e);
}
datas.datas = {};
datas.transform = targetTransform;
datas.prevDist = [1, 1];
datas.direction = direction;
datas.startOffsetWidth = width;
datas.startOffsetHeight = height;
datas.startValue = [1, 1];
// const scaleWidth = getDist(pos1, pos2);
// const scaleHeight = getDist(pos2, pos4);
const isWidth = (!direction[0] && !direction[1]) || direction[0] || !direction[1];
// datas.scaleWidth = scaleWidth;
// datas.scaleHeight = scaleHeight;
// datas.scaleXRatio = scaleWidth / width;
// datas.scaleYRatio = scaleHeight / height;
setDefaultTransformIndex(moveable, e, "scale");
datas.isWidth = isWidth;
function setRatio(ratio: number) {
datas.ratio = ratio && isFinite(ratio) ? ratio : 0;
}
datas.startPositions = getAbsolutePosesByState(moveable.state);
function setFixedDirection(fixedDirection: number[]) {
const result = getFixedDirectionInfo(datas.startPositions, fixedDirection);
datas.fixedDirection = result.fixedDirection;
datas.fixedPosition = result.fixedPosition;
datas.fixedOffset = result.fixedOffset;
}
datas.setFixedDirection = setFixedDirection;
setRatio(getDist(pos1, pos2) / getDist(pos2, pos4));
setFixedDirection([-direction[0], -direction[1]]);
const setMinScaleSize = (min: number[]) => {
datas.minScaleSize = min;
};
const setMaxScaleSize = (max: number[]) => {
datas.maxScaleSize = max;
};
// const setMinScale = (min: number[]) => {
// };
// const setMaxScale = (max: number[]) => {
// };
setMinScaleSize([-Infinity, -Infinity]);
setMaxScaleSize([Infinity, Infinity]);
const params = fillParams<OnScaleStart>(moveable, e, {
direction,
set: (scale: number[]) => {
datas.startValue = scale;
},
setRatio,
setFixedDirection,
setMinScaleSize,
setMaxScaleSize,
...fillTransformStartEvent(moveable, e),
dragStart: Draggable.dragStart(
moveable,
new CustomGesto().dragStart([0, 0], e),
) as OnDragStart,
});
const result = triggerEvent(moveable, "onScaleStart", params);
datas.startFixedDirection = datas.fixedDirection;
if (result !== false) {
datas.isScale = true;
moveable.state.snapRenderInfo = {
request: e.isRequest,
direction,
};
}
return datas.isScale ? params : false;
},
dragControl(
moveable: MoveableManagerInterface<ScalableProps & DraggableProps & GroupableProps, SnappableState>,
e: any) {
resolveTransformEvent(moveable, e, "scale");
const {
datas,
parentKeepRatio,
parentFlag,
isPinch,
dragClient,
isRequest,
useSnap,
resolveMatrix,
} = e;
const {
prevDist,
direction,
startOffsetWidth,
startOffsetHeight,
isScale,
startValue,
isWidth,
ratio,
} = datas;
if (!isScale) {
return false;
}
const props = moveable.props;
const {
throttleScale,
parentMoveable,
} = props;
let sizeDirection = direction;
if (!direction[0] && !direction[1]) {
sizeDirection = [1, 1];
}
const keepRatio = (ratio && (parentKeepRatio != null ? parentKeepRatio : props.keepRatio)) || false;
const state = moveable.state;
const tempScaleValue = [
startValue[0],
startValue[1],
];
function getNextScale() {
const {
distWidth,
distHeight,
} = getOffsetSizeDist(sizeDirection, keepRatio, datas, e);
const distX = startOffsetWidth ? (startOffsetWidth + distWidth) / startOffsetWidth : 1;
const distY = startOffsetHeight ? (startOffsetHeight + distHeight) / startOffsetHeight : 1;
if (!startValue[0]) {
tempScaleValue[0] = distWidth / startOffsetWidth;
}
if (!startValue[1]) {
tempScaleValue[1] = distHeight / startOffsetHeight;
}
let scaleX = (sizeDirection[0] || keepRatio ? distX : 1) * tempScaleValue[0];
let scaleY = (sizeDirection[1] || keepRatio ? distY : 1) * tempScaleValue[1];
if (scaleX === 0) {
scaleX = sign(prevDist[0]) * MIN_SCALE;
}
if (scaleY === 0) {
scaleY = sign(prevDist[1]) * MIN_SCALE;
}
return [scaleX, scaleY];
}
let scale = getNextScale();
if (!isPinch && moveable.props.groupable) {
const snapRenderInfo = state.snapRenderInfo || {};
const stateDirection = snapRenderInfo.direction;
if (isArray(stateDirection) && (stateDirection[0] || stateDirection[1])) {
state.snapRenderInfo = { direction, request: e.isRequest };
}
}
triggerEvent(moveable, "onBeforeScale", fillParams<OnBeforeScale>(moveable, e, {
scale,
setFixedDirection(nextFixedDirection: number[]) {
datas.setFixedDirection(nextFixedDirection);
scale = getNextScale();
return scale;
},
startFixedDirection: datas.startFixedDirection,
setScale(nextScale: number[]) {
scale = nextScale;
},
}, true));
let dist = [
scale[0] / tempScaleValue[0],
scale[1] / tempScaleValue[1],
];
let fixedPosition = dragClient;
let snapDist = [0, 0];
const distSign = sign(dist[0] * dist[1]);
const isSelfPinch = !dragClient && !parentFlag && isPinch;
if (isSelfPinch || resolveMatrix) {
fixedPosition = getTranslateFixedPosition(
moveable,
datas.targetAllTransform,
[0, 0],
[0, 0],
datas,
);
} else if (!dragClient) {
fixedPosition = datas.fixedPosition;
}
if (!isPinch) {
snapDist = checkSnapScale(
moveable,
dist,
direction,
!useSnap && isRequest,
datas,
);
}
if (keepRatio) {
if (sizeDirection[0] && sizeDirection[1] && snapDist[0] && snapDist[1]) {
if (Math.abs(snapDist[0] * startOffsetWidth) > Math.abs(snapDist[1] * startOffsetHeight)) {
snapDist[1] = 0;
} else {
snapDist[0] = 0;
}
}
const isNoSnap = !snapDist[0] && !snapDist[1];
if (isNoSnap) {
// throttle scale value (not absolute scale size)
if (isWidth) {
dist[0] = throttle(dist[0] * tempScaleValue[0], throttleScale!) / tempScaleValue[0];
} else {
dist[1] = throttle(dist[1] * tempScaleValue[1], throttleScale!) / tempScaleValue[1];
}
}
if (
(sizeDirection[0] && !sizeDirection[1])
|| (snapDist[0] && !snapDist[1])
|| (isNoSnap && isWidth)
) {
dist[0] += snapDist[0];
const snapHeight = startOffsetWidth * dist[0] * tempScaleValue[0] / ratio;
dist[1] = sign(distSign * dist[0]) * abs(snapHeight / startOffsetHeight / tempScaleValue[1]);
} else if (
(!sizeDirection[0] && sizeDirection[1])
|| (!snapDist[0] && snapDist[1])
|| (isNoSnap && !isWidth)
) {
dist[1] += snapDist[1];
const snapWidth = startOffsetHeight * dist[1] * tempScaleValue[1] * ratio;
dist[0] = sign(distSign * dist[1]) * abs(snapWidth / startOffsetWidth / tempScaleValue[0]);
}
} else {
dist[0] += snapDist[0];
dist[1] += snapDist[1];
if (!snapDist[0]) {
dist[0] = throttle(dist[0] * tempScaleValue[0], throttleScale!) / tempScaleValue[0];
}
if (!snapDist[1]) {
dist[1] = throttle(dist[1] * tempScaleValue[1], throttleScale!) / tempScaleValue[1];
}
}
if (dist[0] === 0) {
dist[0] = sign(prevDist[0]) * MIN_SCALE;
}
if (dist[1] === 0) {
dist[1] = sign(prevDist[1]) * MIN_SCALE;
}
scale = multiply2(dist, [tempScaleValue[0], tempScaleValue[1]]);
const startOffsetSize = [
startOffsetWidth,
startOffsetHeight,
];
let scaleSize = [
startOffsetWidth * scale[0],
startOffsetHeight * scale[1],
];
scaleSize = calculateBoundSize(
scaleSize,
datas.minScaleSize,
datas.maxScaleSize,
keepRatio ? ratio : false,
);
// if (keepRatio && (isGroup || keepRatioFinally)) {
// if (isWidth) {
// boundingHeight = boundingWidth / ratio;
// } else {
// boundingWidth = boundingHeight * ratio;
// }
// }
scale = countEach(2, i => {
return startOffsetSize[i] ? scaleSize[i] / startOffsetSize[i] : scaleSize[i];
});
dist = countEach(2, i => {
return scale[i] / tempScaleValue[i];
});
const delta = countEach(2, i => prevDist[i] ? dist[i] / prevDist[i] : dist[i]);
const distText = `scale(${dist.join(", ")})`;
const scaleText = `scale(${scale.join(", ")})`;
const nextTransform = convertTransformFormat(
datas, scaleText, distText);
const isZeroScale = !startValue[0] || !startValue[1];
const inverseDist = getScaleDist(
moveable,
isZeroScale ? scaleText : distText,
datas.fixedDirection,
fixedPosition,
datas.fixedOffset,
datas,
isZeroScale,
);
const inverseDelta = isSelfPinch ? inverseDist : minus(inverseDist, datas.prevInverseDist || [0, 0]);
datas.prevDist = dist;
datas.prevInverseDist = inverseDist;
if (
scale[0] === prevDist[0] && scale[1] === prevDist[1]
&& inverseDelta.every(num => !num)
&& !parentMoveable
&& !isSelfPinch
) {
return false;
}
const params = fillParams<OnScale>(moveable, e, {
offsetWidth: startOffsetWidth,
offsetHeight: startOffsetHeight,
direction,
scale,
dist,
delta,
isPinch: !!isPinch,
...fillTransformEvent(
moveable,
nextTransform,
inverseDelta,
isPinch,
e,
),
});
triggerEvent(moveable, "onScale", params);
return params;
},
dragControlEnd(moveable: MoveableManagerInterface<ScalableProps>, e: any) {
const { datas } = e;
if (!datas.isScale) {
return false;
}
datas.isScale = false;
const scaleEndParam = fillEndParams<OnScaleEnd>(moveable, e, {});
triggerEvent(moveable, "onScaleEnd", scaleEndParam);
return scaleEndParam;
},
dragGroupControlCondition: directionCondition,
dragGroupControlStart(moveable: MoveableGroupInterface<any, any>, e: any) {
const { datas } = e;
const params = this.dragControlStart(moveable, e);
if (!params) {
return false;
}
const originalEvents = fillChildEvents(moveable, "resizable", e);
datas.moveableScale = moveable.scale;
const events = triggerChildAbles(
moveable,
this,
"dragControlStart",
e,
(child, ev) => {
return startChildDist(moveable, child, datas, ev);
},
);
const setFixedDirection = (fixedDirection: number[]) => {
params.setFixedDirection(fixedDirection);
events.forEach((ev, i) => {
ev.setFixedDirection(fixedDirection);
startChildDist(moveable, ev.moveable, datas, originalEvents[i]);
});
};
datas.setFixedDirection = setFixedDirection;
const nextParams: OnScaleGroupStart = {
...params,
targets: moveable.props.targets!,
events,
setFixedDirection,
};
const result = triggerEvent(moveable, "onScaleGroupStart", nextParams);
datas.isScale = result !== false;
return datas.isScale ? nextParams : false;
},
dragGroupControl(moveable: MoveableGroupInterface<any, any>, e: any) {
const { datas } = e;
if (!datas.isScale) {
return;
}
catchEvent(moveable, "onBeforeScale", parentEvent => {
triggerEvent(moveable, "onBeforeScaleGroup", fillParams<OnBeforeScaleGroup>(moveable, e, {
...parentEvent,
targets: moveable.props.targets!,
}, true));
});
const params = this.dragControl(moveable, e);
if (!params) {
return;
}
const { dist } = params;
const moveableScale = datas.moveableScale;
moveable.scale = [
dist[0] * moveableScale[0],
dist[1] * moveableScale[1],
];
const keepRatio = moveable.props.keepRatio;
const fixedPosition = datas.fixedPosition;
const events = triggerChildAbles(
moveable,
this,
"dragControl",
e,
(_, ev) => {
const [clientX, clientY] = calculate(
createRotateMatrix(moveable.rotation / 180 * Math.PI, 3),
[
ev.datas.originalX * dist[0],
ev.datas.originalY * dist[1],
1,
],
3,
);
return {
...ev,
parentDist: null,
parentScale: dist,
parentKeepRatio: keepRatio,
// recalculate child fixed position for parent group's dragging.
dragClient: plus(fixedPosition, [clientX, clientY]),
};
},
);
const nextParams: OnScaleGroup = {
targets: moveable.props.targets!,
events,
...params,
};
triggerEvent(moveable, "onScaleGroup", nextParams);
return nextParams;
},
dragGroupControlEnd(moveable: MoveableGroupInterface<any, any>, e: any) {
const { isDrag, datas } = e;
if (!datas.isScale) {
return;
}
this.dragControlEnd(moveable, e);
const events = triggerChildAbles(moveable, this, "dragControlEnd", e);
const nextParams = fillEndParams<OnScaleGroupEnd>(moveable, e, {
targets: moveable.props.targets!,
events,
});
triggerEvent(moveable, "onScaleGroupEnd", nextParams);
return isDrag;
},
/**
* @method Moveable.Scalable#request
* @param {Moveable.Scalable.ScalableRequestParam} e - the Scalable's request parameter
* @return {Moveable.Requester} Moveable Requester
* @example
* // Instantly Request (requestStart - request - requestEnd)
* moveable.request("scalable", { deltaWidth: 10, deltaHeight: 10 }, true);
*
* // requestStart
* const requester = moveable.request("scalable");
*
* // request
* requester.request({ deltaWidth: 10, deltaHeight: 10 });
* requester.request({ deltaWidth: 10, deltaHeight: 10 });
* requester.request({ deltaWidth: 10, deltaHeight: 10 });
*
* // requestEnd
* requester.requestEnd();
*/
request() {
const datas = {};
let distWidth = 0;
let distHeight = 0;
let useSnap = false;
return {
isControl: true,
requestStart(e: IObject<any>) {
useSnap = e.useSnap;
return {
datas,
parentDirection: e.direction || [1, 1],
useSnap,
};
},
request(e: IObject<any>) {
distWidth += e.deltaWidth;
distHeight += e.deltaHeight;
return {
datas,
parentDist: [distWidth, distHeight],
parentKeepRatio: e.keepRatio,
useSnap,
};
},
requestEnd() {
return { datas, isDrag: true, useSnap };
},
};
},
};
/**
* Whether or not target can scaled.
*
* @name Moveable.Scalable#scalable
* @default false
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body);
*
* moveable.scalable = true;
*/
/**
* throttle of scaleX, scaleY when scale.
* @name Moveable.Scalable#throttleScale
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body);
*
* moveable.throttleScale = 0.1;
*/
/**
* Set directions to show the control box. (default: ["n", "nw", "ne", "s", "se", "sw", "e", "w"])
* @name Moveable.Scalable#renderDirections
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* scalable: true,
* renderDirections: ["n", "nw", "ne", "s", "se", "sw", "e", "w"],
* });
*
* moveable.renderDirections = ["nw", "ne", "sw", "se"];
*/
/**
* When resize or scale, keeps a ratio of the width, height. (default: false)
* @name Moveable.Scalable#keepRatio
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* scalable: true,
* });
*
* moveable.keepRatio = true;
*/
/**
* When the scale starts, the scaleStart event is called.
* @memberof Moveable.Scalable
* @event scaleStart
* @param {Moveable.Scalable.OnScaleStart} - Parameters for the scaleStart event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, { scalable: true });
* moveable.on("scaleStart", ({ target }) => {
* console.log(target);
* });
*/
/**
* When scaling, `beforeScale` is called before `scale` occurs. In `beforeScale`, you can get and set the pre-value before scaling.
* @memberof Moveable.Scalable
* @event beforeScale
* @param {Moveable.Scalable.OnBeforeScale} - Parameters for the `beforeScale` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, { scalable: true });
* moveable.on("beforeScale", ({ setFixedDirection }) => {
* if (shiftKey) {
* setFixedDirection([0, 0]);
* }
* });
* moveable.on("scale", ({ target, transform, dist }) => {
* target.style.transform = transform;
* });
*/
/**
* When scaling, the `scale` event is called.
* @memberof Moveable.Scalable
* @event scale
* @param {Moveable.Scalable.OnScale} - Parameters for the `scale` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, { scalable: true });
* moveable.on("scale", ({ target, transform, dist }) => {
* target.style.transform = transform;
* });
*/
/**
* When the scale finishes, the `scaleEnd` event is called.
* @memberof Moveable.Scalable
* @event scaleEnd
* @param {Moveable.Scalable.OnScaleEnd} - Parameters for the `scaleEnd` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, { scalable: true });
* moveable.on("scaleEnd", ({ target, isDrag }) => {
* console.log(target, isDrag);
* });
*/
/**
* When the group scale starts, the `scaleGroupStart` event is called.
* @memberof Moveable.Scalable
* @event scaleGroupStart
* @param {Moveable.Scalable.OnScaleGroupStart} - Parameters for the `scaleGroupStart` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* scalable: true
* });
* moveable.on("scaleGroupStart", ({ targets }) => {
* console.log("onScaleGroupStart", targets);
* });
*/
/**
* When the group scale, the `scaleGroup` event is called.
* @memberof Moveable.Scalable
* @event scaleGroup
* @param {Moveable.Scalable.OnScaleGroup} - Parameters for the `scaleGroup` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* scalable: true
* });
* moveable.on("scaleGroup", ({ targets, events }) => {
* console.log("onScaleGroup", targets);
* events.forEach(ev => {
* const target = ev.target;
* // ev.drag is a drag event that occurs when the group scale.
* const left = ev.drag.beforeDist[0];
* const top = ev.drag.beforeDist[1];
* const scaleX = ev.scale[0];
* const scaleY = ev.scale[1];
* });
* });
*/
/**
* When the group scale finishes, the `scaleGroupEnd` event is called.
* @memberof Moveable.Scalable
* @event scaleGroupEnd
* @param {Moveable.Scalable.OnScaleGroupEnd} - Parameters for the `scaleGroupEnd` event
* @example
* import Moveable from "moveable";
*
* const moveable = new Moveable(document.body, {
* target: [].slice.call(document.querySelectorAll(".target")),
* scalable: true
* });
* moveable.on("scaleGroupEnd", ({ targets, isDrag }) => {
* console.log("onScaleGroupEnd", targets, isDrag);
* });
*/