如何使用Swift / iOS为人类书写笔画设置动画?

时间:2015-03-31 09:55:48

标签: ios swift core-animation uibezierpath cgpath

目标

我正在尝试使用从字形生成的UIBezierPath创建人类书写的动画近似。我理解并且我已经阅读了许多与我的相似的UIBezierPath问题(但不一样)。我的目标是创建一个近似于执行字符和字母书写的人的外观的动画。

背景

我创建了一个游乐场,用于更好地理解用于动画字形的路径,就好像它们是手工绘制的一样。

Apple CAShapeLayer中的一些概念(strokeStart和strokeEnd)似乎无法在动画时按预期运行。我认为通常人们倾向于将中风想象成使用书写工具(基本上是直线或曲线)。我们认为笔划和填充是一条线,因为我们的书写工具不区分笔画和填充。

但是当动画时,路径的轮廓由线段构成(填充是单独处理的,不清楚如何设置填充位置的动画?)。我想要实现的是一个自然的人类书写线/曲线,它显示了一个笔划的开始和结束,以及当动画从头到尾移动时添加的部分。最初看起来很简单,但我认为它可能需要设置填充位置的动画(不确定如何执行此操作),笔划开始/结束(不确定是否需要根据上面提到的动画执行方式的意外警告),以及使用子路径(如何从已知路径重建)。

方法1

所以,我已经考虑了Path(CGPath / UIBezierPath)的想法。每个路径实际上包含构造字形所需的所有子路径,因此可能递归这些子路径并使用CAKeyframeAnimation / CABasicAnimations和显示部分构造的子路径的动画组将是一个很好的方法(尽管每个子路径的填充位置和笔划仍然是需要从头到尾动画?)。

这种方法导致了一个精炼的问题:

如果有一个完整的UIBezierPath / CGPath,如何访问和创建UIBezierPath / CGPath(子路径)?

如何使用路径/子路径信息使用书写工具绘制填充和描边的动画? (似乎这意味着需要同时为CALayer的strokeStart / strokeEnd,position和path属性设置动画)

注意:

正如人们可以在代码中观察到的那样,我确实拥有从字形获得的完成路径。我可以看到路径描述给了我类似路径的信息。如何获取该信息并将其重新编码为子路径人类可写笔画的数组?

(这里的想法是将点信息转换为人类笔画的新数据类型。这意味着需要算法来识别每个填充的起点,斜率,端点和边界)

提示

我在Pleco(成功实现类似算法的iOS应用程序)中注意到,每个笔画都由描述人类可写笔画的闭合路径组成。 UIBezierPath具有基于连续填充填充的封闭路径。需要一种算法来优化重叠填充,以便为每个stroke-type创建不同的封闭路径。

Erica Sadun在github上有一组path utilities可用。我还没有完全探索这些文件,但它们可能对确定离散笔划非常有用。

UIBezierPath结构似乎基于连续线段/曲线的概念。在填充的交叉点处出现汇合点,其表示有向路径变化。可以计算曲线/线段的行程/填充角度,并搜索相应汇流点的其他曲线/线吗? (即在相交填充的间隙上连接一条线段,以产生两条独立的路径 - 假设一条线路拾取了点并用新的线段/曲线重新创建了路径)

内省:是否有更简单的方法?我是否错过了关键API,书籍或更好的方法来处理这个问题?

一些替代方法(无用 - 需要加载GIF或闪存)以产生所需的结果:

Good Example (using Flash),其中一个表示层显示书写笔划的进展。 (如果可能的话,这就是我想要在Swift / iOS中近似的东西) - (alt link - 看左边的动画图像)

A less good example显示使用渐进路径和填充近似书写笔画特征(动画不流畅且需要外部资源):

less-good

