Cómo crear gráficos vectoriales en iOS
() translation by (you can also view the original English article)
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 resuelto 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?
Veamos algunos escenarios en los que deberías 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 varias 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, solo hablemos de 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 que tienen. Cuanto más alto sea 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 aquellas en las que nos centraremos, 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 Bezier cúbicos se pueden conectar de extremo a extremo para formar formas más complicadas.
Cúbicos Béziers
Un Bezier cúbico se define por sus dos puntos finales y dos puntos de control adicionales que determinan su forma. En general, un grado n 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 ninguna 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 clase UIBezierPath
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 la clase UIBezierPath
(NSBezierPath
en OS X), una implementación de una curva Bezier matemática.
La clase UIBezierPath
admite curvas Bezier de grado uno (segmentos de línea recta), dos (curvas cuádruples) y tres (curvas cúbicas).
Programáticamente, un objeto UIBezierPath
puede construirse pieza por pieza agregándole nuevos componentes (subpaths). Para facilitar esto, el objeto UIBezierPath
realiza un seguimiento de la propiedad currentPoint
. 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 eres un desarrollador experimentado que no ha usado Core Graphics o UIBezierPath
antes, deberías poder seguirlo. Si eres nuevo en esto, te recomendamos que hojees la referencia de clase UIBezierPath
(y las funciones subyacentes de Core Graphics) si aún no estás familiarizado. Solo podemos ejercer un número limitado de funciones de la API en un solo tutorial.
Basta de hablar. Vamos a empezar a codificar. 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 por escribir un par de funciones de ayuda.
1 |
import UIKit |
2 |
|
3 |
func randomInt(lower lower: Int, upper: Int) -> Int { |
4 |
assert(lower < upper) |
5 |
return lower + Int(arc4random_uniform(UInt32(upper - lower))) |
6 |
}
|
7 |
|
8 |
func circle(at center: CGPoint, radius: CGFloat) -> UIBezierPath { |
9 |
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true) |
10 |
}
|
La función random(lower:upper:)
utiliza la función incorporada arc4random_uniform()
para generar números aleatorios en el rango lower
y (upper-1)
. La función de circle(at:center:)
genera una ruta Bezier, que representa un círculo con un center
y un radius
dados.
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í.
1 |
P(r, θ) = (a cos(θ), b sin(θ)) |
Asignamos valores aleatorios para las longitudes de sus ejes mayor y menor, de modo que la forma se vea como una nube, más alargada horizontalmente que verticalmente.
Usamos la función stride()
para generar ángulos espaciados regularmente alrededor del círculo, y luego usamos map()
para generar puntos regularmente espaciados en la elipse usando la expresión matemática anterior.
1 |
let a = Double(randomInt(lower: 70, upper: 100)) |
2 |
let b = Double(randomInt(lower: 10, upper: 35)) |
3 |
let ndiv = 12 as Double |
4 |
|
5 |
|
6 |
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.
1 |
let path = UIBezierPath() |
2 |
path.moveToPoint(points[0]) |
3 |
|
4 |
for point in points[1..<points.count] { |
5 |
path.addLineToPoint(point) |
6 |
}
|
7 |
|
8 |
path.closePath() |
Tenga en cuenta que la ruta exacta no importa, porque estaremos llenando la ruta, no acariciándola. Esto significa que no se podrá distinguir 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.
1 |
let minRadius = (Int)(M_PI * a/ndiv) |
2 |
let maxRadius = minRadius + 25 |
3 |
|
4 |
for point in points[0..<points.count] { |
5 |
let randomRadius = CGFloat(randomInt(lower: minRadius, upper: maxRadius)) |
6 |
let circ = circle(at: point, radius: randomRadius) |
7 |
path.appendPath(circ) |
8 |
}
|
9 |
|
10 |
path
|
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, dibujaremos en una imagen (una instancia de UIImage
). 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 deseamos. Y mientras estamos en eso, escribiremos un código para dibujar algunas nubes al azar sobre un fondo azul (que representa el cielo) y dibujar todo esto en la vista en vivo del patio de recreo.
Aquí está el código final:
1 |
import UIKit |
2 |
import XCPlayground |
3 |
|
4 |
func generateRandomCloud() -> UIImage { |
5 |
|
6 |
func randomInt(lower lower: Int, upper: Int) -> Int { |
7 |
assert(lower < upper) |
8 |
return lower + Int(arc4random_uniform(UInt32(upper - lower))) |
9 |
}
|
10 |
|
11 |
func circle(at center: CGPoint, radius: CGFloat) -> UIBezierPath { |
12 |
return UIBezierPath(arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat(2 * M_PI), clockwise: true) |
13 |
}
|
14 |
|
15 |
let a = Double(randomInt(lower: 70, upper: 100)) |
16 |
let b = Double(randomInt(lower: 10, upper: 35)) |
17 |
let ndiv = 12 as Double |
18 |
|
19 |
|
20 |
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)) } |
21 |
|
22 |
let path = UIBezierPath() |
23 |
path.moveToPoint(points[0]) |
24 |
for point in points[1..<points.count] { |
25 |
path.addLineToPoint(point) |
26 |
}
|
27 |
path.closePath() |
28 |
|
29 |
|
30 |
let minRadius = (Int)(M_PI * a/ndiv) |
31 |
let maxRadius = minRadius + 25 |
32 |
|
33 |
for point in points[1..<points.count] { |
34 |
let randomRadius = CGFloat(randomInt(lower: minRadius, upper: maxRadius)) |
35 |
let circ = circle(at: point, radius: randomRadius) |
36 |
path.appendPath(circ) |
37 |
}
|
38 |
|
39 |
//return path
|
40 |
let (width, height) = (path.bounds.width, path.bounds.height) |
41 |
let margin = CGFloat(20) |
42 |
UIGraphicsBeginImageContext(CGSizeMake(path.bounds.width + margin, path.bounds.height + margin)) |
43 |
UIColor.whiteColor().setFill() |
44 |
path.applyTransform(CGAffineTransformMakeTranslation(width/2 + margin/2, height/2 + margin/2)) |
45 |
path.fill() |
46 |
let im = UIGraphicsGetImageFromCurrentImageContext() |
47 |
return im |
48 |
}
|
49 |
|
50 |
|
51 |
class View: UIView { |
52 |
override func drawRect(rect: CGRect) { |
53 |
let ctx = UIGraphicsGetCurrentContext() |
54 |
UIColor.blueColor().setFill() |
55 |
CGContextFillRect(ctx, rect) |
56 |
|
57 |
let cloud1 = generateRandomCloud().CGImage |
58 |
let cloud2 = generateRandomCloud().CGImage |
59 |
let cloud3 = generateRandomCloud().CGImage |
60 |
CGContextDrawImage(ctx, CGRect(x: 20, y: 20, width: CGImageGetWidth(cloud1), height: CGImageGetHeight(cloud1)), cloud1) |
61 |
|
62 |
CGContextDrawImage(ctx, CGRect(x: 300, y: 100, width: CGImageGetWidth(cloud2), height: CGImageGetHeight(cloud2)), cloud2) |
63 |
|
64 |
CGContextDrawImage(ctx, CGRect(x: 50, y: 200, width: CGImageGetWidth(cloud3), height: CGImageGetHeight(cloud3)), cloud3) |
65 |
}
|
66 |
}
|
67 |
|
68 |
XCPlaygroundPage.currentPage.liveView = View(frame: CGRectMake(0, 0, 600, 800)) |
69 |
|
70 |
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 que el Asistente Editor esté abierto. Seleccione Mostrar asistente del editor en el menú Ver.
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 del 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 encontramos la forma de colocar nuestros puntos de control para generar un camino más bonito que se parece a 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, usé Béziers cúbicos incluso para los segmentos de línea recta simplemente por conveniencia, de modo que el código se pudiera escribir 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 que se encuentran 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. Seleccione New > Playground Page en el menú File o cree un nuevo playground si lo prefiere.
Reemplace cualquier código en la nueva página con lo siguiente:
1 |
import UIKit |
2 |
|
3 |
let 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)] |
4 |
|
5 |
let size: CGFloat = 100 |
6 |
let outie_points = outie_coords.map { CGPointApplyAffineTransform(CGPointMake($0.x, $0.y), CGAffineTransformMakeScale(size, size)) } |
7 |
|
8 |
|
9 |
let path = UIBezierPath() |
10 |
path.moveToPoint(CGPointZero) |
11 |
|
12 |
for i in 0.stride(through: outie_points.count - 3, by: 3) { |
13 |
path.addCurveToPoint(outie_points[i+2], controlPoint1: outie_points[i], controlPoint2: outie_points[i+1]) |
14 |
}
|
15 |
|
16 |
path
|
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. Al aplicar una rotación de 90 grados seguida de una traducción apropiada a path
anterior, 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. A pesar de que la silueta de nuestra pieza de rompecabezas se ve bien, su interior no se llenará y enfrentaremos problemas al usarlo como máscara. Podemos observar esto en el patio de recreo. Agregue el siguiente código:
1 |
let transform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(CGFloat(-M_PI/2)), 0, size) |
2 |
|
3 |
let temppath = path.copy() as! UIBezierPath |
4 |
|
5 |
let foursided = UIBezierPath() |
6 |
for i in 0...3 { |
7 |
temppath.applyTransform(transform) |
8 |
foursided.appendPath(temppath) |
9 |
|
10 |
}
|
11 |
|
12 |
foursided
|
Quick Look nos muestra lo siguiente:



