Сглаживание мазков кисти и сила нажатия в WinRT

175

Линии, нарисованные разными программами FingerPaint, имеют одинаковую толщину (24 пиксела), но некоторые сенсорные устройства могут отличать сильные нажатия от слабых, и действительно качественная программа должна реагировать изменением толщины линии.

Существует два свойства, которые могут влиять на толщину линий в программе рисования. Оба свойства определяются объектом PointerPointProperties, возвращаемым свойством Properties класса PointerPoint (который, в свою очередь, возвращается вызовом метода GetCurrentPoint() аргументов события PointerRoutedEventArgs).

Первое свойство ContactRect (тип Rect) предназначено для передачи ограничивающего прямоугольника контактной области пальца (или пера) на экране. Вероятно, это устройство актуально только для самых экзотических сенсорных устройств. На планшете, который я использовал для большинства программ в книге, этот объект Rect всегда имеет нулевые свойства Width и Height независимо от устройства ввода. В первых версиях планшета Microsoft Surface свойства Width и Height содержали малые целые числа (1,2,3...), которые вроде бы не несли полезной информации (впрочем, я могу ошибаться).

Второе свойство Pressure (тип float) может принимать значения в диапазоне от 0 до 1. На планшете, который использовался для тестирования примеров, оно имело постоянное значение 0,5 для пальцев и мыши, но для пера принимало переменные значения, так что у меня была возможность опробовать его. (На первых версиях планшета Microsoft Surface значение Pressure всегда равно 0,5.)

Для простоты программа FingerPaint4 не поддерживает отмену по клавише Esc или редактирование, но реализует захват указателя. Важное отличие заключается в том, что мне пришлось отказаться от рисования с использованием Polyline, потому что Polyline имеет единственное свойство StrokeThickness. В новой программе каждый штрих должен состоять из очень коротких отдельных отрезков с уникальным значением StrokeThickness, вычисленным по значению Pressure, но имеющих одинаковый цвет. Это подразумевает, что в словаре должны храниться значения типа Color (а еще лучше Brush) и предыдущая точка. Так как одно значение теперь заменяется парой, для этой цели стоит определить специальную структуру, которую я назвал PointerInfo:

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Popups;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;
using Windows.UI.Xaml.Input;

namespace WinRTTestApp
{
    public struct PointerInfo
    {
        public Point PreviousPoint;
        public Brush Brush;
    }

    public sealed partial class MainPage : Page
    {
        Dictionary<uint, PointerInfo> pointerDic = new Dictionary<uint, PointerInfo>();
        Random random = new Random();
        byte[] rgb = new byte[3];

        public MainPage()
        {
            InitializeComponent();
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;

            // Создание случайного цвета
            random.NextBytes(rgb);
            Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);

            // Создание объекта PinterInfo
            PointerInfo pinfo = new PointerInfo
            {
                PreviousPoint = point,
                Brush = new SolidColorBrush(color)
            };

            // Включить в словарь
            pointerDic.Add(id, pinfo);

            // Захват указателя
            CapturePointer(e.Pointer);

            base.OnPointerPressed(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);
        }

        protected override void OnKeyDown(KeyRoutedEventArgs e)
        {
            if (e.Key == Windows.System.VirtualKey.Escape)
                ReleasePointerCaptures();

            base.OnKeyDown(e);
        }
        
        // ...
    }
}

Ранние версии PointerPressed создавали объект Polyline, задавали исходную точку и добавляли его на панель Grid и в словарь. В этой программе создается только значение PointerInfo, которое добавляется в словарь.

Гораздо больше полезной работы выполняется в обработчике PointerMoved - прежде всего из-за того, что я решил использовать метод GetIntermediatePoints вместо GetCurrentPoint, что должно было привести (по крайней мере теоретически) к рисованию более плавных линий на Microsoft Surface. Но тут я неожиданно обнаружил одну странность: точки в коллекции следуют в обратном порядке!

Следующий код в цикле перебирает точки. Для каждой новой точки и предыдущей точки строится элемент Line, который добавляется на панель Grid. Затем последняя точка заменяет предыдущую в значении PointerInfo:

