Skip to content

Commit

Permalink
Add a lumens overlay and make lights environmental
Browse files Browse the repository at this point in the history
The new lumens overlay is a visualization of the light strength at each point of a map. The strength of the light is
determined by its lumens and - as always - darkness obscures light of equal or lesser lumens. The lumens overlay treats
lumens less than zero as darkness and renders dark areas as black. On the other end, lumens of 100 or greater are
treated as bright areas, and are rendered as a nearly transparent white. The final special case is lumens equal to zero,
which is a representation of daylight and is not included in the lumens overlay. Any other lumens values are drawn as
shades of grey, with low levels being nearly black, and with higher levels being closer to white.

The environmental lights are an improvement to the aesthetics of lights. Lights now blend with one another
"additively" (technically according to the Screen blend mode) so that they get brighter when they overlap. After
blending the lights, the results are blended with the map using a blend operation that brightens the map while
preserving details (similar to overlay blend mode and soft lighting). This make lights look more like they illuminate
the map rather than sit on top of it.

Both of these features can be toggled in the *View* menu depending on user preferences and game requirements. The lumens
overlay can also have its opacity configured in the Preferences dialog, and the older light and darkness opacities have
been removed. Lights no longer have an opacity congiguration since they are conceptually part of the map and not a layer
rendered on top.

In order to make the lumens overlay as useful as possible, the lumens values are now atached to individual light ranges
rather than to entire light sources. Existing campaigns will have their light sources' lumens values copied to each
range. The **Campaign Properties** dialog's _Light_ tab represents per-range lumens as by appending the lumens after the
optional range color. So these are now valid ranges:
```
r#ff0000+100
r#aa0000+50
r+100
```
These define, respectively, a saturated red range at 100 lumens, a desaturated red range at 50 lumens, and a colourless
range at 100 lumens.
  • Loading branch information
kwvanderlinde committed Feb 14, 2023
1 parent 71d9abf commit 5db464c
Show file tree
Hide file tree
Showing 18 changed files with 871 additions and 597 deletions.
38 changes: 38 additions & 0 deletions src/main/java/net/rptools/maptool/client/AppActions.java
Original file line number Diff line number Diff line change
Expand Up @@ -1578,6 +1578,44 @@ protected void executeAction() {
}
};

/** This is the menu option turns the lumens overlay on and off. */
public static final Action TOGGLE_LUMENS_OVERLAY =
new ZoneAdminClientAction() {
{
init("action.showLumensOverlay");
}

@Override
public boolean isSelected() {
return AppState.isShowLumensOverlay();
}

@Override
protected void executeAction() {
AppState.setShowLumensOverlay(!AppState.isShowLumensOverlay());
MapTool.getFrame().refresh();
}
};

/** This is the menu option turns the lumens overlay on and off. */
public static final Action TOGGLE_SHOW_LIGHTS =
new ZoneAdminClientAction() {
{
init("action.showLights");
}

@Override
public boolean isSelected() {
return AppState.isShowLights();
}

@Override
protected void executeAction() {
AppState.setShowLights(!AppState.isShowLights());
MapTool.getFrame().refresh();
}
};

/** Start entering text into the chat field */
public static final String CHAT_COMMAND_ID = "action.sendChat";

