如何使用 Swift/iOS 制作人类书写笔画的动画?

2023-12-25

客观的

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

背景

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

Apple CAShapeLayer 中的几个概念(中风开始和中风结束)在动画时似乎确实没有按预期运行。我认为人们通常倾向于认为笔画就像是用书写工具完成的(基本上是直线或曲线)。我们将笔画和填充一起视为一条线,因为我们的书写工具不区分笔画和填充。

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

方法一

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

这种方法引出了更精致的问题:

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

如何使用路径/子路径信息对填充和描边进行动画处理,就像用书写工具绘制一样? (似乎这意味着需要同时对 CALayer 的描边开始/描边结束、位置和路径属性进行动画处理)

NB:

As one can observe in the code, I do have the finished paths obtained from glyphs. I can see that the path description gives me path-like information. How would one take that information and recast it as an array of sub paths human-writable strokes?

(这里的想法是将点信息转换为类似人类笔划的新数据类型。这意味着需要一种算法来识别每个填充的起点、坡度、终点和边界)

Hints

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

艾丽卡·萨顿 (Erica Sadun) 有一套路径实用程序 https://github.com/erica/iOS-Drawing/tree/master/C05可以在 github 上找到。我还没有完全探索这些文件,但它们可能对确定离散笔划有用。

UIBezierPath 结构似乎基于连续线段/曲线的概念。填充物交叉处出现汇合点,代表方向路径变化。能否计算一条曲线/线段的描边/填充角度并搜索其他曲线/直线以查找相应的汇合点? (即,将一条线段连接到相交填充的间隙上以生成两条单独的路径 - 假设拾取这些点并使用新的线段/曲线重新创建路径)

反思一下:有没有更简单的方法?我是否缺少一个关键的 API、一本书或解决此问题的更好方法?

一些替代方法(没有用 - 需要加载 gif 或 flash)来产生所需的结果:

很好的例子(使用Flash) http://pic.zdic.net/kai/jt_bh/swf/1a/6211.swf具有显示书写笔划进展的表示层。 (如果可能的话,这就是我想要在 Swift/iOS 中近似的)-(alt link http://www.zdic.net/z/1a/js/6211.htm- 请参阅左侧的动画图像)

一个不太好的例子 http://lost-theory.org/ocrat/chargif/char/ced2.html显示使用渐进路径和填充来近似书写笔划特征(动画不平滑并且需要外部资源):

Flash 版本 http://animation.archchinese.com- 我熟悉创建 Flash 动画,但我不愿意在 1000 年代实现这些动画(更不用说 iOS 不支持它,尽管我也可以转换算法以利用带有 css 动画的 HTML5 画布)。但这个思路似乎有点遥远,毕竟我想要的路径信息存储在我从提供的字体/字符串中提取的字形中。

方法2

我正在考虑使用基于中风的 https://en.wikipedia.org/wiki/Computer_font#Stroke-based_font_formats字体而不是基于轮廓的 https://en.wikipedia.org/wiki/Computer_font#Outline_fontsfont 来获取正确的路径信息(即 fill 表示为路径的信息)。如果成功,这种方法将比近似笔画、笔画类型、交叉点和笔画顺序更清晰。我已经向 Apple 提交了一份雷达建议,建议将基于笔画的字体添加到 iOS (#20426819)。尽管做出了这些努力,我仍然没有放弃形成一种算法,从贝塞尔曲线路径上找到的线段和曲线中解析部分笔划、全笔划、交叉点和汇合点。

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

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

笔画顺序很重要,大多数语言(在本例中为中文)都有明确的定义笔画类型 http://www.clearchinese.com/chinese-writing/strokes.htm and 笔顺规则 http://www.clearchinese.com/chinese-writing/stroke-order.htm似乎提供了一种机制来根据每个 CGPathElement 提供的点信息来确定类型和顺序。

CGPathApply 和 CGPathApplierFunction 作为枚举子路径的方法似乎很有前途(保存到数组并应用填充动画)

