Как оживить написанный человеком штрих с помощью Swift / iOS?

цель

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

фон

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

несколько концепций в Apple CAShapeLayer (strokeStart и strokeEnd) действительно не работают так, как ожидалось при анимации. Я думаю, что обычно люди склонны думать об инсульте, как если бы он был сделан с помощью пишущего инструмента (в основном прямой или изогнутой линии). Мы рассматриваем штрих и заполнение вместе, чтобы быть линией, поскольку наши пишущие инструменты не различают штрих и заполнить.

но при анимации контур пути строится сегментами линии (заливка обрабатывается отдельно, и неясно, как анимировать положение заливки?). То, что я хочу достичь, - это естественная человеческая письменная линия/кривая, которая показывает начало и конец штриха вместе с частью заливки, добавляемой по мере движения анимации от начала до конца. Первоначально это кажется простым, но я думаю, что может потребоваться анимация позиции заполнения (не уверен, как это сделать), stroke start / end (не уверен, что это требуется, учитывая неожиданные предостережения о том, как выполняется анимация, упомянутая выше) и использование под-путей (как восстановить из известного пути).

подход 1

Итак, я рассмотрел идею пути (CGPath/UIBezierPath). Каждый путь фактически содержит все подпространства, необходимые для создания глифа, поэтому, возможно, рекурсия этих подпространств и использование CAKeyframeAnimation / CABasicAnimations и анимации группа, показывающая частично построенные подпространства, была бы хорошим подходом (хотя позиция заполнения и ход каждого подпространства все равно должны быть анимированы от начала до конца?).

этот подход приводит к вопросу уточняю:

как открыть и создать UIBezierPath/CGPath (подпути) если полный UIBezierPath/CGPath?

как анимировать заливку и обводку, как если бы они были нарисованы с помощью пишущего инструмента, используя путь / подпат информацию? (по-видимому, это означает, что нужно будет одновременно анимировать свойства strokeStart/strokeEnd, position и path CALayer)

NB:

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

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

подсказки

я отметил в Pleco (приложение iOS, которое успешно реализует аналогичный алгоритм), что каждый ход состоит из замкнутого пути, который описывает ход, доступный для записи человеком. UIBezierPath имеет замкнутый путь на основе непрерывных подключенных заливок. - алгоритм необходим для уточнения перекрывающихся заливок для создания отдельных замкнутых путей для каждого инсульт-типа.

Эрика Садун имеет набор программы Путь доступно на github. Я не полностью изучил эти файлы, но они могут оказаться полезными при определении дискретных штрихов.

структура UIBezierPath, по-видимому, основана на понятии смежных отрезков/кривой. На пересечениях заливок появляются точки слияния, которые представляют изменение направленного пути. Можно ли вычислить угол штриха/заполнения сегмента кривой / линии и искать другие кривые/линии для соответствующей точки слияния? (т. е. соедините сегмент линии через промежуток пересекающихся заливок, чтобы получить два отдельных пути-предполагая, что один взял точки и воссоздал путь с новым сегментом линии/кривой)

интроспективно: есть ли более простой метод? Мне не хватает критического API, книги или лучшего подхода к этому проблема?

некоторые альтернативные методы (не полезно - требуется загрузка GIF или flash) для получения желаемого результата:

хороший пример (с помощью Flash) с презентационным слоем, показывающим прогрессию письменного хода. (Если возможно, это то, что я хотел бы приблизить в Swift / iOS) - (alt link - смотрите анимацию изображения слева)

менее хороший пример отображение использования прогрессивных путей и заливок для приблизительные характеристики написанного штриха (анимация не гладкая и требует внешних ресурсов):

less-good

Флэш-версии - я знаком с созданием Flash-анимации, но я не склонен реализовывать их в 1000-х годах (не говоря уже о том, что он не поддерживается на iOS, хотя я, вероятно, также мог бы преобразовать алгоритм для использования холста HTML5 с анимацией css). Но эта линия мысли кажется немного далекой, в конце концов, информация о пути, которую я хочу, хранится в глифах, которые я извлек из предоставленных шрифтов/строк.

