More accurate conversion between HSV and RGB color spaces

This is needed to avoid problems when dragging one of the R/G/B
sliders. With the default Android conversion code, the other R/G/B
sliders sometimes move during dragging.
This commit is contained in:
Peter Osterlund 2020-03-28 22:53:49 +01:00
parent 4dcc24cee7
commit 3969a9e841
6 changed files with 192 additions and 49 deletions

View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2020 Peter Österlund
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package net.margaritov.preference.colorpicker;
import static org.junit.Assert.*;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
public class AHSVColorTest {
@BeforeClass
public static void setUpClass() {
}
@AfterClass
public static void tearDownClass() {
}
@Test
public void alpha() {
AHSVColor color = new AHSVColor();
for (int i = 0; i < 255; i++) {
color.setAlpha(i);
assertEquals(i, color.getAlpha());
}
}
@Test
public void hsv() {
AHSVColor color = new AHSVColor();
double[] hsv = new double[3];
for (int i = 0; i < 360; i++) {
hsv[0] = i;
hsv[1] = (i % 17) / 17;
hsv[2] = (i % 11) / 11;
color.setHSV(hsv);
double[] ret = color.getHSV();
assertEquals(hsv[0], ret[0], 1e-10);
assertEquals(hsv[1], ret[1], 1e-10);
assertEquals(hsv[2], ret[2], 1e-10);
}
}
@Test
public void rgb() {
AHSVColor color = new AHSVColor();
for (int i = 0; i < 255; i++) {
int r = (i * 3413) % 255;
int g = (i * 113) % 255;
int b = (i * 1847) % 255;
int c = 0xff000000 + (r << 16) + (g << 8) + b;
color.setARGB(c);
int c2 = color.getARGB();
int r2 = (c2 & 0x00ff0000) >>> 16;
int g2 = (c2 & 0x0000ff00) >>> 8;
int b2 = (c2 & 0x000000ff);
assertEquals(r, r2);
assertEquals(g, g2);
assertEquals(b, b2);
}
}
}

View File

