Nicolas
Nicolas

Reputation: 7121

Merge two RGB gradients

I have two LinearGradients which I want to merge:

My code looks like this:

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);

The red value may change but the two others are constant. I want to use the resulting gradient in a color picker. It has to look like this: (you can see it on here too, you have to click on the R letter on the right pane of the color picker)

gradient

I tried several PorterDuff modes, a few came close but none matched what I need. SCREEN is almost perfect but sometimes it's too light. ADD show red values smaller than 128 as if it was 0. MULTIPLY fills the square with one solid color and that's it. I also tried setting the colors of the gradients to alpha 128. This makes ADD too dark, XOR and SCREEN too pale.

How can I make this gradient correctly? What PorterDuff mode should I use?


I draw the cursor the same color as the selected color to test if the gradient is correctly drawn. (Selected color is calculated with coordinates) For all pivot values except value, the cursor hard to see/invisible.

hsv

Looks like the white gradient turns transparent too quickly. To make it I dew two lineargradients then merged them with ComposeShader and SRC_OVER PorterDuff mode. Then I draw a black rectangle with transparency corresponding to the value (brightness) value. I can post code if you need.

Upvotes: 1

Views: 1776

Answers (1)

kris larson
kris larson

Reputation: 30985

EDIT:

I am going to make some assumptions. Based on the link you referenced, I'll assume you'll want to do something similar where you can change the "pivot" color in real time using a slider control like the vertical slider to the right. Also I'll assume that you want to switch between red/green/blue as the pivot color.

Here's how to increase your performance:

  • Allocate an int array for the colors once and reuse that array.
  • Allocate a bitmap once and reuse the bitmap.
  • Always make the bitmap 256 x 256 and scale the bitmap to the right size when you draw it. This way every calculation counts; no duplicate pixels.

With all those things in mind, here's a rewrite of the routine:

    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;
                }
            }
        }

Here's how I tested it:

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>

Try that out and see if it performs well enough for you. I thought it was reasonable.


I think the underlying Skia library has a Porter-Duff mode that would do this, but it's not available in android.graphics.PorterDuff.Mode.

Okay fine, I guess we'll just have to do it our damn selves:

    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);
    }

Regarding HSV:

Once you switch to HSV color space, some different options open up for you. Now compositing two images like you were originally considering makes sense. I'm just going to give you the thousand words versions of the images. Please don't make me open up PhotoShop.

  • Pivot on Hue:

    I'm picturing a two-way gradient image that could be rendered at development time. This gradient would have zero alpha at the upper right corner, full black at the bottom edge and full white at the upper left corner. As you move through the hue angles, you would just draw a solid color rectangle underneath this image. The color would be the desired hue at full saturation and brightness, so you would see just this color in the upper right corner.

  • Pivot on Saturation:

    Here I'm picturing two gradient images, both could be rendered at development time. The first would be full saturation, where you see the horizontal rainbow at the top blending into black at the bottom. The second would be zero saturation, with white at the top and black at the bottom. You draw the rainbow gradient on the bottom, then draw the white/black gradient on top. Changing the alpha of the top image from zero to full will show the change from full saturation to zero saturation.

  • Pivot on Brightness (Value)

    For this I'm picturing a black rectangle base with another image that is also a horizontal rainbow gradient than tweens vertically to white at the bottom (full brightness). Now you pivot on brightness by changing the rainbow image from full alpha to zero alpha, revealing the black rectangle underneath.

I'd have to do some math to make sure these alpha composites represent the actual color space, but I think I'm pretty close.

Upvotes: 3

Related Questions