Skip to content

Commit 67eeec0

Browse files
committed
Oklch in "Hue-Saturation"
1 parent 898a089 commit 67eeec0

2 files changed

Lines changed: 96 additions & 29 deletions

File tree

src/main/java/pixelitor/filters/HueSat.java

Lines changed: 92 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,12 @@
1717

1818
package pixelitor.filters;
1919

20+
import com.jhlabs.image.ImageMath;
2021
import com.jhlabs.image.PointFilter;
22+
import pixelitor.filters.gui.IntChoiceParam;
23+
import pixelitor.filters.gui.IntChoiceParam.Item;
2124
import pixelitor.filters.gui.RangeParam;
25+
import pixelitor.utils.ColorSpaces;
2226

2327
import java.awt.Color;
2428
import java.awt.image.BufferedImage;
@@ -29,7 +33,7 @@
2933
import static pixelitor.gui.GUIText.SATURATION;
3034

3135
/**
32-
* Hue-Saturation filter
36+
* The Hue-Saturation filter.
3337
*/
3438
public class HueSat extends ParametrizedFilter {
3539
public static final String NAME = HUE + "/" + SATURATION;
@@ -49,6 +53,13 @@ public class HueSat extends ParametrizedFilter {
4953
private static final int MAX_BRI = 100;
5054
private static final int DEFAULT_BRI = 0;
5155

56+
private static final int COLOR_SPACE_HSV = 0;
57+
private static final int COLOR_SPACE_OKLCH = 1;
58+
59+
private final IntChoiceParam colorSpace = new IntChoiceParam("Color Space", new Item[]{
60+
new Item("HSV (Faster)", COLOR_SPACE_HSV),
61+
new Item("Oklch (Better)", COLOR_SPACE_OKLCH),
62+
});
5263
private final RangeParam hue = new RangeParam(HUE, MIN_HUE, DEFAULT_HUE, MAX_HUE);
5364
private final RangeParam saturation = new RangeParam(SATURATION, MIN_SAT, DEFAULT_SAT, MAX_SAT);
5465
private final RangeParam brightness = new RangeParam(BRIGHTNESS, MIN_BRI, DEFAULT_BRI, MAX_BRI);
@@ -61,6 +72,7 @@ public HueSat() {
6172
brightness.setPresetKey("Brightness");
6273

6374
initParams(
75+
colorSpace,
6476
hue,
6577
saturation,
6678
brightness
@@ -69,29 +81,91 @@ public HueSat() {
6981

7082
@Override
7183
public BufferedImage transform(BufferedImage src, BufferedImage dest) {
72-
int hueP = hue.getValue();
73-
int satP = saturation.getValue();
74-
int briP = brightness.getValue();
75-
76-
if (hueP == 0 && satP == 0 && briP == 0) {
84+
if (hue.isZero() && saturation.isZero() && brightness.isZero()) {
7785
return src;
7886
}
7987

88+
if (colorSpace.valueIs(COLOR_SPACE_OKLCH)) {
89+
float hueShift = hue.getValueAsFloat();
90+
// satFactor is a multiplier, e.g., 1.5 for a 50% increase
91+
float satFactor = 1.0f + (float) saturation.getPercentage();
92+
float briShift = (float) brightness.getPercentage();
93+
94+
dest = new OklchImpl(hueShift, satFactor, briShift).filter(src, dest);
95+
return dest;
96+
}
97+
98+
// HSV color space
8099
float satShift = (float) saturation.getPercentage();
81100
float briShift = (float) brightness.getPercentage();
82101
float hueRot = hue.getValueAsFloat() / 360.0f;
83102

84-
dest = new Impl(hueRot, satShift, briShift).filter(src, dest);
103+
dest = new HsvImpl(hueRot, satShift, briShift).filter(src, dest);
85104

86105
return dest;
87106
}
88107

89-
private static class Impl extends PointFilter {
108+
/**
109+
* An implementation of the filter that works in the Oklch color space.
110+
*/
111+
private static class OklchImpl extends PointFilter {
112+
private final float hueShift;
113+
private final float satFactor;
114+
private final float briShift;
115+
116+
protected OklchImpl(float hueShift, float satFactor, float briShift) {
117+
super(NAME);
118+
this.hueShift = hueShift;
119+
this.satFactor = satFactor;
120+
this.briShift = briShift;
121+
}
122+
123+
@Override
124+
public int processPixel(int x, int y, int rgb) {
125+
int a = rgb & 0xFF_00_00_00;
126+
127+
// for the multithreaded performance it's better to
128+
// create this array here instead of reusing it as a class field
129+
float[] oklch = ColorSpaces.srgbToOklch(rgb);
130+
131+
// L is in [0, 1], C is >= 0, h is in [0, 360)
132+
float l = oklch[0];
133+
float c = oklch[1];
134+
float h = oklch[2];
135+
136+
// apply adjustments
137+
h += hueShift;
138+
// normalize hue to be in the range [0, 360)
139+
if (h < 0.0f) {
140+
h += 360.0f;
141+
}
142+
if (h >= 360.0f) {
143+
h -= 360.0f;
144+
}
145+
146+
c *= satFactor;
147+
// chroma can't be negative
148+
c = Math.max(0.0f, c);
149+
150+
l += briShift;
151+
// clamp lightness to [0, 1]
152+
l = ImageMath.clamp01(l);
153+
154+
oklch[0] = l;
155+
oklch[1] = c;
156+
oklch[2] = h;
157+
158+
int newRGB = ColorSpaces.oklchToSrgb(oklch);
159+
return a | (newRGB & 0x00_FF_FF_FF);
160+
}
161+
}
162+
163+
private static class HsvImpl extends PointFilter {
90164
private final float hueRot;
91165
private final float satShift;
92166
private final float briShift;
93167

94-
protected Impl(float hueRot, float satShift, float briShift) {
168+
protected HsvImpl(float hueRot, float satShift, float briShift) {
95169
super(NAME);
96170
this.hueRot = hueRot;
97171
this.satShift = satShift;
@@ -111,32 +185,21 @@ public int processPixel(int x, int y, int rgb) {
111185

112186
tmpHSBArray = Color.RGBtoHSB(r, g, b, tmpHSBArray);
113187

114-
float shiftedHue = tmpHSBArray[0] + hueRot;
115-
float shiftedSat = tmpHSBArray[1] + satShift;
116-
float shiftedBri = tmpHSBArray[2] + briShift;
188+
float newHue = tmpHSBArray[0] + hueRot;
189+
float newSat = tmpHSBArray[1] + satShift;
190+
float newBri = tmpHSBArray[2] + briShift;
117191

118-
if (shiftedSat < 0.0f) {
119-
shiftedSat = 0.0f;
120-
}
121-
if (shiftedSat > 1.0f) {
122-
shiftedSat = 1.0f;
123-
}
124-
125-
if (shiftedBri < 0.0f) {
126-
shiftedBri = 0.0f;
127-
}
128-
if (shiftedBri > 1.0f) {
129-
shiftedBri = 1.0f;
130-
}
192+
newSat = ImageMath.clamp01(newSat);
193+
newBri = ImageMath.clamp01(newBri);
131194

132-
if (shiftedHue < 0 && shiftedHue > -0.00000003) {
195+
if (newHue < 0 && newHue > -0.00000003) {
133196
// workaround for a bug in Color.HSBtoRGB, see issue #87
134-
shiftedHue = 0;
197+
newHue = 0;
135198
}
136199

137-
int newRGB = Color.HSBtoRGB(shiftedHue, shiftedSat, shiftedBri); // alpha is 255 here
200+
int newRGB = Color.HSBtoRGB(newHue, newSat, newBri); // alpha is 255 here
138201
newRGB &= 0x00_FF_FF_FF; // set alpha to 0
139202
return a | newRGB; // add the real alpha
140203
}
141204
}
142-
}
205+
}

src/main/java/pixelitor/filters/gui/IntChoiceParam.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ public int getValue() {
5555
return selectedValue.value();
5656
}
5757

58+
public boolean valueIs(int value) {
59+
return selectedValue.valueIs(value);
60+
}
61+
5862
/**
5963
* Sets the default choice by its integer value.
6064
*/

0 commit comments

Comments
 (0)