38

Objective

I am attempting to create an animated approximation of human writing, using a UIBezierPath generated from a glyph. I understand and I have read the many UIBezierPath questions which sound similar to mine (but are not the same). My goal is to create an animation that approximates the look of a human performing the writing of characters and letters.

Background

I've created a playground that I am using to better understand paths for use in animating glyphs as if they were drawn by hand.

A couple of concepts in the Apple CAShapeLayer (strokeStart and strokeEnd) really don't seem to operate as expected when animated. I think that generally people tend to think of a stroke as if done with a writing instrument (basically a straight or curved line). We consider the stroke and fill together to be a line as our writing instruments do not distinguish between stroke and fill.

But when animated, the outline of a path is constructed by line segments (fill is treated separately and it is unclear how to animate the position of the fill?). What I want to achieve is a natural human written line/curve that shows the start and end of a stroke together with the portion of the fill being added as the animation moves from start to finish. Initially this appears simple but I think it may require animating the fill position (unsure of how to do this), the stroke start/end (not sure if this required given the unexpected caveats with how the animation performs noted above), and making use of sub-paths (how to reconstruct from a known path).

Approach 1

So, I've considered the idea of a Path (CGPath/UIBezierPath). Each path actually contains all of the subpaths required to construct a glyph so perhaps recursing those subpaths and using a CAKeyframeAnimation / CABasicAnimations and an animation group showing the partially constructed subpaths would be a good approach (although the fill position and stroke of each subpath would still need to be animated from start to end?).

This approach leads to the refined question:

How to access and create UIBezierPath/CGPath (subpaths) if one has a complete UIBezierPath/CGPath?

How to animate the fill and stroke as if drawn with a writing instrument using the path/subpath information? (seemingly this implies one would need to animate the strokeStart/strokeEnd, position, and path properties of a CALayer at the same time)

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?

(The idea here would be to convert the point information into a new data type of human-like strokes. This implies a requirement for an algorithm to identify the start, slope, endpoint and boundary of each fill)

Hints

I've noted in Pleco (an iOS app that successfully implements a similar algorithm), that each stroke is composed of a closed path that describes the human-writable stroke. UIBezierPath has a closed path based on continuous connected fills. An algorithm is needed to refine overlapping fills to create distinct closed paths for each stroke-type.

Erica Sadun has a set of path utilities available on github. I haven't fully explored these files but they might prove useful in determining discrete strokes.

UIBezierPath structure seems based on the notion of a contiguous line segments/curve. There are confluence points appearing at the intersections of fills, which represent directional path change. Could one calculate the stroke/fill angle of a curve/line segment and search other curves/lines for a corresponding confluence point? (i.e. connect a line segment across the gap of intersecting fills to produce two separate paths -- assuming one picked up the points and recreated the path with a new line segment/curve)

Introspectively: Is there a simpler method? Am I missing a critical API, a book or a better approach to this problem?

Some alternative methods (not useful - requires loading gifs or flash) for producing the desired outcome:

Good Example (using Flash) with a presentation layer showing progression of the written stroke. (If possible, this is what I would want to approximate in Swift/iOS) - (alt link - see animating image on left)

A less good example showing the use of progressive paths and fills to approximate the written stroke characteristics (animation not smooth and requires external resources):

less-good

A Flash version - I am familiar with creating Flash animations but I am disinclined to implement these in the 1000's (not too mention that its not supported on iOS, although I could probably also convert an algorithm to leverage an HTML5 canvas with css animation). But this line of thought seems a bit far afield, after all, the path information I want is stored in the glyphs that I've extracted from fonts/strings provided.

Approach 2

I am considering the use of a stroke-based font rather than an outline-based font to obtain the correct path information (i.e. one where fill is represented as a path). If successful, this approach would be cleaner than approximating the strokes, stroke-type, intersections, and stroke order. I've already submitted a radar to Apple suggesting that stroke-based fonts be added to iOS (#20426819). Notwithstanding this effort, I still have not given up on forming an algorithm that resolves partial-strokes, full strokes, intersections, and confluence points from the line-segments and curves found on the bezier path.

Updated Thoughts Based On Discussion/Answers

The following additional information is provided based on any ongoing conversations and answers below.

Stroke order is important and most languages (Chinese in this case) have clearly defined stroke types and stroke order rules that appear to provide a mechanism to determine type and order based on the point information provided with each CGPathElement.

CGPathApply and CGPathApplierFunction appear promising as a means to enumerate the subpaths (saved to an array and apply the fill animation)

A mask may be applied to the layer to reveal a portion of the sublayer (I have not used this property before but it appears that if I could move a masked layer over the subpaths that might assist in animating the fill?)

There are a large number of points defined for each path. As if the BezierPath is defined using the outline of the glyph only. This fact makes understanding the start, end, and union of crossing fills an important factor to disambiguate specific fills.

Additional external libraries are available that may allow one to better resolve stroke behavior. Other technology like the Saffron Type System or one of its derivatives may be applicable to this problem domain.

