/*
* @(#)ColorCyclingMemoryImageSource.java 1.1 2010-08-03
*
* Copyright (c) 2009-2010 Werner Randelshofer, Goldau, Switzerland.
* All rights reserved.
*
* You may not use, copy or modify this file, except in compliance with the
* license agreement you entered into with Werner Randelshofer.
* For details see accompanying license terms.
*/
package org.monte.media.ilbm;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.image.BufferedImage;
import java.awt.image.ColorModel;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferByte;
import java.awt.image.DataBufferInt;
import java.awt.image.ImageConsumer;
import java.awt.image.IndexColorModel;
import java.awt.image.MemoryImageSource;
import java.awt.image.Raster;
import java.awt.image.SampleModel;
import java.awt.image.WritableRaster;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Hashtable;
import javax.swing.Timer;
/**
* ColorCyclingMemoryImageSource.
*
* @author Werner Randelshofer
* @version 1.1 2010-08-03 Added method putProperties. Added support for
* blended color cycles.
* <br>1.0.1 2010-11-08 Fixed color cycling rate.
* <br>1.0 2009-12-17 Created.
*/
public class ColorCyclingMemoryImageSource extends MemoryImageSource {
private int width;
private int height;
private ColorModel model;
private Object pixels;
private int pixeloffset;
private int pixelscan;
private Hashtable properties;
private ArrayList<ColorCycle> colorCycles = new ArrayList<ColorCycle>();
private Timer timer;
private HashSet<ImageConsumer> consumers = new HashSet<ImageConsumer>();
/** Whether color cycling is available. */
private boolean isColorCyclingAvailable;
/** Whether color cycling is started. */
private boolean isStarted;
/** Whether color cycles are blended. */
private boolean isBlendedColorCycling;
private volatile ColorModel cycledModel;
/**
* Constructs an ImageProducer object which uses an array of bytes
* to produce data for an Image object.
* @param w the width of the rectangle of pixels
* @param h the height of the rectangle of pixels
* @param cm the specified <code>ColorModel</code>
* @param pix an array of pixels
* @param off the offset into the array of where to store the
* first pixel
* @param scan the distance from one row of pixels to the next in
* the array
* @see java.awt.Component#createImage
*/
public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
byte[] pix, int off, int scan) {
super(w, h, cm, pix, off, scan);
initialize(w, h, cm, (Object) pix, off, scan, new Hashtable());
}
/**
* Constructs an ImageProducer object which uses an array of bytes
* to produce data for an Image object.
* @param w the width of the rectangle of pixels
* @param h the height of the rectangle of pixels
* @param cm the specified <code>ColorModel</code>
* @param pix an array of pixels
* @param off the offset into the array of where to store the
* first pixel
* @param scan the distance from one row of pixels to the next in
* the array
* @param props a list of properties that the <code>ImageProducer</code>
* uses to process an image
* @see java.awt.Component#createImage
*/
public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
byte[] pix, int off, int scan,
Hashtable<?, ?> props) {
super(w, h, cm, pix, off, scan, props);
initialize(w, h, cm, (Object) pix, off, scan, props);
}
/**
* Constructs an ImageProducer object which uses an array of integers
* to produce data for an Image object.
* @param w the width of the rectangle of pixels
* @param h the height of the rectangle of pixels
* @param cm the specified <code>ColorModel</code>
* @param pix an array of pixels
* @param off the offset into the array of where to store the
* first pixel
* @param scan the distance from one row of pixels to the next in
* the array
* @see java.awt.Component#createImage
*/
public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
int[] pix, int off, int scan) {
super(w, h, cm, pix, off, scan);
initialize(w, h, cm, (Object) pix, off, scan, null);
}
/**
* Constructs an ImageProducer object which uses an array of integers
* to produce data for an Image object.
* @param w the width of the rectangle of pixels
* @param h the height of the rectangle of pixels
* @param cm the specified <code>ColorModel</code>
* @param pix an array of pixels
* @param off the offset into the array of where to store the
* first pixel
* @param scan the distance from one row of pixels to the next in
* the array
* @param props a list of properties that the <code>ImageProducer</code>
* uses to process an image
* @see java.awt.Component#createImage
*/
public ColorCyclingMemoryImageSource(int w, int h, ColorModel cm,
int[] pix, int off, int scan,
Hashtable<?, ?> props) {
super(w, h, cm, pix, off, scan, props);
initialize(w, h, cm, (Object) pix, off, scan, props);
}
private void initialize(int w, int h, ColorModel cm,
Object pix, int off, int scan, Hashtable props) {
width = w;
height = h;
model = cm;
pixels = pix;
pixeloffset = off;
pixelscan = scan;
if (props == null) {
props = new Hashtable();
}
properties = props;
}
public int getWidth() {
return width;
}
public int getHeight() {
return height;
}
public ColorModel getColorModel() {
return model;
}
public Hashtable getProperties() {
return properties;
}
@Override
public synchronized void newPixels(byte[] newpix, ColorModel newmodel,
int offset, int scansize) {
this.pixels = newpix;
this.model = newmodel;
this.pixeloffset = offset;
this.pixelscan = scansize;
super.newPixels(newpix, cycledModel == null ? newmodel : cycledModel, offset, scansize);
}
/**
* Changes to a new int array to hold the pixels for this image.
* If the animation flag has been turned on through the setAnimated()
* method, then the new pixels will be immediately delivered to any
* ImageConsumers that are currently interested in the data for
* this image.
* @param newpix the new pixel array
* @param newmodel the specified <code>ColorModel</code>
* @param offset the offset into the array
* @param scansize the distance from one row of pixels to the next in
* the array
* @see #newPixels(int, int, int, int, boolean)
* @see #setAnimated
*/
@Override
public synchronized void newPixels(int[] newpix, ColorModel newmodel,
int offset, int scansize) {
this.pixels = newpix;
this.model = newmodel;
this.pixeloffset = offset;
this.pixelscan = scansize;
super.newPixels(newpix, cycledModel == null ? newmodel : cycledModel, offset, scansize);
}
public void addColorCycle(ColorCycle cc) {
colorCycles.add(cc);
}
@Override
public void addConsumer(ImageConsumer ic) {
super.addConsumer(ic);
consumers.add(ic);
if (isStarted && !consumers.isEmpty()) {
startAnimationTimer();
}
}
@Override
public void removeConsumer(ImageConsumer ic) {
super.removeConsumer(ic);
consumers.remove(ic);
if (isStarted && consumers.isEmpty()) {
stopAnimationTimer();
}
}
@Override
public void setAnimated(boolean b) {
super.setAnimated(b);
isColorCyclingAvailable = b;
if (isColorCyclingAvailable && !consumers.isEmpty() && isStarted) {
startAnimationTimer();
} else {
stopAnimationTimer();
}
}
/** Starts or stops color cycling. */
public void setColorCyclingStarted(boolean b) {
isStarted = b;
if (isColorCyclingAvailable && !consumers.isEmpty() && isStarted) {
startAnimationTimer();
} else {
stopAnimationTimer();
}
}
/** Returns true if color cycling is on. */
public boolean isColorCyclingStarted() {
return isStarted;
}
/** Starts color cycling. */
public void start() {
setColorCyclingStarted(true);
}
/** Stops color cycling. */
public void stop() {
setColorCyclingStarted(false);
}
public boolean isStarted() {
return isColorCyclingStarted();
}
private synchronized void startAnimationTimer() {
if (timer != null) {
return;
}
if (model instanceof IndexColorModel) {
final IndexColorModel icm = (IndexColorModel) model;
final int[] rgbs = new int[icm.getMapSize()];
icm.getRGBs(rgbs);
// Calculate the timer delay
int delay = 1000;
int i = 0;
if (isBlendedColorCycling) {
for (ColorCycle cc : colorCycles) {
if (cc.isActive()) {
// Note: we divide 1000 by 4
// 2 for Nyquist Theorem (double sample rate)
// 2 for blending
int ccDelay = 1000 / 4 * cc.getTimeScale() / cc.getRate();
if (ccDelay < delay) {
delay = Math.max(1, ccDelay);
}
}
}
delay = Math.max(delay, 1000 / 60); // throttle at 60 fps
} else {
for (ColorCycle cc : colorCycles) {
if (cc.isActive()) {
// Note: we divide 1000 by 2 (=double sampling rate)
// because of Nyquist theorem
int ccDelay = 1000 / 2 * cc.getTimeScale() / cc.getRate();
if (ccDelay < delay) {
delay = Math.max(1, ccDelay);
}
}
}
}
timer = new Timer(delay, new ActionListener() {
private int[] previousCycled = new int[rgbs.length];
private int[] cycled = new int[rgbs.length];
long startTime = System.currentTimeMillis();
@Override
public void actionPerformed(ActionEvent evt) {
long now = System.currentTimeMillis();
System.arraycopy(rgbs, 0, cycled, 0, rgbs.length);
for (ColorCycle cc : colorCycles) {
cc.doCycle(cycled, now - startTime);
}
// We only fire new pixels, if the cycles have changed
if (!Arrays.equals(previousCycled, cycled)) {
ColorCyclingMemoryImageSource.super.newPixels((byte[]) pixels, //
cycledModel = new IndexColorModel(8, cycled.length, cycled, 0, false, -1, DataBuffer.TYPE_BYTE),
pixeloffset,
pixelscan);
}
// swap cycled colors
int[] tmp = previousCycled;
previousCycled = cycled;
cycled = tmp;
}
});
timer.setRepeats(true);
timer.start();
}
}
private synchronized void stopAnimationTimer() {
if (timer != null) {
timer.stop();
timer = null;
cycledModel = null;
// Reset colors to their initial state
ColorCyclingMemoryImageSource.super.newPixels((byte[]) pixels, //
model,
pixeloffset,
pixelscan);
}
}
/** Creates a BufferedImage which shares its pixel data with this memory image source. */
public BufferedImage toBufferedImage() {
DataBuffer buf = (pixels instanceof byte[]) ?//
new DataBufferByte((byte[]) pixels, pixelscan * height, pixeloffset) ://
new DataBufferInt((int[]) pixels, pixelscan * height, pixeloffset);
SampleModel sm = model.createCompatibleSampleModel(width, height);
WritableRaster raster = Raster.createWritableRaster(sm, buf, new Point());
return new BufferedImage(model, raster, false, properties);
}
public boolean isColorCyclingAvailable() {
return isColorCyclingAvailable;
}
@SuppressWarnings("unchecked")
public void putProperties(Hashtable props) {
properties.putAll(props);
}
public void setBlendedColorCycling(boolean newValue) {
isBlendedColorCycling = newValue;
for (ColorCycle cc : colorCycles) {
cc.setBlended(newValue);
}
if (isStarted()) {
stop();
start();
}
}
public boolean isBlendedColorCycling() {
return isBlendedColorCycling;
}
}