Статьи

Плавное рисование для iOS в Swift с интерполяцией Эрмита Сплайна

Если вы создаете приложение для рисования или рисования для iOS, вы можете вспомнить из  моего недавнего поста, в котором обсуждались объединенные касания,  способ повысить разрешение касания и получить доступ к промежуточным местоположениям касания, которые могли произойти между вызовами. Однако, если ваше приложение просто рисует прямые линии между каждым местом касания, даже слитными, и ваш пользователь быстро перемещает свой палец или карандаш, они увидят, что их рисунок отображается в виде отрезков.

Есть лучшее решение: использовать  сплайн-интерполяцию  для рисования кривых Безье между каждым касанием и визуализации единой непрерывной кривой. В прошлом я обсуждал сплайн-интерполяцию  при рисовании кривой для прохождения всех контрольных точек фильтра Core Image Tone Curve, и этот проект заимствует этот код, но повторно использует его для приложения для рисования:

Мое демонстрационное приложение  предоставляет пользователю два блока, в каждый из которых он может рисовать рисунок, который отражается в другом блоке. Рамка слева отображает их чертеж с использованием сплайн-интерполяции, а рамка справа — с прямыми линиями для определения местоположения касания и касания местоположения (хотя и со сращенным касанием). Сразу видно, насколько красивее рисунок слева, представленный в виде одной кривой.

Основы SmoothScribble

Оба поля на экране — это подклассифицированные экземпляры ScribbleView, которые соответствуют Scribblable. Класс ScribbleView — это просто UIView, который содержит два дополнительных CAShapeLayer — backgroundLayer для отображения «исторических» каракулей и «рабочий» DrawingLayer для отображения текущего, в процессе выполнения каракулей. 

Протокол Scribblable содержит три метода, вызываемых в начале, во время и в конце жеста каракулей, и метод для его очистки.

    func beginScribble(point: CGPoint)
    func appendScribble(point: CGPoint)
    func endScribble()
    func clearScribble()

Класс SimpleScribbleView 

Давайте сначала посмотрим на простую реализацию. SimpleScribbleView будет рисовать прямые линии, начиная с точки, установленной byginScribble ():

    let simplePath = UIBezierPath()

    func beginScribble(point: CGPoint)
    {
        simplePath.moveToPoint(point)
    }

Затем добавьте линии в его simplePath и обновите путь слоя чертежа при каждом движении:

    func appendScribble(point: CGPoint)
    {
        simplePath.addLineToPoint(point)

        drawingLayer.path = simplePath.CGPath
    }

Наконец, когда пользователь убирает палец с экрана, simplePath добавляется к любому существующему фоновому пути («историческим» каракулям) и затем очищается:

    func endScribble()
    {
        if let backgroundPath = backgroundLayer.path
        {
            simplePath.appendPath(UIBezierPath(CGPath: backgroundPath))
        }

        backgroundLayer.path = simplePath.CGPath

        simplePath.removeAllPoints()

        drawingLayer.path = simplePath.CGPath
    }

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

Класс HermiteScribbleView

Сплайн-интерполированная версия немного отличается. Здесь, в дополнение к UIBezierPath для хранения текущего наброска, есть также массив всех точек, из которых состоит путь, и он должен будет интерполироваться: 

    let hermitePath = UIBezierPath()
    var interpolationPoints = [CGPoint]()

Когда beginScribble () вызывается в HermiteScribbleView, он создает новый массив этих точек интерполяции и заполняет его начальной позицией:

    func beginScribble(point: CGPoint)
    {
        interpolationPoints = [point]
    }

Теперь при каждом перемещении он добавляет новую точку в массив interpolationPoints и использует написанное мной расширение для UIBezierPath с именем interpolatePointsWithHermite (), чтобы построить серию кривых Безье, чтобы получить этот гладкий эрмитовый интерполированный сплайн между всеми точками:

    func appendScribble(point: CGPoint)
    {
        interpolationPoints.append(point)

        hermitePath.removeAllPoints()
        hermitePath.interpolatePointsWithHermite(interpolationPoints)

        drawingLayer.path = hermitePath.CGPath
    }

Наконец, endScribble () из HermiteScribbleView делает почти то же самое, что и его более простой брат, добавляя копию своего «рабочего» слоя к своему «историческому» слою:

    func endScribble()
    {
        if let backgroundPath = backgroundLayer.path
        {
            hermitePath.appendPath(UIBezierPath(CGPath: backgroundPath))
        }

        backgroundLayer.path = hermitePath.CGPath

        hermitePath.removeAllPoints()

        drawingLayer.path = hermitePath.CGPath
    }

Подключение SimpleScribbleView и HermiteScribbleView

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

Приступая к работе, я выяснил, какой из двух представлений является источником, и установил для touchOrigin: 


    if(hermiteScribbleView.frame.contains(location))
    {
       touchOrigin = hermiteScribbleView
    }
    else if (simpleScribbleView.frame.contains(location))
    {
        touchOrigin = simpleScribbleView
    }
    else
    {
        touchOrigin = nil
        return
    }

С этим набором я могу использовать locationInView касания для touchOrigin, чтобы применить одну и ту же информацию касания к обоим представлениям.

Вы, возможно, уже догадались, что так же touchesBegan () контроллера представления вызывает beginScribble ():

    if let adjustedLocationInView = touches.first?.locationInView(touchOrigin)
    {
        hermiteScribbleView.beginScribble(adjustedLocationInView)
        simpleScribbleView.beginScribble(adjustedLocationInView)
    }

AppendScribble () вызывается внутри touchesMoved () контроллера представления:

    coalescedTouches.forEach
    {
        hermiteScribbleView.appendScribble($0.locationInView(touchOrigin))
        simpleScribbleView.appendScribble($0.locationInView(touchOrigin))
    }

Который просто оставляет touchesEnded () для вызова endScribble ():

    hermiteScribbleView.endScribble()
    simpleScribbleView.endScribble()

В заключении

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

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