blob: 69cb3fe42eb40939a8175a4b2b51c9036236aaec [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
Behdad Esfahbod7ed91ec2013-11-27 15:16:28 -050048from .py23 import *
jvrf184f752003-09-16 11:01:40 +000049
50__all__ = ["Transform", "Identity", "Offset", "Scale"]
jvr9e58c902003-08-22 14:56:48 +000051
52
53_EPSILON = 1e-15
54_ONE_EPSILON = 1 - _EPSILON
55_MINUS_ONE_EPSILON = -1 + _EPSILON
56
57
58def _normSinCos(v):
59 if abs(v) < _EPSILON:
60 v = 0
61 elif v > _ONE_EPSILON:
62 v = 1
63 elif v < _MINUS_ONE_EPSILON:
64 v = -1
65 return v
66
67
68class Transform:
jvr6385a4e2003-08-24 16:17:11 +000069
jvr9e58c902003-08-22 14:56:48 +000070 """2x2 transformation matrix plus offset, a.k.a. Affine transform.
jvrf184f752003-09-16 11:01:40 +000071 Transform instances are immutable: all transforming methods, eg.
72 rotate(), return a new Transform instance.
73
74 Examples:
75 >>> t = Transform()
76 >>> t
77 <Transform [1 0 0 1 0 0]>
78 >>> t.scale(2)
79 <Transform [2 0 0 2 0 0]>
80 >>> t.scale(2.5, 5.5)
81 <Transform [2.5 0.0 0.0 5.5 0 0]>
82 >>>
83 >>> t.scale(2, 3).transformPoint((100, 100))
84 (200, 300)
85 """
jvr6385a4e2003-08-24 16:17:11 +000086
jvr9e58c902003-08-22 14:56:48 +000087 def __init__(self, xx=1, xy=0, yx=0, yy=1, dx=0, dy=0):
jvrdeca3982003-09-16 11:30:29 +000088 """Transform's constructor takes six arguments, all of which are
89 optional, and can be used as keyword arguments:
90 >>> Transform(12)
91 <Transform [12 0 0 1 0 0]>
92 >>> Transform(dx=12)
93 <Transform [1 0 0 1 12 0]>
94 >>> Transform(yx=12)
95 <Transform [1 0 12 1 0 0]>
96 >>>
97 """
jvr9e58c902003-08-22 14:56:48 +000098 self.__affine = xx, xy, yx, yy, dx, dy
jvr6385a4e2003-08-24 16:17:11 +000099
Behdad Esfahbod3a9fd302013-11-27 03:19:32 -0500100 def transformPoint(self, p):
jvrf184f752003-09-16 11:01:40 +0000101 """Transform a point.
102
103 Example:
104 >>> t = Transform()
105 >>> t = t.scale(2.5, 5.5)
106 >>> t.transformPoint((100, 100))
107 (250.0, 550.0)
108 """
Behdad Esfahbod3a9fd302013-11-27 03:19:32 -0500109 (x, y) = p
jvr9e58c902003-08-22 14:56:48 +0000110 xx, xy, yx, yy, dx, dy = self.__affine
111 return (xx*x + yx*y + dx, xy*x + yy*y + dy)
jvr6385a4e2003-08-24 16:17:11 +0000112
jvrf184f752003-09-16 11:01:40 +0000113 def transformPoints(self, points):
114 """Transform a list of points.
115
116 Example:
117 >>> t = Scale(2, 3)
118 >>> t.transformPoints([(0, 0), (0, 100), (100, 100), (100, 0)])
119 [(0, 0), (0, 300), (200, 300), (200, 0)]
120 >>>
121 """
122 xx, xy, yx, yy, dx, dy = self.__affine
123 return [(xx*x + yx*y + dx, xy*x + yy*y + dy) for x, y in points]
124
jvr9e58c902003-08-22 14:56:48 +0000125 def translate(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000126 """Return a new transformation, translated (offset) by x, y.
127
128 Example:
129 >>> t = Transform()
130 >>> t.translate(20, 30)
131 <Transform [1 0 0 1 20 30]>
132 >>>
133 """
jvr9e58c902003-08-22 14:56:48 +0000134 return self.transform((1, 0, 0, 1, x, y))
jvr6385a4e2003-08-24 16:17:11 +0000135
jvr9e58c902003-08-22 14:56:48 +0000136 def scale(self, x=1, y=None):
jvrf184f752003-09-16 11:01:40 +0000137 """Return a new transformation, scaled by x, y. The 'y' argument
138 may be None, which implies to use the x value for y as well.
139
140 Example:
141 >>> t = Transform()
142 >>> t.scale(5)
143 <Transform [5 0 0 5 0 0]>
144 >>> t.scale(5, 6)
145 <Transform [5 0 0 6 0 0]>
146 >>>
147 """
jvr9e58c902003-08-22 14:56:48 +0000148 if y is None:
149 y = x
150 return self.transform((x, 0, 0, y, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000151
jvr9e58c902003-08-22 14:56:48 +0000152 def rotate(self, angle):
jvrf184f752003-09-16 11:01:40 +0000153 """Return a new transformation, rotated by 'angle' (radians).
154
155 Example:
156 >>> import math
157 >>> t = Transform()
158 >>> t.rotate(math.pi / 2)
159 <Transform [0 1 -1 0 0 0]>
160 >>>
161 """
jvr9e58c902003-08-22 14:56:48 +0000162 import math
163 c = _normSinCos(math.cos(angle))
164 s = _normSinCos(math.sin(angle))
165 return self.transform((c, s, -s, c, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000166
jvr9e58c902003-08-22 14:56:48 +0000167 def skew(self, x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000168 """Return a new transformation, skewed by x and y.
169
170 Example:
171 >>> import math
172 >>> t = Transform()
173 >>> t.skew(math.pi / 4)
174 <Transform [1.0 0.0 1.0 1.0 0 0]>
175 >>>
176 """
jvr9e58c902003-08-22 14:56:48 +0000177 import math
178 return self.transform((1, math.tan(y), math.tan(x), 1, 0, 0))
jvr6385a4e2003-08-24 16:17:11 +0000179
jvr9e58c902003-08-22 14:56:48 +0000180 def transform(self, other):
jvrf184f752003-09-16 11:01:40 +0000181 """Return a new transformation, transformed by another
182 transformation.
183
184 Example:
185 >>> t = Transform(2, 0, 0, 3, 1, 6)
186 >>> t.transform((4, 3, 2, 1, 5, 6))
187 <Transform [8 9 4 3 11 24]>
188 >>>
189 """
jvr9e58c902003-08-22 14:56:48 +0000190 xx1, xy1, yx1, yy1, dx1, dy1 = other
191 xx2, xy2, yx2, yy2, dx2, dy2 = self.__affine
192 return self.__class__(
193 xx1*xx2 + xy1*yx2,
194 xx1*xy2 + xy1*yy2,
195 yx1*xx2 + yy1*yx2,
196 yx1*xy2 + yy1*yy2,
197 xx2*dx1 + yx2*dy1 + dx2,
198 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000199
jvr9e58c902003-08-22 14:56:48 +0000200 def reverseTransform(self, other):
jvrf184f752003-09-16 11:01:40 +0000201 """Return a new transformation, which is the other transformation
202 transformed by self. self.reverseTransform(other) is equivalent to
203 other.transform(self).
204
205 Example:
206 >>> t = Transform(2, 0, 0, 3, 1, 6)
207 >>> t.reverseTransform((4, 3, 2, 1, 5, 6))
208 <Transform [8 6 6 3 21 15]>
209 >>> Transform(4, 3, 2, 1, 5, 6).transform((2, 0, 0, 3, 1, 6))
210 <Transform [8 6 6 3 21 15]>
211 >>>
212 """
jvr9e58c902003-08-22 14:56:48 +0000213 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
214 xx2, xy2, yx2, yy2, dx2, dy2 = other
215 return self.__class__(
216 xx1*xx2 + xy1*yx2,
217 xx1*xy2 + xy1*yy2,
218 yx1*xx2 + yy1*yx2,
219 yx1*xy2 + yy1*yy2,
220 xx2*dx1 + yx2*dy1 + dx2,
221 xy2*dx1 + yy2*dy1 + dy2)
jvr6385a4e2003-08-24 16:17:11 +0000222
jvr9e58c902003-08-22 14:56:48 +0000223 def inverse(self):
jvrf184f752003-09-16 11:01:40 +0000224 """Return the inverse transformation.
225
226 Example:
jvrdeca3982003-09-16 11:30:29 +0000227 >>> t = Identity.translate(2, 3).scale(4, 5)
228 >>> t.transformPoint((10, 20))
229 (42, 103)
230 >>> it = t.inverse()
231 >>> it.transformPoint((42, 103))
232 (10.0, 20.0)
jvrf184f752003-09-16 11:01:40 +0000233 >>>
234 """
jvr9e58c902003-08-22 14:56:48 +0000235 if self.__affine == (1, 0, 0, 1, 0, 0):
236 return self
237 xx, xy, yx, yy, dx, dy = self.__affine
238 det = float(xx*yy - yx*xy)
239 xx, xy, yx, yy = yy/det, -xy/det, -yx/det, xx/det
240 dx, dy = -xx*dx - yx*dy, -xy*dx - yy*dy
241 return self.__class__(xx, xy, yx, yy, dx, dy)
jvr6385a4e2003-08-24 16:17:11 +0000242
jvr9e58c902003-08-22 14:56:48 +0000243 def toPS(self):
jvrdeca3982003-09-16 11:30:29 +0000244 """Return a PostScript representation:
245 >>> t = Identity.scale(2, 3).translate(4, 5)
246 >>> t.toPS()
247 '[2 0 0 3 8 15]'
248 >>>
249 """
jvr9e58c902003-08-22 14:56:48 +0000250 return "[%s %s %s %s %s %s]" % self.__affine
jvr6385a4e2003-08-24 16:17:11 +0000251
jvr9e58c902003-08-22 14:56:48 +0000252 def __len__(self):
jvrdeca3982003-09-16 11:30:29 +0000253 """Transform instances also behave like sequences of length 6:
254 >>> len(Identity)
255 6
256 >>>
257 """
jvr9e58c902003-08-22 14:56:48 +0000258 return 6
jvr6385a4e2003-08-24 16:17:11 +0000259
jvr9e58c902003-08-22 14:56:48 +0000260 def __getitem__(self, index):
jvrdeca3982003-09-16 11:30:29 +0000261 """Transform instances also behave like sequences of length 6:
262 >>> list(Identity)
263 [1, 0, 0, 1, 0, 0]
264 >>> tuple(Identity)
265 (1, 0, 0, 1, 0, 0)
266 >>>
267 """
jvr9e58c902003-08-22 14:56:48 +0000268 return self.__affine[index]
jvr6385a4e2003-08-24 16:17:11 +0000269
jvr9e58c902003-08-22 14:56:48 +0000270 def __getslice__(self, i, j):
jvrdeca3982003-09-16 11:30:29 +0000271 """Transform instances also behave like sequences and even support
272 slicing:
273 >>> t = Offset(100, 200)
274 >>> t
275 <Transform [1 0 0 1 100 200]>
276 >>> t[4:]
277 (100, 200)
278 >>>
279 """
jvr9e58c902003-08-22 14:56:48 +0000280 return self.__affine[i:j]
jvr6385a4e2003-08-24 16:17:11 +0000281
jvr9e58c902003-08-22 14:56:48 +0000282 def __cmp__(self, other):
jvrdeca3982003-09-16 11:30:29 +0000283 """Transform instances are comparable:
284 >>> t1 = Identity.scale(2, 3).translate(4, 6)
285 >>> t2 = Identity.translate(8, 18).scale(2, 3)
286 >>> t1 == t2
287 1
288 >>>
289
290 But beware of floating point rounding errors:
291 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
292 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
293 >>> t1
294 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
295 >>> t2
296 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
297 >>> t1 == t2
298 0
299 >>>
300 """
jvr9e58c902003-08-22 14:56:48 +0000301 xx1, xy1, yx1, yy1, dx1, dy1 = self.__affine
302 xx2, xy2, yx2, yy2, dx2, dy2 = other
303 return cmp((xx1, xy1, yx1, yy1, dx1, dy1),
304 (xx2, xy2, yx2, yy2, dx2, dy2))
jvr6385a4e2003-08-24 16:17:11 +0000305
jvr9e58c902003-08-22 14:56:48 +0000306 def __hash__(self):
jvrdeca3982003-09-16 11:30:29 +0000307 """Transform instances are hashable, meaning you can use them as
308 keys in dictionaries:
309 >>> d = {Scale(12, 13): None}
310 >>> d
311 {<Transform [12 0 0 13 0 0]>: None}
312 >>>
313
314 But again, beware of floating point rounding errors:
315 >>> t1 = Identity.scale(0.2, 0.3).translate(0.4, 0.6)
316 >>> t2 = Identity.translate(0.08, 0.18).scale(0.2, 0.3)
317 >>> t1
318 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
319 >>> t2
320 <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
321 >>> d = {t1: None}
322 >>> d
323 {<Transform [0.2 0.0 0.0 0.3 0.08 0.18]>: None}
324 >>> d[t2]
325 Traceback (most recent call last):
326 File "<stdin>", line 1, in ?
327 KeyError: <Transform [0.2 0.0 0.0 0.3 0.08 0.18]>
328 >>>
329 """
jvr9e58c902003-08-22 14:56:48 +0000330 return hash(self.__affine)
jvr6385a4e2003-08-24 16:17:11 +0000331
jvr9e58c902003-08-22 14:56:48 +0000332 def __repr__(self):
333 return "<%s [%s %s %s %s %s %s]>" % ((self.__class__.__name__,)
334 + tuple(map(str, self.__affine)))
335
336
337Identity = Transform()
338
339def Offset(x=0, y=0):
jvrf184f752003-09-16 11:01:40 +0000340 """Return the identity transformation offset by x, y.
341
342 Example:
343 >>> Offset(2, 3)
344 <Transform [1 0 0 1 2 3]>
345 >>>
346 """
jvr9e58c902003-08-22 14:56:48 +0000347 return Transform(1, 0, 0, 1, x, y)
348
349def Scale(x, y=None):
jvrf184f752003-09-16 11:01:40 +0000350 """Return the identity transformation scaled by x, y. The 'y' argument
351 may be None, which implies to use the x value for y as well.
352
353 Example:
354 >>> Scale(2, 3)
355 <Transform [2 0 0 3 0 0]>
356 >>>
357 """
jvr9e58c902003-08-22 14:56:48 +0000358 if y is None:
359 y = x
360 return Transform(x, 0, 0, y, 0, 0)
jvrf184f752003-09-16 11:01:40 +0000361
362
363def _test():
jvrdeca3982003-09-16 11:30:29 +0000364 import doctest, transform
365 return doctest.testmod(transform)
jvrf184f752003-09-16 11:01:40 +0000366
367if __name__ == "__main__":
368 _test()