Обратное рисование в WinRT
83Разработка под Windows 8/10 --- Обратное рисование
Мне однажды попался видеоролик, в котором большой цветной рисунок на стене закрашивался валиком с белой краской. Фильм воспроизводился в обратном направлении, и зрителю казалось, что роспись, словно по волшебству, возникает из-под валика при его прохождении по белой поверхности.
Аналогичный прием продемонстрирован в программе 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.