Прозрачность и альфа-канал в WinRT

192

Мы работали с растровыми изображениями и ранее: выводили их на экран, использовали в кистях, растягивали, деформировали и вращали. Но в этой и последующих статьях мы доберемся до самых тонкостей внутреннего устройства растровых изображений и манипуляций с битами отдельных пикселов. Почти во всех программах будет использоваться класс WriteableBitmap, который является производным от ImageSource, а следовательно, может использоваться в качестве источника для Image и ImageBrush:

Object
    DependencyObject
        ImageSource
            BitmapSource
                WriteableBitmap
                BitmapImage

От BitmapSource класс WriteableBitmap наследует метод SetSource, который может использоваться для загрузки файла растровой графики при помощи объекта, реализующего интерфейс IRandomAccessStream.

Особенность класса WriteableBitmap заключается в том, что он определяет свойство PixelBuffer, которое предоставляет доступ к битам пикселов. Вы можете оперировать с пикселами существующего изображения или создать все изображение «с нуля». Позже будет также рассматривается чтение и запись различных форматов графических файлов (таких, как PNG и JPEG) с использованием массивов битов пикселов.

Если вы знакомы с Silverlight-версией WriteableBitmap, вероятно, вас неприятно удивит тот факт, что его версия для Windows Runtime не реализует метод Render, позволяющий отобразить произвольный экземпляр UIElement на поверхности изображения. Это значительно ограничивает возможности применения WriteableBitmap для некоторых типичных задач.

Например, ранее были рассмотрены программы для рисования на экране, которые использовали для представления ввода с указателя элементы Line, Polyline и Path. Возможно, вы заметили, что я не реализовал функцию сохранения рисунка в файле. Один разумный способ сохранения изображений основан на выводе элементов Line, Polyline и Path на растровом изображении и сохранении его в файле. Однако отсутствие метода Render в классе WriteableBitmap значительно усложняет этот процесс.

Далее я покажу, как рисовать линии на растровом изображении. Это позволит мне представить программу FingerPaint (на этот раз без цифр в названии), которая позволяет сохранить рисунок в формате растрового изображения. Позже будет показано, как использовать класс SurfaceImageSource, который также является производным от ImageSource и на котором можно рисовать с использованием графических операций DirectX из кода C++.

Я не собираюсь рассматривать сторонние библиотеки, но для рисования сложной графики на растровых изображениях может пригодиться библиотека WriteableBitmapEx.

Биты пикселов

Растровое изображение образуется из целого количества строк и столбцов. Для любого экземпляра класса, производного от BitmapSource, эти размеры могут быть получены из свойств PixelHeight и PixelWidth.

На концептуальном уровне биты пикселов хранятся в двумерном массиве, размеры которого равны PixelHeight и PixelWidth. Фактически массив имеет всего одно измерение, но основные проблемы возникают с представлением отдельных пикселов. В этом представлении, иногда называемым «цветовым форматом» растрового изображения, может использоваться от одного бита на пиксел (для изображений, состоящих только из черного и белого цветов) до одного байта на пиксел (для изображений, состоящих из оттенков серого или растров с 256-цветовой палитрой), 3 или 4 байтов на пиксел (для полноцветных изображений с прозрачностью или без нее) и даже более для больших цветовых разрешений.

Однако для работы с WriteableBitmap был установлен единый цветовой формат. В любом объекте WriteableBitmap каждый пиксел состоит из четырех байтов. Таким образом, общее количество байтов в массиве пикселов растрового изображения равно:

PixelHeight * PixelWidth * 4

Изображение начинается с верхней строки и следует слева направо. Выравнивание строк отсутствует. Для каждого пиксела байты следуют в определенном порядке:

Синий, Зеленый, Красный, Альфа

Значения байтов лежат в диапазоне от 0 до 255, как в значениях Color. Предполагается, что цветовые значения WriteableBitmap соответствуют схеме sRGB («стандарт RGB»), а следовательно, совместимы со значениями Windows Runtime Color (кроме значения Colors.Transparent — см. далее).

Пикселы WriteableBitmap хранятся в предумноженном альфа-канале (premultiplied alpha). Вскоре я расскажу, что это значит.

