我想要創造一個小人,它可以向四個方向走。我用定時器實現了繪圖的循環執行,並從這個圖片中把各個幀裁切下來並畫出來。
但是,我發現小人的行走動畫是向右的。當小人向左走的時候就非常不自然。我想要在這種時候把圖像翻轉。
於是我嘗試着在QPainter中找到一個flip函數——結果居然沒有!翻轉這樣一個非常常用的功能居然沒有!
方法一
沒有的話,我們就只能自己實現了。我所熟悉的對坐標系進行變形的函數四個:
void shear(qreal sh, qreal sv);
void scale(qreal sx, qreal sy);
void rotate(qreal angle);
void translate(qreal dx, qreal dy)
其中最麻煩的是剪切函數shear
。經過探索,它對於右手系的改變相當於乘上一個矩陣:(使用笛卡爾坐標系而不是默認坐標系)
而對於左手系的改變則是:
這是四個函數中唯一一個可以改變坐標系兩個基底的夾角的函數。因為翻轉需要我們改變整個坐標系的手性,所以這個函數是必不可少的。
對於放縮函數scale
,顯然它對坐標系的改變相當於乘上一個矩陣:
要是sx
可以為負數,我的問題其實就已經解決了。但是這個函數不支持負數。
對於旋轉函數rotate
,它對坐標系的改變相當於乘上一個矩陣:
translate
的作用是改變原點位置,不是對坐標系進行線性變換,所以與矩陣無關。
於是下面就是數學推導。把初始兩個基底向量\((1, 0)\)和\((0, 1)\)變成豎向,放在矩陣里(參考3b1b的線性代數相關視頻):
不妨進行shear(2, 2)
:
此時坐標系就已經變成左手系了。還需要進行一些轉換。進行rotate(-90)
:
記它為矩陣\(A\)。
我們的目標是水平翻轉,所以目標坐標系的兩個基底向量是\((-1, 0)\)和\((0, 1)\)。所以現在我們要找到一個矩陣\(X\),使得\(X \cdot A = \left[\begin{array}{l}-1&0\\0&1\end{array}\right]\)。
求出\(A\)的逆元為:
所以可得
所以再分別進行一次shear(0.5, 0.5)
和scale(2.0/3, 2.0/3)
就可以完成翻轉了。
綜上所述,要在一個以\((cx,cy)\)為左上角的區域內輸出一個寬wid
的被水平翻轉的QPixmap
變量frame
,使用以下代碼:
void drawPixmapFlippedHorizentally(QPainter &qpainter, int cx, int cy, int wid, const QPixmap &frame) {
qpainter.translate(cx + wid - 1, cy);
qpainter.shear(2, 2);
qpainter.rotate(-90);
qpainter.shear(0.5, 0.5);
qpainter.scale(2.0/3, 2.0/3);
qpainter.drawPixmap(QPoint(0, 0), frame);
qpainter.shear(2, 2);
qpainter.rotate(-90);
qpainter.shear(0.5, 0.5);
qpainter.scale(2.0/3, 2.0/3);
qpainter.translate(-cx - wid + 1, -cy);
}
打包成一個函數就可以了。真是不容易啊。
方法二
使用QPainter自帶的setViewport
函數,對展示的坐標系進行轉換。
void setViewport(int x, int y, int width, int height)
作用是把畫布轉換為以\((x,y)\)為原點,\(width\)為寬,\(height\)為高的畫布。
原始的畫布的原點是\((0,0)\),寬是\(width()\),高是\(height()\)(注:這兩個函數返回的是窗口的寬和高)。所以,執行函數qpainter.setViewport(0, 0, width(), height())
就等於把畫布還原到默認狀態。
這個函數也能實現坐標系的變換。改變原點位置與矩陣無關,故只有后面的兩個參數對坐標系需要納入考慮。相當於下面這個矩陣:
其中\(width\)和\(height\)是函數的參數,而\(width()\)和\(height()\)則是窗口的大小。
所以,只需要以下代碼就可以輸出一個翻轉的圖像:
void drawPixmapFlippedHorizentally(QPainter &qpainter, int cx, int cy, int wid, const QPixmap &frame) {
qpainter.setViewport(cx + wid - 1, cy, -width(), height());
qpainter.drawPixmap(QPoint(0, 0), frame);
qpainter.setViewport(0, 0, width(), height());
}
這是我第一次在生活實踐中大量使用線性代數的知識。