protected override void OnPointerMoved(PointerRoutedEventArgs e)
{
    // Получение информации из аргументов события
    uint id = e.Pointer.PointerId;

    // Если идентификатор хранится в словаре, 
    // начинается цикл
    if (pointerDic.ContainsKey(id))
    {
        PointerInfo pointerInfo = pointerDic[id];
        IList<PointerPoint> pointerpts =e.GetIntermediatePoints(this);

        for (int i = pointerpts.Count - 1; i >= 0; i--)
        {
            PointerPoint pointerPoint = pointerpts[i];

            // Для каждой точки создается новый элемент Line,
            // который добавляется на панель Grid
            Point point = pointerPoint.Position;
            float pressure = pointerPoint.Properties.Pressure;

            Line line = new Line
            {
                Stroke = pointerInfo.Brush,
                StrokeThickness = pressure * 24,
                StrokeStartLineCap = PenLineCap.Round,
                StrokeEndLineCap = PenLineCap.Round,
                X1 = pointerInfo.PreviousPoint.X,
                Y1 = pointerInfo.PreviousPoint.Y,
                X2 = point.X,
                Y2 = point.Y
            };
            grid.Children.Add(line);

            // Обновить PointerInfo 
            pointerInfo.PreviousPoint = point;
        }
        // Сохранение PointerInfo в словаре
        pointerDic[id] = pointerInfo;
    }

    base.OnPointerMoved(e);
}

Свойству StrokeThickness задается значение Pressure, увеличенное в 24 раза. Произведение дает максимальную ширину штриха 24 при ширине 12 для устройств, не различающих силу нажатия. Также обратите внимание на то, что свойствам StrokeStartLineCap и StrokeEndLineCap задается значение Round. Попробуйте закомментировать задание этих свойств и посмотрите, что происходит: между отрезками, расположенными под углом, появляются «щели». Круглые завершители закрывают их. Небольшая... ммм... художественная работа, нарисованная пером.

Рисование пером в приложении Windows Runtime

Обратите внимание на изящную плавность линий, отображаемых на устройстве, распознающим силу нажатия.

Насколько мне известно, события PointerMoved могут выдаваться с частотой до 100 раз в секунду, то есть чаще частоты обновления экрана, но недостаточно часто для очень энергичных пальцев.

Сглаживание изменения толщины

Решение одной проблемы нередко открывает другую проблему. Безусловно, распознавание силы нажатия является важной особенностью программы рисования. Тем не менее при очень быстром рисовании штрихов в программе FingerPaint4 вы заметите, что толщина линий увеличивается и уменьшается с дискретным приращением.

Неплавное изменение толщины мазков в приложении Windows Runtime

И это понятно — каждый фрагмент в правой нижней части представляет собой отдельный элемент Line с собственным атрибутом StrokeThickness. Я рисовал эту закорючку с такой скоростью, чтобы сила нажатия значительно изменялась между событиями, из-за чего в приращениях толщины возникают значительные расхождения.

Если считать, что отдельный элемент Line может иметь только одно постоянное значение StrokeThickness, проблема кажется трудноразрешимой. Однако на самом деле решение выглядит просто (по крайней мере концептуально): вместо того чтобы рисовать Line для каждого события, следует нарисовать заполненную фигуру Path из двух дуг разной кривизны, соединенных двумя линиями. Чтобы немного упростить эту задачу, можно использовать структуру Vector, которая должна присутствовать во всех современных операционных системах, но которой нет в Windows Runtime. Ниже приведена структура Vector2 («2» для двух измерений):

using System;
using Windows.Foundation;
using Windows.UI.Xaml.Media;

namespace WinRTTestApp
{
    public struct Vector2
    {
        // Конструкторы
        public Vector2(Point p) : this()
        {
            X = p.X;
            Y = p.Y;
        }

        public Vector2(double x, double y) : this()
        {
            X = x;
            Y = y;
        }       

        public Vector2(double angle) : this()
        {
            X = Math.Cos(Math.PI * angle / 180);
            Y = Math.Sin(Math.PI * angle / 180);
        }

        // Свойства
        public double X { private set; get; }
        public double Y { private set; get; }

        public double LengthSquared
        {
            get { return X * X + Y * Y; }
        }

        public double Length
        {
            get { return Math.Sqrt(LengthSquared); }
        }