A basic issue with the simplest solution of just animating the stroke is that the available iOS fonts are outline fonts rather than stroke-based fonts. Some commercial manufacturers do produce stroke-based fonts. Please feel free to use the link to the playground file if you have one of these for testing.

I think this is a common problem and I will continue to update the post as I move toward a solution. Please let me know in the comments if further information is required or if I might be missing some of the necessary concepts.

mask

Possible Solution

I am always in search of the simplest possible solution. The issue originates from the structure of the fonts being outline fonts rather than stroke-based. I found a sample of a stroke-based font to test and used that to evaluate a proof of concept (see video). I am now in search of an extended single stroke font (which includes Chinese characters) to further evaluate. A less simple solution might be to find a way to create a stroke that follows the fill and then use simple 2D geometry to evaluate which stroke to animate first (For example Chinese rules are very clear on stroke order).

Link to Playground on Github

  • To use the XPCShowView function: Open the File Navigator and File Utilities Inspector
  • Click the playground file and in the FUI (choose Run in Simulator)
  • To access the Assistant Editor: Goto menu View > Assistant Editor
  • To see resources/sources right-click playground file in Finder and Show Package Contents
  • If Playground is blank on opening, copy the file to the desktop and reopen (bug??)

Playground Code

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)




}
Tommie C.
  • 12,895
  • 5
  • 82
  • 100
  • 2
    Before you get too deep in implementation issues, let's check a design question. Are you trying to show the proper stroke order for writing Hanzi/Kanji characters? That information isn't encoded in the font, so if that's your objective you'll need additional resources anyway. (And correlating that info to the glyph info from the font is hard enough that you might as well have your own vector paths, too,) – rickster Apr 02 '15 at 17:55
  • @rickster - Thanks for the follow-up question. I am interested in correct stroke order. The caveat sounds like a useful bit of insight, however the number of glyphs are in the tens of thousands and the [stroke order rules](http://www.archchinese.com/chinese_stroke_order_rules.html) are well established. I believe that the time invested to use the point information to determine stroke order outweighs building individual vector paths. – Tommie C. Apr 02 '15 at 18:08
  • 1
    But those rules are designed for a human to interpret. Before you can decide what order the four strokes go in for, say, 「木」, you have to figure out that it *is* four strokes, and what those strokes are. You have to determine that the non-convex area enclosed in the path you get from the glyph info represents the union of two rectangles and two diagonal curvy lines (not four rectangles plus a single angled stroke as in 「へ」, etc). That's a non-trivial computer vision task. And your algorithm has to get it right for thousands of very different characters. – rickster Apr 02 '15 at 18:41
  • @rickster Yes, a difficult but not impossible task versus writing singular vector paths and loading those for more than 50k character variations (imho-more difficult). I just think that an algorithm that uses the points for each path element should allow one to determine relative position and perform the right animation based on the small number of rules. I'm not certain at all that I need the union information. Wouldn't the relative CGPoints give enough info on [strokes types](http://www.clearchinese.com/chinese-writing/strokes.htm)? Just thinking out loud on this I haven't gotten that far. – Tommie C. Apr 02 '15 at 18:54
  • 1
    As @rickster notes, I really doubt that you are going to be able to dissect final glyph path information and reverse that into correct stroke order. Consider just your first example of 我. That's 7 strokes. But the resulting glyph path has over 75 elements. Something as trivial as 一 can have many drawing elements to include the serif. I don't think this direction is going to work. You should be searching for a stroke-order dictionary that you can license. – Rob Napier Apr 02 '15 at 20:09
  • @rickster where did you get 50k character variations from? – Kex Apr 02 '15 at 20:12
  • At a minimum, I would spend a lot of time looking for a good font to dissect. You want something extremely simple and graphic. The font you're using is way too pretty. – Rob Napier Apr 02 '15 at 20:14
  • @rickster you know the app Pleco right? that does the drawing animation for every single character – Kex Apr 02 '15 at 20:17
  • @RobNapier - Thanks, that is a valid concern, I may want to look at font variations to see how that affects the problem. I note that there are only two close paths in the glyph but further investigation into the underlying CGPathElement/UIBezierPath structure is required before I can declare it insoluble. For example the point information structure itself seems likely to help me determine the Chinese stroke type (and when to dissimilate it from other distinct stroke types) – Tommie C. Apr 02 '15 at 20:27
  • @Kex as you've noted, some other solutions exist. I doubt these are gif representations so I remain optimistic that I may not be seeing the entire pattern. BTW, I noted the 50k number although the exact number of Chinese characters is debatable, it seems well beyond a reasonable resource limit. – Tommie C. Apr 02 '15 at 20:28
  • 1
    BTW, this is the kind of technology I would be looking at licensing to build this: http://www.wenlin.com/cdl/ – Rob Napier Apr 02 '15 at 20:47
  • It's about 3000-5000 Chinese characters that are used in daily language. The rest are historical. If you want to represent more than the base 3-5k it's going to be another task in itself digging up characters from lost texts. – Kex Apr 03 '15 at 08:18

3 Answers3

7

A couple of concepts in the Apple CAShapeLayer (strokeStart and strokeEnd) really don't seem to operate as expected when animated.

But surely animating the strokeEnd is exactly what you want to do. Use multiple CAShapeLayers over top of one another, each one representing one stroke of the pen to form the desired character shape.

enter image description here

matt
  • 515,959
  • 87
  • 875
  • 1,141
  • I am not sure that I understand how to enact your suggestion. When I use the strokeEnd, I see the path boundary being constructed. What I want to achieve is a human-writable stroke, which means that both fill and stroke need to be animated in a particular stroke order. A glyph produces one or more closed paths that surround the entire shape of each distinct path (without separating the fill). Thus overlapping fills don't allow me to perform a human writable stroke. I may have missed something in your response that allows me to achieve that, if so please elaborate. – Tommie C. Apr 03 '15 at 16:56
  • 1
    I've added an animated gif showing a very simple example (animation of drawing a simple "X"). Two strokes, two shape layers on top of each other. It seems to me that this is exactly what you're describing, is it not? The difference is merely that I don't know the path shapes to form the strokes of a Chinese character - _that_ part is left as an exercise for the reader! – matt Apr 03 '15 at 16:58
  • 1
    The question is very specific about achieving the animation using a UIBezierPath generated from a glyph. I am not sure that this qualifies as a valid answer. It could but I don't know how it was constructed -- Possibly with paths created independent of glyphs? – Tommie C. Apr 03 '15 at 17:02
  • 1
    As I said, I don't know how you're going to get the path shapes for the strokes. But once you have them, one path per stroke, you can stroke them in an animated fashion by animating the stroke end of a shape layer, which is all the above example is doing. – matt Apr 03 '15 at 17:04
  • TommieC., it seems you are representing each part of the character as a closed path drawn with a fill. @matt is instead suggesting you represent each part of the character as an open path drawn with a stroke. If you want to animate strokeEnd, you will need to use the second approach. – johnpatrickmorgan Sep 06 '15 at 09:20
  • hi @TommieC. your comments are somewhat confusing. this is exactly how you do it. Did you ever finish the project?! – Fattie Mar 18 '18 at 16:48
6

You want to look at CGPathApply (this is the short answer to your more refined question). You supply it with a function and it will call that function for each element (these will be lines and arc and closes) of the path. You can use that to reconstruct each closed item, and stash them into a list. Then you can figure out which direction each item is drawn in (I think this could actually be the hardest part) and rather then using strokeStart/strokeEnd one each subpath draw it in a layer with a mask and move the mask across the layer.

Stripes
  • 4,161
  • 2
  • 25
  • 29
  • Thanks for the hint, I'll begin investigating. I don't have the graphics background to really understand the power/capabilities implied with the use of a "mask". Additionally, I'm looking for a bit more detail so that this can be established as the canonical answer to what I think is a common problem. Anything that you can add to assist in making the "how" of this solution, clearer would be appreciated. – Tommie C. Apr 02 '15 at 17:27
1

Progress Report

This answer is posted to emphasize the significant progress being made on solving this question. With so much detail added to the question, I just wanted to clarify the progress on the solution and ultimately (when achieved), the definitive answer. Although I did select an answer that was helpful, please consider this post for the complete solution.

Approach # 1

Use existing UIBezierPath information to identify segments of the path (and ultimately) make use of those segments (and their coordinates) to stroke each subpath (according to available language rules).

(Current Thinking)

Erica Sadun is producing a SwiftSlowly repo on Github that supplies many functions on paths, including what appears to be a promising library on Segments (of a UIBezierPath), Line Intersections and many functions to act on these items. I have not had the time to review completely but I can envision that one might deconstruct a given path into segments based on the known stroke types. Once all stroke types are known for a given path, one might then evaluate the relative path coordinates to assign stroke-order. After that simply animate the strokes (a subclass of UIBezierPath) according to their stroke order.

Approach # 2

Use a stroke-based font instead of an outline-based font.

(Current Thinking)

I have found a sample of a stroke-based font and been able to animate the stroke. These fonts come with a built-in stroke order. I do not have access to a completed stroke-based font that also supports Chinese but encourage anyone with knowledge of such a font to reply in comments.

I have made a recommendation to Apple that they supply stroke-based fonts in future releases. The Swift Playground notes and the files (with sample stroke fonts) are included in the question above. Please comment or post an answer if you have something constructive to add to this solution.

Stroke Order Rules

See the stroke order rules as described on the Clear Chinese website.

stroke-order-rules

Tommie C.
  • 12,895
  • 5
  • 82
  • 100
  • Did you find a solution to this? – Crashalot Oct 16 '18 at 03:52
  • @Crashalot - No, I did not find a satisfactory solution. I had to put this particular issue on the back burner. I do think that the answer selected is likely the best approach. I even think that based on the dimensions of each stroke, relative to the other strokes that I should be able to determine the stroke order using the known rules. Just busy on other projects. – Tommie C. Oct 16 '18 at 03:59