Порядок «синий-зеленый-красный-альфа» на первый взгляд противоположен тому, который обычно используется для обозначения цветовых байтов (и их порядку в методе Color.FromArgb), но он вполне логичен, если учесть, что пиксел WriteableBitmap в действительности представляет собой 32-разрядное целое без знака, у которого в старшем байте хранится альфа-канал, а в младшем — синяя составляющая. В операционных системах на базе микропроцессоров Intel это целое число хранится в прямом (little-endian) порядке байтов.

Давайте построим растровое изображение. Для этого мы создадим объект WriteableBitmap и заполним его пикселами. Чтобы упростить вычисления, WriteableBitmap будет состоять из 256 строк и 256 столбцов. Левый верхний угол будет черным, правый верхний — синим, левый нижний — красным, и правый нижний — фиолетовый (сочетание красного и синего). Окраска представляет собой разновидность градиента, но она отличается от градиентов, доступных в Windows Runtime.

Файл XAML определяет элемент Image, которому присваивается экземпляр класса производного от ImageSource:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Image Name="image" />
    </Grid>
</Page>

Создать экземпляр WriteableBitmap в XAML невозможно, потому что у этого класса нет конструктора без параметров. Файл фонового кода создает и строит WriteableBitmap в обработчике события Loaded. Ниже приведен полный файл вместе с необходимыми директивами using. Сам класс WriteableBitmap определяется в пространстве имен Windows.UI.Xaml.Media.Imaging:

using System.IO;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Controls;
using System.Runtime.InteropServices.WindowsRuntime;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += MainPage_Loaded;
        }

        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            WriteableBitmap bitmap = new WriteableBitmap(256, 256);
            byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];

            for (int y = 0; y < bitmap.PixelHeight; y++)
                for (int x = 0; x < bitmap.PixelWidth; x++)
                {
                    int index = 4 * (y * bitmap.PixelWidth + x);
                    pixels[index + 0] = (byte)x;    // Blue
                    pixels[index + 1] = 0;          // Green
                    pixels[index + 2] = (byte)y;    // Red
                    pixels[index + 3] = 255;        // Alpha
                }

            using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
            {
                await pixelStream.WriteAsync(pixels, 0, pixels.Length);
            }
            
            bitmap.Invalidate();
            image.Source = bitmap;
        }
    }
}

Конструктору WriteableBitmap должна передаваться ширина и высота изображения в пикселах. На основании этих размеров программа выделяет память для массива байтов:

byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];

Размер массива для WriteableBitmap всегда вычисляется по этой формуле.

Циклы по строкам и столбцам перебирают все пикселы изображения. Индекс для обращения к конкретному пикселу в массиве вычисляется следующим образом:

int index = 4 * (y * bitmap.PixelWidth + x);

Далее значение пиксела задается в порядке «синий-зеленый-красный-альфа».

В этом конкретном примере два цикла обращаются к пикселам в порядке их хранения в массиве, так что индекс не приходится пересчитывать заново для каждого пиксела. Его можно инициализировать нулем, а потом увеличивать следующим образом:

int index = 0;
   
for (int y = 0; y < bitmap.PixelHeight; y++)
    for (int x = 0; x < bitmap.PixelWidth; x++)
    {
        int index = 4 * (y * bitmap.PixelWidth + x);
        pixels[index++] = (byte)x;    // Blue
        pixels[index++] = 0;          // Green
        pixels[index++] = (byte)y;    // Red
        pixels[index++] = 255;        // Alpha
    }

Такое решение почти наверняка будет работать быстрее моего, но в целом оно обладает меньшей гибкостью.

Также можно определить цикл по index с вычислением x и y по текущему значению переменной. Важна не конкретная реализация, а то, чтобы в результате перебора были обработаны все пикселы (не всегда, конечно, но в большинстве случаев).

После того, как массив byte будет заполнен, пикселы необходимо перенести в объект WriteableBitmap. Этот процесс на первый взгляд выглядит довольно странно. Свойство PixelBuffer, определяемое WriteableBitmap, относится к типу IBuffer, который определяет всего два свойства: Capacity и Length. Как было указано ранее, объект IBuffer обычно представляет область памяти, находящуюся под управлением операционной системы с механизмом подсчета ссылок; когда память становится ненужной, объект автоматически уничтожается. Байты необходимо перенести в такой буфер.

