35
votes

I want to be able to take two RGB-256 vectors and calculate the result of their mixture. Also I want to be able to give each vector a different weight. I've experimented with it using the Word color plate, and I've seen that while some colors do mix according to a weighted average:

0.5*red(255,0,0) + 0.5*yellow(255,255,0) = orange(255,127,0)

others don't:

0.5*yellow(255,255,0) + 0.5*blue(0,0,255) = gray (127,127,127) and not green (0,255,0)

Is there an algorithm for accurate calculation for all colors or am I forced to do it using a look up table?

13

13 Answers

34
votes

The best explanation is that the RGB color model is somewhat unintuitive for us humans.
It's perfectly logical for a video display device (such as a monitor or television) that only knows how to display colors by mixing varying amounts of 3 pre-defined colors: red, green, and blue. But that's not how you and I learned to mix colors in grade school.

In RGB, the color white is represented as (255, 255, 255), which equates to "all on." The full value for each of the red, green, and blue color components is displayed, which creates such a high light intensity that we perceive the color as white. Conversely, the color black (0, 0, 0) is the "default" state of the display device—when no color light is displayed ("0"), the result is black, or the absence of color.

When you fully mix yellow (255, 255, 0) and blue (0, 0, 255), you get the color represented by (255, 255, 255), or white. When you multiply white by 0.5, you get a gray color, because 255 * 0.5 = 127.

It's probably more of an anomaly than anything that you get the expected result when mixing red and yellow. Red + yellow is (510, 255, 0), so when you multiply that by 0.5, you get orange (255, 127, 0).

It turns out that what you and I learned in grade school is really more accurately known as a subtractive color model, versus the additive color model used by RGB. Looking at the two diagrams at the bottom, you'll see the RGB color model (additive) on the left, versus the CMYK color model (subtractive) on the right. You should be able to immediately see the problem. Using a subtractive color model instead will produce the results that you want.

         RGB Color Model                                 CMYK Color Model

EDIT: Of course, that's easier said than done. That is, even if you adopt the CMYK color model instead of RGB (which is already difficult, because conversions from RGB to CMYK are heavily device-dependent and far from straightforward), this will probably still not satisfy the urge to mix pure blue (0, 0, 255) with pure yellow (255, 255, 0) and get green. CMYK uses cyan, magenta, and yellow as the 3 primary colors, instead of red, yellow, and blue. And you have a lot of work ahead of you if you want to go about implementing the RYB color model in the language of your choice.

I don't know of any such algorithm to combine RGB colors realistically. I was attempting in my answer to explain why it would be impossible, or extremely difficult at the very least. You could leave this question open a few more days and see if anyone else has any ideas, but I'd go ahead and start on that look-up table if you need to stick with the RGB model. ;-)

5
votes

Based on this documented response and this alghoritm aware response, I have tryed a simple interface to mix colors using additive and substractive aproach.

You must confirm that primary colors of RGB and CMYK gives you the secondary colors in the first response diagrams:

  • Red + Blue = Magenta (on additive)
  • Yellow + Cyan = Green (on substractive)
  • and so on ...
import javax.swing.*;
import javax.swing.border.BevelBorder;
import javax.swing.border.CompoundBorder;
import javax.swing.border.LineBorder;
import javax.swing.border.MatteBorder;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.Hashtable;
import java.util.Vector;

/**
 * Color Mixing alghoritms
 * User: alberto
 * Date: 29/01/13
 * Time: 21:28
 */
public class ColorMix {

    Vector<JLabel> firstMixColors;
    Vector<JLabel> secondMixColors;
    JComboBox/*<Mixer>*/ comboBox;
    JLabel firstMixColor;
    JLabel firstSel;
    JLabel secondSel;
    JLabel finalColor;

