Статьи

ForceSketch: приложение для рисования в 3D Touch, использующее CIImageAccumulator

Следуя моим недавним публикациям о  3D Touch  и  объединении касаний , объединение этих двух вещей в простом приложении для рисования казалось очевидным следующим шагом. Это также дает мне возможность повозиться с тем,  CIImageAccumulator что было недавно представлено в iOS 9.

Мое небольшое демонстрационное приложение  ForceSketch позволяет пользователю рисовать на экране iPhone 6. И вес линии, и цвет линии связаны с давлением прикосновения. Как и в моей   демонстрации ChromaTouch , давление контролирует оттенок, поэтому самое легкое касание — красный, поворачивающийся к зеленому при трети максимального давления, к синему при двух третях и обратно к красному при максимальном давлении. 

Как только пользователь поднимает палец, два фильтра Core Image, CIColorControls и CIGaussianBlur включаются и исчезают из рисунка.

Механика рисования ForceSketch

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

Вводное  guard утверждение гарантирует, что у меня есть необязательные константы для наиболее важных элементов:

guard let touch = touches.first,
        event = event,
        coalescedTouches = event.coalescedTouchesForTouch(touch) else
    {
        return
    }

Следующим шагом является подготовка к созданию объекта изображения. Для этого мне нужно начать контекст изображения и создать ссылку на текущий контекст:

    UIGraphicsBeginImageContext(view.frame.size)

    let cgContext = UIGraphicsGetCurrentContext()

Чтобы обеспечить максимальную точность жеста пользователя, я зацикливаюсь на объединенных касаниях — это дает мне все промежуточные касания, которые могли произойти между вызовами touchesMoved ().

    for coalescedTouch in coalescedTouches {

Используя  force свойство каждого касания, я создаю константы для цвета и веса отрезков. Чтобы убедиться, что пользователи не-3D Touch устройств звонят, все еще используют приложение, я проверяю  forceTouchCapability и даю этим пользователям фиксированный вес и цвет:

    let lineWidth = (traitCollection.forceTouchCapability == UIForceTouchCapability.Available) ?
        (coalescedTouch.force / coalescedTouch.maximumPossibleForce) * 20 :
        10

    let lineColor = (traitCollection.forceTouchCapability == UIForceTouchCapability.Available) ?
        UIColor(hue: coalescedTouch.force / coalescedTouch.maximumPossibleForce, saturation: 1, brightness: 1, alpha: 1).CGColor :

        UIColor.grayColor().CGColor

С помощью этих констант я могу установить ширину линии и цвет обводки в графическом контексте:


    CGContextSetLineWidth(cgContext, lineWidth)
    CGContextSetStrokeColorWithColor(cgContext, lineColor)

… и теперь я готов определить начало и конец моего отрезка для этого слияния:


    CGContextMoveToPoint(cgContext,
        previousTouchLocation!.x,
        previousTouchLocation!.y)

    CGContextAddLineToPoint(cgContext,
        coalescedTouch.locationInView(view).x,

        coalescedTouch.locationInView(view).y)

Заключительные шаги внутри объединенного цикла касаний должны пройти путь и обновить  previousTouchLocation:

   CGContextStrokePath(cgContext)


    previousTouchLocation = coalescedTouch.locationInView(view)

После того как все штрихи были добавлены в графический контекст, для создания UIImage экземпляра требуется одна строка кода,  а затем конец контекста:

let drawnImage = UIGraphicsGetImageFromCurrentImageContext()


    UIGraphicsEndImageContext()

Отображение нарисованных линий

Чтобы отобразить только что нарисованные линии  drawnImage, я использую  CISourceOverCompositing фильтр с изображением  drawnImage в качестве переднего плана и текущее изображение аккумулятора изображения в качестве фона: 

compositeFilter.setValue(CIImage(image: drawnImage),
        forKey: kCIInputImageKey)
    compositeFilter.setValue(imageAccumulator.image(),
        forKey: kCIInputBackgroundImageKey)

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

   imageAccumulator.setImage(compositeFilter.valueForKey(kCIOutputImageKey) as! CIImage)


    imageView.image = UIImage(CIImage: imageAccumulator.image())

Blurry Fade Out

Как только пользователь поднимает палец, я делаю «размытое затухание» нарисованного изображения. Этот эффект использует два фильтра Core Image, которые определены как константы: 

 let hsb = CIFilter(name: "CIColorControls",
        withInputParameters: [kCIInputBrightnessKey: 0.05])!
    let gaussianBlur = CIFilter(name: "CIGaussianBlur",
        withInputParameters: [kCIInputRadiusKey: 1])!

Первая часть эффекта заключается в использовании a,  CADisplayLink который будет вызываться  step() при каждом обновлении экрана:

let displayLink = CADisplayLink(target: self, selector: Selector("step"))
    displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)

I rely on previousTouchLocation being nil to infer the user has finished their touch. If that’s the case, I simply pass the accumulator’s current image into the HSB / colour control filter, pass that filter’s output into the Gaussian Blur and finally the blur’s output back into the accumulator:

hsb.setValue(imageAccumulator.image(), forKey: kCIInputImageKey)
    gaussianBlur.setValue(hsb.valueForKey(kCIOutputImageKey) as! CIImage, forKey: kCIInputImageKey)

    imageAccumulator.setImage(gaussianBlur.valueForKey(kCIOutputImageKey) as! CIImage)


    imageView.image = UIImage(CIImage: imageAccumulator.image())

Source Code

As always, the source code for this project is available in my GitHub repository here. Enjoy!