Java 如何让缓冲图像类显示在GUI中?

Java 如何让缓冲图像类显示在GUI中?,java,graphics,timer,bufferedimage,Java,Graphics,Timer,Bufferedimage,我有一个程序,使用定时器切换图像来制作动画。当程序在其最后一个图像上时,我使用一个类来创建该图像的缓冲图像,并在其上添加文本。当显示动画的最后一个图像时,我想将显示的图像更改为缓冲图像。我不能让它工作。代码按原样播放,就好像粗体部分不存在一样。如果我删除上面的行,它会显示上面有文本的图像,而不会显示其他内容。我应该对代码进行哪些编辑以修复此问题 制作动画的类 **import java.awt.event.*; import java.awt.Graphics; import java.

我有一个程序,使用定时器切换图像来制作动画。当程序在其最后一个图像上时,我使用一个类来创建该图像的缓冲图像,并在其上添加文本。当显示动画的最后一个图像时,我想将显示的图像更改为缓冲图像。我不能让它工作。代码按原样播放,就好像粗体部分不存在一样。如果我删除上面的行,它会显示上面有文本的图像,而不会显示其他内容。我应该对代码进行哪些编辑以修复此问题

制作动画的类

**import java.awt.event.*;
  import java.awt.Graphics;
  import java.awt.Color;
  import java.awt.Font;
  import java.awt.image.*;

  import java.io.*;
  import java.io.File;

  import java.awt.*;
  import java.awt.image.BufferedImage;

  import java.net.URL;

  import javax.swing.*;
  import javax.swing.*;

  import javax.imageio.ImageIO;

  /**
   * Write a description of class Reveal here.
   *
   * @author (your name)
   * @version (a version number or a date)
   */
  public class Reveal extends JPanel
  {
      private JPanel panel = new JPanel();       //a panel to house the label
      private JLabel label = new JLabel();       //a label to house the image
      private String[] image = {"Jack in the Box 1.png","Jack in the Box 2.png","Jack in the Box 3.png","Jack in the Box 4.png","Jack in the Box 5.png","Jack in the Box 6.png","Jack in the Box 7.png"}; //an array to hold the frames of the animation
      private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
      private JFrame f;

private TextOverlay TO;

private Timer timer;
private Timer timer2;
int x = 0;
int y = 4;
int counter = 0;
/**
 * Constructor for objects of class Reveal
 */
public Reveal(String name, int number) 
{ 
    TO = new TextOverlay("Jack in the Box 7.png", name, number);

    for (int h = 0; h < 7; h++){
      icon[h] = new ImageIcon(image[h]);
      icon[h].getImage();
    }

    JFrame f = new JFrame();
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    f.setVisible(true);

    //Sets the size of the window
    f.setSize(800,850);
    panel = new JPanel();
    label = new JLabel();
    label.setIcon( icon[x] );
    panel.add(label);


    setVisible(true);

    f.add(panel);
    display(name, number);
    **f.add(TO);**

}

public void display(String name, int number){
    timer = new Timer(150, new ActionListener(){
        public void actionPerformed(ActionEvent e) {
            if (counter > 27){
            timer.stop();
            timer2.start(); //starts the second half of the animation
          }else{

            if (x != 3){
                x++;
            }else{
                x = 0;
            }
            label.setIcon( icon[x] );
            counter++;
          } //ends if-else
        } //ends action method
    }); //ends timer

    timer2 = new Timer(250, new ActionListener(){
        public void actionPerformed(ActionEvent e){ 
          if (y > 6) {   
            timer2.stop();
          }else{
            label.setIcon( icon[y] );
            y++;
          } //ends if-else
        } //ends action method
    }); //ends timer2

    timer.start();
    }

}
**

}因此,您似乎对Swing的工作原理有误解,您可能会找到一些帮助

基本上,当您
启动
计时器
时,它不会在这一点阻塞,直到计时器结束(即使它停止了,也不会按您希望的方式工作)。相反,将创建一个新线程,并在指定的时间段后,向事件调度线程发出请求,以执行提供的
Runnable

这意味着当你做一些像