    public ColorMix() {
        firstMixColors = new Vector<JLabel>();
        Vector<Mixer> mixers = new Vector<Mixer>();
        mixers.add(new AdditiveMixer());
        mixers.add(new SustractiveMixer());
        mixers.add(new TertiaryMixer());
        mixers.add(new DilutingSustractiveMixer());

        comboBox = new JComboBox(new DefaultComboBoxModel(mixers));
        firstMixColor = buildColorLabel();
        firstSel = buildColorLabel();
        secondSel = buildColorLabel();
        secondMixColors = new Vector<JLabel>();
        secondMixColors.add(firstSel);
        secondMixColors.add(secondSel);
        finalColor = buildColorLabel();
        comboBox.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                calculateMixes();
            }
        });
        buildGUI();
    }

    private JLabel buildColorLabel() {
        JLabel label = new JLabel();
        label.setOpaque(true);
        label.setHorizontalAlignment(SwingConstants.CENTER);
        label.setHorizontalTextPosition(SwingConstants.CENTER);
        label.setBorder(BorderFactory.createLineBorder(Color.BLACK));
        label.setPreferredSize(new Dimension(100,25));
        return label;
    }

    public void buildGUI() {
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        frame.setTitle("Mixing colors");

        frame.setLayout(new GridBagLayout());
        GridBagConstraints cc = new GridBagConstraints();
        cc.fill = GridBagConstraints.BOTH;
        cc.insets = new Insets(5, 5, 5, 5);
        cc.weightx = .2;
        cc.weighty = 1;
        frame.getContentPane().add(buildColorPanel(0), cc);
        frame.getContentPane().add(buildColorPanel(1), cc);
        cc.gridy = 1;
        JPanel firstMix = new JPanel(new GridBagLayout());
        GridBagConstraints ccCol = new GridBagConstraints();
        ccCol.fill = GridBagConstraints.BOTH;
        ccCol.insets = new Insets(5, 5, 5, 5);
        ccCol.weightx = 1;
        ccCol.weighty = 1;

        ccCol.gridx = 0;
        ccCol.gridy = 0;
        ccCol.gridheight = 2;
        firstMix.add(firstMixColor, ccCol);
        ccCol.fill = GridBagConstraints.HORIZONTAL;
        ccCol.weightx = 0.2;
        ccCol.weighty = 0.5;
        ccCol.gridx = 1;
        ccCol.gridy = 0;
        ccCol.gridheight = 1;
        ccCol.gridwidth = 1;
        firstMix.add(new JButton(new AbstractAction("Set First") {
            @Override
            public void actionPerformed(ActionEvent e) {
                setBackgroundToLabel(firstSel, firstMixColor.getBackground());
                calculateMixes();
            }
        }), ccCol);
        ccCol.gridx = 1;
        ccCol.gridy = 1;
        firstMix.add(new JButton(new AbstractAction("Set Second") {
            @Override
            public void actionPerformed(ActionEvent e) {
                setBackgroundToLabel(secondSel, firstMixColor.getBackground());
                calculateMixes();
            }
        }), ccCol);
        firstMix.setBorder(BorderFactory.createTitledBorder("Secondary Colors"));
        frame.getContentPane().add(firstMix, cc);
        cc.weightx = .6;

        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints ccColor = new GridBagConstraints();
        ccColor.fill = GridBagConstraints.BOTH;
        ccColor.insets = new Insets(5, 5, 5, 5);
        ccColor.weightx = 1;
        ccColor.weighty = 1;
        panel.add(firstSel, ccColor);
        ccColor.gridx = 1;
        panel.add(secondSel, ccColor);
        ccColor.gridx = 0;
        ccColor.gridy = 1;
        ccColor.weighty = 0;
        ccColor.gridwidth = 2;
        panel.add(finalColor, ccColor);
        ccColor.gridy = 2;
        panel.add(comboBox, ccColor);
        panel.setBorder(BorderFactory.createTitledBorder("Tertiary Colors"));
        frame.getContentPane().add(panel, cc);
        frame.pack();
        frame.setVisible(true);
    }


    public static void main(String[] args) {
        new ColorMix();
    }

    private JComponent buildColorPanel(int selectedIndex) {
        final JLabel pColor = buildColorLabel();
        firstMixColors.add(pColor);
        JPanel pSelectColor = new JPanel(new GridBagLayout());
        GridBagConstraints cc = new GridBagConstraints();
        cc.fill = GridBagConstraints.BOTH;
        cc.insets = new Insets(5, 5, 5, 5);
        cc.weightx = 1;
        cc.weighty = 1;
        final JSlider slidRed = buildSlider(pSelectColor, cc);
        final JSlider slidGreen = buildSlider(pSelectColor, cc);
        final JSlider slidBlue = buildSlider(pSelectColor, cc);
        pSelectColor.add(pColor, cc);
        final JComboBox comboColores = buildColorCombo();
        comboColores.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                Color color = (Color) comboColores.getSelectedItem();
                slidRed.setValue(color.getRed());
                slidGreen.setValue(color.getGreen());
                slidBlue.setValue(color.getBlue());
            }
        });
        comboColores.setSelectedIndex(selectedIndex);
        cc.gridy = 1;
        cc.gridwidth = 4;
        cc.weighty = 0;
        pSelectColor.add(comboColores, cc);
        ChangeListener changeListener = new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                setBackgroundToLabel(pColor, new Color(slidRed.getValue(), slidGreen.getValue(), slidBlue.getValue()));
                calculateMixes();
            }
        };
        slidRed.addChangeListener(changeListener);
        slidGreen.addChangeListener(changeListener);
        slidBlue.addChangeListener(changeListener);
        pSelectColor.setBorder(BorderFactory.createBevelBorder(BevelBorder.LOWERED));
        changeListener.stateChanged(null);
        return pSelectColor;
    }

    private JComboBox buildColorCombo() {
        Color TRANSPARENT = new Color(0, 0, 0, 0);

        Vector<Color> colors = new Vector<Color>();

        colors.add(new NamedColor(Color.RED, "Red"));
        colors.add(new NamedColor(Color.GREEN, "Green"));
        colors.add(new NamedColor(Color.BLUE, "Blue"));

        colors.add(new NamedColor(Color.YELLOW, "Yellow"));
        colors.add(new NamedColor(Color.MAGENTA, "Magenta"));
        colors.add(new NamedColor(Color.CYAN, "Cyan"));

        colors.add(new NamedColor(Color.WHITE, "White"));
        colors.add(new NamedColor(Color.LIGHT_GRAY, "Light Gray"));
        colors.add(new NamedColor(Color.GRAY, "Gray"));
        colors.add(new NamedColor(Color.DARK_GRAY, "Dark Gray"));
        colors.add(new NamedColor(Color.BLACK, "Black"));
        colors.add(new NamedColor(Color.PINK, "Pink"));
        colors.add(new NamedColor(Color.ORANGE, "Orange"));

        colors.add(new NamedColor(TRANSPARENT, "transparent"));
        //http://www.w3schools.com/css/css_colornames.asp
        colors.add(new NamedColor(new Color(0xf0f8ff), "aliceblue"));
        colors.add(new NamedColor(new Color(0xfaebd7), "antiquewhite"));
        colors.add(new NamedColor(new Color(0x00ffff), "aqua"));
        colors.add(new NamedColor(new Color(0x7fffd4), "aquamarine"));
        colors.add(new NamedColor(new Color(0xf0ffff), "azure"));
        colors.add(new NamedColor(new Color(0xf5f5dc), "beige"));
        colors.add(new NamedColor(new Color(0xffe4c4), "bisque"));
        colors.add(new NamedColor(new Color(0x000000), "black"));
        colors.add(new NamedColor(new Color(0xffebcd), "blanchedalmond"));
        colors.add(new NamedColor(new Color(0x0000ff), "blue"));
        colors.add(new NamedColor(new Color(0x8a2be2), "blueviolet"));
        colors.add(new NamedColor(new Color(0xa52a2a), "brown"));
        colors.add(new NamedColor(new Color(0xdeb887), "burlywood"));
        colors.add(new NamedColor(new Color(0x5f9ea0), "cadetblue"));
        colors.add(new NamedColor(new Color(0x7fff00), "chartreuse"));
        colors.add(new NamedColor(new Color(0xd2691e), "chocolate"));
        colors.add(new NamedColor(new Color(0xff7f50), "coral"));
        colors.add(new NamedColor(new Color(0x6495ed), "cornflowerblue"));
        colors.add(new NamedColor(new Color(0xfff8dc), "cornsilk"));
        colors.add(new NamedColor(new Color(0xdc143c), "crimson"));
        colors.add(new NamedColor(new Color(0x00ffff), "cyan"));
        colors.add(new NamedColor(new Color(0x00008b), "darkblue"));
        colors.add(new NamedColor(new Color(0x008b8b), "darkcyan"));
        colors.add(new NamedColor(new Color(0xb8860b), "darkgoldenrod"));
        colors.add(new NamedColor(new Color(0xa9a9a9), "darkgray"));
        colors.add(new NamedColor(new Color(0xa9a9a9), "darkgrey"));
        colors.add(new NamedColor(new Color(0x006400), "darkgreen"));
        colors.add(new NamedColor(new Color(0xbdb76b), "darkkhaki"));
        colors.add(new NamedColor(new Color(0x8b008b), "darkmagenta"));
        colors.add(new NamedColor(new Color(0x556b2f), "darkolivegreen"));
        colors.add(new NamedColor(new Color(0xff8c00), "darkorange"));
        colors.add(new NamedColor(new Color(0x9932cc), "darkorchid"));
        colors.add(new NamedColor(new Color(0x8b0000), "darkred"));
        colors.add(new NamedColor(new Color(0xe9967a), "darksalmon"));
        colors.add(new NamedColor(new Color(0x8fbc8f), "darkseagreen"));
        colors.add(new NamedColor(new Color(0x483d8b), "darkslateblue"));
        colors.add(new NamedColor(new Color(0x2f4f4f), "darkslategray"));
        colors.add(new NamedColor(new Color(0x2f4f4f), "darkslategrey"));
        colors.add(new NamedColor(new Color(0x00ced1), "darkturquoise"));
        colors.add(new NamedColor(new Color(0x9400d3), "darkviolet"));
        colors.add(new NamedColor(new Color(0xff1493), "deeppink"));
        colors.add(new NamedColor(new Color(0x00bfff), "deepskyblue"));
        colors.add(new NamedColor(new Color(0x696969), "dimgray"));
        colors.add(new NamedColor(new Color(0x696969), "dimgrey"));
        colors.add(new NamedColor(new Color(0x1e90ff), "dodgerblue"));
        colors.add(new NamedColor(new Color(0xb22222), "firebrick"));
        colors.add(new NamedColor(new Color(0xfffaf0), "floralwhite"));
        colors.add(new NamedColor(new Color(0x228b22), "forestgreen"));
        colors.add(new NamedColor(new Color(0xff00ff), "fuchsia"));
        colors.add(new NamedColor(new Color(0xdcdcdc), "gainsboro"));
        colors.add(new NamedColor(new Color(0xf8f8ff), "ghostwhite"));
        colors.add(new NamedColor(new Color(0xffd700), "gold"));
        colors.add(new NamedColor(new Color(0xdaa520), "goldenrod"));
        colors.add(new NamedColor(new Color(0x808080), "gray"));
        colors.add(new NamedColor(new Color(0x808080), "grey"));
        colors.add(new NamedColor(new Color(0x008000), "green"));
        colors.add(new NamedColor(new Color(0xadff2f), "greenyellow"));
        colors.add(new NamedColor(new Color(0xf0fff0), "honeydew"));
        colors.add(new NamedColor(new Color(0xff69b4), "hotpink"));
        colors.add(new NamedColor(new Color(0xcd5c5c), "indianred"));
        colors.add(new NamedColor(new Color(0x4b0082), "indigo"));
        colors.add(new NamedColor(new Color(0xfffff0), "ivory"));
        colors.add(new NamedColor(new Color(0xf0e68c), "khaki"));
        colors.add(new NamedColor(new Color(0xe6e6fa), "lavender"));
        colors.add(new NamedColor(new Color(0xfff0f5), "lavenderblush"));
        colors.add(new NamedColor(new Color(0x7cfc00), "lawngreen"));
        colors.add(new NamedColor(new Color(0xfffacd), "lemonchiffon"));
        colors.add(new NamedColor(new Color(0xadd8e6), "lightblue"));
        colors.add(new NamedColor(new Color(0xf08080), "lightcoral"));
        colors.add(new NamedColor(new Color(0xe0ffff), "lightcyan"));
        colors.add(new NamedColor(new Color(0xfafad2), "lightgoldenrodyellow"));
        colors.add(new NamedColor(new Color(0xd3d3d3), "lightgray"));
        colors.add(new NamedColor(new Color(0xd3d3d3), "lightgrey"));
        colors.add(new NamedColor(new Color(0x90ee90), "lightgreen"));
        colors.add(new NamedColor(new Color(0xffb6c1), "lightpink"));
        colors.add(new NamedColor(new Color(0xffa07a), "lightsalmon"));
        colors.add(new NamedColor(new Color(0x20b2aa), "lightseagreen"));
        colors.add(new NamedColor(new Color(0x87cefa), "lightskyblue"));
        colors.add(new NamedColor(new Color(0x778899), "lightslategray"));
        colors.add(new NamedColor(new Color(0x778899), "lightslategrey"));
        colors.add(new NamedColor(new Color(0xb0c4de), "lightsteelblue"));
        colors.add(new NamedColor(new Color(0xffffe0), "lightyellow"));
        colors.add(new NamedColor(new Color(0x00ff00), "lime"));
        colors.add(new NamedColor(new Color(0x32cd32), "limegreen"));
        colors.add(new NamedColor(new Color(0xfaf0e6), "linen"));
        colors.add(new NamedColor(new Color(0xff00ff), "magenta"));
        colors.add(new NamedColor(new Color(0x800000), "maroon"));
        colors.add(new NamedColor(new Color(0x66cdaa), "mediumaquamarine"));
        colors.add(new NamedColor(new Color(0x0000cd), "mediumblue"));
        colors.add(new NamedColor(new Color(0xba55d3), "mediumorchid"));
        colors.add(new NamedColor(new Color(0x9370d8), "mediumpurple"));
        colors.add(new NamedColor(new Color(0x3cb371), "mediumseagreen"));
        colors.add(new NamedColor(new Color(0x7b68ee), "mediumslateblue"));
        colors.add(new NamedColor(new Color(0x00fa9a), "mediumspringgreen"));
        colors.add(new NamedColor(new Color(0x48d1cc), "mediumturquoise"));
        colors.add(new NamedColor(new Color(0xc71585), "mediumvioletred"));
        colors.add(new NamedColor(new Color(0x191970), "midnightblue"));
        colors.add(new NamedColor(new Color(0xf5fffa), "mintcream"));
        colors.add(new NamedColor(new Color(0xffe4e1), "mistyrose"));
        colors.add(new NamedColor(new Color(0xffe4b5), "moccasin"));
        colors.add(new NamedColor(new Color(0xffdead), "navajowhite"));
        colors.add(new NamedColor(new Color(0x000080), "navy"));
        colors.add(new NamedColor(new Color(0xfdf5e6), "oldlace"));
        colors.add(new NamedColor(new Color(0x808000), "olive"));
        colors.add(new NamedColor(new Color(0x6b8e23), "olivedrab"));
        colors.add(new NamedColor(new Color(0xffa500), "orange"));
        colors.add(new NamedColor(new Color(0xff4500), "orangered"));
        colors.add(new NamedColor(new Color(0xda70d6), "orchid"));
        colors.add(new NamedColor(new Color(0xeee8aa), "palegoldenrod"));
        colors.add(new NamedColor(new Color(0x98fb98), "palegreen"));
        colors.add(new NamedColor(new Color(0xafeeee), "paleturquoise"));
        colors.add(new NamedColor(new Color(0xd87093), "palevioletred"));
        colors.add(new NamedColor(new Color(0xffefd5), "papayawhip"));
        colors.add(new NamedColor(new Color(0xffdab9), "peachpuff"));
        colors.add(new NamedColor(new Color(0xcd853f), "peru"));
        colors.add(new NamedColor(new Color(0xffc0cb), "pink"));
        colors.add(new NamedColor(new Color(0xdda0dd), "plum"));
        colors.add(new NamedColor(new Color(0xb0e0e6), "powderblue"));
        colors.add(new NamedColor(new Color(0x800080), "purple"));
        colors.add(new NamedColor(new Color(0xff0000), "red"));
        colors.add(new NamedColor(new Color(0xbc8f8f), "rosybrown"));
        colors.add(new NamedColor(new Color(0x4169e1), "royalblue"));
        colors.add(new NamedColor(new Color(0x8b4513), "saddlebrown"));
        colors.add(new NamedColor(new Color(0xfa8072), "salmon"));
        colors.add(new NamedColor(new Color(0xf4a460), "sandybrown"));
        colors.add(new NamedColor(new Color(0x2e8b57), "seagreen"));
        colors.add(new NamedColor(new Color(0xfff5ee), "seashell"));
        colors.add(new NamedColor(new Color(0xa0522d), "sienna"));
        colors.add(new NamedColor(new Color(0xc0c0c0), "silver"));
        colors.add(new NamedColor(new Color(0x87ceeb), "skyblue"));
        colors.add(new NamedColor(new Color(0x6a5acd), "slateblue"));
        colors.add(new NamedColor(new Color(0x708090), "slategray"));
        colors.add(new NamedColor(new Color(0x708090), "slategrey"));
        colors.add(new NamedColor(new Color(0xfffafa), "snow"));
        colors.add(new NamedColor(new Color(0x00ff7f), "springgreen"));
        colors.add(new NamedColor(new Color(0x4682b4), "steelblue"));
        colors.add(new NamedColor(new Color(0xd2b48c), "tan"));
        colors.add(new NamedColor(new Color(0x008080), "teal"));
        colors.add(new NamedColor(new Color(0xd8bfd8), "thistle"));
        colors.add(new NamedColor(new Color(0xff6347), "tomato"));
        colors.add(new NamedColor(new Color(0x40e0d0), "turquoise"));
        colors.add(new NamedColor(new Color(0xee82ee), "violet"));
        colors.add(new NamedColor(new Color(0xf5deb3), "wheat"));
        colors.add(new NamedColor(new Color(0xffffff), "white"));
        colors.add(new NamedColor(new Color(0xf5f5f5), "whitesmoke"));
        colors.add(new NamedColor(new Color(0xffff00), "yellow"));
        colors.add(new NamedColor(new Color(0x9acd32), "yellowgreen"));

        JComboBox comboBox = new JComboBox(new DefaultComboBoxModel(colors));
        comboBox.setRenderer(new DefaultListCellRenderer() {
            protected Color backgroundColor = Color.BLACK;

            {
                setBorder(new CompoundBorder(
                        new MatteBorder(2, 5, 2, 5, Color.white)
                        , new LineBorder(Color.black)));
            }

            public Component getListCellRendererComponent(JList list, Object obj,
                                                          int row, boolean sel, boolean hasFocus) {
                if (obj instanceof Color)
                    backgroundColor = (Color) obj;
                setText(obj.toString());
                return this;
            }

            public void paint(Graphics g) {
                setBackground(backgroundColor);
                super.paint(g);
            }
        });


        return comboBox;
    }

    class NamedColor extends Color {
        private String name;

        NamedColor(Color color, String name) {
            super(color.getRed(), color.getGreen(), color.getBlue());
            this.name = name;
        }

        @Override
        public String toString() {
            return name;
        }
    }

    private void calculateMixes() {
        calculateFirstMix();
        calculateSecondMix();
    }

    private void calculateFirstMix() {
        calculateMix(firstMixColors, firstMixColor);
    }

    private void calculateSecondMix() {
        calculateMix(secondMixColors, finalColor);
    }

    private void calculateMix(Vector<JLabel> mixColors, JLabel finalColor) {
        Color bg = ((Mixer) comboBox.getSelectedItem()).calculateMix(mixColors);
        setBackgroundToLabel(finalColor, bg);
    }

    private void setBackgroundToLabel(JLabel label, Color color) {
        label.setBackground(color);
        label.setText(color.getRed() + "," + color.getGreen() + "," + color.getBlue());
    }

    interface Mixer {
        Color calculateMix(Vector<JLabel> colores);
    }

    /**
     * Implement a additive mix of colors
     */
    static class AdditiveMixer implements Mixer {
        public Color calculateMix(Vector<JLabel> colores) {
            int red = 0;
            int green = 0;
            int blue = 0;
            for (int i = 0; i < colores.size(); i++) {
                Color background = colores.get(i).getBackground();
                red += background.getRed();
                green += background.getGreen();
                blue += background.getBlue();
            }
            return new Color(Math.min(255, red), Math.min(255, green), Math.min(255, blue));
        }

        @Override
        public String toString() {
            return "Additive";
        }
    }

    /**
     * Implement a sustractive mix of colors
     */
    static class SustractiveMixer implements Mixer {
        public Color calculateMix(Vector<JLabel> colores) {
            int red = 1;
            int green = 1;
            int blue = 1;
            for (int i = 0; i < colores.size(); i++) {
                Color background = colores.get(i).getBackground();
                red *= background.getRed();
                green *= background.getGreen();
                blue *= background.getBlue();
            }
            return new Color(Math.min(255, red / 255), Math.min(255, green / 255), Math.min(255, blue / 255));
        }

        @Override
        public String toString() {
            return "Sustractive";
        }
    }

    /**
     * Implement a diluting/sustractive mix of colors
     */
    static class DilutingSustractiveMixer implements Mixer {
        public Color calculateMix(Vector<JLabel> colores) {
            int red = 0;
            int green = 0;
            int blue = 0;
            for (int i = 0; i < colores.size(); i++) {
                Color background = colores.get(i).getBackground();
                red += Math.pow(255 - background.getRed(), 2);
                green += Math.pow(255 - background.getGreen(), 2);
                blue += Math.pow(255 - background.getBlue(), 2);
            }
            return new Color(Math.min(255, (int)Math.sqrt(red / colores.size())), Math.min(255, (int)Math.sqrt(green / colores.size())), Math.min(255, (int)Math.sqrt(blue / colores.size())));
        }

        @Override
        public String toString() {
            return "Diluting/Sustractive";
        }
    }

    /**
     * Implement a diluting/sustractive mix of colors
     */
    static class TertiaryMixer implements Mixer {
        public Color calculateMix(Vector<JLabel> colores) {
            Color background1 = colores.get(0).getBackground();
            int red = background1.getRed();
            int green = background1.getGreen();
            int blue = background1.getBlue();
            Color background2 = colores.get(1).getBackground();
            red -= background2.getRed();
            green -= background2.getGreen();
            blue -= background2.getBlue();
            return new Color(Math.min(255, background1.getRed() - (red/2)), Math.min(255, background1.getGreen() - (green/2)), background1.getBlue() - (blue/2));
        }

        @Override
        public String toString() {
            return "Tertiary";
        }
    }

    private JSlider buildSlider(JPanel container, GridBagConstraints upperCC) {
        JPanel panel = new JPanel(new GridBagLayout());
        GridBagConstraints cc = new GridBagConstraints();
        cc.fill = GridBagConstraints.BOTH;
        cc.insets = new Insets(5, 5, 5, 5);
        cc.weightx = 1;
        cc.weighty = 0.7;

        final JSlider slider = new JSlider(JSlider.VERTICAL, 0, 255, 0);
        slider.setFont(new Font("Serif", Font.PLAIN, 4));

        Hashtable<Integer, JLabel> labels = new Hashtable<Integer, JLabel>();
        labels.put(0, new JLabel("0"));
        labels.put(128, new JLabel("128"));
        labels.put(255, new JLabel("255"));
        panel.add(slider, cc);
        final JTextField field = new JTextField();
        field.setEditable(false);
        slider.addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                field.setText(String.valueOf(slider.getValue()));
            }
        });
        cc.gridx = 0;
        cc.gridy = 1;
        cc.weighty = 0;

        panel.add(field, cc);
        slider.setLabelTable(labels);
        slider.setPaintLabels(true);

        container.add(panel, upperCC);

        return slider;
    }
}

