views:

445

answers:

2

Hello!

I have a gradient that changes its colors, I want the text inside it should always be visible.

I rather doing it dynamically if there is any out-the-box resource; I want a 'magic brush' that negates the color.

Any experiments?

+3  A: 

Well, color inversion could possibly be done as a bitmap effect, but there's a simpler way.

Make a Grid that will be the container for 3 child panels so that these child panels will overlap each other completely:

Put the text where you want it in a panel that has a Transparent background (they do by default). Name this panel 'mask'.

Make another panel called 'mainbackground' and give it the main gradient as its background. Put this after the 'mask' panel so that it covers the text

Make another panel called 'invertedforeground' and give it the opposite gradient. For each color value in the main gradient, give this one the opposite (e.g., if one color is #FF0000, put #00FFFF). You could animate this gradient just as you can animate the first, just with opposite values. Then you set the OpacityMask of this panel to a VisualBrush and set the VisualBrushes's Visual property to {Binding ElementName=mask}.

<Grid>
    <Grid.Resources>
        <local:MyColorConverter x:Key="colorConverter" />
    </Grid.Resources>
    <Grid
        Name="mask">
        <TextBlock
            Name="mytext"
            HorizontalAlignment="Center"
            VerticalAlignment="Center"
            FontSize="32"
            Foreground="White"
            FontWeight="Bold">Blah blah blah</TextBlock>
    </Grid>

    <Grid Name="mainbackground">
        <Grid.Background>
            <LinearGradientBrush
                ColorInterpolationMode="ScRgbLinearInterpolation"
                EndPoint="1,0">
                <GradientStop x:Name="stop1"
                    Color="#FF0000"
                    Offset="0" />
                <GradientStop x:Name="stop2"
                    Color="#00FF00"
                    Offset="0.5" />
                <GradientStop x:Name="stop3"
                    Color="#0000FF"
                    Offset="1" />
            </LinearGradientBrush>
        </Grid.Background>
    </Grid>

    <Grid Name="invertedforeground">
        <Grid.Background>
            <LinearGradientBrush
                ColorInterpolationMode="ScRgbLinearInterpolation"
                EndPoint="1,0">
                <GradientStop
                    Color="{Binding ElementName=stop1, Path=Color, Converter={StaticResource colorConverter}}"
                    Offset="0" />
                <GradientStop
                    Color="{Binding ElementName=stop2, Path=Color, Converter={StaticResource colorConverter}}"
                    Offset="0.5" />
                <GradientStop
                    Color="{Binding ElementName=stop3, Path=Color, Converter={StaticResource colorConverter}}"
                    Offset="1" />
            </LinearGradientBrush>
        </Grid.Background>
        <Grid.OpacityMask>
            <VisualBrush
                Visual="{Binding ElementName=mask}" />
        </Grid.OpacityMask>
    </Grid>
</Grid>

You could probably use binding and value converters so that you only need to animate one gradient and the other simply follows.


Edit: I tried setting an inverted Foreground brush for the text, but it would stick to the TextBlock's coordinates, so I reverted to the previous solution of using the text as an OpacityMask.


Edit 2: I added example usage of a custom IValueConverter and binding the text gradient's colors to the original gradient's. You could also use binding and a converter somewhere higher up, such as binding invertedforeground's Background property to mainbackground's Background property and the converter takes the input gradient brush and returns a different gradient brush (this allows you to create a gradient with a much different configuration as the original).

Joel B Fant
+1 for syncing the gradients' layout using an OpacityMask. Two additional considerations: 1. The inverted brush can be computed from the foreground brush using binding with an IValueConverter rather than doing it manually. 2. The color computation is more difficult than a a straight color inversion, for example inverting #808080 gives you the almost-identical #7F7F7F.
Ray Burns
#1: Indeed it can. I think I'll show the binding, at the least. #2: That is the definition of an inversion; gray inverts to gray. The design decision to use a gradient on the text to differentiate it from a background gradient is a breeding ground for edge cases in the first place. The OP hasn't provided us with much information to simplify our answers.
Joel B Fant
+2  A: 

Joel gave an excellent answer on how to align the gradient brushes. I would like to touch on the complexities of automatically creating a new gradient brush that is guaranteed to be visible against the old one.

In WPF colors are modeled three-dimensionally, since it requires three numbers (such as R/G/B or H/S/B) to define a WPF color, not counting the alpha component. A given gradient fill can be viewed as a path that proceeds from one color point to another in the three dimensional color space. To create a reverse gradient that contrasts at every point requires creation of an additional path that at no point comes "too close" to the original path. "Too close" for this purpose is any two colors that are hard for the human eye to distinguish. This is in fact subjective. The 4% or so of people who are color-blind will have a different interpretation of "too close" than those who aren't.

For a non-colorblind person where "too close" is reasonably defined there will always be a multitude of paths that satisfy the criteria. In this case additional criteria are required to decide which one is "better". For example, should the text contrast as sharply as possible with the background, or it should it have the same general hue most of the way?

On the other hand, a conservative definition of "too close" that takes everyone's color perception into account such as "luminance must differ by at least 25%" will suffer from the opposite problem: The only gradient which satisfies the condition at every point must actually be discontinuous, that is it must jump from one color to a distant color all at once. Consider, for example, the simple gradient from black to white. If only luminance is involved the contrasting background must have a discontinuity otherwise it will match at some point.

For these reasons, creating a general algorithm to produce a contrasting background is more of an art than a science. Multiple algorithms can be used, and different algorithms will be appropriate in different situations.

A simple algorithm that has been used for this purpose is to keep the hue and saturation the same the gradient and set the luminance to (luminance + 50%) mod 100%. This algorithm does not produce very aesthetic results in most cases, however, and it never has a luminance variation of more than 50%. A modification of this algorithm is to invert or shift the hue and saturation values as well.

An even simpler algorithm to compute a contrasting luminance is luminance>50% ? 0% : 100%. This also can have aesthetic problems.

The bottom line here is that there is no one right answer to inverting your gradient colors. But if you have got an algorithm to do it, using Joel's opacity mask technique along with a binding and an IValueConverter that implements your algorithm will do the trick.

Ray Burns