ios swift core-animation uibezierpath cgpath

¿Cómo animar un trazo humano escrito usando Swift/iOS?



core-animation uibezierpath (3)

Objetivo

Estoy intentando crear una aproximación animada de escritura humana, usando un UIBezierPath generado a partir de un glifo. Entiendo y he leído las muchas preguntas de UIBezierPath que suenan similares a las mías (pero no son lo mismo). Mi objetivo es crear una animación que se aproxime al aspecto de un ser humano interpretando caracteres y letras.

Fondo

Creé un campo de juego que estoy usando para comprender mejor los caminos para usar en la animación de glifos como si fueran dibujados a mano.

Un par de conceptos en el Apple CAShapeLayer (strokeStart y strokeEnd) realmente no parecen funcionar como se espera cuando está animado. Creo que, en general, las personas tienden a pensar en un accidente cerebrovascular como si lo hicieran con un instrumento de escritura (básicamente una línea recta o curva). Consideramos que el trazo y el relleno son una línea, ya que nuestros instrumentos de escritura no distinguen entre trazo y relleno.

Pero cuando está animado, el contorno de un camino está construido por segmentos de línea (el relleno se trata por separado y no está claro cómo animar la posición del relleno?). Lo que quiero lograr es una línea / curva natural escrita por humanos que muestre el inicio y el final de un trazo junto con la parte del relleno que se agrega a medida que la animación se mueve de principio a fin. Inicialmente, esto parece simple, pero creo que puede requerir animar la posición de llenado (no estoy seguro de cómo hacerlo), el inicio / finalización del trazo (no estoy seguro si esto es necesario debido a las advertencias inesperadas sobre cómo funciona la animación indicada anteriormente) y hacer uso de subrutas (cómo reconstruir a partir de una ruta conocida).

Enfoque 1

Entonces, he considerado la idea de una ruta (CGPath / UIBezierPath). Cada ruta en realidad contiene todos los subtrazos necesarios para construir un glifo, por lo que tal vez recurrir a esos subcampos y usar CAKeyframeAnimation / CABasicAnimations y un grupo de animación que muestre los subcampos parcialmente construidos sería una buena aproximación (aunque la posición de relleno y el trazo de cada subpaso seguirían necesita ser animado de principio a fin?).

Este enfoque lleva a la pregunta refinada:

¿Cómo acceder y crear UIBezierPath / CGPath (subpaths) si uno tiene un UIBezierPath / CGPath completo?

¿Cómo animar el relleno y el trazo como si se dibujara con un instrumento de escritura utilizando la información de ruta / subtrayecto? (aparentemente esto implica que uno necesitaría animar las propiedades strokeStart / strokeEnd, position y path de un CALayer al mismo tiempo)

NÓTESE BIEN:

Como se puede observar en el código, tengo las rutas terminadas obtenidas de los glifos. Veo que la descripción de la ruta me proporciona información similar a una ruta. ¿Cómo tomaría esa información y la refundiría como una serie de caminos secundarios golpes de escritura humana?

(La idea aquí sería convertir la información de puntos en un nuevo tipo de datos de trazos similares a humanos. Esto implica un requisito para que un algoritmo identifique el inicio, la pendiente, el punto final y el límite de cada relleno)

Sugerencias

Noté en Pleco (una aplicación de iOS que implementa con éxito un algoritmo similar) que cada trazo está compuesto por una ruta cerrada que describe el trazo de escritura humana. UIBezierPath tiene un camino cerrado basado en rellenos conectados continuos. Se necesita un algoritmo para refinar los rellenos superpuestos para crear distintas rutas cerradas para cada stroke-type .

Erica Sadun tiene un conjunto de utilidades de ruta disponibles en github. No he explorado completamente estos archivos, pero podrían ser útiles para determinar los trazos discretos.

La estructura de UIBezierPath parece estar basada en la noción de una línea / curva de línea contigua. Hay puntos de confluencia que aparecen en las intersecciones de los rellenos, que representan un cambio de ruta direccional. ¿Podría uno calcular el ángulo de trazo / llenado de un segmento de curva / línea y buscar otras curvas / líneas para un punto de confluencia correspondiente? (es decir, conectar un segmento de línea a través del espacio de rellenos que se cruzan para producir dos caminos separados, suponiendo que uno recogió los puntos y recreó el camino con un nuevo segmento de línea / curva)