Color Mixing Interface

Basically, This operations are like logic AND and logic OR. (Well not exactly)

  • On additive alghoritm, the colors overlap, but for every primary it can only be 255 (Logic OR operation)
  • On Substractive alghoritm, if some primary is not present, the result will not have that (CYAN has no RED, YELLOW has no BLUE: you get GREEN) (Logic AND Operation)

Based on this you can obtain tertiary colors from mix of primary and secundary colors, so I make a tiny implementation which works pretty well:

NewColor.R = Color1.R - (Color1.R - Color2.R)/2
NewColor.G = Color1.G - (Color1.G - Color2.G)/2
NewColor.B = Color1.B - (Color1.B - Color2.B)/2

There is also an diluting/substractive alghoritm mentioned on second cited response, but I cannot say what It is based on, just added to test.

4
votes

The REAL answer is to change RGB color vector space into one that's additive, then change it back to RGB. In this new vector space, when you add two light vectors it takes into account the additive properties of light and our perception of color to produce an additive color.

It turns out the CIE XYZ vector space works well for that purpose.

The XYZ vectors are additive in this space, and mix as light sources do.

See this paper on color mixing by Cree:

X_mix = X1 + X2 + ...

Y_mix = Y1 + Y2 + ...

Z_mix = Z1 + Z2 + ...

