androidgradientrgbporter-duff

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.


Solution

  • 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:

    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.

    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.