可以将遮罩应用于该层以显示子层的一部分 (我以前没有使用过这个属性,但看来我是否可以在子路径上移动蒙版层,这可能有助于动画填充?)

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

额外的外部库 http://www.wenlin.com/cdl/可以让人们更好地解决中风行为。其他技术如藏红花型系统 https://en.wikipedia.org/wiki/Saffron_Type_System#Stylized_stroke_fonts或其派生之一可能适用于该问题领域。

仅对笔划进行动画处理的最简单解决方案的一个基本问题是,可用的 iOS 字体是轮廓字体,而不是基于笔划的字体。一些商业制造商确实生产基于笔画的字体。请随意使用链接到游乐场文件 https://github.com/mingsai/glyphs-playground如果您有其中之一用于测试。

我认为这是一个常见问题,随着我寻求解决方案,我将继续更新帖子。如果需要更多信息或者我可能缺少一些必要的概念,请在评论中告诉我。

可能的解决方案

我总是在寻找最简单的解决方案。该问题源于字体的结构是轮廓字体而不是基于笔划的字体。我找到了一个基于笔画的字体样本进行测试,并用它来评估概念证明 (看视频 http://googledrive.com/host/0B8D3kbf8rViMY1FnSF8tWmlySGs)。我现在正在寻找一种扩展的单笔画字体(包括汉字)来进一步评估。一个不太简单的解决方案可能是找到一种方法来创建跟随填充的笔画,然后使用简单的 2D 几何体来评估首先对哪个笔画进行动画处理(例如,中国规则对笔画顺序非常明确)。

链接到 Github 上的 Playground https://github.com/mingsai/glyphs-playground

  • 要使用 XPCShowView 功能: 打开文件导航器和文件 公用事业检查员
  • 单击 Playground 文件并在 FUI 中(选择 在模拟器中运行)
  • 要访问助理编辑器:转到菜单“视图”>“助理编辑器”
  • 要查看资源/源,请在 Finder 中右键单击 Playground 文件并显示包内容
  • 如果 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)




}

Apple CAShapeLayer 中的几个概念(中风开始和中风结束)在动画时似乎确实没有按预期运行。

但肯定是动画strokeEnd is exactly你想做什么。使用多个 CAShapeLayer 相互叠加,每个 CAShapeLayer 代表笔的一笔,以形成所需的字符形状。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