You can then change basis back to RGB. There are many libraries available for changing color between vector spaces and CIEXYZ is standardized and widely supported.

This method produces realistic results that suit most of my purposes.

More information about CIE 1931 color space.

3
votes

Looking at Cody Gray's answer, I think I can suggest how to combine colors. Converting color wheel to RGB:

                cyan(0, 255, 255)
        blue(0, 0, 255) green(0, 255, 0)
magenta(255, 0, 255) red(255, 0, 0) yellow(255, 255, 0)

Without extra complications, colors can be combined like this: invert both colors, add them together and invert the result (ActionScript):

sum(0, 255, 255,   255, 0, 255, "cyan + magenta =");
sum(255, 0, 0,     0, 255, 0,   "red + green =");
sum(0, 0, 0,       0, 0, 0,     "black + black =");
sum(0, 0, 0,       255, 255, 255, "black + white =");

function sum(c1:int, c2:int, c3:int, b1:int, b2:int, b3:int, m:String):void {
    c1 = 255 - c1; c2 = 255 - c2; c3 = 255 - c3;
    b1 = 255 - b1; b2 = 255 - b2; b3 = 255 - b3;
    var d1:int = c1 + b1;
    var d2:int = c2 + b2;
    var d3:int = c3 + b3;
    d1 = 255 - d1; d2 = 255 - d2; d3 = 255 - d3;
    d1 = clamp(d1); d2 = clamp(d2); d3 = clamp(d3);
    trace(m, d1, d2, d3);
}

