Обратное рисование в WinRT

83

Мне однажды попался видеоролик, в котором большой цветной рисунок на стене закрашивался валиком с белой краской. Фильм воспроизводился в обратном направлении, и зрителю казалось, что роспись, словно по волшебству, возникает из-под валика при его прохождении по белой поверхности.

Аналогичный прием продемонстрирован в программе ReversePaint. Файл XAML содержит ссылку на растровое изображение с моего веб-сайта, а также определяет другой элемент Image, расположенный поверх него:

<Page ...>

    <Grid Background="#FF1D1D1D">
        <Image Source="http://professorweb.ru/downloads/ted-image.jpg" />
        <Image Name="whiteImg" />
    </Grid>
</Page>

Изображение второго элемента Image создается в обработчике Loaded с таким же размером, как у загруженного изображения, и состоит исключительно из пикселов белого цвета. Как и в приложении FingerPaint, имеется метод CalculateImageScaleAndOffset, который вычисляет множители для масштабирования ввода по координатам изображения. Чтобы упростить код, я удалил из обработчиков событий комментарии, но общая логика вам уже знакома. Метод OnPointerMoved вызывает упрощенную форму RenderOnBitmap с двумя точками, фиксированной толщиной линии и значением Color, представляющим прозрачность:

using System;
using System.Collections.Generic;
using System.IO;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media.Imaging;
using System.Runtime.InteropServices.WindowsRuntime;

namespace WinRTTestApp
{
    public sealed partial class MainPage : Page
    {
        Dictionary<uint, Point> pointerDic = new Dictionary<uint, Point>();
        List<double> xCollection = new List<double>();

        Stream pixelStream;
        WriteableBitmap bitmap;
        byte[] pixels;

        Point imageOffset = new Point();
        double imageScale = 1;

        public MainPage()
        {
            this.InitializeComponent();

            SizeChanged += OnMainPageSizeChanged;
            Loaded += OnMainPageLoaded;
        }

        private void OnMainPageSizeChanged(object sender, SizeChangedEventArgs e)
        {
            if (bitmap != null)
                CalculateImageScaleAndOffset();
        }

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

            // Инициализация пикселов белым цветом
            for (int index = 0; index < pixels.Length; index++)
                pixels[index] = 0xFF;

            pixelStream = bitmap.PixelBuffer.AsStream();
            await pixelStream.WriteAsync(pixels, 0, pixels.Length);
            bitmap.Invalidate();

            // Изображение связывается с элементом Image
            whiteImg.Source = bitmap;
            CalculateImageScaleAndOffset();
        }

        private void CalculateImageScaleAndOffset()
        {
            imageScale = Math.Min(this.ActualWidth / bitmap.PixelWidth,
                                  this.ActualHeight / bitmap.PixelHeight);
            imageOffset = new Point((this.ActualWidth - imageScale * bitmap.PixelWidth) / 2,
                                    (this.ActualHeight - imageScale * bitmap.PixelHeight) / 2);
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs e)
        {
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;
            pointerDic.Add(id, point);
            CapturePointer(e.Pointer);
            base.OnPointerPressed(e);
        }

        protected override void OnPointerMoved(PointerRoutedEventArgs e)
        {
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;

            if (pointerDic.ContainsKey(id))
            {
                Point previousPoint = pointerDic[id];

                // Вывод линии
                RenderOnBitmap(previousPoint, point, 12, new Color());

                pointerDic[id] = point;
            }
            base.OnPointerMoved(e);
        }

        protected override void OnPointerReleased(PointerRoutedEventArgs e)
        {
            uint id = e.Pointer.PointerId;

            if (pointerDic.ContainsKey(id))
                pointerDic.Remove(id);

            base.OnPointerReleased(e);
        }

        protected override void OnPointerCaptureLost(PointerRoutedEventArgs e)
        {
            uint id = e.Pointer.PointerId;

            if (pointerDic.ContainsKey(id))
                pointerDic.Remove(id);

            base.OnPointerCaptureLost(e);
        }

        private void RenderOnBitmap(Point point1, Point point2, double radius, Color color)
        {
            bool bitmapNeedsUpdate = false;

            // Поправка на масштабирование элемента
            Point center1 = ScaleToBitmap(point1);
            Point center2 = ScaleToBitmap(point2);

            // Создание объекта, представляющего линию
            RoundCappedLine line = new RoundCappedLine(center1, center2, radius);

            // Определение минимальной и максимальной вертикальных координат
            int yMin = (int)Math.Min(center1.Y - radius, center2.Y - radius);
            int yMax = (int)Math.Max(center1.Y + radius, center2.Y + radius);

            yMin = Math.Max(0, Math.Min(bitmap.PixelHeight, yMin));
            yMax = Math.Max(0, Math.Min(bitmap.PixelHeight, yMax));

            // Перебор всех координат y, содержащих часть сегмента
            for (int y = yMin; y < yMax; y++)
            {
                // Получение диапазона координат x в сегменте
                xCollection.Clear();
                line.GetAllX(y, xCollection);

                if (xCollection.Count == 2)
                {
                    // Определение минимальной и максимальной горизонтальных координат
                    int xMin = (int)(Math.Min(xCollection[0], xCollection[1]) + 0.5f);
                    int xMax = (int)(Math.Max(xCollection[0], xCollection[1]) + 0.5f);

                    xMin = Math.Max(0, Math.Min(bitmap.PixelWidth, xMin));
                    xMax = Math.Max(0, Math.Min(bitmap.PixelWidth, xMax));

                    // Перебор значений x
                    for (int x = xMin; x < xMax; x++)
                    {
                        {
                            // Задание пиксела
                            int index = 4 * (y * bitmap.PixelWidth + x);
                            pixels[index + 0] = color.B;
                            pixels[index + 1] = color.G;
                            pixels[index + 2] = color.R;
                            pixels[index + 3] = color.A;
                            bitmapNeedsUpdate = true;
                        }
                    }
                }
            }

            // Обновить изображение
            if (bitmapNeedsUpdate)
            {
                // Определение начального индекса и количества пикселов
                int start = 4 * yMin * bitmap.PixelWidth;
                int count = 4 * (yMax - yMin) * bitmap.PixelWidth;

                pixelStream.Seek(start, SeekOrigin.Begin);
                pixelStream.Write(pixels, start, count);
                bitmap.Invalidate();
            }
        }

        private Point ScaleToBitmap(Point pt)
        {
            return new Point((pt.X - imageOffset.X) / imageScale,
                             (pt.Y - imageOffset.Y) / imageScale);
        }
    }
}

Метод RenderOnBitmap проще аналогичного метода из приложения FingerPaint, потому что линии имеют постоянную толщину, поэтому везде используется класс RoundCappedLine. Результат «рисования» на белом покрытии нескольких «линий» из прозрачных пикселов:

Пример использования ластика

Обратите внимание на то, как метод PointerMoved вызывает RenderOnBitmap:

RenderOnBitmap(previousPoint, point, 12, new Color());

Конструктор Color создает значение Color, у которого свойства A, R, G и B равны нулю — этот цвет иногда называют «прозрачным черным». Для помещения цветовых данных в объект WriteableBitmap этот конструктор Color подходит гораздо лучше статического свойства Colors.Transparent. Свойство Colors.Transparent возвращает значение Color, у которого свойство A равно нулю, а свойства R, G и B равны 255. Этот цвет иногда называют «прозрачным белым», но он не является прозрачным цветом в формате с предумножением альфа-канала! Для WriteableBitmap необходимы цвета с предумножением альфа-канала, а это означает, что ни одно из свойств R, G и B не может быть больше A.

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