Cómo crear gráficos vectoriales en iOS

Introducción

Los recursos gráficos en el mundo digital son de dos tipos básicos, raster y vector. Las imágenes de trama son esencialmente una matriz rectangular de intensidades de píxeles. Los gráficos vectoriales, por otro lado, son representaciones matemáticas de formas..

Si bien hay situaciones en las que las imágenes de trama son irremplazables (fotos, por ejemplo), en otros escenarios, los gráficos vectoriales hacen sustitutos capaces. Los gráficos vectoriales hacen que la tarea de crear recursos gráficos para resoluciones de pantalla múltiples sea trivial. Al momento de escribir, hay al menos media docena de resoluciones de pantalla con las que lidiar en la plataforma iOS.

Una de las mejores cosas de los gráficos vectoriales es que pueden procesarse en cualquier resolución sin dejar de ser absolutamente nítidos y suaves. Esta es la razón por la cual las fuentes PostScript y TrueType se ven nítidas en cualquier ampliación. Debido a que las pantallas de los teléfonos inteligentes y las computadoras son rasteras en la naturaleza, en última instancia, la imagen vectorial debe representarse en la pantalla como una imagen raster con la resolución adecuada. Esto generalmente es atendido por la biblioteca de gráficos de bajo nivel y el programador no tiene que preocuparse por esto.

1. Cuándo usar gráficos vectoriales?

Echemos un vistazo a algunos escenarios en los que debería considerar el uso de gráficos vectoriales..

Iconos de aplicaciones y menús, elementos de la interfaz de usuario

Hace unos años, Apple evitó el skeuomorphism en la interfaz de usuario de sus aplicaciones y el propio iOS, en favor de diseños audaces y geométricamente precisos. Eche un vistazo a los iconos de la aplicación Cámara o Foto, por ejemplo.

Más probable que no, fueron diseñados utilizando herramientas de gráficos vectoriales. Los desarrolladores tuvieron que seguir su ejemplo y la mayoría de las aplicaciones populares (que no son juegos) se sometieron a una metamorfosis completa para ajustarse a este paradigma de diseño..

Juegos

Los juegos con gráficos simples (piense en asteroides) o temas geométricos (Super Hexagon y Geometry Jump vienen a la mente) pueden tener sus sprites renderizados a partir de vectores. Lo mismo se aplica a los juegos que tienen niveles generados de manera procesal..

Imágenes

Imágenes en las que desea inyectar una pequeña cantidad de aleatoriedad para obtener múltiples versiones de la misma forma básica.

2. Curvas de Bezier

¿Qué son las curvas de Bezier? Sin profundizar en la teoría matemática, hablemos sobre las características que son de uso práctico para los desarrolladores..

Grados de libertad

Las curvas de Bézier se caracterizan por la cantidad de grados de libertad ellos tienen. Cuanto más alto es este grado, más variación puede incorporar la curva (pero también más matemáticamente compleja es).

Beziers grado uno son segmentos de línea recta. Grado dos curvas se llaman curvas cuádruples. Las curvas de grado tres (cúbicas) son las que nos enfocaremos, porque ofrecen un buen compromiso entre flexibilidad y complejidad.

Los Beziers cúbicos pueden representar no solo curvas suaves, sino también bucles y cúspides. Varios segmentos de Bézier cúbicos se pueden conectar de extremo a extremo para formar formas más complicadas.

Béziers Cúbicos

Un Bezier cúbico se define por sus dos puntos finales y dos puntos de control adicionales que determinan su forma. En general, un título norte Bezier tiene (n-1) Puntos de control, sin contar los puntos finales..

Una característica atractiva de Beziers cúbicos es que estos puntos tienen una interpretación visual significativa. La línea que conecta un punto final a su punto de control adyacente actúa como una tangente a la curva en el punto final. Este hecho es útil para diseñar formas. Vamos a explotar esta propiedad más adelante en el tutorial..

Transformaciones geométricas

Debido a la naturaleza matemática de estas curvas, puede aplicarles transformaciones geométricas fácilmente, como escala, rotación y traslación, sin pérdida de fidelidad..