function clamp(value:int):int {
    if (value < 0) return 0;
    if (value > 255) return 255;
    return value;
}

Output:

cyan + magenta = 0 0 255
red + green = 0 0 0
black + black = 0 0 0
black + white = 0 0 0

See if that fit for you.

Edit: I'm not pretending this is physically correct, it's just an attempt to approximate. The idea of lookup table sounds crazy for me for two reasons: it depends on two arguments, so its size will be very large; and laws of nature are usually continuous with no or rare corner cases. If you can fill lookup table, you should know the algorithm - so just write a function for it.

3
votes

I just wanted to point out why you are getting grey when you add Blue. It's because you are adding Blue, and not Cyan:

enter image description here

  • If you add Cyan to Yellow, you get Green
  • If you add Blue (i.e. cyan+magents) to Yellow, you get Grey.

Or, more mathematically:

Yellow + (Cyan          ) = Green  
Yellow + (Cyan + Magenta) = Gray
Yellow + (Blue)           = Gray

You are adding Blue, when you meant to be adding Cyan.

0.5*Yellow(255,255,0) + 0.5*Cyan(0,255,255) = VeryLightLimeGreen(128,255,128)
2
votes

Here is a Java implementation of the Kubelka-Munk Theory of Reflectance for mixing RGB colors. This implementation uses a simplified version of the Kubelka-Munk model that assumes all colors have the same concentration when blending and that all colors are opaque.