К счастью, существует метод расширения AsStream, позволяющий интерпретировать объект IBuffer как объект .NET Stream:

Stream pixelStream = bitmap.PixelBuffer.AsStream()

Чтобы использовать этот метод расширения, необходимо включить в программу директиву using для пространства имен System.Runtime.InteropServices.WindowsRuntime. Без этой директивы InlelliSense не будет знать о существовании этого метода.

Далее обычный метод Write, определяемый классом Stream, используется для записи байтового массива в объект Stream; также можно использовать метод WriteAsync, как сделано у меня. Так как изображение невелико, а вызов просто передает массив байтов через API, метод Write отработает достаточно быстро для выполнения операции в потоке пользовательского интерфейса. Далее объект Stream уничтожается «вручную» или автоматически, или же логика Stream размешается в директиве using, как это сделано у меня:

using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
{
    await pixelStream.WriteAsync(pixels, 0, pixels.Length);
}

Привыкните к тому, что при каждом изменении пикселов WriteableBitmap следует вызывать для изображения Invalidate:

bitmap.Invalidate();

Этот вызов требует перерисовки растрового изображения. В этом конкретном контексте его присутствие не обязательно, но в других случаях он важен. Остается вывести построенное изображение. Программа просто задает его свойству Source элемента Image в файле XAML:

image.Source = bitmap;

Результат:

Создание градиента с помощью растровой графики в приложении Windows Runtime

Если сохранить объект Stream и массив пикселов в поле для дальнейших манипуляций с растровым изображением (например, его изменения со временем), перед вызовом WriteAsync следует вставить вызов Seek для возвращения текущей позиции к началу:

pixelStream.Seek(0, SeekOrigin.Begin);

Учтите, что в объект растрового изображения можно записать только часть массива байтов. Предположим, вы изменили пикселы в диапазоне от (x1, y1) до (x2, y2) (не включая последнюю точку). Сначала определите индексы байтов, соответствующих этим двум координатам:

int index1 = 4 * (y1 * bitmap.PixelWidth + x1);
int index2 = 4 * (y2 * bitmap.PixelWidth + x2);

Затем укажите, что вы собираетесь обновить пикселы от index1 до index2:

pixelStream.Seek(index, SeekOrigin.Begin);
pixelStream.Write(pixels, index1, index2 - index1);
bitmap.Invalidate();

Попробуем реализовать другую разновидность пользовательского градиента. В следующей программе CircularGradient градиент вычисляется на основании угла конкретного пиксела относительно центра изображения (вычисления проще, чем можно ожидать).

Файл XAML определяет Ellipse с толстым контуром и объектом ImageBrush для свойства Stroke. Анимация поворачивает объект Ellipse относительно центра:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Ellipse Width="580"
                 Height="580"
                 StrokeThickness="45"
                 RenderTransformOrigin="0.5 0.5">
            <Ellipse.Stroke>
                <ImageBrush x:Name="imageBrush" />
            </Ellipse.Stroke>

            <Ellipse.RenderTransform>
                <RotateTransform x:Name="rotateTransform" />
            </Ellipse.RenderTransform>
        </Ellipse>
    </Grid>

    <Page.Triggers>
        <EventTrigger>
            <BeginStoryboard>
                <Storyboard>
                    <DoubleAnimation Storyboard.TargetName="rotateTransform"
                                     Storyboard.TargetProperty="Angle"
                                     RepeatBehavior="Forever"
                                     From="0" To="360" Duration="0:0:3" />
                </Storyboard>
            </BeginStoryboard>
        </EventTrigger>
    </Page.Triggers>
</Page>

Обработчик Loaded в файле фонового кода почти не отличается от предыдущей программы. Два цикла перебирают строки и столбцы изображения, каждый пиксел расположен в позиции (x,y) относительно левого верхнего угла. Пиксел в центре имеет координаты (bitmap.PixelWidth/2, bitmap.PixelHeight/2). В результате вычитания координат центра из координат конкретного пиксела и деления на ширину и высоту изображения координаты пиксела преобразуются в значения из диапазона от -1/2 до 1/2, которые затем можно передать методу Math.Atan2 дли получения нужного угла:

using System.IO;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Controls;
using System.Runtime.InteropServices.WindowsRuntime;
using System;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += MainPage_Loaded;
        }

        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            WriteableBitmap bitmap = new WriteableBitmap(256, 256);
            byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];
            int index = 0;
            int centerX = bitmap.PixelWidth / 2;
            int centerY = bitmap.PixelHeight / 2;

            for (int y = 0; y < bitmap.PixelHeight; y++)
                for (int x = 0; x < bitmap.PixelWidth; x++)
                {
                    double angle =
                        Math.Atan2(((double)y - centerY) / bitmap.PixelHeight,
                                   ((double)x - centerX) / bitmap.PixelWidth);

                    double fraction = angle / (2 * Math.PI);

                    pixels[index++] = (byte)(fraction * 255);       // Blue
                    pixels[index++] = 0;                            // Green
                    pixels[index++] = (byte)(255 * (1 - fraction)); // Red
                    pixels[index++] = 255;                          // Alpha
                }

            using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
            {
                await pixelStream.WriteAsync(pixels, 0, pixels.Length);
            }

            bitmap.Invalidate();
            imageBrush.ImageSource = bitmap;
        }
    }
}

Этот угол преобразуется в дробную величину в диапазоне от 0 до 1 для вычисления градиента. Вот как выглядит полное растровое изображение, используемое объектом ImageBrush, заданным свойству Fill объекта Ellipse:

Создание растрового градиента для окружности в приложении Windows Runtime

Как было показано ранее, кисти в Windows Runtime обычно растягиваются по элементу, к которому они применяются. С кистью ImageBrush происходит то же самое, так что в каком-то смысле размер базового изображения не так уж важен... До определенной степени, конечно, — слишком маленькое изображение не обладает достаточной детализацией, а слишком большое превращается в напрасную трату пикселов.

Когда растровое изображение отображается на поверхности (скажем, на экране монитора), его пикселы не всегда просто передаются на поверхность. Если растровое изображение поддерживает прозрачность, каждый пиксел должны объединяться с цветом существующей поверхности в соответствующей точке на основании альфа-канала этого пиксела. Если альфа-канал равен 255 (полная непрозрачность), пиксел изображения просто копируется на поверхность. Если альфа-канал равен 0 (прозрачность), пиксел вообще не копируется. Если альфа-канал равен 128, результатом является среднее значение цвета пиксела изображения и цвета поверхности перед выводом.

Следующие формулы демонстрируют соответствующие вычисления для одного пиксела. В реальности значения A, R, G и B лежат в диапазоне от 0 до 255, но следующие упрощенные формулы предполагают, что они были нормализованы до диапазона от 0 до 1. Подстрочные пояснения обозначают «результат» отображения частично прозрачного пиксела «изображения» на существующей «поверхности»:

Обратите внимание на второе умножение в каждой строке. В нем задействован только сам пиксел изображения, но не поверхность. Отсюда следует, что весь процесс отображения растрового изображения на поверхности можно ускорить предварительным умножением значений R, G и В пиксела на величину А:

Допустим, растровое изображение без предумножения альфа-канала содержит пиксел со значением ARGB (192,40,60,255). Альфа-канал 192 обозначает 75-процентную непрозрачность (192, деленное на 255). Эквивалентный пиксел с предумножением альфа-канала имеет вид (192, 30, 45, 192): красная, зеленая и синяя составляющие были умножены на 75 %.

При отображении WriteableBitmap операционная система предполагает, что пиксел имеет формат с предумножением альфа-канала. У произвольного пиксела ни одно из значений R, G и В не может превышать значение А. Если это условие не выполняется, ничего ужасного не случится, но вы не получите желаемые цвета и уровни прозрачности.

Рассмотрим несколько примеров. В статье "Масштабирование элементов в WinRT" было показано, как перевернуть изображение и «растворить» его, чтобы оно выглядело как отражение. Но поскольку Windows Runtime не поддерживает маски прозрачности, для реализации эффекта прозрачности мне пришлось накрыть изображение полупрозрачным прямоугольником.