La siguiente imagen muestra una muestra de diferentes tipos de formas que puede tomar un solo Bezier cúbico. Observe cómo los segmentos de la línea verde actúan como tangentes a la curva..

3. Core Graphics y la UIBezierPath Clase

En iOS y OS X, los gráficos vectoriales se implementan utilizando la biblioteca Core Graphics basada en C. Construido sobre esto está UIKit / Cocoa, que agrega una apariencia de orientación a objetos. El caballo de batalla es el UIBezierPath clase (NSBezierPath en OS X), una implementación de una curva Bezier matemática.

los UIBezierPath la clase admite curvas Bezier de grado uno (segmentos de línea recta), dos (curvas cuádruples) y tres (curvas cúbicas).

Programáticamente, un UIBezierPath el objeto se puede construir pieza por pieza agregándole nuevos componentes (subpaths). Para facilitar esto, el UIBezierPath objeto realiza un seguimiento de la currentPoint propiedad. Cada vez que agregue un nuevo segmento de ruta, el último punto del segmento agregado se convertirá en el punto actual. Cualquier dibujo adicional que hagas generalmente comienza en este punto. Puede mover este punto explícitamente a una ubicación deseada.

La clase tiene métodos de conveniencia para hacer formas de uso común, como arcos y círculos, rectángulos (redondeados), etc. Internamente, estas formas se han construido mediante la conexión de varias rutas secundarias..

El camino general puede ser una forma abierta o cerrada. Incluso puede ser de auto-intersección o tener múltiples componentes cerrados.

4. Empezando

Este tutorial está destinado a servir como una mirada más allá de lo básico en la generación de gráficos vectoriales. Pero incluso si usted es un desarrollador experimentado que no ha usado Core Graphics o UIBezierPath antes, debería ser capaz de seguir adelante. Si eres nuevo en esto, te recomiendo hojear el UIBezierPath referencia de clase (y las funciones subyacentes de Core Graphics) si aún no está familiarizado con ella. Solo podemos ejercer un número limitado de funciones de la API en un solo tutorial.

Basta de hablar. Vamos a empezar a programar. En el resto de este tutorial, presentaré dos escenarios donde los gráficos vectoriales son la herramienta ideal para usar.

Arranque Xcode, cree un nuevo patio de recreo y configure la plataforma en iOS. Por cierto, los patios de juego de Xcode son otra razón por la que ahora es divertido trabajar con gráficos vectoriales. Puede modificar su código y obtener retroalimentación visual instantánea. Tenga en cuenta que debe usar la última versión estable de Xcode, que es 7.2 en el momento de escribir este artículo..

Escenario 1: Haciendo formas de nubes

Nos gustaría generar imágenes de nubes que se adhieran a una forma básica de la nube mientras tenemos algo de aleatoriedad para que cada nube se vea diferente. El diseño básico en el que me he conformado es una forma compuesta, definida por varios círculos de radios aleatorios centrados a lo largo de una trayectoria elíptica de tamaño aleatorio (dentro de los rangos apropiados).

Para aclarar, esto es lo que parece el objeto general si acariciamos la ruta del vector en lugar de rellenarla.

Si su geometría está un poco oxidada, entonces esta imagen de Wikipedia muestra cómo se ve una elipse.

Algunas funciones de utilidad

Empecemos escribiendo un par de funciones de ayuda..

"javascript importar UIKit

func randomInt (inferior inferior: Int, superior: Int) -> Int assert (inferior < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower)))

func circle (en el centro: CGPoint, radio: CGFloat) -> UIBezierPath return UIBezierPath (centro del arco: centro, radio: radio, inicioAngle: 0, finalAngle: CGFloat (2 * M_PI), en sentido horario: verdadero) "

los aleatorio (inferior: superior :) función utiliza el incorporado arc4random_uniform () Función para generar números aleatorios en el rango. inferior y (superior-1). los círculo (en: centro :) La función genera una ruta Bezier, que representa un círculo con un dado. centrar y radio.

Generando puntos y caminos

