2016-09-01 18:50:03 +00:00
|
|
|
#lang racket/base
|
|
|
|
;; 2D affine transformation matrices.
|
|
|
|
|
|
|
|
;; These are *active* transformations: they transform vectors to new
|
|
|
|
;; vectors, rather than coordinate systems to new coordinate systems.
|
|
|
|
|
|
|
|
(provide (struct-out transformation-matrix)
|
|
|
|
|
|
|
|
identity-transformation
|
|
|
|
translation-transformation
|
|
|
|
rotation-transformation
|
|
|
|
stretching-transformation
|
|
|
|
shearing-transformation
|
|
|
|
invert-transformation
|
|
|
|
compose-transformation
|
|
|
|
|
|
|
|
transform-point
|
|
|
|
transform-vector
|
|
|
|
untransform-point
|
|
|
|
untransform-vector)
|
|
|
|
|
|
|
|
(require (only-in racket/math pi))
|
|
|
|
(require racket/match)
|
|
|
|
|
|
|
|
(struct transformation-matrix (a b c d tx ty) #:prefab)
|
|
|
|
|
|
|
|
(define identity-transformation
|
|
|
|
(transformation-matrix 1 0 0 1 0 0))
|
|
|
|
|
|
|
|
(define (translation-transformation x y)
|
|
|
|
(transformation-matrix 1 0 0 1 x y))
|
|
|
|
|
|
|
|
(define (rad deg) (* deg (/ pi 180.0)))
|
|
|
|
|
|
|
|
(define (rotation-transformation theta-d)
|
|
|
|
(match theta-d
|
|
|
|
[(or 0 360) identity-transformation]
|
|
|
|
[(or 90 -270) (transformation-matrix 0 1 -1 0 0 0)]
|
|
|
|
[180 (transformation-matrix -1 0 0 -1 0 0)]
|
|
|
|
[(or 270 -90) (transformation-matrix 0 -1 1 0 0 0)]
|
|
|
|
[_
|
|
|
|
(define theta-r (rad theta-d))
|
|
|
|
(define c (cos theta-r))
|
|
|
|
(define s (sin theta-r))
|
|
|
|
(transformation-matrix c s (- s) c 0 0)]))
|
|
|
|
|
|
|
|
(define (stretching-transformation sx [sy sx])
|
|
|
|
(transformation-matrix sx 0 0 sy 0 0))
|
|
|
|
|
|
|
|
(define (shearing-transformation sx sy)
|
|
|
|
(transformation-matrix 1 sy sx 1 0 0))
|
|
|
|
|
|
|
|
(define (invert-transformation m)
|
|
|
|
(define det (determinant m))
|
|
|
|
(when (zero? det) (error 'invert-transformation "Zero determinant"))
|
|
|
|
(define -det (- det))
|
|
|
|
(match-define (transformation-matrix a b c d tx ty) m)
|
|
|
|
(transformation-matrix (/ d det)
|
|
|
|
(/ b -det)
|
|
|
|
(/ c -det)
|
|
|
|
(/ a det)
|
|
|
|
(/ (- (* c ty) (* d tx)) det)
|
|
|
|
(/ (- (* b tx) (* a ty)) det)))
|
|
|
|
|
|
|
|
(define (determinant m)
|
|
|
|
(match-define (transformation-matrix a b c d _ _) m)
|
|
|
|
(- (* a d) (* b c)))
|
|
|
|
|
|
|
|
(define (compose-transformation* m1 m0)
|
|
|
|
(match-define (transformation-matrix a b c d tx ty) m1)
|
|
|
|
(match-define (transformation-matrix e f g h sx sy) m0)
|
|
|
|
(transformation-matrix (+ (* a e) (* c f))
|
|
|
|
(+ (* b e) (* d f))
|
|
|
|
(+ (* a g) (* c h))
|
|
|
|
(+ (* b g) (* d h))
|
|
|
|
(+ (* a sx) (* c sy) tx)
|
|
|
|
(+ (* b sx) (* d sy) ty)))
|
|
|
|
|
|
|
|
(define compose-transformation
|
|
|
|
(case-lambda
|
|
|
|
[() identity-transformation]
|
|
|
|
[(m) m]
|
|
|
|
[(m1 m0) (compose-transformation* m1 m0)]
|
|
|
|
[mtxs (foldr compose-transformation* identity-transformation mtxs)]))
|
|
|
|
|
|
|
|
(define (transform-point m v)
|
|
|
|
(match-define (transformation-matrix a b c d tx ty) m)
|
|
|
|
(define x (real-part v))
|
|
|
|
(define y (imag-part v))
|
|
|
|
(make-rectangular (+ (* a x) (* c y) tx)
|
|
|
|
(+ (* b x) (* d y) ty)))
|
|
|
|
|
|
|
|
(define (transform-vector m v)
|
|
|
|
(match-define (transformation-matrix a b c d _ _) m)
|
|
|
|
(define x (real-part v))
|
|
|
|
(define y (imag-part v))
|
|
|
|
(make-rectangular (+ (* a x) (* c y))
|
|
|
|
(+ (* b x) (* d y))))
|
|
|
|
|
|
|
|
(define (untransform-point m v)
|
|
|
|
(transform-point (invert-transformation m) v))
|
|
|
|
|
|
|
|
(define (untransform-vector m v)
|
|
|
|
(transform-vector (invert-transformation m) v))
|
|
|
|
|
|
|
|
(module+ test
|
|
|
|
(require rackunit)
|
|
|
|
|
|
|
|
(define eps 0.00001)
|
|
|
|
(define invrt2 (/ (sqrt 2)))
|
|
|
|
|
|
|
|
(define (within-eps a b) (< (magnitude (- a b)) eps))
|
|
|
|
|
|
|
|
(define-binary-check (check-transformation~? actual expected)
|
|
|
|
(match-let (((transformation-matrix aa ab ac ad atx aty) actual)
|
|
|
|
((transformation-matrix ea eb ec ed etx ety) expected))
|
|
|
|
(and (within-eps aa ea)
|
|
|
|
(within-eps ab eb)
|
|
|
|
(within-eps ac ec)
|
|
|
|
(within-eps ad ed)
|
|
|
|
(within-eps atx etx)
|
|
|
|
(within-eps aty ety))))
|
|
|
|
|
|
|
|
(check-= (transform-point (rotation-transformation 0) +i) +i eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 90) +i) -1 eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 180) +i) -i eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 270) +i) 1 eps)
|
|
|
|
(check-= (transform-point (rotation-transformation -90) +i) 1 eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 360) +i) +i eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (rotation-transformation 0) 1) 1 eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 90) 1) +i eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 180) 1) -1 eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 270) 1) -i eps)
|
|
|
|
(check-= (transform-point (rotation-transformation -90) 1) -i eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 360) 1) 1 eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (rotation-transformation -45) 1) (make-rectangular invrt2 (- invrt2)) eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 45) 1) (make-rectangular invrt2 invrt2) eps)
|
|
|
|
(check-= (transform-point (rotation-transformation 135) 1) (make-rectangular (- invrt2) invrt2) eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (stretching-transformation 2) 1) 2 eps)
|
|
|
|
(check-= (transform-point (stretching-transformation 2) +i) +2i eps)
|
|
|
|
(check-= (transform-point (stretching-transformation 2) 1+i) 2+2i eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (compose-transformation (translation-transformation 0 2)
|
2016-09-27 21:08:24 +00:00
|
|
|
(rotation-transformation 45))
|
|
|
|
1)
|
2016-09-01 18:50:03 +00:00
|
|
|
(make-rectangular invrt2 (+ invrt2 2))
|
|
|
|
eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (compose-transformation (rotation-transformation 45)
|
2016-09-27 21:08:24 +00:00
|
|
|
(translation-transformation 0 2))
|
|
|
|
1)
|
2016-09-01 18:50:03 +00:00
|
|
|
-0.7071067811865474+2.121320343559643i
|
|
|
|
eps)
|
|
|
|
|
|
|
|
(check-= (transform-point (invert-transformation
|
2016-09-27 21:08:24 +00:00
|
|
|
(compose-transformation (rotation-transformation 45)
|
|
|
|
(translation-transformation 0 2)))
|
|
|
|
-0.7071067811865474+2.121320343559643i)
|
2016-09-01 18:50:03 +00:00
|
|
|
1
|
|
|
|
eps)
|
|
|
|
|
|
|
|
(check-transformation~? (compose-transformation (rotation-transformation -90)
|
|
|
|
(translation-transformation 0 2)
|
|
|
|
(rotation-transformation 90))
|
|
|
|
(translation-transformation 2 0))
|
|
|
|
|
|
|
|
(check-transformation~? (compose-transformation (rotation-transformation -45)
|
|
|
|
(translation-transformation 0 (* 2 (sqrt 2)))
|
|
|
|
(rotation-transformation 45))
|
|
|
|
(translation-transformation 2 2))
|
|
|
|
|
|
|
|
;; Cairo's drawing model has *device coordinates* and *user
|
|
|
|
;; coordinates*. In the Cairo tutorial, we are given the task of
|
|
|
|
;; mapping a 1.0x1.0 workspace onto the 100x100 pixel square in the
|
|
|
|
;; middle of a 120x120 pixel surface, and shown three different ways
|
|
|
|
;; of achieving this:
|
|
|
|
;;
|
|
|
|
;; - cairo_translate (cr, 10, 10); cairo_scale (cr, 100, 100);
|
|
|
|
;;
|
|
|
|
;; - cairo_scale (cr, 100, 100); cairo_translate (cr, 0.1, 0.1);
|
|
|
|
;;
|
|
|
|
;; - cairo_matrix_t mat; cairo_matrix_init (&mat, 100, 0, 0, 100, 10, 10);
|
|
|
|
;; cairo_transform (cr, &mat);
|
|
|
|
;;
|
|
|
|
;; Let's see what those look like here. We'll assume a right-handed
|
|
|
|
;; coordinate system for both the workspace and the surface, so we
|
|
|
|
;; can judge a correct outcome by seeing that (0,0) on the workspace
|
|
|
|
;; should map to (10,10) on the surface, that (1,1) on the workspace
|
|
|
|
;; should map to (110,110), and that the other two corners should
|
|
|
|
;; map correspondingly.
|
|
|
|
|
|
|
|
(let ()
|
|
|
|
(define (apply-to-inputs m)
|
|
|
|
(map (lambda (v) (transform-point m v))
|
|
|
|
(list 0 1 1+i +i)))
|
|
|
|
|
|
|
|
(define expected-outputs (list 10+10i 110+10i 110+110i 10+110i))
|
|
|
|
|
|
|
|
(define-binary-check (check-list~? actual expected)
|
|
|
|
(andmap within-eps actual expected))
|
|
|
|
|
|
|
|
(check-list~? (apply-to-inputs (compose-transformation (translation-transformation 10 10)
|
|
|
|
(stretching-transformation 100 100)))
|
|
|
|
expected-outputs)
|
|
|
|
(check-list~? (apply-to-inputs (compose-transformation (stretching-transformation 100 100)
|
|
|
|
(translation-transformation 0.1 0.1)))
|
|
|
|
expected-outputs)
|
|
|
|
(check-list~? (apply-to-inputs (transformation-matrix 100 0 0 100 10 10))
|
|
|
|
expected-outputs))
|
|
|
|
|
|
|
|
;; The Cairo tutorial also makes this note regarding line widths:
|
|
|
|
;; "While you're operating under a scale, the width of your line is
|
|
|
|
;; multiplied by that scale." That is, in Cairo, you reason about
|
|
|
|
;; line widths in user coordinates, just as with everything else.
|
|
|
|
|
|
|
|
(let* ((m (transformation-matrix 100 0 0 100 10 10)))
|
|
|
|
;; transform-vector is analogous to Cairo's
|
|
|
|
;; "cairo_user_to_device_distance" function.
|
|
|
|
(check-= (transform-vector m 0.01+0.01i) 1+i eps)
|
|
|
|
;; untransform-vector is analogous to Cairo's
|
|
|
|
;; "cairo_device_to_user_distance" function.
|
|
|
|
(check-= (untransform-vector m 1+i) 0.01+0.01i eps))
|
|
|
|
|
|
|
|
)
|