В проекте ReflectedAlphaImage было использовано другое решение. Файл XAML содержит два элемента Image, занимающих одну ячейку панели Grid из двух строк. Для второго элемента Image задаются свойства RenderTransformOrigin и ScaleTransform, обеспечивающие его «отражение» относительно нижней стороны, но изображение при этом не указывается:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Grid.RowDefinitions>
            <RowDefinition />
            <RowDefinition />
        </Grid.RowDefinitions>

        <Image Source="http://professorweb.ru/downloads/ted-image.jpg"
               HorizontalAlignment="Center" />

        <Image Name="reflectedImage" 
               RenderTransformOrigin="0 1"
               HorizontalAlignment="Center">
            <Image.RenderTransform>
                <ScaleTransform ScaleY="-1" />
            </Image.RenderTransform>
        </Image>
    </Grid>
</Page>

Растровое изображение, на которое ссылается первый элемент Image, должно загружаться независимо в файле фонового кода. (Возможно, у вас возник вопрос — нельзя ли получить объект WriteableBitmap на основе объекта, заданного свойству Source первого объекта Image? Но этот объект относится к типу BitmapSource, а создать WriteableBitmap по BitmapSource невозможно.) Если изменять загруженное изображение не требуется, конструктор может выглядеть примерно так:

Loaded += async (sender, e) =>
    {
        Uri uri = new Uri("http://professorweb.ru/downloads/ted-image.jpg");
        RandomAccessStreamReference refStream = RandomAccessStreamReference.CreateFromUri(uri);
        IRandomAccessStreamWithContentType fileStream = await refStream.OpenReadAsync();
        WriteableBitmap bitmap = new WriteableBitmap(1, 1);
        bitmap.SetSource(fileStream);
        reflectedImage.Source = bitmap;
    };

Этот код следует поместить в обработчик Loaded, потому что в нем используется асинхронное выполнение. Обратите внимание на возможность создания WriteableBitmap с фактически «неизвестным» размером при поступлении данных из метода SetSource. Читая поток JPEG, объект WriteableBitmap может определить фактические размеры в пикселах.

Однако когда объект FileStream передается методу SetSource объекта WriteableBitmap, а также при его задании свойству Source элемента Image, растровое изображение еще не загружено. Загрузка осуществляется асинхронно в коде WriteableBitmap. Это означает, что приступать к изменению пикселов пока нельзя, потому что данные еще не получены! Конечно, было бы удобно, если бы класс WriteableBitmap определял событие, инициируемое при завершении загрузки растрового изображения в SetSource, но такого события нет. Событие ImageOpened элемента Image также не может предоставить эту информацию WriteableBitmap.

Итак, нам остается загрузить растровый файл самостоятельно, а потом внести в него изменения. Код, который я собираюсь привести, можно немного упростить при помощи вспомогательных классов, упоминавшихся ранее, но давайте сначала посмотрим, как это делается без классов. Процесс выглядит так:

