Modos de fusión separables

Como has visto en el artículo Modos de fusión SkiaSharp Porter-Duff los modos de fusión Porter-Duff suelen realizar operaciones de recorte. Los modos de fusión separables son diferentes. Los modos separables modifican los componentes individuales de color rojo, verde y azul de una imagen. Los modos de fusión separables pueden mezclar color para demostrar que la combinación de rojo, verde y azul es realmente blanca:

Colores primarios

Aclaramiento y oscurecimiento de dos maneras

Es habitual tener un mapa de bits que sea demasiado oscuro o demasiado claro. Puedes usar modos de fusión separables para aclarar o oscurecer la imagen. De hecho, dos de los modos de fusión separables de la enumeración SKBlendMode se denominan Lighten y Darken.

Estos dos modos se muestran en la página Claro y Oscuro. El archivo XAML crea una instancia de dos objetos SKCanvasView y dos vistas Slider:

<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.LightenAndDarkenPage"
             Title="Lighten and Darken">
    <StackLayout>
        <skia:SKCanvasView x:Name="lightenCanvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="lightenSlider"
                Margin="10"
                ValueChanged="OnSliderValueChanged" />

        <skia:SKCanvasView x:Name="darkenCanvasView"
                           VerticalOptions="FillAndExpand"
                           PaintSurface="OnCanvasViewPaintSurface" />

        <Slider x:Name="darkenSlider"
                Margin="10"
                ValueChanged="OnSliderValueChanged" />
    </StackLayout>
</ContentPage>

El primer par SKCanvasView y Slider muestra SKBlendMode.Lighten y el segundo muestra SKBlendMode.Darken. Las dos vistas Slider comparten el mismo controlador ValueChanged y los dos objetos SKCanvasView comparten el mismo controlador PaintSurface. Ambos controladores de eventos comprueban qué objeto desencadena el evento:

public partial class LightenAndDarkenPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                typeof(SeparableBlendModesPage),
                "SkiaSharpFormsDemos.Media.Banana.jpg");

    public LightenAndDarkenPage ()
    {
        InitializeComponent ();
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs args)
    {
        if ((Slider)sender == lightenSlider)
        {
            lightenCanvasView.InvalidateSurface();
        }
        else
        {
            darkenCanvasView.InvalidateSurface();
        }
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Find largest size rectangle in canvas
        float scale = Math.Min((float)info.Width / bitmap.Width,
                               (float)info.Height / bitmap.Height);
        SKRect rect = SKRect.Create(scale * bitmap.Width, scale * bitmap.Height);
        float x = (info.Width - rect.Width) / 2;
        float y = (info.Height - rect.Height) / 2;
        rect.Offset(x, y);

        // Display bitmap
        canvas.DrawBitmap(bitmap, rect);

        // Display gray rectangle with blend mode
        using (SKPaint paint = new SKPaint())
        {
            if ((SKCanvasView)sender == lightenCanvasView)
            {
                byte value = (byte)(255 * lightenSlider.Value);
                paint.Color = new SKColor(value, value, value);
                paint.BlendMode = SKBlendMode.Lighten;
            }
            else
            {
                byte value = (byte)(255 * (1 - darkenSlider.Value));
                paint.Color = new SKColor(value, value, value);
                paint.BlendMode = SKBlendMode.Darken;
            }

            canvas.DrawRect(rect, paint);
        }
    }
}

El controlador PaintSurface calcula un rectángulo adecuado para el mapa de bits. El controlador muestra ese mapa de bits y, después, un rectángulo sobre el mapa de bits mediante un objeto SKPaint con su propiedad BlendMode establecida SKBlendMode.Lighten en o SKBlendMode.Darken. La propiedad Color es un tono gris basado en Slider. Para el modo Lighten, el color oscila entre negro y blanco, pero para el modo Darken abarca de blanco a negro.

Las capturas de pantalla de izquierda a derecha muestran valores Slider cada vez más grandes, ya que la imagen superior se vuelve más clara y la imagen inferior se vuelve más oscura:

Aclarar y oscurecer

Este programa muestra la forma normal en que se usan los modos de fusión separables: el destino es una imagen de algún tipo, muy a menudo un mapa de bits. El origen es un rectángulo que se muestra mediante un objeto SKPaint con su propiedad BlendMode establecida en un modo de fusión separable. El rectángulo puede ser un color sólido (como sucede aquí) o un degradado. La transparencia no se usa generalmente con los modos de fusión separables.