        public Vector2 Normalized
        {
            get
            {
                double length = this.Length;

                if (length != 0)
                {
                    return new Vector2(this.X / length, this.Y / length);
                }
                return new Vector2();
            }
        }

        // Методы
        public Vector2 Rotate(double angle)
        {
            RotateTransform xform = new RotateTransform { Angle = angle };
            Point pt = xform.TransformPoint(new Point(X, Y));
            return new Vector2(pt.X, pt.Y);
        }

        // Статические методы
        public static double AngleBetween(Vector2 v1, Vector2 v2)
        {
            return 180 * (Math.Atan2(v2.Y, v2.X) - Math.Atan2(v1.Y, v1.X)) / Math.PI;
        }

        // Перегрузка операторов
        public static Vector2 operator +(Vector2 v1, Vector2 v2)
        {
            return new Vector2(v1.X + v2.X, v1.Y + v2.Y);
        }

        public static Point operator +(Vector2 v, Point p)
        {
            return new Point(v.X + p.X, v.Y + p.Y);
        }

        public static Point operator +(Point p, Vector2 v)
        {
            return new Point(v.X + p.X, v.Y + p.Y);
        }

        public static Vector2 operator -(Vector2 v1, Vector2 v2)
        {
            return new Vector2(v1.X - v2.X, v1.Y - v2.Y);
        }

        public static Point operator -(Point p, Vector2 v)
        {
            return new Point(p.X - v.X, p.Y - v.Y);
        }

        public static Vector2 operator *(Vector2 v, double d)
        {
            return new Vector2(d * v.X, d * v.Y);
        }

        public static Vector2 operator *(double d, Vector2 v)
        {
            return new Vector2(d * v.X, d * v.Y);
        }

        public static Vector2 operator /(Vector2 v, double d)
        {
            return new Vector2(v.X / d, v.Y / d);
        }

        public static Vector2 operator -(Vector2 v)
        {
            return new Vector2(-v.X, -v.Y);
        }

        public static explicit operator Point(Vector2 v)
        {
            return new Point(v.X, v.Y);
        }

        // Переопределения
        public override string ToString()
        {
            return String.Format("({0} {1})", X, Y);
        }
    }
}

Проект FingerPaint5 сохраняет предыдущий радиус (определяемый силой нажатия) вместе с предыдущей точкой. На этой диаграмме я представил два последовательных местонахождения пальца в виде кругов с независимыми радиусами: меньший круг с центром c0 и радиусом r0 и больший круг с центром c1 и радиусом r1:

Наглядное отображение обработки нажатий с разным усилием

Наша цель — построить объект Path, вмещающий эти два круга и область между ними. Для этого необходимо соединить два круга линиями, касательными к обоим кругам, а это не так просто (с математической точки зрения). Для начала соединим центры двух кругов линией d:

Соединение точек касания

По значению Vector2 можно получить длину этой линии и нормализованный вектор, представляющий ее направление:

Vector2 vectorCenters = new Vector2(c0) - new Vector2(c1);
double d = vectorCenters.Length;
vectorCenters = vectorCenters.Normalized;

Теперь определим другое расстояние e на основании d и радиусов двух кругов. Точка F находится на расстоянии e от c0 в том же направлении, что и вектор, соединяющий два центра:

double e = d * r0 / (r1 - r0);
Point F = c0 + e * vectorCenters;

Вот как это выглядит:

Определение фокуса ввода

Я назвал эту точку F («точка фокуса»). Я утверждаю, что существуют линии, начинающиеся в точке F, касательные к обеим окружностям, то есть образующие прямой угол с радиусом:

Касательные к точке фокуса

Данный факт обусловлен способом определения e. Отношение e к r0 равно отношению суммы d и e к r1. Угол a (в правой части фигуры) вычисляется по формуле:

double alpha = 180 * Math.Asin(r0 / e) / Math.PI;

Если аргумент метода Math.Asin больше 1, метод возвращает NaN («не число»). Это может произойти только в том случае, если сумма r0 и d меньше r1, то есть меньший круг полностью заключен в большем, что позволяет легко выявить эту проблему.

Длины сторон треугольников от точки F до точки касания вычисляются по теореме Пифагора:

double leg0 = Math.Sqrt(e * e - r0 * r0);
double leg1 = Math.Sqrt((e + d) * (e + d) - r1 * r1);