https://github.com/benjholla/ColorMixer

2
votes

I needed to do the same for one of my libraries. I applied the suggested approach of this paper, and the result is pretty acceptable.

The process is pretty straightforward. It requires converting the colors to the RYB color space, makes the mix, and converting back the result to RGB. They propose a conversion equation in agreement with the next table:

RGB Color RGB values RYB values
Black R: 0 | G: 0 | B: 0 R: 255 | Y: 255 | B: 255
Red R: 255 | G: 0 | B: 0 R: 255 | Y: 0 | B: 0
Green R: 0 | G: 255 | B: 0 R: 0 | Y: 255 | B: 255
Blue R: 0 | G: 0 | B: 255 R: 0 | Y: 0 | B: 255
Yellow R: 255 | G: 255 | B: 0 R: 0 | Y: 255 | B: 0
Magenta R: 255 | G: 0 | B: 255 R: 255 | Y: 0 | B: 127.5
Cyan R: 0 | G: 255 | B: 255 R: 0 | Y: 127.5 | B: 255
White R: 255 | G: 255 | B: 255 R: 0 | Y: 0 | B: 0

Here are the proposed formulas (take the TypeScript code as pseudocode):

type RYBObject = {
    r: number;
    y: number;
    b: number;
};

export const rgbToRYB = (r: number, g: number, b: number): RYBObject => {
    const Iw = Math.min(r, g, b);
    const Ib = Math.min(255 - r, 255 - g, 255 - b);
    const rRGB = r - Iw;
    const gRGB = g - Iw;
    const bRGB = b - Iw;
    const minRG = Math.min(rRGB, gRGB);
    const rRYB = rRGB - minRG;
    const yRYB = (gRGB + minRG) / 2;
    const bRYB = (bRGB + gRGB - minRG) / 2;
    const n = Math.max(rRYB, yRYB, bRYB, 1) / Math.max(rRGB, gRGB, bRGB, 1);
    return {
        r: rRYB / n + Ib,
        y: yRYB / n + Ib,
        b: bRYB / n + Ib
    };
};

type RGBObject = {
    r: number;
    g: number;
    b: number;
};

export const rybToRGB = (r: number, y: number, b: number): RGBObject => {
    const Iw = Math.min(r, y, b);
    const Ib = Math.min(255 - r, 255 - y, 255 - b);
    const rRYB = r - Iw;
    const yRYB = y - Iw;
    const bRYB = b - Iw;
    const minYB = Math.min(yRYB, bRYB);
    const rRGB = rRYB + yRYB - minYB;
    const gRGB = yRYB + 2 * minYB;
    const bRGB = 2 * (bRYB - minYB);
    const n = Math.max(rRGB, gRGB, bRGB, 1) / Math.max(rRYB, yRYB, bRYB, 1);
    return {
        r: rRGB / n + Ib,
        g: gRGB / n + Ib,
        b: bRGB / n + Ib
    };
};

And you can use this method to mix the colors:

const mixColors = (c1: RYBObject, c2: RYBObject): RYBObject => ({
    r: Math.min(255, c1.r + c2.r),
    y: Math.min(255, c1.y + c2.y),
    b: Math.min(255, c1.b + c2.b)
});

// color1 and color2 are the input colors in the shape of RGBObject
const mixedColor = rybToRGB(
    mixColors(
        rgbToRYB(color1),
        rgbToRYB(color2)
    )
);

Here you can see the result using the library that I built which uses the above methods to achieve the mix:

const mixes = [
  '#FF0000',
  '#FFFF00',
  '#0000FF',
  [1, 3],
  [1, 2],
  [2, 3],
  [1, 2, 3]
];

const paths = document.querySelectorAll('#planes path');

paths.forEach((path, index) => {
    let color = '#CCCCCC';
    if (mixes[index]) {
      if (typeof mixes[index] === 'string') {
        color = mixes[index];
      } else {
        const colors = mixes[index].map((i) => mixes[i - 1]);
        color = mixes[index] = ColorTranslator.getMixHEX(colors, Mix.SUBTRACTIVE);           
      }
    }  
    path.setAttribute('fill', color);
});
body, html {
  height: 100%;
}