f.add(panel);
display(name, number);
f.add(TO);
实际上,您正在将
添加到
JLabel
组件上(因为框架使用的是
边框布局
中间位置是默认位置

相反,在第二个计时器完成时,您需要删除标签并将
添加到
组件

timer2 = new Timer(250, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        if (y > 6) {
            timer2.stop();
            Container parent = label.getParent();
            parent.remove(label);
            parent.add(TO);
            parent.revalidate();
        } else {
            label.setIcon(icon[y]);
            y++;
        } //ends if-else
    } //ends action method
}); //ends timer2
可运行的示例。。。
导入java.awt.event.*;
导入java.awt.Graphics;
导入java.awt.Color;
导入java.awt.Font;
导入java.awt.*;
导入java.awt.image.buffereImage;
导入javax.swing.*;
导入javax.swing.border.LineBorder;
公共类JPanel{
公共静态void main(字符串[]args){
invokeLater(新的Runnable(){
@凌驾
公开募捐{
试一试{
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
}catch(ClassNotFoundException |实例化Exception | IllegalacessException |不支持ookandfeelException ex){
例如printStackTrace();
}
新揭示(“测试”,5);
}
});
}
private JPanel panel=new JPanel();//用于放置标签的面板
private JLabel=new JLabel();//存放图像的标签
私有图像图标[]图标=新图像图标[7];//要作为图像的图标数组
私有jf框架;
私有文本覆盖到;
私人定时器;
私人定时器2;
int x=0;
int y=4;
int计数器=0;
/**
*类的对象的构造函数
*/
公共显示(字符串名称、整数){
TO=新文本覆盖(“框中的插孔7.png”,名称,编号);
对于(int h=0;h<7;h++){
图标[h]=新图像图标(生成图像(h));
图标[h].getImage();
}
JFrame f=新的JFrame();
f、 setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f、 setVisible(真);
//设置窗口的大小
f、 设置大小(800850);
panel=newJPanel(newGridBagLayout());
label=新的JLabel();
label.setIcon(图标[x]);
label.setboorder(新线条边框(颜色为红色));
面板。添加(标签);
f、 添加(面板);
显示器(名称、编号);
//f.添加(至);
setVisible(真);
}
公共无效显示(字符串名称、整数){
计时器=新计时器(150,新ActionListener(){
已执行的公共无效操作(操作事件e){
如果(计数器>27){
timer.stop();
timer2.start();//开始动画的后半部分
}否则{
如果(x!=3){
x++;
}否则{
x=0;
}
label.setIcon(图标[x]);
计数器++;
}//否则结束
}//结束操作方法
});//结束计时器
timer2=新计时器(250,新ActionListener(){
已执行的公共无效操作(操作事件e){
如果(y>6){
timer2.stop();
容器父级=label.getParent();
移除(标签);
父。添加(到);
parent.revalidate();
}否则{
label.setIcon(图标[y]);
y++;
}//否则结束
}//结束操作方法
});//结束时间2
timer.start();
}
受保护的缓冲区映像生成映像(int h){
BuffereImage img=新的BuffereImage(100100,BuffereImage.TYPE_INT_ARGB);
Graphics2D g2d=img.createGraphics();
FontMetrics fm=g2d.getFontMetrics();
字符串文本=整数.toString(h);
intx=(100-fm.stringWidth(text))/2;
int y=((100-fm.getHeight())/2)+fm.getAscent();
g2d.setColor(Color.BLUE);
g2d.fillRect(0,0,100,100);
g2d.setColor(Color.BLACK);
g2d.抽绳(文本,x,y);
g2d.dispose();
返回img;
}
公共类TextOverlay扩展了JPanel{
私有缓冲图像;
私有字符串名称;
私有字符串文件;
私有整数;
公共文本覆盖(字符串f、字符串s、整数n){
name=s;
数字=n;
fileX=f;
image=makeImage(n);
图像=进程(图像、名称、编号);
}
@凌驾
受保护组件(图形g){
超级组件(g);
g、 drawImage(图像,0,0,this);
}
@凌驾
公共维度getPreferredSize(){
返回新维度(100100);
}
专用BuffereImage进程(BuffereImage旧、字符串名称、整数){
我
timer2 = new Timer(250, new ActionListener() {
    public void actionPerformed(ActionEvent e) {
        if (y > 6) {
            timer2.stop();
            Container parent = label.getParent();
            parent.remove(label);
            parent.add(TO);
            parent.revalidate();
        } else {
            label.setIcon(icon[y]);
            y++;
        } //ends if-else
    } //ends action method
}); //ends timer2
import java.awt.event.*;
import java.awt.Graphics;
import java.awt.Color;
import java.awt.Font;
import java.awt.*;
import java.awt.image.BufferedImage;
import javax.swing.*;
import javax.swing.border.LineBorder;

public class Reveal extends JPanel {

    public static void main(String[] args) {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                    ex.printStackTrace();
                }
                new Reveal("Test", 5);
            }
        });
    }

    private JPanel panel = new JPanel();       //a panel to house the label
    private JLabel label = new JLabel();       //a label to house the image
    private ImageIcon[] icon = new ImageIcon[7]; //an array of icons to be the images
    private JFrame f;

    private TextOverlay TO;

    private Timer timer;
    private Timer timer2;
    int x = 0;
    int y = 4;
    int counter = 0;

    /**
     * Constructor for objects of class Reveal
     */
    public Reveal(String name, int number) {
        TO = new TextOverlay("Jack in the Box 7.png", name, number);

        for (int h = 0; h < 7; h++) {
            icon[h] = new ImageIcon(makeImage(h));
            icon[h].getImage();
        }

        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.setVisible(true);

        //Sets the size of the window
        f.setSize(800, 850);
        panel = new JPanel(new GridBagLayout());
        label = new JLabel();
        label.setIcon(icon[x]);
        label.setBorder(new LineBorder(Color.RED));
        panel.add(label);

        f.add(panel);
        display(name, number);
//      f.add(TO);

        setVisible(true);
    }

    public void display(String name, int number) {
        timer = new Timer(150, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (counter > 27) {
                    timer.stop();
                    timer2.start(); //starts the second half of the animation
                } else {

                    if (x != 3) {
                        x++;
                    } else {
                        x = 0;
                    }
                    label.setIcon(icon[x]);
                    counter++;
                } //ends if-else
            } //ends action method
        }); //ends timer

        timer2 = new Timer(250, new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (y > 6) {
                    timer2.stop();
                    Container parent = label.getParent();
                    parent.remove(label);
                    parent.add(TO);
                    parent.revalidate();
                } else {
                    label.setIcon(icon[y]);
                    y++;
                } //ends if-else
            } //ends action method
        }); //ends timer2

        timer.start();
    }

    protected BufferedImage makeImage(int h) {
        BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        FontMetrics fm = g2d.getFontMetrics();
        String text = Integer.toString(h);
        int x = (100 - fm.stringWidth(text)) / 2;
        int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
        g2d.setColor(Color.BLUE);
        g2d.fillRect(0, 0, 100, 100);
        g2d.setColor(Color.BLACK);
        g2d.drawString(text, x, y);
        g2d.dispose();
        return img;
    }

    public class TextOverlay extends JPanel {

        private BufferedImage image;
        private String name;
        private String fileX;
        private int number;

        public TextOverlay(String f, String s, int n) {
            name = s;
            number = n;
            fileX = f;

            image = makeImage(n);

            image = process(image, name, number);
        }

        @Override
        protected void paintComponent(Graphics g) {
            super.paintComponent(g);
            g.drawImage(image, 0, 0, this);
        }

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

        private BufferedImage process(BufferedImage old, String name, int number) {
            int w = old.getWidth();
            int h = old.getHeight();
            BufferedImage img = new BufferedImage(w, h, BufferedImage.TYPE_INT_ARGB);
            Graphics2D g2d = img.createGraphics();
            g2d.drawImage(old, 0, 0, w, h, this);
            g2d.setPaint(Color.black);
            g2d.setFont(new Font("Franklin Gothic Demi Cond", Font.PLAIN, 30));
            String s1 = name;
            String s2 = Integer.toString(number);;
            FontMetrics fm = g2d.getFontMetrics();
            g2d.drawString(s1, 40, 90);
            g2d.drawString(s2, 40, 140);
            g2d.dispose();
            return img;
        }
    }

}
import java.awt.Color;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Graphics2D;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.geom.Point2D;
import java.awt.image.BufferedImage;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import javax.swing.*;

