Search code examples
javagraphicstimerbufferedimage

How to get my Buffered Image class to display in my GUI?


I have a program that does an animation using timers switching images. When the program is on its last image I use a class to create a buffered image of that image with text over it. When the last image of the animation is displayed I want to change the image displayed to the buffered image. I can't get it to work. The code as is plays as if the bolded section isnt there. If I delete the line above it, it displays the image with text over it and nothing else. What edits should I make to my code to fix this?

The Class that does the animation

**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();
    }

}
**

The class that puts text over an image

import java.io.*;
import java.awt.*;
import javax.swing.*;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;

/**
 * @see     https://stackoverflow.com/questions/2658663
 */
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;

    try {
        image = ImageIO.read(new File(fileX));
    } catch (IOException e) {
        e.printStackTrace();
    }
    image = process(image, name, number);
}



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

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;
}

}


Solution

  • So, you seem to have a misunderstanding of how Swing works, you might find How to Use Swing Timers and Concurrency in Swing of some assistance.

    Basically, when you start a Timer, it doesn't block at this point until the timer ends (and even if it did, your wouldn't work the way you wanted it to). Instead, a new thread is created and after the specified period a request is placed on Event Dispatching Thread to execute the supplied Runnable.

    This means that when you do something like...

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

    You are actually adding the TO component onto of the JLabel (because the frame is using a BorderLayout and the CENTRE position is the default position.

    Instead, in your second timer completes, you need to remove the label and add the TO component...

    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
    

    Runnable Example...

    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;
            }
        }
    
    }
    

    A "slightly" different approach...

    Animation is actually a really complex subject which is not easy to implement well.

    This is why, when faced with problems like these, I prefer to look at libraries which have already been implemented to help solve them. I'd recommend having a look at:

    as some starting points.

    While I prefer to use libraries, sometimes it's not possible or the libraries don't fit my overall needs ... that and I like to dabble ... it's kind of a hobby.

    Based on what I can understand from your code, you're trying to start out with a fast animation and then slow it down till you get to the last frame. In animation theory, this is commonly known as easement, more specifically, "slow/ease out".

    The following borrows from a bunch of snippets I've been playing with (to devise a more reusable library) that will basically (randomly) display the images over a period of 4 seconds, with the animation slowing down and finally, presenting the "lucky" number

    Really bad screen grab

    nb The gif animation is actually really slow, you'll need to run it to see the difference

    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();
                }
            }
    
        }
    
    }