svg {
  height: 100%;
  width: 100%;
}
<script src="https://unpkg.com/[email protected]/dist/colortranslator.web.js"></script>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 500 500">
    <g id="planes">
        <path d="M250,210.9929A144.2657,144.2657,0,0,1,392.9611,192.67a144.125,144.125,0,1,0-287.0788-18.3437,145.5121,145.5121,0,0,0,1.1566,18.3437A144.2657,144.2657,0,0,1,250,210.9929Z"/>
        <path d="M409.4255,200.84a145.5235,145.5235,0,0,0-16.4644-8.17,144.2654,144.2654,0,0,1-87.3487,114.6464A144.2658,144.2658,0,0,1,250,440.2858,144.125,144.125,0,1,0,409.4255,200.84Z"/>
        <path d="M194.3876,307.3163A144.2654,144.2654,0,0,1,107.0389,192.67,144.1177,144.1177,0,1,0,250,440.2858,144.2658,144.2658,0,0,1,194.3876,307.3163Z"/>
        <path d="M194.3876,307.3163A144.5321,144.5321,0,0,1,250,210.9929,144.2657,144.2657,0,0,0,107.0389,192.67,144.2654,144.2654,0,0,0,194.3876,307.3163Z"/>
        <path d="M250,210.9929a144.5321,144.5321,0,0,1,55.6124,96.3234A144.2654,144.2654,0,0,0,392.9611,192.67,144.2657,144.2657,0,0,0,250,210.9929Z"/>
        <path d="M305.6124,307.3163a144.532,144.532,0,0,1-111.2248,0A144.2658,144.2658,0,0,0,250,440.2858,144.2658,144.2658,0,0,0,305.6124,307.3163Z"/>
        <path d="M250,210.9929a144.5321,144.5321,0,0,0-55.6124,96.3234,144.532,144.532,0,0,0,111.2248,0A144.5321,144.5321,0,0,0,250,210.9929Z"/>
    </g>
    <g id="letters">
        <path d="M249.5908,115.1084h-.04l-2.2608,1.22-.34-1.34,2.8408-1.52h1.5v13.0029h-1.7Z"/>
        <path d="M254.2119,265.5859v1.16l-5.6611,11.8428h-1.82l5.6406-11.503v-.04H246.01v-1.46Z"/>
        <path d="M377.5254,347.1016v-1.08l1.38-1.34c3.32-3.1611,4.8213-4.8418,4.8408-6.8017a2.32,2.32,0,0,0-2.58-2.541,4.427,4.427,0,0,0-2.7607,1.1006l-.5606-1.24a5.7088,5.7088,0,0,1,3.6807-1.32,3.6594,3.6594,0,0,1,3.9814,3.7813c0,2.4-1.74,4.3408-4.4814,6.9814l-1.04.96v.04h5.8417v1.46Z"/>
        <path d="M109.436,345.08a5.88,5.88,0,0,0,2.88.82c2.2607,0,2.9609-1.44,2.9409-2.5195-.02-1.82-1.6606-2.6016-3.3608-2.6016h-.98v-1.32h.98c1.28,0,2.9009-.66,2.9009-2.2,0-1.04-.66-1.96-2.2808-1.96a4.7821,4.7821,0,0,0-2.6006.8594l-.46-1.2793a6.1819,6.1819,0,0,1,3.4008-1c2.5606,0,3.7207,1.5195,3.7207,3.1a3.2286,3.2286,0,0,1-2.4,3.0606v.041a3.3856,3.3856,0,0,1,2.9,3.34c0,2.0811-1.62,3.9014-4.7412,3.9014a6.6111,6.6111,0,0,1-3.38-.88Z"/>
        <path d="M169.7305,235.2827v-3.5405H163.69v-1.1607L169.49,222.28h1.9v8.082h1.8208v1.38h-1.8208v3.5405Zm0-4.9209v-4.3413q0-1.02.06-2.04h-.06c-.4.76-.72,1.32-1.08,1.92l-3.1807,4.4209v.04Z"/>
        <path d="M325.0127,223.76h-4.9609l-.5,3.3408a6.9313,6.9313,0,0,1,1.06-.08,5.4945,5.4945,0,0,1,2.8008.7,3.7056,3.7056,0,0,1,1.86,3.3408,4.511,4.511,0,0,1-4.8409,4.4409,6.3659,6.3659,0,0,1-3.24-.8l.44-1.34a5.8208,5.8208,0,0,0,2.7812.72,2.8686,2.8686,0,0,0,3.08-2.8208c-.02-1.68-1.14-2.88-3.74-2.88a13.4421,13.4421,0,0,0-1.8008.14l.84-6.2417h6.2217Z"/>
        <path d="M253.1924,362.9092a6.3162,6.3162,0,0,0-1.32.08,5.2845,5.2845,0,0,0-4.5215,4.6211h.06a3.9634,3.9634,0,0,1,7.0621,2.7,4.3479,4.3479,0,0,1-4.3213,4.5811c-2.7808,0-4.6011-2.16-4.6011-5.541a8.1752,8.1752,0,0,1,2.2-5.8613,7.08,7.08,0,0,1,4.1616-1.9209,8.557,8.557,0,0,1,1.28-.1Zm-.5,7.5019c0-1.86-1.0606-2.9814-2.6812-2.9814a2.8967,2.8967,0,0,0-2.52,1.6006,1.4976,1.4976,0,0,0-.2.78c.04,2.1407,1.02,3.7207,2.8609,3.7207C251.6719,373.5312,252.6924,372.2715,252.6924,370.4111Z"/>
     </g>
 </svg>

And in this CodePen you can check the differences between mixing the colors in additive mode vs subtractive mode.

1
votes

I don't think the answers above give adequate mixing results.

I've been working on this problem with both RGB and RYB (after converting from RGB). The conversion for RGB to RYB here is good: http://www.insanit.net/tag/rgb-to-ryb/ (I will share my code upon request).

If mixing as light, it's not too bad (see below). If you want to mix as physical materials such as paint it's a little trickier - I'm working on an app to explore it.

Back to the original question - here's my code for RGB mixing. RgbColor is a custom class but I think this makes sense on its own:

-(RgbColor*)mixWith:(RgbColor *)aColor {
    int r1, g1, b1, r2, g2, b2, r3, g3, b3, m1, m2, w1, w2, w3; //colors and maxes, white
    float br; // brightness of resulting color

r1 = self.redVal;
g1 = self.greenVal;
b1 = self.blueVal;
r2 = aColor.redVal;
g2 = aColor.greenVal;
b2 = aColor.blueVal;

w1 = MIN(r1, MIN(g1, b1));
w2 = MIN(r2, MIN(g2, b2));

// remove white before mixing
r1 -= w1;
g1 -= w1;
b1 -= w1;
r2 -= w2;
g2 -= w2;
b2 -= w2;

m1 = MAX(r1, MAX(g1, b1));
m2 = MAX(r2, MAX(g2, b2));

br = (m1+m2)/(2*255.0);

r3 = (r1+r2)*br;
g3 = (g1+g2)*br;
b3 = (b1+b2)*br;

// average whiteness and add into final color
w3 = (w1+w2)/2;

r3 += w3;
g3 += w3;
b3 += w3;

[self setRedVal:[[NSNumber numberWithFloat:r3] intValue]];
[self setGreenVal:[[NSNumber numberWithFloat:g3] intValue]];
[self setBlueVal:[[NSNumber numberWithFloat:b3] intValue]];
return self;
}
0
votes

Mixing yellow (= red+green) and blue does give white color as per physics, see http://en.wikipedia.org/wiki/Additive_color .

0
votes

The "Yellow" in RGB model is not the same as the Yellow in RYB model which when mixed with Blue is supposed to give Green.

As an example: (255, 255, 0) is (approximately) twice as "intense" as (0, 0, 255) in RGB model, while in RYB model, EQUAL amounts of Yellow and Blue are supposed to give Green. Similarly, Red and Blue in the two models are different.

Think of them as vector spaces RGB and R'Y'B'.

If some kind of relation like:

R = i1*R' + j1*Y' + k1*B';
G = i2*R' + j2*Y' + k2*B';
B = i3*R' + j3*Y' + k3*B';

holds, you can do your algebra by first converting the individual colors (operands) from RGB to R'Y'B' space.

There are 9 unknowns (i,j,k variables), so you need 9 equations (3 color equalities in these 2 spaces).

Unfortunately, I think that the models don't scale linearly, and hence for precision you might have to use lookup tables.

Another good idea may be to convert to HSV or YCbCr space, because the color information is more clearly abstracted out in those space. (If an RGB to RYB conversion does exist, an RGB->YCbCr->RYB route might be easier to find).

0
votes

Having the same issue with mixing colors in either a dark or ligth theme in my app, I was looking for a quick and easy solution.

In dark theme, I simply or'd the RGB colors and the result was pretty ok, however in light theme this resulted in very light colors becoming invisible.

I simply tried and'd colors and that worked pretty well. blue + yellow gave me green, magenta + cyan gave me blue at least.

So, in dark theme (back background) I do:

mixed_color = color1 | color2; // Dark theme, like RGB mixing

In light theme:

mixed_color = color1 & color2; // Light theme, like CMY mixing

There's unlikely any realism in the mix, but for my need (far away from photography software) that was highly satisfactory.

0
votes

A lot has already been said on the matter, but I just wanted to share a way of mixing colors (considering working with light: addition scheme).

I tried the example code from Damien Del Russo which seemed a nice approach in my own taste using white average to calculate rgb mix. I wanted to compare the results against my own (basic & linear) code.

Former code unfortunately can return values which exceeds 255 in some cases... Yet in these cases, the result is the same for me if a ratio is applied to get back to 0-255 range.

For other cases (mostly with a white component), I can see some differences in the results. The difference is more about luminosity than chromaticity. If I try to scale down the values I obtain, I get very close to the former code results...

I'm not sure yet which way of calculating it gives the closer results in terms of luminosity, but if I get an answer, I'll update this post to share the results.

Knowing that, here's my unfinished code (in python, as it's my principles test bench) to mix n tuples of rgb colors:

def rgb_mix_colors(rgb_scale, *colors):
    """ color mix
    :param rgb_scale: scale of values
    :param colors: list of colors (tuple of rgb values)
    :return: relative mix of rgb colors """
    r = g = b = 0

    for item in colors:
        try:
            if not isinstance(item, tuple):
                raise TypeError
            if item[0] > rgb_scale or item[1] > rgb_scale or item[2] > rgb_scale:
                raise ValueError
        except (TypeError, ValueError):
            print "WARNING: Value is outside range or unhandled parameter given as function argument!"
        else:
            r += item[0]    # add red value from item
            g += item[1]    # add green value from item
            b += item[2]    # add blue value from item

    ratio = max(r, g, b)
    if ratio > rgb_scale:
        ratio = float(rgb_scale) / ratio
        r *= ratio
        g *= ratio
        b *= ratio

    return int(r), int(g), int(b)


if __name__ == "__main__":
    col_list = [(512, 10, 256),
                (30, 120, 50),
                (50, 40, 512),
                "exception",        # should raise TypeError when mixing
                (3800, 20, 50),     # should raise ValueError when mixing
                (512, 10, 512)]

    # example with a scale defined at 1024 instead of default, providing list of tuples as params already packed as list
    print "2 warning messages should be displayed on the next line:"
    print rgb_mix_colors(1024, *col_list)
    print rgb_mix_colors(255, (0, 255, 0), (0, 32, 255))

0
votes

The biggest issue in this case is your expectations based on misleading assumptions.

The statement that mixing yellow and blue produces green is a ginormous simplification. Because it depends on what yellow, what blue and in what proportion. Also, mixing light acts quite opposite as mixing paint.

When you imagine a color wheel, mixing (1:1 ratio) any two colors, which lie directly opposite each other, always creates white black or gray no matter what model you use. And in fact, it corresponds to reality.

Try to convert your colors to HSV model (Hue - 0° to 360° on color wheel, Saturation, Value) This model is much more closer to how humans percieve colors and it also allows you make those unrealistic result you expect to happen.

Take some time to familiarize yourself with the model and you will find, for example, that in order to get that green you want (R:0 G:255 B:0 → H:120° S:100% V:100%), you need to:
mix yellow (R:255 G:255 B:0 → H:60° S:100% V:100%)
and cyan (R:0 G:255 B:255 → H:180° S:100% V:100%)

It's almost just an average of the values, but beware, there are three different situations:

1: Larger H - Smaller H = 180°
In this case colors lie against each other - Hue is undefined

2: Larger H - Smaller H < 180°
(Larger H + Smaller H)/2 = Result H
e.g. Your case: (60°+180°)/2 = 120°

3: Larger H - Smaller H > 180°
(Larger H + Smaller H + 360°)/2 - 360°
e.g.: (330° + 30° + 360°)/2 - 360° = 0