blob: 769475f0ddb2170f957e793fe0fdf313e60183f5 [file] [log] [blame]
jvrf184f752003-09-16 11:01:40 +00001"""Affine 2D transformation matrix class.
2
3The Transform class implements various transformation matrix operations,
4both on the matrix itself, as well as on 2D coordinates.
5
6Transform instances are effectively immutable: all methods that operate on the
7transformation itself always return a new instance. This has as the
8interesting side effect that Transform instances are hashable, ie. they can be
9used as dictionary keys.
10
11This module exports the following symbols:
12
13 Transform -- this is the main class
14 Identity -- Transform instance set to the identity transformation
15 Offset -- Convenience function that returns a translating transformation
16 Scale -- Convenience function that returns a scaling transformation
17
18Examples:
19
20 >>> t = Transform(2, 0, 0, 3, 0, 0)
21 >>> t.transformPoint((100, 100))
22 (200, 300)
23 >>> t = Scale(2, 3)
24 >>> t.transformPoint((100, 100))
25 (200, 300)
26 >>> t.transformPoint((0, 0))
27 (0, 0)
28 >>> t = Offset(2, 3)
29 >>> t.transformPoint((100, 100))
30 (102, 103)
31 >>> t.transformPoint((0, 0))
32 (2, 3)
33 >>> t2 = t.scale(0.5)
34 >>> t2.transformPoint((100, 100))
35 (52.0, 53.0)
36 >>> import math
37 >>> t3 = t2.rotate(math.pi / 2)
38 >>> t3.transformPoint((0, 0))
39 (2.0, 3.0)
40 >>> t3.transformPoint((100, 100))
41 (-48.0, 53.0)
42 >>> t = Identity.scale(0.5).translate(100, 200).skew(0.1, 0.2)
43 >>> t.transformPoints([(0, 0), (1, 1), (100, 100)])
44 [(50.0, 100.0), (50.550167336042726, 100.60135501775433), (105.01673360427253, 160.13550177543362)]
45 >>>
46"""
47
48
49__all__ = ["Transform", "Identity", "Offset", "Scale"]
jvr9e58c902003-08-22 14:56:48 +000050
51
52_EPSILON = 1e-15
53_ONE_EPSILON = 1 - _EPSILON
54_MINUS_ONE_EPSILON = -1 + _EPSILON
55
56
57def _normSinCos(v):
58 if abs(v) < _EPSILON:
59 v = 0
60 elif v > _ONE_EPSILON:
61 v = 1
62 elif v < _MINUS_ONE_EPSILON:
63 v = -1
64 return v
65
66
67class Transform:
jvr6385a4e2003-08-24 16:17:11 +000068
jvr9e58c902003-08-22 14:56:48 +000069 """2x2 transformation matrix plus offset, a.k.a. Affine transform.
jvrf184f752003-09-16 11:01:40 +000070 Transform instances are immutable: all transforming methods, eg.
71 rotate(), return a new Transform instance.
72
73 Examples:
74 >>> t = Transform()
75 >>> t
76 <Transform [1 0 0 1 0 0]>
77 >>> t.scale(2)
78 <Transform [2 0 0 2 0 0]>
79 >>> t.scale(2.5, 5.5)
80 <Transform [2.5 0.0 0.0 5.5 0 0]>
81 >>>
82 >>> t.scale(2, 3).transformPoint((100, 100))
83 (200, 300)
84 """
jvr6385a4e2003-08-24 16:17:11 +000085
jvr9e58c902003-08-22 14:56:48 +000086 def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0):
jvrdeca3982003-09-16 11:30:29 +000087 """Transform's constructor takes six arguments, all of which are
88 optional, and can be used as keyword arguments:
89 >>> Transform(12)
90 <Transform [12 0 0 1 0 0]>
91 >>> Transform(dx=12)
92 <Transform [1 0 0 1 12 0]>
93 >>> Transform(yx=12)
94 <Transform [1 0 12 1 0 0]>
95 >>>
96 """
jvr9e58c902003-08-22 14:56:48 +000097 self.__affine = xx, xy, yx, yy, dx, dy
jvr6385a4e2003-08-24 16:17:11 +000098
jvr9e58c902003-08-22 14:56:48 +000099 def transformPoint(self, (x, y)):
jvrf184f752003-09-16 11:01:40 +0000100 """Transform a point.
101
102 Example:
103 >>> t = Transform()
104 >>> t = t.scale(2.5, 5.5)
105 >>> t.transformPoint((100, 100))
106 (250.0, 550.0)
107 """
jvr9e58c902003-08-22 14:56:48 +0000108 xx, xy, yx, yy, dx, dy = self.__affine
109 return (xx*x + yx*y + dx, xy*x + yy*y + dy)
jvr6385a4e2003-08-24 16:17:11 +0000110
jvrf184f752003-09-16 11:01:40 +0000111 def transformPoints(self, points):
112 """Transform a list of points.
113
114 Example:
115 >>> t = Scale(2, 3)
116 >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
117 [(0, 0), (0, 300), (200, 300), (200, 0)]
118 >>>
119 """
120 xx, xy, yx, yy, dx, dy = self.__affine
121 return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
122
jvr9e58c902003-08-22 14:56:48 +0000123 def translate(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000124 """Return a new transformation, translated (offset) by x, y.
125
126 Example:
127 >>> t = Transform()
128 >>> t.translate(20, 30)
129 <Transform [1 0 0 1 20 30]>
130 >>>
131 """
jvr9e58c902003-08-22 14:56:48 +0000132 return self.transform((1, 0, 0, 1, x, y))
jvr6385a4e2003-08-24 16:17:11 +0000133
jvr9e58c902003-08-22 14:56:48 +0000134 def scale(self, x=1, y=None):
jvrf184f752003-09-16 11:01:40 +0000135 """Return a new transformation, scaled by x, y. The 'y' argument
136 may be None, which implies to use the x value for y as well.
137
138 Example:
139 >>> t = Transform()
140 >>> t.scale(5)
141 <Transform [5 0 0 5 0 0]>
142 >>> t.scale(5, 6)
143 <Transform [5 0 0 6 0 0]>
144 >>>
145 """
jvr9e58c902003-08-22 14:56:48 +0000146 if y is None:
147 y = x
148 return self.transform((x, 0, 0, y, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000149
jvr9e58c902003-08-22 14:56:48 +0000150 def rotate(self, angle):
jvrf184f752003-09-16 11:01:40 +0000151 """Return a new transformation, rotated by 'angle' (radians).
152
153 Example:
154 >>> import math
155 >>> t = Transform()
156 >>> t.rotate(math.pi / 2)
157 <Transform [0 1 -1 0 0 0]>
158 >>>
159 """
jvr9e58c902003-08-22 14:56:48 +0000160 import math
161 c = _normSinCos(math.cos(angle))
162 s = _normSinCos(math.sin(angle))
163 return self.transform((c, s, -s, c, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000164
jvr9e58c902003-08-22 14:56:48 +0000165 def skew(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000166 """Return a new transformation, skewed by x and y.
167
168 Example:
169 >>> import math
170 >>> t = Transform()
171 >>> t.skew(math.pi / 4)
172 <Transform [1.0 0.0 1.0 1.0 0 0]>
173 >>>
174 """
jvr9e58c902003-08-22 14:56:48 +0000175 import math
176 return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000177
jvr9e58c902003-08-22 14:56:48 +0000178 def transform(self, other):
jvrf184f752003-09-16 11:01:40 +0000179 """Return a new transformation, transformed by another
180 transformation.
181
182 Example:
183 >>> t = Transform(2, 0, 0, 3, 1, 6)
184 >>> t.transform((4, 3, 2, 1, 5, 6))
185 <Transform [8 9 4 3 11 24]>
186 >>>
187 """
jvr9e58c902003-08-22 14:56:48 +0000188 xx1, xy1, yx1, yy1, dx1, dy1 = other
189 xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine
190 return self.__class__(
191 xx1*xx2 + xy1*yx2,
192 xx1*xy2 + xy1*yy2,
193 yx1*xx2 + yy1*yx2,
194 yx1*xy2 + yy1*yy2,
195 xx2*dx1 + yx2*dy1 + dx2,
196 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000197
jvr9e58c902003-08-22 14:56:48 +0000198 def reverseTransform(self, other):
jvrf184f752003-09-16 11:01:40 +0000199 """Return a new transformation, which is the other transformation
200 transformed by self. self.reverseTransform(other) is equivalent to
201 other.transform(self).
202
203 Example:
204 >>> t = Transform(2, 0, 0, 3, 1, 6)
205 >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
206 <Transform [8 6 6 3 21 15]>
207 >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
208 <Transform [8 6 6 3 21 15]>
209 >>>
210 """
jvr9e58c902003-08-22 14:56:48 +0000211 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
212 xx2, xy2, yx2, yy2, dx2, dy2 = other
213 return self.__class__(
214 xx1*xx2 + xy1*yx2,
215 xx1*xy2 + xy1*yy2,
216 yx1*xx2 + yy1*yx2,
217 yx1*xy2 + yy1*yy2,
218 xx2*dx1 + yx2*dy1 + dx2,
219 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000220
jvr9e58c902003-08-22 14:56:48 +0000221 def inverse(self):
jvrf184f752003-09-16 11:01:40 +0000222 """Return the inverse transformation.
223
224 Example:
jvrdeca3982003-09-16 11:30:29 +0000225 >>> t = Identity.translate(2, 3).scale(4, 5)
226 >>> t.transformPoint((10, 20))
227 (42, 103)
228 >>> it = t.inverse()
229 >>> it.transformPoint((42, 103))
230 (10.0, 20.0)
jvrf184f752003-09-16 11:01:40 +0000231 >>>
232 """
jvr9e58c902003-08-22 14:56:48 +0000233 if self.__affine == (1, 0, 0, 1, 0, 0):
234 return self
235 xx, xy, yx, yy, dx, dy = self.__affine
236 det = float(xx*yy - yx*xy)
237 xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
238 dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
239 return self.__class__(xx, xy, yx, yy, dx, dy)
jvr6385a4e2003-08-24 16:17:11 +0000240
jvr9e58c902003-08-22 14:56:48 +0000241 def toPS(self):
jvrdeca3982003-09-16 11:30:29 +0000242 """Return a PostScript representation:
243 >>> t = Identity.scale(2, 3).translate(4, 5)
244 >>> t.toPS()
245 '[2 0 0 3 8 15]'
246 >>>
247 """
jvr9e58c902003-08-22 14:56:48 +0000248 return "[%s %s %s %s %s %s]" % self.__affine
jvr6385a4e2003-08-24 16:17:11 +0000249
jvr9e58c902003-08-22 14:56:48 +0000250 def __len__(self):
jvrdeca3982003-09-16 11:30:29 +0000251 """Transform instances also behave like sequences of length 6:
252 >>> len(Identity)
253 6
254 >>>
255 """
jvr9e58c902003-08-22 14:56:48 +0000256 return 6
jvr6385a4e2003-08-24 16:17:11 +0000257
jvr9e58c902003-08-22 14:56:48 +0000258 def __getitem__(self, index):
jvrdeca3982003-09-16 11:30:29 +0000259 """Transform instances also behave like sequences of length 6:
260 >>> list(Identity)
261 [1, 0, 0, 1, 0, 0]
262 >>> tuple(Identity)
263 (1, 0, 0, 1, 0, 0)
264 >>>
265 """
jvr9e58c902003-08-22 14:56:48 +0000266 return self.__affine[index]
jvr6385a4e2003-08-24 16:17:11 +0000267
jvr9e58c902003-08-22 14:56:48 +0000268 def __getslice__(self, i, j):
jvrdeca3982003-09-16 11:30:29 +0000269 """Transform instances also behave like sequences and even support
270 slicing:
271 >>> t = Offset(100, 200)
272 >>> t
273 <Transform [1 0 0 1 100 200]>
274 >>> t[4:]
275 (100, 200)
276 >>>
277 """
jvr9e58c902003-08-22 14:56:48 +0000278 return self.__affine[i:j]
jvr6385a4e2003-08-24 16:17:11 +0000279
jvr9e58c902003-08-22 14:56:48 +0000280 def __cmp__(self, other):
jvrdeca3982003-09-16 11:30:29 +0000281 """Transform instances are comparable:
282 >>> t1 = Identity.scale(2, 3).translate(4, 6)
283 >>> t2 = Identity.translate(8, 18).scale(2, 3)
284 >>> t1 == t2
285 1
286 >>>
287
288 But beware of floating point rounding errors:
289 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
290 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
291 >>> t1
292 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
293 >>> t2
294 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
295 >>> t1 == t2
296 0
297 >>>
298 """
jvr9e58c902003-08-22 14:56:48 +0000299 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
300 xx2, xy2, yx2, yy2, dx2, dy2 = other
301 return cmp((xx1, xy1, yx1, yy1, dx1, dy1),
302 (xx2, xy2, yx2, yy2, dx2, dy2))
jvr6385a4e2003-08-24 16:17:11 +0000303
jvr9e58c902003-08-22 14:56:48 +0000304 def __hash__(self):
jvrdeca3982003-09-16 11:30:29 +0000305 """Transform instances are hashable, meaning you can use them as
306 keys in dictionaries:
307 >>> d = {Scale(12, 13): None}
308 >>> d
309 {<Transform [12 0 0 13 0 0]>: None}
310 >>>
311
312 But again, beware of floating point rounding errors:
313 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
314 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
315 >>> t1
316 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
317 >>> t2
318 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
319 >>> d = {t1: None}
320 >>> d
321 {<Transform [0.2 0.0 0.0 0.3 0.08 0.18]>: None}
322 >>> d[t2]
323 Traceback (most recent call last):
324 File "<stdin>", line 1, in ?
325 KeyError: <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
326 >>>
327 """
jvr9e58c902003-08-22 14:56:48 +0000328 return hash(self.__affine)
jvr6385a4e2003-08-24 16:17:11 +0000329
jvr9e58c902003-08-22 14:56:48 +0000330 def __repr__(self):
331 return "<%s [%s %s %s %s %s %s]>" % ((self.__class__.__name__,)
332 + tuple(map(str, self.__affine)))
333
334
335Identity = Transform()
336
337def Offset(x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000338 """Return the identity transformation offset by x, y.
339
340 Example:
341 >>> Offset(2, 3)
342 <Transform [1 0 0 1 2 3]>
343 >>>
344 """
jvr9e58c902003-08-22 14:56:48 +0000345 return Transform(1, 0, 0, 1, x, y)
346
347def Scale(x, y=None):
jvrf184f752003-09-16 11:01:40 +0000348 """Return the identity transformation scaled by x, y. The 'y' argument
349 may be None, which implies to use the x value for y as well.
350
351 Example:
352 >>> Scale(2, 3)
353 <Transform [2 0 0 3 0 0]>
354 >>>
355 """
jvr9e58c902003-08-22 14:56:48 +0000356 if y is None:
357 y = x
358 return Transform(x, 0, 0, y, 0, 0)
jvrf184f752003-09-16 11:01:40 +0000359
360
361def _test():
jvrdeca3982003-09-16 11:30:29 +0000362 import doctest, transform
363 return doctest.testmod(transform)
jvrf184f752003-09-16 11:01:40 +0000364
365if __name__ == "__main__":
366 _test()