Centrémonos ahora en generar los puntos a lo largo del camino elíptico. Una elipse centrada en el origen del sistema de coordenadas con sus ejes alineados a lo largo de los ejes de coordenadas tiene una forma matemática particularmente simple que se ve así..

P (r, θ) = (a cos (θ), b sin (θ))

Asignamos valores aleatorios para las longitudes de sus ejes mayor y menor para que la forma se vea como una nube, más alargada horizontalmente que verticalmente.

Usamos el paso() función para generar ángulos regularmente espaciados alrededor del círculo, y luego usar mapa() para generar puntos espaciados regularmente en la elipse usando la expresión matemática anterior.

"javascript deja a = Doble (randomInt (inferior: 70, superior: 100)) deja b = Doble (randomInt (inferior: 10, superior: 35)) deja ndiv = 12 como Doble

let points = (0.0) .stride (to: 1.0, by: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0))  "

Generamos la "masa" central de la nube uniendo los puntos a lo largo del camino elíptico. Si no lo hacemos, obtendremos un gran vacío en el centro..

"javascript let path = UIBezierPath () path.moveToPoint (puntos [0])

para puntos en puntos [1…

path.closePath () "

Tenga en cuenta que la ruta exacta no importa, ya que estaremos llenando la ruta, no acariciándola. Esto significa que no será distinguible de los círculos..

Para generar los círculos, primero elegimos heurísticamente un rango para los radios de círculo aleatorio. El hecho de que estemos desarrollando esto en un patio de recreo me ayudó a jugar con los valores hasta que obtuve un resultado con el que estuve satisfecho..

"javascript deja minRadius = (Int) (M_PI * a / ndiv) deja maxRadius = minRadius + 25

para puntos en puntos [0…

camino"

Vista previa del resultado

Puede ver el resultado haciendo clic en el icono "ojo" en el panel de resultados a la derecha, en la misma línea que la declaración de "ruta".

Toques finales

¿Cómo rasterizamos esto para obtener el resultado final? Necesitamos lo que se conoce como un "contexto gráfico" en el que dibujar las rutas. En nuestro caso, estaremos dibujando en una imagen (una UIImage ejemplo). Es en este punto que necesita establecer varios parámetros que especifiquen cómo se representará la ruta final, como los colores y los anchos de trazo. Finalmente, acaricias o rellenas tu camino (o ambos). En nuestro caso, queremos que nuestras nubes sean blancas, y solo queremos llenarlas.

Empaquetemos este código en una función para que podamos generar tantas nubes como deseemos. Y mientras estamos en eso, escribiremos un código para dibujar algunas nubes al azar sobre un fondo azul (que representa el cielo) y para dibujar todo esto en la vista en vivo del patio de recreo..

Aquí está el código final:

"javascript import UIKit import XCPlayground

func generarRandomCloud () -> UIImage

func randomInt (inferior inferior: Int, superior: Int) -> Int assert (inferior < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower)))  func circle(at center: CGPoint, radius: CGFloat) -> UIBezierPath return UIBezierPath (arcCenter: center, radius: radio, startAngle: 0, endAngle: CGFloat (2 * M_PI), en sentido horario: verdadero) let a = Double (randomInt (inferior: 70, upper: 100)) let b = Double (randomInt (inferior: 10, superior: 35)) deja ndiv = 12 como Double let points = (0.0) .stride (to: 1.0, by: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0)) let path = UIBezierPath () path.moveToPoint (puntos [0]) para puntos en puntos [1 ...  

Vista de clase: UIView override func drawRect (rect: CGRect) let ctx = UIGraphicsGetCurrentContext () UIColor.blueColor (). setFill () CGContextFillRect (ctx, rect)

 let cloud1 = generateRandomCloud (). CGImage let cloud2 = generateRandomCloud (). CGImage let cloud3 = generateRandomCloud (). CGImage CGContextDrawImage (ctx, CGRect (x: 20, y: 20, width: CGImageGetWidth (cloud1), altura: CGImagen) )), cloud1) CGContextDrawImage (ctx, CGRect (x: 300, y: 100, ancho: CGImageGetWidth (cloud2), altura: CGImageGetHeight (cloud2)), cloud2) CGContextDrawImage (ctx, CGRect (x: 50, y: 200, ancho: CGImageGetWidth (cloud3), altura: CGImageGetHeight (cloud3)), cloud3) 

