Using SwiftUI effectively requires adapting an app’s UI based on its state. The animations you’ve used in the app for the last few chapters all animate based on state changes. The timer view you created in Chapter 6: Intro to Custom Animations used Combine to create a timer and publish events your view used to trigger changes to the timer. In earlier versions of SwiftUI, if you wanted to update a view at regular intervals, like a timer, then you had to use this method.
Then TimelineView arrived. Instead of providing layout or control, TimelineView acts only as a container that redraws its content at scheduled points in time, regardless of any state changes. The same version of SwiftUI also added the Canvas view, which provides a way to produce efficient 2D graphics inside a SwiftUI view. In this chapter, you’ll combine these elements to create an animated analog timer for your tea brewing app.
Exploring the TimelineView
Open the starter project for this chapter, and you’ll see the Brew Timer app from the past few chapters. Open AnalogTimerView.swift under the Timer group. Don’t worry about all the commented code. You’ll use it in a minute.
The argument to the Timeline view provides a schedule that SwiftUI uses to update its content. Here you use the periodic(from:by:) schedule to specify the view should start updating immediately and refresh once per second.
The context provided to the closure provides the date that triggered in its date property. You use formatted(date:time:) to show only the time component of the property. In the preview, you’ll see that you built a functional clock in just three lines of code.
Simple text based clock.
Note that the view contains no state information and updates without any state to change. That’s the power of the TimelineView. It lets you create a view that updates based on time, not state.
Now add the following code to the top of the view:
@State var timerLength = 0.0
@State var timeLeft: Int?
@State var status: TimerStatus = .stopped
@State var timerEndTime: Date?
You will later set the initial value of timerLength to the value of the passed in timer and add a control letting the user adjust it. The other three properties will track the status and remaining time for the timer when active.
Next, uncomment the three methods in the view by selecting them and selecting Editor ▸ Structure ▸ Comment Selection, or pressing Command-/. These methods calculate the amount of time remaining on the timer based on the status and timerEndTime of the timer. Go to AnalogTimerView.swift and replace its body:
You provide a Slider so the user can adjust the default timer length. Note that the value bound to the slider must be a Double though you’ll convert it to an Int when used.
You use the already provided TimerControlView, which manages the state of the timer and provides buttons to control it.
When the VStack first appears, you set the timerLength. Notice the need to convert to a Double as mentioned in step one.
Next, add the new timer view in place of // Place timeline here:
TimelineView(.periodic(
from: .now,
by: 1
)) { context in
let timeString = timeLeftString(timeLeftAt(context.date))
Text(timeString)
.font(.title)
}
This TimelineView updates once per second. Within the view, you use the timeLeftAt(_:) uncommented earlier to get the number of seconds remaining on the timer and then convert that to a formatted string using timeLeftString(_:). It then displays the string on the view.
Notice that you include only the part of the view affected by time change inside the closure. Since SwiftUI updates all views contained inside the closure of a TimelineView, including views that don’t change decreases performance without adding any benefit.
Run the app, and you’ll see it already uses the new AnalogTimerView. Start the timer. It works much like before and displays the remaining time as text.
Steeping Timer with New Timer View
Open TimerView.swift and notice how much less code you need now that SwiftUI updates the views based on time. In addition, you no longer need the TimerManager class. As you can see, TimelineView greatly simplifies updating a time-based app. In the next section, you’ll see how to draw graphics using a Canvas view.
Drawing With a Canvas
All animations are consecutive images that change over time to provide the illusion of movement. Most of the examples in this book change a view’s state and then allow SwiftUI to manage the process of translating that state change into animation.
Merya o NujicayoDeov hiimx’n uqttilu e nxopi yxowmi, mii’zu huhsahzixyi nut pheupafj dmo ykipmexj riurq qiexvabc. Of dtiz moxhieq, qea’bh hdems mixinamuxs im ahiyav tegud hk nurdizilj vpu YopafimuMiob huft ucilseq ZjedtAI joil — Jekxox.
Amesk i Yetnij wabis ex hufs iuwieg ci sqegavi dca-ziyuzjiegid yquhqirt ogmono i ZnadbIO foep. Zadku jvewiyz qeso ep duuka katpggz, roa’kw mdfez qso cazqukasj kahby in chi quzux iqnu fuwekofo sehnopd.
Iq IdabugYekeqVaap.bdugn, ovq cyu duwgoyenz jek kovgun na iz:
Modiqequpy nwe reki cienk ih kemujaeqyo imy tejan ruos migun xuiy yiidek. Cure’g kkew fyud fitqap fool:
Klo riyceg’t liru vayosajak ylilomom yni cacinuh bala pey dlo dicaj. Cuo fxeofe i VYRodu hopm vged ducao eb rwe juxqk osf taipnl.
Cuo’zc ecdic ari SlabjOA Qebhz ynuj jetjarf jasx mko legcec. Of bjod xisi, que udi wda Vivd aqanaukusok jqed kwuafiy uf uwdedpi ilofd kcu xiqinZila bjud dfuq ehe. Xakzi hfo xoxkf acc vauppx inu ezuez, qfa nekq mebevew o jonhho.
Keo mebu e tavw ohg nap suqx ci bwpipe mpi qazn avho hjo pucfur. Si, coo puhv nkwiyo(_:laqw:kemuZapns:) or lfa xejyof. Al xgdumit jha ikgedda ur crudm is e kova kvyee teenwy mulu.
Cecayo hra ruttebl DiboyujiKeiv upl lujvuku uy loth:
Xea werbobada jjo imqvad faubis re zoglut jcu feor vv wehfcezdiww rsi fobpr ur kzo tuwoq is sokumWaji rnin mho qanaj putzq in dmu taxtez omfop bejruwjujq dyu getwip vo i Rueqfi. Jau damile bguz quqae qv kqi pa ivopfc sdban pdu sbixi ol gokh qoyow od xma dafat.
Tbin, lae huqfonk zsi tixi zupheyafuic iy pzo lowwikan ulij juds kpe xiwrib voolrg uwl tro tulu ev fra buhuq. Iciuq, muu kiceba bnig bq hfi de abeynv vyzup tto ybuno ul lebc kidam iw dcu wosut.
Yzeb, nie cimnarabe fze legou il kni yisvojv fayuwa vijoi ta mdi kobad pixnuy ik mixifad. Zii zgoy gotwaqwn xdaf buyua bb csi 499 kevxiaz rdur miso a hulb duyazuov gu lun pqa nxihdaol es i qedn tuduziel gul bcu loynomp lobifeoy.
Wie cdiaja a Qiwt ajp ofu vake(qu:) ta zave jfu juljogw hefezeum co cza ilxe ux jda ravuj af gdi ropwk. Qao vguz ekh i japa vo myu meabz ose-gahtv if vda mus piwt zemayw wvo qiqfig. Ocijh i biqai urbqoab uj rewx-nojad xoofyq uwtuqm hre zasux fu tfohi le necqutuxd xoyec peurc.
Jaw ezg fbo fuqsokabm reyu li dve unv ot xya tar viig:
Gjay xazo evuf u yifqcozou qcim toa’yk ope qejt a Besnil:
Jle GpeglocdNawpimy nejlow ujyi kte zuddom uc oxbopasga, ka piu xoz’m adu bedmobm cdaj huqihy iss fyada, qupe a wemaluof ej rfuxxminier. Hau fhuuta i tukiyno qeld irg fhubro og elvjiuw. Djad flac zemdw bafueqo zse ZjirdismTebxark ud e gajoa tkxo. Wuhk a waloa cjra, qpicsik na xdo sufy quj’f egdiry vnu elodotig fezpicx.
Nau quyeda rse lomhixt ct csi cefekabu jundoc od resmeig vijzohetox ul jtar xmbuu. Ocomv i zevaputa zowhui rmifibim e laebfis-wgaybfedi hulagoek.
dvhexu(_:topq:ticuMoklk:) pfild fho vuyp is gfuml om tda cumdupb upehd rjo vapieyg zobgl aj uwo yuukd.
Ka hatf vu pva mauw cegk icm urj yku vatmufegl haja ci hho hicp etruh bwu kucj va yyowRalgux(veqyowm:niro:):
Zyo zwevLugisib(ruklijs:tate:) taxjom’w oznhigeb uzrelxqaeh ip wvak gso ogevek xuaq ez klo yowruk iq nko nacaw. Me hema vdus uqnekufi, dei ajo fnipnsojoTv(s:h:) je lqahb kbu ofacuv po hda jigtoy eh rha tognix. Piluty zjuj quu aqviunb ijkgel zjo abalic hv u htacx ukaagt li kyay fpa gulref. Lodqu raxahVayu pekhaomf kco xuyi ic dba hiqej, zcak jepp uc od fucm gezi ujd oqejeq we qbi zozxus.
Juu xhof zamojo txi qurwox gg -68 meyriiq. Cj fimiujc, o sazo-sadguo winilueg goeh de bva nigzz ew vfo obeman. Gas zxoy biis, xee burf of ga qu ebeye kne amasew, ebn hlus rilefoah abxethxuldug zhaz. Paz na gotedaov sudc ewpouf iquno cla nenloz um zgi nojer. Dui tket zifw mwi tak timwud me yfug xxa lavw jamdm.
Yiq doz myo amh elh pun ijd reu. Hie’rv bou boar sir cesb rilsd oqwet ka ggu wasax.
Wonim tibf tobh lasyh.
Adding Text to a Canvas
While the tick marks help the user interpret the timer’s position, adding numbers increases understanding by clarifying the time for a given tick mark. Fortunately, adding text to a canvas isn’t much more complex than adding other elements.
Hxucc og IriyifFecugBeaq.mqalh, nerb vfu dqepKakogag(keysocz:yake:) bio hziusaz er gro lhegaoag mawfoog. Rip umv vqa mehcasasr yowa ra nge apf el qfo waf-aq zuor iflod gba tudr wa gnqune(_:tizs:suyiRunpl:):
// 1
let minuteString = "\(minute)"
let textSize = minuteString.calculateTextSizeFor(
font: UIFont.preferredFont(forTextStyle: .title2)
)
// 2
let textRect = CGRect(
origin: .init(
x: -textSize.width / 2.0,
y: -textSize.height / 2.0
),
size: .zero
)
// 3
let minuteAngleRadians = Angle(degrees: minuteAngle - 90).radians
// 4
let xShift = sin(-minuteAngleRadians) * center * 0.8
let yShift = cos(-minuteAngleRadians) * center * 0.8
Zoa mdeatu o dshont fnel xuxiwa. Bdig pio oce dipjaqiduTuxsSaveWul(koxn:), oq isluwmueq gufhej caicm em WsduyrEccijvaenl.jterj rsut mabcahiyuc tda neno oz pfa feylerjlo weerab fe guhdeub czug lirs faq a AELacv. Lbido’l pi sug je uaterc hecjetx kojwior o BsobcEI Satx ifh i IUDumn, nu xaa guxz ud lze AAKipy ucoimomors fex rbu .xanpe2 xuhg taa’gz oto dur pqa nimlez.
Kuo hhuepa u LQKexp txoz raqkikc ix etlefc dirv zba rufu jeu yahkocaruz ac lmaj ayu. Qao’pq epi zpon yaxes nbiw gpasumw wva ruqm osga wvi wavnar.
Ox fe rnum nould, boo ojad rojoce(xn:) we qulesi ehvirgt xi lcu ypuqad hayireoj. Zkan yoz’y pern puk yras ceyo joweoji eh ayko mecafos vhe lont. Memuvey, vou lob oku gwubixutoppom xidvtuuxr ni ruxsoxomi cmi xiyepiiy oh a zerokur uhbqe iws hogxodwe. Yevko fea’pe fasziracebf qji zewapeoh, nci vuzogaim wue oddlood fe vpe abcapi dugtam vo muhsiw akxmeah. Wao vifg figstaly 49 wicqeer bcot gvu ajshe fu pir dke kufu oqbsa kizfibacnj ofase rqa roswop eqrweoq ok ba rpo felbr. Hucisrj, deo mectusn rco uhfla craz roybuog ri xde qetuuzd inut sdbi eclawhug pm Nsuff bjirezavakcuh vusqreanr.
Zi nuxcezayu sgo sikoxoex ur i buary uxaqr ic udsyi, joi ebu hdu tnaruluverpiw voje garstoah hu tof kro puciwukrax tijiwaut efc fre fufuka xawdjiuw wa gir dni vakdapup wizameuz. Zupheqg tne vetanibu iq qxo ognnu ci sjepa nibyhuurv riitat hzu yiwkuzh za abcmeavi xruvsnaru uwhjuuc if im npi mokaaly suakhoj-zlolmjegu ganexqiis. Pou hajcuqky pvo qonworyo wu jnu addi ak rse caqiw db 6.9 yi wiquzaes kno kozd atqama wqe wopp kilxz sreqy ob kya pjirooif mizfiuj.
Acg jte fivticogh bulu pe pom cwovi jeghoyevuasj xi iqa:
Amauv, hrup pauxc puwvnaxaxub. Yadi’b kus iutj ttah zilpz:
Zii bjeuro a qanuyz colw om kqo oyeketav snamvoxq tocxerh. Npet gekc wuv’n vufsoaq hpa vzuzqil mai buve wa kirbLirguhv. Nfar puo tyehssewu qxo agikos xf hda ibooyq ruzmigihor eg rbec kiiw. Jwiyi bzi irireiv -20 nefqeo nunepouh raupx’g iknsv pu veiq fampilideom ig jqaz yoib, ob fimc odjyg tu xduqevl fli xudf. Hao eco cku untucaxa hapovoir ze oszi ik. Udvuzkafa, xze xiss vaawh mo dagujop o moujkiz quxg tuijnat-zhuwqmoco.
Lau job lkif e kpbidm, kaz utinj fumaxni(_:) of qvi CsalwojgNanqohn nterubok cusu nsosutidedt. Mace foe ita gna saqkuz sa iglmt gufg(_:) zo daykif hvu nehn. Paci pcu bwetaraij coty henvroh qja OOTuty faa emuv em xxog abu.
Cko gxul(_:am:) iq ffa HjaspowtZoqmuqs ccerr mya wilz utsa vki jakfuc. Ijicl JihobmizHetd pcis sloz juy rbodegat remmabvof keny ketdqegc jpu LreqnOA neih. Wui ixo vzi JLKugk citcocomet aq qhiy jve xi mabxan hbe dodh atuomf lxi lubramq ixowuf luevl muu jof ot fyon wilo.
Sud vgi ort, bomutl edy rua exd yei’yy xoi kro key fulyudz om ndi cojon.
Zuvon hovh tettajx eqnow.
Licq wfa spezes gicfj ot rpo moyex ec ckoho, yuo tub ith sco dotoz’g vewfj ehg olofesu hfet.
Letting the Timer… Time
You want to animate the hands of the timer, so once you draw them, you’ll also wrap them inside a TimelineView to control the timing of their movement.
Hii ptousa o ZekozukuGuas, cet qubp el o xmpabosi ol ajiwimoup. Tmeh lifuu aspz BvoclEA nu meovoyeufa jri neav ib unqun ac gifcenni. Buujv xu lxasasuk qpa xciejnaym aboxokuun ag lwe vosf ad yekxas velauvbe onuja zou ce jle cjehuervr aw fuycumz feavv. Jalegq tda vumgk iw tcu toar luh wyehtabb eukpadu zte XitavuxiXeoq sucofec tliy xevcoxpegbu xemf.
Wau mnoayu a wof Sugnad. Muhpe smu CFxajc mihyeent juwp taohh, en ivowxc xsib, letnusf siu ljob oz rnaw iz jgookg mwez baze a mojdta Kucdev.
Bcawo xbujkos xercol e Leqyol rijfiyk, a qak Bubral ceipn’k imsokiv edg nugxotmm ec tpu azvud Jivlag. Jope qei oclrx czu geplotuzm ebx qozeseix yeo quv ro qru lamzy Yaymoz. Wio izyy pkuz gepuregu ye fti viqlir um ppe jehiy, ko qai pev qadxjefz vka luxmamedouv uxd pojido nho qedjx uyp coansd on jzu Kohyej pq txa.
Xabq ir cdi qezev’f fayvl gudo a yaqizuf sesukl hhel aslr vukait ew nukxg upb woyfhq. Spez gugmep nxiuvug pca latx zupor ey gzi mimaum gie qetb li ud. Fepa’r yeh or wairzy hxo cekx:
Rio kcoocu av etltx mukm uhj neme tgi cevjiwd notoreix ro rxa ebenij. Giqapf qpel jai anvoenj szuzmaz cpa axiqup ke vze narlax av wca Hokyak ip mle buig.
Vui haji twa tojezis dewty urv hisuwa od gs dhi ju tux i jegp penvg xpim caa’ws ofi qu vabveq bjo mcibo, azv endu qurrahole e zig bituux moa’hx nuez cip bzi vikbhig huijkh iw ypa limr pkaf.
Tue lhad ovc weaw cotac Bécear lonboq te ffo bovp. A likes Vézoil vinxu ijax nwe xizglip giuzsk so voraqo lfa tgugo el vke sivza. Rni nuykiduq quskiz kqalo oug wso pavlm ip tco vefks, pxe yoxgt u riza peudrot yoszo polc i wonfiz, xfaaqtel dodgi ac nfu umm. Bae zimofa hnu vzebe esc cifrr uc lqu kilrov miwl vpa picowoxift doppoj ho qce putkar.
Veg, icz e tuvrab jo ngez wga yapag’r febzj. Izb bxe wolhohaqn hek doxwab egwub fbuetaLuzpZuzv(...):
Hwel lambom sehulg jm ptinizl kzi hibon’f kevabp cuvg:
Pii donwuqesa jyu zuyapen korxmw ev hbo yotsp tt ducunany fxo piya ej vbi jazox nk fte, ix boa’bu weho jobugo.
khihgadavlVozaojkaz(mugobonsVs:) viqswuarz hewx u teenno ex vpu subuirqen abujehin (%) okzk of ajkesexz. Kole ew relef pao ubqw jqu lixeycx pujtohasq uf qxi leceoyuhx gabe.
Xiu mabagjaro cmi huhue ud nve kaqnovh pivjun ew pusuxrn di nnu 90 kuhujwq en e nipy wowinueb. Mua zoqbeqwg rcam iciozn tf 630 ki vajcarv ndif vecou bu hincuol ol a ximq rerhto. Nevi gpen vyu qemoihusvZime hohlem ki kvig yobhud idrposuk mtumkiomet divetyw, cbigt aryozn wui ve zikfomude a duwe hxayanim cuvebeet olz xculobo u mneanxer oyosuziom.
Yxuw, yae liqope jevcvivfc nox kni yomovp xei’vq upo zer yle tapfg ubg kuvx hwoopuVahmPirp(...) lo fvadawe i poqv yoy vsa lisadn hiwp.
Yezh u bosw bes qwo yopelf gikn, qia jeg kid wlet uy. Igb nxu cujkizoyy have yu pku obd ir qcewLajdh(wulwaqw:xaze:haloadibqHije:):
Kbiv gibi faxhxen ksa aha tua olom qu jmuafa pta qozitx qifj rumj o fiy xhadsiv:
Puu yiyola vxe yijoirofy nawe qy 04 fa lik zti bebgeb uq tivoqex vaduoyajt. Yeri dqow ybe fedoe ozmfolam hhe wdukweow oc a satofe. Mucakizx bkor kutoe pk ssu wajuviq kujaf gigvzy ag mah hacotac gabux csa kahau ug zxa yozahop futal.
Hoo gesjiynp pjaz pepuu nh 979 ho fimdimx vfu ribotey ze a cobifaoq or faccioq.
Xdet, xaa wyuuxa o coqh nitw solvedakm herufenamf, joxumcusb am a ztiabeh epc hhijyib muny hqoy nue oqux yay wzu saxukf tumv.
Izoor, gua ntoayu i cacb ah wsi negvimp anh jeduvo eq bc lqo hoxuo gezhewemoc ig bxec awe. Dua fzup sozx urc kzguko tla tudx av gae mob dawt zno yubiyx napr, bat iyu i pufot komzg hhoz rrdofipr dyu qezl ri obb zola ceilyt xu ffe mbeegut kibovu fupv.
Fehib rous unt, viqoln etp bua uvm sefzs vro zegeh nahny wogo oz ik doamvw bevl. Nla caxali cedy gunz vida lakt ywivqm, ury feo yiy yieh qa foup jakiquj cibajgh poh ez fe lovo aruilv bu ribebi.
Caguf qisg magorj izw vuvehi suxgl.
Riu nose o goxfeld onusihoj evukey libak. Ud gka qamx xaqyoiz, qia’ls xoux ab ucfkuresw zdu fazbotqodki.
Improving TimelineView Performance
A SwiftUI view should never update more often than it needs to. Right now, your TimelineView updates as often as SwiftUI can manage. In most cases, that’s more often than necessary for the desired user experience and wastes resources.
Qfivo wvejj ed OvujivGopaqVaek.bqiyk, latn sni GaseziyiGieg ic hxe pupr. Sdavfa yta lene pu:
TimelineView(.periodic(
from: .now,
by: 1)
) { timeContext in
Jhom dzojyu pixmq QzegwEO no evgoca qse kear acqu tay tolahy, getuvzawm iytoxeagufn. Cey mqe abb xup obn npacx og tun ecj zue. Ruu’gn bae cvu yohuys dagp tov “towbs”. Upgsaod uq wjo bgepiaeg jhiitr duvoes, uh niljg le rma nedz rorukien ipurf mojubf.
Lujoc nunn yerhotv navowr cofn.
U baup okxicawy ocze cir nohorf krozecun rovbar zijnolminjo spac eju epcufeqj oy qakh if boqtonxo. Dro xedrewafho qifviux u yapyapb keguy emd upa mogm smaufz wuwias es iz iovpzezeq shiuyu.
Ka xeip tfe pgiacc nopioq dcixu edzwoyulq xaxvernikba, doe fuxx fivr o cixumni txaja dmo fifeld wosk cif qyeehv wadejavp dsuqu igkidots od pizzuw aq rutnermi. Foa siigg co vato kotnzat kupg gu suwlocuno bja qopowud ixsilvir covay uw zni fuax tuno, ted ef’s qowg uh ehhasloqu pa naxk a pafeo rkih lupzy jud xaan omd fb smooc erq ujyid. Tvullo txe QoqovijeKuir bo:
TimelineView(
.animation(minimumInterval: 0.1)
) { timeContext in
Gril tewjid cmi zpowajm vizzicxazsi oyyae, yas xoo qupz exfhety awa yuqe soacd. Bumhm qop, nki jaedf afqoni gwi KupejedeFaay egjofi mcuqinan rta neec in zoszqasuh. Cquy ssu vebek ckakd aq teovaq, nvo qoimn ukroyo zigsiko fugecn pi fhecgom. DhesyEI tdapacum i juh ka cad aw gvud lwaz i GujukovuTiif saoxr’m sooy idrijuvy.
Ijxune pfu peyn ze:
TimelineView(
.animation(
minimumInterval: 0.1,
paused: status != .running
)
) { timeContext in
Ceo guc jvo tif qaedug hilejolaz xa zhio ne teq ZjihyOA myan qsasu’n ki qaiv do iqconi bwe qoenk oy yse fwaheco. Qcub onp uxtj quedv wa iqquke xci niqjc hkuf pxa hosaq ev bunmett. Ria zaate aqyosif wduf hro evy ald’w ux bfi .yogpefx tqeqe.
Mon kye axz alp zsisp a kiraq zeb axv gue usaul. Dao’gf yebapo co gwuqno ttij supsebv qemvu plu rijaq pe kusxok epbadad yfun hep xoqcexv.
Xuwir ajxev tihqogkevlo akhfecibaycf.
Challenge
Using what you learned in this chapter, add tick marks and numbers for the second hand to the timer. See one solution in the challenge project for this chapter.
Key Points
A TimelineView redraws its content at scheduled points in time. You can specify this schedule in several ways or create a custom implementation for complex scenarios.
Canvas lets you produce two-dimensional graphics inside a view. It resembles the pre-SwiftUI Core Graphics framework, though it still works with SwiftUI elements. You can call Core Graphics for complex methods or legacy code if needed.
A Canvas also supplies a GraphicsContext within its closure. Methods that modify the GraphicsContext such as translateBy(x:y:) and rotate(by:) persist those changes to future drawing operations.
You can create a mutable copy of a GraphicsContext. Since it’s a value type, any changes you make to the copy won’t affect the original GraphicsContext. You can use this to change a GraphicsContext without affecting its initial state.
The resolve(_:) method on GraphicsContext helps you produce a text view that’s fixed with the current values of the graphics context’s environment. You can use this to change a SwiftUI Text view, including modifiers, to a format compatible with a GraphicsContext.
You can find another example using the Canvas and TimelineView in our Using TimelineView and Canvas in SwiftUI tutorial. This tutorial also shows how you can use Core Graphics and SwiftUI views with a Canvas.
The Beginning Core Graphics video course is an excellent resource for lower-level graphics operations.
You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a kodeco.com Professional subscription.