PhotoShop中的自由变换UI
如上图,在ps的自由变换中常见的操作是,黑色矩形经过旋转变为绿色矩形,再缩放变为蓝色矩形
数学建模求解
一个矩形的变换由以下参数记录坐上角坐标(X,Y),矩形宽高(W,H),矩形旋转角度(A)。把上图的变换进行数学建模,得到下面的图:
其中:
- BDEF为要变换的矩形基准,点A是对角线的交点,B(rectX, rectY),BD=rectW,BF=rectH,∠BAM=rectA ;B’D’E’F’为缩放后的矩形基准
- M(Xm, Ym)点是鼠标拖动B点围绕A点旋转rectA 度后的位置,连接AM交圆A于点C(A即旋转中心,C即旋转基准点)
- P(Xp, Yp)为拖拽时的鼠标坐标;过P作AM的垂线,垂足为G(G即缩放基准点,CG比CH即是缩放比例)
转换为数学问题就是:
- 由图,已知 , , , , 。
- 求 、 、 、
求解过程
1. 求A(Xa, Ya)
已知矩形顶点和宽高,求对角线交点
2.求rectA(∠BAC)
有三点ABC,求∠BAC,有公式:
所以:
- = +
- = +
- = +
判断象限
因为cos无法判断计算出来的角是否大于180度,所以需要根据鼠标位置判断角度
如图,
,
,但根据夹角公式算出来的cos是一样的,都是
判断的方式实际上也很简单,根据M的坐标判断M在直线AB上面还是下面即可
- 把 代入方程即可
if ((rectH / rectW)*(Xm - rectX) + rectY < Ym) {
rectA = 360 - rectA
}
3. 求C(Xc, Yc)
点
围绕点
旋转
度,有公式:
所以:
4. 求G(Xg, Yg)
求直线
与直线
的交点
,有公式:
所以:
5. 计算缩放比例t
因为
所以
6. 计算结果
代码实现
这是在QT qml界面实现的代码
import QtQuick 2.1
Item {
id: item
anchors.fill: parent
rotation: 0
property real widthScale: 1.0
property real heightScale: 1.0
property real aspectRatio: 0.0
property int handleSize: 10
property int borderSize: 2
property alias uiRectangle: uiRectangle
property color uiColor: Qt.rgba(255, 255, 255, 1)
property color handleColor: Qt.rgba(0, 255, 0, 0.5)
//property color handleColor: 'transparent'
property real rectX: 0
property real rectY: 0
property real rectW: 0
property real rectH: 0
property real rectA: 0
//回调函数通知上层根据新的属性变换图片
signal rectChanged(X, Y, W, H, A)
//更新UI
function updateRect() {
rectChanged(rectX, rectY, rectW, rectH, rectA)
uiRectangle.x = rectX
uiRectangle.y = rectY
uiRectangle.width = rectW
uiRectangle.height = rectH
uiRectangle.rotation = rectA
//console.log("updateRect", rectX, rectY, rectW, rectH, rectA)
}
function transformPointX(x, y, A) {
return x * Math.cos(A) - y * Math.sin(A) + rectX + rectW / 2
}
function transformPointY(x, y, A) {
return x * Math.sin(A) + y * Math.cos(A) + rectY + rectH / 2
}
//更新控制点坐标
function updateHandle() {
var centerX = rectW / 2
var centerY = rectH / 2
var hs2 = handleSize / 2
var A = rectA * Math.PI / 180
topLeft_ScaleHandle.x = transformPointX(-centerX + hs2, -centerY + hs2, A) - hs2
topLeft_ScaleHandle.y = transformPointY(-centerX + hs2, -centerY + hs2, A) - hs2
topRight_ScaleHandle.x = transformPointX(centerX - hs2, -centerY + hs2, A) - hs2
topRight_ScaleHandle.y = transformPointY(centerX - hs2, -centerY + hs2, A) - hs2
bottomLeft_ScaleHandle.x = transformPointX(-centerX + hs2, centerY - hs2, A) - hs2
bottomLeft_ScaleHandle.y = transformPointY(-centerX + hs2, centerY - hs2, A) - hs2
bottomRight_ScaleHandle.x = transformPointX(centerX - hs2, centerY - hs2, A) - hs2
bottomRight_ScaleHandle.y = transformPointY(centerX - hs2, centerY - hs2, A) - hs2
topLeft_RotateHandle.x = transformPointX(-centerX - handleSize, -centerY - handleSize, A) - handleSize
topLeft_RotateHandle.y = transformPointY(-centerX - handleSize, -centerY - handleSize, A) - handleSize
topRight_RotateHandle.x = transformPointX(centerX + handleSize, -centerY - handleSize, A) - handleSize
topRight_RotateHandle.y = transformPointY(centerX + handleSize, -centerY - handleSize, A) - handleSize
bottomLeft_RotateHandle.x = transformPointX(-centerX - handleSize, centerY + handleSize, A) - handleSize
bottomLeft_RotateHandle.y = transformPointY(-centerX - handleSize, centerY + handleSize, A) - handleSize
bottomRight_RotateHandle.x = transformPointX(centerX + handleSize, centerY + handleSize, A) - handleSize
bottomRight_RotateHandle.y = transformPointY(centerX + handleSize, centerY + handleSize, A) - handleSize
}
//初始化数据
function setHandles(X, Y, W, H, A) {
rectX = X
rectY = Y
rectW = W
rectH = H
rectA = A
updateRect()
updateHandle()
}
//鼠标拖拽旋转,计算图片旋转角度
function getAngle(mx, my) {
var ox = rectX + rectW / 2
var oy = rectY + rectH / 2
var ax = rectX
var ay = rectY
var bx = mx
var by = my
// cosA = (AB^2+AC^2-BC^2)/(2*AB*AC) 三点求夹角
var AB2 = (ax - ox) * (ax - ox) + (ay - oy) * (ay - oy)
var AC2 = (ox - bx) * (ox - bx) + (oy - by) * (oy - by)
var BC2 = (ax - bx) * (ax - bx) + (ay - by) * (ay - by)
var cosA = (AB2 + AC2 - BC2) / (2 * Math.sqrt(AB2) * Math.sqrt(AC2))
var A = Math.acos(cosA) * 180 / Math.PI
// y = k(x-x0)+y0 判断在直线上面还是下面
if ((bx - rectX) / aspectRatio + rectY < by) {
A = 360 - A
}
return A
}
//鼠标拖拽缩放,计算图片缩放比例
function setScale(Xm, Ym, angle) {
//计算缩放基准点坐标C(旋转中心为A, B围绕A点旋转angle度到C)
var A = angle * Math.PI / 180
var Xa = rectX + rectW / 2
var Ya = rectY + rectH / 2
var Xc = (rectX - Xa) * Math.cos(A) - (rectY - Ya) * Math.sin(A) + Xa
var Yc = (rectX - Xa) * Math.sin(A) + (rectY - Ya) * Math.cos(A) + Ya
//鼠标坐标修正(过鼠标坐标对对角线作垂线,计算垂足P)
var k1 = (Ya - Yc) / (Xa - Xc)
var b1 = Yc - k1 * Xc
var k2 = -1 / k1
var b2 = Ym - k2 * Xm
var Xp = (b2 - b1) / (k1 - k2)
var Yp = k1 * Xp + b1
//计算缩放比例
var t = (Xp - Xc) / (Xa - Xc) / 2
//console.log("setScale", Xc, Yc, Xp ,Yp, A, t)
//应用到变换
rectX = t * (rectX - Xc) + Xc
rectY = t * (rectY - Yc) + Yc
rectW = t * rectW
rectH = t * rectH
}
function image(path) {
return "file:///" + RESOURCE_PATH + "/icon/" + path
}
Rectangle {
id: uiRectangle
color: 'transparent' //Qt.rgba(255, 0, 0, 0.5)
border.width: borderSize
border.color: uiColor
rotation: 0
MouseArea {
id: positionMouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeAllCursor
drag.target: uiRectangle
onPositionChanged: {
rectX = uiRectangle.x
rectY = uiRectangle.y
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
Image {
source: image("rotate_arrow_tl.png")
x: handleSize - this.width
y: handleSize - this.height
}
Image {
source: image("rotate_arrow_tr.png")
x: uiRectangle.width - handleSize
y: handleSize - this.height
}
Image {
source: image("rotate_arrow_bl.png")
x: handleSize - this.width
y: uiRectangle.height - handleSize
}
Image {
source: image("rotate_arrow_br.png")
x: uiRectangle.width - handleSize
y: uiRectangle.height - handleSize
}
Rectangle {
color: uiColor
x: 0
y: 0
width: handleSize
height: handleSize
}
Rectangle {
color: uiColor
x: uiRectangle.width - handleSize
y: 0
width: handleSize
height: handleSize
}
Rectangle {
color: uiColor
x: 0
y: uiRectangle.height - handleSize
width: handleSize
height: handleSize
}
Rectangle {
color: uiColor
x: uiRectangle.width - handleSize
y: uiRectangle.height - handleSize
width: handleSize
height: handleSize
}
}
Rectangle {
id: topLeft_RotateHandle
color: handleColor
width: handleSize * 2
height: handleSize * 2
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeBDiagCursor
onPositionChanged: {
var A = getAngle(mouseX + parent.x, mouseY + parent.y)
rectA = A
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
Rectangle {
id: topRight_RotateHandle
color: handleColor
width: handleSize * 2
height: handleSize * 2
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeFDiagCursor
onPositionChanged: {
var A = getAngle(mouseX + parent.x, mouseY + parent.y)
A -= 2 * Math.atan2(rectW, rectH) * 180 / Math.PI
rectA = A
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
Rectangle {
id: bottomLeft_RotateHandle
color: handleColor
width: handleSize * 2
height: handleSize * 2
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeFDiagCursor
onPositionChanged: {
var A = getAngle(mouseX + parent.x, mouseY + parent.y)
A -= 2 * Math.atan2(rectW, rectH) * 180 / Math.PI + 180
rectA = A
updateRect()
}
onReleased: {
updateHandle()
}
}
}
Rectangle {
id: bottomRight_RotateHandle
color: handleColor
width: handleSize * 2
height: handleSize * 2
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeBDiagCursor
onPositionChanged: {
var A = getAngle(mouseX + parent.x, mouseY + parent.y)
A -= 180
rectA = A
updateRect()
}
onReleased: {
updateHandle()
}
}
}
Rectangle {
id: topLeft_ScaleHandle
color: handleColor
width: handleSize
height: handleSize
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeFDiagCursor
onPositionChanged: {
setScale(mouseX + parent.x, mouseY + parent.y, rectA + 180)
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
Rectangle {
id: topRight_ScaleHandle
color: handleColor
width: handleSize
height: handleSize
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeBDiagCursor
onPositionChanged: {
var A = 180 + rectA + (2 * Math.atan2(rectW, rectH) * 180 / Math.PI)
setScale(mouseX + parent.x, mouseY + parent.y, A)
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
Rectangle {
id: bottomLeft_ScaleHandle
color: handleColor
width: handleSize
height: handleSize
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeBDiagCursor
onPositionChanged: {
var A = rectA + (2 * Math.atan2(rectW, rectH) * 180 / Math.PI)
setScale(mouseX + parent.x, mouseY + parent.y, A)
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
Rectangle {
id: bottomRight_ScaleHandle
color: handleColor
width: handleSize
height: handleSize
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton
cursorShape: Qt.SizeFDiagCursor
onPositionChanged: {
setScale(mouseX + parent.x, mouseY + parent.y, rectA)
updateRect()
}
onReleased: {
updateRect()
updateHandle()
}
}
}
}