// 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.are.viewer;
import java.awt.AlphaComposite;
import java.awt.Color;
import java.awt.Graphics2D;
import java.awt.Image;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.util.Arrays;
import org.infinity.gui.layeritem.BasicAnimationProvider;
import org.infinity.resource.graphics.BamDecoder;
import org.infinity.resource.graphics.BamV1Decoder;
import org.infinity.resource.graphics.ColorConvert;
/**
* Implements functionality for properly displaying background animations.
*/
public class BackgroundAnimationProvider implements BasicAnimationProvider
{
private static final Color TransparentColor = new Color(0, true);
// upscaled luma weights for use with faster right-shift (by 16)
private static final int LumaR = 19595, LumaG = 38470, LumaB = 7471;
// lookup table for alpha transparency on blended animations
private static final int[] TableAlpha = new int[256];
static {
for (int i = 0; i < TableAlpha.length; i++) {
TableAlpha[i] = (int)(Math.pow((double)i / 255.0, 0.5) * 256.0);
}
}
private BamDecoder bam;
private BamDecoder.BamControl control;
private boolean isActive, isActiveIgnored, isBlended, isMirrored, isLooping, isSelfIlluminated,
isMultiPart, isPaletteEnabled;
private int lighting, baseAlpha;
private int firstFrame, lastFrame;
private int[] palette; // external palette
private Rectangle imageRect;
private BufferedImage image, working;
public BackgroundAnimationProvider()
{
this(null);
}
public BackgroundAnimationProvider(BamDecoder bam)
{
setDefaults();
setAnimation(bam);
}
/** Returns the BAM animation object. */
public BamDecoder getAnimation()
{
return bam;
}
public void setAnimation(BamDecoder bam)
{
if (bam != null) {
this.bam = bam;
} else {
// setting pseudo bam
this.bam = BamDecoder.loadBam(ColorConvert.createCompatibleImage(1, 1, true));
}
this.control = this.bam.createControl();
this.control.setMode(BamDecoder.BamControl.Mode.INDIVIDUAL);
this.control.setSharedPerCycle(!isMultiPart());
if (this.control instanceof BamV1Decoder.BamV1Control) {
((BamV1Decoder.BamV1Control)this.control).setTransparencyMode(BamV1Decoder.TransparencyMode.NORMAL);
}
resetFrame();
updateCanvas();
updateGraphics();
}
/** Sets an external palette that can be used to recolor the BAM animation. */
public void setPalette(int[] palette)
{
if (palette != null && palette.length >= 256) {
this.palette = new int[256];
System.arraycopy(palette, 0, this.palette, 0, 256);
} else {
this.palette = null;
}
if (isPaletteEnabled() && control instanceof BamV1Decoder.BamV1Control) {
((BamV1Decoder.BamV1Control)control).setExternalPalette(this.palette);
}
updateGraphics();
}
public boolean isPaletteEnabled()
{
return isPaletteEnabled;
}
public void setPaletteEnabled(boolean set)
{
if (set != isPaletteEnabled) {
isPaletteEnabled = set;
if (control instanceof BamV1Decoder.BamV1Control) {
if (isPaletteEnabled) {
((BamV1Decoder.BamV1Control)control).setExternalPalette(this.palette);
} else {
((BamV1Decoder.BamV1Control)control).setExternalPalette(null);
}
}
updateGraphics();
}
}
/** Returns the active/visibility state of the animation. */
public boolean isActive()
{
return isActive;
}
/** Specify whether to actually display the animation on screen. */
public void setActive(boolean set)
{
if (set != isActive) {
isActive = set;
updateGraphics();
}
}
/**
* Returns whether to ignore the activation state of the animation.
*/
public boolean isActiveIgnored()
{
return isActiveIgnored;
}
/**
* Specify whether to ignore the activation state of the animation and display it regardless.
*/
public void setActiveIgnored(boolean set)
{
if (set != isActiveIgnored) {
isActiveIgnored = set;
updateGraphics();
}
}
/** Returns whether the current frame of all available cycles are displayed simultanuously. */
public boolean isMultiPart()
{
return isMultiPart;
}
/** Sets whether the current frame of all available cycles are displayed simultanuously. */
public void setMultiPart(boolean set)
{
if (set != isMultiPart) {
isMultiPart = set;
control.setSharedPerCycle(!isMultiPart);
updateCanvas();
updateGraphics();
}
}
/** Returns whether the animation uses brightness as alpha transparency. */
public boolean isBlended()
{
return isBlended;
}
/** Sets whether the animation uses its brightness as alpha transparency. */
public void setBlended(boolean set)
{
if (set != isBlended) {
isBlended = set;
updateGraphics();
}
}
/** Returns whether the animation is drawn mirrored on the x axis. */
public boolean isMirrored()
{
return isMirrored;
}
/** Sets whether the animation is mirrored on the x axis. */
public void setMirrored(boolean set)
{
if (set != isMirrored) {
isMirrored = set;
updateCanvas();
updateGraphics();
}
}
/** Returns whether the animation ignores lighting conditions. */
public boolean isSelfIlluminated()
{
return isSelfIlluminated;
}
/** Sets whether the animation ignores lighting conditions */
public void setSelfIlluminated(boolean set)
{
if (set != isSelfIlluminated) {
isSelfIlluminated = set;
updateGraphics();
}
}
/** Returns the lighting condition of the animation. */
public int getLighting()
{
return lighting;
}
/** Defines a new lighting condition for the animation. No change if the animation is self-illuminated. */
public void setLighting(int state)
{
switch (state) {
case ViewerConstants.LIGHTING_DAY:
case ViewerConstants.LIGHTING_TWILIGHT:
case ViewerConstants.LIGHTING_NIGHT:
if (state != lighting) {
lighting = state;
if (!isSelfIlluminated()) {
updateGraphics();
}
}
break;
}
}
public int getBaseAlpha()
{
return baseAlpha;
}
public void setBaseAlpha(int alpha)
{
if (alpha >= 0 && alpha < 256 && alpha != baseAlpha) {
baseAlpha = alpha;
updateGraphics();
}
}
/** Sets a new looping state. */
public void setLooping(boolean set)
{
if (set != isLooping) {
isLooping = set;
}
}
/**
* Returns the currently selected cycle if {@link #isMultiPart()} is {@code false}.
* @return The currently selected cycle
*/
public int getCycle()
{
if (!isMultiPart()) {
return control.cycleGet();
} else {
return -1;
}
}
/**
* Sets the current BAM cycle. Does nothing if {@link #isMultiPart()} is {@code true}.
* @param cycle The BAM cycle to set.
*/
public void setCycle(int cycle)
{
control.cycleSet(cycle);
updateCanvas();
updateGraphics();
}
/**
* Returns the initial starting frame of the animation when starting playback.
* @return The starting frame of the animation.
*/
public int getStartFrame()
{
return firstFrame;
}
/**
* Sets the initial starting frame of the animation when starting playback.
* @param frameIdx The starting frame of the animation.
*/
public void setStartFrame(int frameIdx)
{
if (frameIdx >= 0 && frameIdx < control.cycleFrameCount()) {
if (lastFrame >= 0 && frameIdx > lastFrame) {
firstFrame = lastFrame;
} else {
firstFrame = frameIdx;
}
if (control.cycleGetFrameIndex() < firstFrame) {
control.cycleSetFrameIndex(firstFrame);
updateGraphics();
}
}
}
/**
* Returns the frame index after which the animation sequence ends.
*/
public int getFrameCap()
{
return (lastFrame < 0) ? control.cycleFrameCount() - 1 : lastFrame;
}
/**
* Sets the frame index after which the animation sequence ends.
* @param frameIdx Index of the last frame in the animation sequence to be displayed.
* Set to -1 to disable frame cap.
*/
public void setFrameCap(int frameIdx)
{
if (frameIdx < 0) {
lastFrame = -1;
} else if (frameIdx < control.cycleFrameCount()) {
lastFrame = frameIdx;
if (firstFrame > lastFrame) {
firstFrame = lastFrame;
}
if (control.cycleGetFrameIndex() > lastFrame) {
control.cycleSetFrameIndex(lastFrame);
updateGraphics();
}
}
}
//--------------------- Begin Interface BasicAnimationProvider ---------------------
@Override
public Image getImage()
{
return image;
}
@Override
public boolean advanceFrame()
{
boolean retVal = false;
if (lastFrame >= 0) {
if (control.cycleGetFrameIndex() < lastFrame) {
retVal = control.cycleNextFrame();
}
} else {
retVal = control.cycleNextFrame();
}
updateGraphics();
return retVal;
}
@Override
public void resetFrame()
{
control.cycleSetFrameIndex(firstFrame);
updateGraphics();
}
@Override
public boolean isLooping()
{
return isLooping;
}
@Override
public Point getLocationOffset()
{
return imageRect.getLocation();
}
//--------------------- End Interface BasicAnimationProvider ---------------------
// Sets sane default values for all properties
private void setDefaults()
{
isActive = true;
isActiveIgnored = false;
isBlended = false;
isMirrored = false;
isLooping = true;
isSelfIlluminated = true;
isMultiPart = false;
isPaletteEnabled = false;
palette = null;
lighting = ViewerConstants.LIGHTING_TWILIGHT;
baseAlpha = 255;
firstFrame = 0;
lastFrame = -1;
imageRect = null;
image = null;
working = null;
}
// Updates the global image object to match the shared size of the current BAM (cycle).
private void updateCanvas()
{
imageRect = control.calculateSharedCanvas(isMirrored());
if (working == null || image == null ||
image.getWidth() != imageRect.width || image.getHeight() != imageRect.height) {
image = ColorConvert.createCompatibleImage(imageRect.width, imageRect.height, true);
working = new BufferedImage(imageRect.width, imageRect.height, BufferedImage.TYPE_INT_ARGB);
}
}
// Renders the current frame
private synchronized void updateGraphics()
{
if (image != null) {
if (isActive() || isActiveIgnored()) {
// preparing frames
int[] frameIndices = null;
if (isMultiPart()) {
frameIndices = new int[control.cycleCount()];
for (int i = 0, cCount = control.cycleCount(); i < cCount; i++) {
frameIndices[i] = control.cycleGetFrameIndexAbsolute(i, control.cycleGetFrameIndex());
}
} else {
frameIndices = new int[]{control.cycleGetFrameIndexAbsolute()};
}
// clearing old content
Graphics2D g = image.createGraphics();
try {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
g.setColor(TransparentColor);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
// rendering frame
for (int i = 0; i < frameIndices.length; i++) {
// fetching frame data
int[] buffer = ((DataBufferInt)working.getRaster().getDataBuffer()).getData();
Arrays.fill(buffer, 0);
if (bam instanceof BamV1Decoder) {
((BamV1Decoder)bam).frameGet(control, frameIndices[i], working);
} else {
bam.frameGet(control, frameIndices[i], working);
}
// post-processing frame
buffer = ((DataBufferInt)working.getRaster().getDataBuffer()).getData();
int canvasWidth = working.getWidth();
int canvasHeight = working.getHeight();
int frameWidth = bam.getFrameInfo(frameIndices[i]).getWidth();
int frameHeight = bam.getFrameInfo(frameIndices[i]).getHeight();
if (isMirrored()) {
mirrorImage(buffer, canvasWidth, canvasHeight, frameWidth, frameHeight);
}
if (baseAlpha < 255) {
applyAlpha(buffer, canvasWidth, canvasHeight, frameWidth, frameHeight, getBaseAlpha());
}
if (isBlended()) {
applyBlending(buffer, canvasWidth, canvasHeight, frameWidth, frameHeight);
}
if (!isSelfIlluminated()) {
applyLighting(buffer, canvasWidth, canvasHeight, frameWidth, frameHeight, getLighting());
}
buffer = null;
// rendering frame
int left, top;
if (isMirrored()) {
left = -imageRect.x - (bam.getFrameInfo(frameIndices[i]).getWidth() - bam.getFrameInfo(frameIndices[i]).getCenterX() - 1);
top = -imageRect.y - bam.getFrameInfo(frameIndices[i]).getCenterY();
} else {
left = -imageRect.x - bam.getFrameInfo(frameIndices[i]).getCenterX();
top = -imageRect.y - bam.getFrameInfo(frameIndices[i]).getCenterY();
}
g.drawImage(working, left, top, left+frameWidth, top+frameHeight, 0, 0, frameWidth, frameHeight, null);
}
} finally {
g.dispose();
g = null;
}
} else {
Graphics2D g = image.createGraphics();
try {
g.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC));
g.setColor(TransparentColor);
g.fillRect(0, 0, image.getWidth(), image.getHeight());
} finally {
g.dispose();
g = null;
}
}
}
}
// Mirrors image along the x axis
private void mirrorImage(int[] buffer, int cw, int ch, int fw, int fh)
{
if (buffer != null && cw > 0 && ch > 0 && fw > 0 && fh > 0) {
int maxOfs = fh*cw;
if (buffer.length >= maxOfs) {
int ofs = 0;
while (ofs < maxOfs) {
int left = 0;
int right = fw - 1;
while (left < right) {
int pixel = buffer[ofs+left];
buffer[ofs+left] = buffer[ofs+right];
buffer[ofs+right] = pixel;
left++;
right--;
}
ofs += cw;
}
}
}
}
// applies a global alpha value to all pixels
private void applyAlpha(int[] buffer, int cw, int ch, int fw, int fh, int alpha)
{
if (buffer != null && cw > 0 && ch > 0 && fw > 0 && fh > 0) {
int maxOfs = fh*cw;
if (buffer.length >= maxOfs) {
if (alpha < 0) alpha = 0; else if (alpha > 255) alpha = 255;
alpha *= 65793; // upscaling for use with faster right-shift (by 24)
int ofs = 0;
while (ofs < maxOfs) {
for (int x = 0; x < fw; x++) {
int pixel = buffer[ofs+x];
if ((pixel & 0xff000000) != 0) {
int a = (pixel >>> 24) & 0xff; // using right shift because of upscaled alpha
a = (a*alpha) >>> 24;
buffer[ofs+x] = (a << 24) | (pixel & 0x00ffffff);
}
}
ofs += cw;
}
}
}
}
// Blends pixels based on their brightness
private void applyBlending(int[] buffer, int cw, int ch, int fw, int fh)
{
if (buffer != null && cw > 0 && ch > 0 && fw > 0 && fh > 0) {
int maxOfs = fh*cw;
if (buffer.length >= maxOfs) {
int ofs = 0;
while (ofs < maxOfs) {
for (int x = 0; x < fw; x++) {
int pixel = buffer[ofs+x];
if ((pixel & 0xff000000) != 0) {
int a = (pixel >>> 24) & 0xff;
int r = (pixel >>> 16) & 0xff;
int g = (pixel >>> 8) & 0xff;
int b = pixel & 0xff;
a = (a*TableAlpha[((r*LumaR) + (g*LumaG) + (b*LumaB)) >>> 16]) >>> 8;
if (a > 255) a = 255;
buffer[ofs+x] = (a << 24) | (pixel & 0x00ffffff);
}
}
ofs += cw;
}
}
}
}
// Applies lightning conditions to all pixels
private void applyLighting(int[] buffer, int cw, int ch, int fw, int fh, int lighting)
{
if (buffer != null && cw > 0 && ch > 0 && fw > 0 && fh > 0) {
int maxOfs = fh*cw;
if (buffer.length >= maxOfs) {
if (lighting < ViewerConstants.LIGHTING_DAY) lighting = ViewerConstants.LIGHTING_DAY;
else if (lighting > ViewerConstants.LIGHTING_NIGHT) lighting = ViewerConstants.LIGHTING_NIGHT;
int ofs = 0;
while (ofs < maxOfs) {
for (int x = 0; x < fw; x++) {
int pixel = buffer[ofs+x];
if ((pixel & 0xff000000) != 0) {
int r = (pixel >>> 16) & 0xff;
int g = (pixel >>> 8) & 0xff;
int b = pixel & 0xff;
r = (r*TilesetRenderer.LightingAdjustment[lighting][0]) >>> TilesetRenderer.LightingAdjustmentShift;
g = (g*TilesetRenderer.LightingAdjustment[lighting][1]) >>> TilesetRenderer.LightingAdjustmentShift;
b = (b*TilesetRenderer.LightingAdjustment[lighting][2]) >>> TilesetRenderer.LightingAdjustmentShift;
if (r > 255) r = 255;
if (g > 255) g = 255;
if (b > 255) b = 255;
buffer[ofs+x] = (pixel & 0xff000000) | (r << 16) | (g << 8) | b;
}
}
ofs += cw;
}
}
}
}
}