window fade in/out effect

This commit is contained in:
Autumn Lamonte 2022-02-05 12:01:06 -06:00
parent edc1303af1
commit a229c8cbd0
10 changed files with 519 additions and 21 deletions

View file

@ -13,8 +13,8 @@ like this:
![Terminal, Image, Table](/screenshots/new_demo1.png?raw=true "Terminal, Image, Table")
...or anything in between. Translucent windows -- including layered
images -- are supported and generally look like as one would expect in
a modern graphical environment...but it's mostly text. Translucent
images -- are supported and generally look as one would expect in a
modern graphical environment...but it's mostly text. Translucent
windows were inspired in part by
[notcurses](https://github.com/dankamongmen/notcurses).

View file

@ -51,6 +51,9 @@ import jexer.bits.CellAttributes;
import jexer.bits.Clipboard;
import jexer.bits.ColorTheme;
import jexer.bits.StringUtils;
import jexer.effect.Effect;
import jexer.effect.WindowFadeInEffect;
import jexer.effect.WindowFadeOutEffect;
import jexer.event.TCommandEvent;
import jexer.event.TInputEvent;
import jexer.event.TKeypressEvent;
@ -407,6 +410,11 @@ public class TApplication implements Runnable {
*/
protected boolean translucence = true;
/**
* The list of desktop/window effects to run.
*/
private List<Effect> effects = new LinkedList<Effect>();
/**
* WidgetEventHandler is the main event consumer loop. There are at most
* two such threads in existence: the primary for normal case and a
@ -905,7 +913,7 @@ public class TApplication implements Runnable {
addTimer(millis, true,
new TAction() {
public void DO() {
TApplication.this.doRepaint();
doRepaint();
}
}
);
@ -919,9 +927,9 @@ public class TApplication implements Runnable {
addTimer(millis, true,
new TAction() {
public void DO() {
TApplication.this.doRepaint();
doRepaint();
// Update idle checks.
TApplication.this.getBackend().hasEvents();
getBackend().hasEvents();
}
}
);
@ -932,6 +940,7 @@ public class TApplication implements Runnable {
TTimer animationTimer = addTimer(1000 / ANIMATION_FPS, true,
new TAction() {
public void DO() {
runEffects();
doRepaint();
}
}
@ -1824,6 +1833,38 @@ public class TApplication implements Runnable {
}
}
/**
* Run the desktop and window effects.
*/
private void runEffects() {
// System.err.println("runEffects() enter");
synchronized (effects) {
if (effects.size() == 0) {
// System.err.println("runEffects() NOP");
return;
}
}
List<Effect> effectsToRun = new ArrayList<Effect>();
List<Effect> effectsToRemove = new ArrayList<Effect>();
synchronized (effects) {
effectsToRun.addAll(effects);
}
while (effectsToRun.size() > 0) {
Effect effect = effectsToRun.remove(0);
if (effect.isCompleted()) {
effectsToRemove.add(effect);
}
effect.update();
}
if (effectsToRemove.size() > 0) {
synchronized (effects) {
effects.removeAll(effectsToRemove);
}
}
// System.err.println("runEffects() exit");
}
/**
* Do stuff when there is no user input.
*/
@ -1859,6 +1900,11 @@ public class TApplication implements Runnable {
timers.addAll(keepTimers);
}
if (debugThreads) {
System.err.printf(System.currentTimeMillis() + " " +
Thread.currentThread() + " doIdle() 3\n");
}
// Call onIdle's
for (TWindow window: windows) {
window.onIdle();
@ -1879,6 +1925,11 @@ public class TApplication implements Runnable {
}
doRepaint();
if (debugThreads) {
System.err.printf(System.currentTimeMillis() + " " +
Thread.currentThread() + " doIdle() - exit\n");
}
}
/**
@ -2597,7 +2648,7 @@ public class TApplication implements Runnable {
// Recreate the shadow effect by blending a black rectangle over just
// the shadow region.
final int shadowOpacity = 30;
final int shadowAlpha = shadowOpacity * 255 / 100;
final int shadowAlpha = shadowOpacity * window.getAlpha() / 100;
screen.blendRectangle(windowX + windowWidth, windowY + 1,
2, windowHeight - 1, 0x000000, shadowAlpha);
screen.blendRectangle(windowX + 2, windowY + windowHeight,
@ -3190,6 +3241,18 @@ public class TApplication implements Runnable {
// visible on screen.
window.onPreClose();
// If the window has a close effect, kick that off.
if (!window.disableCloseEffect()) {
String windowCloseEffect = System.getProperty("jexer.effect.windowClose",
"none").toLowerCase();
if (windowCloseEffect.equals("fade")) {
synchronized (effects) {
effects.add(new WindowFadeOutEffect(window));
}
}
}
synchronized (windows) {
window.stopMovements();
@ -3380,6 +3443,15 @@ public class TApplication implements Runnable {
desktop.setActive(false);
}
// If the window has an open effect, kick that off.
String windowOpenEffect = System.getProperty("jexer.effect.windowOpen",
"none").toLowerCase();
if (windowOpenEffect.equals("fade")) {
synchronized (effects) {
effects.add(new WindowFadeInEffect(window));
}
}
}
/**

View file

@ -252,6 +252,11 @@ public class TWindow extends TWidget {
*/
private int alpha = 255;
/**
* The window open effect timer.
*/
private TTimer openEffectTimer = null;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
@ -339,9 +344,6 @@ public class TWindow extends TWidget {
// Center window if specified
center();
// Add me to the application
application.addWindowToApplication(this);
// Set default borders
setBorderStyleForeground(null);
setBorderStyleInactive(null);
@ -358,6 +360,9 @@ public class TWindow extends TWidget {
// SQUASH
}
setAlpha(opacity * 255 / 100);
// Add me to the application
application.addWindowToApplication(this);
}
// ------------------------------------------------------------------------
@ -1860,4 +1865,15 @@ public class TWindow extends TWidget {
return alpha;
}
/**
* If true, disable any window closing effect. This is used by the
* window closing effects themselves so that they can be closed when
* finished.
*
* @return true if the window close effect should be disabled
*/
public boolean disableCloseEffect() {
return false;
}
}

View file

@ -1931,10 +1931,9 @@ public class ECMA48Terminal extends LogicalScreen
if (imageThreadCount > 1) {
// Collect all the encoded images.
while (imageResults.size() > 0) {
try {
Future<String> image = imageResults.get(0);
try {
sb.append(image.get());
imageResults.remove(0);
} catch (InterruptedException e) {
// SQUASH
// e.printStackTrace();
@ -1942,6 +1941,7 @@ public class ECMA48Terminal extends LogicalScreen
// SQUASH
// e.printStackTrace();
}
imageResults.remove(0);
}
imageExecutor.shutdown();
}

View file

@ -1354,6 +1354,38 @@ public class LogicalScreen implements Screen {
return other;
}
/**
* Obtain a snapshot copy of a rectangular portion of the screen of the
* PHYSICAL screen - what was LAST emitted.
*
* @param x left column of rectangle. 0 is the left-most column.
* @param y top row of the rectangle. 0 is the top-most row.
* @param width number of columns to copy
* @param height number of rows to copy
* @return a copy of the screen's data from this rectangle. Any cells
* outside the actual screen dimensions will be blank.
*/
public Screen snapshotPhysical(final int x, final int y, final int width,
final int height) {
LogicalScreen other = null;
synchronized (this) {
other = new LogicalScreen(width, height);
for (int row = y; (row < y + height) && (row < this.height); row++) {
if (row < 0) {
continue;
}
for (int col = x; (col < x + width) && (col < this.width); col++) {
if (col < 0) {
continue;
}
other.logical[col - x][row - y] = new Cell(physical[col][row]);
}
}
}
return other;
}
/**
* Copy all of screen's data to this screen.
*
@ -1468,6 +1500,8 @@ public class LogicalScreen implements Screen {
return;
}
long now = System.currentTimeMillis();
/*
* We need to blend the background colors of other's cells over the
* cells of this screen (foreground and background), honoring our
@ -1480,6 +1514,8 @@ public class LogicalScreen implements Screen {
BufferedImage.TYPE_INT_ARGB);
BufferedImage thisBackground = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
BufferedImage overForeground = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
BufferedImage overBackground = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
BufferedImage thisOldBackground = new BufferedImage(width, height,
@ -1515,6 +1551,16 @@ public class LogicalScreen implements Screen {
}
Cell over = otherScreen.getCharXY(col - x, row - y);
int overFg = over.getForeColorRGB();
if (over.isPulse()) {
overFg = over.getForeColorPulseRGB(backend, now);
} else if (overFg < 0) {
if (backend != null) {
overFg = backend.attrToForegroundColor(over).getRGB();
} else {
overFg = SwingTerminal.attrToForegroundColor(over).getRGB();
}
}
int overBg = over.getBackColorRGB();
if (overBg < 0) {
if (backend != null) {
@ -1526,18 +1572,23 @@ public class LogicalScreen implements Screen {
thisFg |= OPAQUE;
thisBg |= OPAQUE;
overBg |= OPAQUE;
overFg |= OPAQUE;
thisForeground.setRGB(col - x, row - y, thisFg);
thisBackground.setRGB(col - x, row - y, thisBg);
thisOldBackground.setRGB(col - x, row - y, thisBg);
overForeground.setRGB(col - x, row - y, overFg);
overBackground.setRGB(col - x, row - y, overBg);
}
}
// The three bitmaps are ready. We have skipped over
// cells/pixels that cannot overlap. Now blit overBackground
// over both thisForeground and thisBackground, and then assign
// cell colors and cell chars/images.
// The four bitmaps are ready. We have skipped over cells/pixels
// that cannot overlap. Now blit overBackground over both
// thisForeground and thisBackground, and then assign cell colors
// and cell chars/images.
//
// Also blit overForeground over thisBackground to handle the new
// layer's glyph opacity.
float fAlpha = (float) (alpha / 255.0);
Graphics2D g2d = thisForeground.createGraphics();
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
@ -1551,6 +1602,15 @@ public class LogicalScreen implements Screen {
g2d.drawImage(overBackground, 0, 0, null);
g2d.dispose();
BufferedImage glyphForeground = new BufferedImage(width, height,
BufferedImage.TYPE_INT_ARGB);
g2d = glyphForeground.createGraphics();
g2d.drawImage(thisBackground, 0, 0, null);
g2d.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER,
fAlpha));
g2d.drawImage(overForeground, 0, 0, null);
g2d.dispose();
for (int row = y; (row < y + height) && (row < this.height); row++) {
if (row < 0) {
continue;
@ -1565,6 +1625,7 @@ public class LogicalScreen implements Screen {
int thisBg = thisBackground.getRGB(col - x, row - y);
int thisOldBg = thisOldBackground.getRGB(col - x, row - y);
int overBg = overBackground.getRGB(col - x, row - y);
int overFg = glyphForeground.getRGB(col - x, row - y);
thisCell.setBackColorRGB(thisBg | OPAQUE);
thisCell.setForeColorRGB(thisFg | OPAQUE);
@ -1595,6 +1656,7 @@ public class LogicalScreen implements Screen {
if (imageId > 0) {
thisCell.setImage(newImage, imageId);
thisCell.mixImageId(overBg);
thisCell.mixImageId(alpha);
} else {
thisCell.setImage(newImage);
}
@ -1632,17 +1694,13 @@ public class LogicalScreen implements Screen {
// The overlaying cell has a character, use it.
thisCell.setChar(overCell.getChar());
int fg = overCell.getForeColorRGB();
if (fg < 0) {
thisCell.setForeColor(overCell.getForeColor());
} else {
thisCell.setForeColorRGB(fg);
}
thisCell.setForeColorRGB(overFg);
thisCell.setBold(overCell.isBold());
thisCell.setBlink(overCell.isBlink());
thisCell.setUnderline(overCell.isUnderline());
thisCell.setProtect(overCell.isProtect());
thisCell.setAnimations(overCell.getAnimations());
thisCell.setPulse(false, false, 0);
if (!overCell.isImage()) {
// If we had an image, destroy it. Text ALWAYS
@ -1678,6 +1736,7 @@ public class LogicalScreen implements Screen {
if (imageId > 0) {
thisCell.setImage(newImage, imageId);
thisCell.mixImageId(thisOldBg);
thisCell.mixImageId(alpha);
} else {
thisCell.setImage(newImage);
}
@ -1713,6 +1772,7 @@ public class LogicalScreen implements Screen {
if (imageId > 0) {
thisCell.setImage(newImage, imageId);
thisCell.mixImageId(overCell);
thisCell.mixImageId(alpha);
} else {
thisCell.setImage(newImage);
}
@ -1747,6 +1807,7 @@ public class LogicalScreen implements Screen {
thisCell.setImage(newImage, imageId);
thisCell.mixImageId(overCell);
thisCell.mixImageId(overBg);
thisCell.mixImageId(alpha);
} else {
thisCell.setImage(newImage);
}
@ -1782,6 +1843,7 @@ public class LogicalScreen implements Screen {
thisCell.setImage(newImage, imageId);
thisCell.mixImageId(overBg);
thisCell.mixImageId(thisOldBg);
thisCell.mixImageId(alpha);
} else {
thisCell.setImage(newImage);
}

View file

@ -511,6 +511,20 @@ public interface Screen {
*/
public Screen snapshot();
/**
* Obtain a snapshot copy of a rectangular portion of the screen of the
* PHYSICAL screen - what was LAST emitted.
*
* @param x left column of rectangle. 0 is the left-most column.
* @param y top row of the rectangle. 0 is the top-most row.
* @param width number of columns to copy
* @param height number of rows to copy
* @return a copy of the screen's data from this rectangle. Any cells
* outside the actual screen dimensions will be blank.
*/
public Screen snapshotPhysical(final int x, final int y, final int width,
final int height);
/**
* Obtain a snapshot copy of a rectangular portion of the screen.
*

View file

@ -0,0 +1,49 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2022 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte Trans Liberation Now
* @version 1
*/
package jexer.effect;
/**
* A desktop or window effect does a blingy transformation before the screen
* is sent to the device.
*/
public interface Effect {
/**
* Update the effect.
*/
public void update();
/**
* If true, the effect is completed and can be removed.
*
* @return true if this effect is finished
*/
public boolean isCompleted();
}

View file

@ -0,0 +1,100 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2022 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte Trans Liberation Now
* @version 1
*/
package jexer.effect;
import jexer.TWindow;
/**
* A desktop or window effect does a blingy transformation before the screen
* is sent to the device.
*/
public class WindowFadeInEffect implements Effect {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* The window to fade in.
*/
private TWindow window;
/**
* The window's original alpha value we are ramping up to.
*/
private int targetAlpha = 0;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Public contructor.
*
* @param window the window to fade in
*/
public WindowFadeInEffect(final TWindow window) {
this.window = window;
targetAlpha = window.getAlpha();
window.setAlpha(64);
}
// ------------------------------------------------------------------------
// Effect -----------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Update the effect.
*/
public void update() {
if (!window.isShown()) {
return;
}
int alpha = window.getAlpha();
if (alpha < targetAlpha) {
// Aiming for 1/8 second, at 32 FPS = 4 frames. 256 / 4 = 64.
alpha = Math.min(alpha + 64, targetAlpha);
window.setAlpha(alpha);
}
}
/**
* If true, the effect is completed and can be removed.
*
* @return true if this effect is finished
*/
public boolean isCompleted() {
return (window.getAlpha() >= targetAlpha);
}
}

View file

@ -0,0 +1,152 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2022 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte Trans Liberation Now
* @version 1
*/
package jexer.effect;
import jexer.TWindow;
import jexer.backend.Screen;
import jexer.event.TInputEvent;
/**
* A desktop or window effect does a blingy transformation before the screen
* is sent to the device.
*/
public class WindowFadeOutEffect implements Effect {
// ------------------------------------------------------------------------
// Constants --------------------------------------------------------------
// ------------------------------------------------------------------------
// ------------------------------------------------------------------------
// Variables --------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* The fake window to fade out.
*/
private TWindow fakeWindow;
/**
* The region of the screen the window last rendered to.
*/
private Screen oldScreen;
/**
* The alpha value to set fakeWindow to.
*/
private int alpha;
// ------------------------------------------------------------------------
// Constructors -----------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Public contructor.
*
* @param window the window to fade in
*/
public WindowFadeOutEffect(final TWindow window) {
final Screen oldScreen = window.getScreen().snapshotPhysical(
window.getX(), window.getY(),
window.getWidth(), window.getHeight());
alpha = window.getAlpha();
final int x = window.getX();
final int y = window.getY();
window.getApplication().invokeLater(new Runnable() {
public void run() {
fakeWindow = new TWindow(window.getApplication(), "",
window.getX(), window.getY(),
window.getWidth(), window.getHeight(),
TWindow.MODAL) {
// Disable all inputs.
@Override
public void handleEvent(final TInputEvent event) {
// NOP
}
// Draw the old screen.
@Override
public void draw() {
for (int y = 0; y < getHeight(); y++) {
for (int x = 0; x < getWidth(); x++) {
putCharXY(x, y, oldScreen.getCharXY(x, y));
}
}
}
@Override
public boolean disableCloseEffect() {
return true;
}
};
fakeWindow.setX(x);
fakeWindow.setY(y);
fakeWindow.setAlpha(alpha);
}
});
}
// ------------------------------------------------------------------------
// Effect -----------------------------------------------------------------
// ------------------------------------------------------------------------
/**
* Update the effect.
*/
public void update() {
if (fakeWindow == null) {
return;
}
if (alpha > 0) {
// Aiming for 1/8 second, at 32 FPS = 4 frames. 256 / 4 = 64.
alpha = Math.max(alpha - 96, 0);
fakeWindow.setAlpha(alpha);
}
}
/**
* If true, the effect is completed and can be removed.
*
* @return true if this effect is finished
*/
public boolean isCompleted() {
if (fakeWindow != null) {
if (alpha == 0) {
fakeWindow.close();
return true;
}
}
return false;
}
}

View file

@ -0,0 +1,33 @@
/*
* Jexer - Java Text User Interface
*
* The MIT License (MIT)
*
* Copyright (C) 2022 Autumn Lamonte
*
* Permission is hereby granted, free of charge, to any person obtaining a
* copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
* FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
* DEALINGS IN THE SOFTWARE.
*
* @author Autumn Lamonte Trans Liberation Now
* @version 1
*/
/**
* Desktop and window effects.
*/
package jexer.effect;