Структура Vector2 содержит удобный метод Rotate, позволяющий повернуть вектор vCenters на α и α-градусов:

Vector2 vectorRight = -vectorCenters.Rotate(alpha);
Vector2 vectorLeft = -vectorCenters.Rotate(-alpha);

«Правая» и «левая» части в именах переменных определяются относительно F. На рисунке вектор vRight соответствует верхней, a vLeft — нижней касательной. По векторам и длинам вычисляются фактические точки касания:

Point p0R = F + leg0 * vectorRight;
Point p0L = F + leg0 * vectorLeft;
Point p1R = F + leg1 * vectorRight;
Point p1L = F + leg1 * vectorLeft;

Далее эти точки могут использоваться для конструирования объекта PathGeometry, состоящего из двух объектов ArcSegment и двух объектов LineSegment (выделен жирным контуром):

Дуга, построенная из двух касательных

Обратите внимание: угловая величина ArcSegment меньшего круга всегда меньше 180°, а у большего круга она всегда превышает 180°. Эти характеристики влияют на свойство IsLargeArc объекта ArcSegment. Также следует помнить, что один из двух объектов LineSegment может быть создан автоматически, если указать, что фигура должна быть замкнутой.

Ниже приведен непосредственный алгоритм, использованный в приложении FingerPaint5. Не забудьте о необходимости реализации относительно простых случаев с совпадением двух радиусов или нахождении одного круга в другом:

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Input;
using Windows.UI.Popups;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public struct PointerInfo
    {
        public Point PreviousPoint;
        public Brush Brush;
    }

    public sealed partial class MainPage : Page
    {
        Dictionary<uint, PointerInfo> pointerDic = new Dictionary<uint, PointerInfo>();
        Random random = new Random();
        byte[] rgb = new byte[3];

        public MainPage()
        {
            InitializeComponent();
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;
            Point point = e.GetCurrentPoint(this).Position;

            // Создание случайного цвета
            random.NextBytes(rgb);
            Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);

            // Создание объекта PinterInfo
            PointerInfo pinfo = new PointerInfo
            {
                PreviousPoint = point,
                Brush = new SolidColorBrush(color)
            };

            // Включить в словарь
            pointerDic.Add(id, pinfo);

            // Захват указателя
            CapturePointer(e.Pointer);

            base.OnPointerPressed(e);

        }

        private void polyline_PointerPressed(object sender, PointerRoutedEventArgs e)
        {
            e.Handled = true;
        }

        private async void polyline_RightTapped(object sender, RightTappedRoutedEventArgs e)
        {
            Polyline polyline = sender as Polyline;
            PopupMenu popupMenu = new PopupMenu();
            popupMenu.Commands.Add(new UICommand("Изменить цвет", menu_ChangeColor, polyline));
            popupMenu.Commands.Add(new UICommand("Удалить", menu_Delete, polyline));
            await popupMenu.ShowAsync(e.GetPosition(this));
        }

        private void menu_ChangeColor(IUICommand command)
        {
            Polyline polyline = command.Id as Polyline;
            random.NextBytes(rgb);
            Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);
            (polyline.Stroke as SolidColorBrush).Color = color;
        }

        private void menu_Delete(IUICommand command)
        {
            Polyline polyline = command.Id as Polyline;
            grid.Children.Remove(polyline);
        }

        protected override void OnPointerMoved(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;

            // Если идентификатор хранится в словаре, 
            // начинается цикл
            if (pointerDic.ContainsKey(id))
            {
                PointerInfo pointerInfo = pointerDic[id];
                IList<PointerPoint> pointerpts =e.GetIntermediatePoints(this);

                for (int i = pointerpts.Count - 1; i >= 0; i--)
                {
                    PointerPoint pointerPoint = pointerpts[i];

                    // Для каждой точки создается новый элемент Line,
                    // который добавляется на панель Grid
                    Point point = pointerPoint.Position;
                    float pressure = pointerPoint.Properties.Pressure;

                    Line line = new Line
                    {
                        Stroke = pointerInfo.Brush,
                        StrokeThickness = pressure * 24,
                        StrokeStartLineCap = PenLineCap.Round,
                        StrokeEndLineCap = PenLineCap.Round,
                        X1 = pointerInfo.PreviousPoint.X,
                        Y1 = pointerInfo.PreviousPoint.Y,
                        X2 = point.X,
                        Y2 = point.Y
                    };
                    grid.Children.Add(line);

                    // Обновить PointerInfo 
                    pointerInfo.PreviousPoint = point;
                }
                // Сохранение PointerInfo в словаре
                pointerDic[id] = pointerInfo;
            }

            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);
        }

        protected override void OnKeyDown(KeyRoutedEventArgs e)
        {
            if (e.Key == Windows.System.VirtualKey.Escape)
                ReleasePointerCaptures();

            base.OnKeyDown(e);
        }

        private Geometry CreateLineTaperedGeometry(Point c0, double r0, Point c1, double r1)
        {
            // Центры и радиусы меняются местами, чтобы 
            // точка c0 была центром меньшего круга
            if (r1 < r0)
            {
                double radius = r0;
                r0 = r1;
                r1 = radius;

                Point point = c0;
                c0 = c1;
                c1 = point;
            }

            // Получение вектора из точки c1 в c0
            Vector2 vCenters = new Vector2(c0) - new Vector2(c1);

            // Получение длины и нормализованной версии
            double d = vCenters.Length;
            vCenters = vCenters.Normalized;

            // Проверка возможного нахождения одного круга в границах другого
            bool enclosed = r0 + d < r1;

            // Определение точек касания, вычисленных по двум алгоритмам
            Point t0L = new Point();
            Point t0R = new Point();
            Point t1L = new Point();
            Point t1R = new Point();

            // Два круга имеют одинаковый размеры
            if (r0 == r1 || enclosed)
            {
                // Поворот вектора, соединяющего центры, на 90 градусов
                Vector2 vLeft = new Vector2(-vCenters.Y, vCenters.X);

                // Поворот на -90 градусов
                Vector2 vRight = -vLeft;

                // Определение точек касания
                t0R = c0 + r0 * vRight;
                t0L = c0 + r0 * vLeft;
                t1R = c1 + r1 * vRight;
                t1L = c1 + r1 * vLeft;
            }
            // Для двух кругов разных размеров требуются чуть
            // более сложные вычисления
            else
            {
                // Вычисление точки фокуса F от c0
                double e = d * r0 / (r1 - r0);
                Point F = e * vCenters + c0;

                // Определение угла и длины сторон прямоугольного треугольника
                double leg0 = Math.Sqrt(e * e - r0 * r0);
                double leg1 = Math.Sqrt((e + d) * (e + d) - r1 * r1);
                double alpha = 180 * Math.Asin(r0 / e) / Math.PI;

                // Векторы касательных
                Vector2 vRight = -vCenters.Rotate(alpha);
                Vector2 vLeft = -vCenters.Rotate(-alpha);

                // Определение точек касания
                t0R = F + leg0 * vRight;
                t0L = F + leg0 * vLeft;
                t1R = F + leg1 * vRight;
                t1L = F + leg1 * vLeft;
            }

            // Создание объекта PathGeometry с неявным замыканием
            PathGeometry pathGeometry = new PathGeometry();
            PathFigure pathFigure = new PathFigure
            {
                StartPoint = t0R,
                IsClosed = true,
                IsFilled = true
            };
            pathGeometry.Figures.Add(pathFigure);

            // Дуги для меньшего круга
            ArcSegment arc0Segment = new ArcSegment
            {
                Point = t0L,
                Size = new Size(r0, r0),
                SweepDirection = SweepDirection.Clockwise,
                IsLargeArc = false
            };
            pathFigure.Segments.Add(arc0Segment);

            // Линия, соединяющая меньший круг с большим
            LineSegment lineSegment = new LineSegment
            {
                Point = t1L
            };
            pathFigure.Segments.Add(lineSegment);

            // Дуга для большого круга
            ArcSegment arc = new ArcSegment
            {
                Size = new Size(r1, r1),
                Point = t1R,
                SweepDirection = SweepDirection.Clockwise,
                IsLargeArc = true
            };
            pathFigure.Segments.Add(arc);

            return pathGeometry;
        }
    }
}

