MetaPict
(require metapict) | package: metapict |
1 Introduction
The metapict library provides functions and data structures useful for generating picts. The library includes support for points, vectors, Bezier curves, and, general curves.
The algorithm used to calculate a nice curve from points and tangents is the same as the one used in MetaPost.
With this library I to hope narrow the gap between Scribble and LaTeX + MetaPost/Tikz. If you find any features in MetaPost or Tikz that you miss, don’t hesitate to mail me.
2 Guide
2.1 Coordinates
2.1.1 Points
Note: This is different from racket/pict which reverses the direction of the y-axis.
Consider these points:
p1=(0,100) |
| p2=(100,100) |
| p3=(200,100) |
p4=(0,0) |
| p5=(100,0) |
| p6=(200,0) |
Notice that the point p4=(0,0) is the reference point. The point p3=(200,100) is located 200 units to the right of p4 and 100 units upwards.
In order to write a MetaPict program to draw a shape, a good strategy is to draw the shape on paper. Determine the coordinates for the key points, and then write the MetaPict program that draws lines or curves between the points.
The .. between the two points connects the two points with a line.
> (def p1 (pt 0 100)) > (def p2 (pt 100 100)) > (def p3 (pt 200 100)) > (def p4 (pt 0 0)) > (def p5 (pt 100 0)) > (def p6 (pt 200 0))
> (with-window (window -10 210 -5 105) (draw (curve p1 .. p6)))
If you zoom, you will see that the lines have a thickness and that the ends are rounded. Imagine that you have a pen with a circular nib. The drawings produced by MetaPict will try to mimick the result you get by drawing with such a pen. In the chapter on pens you will learn to the control the thickness of the pen and the shape of the ends of lines.
2.1.2 Displacements
In the example above the point p2=(100,100) was described as being 100 to the right and 100 upwards relative to the reference point (0,0).
An alternative way of describing the location of p2 would be to say that is located 100 to the right of p1 (and 0 upwards).
Such a displacement can be described with a vector. Since Racket uses the name "vector", we will represent displacement vectors with a vec structure. To displace a point p with a vector v, use pt+.
> (def v (vec 100 0)) > (def p1 (pt 0 100)) > (def p2 (pt+ p1 v)) > (def p3 (pt+ p2 v)) > (def p4 (pt 0 0)) > (def p5 (pt+ p4 v)) > (def p6 (pt+ p5 v))
> (with-window (window -10 210 -5 105) (draw (curve p1 .. p6) (curve p2 .. p5) (curve p3 .. p4)))
Note: defv is short for define-values.
> (def v (vec 100 0)) > (def 2v (vec* 2 v)) > (defv (p1 p2 p3) (values (pt 0 100) (pt+ p1 v) (pt+ p1 2v))) > (defv (p4 p5 p6) (values (pt 0 0) (pt+ p4 v) (pt+ p4 2v)))
> (with-window (window -10 210 -5 105) (draw (curve p1 .. p6) (curve p2 .. p5) (curve p3 .. p4)))
The displacements left, right, up, and, down. are predefined. As are the vector operations vec+,vec-, and, vec*. The displacement that moves a point a to point b is given by (pt- b a).
> (list left right up down) (list (vec -1 0) (vec 1 0) (vec 0 1) (vec 0 -1))
> (vec+ left up) (vec -1 1)
> (vec- left up) (vec -1 -1)
> (vec* 3 right) (vec 3 0)
> (pt- (pt 2 4) (pt 7 8)) (vec -5 -4)
It is common to need points that lie between two point A and B. The mediation operation is called med. The call (med 0.25 A B) will compute the point M on the line from A to B whose distance from A is 25% of the length of AB.
> (def A (pt 0 0)) > (def B (pt 3 2))
> (with-window (window -1 4 -1 3) (draw (dot-label "A" A (top)) (dot-label "C" (med 0.25 A B) (top)) (dot-label "D" (med 0.5 A B) (bot)) (dot-label "E" (med 0.75 A B) (bot)) (dot-label "B" B (bot))))
Note: (med x A B) is equivalent to (pt+ A (vec* x (pt- B A))).
Let’s use the knowledge from this section to write a small program to generate the character A. The shape depends on the parameters w (width), h (height) and the placement of the bar α.
> (define (A w h α) (set-curve-pict-size w h) (def p1 (pt 0 0)) (def p2 (pt (/ w 2) h)) (def p3 (pt w 0)) (def p4 (med α p1 p2)) (def p5 (med α p3 p2)) (with-window (window 0 w 0 h) (draw (curve p1 .. p2) (curve p2 .. p3) (curve p4 .. p5))))
> (list (A 10 20 0.3) (A 10 20 0.4) (A 10 20 0.5) (A 10 20 0.6)) '(
![]()
![]()
![]()
)
3 Reference
3.1 Representation
(require metapict/structs) | package: metapict |
This section describes the representation of the MetaPict concepts.
> (def A (pt 3 4)) > A (pt 3 4)
> (defm (pt x y) A) > (list x y) '(3 4)
> (penwidth 4 (draw (pt 0 0) (pt 1/2 1/2) (pt 1 0)))
> (def v (vec 3 4)) > v (vec 3 4)
> (defm (vec x y) v) > (list x y) '(3 4)
> (def O origo)
> (with-window (window -1 5 -1 5) (ahlength (px 5)) (draw-arrow (curve O -- (pt+ O v))))
struct
(struct bez (p0 p1 p2 p3) #:extra-constructor-name make-bez) p0 : pt? p1 : pt? p2 : pt? p3 : pt?
struct
(struct window (minx maxx miny maxy) #:extra-constructor-name make-window) minx : real? maxx : real? miny : real? maxy : real?
> (draw unitcircle (curve (pt 0 1.4) -- (pt 0 0) -- (pt 1.4 0) -- cycle))
> (with-window (window -2 2 -2 2) (draw unitcircle (curve (pt 0 1.4) -- (pt 0 0) -- (pt 1.4 0) -- cycle)))
3.2 Points and Vectors (pt and vec)
(require metapict/pt-vec) | package: metapict |
3.2.1 Points and Vectors
Points and vectors are represented as pt and vec structures respectively. Think of points as positions and of vectors as displacements.
> (def A (pt 3 4)) > A (pt 3 4)
> (pt-x A) 3
> (penwidth 4 (draw (pt 0 0) (pt 1/2 1/2) (pt 1 0)))
3.2.1.1 Predefined Points and Vectors
The most common points and vectors have predefined names.
> (penwidth 4 (draw (color "red" (draw origo)) (color "green" (draw (pt+ origo north))) (color "blue" (draw (pt+ origo south))) (color "magenta" (draw (pt+ origo left))) (color "purple" (draw (pt+ origo right)))))
3.2.1.2 Point Operations
procedure
A : pt? v : vec? (pt+ A B) → pt? A : pt? B : pt? (pt+ A B-or-v ...) → pt? A : pt B-or-v : (or pt? vec?)
The form (pt+ A v) returns the displacement of the point A with the vector v. That is, (a1+v1,a2+v2) is returned.
The form (pt+ A B) adds the coordinates of A and B pairwise. The point A is thus displaced with the vector OB. That is, (a1+b1,a2+b2) is returned.
The form (pt+) returns origo, (pt 0 0).
The form (pt+ A) returns the point A.
> (pt+ (pt 1 2) (vec 3 7)) (pt 4 9)
> (pt+ (pt 1 2) (pt 3 7)) (pt 4 9)
> (pt+) (pt 0 0)
> (pt+ (pt 1 2)) (pt 1 2)
> (pt+ (pt 0.3 0.4) (vec 3 0) (vec 4 0)) (pt 7.3 0.4)
The form (pt- A v) returns the displacement of the point A with the opposite of vector v. If A=(a1,a2) and v=(v1,v2) then the vector (a1-v1,a2-v2) is returned.
The form (med 1/3 A B) returns the point that lies one-third of the way from A to B.
> (def A (pt 0 0)) > (def B (pt 2 1)) > (list (med 0 A B) (med 1/3 A B) (med 1/2 A B) (med 2/3 A B) (med 1 A B)) (list (pt 0 0) (pt 2/3 1/3) (pt 1 1/2) (pt 4/3 2/3) (pt 2 1))
> (set-curve-pict-size 100 50)
> (with-window (window -0.2 2.2 -0.1 1.1) (penwidth 4 (draw* (for/list ([r '(0 1/3 1/2 2/3 1)] [c '("red" "orange" "yellow" "green" "blue")]) (color c (draw (med r A B)))))))
> (require racket/math) > (set-curve-pict-size 50 50)
> (with-window (window -1.1 1.1 -1.1 1.1) (penwidth 4 (draw* (for/list ([θ (in-range 0 (* 2 pi) (/ (* 2 pi) 12))]) (pt@ 1 θ)))))
> (pt@d 1 45) (pt 0.7071067811865476 0.7071067811865475)
> (pt@ 1 (/ pi 4)) (pt 0.7071067811865476 0.7071067811865475)
3.2.1.3 Vector Operations
In this section the coordinates of vecs v and w will be referred to as v=(v1,v2) and w=(w1,w2).
In terms of displacements the vector sum v+w can be thought of as the result of the displament v followed by the displacement w.
> (def v (vec 2 0)) > (def w (vec 0 3)) > (def v+w (vec+ v w)) > v+w (vec 2 3)
> (define (arrow v [offset (vec 0 0)]) (def A (pt+ origo offset)) (draw-arrow (curve A -- (pt+ A v)))) > (ahlength (px 12))
> (with-window (window -0.2 3.2 -0.2 3.2) (penwidth 2 (draw (color "red" (arrow v)) (color "green" (arrow w v)) (color "blue" (arrow v+w)))))
> (rot90 (pt 1 0)) (pt 0 1)
> (rot90 (vec 1 0)) (vec 0 1)
> (rot-90 (pt 1 0)) (pt 0 -1)
> (rot-90 (vec 1 0)) (vec 0 -1)
3.3 Colors
(require metapict/color) | package: metapict |
procedure
(make-color* name) → (is-a?/c color%)
name : string? (make-color* r g b α) → (is-a?/c color%) r : real? g : real? b : real? α : 1.0
Given a color name as a string, make-color* returns a color% object.
Given real numbers to use as the color components, make-color* works like make-color, but accepts both non-integer numbers, and numbers outside the range 0–255. For a real number x the value used is (min 255 (max 0 (exact-floor x))).
The optional argument α is the transparency. The default value is 1. Given a transparency outside the interval 0–1 whichever value of 0 and 1 is closest to α is used.
> (def red-ish (make-color* 300 -12 42.3)) > (def purple (make-color* "purple")) > (color->list red-ish) '(255 0 42 1.0)
> (color->list purple) '(160 32 240 1.0)
> (with-window (window 0 1 0 1) (beside (color red-ish (fill unitsquare)) (color purple (fill unitsquare))))
procedure
c : (is-a?/c color%) p : pict? (color f c p) → pict? f : real? c : (is-a?/c color%) p : pict?
As a match pattern (color r g b a) matches both color% objects and color names (represented as strings). The variables r, g, and, b will be bound to the red, green, and, blue components of the color. The variable a will be bound to the transparency.
> (with-window (window 0 1 0 1) (apply beside (for/list ([f (in-range 0 11/10 1/10)]) (color f "red" (fill unitsquare)))))
> (require racket/match)
> (match "magenta" [(color r g b a) (list r g b a)]) '(255 0 255 1.0)
procedure
(color->list c) → (listof real?)
c : color
> (color->list "magenta") '(255 0 255 1.0)
> (color->list (color+ "red" "blue")) '(255 0 255 1.0)
> (color->list (color* 0.5 "blue")) '(0 0 127 1.0)
> (with-window (window 0 1 0 1) (apply beside (for/list ([t (in-range 0 11/10 1/10)]) (color (color-med t "red" "yellow") (fill unitsquare)))))
procedure
(color-med* t cs) → (is-a?/c color%)
t : real? cs : (listof color)
> (with-window (window 0 1 0 1) (apply beside (for/list ([t (in-range 0 11/10 1/10)]) (color (color-med* t '("red" "yellow" "blue" "green")) (fill unitsquare)))))
procedure
(change-red c r) → (is-a?/c color%)
c : color r : real?
procedure
(change-blue c r) → (is-a?/c color%)
c : color r : real?
procedure
(change-green c r) → (is-a?/c color%)
c : color r : real?
procedure
(change-alpha c r) → (is-a?/c color%)
c : color r : real?
> (color->list (change-red "blue" 42)) '(42 0 255 1.0)
3.4 Pict
(require metapict/pict) | package: metapict |
3.4.1 Pict Adjusters
All images in MetaPict are represented as picts. A pict is a structure that holds information on how to draw a picture. A pict can be rendered to produce an image in various formats such as png, pdf, and, svg.
The standard library pict defines several functions to construct and manipulate picts. MetaPict provides and offers some extra operations. Since they are not MetaPict specific, they are also useful outside of the world of MetaPict.
A few of the pict operations are provided under new names. The basic concept in MetaPict is the curve. Therefore it makes sense for, say, circle to return a curve. In the pict library the name circle returns a pict, so to avoid a name conflict it is exported as circle-pict.
An attempt have been made to make the pict the last argument of all operations. This explains the existance of a few functions whose functionality overlap with the pict library.
The operations in this section operate on picts, so use draw to convert curves into picts.
3.4.1.1 Pen Adjusters
> (color "red" (filldraw unitcircle))
> (penwidth 4 (beside (pencolor "red" (brushcolor "orange" (filldraw unitcircle))) (pencolor "red" (filldraw unitcircle))))
> (beside (penwidth 3 (penscale 2 (draw unitcircle))) (penscale 3 (penwidth 2 (draw unitcircle))) (penwidth 6 (draw unitcircle)))
> (define (styled-circle style) (draw (color "red" (filldraw unitsquare)) (penstyle style (draw unitcircle)) (label-bot (~a style) (pt 0 0)))) > (def styles1 '(solid transparent hilite)) > (def styles2 '(dot short-dash long-dash dot-dash))
> (above (beside* (map styled-circle styles1)) (beside* (map styled-circle styles2)))
> (define (squiggle cap) (def l (curve (pt -1/2 0) -- (pt 0 0) .. (pt 1/2 1/2))) (penwidth 20 (draw (pencap cap (color "red" (draw l))) (pencap 'butt (color "black" (draw l))) (label-bot (~a cap) (pt 0 -1/2))))) > (def caps '(round projecting butt)) > (beside* (map squiggle caps))
> (define (squiggle join) (def l (curve (pt -1/2 0) -- (pt 0 0) .. (pt 1/2 1/2))) (draw (penwidth 40 (penjoin join (draw l))) (penwidth 2 (color "red" (draw (circle (pt 1/4 -1/3) 1/3)))) (label-bot (~a join) (pt 0 -1/2)))) > (def joins '(round bevel miter)) > (beside* (map squiggle joins))
> (def teacher-pen (new pen% [color "red"] [width 1] [style 'solid] [cap 'round] [join 'round] [stipple #f])) > (pen teacher-pen (draw unitcircle))
3.4.1.2 Brush Adjusters
> (def hatch (new brush% [color "black"] [style 'crossdiag-hatch])) > (brush hatch (filldraw unitcircle))
procedure
(brushcolor c p) → pict
c : color? p : pict?
> (brushcolor "red" (fill unitcircle))
procedure
(brushstyle s p) → pict
s : style? p : pict?
> (define (styled-circle style) (draw (color "red" (filldraw (scaled 0.7 unitsquare))) (brushcolor "black" (brushstyle style (fill (scaled 0.7 unitcircle)))) (brushcolor "white" (draw (label-bot (~a style) (pt 0 -0.7)))))) > (def styles1 '(solid transparent hilite)) > (def styles2 '(bdiagonal-hatch fdiagonal-hatch crossdiag-hatch)) > (def styles3 '(horizontal-hatch vertical-hatch cross-hatch))
> (above (beside* (map styled-circle styles1)) (beside* (map styled-circle styles2)) (beside* (map styled-circle styles3)))
procedure
(brushstipple s p) → pict
s : style? p : pict?
> (set-curve-pict-size 256 256) > (define stipple (bitmap "texture.png"))
> (with-window (window -1 1 -1 1) (beside stipple (blank 64 64) (brushstipple (pict->bitmap stipple) (fill (circle 1)))))
procedure
(brushgradient TODO:TO-BE-DETERMINED) → pict
TODO:TO-BE-DETERMINED : gradient?
3.5 Bezier Curves
(require metapict/bez) | package: metapict |
A Bezier curve from point A to point B with control points A+ and B- is represented as an instance of a bez structure: (bez A A+ B- B).
Graphically such a curve begins at point A and ends in point B. The curve leaves point A directed towards the control point A+. The direction in which the curve enters the end point B is from B-.
The points A and B are referred to as start and end point of the Bezier curve. The points A+ and B- are refererred to as control points. The point A+ is the post control of A and the point B- is the pre control of B.
Most users will not have reason to work with bez structures directly. The curve constructor is intended to cover all use cases.
Each point on the Bezier curve corresponds to a real number t between 0 and 1. The correspondence is called a parameterization of the curve. The number t is called a parameter. Thus for each value of the parameter t between 0 and 1, you get a point on the curve. The parameter value t=0 corresponds to the start point A and the parameter value t=1 corresponds to the end point.
Let’s see an example of a Bezier curve and its construction.
procedure
(point-of-bez b t) → pt?
b : bez? t : real?
> (def b (bez (pt 0 0) (pt 0 1) (pt 3 2) (pt 5 0)))
> (for/list ([t '(0 1/2 1)]) (point-of-bez b t)) (list (pt 0 0) (pt 7/4 9/8) (pt 5 0))
> (bez~ (bez (pt 0 0) (pt 0 1) (pt 3 2) (pt 5 0)) (bez (pt #i5e-05 0) (pt 0 1) (pt 3 2) (pt 5 0))) #t
procedure
(bez-reverse b) → bez
b : bez?
> (def b (bez (pt 0 0) (pt 0 1) (pt 3 2) (pt 5 0))) > (def (γ t) (point-of-bez b t)) > (def (φ t) (point-of-bez (bez-reverse b) t)) > (def ts (in-range 0 5/4 1/4)) > (cons 'γ (for/list ([t ts]) (γ t))) (list 'γ (pt 0 0) (pt 1/2 45/64) (pt 7/4 9/8) (pt 27/8 63/64) (pt 5 0))
> (cons 'φ (for/list ([t ts]) (φ t))) (list 'φ (pt 5 0) (pt 27/8 63/64) (pt 7/4 9/8) (pt 1/2 45/64) (pt 0 0))
> (def b (bez (pt 0 0) (pt 0 1) (pt 3 2) (pt 5 0))) > (defv (b1 b2) (split-bez b 1/3))
> (with-window (window -1 6 -1 6) (penwidth 4 (draw (color "red" (draw b1)) (color "blue" (draw b2)))))
procedure
(bez-subpath b t0 t1) → bez?
b : bez? t0 : real? t1 : real?
> (def b (bez (pt 0 0) (pt 0 1) (pt 3 2) (pt 5 0)))
> (with-window (window -1 6 -1 6) (for/draw ([t (in-range 0 5/4 1/4)] [c '("red" "blue" "green" "magenta")]) (penwidth 4 (beside (draw b) (color c (draw (bez-subpath b t (+ t 1/4))))))))
procedure
(bez-intersection-point b1 b2) → (or pt? #f)
b1 : bez? b2 : bez?
> (def b1 (bez (pt 0.0 0.0) (pt 1.0 1.0) (pt 2.0 2.0) (pt 3.0 3.0))) > (def b2 (bez (pt 0.0 3.0) (pt 1.0 2.0) (pt 2.0 1.0) (pt 3.0 0.0))) > (defv (p) (bez-intersection-point b1 b2)) > p (pt 1.4999999999999996 1.4999999999999996)
> (def b3 (bez (pt 0 4) (pt 1 4) (pt 2 4) (pt 3 4))) > (bez-intersection-point b1 b3) #f
> (with-window (window 0 5 0 5) (draw b1 b2 (color "red" (penwidth 8 (draw p))) b3))
procedure
(bez-intersection-times b1 b2) →
real? real? b1 : bez? b2 : bez?
> (def b1 (bez (pt 0 0) (pt 1 1) (pt 2 2) (pt 3 3))) > (def b2 (bez (pt 0 3) (pt 1 2) (pt 2 1) (pt 3 0))) > (defv (t1 t2) (bez-intersection-times b1 b2)) > (defv (p1 p2) (values (point-of-bez b1 t1) (point-of-bez b2 t2))) > (list p1 p2)
(list
(pt 1.4999999999999998 1.4999999999999998)
(pt 1.4999999999999998 1.5000000000000002))
> (def b3 (bez (pt 0 4) (pt 1 4) (pt 2 4) (pt 3 4))) > (bez-intersection-times b1 b3) #f
> (with-window (window 0 5 0 5) (draw b1 b2 (color "red" (penwidth 8 (draw p1))) b3))
procedure
(bez-intersection-point-and-times b1 b2)
→ (or (list pt? real? real?) #f) b1 : bez? b2 : bez?
> (def b1 (bez (pt 0.0 0.0) (pt 1.0 1.0) (pt 2.0 2.0) (pt 3.0 3.0))) > (def b2 (bez (pt 0.0 3.0) (pt 1.0 2.0) (pt 2.0 1.0) (pt 3.0 0.0))) > (bez-intersection-point-and-times b1 b2)
(list
(pt 1.4999999999999996 1.4999999999999996)
0.4999999999999999
0.4999999999999999)
> (defm (list p t1 t2) (bez-intersection-point-and-times b1 b2)) > (def b3 (bez (pt 0 4) (pt 1 4) (pt 2 4) (pt 3 4))) > (bez-intersection-times b1 b3) #f
> (with-window (window 0 5 0 5) (draw b1 b2 (color "red" (penwidth 8 (draw p))) b3))
procedure
(draw-bez dc b [ #:transformation t #:pen-transformation pent]) → (void) dc : (is-a dc<%>) b : bez? t : trans? = #f pent : trans? = #f
procedure
(draw-bezs dc bs [ #:transformation t #:pen-transformation pent]) → (void) dc : (is-a dc<%>) bs : (listof bez?) t : trans? = #f pent : trans? = #f
procedure
(bez->dc-path b [t]) → (is-a? dc<%>)
b : bez? t : trans? = #f
procedure
(bezs->dc-path bs [t]) → (is-a? dc<%>)
bs : (listof bez?) t : trans? = #f
procedure
(bez/dirs+tensions p0 p3 w0 w3 [τ0 τ3]) → bez?
p0 : pt? p3 : pt? w0 : vec? w3 : vec? τ0 : real? = 1 τ3 : real? = 1
> (defv (p0 p3 w0 w3 τ0 τ3) (values (pt 0 0) (pt 5 0) (vec 0 1) (vec 0 -1) 1 1)) > (def b (bez/dirs+tensions p0 p3 w0 w3 τ0 τ3)) > b
(bez
(pt 0 0)
(pt 2.041077998578922e-16 3.333333333333333)
(pt 5.000000000000001 -3.333333333333335)
(pt 5 0))
> (with-window (window -5 11 -5 11) (draw b))
procedure
(control-points p0 p3 θ φ τ0 τ3) → bez?
p0 : pt? p3 : pt θ : real? φ : real? τ0 : real? τ3 : real?
> (defv (p0 p3 θ φ τ0 τ3) (values (pt 0 0) (pt 5 0) pi/2 -pi/2 1 1)) > (defv (p1 p2) (control-points p0 p3 θ φ τ0 τ3)) > (def b (bez p0 p1 p2 p3)) > b
(bez
(pt 0 0)
(pt 2.041077998578922e-16 3.333333333333333)
(pt 5.0 -3.333333333333333)
(pt 5 0))
> (with-window (window -5 11 -5 11) (draw b))
3.6 Curves
(require metapict/curve) | package: metapict |
General curves are drawn by gluing together a series of Bezier curves. Conceptually a curve consists of multiple Bezier curves glued together. Such a curve can be either open or closed (a loop).
The representation of a curve consists simply of a list of Bezier curves and a flag indicating whether the curve is closed. For two consecutive Bezier curves in the list, the end point of the first and the start point of the second must be equal.
The actual representation uses a curve: structure: (struct curve: (closed? bezs) #:transparent #:reflection-name 'curve).
Most users will not have reason to work with curve: structures directly. The curve constructor is intended to cover all use cases. The constructor can be used to construct both curved as well as straight lines.
The syntax of curve will be detailed later, but let’s look at some examples. First when mulitple points separated by .. are given, a smooth curve through the points will be constructed: (curve p0 .. p1 .. p2 .. p3 .. p4).
As a concrete example, let’s look at the points (0,0) (60,40) (40,90) (10,70) (30,50).
> (def p0 (pt 0 0)) > (def p1 (pt 60 40)) > (def p2 (pt 40 90)) > (def p3 (pt 10 70)) > (def p4 (pt 30 50))
> (define (label-points) (for/draw ([i '(0 1 2 3 4)] [p (list p0 p1 p2 p3 p4)] [d (list (lft) (rt) (top) (lft) (top))]) (dot-label (~a i) p d))) > (set-curve-pict-size 120 120)
> (with-window (window -20 100 -20 100) (draw (curve p0 .. p1 .. p2 .. p3 .. p4) (label-points)))
> (with-window (window -20 100 -20 100) (draw (curve p0 .. p1 .. p2 .. p3 .. p4 .. cycle) (label-points)))
There is more to the curve constructor (it is possible to specify directions in which a curve enters and leaves a points), but let’s return to operations defined on curves.
The number of Bezier curves used to represent the curve is called the length. The function curve-length returns this length n.
Each point on a curve corresponds to a real number t between 0 and n. The correspondence is called a parameterization of the curve. The number t is called a parameter. Thus for each value of the parameter t between 0 and n, you get a point on the curve. The parameter value t=0 corresponds to the start point A and the parameter value t=n corresponds to the end point.
procedure
(curve-length c) → integer?
c : curve?
> (curve-length (curve p0 .. p1 .. p2 .. p3 .. p4)) 4
> (let () (define c (curve p0 .. p1 .. p2 .. p3 .. p4)) (define (label-parameter-values) (for/draw ([i '(0 1 2 3 4)] [d (list (lft) (rt) (top) (lft) (top))]) (define p (point-of c i)) (dot-label (~a "t=" i) p d))) (set-curve-pict-size 120 120) (with-window (window -20 100 -20 100) (draw c (label-parameter-values))))
Since the start and end point of a curve are used often, the following short hands are available:
procedure
(start-point c) → pt?
c : curve?
> (let () (def c (curve (pt 0 0) .. (pt 1 3) .. (pt 2 5))) (list (start-point c) (end-point c))) (list (pt 0 0) (pt 2.0 5.0))
Given a curve c parameterized from 0 to n from a start point to an end point, one can use curve-reverse to create a curve where the parameterization is reversed.
procedure
(curve-reverse c) → curve?
c : curve?
> (let () (def c (curve (pt 0 0) .. (pt 1 3) .. (pt 2 5))) (def r (curve-reverse c)) (list (start-point c) (end-point c)) (list (start-point r) (end-point r))) (list (pt 2 5) (pt 0.0 0.0))
procedure
(curve-append c1 c2) → curve?
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 1 1))) (def c2 (curve (pt 1 1) .. (pt 2 0))) (def c (curve-append c1 c2)) (with-window (window 0 2 0 2) (draw (linewidth 6 (color "red" (draw c1))) (linewidth 6 (color "blue" (draw c2))) (linewidth 2 (color "white" (draw c))))))
We will now turn to operations involving two curves. The first problem we will look at is intersections between two curves.
procedure
(intersection-times c1 c2) → (or #f number?)
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 2 3))) (def c2 (curve (pt 0 2) .. (pt 4 1))) (defv (t u) (intersection-times c1 c2)) (list (list 't t 'u u) (list (point-of c1 t) (point-of c2 u))))
(list
'(t 0.5714285714285715 u 0.2857142857142857)
(list
(pt 1.1428571428571432 1.7142857142857146)
(pt 1.1428571428571428 1.7142857142857144)))
procedure
(intersection-point c1 c2) → (or #f pt?)
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 2 4))) (def c2 (curve (pt 0 4) .. (pt 4 0))) (def P (intersection-point c1 c2)) (with-window (window 0 4 0 4) (draw c1 c2 P (dot-label-rt "P" P))))
procedure
(intersection-point-and-times c1 c2)
→ (or #f (list pt? number? number?)) c1 : curve? c2 : curve?
procedure
(intersection-points c1 c2) → (list-of pt?)
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 3 1) .. (pt 2 3) .. (pt 2 1) .. (pt 2.5 1))) (def c2 (curve (pt 0 1) .. (pt 1 3) .. (pt 3 0) .. (pt 4 1))) (def Ps (intersection-points c1 c2)) (with-window (window -1 4 -1 4) (draw c1 (linewidth 2 (color "blue" (draw c2))) (color "red" (for/draw ([P Ps]) (dot-label "" P (cnt)))))))
If a curve is too long, the function subcurve can be used to make a shorter one.
> (let () (def c (curve (pt 0 0) up .. (pt 1 3) .. (pt 2 0) .. (pt 3 3) .. (pt 3 2))) (def s (subcurve c 1 3)) (with-window (window -1 4 -1 4) (draw (linewidth 6 (color "red" (draw c))) (linewidth 2 (color "white" (draw s))))))
procedure
(cut-before c1 c2) → curve?
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 2 2))) (def c2 (curve (pt 0 2) .. (pt 2 0))) (def c (cut-before c1 c2)) (with-window (window 0 2 0 2) (draw (linewidth 6 (color "red" (draw c1))) (linewidth 6 (color "blue" (draw c2))) (linewidth 2 (color "white" (draw c))))))
procedure
(cut-after c1 c2) → curve?
c1 : curve? c2 : curve?
> (let () (def c1 (curve (pt 0 0) .. (pt 2 2))) (def c2 (curve (pt 0 2) .. (pt 2 0))) (def c (cut-after c1 c2)) (with-window (window 0 2 0 2) (draw (linewidth 6 (color "red" (draw c1))) (linewidth 6 (color "blue" (draw c2))) (linewidth 2 (color "white" (draw c))))))
procedure
(post-control c t) → pt?
c : curve? t : number?
procedure
(pre-control c t) → pt?
c : curve? t : number?
procedure
(direction-of c t) → vec?
c : curve? t : number?
> (let () (def c (curve (pt 1 0) .. (pt 1 3) .. (pt 4 1))) (def p (point-of c 1)) (def d (direction-of c 1)) (with-window (window 0 6 0 6) (draw c (draw-arrow (curve p .. (pt+ p d))) (dot-label-top "P" p))))
> (let () (def c1 (curve (pt 1 0) .. (pt 1 3))) (def c2 (curve (pt 1 0) .. (pt 1 3) .. cycle)) (list (cyclic? c1) (cyclic? c2))) '(#f #t)
procedure
(intercurve α c1 c2) → curve?
α : number? c1 : curve? c2 : curve?
In other words we can interpolate curves.
> (let () (def heart ; a heart shaped curve (curve (pt 100 162) .. (pt 140 178) right .. (pt 195 125) down .. (pt 100 0) (curl 0) .. up (pt 5 125) .. right (pt 60 178) .. (pt 100 162))) (def fig-heart ; a heart pict (with-window (window -100 300 -100 300) (draw heart))) (def heart-line ; used to interpolate from line to heart below (curve (pt 100 0) -- (pt 300 0) -- (pt 200 0) -- (pt 100 0) -- (pt 0 0) -- (pt -100 0) -- (pt 100 0))) (def fig-line-to-heart ; shows interpolation between two curves of same length (with-window (window -100 300 -100 300) (draw* (for/list ([i (in-range 8)]) (def α (/ i 8)) (color α "red" (draw (intercurve α heart-line heart))))))) (beside fig-heart fig-line-to-heart))
3.7 Drawing and Filling
(require metapict/draw) | package: metapict |
3.7.1 Drawing and Filling
A curve represents the path of a curve. Use draw and fill to create a picture in the form of a pict. Given a single curve draw will use the current pen to stroke the path and fill will use the current brush to fill it.
The size of the pict created by draw and fill functions is determined by the parameters curve-pict-width and curve-pict-height.
The position of points, curves etc. are given in logical coordinates. A pict will only draw the section of the coordinate plane that is given by the parameter curve-pict-window. This parameter holds the logical window (an x- and y-range) that will be drawn.
Given no arguments a blank pict will be returned.
Given multiple arguments draw will convert each argument into a pict, then layer the results using cc-superimpose. In other words: it draw the arguments in order, starting with the first.
> (draw (curve (pt -1 0) .. (pt 0 1) .. (pt 1 0)) (pt 0 0) (bez (pt -1/2 0) (pt -1/2 1) (pt 1/2 1) (pt 1/2 0)) (label-bot "Origo" (pt 0 0)))
A curve divides the points of the plane in two: the inside and the outside. The inside is drawn with the brush and the outside is left untouched.
For a simple non-intersecting curve it is simple to decide whether a point is on the inside or outside. For self-intersecting curves the so-called winding rule is used. The winding rule is also used when filling multiple curves
Given a point P consider a ray from P towards infinity. For each intersection between the ray and the curve(s), determine whether the curve crosses right-to-left or left-to-right. Each right-to-left crossing counts as +1 and each left-to-right crossing as -1. If the total sum of the counts are non-zero, then then point will be filled.
> (defm (list r1 r2 r3 r4) (map curve-reverse circles))
For (fill c1 c3) the winding numbers are -1+1=0 and +1. Therefore the points inside c3 are not filled.
4 Examples
4.1 Rotating Triangle
This example was inspired by Alain Matthes’s rotated triangle TikZ example.
> (require metapict) > (def N 18) > (set-curve-pict-size 300 300)
> (with-window (window -0.1 1.1 -0.1 1.1) (defv (A B C) (values (pt 0 0) (pt@d 1 60) (pt 1 0))) (first-value (for/fold ([drawing (draw)] [A A] [B B] [C C]) ([n N]) (def triangle (curve A -- B -- C -- cycle)) (def shade (color-med (expt (/ n N) 0.4) "red" "yellow")) (def filled (color shade (fill triangle))) (values (draw drawing filled triangle) (med 0.12 A B) (med 0.12 B C) (med 0.12 C A)))))
4.2 Rooty Helix
The example shows the lengths of sqrt(n) for values of n from 1 to 86. The design is from Felix Lindemann’s rooty helix TikZ example.
> (require metapict) > (def max-r 86) > (def dark-green (make-color* 175 193 36)) > (def almost-black (make-color* 50 50 50))
> (define (shade r) (cond [(<= 0 r 1/2) (color-med (* 2 r) "white" dark-green)] [(<= r 1) (color-med (* 2 (- r 1/2)) dark-green almost-black)] [else (error 'shader (~a "got: " r))]))
> (define (spiral drawing max-r) (def (node p r) (def circ (circle p 1.5)) (def filled (color "white" (fill circ))) (def label (label-cnt (~a r) p)) (draw filled circ label)) (defv (spiral θ) (for/fold ([drawing drawing] [θ 0]) ([r (in-range 1 max-r)]) (def √r (sqrt r)) (def (rotθ c) (scaled 4 (rotated θ c))) (defv (A B C) (values (pt 0 0) (rotθ (pt √r 0)) (rotθ (pt √r 1)))) (def triangle (curve A -- B -- C -- cycle)) (def filled (color (shade (/ r 86)) (fill triangle))) (values (draw drawing filled triangle (node B r)) (+ θ (acos (sqrt (/ r (+ 1 r)))))))) (draw spiral (node (scaled 4 (pt@ (sqrt max-r) θ)) max-r))) > (set-curve-pict-size 600 600)
> (with-window (window -40 40 -40 40) (penwidth 0 (for/fold ([drawing (draw)]) ([r '(86 38 15)]) (spiral drawing r))))
4.3 Glider - Hacker Emblem
> (set-curve-pict-size 100 100)
> (with-window (window 0 3 0 3) (margin 5 (draw (grid (pt 0 0) (pt 3 3) (pt 0 0) #:step 1) (for/draw ([p (list (pt 0 0) (pt 1 0) (pt 2 0) (pt 2 1) (pt 1 2))]) (color "black" (fill (circle (pt+ p (vec 0.5 0.5)) 0.42)))))))
4.4 Puzzle: The Missing Square
The two figures are made from the same colored pieces. It seems a square is missing from the bottom figure.
> (define red (curve (pt 0 0) -- (pt 8 0) -- (pt 8 3) -- cycle)) > (define blue (curve (pt 0 0) -- (pt 5 0) -- (pt 5 2) -- cycle)) > (define green (curve (pt 0 0) -- (pt 5 0) -- (pt 5 2) -- (pt 2 2) -- (pt 2 1) -- (pt 0 1) -- cycle)) > (define yellow (curve (pt 0 0) -- (pt 2 0) -- (pt 2 1) -- (pt 5 1) -- (pt 5 2) -- (pt 0 2) -- cycle))
> (define (draw-pieces positions) (for/draw ([p positions] [d (list red green yellow blue)] [c (list "red" "green" "yellow" "blue")]) (def fill-color (change-alpha (color-med 0.2 c "magenta") 0.7)) (def piece (shifted p d)) (draw (color fill-color (fill piece)) piece))) > (set-curve-pict-size 400 (* 13/15 400))
> (with-window (window -1 14 -1 12) (define upper (list (pt 0 0) (pt 8 0) (pt 8 1) (pt 8 3))) (define lower (list (pt 5 2) (pt 8 0) (pt 5 0) (pt 0 0))) (margin 2 (draw (color "gray" (draw (grid (pt -1 -1) (pt 14 12) (pt 0 0) #:step 1))) (draw-pieces (map (shifted (pt 0 6)) upper)) (draw-pieces lower))))
4.5 The Olympic Rings
> (struct ring (center color)) > (define r1 (ring (pt -4 0) (make-color* 0 129 188))) > (define r2 (ring (pt -2 -1.8) (make-color* 252 177 49))) > (define r3 (ring (pt 0 0) (make-color* 35 34 35))) > (define r4 (ring (pt 2 -1.8) (make-color* 0 157 87))) > (define r5 (ring (pt 4 0) (make-color* 238 50 78)))
> (define (draw-rings . rings) (for/draw ([r rings]) (defm (ring p c) r) (def c1 (circle p 1.9)) (def c2 (circle p 1.5)) (draw (color c (fill c1 (curve-reverse c2))) (penwidth 4 (color "white" (draw c1 c2)))))) > (set-curve-pict-size 200 100)
> (with-window (window -6 6 -4 2) (draw (clipped (draw-rings r5 r4 r3 r2 r1) (rectangle (pt -6 2) (pt 6 -1.0))) (clipped (draw-rings r1 r2 r3 r4 r5) (rectangle (pt -6 -0.8) (pt 6 -3.8))))) clipped: expected a number of curves followed by a pict ,
got: (#<pict> #(struct:curve #t (#(struct:bez #(struct:pt -6
-1.0) #(struct:pt -2.0 -1.0) #(struct:pt 2.0 -1.0)
#(struct:pt 6 -1.0)) #(struct:bez #(struct:pt 6 -1.0)
#(struct:pt 6 0.0) #(struct:pt 6 1.0) #(struct:pt 6 2.0))
#(struct:bez #(struct:pt 6 2.0) #(struct:pt 2.0 2.0)
#(struct:pt -2.0 2.0) #(struct:pt -6 2.0)) #(struct:bez
#(struct:pt -6 2.0) #(struct:pt -6 1.0) #(struct:pt -6 0.0)
#(struct:pt -6 -1.0)))))
4.6 Cuboid
> (require metapict) > (def p1 (pt -7 1.5)) > (def p2 (pt 8 1.5)) > (def a1 (pt 0 0)) > (def a2 (pt 0 -2)) > (def a3 (med 0.8 p1 a2)) > (def a4 (med 0.8 p1 a1)) > (def a7 (med 0.7 p2 a2)) > (def a8 (med 0.7 p2 a1)) > (def a5 (intersection-point (curve a8 -- p1) (curve a4 -- p2))) > (def a6 (intersection-point (curve a7 -- p1) (curve a3 -- p2))) > (def f6 (curve a2 -- a3 -- a6 -- a7 -- cycle)) > (def f3 (curve a3 -- a4 -- a5 -- a6 -- cycle)) > (def f4 (curve a5 -- a6 -- a7 -- a8 -- cycle)) > (def (a i) (vector-ref (vector #f a1 a2 a3 a4 a5 a6 a7 a8) i)) > (set-curve-pict-size 300 240)
> (with-window (window -2 3 -2.5 1.5) (draw (for/draw ([f (list f3 f4 f6)] [c (map (λ (x) (color* x "gray")) '(0.9 0.7 0.6))]) (color c (fill f))) (penwidth 2 (for/draw ([line '((5 6) (3 6) (7 6) (1 2) (3 4) (7 8) (1 4) (1 8) (2 3) (2 7) (4 5) (8 5))]) (defm (list i j) line) (curve (a i) -- (a j)))) (penwidth 8 (color "red" (draw a1 a2 a3 a4 a5 a6 a7 a8)))))
4.7 RGB Triangle
This examples shows how linear gradients can be used to fill a triangle. The first example use gradients from one color to another along the edge of a triangle. The second example shows how fading from a color c to (change-alpha c 0) is done.
The rgb-triangle was inspired by Andrew Stacey’s RGB Triangle.
> (require file/convertible racket/draw racket/gui) cannot instantiate `racket/gui/base' a second time in the
same process
> (defv (O A B C) (values (pt 0 0) (pt@d 1 90) (pt@d 1 210) (pt@d 1 330)))
> (with-window (window -1 1 -1 1) (def ABC (curve A -- B -- C -- cycle)) (def (tri P Q c) (brushgradient P Q (list c (change-alpha c 0)) (fill ABC))) (draw (tri A (pt@d 1/2 (+ 90 180)) "red") (tri B (pt@d 1/2 (+ 210 180)) "green") (tri C (pt@d 1/2 (- 330 180)) "blue"))) brushgradient: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2 plus an optional argument with keyword
#:height-factor
given: 4
arguments...:
(pt 6.123233995736766e-17 1.0)
(pt -9.184850993605148e-17 -0.5)
(list "red" (object:color% ...))
#<pict>
> (with-window (window -1 1 -1 1) (def (tri P Q . colors) (brushgradient P Q colors (fill (curve P -- Q -- O -- cycle)))) (draw (tri A B "yellow" "red") (tri B C "red" "blue") (tri C A "blue" "yellow"))) brushgradient: arity mismatch;
the expected number of arguments does not match the given
number
expected: 2 plus an optional argument with keyword
#:height-factor
given: 4
arguments...:
(pt 6.123233995736766e-17 1.0)
(pt -0.8660254037844386 -0.5000000000000001)
'("yellow" "red")
#<pict>