using System.IO;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Media.Imaging;
using Windows.UI.Xaml.Controls;
using System.Runtime.InteropServices.WindowsRuntime;
using System;
using Windows.Storage.Streams;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
            Loaded += MainPage_Loaded;
        }

        private async void MainPage_Loaded(object sender, RoutedEventArgs e)
        {
            Uri uri = new Uri("http://professorweb.ru/downloads/ted-image.jpg");
            RandomAccessStreamReference refStream = RandomAccessStreamReference.CreateFromUri(uri);

            // Создание буфера для чтения потока
            Windows.Storage.Streams.Buffer buffer = null;

            // Чтение всего файла
            using (IRandomAccessStreamWithContentType fileStream = await refStream.OpenReadAsync())
            {
                buffer = new Windows.Storage.Streams.Buffer((uint)fileStream.Size);
                await fileStream.ReadAsync(buffer, (uint)fileStream.Size, InputStreamOptions.None);
            }

            // Создание объекта WriteableBitmap с неизвестным размером
            WriteableBitmap bitmap = new WriteableBitmap(1, 1);

            // Создание потока памяти для передачи данных
            using (InMemoryRandomAccessStream memoryStream = new InMemoryRandomAccessStream())
            {
                await memoryStream.WriteAsync(buffer);
                memoryStream.Seek(0);

                // Поток в памяти используется как источник данных Bitmap
                bitmap.SetSource(memoryStream);
            }

            // Получение пикселов из растрового изображения
            byte[] pixels = new byte[bitmap.PixelWidth * bitmap.PixelHeight * 4];
            int index = 0;

            using (Stream pixelStream = bitmap.PixelBuffer.AsStream())
            {
                await pixelStream.ReadAsync(pixels, 0, pixels.Length);

                // Применение прозрачности к пикселам
                for (int y = 0; y < bitmap.PixelHeight; y++)
                {
                    double opacity = (double)y / bitmap.PixelHeight;

                    for (int x = 0; x < bitmap.PixelWidth; x++)
                        for (int i = 0; i < 4; i++)
                        {
                            pixels[index] = (byte)(opacity * pixels[index]);
                            index++;
                        }
                }

                // Пикселы помещаются обратно в изображение
                pixelStream.Seek(0, SeekOrigin.Begin);
                await pixelStream.WriteAsync(pixels, 0, pixels.Length);
            }

            bitmap.Invalidate();
            reflectedImage.Source = bitmap;
        }
    }
}

Имя класса Buffer должно задаваться полностью уточненным, с включением пространства имен Windows.Storage.Streams, потому что в пространстве имен System тоже присутствует класс с именем Buffer.

Одна из наших целей - передача объекта типа IRandomAccessStream методу SetSource объекта WriteableBitmap. Однако мы хотим немедленно приступить к работе с пикселами полученного изображения, а это невозможно, пока файл не будет прочитан полностью.

Этим объясняется создание объекта Buffer для чтения объекта fileStream и последующее использование того же объекта Buffer для чтения содержимого в InMemoryRandomAccessStream. Как подсказывает название, класс InMemoryRandomAccessStream реализует интерфейс IRandomAccessStream, чтобы его экземпляры можно было передавать методу SetSource класса WriteableBitmap (обратите внимание на необходимость предварительного обнуления позиции в потоке).

Важно понимать, что здесь мы работаем с двумя разными блоками данных. Объект fileStream соответствует файлу PNG, который в данном случае представляет собой блок из 82 824 байт сжатых графических данных. Объект InMemoryRandomAccessStream содержит тот же блок данных. После того как поток будет передан методу SetSource класса WriteableBitmap, он декодируется на строки и столбцы пикселов. Размер массива pixels составляет 512 000 байт, и объект pixelStream работает с этими распакованными пикселами. Объект pixelStream сначала используется для чтения пикселов в массив pixels, а затем для их записи обратно в изображение.

Если вы хотите посмотреть, что произойдет при изменении одного лишь альфа байта, замените следующий код внутреннего цикла:

if (i == 3) {
   pixels[index] = (byte)(opacity * pixels[index]);
   index++;
}

Между двумя вызовами выполняется непосредственное применение градиентной прозрачности. Если бы среда Windows Runtime не предполагала, что пикселы WriteableBitmap хранятся в формате с предумножением альфа-канала, то достаточно было бы модифицировать только альфа-байт. С предумножением альфа-канала также необходимо модифицировать и цветовые данные. Результат выглядит так:

Создание маски прозрачности в Windows Runtime

Нужный уровень прозрачности достигается, но только при использовании белого фона. Если фон будет черным, прозрачности не будет вообще! Взгляните на формулы, и вам все станет ясно.

Предположим, вы хотите изменить проект CircularGradient так, чтобы в нем использовался градиент от однородного цвета к полной прозрачности. Измененный код для задания четырех байтов выглядит так:

pixels[index++] = (byte)(fraction * 255);       // Blue
pixels[index++] = 0;        // Green
pixels[index++] = 0;        // Red
pixels[index++] = (byte)(fraction * 255);       // Alpha

Синему и альфа-компоненту присваиваются одинаковые значения. В формате без предумножения альфа-канала синяя составляющая всегда будет равна 255. Результат:

Создание синего прозрачного градиента
Пройди тесты
Лучший чат для C# программистов