I'm writing a Mandelbrot fractal viewer, and I would like to implement color cycling in a smart way. Given an image, I would like to modify its IndexColorModel.
As far as I can tell, there's no way to modify an IndexColorModel, and there's no way to give an image a new IndexColorModel. In fact, I think there's no way to extract its color model or image data.
It seems that the only solution is to hold on to the raw image data and color palette that were used to create the image, manually create a new palette with the rotated colors, create a new IndexColorModel, then create a whole new image from the data and new color model.
This all seems like too much work. Is there an easier and faster way?
Here's the best solution I can come up with. This code creates a 1000x1000 pixel image and shows an animation of the colors cycling at about 30 frames per second.
(old)
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
public class ColorCycler {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame jFrame = new JFrame("Color Cycler");
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.add(new MyPanel());
jFrame.pack();
jFrame.setVisible(true);
}
}
class MyPanel extends JPanel implements ActionListener {
private byte[] reds = new byte[216];
private byte[] greens = new byte[216];
private byte[] blues = new byte[216];
private final byte[] imageData = new byte[1000 * 1000];
private Image image;
public MyPanel() {
generateColors();
generateImageData();
(new Timer(35, this)).start();
}
// The window size is 1000x1000 pixels.
public Dimension getPreferredSize() {
return new Dimension(1000, 1000);
}
// Generate 216 unique colors for the color model.
private void generateColors() {
int index = 0;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
for (int k = 0; k < 6; k++, index++) {
reds[index] = (byte) (i * 51);
greens[index] = (byte) (j * 51);
blues[index] = (byte) (k * 51);
}
}
}
}
// Create the image data for the MemoryImageSource.
// This data is created once and never changed.
private void generateImageData() {
for (int i = 0; i < 1000 * 1000; i++) {
imageData[i] = (byte) (i % 216);
}
}
// Draw the image.
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, 1000, 1000, null);
}
// This method is called by the timer every 35 ms.
// It creates the modified image to be drawn.
@Override
public void actionPerformed(ActionEvent e) { // Called by Timer.
reds = cycleColors(reds);
greens = cycleColors(greens);
blues = cycleColors(blues);
IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
image = createImage(new MemoryImageSource(1000, 1000, colorModel, imageData, 0, 1000));
repaint();
}
// Cycle the colors to the right by 1.
private byte[] cycleColors(byte[] colors) {
byte[] newColors = new byte[216];
newColors[0] = colors[215];
System.arraycopy(colors, 0, newColors, 1, 215);
return newColors;
}
}
Edit 2:
Now I precompute the IndexColorModels. This means that on each frame I only need to update the MemoryImageSource with a new IndexColorModel. This seems like the best solution.
(I also just noticed that in my fractal explorer, I can reuse the single set of precomputed IndexColorModels on every image I generate. That means the one-time cost of 140K lets me color cycle everything in real-time. This is great.)
Here's the code:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
public class ColorCycler {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame jFrame = new JFrame("Color Cycler");
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.add(new MyPanel());
jFrame.pack();
jFrame.setVisible(true);
}
}
class MyPanel extends JPanel implements ActionListener {
private final IndexColorModel[] colorModels = new IndexColorModel[216];
private final byte[] imageData = new byte[1000 * 1000];
private final MemoryImageSource imageSource;
private final Image image;
private int currentFrame = 0;
public MyPanel() {
generateColorModels();
generateImageData();
imageSource = new MemoryImageSource(1000, 1000, colorModels[0], imageData, 0, 1000);
imageSource.setAnimated(true);
image = createImage(imageSource);
(new Timer(35, this)).start();
}
// The window size is 1000x1000 pixels.
public Dimension getPreferredSize() {
return new Dimension(1000, 1000);
}
// Generate 216 unique colors models, one for each frame.
private void generateColorModels() {
byte[] reds = new byte[216];
byte[] greens = new byte[216];
byte[] blues = new byte[216];
int index = 0;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
for (int k = 0; k < 6; k++, index++) {
reds[index] = (byte) (i * 51);
greens[index] = (byte) (j * 51);
blues[index] = (byte) (k * 51);
}
}
}
for (int i = 0; i < 216; i++) {
colorModels[i] = new IndexColorModel(8, 216, reds, greens, blues);
reds = cycleColors(reds);
greens = cycleColors(greens);
blues = cycleColors(blues);
}
}
// Create the image data for the MemoryImageSource.
// This data is created once and never changed.
private void generateImageData() {
for (int i = 0; i < 1000 * 1000; i++) {
imageData[i] = (byte) (i % 216);
}
}
// Draw the image.
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, 1000, 1000, null);
}
// This method is called by the timer every 35 ms.
// It updates the ImageSource of the image to be drawn.
@Override
public void actionPerformed(ActionEvent e) { // Called by Timer.
currentFrame++;
if (currentFrame == 216) {
currentFrame = 0;
}
imageSource.newPixels(imageData, colorModels[currentFrame], 0, 1000);
repaint();
}
// Cycle the colors to the right by 1.
private byte[] cycleColors(byte[] colors) {
byte[] newColors = new byte[216];
newColors[0] = colors[215];
System.arraycopy(colors, 0, newColors, 1, 215);
return newColors;
}
}
Edit: (old)
Heisenbug suggested that I use the newPixels() method of MemoryImageSource. The answer has since been deleted, but it turned out to be a good idea. Now I only create one MemoryImageSource and one Image. On each frame I create a new IndexColorModel and update the MemoryImageSource.
Here's the updated code: (old)
import java.awt.*;
import java.awt.event.*;
import java.awt.image.*;
import javax.swing.*;
public class ColorCycler {
public static void main(String[] args) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
createAndShowGUI();
}
});
}
private static void createAndShowGUI() {
JFrame jFrame = new JFrame("Color Cycler");
jFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jFrame.add(new MyPanel());
jFrame.pack();
jFrame.setVisible(true);
}
}
class MyPanel extends JPanel implements ActionListener {
private byte[] reds = new byte[216];
private byte[] greens = new byte[216];
private byte[] blues = new byte[216];
private final byte[] imageData = new byte[1000 * 1000];
private final MemoryImageSource imageSource;
private final Image image;
public MyPanel() {
generateColors();
generateImageData();
IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
imageSource = new MemoryImageSource(1000, 1000, colorModel, imageData, 0, 1000);
imageSource.setAnimated(true);
image = createImage(imageSource);
(new Timer(35, this)).start();
}
// The window size is 1000x1000 pixels.
public Dimension getPreferredSize() {
return new Dimension(1000, 1000);
}
// Generate 216 unique colors for the color model.
private void generateColors() {
int index = 0;
for (int i = 0; i < 6; i++) {
for (int j = 0; j < 6; j++) {
for (int k = 0; k < 6; k++, index++) {
reds[index] = (byte) (i * 51);
greens[index] = (byte) (j * 51);
blues[index] = (byte) (k * 51);
}
}
}
}
// Create the image data for the MemoryImageSource.
// This data is created once and never changed.
private void generateImageData() {
for (int i = 0; i < 1000 * 1000; i++) {
imageData[i] = (byte) (i % 216);
}
}
// Draw the image.
protected void paintComponent(Graphics g) {
super.paintComponent(g);
g.drawImage(image, 0, 0, 1000, 1000, null);
}
// This method is called by the timer every 35 ms.
// It updates the ImageSource of the image to be drawn.
@Override
public void actionPerformed(ActionEvent e) { // Called by Timer.
reds = cycleColors(reds);
greens = cycleColors(greens);
blues = cycleColors(blues);
IndexColorModel colorModel = new IndexColorModel(8, 216, reds, greens, blues);
imageSource.newPixels(imageData, colorModel, 0, 1000);
repaint();
}
// Cycle the colors to the right by 1.
private byte[] cycleColors(byte[] colors) {
byte[] newColors = new byte[216];
newColors[0] = colors[215];
System.arraycopy(colors, 0, newColors, 1, 215);
return newColors;
}
}
See Question&Answers more detail:
os