/*
* Copyright 2008 Eckhart Arnold (eckhart_arnold@hotmail.com).
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package de.eckhartarnold.client;
import java.lang.String;
import java.lang.Math;
import java.util.ArrayList;
import java.util.HashMap;
import com.google.gwt.core.client.Duration;
import com.google.gwt.core.client.GWT;
import com.google.gwt.event.dom.client.ClickEvent;
import com.google.gwt.event.dom.client.ClickHandler;
import com.google.gwt.event.dom.client.ErrorEvent;
import com.google.gwt.event.dom.client.ErrorHandler;
import com.google.gwt.event.dom.client.HasClickHandlers;
import com.google.gwt.event.dom.client.HasMouseDownHandlers;
import com.google.gwt.event.dom.client.HasMouseMoveHandlers;
import com.google.gwt.event.dom.client.HasMouseUpHandlers;
import com.google.gwt.event.dom.client.HasMouseWheelHandlers;
import com.google.gwt.event.dom.client.HasTouchStartHandlers;
//import com.google.gwt.event.dom.client.HasTouchEndHandlers;
//import com.google.gwt.event.dom.client.HasTouchMoveHandlers;
//import com.google.gwt.event.dom.client.HasTouchStartHandlers;
import com.google.gwt.event.dom.client.LoadEvent;
import com.google.gwt.event.dom.client.LoadHandler;
import com.google.gwt.event.dom.client.MouseDownEvent;
import com.google.gwt.event.dom.client.MouseDownHandler;
import com.google.gwt.event.dom.client.MouseMoveEvent;
import com.google.gwt.event.dom.client.MouseMoveHandler;
import com.google.gwt.event.dom.client.MouseUpEvent;
import com.google.gwt.event.dom.client.MouseUpHandler;
import com.google.gwt.event.dom.client.MouseWheelEvent;
import com.google.gwt.event.dom.client.MouseWheelHandler;
import com.google.gwt.event.dom.client.TouchStartEvent;
import com.google.gwt.event.dom.client.TouchStartHandler;
//import com.google.gwt.event.dom.client.TouchEndEvent;
//import com.google.gwt.event.dom.client.TouchEndHandler;
//import com.google.gwt.event.dom.client.TouchMoveEvent;
//import com.google.gwt.event.dom.client.TouchMoveHandler;
//import com.google.gwt.event.dom.client.TouchStartEvent;
//import com.google.gwt.event.dom.client.TouchStartHandler;
import com.google.gwt.event.shared.HandlerRegistration;
import com.google.gwt.user.client.Event;
import com.google.gwt.user.client.Window;
import com.google.gwt.user.client.ui.AbsolutePanel;
import com.google.gwt.user.client.ui.Composite;
import com.google.gwt.user.client.ui.Image;
import com.google.gwt.user.client.ui.SimplePanel;
import com.google.gwt.user.client.ui.Widget;
/**
* Displays a photo on an absolute panel.
*
* <p>The image is scaled so as to use as much space of the
* panel as possible without distorting the aspect ratio of the image.
* When the image is exchanged the old image is faded out while
* the new image is faded in smoothly.
*
* @author eckhart
*/
public class ImagePanel extends Composite implements HasMouseMoveHandlers,
HasMouseWheelHandlers, SourcesAttachmentEvents, ResizeListener,
HasClickHandlers, HasMouseDownHandlers, HasMouseUpHandlers ,
HasTouchStartHandlers /* , HasTouchMoveHandlers, HasTouchEndHandlers */ {
/**
* Interface for event listeners of <code>ImagePanel</code>.
*/
public interface DisplayListener {
/**
* Called by the time a new image is
* fully displayed (i.e. loaded and faded in).
*/
void onDisplay();
/**
* Called when image starts fading. <code>onFade()</code>
* will not be called if fading is disabled or skipped for some other
* reason!
*/
void onFade();
}
private class ImageLoadHandler implements LoadHandler {
private int fading;
private NotifyingFade fader = null;
private Widget lastSender = null;
private Duration loadDuration = new Duration();
public ImageLoadHandler(int fading) {
this.fading = fading;
}
public boolean isLoaded() {
return lastSender != null;
}
public void onLoad(LoadEvent event) {
Widget sender = (Widget) event.getSource();
if (sender == active && sender != lastSender) { // for some reason LOAD events are sometimes fired twice (due to bad IE compatitibilty tests?)!?
lastSender = sender;
// adjustSize((Image) sender); <- already done by exchangeImage()!
if (fading == 0) {
Fade.setOpacity((Image) sender, 1.0);
if (passive != null) Fade.setOpacity(passive, 0.0);
fireDisplay();
} else if (fading > 0) {
startFading(this);
} else { // fading < 0
if (fader != null && fader.isComplete()) {
fireDisplay();
}
}
// use smaller sized images in the future if connection is slow...
int elapsed = loadDuration.elapsedMillis();
GWT.log("Duration : " + String.valueOf(elapsed));
if (elapsed > duration) {
GWT.log(String.valueOf(elapsed) + "; " + String.valueOf(duration));
if (sizeBias < sizes.length) sizeBias++;
} else {
while (sizeBias > 0 && elapsed < duration / 3) {
sizeBias--;
elapsed = elapsed * 3;
}
}
GWT.log("sizeBias : " + String.valueOf(sizeBias));
}
}
public void connectFader(NotifyingFade fader) {
if (this.fader == null) {
this.fader = fader;
if (fader.isComplete() && isLoaded()) fireDisplay();
}
}
}
private class ImageErrorHandler implements ErrorHandler {
public void onError(ErrorEvent event) {
Image img = (Image) event.getSource();
GWT.log("ImagePanel.ImageErrorHandler.onError:\n "+img.getUrl(), null);
}
}
private class NotifyingFade extends Fade {
private ImageLoadHandler loadListener;
private boolean completed = false;
NotifyingFade(Widget widget, ImageLoadHandler loadHandler) {
super(widget, 0.0, 1.0, FADE_IN_STEPS);
this.loadListener = loadHandler;
loadHandler.connectFader(this);
}
public boolean isComplete() {
return completed;
}
@Override
protected void onComplete() {
super.onComplete();
completed = true;
if (loadListener.isLoaded()) fireDisplay();
}
}
/**
* A Fade class for fading out the old image that triggers fading in the new
* image only after the old image has faded out completely.
*/
private class ChainedFade extends Fade {
private Fade nextFade;
ChainedFade(Widget widget, Fade nextFade, double steps) {
super(widget, 1.0, 0.0, steps);
this.nextFade = nextFade;
}
@Override
protected void onComplete() {
super.onComplete();
nextFade.run(Math.abs(fading));
}
}
// possible anchors of the notifier overlay image
public static int WEST = 1, CENTER = 2, EAST = 3;
private static final double FADE_IN_STEPS = 0.10;
private static final double FADE_OUT_STEPS = 0.13;
/**
* The currently active image. When a new photo is loaded the image that
* was displayed previously is declared passive and a new active image
* is created in the front that takes the new photo and is faded in.
*/
protected Image active;
/**
* An envelope panel that wraps the <code>AbsolutePanel</code>. This is
* necessary for working around some browser differences in connection with
* resizing.
*/
protected SimplePanel envelope;
/**
* A semi transparent notification image above the slide show images. The
* notifier image can be used to display visual feed back for touch events.
*/
protected Image notifier;
/** Flag storing the anchor of the notifier (WEST, CENTER or EAST) */
protected int notifierAnchor;
/**
* The main widget of the image panel. The images are placed on panel and
* their size adjusted.
*/
protected AbsolutePanel panel;
/**
* The image that was displayed out and is either already invisible,
* i.e. faded out, or just about to be faded out.
*/
protected Image passive;
private int duration = 5000; // duration in msecs for which the an image will be displayed
private int fading = -750; // a negative value indicates that fading in and fading out should take place in sequence
private NotifyingFade fadeIn;
private Fade fadeOut;
private String[] imageNames;
private int panelW, panelH;
private int sizeStep = -1; // a negative value means: no multiple image sizes present
private int sizeBias = 0; // bias in case of slow connection
private int[][] sizes;
private ArrayList<AttachmentListener> attachmentListeners;
private DisplayListener displayListener;
private ImageErrorHandler stdImageErrorHandler = new ImageErrorHandler();
/**
* Constructor of class <code>ImagePanel</code>. Before the flip panel becomes
* visible, it must be added somewhere to the DOM tree (e.g. to the root panel)
* <em>and</em> the <code>onResized</code> method must be called.
*/
public ImagePanel(ImageCollectionInfo collection) {
HashMap<String, String> info = collection.getInfo();
// for (String key: info.keySet()) {
// GWT.log(key+": "+info.get(key));
// }
String numStr = info.get("display duration");
if (numStr != null && !numStr.isEmpty()) {
duration = Integer.parseInt(numStr);
assert duration > 500;
}
numStr = info.get("image fading");
if (numStr != null && !numStr.isEmpty()) {
fading = Integer.parseInt(numStr);
}
envelope = new SimplePanel();
panel = new AbsolutePanel();
panel.addStyleName("imageBackground");
envelope.setWidget(panel);
// envelope.setSize("100%", "100%");
initWidget(envelope);
setSize("100%", "100%");
sinkEvents(Event.ONCLICK | Event.MOUSEEVENTS | Event.ONMOUSEWHEEL |
Event.ONTOUCHSTART );
}
/* (non-Javadoc)
* @see de.eckhartarnold.client.SourcesAttachmentEvents#addAttachmentListener(de.eckhartarnold.client.AttachmentListener)
*/
public void addAttachmentListener(AttachmentListener listener) {
if (attachmentListeners == null)
attachmentListeners = new ArrayList<AttachmentListener>();
attachmentListeners.add(listener);
}
@Override
public HandlerRegistration addMouseDownHandler(MouseDownHandler handler) {
return addHandler(handler, MouseDownEvent.getType());
}
@Override
public HandlerRegistration addClickHandler(ClickHandler handler) {
return addHandler(handler, ClickEvent.getType());
}
/* (non-Javadoc)
* @see com.google.gwt.event.dom.client.HasMouseMoveHandlers#addMouseMoveHandler(com.google.gwt.event.dom.client.MouseMoveHandler)
*/
@Override
public HandlerRegistration addMouseMoveHandler(MouseMoveHandler handler) {
return addHandler(handler, MouseMoveEvent.getType());
}
@Override
public HandlerRegistration addMouseUpHandler(MouseUpHandler handler) {
return addHandler(handler, MouseUpEvent.getType());
}
/* (non-Javadoc)
* @see com.google.gwt.event.dom.client.HasMouseWheelHandlers#addMouseWheelHandler(com.google.gwt.event.dom.client.MouseWheelHandler)
*/
@Override
public HandlerRegistration addMouseWheelHandler(MouseWheelHandler handler) {
return addHandler(handler, MouseWheelEvent.getType());
}
// /* (non-Javadoc)
// * @see com.google.gwt.event.dom.client.HasTouchEndHandlers#addTouchEndHandler(com.google.gwt.event.dom.client.TouchEndHandler)
// */
// public HandlerRegistration addTouchEndHandler(TouchEndHandler handler) {
// return addHandler(handler, TouchEndEvent.getType());
// }
//
// /* (non-Javadoc)
// * @see com.google.gwt.event.dom.client.HasTouchMoveHandlers#addTouchStartHandler(com.google.gwt.event.dom.client.TouchMoveHandler)
// */
// public HandlerRegistration addTouchMoveHandler(TouchMoveHandler handler) {
// return addHandler(handler, TouchMoveEvent.getType());
// }
//
/* (non-Javadoc)
* @see com.google.gwt.event.dom.client.HasTouchStartHandlers#addTouchStartHandler(com.google.gwt.event.dom.client.TouchStartHandler)
*/
public HandlerRegistration addTouchStartHandler(TouchStartHandler handler) {
return addHandler(handler, TouchStartEvent.getType());
}
/**
* Removes the image from the panel so that an empty panel
* is shown.
*/
public void clear() {
cancelFading(false);
if (active != null) {
panel.remove(active);
active = null;
}
if (passive != null) {
panel.remove(passive);
passive = null;
}
}
/**
* Returns the duration for which an image will be displayed without fading.
* @return The duration for image display in milliseconds.
*/
public int getDuration() {
return duration;
}
/**
* Returns the fading duration. A negative value indicates that fading is
* sequentially, only after the last image has faded out completely,
* the new image fades in. In this case the actual fading time is twice
* the absolute value of <code>fading</code>
* @return The duration of fading in milliseconds.
*/
public int getFading() {
return fading;
}
/**
* Returns the URL of the <em>largest size version(!)</em> of the currently
* displayed image.
* @return the image's URL
*/
public String getImageURL() {
return imageNames[imageNames.length-1];
}
// /* (non-Javadoc)
// * @see com.google.gwt.user.client.ui.Composite#onBrowserEvent(com.google.gwt.user.client.Event)
// */
// @Override
// public void onBrowserEvent(Event event) {
// switch (event.getTypeInt()) {
// case Event.ONCLICK: {
// if (clickListeners != null) {
// clickListeners.fireClick(this);
// }
// break;
// }
// case Event.ONMOUSEDOWN:
// case Event.ONMOUSEUP:
// case Event.ONMOUSEMOVE:
// case Event.ONMOUSEOVER:
// case Event.ONMOUSEOUT: {
// if (mouseListeners != null) {
// mouseListeners.fireMouseEvent(this, event);
// }
// break;
// }
// case Event.ONMOUSEWHEEL: {
// if (mouseWheelListeners != null) {
// mouseWheelListeners.fireMouseWheelEvent(this, event);
// }
// break;
// }
// }
// }
/**
* Returns the panel proper, i.e. the absolute panel on which the images
* are placed.
* @return An absolute panel inside the image panel.
*/
public AbsolutePanel getPanel() {
return panel;
}
/**
* Resizes the displayed image(s), if the containing
* <code>FlipImagePanel</code> has been resized.
* The <code>resize</code> method is called automatically when any of
* the methods <code>setSize, setPixelSize, setWidth, setHeight</code>
* is called.
*/
public void onResized() {
assert envelope.getWidget() == null : "prepareResized() must be called before onResized()!";
int lastPanelW = panelW;
int lastPanelH = panelH;
// envelope.clear();
panelW = envelope.getOffsetWidth();
panelH = envelope.getOffsetHeight();
if (panelH <= 100) { // unlikely small height: non-quirks mode !?
panelH = Window.getClientHeight();
if (lastPanelH == panelH) lastPanelH--;
}
// GWT.log("ImagePanel.onResized: panelH: "+String.valueOf(panelH));
envelope.setWidget(panel);
if (lastPanelW != panelW || lastPanelH != panelH) {
panel.setPixelSize(panelW, panelH);
if (active != null) adjustSize(active);
if (sizeStep >= 0) {
int newStep = pickSize(sizes);
if (newStep != sizeStep) {
sizeStep = newStep;
quickExchangeImage(imageNames[sizeStep]);
return;
}
}
if (passive != null) adjustSize(passive);
positionNotifier();
}
}
/**
* Removes the {@link AbsolutePanel} so that the image panels size
* can be determined correctly by the browser before calling
* <code>onResized</code>. The <code>AbsolutePanel</code>
* is restored by method <code>onResized</code>. This method therefore
* should always be used in conjunction with the method
* <code>onResized</code>.
*/
public void prepareResized() {
panel.setSize("100%", "100%"); // required for Internet Explorer 7 compatibility!
envelope.clear();
}
/* (non-Javadoc)
* @see de.eckhartarnold.client.SourcesAttachmentEvents#removeAttachmentListener(de.eckhartarnold.client.AttachmentListener)
*/
public void removeAttachmentListener(AttachmentListener listener) {
if (attachmentListeners != null) {
attachmentListeners.remove(listener);
if (attachmentListeners.isEmpty()) attachmentListeners = null;
}
}
/**
* Sets the duration for which an image will be displayed without fading.
* @param msecs The duration for image display in milliseconds.
*/
public void setDuration(int msecs) {
assert msecs > 500;
duration = msecs;
}
/**
* Sets the duration for fading in a new image. A negative value of the
* parameter <code>msecs</code> indicates that fading in and fading out does
* not occur synchronously but in sequence. Please observe that in this case
* the total fading phase takes twice as long!
*
* @param msecs The duration of fading in or out in milliseconds.
*/
public void setFading(int msecs) {
this.fading = msecs;
if (msecs == 0) {
cancelFading(true);
}
}
/**
* Sets the notifier image, i.e. an overlay image above the slides that may
* be used to indicate feedback to touch events or the like. The notifier
* images opacity will be set to fully transparent in the beginning. It is
* the callers duty to change this so that it may become visible to the user.
* Any previously set notifier will be removed from the panel. It is possible
* to set the notifier to <code>null</code> thereby clearing the notifier.
* @param notifier the notifier image to be added to the panel
* @param anchor the anchor, i.e. position of the image, e.g. WEST,
* CENTER or EAST.
*/
public void setNotifier(Image notifier, int anchor) {
if (this.notifier != null) {
panel.remove(this.notifier);
}
this.notifier = notifier;
this.notifierAnchor = anchor;
if (notifier != null) {
notifier.addStyleName("notifier");
Fade.setOpacity(notifier, 0.0);
panel.add(notifier);
positionNotifier();
}
}
// @Override
// public void setHeight(String height) {
// super.setHeight(height);
// onResized();
// }
//
// @Override
// public void setPixelSize(int width, int height) {
// super.setPixelSize(width, height);
// onResized();
// }
//
// @Override
// public void setSize(String width, String height) {
// super.setSize(width, height);
// onResized();
// }
//
// @Override
// public void setWidth(String width) {
// super.setWidth(width);
// onResized();
// }
/**
* Shows the image that the given URl points to.
* @param url the URL of the image to show
* @param notifier a callback that is issued when the image is loaded and
* has faded in
*/
public void showImage(String url, DisplayListener notifier) {
sizeStep = -1;
imageNames[imageNames.length-1] = url;
exchangeImage(url, notifier);
}
/**
* Shows a picture for which representations with different image sizes
* exist. The representation, the size of which best fits the
* <code>FlipImagePanel</code> widget's size is picked. If the widget is
* resized so that another representation would be more appropriate,
* the image is automatically exchanged.
*
* @param urls the URL of different sized versions of the same image,
* each of them corresponding to one of the sizes
* @param sizes the sizes of the different versions of the images.
* @param notifier a callback that is issued when the image is loaded and
* has faded in
*/
public void showImage(String urls[], int[][] sizes,
DisplayListener notifier) {
assert sizes.length == urls.length;
imageNames = urls;
this.sizes = sizes;
sizeStep = pickSize(sizes);
exchangeImage(urls[sizeStep], notifier);
}
/**
* Returns the currently selected size step, if image URLs for several
* size steps have been specified by the last call of method
* <code>showImage()</code>. Returns -1 otherwise.
*
* This method has been made package private, because the information about
* the selected size step is needed for prefetching. See {@link Slideshow.show}.
*
* @return the currently selected size step or -1, if not several size steps
* have been specified.
*/
int getSizeStep() {
return sizeStep;
}
/**
* Exchanges a currently displayed image with the image that the given URL
* points to. If fading is enabled the old image will be faded out and
* the new image faded in simultaneously.
* @param url the URL of the image to show
* @param notifier a callback that is issued when the image is loaded and
* has faded in
*/
protected void exchangeImage(String url, DisplayListener notifier) {
cancelFading(false);
Image discard = passive;
passive = active;
active = new Image();
active.addStyleName("slide");
Fade.setOpacity(active, 0.0);
displayListener = notifier;
ImageLoadHandler loadHandler = new ImageLoadHandler(fading);
active.addLoadHandler(loadHandler);
active.addErrorHandler(stdImageErrorHandler);
if (discard != null) {
panel.remove(discard);
discard = null;
}
active.setUrl(url);
panel.add(active);
adjustSize(active);
if (fading < 0) startFading(loadHandler);
Compatibility.fireLoadOnIE(active, url, loadHandler);
}
@Override
protected void onLoad() {
for (AttachmentListener a: attachmentListeners)
a.onLoad(this);
}
@Override
protected void onUnload() {
cancelFading(false);
for (AttachmentListener a: attachmentListeners)
a.onUnload(this);
}
/**
* Exchanges the current image quickly without fading.
*
* @param url the URL of the new image to be displayed
*/
protected void quickExchangeImage(String url) {
int fading = getFading();
setFading(0);
exchangeImage(url, displayListener);
setFading(fading);
// cancelFading(false);
// Image discard = passive;
// passive = active;
// active = new Image();
// Fade.setOpacity(active, 0.0);
// active.addLoadListener(quickLoadListener);
// if (discard != null) panel.remove(discard);
// active.setUrl(url);
// panel.add(active);
// // Fade.setOpacity(passive, 1.0);
// Compatibility.fireLoadOnIE(active, url, quickLoadListener, null);
}
/**
* Adjusts the size of an image in the panel so that it fits the size of the panel.
* The aspect ration of the image is preserved.
* @param img the image the size of which is to be adjusted.
*/
private void adjustSize(Image img) {
int w,h, imgW,imgH;
if (img == null) return;
if (sizeStep >= 0) {
imgW = sizes[sizeStep][0];
imgH = sizes[sizeStep][1];
} else {
imgW = img.getWidth();
imgH = img.getHeight();
}
if (imgW == 0 || imgH == 0) return;
w = panelW;
h = imgH * panelW / imgW;
if (h > panelH) {
h = panelH;
w = imgW * panelH / imgH;
panel.setWidgetPosition(img, (panelW-w)/2, 0);
} else {
panel.setWidgetPosition(img, 0, (panelH-h)/2);
}
img.setPixelSize(w, h);
}
/**
* Calls the display listener. Makes sure that the display listener
* is never called twice.
*/
private void fireDisplay() {
if (displayListener != null) {
displayListener.onDisplay();
displayListener = null;
}
}
private void fireFade() {
if (displayListener != null) {
displayListener.onFade();
}
}
/**
* Cancels any ongoing image fading process and sets the opacity to the
* target value.
*
* @param complete if true, the opacity of the image will be set to its
* target value, otherwise it will be left where it is
* at the moment of canceling.
*/
private void cancelFading(boolean complete) {
if (fadeOut != null) {
fadeOut.setCompleteOnCancel(complete);
fadeOut.cancel();
fadeOut = null;
}
if (fadeIn != null) {
fadeIn.setCompleteOnCancel(complete);
fadeIn.cancel();
fadeIn = null;
}
}
/**
* Picks the most suitable of several steps of image sizes for the current
* size of the <code>FlipImagePanel</code>. The <code>sizes</code> must be
* a two dimensional array of integers containing one or more discrete steps
* of width,height values.
*
* @param sizes an array of discrete display size steps. Each step is
* described by two integer values resembling the width and
* height. The size steps must be ordered from smallest to
* largest.
* @return the size step which is most suitable
*/
private int pickSize(int[][] sizes) {
for (int i = 0; i < sizes.length - sizeBias; i++) {
if (sizes[i][0] >= panelW || sizes[i][1] >= panelH) {
return i;
}
}
return Math.max(0, sizes.length - sizeBias - 1);
// // alternative algorithm:
// int ret, diff = Integer.MAX_VALUE;
// for (int i = 0; i <= sizes.length - sizeBias; i++) {
// int dx = sizes[i][0] - panelW;
// int dy = sizes[i][0] - panelH;
// int cmp = dx*dx + dy*dy;
// if (cmp < diff) {
// ret = i;
// diff = cmp;
// }
// }
// return Math.max(0, ret);
}
/**
* Positions the notifier image on the panel according to the the anchor of
* the notifier (<code>notifierAnchor</code>).
*/
private void positionNotifier() {
if (notifier != null) {
int w = notifier.getWidth();
int h = notifier.getHeight();
int frameX = panelW / 10;
int x = panelW / 2;
int y = panelH / 2 - h / 2;
if (notifierAnchor == WEST) {
x = panelW - w - frameX;
} else if (notifierAnchor == EAST) {
x = frameX;
}
panel.setWidgetPosition(notifier, x, y);
}
}
/**
* Starts the fading out of the last ("passive") image and the fading in
* of the current ("active") image.
*/
private void startFading(ImageLoadHandler loadListener) {
cancelFading(true);
fadeIn = new NotifyingFade(active, loadListener);
if (passive != null) {
if (fading > 0) {
fadeOut = new Fade(passive, 1.0, 0.0, FADE_OUT_STEPS);
fadeIn.run(fading);
} else {
fadeOut = new ChainedFade(passive, fadeIn, FADE_OUT_STEPS);
}
fadeOut.run(Math.abs(fading));
} else {
fadeIn.run(Math.abs(fading));
}
fireFade();
}
}