/*
* Copyright 2017 Laszlo Balazs-Csiki
*
* This file is part of Pixelitor. Pixelitor is free software: you
* can redistribute it and/or modify it under the terms of the GNU
* General Public License, version 3 as published by the Free
* Software Foundation.
*
* Pixelitor is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with Pixelitor. If not, see <http://www.gnu.org/licenses/>.
*/
package pixelitor.layers;
import org.jdesktop.swingx.painter.AbstractLayoutPainter;
import org.junit.Before;
import org.junit.Test;
import pixelitor.Composition;
import pixelitor.TestHelper;
import pixelitor.filters.Invert;
import pixelitor.filters.NoOpFilter;
import pixelitor.filters.OneColorFilter;
import pixelitor.filters.painters.AreaEffects;
import pixelitor.filters.painters.TextSettings;
import java.awt.Color;
import java.awt.Font;
import static org.assertj.core.api.Assertions.assertThat;
import static pixelitor.Composition.fromImage;
import static pixelitor.utils.ImageUtils.create1x1Image;
public class LayerBlendingModesTest {
private Composition comp;
private ImageLayer lowerLayer;
private Layer upperLayer;
private final Color lowerColor = new Color(211, 141, 86);
private final Color upperColor = new Color(119, 86, 132);
private AdjustmentLayer invertAdjustment;
private AdjustmentLayer alwaysUpperColorAdjustment;
private TextLayer upperColorTextLayer;
@Before
public void setUp() {
comp = fromImage(create1x1Image(lowerColor), null, "test");
TestHelper.setupAnICFor(comp);
this.upperLayer = new ImageLayer(comp, create1x1Image(upperColor), "Layer 2", null);
comp.addLayerNoGUI(upperLayer);
lowerLayer = (ImageLayer) comp.getLayer(0);
assert lowerLayer.getComp().checkInvariant();
assert lowerLayer.getComp() == this.upperLayer.getComp();
assert this.upperLayer == comp.getActiveLayer();
invertAdjustment = new AdjustmentLayer(comp, "Invert", new Invert());
alwaysUpperColorAdjustment = new AdjustmentLayer(comp, "One Color", new OneColorFilter(upperColor));
upperColorTextLayer = createTestTextLayerWithColor(upperColor);
}
@Test
public void testNormal() {
testBlendingMode(BlendingMode.NORMAL, upperColor);
}
@Test
public void testDarken() {
// MIN(211, 119) = 119
// MIN(141, 86) = 86
// MIN(86, 132) = 86
testBlendingMode(BlendingMode.DARKEN, new Color(119, 86, 86));
}
@Test
public void testMultiply() {
// 211 * 119 / 255 = 98
// 141 * 86 / 255 = 48
// 86 * 132 / 255 = 45
testBlendingMode(BlendingMode.MULTIPLY, new Color(98, 48, 45));
}
@Test
public void testColorBurn() {
testBlendingMode(BlendingMode.COLOR_BURN, new Color(161, 0, 0));
}
@Test
public void testLighten() {
// MAX(211, 119) = 211
// MAX(141, 86) = 141
// MAX(86, 132) = 132
testBlendingMode(BlendingMode.LIGHTEN, new Color(211, 141, 132));
}
@Test
public void testScreen() {
// 255 - (255 - 211)(255 - 119)/255 = 232
testBlendingMode(BlendingMode.SCREEN, new Color(232, 179, 173));
}
@Test
public void testColorDodge() {
testBlendingMode(BlendingMode.COLOR_DODGE, new Color(255, 213, 178));
}
@Test
public void testLinearDodge() {
// 211 + 119 = 330 -> 255
// 141 + 86 = 227
// 86 + 132 = 218
testBlendingMode(BlendingMode.LINEAR_DODGE, new Color(255, 227, 218));
}
@Test
public void testOverlay() {
// PS: 208, 104, 89
testBlendingMode(BlendingMode.OVERLAY, new Color(208, 102, 90));
}
@Test
public void testSoftLight() {
testBlendingMode(BlendingMode.SOFT_LIGHT, new Color(209, 120, 88));
}
@Test
public void testHardLight() {
// PS: 197, 95, 91
testBlendingMode(BlendingMode.HARD_LIGHT, new Color(196, 96, 91));
}
@Test
public void testDifference() {
// ABS(211 - 119) = 92
// ABS(141 - 86) = 55
// ABS(86 - 132) = 46
testBlendingMode(BlendingMode.DIFFERENCE, new Color(92, 55, 46));
}
@Test
public void testExclusion() {
// PS: 134, 131, 128
testBlendingMode(BlendingMode.EXCLUSION, new Color(133, 132, 129));
}
@Test
public void testHue() {
// PS: 205, 115, 240
testBlendingMode(BlendingMode.HUE, new Color(176, 86, 211));
}
@Test
public void testSaturation() {
// PS: 176, 150, 130
testBlendingMode(BlendingMode.SATURATION, new Color(211, 170, 137));
}
@Test
public void testColor() {
// PS: 174, 141, 187
testBlendingMode(BlendingMode.COLOR, new Color(190, 137, 211));
}
@Test
public void testLuminosity() {
// PS: 156, 86, 31
testBlendingMode(BlendingMode.LUMINOSITY, new Color(132, 88, 54));
}
private void testBlendingMode(BlendingMode blendingMode, Color expectedColor) {
// check that the blending mode is working as expected
upperLayer.setBlendingMode(blendingMode, false, true, true);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// a white mask for the upper layer should change nothing
upperLayer.addMask(LayerMaskAddType.REVEAL_ALL);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// a white mask for the lower layer should change nothing
lowerLayer.addMask(LayerMaskAddType.REVEAL_ALL);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// upper layer with a black mask: expect lower color
upperLayer.deleteMask(true);
upperLayer.addMask(LayerMaskAddType.HIDE_ALL);
assertThat(getResultingColor()).isEqualTo(lowerColor);
upperLayer.deleteMask(true);
// adding an invert adjustment should deliver the inverted color
Color inverted = invert(expectedColor);
comp.addLayerNoGUI(invertAdjustment);
assertThat(comp.getNumLayers()).isEqualTo(3);
assertThat(getResultingColor()).isEqualTo(inverted);
// adding a white mask to the adjustment should change nothing
invertAdjustment.addMask(LayerMaskAddType.REVEAL_ALL);
assertThat(getResultingColor()).isEqualTo(inverted);
// with a black mask, the adjustment should have no effect
invertAdjustment.deleteMask(true);
invertAdjustment.addMask(LayerMaskAddType.HIDE_ALL);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// merging down the invert adjustment with black mask should have no effect
comp.mergeDown(false);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// adding a no-op adjustment layer should change nothing
AdjustmentLayer noOpAdjustment = new AdjustmentLayer(comp, "No-op", new NoOpFilter());
comp.addLayerNoGUI(noOpAdjustment);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// merging down the no-op adjustment with black mask should have no effect
comp.mergeDown(false);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// delete the upper layer
comp.deleteLayer(upperLayer, true, false);
assertThat(comp.getNumLayers()).isEqualTo(1);
// test the blending mode with an OneColorFilter that outputs the upper color
comp.addLayerNoGUI(alwaysUpperColorAdjustment);
alwaysUpperColorAdjustment.setBlendingMode(blendingMode, false, true, true);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// adjustment layer with with white mask
alwaysUpperColorAdjustment.addMask(LayerMaskAddType.REVEAL_ALL);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// adjustment layer with with black mask, expect lower color
alwaysUpperColorAdjustment.deleteMask(true);
alwaysUpperColorAdjustment.addMask(LayerMaskAddType.HIDE_ALL);
assertThat(getResultingColor()).isEqualTo(lowerColor);
// merging down the adjustment with black mask should have no effect
comp.mergeDown(false);
assertThat(getResultingColor()).isEqualTo(lowerColor);
assertThat(comp.getNumLayers()).isEqualTo(1);
// test with text layer
comp.addLayerNoGUI(upperColorTextLayer);
upperColorTextLayer.setBlendingMode(blendingMode, false, true, true);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// text layer with white mask
upperColorTextLayer.addMask(LayerMaskAddType.REVEAL_ALL);
assertThat(getResultingColor()).isEqualTo(expectedColor);
// text layer with with black mask, expect lower color
upperColorTextLayer.deleteMask(true);
upperColorTextLayer.addMask(LayerMaskAddType.HIDE_ALL);
assertThat(getResultingColor()).isEqualTo(lowerColor);
// merging down the text layer with black mask should have no effect
comp.mergeDown(false);
assertThat(getResultingColor()).isEqualTo(lowerColor);
assertThat(comp.getNumLayers()).isEqualTo(1);
// merging down the upper layer should result in the expected color
comp.addLayerNoGUI(upperLayer);
assertThat(comp.getNumLayers()).isEqualTo(2);
upperLayer.setBlendingMode(blendingMode, false, true, true);
assertThat(getResultingColor()).isEqualTo(expectedColor);
comp.mergeDown(false);
assertThat(getResultingColor()).isEqualTo(expectedColor);
assertThat(comp.getNumLayers()).isEqualTo(1);
}
private TextLayer createTestTextLayerWithColor(Color color) {
TextLayer layer = new TextLayer(comp);
layer.setSettings(new TextSettings(
"T", // a huge T should cover everything
new Font(Font.SANS_SERIF, Font.BOLD, 100),
color,
new AreaEffects(),
AbstractLayoutPainter.HorizontalAlignment.CENTER,
AbstractLayoutPainter.VerticalAlignment.CENTER,
false
));
return layer;
}
private Color getResultingColor() {
return new Color(comp.calculateCompositeImage().getRGB(0, 0));
}
private static Color invert(Color in) {
return new Color(
255 - in.getRed(),
255 - in.getGreen(),
255 - in.getBlue(),
in.getAlpha()
);
}
}