package com.yoursway.swt.animations.flip;
import static com.yoursway.utils.Listeners.newListenersByIdentity;
import static java.lang.Math.PI;
import static java.lang.Math.cos;
import static java.lang.Math.sin;
import static java.lang.System.currentTimeMillis;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import com.yoursway.swt.overlay.Overlay;
import com.yoursway.swt.overlay.OverlayFactory;
import com.yoursway.swt.overlay.TemporaryCanvasOverlayFactory;
import com.yoursway.utils.Listeners;
/**
* Given two controls with equal positions and sizes, executes a “flip”
* animation that hides the first control and shows the second one.
*/
public class Flipper {
private static final int FPS = 30;
private static final double Z_EYE = 1;
private static final double Z_OBJECT = Z_EYE + 5;
private Control source;
private Control destination;
private final Display display;
private final OverlayFactory overlayFactory;
private final int iterations;
private final double ymax;
private transient Listeners<FlipperListener> listeners = newListenersByIdentity();
private Overlay overlay;
private Image sourceScreenshot;
private Image destinationScreenshot;
private Image offscreenImage;
private GC offscreenGC;
private double xmax;
public synchronized void addListener(FlipperListener listener) {
listeners.add(listener);
}
public synchronized void removeListener(FlipperListener listener) {
listeners.remove(listener);
}
public Flipper(Control source, Control destination) {
this(source, destination, 370);
}
public Flipper(Control source, Control destination, long duration) {
this(source, destination, new TemporaryCanvasOverlayFactory(), duration);
}
public Flipper(Control source, Control destination, OverlayFactory overlayFactory, long duration) {
if (source == null)
throw new NullPointerException("source is null");
if (destination == null)
throw new NullPointerException("destination is null");
if (overlayFactory == null)
throw new NullPointerException("overlayFactory is null");
if (duration < 0)
throw new IllegalArgumentException("duration < 0");
if (duration > 5000)
throw new IllegalArgumentException("duration > 5000 (artificial limit to protect user)");
this.source = source;
this.destination = destination;
this.overlayFactory = overlayFactory;
this.iterations = (int) (duration * FPS / 1000);
this.display = source.getDisplay();
this.ymax = calculateMaximumRawValueOfY();
double zmin = Z_OBJECT - 1.0 /* max of Math.sin on 0 .. Pi/2 */;
this.xmax = 1 * Z_EYE / zmin;
}
private double calculateMaximumRawValueOfY() {
double ymax = 0;
for (int i = 0; i <= iterations; i++) {
double angle = PI * i / iterations;
double cosAngle = cos(angle);
double sinAngle = sin(angle);
double zFar = Z_OBJECT + sinAngle;
double zNear = Z_OBJECT - sinAngle;
double y = cosAngle;
double y1pr = y * Z_EYE / zFar;
double y2pr = y * Z_EYE / zNear;
ymax = Math.max(ymax, Math.max(y1pr, y2pr));
}
return ymax;
}
public Control getTopControl() {
return source;
}
public void flip() {
sourceScreenshot = capture(source);
destinationScreenshot = capture(destination);
Rectangle screenshotBounds = sourceScreenshot.getBounds();
int canvasOverhangWidth = (int) (screenshotBounds.width * xmax / (1.0 * Z_EYE / Z_OBJECT))
- screenshotBounds.width;
overlay = overlayFactory.createOverlay(source, new Rectangle(-canvasOverhangWidth / 2, 0,
screenshotBounds.width + canvasOverhangWidth, screenshotBounds.height));
overlay.enableBackgroundErasing(source.getDisplay().getSystemColor(SWT.COLOR_BLACK));
Rectangle bounds = overlay.getBounds();
offscreenImage = new Image(display, bounds.width, bounds.height);
offscreenGC = new GC(offscreenImage);
Control temp = source;
source = destination;
destination = temp;
Thread thread = new FlipExecuter(offscreenImage, offscreenGC, bounds);
thread.setName("Flip animation");
thread.setDaemon(true);
thread.start();
}
private Image capture(Control control) {
Point screenshotSize = control.getSize();
Image image = new Image(display, screenshotSize.x, screenshotSize.y);
GC screenshotGc = new GC(image);
control.print(screenshotGc);
screenshotGc.dispose();
return image;
}
private final class FlipExecuter extends Thread {
private final Image offscreenImage;
private final GC offscreenGC;
private final Rectangle canvasBounds;
private FlipExecuter(Image offscreenImage, GC offscreenGC, Rectangle canvasBounds) {
this.offscreenImage = offscreenImage;
this.offscreenGC = offscreenGC;
this.canvasBounds = canvasBounds;
}
@Override
public void run() {
try {
animate();
} catch (Throwable e) {
e.printStackTrace(System.err);
System.err.println("Flip animation failed.");
}
display.asyncExec(new Runnable() {
public void run() {
try {
for (FlipperListener listener : listeners)
listener.flipped();
} finally {
sourceScreenshot.dispose();
destinationScreenshot.dispose();
overlay.dispose();
}
}
});
}
private void animate() throws InterruptedException {
double canvasHalfWidth = canvasBounds.width / 2.0;
double canvasHalfHeight = canvasBounds.height / 2.0;
Rectangle sourceScreenshotBounds = sourceScreenshot.getBounds();
int screenshotWidth = sourceScreenshotBounds.width;
int screenshotHeight = sourceScreenshotBounds.height;
Rectangle offscreenImageBounds = offscreenImage.getBounds();
boolean firstHalf = true;
for (int i = 0; i <= iterations; i++) {
long start = currentTimeMillis();
double angle = Math.PI * i / iterations;
double cosAngle = cos(angle);
double sinAngle = sin(angle);
double zFar = Z_OBJECT + sinAngle;
double zNear = Z_OBJECT - sinAngle;
double y = cosAngle;
double x = 1;
firstHalf = (angle < Math.PI / 2);
double x1pr = (x * Z_EYE / zFar) / xmax * canvasHalfWidth;
double y1pr = (y * Z_EYE / zFar) / ymax * canvasHalfHeight;
double x2pr = (x * Z_EYE / zNear) / xmax * canvasHalfWidth;
double y2pr = (y * Z_EYE / zNear) / ymax * canvasHalfHeight;
int xTopLeft = (int) (canvasHalfWidth - x1pr + 0.5);
int xTopRight = (int) (canvasHalfWidth + x1pr + 0.5);
int xBottomLeft = (int) (canvasHalfWidth - x2pr + 0.5);
int xBottomRight = (int) (canvasHalfWidth + x2pr + 0.5);
int yTop = (int) (canvasHalfHeight - y1pr + 0.5);
int yBottom = (int) (canvasHalfHeight + y2pr + 0.5);
overlay.drawOverlayBackgroundOnto(offscreenGC, offscreenImageBounds);
int minY = Math.min(yBottom, yTop);
int maxY = Math.max(yBottom, yTop);
for (int projY = minY; projY < maxY; projY++) {
double factor = ((double) projY - yBottom) / (yTop - yBottom);
double yt1 = projY;
double yt2 = projY + 1;
double factor1 = (yt1 - yBottom) / (yTop - yBottom);
double factor2 = (yt2 - yBottom) / (yTop - yBottom);
Image currentScreenshot = (firstHalf ? sourceScreenshot : destinationScreenshot);
if (firstHalf) {
factor1 = 1 - factor1;
factor2 = 1 - factor2;
}
double ysrc1c = screenshotHeight * factor1;
double ysrc2c = screenshotHeight * factor2;
int projXLeft = (int) ((xTopLeft - xBottomLeft) * factor + xBottomLeft + 0.5);
int projXRight = (int) ((xTopRight - xBottomRight) * factor + xBottomRight + 0.5);
double ysrc1 = Math.min(ysrc1c, ysrc2c);
double ysrc2 = Math.max(ysrc1c, ysrc2c);
int yy1 = (int) Math.ceil(ysrc1);
int yy2 = (int) (ysrc2 - 0.00001);
if (yy2 < yy1)
continue;
offscreenGC.drawImage(currentScreenshot, 0, yy1, screenshotWidth, yy2 - yy1 + 1,
projXLeft, projY, projXRight - projXLeft + 1, 1);
}
overlay.renderOffscreenImage(offscreenImage);
long span = (int) (System.currentTimeMillis() - start);
long delay = 1000 / FPS - span;
Thread.sleep(delay > 0 ? delay : 0);
}
}
}
}