// Near Infinity - An Infinity Engine Browser and Editor
// Copyright (C) 2001 - 2005 Jon Olav Hauglid
// See LICENSE.txt for license information
package org.infinity.resource.video;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Rectangle;
import java.awt.RenderingHints;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.image.BufferedImage;
import javax.swing.JComponent;
import javax.swing.SwingConstants;
import org.infinity.gui.RenderCanvas;
/**
* A component optimized to display rapidly changing graphics data (e.g. videos, animations),
* providing support for scaling (with and without aspect ratio preservation) and filtering.
*/
public class ImageRenderer extends JComponent implements VideoBuffer, ComponentListener, SwingConstants
{
// interpolation types used in scaling
public static final Object TYPE_NEAREST_NEIGHBOR = RenderingHints.VALUE_INTERPOLATION_NEAREST_NEIGHBOR;
public static final Object TYPE_BILINEAR = RenderingHints.VALUE_INTERPOLATION_BILINEAR;
public static final Object TYPE_BICUBIC = RenderingHints.VALUE_INTERPOLATION_BICUBIC;
private final BasicVideoBuffer videoBuffer;
private final RenderCanvas canvas;
private boolean isAspect;
/**
* Creates an empty component. Call {@link #createBuffer(int, int, int)} to properly make use
* of the component's features.
*/
public ImageRenderer()
{
this(0, 0, 0);
}
/**
* Creates a buffer chain with the specified parameters.
* @param numBuffers The number of buffers to create.
* @param width The new width of the buffer in pixels.
* @param height The new height of the buffer in pixels.
*/
public ImageRenderer(int numBuffers, int width, int height)
{
setOpaque(false);
setLayout(null);
videoBuffer = new BasicVideoBuffer();
canvas = new RenderCanvas();
add(canvas);
createBuffer(numBuffers, width, height);
addComponentListener(this);
}
/**
* Creates a new buffer chain with the specified dimensions. Old buffers will be discarded.
* @param numBuffers The number of buffers to create.
* @param width The new width of the buffer in pixels.
* @param height The new height of the buffer in pixels.
* @return {@code true} if the buffer chain has been created successfully,
* {@code false} otherwise.
*/
public boolean createBuffer(int numBuffers, int width, int height)
{
if (numBuffers > 0 && width > 1 && height > 1) {
boolean res = videoBuffer.create(numBuffers, width, height, false);
if (res) {
updateDefaultSize();
updateCanvasBounds();
updateRenderer();
}
return res;
}
return false;
}
/**
* Gets the alignment of the component's content along the X axis.
* @return One of the following constants defined in SwingConstants: LEFT, CENTER or RIGHT.
*/
public int getHorizontalAlignment()
{
return canvas.getHorizontalAlignment();
}
/**
* Sets the alignment of the component's content along the X axis. The default property is CENTER.
* @param alignment One of the following constants defined in SwingConstants: LEFT, CENTER or RIGHT.
*/
public void setHorizontalAlignment(int alignment)
{
if (alignment != canvas.getHorizontalAlignment()) {
canvas.setHorizontalAlignment(alignment);
updateCanvasBounds();
}
}
/**
* Gets the alignment of the component's content along the Y axis.
* @return One of the following constants defined in SwingConstants: TOP, CENTER or BOTTOM.
*/
public int getVerticalAlignment()
{
return canvas.getVerticalAlignment();
}
/**
* Sets the alignment of the component's content along the Y axis. The default property is CENTER.
* @param alignment One of the following constants defined in SwingConstants: TOP, CENTER or BOTTOM.
*/
public void setVerticalAlignment(int alignment)
{
if (alignment != canvas.getVerticalAlignment()) {
canvas.setVerticalAlignment(alignment);
updateCanvasBounds();
}
}
/**
* Returns the width of the image buffers set in the constructor or {@code createBuffer()} method.
* @return Image buffer's width in pixels.
*/
public int getBufferWidth()
{
if (videoBuffer.frontBuffer() != null) {
return videoBuffer.frontBuffer().getWidth(null);
} else {
return 0;
}
}
/**
* Returns the height of the image buffers set in the constructor or {@code createBuffer()} method.
* @return Image buffer's height in pixels.
*/
public int getBufferHeight()
{
if (videoBuffer.frontBuffer() != null) {
return videoBuffer.frontBuffer().getHeight(null);
} else {
return 0;
}
}
/**
* Returns whether scaling has been activated.
* @return {@code true} if scaling has been activated.
*/
public boolean getScalingEnabled()
{
return canvas.isScalingEnabled();
}
/**
* Sets whether the image buffer's content should be drawn scaled.
* @param enable {@code true} to enable scaling.
*/
public void setScalingEnabled(boolean enable)
{
if (enable != canvas.isScalingEnabled()) {
canvas.setScalingEnabled(enable);
updateDefaultSize();
updateCanvasBounds();
}
}
/**
* Returns the interpolation type used when scaling has been enabled.
* @return The interpolation type.
*/
public Object getInterpolationType()
{
return canvas.getInterpolationType();
}
/**
* Specify the interpolation type used when scaling has been enabled.
* One of TYPE_NEAREST_NEIGHBOR, TYPE_BILINEAR and TYPE_BICUBIC (Default: TYPE_NEAREST_NEIGHBOR).
* @param interpolationType The new interpolation type to set.
*/
public void setInterpolationType(Object interpolationType)
{
canvas.setInterpolationType(interpolationType);
}
/**
* Returns whether the aspect ratio of the displayed image will be preserved.
* @return {@code true} if aspect ratio preservation has been enabled,
* {@code false} otherwise.
*/
public boolean getAspectRatioEnabled()
{
return isAspect;
}
/**
* Sets whether the aspect ratio of the displayed graphics should be preserved. This flag is used
* in conjunction with with {@link #setScalingEnabled(boolean)}.
* @param enable Set to {@code true} if aspect ratio should be preserved.
*/
public void setAspectRatioEnabled(boolean enable)
{
if (enable != isAspect) {
isAspect = enable;
if (canvas.isScalingEnabled()) {
updateCanvasBounds();
}
}
}
/**
* Updates the renderer to use the current front buffer to be displayed. The renderer will
* display a cached image otherwise.
*/
public void updateRenderer()
{
canvas.setImage(videoBuffer.frontBuffer());
}
/**
* Removes old content from all buffers in the buffer chain.
*/
public void clearBuffers()
{
for (int i = 0; i < videoBuffer.bufferCount(); i++) {
BufferedImage img = (BufferedImage)videoBuffer.frontBuffer();
if (img != null) {
Graphics2D g = img.createGraphics();
g.setColor(Color.BLACK);
g.fillRect(0, 0, img.getWidth(), img.getHeight());
g.dispose();
}
videoBuffer.flipBuffers();
}
updateRenderer();
}
//--------------------- Begin Interface VideoBuffer ---------------------
@Override
public Image frontBuffer()
{
return videoBuffer.frontBuffer();
}
@Override
public Image backBuffer()
{
return videoBuffer.backBuffer();
}
@Override
public synchronized void flipBuffers()
{
videoBuffer.flipBuffers();
}
@Override
public int bufferCount()
{
return videoBuffer.bufferCount();
}
@Override
public void attachData(Object data)
{
videoBuffer.attachData(data);
}
@Override
public Object fetchData()
{
return videoBuffer.fetchData();
}
//--------------------- End Interface VideoBuffer ---------------------
//--------------------- Begin Interface ComponentListener ---------------------
@Override
public void componentHidden(ComponentEvent event)
{
}
@Override
public void componentMoved(ComponentEvent event)
{
}
@Override
public void componentResized(ComponentEvent event)
{
if (event.getSource() == this) {
updateCanvasBounds();
}
}
@Override
public void componentShown(ComponentEvent event)
{
}
//--------------------- End Interface ComponentListener ---------------------
private void updateDefaultSize()
{
if (getScalingEnabled()) {
setPreferredSize(getMinimumSize());
} else {
setPreferredSize(new Dimension(getBufferWidth(), getBufferHeight()));
}
}
private void updateCanvasBounds()
{
if (getWidth() > 0 && getHeight() > 0) {
Rectangle newBounds = new Rectangle();
// updating canvas size
if (canvas.isScalingEnabled()) {
newBounds.width = getWidth();
newBounds.height = getHeight();
if (isAspect && getBufferHeight() > 0) {
float ar = (float)getBufferWidth() / (float)getBufferHeight();
if ((float)newBounds.width / ar <= newBounds.height) {
// use width as base
newBounds.height = (int)((float)newBounds.width / ar);
} else {
// use height as base
newBounds.width = (int)((float)newBounds.height * ar);
}
}
} else {
newBounds.width = getBufferWidth();
newBounds.height = getBufferHeight();
}
// updating canvas position
switch (canvas.getHorizontalAlignment()) {
case SwingConstants.LEFT:
newBounds.x = 0;
break;
case SwingConstants.RIGHT:
newBounds.x = getWidth() - newBounds.width;
break;
default:
newBounds.x = (getWidth() - newBounds.width) / 2;
}
switch (canvas.getVerticalAlignment()) {
case SwingConstants.TOP:
newBounds.y = 0;
break;
case SwingConstants.BOTTOM:
newBounds.y = getHeight() - newBounds.height;
break;
default:
newBounds.y = (getHeight() - newBounds.height) / 2;
}
canvas.setBounds(newBounds);
}
}
}