A medida que experimentes con este programa, descubrirás que estos dos modos de fusión no iluminan ni oscurecen la imagen uniformemente. En su lugar, Slider parece establecer un umbral de algún tipo. Por ejemplo, a medida que aumenta Slider para el modo Lighten, las áreas más oscuras de la imagen obtienen luz en primer lugar, mientras que las áreas más claras siguen siendo las mismas.

Para el modo Lighten, si el píxel de destino es el valor de color RGB (Dr, Dg, Db) y el píxel de origen es el color (Sr, Sg, Sb), la salida es (Or, Og, Ob) calculada de la siguiente manera:

Or = max(Dr, Sr) Og = max(Dg, Sg) Ob = max(Db, Sb)

Para el rojo, verde y azul por separado, el resultado es el valor mayor entre el destino y el origen. Esto produce el efecto de aclarar primero las áreas oscuras del destino.

El modo Darken es similar, salvo que el resultado es el menor entre el destino y el origen:

Or = min(Dr, Sr) Og = min(Dg, Sg) Ob = min(Db, Sb)

Los componentes rojo, verde y azul se controlan por separado, por lo que estos modos de fusión se conocen como modos de fusión separables. Por este motivo, las abreviaturas Dc y Sc se pueden usar para los colores de origen y destino, y se entiende que los cálculos se aplican a cada uno de los componentes rojo, verde y azul por separado.

En la tabla siguiente se muestran todos los modos de fusión separables con breves explicaciones de lo que hacen. En la segunda columna se muestra el color de origen que no produce ningún cambio:

Modo de fusión Sin cambios Operación
Plus Negro Aclara agregando colores: Sc + Dc
Modulate Blanco Oscurece multiplicando colores: Sc·Dc
Screen Negro Complementa el producto de complementos: Sc + Dc – Sc·Dc
Overlay Gris Inverso de HardLight
Darken Blanco Mínimo de colores: min(Sc, Dc)
Lighten Negro Máximo de colores: max(Sc, Dc)
ColorDodge Negro Ilumina el destino basado en el origen
ColorBurn Blanco Oscurece el destino basado en el origen
HardLight Gris Similar al efecto del contenido destacado áspero
SoftLight Gris Similar al efecto del contenido destacado suave
Difference Negro Resta el más oscuro del más claro: Abs(Dc – Sc)
Exclusion Negro Similar a Difference pero con menor contraste
Multiply Blanco Oscurece multiplicando colores: Sc·Dc

Los algoritmos más detallados se pueden encontrar en la especificación Composición y combinación de nivel 1 de W3C y Referencia SkBlendMode de Skia, aunque la notación en estos dos orígenes no es la misma. Ten en cuenta que Plus normalmente se considera un modo de fusión Porter-Duff y Modulate no forma parte de la especificación de W3C.

Si el origen es transparente, para todos los modos de combinación separables, excepto para Modulate, el modo de fusión no tiene ningún efecto. Como has visto anteriormente, el modo de fusión Modulate incorpora el canal alfa en la multiplicación. De lo contrario, Modulate tiene el mismo efecto que Multiply.

Observa los dos modos denominados ColorDodge y ColorBurn. Las palabras subexposición y sobreexposición se originaron en prácticas fotográficas en un cuarto oscuro. Un ampliador hace una impresión fotográfica dirigiendo la luz a través de un negativo. Sin luz, la impresión es blanca. La impresión se oscurece conforme se refleja más luz en la impresión durante un periodo de tiempo más largo. Los profesionales de impresiones suelen usar una mano o un objeto pequeño para impedir que parte de la luz se refleje sobre una parte determinada de la impresión, lo que hace que esa zona sea más clara. Esto se conoce como subexposición. Por el contrario, el material opaco con un agujero en él (o utilizar manos para bloquear la mayor parte de la luz) podría usarse para dirigir más luz en un punto determinado para oscurecerlo, llamado sobreexposición.

El programa Subexposición y sobreexposición es muy similar a Aclaramiento y oscurecimiento. El archivo XAML está estructurado igual, pero con nombres de elemento diferentes, y el archivo de código subyacente también es bastante similar, pero el efecto de estos dos modos de fusión es bastante diferente:

Sobreexposición y subexposición

En el caso de los valores pequeños Slider, el modo Lighten aclara primero las áreas oscuras, mientras que ColorDodge aclara más uniformemente.

Los programas de aplicaciones de procesamiento de imágenes suelen permitir que la subexposición y la sobreexposición se restrinjan a áreas específicas, al igual que en un cuarto oscuro. Esto se puede lograr mediante degradados o mediante un mapa de bits con diferentes tonos de gris.

