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

Tom*_* C. 36 core-animation cgpath ios uibezierpath swift

目的

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

背景

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

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

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

方法1

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

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

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

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

注意:

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

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

提示

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

Erica Sadun 在github上提供了一组路径实用程序.我没有完全探索这些文件,但它们可能在确定离散笔划时非常有用.

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

内省:是否有更简单的方法?我错过了一个关键的API,一本书或更好的方法来解决这个问题吗?

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

良好的示例(使用Flash),其中一个表示层显示书写笔划的进展.(如果可能的话,这就是我想要在Swift/iOS中近似的东西) - (alt链接 - 请参阅左边的动画图像)

一个不太好的例子,显示使用渐进路径和填充来近似书写笔划特征(动画不流畅且需要外部资源):

不太好

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

方法2

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

基于讨论/答案的最新想法

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

笔划顺序很重要,大多数语言(本例中为中文)都有明确定义的笔划类型笔划顺序规则,它们似乎提供了一种机制,可根据每个CGPathElement提供的点信息确定类型和顺序.

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

可以将一个蒙版应用于图层以显示子图层的一部分(我之前没有使用过此属性,但看起来如果我可以在子路径上移动可能有助于填充动画的蒙版图层?)

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

可以使用其他外部库,以便更好地解决笔划行为.其他技术如Saffron Type System或其衍生产品之一可能适用于此问题域.

仅仅为动画设置动画的最简单解决方案的基本问题是可用的iOS字体是轮廓字体而不是基于笔划的字体.一些商业制造商确实生产基于笔划的字体.如果你有其中一个用于测试,请随意使用操场文件链接.

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

面具

可能解决方案

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

链接到Github上的游乐场

  • 要使用XPCShowView功能:打开File Navigator和File Utilities Inspector
  • 单击playground文件并在FUI中(选择Run in Simulator)
  • 要访问"辅助编辑器":转到菜单"视图">"辅助编辑器"
  • 要查看资源/来源,请右键单击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)




}
Run Code Online (Sandbox Code Playgroud)

Str*_*pes 6

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


mat*_*att 6

Apple CAShapeLayer中的一些概念(strokeStart和strokeEnd)在动画时似乎并没有按预期运行.

但可以肯定的动画strokeEnd正是你想做的事.使用多个CAShapeLayers相互叠加,每个CAShapeLayers代表笔的一个笔划以形成所需的字符形状.

在此输入图像描述