@ -18,15 +18,15 @@ package net.margaritov.preference.colorpicker;
import android.graphics.Color;
/** Represents a color in HSV float format and an alpha value. */
/** Represents a color in HSV double format and an integer alpha value (0-255). */
class AHSVColor {
private int alpha = 0xff;
private float[] hsv = new float[]{360f, 0f, 0f};
private double[] hsv = new double[]{360, 0, 0};
AHSVColor() { }
/** Set hue,sat,val values. Preserve alpha. */
void setHSV(float[] hsv) {
void setHSV(double[] hsv) {
this.hsv[0] = hsv[0];
this.hsv[1] = hsv[1];
this.hsv[2] = hsv[2];
@ -43,8 +43,8 @@ class AHSVColor {
int r = Color.red(color);
int g = Color.green(color);
int b = Color.blue(color);
float oldHue = hsv[0];
Color.RGBToHSV(r, g, b, hsv);
double oldHue = hsv[0];
ARGBToHSV(Color.rgb(r, g, b), hsv);
if (hsv[1] <= 0f)
hsv[0] = oldHue;
}
@ -67,8 +67,8 @@ class AHSVColor {
}
/** Get hue,sat,val values. */
float[] getHSV() {
return new float[]{hsv[0], hsv[1], hsv[2]};
double[] getHSV() {
return new double[]{hsv[0], hsv[1], hsv[2]};
}
/** Get alpha value. */
@ -78,7 +78,7 @@ class AHSVColor {
/** Get ARGB color value. */
int getARGB() {
return Color.HSVToColor(alpha, hsv);
return HSVToARGB(alpha, hsv);
}
/** Get red (0), green (1), or blue (2) color component. */
@ -91,4 +91,72 @@ class AHSVColor {
default: throw new RuntimeException("Internal error");
}
}
private static int HSVToARGB(int alpha, double[] hsv) {
double h = hsv[0] % 360;
double s = hsv[1];
double v = hsv[2];
double c = v * s;
double m = v - c;
double x = c * (1 - Math.abs(((h / 60.0) % 2) - 1));
double r = 0, g = 0, b = 0;
switch ((int)Math.floor(h / 60.0)) {
case 0: r = c; g = x; break;
case 1: r = x; g = c; break;
case 2: g = c; b = x; break;
case 3: g = x; b = c; break;
case 4: r = x; b = c; break;
case 5: r = c; b = x; break;
}
int red = Math.min(Math.max((int)Math.round((r + m) * 255), 0), 255);
int green = Math.min(Math.max((int)Math.round((g + m) * 255), 0), 255);
int blue = Math.min(Math.max((int)Math.round((b + m) * 255), 0), 255);
return Color.argb(alpha, red, green, blue);
}
private static void ARGBToHSV(int color, double[] hsv) {
int red = Color.red(color);
int green = Color.green(color);
int blue = Color.blue(color);
double r = red / 255.0;
double g = green / 255.0;
double b = blue / 255.0;
int maxI = 0;
double cMax = r;
if (cMax < g) {
cMax = g;
maxI = 1;
}
if (cMax < b) {
cMax = b;
maxI = 2;
}
double cMin = Math.min(Math.min(r, g), b);
double d = cMax - cMin;
double v = cMax;
double s = (cMax > 0) ? (d / cMax) : 0.0;
double h;
if (d <= 0) {
h = 0;
} else if (maxI == 0) {
h = 60 * (g - b) / d;
if (h < 0)
h += 360;
} else if (maxI == 1) {
h = 60 * ((b - r) / d + 2);
} else {
h = 60 * ((r - g) / d + 4);
}
hsv[0] = h;
hsv[1] = s;
hsv[2] = v;
}
}

View File

@ -56,19 +56,19 @@ class AlphaGradientPanel extends GradientPanel {
@Override
void updateColor(Point point) {
int alpha = pointToAlpha(point.x);
int alpha = pointToAlpha(point);
color.setAlpha(alpha);
}
private Point alphaToPoint(int alpha) {
float width = rect.width();
return new Point((int)(width - (alpha * width / 0xff) + rect.left),
(int)rect.top);
double width = rect.width();
return new Point((int)Math.round(width - (alpha * width / 0xff) + rect.left),
Math.round(rect.top));
}
private int pointToAlpha(int x) {
private int pointToAlpha(Point p) {
int width = (int)rect.width();
x = Math.min(Math.max(x - (int)rect.left, 0), width);
int x = Math.min(Math.max(p.x - (int)rect.left, 0), width);
return 0xff - (x * 0xff / width);
}
}

View File

@ -51,20 +51,20 @@ public class HueGradientPanel extends GradientPanel {
@Override
void updateColor(Point point) {
float[] hsv = color.getHSV();
hsv[0] = pointToHue(point.y);
double[] hsv = color.getHSV();
hsv[0] = pointToHue(point);
color.setHSV(hsv);
}
private Point hueToPoint(float hue) {
float height = rect.height();
return new Point((int)rect.left,
(int)(height - (hue * height / 360f) + rect.top));
private Point hueToPoint(double hue) {
double height = rect.height();
return new Point(Math.round(rect.left),
(int)Math.round((height - (hue * height / 360) + rect.top)));
}
private float pointToHue(float y) {
float height = rect.height();
y = Math.min(Math.max(y - rect.top, 0f), height);
return 360f - (y * 360f / height);
private double pointToHue(Point p) {
double height = rect.height();
double y = Math.min(Math.max(p.y - rect.top, 0f), height);
return 360 - (y * 360 / height);
}
}

View File

@ -1,7 +1,6 @@
package net.margaritov.preference.colorpicker;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.LinearGradient;
import android.graphics.Point;
import android.graphics.RectF;
@ -56,13 +55,13 @@ public class RGBGradientPanel extends GradientPanel {
private Point rgbComponentToPoint(int val) {
if (horizontal) {
float width = rect.width();
return new Point((int)((val * width / 0xff) + rect.left),
(int)rect.top);
double width = rect.width();
return new Point((int)Math.round((val * width / 0xff) + rect.left),
Math.round(rect.top));
} else {
float height = rect.height();
return new Point((int)rect.left,
(int)(rect.bottom - (val * height / 0xff)));
double height = rect.height();
return new Point(Math.round(rect.left),
(int)Math.round(rect.bottom - (val * height / 0xff)));
}
}

View File

@ -19,7 +19,6 @@ package net.margaritov.preference.colorpicker;
import android.graphics.Canvas;
import android.graphics.ComposeShader;
import android.graphics.LinearGradient;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.PorterDuff;
import android.graphics.RectF;
@ -42,9 +41,9 @@ public class SatValGradientPanel extends GradientPanel {
@Override
protected void setGradientPaint() {
float[] hsv = color.getHSV();
hsv[1] = 1f;
hsv[2] = 1f;
double[] hsv = color.getHSV();
hsv[1] = 1;
hsv[2] = 1;
AHSVColor hue = new AHSVColor();
hue.setHSV(hsv);
Shader satShader = new LinearGradient(rect.left, rect.top, rect.right, rect.top,
@ -55,7 +54,7 @@ public class SatValGradientPanel extends GradientPanel {
@Override
protected void drawTracker(Canvas canvas) {
float[] hsv = color.getHSV();
double[] hsv = color.getHSV();
Point p = satValToPoint(hsv[1], hsv[2]);
float r = PALETTE_CIRCLE_TRACKER_RADIUS;
@ -67,28 +66,28 @@ public class SatValGradientPanel extends GradientPanel {
@Override
void updateColor(Point point) {
float[] hsv = color.getHSV();
float[] result = pointToSatVal(point.x, point.y);
double[] hsv = color.getHSV();
double[] result = pointToSatVal(point);
hsv[1] = result[0];
hsv[2] = result[1];
color.setHSV(hsv);
}
private Point satValToPoint(float sat, float val) {
final float width = rect.width();
final float height = rect.height();
private Point satValToPoint(double sat, double val) {
double width = rect.width();
double height = rect.height();
return new Point((int)(sat * width + rect.left),
(int)((1f - val) * height + rect.top));
return new Point((int)Math.round(sat * width + rect.left),
(int)Math.round((1 - val) * height + rect.top));
}
private float[] pointToSatVal(float x, float y) {
float width = rect.width();
float height = rect.height();
private double[] pointToSatVal(Point p) {
double width = rect.width();
double height = rect.height();
x = Math.min(Math.max(x - rect.left, 0f), width);
y = Math.min(Math.max(y - rect.top, 0f), height);
double x = Math.min(Math.max(p.x - rect.left, 0), width);
double y = Math.min(Math.max(p.y - rect.top, 0), height);
return new float[]{ x / width, 1f - y / height };
return new double[]{ x / width, 1 - y / height };
}
}