Observe que el interior de la pieza no está sombreado, lo que indica que no se ha llenado.
Puede averiguar los comandos de dibujo utilizados para
construir un UIBezierPath
complejo examinando su propiedad
debugDescription
en el playground.
Resolviendo el problema de llenado
Las transformaciones geométricas en UIBezierPath
funcionan bastante bien para el caso de uso común, es decir, cuando ya tiene una forma cerrada o la forma que está transformando es intrínsecamente abierta, y desea generar versiones de ellas 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 (utilizando la función CGPathApply()
de la 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. Primero aplicamos las transformaciones geométricas a los puntos mismos, a través de la función CGPointApplyAffineTransform()
, 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 en el que 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 de nuevo, 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 puse la coordenada y de cada punto en outie_points
a cero . Esto nos da:
1 |
let innie_points = outie_points.map { CGPointMake($0.x, -$0.y) } |
2 |
let flat_points = outie_points.map { CGPointMake($0.x, 0) } |
Como ejercicio, puede generar curvas de Bézier a partir de estos bordes y verlos utilizando la Quick Look.
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 playground con lo siguiente:
1 |
import UIKit |
2 |
import XCPlayground |
3 |
|
4 |
|
5 |
enum Edge { |
6 |
case Outie |
7 |
case Innie |
8 |
case Flat |
9 |
}
|
10 |
|
11 |
func jigsawPieceMaker(size size: CGFloat, edges: [Edge]) -> UIBezierPath { |
12 |
|
13 |
func incrementalPathBuilder(firstPoint: CGPoint) -> ([CGPoint]) -> UIBezierPath { |
14 |
let path = UIBezierPath() |
15 |
path.moveToPoint(firstPoint) |
16 |
return { |
17 |
points in |
18 |
assert(points.count % 3 == 0) |
19 |
for i in 0.stride(through: points.count - 3, by: 3) { |
20 |
path.addCurveToPoint(points[i+2], controlPoint1: points[i], controlPoint2: points[i+1]) |
21 |
}
|
22 |
|
23 |
return path |
24 |
}
|
25 |
}
|
26 |
|
27 |
|
28 |
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)] |
29 |
|
30 |
let outie_points = outie_coords.map { CGPointApplyAffineTransform(CGPointMake($0.x, $0.y), CGAffineTransformMakeScale(size, size)) } |
31 |
let innie_points = outie_points.map { CGPointMake($0.x, -$0.y) } |
32 |
let flat_points = outie_points.map { CGPointMake($0.x, 0) } |
33 |
|
34 |
var shapeDict: [Edge: [CGPoint]] = [.Outie: outie_points, .Innie: innie_points, .Flat: flat_points] |
35 |
|
36 |
|
37 |
let transform = CGAffineTransformTranslate(CGAffineTransformMakeRotation(CGFloat(-M_PI/2)), 0, size) |
38 |
let path_builder = incrementalPathBuilder(CGPointZero) |
39 |
var path: UIBezierPath! |
40 |
for edge in edges { |
41 |
path = path_builder(shapeDict[edge]!) |
42 |
|
43 |
for (e, pts) in shapeDict { |
44 |
let tr_pts = pts.map { CGPointApplyAffineTransform($0, transform) } |
45 |
shapeDict[e] = tr_pts |
46 |
}
|
47 |
}
|
48 |
|
49 |
path.closePath() |
50 |
return path |
51 |
}
|
52 |
|
53 |
|
54 |
let piece1 = jigsawPieceMaker(size: 100, edges: [.Innie, .Outie, .Flat, .Innie]) |
55 |
|
56 |
let piece2 = jigsawPieceMaker(size: 100, edges: [.Innie, .Innie, .Innie, .Innie]) |
57 |
piece2.applyTransform(CGAffineTransformMakeRotation(CGFloat(M_PI/3))) |
58 |
Solo hay algunas cosas más interesantes en el código que me gustaría aclarar:
- Utilizamos una
enum
para definir las diferentes formas de borde. Almacenamos los puntos en un diccionario que usa los valores de enumeración como claves. - Reunimos las rutas secundarias (que consisten en cada borde de la forma de pieza de cuatro lados del rompecabezas) en la función
incrementalPathBuilder(_)
, definida internamente para la funciónjigsawPieceMaker(size:edges:)
. - Ahora que la pieza de rompecabezas se rellena correctamente, como podemos ver en la salida de Vista rápida, podemos usar de forma segura el método
applyTransform(_:)
para aplicar una transformación 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.