On this page:
1 Blending numbers
2 Blending points
3 Drawing curves
4 Blending curves
5 Challenges
8.5

Lab 8: Blending curves

Note: Whenever you design or write a function, you need to follow the design recipe.

Note: Use the “Intermediate Student” language on this assignment.

Note: This lab is long, but don’t worry. You’ll still get credit even if you don’t finish the whole thing.

In this lab, we’ll write some programs to generate and draw lines and curves. Given two lines or curves, we will also be able to generate a line or curve that is a blend of the two. This technique is often used in computer graphics to create images and videos of different things that morph or blend together.

For example, check out the following Animorphs image.

There are two images, one of a child and the other of a tiger, along with the progression of images from child to tiger. The image closest to the child is more child than tiger, but as the images get closer to the tiger they become more tiger than child.

Doing the same with straight lines instead of images can create some beautiful art:

(The photo above is of a sculpture by Bailee Koi entitled “Over. Under. Through. Tighten. Over. Under. Through. Tighten. Over. Under. Through. Tighten. Over. Under. Through. Tighten.”, shown at our Grunwald Gallery of Art in April 2019.)

1 Blending numbers

Let’s start by blending numbers.

Exercise 1. Design a function number-between-20-and-180 that takes a number n between 0 and 100 as input and answers this question: Imagine you are walking on the number line from the number 20 to the number 180. Where are you after going n% of the way?
  • For example, if the input n is 0 then the output should be 20, since 20 is 0% on the way from 20 to 180.

  • If the input n is 50 then the output should be 100, since 100 is 50% on the way from 20 to 180.

  • Finally, if the input n is 100 then the output should be 180, since 180 is 100% on the way from 20 to 180.

Hint 1: Understand the 3 sentences above and turn them into 3 check-expects.

Hint 2: Think about the distance between the starting point 20 and the destination 180. What is 1% of that distance? Add a check-expect for when the input n is 1.

Hint 3: Don’t do any arithmetic in your head. Make DrRacket do it by putting the arithmetic in your definition.

Exercise 2. Design a function number-between-90-and-10 that takes a number n between 0 and 100 as input and answers this question: Imagine you are walking on the number line from the number 90 to the number 10. Where are you after going n% of the way?
  • For example, if the input n is 0 then the output should be 90, since 90 is 0% on the way from 90 to 10.

  • If the input n is 50 then the output should be 50, since 50 is 50% on the way from 90 to 10.

  • Finally, if the input n is 100 then the output should be 10, since 10 is 100% on the way from 90 to 10.

Hint 1: Understand the 3 sentences above and turn them into 3 check-expects.

Hint 2: Think about the distance between the starting point 90 and the destination 10. What is 1% of that distance? Add a check-expect for when the input n is 1.

Hint 3: The formula should be very similar to the previous one. The more similar, the better. That’s why you shouldn’t do any arithmetic in your head.

Exercise 3. Abstract from the previous two functions. Recall from Lecture 16: Abstraction the abstraction recipe for functions:
  1. Design at least 2 similar functions. You just did this.

  2. Identify inessential differences and revise functions to remove them.

    Hint! You may need to revise the arithmetic formulas to make the two functions more similar. For instance, one student had to change (- 90 (* 0.8 n)) to (+ 90 (* -0.8 n)). Another student had to change 1.6 back to (/ (- 180 20) 100).

  3. Circle essential differences and pick input names for each of them.

    Hint! If you circle anything bigger than a number, it is too big and you must go back to making the two functions more similar. You should end up with just two essential differences: the number 20 or 90, and the number 180 or 10.

  4. Design the abstraction by replacing differences with input names. (The new abstraction should have its own signature and purpose statement. If the original tests do not provide sufficient coverage for the abstraction, include additional tests.)

  5. Redefine the original functions using the new abstraction. (You don’t need to submit the original versions.) The redefined functions should have the same signatures and purpose statements as before as well as pass the same tests as before.

Call your new abstraction number-between. Remember that it should have its own signature and purpose statement. And don’t forget to redefine the original functions using the new abstraction.

2 Blending points

Let’s work our way up to points.

Exercise 4. Design a function
; line1 : NaturalNumber -> Posn
so that (line1 n) answers the following question: Imagine you are walking on an empty scene from (make-posn 20 90) to (make-posn 180 10). Where are you after going n% of the way?
  • For example, if the input n is 0 then the output should be the starting point (make-posn 20 90).

  • If the input n is 50 then the output should be (make-posn 100 50), since that is the midpoint between (make-posn 20 90) and (make-posn 180 10).

  • Finally, if the input n is 100 then the output should be the destination (make-posn 180 10).