如何使用 Swift/iOS 制作人类书写笔画的动画? 的相关文章

  • 如何快速更改按钮图像?

    我正在开发一个有按钮的应用程序 该按钮没有文本 图像或背景 所以我想做的就是在viewDidLoad函数中给它一个图像 这就是我所拥有的 IBOutlet var tapButton UIButton override func viewD
  • 如何在 Swift 3 中解析 JSON 数组 [重复]

    这个问题在这里已经有答案了 我从 Socket 获取了一些我想访问的数据 但收到错误消息 指出每次都无法将 NSArray 转换为 NSDictionary struct SocketEventHandler let event Strin
  • Swift - 选择值后隐藏 pickerView

    我发现了类似的问题 他们的答案很有帮助 但我坚持最后一件事 我试图在点击字段时显示 pickerView 然后选择数据时 我希望 pickerView 隐藏 我可以从 pickerView 获取数据来隐藏 但是 pickerView 后面仍
  • 如何检测Retina高清显示屏?

    UIScreen有一个新的 nativeScaleiOS 8 中的属性 但文档没有提及它 property nonatomic readonly CGFloat nativeScale 还有一个scale属性 但文档说它是 2 用于视网膜显
  • PresentModalViewController 不执行任何操作

    我有一个 UIViewController parent 调用presentModalViewController与另一个 UIViewController child on viewDidLoad If parent没有 UINaviga
  • 在后台任务中安排通知

    我正在为 iOS 开发一个日历 闹钟应用程序 它与网络服务器同步 当在服务器上添加活动时 会发出推送通知 以便 iOS 客户端可以获取新数据 并根据需要更新和安排下一次警报的时间 本地通知 但这仅在应用程序在客户端打开时才有效 我希望客户端
  • Swift 中带圆角的 NSWindow

    我想要一个圆角的窗户 但我在每个角落都有一个白点 Code let effect NSVisualEffectView frame NSRect x 0 y 0 width 0 height 0 effect blendingMode be
  • iOS UIButton 带有圆角和背景 bug

    我发现圆形 UIButton 存在一个奇怪的问题 这是我创建此按钮的代码块 let roundedButton UIButton type System roundedButton frame CGRectMake 100 100 100
  • Swift 3.0 Pin 颜色注释在 MapView 中没有改变

    我有兴趣根据案例场景更改注释的图钉颜色 在一个函数中 我发送了一个数组 用于确定引脚注释的颜色 到目前为止 我已经设置了一个名为 ColorPointAnnotation 的子类 它将确定 pinColor 然后 在 switch 语句中
  • 使用 Google place API 从 lat long 获取附近的地点

    我正在使用 google place API 即 https maps googleapis com maps api place search json location 33 7167 73 0667 radius 500 type f
  • 无法转换“UINavigationController”类型的值

    我正在为我的应用程序实现一个搜索界面 因此基本上我会将搜索关键字从一个 ViewController 传递到另一个 ViewController 我已经多次进行过这种类型的参数传递 但这次似乎有些奇怪 目标 ViewController 嵌
  • Objective C UIImagePNGRepresentation内存问题(使用ARC)

    我有一个基于 ARC 的应用程序 它从 Web 服务加载大约 2 000 个相当大 1 4MB 的 Base64 编码图像 它将 Base64 解码后的字符串转换为 png图像文件并将其保存到磁盘 这一切都是在一个循环中完成的 我不应该有任
  • 应用程序传输安全已禁用,但仍然出现 SSL 握手错误

    我在通过 HTTPS SSL 连接到 API 时遇到问题 我已经使用下面的字典完全禁用了应用程序传输安全性 ATS 尽管 SSL 证书通过了 NSCURL 的所有测试
  • 将数字分解为单个数字的数组

    如果我有整数 123 并且我想将数字分解为数组 1 2 3 最好的方法是什么 我已经搞乱了很多 并且我有以下工作 var number 123 var digits Array String number map Int strtoul S
  • 如何将音乐从我的应用程序切换到 iPod

    我在用MusicPlayerController我的应用程序中的对象来播放音乐 我知道当 iPhone ipod 应用程序终止时 可以继续播放我的应用程序音乐 我该怎么做 这涉及到一些事情 您必须在两种音乐播放器之间进行选择 应用程序音乐播
  • 我什么时候应该对 IBOutlet 使用弱或强限定符? [复制]

    这个问题在这里已经有答案了 可能的重复 ARC 下 IBOutlets 应该强还是弱 https stackoverflow com questions 7678469 should iboutlets be strong or weak
  • 在 Instruments 中查找内存泄漏行

    我是 iOS 中的仪器新手 我正在尝试使用 Xcode 4 5 2 并按照本教程查找仪器中的内存泄漏 http soulwithmobiletechnology blogspot sg 2011 04 how to check memory
  • Xcode 9 中的“addingPercentEncoding”是否损坏?

    在 Swift 3 x 和 Xcode 9 beta 2 中 使用addingPercentEncoding https developer apple com documentation swift string 1690785 addi
  • 显示键盘时如何在 TextView 下方添加更多填充

    当我在 ScrollView 中有 TextField 并点击它时 键盘会按预期显示 但似乎 TextField 已向上移动到足以显示输入区域 但我希望移动到足够的位置 以便整体可见 否则它看起来像是被剪裁了的 我找不到改变这种行为的方法
  • 如何获取 UIWebView 中元素的位置?

    我在 iPad 程序中加载了 html 的 UIWebView 通过使用 webkit column width 我将 html 分为几列 padding 0px height 1024px webkit column gap 0px we

随机推荐