AffineTransform类描述了一种二维仿射变换的功能,它是一种二维坐标到二维坐标之间的线性变换,保持二维图形的“平直性”(译注:straightness,即变换后直线还是直线不会打弯,圆弧还是圆弧)和“平行性”(译注:parallelness,其实是指保二维图形间的相对位置关系不变,平行线还是平行线,相交直线的交角不变。大二学过的复变,“保形变换/保角变换”都还记得吧,数学就是王道啊!)。仿射变换可以通过一系列的原子变换的复合来实现,包括:平移(Translation)、缩放(Scale)、翻转(Flip)、旋转(Rotation)和剪切(Shear)。
此类变换可以用一个3×3的矩阵来表示,其最后一行为(0, 0, 1)。该变换矩阵将原坐标(x, y)变换为新坐标(x', y'),这里原坐标和新坐标皆视为最末一行为(1)的三维列向量,原列向量左乘变换矩阵得到新的列向量:
1
2
3
|
[x'] [m00 m01 m02] [x] [m00*x+m01*y+m02] [y'] = [m10 m11 m12] [y] = [m10*x+m11*y+m12] [1 ] [ 0 0 1 ] [1] [ 1 ] |
几种典型的仿射变换:
1
|
public static AffineTransform getTranslateInstance( double tx, double ty) |
平移变换,将每一点移动到(x+tx, y+ty),变换矩阵为:
1
2
3
|
[ 1 0 tx ] [ 0 1 ty ] [ 0 0 1 ] |
(译注:平移变换是一种“刚体变换”,rigid-body transformation,中学学过的物理,都知道啥叫“刚体”吧,就是不会产生形变的理想物体,平移当然不会改变二维图形的形状。同理,下面的“旋转变换”也是刚体变换,而“缩放”、“错切”都是会改变图形形状的。)
1
|
public static AffineTransform getScaleInstance( double sx, double sy) |
缩放变换,将每一点的横坐标放大(缩小)至sx倍,纵坐标放大(缩小)至sy倍,变换矩阵为:
1
2
3
|
[ sx 0 0 ] [ 0 sy 0 ] [ 0 0 1 ] |
1
|
public static AffineTransform getShearInstance( double shx, double shy) |
剪切变换,变换矩阵为:
1
2
3
|
[ 1 shx 0 ] [ shy 1 0 ] [ 0 0 1 ] |
相当于一个横向剪切与一个纵向剪切的复合
1
2
3
|
[ 1 0 0 ][ 1 shx 0 ] [ shy 1 0 ][ 0 1 0 ] [ 0 0 1 ][ 0 0 1 ] |
(译注:“剪切变换”又称“错切变换”,指的是类似于四边形不稳定性那种性质,街边小商店那种铁拉门都见过吧?想象一下上面铁条构成的菱形拉动的过程,那就是“错切”的过程。)
1
|
public static AffineTransform getRotateInstance( double theta) |
旋转变换,目标图形围绕原点顺时针旋转theta弧度,变换矩阵为:
1
2
3
4
|
[ cos(theta) -sin(theta) 0 ] [ sin(theta) cos(theta) 0 ] [ 0 0 1 ] |
1
|
public static AffineTransform getRotateInstance( double theta, double x, double y) |
旋转变换,目标图形以(x, y)为轴心顺时针旋转theta弧度,变换矩阵为:
1
2
3
|
[ cos(theta) -sin(theta) x-x*cos+y*sin] [ sin(theta) cos(theta) y-x*sin-y*cos ] [ 0 0 1 ] |
相当于两次平移变换与一次原点旋转变换的复合:
1
2
3
|
[1 0 -x][cos(theta) -sin(theta) 0][1 0 x] [0 1 -y][sin(theta) cos(theta) 0][0 1 y] [0 0 1 ][ 0 0 1 ][0 0 1] |
几何中,一个向量空间进行一次线性变换并接上一个平移,这么一个过程就称为仿射变换或放射映射。
可以简单地表示为:y = Ax + b ,其中有下标的字母表示向量,而粗体的字母A表示一个矩阵。
如果暂时无法理解也没有关系(我也没理解 ^_^#),没关系,我们这里仅使用了它的几个特例:平移和旋转变换。
按照惯例,下面先把整个代码贴出来:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
|
import java.applet.Applet; import java.awt.BorderLayout; import java.awt.Checkbox; import java.awt.CheckboxGroup; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Panel; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.Random; public class AffineTest extends Applet implements ItemListener{ private Rectangle2D rect; private Checkbox rotateFirst; private Checkbox translateFirst; public void init() { setLayout( new BorderLayout()); CheckboxGroup cbg = new CheckboxGroup(); Panel p = new Panel(); rotateFirst = new Checkbox( "rotate, translate" , cbg, true ); rotateFirst.addItemListener( this ); p.add(rotateFirst); translateFirst = new Checkbox( "translate, rotate" , cbg, false ); translateFirst.addItemListener( this ); p.add(translateFirst); add(p, BorderLayout.SOUTH); rect = new Rectangle2D.Float(- 0 .5f, - 0 .5f, 1 .0f, 1 .0f); } public void paint(Graphics g) { Graphics2D g2d = (Graphics2D)g; final AffineTransform identify = new AffineTransform(); boolean rotate = rotateFirst.getState(); Random r = new Random(); final double oneRadian = Math.toRadians( 1.0 ); for ( double radians = 0.0 ; radians < 2.0 *Math.PI ; radians += oneRadian) { g2d.setTransform(identify); if (rotate) { g2d.translate( 100 , 100 ); g2d.rotate(radians); } else { g2d.rotate(radians); g2d.translate( 100 , 100 ); } g2d.scale( 100 , 100 ); g2d.setColor( new Color(r.nextInt())); g2d.fill(rect); } } @Override public void itemStateChanged(ItemEvent arg0) { // TODO Auto-generated method stub repaint(); } } import java.applet.Applet; import java.awt.BorderLayout; import java.awt.Checkbox; import java.awt.CheckboxGroup; import java.awt.Color; import java.awt.Graphics; import java.awt.Graphics2D; import java.awt.Panel; import java.awt.event.ItemEvent; import java.awt.event.ItemListener; import java.awt.geom.AffineTransform; import java.awt.geom.Rectangle2D; import java.util.Random; public class AffineTest extends Applet implements ItemListener{ private Rectangle2D rect; private Checkbox rotateFirst; private Checkbox translateFirst; public void init() { setLayout( new BorderLayout()); CheckboxGroup cbg = new CheckboxGroup(); Panel p = new Panel(); rotateFirst = new Checkbox( "rotate, translate" , cbg, true ); rotateFirst.addItemListener( this ); p.add(rotateFirst); translateFirst = new Checkbox( "translate, rotate" , cbg, false ); translateFirst.addItemListener( this ); p.add(translateFirst); add(p, BorderLayout.SOUTH); rect = new Rectangle2D.Float(- 0 .5f, - 0 .5f, 1 .0f, 1 .0f); } public void paint(Graphics g) { Graphics2D g2d = (Graphics2D)g; final AffineTransform identify = new AffineTransform(); boolean rotate = rotateFirst.getState(); Random r = new Random(); final double oneRadian = Math.toRadians( 1.0 ); for ( double radians = 0.0 ; radians < 2.0 *Math.PI ; radians += oneRadian) { g2d.setTransform(identify); if (rotate) { g2d.translate( 100 , 100 ); g2d.rotate(radians); } else { g2d.rotate(radians); g2d.translate( 100 , 100 ); } g2d.scale( 100 , 100 ); g2d.setColor( new Color(r.nextInt())); g2d.fill(rect); } } @Override public void itemStateChanged(ItemEvent arg0) { // TODO Auto-generated method stub repaint(); } } |
对比可知,仿射变换的顺序是不能随便交换的。