Exploración de los modos de fusión separables

La página Modos de fusión separables permite examinar todos los modos de fusión separables. Muestra un destino de mapa de bits y un origen de rectángulo coloreado mediante uno de los modos de fusión.

El archivo XAML define un elemento Picker (para seleccionar el modo de fusión) y cuatro controles deslizantes. Los tres primeros controles deslizantes permiten establecer los componentes rojo, verde y azul del origen. El cuarto control deslizante está pensado para invalidar esos valores estableciendo un tono gris. Los controles deslizantes individuales no se identifican, pero los colores indican su función:

<?xml version="1.0" encoding="utf-8" ?>
<ContentPage xmlns="http://xamarin.com/schemas/2014/forms"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             xmlns:skia="clr-namespace:SkiaSharp;assembly=SkiaSharp"
             xmlns:skiaviews="clr-namespace:SkiaSharp.Views.Forms;assembly=SkiaSharp.Views.Forms"
             x:Class="SkiaSharpFormsDemos.Effects.SeparableBlendModesPage"
             Title="Separable Blend Modes">

    <StackLayout>
        <skiaviews:SKCanvasView x:Name="canvasView"
                                VerticalOptions="FillAndExpand"
                                PaintSurface="OnCanvasViewPaintSurface" />

        <Picker x:Name="blendModePicker"
                Title="Blend Mode"
                Margin="10, 0"
                SelectedIndexChanged="OnPickerSelectedIndexChanged">
            <Picker.ItemsSource>
                <x:Array Type="{x:Type skia:SKBlendMode}">
                    <x:Static Member="skia:SKBlendMode.Plus" />
                    <x:Static Member="skia:SKBlendMode.Modulate" />
                    <x:Static Member="skia:SKBlendMode.Screen" />
                    <x:Static Member="skia:SKBlendMode.Overlay" />
                    <x:Static Member="skia:SKBlendMode.Darken" />
                    <x:Static Member="skia:SKBlendMode.Lighten" />
                    <x:Static Member="skia:SKBlendMode.ColorDodge" />
                    <x:Static Member="skia:SKBlendMode.ColorBurn" />
                    <x:Static Member="skia:SKBlendMode.HardLight" />
                    <x:Static Member="skia:SKBlendMode.SoftLight" />
                    <x:Static Member="skia:SKBlendMode.Difference" />
                    <x:Static Member="skia:SKBlendMode.Exclusion" />
                    <x:Static Member="skia:SKBlendMode.Multiply" />
                </x:Array>
            </Picker.ItemsSource>

            <Picker.SelectedIndex>
                0
            </Picker.SelectedIndex>
        </Picker>

        <Slider x:Name="redSlider"
                MinimumTrackColor="Red"
                MaximumTrackColor="Red"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="greenSlider"
                MinimumTrackColor="Green"
                MaximumTrackColor="Green"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="blueSlider"
                MinimumTrackColor="Blue"
                MaximumTrackColor="Blue"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Slider x:Name="graySlider"
                MinimumTrackColor="Gray"
                MaximumTrackColor="Gray"
                Margin="10, 0"
                ValueChanged="OnSliderValueChanged" />

        <Label x:Name="colorLabel"
               HorizontalTextAlignment="Center" />

    </StackLayout>
</ContentPage>

El archivo de código subyacente carga uno de los recursos de mapa de bits y lo dibuja dos veces, una vez en la mitad superior del lienzo y otra vez en la mitad inferior:

public partial class SeparableBlendModesPage : ContentPage
{
    SKBitmap bitmap = BitmapExtensions.LoadBitmapResource(
                        typeof(SeparableBlendModesPage),
                        "SkiaSharpFormsDemos.Media.Banana.jpg"); 

    public SeparableBlendModesPage()
    {
        InitializeComponent();
    }

    void OnPickerSelectedIndexChanged(object sender, EventArgs args)
    {
        canvasView.InvalidateSurface();
    }

    void OnSliderValueChanged(object sender, ValueChangedEventArgs e)
    {
        if (sender == graySlider)
        {
            redSlider.Value = greenSlider.Value = blueSlider.Value = graySlider.Value;
        }

        colorLabel.Text = String.Format("Color = {0:X2} {1:X2} {2:X2}",
                                        (byte)(255 * redSlider.Value),
                                        (byte)(255 * greenSlider.Value),
                                        (byte)(255 * blueSlider.Value));

        canvasView.InvalidateSurface();
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        // Draw bitmap in top half
        SKRect rect = new SKRect(0, 0, info.Width, info.Height / 2);
        canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);