public class Reveal extends JPanel {

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

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

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

    public class TestPane extends JPanel {

        private IntAnimatable animatable;

        private List<ImageIcon> icons = new ArrayList<>(25);
        private JLabel label = new JLabel();

        public TestPane() {
            setLayout(new GridBagLayout());
            IntRange range = new IntRange(0, 111);
            animatable = new IntAnimatable(range, Duration.ofSeconds(4), Easement.SLOWOUT, new AnimatableListener<Integer>() {
                @Override
                public void animationChanged(Animatable<Integer> animator) {
                    int value = animator.getValue();
                    int index = value % 7;
                    ImageIcon icon = icons.get(index);
                    if (label.getIcon() != icon) {
                        label.setIcon(icon);
                    }
                }
            }, new AnimatableLifeCycleAdapter<Integer>() {
                @Override
                public void animationCompleted(Animatable<Integer> animator) {
                    BufferedImage img = makeImage(3);
                    writeTextOverImage("Lucky number", img);
                    ImageIcon luckNumber = new ImageIcon(img);
                    label.setIcon(luckNumber);
                }
            });

            for (int index = 0; index < 7; index++) {
                icons.add(new ImageIcon(makeImage(index)));
            }
            Collections.shuffle(icons);

            add(label);

            Animator.INSTANCE.add(animatable);
        }

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

    }