подход 2

Я рассматриваю использование инсульта на основе шрифт, а не контур на основе шрифт для получения правильной информации о пути (т. е. тот, где заливка представлена как путь). В случае успеха этот подход будет более чистым, чем аппроксимация штрихов, типов штрихов, пересечений и порядка штрихов. Я уже отправил радар в Apple, предлагая добавить шрифты на основе штрихов в iOS (#20426819). Несмотря на эти усилия, я все еще не отказался от формирования алгоритма, который разрешает частичные штрихи, полные штрихи, пересечения и точки слияния из линейных сегментов и кривых, найденных на пути Безье.

Обновленные Мысли, Основанные На Обсуждении / Ответах

следующая дополнительная информация предоставляется на основе любой текущей беседы и ответы ниже.

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

CGPathApply и CGPathApplierFunction выглядят многообещающими как средство для перечисления подпространств (сохраненных в массиве и применения заливки анимация)

маска может быть применена к слою, чтобы показать часть подслоя (Я не использовал это свойство раньше, но кажется, что если бы я мог переместить маскированный слой по подпутям, которые могли бы помочь в анимации заполнения?)

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

дополнительные внешние библиотеки доступны, которые могут позволить лучше решить поведение инсульта. Другие технологии, такие как Система Типа Шафран или одна из его производных может быть применима к этой проблемной области.

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

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

mask

Возможное Решение

Я всегда в поиск простейшего из возможных решений. Проблема возникает из структуры шрифтов, являющихся набросками шрифтов, а не на основе штрихов. Я нашел образец шрифта на основе штриха для тестирования и использовал его для оценки доказательство концепции (посмотреть видео). Теперь я ищу расширенный шрифт с одним штрихом (который включает китайские иероглифы) для дальнейшей оценки. Менее простым решением может быть поиск способа создания штриха, следующего за заливкой, а затем использование простая 2D-геометрия для оценки того, какой ход анимировать первым (например, китайские правила очень ясны в порядке хода).

ссылка на игровую площадку на Github

  • для использования функции XPCShowView: откройте файловый навигатор и файл Утилиты Инспектора
  • щелкните файл playground и в FUI (выберите Запуск в симуляторе)
  • получить доступ к помощник редактора: перейти к меню " Вид " > помощник Редактор
  • чтобы увидеть ресурсы / источники щелкните правой кнопкой мыши файл playground в Finder и показать содержимое пакета
  • если Playground пуст при открытии, скопируйте файл на рабочий стол и снова откройте (ошибка??)

Код Игровой Площадки

import CoreText
import Foundation
import UIKit
import QuartzCore
import XCPlayground

//research layers
//var l:CALayer? = nil
//var txt:CATextLayer? = nil
//var r:CAReplicatorLayer? = nil
//var tile:CATiledLayer? = nil
//var trans:CATransformLayer? = nil
//var b:CAAnimation?=nil

//  Setup playground to run in full simulator (⌘-0:Select Playground File; ⌘-alt-0:Choose option Run in Full Simulator)

//approach 2 using a custom stroke font requires special font without an outline whose path is the actual fill
var customFontPath = NSBundle.mainBundle().pathForResource("cwTeXFangSong-zhonly", ofType: "ttf")

//  Within the playground folder create Resources folder to hold fonts. NB - Sources folder can also be created to hold additional Swift files
//ORTE1LOT.otf
//NISC18030.ttf
//cwTeXFangSong-zhonly
//cwTeXHei-zhonly
//cwTeXKai-zhonly
//cwTeXMing-zhonly
//cwTeXYen-zhonly

var customFontData = NSData(contentsOfFile: customFontPath!) as! CFDataRef
var error:UnsafeMutablePointer<Unmanaged<CFError>?> = nil
var provider:CGDataProviderRef = CGDataProviderCreateWithCFData ( customFontData )
var customFont = CGFontCreateWithDataProvider(provider) as CGFont!

let registered =  CTFontManagerRegisterGraphicsFont(customFont, error)

if !registered  {
    println("Failed to load custom font: ")

}

let string:NSString = "五"
//"ABCDEFGHIJKLMNOPQRSTUVWXYZ一二三四五六七八九十什我是美国人"

//use the Postscript name of the font
let font =  CTFontCreateWithName("cwTeXFangSong", 72, nil)
//HiraMinProN-W6
//WeibeiTC-Bold
//OrachTechDemo1Lotf
//XinGothic-Pleco-W4
//GB18030 Bitmap

var count = string.length

//must initialize with buffer to enable assignment within CTFontGetGlyphsForCharacters
var glyphs = Array<CGGlyph>(count: string.length, repeatedValue: 0)
var chars = [UniChar]()

for index in 0..<string.length {

    chars.append(string.characterAtIndex(index))

}

//println ("(chars)") //ok

//println(font)
//println(chars)
//println(chars.count)
//println(glyphs.count)

let gotGlyphs = CTFontGetGlyphsForCharacters(font, &chars, &glyphs, chars.count)

//println(glyphs)
//println(glyphs.count)

if gotGlyphs {

    // loop and pass paths to animation function
    let cgpath = CTFontCreatePathForGlyph(font, glyphs[0], nil)

    //how to break the path apart?
    let path = UIBezierPath(CGPath: cgpath)
    //path.hashValue
    //println(path)

    //  all shapes are closed paths
    //  how to distinguish overlapping shapes, confluence points connected by line segments?
    //  compare curve angles to identify stroke type
    //  for curves that intersect find confluence points and create separate line segments by adding the linesegmens between the gap areas of the intersection

    /* analysis of movepoint

        This method implicitly ends the current subpath (if any) and 
        sets the current point to the value in the point parameter. 
        When ending the previous subpath, this method does not actually 
        close the subpath. Therefore, the first and last points of the 
        previous subpath are not connected to each other.

        For many path operations, you must call this method before 
        issuing any commands that cause a line or curve segment to be 
        drawn.
*/


    //CGPathApplierFunction should allow one to add behavior to each glyph obtained from a string (Swift version??)

//    func processPathElement(info:Void,  element: CGPathElement?) {
//        var pointsForPathElement=[UnsafeMutablePointer<CGPoint>]()
//        if let e = element?.points{
//            pointsForPathElement.append(e)
//            
//        }
//    }
//    
//    var pathArray = [CGPathElement]() as! CFMutableArrayRef

    //var pathArray = Array<CGPathElement>(count: 4, repeatedValue: 0)

    //CGPathApply(<#path: CGPath!#>, <#info: UnsafeMutablePointer<Void>#>, function: CGPathApplierFunction)


//    CGPathApply(path.CGPath, info: &pathArray, function:processPathElement)

    /*
    NSMutableArray *pathElements = [NSMutableArray arrayWithCapacity:1];
    // This contains an array of paths, drawn to this current view
    CFMutableArrayRef existingPaths = displayingView.pathArray;
    CFIndex pathCount = CFArrayGetCount(existingPaths);
    for( int i=0; i < pathCount; i++ ) {
    CGMutablePathRef pRef = (CGMutablePathRef) CFArrayGetValueAtIndex(existingPaths, i);
    CGPathApply(pRef, pathElements, processPathElement);
    }
    */

    //given the structure
    let pathString = path.description
//    println(pathString)
    //regex patthern matcher to produce subpaths?
    //...
    //must be simpler method
    //...

    /* 
        NOTES:
        Use assistant editor to view 
        UIBezierPath String

        http://www.google.com/fonts/earlyaccess
        Stroke-based fonts
        Donald Knuth

    */
//    var redColor = UIColor.redColor()
//    redColor.setStroke()


    var pathLayer = CAShapeLayer()
    pathLayer.frame = CGRect(origin: CGPointZero, size: CGSizeMake(300.0,300.0))
    pathLayer.lineJoin = kCALineJoinRound
    pathLayer.lineCap = kCALineCapRound
    //pathLayer.backgroundColor = UIColor.whiteColor().CGColor
    pathLayer.strokeColor = UIColor.redColor().CGColor
    pathLayer.path = path.CGPath


    //    pathLayer.backgroundColor = UIColor.redColor().CGColor

    // regarding strokeStart, strokeEnd
    /* These values define the subregion of the path used to draw the
    * stroked outline. The values must be in the range [0,1] with zero
    * representing the start of the path and one the end. Values in
    * between zero and one are interpolated linearly along the path
    * length. strokeStart defaults to zero and strokeEnd to one. Both are
    * animatable. */
    var pathAnimation = CABasicAnimation(keyPath: "strokeEnd")
    pathAnimation.duration = 10.0
    pathAnimation.fromValue = NSNumber(float: 0.0)
    pathAnimation.toValue = NSNumber(float: 1.0)

    /*
    var fillAnimation = CABasicAnimation (keyPath: "fill")
    fillAnimation.fromValue = UIColor.blackColor().CGColor
    fillAnimation.toValue = UIColor.blueColor().CGColor
    fillAnimation.duration = 10.0
   pathLayer.addAnimation(fillAnimation, forKey: "fillAnimation") */

    //given actual behavior of boundary animation, it is more likely that some other animation will better simulate a written stroke

    var someView = UIView(frame: CGRect(origin: CGPointZero, size: CGSizeMake(300.0, 300.0)))
    someView.layer.addSublayer(pathLayer)


    //SHOW VIEW IN CONSOLE (ASSISTANT EDITOR)
     XCPShowView("b4Animation", someView)

    pathLayer.addAnimation(pathAnimation, forKey: "strokeEndAnimation")

    someView.layer.addSublayer(pathLayer)

    XCPShowView("Animation", someView)




}

3 ответов


вы хотите посмотреть CGPathApply (это короткий ответ на ваш более изысканный вопрос). Вы предоставляете ему функцию, и он вызовет эту функцию для каждого элемента (это будут линии, дуги и закрытия) пути. Вы можете использовать это для восстановления каждого закрытого элемента и сохранения их в список. Затем вы можете выяснить, в каком направлении каждый элемент рисуется (я думаю, что это может быть на самом деле самая сложная часть), а затем с помощью strokeStart/strokeEnd каждый подпут слой с маской и перемещение маски по слою.


несколько концепций в Apple CAShapeLayer (strokeStart и strokeEnd) действительно не работают так, как ожидалось, когда анимированы.

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

enter image description here


Отчет

этот ответ размещен, чтобы подчеркнуть значительный прогресс, достигнутый в решении этого вопроса. С таким большим количеством деталей, добавленных к вопросу, я просто хотел уточнить прогресс в решении и в конечном итоге (когда это будет достигнуто), окончательный ответ. Хотя я выбрал ответ, который был полезен, пожалуйста, рассмотрите этот пост для полного решения.

подход # 1

используйте существующую информацию UIBezierPath для определите сегменты пути (и в конечном итоге) используйте эти сегменты (и их координаты) для обводки каждого подпространства (в соответствии с доступными языковыми правилами).

(Тока Думаю)

Эрика Садун производит SwiftSlowly РЕПО на Github, который предоставляет множество функций на путях, включая то, что кажется многообещающей библиотекой на сегментах (UIBezierPath), пересечениях линий и многих функциях для действия на этих элементах. У меня не было времени обзор полностью, но я могу представить, что можно деконструировать данный путь на сегменты на основе известного типы инсульта. После того, как все типы штрихов известны для данного пути, можно оценить относительные координаты пути для назначения инсульт-заказа. После этого просто анимируйте штрихи (подкласс UIBezierPath) в соответствии с их порядком штрихов.

подход # 2

используйте шрифт на основе штрихов вместо шрифт на основе контура.

(Тока Думаю)

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

Я сделал рекомендацию Apple, чтобы они поставляли шрифты на основе штрихов в будущих выпусках. Swift Playground отмечает и файлы (с образцами шрифтов штрихов) включены в вопрос выше. Прокомментируйте или опубликуйте ответ, если у вас есть что-то конструктивное, чтобы добавить к этому решению.