Draw 2D elm-geometry values as SVG.
For the examples, assume that the following imports are present:
import Svg exposing (Svg)
import Svg.Attributes as Attributes
import Geometry.Svg as Svg
import Angle
import PixelsAlso assume that any necessary elm-geometry modules/types have been imported
using the following format:
import Point2d exposing (Point2d)All examples use a Y-up coordinate system instead of SVG's Y-down (window)
coordinate system; they were all rendered with a final relativeTo
call to flip the example 'upside down' for display.
These functions turn elm-geometry 2D values into SVG elements with geometric
attributes such as points and transform set appropriately. Each function
also accepts a list of additional SVG attributes such as fill or stroke that
should be added to the resulting element.
Draw a Triangle2d as an SVG <polygon> with the given attributes.
triangle : Svg msg
triangle =
Svg.triangle2d
[ Attributes.stroke "blue"
, Attributes.strokeWidth "10"
, Attributes.strokeLinejoin "round"
, Attributes.fill "orange"
]
(Triangle2d.fromVertices
( Point2d.pixels 100 100
, Point2d.pixels 200 100
, Point2d.pixels 100 200
)
)Draw a Polyline2d as an SVG <polyline> with the given attributes.
polyline : Svg msg
polyline =
Svg.polyline2d
[ Attributes.stroke "blue"
, Attributes.fill "none"
, Attributes.strokeWidth "5"
, Attributes.strokeLinecap "round"
, Attributes.strokeLinejoin "round"
]
(Polyline2d.fromVertices
[ Point2d.pixels 100 100
, Point2d.pixels 120 200
, Point2d.pixels 140 100
, Point2d.pixels 160 200
, Point2d.pixels 180 100
, Point2d.pixels 200 200
]
)Draw a Polygon2d as an SVG <polygon> with the given attributes.
polygon : Svg msg
polygon =
Svg.polygon2d
[ Attributes.stroke "blue"
, Attributes.fill "orange"
, Attributes.strokeWidth "3"
]
(Polygon2d.withHoles
[ [ Point2d.pixels 150 185
, Point2d.pixels 165 160
, Point2d.pixels 135 160
]
]
[ Point2d.pixels 100 200
, Point2d.pixels 120 150
, Point2d.pixels 180 150
, Point2d.pixels 200 200
]
)Draw a Rectangle2d as an SVG <rectangle> with the given attributes.
rectangle : Svg msg
rectangle =
let
axes =
Frame2d.atPoint (Point2d.pixels 150 150)
|> Frame2d.rotateBy (Angle.degrees 20)
in
Svg.rectangle2d
[ Attributes.stroke "blue"
, Attributes.fill "orange"
, Attributes.strokeWidth "4"
, Attributes.rx "15"
, Attributes.ry "15"
]
(Rectangle2d.withAxes axes
( Pixels.pixels 120, Pixels.pixels 80 )
)Draw an Arc2d as an SVG <path> with the given attributes.
arc : Svg msg
arc =
Svg.arc2d
[ Attributes.stroke "blue"
, Attributes.strokeWidth "5"
]
(Point2d.pixels 150 50
|> Arc2d.sweptAround (Point2d.pixels 100 100)
(Angle.degrees 90)
)Draw an EllipticalArc2d as an SVG <path> with the given attributes.
ellipticalArc : Svg msg
ellipticalArc =
Svg.ellipticalArc2d
[ Attributes.stroke "blue"
, Attributes.fill "none"
, Attributes.strokeWidth "5"
, Attributes.strokeLinecap "round"
]
(EllipticalArc2d.with
{ centerPoint = Point2d.pixels 100 10
, xDirection = Direction2d.x
, xRadius = Pixels.pixels 50
, yRadius = Pixels.pixels 100
, startAngle = Angle.degrees 0
, sweptAngle = Angle.degrees 180
}
)Draw a Circle2d as an SVG <circle> with the given attributes.
circle : Svg msg
circle =
Svg.circle2d
[ Attributes.fill "orange"
, Attributes.stroke "blue"
, Attributes.strokeWidth "2"
]
(Circle2d.withRadius (Pixels.pixels 10)
(Point2d.pixels 150 150)
)Draw an Ellipse2d as an SVG <ellipse> with the given attributes.
ellipse : Svg msg
ellipse =
Svg.ellipse2d
[ Attributes.fill "orange"
, Attributes.stroke "blue"
, Attributes.strokeWidth "2"
]
(Ellipse2d.with
{ centerPoint = Point2d.pixels 150 150
, xDirection = Direction2d.degrees -30
, xRadius = Pixels.pixels 60
, yRadius = Pixels.pixels 30
}
)Draw a quadratic spline as an SVG <path> with the given attributes.
quadraticSpline : Svg msg
quadraticSpline =
let
firstControlPoint =
Point2d.pixels 50 50
secondControlPoint =
Point2d.pixels 100 150
thirdControlPoint =
Point2d.pixels 150 100
spline =
QuadraticSpline2d.fromControlPoints
firstControlPoint
secondControlPoint
thirdControlPoint
controlPoints =
[ firstControlPoint
, secondControlPoint
, thirdControlPoint
]
drawPoint point =
Svg.circle2d [] <|
Circle2d.withRadius (Pixels.pixels 3)
point
in
Svg.g [ Attributes.stroke "blue" ]
[ Svg.quadraticSpline2d
[ Attributes.strokeWidth "3"
, Attributes.strokeLinecap "round"
, Attributes.fill "none"
]
spline
, Svg.polyline2d
[ Attributes.strokeWidth "1"
, Attributes.fill "none"
, Attributes.strokeDasharray "3 3"
]
(Polyline2d.fromVertices controlPoints)
, Svg.g [ Attributes.fill "white" ]
(List.map drawPoint controlPoints)
]Draw a cubic spline as an SVG <path> with the given attributes.
cubicSpline : Svg msg
cubicSpline =
let
firstControlPoint =
Point2d.pixels 50 50
secondControlPoint =
Point2d.pixels 100 150
thirdControlPoint =
Point2d.pixels 150 25
fourthControlPoint =
Point2d.pixels 200 125
spline =
CubicSpline2d.fromControlPoints
firstControlPoint
secondControlPoint
thirdControlPoint
fourthControlPoint
controlPoints =
[ firstControlPoint
, secondControlPoint
, thirdControlPoint
, fourthControlPoint
]
drawPoint point =
Svg.circle2d [] <|
Circle2d.withRadius (Pixels.pixels 3)
point
in
Svg.g [ Attributes.stroke "blue" ]
[ Svg.cubicSpline2d
[ Attributes.strokeWidth "3"
, Attributes.strokeLinecap "round"
, Attributes.fill "none"
]
spline
, Svg.polyline2d
[ Attributes.strokeWidth "1"
, Attributes.fill "none"
, Attributes.strokeDasharray "3 3"
]
(Polyline2d.fromVertices controlPoints)
, Svg.g [ Attributes.fill "white" ]
(List.map drawPoint controlPoints)
]Draw a bounding box as an SVG <rect> with the given attributes.
These functions allow you to use all the normal elm-geometry 2D transformations on
arbitrary fragments of SVG. For example,
Svg.mirrorAcross Axis2d.x
(Svg.lineSegment2d [] lineSegment)draws a line segment as SVG and then mirrors that SVG fragment. This is visually the same as
Svg.lineSegment2d []
(LineSegment2d.mirrorAcross Axis2d.x lineSegment)which instead mirrors the line segment first and then draws the mirrored line segment as SVG.
In the above example only a single SVG element was transformed, but all of these transformation functions work equally well on arbitrarily complex fragments of SVG such as nested groups of elements of different types:
Svg.rotateAround Point2d.origin
(Angle.degrees 30)
(Svg.g [ Attributes.stroke "blue" ]
[ Svg.lineSegment2d [] lineSegment
, Svg.circle2d [] someCircle
, Svg.g [ Attributes.fill "orange" ]
[ Svg.triangle2d [] firstTriangle
, Svg.triangle2d [] secondTriangle
]
]
)If the transformation changes frequently (an animated rotation angle, for example) while the geometry itself remains constant, using an SVG transformation can be more efficient since the geometry itself does not have to be recreated (the SVG virtual DOM only has to update a transformation matrix).
Scale arbitrary SVG around a given point by a given scale.
scaled : Svg msg
scaled =
let
scales =
[ 1.0, 1.5, 2.25 ]
referencePoint =
Point2d.pixels 100 100
referencePoint =
Svg.circle2d [ Attributes.fill "black" ] <|
Circle2d.withRadius (Pixels.pixels 3)
referencePoint
scaledCircle : Float -> Svg msg
scaledCircle scale =
Svg.scaleAbout referencePoint scale circle
in
Svg.g []
(referencePoint
:: List.map scaledCircle scales
)Note how everything is scaled, including the stroke width of the circles. This
may or may not be what you want; if you wanted the same stroke width on all
circles, you could instead scale the Circle2d values themselves using
Circle2d.scaleAbout and then draw the scaled circles with a specific stroke
width using Svg.circle2d.
Rotate arbitrary SVG around a given point by a given angle.
rotated : Svg msg
rotated =
let
angles =
Parameter1d.steps 9 <|
Quantity.interpolateFrom
(Angle.degrees 0)
(Angle.degrees 270)
referencePoint =
Point2d.pixels 200 150
referenceCircle =
Svg.circle2d [ Attributes.fill "black" ] <|
(Circle2d.withRadius (Pixels.pixels 3)
referencePoint
)
rotatedCircle : Float -> Svg msg
rotatedCircle angle =
Svg.rotateAround referencePoint angle circle
in
Svg.g [] <|
referenceCircle
:: List.map rotatedCircle anglesTranslate arbitrary SVG by a given displacement.
translated : Svg msg
translated =
Svg.g []
[ polyline
, polyline
|> Svg.translateBy (Vector2d.pixels 0 40)
, polyline
|> Svg.translateBy (Vector2d.pixels 5 -60)
]Mirror arbitrary SVG across a given axis.
mirrored : Svg msg
mirrored =
let
horizontalAxis =
Axis2d.through (Point2d.pixels 0 220)
Direction2d.x
horizontalSegment =
LineSegment2d.along horizontalAxis
(Pixels.pixels 50)
(Pixels.pixels 250)
angledAxis =
Axis2d.through (Point2d.pixels 0 150)
(Direction2d.degrees -10)
angledSegment =
LineSegment2d.along angledAxis
(Pixels.pixels 50)
(Pixels.pixels 250)
in
Svg.g []
[ polygon
, Svg.mirrorAcross horizontalAxis polygon
, Svg.mirrorAcross angledAxis polygon
, Svg.g
[ Attributes.strokeWidth "0.5"
, Attributes.stroke "black"
, Attributes.strokeDasharray "3 3"
]
[ Svg.lineSegment2d [] horizontalSegment
, Svg.lineSegment2d [] angledSegment
]
]Similar to the above transformations, these functions allow elm-geometry
coordinate conversion transformations to be applied to arbitrary SVG elements.
Convert SVG expressed in global coordinates to SVG expressed in coordinates
relative to a given reference frame. Using relativeTo can be useful for
transforming between model space and screen space - SVG coordinates start in the
top left, so positive Y is down, while in mathematical/geometric contexts
positive Y is usually up.
For example, you might develop an SVG scene in a coordinate system where X and Y each range from 0 to 300 and positive Y is up. To turn this into a 300x300 SVG drawing, first define the top-left SVG frame (coordinate system) in terms of the model coordinate system:
topLeftFrame =
Frame2d.atPoint (Point2d.pixels 0 300)
|> Frame2d.reverseY(As expressed in the model frame, the top-left SVG frame is at the point
(0, 300) and its Y direction is equal to the global negative Y direction.) If
scene is an SVG element representing your scene, you could then transform it
into top-left SVG window coordinates and render the result to HTML with
Svg.svg
[ Attributes.width "300"
, Attributes.height "300"
]
[ Svg.relativeTo topLeftFrame scene ]Take SVG defined in local coordinates relative to a given reference frame, and return that SVG expressed in global coordinates.
This can be useful for taking a chunk of SVG and 'stamping' it in different positions with different orientations:
placed : Svg msg
placed =
let
vertices =
[ Point2d.origin
, Point2d.pixels 40 0
, Point2d.pixels 50 25
, Point2d.pixels 10 25
]
stamp =
Svg.polygon2d
[ Attributes.fill "orange"
, Attributes.stroke "blue"
, Attributes.strokeWidth "2"
]
(Polygon2d.singleLoop vertices)
frames =
[ Frame2d.atPoint (Point2d.pixels 25 25)
, Frame2d.atPoint (Point2d.pixels 100 25)
, Frame2d.atPoint (Point2d.pixels 175 25)
|> Frame2d.rotateBy (Angle.degrees 20)
, Frame2d.atPoint (Point2d.pixels 25 150)
, Frame2d.atPoint (Point2d.pixels 100 100)
|> Frame2d.rotateBy (Angle.degrees 20)
, Frame2d.atPoint (Point2d.pixels 150 150)
|> Frame2d.rotateBy (Angle.degrees -30)
]
in
Svg.g []
(frames
|> List.map
(\frame -> Svg.placeIn frame stamp)
)
Draw a
LineSegment2das an SVG<polyline>with the given attributes.