diff --git a/platform/editor-ui-api/src/com/intellij/ide/ui/ColorBlindnessMatrix.java b/platform/editor-ui-api/src/com/intellij/ide/ui/ColorBlindnessMatrix.java new file mode 100644 index 000000000000..8003cce98c18 --- /dev/null +++ b/platform/editor-ui-api/src/com/intellij/ide/ui/ColorBlindnessMatrix.java @@ -0,0 +1,109 @@ +package com.intellij.ide.ui; + +import com.intellij.util.Matrix; +import com.intellij.util.Vector; + +/** + * @author Sergey.Malenkov + */ +final class ColorBlindnessMatrix { + private static final Matrix CORRECTION = Matrix.createIdentity(3); + + private static final Matrix RGB_LMS; // a matrix to convert a RGB color to the LMS space + private static final Matrix LMS_RGB; // a matrix to convert a LMS color to the RGB space + + private static final Vector WHITE_BLUE; + private static final Vector WHITE_RED; + + static { + // a matrix to convert a RGB color to the XYZ space + Matrix RGB_XYZ = Matrix.create(3, + 0.4124, 0.2126, 0.0193, + 0.3576, 0.7152, 0.1192, + 0.1805, 0.0722, 0.9505); + + // a matrix to convert an XYZ color to the LMS space + Matrix XYZ_LMS = Matrix.create(3, + 0.7328, -0.7036, 0.0030, + 0.4296, 1.6975, 0.0136, + -0.1624, 0.0061, 0.9834); + + // create direct conversion from RGB to LMS and vice versa + RGB_LMS = RGB_XYZ.multiply(XYZ_LMS); + LMS_RGB = RGB_LMS.inverse(); + + // To simulate color blindness we remove the data lost by the absence of a cone. + // This cannot be done by just zeroing out the corresponding LMS component, + // because it would create a color outside of the RGB gammut. Instead, + // we project the color along the axis of the missing component + // onto a plane within the RGB gammut: + // - since the projection happens along the axis of the missing component, + // a color blind viewer perceives the projected color the same. + // - We use the plane defined by 3 points in LMS space: black, white and + // blue and red for protanopia/deuteranopia and tritanopia respectively. + Vector red = RGB_LMS.getRow(0); // LMS space red + Vector blue = RGB_LMS.getRow(2); // LMS space blue + Vector white = Vector.create(1, 1, 1).multiply(RGB_LMS); // LMS space white + + // To find the planes we solve the a*L + b*M + c*S = 0 equation + // for the LMS values of the three known points. This equation is trivially solved, + // and has for solution the following cross-products: + WHITE_BLUE = cross(white, blue); // protanopia/deuteranopia + WHITE_RED = cross(white, red); // tritanopia + } + + private static Vector cross(Vector left, Vector right) { + return Vector.create( + left.get(1) * right.get(2) - left.get(2) * right.get(1), + left.get(2) * right.get(0) - left.get(0) * right.get(2), + left.get(0) * right.get(1) - left.get(1) * right.get(0)); + } + + private static Matrix calculate(Matrix simulation, Matrix correction) { + // We will calculate the error between the color and the color + // viewed by a color blind user and "spread" this error onto the healthy cones. + // The correction matrix perform this last step and have been chosen arbitrarily. + Matrix matrix = simulation.multiply(RGB_LMS); + if (correction == null) correction = CORRECTION; + return LMS_RGB.multiply(matrix.plus(correction.multiply(RGB_LMS.minus(matrix)))); + } + + static final class Protanopia { + private static final double V1 = -WHITE_BLUE.get(1) / WHITE_BLUE.get(0); + private static final double V2 = -WHITE_BLUE.get(2) / WHITE_BLUE.get(0); + private static final Matrix SIMULATION = Matrix.create(3, 0, 0, 0, V1, 1, 0, V2, 0, 1); + private static final Matrix CORRECTION = Matrix.create(3, 1, .7, .7, 0, 1, 0, 0, 0, 1); + + static final Matrix MATRIX = calculate(CORRECTION); + + static Matrix calculate(Matrix correction) { + return ColorBlindnessMatrix.calculate(SIMULATION, correction); + } + } + + static final class Deuteranopia { + private static final double V1 = -WHITE_BLUE.get(0) / WHITE_BLUE.get(1); + private static final double V2 = -WHITE_BLUE.get(2) / WHITE_BLUE.get(1); + private static final Matrix SIMULATION = Matrix.create(3, 1, V1, 0, 0, 0, 0, 0, V2, 1); + private static final Matrix CORRECTION = Matrix.create(3, 1, 0, 0, .7, 1, .7, 0, 0, 1); + + static final Matrix MATRIX = calculate(CORRECTION); + + static Matrix calculate(Matrix correction) { + return ColorBlindnessMatrix.calculate(SIMULATION, correction); + } + } + + static final class Tritanopia { + private static final double V1 = -WHITE_RED.get(0) / WHITE_RED.get(2); + private static final double V2 = -WHITE_RED.get(1) / WHITE_RED.get(2); + private static final Matrix SIMULATION = Matrix.create(3, 1, 0, V1, 0, 1, V2, 0, 0, 0); + private static final Matrix CORRECTION = Matrix.create(3, 1, 0, 0, 0, 1, 0, .7, .7, 1); + + static final Matrix MATRIX = calculate(CORRECTION); + + static Matrix calculate(Matrix correction) { + return ColorBlindnessMatrix.calculate(SIMULATION, correction); + } + } +} diff --git a/platform/editor-ui-api/src/com/intellij/ide/ui/ColorConverter.java b/platform/editor-ui-api/src/com/intellij/ide/ui/ColorConverter.java new file mode 100644 index 000000000000..3d0270b4c369 --- /dev/null +++ b/platform/editor-ui-api/src/com/intellij/ide/ui/ColorConverter.java @@ -0,0 +1,18 @@ +package com.intellij.ide.ui; + +/** + * @author Sergey.Malenkov + */ +class ColorConverter { + public int convert(int red, int green, int blue, int alpha) { + return (fix(red) << 16) | (fix(green) << 8) | fix(blue) | (fix(alpha) << 24); + } + + public int convert(int argb) { + return convert(0xFF & (argb >> 16), 0xFF & (argb >> 8), 0xFF & argb, 0xFF & (argb >> 24)); + } + + private static int fix(int value) { + return value < 0 ? 0 : value > 255 ? 255 : value; + } +} diff --git a/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixConverter.java b/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixConverter.java new file mode 100644 index 000000000000..86dea2bb0f89 --- /dev/null +++ b/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixConverter.java @@ -0,0 +1,51 @@ +package com.intellij.ide.ui; + +import com.intellij.util.Matrix; +import com.intellij.util.Vector; + +/** + * @author Sergey.Malenkov + */ +final class MatrixConverter extends ColorConverter { + private final Double myWeight; + private final Matrix myMatrix; + + public MatrixConverter(Matrix matrix) { + this(null, matrix); + } + + public MatrixConverter(Double weight, Matrix matrix) { + if (weight != null && !(0 < weight && weight < 1)) throw new IllegalArgumentException("unsupported weight"); + int rows = matrix.getRows(); + if (rows != 3 && rows != 4) throw new IllegalArgumentException("unsupported rows"); + int columns = matrix.getColumns(); + if (columns != 3 && columns != 4) throw new IllegalArgumentException("unsupported columns"); + myWeight = weight; + myMatrix = matrix; + } + + @Override + public int convert(int red, int green, int blue, int alpha) { + Vector vector = myMatrix.getRows() == 4 + ? Vector.create(red, green, blue, alpha) + : Vector.create(red, green, blue); + + Vector result = vector.multiply(myMatrix); + if (myWeight != null) { + if (vector.getSize() != result.getSize()) { + vector = result.getSize() == 4 + ? Vector.create(red, green, blue, alpha) + : Vector.create(red, green, blue); + } + vector = vector.multiply(1 - myWeight); + result = result.multiply(myWeight).plus(vector); + } + red = (int)result.get(0); + green = (int)result.get(1); + blue = (int)result.get(2); + if (result.getSize() == 4) { + alpha = (int)result.get(3); + } + return super.convert(red, green, blue, alpha); + } +} diff --git a/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixFilter.java b/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixFilter.java index 13aea6ee9393..81ef46efb0ac 100644 --- a/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixFilter.java +++ b/platform/editor-ui-api/src/com/intellij/ide/ui/MatrixFilter.java @@ -15,86 +15,10 @@ */ package com.intellij.ide.ui; -import com.intellij.util.Matrix; -import com.intellij.util.Vector; - import java.awt.image.ImageFilter; +import java.awt.image.RGBImageFilter; -final class MatrixFilter extends WeightFilter { - private static final Matrix RGB_LMS; // a matrix to convert a RGB color to the LMS space - private static final Matrix LMS_RGB; // a matrix to convert a LMS color to the RGB space - - private static final Matrix PROTANOPIA_SIMULATION; - private static final Matrix PROTANOPIA_CORRECTION; - - private static final Matrix DEUTERANOPIA_SIMULATION; - private static final Matrix DEUTERANOPIA_CORRECTION; - - private static final Matrix TRITANOPIA_SIMULATION; - private static final Matrix TRITANOPIA_CORRECTION; - - static { - Matrix RGB_XYZ = Matrix.create(3, // a matrix to convert a RGB color to the XYZ space - 0.4124, 0.2126, 0.0193, - 0.3576, 0.7152, 0.1192, - 0.1805, 0.0722, 0.9505); - Matrix XYZ_LMS = Matrix.create(3, // a matrix to convert an XYZ color to the LMS space - 0.7328, -0.7036, 0.0030, - 0.4296, 1.6975, 0.0136, - -0.1624, 0.0061, 0.9834); - - // create direct conversion from RGB to LMS and vice versa - - RGB_LMS = RGB_XYZ.multiply(XYZ_LMS); - LMS_RGB = RGB_LMS.inverse(); - - // To simulate color blindness we remove the data lost by the absence of a cone. - // This cannot be done by just zeroing out the corresponding LMS component, - // because it would create a color outside of the RGB gammut. Instead, - // we project the color along the axis of the missing component - // onto a plane within the RGB gammut: - // - since the projection happens along the axis of the missing component, - // a color blind viewer perceives the projected color the same. - // - We use the plane defined by 3 points in LMS space: black, white and - // blue and red for protanopia/deuteranopia and tritanopia respectively. - - Vector red = RGB_LMS.getRow(0); // LMS space red - Vector blue = RGB_LMS.getRow(2); // LMS space blue - Vector white = Vector.create(1, 1, 1).multiply(RGB_LMS); // LMS space white - - // To find the planes we solve the a*L + b*M + c*S = 0 equation - // for the LMS values of the three known points. This equation is trivially solved, - // and has for solution the following cross-products: - - Vector p0 = cross(white, blue); // protanopia/deuteranopia - Vector p1 = cross(white, red); // tritanopia - - // The following 3 matrices perform the projection of a LMS color - // onto the given plane along the selected axis - - PROTANOPIA_SIMULATION = Matrix.create(3, - 0, 0, 0, - -p0.get(1) / p0.get(0), 1, 0, - -p0.get(2) / p0.get(0), 0, 1); - DEUTERANOPIA_SIMULATION = Matrix.create(3, - 1, -p0.get(0) / p0.get(1), 0, - 0, 0, 0, - 0, -p0.get(2) / p0.get(1), 1); - TRITANOPIA_SIMULATION = Matrix.create(3, - 1, 0, -p1.get(0) / p1.get(2), - 0, 1, -p1.get(1) / p1.get(2), - 0, 0, 0); - - // We will calculate the error between the color and the color - // viewed by a color blind user and "spread" this error onto the healthy cones. - // The matrices below perform this last step and have been chosen arbitrarily. - // The amount of correction can be adjusted here. - - PROTANOPIA_CORRECTION = Matrix.create(3, 1, .7, .7, 0, 1, 0, 0, 0, 1); - DEUTERANOPIA_CORRECTION = Matrix.create(3, 1, 0, 0, .7, 1, .7, 0, 0, 1); - TRITANOPIA_CORRECTION = Matrix.create(3, 1, 0, 0, 0, 1, 0, .7, .7, 1); - } - +final class MatrixFilter extends RGBImageFilter { public static ImageFilter get(ColorBlindness blindness) { if (blindness == ColorBlindness.protanopia) return protanopia; if (blindness == ColorBlindness.deuteranopia) return deuteranopia; @@ -102,45 +26,37 @@ final class MatrixFilter extends WeightFilter { return null; } - public static final ImageFilter protanopia = forProtanopia(null, true); - public static final ImageFilter deuteranopia = forDeuteranopia(null, true); - public static final ImageFilter tritanopia = forTritanopia(null, true); + public static final ImageFilter protanopia = forProtanopia(null); + public static final ImageFilter deuteranopia = forDeuteranopia(null); + public static final ImageFilter tritanopia = forTritanopia(null); - public static ImageFilter forProtanopia(Double weight, boolean fix) { - return new MatrixFilter("Protanopia", weight, PROTANOPIA_SIMULATION, fix ? PROTANOPIA_CORRECTION : null); + public static ImageFilter forProtanopia(Double weight) { + return new MatrixFilter("Protanopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Protanopia.MATRIX)); } - public static ImageFilter forDeuteranopia(Double weight, boolean fix) { - return new MatrixFilter("Deuteranopia", weight, DEUTERANOPIA_SIMULATION, fix ? DEUTERANOPIA_CORRECTION : null); + public static ImageFilter forDeuteranopia(Double weight) { + return new MatrixFilter("Deuteranopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Deuteranopia.MATRIX)); } - public static ImageFilter forTritanopia(Double weight, boolean fix) { - return new MatrixFilter("Tritanopia", weight, TRITANOPIA_SIMULATION, fix ? TRITANOPIA_CORRECTION : null); + public static ImageFilter forTritanopia(Double weight) { + return new MatrixFilter("Tritanopia (correction matrix)", new MatrixConverter(weight, ColorBlindnessMatrix.Tritanopia.MATRIX)); } - private final Matrix myMatrix; + private final String myName; + private final ColorConverter myConverter; - private MatrixFilter(String name, Double weight, Matrix simulation, Matrix correction) { - super(name + (correction == null ? " (simulation matrix)" : " (correction matrix)"), weight); - Matrix matrix = simulation.multiply(RGB_LMS); - if (correction != null) { - Matrix minus = RGB_LMS.minus(matrix); - Matrix multiply = correction.multiply(minus); - matrix = matrix.plus(multiply); - } - myMatrix = LMS_RGB.multiply(matrix); + private MatrixFilter(String name, ColorConverter converter) { + myName = name; + myConverter = converter; } @Override - int toRGB(int srcR, int srcG, int srcB) { - Vector vector = Vector.create(srcR, srcG, srcB).multiply(myMatrix); - return toRGB(srcR, srcG, srcB, fix(vector.get(0)), fix(vector.get(1)), fix(vector.get(2))); + public String toString() { + return myName; } - private static Vector cross(Vector left, Vector right) { - return Vector.create( - left.get(1) * right.get(2) - left.get(2) * right.get(1), - left.get(2) * right.get(0) - left.get(0) * right.get(2), - left.get(0) * right.get(1) - left.get(1) * right.get(0)); + @Override + public int filterRGB(int x, int y, int rgb) { + return myConverter.convert(rgb); } } diff --git a/platform/platform-impl/src/com/intellij/ide/ui/ColorBlindnessInternalAction.java b/platform/platform-impl/src/com/intellij/ide/ui/ColorBlindnessInternalAction.java index d54cca4ea1db..463ae0475ebc 100644 --- a/platform/platform-impl/src/com/intellij/ide/ui/ColorBlindnessInternalAction.java +++ b/platform/platform-impl/src/com/intellij/ide/ui/ColorBlindnessInternalAction.java @@ -19,6 +19,8 @@ import com.intellij.openapi.actionSystem.AnActionEvent; import com.intellij.openapi.project.DumbAwareAction; import com.intellij.openapi.ui.ComboBox; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.ui.components.panels.VerticalLayout; +import com.intellij.util.Matrix; import com.intellij.util.ui.ImageUtil; import com.intellij.util.ui.JBDimension; import com.intellij.util.ui.JBInsets; @@ -27,11 +29,12 @@ import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import javax.swing.*; +import javax.swing.event.ChangeListener; import java.awt.*; import java.awt.event.ItemEvent; -import java.awt.event.ItemListener; import java.awt.image.BufferedImage; import java.awt.image.ImageFilter; +import java.awt.image.RGBImageFilter; /** * @author Sergey.Malenkov @@ -45,6 +48,35 @@ public class ColorBlindnessInternalAction extends DumbAwareAction { private static final class ColorDialog extends DialogWrapper { private final ColorView myView = new ColorView(); private final JComboBox myCombo = new ComboBox<>(FilterItem.ALL); + private final JSlider myFirstSlider = createSlider(); + private final JSlider mySecondSlider = createSlider(); + + private static JSlider createSlider() { + JSlider slider = new JSlider(0, 100); + slider.setBorder(BorderFactory.createEmptyBorder(10, 0, 0, 0)); + slider.setMajorTickSpacing(10); + slider.setMinorTickSpacing(1); + slider.setPaintTicks(true); + slider.setPaintLabels(true); + slider.setVisible(false); + return slider; + } + + private static void showSlider(JSlider slider, ChangeListener listener) { + slider.setValue(70); + slider.setVisible(true); + slider.addChangeListener(listener); + } + + private static void hideSlider(JSlider slider, ChangeListener listener) { + slider.removeChangeListener(listener); + slider.setVisible(false); + } + + private void updateFilter(MutableFilter filter) { + filter.update(myFirstSlider.getValue() / 100.0, mySecondSlider.getValue() / 100.0); + myView.set(filter); + } private ColorDialog(AnActionEvent event) { super(event.getProject()); @@ -64,11 +96,38 @@ public class ColorBlindnessInternalAction extends DumbAwareAction { myView.setMinimumSize(new JBDimension(360, 200)); myView.setPreferredSize(new JBDimension(720, 400)); - myCombo.addItemListener(myView); + ChangeListener listener = event -> { + if (myView.myFilter instanceof MutableFilter) { + updateFilter((MutableFilter)myView.myFilter); + } + }; + + myCombo.addItemListener(event -> { + if (ItemEvent.SELECTED == event.getStateChange()) { + Object object = event.getItem(); + if (object instanceof FilterItem) { + FilterItem item = (FilterItem)object; + if (item.myFilter instanceof MutableFilter) { + showSlider(myFirstSlider, listener); + showSlider(mySecondSlider, listener); + updateFilter((MutableFilter)item.myFilter); + } + else { + hideSlider(myFirstSlider, listener); + hideSlider(mySecondSlider, listener); + myView.set(item.myFilter); + } + } + } + }); + JPanel control = new JPanel(new VerticalLayout(10)); + control.add(VerticalLayout.TOP, myCombo); + control.add(VerticalLayout.TOP, myFirstSlider); + control.add(VerticalLayout.TOP, mySecondSlider); JPanel panel = new JPanel(new BorderLayout(10, 10)); panel.add(BorderLayout.CENTER, myView); - panel.add(BorderLayout.SOUTH, myCombo); + panel.add(BorderLayout.SOUTH, control); return panel; } @@ -95,21 +154,39 @@ public class ColorBlindnessInternalAction extends DumbAwareAction { new FilterItem(null), new FilterItem(DaltonizationFilter.protanopia), new FilterItem(MatrixFilter.protanopia), + new FilterItem(new MutableFilter("Protanopia (mutable)") { + @Override + ColorConverter getConverter(double first, double second) { + Matrix matrix = Matrix.create(3, 1, first, second, 0, 1, 0, 0, 0, 1); + return new MatrixConverter(ColorBlindnessMatrix.Protanopia.calculate(matrix)); + } + }), new FilterItem(DaltonizationFilter.deuteranopia), new FilterItem(MatrixFilter.deuteranopia), + new FilterItem(new MutableFilter("Deuteranopia (mutable)") { + @Override + ColorConverter getConverter(double first, double second) { + Matrix matrix = Matrix.create(3, 1, 0, 0, first, 1, second, 0, 0, 1); + return new MatrixConverter(ColorBlindnessMatrix.Deuteranopia.calculate(matrix)); + } + }), new FilterItem(DaltonizationFilter.tritanopia), new FilterItem(MatrixFilter.tritanopia), + new FilterItem(new MutableFilter("Tritanopia (mutable)") { + @Override + ColorConverter getConverter(double first, double second) { + Matrix matrix = Matrix.create(3, 1, 0, 0, 0, 1, 0, first, second, 1); + return new MatrixConverter(ColorBlindnessMatrix.Tritanopia.calculate(matrix)); + } + }), new FilterItem(SimulationFilter.protanopia), - new FilterItem(MatrixFilter.forProtanopia(null, false)), new FilterItem(SimulationFilter.deuteranopia), - new FilterItem(MatrixFilter.forDeuteranopia(null, false)), new FilterItem(SimulationFilter.tritanopia), - new FilterItem(MatrixFilter.forTritanopia(null, false)), new FilterItem(SimulationFilter.achromatopsia), }; } - private static final class ColorView extends JComponent implements ItemListener { + private static final class ColorView extends JComponent { private ImageFilter myFilter; private Image myImage; @@ -138,17 +215,36 @@ public class ColorBlindnessInternalAction extends DumbAwareAction { g.drawImage(myImage, bounds.x, bounds.y, bounds.width, bounds.height, this); } + void set(ImageFilter filter) { + myImage = null; + myFilter = filter; + repaint(); + } + } + + private abstract static class MutableFilter extends RGBImageFilter { + private final String myName; + private ColorConverter myConverter; + + private MutableFilter(String name) { + myName = name; + update(.7, .7); + } + + private void update(double first, double second) { + myConverter = getConverter(first, second); + } + + abstract ColorConverter getConverter(double first, double second); + @Override - public void itemStateChanged(ItemEvent event) { - if (ItemEvent.SELECTED == event.getStateChange()) { - Object object = event.getItem(); - if (object instanceof FilterItem) { - FilterItem item = (FilterItem)object; - myFilter = item.myFilter; - myImage = null; - repaint(); - } - } + public String toString() { + return myName; + } + + @Override + public int filterRGB(int x, int y, int rgb) { + return myConverter.convert(rgb); } } }