Le risorse grafiche nel mondo digitale sono di due tipi base, raster e vettoriale. Le immagini raster sono essenzialmente una matrice rettangolare di intensità di pixel. La grafica vettoriale, d'altra parte, sono rappresentazioni matematiche di forme.
Mentre ci sono situazioni in cui le immagini raster sono insostituibili (foto, ad esempio), in altri scenari, la grafica vettoriale rende sostituti capaci. La grafica vettoriale rende banale il compito di creare risorse grafiche per risoluzioni multiple dello schermo. Al momento della stesura di questo articolo, ci sono almeno una mezza dozzina di risoluzioni dello schermo per fare i conti con la piattaforma iOS.
Una delle cose migliori della grafica vettoriale è che possono essere rese a qualsiasi risoluzione pur rimanendo assolutamente nitide e lisce. Questo è il motivo per cui i font PostScript e TrueType appaiono nitidi a qualsiasi ingrandimento. Poiché gli schermi di smartphone e computer sono di natura raster, in definitiva, l'immagine vettoriale deve essere resa sul display come immagine raster alla risoluzione appropriata. Di solito questo è gestito dalla libreria grafica di basso livello e il programmatore non deve preoccuparsi di ciò.
Diamo un'occhiata ad alcuni scenari in cui dovresti considerare l'utilizzo della grafica vettoriale.
Alcuni anni fa, Apple ha evitato lo skeuomorphism nell'interfaccia utente delle sue app e iOS stesso, in favore di progetti audaci e geometricamente precisi. Dai un'occhiata alle icone delle app Camera o Foto, per esempio.
Più probabile che no, sono stati progettati utilizzando strumenti di grafica vettoriale. Gli sviluppatori dovevano seguire l'esempio e la maggior parte delle app (non di gioco) popolari subì una metamorfosi completa per conformarsi a questo paradigma di design.
I giochi con una grafica semplice (pensa Asteroids) o temi geometrici (Super Hexagon e Geometry Jump vengono in mente) possono avere i loro sprite resi da vettori. Lo stesso vale per i giochi con livelli generati proceduralmente.
Immagini in cui si desidera iniettare una piccola quantità di casualità per ottenere più versioni della stessa forma di base.
Cosa sono le curve di Bezier? Senza approfondire la teoria matematica, parliamo solo delle funzionalità che sono di uso pratico per gli sviluppatori.
Le curve di Bezier sono caratterizzate da quante gradi di libertà loro hanno. Più alto è questo grado, maggiore è la variazione della curva che può essere incorporata (ma anche il più matematicamente complesso che sia).
Grado uno Beziers sono segmenti di linea retta. Le due curve di grado sono chiamate curve quadricipite. Le tre curve di grado (cubiche) sono quelle su cui ci concentreremo, perché offrono un buon compromesso tra flessibilità e complessità.
I Bezier cubici possono rappresentare non solo semplici curve morbide, ma anche loop e cuspidi. Diversi segmenti Bezier cubici possono essere agganciati in modo da formare forme più complicate.
Un Bezier cubico è definito dai suoi due punti finali e da due punti di controllo aggiuntivi che ne determinano la forma. In generale, una laurea n Bezier ha (N-1) punti di controllo, senza contare i punti finali.
Una caratteristica interessante di Beziers cubici è che questi punti hanno un'interpretazione visiva significativa. La linea che collega un punto finale al suo punto di controllo adiacente funge da tangente alla curva nel punto finale. Questo fatto è utile per progettare forme. Sfrutteremo questa proprietà più avanti nel tutorial.
A causa della natura matematica di queste curve, è possibile applicare facilmente trasformazioni geometriche, come ridimensionamento, rotazione e traslazione, senza alcuna perdita di fedeltà.
L'immagine seguente mostra un campionamento di diversi tipi di forme che un singolo Bezier cubico può prendere. Nota come i segmenti della linea verde agiscono come tangenti alla curva.
UIBezierPath
ClasseSu iOS e OS X, la grafica vettoriale è implementata utilizzando la libreria Core Graphics basata su C. Costruito sopra questo è UIKit / Cocoa, che aggiunge un rivestimento di orientamento dell'oggetto. Il cavallo di battaglia è il UIBezierPath
classe (NSBezierPath
su OS X), un'implementazione di una curva matematica di Bézier.
Il UIBezierPath
la classe supporta le curve di Bezier di primo grado (segmenti di linea retta), due (curve di quad) e tre (curve cubiche).
Programmaticamente, a UIBezierPath
l'oggetto può essere costruito pezzo per pezzo aggiungendo nuovi componenti (sottotracciati) ad esso. Per facilitare questo, il UIBezierPath
oggetto tiene traccia del currentPoint
proprietà. Ogni volta che si aggiunge un nuovo segmento di percorso, l'ultimo punto del segmento aggiunto diventa il punto corrente. Qualsiasi disegno aggiuntivo che fai inizia generalmente a questo punto. È possibile spostare esplicitamente questo punto in una posizione desiderata.
La classe ha metodi convenienti per creare forme comunemente usate, come archi e cerchi, rettangoli (arrotondati), ecc. Internamente, queste forme sono state costruite collegando diversi sottotracciati.
Il percorso complessivo può essere una forma aperta o chiusa. Può persino essere autointersecante o avere più componenti chiusi.
Questo tutorial intende servire da sguardo oltre la base alla generazione della grafica vettoriale. Ma anche se sei uno sviluppatore esperto che non ha utilizzato Core Graphics o UIBezierPath
prima, dovresti essere in grado di seguire. Se sei nuovo a questo, ti consiglio di sfogliare il UIBezierPath
riferimento di classe (e le funzioni Core Graphics sottostanti) se non lo conosci già. Possiamo solo esercitare un numero limitato di funzioni dell'API in un singolo tutorial.
Basta parlare. Iniziamo la codifica. Nel resto di questo tutorial presenterò due scenari in cui la grafica vettoriale è lo strumento ideale da utilizzare.
Accendi Xcode, crea un nuovo parco giochi e imposta la piattaforma su iOS. Incidentalmente, i campi da gioco Xcode sono un'altra ragione per cui lavorare con la grafica vettoriale è ora divertente. Puoi modificare il codice e ottenere un feedback visivo immediato. Nota che dovresti usare l'ultima versione stabile di Xcode, che è 7.2 al momento della stesura di questo documento.
Vogliamo generare immagini di nuvole che aderiscono a una forma di nuvola di base pur avendo una certa casualità in modo che ogni nuvola abbia un aspetto diverso. Il progetto di base su cui mi sono basato è una forma composta, definita da più cerchi di raggi casuali centrati lungo un percorso ellittico di dimensioni casuali (entro intervalli appropriati).
Per chiarire, ecco come appare l'oggetto generale se accarezziamo il tracciato vettoriale anziché riempirlo.
Se la tua geometria è un po 'arrugginita, questa immagine di Wikipedia mostra l'aspetto di un'ellisse.
Iniziamo scrivendo un paio di funzioni di supporto.
"javascript import UIKit
func randomInt (lower lower: Int, upper: Int) -> Int assert (inferiore < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower)))
cerchio funzionale (al centro: CGPoint, raggio: CGFloat) -> UIBezierPath return UIBezierPath (arcCenter: center, raggio: radius, startAngle: 0, endAngle: CGFloat (2 * M_PI), in senso orario: true) "
Il casuale (inferiore: :) superiore
la funzione usa il built-in arc4random_uniform ()
funzione per generare numeri casuali nell'intervallo inferiore
e (Superiore-1)
. Il cerchio (al: Centro :)
la funzione genera un percorso di Bézier, che rappresenta un cerchio con un dato centro
e raggio
.
Concentriamoci ora sulla generazione dei punti lungo il percorso ellittico. Un'ellisse centrata sull'origine del sistema di coordinate con i suoi assi allineati lungo gli assi delle coordinate ha una forma matematica particolarmente semplice che assomiglia a questo.
P (r, θ) = (a cos (θ), b sin (θ))
Assegniamo valori casuali per le lunghezze del suo asse maggiore e minore in modo che la forma assomigli a una nuvola, più allungata orizzontalmente che verticalmente.
Noi usiamo il passo()
funzione per generare angoli regolarmente distanziati attorno al cerchio e quindi utilizzare carta geografica()
per generare punti regolarmente distanziati sull'ellisse usando l'espressione matematica sopra.
"javascript let a = Double (randomInt (inferiore: 70, upper: 100)) let b = Double (randomInt (lower: 10, upper: 35)) let ndiv = 12 come Double
let points = (0.0) .stride (a: 1.0, di: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0)) "
Generiamo la "massa" centrale della nuvola unendo i punti lungo il percorso ellittico. Se non lo facciamo, otterremo un grande vuoto al centro.
"javascript let path = UIBezierPath () percorso.moveToPoint (punti [0])
per point in points [1 ... path.closePath ()" Nota che il percorso esatto non ha importanza, perché riempiremo il percorso, non lo accarezzeremo. Ciò significa che non sarà distinguibile dai cerchi. Per generare i cerchi, dobbiamo prima euristicamente scegliere un intervallo per i raggi del cerchio casuale. Il fatto che lo stiamo sviluppando in un parco giochi mi ha aiutato a giocare con i valori fino a quando ho ottenuto un risultato di cui ero soddisfatto. "javascript let minRadius = (Int) (M_PI * a / ndiv) let maxRadius = minRadius + 25 per point in points [0 ... sentiero" È possibile visualizzare il risultato facendo clic sull'icona "occhio" nel pannello dei risultati a destra, sulla stessa riga dell'istruzione "percorso". Come possiamo rasterizzare questo per ottenere il risultato finale? Abbiamo bisogno di ciò che è noto come "contesto grafico" in cui disegnare i percorsi. Nel nostro caso, disegneremo in un'immagine (a Raggruppiamo questo codice in una funzione in modo che possiamo generare tutte le nuvole che vogliamo. E mentre ci siamo, scriveremo del codice per disegnare alcune nuvole casuali su uno sfondo blu (che rappresenta il cielo) e per disegnare tutto questo nel campo giochi live view. Ecco il codice finale: "javascript import UIKit import XCPlayground func generateRandomCloud () -> UIImage vista classe: UIView override func drawRect (rect: CGRect) let ctx = UIGraphicsGetCurrentContext () UIColor.blueColor (). setFill () CGContextFillRect (ctx, rect) XCPlaygroundPage.currentPage.liveView = Visualizza (frame: CGRectMake (0, 0, 600, 800)) " E questo è il risultato finale: I silhouttes delle nuvole appaiono un po 'sfocati nell'immagine sopra, ma questo è semplicemente un artefatto di ridimensionamento. La vera immagine di output è nitida. Per vederlo nel tuo parco giochi, assicurati che il Assistente editore è aperto. Selezionare Mostra Editor assitente dal vista menu. I pezzi del puzzle di solito hanno una "cornice" quadrata, con ciascun bordo che è o piatto, con una linguetta arrotondata che sporge verso l'esterno, o una fessura della stessa forma da tesellare con una linguetta da un pezzo adiacente. Ecco una sezione di un tipico puzzle. Se stavi sviluppando un'app di jigsaw puzzle, dovresti usare una maschera a forma di pezzo di puzzle per segmentare l'immagine che rappresenta il puzzle. Puoi utilizzare maschere raster pregenerate fornite con l'app, ma dovrai includere diverse varianti per adattarle a tutte le possibili variazioni di forma dei quattro bordi. Con la grafica vettoriale, puoi generare la maschera per ogni tipo di pezzo al volo. Inoltre, sarebbe più semplice ospitare altre varianti, ad esempio se si desideravano pezzi rettangolari o obliqui (invece di pezzi quadrati). Come progettiamo effettivamente il pezzo del puzzle, vale a dire, come possiamo capire come posizionare i nostri punti di controllo per generare un tracciato più luminoso che assomiglia alla scheda curva? Richiama la proprietà di tangenza utile dei Beziers cubici che ho menzionato prima. Puoi iniziare disegnando un'approssimazione alla forma desiderata, suddividendola in segmenti stimando quanti segmenti cubici avrai bisogno (conoscendo i tipi di forme che un singolo segmento cubico può contenere) e quindi disegnando tangenti a questi segmenti per capire dove potresti posizionare i tuoi punti di controllo. Ecco un diagramma che spiega di cosa sto parlando. Ho determinato che per rappresentare la forma tab, quattro segmenti Bezier avrebbero funzionato bene: Notare i segmenti della linea tratteggiata verde e gialla che agiscono come tangenti ai segmenti a forma di S, che mi hanno aiutato a stimare dove posizionare i punti di controllo. Nota anche che ho visualizzato il pezzo come avente una lunghezza di un'unità, motivo per cui tutte le coordinate sono frazioni di una. Avrei potuto facilmente fare in modo che la mia curva fosse, ad esempio, lunga 100 punti (ridimensionando i punti di controllo di un fattore di 100). L'indipendenza dalla risoluzione della grafica vettoriale indica che questo non è un problema. Infine, ho usato Beziers cubici anche per i segmenti della linea retta puramente per comodità, in modo che il codice potesse essere scritto in modo più conciso e uniforme. Ho saltato il disegno dei punti di controllo dei segmenti retti nel diagramma per evitare confusione. Ovviamente, un Bezier cubo che rappresenta una linea ha semplicemente i punti finali e i punti di controllo tutti situati lungo la linea stessa. Il fatto che tu stia sviluppando questo in un parco giochi significa che puoi facilmente "rejig" i valori del punto di controllo per trovare una forma che ti soddisfi e ottenere un feedback istantaneo. Iniziamo. Puoi utilizzare lo stesso parco giochi di prima aggiungendo una nuova pagina. Scegliere Nuovo> Pagina Playground dal File menu o creare un nuovo parco giochi, se preferisci. Sostituisci qualsiasi codice sulla nuova pagina con il seguente: "javascript import UIKit lasciate 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 (dimensioni, dimensioni)) lascia path = UIBezierPath () path.moveToPoint (CGPointZero) per i in 0.stride (tramite: outie_points.count - 3, per: 3) path.addCurveToPoint (outie_points [i + 2], controlPoint1: outie_points [i], controlPoint2: outie_points [i + 1]) sentiero" Nota che abbiamo deciso di rendere il nostro percorso di 100 punti applicando una trasformazione di scala ai punti. Vediamo il seguente risultato usando la funzione "Quick Look": Fin qui tutto bene. Come generiamo quattro lati del pezzo di puzzle? La risposta è (come puoi intuire), utilizzando trasformazioni geometriche. Applicando una rotazione di 90 gradi seguita da una traduzione appropriata a C'è un avvertimento qui, sfortunatamente. La trasformazione non unirà automaticamente i singoli segmenti. Anche se la silhouette del nostro puzzle sembra a posto, il suo interno non sarà riempito e affronteremo problemi usandolo come maschera. Possiamo osservare questo nel parco giochi. Aggiungi il seguente codice: "javascript let transform = CGAffineTransformTranslate (CGAffineTransformMakeRotation (CGFloat (-M_PI / 2)), 0, dimensione) lasciare che temppath = path.copy () come! UIBezierPath let foursided = UIBezierPath () per i in 0 ... 3 temppath.applyTransform (transform) foursided.appendPath (temppath) foursided" Quick Look ci mostra quanto segue: Notare come l'interno del pezzo non è ombreggiato, indicando che non è stato riempito. Puoi trovare i comandi di disegno usati per costruire un complesso Geometrica si trasforma Un approccio sarebbe quello di pasticciare con le parti interne del percorso (usando il Ma questa opzione sembra un po 'hacker ed è per questo che ho optato per un approccio diverso. Applichiamo le trasformazioni geometriche ai punti stessi per primi, tramite il Come si genera la scheda "innie"? Potremmo applicare di nuovo una trasformata geometrica, con un fattore di scala negativo nella direzione y (invertendo la sua forma), ma ho optato per farlo manualmente invertendo semplicemente le coordinate y dei punti in Per quanto riguarda la scheda a bordi piatti, mentre avrei potuto semplicemente usare un segmento di linea retta per rappresentarlo, per evitare di dover specializzare il codice per casi distinti, ho semplicemente impostato la coordinata y di ogni punto in Come esercizio, è possibile generare curve di Bezier da questi bordi e visualizzarle utilizzando Quick Look. Ora sai abbastanza per me per blitz te con l'intero codice, che lega tutto insieme in una singola funzione. Sostituisci tutti i contenuti della pagina del parco giochi con quanto segue: "javascript import UIKit import XCPlayground enum Edge caso Outie case Innie case Flat func jigsawPieceMaker (dimensione: CGFloat, bordi: [Bordo]) -> UIBezierPath lasciare piece1 = jigsawPieceMaker (dimensione: 100, bordi: [.Innie, .Outie, .Flat, .Innie]) let piece2 = jigsawPieceMaker (dimensione: 100, bordi: [.Innie, .Innie, .Innie, .Innie]) piece2.applyTransform (CGAffineTransformMakeRotation (CGFloat (M_PI / 3))) " Ci sono solo alcune cose più interessanti nel codice che vorrei chiarire: Spero di averti convinto che la possibilità di generare grafica vettoriale a livello di programmazione può essere un'abilità utile da avere nel tuo arsenale. Speriamo che sarai ispirato a pensare (e programmare) ad altre interessanti applicazioni per la grafica vettoriale che puoi incorporare nelle tue app.Anteprima del risultato
Tocchi finali
UIImage
esempio). È a questo punto che è necessario impostare diversi parametri che specificano il rendering del percorso finale come, ad esempio, i colori e le larghezze dei tratti. Infine, tratti o riempi il tuo percorso (o entrambi). Nel nostro caso, vogliamo che le nostre nuvole siano bianche e vogliamo solo riempirle.func randomInt (lower lower: Int, upper: Int) -> Int assert (inferiore < upper) return lower + Int(arc4random_uniform(UInt32(upper - lower))) func circle(at center: CGPoint, radius: CGFloat) -> UIBezierPath return UIBezierPath (arcCenter: center, radius: radius, startAngle: 0, endAngle: CGFloat (2 * M_PI), in senso orario: true) let a = Double (randomInt (inferiore: 70, upper: 100)) let b = Double (randomInt (lower: 10, upper: 35)) let ndiv = 12 come Double let points = (0.0) .stride (a: 1.0, di: 1 / ndiv) .map CGPoint (x: a * cos (2 * M_PI * $ 0), y: b * sin (2 * M_PI * $ 0)) lascia path = UIBezierPath () percorso.moveToPoint (punti [0]) per il punto nei punti [1 ...
let cloud1 = generateRandomCloud (). CGImage lascia cloud2 = generateRandomCloud (). CGImage lascia cloud3 = generateRandomCloud (). CGImage CGContextDrawImage (ctx, CGRect (x: 20, y: 20, larghezza: CGImageGetWidth (cloud1), altezza: CGImageGetHeight (cloud1 )), cloud1) CGContextDrawImage (ctx, CGRect (x: 300, y: 100, larghezza: CGImageGetWidth (cloud2), altezza: CGImageGetHeight (cloud2)), cloud2) CGContextDrawImage (ctx, CGRect (x: 50, y: 200, width: CGImageGetWidth (cloud3), height: CGImageGetHeight (cloud3)), cloud3)
Scenario 2: generazione di pezzi di puzzle
Accomodando le variazioni con la grafica vettoriale
Progettare il confine di un pezzo di puzzle
Collegamento della forma ai punti di controllo della curva di Bezier
Iniziare
Generazione di tutti e quattro i lati mediante trasformazioni geometriche
sentiero
sopra, possiamo facilmente generare il resto dei lati.Avvertenza: un problema con il riempimento interno
UIBezierPath
esaminando il suo debugDescription
proprietà nel parco giochi.Risolvere il problema di riempimento
UIBezierPath
funziona abbastanza bene per il caso di uso comune, cioè quando hai già una forma chiusa o la forma che stai trasformando è intrinsecamente aperta e vuoi generare versioni geometricamente trasformate di esse. Il nostro caso d'uso è diverso. Il percorso agisce come un sottotracciato in una forma più ampia che stiamo costruendo e il cui interno intendiamo riempire. Questo è un po 'più complicato.CGPathApply ()
funzione dall'API Core Graphics) e unire manualmente i segmenti insieme per ottenere una forma singola, chiusa e correttamente riempita.CGPointApplyAffineTransform ()
funzione, applicando esattamente la stessa trasformazione che abbiamo tentato di utilizzare un momento fa. Quindi utilizziamo i punti trasformati per creare il sottotracciato, che viene aggiunto alla forma generale. Alla fine del tutorial, vedremo un esempio in cui possiamo applicare correttamente una trasformazione geometrica al percorso di Bezier.Generazione delle variazioni del bordo pezzo
outie_points
.outie_points
a zero. Questo ci dà:javascript let innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) let flat_points = outie_points.map CGPointMake ($ 0.x, 0)
func incrementalPathBuilder (firstPoint: CGPoint) -> ([CGPoint]) -> UIBezierPath let path = UIBezierPath () path.moveToPoint (firstPoint) return punti in assert (points.count% 3 == 0) per i in 0. passo (attraverso: points.count - 3, per: 3) path.addCurveToPoint (points [i + 2], controlPoint1: points [i], controlPoint2: points [i + 1]) return path lascia 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)] let outie_points = outie_coords.map CGPointApplyAffineTransform (CGPointMake ($ 0.x, $ 0.y), CGAffineTransformMakeScale (dimensioni, dimensioni)) let innie_points = outie_points.map CGPointMake ($ 0.x, - $ 0.y) let 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 oat (-M_PI / 2)), 0, dimensione) lascia path_builder = incrementalPathBuilder (CGPointZero) var percorso: UIBezierPath! per edge in edges path = path_builder (shapeDict [edge]!) per (e, pts) in shapeDict let tr_pts = pts.map CGPointApplyAffineTransform ($ 0, transform) shapeDict [e] = tr_pts path.closePath ( ) sentiero di ritorno
enum
per definire le diverse forme del bordo. Memorizziamo i punti in un dizionario che utilizza i valori di enumerazione come chiavi.incrementalPathBuilder (_)
funzione, definita internamente al jigsawPieceMaker (dimensione: bordi :)
funzione.applyTransform (_ :)
metodo per applicare una trasformazione geometrica alla forma. Ad esempio, ho applicato una rotazione di 60 gradi al secondo pezzo.Conclusione