XCPlaygroundPage.currentPage.liveView = Vista (marco: CGRectMake (0, 0, 600, 800))

"

Y así es como se ve el resultado final:

Las siluetas de las nubes aparecen un poco borrosas en la imagen anterior, pero esto es simplemente un artefacto de cambio de tamaño. La verdadera imagen de salida es nítida..

Para verlo en su propio patio de juegos, asegúrese de Editor asistente Esta abierto. Seleccionar Mostrar editor asistente desde el Ver menú.

Escenario 2: Generando piezas de rompecabezas

Las piezas del rompecabezas generalmente tienen un “marco” cuadrado, con cada borde plano, con una pestaña redondeada que sobresale hacia afuera, o una ranura de la misma forma para tesellate con una pestaña de una pieza adyacente. Aquí hay una sección de un rompecabezas típico.

Variaciones acogedoras con gráficos vectoriales

Si estuvieras desarrollando una aplicación de rompecabezas, querrías usar una máscara con forma de pieza de rompecabezas para segmentar la imagen que representa el rompecabezas. Podría optar por las máscaras ráster pregeneradas que envió con la aplicación, pero tendría que incluir varias variaciones para acomodar todas las posibles variaciones de forma de los cuatro bordes..

Con los gráficos vectoriales, puede generar la máscara para cualquier tipo de pieza sobre la marcha. Además, sería más fácil acomodar otras variaciones, como si quisieras piezas rectangulares u oblicuas (en lugar de piezas cuadradas).

Diseñando el límite de la pieza de rompecabezas

¿Cómo diseñamos realmente la pieza del rompecabezas, es decir, cómo descubrimos cómo colocar nuestros puntos de control para generar un camino más parecido al de la pestaña curva??

Recordemos la útil propiedad tangencial de los Beziers cúbicos que mencioné anteriormente. Puede comenzar dibujando una aproximación a la forma deseada, dividiéndola en segmentos estimando cuántos segmentos cúbicos necesitará (sabiendo los tipos de formas que puede acomodar un solo segmento cúbico) y luego dibujando tangentes a estos segmentos para averiguar Donde puedes colocar tus puntos de control. Aquí hay un diagrama que explica de qué estoy hablando..

Relacionar la forma con los puntos de control de la curva Bezier

Determiné que para representar la forma de la pestaña, cuatro segmentos Bezier harían muy bien:

  • dos que representan los segmentos de línea recta en cada extremo de la forma
  • dos que representan los segmentos en forma de S que representan la pestaña en el centro

Observe que los segmentos de línea discontinua verde y amarilla actúan como tangentes a los segmentos en forma de S, lo que me ayudó a estimar dónde colocar los puntos de control. Tenga en cuenta también que visualicé la pieza con una longitud de una unidad, por lo que todas las coordenadas son fracciones de una. Fácilmente podría haber hecho que mi curva tuviera, digamos, 100 puntos de largo (escalando los puntos de control por un factor de 100). La independencia de resolución de los gráficos vectoriales significa que esto no es un problema.

Por último, utilicé Béziers cúbicos incluso para los segmentos de línea recta simplemente por conveniencia, para que el código pudiera escribirse de manera más concisa y uniforme..

Me salté dibujando los puntos de control de los segmentos rectos en el diagrama para evitar el desorden. Por supuesto, un Bezier cúbico que representa una línea simplemente tiene los puntos finales y los puntos de control, todos a lo largo de la misma línea..

El hecho de que estés desarrollando esto en un patio de recreo significa que puedes "reajustar" fácilmente los valores de los puntos de control para encontrar una forma que te guste y obtener retroalimentación instantánea.

Empezando

Empecemos. Puedes usar el mismo campo de juego que antes agregando una nueva página. Escoger Nuevo> Página de juegos desde el Expediente menú o crear un nuevo parque infantil si lo prefiere.

Reemplace cualquier código en la nueva página con lo siguiente:

"javascript importar UIKit

Deje que outie_coords: [(x: CGFloat, y: CGFloat)] = [(1.0 / 9, 0), (2.0 / 9, 0), (1.0 / 3, 0), (37.0 / 60, 0), (1.0 / 6, 1.0 / 3), (1.0 / 2, 1.0 / 3), (5.0 / 6, 1.0 / 3), (23.0 / 60, 0), (2.0 / 3, 0), (7.0 / 9, 0 ), (8.0 / 9, 0), (1.0, 0)]

let size: CGFloat = 100 let outie_points = outie_coords.map CGPointApplyAffineTransform (CGPointMake ($ 0.x, $ 0.y), CGAffineTransformMakeScale (tamaño, tamaño))

let path = UIBezierPath () path.moveToPoint (CGPointZero)

para i en 0.stride (a través de: outie_points.count - 3, por: 3) path.addCurveToPoint (outie_points [i + 2], controlPoint1: outie_points [i], controlPoint2: outie_points [i + 1])]

camino"

Generando los cuatro lados usando transformaciones geométricas

Tenga en cuenta que decidimos hacer que nuestro camino fuera de 100 puntos aplicando una transformación de escala a los puntos..

Vemos el siguiente resultado usando la función "Vista rápida":

Hasta ahora tan bueno. ¿Cómo generamos cuatro lados de la pieza de rompecabezas? La respuesta es (como puedes adivinar), usando transformaciones geométricas. Aplicando una rotación de 90 grados seguida de una traducción apropiada a camino Por encima, podemos generar fácilmente el resto de los lados..

Advertencia: un problema con el relleno interior

Hay una advertencia aquí, por desgracia. La transformación no se unirá automáticamente a segmentos individuales juntos. A pesar de que la silueta de nuestra pieza de rompecabezas se ve bien, su interior no se llenará y tendremos problemas para usarlo como máscara. Podemos observar esto en el patio de recreo. Agregue el siguiente código:

"javascript let transform = CGAffineTransformTranslate (CGAffineTransformMakeRotation (CGFloat (-M_PI / 2)), 0, tamaño)

Deje que temppath = path.copy () as! UIBezierPath

let foursided = UIBezierPath () para i en 0 ... 3 temppath.applyTransform (transform) foursided.appendPath (temppath)

a cuatro lados

Quick Look nos muestra lo siguiente:

Observe que el interior de la pieza no está sombreado, lo que indica que no se ha llenado.

Puedes averiguar los comandos de dibujo utilizados para construir un complejo. UIBezierPath examinando su debugDescription propiedad en el patio de recreo.

Resolviendo el problema de llenado

Transformaciones geométricas en UIBezierPath funciona bastante bien para el caso de uso común, es decir, cuando ya tienes una forma cerrada o la forma que estás transformando es intrínsecamente abierta, y deseas generar versiones de ellos geométricamente transformadas. Nuestro caso de uso es diferente. El camino actúa como un subpath en una forma más grande que estamos construyendo y cuyo interior pretendemos rellenar. Esto es un poco más complicado.

Un enfoque sería entrometerse con las partes internas de la ruta (usando el CGPathApply () función de Core Graphics API) y unir manualmente los segmentos para obtener una forma única, cerrada y con el relleno adecuado.

Pero esa opción se siente un poco pirateada y es por eso que opté por un enfoque diferente. Aplicamos las transformaciones geométricas a los puntos mismos primero, a través de la CGPointApplyAffineTransform () Función, aplicando exactamente la misma transformación que intentamos usar hace un momento. Luego usamos los puntos transformados para crear la ruta secundaria, que se adjunta a la forma general. Al final del tutorial, veremos un ejemplo donde podemos aplicar correctamente una transformación geométrica a la ruta Bezier.

Generando las variaciones del borde de la pieza

¿Cómo generamos la pestaña "innie"? Podríamos aplicar una transformada geométrica nuevamente, con un factor de escala negativo en la dirección y (invirtiendo su forma), pero opté por hacerlo manualmente simplemente invirtiendo las coordenadas y de los puntos en outie_points.

