PhotoShop中的自由变换UI实现

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即是缩放比例)

转换为数学问题就是:

  • 由图,已知 B ( r e c t X , r e c t Y ) B D = r e c t W B F = r e c t H M ( X m , Y m ) P ( X p , Y p )
  • B A C = r e c t A B ( r e c t X , r e c t Y ) B D = r e c t W B F = r e c t H

求解过程

1. 求A(Xa, Ya)

已知矩形顶点和宽高,求对角线交点

  • X a = r e c t X + r e c t W / 2
  • Y a = r e c t Y + r e c t H / 2

2.求rectA(∠BAC)

有三点ABC,求∠BAC,有公式:

c o s A = A B 2 + A B 2 B C 2 2 A B A C

所以:

  • A B 2 = ( X a r e c t X ) 2 + ( Y a r e c t Y ) 2
  • A M 2 = ( X a X m ) 2 + ( Y a Y m ) 2
  • B M 2 = ( X m r e c t X ) 2 + ( Y m r e c t Y ) 2
  • c o s A = ( A B 2 + A B 2 B M 2 ) / ( 2 A B 2 A M 2 )
  • r e c t A = a c o s ( c o s A ) 180 / π

判断象限

因为cos无法判断计算出来的角是否大于180度,所以需要根据鼠标位置判断角度
这里写图片描述
如图, B A C 1 = 150 ° B A C 2 = 210 ° ,但根据夹角公式算出来的cos是一样的,都是 3 / 2

判断的方式实际上也很简单,根据M的坐标判断M在直线AB上面还是下面即可

  • l A B : y = r e c t H r e c t W ( x r e c t X ) + r e c t Y
  • M ( X m , Y m ) 代入方程即可
if ((rectH / rectW)*(Xm - rectX) + rectY < Ym) {
    rectA = 360 - rectA
}

3. 求C(Xc, Yc)

( x , y ) 围绕点 ( x 0 , y 0 ) 旋转 α 度,有公式:

x = ( x x 0 ) c o s α ( y y 0 ) s i n α + x 0 y = ( x x 0 ) s i n α + ( y y 0 ) c o s α + y 0

所以:

  • A = r e c t A π / 180
  • X c = ( r e c t X X a ) c o s A ( r e c t Y Y a ) s i n A + X a
  • Y c = ( r e c t X X a ) s i n A + ( r e c t Y Y a ) c o s A + Y a

4. 求G(Xg, Yg)

求直线 l 1 : y = k 1 x + b 1 与直线 l 2 : y = k 2 x + b 2 的交点 G ( X g , Y g ) ,有公式:

X g = b 2 b 1 k 1 k 2 Y g = k 1 X p + b 1

所以:

  • k 1 = ( Y a Y c ) / ( X a X c )
  • b 1 = Y c k 1 X c
  • k 2 = 1 / k 1
  • b 2 = Y p k 2 X p
  • X g = ( b 2 b 1 ) / ( k 1 k 2 )
  • Y g = k 1 X g + b 1

5. 计算缩放比例t

因为

t = C G C H = C G 2 A C = X g X c 2 ( X a X c )

所以

  • t = ( X p X c ) / ( X a X c ) / 2

6. 计算结果

  • r e c t X = t ( r e c t X X c ) + X c
  • r e c t Y = t ( r e c t Y Y c ) + Y c
  • r e c t W = t r e c t W
  • r e c t H = t r e c t H

代码实现

这是在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()
            }
        }
    }

}

猜你喜欢

转载自blog.csdn.net/aa13058219642/article/details/80677218