Оставшийся код FingerPaintS должен быть полностью понятен к настоящему моменту. Переопределения OnPointerReleased и OnPointerCaptureLost выглядят так же, как в FingerPaint4. Внутренний класс PointerInfo теперь включает поле PreviousRadius:

using System;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI;
using Windows.UI.Input;
using Windows.UI.Popups;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace WinRTTestApp
{
    public struct PointerInfo
    {
        public Point PreviousPoint;
        public Brush Brush;
        public double PreviousRadius;
    }

    public sealed partial class MainPage : Page
    {
        Dictionary<uint, PointerInfo> pointerDic = new Dictionary<uint, PointerInfo>();
        Random random = new Random();
        byte[] rgb = new byte[3];

        public MainPage()
        {
            InitializeComponent();
        }

        protected override void OnPointerPressed(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;
            PointerPoint pointer = e.GetCurrentPoint(this);

            // Создание случайного цвета
            random.NextBytes(rgb);
            Color color = Color.FromArgb(255, rgb[0], rgb[1], rgb[2]);

            // Создание объекта PinterInfo
            PointerInfo pinfo = new PointerInfo
            {
                PreviousRadius = pointer.Properties.Pressure * 24,
                PreviousPoint = pointer.Position,                
                Brush = new SolidColorBrush(color)
            };

            // Включить в словарь
            pointerDic.Add(id, pinfo);

            // Захват указателя
            CapturePointer(e.Pointer);

            base.OnPointerPressed(e);

        }

        protected override void OnPointerMoved(PointerRoutedEventArgs e)
        {
            // Получение информации из аргументов события
            uint id = e.Pointer.PointerId;

            // Если идентификатор хранится в словаре, 
            // начинается цикл
            if (pointerDic.ContainsKey(id))
            {
                PointerInfo pointerInfo = pointerDic[id];
                IList<PointerPoint> pointerpts =e.GetIntermediatePoints(this);

                for (int i = pointerpts.Count - 1; i >= 0; i--)
                {
                    PointerPoint pointerPoint = pointerpts[i];

                    // Для каждой точки создается новый элемент Line,
                    // который добавляется на панель Grid
                    Point point = pointerPoint.Position;
                    float pressure = pointerPoint.Properties.Pressure;
                    double radius = pressure * 24;

                    Geometry geometry = CreateLineTaperedGeometry(pointerInfo.PreviousPoint,
                                                  pointerInfo.PreviousRadius,
                                                  point, radius);

                    Path path = new Path
                    {
                        Data = geometry,
                        Fill = pointerInfo.Brush
                    };
                    grid.Children.Add(path);

                    // Обновить PointerInfo
                    pointerInfo.PreviousPoint = point;
                    pointerInfo.PreviousRadius = radius;
                }
                // Сохранение PointerInfo в словаре
                pointerDic[id] = pointerInfo;
            }

            base.OnPointerMoved(e);
        }

        protected override void OnPointerReleased(PointerRoutedEventArgs e)
        {
            // ...
        }

        protected override void OnPointerCaptureLost(PointerRoutedEventArgs e)
        {
            // ...
        }

        private Geometry CreateLineTaperedGeometry(Point c0, double r0, Point c1, double r1)
        {
            // ...
        }
    }
}

Теперь даже при очень быстром рисовании на устройстве, распознающем силу нажатия, толщина линий изменяется плавно, а не дискретно:

Плавное изменение толщины линий

Как сохранить рисунок?

Ни одна из наших программ не позволяла сохранить рисунок, но как реализовать такую возможность?

Каждая программа рисует посредством добавления на панель Grid объектов Polyline, Line или Path. Один из способов сохранения основан на обращении к этим объектам и сохранении всех точек вместе с прочей информацией в файле (вероятно в формате XML). Также можно добавить функцию загрузки данных и создания новых элементов Polyline, Line или Path на основании этой информации.

Однако может оказаться, что вы предпочитаете сохранить растровое представление своего рисунка. В самом деле, для программы FingerPaint идея выполнения всего графического вывода на растровом изображении выглядит вполне естественно.

Это возможно, но не так просто, как может показаться на первый взгляд. Простейший способ заключается в использовании WriteableBitmap, но вам придется реализовать собственную логику рисования линий на этом растровом изображении. Я покажу, как это делается, чуть позже. Также можно использовать DirectX с написанием части кода на C++.

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