    protected void writeTextOverImage(String text, BufferedImage img) {
        Graphics2D g2d = img.createGraphics();
        Font font = g2d.getFont();
        font = font.deriveFont(Font.BOLD, font.getSize2D() + 2);
        g2d.setFont(font);
        FontMetrics fm = g2d.getFontMetrics();
        int width = img.getWidth();
        int height = img.getWidth();
        int x = (width - fm.stringWidth(text)) / 2;
        int y = fm.getAscent();
        g2d.setColor(Color.YELLOW);
        g2d.drawString(text, x, y);
        g2d.dispose();
    }

    protected BufferedImage makeImage(int h) {
        BufferedImage img = new BufferedImage(100, 100, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g2d = img.createGraphics();
        FontMetrics fm = g2d.getFontMetrics();
        String text = Integer.toString(h);
        int x = (100 - fm.stringWidth(text)) / 2;
        int y = ((100 - fm.getHeight()) / 2) + fm.getAscent();
        g2d.setColor(Color.BLUE);
        g2d.fillRect(0, 0, 100, 100);
        g2d.setColor(Color.WHITE);
        g2d.drawString(text, x, y);
        g2d.dispose();
        return img;
    }

    /**** Range ****/
    /* 
    A lot of animation is done from one point to another, this just
    provides a self contained concept of a range which can be used to 
    calculate the value based on the current progression over time
    */

    public abstract class Range<T> {

        private T from;
        private T to;

        public Range(T from, T to) {
            this.from = from;
            this.to = to;
        }

        public T getFrom() {
            return from;
        }

        public T getTo() {
            return to;
        }

        @Override
        public String toString() {
            return "From " + getFrom() + " to " + getTo();
        }

        public abstract T valueAt(double progress);

    }

    public class IntRange extends Range<Integer> {

        public IntRange(Integer from, Integer to) {
            super(from, to);
        }

        public Integer getDistance() {
            return getTo() - getFrom();
        }

        @Override
        public Integer valueAt(double progress) {
            int distance = getDistance();
            int value = (int) Math.round((double) distance * progress);
            value += getFrom();

            return value;
        }
    }

    /**** Animatable ****/
    /*
    The core concept of something that is animatable.  This basic wraps up the 
    logic for calculating the progression of the animation over a period of time
    and then use that to calculate the value of the range and then the observers
    are notified so they can do stuff
    */

    public class IntAnimatable extends AbstractAnimatableRange<Integer> {

        public IntAnimatable(IntRange animationRange, Duration duration, Easement easement, AnimatableListener<Integer> listener, AnimatableLifeCycleListener<Integer> lifeCycleListener) {
            super(animationRange, duration, easement, listener, lifeCycleListener);
        }

    }

    public interface AnimatableListener<T> {

        public void animationChanged(Animatable<T> animator);
    }

    public interface AnimatableLifeCycleListener<T> {

        public void animationStopped(Animatable<T> animator);

        public void animationCompleted(Animatable<T> animator);

        public void animationStarted(Animatable<T> animator);

        public void animationPaused(Animatable<T> animator);
    }

    public interface Animatable<T> {

        public T getValue();

        public void tick();

        public Duration getDuration();

        public Easement getEasement();

        // Wondering if these should be part of a secondary interface
        // Provide a "self managed" unit of work
        public void start();

        public void stop();

        public void pause();
    }

    public class AnimatableLifeCycleAdapter<T> implements AnimatableLifeCycleListener<T> {

        @Override
        public void animationStopped(Animatable<T> animator) {
        }

        @Override
        public void animationCompleted(Animatable<T> animator) {
        }

        @Override
        public void animationStarted(Animatable<T> animator) {
        }

        @Override
        public void animationPaused(Animatable<T> animator) {
        }

    }

    public abstract class AbstractAnimatable<T> implements Animatable<T> {

        private LocalDateTime startTime;
        private Duration duration = Duration.ofSeconds(5);
        private AnimatableListener<T> animatableListener;
        private AnimatableLifeCycleListener<T> lifeCycleListener;
        private Easement easement;
        private double rawOffset;

        public AbstractAnimatable(Duration duration, AnimatableListener<T> listener) {
            this.animatableListener = listener;
            this.duration = duration;
        }

        public AbstractAnimatable(Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            this(duration, listener);
            this.lifeCycleListener = lifeCycleListener;
        }

        public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener) {
            this(duration, listener);
            this.easement = easement;
        }

        public AbstractAnimatable(Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            this(duration, easement, listener);
            this.lifeCycleListener = lifeCycleListener;
        }

        public void setEasement(Easement easement) {
            this.easement = easement;
        }

        @Override
        public Easement getEasement() {
            return easement;
        }

        public Duration getDuration() {
            return duration;
        }

        protected void setDuration(Duration duration) {
            this.duration = duration;
        }

        public double getCurrentProgress(double rawProgress) {
            Easement easement = getEasement();
            double progress = Math.min(1.0, Math.max(0.0, getRawProgress()));
            if (easement != null) {
                progress = easement.interpolate(progress);
            }
            return Math.min(1.0, Math.max(0.0, progress));
        }

        public double getRawProgress() {
            if (startTime == null) {
                return 0.0;
            }
            Duration duration = getDuration();
            Duration runningTime = Duration.between(startTime, LocalDateTime.now());
            double progress = rawOffset + (runningTime.toMillis() / (double) duration.toMillis());

            return Math.min(1.0, Math.max(0.0, progress));
        }

        @Override
        public void tick() {
            if (startTime == null) {
                startTime = LocalDateTime.now();
                fireAnimationStarted();
            }
            double rawProgress = getRawProgress();
            double progress = getCurrentProgress(rawProgress);
            if (rawProgress >= 1.0) {
                progress = 1.0;
            }

            tick(progress);

            fireAnimationChanged();
            if (rawProgress >= 1.0) {
                fireAnimationCompleted();
            }
        }

        protected abstract void tick(double progress);

        @Override
        public void start() {
            if (startTime != null) {
                // Restart?
                return;
            }
            Animator.INSTANCE.add(this);
        }

        @Override
        public void stop() {
            stopWithNotitifcation(true);
        }

        @Override
        public void pause() {
            rawOffset += getRawProgress();
            stopWithNotitifcation(false);

            double remainingProgress = 1.0 - rawOffset;
            Duration remainingTime = getDuration().minusMillis((long) remainingProgress);
            setDuration(remainingTime);

            lifeCycleListener.animationStopped(this);
        }

        protected void fireAnimationChanged() {
            if (animatableListener == null) {
                return;
            }
            animatableListener.animationChanged(this);
        }

        protected void fireAnimationCompleted() {
            stopWithNotitifcation(false);
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationCompleted(this);
        }

        protected void fireAnimationStarted() {
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationStarted(this);
        }

        protected void fireAnimationPaused() {
            if (lifeCycleListener == null) {
                return;
            }
            lifeCycleListener.animationPaused(this);
        }

        protected void stopWithNotitifcation(boolean notify) {
            Animator.INSTANCE.remove(this);
            startTime = null;
            if (notify) {
                if (lifeCycleListener == null) {
                    return;
                }
                lifeCycleListener.animationStopped(this);
            }
        }

    }

