diff --git a/src/Gemini/Framework/ShaderEffects/Converters/BindingProxy.cs b/src/Gemini/Framework/ShaderEffects/Converters/BindingProxy.cs new file mode 100644 index 00000000..4516ba94 --- /dev/null +++ b/src/Gemini/Framework/ShaderEffects/Converters/BindingProxy.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; + +namespace Gemini.Framework.ShaderEffects.Converters +{ + // From http://www.thomaslevesque.com/2011/03/21/wpf-how-to-bind-to-data-when-the-datacontext-is-not-inherited/ + public class BindingProxy : Freezable + { + #region Overrides of Freezable + + protected override Freezable CreateInstanceCore() + { + return new BindingProxy(); + } + + #endregion + + public object Data + { + get { return (object)GetValue(DataProperty); } + set { SetValue(DataProperty, value); } + } + + // Using a DependencyProperty as the backing store for Data. This enables animation, styling, binding, etc... + public static readonly DependencyProperty DataProperty = + DependencyProperty.Register("Data", typeof(object), typeof(BindingProxy), new UIPropertyMetadata(null)); + } +} diff --git a/src/Gemini/Framework/ShaderEffects/Converters/IsNullConverter.cs b/src/Gemini/Framework/ShaderEffects/Converters/IsNullConverter.cs new file mode 100644 index 00000000..3faa4a96 --- /dev/null +++ b/src/Gemini/Framework/ShaderEffects/Converters/IsNullConverter.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; + +namespace Gemini.Framework.ShaderEffects.Converters +{ + // From https://github.com/MahApps/MahApps.Metro/blob/9c331aa20b2b3fd6a9426e0687cfc535511bf134/MahApps.Metro/Converters/IsNullConverter.cs + + /// + /// Converts the value from true to false and false to true. + /// + public sealed class IsNullConverter : IValueConverter + { + private static IsNullConverter _instance; + + // Explicit static constructor to tell C# compiler + // not to mark type as beforefieldinit + static IsNullConverter() + { + } + + private IsNullConverter() + { + } + + public static IsNullConverter Instance + { + get { return _instance ?? (_instance = new IsNullConverter()); } + } + + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return null == value; + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + return Binding.DoNothing; + } + } +} diff --git a/src/Gemini/Framework/ShaderEffects/Converters/SolidColorBrushToColorConverter.cs b/src/Gemini/Framework/ShaderEffects/Converters/SolidColorBrushToColorConverter.cs new file mode 100644 index 00000000..fa372e55 --- /dev/null +++ b/src/Gemini/Framework/ShaderEffects/Converters/SolidColorBrushToColorConverter.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows.Data; +using System.Windows.Media; + +namespace Gemini.Framework.ShaderEffects.Converters +{ + public class SolidColorBrushToColorConverter : IValueConverter + { + public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + SolidColorBrush brush = value as SolidColorBrush; + if (brush != null) + return brush.Color; + + return default(Color?); + } + + public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture) + { + if (value != null) + { + Color color = (Color)value; + return new SolidColorBrush(color); + } + + return default(SolidColorBrush); + } + } +} diff --git a/src/Gemini/Framework/ShaderEffects/Scripts/ThemedImage.fx b/src/Gemini/Framework/ShaderEffects/Scripts/ThemedImage.fx new file mode 100644 index 00000000..f7ab7241 --- /dev/null +++ b/src/Gemini/Framework/ShaderEffects/Scripts/ThemedImage.fx @@ -0,0 +1,145 @@ +/// ThemedImageEffect +/// Converts an input image into an image that blends in with the target background. + +/// The input image +sampler2D Input : register(S0); + +/// Background color of the image. +/// #FFF6F6F6 +float4 Background : register(C0); + +/// 1.0 if the image should be rendered enabled, 0.0 if it should be disabled (grayscaled). +/// 0.0 +/// 1.0 +/// 0.0 +float IsEnabled : register(C1); + +float3 rgb2hsl(in float4 RGB) +{ + float r = RGB.r; // Red, range: [0..1] + float g = RGB.g; // Green, range: [0..1] + float b = RGB.b; // Blue, range: [0..1] + + float maxChannel = (r > g && r > b) ? r : (g > b) ? g : b; + float minChannel = (r < g && r < b) ? r : (g < b) ? g : b; + + float h = (maxChannel + minChannel) / 2.0f; // Hue, range: [0..1] + float s = (maxChannel + minChannel) / 2.0f; // Saturation, range: [0..1] + float l = (maxChannel + minChannel) / 2.0f; // Lightness, range: [0..1] + + if (maxChannel == minChannel) + { + h = s = 0.0f; + } + else + { + float d = maxChannel - minChannel; + s = (l > 0.5f) ? d / (2.0f - maxChannel - minChannel) : d / (maxChannel + minChannel); + + if (r > g && r > b) // maxChannel == r + h = (g - b) / d + (g < b ? 6.0f : 0.0f); + else if (g > b) // maxChannel == g + h = (b - r) / d + 2.0f; + else // maxChannel = b + h = (r - g) / d + 4.0f; + + h /= 6.0f; + } + + return float3(h, s, l); +} + +float hue2rgb(in float p, in float q, in float t) +{ + if (t < 0.0f) t += 1.0f; + if (t > 1.0f) t -= 1.0f; + if (t < 1.0f / 6.0f) return p + (q - p) * 6.0f * t; + if (t < 1.0f / 2.0f) return q; + if (t < 2.0f / 3.0f) return p + (q - p) * (2.0f / 3.0f - t) * 6.0f; + return p; +} + +float3 hsl2rgb(in float3 HSL) +{ + float h = HSL[0]; + float s = HSL[1]; + float l = HSL[2]; + + float q = (l < 0.5f) ? l * (1.0f + s) : l + s - l * s; + float p = 2.0f * l - q; + + float r = hue2rgb(p, q, h + 1.0f / 3.0f); + float g = hue2rgb(p, q, h); + float b = hue2rgb(p, q, h - 1.0f / 3.0f); + + return float3(r, g, b); +} + +float3 rgb2gray(in float3 RGB) +{ + // https://en.wikipedia.org/wiki/Relative_luminance + // https://en.wikipedia.org/wiki/SRGB + // Convert the gamma compressed RGB values to linear RGB + RGB.r = (RGB.r <= 0.04045f) ? RGB.r / 12.92f : pow((RGB.r + 0.055f) / 1.055f, 2.4f); + RGB.g = (RGB.g <= 0.04045f) ? RGB.g / 12.92f : pow((RGB.g + 0.055f) / 1.055f, 2.4f); + RGB.b = (RGB.b <= 0.04045f) ? RGB.b / 12.92f : pow((RGB.b + 0.055f) / 1.055f, 2.4f); + float y = dot(float3(0.2126f, 0.7152f, 0.0722f), RGB.rgb); + return y <= 0.0031308f ? 12.92f * y : 1.055f * pow(y, 1.0f/2.4f) - 0.055f; +} + +float4 main(float2 uv : TEXCOORD) : COLOR +{ + // This performs two conversions. + // 1. The lightness of the image is transformed so that the constant "halo" lightness blends in with the background. + // - This has the effect of eliminating the halo visually. + // - The "halo" lightness is an immutable constant, and is not calculated from the input image. + // 2. The image is converted to grayscale if the IsEnabled parameter is 0. + + // Refer to http://stackoverflow.com/questions/36778989/vs2015-icon-guide-color-inversion + + // First, loopk up original image color + float4 pxRGBA = tex2D(Input, uv.xy); + + // For performance reason, the WPF multiplies each color channel + // by the alpha channel before sending the Input into HLSL shader. + // So let's convert the color to be non-premultiplied. + if (pxRGBA.a > 0.0) pxRGBA.rgb /= pxRGBA.a; + + // Convert the color space from RGB to HSL. + float3 pxHSL = rgb2hsl(pxRGBA); + + // Define lightness of default outlined color(#F6F6F6) + float haloHSL2 = 0.96470588235294f; + + // Lightness of Background color is bgHSL[2] + float3 bgHSL = rgb2hsl(Background); + + if (bgHSL[2] < 0.5f) + { + haloHSL2 = 1.0f - haloHSL2; + pxHSL[2] = 1.0f - pxHSL[2]; + } + + if(pxHSL[2] <= haloHSL2) + { + pxHSL[2] = bgHSL[2] / haloHSL2 * pxHSL[2]; + } + else + { + pxHSL[2] = (1.0f - bgHSL[2]) / (1.0f - haloHSL2) * (pxHSL[2] - 1.0f) + 1.0f; + } + + // Convert the color space from HSL to RGB. + pxRGBA.rgb = hsl2rgb(pxHSL); + + // if !IsEnabled, convert to grayscale. + if (IsEnabled == 0.0f) + { + pxRGBA.rgb = rgb2gray(pxRGBA.rgb); + } + + // Convert the color to a PreMultiplied color. + pxRGBA.rgb *= pxRGBA.a; + + return pxRGBA; +} \ No newline at end of file diff --git a/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.cs b/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.cs new file mode 100644 index 00000000..959832f1 --- /dev/null +++ b/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Windows; +using System.Windows.Media; + +namespace Gemini.Framework.ShaderEffects +{ + /// Converts an input image into an image that blends in with the target background. + public class ThemedImageEffect : ShaderEffectBase + { + public static readonly DependencyProperty InputProperty = RegisterPixelShaderSamplerProperty("Input", typeof(ThemedImageEffect), 0); + + public static readonly DependencyProperty BackgroundProperty = DependencyProperty.Register("Background", typeof(Color), typeof(ThemedImageEffect), new UIPropertyMetadata(Color.FromArgb(255, 246, 246, 246), PixelShaderConstantCallback(0))); + + public static readonly DependencyProperty IsEnabledProperty = DependencyProperty.Register("IsEnabled", typeof(double), typeof(ThemedImageEffect), new UIPropertyMetadata(((double)(1D)), PixelShaderConstantCallback(1))); + + public ThemedImageEffect() + { + UpdateShaderValue(InputProperty); + UpdateShaderValue(BackgroundProperty); + UpdateShaderValue(IsEnabledProperty); + } + + public Brush Input + { + get { return ((Brush)(GetValue(InputProperty))); } + set { SetValue(InputProperty, value); } + } + + /// Background color of the image. + public Color Background + { + get { return ((Color)(GetValue(BackgroundProperty))); } + set { SetValue(BackgroundProperty, value); } + } + + /// 1.0 if the image should be rendered enabled, 0.0 if it should be disabled (grayscaled). + public double IsEnabled + { + get { return ((double)(this.GetValue(IsEnabledProperty))); } + set { this.SetValue(IsEnabledProperty, value); } + } + } +} diff --git a/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.ps b/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.ps new file mode 100644 index 00000000..00763ab6 Binary files /dev/null and b/src/Gemini/Framework/ShaderEffects/ThemedImageEffect.ps differ diff --git a/src/Gemini/Gemini.csproj b/src/Gemini/Gemini.csproj index cc230fc5..8c17c0f2 100644 --- a/src/Gemini/Gemini.csproj +++ b/src/Gemini/Gemini.csproj @@ -31,11 +31,12 @@ + SettingsSingleFileGenerator Settings.Designer.cs - + ResXFileCodeGenerator Resources.de.Designer.cs diff --git a/src/Gemini/Themes/VS2013/Controls/Menu.xaml b/src/Gemini/Themes/VS2013/Controls/Menu.xaml index 97c96108..a60e84d3 100644 --- a/src/Gemini/Themes/VS2013/Controls/Menu.xaml +++ b/src/Gemini/Themes/VS2013/Controls/Menu.xaml @@ -13,8 +13,12 @@ --> - + + + M 0,5.1 L 1.7,5.2 L 3.4,7.1 L 8,0.4 L 9.2,0 L 3.3,10.8 Z @@ -115,7 +119,7 @@ - + @@ -147,8 +151,7 @@ Background="Transparent" Padding="2,0,1,0"> - + ContentSource="Icon"> + + + + @@ -222,7 +231,13 @@ HorizontalAlignment="Center" VerticalAlignment="Center" Height="16" Width="16" - ContentSource="Icon" /> + ContentSource="Icon"> + + + + diff --git a/src/Gemini/Themes/VS2013/Controls/Toolbar.xaml b/src/Gemini/Themes/VS2013/Controls/Toolbar.xaml index 76bb1095..ec5e7cc2 100644 --- a/src/Gemini/Themes/VS2013/Controls/Toolbar.xaml +++ b/src/Gemini/Themes/VS2013/Controls/Toolbar.xaml @@ -2,7 +2,11 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:c="clr-namespace:Gemini.Modules.ToolBars.Controls" xmlns:shaderEffects="clr-namespace:Gemini.Framework.ShaderEffects" + xmlns:converters="clr-namespace:Gemini.Framework.ShaderEffects.Converters" xmlns:controls="clr-namespace:Gemini.Framework.Controls"> + + +