Hint 1: Understand the 3 sentences above and turn them into 3 check-expects. Follow the design recipe, not the abstraction recipe.

Hint 2: You don’t need the Pythagorean theorem or trigonometry. Don’t even do any arithmetic. Just use number-between twice (once for X and once for Y), and package the results into a Posn.

Hint 3: If you’re not sure how to package two numbers into a Posn, review Lecture 14: Built-in structures Exercise 1.

Exercise 5. Design a function
; line2 : NaturalNumber -> Posn
so that (line2 n) answers the following question: Imagine you are walking on an empty scene from (make-posn 30 200) to (make-posn 10 0). Where are you after going n% of the way?
  • For example, if the input n is 0 then the output should be the starting point (make-posn 30 200).

  • If the input n is 50 then the output should be (make-posn 20 100), since that is the midpoint between (make-posn 30 200) and (make-posn 10 0).

  • Finally, if the input n is 100 then the output should be the destination (make-posn 10 0).

Hint 1: Understand the 3 sentences above and turn them into 3 check-expects. Follow the design recipe, not the abstraction recipe.

Hint 2: You don’t need the Pythagorean theorem or trigonometry. Don’t even do any arithmetic. Just use number-between twice (once for X and once for Y), and package the results into a Posn.

Hint 3: The formula should be very similar to the previous one. The more similar, the better. That’s why you shouldn’t do any arithmetic.

Exercise 6. Design a function
; posn-between : Posn Posn NaturalNumber -> Posn
so that (posn-between start end n) answers the following question: Imagine you are walking on an empty scene from start to end. Where are you after going n% of the way?
  • As with number-between, if the input n is 0 then the output should be start.

  • If the input n is 50 then the output should be the midpoint between start and end.

  • Finally, if the input n is 100 then the output should be end.

Hint 1: Understand the 3 sentences above and turn them into lots of check-expects.

Hint 2: You don’t need the Pythagorean theorem or trigonometry. Don’t even do any arithmetic. Just use number-between twice (once for X and once for Y), and package the results into a Posn.

Hint 3: Follow the template for processing Posns. It has no cond.

Exercise 7. Redefine line1 and line2 to use posn-between, without affecting what those functions do at all.

3 Drawing curves

Let’s draw some curves on a 200x200 canvas.
(require 2htdp/image)
(define background (empty-scene 200 200))

Here’s a function that draws a curve by drawing many points:
; draw-wave : NaturalNumber Image -> Image
; draw the first that-many points of a wave on the given image
(check-expect (draw-wave 0 background) background)
(check-expect (draw-wave 3 background) )
(check-expect (draw-wave 100 background) )
(define (draw-wave n bg)
  (cond [(= n 0) bg]
        [else (place-image (circle 1 "solid" "dark green")
                           (* n 2)
                           (+ 70 (* 30 (sin (/ n 5))))
                           (draw-wave (- n 1) bg))]))
Don’t worry about the formula for Y coordinates above, but do examine the number of dots in the expected outputs.

Exercise 8. Which data definition’s processing template does draw-wave follow? If you’re not sure, review Lecture 13: More self-reference Exercise 3.

We can draw a straight line in the same way:
; draw-line1 : NaturalNumber Image -> Image
; draw the first that-many points of a straight line on the given image
(check-expect (draw-line1 0 background) background)
(check-expect (draw-line1 3 background) )
(check-expect (draw-line1 100 background) )
(define (draw-line1 n bg)
  (cond [(= n 0) bg]
        [else (place-image (circle 1 "solid" "blue")
                           (posn-x (line1 n))
                           (posn-y (line1 n))
                           (draw-line1 (- n 1) bg))]))

Exercise 9. Which data definition’s processing template does draw-line1 follow? If you’re not sure, review Lecture 14: Built-in structures Exercise 5.

Exercise 10. How many dots is (draw-line1 99 background) made of?

Let’s now abstract from the two functions above, draw-wave and draw-line1. First we need to make them more similar. For that, we often need to introduce a little helper function.

Exercise 11. Download a clean copy of the two functions. Make sure the tests pass; they rely on your line1 function.

Exercise 12. Design a helper function
; wave : NaturalNumber -> Posn
so that you can then change the else clause of draw-wave to
[else (place-image (circle 1 "solid" "dark green")
                   (posn-x (wave n))
                   (posn-y (wave n))
                   (draw-wave (- n 1) bg))]
