合并两个 RGB 渐变
Merge two RGB gradients
我有两个要合并的 LinearGradients:
- 一条水平线从
rgb(0, 0, 0)
到 rgb(0, 255, 0)
(黑色到绿色)
- 一条垂直线从
rgb(0, 0, 0)
到 rgb(0, 0, 255)
(黑色到蓝色)
我的代码如下所示:
Shader horizontal = new LinearGradient(0, 0, width, 0, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 255, 0)}, null, Shader.TileMode.CLAMP);
Shader vertical = new LinearGradient(0, 0, 0, height, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 0, 255)}, null, Shader.TileMode.CLAMP);
ComposeShader shader = new ComposeShader(horizontal, vertical, mode);
paint.setShader(shader);
红色值可能会改变,但其他两个是不变的。我想在颜色选择器中使用生成的渐变。它必须看起来像这样:(您也可以在 here 上看到它,您必须单击颜色选择器右侧窗格中的 R 字母)
我尝试了几种 PorterDuff 模式,其中一些接近,但 none 符合我的需要。 SCREEN
近乎完美,但有时太轻了。 ADD
将小于 128 的红色值显示为 0。MULTIPLY
用一种纯色填充正方形,仅此而已。我还尝试将渐变的颜色设置为 alpha 128。这使得 ADD
太暗,XOR
和 SCREEN
太苍白。
如何正确制作这个渐变?我应该使用哪种 PorterDuff 模式?
我将光标绘制为与所选颜色相同的颜色,以测试渐变是否正确绘制。 (选中的颜色是用坐标计算的)对于除value以外的所有pivot值,光标很难see/invisible.
看起来白色渐变变透明的速度太快了。为了做到这一点,我使用了两个线性梯度,然后将它们与 ComposeShader
和 SRC_OVER
PorterDuff 模式合并。然后我绘制一个黑色矩形,其透明度对应于值(亮度)值。如果你需要,我可以 post 编码。
编辑:
我要做一些假设。根据您引用的 link,我假设您想要做一些类似的事情,您可以使用滑块控件(如右侧的垂直滑块)实时更改“枢轴”颜色。此外,我假设您想在 red/green/blue 作为主色之间切换。
提高绩效的方法如下:
- 为颜色分配一个
int
数组一次并重复使用该数组。
- 分配一次位图并重新使用位图。
- 始终将位图设为 256 x 256,并在绘制时将位图缩放到合适的大小。这样每次计算都很重要;没有重复的像素。
考虑到所有这些事情,这里是对例程的重写:
private void changeColor(int w, int h, int[] pixels, char pivotColor, int pivotColorValue, boolean initial) {
if (pivotColorValue < 0 || pivotColorValue > 255) {
throw new IllegalArgumentException("color value must be between 0 and 255, was " + pivotColorValue);
}
if (initial) {
// set all the bits of the color
int alpha = 0xFF000000;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int r = 0, b = 0, g = 0;
switch (pivotColor) {
case 'R':
case 'r':
r = pivotColorValue << 16;
g = (256 * x / w) << 8;
b = 256 * y / h;
break;
case 'G':
case 'g':
r = (256 * x / w) << 16;
g = pivotColorValue << 8;
b = 256 * y / h;
break;
case 'B':
case 'b':
r = (256 * x / w) << 16;
g = (256 * y / h) << 8;
b = pivotColorValue;
break;
}
int index = y * w + x;
pixels[index] = alpha | r | g | b;
}
}
} else {
// only set the bits of the color that is changing
int colorBits = 0;
switch (pivotColor) {
case 'R':
case 'r':
colorBits = pivotColorValue << 16;
break;
case 'G':
case 'g':
colorBits = pivotColorValue << 8;
break;
case 'B':
case 'b':
colorBits = pivotColorValue;
break;
}
for (int i = 0; i < pixels.length; i++) {
switch (pivotColor) {
case 'R':
case 'r':
pixels[i] = (pixels[i] & 0xFF00FFFF) | colorBits;
break;
case 'G':
case 'g':
pixels[i] = (pixels[i] & 0xFFFF00FF) | colorBits;
break;
case 'B':
case 'b':
pixels[i] = (pixels[i] & 0xFFFFFF00) | colorBits;
break;
}
}
}
我是这样测试的:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ImageView mImageView;
private Bitmap mBitmap;
private int[] mPixels;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("Demo");
mPixels = new int[256 * 256];
mBitmap = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888);
mImageView = (ImageView) findViewById(R.id.imageview);
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', 0, true);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "initial elapsed time: " + elapsed + " ms");
SeekBar seekBar = (SeekBar) findViewById(R.id.seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', progress, false);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "elapsed time: " + elapsed + " ms");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
}
// changeColor method goes here
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"/>
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:max="255"/>
</LinearLayout>
尝试一下,看看它的性能是否适合您。我觉得挺有道理的。
我认为底层的 Skia 库有一个 Porter-Duff 模式可以做到这一点,但它在 android.graphics.PorterDuff.Mode
中不可用。
好吧好吧,我想我们只能自己动手了:
private Bitmap makeColorPicker(int w, int h, int r) {
if (r < 0 || r > 255) {
throw new IllegalArgumentException("red value must be between 0 and 255, was " + r);
}
// need to manage memory, OutOfMemoryError could happen here
int[] pixels = new int[w * h];
int baseColor = 0xFF000000 | (r << 16); // alpha and red value
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int g = (256 * x / w) << 8;
int b = 256 * y / h;
int index = y * w + x;
pixels[index] = baseColor | g | b;
}
}
return Bitmap.createBitmap(pixels, w, h, Bitmap.Config.ARGB_8888);
}
关于单纯疱疹病毒:
一旦您切换到 HSV 颜色 space,就会为您打开一些不同的选项。现在像您最初考虑的那样合成两个图像是有道理的。我只是给你千字版本的图像。请不要让我打开 PhotoShop。
色调轴:
我正在描绘可以在开发时呈现的双向渐变图像。此渐变在右上角的 alpha 为零,在底部边缘为全黑,在左上角为全白。当您移动色调角度时,您只需在该图像下方绘制一个纯色矩形。在完全饱和度和亮度下,颜色将是所需的色调,因此您会在右上角看到这种颜色。
饱和度枢轴:
这里我画了两个渐变图像,都可以在开发时渲染。第一个是完全饱和,你会看到顶部的水平彩虹在底部混合成黑色。第二种是零饱和度,顶部为白色,底部为黑色。您在底部绘制彩虹渐变,然后在顶部绘制 white/black 渐变。将顶部图像的 alpha 从零更改为完全将显示从完全饱和度到零饱和度的变化。
亮度轴(值)
为此,我正在描绘一个黑色矩形底座和另一个图像,该图像也是水平彩虹渐变,而不是垂直补间到底部的白色(全亮度)。现在,您可以通过将彩虹图像从全 alpha 更改为零 alpha 来调整亮度,从而显示下面的黑色矩形。
我必须做一些数学运算以确保这些 alpha 合成代表实际颜色 space,但我认为我已经非常接近了。
我有两个要合并的 LinearGradients:
- 一条水平线从
rgb(0, 0, 0)
到rgb(0, 255, 0)
(黑色到绿色) - 一条垂直线从
rgb(0, 0, 0)
到rgb(0, 0, 255)
(黑色到蓝色)
我的代码如下所示:
Shader horizontal = new LinearGradient(0, 0, width, 0, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 255, 0)}, null, Shader.TileMode.CLAMP); Shader vertical = new LinearGradient(0, 0, 0, height, new float[]{Color.rgb(0, 0, 0), Color.rgb(0, 0, 255)}, null, Shader.TileMode.CLAMP); ComposeShader shader = new ComposeShader(horizontal, vertical, mode); paint.setShader(shader);
红色值可能会改变,但其他两个是不变的。我想在颜色选择器中使用生成的渐变。它必须看起来像这样:(您也可以在 here 上看到它,您必须单击颜色选择器右侧窗格中的 R 字母)
我尝试了几种 PorterDuff 模式,其中一些接近,但 none 符合我的需要。 SCREEN
近乎完美,但有时太轻了。 ADD
将小于 128 的红色值显示为 0。MULTIPLY
用一种纯色填充正方形,仅此而已。我还尝试将渐变的颜色设置为 alpha 128。这使得 ADD
太暗,XOR
和 SCREEN
太苍白。
如何正确制作这个渐变?我应该使用哪种 PorterDuff 模式?
我将光标绘制为与所选颜色相同的颜色,以测试渐变是否正确绘制。 (选中的颜色是用坐标计算的)对于除value以外的所有pivot值,光标很难see/invisible.
看起来白色渐变变透明的速度太快了。为了做到这一点,我使用了两个线性梯度,然后将它们与 ComposeShader
和 SRC_OVER
PorterDuff 模式合并。然后我绘制一个黑色矩形,其透明度对应于值(亮度)值。如果你需要,我可以 post 编码。
编辑:
我要做一些假设。根据您引用的 link,我假设您想要做一些类似的事情,您可以使用滑块控件(如右侧的垂直滑块)实时更改“枢轴”颜色。此外,我假设您想在 red/green/blue 作为主色之间切换。
提高绩效的方法如下:
- 为颜色分配一个
int
数组一次并重复使用该数组。 - 分配一次位图并重新使用位图。
- 始终将位图设为 256 x 256,并在绘制时将位图缩放到合适的大小。这样每次计算都很重要;没有重复的像素。
考虑到所有这些事情,这里是对例程的重写:
private void changeColor(int w, int h, int[] pixels, char pivotColor, int pivotColorValue, boolean initial) {
if (pivotColorValue < 0 || pivotColorValue > 255) {
throw new IllegalArgumentException("color value must be between 0 and 255, was " + pivotColorValue);
}
if (initial) {
// set all the bits of the color
int alpha = 0xFF000000;
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int r = 0, b = 0, g = 0;
switch (pivotColor) {
case 'R':
case 'r':
r = pivotColorValue << 16;
g = (256 * x / w) << 8;
b = 256 * y / h;
break;
case 'G':
case 'g':
r = (256 * x / w) << 16;
g = pivotColorValue << 8;
b = 256 * y / h;
break;
case 'B':
case 'b':
r = (256 * x / w) << 16;
g = (256 * y / h) << 8;
b = pivotColorValue;
break;
}
int index = y * w + x;
pixels[index] = alpha | r | g | b;
}
}
} else {
// only set the bits of the color that is changing
int colorBits = 0;
switch (pivotColor) {
case 'R':
case 'r':
colorBits = pivotColorValue << 16;
break;
case 'G':
case 'g':
colorBits = pivotColorValue << 8;
break;
case 'B':
case 'b':
colorBits = pivotColorValue;
break;
}
for (int i = 0; i < pixels.length; i++) {
switch (pivotColor) {
case 'R':
case 'r':
pixels[i] = (pixels[i] & 0xFF00FFFF) | colorBits;
break;
case 'G':
case 'g':
pixels[i] = (pixels[i] & 0xFFFF00FF) | colorBits;
break;
case 'B':
case 'b':
pixels[i] = (pixels[i] & 0xFFFFFF00) | colorBits;
break;
}
}
}
我是这样测试的:
public class MainActivity extends AppCompatActivity {
private static final String TAG = "MainActivity";
private ImageView mImageView;
private Bitmap mBitmap;
private int[] mPixels;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
setTitle("Demo");
mPixels = new int[256 * 256];
mBitmap = Bitmap.createBitmap(256, 256, Bitmap.Config.ARGB_8888);
mImageView = (ImageView) findViewById(R.id.imageview);
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', 0, true);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "initial elapsed time: " + elapsed + " ms");
SeekBar seekBar = (SeekBar) findViewById(R.id.seekbar);
seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
long start = SystemClock.elapsedRealtime();
changeColor(256, 256, mPixels, 'r', progress, false);
mBitmap.setPixels(mPixels, 0, 256, 0, 0, 256, 256);
mImageView.setImageBitmap(mBitmap);
long elapsed = SystemClock.elapsedRealtime() - start;
Log.d(TAG, "elapsed time: " + elapsed + " ms");
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) { }
@Override
public void onStopTrackingTouch(SeekBar seekBar) { }
});
}
// changeColor method goes here
}
activity_main.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/activity_main"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:paddingBottom="@dimen/activity_vertical_margin"
android:paddingLeft="@dimen/activity_horizontal_margin"
android:paddingRight="@dimen/activity_horizontal_margin"
android:paddingTop="@dimen/activity_vertical_margin">
<ImageView
android:id="@+id/imageview"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:scaleType="fitCenter"/>
<SeekBar
android:id="@+id/seekbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="24dp"
android:max="255"/>
</LinearLayout>
尝试一下,看看它的性能是否适合您。我觉得挺有道理的。
我认为底层的 Skia 库有一个 Porter-Duff 模式可以做到这一点,但它在 android.graphics.PorterDuff.Mode
中不可用。
好吧好吧,我想我们只能自己动手了:
private Bitmap makeColorPicker(int w, int h, int r) {
if (r < 0 || r > 255) {
throw new IllegalArgumentException("red value must be between 0 and 255, was " + r);
}
// need to manage memory, OutOfMemoryError could happen here
int[] pixels = new int[w * h];
int baseColor = 0xFF000000 | (r << 16); // alpha and red value
for (int y = 0; y < h; y++) {
for (int x = 0; x < w; x++) {
int g = (256 * x / w) << 8;
int b = 256 * y / h;
int index = y * w + x;
pixels[index] = baseColor | g | b;
}
}
return Bitmap.createBitmap(pixels, w, h, Bitmap.Config.ARGB_8888);
}
关于单纯疱疹病毒:
一旦您切换到 HSV 颜色 space,就会为您打开一些不同的选项。现在像您最初考虑的那样合成两个图像是有道理的。我只是给你千字版本的图像。请不要让我打开 PhotoShop。
色调轴:
我正在描绘可以在开发时呈现的双向渐变图像。此渐变在右上角的 alpha 为零,在底部边缘为全黑,在左上角为全白。当您移动色调角度时,您只需在该图像下方绘制一个纯色矩形。在完全饱和度和亮度下,颜色将是所需的色调,因此您会在右上角看到这种颜色。
饱和度枢轴:
这里我画了两个渐变图像,都可以在开发时渲染。第一个是完全饱和,你会看到顶部的水平彩虹在底部混合成黑色。第二种是零饱和度,顶部为白色,底部为黑色。您在底部绘制彩虹渐变,然后在顶部绘制 white/black 渐变。将顶部图像的 alpha 从零更改为完全将显示从完全饱和度到零饱和度的变化。
亮度轴(值)
为此,我正在描绘一个黑色矩形底座和另一个图像,该图像也是水平彩虹渐变,而不是垂直补间到底部的白色(全亮度)。现在,您可以通过将彩虹图像从全 alpha 更改为零 alpha 来调整亮度,从而显示下面的黑色矩形。
我必须做一些数学运算以确保这些 alpha 合成代表实际颜色 space,但我认为我已经非常接近了。