Introspectivamente: ¿Hay un método más simple? ¿Me falta una API crítica, un libro o un mejor enfoque para este problema?

Algunos métodos alternativos (no son útiles, se requiere cargar gifs o flash) para producir el resultado deseado:

Buen ejemplo (usando Flash) con una capa de presentación que muestra la progresión del trazo escrito. (Si es posible, esto es lo que me gustaría aproximar en Swift / iOS) - ( enlace alt - ver animación de la imagen a la izquierda)

Un ejemplo menos bueno que muestra el uso de trazados y rellenos progresivos para aproximar las características de trazos escritos (la animación no es uniforme y requiere recursos externos):

Una versión Flash : estoy familiarizado con la creación de animaciones Flash pero no estoy dispuesto a implementarlas en los 1000 (sin mencionar que no es compatible con iOS, aunque probablemente también podría convertir un algoritmo para aprovechar un lienzo HTML5 con animación css). Pero esta línea de pensamiento parece estar un poco lejos, después de todo, la información de ruta que quiero está almacenada en los glifos que he extraído de las fuentes / cadenas proporcionadas.

Enfoque 2

Estoy considerando el uso de una fuente stroke-based lugar de una fuente stroke-based el outline-based para obtener la información de ruta correcta (es decir, una donde el relleno se representa como una ruta). Si tiene éxito, este enfoque sería más limpio que la aproximación de los trazos, el tipo de trazo, las intersecciones y el orden de trazo. Ya envié un radar a Apple sugiriendo que las fuentes basadas en trazos se agreguen a iOS (# 20426819). A pesar de este esfuerzo, todavía no he abandonado la formación de un algoritmo que resuelve los trazos parciales, trazos completos, intersecciones y puntos de confluencia de los segmentos de línea y las curvas que se encuentran en la ruta de bezier.

Pensamientos actualizados basados ​​en discusión / respuestas

La siguiente información adicional se proporciona en función de las conversaciones y respuestas que se encuentran a continuación.

El orden de trazo es importante y la mayoría de los lenguajes (chino en este caso) tienen stroke-type claramente definidos y reglas de orden de trazo que parecen proporcionar un mecanismo para determinar el tipo y orden en función de la información de punto proporcionada con cada CGPathElement.

CGPathApply y CGPathApplierFunction parecen prometedores como medio para enumerar los subtrazos (guardados en una matriz y aplicar la animación de relleno)

Se puede aplicar una máscara a la capa para revelar una parte de la subcapa (no he usado esta propiedad anteriormente, pero parece que si pudiera mover una capa enmascarada sobre los subtrazos que podría ayudar a animar el relleno?)

Hay una gran cantidad de puntos definidos para cada ruta. Como si BezierPath se definiera utilizando solo el contorno del glifo. Este hecho hace que la comprensión del inicio, el final y la unión de los rellenos cruzados sea un factor importante para eliminar la ambigüedad de los rellenos específicos.

Se encuentran disponibles bibliotecas externas adicionales que pueden permitir una mejor resolución del comportamiento de las brazadas. Otra tecnología como el Sistema de Tipo de Azafrán o uno de sus derivados puede ser aplicable a este dominio de problema.

Un problema básico con la solución más simple de simplemente animar el trazo es que las fuentes disponibles de iOS son fuentes de contorno en lugar de fuentes basadas en trazos. Algunos fabricantes comerciales producen fuentes basadas en trazos. Por favor, siéntase libre de usar el enlace al archivo del patio de recreo si tiene uno de estos para probar.

Creo que este es un problema común y continuaré actualizando la publicación a medida que avance hacia una solución. Por favor, hágamelo saber en los comentarios si se requiere más información o si me pueden faltar algunos de los conceptos necesarios.

Solución posible

Siempre estoy en busca de la solución más simple posible. El problema se origina en la estructura de las fuentes que son fuentes de contorno en lugar de basadas en trazos. Encontré una muestra de una fuente basada en trazos para probar y la usé para evaluar una prueba de concepto ( ver video ). Ahora estoy buscando una fuente ampliada de un solo trazo (que incluya caracteres chinos) para evaluar mejor. Una solución menos simple podría ser encontrar una forma de crear un trazo que siga al relleno y luego usar una geometría 2D simple para evaluar qué trazo animar primero (por ejemplo, las reglas chinas son muy claras en el orden de los trazos).

Enlace a Playground en Github

  • Para usar la función XPCShowView: abra el Explorador de archivos y el Inspector de utilidades de archivos
  • Haga clic en el archivo del patio de recreo y en el FUI (elija Ejecutar en el simulador)
  • Para acceder al Editor Asistente: Ir al menú Ver> Editor Asistente
  • Para ver recursos / fuentes, haga clic con el botón derecho en Archivo de juegos en Buscador y Mostrar contenido del paquete
  • Si Playground está en blanco al abrir, copie el archivo en el escritorio y vuelva a abrir (error ??)

Código de área de juegos

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) }


