Tutorial 11: Easing and Tweening

Easing

Easing is a form of interpolation used in animated and interactive systems for controlling the movement of an object between control points that mark the beginning and end of a movement. At it's simplest, by moving a point a fraction of the total distance each frame, a shape can appear to accelerate (or decelerate) as it approaches a target location. Here's a simple example of easing:

  • Show Sketch
/** @peep sketchcode */
float x; // Current x-coordinate
float y; // Current y-coordinate
float targetX; // Destination x-coordinate
float targetY; // Destination y-coordinate
float easing; // Size of each step along the path
 
void setup() {
  size(200, 200);
  background(0);
  noStroke();
  // Set the initial position
  x = 20;
  y = 20;
  // Set the final/target position
  targetX = width - 20;
  targetY = height - 20;
  // Adapt the position by 1% each update
  easing = 0.01;
}
 
void draw() {
  // Fade the background
  fill(0, 12);
  rect(0, 0, width, height);
  // If the current position is more than 1 pixel away from target  
  if (dist(x, y, targetX, targetY) > 1.0) {
    // Update the current position towards the target position
    x += (targetX - x) * easing;
    y += (targetY - y) * easing;
  }
  // Draw an ellipse at the current position
  fill(255);
  ellipse(x, y, 20, 20);
}

The core of the above sketch is the following lines that update the current values for x and y towards targetX and targetY:

    x += (targetX - x) * easing;
    y += (targetY - y) * easing;

    Experiment with the value assigned to easing to see the effect that this has on the movement of the ellipse.

    This example of easing has a simple if statement to determine if the values for x and y still need to be updated, i.e., if the distance between the current position and the target position is greater that 1 pixel.

    Add an else clause to the conditional to set random values for targetX and targetY when the if test fails. Slow down the rate of fading (using the modulo technique from the previous tutorial) to achieve something like the following:

    Easing to Random Targets

    Easing isn't limited to animating the positions of drawn elements, it can be applied to animate any floating point value smoothly.

    Extend the sketch you created above to add a variable, e.g., radius, to control the size of the circle and another variable, e.g., targetRadius, to hold a value that the size should ease towards. Add a line to the body of the if statement that updates the value of radius each time draw() is called in the same way that x and y are updated. Add another line to the else clause that you created for the last sketch to choose a new random size for the circle to ease towards. You should be able to produce a drawing something like the following:

    Easing the Radius

    How can you update the test performed by the if statement to ensure that the circle is sufficiently close in both position and size before choosing new target values?

    Continue to adapt your sketch to ease the colour that the shape is drawn in. You can do this either by easing the values for one or more of the red, green and blue components, or (if you set the colour mode appropriately) the hue, saturation and brightness used to set the fill colour. Here is an example of choosing a target hue at random:

    Easing with Random Hues

    If new values of the target hue are chosen by adding a random amount to the current hue, a slightly more subtle effect can be achieved:

    Easing with Varying Hues

    Easing Functions

    So far, we have been easing floating point values, which is possible with a single line of code. Easing between integer values poses a slightly more complex problem because as difference between the values gets small the amount calculated by the easing becomes less than 1 and so doesn't affect the integer value. We can write a function to make sure that we account for this when easing integers:

      int easeInt(int src, int dst, float easing) {
        if (src == dst) return dst; // If no change is required, return
        float ease = (dst - src) * easing;
        if (abs(ease) < 1) ease /= abs(ease); // Make the value of ease at least 1
        return int(src + ease);
      }

      The easeInt() function that ensures that the returned value is either the desired destination value, dst, or at least 1 more or less that the given value src.

      Easing Colours

      Given this function we can also write a function that makes it simple to ease between the values stored in a color variable. In the previous chapter you will have used variables that stored float values and only converted them to a color when used by a fill() function. The following functions use easeInt() to ease between color values stored as RGB or HSB.

        color easeRGB(color src, Color dst, float easing) {
          return color(easeInt(int(red(src)), int(red(dst)), easing),
                       easeInt(int(green(src)), int(green(dst)), easing),
                       easeInt(int(blue(src)), int(blue(dst)), easing));
        }
         
        color easeHSB(color src, Color dst, float easing) {
          return color(easeInt(int(hue(src)), int(hue(dst)), easing),
                       easeInt(int(saturation(src)), int(saturation(dst)), easing),
                       easeInt(int(brightness(src)), int(brightness(dst)), easing));
        }

        Easing the Background

        The following sketch uses easing to update all of the pixels in the current display window so that they fade towards a desired colour, i.e., so that they fade towards a background colour.

        • Show Sketch
        /** @peep sketchcode */
        color orange = color(204, 102, 0);
        color lightBlue = color(66, 168, 237);
        color darkBlue = color(0, 102, 153);
         
        void setup() {
          size(200, 200);
          background(orange);
          noStroke();
        }
         
        void draw() {
          // Ease the background pixels towards orange
          easeBackground(orange, 0.05);
          // Draw some fancy squiggly things... (coming in Tutorial 9.3)
          translate(width/2, height/2);
          for (int i = 0; i < 10; i++) {
            float nx = 2 * width * (noise((frameCount + i*1937) * 0.013) - 0.5);
            float ny = 2 * height * (noise((frameCount + i*1937) * 0.017) - 0.5);
            if (i % 2 == 0) { fill(lightBlue); } else { fill(darkBlue); }
            ellipse(nx, ny, 10, 10);
          }
        }
         
        /** Ease all of the pixels in the display window towards a given colour.
         *  Only pixels that are different in colour from the desired colour are
         *  updated, making it (reasonably) efficient. The use of easeInt() to
         *  calculate the new pixel values ensures that the easing will result in
         *  the desired colour and not get stuck at a shade just lighter or darker
         *  than it.
         */
        void easeBackground(color dst, float easing) {
          int dstR = int(red(dst));
          int dstG = int(green(dst));
          int dstB = int(blue(dst));
          loadPixels();
          for (int i = 0; i < pixels.length; i++) {
            color src = pixels[i];
            if (src != dst) {
              pixels[i] = color(easeInt(int(red(src)), dstR, easing),
                                easeInt(int(green(src)), dstG, easing),
                                easeInt(int(blue(src)), dstB, easing));
            }
          }
          updatePixels();
        }
         
        /** Ease an integer value from src towards dst, taking care to only change the
         *  value if the difference between src and dst is greater than one, and
         *  (if a change is needed) always changing by at least 1.
         */
        int easeInt(int src, int dst, float easing) {
          if (src == dst) return dst; // If no change is required, return
          float ease = (dst - src) * easing;
          if (abs(ease) < 1) ease /= abs(ease); // Make the value of ease at least 1
          return int(src + ease);
        }

        This approach has the advantage over the previous technique that we've used to fade drawings to a background colour, using a transparent rectangle, that it will not leave any residue of the drawing thanks to the use of easeInt(). The disadvantage of this approach is that it is more computationally expensive because the easeBackground() function has to check every pixel. So, as the sketch gets larger the function will take longer.

        Experiment with the above sketch. Try to create a sketch that will changes the target background colour to one of the three palette colours every 100 frames. The easiest way to achieve this is to put all of the colours in an array, e.g., called palette and maintain a separate variable for the current target background colour, which can be changed to a random member of the palette array every 100 frames. Because this will affect every pixel in the sketch, it will show what sort of impact this has on animation speed, to maintain a good frame rate you may want to reduce your sketch size to be less than 200x200 pixels (depending on the speed of your computer). More likely, to ensure that the sketch runs at a consistent speed (not speeding up and slowing down with the changes in the background colour) then you'll also want to use the frameRate() function to limit the maximum number of frames per second to an amount that can be achieved even if all of the pixels must be updated, e.g., frameRate(10).

        Easing Pixels

        We can use a similar approach to the one above to fade a drawing towards a background image. The code for easeBackground() is quite similar but instead of taking a color parameter it takes a PImage parameter, and it calculates the value for dst from each pixel in the destination image, dstImage. Running this sketch you'll see that the easing function does a good job of making sure that all traces of the drawing are faded to the background image.

        • Show Sketch
        /** @peep sketchcode */
        /* @pjs preload="/uploads/1/jacaranda.jpg"; */
        color[] palette = { color(204, 102, 0), color(66, 168, 237), color(0, 102, 153) };
         
        PImage tree;
         
        void setup() {
          size(200, 200);
          noStroke();
          tree = loadImage("/uploads/1/jacaranda.jpg");
          background(tree);
        }
         
        void draw() {
          easeBackground(tree, 0.01);
          // Draw some fancy squiggly things... (coming in Tutorial 9.3)
          translate(width/2, height/2);
          for (int i = 0; i < 10; i++) {
            float nx = 2 * width * (noise((frameCount + i*1937) * 0.013) - 0.5);
            float ny = 2 * height * (noise((frameCount + i*1937) * 0.017) - 0.5);
            fill(palette[i % palette.length]);
            ellipse(nx, ny, 10, 10);
          }
        }
         
        void easeBackground(PImage dstImage, float easing) {
          if (width != dstImage.width || height != dstImage.height) return;
          loadPixels();
          dstImage.loadPixels();
          for (int i = 0; i < pixels.length; i++) {
            color src = pixels[i];
            color dst = dstImage.pixels[i];
            if (src != dst) {
              pixels[i] = color(easeInt(int(red(src)), int(red(dst)), easing),
                                easeInt(int(green(src)), int(green(dst)), easing),
                                easeInt(int(blue(src)), int(blue(dst)), easing));
            }
          }
          updatePixels();
        }
         
        int easeInt(int src, int dst, float easing) {
          if (src == dst) return dst; // If no change is required, return
          float ease = (dst - src) * easing;
          if (abs(ease) < 1) ease /= abs(ease); // Make the value of ease at least 1
          return int(src + ease);
        }

        We can use a similar approach to the one above for easing between images. In this sketch two images are loaded into an array and a third image is created in memory. A copy of one of the images loaded into the is made to the image in memory and a fourth PImage variable is created to point to the image that it should ease towards. Every 100 frames the destination image that the sketch should fade towards is changed.

        • Show Sketch
        /** @peep sketchcode */
        /* @pjs preload="/uploads/1/jacaranda.jpg, /uploads/1/kitty.jpg"; */
        PImage[] images;
        PImage img;
        PImage dst;
         
        void setup() {
          size(200, 300);
          images = new PImage[2];
          images[0] = loadImage("/uploads/1/jacaranda.jpg");
          images[1] = loadImage("/uploads/1/kitty.jpg");
          img = createImage(images[0].width, images[0].height, RGB);
          img.copy(images[0], 0, 0, images[0].width, images[0].height, 0, 0, img.width, img.height);
          dst = images[1];
        }
         
        void draw() {
          if (frameCount % 100 == 0) { // If we've complete 100 updates, switch the image we're fading to
            if (dst == images[0]) { dst = images[1]; } else { dst = images[0]; }
          }
          // Ease the pixels
          easePixels(img, dst, 0.04);
          image(img, 0, 0);
          image(images[0], 0, 200, 100, 100);
          image(images[1], 100, 200, 100, 100);
        }
         
        void easePixels(PImage srcImage, PImage dstImage, float easing) {
          if (srcImage.width != dstImage.width || srcImage.height != dstImage.height) return;
          srcImage.loadPixels();
          dstImage.loadPixels();
          for (int i = 0; i < srcImage.pixels.length; i++) {
            color src = srcImage.pixels[i];
            color dst = dstImage.pixels[i];
            if (src != dst) {
              srcImage.pixels[i] = color(easeInt(int(red(src)), int(red(dst)), easing),
                                         easeInt(int(green(src)), int(green(dst)), easing),
                                         easeInt(int(blue(src)), int(blue(dst)), easing));
            }
          }
          srcImage.updatePixels();
        }
         
        int easeInt(int src, int dst, float easing) {
          if (src == dst) return dst; // If no change is required, return
          float ease = (dst - src) * easing;
          if (abs(ease) < 1) ease /= abs(ease); // Make the value of ease at least 1
          return int(src + ease);
        }

        Tweening

        I'm going to use the term "tweening" to refer to the use of easing techniques to animate shapes, to distinguish it from "easing". The principle is the same, however, points can be progressively changed from one position to another over time. The difference is that to transform a shape we must ease multiple points that define the outline of the shape. For example, the following sketch tweens between a large square (shape1) and a small diamond (shape2):

        • Show Sketch
        /** @peep sketchcode */
        float[][] shape1 = {{-80, -80}, {80, -80}, {80, 80}, {-80, 80}}; // Source shape
        float[][] shape2 = {{-60, 0}, {0, -20}, {60, 0}, {0, 20}}; // Destination shape
        float[][] points;
        float easing = 0.02; // Size of each step along the path
         
        void setup() {
          size(200, 200);
          background(0);
          // Make a copy of the shape1 array in points
          points = new float[shape1.length][2];
          for (int i = 0; i < shape1.length; i++) {
            points[i][0] = shape1[i][0];
            points[i][1] = shape1[i][1];
          }
        }
         
        void draw() {
          // Fade the background
          noStroke();
          fill(0, 12);
          rect(0, 0, width, height);
          // Update current points towards the target shape
          for (int i = 0; i < points.length; i++) {
            if (dist(points[i][0], points[i][1], shape2[i][0], shape2[i][1]) > 1.0) {
              points[i][0] += (shape2[i][0] - points[i][0]) * easing;
              points[i][1] += (shape2[i][1] - points[i][1]) * easing;
            }
          }
         
          // Draw the current shape
          noFill();
          stroke(255);
          translate(width/2, height/2);
          beginShape();
          for (int i = 0; i < points.length; i++) {
            vertex(points[i][0], points[i][1]);
          }
          endShape(CLOSE);
        }

        By using some additional variables to record which of the shapes is the current source and destination for the tween we can easily swap the shapes used for the tween when the current points are within 1 pixel of the current destination. The tweenCompleted variable is used to determine whether all of the points are close enough to the destination.

        • Show Sketch
        /** @peep sketchcode */
        float[][] shape1 = {{-80, -80}, {80, -80}, {80, 80}, {-80, 80}};
        float[][] shape2 = {{-60, 0}, {0, -20}, {60, 0}, {0, 20}}; // Destination shape
        float[][] src, dst, points;
        float easing = 0.02; // Size of each step along the path
         
        void setup() {
          size(200, 200);
          background(0);
          // Record the initial source and destination shapes
          src = shape1;
          dst = shape2;
          // Make a copy of the shape1 array in points
          points = new float[src.length][2];
          for (int i = 0; i < src.length; i++) {
            points[i][0] = src[i][0];
            points[i][1] = src[i][1];
          }
        }
         
        void draw() {
          // Fade the background
          noStroke();
          fill(0, 12);
          rect(0, 0, width, height);
         
          boolean tweenCompleted = true; // Flag to record when the tween is done
          // Update current points towards the target shape
          for (int i = 0; i < points.length; i++) {
            if (dist(points[i][0], points[i][1], dst[i][0], dst[i][1]) > 1.0) {
              points[i][0] += (dst[i][0] - points[i][0]) * easing;
              points[i][1] += (dst[i][1] - points[i][1]) * easing;
              // If any of the points are more than 1 pixel from target, then not done
              tweenCompleted = false;
            }
          }
          // If the tween is done, swap the source and destination shapes
          if (tweenCompleted) {
            float[][] tmp = src; // Make a temporary copy of the source
            src = dst; // Copy the destination to the source
            dst = tmp; // Copy the temporary copy to the destination
          }
         
          // Draw the current shape
          noFill();
          stroke(255);
          translate(width/2, height/2);
          beginShape();
          for (int i = 0; i < points.length; i++) {
            vertex(points[i][0], points[i][1]);
          }
          endShape(CLOSE);
        }

        The code that swaps the source and destination shapes is something that you may see in different forms quite often, it uses a temporary variable to make a copy of the value for the source before copying the value of destination variable to the source shape. Finally the temporary value is copied to the destination shape.

        Instead of using two variables, shape1 and shape2, to store the values of the points we can use an array to store the values of the points for these shapes. The following sketch shows how this works:

        • Show Sketch
        /** @peep sketchcode */
        float[][] shapes = {
          {{-80, -80}, {80, -80}, {80, 80}, {-80, 80}},
          {{-60, 0}, {0, -20}, {60, 0}, {0, 20}}
        };
        float[][] src, dst, points;
        float easing = 0.02; // Size of each step along the path
         
        void setup() {
          size(200, 200);
          background(0);
          // Record the initial source and destination shapes
          src = shapes[0];
          dst = shapes[1];
          // Make a copy of the source shape in points
          points = new float[src.length][2];
          for (int i = 0; i < src.length; i++) {
            points[i][0] = src[i][0];
            points[i][1] = src[i][1];
          }
        }
         
        void draw() {
          // Fade the background
          noStroke();
          fill(0, 12);
          rect(0, 0, width, height);
         
          boolean tweenCompleted = true; // Flag to record when the tween is done
          // Update current points towards the target shape
          for (int i = 0; i < points.length; i++) {
            if (dist(points[i][0], points[i][1], dst[i][0], dst[i][1]) > 1.0) {
              points[i][0] += (dst[i][0] - points[i][0]) * easing;
              points[i][1] += (dst[i][1] - points[i][1]) * easing;
              // If any of the points are more than 1 pixel from target, then not done
              tweenCompleted = false;
            }
          }
          // If the tween is done, swap the source and destination shapes
          if (tweenCompleted) {
            float[][] tmp = src; // Make a temporary copy of the source
            src = dst; // Copy the destination to the source
            dst = tmp; // Copy the temporary copy to the destination
          }
         
          // Draw the current shape
          noFill();
          stroke(255);
          translate(width/2, height/2);
          beginShape();
          for (int i = 0; i < points.length; i++) {
            vertex(points[i][0], points[i][1]);
          }
          endShape(CLOSE);
        }

        Now that the shapes are stored in an array, we can easily add more shapes. The following sketch uses 6 different shapes. Instead of simply swapping the source and destination shapes when a tween in completed, this sketch chooses a new destination shape at random from the array.

        • Show Sketch
        /** @peep sketchcode */
        float[][] shapes = {
          {{-80, -80}, {80, -80}, {80, 80}, {-80, 80}},
          {{-60, 0}, {0, -20}, {60, 0}, {0, 20}},
          {{-40, -40}, {40, -40}, {40, 40}, {-40, 40}},
          {{-80, 0}, {0, -80}, {80, 0}, {0, 80}},
          {{-20, -80}, {20, -80}, {20, 80}, {-20, 80}},
          {{-80, -20}, {80, -20}, {80, 20}, {-80, 20}}};
        float[][] src, dst, points;
        float easing = 0.02; // Size of each step along the path
         
        void setup() {
          size(200, 200);
          background(0);
          // Record the initial source and destination shapes
          src = shapes[0];
          dst = shapes[1];
          // Make a copy of the source shape in points
          points = new float[src.length][2];
          for (int i = 0; i < src.length; i++) {
            points[i][0] = src[i][0];
            points[i][1] = src[i][1];
          }
        }
         
        void draw() {
          // Fade the background
          noStroke();
          fill(0, 12);
          rect(0, 0, width, height);
         
          boolean tweenCompleted = true; // Flag to record when the tween is done
          // Update current points towards the target shape
          for (int i = 0; i < points.length; i++) {
            if (dist(points[i][0], points[i][1], dst[i][0], dst[i][1]) > 1.0) {
              points[i][0] += (dst[i][0] - points[i][0]) * easing;
              points[i][1] += (dst[i][1] - points[i][1]) * easing;
              // If any of the points are more than 1 pixel from target, then not done
              tweenCompleted = false;
            }
          }
          // If the tween is done, choose a new destination
          if (tweenCompleted) {
            src = dst;
            dst = shapes[int(random(shapes.length))];
          }
         
          // Draw the current shape
          noFill();
          stroke(255);
          translate(width/2, height/2);
          beginShape();
          for (int i = 0; i < points.length; i++) {
            vertex(points[i][0], points[i][1]);
          }
          endShape(CLOSE);
        }

        The limitation of the above sketch is that all of the shapes must contain the same number of points, but this limitation can be overcome by making doubling up points in a shape that has fewer sides.

        • Show Sketch
        /** @peep sketchcode */
        float[][] shapes = {
          {{-20, -80}, { 20, -80}, { 80, -80},
           { 80, -20}, { 80,  20}, { 80,  80},
           { 20,  80}, {-20,  80}, {-80,  80},
           {-80,  20}, {-80, -20}, {-80, -80}},
          {{-20, -80}, { 20, -80}, { 20, -20},
           { 80, -20}, { 80,  20}, { 20,  20},
           { 20,  80}, {-20,  80}, {-20,  20},
           {-80,  20}, {-80, -20}, {-20, -20}},
          {{-30, -80}, { 30, -80}, { 60, -60},
           { 80, -30}, { 80,  30}, { 60,  60},
           { 30,  80}, {-30,  80}, {-60,  60},
           {-80,  30}, {-80, -30}, {-60, -60}}};
        float[][] src, dst, points;
        float easing = 0.02; // Size of each step along the path
         
        void setup() {
          size(200, 200);
          background(0);
          // Record the initial source and destination shapes
          src = shapes[0];
          dst = shapes[1];
          // Make a copy of the source shape in points
          points = new float[src.length][2];
          for (int i = 0; i < src.length; i++) {
            points[i][0] = src[i][0];
            points[i][1] = src[i][1];
          }
        }
         
        void draw() {
          // Fade the background
          noStroke();
          fill(0, 12);
          rect(0, 0, width, height);
         
          boolean tweenCompleted = true; // Flag to record when the tween is done
          // Update current points towards the target shape
          for (int i = 0; i < points.length; i++) {
            if (dist(points[i][0], points[i][1], dst[i][0], dst[i][1]) > 1.0) {
              points[i][0] += (dst[i][0] - points[i][0]) * easing;
              points[i][1] += (dst[i][1] - points[i][1]) * easing;
              // If any of the points are more than 1 pixel from target, then not done
              tweenCompleted = false;
            }
          }
          // If the tween is done, choose a new destination
          if (tweenCompleted) {
            src = dst;
            dst = shapes[int(random(shapes.length))];
          }
         
          // Draw the current shape
          noFill();
          stroke(255);
          translate(width/2, height/2);
          beginShape();
          for (int i = 0; i < points.length; i++) {
            vertex(points[i][0], points[i][1]);
          }
          endShape(CLOSE);
        }

        Use a piece of paper, or the tool below, to design a set of shapes to animate with a tween using the above sketch.

        • Show Code
        /** @peep sketch */
        ArrayList points;
        PVector currentPoint;
         
        void setup() {
          size(200, 200);
          points = new ArrayList();
          textFont(loadFont("sansserif"), 18);
          textAlign(CENTER, CENTER);
        }
         
        void draw() {
          background(204);
          stroke(0, 32);
          strokeWeight(1);
          translate(width/2, height/2);
          for (int x = -width/2; x <= width/2; x += 10) line(x, -height/2, x, height);
          for (int y = -width/2; y <= height/2; y += 10) line(-width/2, y, width, y);
          stroke(0, 64);
          line(-width/2, 0, width/2, 0);
          line(0, -height/2, 0, height/2);
         
          noStroke();
          fill(96, 64, 0, 128);
          Iterator it = points.iterator();
          while (it.hasNext()) {
            PVector p = (PVector) it.next();
            ellipse(p.x, p.y, 8, 8);
            fill(0, 64);
          }
         
          if (points.size() > 1) {
            noFill();
            stroke(0, 64);
            strokeWeight(4);
            beginShape();
            it = points.iterator();
            while (it.hasNext()) {
              PVector p = (PVector) it.next();
              vertex(p.x, p.y);
            }
            endShape(CLOSE);
          }
         
          if (currentPoint != null) {
            noStroke();
            fill(191, 0, 0);
            ellipse(currentPoint.x, currentPoint.y, 8, 8);
         
            fill(255);
            String strCoords = "(" + currentPoint.x + ", " + currentPoint.y + ")";
            text(strCoords, 0, 0);
          }
        }
         
        void mousePressed() {
          float x = 10 * round(0.1 * (mouseX - width/2));
          float y = 10 * round(0.1 * (mouseY - height/2));
          currentPoint = null;
          Iterator it = points.iterator();
          while (it.hasNext()) {
            PVector p = (PVector) it.next();
            if ((p.x == x) && (p.y == y)) {
              currentPoint = p;
            }
          }
          if (currentPoint == null) {
            currentPoint = new PVector(x, y);
            points.add(currentPoint);
          }
        }
         
        void mouseDragged() {
          float x = 10 * round(0.1 * (mouseX - width/2));
          float y = 10 * round(0.1 * (mouseY - height/2));
          if (currentPoint != null) {
            currentPoint.x = x;
            currentPoint.y = y;
          }
        }

        Easing using Curves

        Simple curves can provide paths for shapes in motion. Here's the curve for y = x^2, it uses the pow() function:

        • Show Code
        /** @peep sketch */
        size(200, 200);
        background(204);
        stroke(0, 32);
        for (int x = 0; x <= width; x += 10) { line(x, 0, x, height); }
        for (int y = 0; y <= height; y += 10) { line(0, y, width, y); }
        noFill();
        stroke(0);
        beginShape();
        float exponent = 2.0;
        for (float x = 0; x <= 1; x += 0.001) {
          float y = pow(x, exponent);
          vertex(x*width, y*width);
        }
        endShape();
        popMatrix();

        Experiment with different values for exponent to see how this affects the shape of the curve.

        Instead of drawing the entire curve in one frame, it's possible to calculate each step of the curve on consecutive frames. The following example presents a very general way to write this code.

        • Show Sketch
        /** @peep sketchcode */
        float beginX; // Initial x-coordinate
        float beginY; // Initial y-coordinate
        float endX; // Final x-coordinate
        float endY; // Final y-coordinate
        float distX; // Distance to travel in x
        float distY; // Distance to travel in y
        float step; // Size of each step along the path
        float pct; // Percentage traveled (0.0 to 1.0)
        float exponent; // Determines the curve
         
        void setup() {
          size(200, 200);
          background(0);
          noStroke();
          beginX = 20;
          beginY = 20;
          endX = width - 20;
          endY = height - 20;
          distX = endX - beginX;
          distY = endY - beginY;
          step = 0.01;
          pct = 0.0;
          exponent = 2.0;
        }
         
        void draw() {
          fill(0, 2);
          rect(0, 0, width, height);
          if (pct < 1.0) {
            float rateX = pct;
            float rateY = pow(pct, exponent);
            float x = beginX + (rateX * distX);
            float y = beginY + (rateY * distY);
            fill(255);
            ellipse(x, y, 20, 20);
            pct += step;
          }
        }

        If we want to use the curve to control the speed of the objects motion along a straight line this can be easily accomplished:

        • Show Sketch
        /** @peep sketchcode */
        float beginX; // Initial x-coordinate
        float beginY; // Initial y-coordinate
        float endX; // Final x-coordinate
        float endY; // Final y-coordinate
        float distX; // Distance to travel in x
        float distY; // Distance to travel in y
        float step; // Size of each step along the path
        float pct; // Percentage traveled (0.0 to 1.0)
        float exponent; // Determines the curve
         
        void setup() {
          size(200, 200);
          background(0);
          noStroke();
          beginX = 20;
          beginY = 20;
          endX = width - 20;
          endY = height - 20;
          distX = endX - beginX;
          distY = endY - beginY;
          step = 0.01;
          pct = 0.0;
          exponent = 2.0;
        }
         
        void draw() {
          fill(0, 2);
          rect(0, 0, width, height);
          if (pct < 1.0) {
            pct += step;
          }
          float rate = pow(pct, exponent);
          float x = beginX + (rate * distX);
          float y = beginY + (rate * distY);
          fill(255);
          ellipse(x, y, 20, 20);
        }

        Experiment with the value of exponent to see the different types of easing that you can create.

        Comments

        Nobody has said anything yet.