    public abstract class AbstractAnimatableRange<T> extends AbstractAnimatable<T> {

        private Range<T> range;
        private T value;

        public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener) {
            super(duration, listener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            super(duration, listener, lifeCycleListener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener) {
            super(duration, easement, listener);
            this.range = range;
        }

        public AbstractAnimatableRange(Range<T> range, Duration duration, Easement easement, AnimatableListener<T> listener, AnimatableLifeCycleListener<T> lifeCycleListener) {
            super(duration, easement, listener, lifeCycleListener);
            this.range = range;
        }

        protected void tick(double progress) {
            setValue(range.valueAt(progress));
        }

        protected void setValue(T value) {
            this.value = value;
        }

        @Override
        public T getValue() {
            return value;
        }

    }

    /*
    Easement, complicated, but fun
    */
    public enum Easement {
        SLOWINSLOWOUT(1d, 0d, 0d, 1d), FASTINSLOWOUT(0d, 0d, 1d, 1d), SLOWINFASTOUT(0d, 1d, 0d, 0d), SLOWIN(1d, 0d, 1d, 1d), SLOWOUT(0d, 0d, 0d, 1d);
        private final double[] points;
        private final List<PointUnit> normalisedCurve;

        private Easement(double x1, double y1, double x2, double y2) {
            points = new double[]{x1, y1, x2, y2};
            final List<Double> baseLengths = new ArrayList<>();
            double prevX = 0;
            double prevY = 0;
            double cumulativeLength = 0;
            for (double t = 0; t <= 1; t += 0.01) {
                Point2D xy = getXY(t);
                double length = cumulativeLength + Math.sqrt((xy.getX() - prevX) * (xy.getX() - prevX) + (xy.getY() - prevY) * (xy.getY() - prevY));
                baseLengths.add(length);
                cumulativeLength = length;
                prevX = xy.getX();
                prevY = xy.getY();
            }
            normalisedCurve = new ArrayList<>(baseLengths.size());
            int index = 0;
            for (double t = 0; t <= 1; t += 0.01) {
                double length = baseLengths.get(index++);
                double normalLength = length / cumulativeLength;
                normalisedCurve.add(new PointUnit(t, normalLength));
            }
        }

        public double interpolate(double fraction) {
            int low = 1;
            int high = normalisedCurve.size() - 1;
            int mid = 0;
            while (low <= high) {
                mid = (low + high) / 2;
                if (fraction > normalisedCurve.get(mid).getPoint()) {
                    low = mid + 1;
                } else if (mid > 0 && fraction < normalisedCurve.get(mid - 1).getPoint()) {
                    high = mid - 1;
                } else {
                    break;
                }
            }
            /*
         * The answer lies between the "mid" item and its predecessor.
             */
            final PointUnit prevItem = normalisedCurve.get(mid - 1);
            final double prevFraction = prevItem.getPoint();
            final double prevT = prevItem.getDistance();
            final PointUnit item = normalisedCurve.get(mid);
            final double proportion = (fraction - prevFraction) / (item.getPoint() - prevFraction);
            final double interpolatedT = prevT + (proportion * (item.getDistance() - prevT));
            return getY(interpolatedT);
        }

        protected Point2D getXY(double t) {
            final double invT = 1 - t;
            final double b1 = 3 * t * invT * invT;
            final double b2 = 3 * t * t * invT;
            final double b3 = t * t * t;
            final Point2D xy = new Point2D.Double((b1 * points[0]) + (b2 * points[2]) + b3, (b1 * points[1]) + (b2 * points[3]) + b3);
            return xy;
        }

        protected double getY(double t) {
            final double invT = 1 - t;
            final double b1 = 3 * t * invT * invT;
            final double b2 = 3 * t * t * invT;
            final double b3 = t * t * t;
            return (b1 * points[2]) + (b2 * points[3]) + b3;
        }

        protected class PointUnit {

            private final double distance;
            private final double point;

            public PointUnit(double distance, double point) {
                this.distance = distance;
                this.point = point;
            }

            public double getDistance() {
                return distance;
            }

            public double getPoint() {
                return point;
            }
        }

    }

    /**** Core Animation Engine ****/

    public enum Animator {
        INSTANCE;
        private Timer timer;
        private List<Animatable> properies;

        private Animator() {
            properies = new ArrayList<>(5);
            timer = new Timer(5, new ActionListener() {
                @Override
                public void actionPerformed(ActionEvent e) {
                    List<Animatable> copy = new ArrayList<>(properies);
                    Iterator<Animatable> it = copy.iterator();
                    while (it.hasNext()) {
                        Animatable ap = it.next();
                        ap.tick();
                    }
                    if (properies.isEmpty()) {
                        timer.stop();
                    }
                }
            });
        }

        public void add(Animatable ap) {
            properies.add(ap);
            timer.start();
        }

        protected void removeAll(List<Animatable> completed) {
            properies.removeAll(completed);
        }

        public void remove(Animatable ap) {
            properies.remove(ap);
            if (properies.isEmpty()) {
                timer.stop();
            }
        }

    }

}