2

I am making a simple animation in Processing. I want to animate an image from its starting point to a defined x,y value on the screen.

I have 2 methods, update() and draw(), which are run on every tick. update() is where the code will go to process the x/y coordinates to provide to the draw() method on the next tick.

The draw() method then draws the image, passing in the updated x and y values.

minimal example:

class ScrollingNote {
  float x;
  float y;
  float destX;
  float destY;
  PImage noteImg;

  ScrollingNote(){
    noteImg = loadImage("image-name.png");
    this.x = width/2;
    this.y = 100;
    this.destX = 100;
    this.destY = height;
  }

  void update(){
    // TODO: adjust this.x and this.y 
    // to draw the image slightly closer to
    // this.destX and this.destY on the redraw
    // ie in this example we are animating from x,y to destX, destY
  }

  void draw(){
    image( noteImg, this.x, this.y );
  }
}

What sort of calculation do I need to make to adjust the x/y coordinates to make the image draw slightly closer to the destination?

tdc
  • 5,174
  • 12
  • 53
  • 102
  • You will need to know your start position and your target position and the amount of time you plan for the movement. If you plan on moving until you reach your target, then you will need to know the amount of change to apply on each update... – MadProgrammer Apr 22 '14 at 01:58
  • Both the start position and target positions will be constant and are known. I will play with the amount to change on each update. I just don't know the calculation needed. – tdc Apr 22 '14 at 01:59
  • 1
    Well, I tend to use something like `startValue + ((endValue - startValue) * fractionOverTime)` where `fractionOverTime` is a percentage of the time used from 0-1. This assumes you know the amount of time the movement has and how much time has passed – MadProgrammer Apr 22 '14 at 02:03
  • Thanks for the tip, that helped get me on the right track. Couple questions: 1) how do I make it so that the movement is constant? Right now using the code you provided it seems to "tween" in (fast at first, then very slow as the multi adds up). 2) how can I make this work when applying a rotateX(radians(50)) to the image? I want a "skewed" perspective so it's like the object is coming in from a distance. – tdc Apr 22 '14 at 02:29
  • It's difficult to say without seeing any code. If the ticks are constant and the refresh process works, then the above should provide a smooth transition. Rotation is pretty much the same, you have a start angel and an end angel and time... – MadProgrammer Apr 22 '14 at 02:59
  • @MadProgrammer: the app is 30fps and the ticks are called for each frame. How do I calculate `fractionOverTime` in your example? I just stubbed in `.01` which is why I'm getting the easing I think. – tdc Apr 22 '14 at 03:04
  • Well, you need to know when it started and how long it can run for. Based on each tick you would then need to calculate the amount of time it's been running and convert that to a percentage – MadProgrammer Apr 22 '14 at 03:13

2 Answers2

3

This is a very basic example of time period based animation

