Skip to content

Transformations

Ian Mackenzie edited this page Jul 12, 2018 · 3 revisions

Introduction

Many elm-geometry functions deal with different kinds of transformations - translations, rotations, scalings, mirrors, and projections. Unlike most other geometric libraries, however, elm-geometry does not use matrices to define transformations (in fact, matrices are not used anywhere). Instead of having functions to create transformation matrices which can then later be applied to values, transformations in elm-geometry are just functions that can be used directly:

rotatedPoint =
    originalPoint |> Point2d.rotateAround Point2d.origin angle

This has many advantages. First, partial function application means that transformations can be directly used with higher-order functions like List.map:

pointsOnXAxis =
    [ Point2d.fromCoordinates ( 1, 0 )
    , Point2d.fromCoordinates ( 2, 0 )
    , Point2d.fromCoordinates ( 3, 0 )
    ]

pointsOnXAxis
    |> List.map
        (Point2d.rotateAround Point2d.origin (degrees 90))
--> [ Point2d.fromCoordinates ( 0, 1 )
--> , Point2d.fromCoordinates ( 0, 2 )
--> , Point2d.fromCoordinates ( 0, 3 )
--> ]

Second, transformations can be composed like any other functions to produce composite transformations (no more having to remember multiplication order of matrices!):

rotate90Degrees : Point2d -> Point2d
rotate90Degrees point =
    Point2d.rotateAround Point2d.origin (degrees 90) point

scale150Percent : Point2d -> Point2d
scale150Percent point =
    Point2d.scaleAbout Point2d.origin 1.5 point

rotateThenScale : Point2d -> Point2d
rotateThenScale point =
    point |> rotate90Degrees |> scale150Percent

rotateThenScale (Point2d.fromCoordinates ( 1, 0 ))
--> Point2d.fromCoordinates ( 0, 1.5 )

rotateThenScale (Point2d.fromCoordinates ( 0, 2 ))
--> Point2d.fromCoordinates ( -3, 0 )

(Yes, in this particular case it doesn't actually matter whether you rotate first and then scale or the other way around, but you get the idea.)

Transformation functions generally work very similarly to each other; for example, Point2d.mirrorAcross behaves much like Triangle3d.mirrorAcross even though they work on different data types in different dimensions.

The behaviour of a transformation on a complex object is generally the result of applying the same transformation on its components. For example, rotating a Frame3d is equivalent to rotating its origin point and basis directions and then constructing a new frame from the rotated values.

Scaling

Scaling works the same way in both 2D and 3D. Various modules include scaleAbout functions which all accept the same arguments in the same order: a point to scale about, a value to scale by, and finally the value to scale:

point =
    Point2d.fromCoordinates ( 1, 1 )

lineSegment =
    LineSegment2d.fromEndpoints
        ( Point2d.fromCoordinates ( 1, 1 )
        , Point2d.fromCoordinates ( 3, 2 )
        )

LineSegment2d.scaleAbout point 2.0 lineSegment
--> LineSegment2d.fromEndpoints
-->     ( Point2d.fromCoordinates ( 1, 1 )
-->     , Point2d.fromCoordinates ( 5, 3 )
-->     )

The point to scale about remains fixed, and all other points expand away from it or contract towards it depending on the given scale.

Translation

Translation also works the same way in both 2D and 3D - translateBy functions all take a single vector argument specifying the displacement to translate by, and then the value to translate:

axis =
    Axis2d.through (Point2d.fromCoordinates ( 2, 3 ))
        Direction2d.x

displacement =
    Vector2d.fromComponents ( 1, 2 )

Axis2d.translateBy displacement axis
--> Axis2d.through (Point2d.fromCoordinates ( 3, 5 ))
-->     Direction2d.x

Rotation

Mirroring

Projection

Clone this wiki locally