Expand Down
24 changes: 6 additions & 18 deletions src/main/java/net/rptools/maptool/client/AppPreferences.java
Original file line number Diff line number Diff line change
Expand Up @@ -84,11 +84,8 @@ public class AppPreferences {
private static final String KEY_AURA_OVERLAY_OPACITY = "auraOverlayOpacity";
private static final int DEFAULT_AURA_OVERLAY_OPACITY = 60;

private static final String KEY_LIGHT_OVERLAY_OPACITY = "lightOverlayOpacity";
private static final int DEFAULT_LIGHT_OVERLAY_OPACITY = 60;

private static final String KEY_DARKNESS_OVERLAY_OPACITY = "darknessOverlayOpacity";
private static final int DEFAULT_DARKNESS_OVERLAY_OPACITY = 200;
private static final String KEY_LUMENS_OVERLAY_OPACITY = "lumensOverlayOpacity";
private static final int DEFAULT_LUMENS_OVERLAY_OPACITY = 120;

private static final String KEY_FOG_OVERLAY_OPACITY = "fogOverlayOpacity";
private static final int DEFAULT_FOG_OVERLAY_OPACITY = 100;
Expand Down Expand Up @@ -341,21 +338,12 @@ public static int getAuraOverlayOpacity() {
return range0to255(value);
}

public static void setLightOverlayOpacity(int size) {
prefs.putInt(KEY_LIGHT_OVERLAY_OPACITY, range0to255(size));
}

public static int getLightOverlayOpacity() {
int value = prefs.getInt(KEY_LIGHT_OVERLAY_OPACITY, DEFAULT_LIGHT_OVERLAY_OPACITY);
return range0to255(value);
}

public static void setDarknessOverlayOpacity(int size) {
prefs.putInt(KEY_DARKNESS_OVERLAY_OPACITY, range0to255(size));
public static void setLumensOverlayOpacity(int size) {
prefs.putInt(KEY_LUMENS_OVERLAY_OPACITY, range0to255(size));
}

public static int getDarknessOverlayOpacity() {
int value = prefs.getInt(KEY_DARKNESS_OVERLAY_OPACITY, DEFAULT_DARKNESS_OVERLAY_OPACITY);
public static int getLumensOverlayOpacity() {
int value = prefs.getInt(KEY_LUMENS_OVERLAY_OPACITY, DEFAULT_LUMENS_OVERLAY_OPACITY);
return range0to255(value);
}

Expand Down
18 changes: 18 additions & 0 deletions src/main/java/net/rptools/maptool/client/AppState.java
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,8 @@ public class AppState {
private static boolean enforceNotification = false;
private static File campaignFile;
private static int gridSize = 1;
private static boolean showLumensOverlay = true;
private static boolean showLights = false;
private static boolean showAsPlayer = false;
private static boolean showLightSources = false;
private static boolean zoomLocked = false;
Expand Down Expand Up @@ -172,6 +174,22 @@ public static boolean getShowTextLabels() {
return showTextLabels;
}

public static boolean isShowLights() {
return showLights;
}

public static void setShowLights(boolean show) {
showLights = show;
}

public static boolean isShowLumensOverlay() {
return showLumensOverlay;
}

public static void setShowLumensOverlay(boolean show) {
showLumensOverlay = show;
}

public static boolean isShowAsPlayer() {
return showAsPlayer;
}
Expand Down
10 changes: 10 additions & 0 deletions src/main/java/net/rptools/maptool/client/ui/AppMenuBar.java
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,16 @@ protected JMenu createViewMenu() {
gridSizeMenu.add(gridSize5);
menu.add(gridSizeMenu);

menu.addSeparator();
JCheckBoxMenuItem toggleLumensOverlay =
new RPCheckBoxMenuItem(AppActions.TOGGLE_LUMENS_OVERLAY, menu);
toggleLumensOverlay.setSelected(AppState.isShowLumensOverlay());
menu.add(toggleLumensOverlay);
JCheckBoxMenuItem toggleShowLights =
new RPCheckBoxMenuItem(AppActions.TOGGLE_SHOW_LIGHTS, menu);
toggleShowLights.setSelected(AppState.isShowLights());
menu.add(toggleShowLights);

menu.addSeparator();
menu.add(new RPCheckBoxMenuItem(AppActions.TOGGLE_DRAW_MEASUREMENTS, menu));
menu.add(new RPCheckBoxMenuItem(AppActions.TOGGLE_DOUBLE_WIDE, menu));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;
import java.util.regex.Pattern;
import javax.swing.*;
import net.rptools.lib.FileUtil;
import net.rptools.maptool.client.AppConstants;
Expand Down Expand Up @@ -303,14 +304,14 @@ private String updateSightPanel(Map<String, SightType> sightTypeMap) {
Color color = (Color) light.getPaint().getPaint();
builder.append(toHex(color));
}

final var lumens = light.getLumens();
if (lumens >= 0) {
builder.append('+');
}
builder.append(Integer.toString(lumens, 10));
builder.append(' ');
}
}

if (source.getLumens() != 0) {
builder.append("lumens=").append(source.getLumens()).append(' ');
}
}
builder.append('\n');
}
Expand Down Expand Up @@ -381,9 +382,13 @@ private String updateLightPanel(Map<String, Map<GUID, LightSource>> lightSources
Color color = (Color) light.getPaint().getPaint();
builder.append(toHex(color));
}
}
if (lightSource.getLumens() != 0) {
builder.append(" lumens=").append(lightSource.getLumens());
if (lightSource.getType() == LightSource.Type.NORMAL) {
final var lumens = light.getLumens();
if (lumens >= 0) {
builder.append('+');
}
builder.append(Integer.toString(lumens, 10));
}
}
builder.append('\n');
}
Expand Down Expand Up @@ -501,33 +506,51 @@ private void commitSightMap(final String text) {
errmsg = "msg.error.mtprops.sight.multiplier"; // (ditto)
magnifier = StringUtil.parseDecimal(toBeParsed);
} else if (arg.startsWith("r")) { // XXX Why not "r=#" instead of "r#"??
Color personalLightColor = null;
toBeParsed = arg.substring(1);

split = toBeParsed.indexOf('#');
if (split > 0) {
String colorString = toBeParsed.substring(split); // Keep the '#'
toBeParsed = toBeParsed.substring(0, split);
personalLightColor = Color.decode(colorString);
}

errmsg = "msg.error.mtprops.sight.range";
pLightRange = StringUtil.parseDecimal(toBeParsed);

if (personalLight == null) {
personalLight = new LightSource();
}
DrawableColorPaint personalLightPaint =
personalLightColor != null ? new DrawableColorPaint(personalLightColor) : null;
personalLight.add(new Light(shape, 0, pLightRange, arc, personalLightPaint));
personalLight.setScaleWithToken(scaleWithToken);
} else if (arg.toUpperCase().startsWith("LUMENS=")) {
if (personalLight != null) {
personalLight.setLumens(Integer.parseInt(arg.substring(7)));
final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?");
final var matcher = rangeRegex.matcher(toBeParsed);
if (matcher.find()) {
pLightRange = StringUtil.parseDecimal(matcher.group(1));
final var colorString = matcher.group(2);
final var lumensString = matcher.group(3);
// Note that Color.decode() _wants_ the leading "#", otherwise it might not treat
// the value as a hex code.
Color personalLightColor = null;
if (colorString != null) {
personalLightColor = Color.decode(colorString);
}
int perRangeLumens = 100;
if (lumensString != null) {
perRangeLumens = Integer.parseInt(lumensString, 10);
if (perRangeLumens == 0) {
errlog.add(
I18N.getText("msg.error.mtprops.sight.zerolumens", reader.getLineNumber()));
perRangeLumens = 100;
}
}

if (personalLight == null) {
personalLight = new LightSource();
personalLight.setType(LightSource.Type.NORMAL);
}
personalLight.add(
new Light(
shape,
0,
pLightRange,
arc,
personalLightColor == null
? null
: new DrawableColorPaint(personalLightColor),
perRangeLumens,
false,
false));
personalLight.setScaleWithToken(scaleWithToken);
} else {
errlog.add(
I18N.getText(
"msg.error.mtprops.sight.lumensWithoutLight", reader.getLineNumber()));
throw new ParseException(
String.format("Unrecognized personal light syntax: %s", arg), 0);
}
} else if (arg.startsWith("arc=") && arg.length() > 4) {
toBeParsed = arg.substring(4);
Expand Down Expand Up @@ -663,16 +686,6 @@ private Map<String, Map<GUID, LightSource>> commitLightMap(
lightSource.setScaleWithToken(true);
continue;
}
// Lumens designation
if (arg.toUpperCase().startsWith("LUMENS=")) {
try {
lightSource.setLumens(Integer.parseInt(arg.substring(7)));
continue;
} catch (NullPointerException noe) {
errlog.add(
I18N.getText("msg.error.mtprops.light.lumens", reader.getLineNumber(), arg));
}
}
// Shape designation ?
try {
shape = ShapeType.valueOf(arg.toUpperCase());
Expand Down Expand Up @@ -719,14 +732,32 @@ private Map<String, Map<GUID, LightSource>> commitLightMap(
}
continue;
}

Color color = null;
int perRangeLumens = 100;
distance = arg;
split = arg.indexOf('#');
if (split > 0) {
String colorString = arg.substring(split); // Keep the '#'
distance = arg.substring(0, split);
color = Color.decode(colorString);

final var rangeRegex = Pattern.compile("([^#+-]*)(#[0-9a-fA-F]+)?([+-]\\d*)?");
final var matcher = rangeRegex.matcher(arg);
if (matcher.find()) {
distance = matcher.group(1);
final var colorString = matcher.group(2);
final var lumensString = matcher.group(3);
// Note that Color.decode() _wants_ the leading "#", otherwise it might not treat the
// value as a hex code.
if (colorString != null) {
color = Color.decode(colorString);
}
if (lumensString != null) {
perRangeLumens = Integer.parseInt(lumensString, 10);
if (perRangeLumens == 0) {
errlog.add(
I18N.getText("msg.error.mtprops.light.zerolumens", reader.getLineNumber()));
perRangeLumens = 100;
}
}
}

boolean isAura = lightSource.getType() == LightSource.Type.AURA;
if (!isAura && (gmOnly || owner)) {
errlog.add(I18N.getText("msg.error.mtprops.light.gmOrOwner", reader.getLineNumber()));
Expand All @@ -741,7 +772,8 @@ private Map<String, Map<GUID, LightSource>> commitLightMap(
offset,
StringUtil.parseDecimal(distance),
arc,
color != null ? new DrawableColorPaint(color) : null,
color == null ? null : new DrawableColorPaint(color),
perRangeLumens,
gmOnly,
owner);
lightSource.add(t);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,8 +97,7 @@ public class PreferencesDialog extends JDialog {
private final JSpinner haloLineWidthSpinner;
private final JSpinner haloOverlayOpacitySpinner;
private final JSpinner auraOverlayOpacitySpinner;
private final JSpinner lightOverlayOpacitySpinner;
private final JSpinner darknessOverlayOpacitySpinner;
private final JSpinner lumensOverlayOpacitySpinner;
private final JSpinner fogOverlayOpacitySpinner;
private final JCheckBox useHaloColorAsVisionOverlayCheckBox;
private final JCheckBox autoRevealVisionOnGMMoveCheckBox;
Expand Down Expand Up @@ -334,8 +333,7 @@ public PreferencesDialog() {
haloLineWidthSpinner = panel.getSpinner("haloLineWidthSpinner");
haloOverlayOpacitySpinner = panel.getSpinner("haloOverlayOpacitySpinner");
auraOverlayOpacitySpinner = panel.getSpinner("auraOverlayOpacitySpinner");
lightOverlayOpacitySpinner = panel.getSpinner("lightOverlayOpacitySpinner");
darknessOverlayOpacitySpinner = panel.getSpinner("darknessOverlayOpacitySpinner");
lumensOverlayOpacitySpinner = panel.getSpinner("lumensOverlayOpacitySpinner");
fogOverlayOpacitySpinner = panel.getSpinner("fogOverlayOpacitySpinner");
mapVisibilityWarning = panel.getCheckBox("mapVisibilityWarning");

Expand Down Expand Up @@ -709,19 +707,11 @@ protected void storeSpinnerValue(int value) {
MapTool.getFrame().refresh();
}
});
lightOverlayOpacitySpinner.addChangeListener(
lumensOverlayOpacitySpinner.addChangeListener(
new ChangeListenerProxy() {
@Override
protected void storeSpinnerValue(int value) {
AppPreferences.setLightOverlayOpacity(value);
MapTool.getFrame().refresh();
}
});
darknessOverlayOpacitySpinner.addChangeListener(
new ChangeListenerProxy() {
@Override
protected void storeSpinnerValue(int value) {
AppPreferences.setDarknessOverlayOpacity(value);
AppPreferences.setLumensOverlayOpacity(value);
MapTool.getFrame().refresh();
}
});
Expand Down Expand Up @@ -1093,10 +1083,8 @@ private void setInitialState() {
new SpinnerNumberModel(AppPreferences.getHaloOverlayOpacity(), 0, 255, 1));
auraOverlayOpacitySpinner.setModel(
new SpinnerNumberModel(AppPreferences.getAuraOverlayOpacity(), 0, 255, 1));
lightOverlayOpacitySpinner.setModel(
new SpinnerNumberModel(AppPreferences.getLightOverlayOpacity(), 0, 255, 1));
darknessOverlayOpacitySpinner.setModel(
new SpinnerNumberModel(AppPreferences.getDarknessOverlayOpacity(), 0, 255, 1));
lumensOverlayOpacitySpinner.setModel(
new SpinnerNumberModel(AppPreferences.getLumensOverlayOpacity(), 0, 255, 1));
fogOverlayOpacitySpinner.setModel(
new SpinnerNumberModel(AppPreferences.getFogOverlayOpacity(), 0, 255, 1));

Expand Down

0 comments on commit 5db464c

Please sign in to comment.