A Flash version - 我对创建Flash动画非常熟悉,但我不愿意在1000版本中实现这些功能(不要太提及它在iOS上不受支持,尽管我可能还会将算法转换为利用带有CSS动画的HTML5画布。但是这种想法似乎有点远,毕竟,我想要的路径信息存储在我从提供的字体/字符串中提取的字形中。

方法2

我正在考虑使用stroke-based字体而不是outline-based字体来获取正确的路径信息(即填充表示为路径的信息)。如果成功,这种方法比近似笔画,笔画类型,交叉点和笔画顺序更清晰。我已经向Apple提交了雷达,建议将基于笔划的字体添加到iOS(#20426819)。尽管有这些努力,我仍然没有放弃形成一种算法,该算法解决了贝塞尔曲线上的线段和曲线的部分笔划,全笔画,交叉点和汇合点。

根据讨论/答案更新了想法

以下附加信息是根据以下任何正在进行的对话和答案提供的。

笔划顺序非常重要,大多数语言(本例中为中文)已明确定义stroke typesstroke order rules,它们似乎提供了一种机制,可根据每个CGPathElement提供的点信息确定类型和顺序

CGPathApply和CGPathApplierFunction看起来很有希望作为枚举子路径的方法(保存到数组并应用填充动画)

可以将掩模应​​用于该层以露出子层的一部分 (我之前没有使用过这个属性,但看起来如果我可以在子路径上移动一个可能有助于填充动画的蒙面图层?)

为每条路径定义了大量的点。好像仅使用字形轮廓定义BezierPath。这一事实使得理解交叉填充的开始,结束和结合是消除特定填充的歧义的重要因素。

可以使用额外的external libraries来更好地解决中风行为。其他技术(如Saffron Type System或其衍生产品)可能适用于此问题域。

仅仅为动画设置动画的最简单解决方案的基本问题是可用的iOS字体是轮廓字体而不是基于笔划的字体。一些商业制造商确实生产基于笔划的字体。如果您有其中一个用于测试,请随时使用link to the playground file

我认为这是一个常见问题,我会在解决问题时继续更新帖子。如果需要进一步的信息或者我可能遗漏了一些必要的概念,请在评论中告诉我。

mask

可能的解决方案

我一直在寻找最简单的解决方案。问题源于字体的结构是轮廓字体而不是基于笔画的字体。我发现了一个基于笔划的字体样本进行测试,并使用它来评估概念验证see video)。我现在正在寻找扩展的单笔划字体(包括中文字符)以进一步评估。一个不太简单的解决方案可能是找到一种方法来创建一个跟随填充的笔划,然后使用简单的2D几何来评估首先设置动画的笔划(例如中文规则在笔划顺序上非常清晰)。

Link to Playground on Github

  • 要使用XPCShowView功能:打开文件导航器和文件 公用事业检查员
  • 单击playground文件并在FUI中(选择 在模拟器中运行)
  • 要访问助理编辑器:转到菜单查看>助理编辑
  • 要查看资源/来源,请右键单击Finder中的playground文件并显示包内容
  • 如果Playground在打开时为空白,请将文件复制到桌面并重新打开(bug ??)

游乐场代码

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 个答案:

答案 0 :(得分:6)

您想要查看CGPathApply(这是您更精确的问题的简短答案)。您为它提供了一个函数,它将为该路径的每个元素(这些将是行和弧并关闭)调用该函数。您可以使用它来重建每个已关闭的项目,并将它们存储到列表中。然后你可以弄清楚每个项目的绘制方向(我认为这实际上可能是最难的部分)而不是使用strokeStart / strokeEnd每个子路径在一个带有蒙版的图层中绘制它并在整个图层上移动蒙版。 / p>

答案 1 :(得分:6)

  

Apple CAShapeLayer中的一些概念(strokeStart和strokeEnd)在动画时似乎无法按预期运行。

但肯定动画strokeEnd 正好你想做什么。在顶部使用多个CAShapeLayers,每个CAShapeLayers代表笔的一个笔划,以形成所需的字符形状。

enter image description here

答案 2 :(得分:1)

进度报告

这个答案是为了强调在解决这个问题上取得的重大进展。由于问题中添加了如此多的细节,我只是想澄清解决方案的进展并最终(当实现时)明确答案。虽然我确实选择了一个有用的答案,但请考虑这篇文章以获得完整的解决方案。

方法#1

使用现有的UIBezierPath信息来识别路径的片段(并最终)利用这些片段(及其坐标)来描边每个子路径(根据可用的语言规则)。

(当前思考)

Erica Sadun正在Github上制作一个SwiftSlowly回购,它在路径上提供了许多功能,包括看起来像是一个很有前途的Segments(UIBezierPath),Line Intersections和许多函数来处理这些项目。我没有时间完全复习,但我可以设想,可以根据已知的stroke types将给定路径解构为段。一旦知道了给定路径的所有笔划类型,就可以评估相对路径坐标以分配stroke-order。之后,根据它们的笔划顺序简单地为笔划(UIBezierPath的子类)设置动画。

方法#2

使用基于笔划的字体而不是基于轮廓的字体。

(当前思考)

我找到了一个基于笔划的字体样本,并且能够为笔划设置动画。这些字体带有内置笔画顺序。我无法访问也支持中文的已完成的基于笔划的字体,但鼓励任何知道此类字体的人在评论中回复。

我已向Apple推荐他们在未来版本中提供基于笔划的字体。 Swift Playground备注和文件(带有样本笔划字体)包含在上面的问题中。如果您有建设性的东西可以添加到此解决方案,请发表评论或发布答案。

笔画顺序规则

请参阅清除中文website上所述的笔画顺序规则。

stroke-order-rules