0

Given a list of true-color full frames in BufferedImage and a list of frame durations, how can I create an Image losslessly, that when put on a JLabel, will animate?

From what I can find, I could create an ImageWriter wrapping a ByteArrayOutputStream, write IIOImage frames to it, then Toolkit.getDefaultToolkit().createImage the stream into a ToolkitImage.

There are two problems with this attempt.

  • ImageWriter can only be instantiated with one of the known image encoders, and there is none for a lossless true-color animated image format (e.g. MNG),
  • It encodes (compresses) the image, then decompresses it again, becoming an unnecessary performance hazard.

[Edit]
Some more concise constraints and requirements. Please don't come up with anything that bends these rules.

What I don't want:

  • Making an animation thread and painting/updating each frame of the animation myself,
  • Using any kind of 3rd party library,
  • Borrowing any external process, for example a web browser,
  • Display it in some kind of video player object or 3D-accelerated scene (OpenGL/etc),
  • Work directly with classes from the sun.* packages

What I do want:

  • Frame size can be as large as monitor size. Please don't worry about performance. I'll worry about that. You'll just worry about correctness.
  • Frames all have the same size,
  • an Image subclass. I should be able to draw the image like g.drawImage(ani, 0, 0, this) and it would animate, or wrap it in an ImageIcon and display it on a JLabel/JButton/etc and it would animate,
  • Each frame can each have a different delay, from 10ms up to a second,
  • Animation can loop or can end, and this is defined once per animation (just like GIF),
  • I can use anything packaged with Oracle Java 8 (e.g. JavaFX),
  • Whatever happens, it should integrate with SWING

Optional:

  • Frames can have transparency. If needed, I can opaquify my images beforehand as the animation will be shown on a known background (single color) anyway.
  • I don't care if I have to subclass Image myself and add an animation thread in there that will cooperate with the ImageObserver, or write my own InputStreamImageSource, but I don't know how.
  • If I can somehow display a JavaFX scene with some HTML and CSS code that animates my images, then that's fine too. BUT as long as it's all encapsulated in a single SWING-compatible object that I can pass around.
Community
  • 1
  • 1
Mark Jeronimus
  • 9,278
  • 3
  • 37
  • 50
  • 1
    I wouldn't use a JLabel. I'd draw the images directly on a JPanel using the paintComponent method. How big are your images? – Gilbert Le Blanc Sep 04 '15 at 20:56
  • Image size can be anything from icon to monitor resolution (not bigger). I don't actually require a JLabel (I'll have to rewrite one of my applications that currently uses JLabel) But mainly, I asked for JLabel compatibility as an easy way to mean: one `(Toolkit)Image` that contains all frames. – Mark Jeronimus Sep 05 '15 at 08:42
  • With different size images, animation is jumpier. How long do you want each image to be displayed? – Gilbert Le Blanc Sep 05 '15 at 11:46

2 Answers2

1

You're right that ImageIO isn't an option, as the only animated format for which support is guaranteed is GIF.

You say you don't want to make an animation thread, but what about a JavaFX Animation object, like a Timeline?

public JComponent createAnimationComponent(List<BufferedImage> images,
                                           List<Long> durations) {

    Objects.requireNonNull(images, "Image list cannot be null");
    Objects.requireNonNull(durations, "Duration list cannot be null");

    if (new ArrayList<Object>(images).contains(null)) {
        throw new IllegalArgumentException("Null image not permitted");
    }
    if (new ArrayList<Object>(durations).contains(null)) {
        throw new IllegalArgumentException("Null duration not permitted");
    }

    int count = images.size();
    if (count != durations.size()) {
        throw new IllegalArgumentException(
            "Lists must have the same number of elements");
    }

    ImageView view = new ImageView();
    ObjectProperty<Image> imageProperty = view.imageProperty();

    Rectangle imageSize = new Rectangle();
    KeyFrame[] frames = new KeyFrame[count];
    long time = 0;
    for (int i = 0; i < count; i++) {
        Duration duration = Duration.millis(time);
        time += durations.get(i);

        BufferedImage bufImg = images.get(i);
        imageSize.add(bufImg.getWidth(), bufImg.getHeight());

        Image image = SwingFXUtils.toFXImage(bufImg, null);
        KeyValue imageValue = new KeyValue(imageProperty, image,
            Interpolator.DISCRETE);
        frames[i] = new KeyFrame(duration, imageValue);
    }

    Timeline timeline = new Timeline(frames);
    timeline.setCycleCount(Animation.INDEFINITE);
    timeline.play();

    JFXPanel panel = new JFXPanel();
    panel.setScene(new Scene(new Group(view)));
    panel.setPreferredSize(imageSize.getSize());

    return panel;
}

(I don't know why it's necessary to set the JFXPanel's preferred size explicitly, but it is. Probably a bug.)

Note that, like all JavaFX code, it has to be run in the JavaFX Application Thread. If you're using it from a Swing application, you can do something like this:

public JComponent createAnimationComponentFromAWTThread(
                                final List<BufferedImage> images,
                                final List<Long> durations)
throws InterruptedException {

    final JComponent[] componentHolder = { null };

    Platform.runLater(new Runnable() {
        @Override
        public void run() {
            synchronized (componentHolder) {
                componentHolder[0] =
                    createAnimationComponent(images, durations);
                componentHolder.notifyAll();
            }
        }
    });

    synchronized (componentHolder) {
        while (componentHolder[0] == null) {
            componentHolder.wait();
        }
        return componentHolder[0];
    }
}

But that's still not quite enough. You first have to initialize JavaFX by calling Application.launch, either with an explicit method call, or implicitly by specifying your Application subclass as the main class.

VGR
  • 40,506
  • 4
  • 48
  • 63
  • I had to fix two small major issues with this, and after that it worked! First initialize the JavaFX thread with `new JFXPanel();` and add the first image at the end of the timeline with the final timestamp – Mark Jeronimus Sep 06 '15 at 13:06
  • Okay, currently does NOT integrate with SWING. When I do `frame.addMouseListener(x)` and `frame.setContentPane(thatJFXPanel)`, my mouse events are not coming through. Any idea if there is a solid fix (not just a hack for mouse events but to make it completely and fully behave like a JComponent? (I can't seem to make it transparent either) – Mark Jeronimus Sep 08 '15 at 20:08
  • I doubt it. I suspect you could solve the mouse event problem with JLayer, but since JavaFX, not Swing, is responsible for the contents of a JFXPanel, I doubt it can be made to support all AWT/Swing capabilities. – VGR Sep 09 '15 at 01:37
0

something like

public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.drawImage(getImageForCurrentTime(), 3, 4, this);
}

Nunser
  • 4,512
  • 8
  • 25
  • 37
sherif
  • 2,282
  • 19
  • 21
  • If I wanted to just draw frames in my own animated loop, I wouldn't have to ask here. – Mark Jeronimus Sep 05 '15 at 08:43
  • Then Im missing something I don't understand why this solution is not acceptable you just need to check the System.currentTimeInMilis() and have some logic to decide which image you want to load – sherif Sep 06 '15 at 14:55