It will animate a Animatable object over a 5 second period. You could simply use a List and update/paint multiple objects simultaneously if you wanted to get fancy.

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.AffineTransform;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.Timer;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class AnimationTest {

    public static void main(String[] args) {
        new AnimationTest();
    }

    public AnimationTest() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                }

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public interface Animatable {

        public void update(double progress);
        public void draw(Graphics2D g2d);

    }

    public static class FlyingSquiral implements Animatable {

        private final Point startPoint;
        private final Point targetPoint;
        private final double startAngel;
        private final double targetAngel;

        private Point location;
        private double angle;

        public FlyingSquiral(Point startPoint, Point targetPoint, double startAngel, double targetAngel) {
            this.startPoint = startPoint;
            this.targetPoint = targetPoint;
            this.startAngel = startAngel;
            this.targetAngel = targetAngel;

            location = new Point(startPoint);
            angle = startAngel;
        }

        @Override
        public void update(double progress) {

            location.x = (int)Math.round(startPoint.x + ((targetPoint.x - startPoint.x) * progress));
            location.y = (int)Math.round(startPoint.y + ((targetPoint.y - startPoint.y) * progress));
            angle = startAngel + ((targetAngel - startAngel) * progress);

        }

        @Override
        public void draw(Graphics2D g2d) {

            Graphics2D clone = (Graphics2D) g2d.create();

            clone.setRenderingHint(RenderingHints.KEY_ALPHA_INTERPOLATION, RenderingHints.VALUE_ALPHA_INTERPOLATION_QUALITY);
            clone.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
            clone.setRenderingHint(RenderingHints.KEY_COLOR_RENDERING, RenderingHints.VALUE_COLOR_RENDER_QUALITY);
            clone.setRenderingHint(RenderingHints.KEY_DITHERING, RenderingHints.VALUE_DITHER_ENABLE);
            clone.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, RenderingHints.VALUE_FRACTIONALMETRICS_ON);
            clone.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            clone.setRenderingHint(RenderingHints.KEY_RENDERING, RenderingHints.VALUE_RENDER_QUALITY);
            clone.setRenderingHint(RenderingHints.KEY_STROKE_CONTROL, RenderingHints.VALUE_STROKE_PURE);
            AffineTransform at = new AffineTransform();
            at.translate(location.x, location.y);
            at.rotate(Math.toRadians(angle), 25, 25);
            clone.setTransform(at);
            clone.draw(new Rectangle(0, 0, 50, 50));
            clone.dispose();

        }

    }

    public static class TestPane extends JPanel {

        public static final long DURATION = 5000;
        private long startTime;
        private boolean started = false;

        private FlyingSquiral squiral;

        public TestPane() {
            squiral = new FlyingSquiral(
                    new Point(0, 0), 
                    new Point(150, 150), 
                    0d, 360d);
            Timer timer = new Timer(40, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    if (!started) {
                        startTime = System.currentTimeMillis();
                        started = true;
                    }
                    long time = System.currentTimeMillis();
                    long duration = time - startTime;
                    if (duration > DURATION) {
                        duration = DURATION;
                        ((Timer)e.getSource()).stop();
                    }
                    double progress = (double)duration / (double)DURATION;
                    squiral.update(progress);
                    repaint();
                }
            });
            timer.start();
        }

        @Override
        public Dimension getPreferredSize() {
            return new Dimension(200, 200);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            Graphics2D g2d = (Graphics2D) g.create();
            squiral.draw(g2d);
            g2d.dispose();
        }
    }

}

Equally, you could use a constraint based animation, where by the object keeps moving until it meets it's required constraints (angel/position). Each has pros and cons.

I prefer a time period based approach as it allows me to apply different transformations without needing to care about pre-calculating the delta. Try this, change the target angel from 360 to 720 and run it again.

I also prefer to use an animation library, as they add additional features, like interpolation, allowing to change the speed of the animation at certain points in time without changing the duration, this would allow you to do things like slow in, slow out (ramp up/out) effects, making the animation more appealing.

Take a look at...

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
1

If you are using Processing 2.0 this can be done via Ani library. To get same output like @MadProgrammer you just setup basic sketch with Ani.init(this) then in draw() function move box via translate() and rotate it via rotate() functions. Whole animation begins after first mouse click.

import de.looksgood.ani.*;
import de.looksgood.ani.easing.*;

float posX = 25, posY = 25;
float angleRotation = 0;

void setup () {
 size (200, 200);
 background (99);  
 noFill ();
 stroke (0);
 Ani.init(this);
 frameRate (30);
 rectMode(CENTER); 
}

void draw () {
  background (225);  
  translate(posX, posY);
  rotate(radians(angleRotation));
  rect(0, 0, 50, 50);    
}

void mousePressed() {
  Ani.to(this, 5, "posX", 175, Ani.LINEAR);
  Ani.to(this, 5, "posY", 175, Ani.LINEAR);
  Ani.to(this, 5, "angleRotation", 360, Ani.LINEAR);
}

Manually you can get similar result just by increasing posX, posY and angleRotation within draw loop.

Majlik
  • 1,082
  • 1
  • 10
  • 20