Вот последние в моей серии экспериментов, посвященных возможностям 3D Touch . Мое приложение 3DReTouch позволяет пользователю выбрать одну из нескольких настроек изображения и применить эту настройку локально с интенсивностью, основанной на силе их прикосновения. Быстрое встряхивание их iPhone удаляет все их настройки и позволяет им начать все сначала.
Как и ForceSketch , это приложение использует CIImageAccumulator, а также использует маску Core Core для выборочного наложения фильтра на изображение с использованием радиального градиента, центрированного по касанию пользователя.
Основы
Предустановленные фильтры — это константы типа Filter, и вместе с экземпляром соответствующего фильтра Core Image они содержат сведения о том, какие из их параметров зависят от силы прикосновения. Например, фильтр повышения резкости является фильтром повышения резкости, и его входная резкость изменяется в зависимости от силы
Filter(name: "Sharpen", ciFilter: CIFilter(name: "CISharpenLuminance")!,
variableParameterName: kCIInputSharpnessKey,
variableParameterDefault: 0,
variableParameterMultiplier: 0.25)
Массив фильтров действует как поставщик данных для UIPickerView . Когда вид выбора изменяется, я устанавливаю currentFilter для выбранного элемента для использования позже:
func pickerView(pickerView: UIPickerView, didSelectRow row: Int, inComponent component: Int)
{
currentFilter = filters[row]
}
Фильтрация изображений происходит в фоновом потоке путем выбора ожидающих элементов из очереди «первым пришел — первым обслужен» , поэтому я использую CADisplayLink для планирования этой задачи при каждом обновлении кадра.
Сенсорная обработка
С обоими обработчиками касаний touchesBegan и touchesMoved я хочу создать структуру PendingUpdate, содержащую силу, положение и текущий фильтр касания, и добавить его в свою очередь. Это делается внутри applyFilterFromTouches (), и в первую очередь защита гарантирует, что у нас есть касание, и оно находится на границе изображения:
guard let touch = touches.first
where imageView.frame.contains(touches.first!.locationInView(imageView)) else
{
return
}
Затем я нормализую силу касания или создаю значение по умолчанию для устройств без 3D Touch:
let normalisedForce = traitCollection.forceTouchCapability == UIForceTouchCapability.Available ?
touch.force / touch.maximumPossibleForce :
CGFloat(0.5)
Затем, используя масштаб изображения, я могу вычислить позицию касания на реальном изображении, создать свой объект PendingUpdate и добавить его в очередь:
let imageScale = imageViewSide / fullResImageSide
let location = touch.locationInView(imageView)
let pendingUpdate = PendingUpdate(center: CIVector(x: location.x / imageScale, y: (imageViewSide - location.y) / imageScale),
radius: 80,
force: normalisedForce,
filter: currentFilter)
Применение фильтра
Моя функция update () вызывается из CADisplayLink:
let displayLink = CADisplayLink(target: self, selector: Selector("update"))
displayLink.addToRunLoop(NSRunLoop.mainRunLoop(), forMode: NSDefaultRunLoopMode)
Используя guard (снова!), Я гарантирую, что на самом деле ожидается обновление для процесса:
guard pendingUpdatesToApply.count > 0 else
{
return
}
… и если есть, я удаляю его и назначаю pendingUpdate:
let pendingUpdate = pendingUpdatesToApply.removeFirst()
Остальная часть метода происходит внутри dispatch_async и разбивается на несколько частей. Прежде всего, я обновляю положение моего радиального градиента в зависимости от местоположения касания:
let gradientFilter = CIFilter(name: "CIGaussianGradient",
withInputParameters: [
"inputColor1": CIColor(red: 0, green: 0, blue: 0, alpha: 0),
"inputColor0": CIColor(red: 1, green: 1, blue: 1, alpha: 1)])!
self.gradientFilter.setValue(pendingUpdate.center,
forKey: kCIInputCenterKey)
Затем я установил фильтр Core Image для параметров объекта pendingUpdate. Ему нужно входное изображение, которое я извлекаю из накопителя изображений, и оно требует обновления своего зависимого от силы параметра (variableParameterName) на основе силы касания:
pendingUpdate.filter.ciFilter.setValue(self.imageAccumulator.image(), forKey: kCIInputImageKey)
pendingUpdate.filter.ciFilter.setValue(
pendingUpdate.filter.variableParameterDefault + (pendingUpdate.force * pendingUpdate.filter.variableParameterMultiplier),
forKey: pendingUpdate.filter.variableParameterName)
С их помощью я могу заполнить параметры фильтра Blend With Mark . Он будет использовать градиент в качестве маски для наложения вновь отфильтрованного изображения поверх существующего изображения из аккумулятора только там, где пользователь коснулся:
Итак, для маски необходимо базовое изображение, отфильтрованное изображение и градиент:
let blendWithMask = CIFilter(name: "CIBlendWithMask")!
self.blendWithMask.setValue(self.imageAccumulator.image(), forKey: kCIInputBackgroundImageKey)
self.blendWithMask.setValue(pendingUpdate.filter.ciFilter.valueForKey(kCIOutputImageKey) as! CIImage,
forKey: kCIInputImageKey)
self.blendWithMask.setValue(self.gradientFilter.valueForKey(kCIOutputImageKey) as! CIImage,
forKey: kCIInputMaskImageKey)
Наконец, я могу взять вывод этой смеси, переназначить ее на аккумулятор и создать UIImage для отображения на экране:
self.imageAccumulator.setImage(self.blendWithMask.valueForKey(kCIOutputImageKey) as! CIImage)
let finalImage = UIImage(CIImage: self.blendWithMask.valueForKey(kCIOutputImageKey) as! CIImage)
В заключении
В качестве технической демонстрации 3D ReTouch демонстрирует, как 3D Touch от Apple можно использовать для эффективного управления мощностью фильтра Core Image. Однако я подозреваю, что iPhone не идеальное устройство для этого — мои пухлые пальцы блокируют то, что происходит на экране. IPad Pro, с его Карандашем, был бы намного более подходящим устройством. Кроме того, имитация отдельного трекпада (аналогично глубокому нажатию на клавиатуре iOS) может работать лучше.
Как всегда, исходный код для этого можно найти в моем репозитории GitHub здесь . Наслаждайтесь!