        // Draw bitmap in bottom halr
        rect = new SKRect(0, info.Height / 2, info.Width, info.Height);
        canvas.DrawBitmap(bitmap, rect, BitmapStretch.Uniform);

        // Get values from XAML controls
        SKBlendMode blendMode =
            (SKBlendMode)(blendModePicker.SelectedIndex == -1 ?
                                        0 : blendModePicker.SelectedItem);

        SKColor color = new SKColor((byte)(255 * redSlider.Value),
                                    (byte)(255 * greenSlider.Value),
                                    (byte)(255 * blueSlider.Value));

        // Draw rectangle with blend mode in bottom half
        using (SKPaint paint = new SKPaint())
        {
            paint.Color = color;
            paint.BlendMode = blendMode;
            canvas.DrawRect(rect, paint);
        }
    }
}

Hacia la parte inferior del controlador PaintSurface, se dibuja un rectángulo sobre el segundo mapa de bits con el modo de fusión y el color seleccionados. Puedes comparar el mapa de bits modificado en la parte inferior con el mapa de bits original en la parte superior:

Modos de fusión separables

Colores primarios aditivos y sustractivos

La página Colores primarios dibuja tres círculos superpuestos en rojo, verde y azul:

Colores primarios aditivos

Estos son los colores primarios aditivos. Una combinación de dos de ellos produce cian, magenta y amarillo, y una combinación de los tres genera blanco.

Estos tres círculos están dibujados con el modo SKBlendMode.Plus, pero también puedes usar Screen, Lighten o Difference para el mismo efecto. Este es el programa:

public class PrimaryColorsPage : ContentPage
{
    bool isSubtractive;

    public PrimaryColorsPage ()
    {
        Title = "Primary Colors";

        SKCanvasView canvasView = new SKCanvasView();
        canvasView.PaintSurface += OnCanvasViewPaintSurface;

        // Switch between additive and subtractive primaries at tap
        TapGestureRecognizer tap = new TapGestureRecognizer();
        tap.Tapped += (sender, args) =>
        {
            isSubtractive ^= true;
            canvasView.InvalidateSurface();
        };
        canvasView.GestureRecognizers.Add(tap);

        Content = canvasView;
    }

    void OnCanvasViewPaintSurface(object sender, SKPaintSurfaceEventArgs args)
    {
        SKImageInfo info = args.Info;
        SKSurface surface = args.Surface;
        SKCanvas canvas = surface.Canvas;

        canvas.Clear();

        SKPoint center = new SKPoint(info.Rect.MidX, info.Rect.MidY);
        float radius = Math.Min(info.Width, info.Height) / 4;
        float distance = 0.8f * radius;     // from canvas center to circle center
        SKPoint center1 = center + 
            new SKPoint(distance * (float)Math.Cos(9 * Math.PI / 6),
                        distance * (float)Math.Sin(9 * Math.PI / 6));
        SKPoint center2 = center +
            new SKPoint(distance * (float)Math.Cos(1 * Math.PI / 6),
                        distance * (float)Math.Sin(1 * Math.PI / 6));
        SKPoint center3 = center +
            new SKPoint(distance * (float)Math.Cos(5 * Math.PI / 6),
                        distance * (float)Math.Sin(5 * Math.PI / 6));

        using (SKPaint paint = new SKPaint())
        {
            if (!isSubtractive)
            {
                paint.BlendMode = SKBlendMode.Plus; 
                System.Diagnostics.Debug.WriteLine(paint.BlendMode);

                paint.Color = SKColors.Red;
                canvas.DrawCircle(center1, radius, paint);

                paint.Color = SKColors.Lime;    // == (00, FF, 00)
                canvas.DrawCircle(center2, radius, paint);

                paint.Color = SKColors.Blue;
                canvas.DrawCircle(center3, radius, paint);
            }
            else
            {
                paint.BlendMode = SKBlendMode.Multiply
                System.Diagnostics.Debug.WriteLine(paint.BlendMode);

                paint.Color = SKColors.Cyan;
                canvas.DrawCircle(center1, radius, paint);

                paint.Color = SKColors.Magenta;
                canvas.DrawCircle(center2, radius, paint);

                paint.Color = SKColors.Yellow;
                canvas.DrawCircle(center3, radius, paint);
            }
        }
    }
}

El programa incluye un elemento TabGestureRecognizer. Al pulsar o hacer clic en la pantalla, el programa usa SKBlendMode.Multiply para mostrar los tres primarios sustractivos:

Colores primarios sustractivos

El modo Darken también funciona para este mismo efecto.