without affecting what draw-wave does at all.

Exercise 13. Finish abstracting from draw-wave and draw-line1. You should end up with just two essential differences:
  • the function wave or line1,

  • and the color string "dark green" or "blue".

Call your new abstraction draw. Remember that it should have its own signature and purpose statement. And don’t forget to redefine the original functions using the new abstraction.

Exercise 14. Use draw and line2 to draw a straight line from (make-posn 30 200) to (make-posn 10 0), made of 100 dots. What color would you like? We suggest red.

4 Blending curves

Helper functions like wave, line1, and line2 each represent a curve as a function from numbers to positions. We can make more curves by blending existing curves.

Here’s a curve that starts as a straight line and ends as a wave:
; curve1 : NaturalNumber -> Posn
; start like line1 and end like wave
(check-within (curve1 0) (line1 0) 0.001)
(check-within (curve1 100) (wave 100) 0.001)
(check-within (curve1 50)
              (posn-between (line1 50)
                            (wave 50)
                            50)
              0.001)
(define (curve1 n)
  (posn-between (line1 n) (wave n) n))

And here’s a curve that blends two straight lines:
; curve2 : NaturalNumber -> Posn
; start like line2 and end like line1
(check-expect (curve2 0) (line2 0))
(check-expect (curve2 100) (line1 100))
(check-expect (curve2 50)
              (posn-between (line2 50)
                            (line1 50)
                            50))
(define (curve2 n)
  (posn-between (line2 n) (line1 n) n))

Exercise 15. Draw the two pictures to the side above. You don’t have to use the same colors.

Exercise 16. Abstract from the previous two functions, curve1 and curve2. There are just two differences, which are both functions from NaturalNumber to Posn. Call your new abstraction blend. Remember that it should have its own signature and purpose statement. And don’t forget to redefine the original functions using the new abstraction.

Exercise 17. Use your new abstraction to draw a curve that starts like line2 and ends like wave. To draw it (using draw as 100 dots), you’ll have to design a helper function
; curve3 : NaturalNumber -> Posn

Exercise 18. Use your new abstraction to draw a curve that starts like line1 and ends like line2. To draw it (using draw as 100 dots), you’ll have to design a helper function
; curve4 : NaturalNumber -> Posn

Exercise 19. We can even blend curves that themselves result from blending. Use your new abstraction to draw a curve that starts like curve2 and ends like curve4. To draw it (using draw as 100 dots), you’ll have to design a helper function
; curve5 : NaturalNumber -> Posn

Exercise 20. Redefine line1 and line2 to use blend instead of posn-between. You’ll have to design helper functions that return the same Posn no matter what NaturalNumber they get.

Curves that are built up from straight lines by blending are called Bézier curves. The level of the blending, like the complexity of a recipe, is called the order of a Bézier curve:
  • A fixed point has order 0.

  • A straight line has order 1.

  • Blending straight lines yields curves of order 2, like curve2 and curve4.

Exercise 21. What is the order of curve5?

5 Challenges

Exercise 22. Design a function that takes a function from NaturalNumbers to Posns and estimates the length of the curve it represents. Hint: Use the Pythagorean theorem to measure the distance between adjacent points.

Exercise 23. To make it easier to produce the picture below, finish designing this function:
; A Style is (make-style [NaturalNumber -> Posn] String)
(define-struct style [curve color])
 
; A ListOfStyles is one of:
; - empty
; - (cons Style ListOfStyles)
 
; draw-styles : ListOfStyles Image -> Image
; draw the given styles on the given image
(check-expect (draw-styles empty background) background)
(check-expect (draw-styles
               (list (make-style curve5 (make-color 255 0 255))
                     (make-style curve2 (make-color 150 0 255))
                     (make-style curve4 (make-color 255 0 150))
                     (make-style line1  (make-color   0 0 255))
                     (make-style line2  (make-color 255 0   0)))
               background)
              )

Exercise 24. Build a big-bang program for editing a ListOfStyles. Here are three operations on a ListOfStyles that would be useful to provide:
  • Add a fixed point, which is a Bézier curve of order 0.

  • Add a curve by blending two existing curves.

  • Remove a curve.

Hint: Use local or lambda.

Exercise 25. This lab and the secret messages section of Problem set 7: Lists were both written by Paulette D. Koronkevich, who was an amazing student in this course. When Paulette was an undergraduate instructor, she made videos for this course. Now she is doing research in graduate school. If you’ve enjoyed this course so far, now is a good time to start thinking about becoming an undergraduate instructor and about doing research!