Un par de conceptos en el Apple CAShapeLayer (strokeStart y strokeEnd) realmente no parecen funcionar como se espera cuando está animado.

Pero seguramente animar el strokeEnd es exactamente lo que quieres hacer. Use múltiples CAShapeLayers uno encima del otro, cada uno representa un trazo del lápiz para formar la forma del carácter deseado.


Informe de progreso

Esta respuesta se publica para enfatizar el progreso significativo que se está logrando al resolver esta pregunta. Con tantos detalles agregados a la pregunta, solo quería aclarar el progreso de la solución y finalmente (cuando se logra), la respuesta definitiva. Aunque seleccioné una respuesta que fue útil, considere esta publicación para obtener la solución completa.

Enfoque # 1

Utilice la información UIBezierPath existente para identificar segmentos de la ruta (y, en última instancia) utilice esos segmentos (y sus coordenadas) para recorrer cada subpaso (de acuerdo con las reglas de idioma disponibles).

(Pensamiento actual)

Erica Sadun está produciendo un repositorio SwiftSlowly en Github que proporciona muchas funciones en las rutas, incluyendo lo que parece ser una biblioteca prometedora en Segmentos (de un UIBezierPath), Intersecciones de línea y muchas funciones para actuar sobre estos elementos. No he tenido tiempo de revisarlo completamente, pero puedo imaginar que uno podría deconstruir una ruta determinada en segmentos basados ​​en los stroke-type conocidos. Una vez que se conocen todos los tipos de trazos para una ruta determinada, se pueden evaluar las coordenadas de la ruta relativa para asignar el orden de las trazos . Después de eso simplemente anima los trazos (una subclase de UIBezierPath) de acuerdo con su orden de trazo.

Enfoque # 2

Use una fuente basada en trazo en lugar de una fuente basada en el contorno.

(Pensamiento actual)

He encontrado una muestra de una fuente basada en trazos y he podido animar el trazo. Estas fuentes vienen con un orden de trazo incorporado. No tengo acceso a una fuente completa basada en trazo que también sea compatible con chino, pero aliento a cualquier persona con conocimiento de dicha fuente a responder en los comentarios.

Le he hecho una recomendación a Apple de que suministren fuentes basadas en trazos en lanzamientos futuros. Las notas de Swift Playground y los archivos (con fuentes de trazo de muestra) se incluyen en la pregunta anterior. Comente o publique una respuesta si tiene algo constructivo para agregar a esta solución.


Desea ver CGPathApply (esta es la respuesta corta a su pregunta más refinada). Usted le proporciona una función y llamará a esa función para cada elemento (estos serán líneas y arcos y se cierra) de la ruta. Puede usar eso para reconstruir cada elemento cerrado y esconderlos en una lista. Luego puede averiguar en qué dirección se dibuja cada elemento (creo que esta podría ser la parte más difícil) y en lugar de usar strokeStart / strokeEnd one cada subpath dibujarlo en una capa con una máscara y mover la máscara a través de la capa.