En cuanto a la pestaña de bordes planos, si bien podría haber utilizado un segmento de línea recta para representarlo, para evitar tener que especializar el código para casos distintos, simplemente establezco la coordenada y de cada punto en outie_points a cero. Esto nos da:

javascript deja innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) deja flat_points = outie_points.map CGPointMake ($ 0.x, 0)

Como ejercicio, puede generar curvas de Bézier a partir de estos bordes y verlos con la Vista rápida.

Ahora sabes lo suficiente para que te bombardee con todo el código, que une todo en una sola función.

Reemplace todo el contenido de la página de juegos con lo siguiente:

"javascript import UIKit import XCPlayground

enum Edge caso Outie caso Innie caso plano

func jigsawPieceMaker (tamaño tamaño: CGFloat, bordes: [Edge]) -> UIBezierPath

func incrementalPathBuilder (firstPoint: CGPoint) -> ([CGPoint]) -> UIBezierPath let path = UIBezierPath () path.moveToPoint (firstPoint) return points in assert (points.count% 3 == 0) para i en 0. stride (a través de: points.count - 3, por: 3) path.addCurveToPoint (points [i + 2], controlPoint1: points [i], controlPoint2: points [i + 1]) return path let outie_coords: [(x: CGFloat, y: CGFloat)] = [/ * (0, 0), * / (1.0 / 9, 0), (2.0 / 9, 0), (1.0 / 3, 0), (37.0 / 60, 0), (1.0 / 6, 1.0 / 3), (1.0 / 2, 1.0 / 3), (5.0 / 6, 1.0 / 3), (23.0 / 60, 0), (2.0 / 3, 0) , (7.0 / 9, 0), (8.0 / 9, 0), (1.0, 0)] deja outie_points = outie_coords.map CGPointApplyAffineTransform (CGPointMake ($ 0.x, $ 0.y), CGAffineTransformMakeScale (tamaño, tamaño))  deja que innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) deja flat_points = outie_points.map CGPointMake ($ 0.x, 0) var shapeDict: [Edge: [CGPoint]] = [.Outie : outie_points, .Innie: innie_points, .Flat: flat_points] let transform = CGAffineTransformTranslate (CGAffineTransformMakeRotation (CGFl avena (-M_PI / 2)), 0, tamaño) deje path_builder = incrementalPathBuilder (CGPointZero) var path: UIBezierPath! para el borde en los bordes path = path_builder (shapeDict [edge]!) para (e, pts) en shapeDict let tr_pts = pts.map CGPointApplyAffineTransform ($ 0, transform)) shapeDict [e] = tr_pts path.closePath ( ) vía de retorno  

Let piece1 = jigsawPieceMaker (tamaño: 100, bordes: [.Innie, .Outie, .Flat, .Innie])

let piece2 = jigsawPieceMaker (tamaño: 100, bordes: [.Innie, .Innie, .Innie, .Innie]) piece2.applyTransform (CGAffineTransformMakeRotation (CGFloat (M_PI / 3)))

"

Solo hay algunas cosas más interesantes en el código que me gustaría aclarar:

  • Usamos un enumerar Para definir las diferentes formas de borde. Almacenamos los puntos en un diccionario que utiliza los valores de enumeración como claves..
  • Unimos las rutas secundarias (que consisten en cada borde de la forma de pieza de rompecabezas de cuatro lados) en el incrementalPathBuilder (_) Función, definida internamente a la jigsawPieceMaker (tamaño: bordes :) función.
  • Ahora que la pieza de rompecabezas está llena correctamente, como podemos ver en la salida de Quick Look, podemos usar con seguridad applyTransform (_ :) Método para aplicar una transformada geométrica a la forma. Como ejemplo, he aplicado una rotación de 60 grados a la segunda pieza..

Conclusión

Espero haberte convencido de que la capacidad de generar gráficos vectoriales mediante programación puede ser una habilidad útil para tener en tu arsenal. Con suerte, se sentirá inspirado para pensar (y codificar) otras aplicaciones interesantes para gráficos vectoriales que puede incorporar en sus propias aplicaciones..