From e45e01879c5ef791e221a66139b63f84a488a6cc Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Thu, 16 Jul 2026 11:14:01 -0700 Subject: [PATCH 1/5] improve retro card style --- .../CardArt/Pixel/Contents.json | 6 + .../Pixel/PixelJack.imageset/Contents.json | 23 ++ .../Pixel/PixelJack.imageset/pixel-jack.png | Bin 0 -> 647 bytes .../PixelJack.imageset/pixel-jack@2x.png | Bin 0 -> 1170 bytes .../PixelJack.imageset/pixel-jack@3x.png | Bin 0 -> 1477 bytes .../Pixel/PixelKing.imageset/Contents.json | 23 ++ .../Pixel/PixelKing.imageset/pixel-king.png | Bin 0 -> 641 bytes .../PixelKing.imageset/pixel-king@2x.png | Bin 0 -> 1122 bytes .../PixelKing.imageset/pixel-king@3x.png | Bin 0 -> 1376 bytes .../Pixel/PixelQueen.imageset/Contents.json | 23 ++ .../Pixel/PixelQueen.imageset/pixel-queen.png | Bin 0 -> 645 bytes .../PixelQueen.imageset/pixel-queen@2x.png | Bin 0 -> 1106 bytes .../PixelQueen.imageset/pixel-queen@3x.png | Bin 0 -> 1364 bytes .../Views/AppearanceSettingsViews.swift | 4 +- ComputerSolitaire/Views/Cards/CardView.swift | 14 +- .../Views/Cards/Styles/PixelCardViews.swift | 368 +++++++++++++----- ComputerSolitaire/Views/SettingsView.swift | 9 +- .../Shared/CardStyleTests.swift | 50 +++ 18 files changed, 419 insertions(+), 101 deletions(-) create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@3x.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png create mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@3x.png diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json new file mode 100644 index 0000000..192fe2d --- /dev/null +++ b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pixel-jack.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pixel-jack@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pixel-jack@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png new file mode 100644 index 0000000000000000000000000000000000000000..cc8843b2809166a024c8b2549ea0975a2c702f01 GIT binary patch literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@4u1Ks>lnoCyFTrm`0e?1pkV)I z*~vhPu_VYZn8D%MjWiG^$=lt9p@UV{1IS@7@$_|Nf5k4q%c)cJA=U^eq*~${QIe8a zl4_M)lnSI6j0}v-bPddP4b4IfjjT*8tc;Ac4GgRd49F|lWw8`)_ zYuAg;HI4i|R$+7Y&I$!3Vs1^CzCfi2^EbnMhZWN7n>i-2h+ez!@qP~-OVXxtDRR|o&Ai7^abb~cbKj|qUoo>yU5`up$VmdKI;Vst0A|GO&j0`b literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..16c6922455561133e1421fec7391df7c061a82c9 GIT binary patch literal 1170 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^RkyUE;QM+bh1wv*FkEqULX9SBNc zc-fb6=bBbd(e?|Ab6&Y?`qtG2%|BK+b#ml6|G9EobJL~o&h6K@y(3pSaQ4@^{Tv%+ zu5tV~Z`pqZNwpZRn^jKJ6E9icTQNsvk2_&Um{rHxO3&(V z<#c2ZTyi2Rv-e_vXJ%W6BG1|&!OP9PqNO$Ka>RY|uYG(h`pJQl*Z1E!nImfyUnQR~ zEq~_le!hm-GnPQFsHtLic`hpH&Ulg7_G5yF(peR|=l?p3_9%b;kr+Cy^?!qe)Vhnx zzK5PCa%M1XVRz#&xx84*%lpH`=7w8KdenTTvAAS*`nTMEG4UO5h(t!^0?V$dmV$^J zW3l+=qo&j2pkFcpf%~>x=%X}-Rt_Y#HS^JF?q*gtty2x!U4X9t2MN_7HOT1 zJLM8M-}K;yRbHPLNQzX(Ph2_Y>O9`hF3S!j?tbgiy)x|FsR?Dby#tSQUVk__b9-%I z=!;jg9R1&Jb9QCl=)LRsqZPc(mh4=2g%ee;MHNOc`QH@W#-+dOuHh5h>AHXa|0~_fyI!R|E8qL}vHGQ2 zzvKQ|>4LsbOO|dq5yw2|%9N*}TO+k~fAancxnd{rxcB#v7$-``{S z)>O0R<3}yqy9d5x8m@o6;^g_AS1VHE=c_;e`FDv={~__;-VKK>STDNXw@5iZCFuOW zqII_hq#TcVD39JNIop^m?zC5||vLWOlz6XGaALMjq<*3qF_-8vfE zK@{y`XjO!whY$pt&g&FBVjrH`6-1T9gp$P~x z(1AihD3tRRfBpv`9FGkNiu-?{rHY#=1hoH4Aclko5{Loekv}j7Ak-8zH2gOtbO#V# zEfR--Uvl?+Q$6Y!T?RcYc9n5}qaKg-Z+~zO7!mb^3UpzAS-$={6DpC3R>oBp6&?FN zP!^JTtJFT{-fLHyYOSe9!@Au~EqR=Dqu0*@nf%)zDJ1^A@z@Nblz33@fl`>9vZ;lb zOT4!91{H0e;ax(-;3;FRI!rI^Mh8yzxh@`x_0*4O*smJRFW7y84*hh6?=k@2q(f2) zYi(zk-sQ&hGD~@tK(xSVlJYU5n0982!Xea#s(|EOf#-Yf(7My(9b5zfJ-0o7)INvG zA06BOc&N%1;fZ?T>>5k9Wae#2l7~C;#%rhVIq&5zc*RRUG(ZzWQicQCHgFXYoUKX9 zgm(z7r3Yb|KjF7`w4!b?;qEq`$*{mu=4(bFBN*KYhZAAxBT=6>qj@(ERu}D+@aopB zUNHtVA1n?A`99@^OImL0NnJNo=rbV?25>R_(`UTJjV`r5?)G7SnqBFJEbk7j&%BE= zR5zHUIj49hrENu)F0z#fF^=_X&9S^(`@EUvj)oLg-*l>1+YS^n?A%1)r74`Z?d)8& zLvC144vVF;{n}|J8)u)N?i1k}M^tuOBl?8I@X)s3mBUR!y{dJ(0g zDalQrlrkBeJ*EB!A=EBbyEB>aX8yHCOP;mzPgx%#y;jgD>-(vjsLJxR-9b(huIO>_&!uTI|G)K}Dy*~3oEF&P7RR~B`l zp5OYN5ZRbaNtB`K?xC3MfT%Ki7d=m~# zBP)x8?SqhJ=|%}6THhRkW;@FZcwtJq4VN|(b>yR9XB}=^v@7@=P$=}bEAi=3W~#c9 zk31=Xh|Nw0x!>*d0rRTr;hkcu)kbgyU3>&b5pk^M#`;jq1*|mgOGMvn zi7R!D{vmEKZI0SCv8wU3^3}#T$!E9sMp^u2=15^;y;gnk!-i#qno~s zE52FyP}bLzr*E{h(QrShkG_Pb#KN=roKtgzQT-_L?O;6KpM)sa{#XB@kCL($rt5zWzn{ OxB!F=5?*6pL literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json new file mode 100644 index 0000000..9b06f08 --- /dev/null +++ b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pixel-king.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pixel-king@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pixel-king@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png new file mode 100644 index 0000000000000000000000000000000000000000..66fcfcee31f2d2387f9b1127f559523acc4d833c GIT binary patch literal 641 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@*L4g#_~pMnzy4|OL~;8rpx~bG zOY49XV@Z%-FoVOh8)+a;lDE4HLkFv@2av;F;_2(k{)%0IS6a3B##E3&swJ)wB`Jv| zsaDBFsX&Us$iT=<*T7uY&@9Bz*viP<%Gg}nz`)ADz|fNUGm3`X{FKbJO57S8Qg-_T zHE6(XD9OxCEiOsSEx@fuyl~b8prcJaT^vI+&ih{SEoxB^U`;5PXu7_4P41c^*(JxM zpD=_RvdfzK-QG#=@%bL^wf?h~Ei_za$M>vG>U#IjT`A2hp*tmuRE`4T~Tl7xkxQpQf0YOF2{}$_SR0ba1 z@vHUcsq=drQm<9+7inKPx6EBN>(*A^&;=MMKw3;>%^~cpBjIR zQ>o5*YU%po%xNX89D!V#iW0$_hhgx<^3qf1PpWrPgg&e IbxsLQ00gM)a{vGU literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..5352ac84b68e56c8857b1e42477350e20ddb7096 GIT binary patch literal 1122 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^9`V9i6Brnn#XMacLp+X`PCL79jsZ`rdVYg!g1quw+a<5$ zI?L*p?qe_xz4u?RDu-P{V%FKeKZ|mbt^MTsb6KnI=>4%@!hZMS1SthS^IHdaRg-QP zES(dPA#{k%E8WG@f_Gb^{wjse6qSQ)nd{CS5i?t&trYELpOiEGs6zZ($yu>KJ6GJj zAsT3{|9Gv=mM`|*iH)0=T1$VK;@EukoVs9dFFQwb8(-*|E|1jW@=u~kW!+m`H=btt z6t=&6h4tcJt9M#T*Hm0$()?ub&7x*{>b}^zxd9&g4qm7+s&K0G_?{6dky__{r7ubD zf?T15`74gZ4C{rre{^m6l6k4gQ*Qp&VxGXnEoql{ozGpm{4cigbl1HQ6;=P0Yrd`h zF@xPVRW^2C!-@~s)0;-la|^VpOEKlaYLhcy_l{|W4W{POfE&S63au*lRaHG zuNF9d`Swh{Ct9V;wg&l&i_XbhYcg@oUfvY;dr`Y5MC_l?_xj5IV`>RYZfU-{`ODQ` zqUW3btz%4eUCTR`sgyFcY~h{rVqRNqR?FWuj=z>OJdu_3wu4r#mwE;%~X%fBQA<_Jut)ciIz!f0o}p zaZTz$=Ejhl?3+&8+>P6?#C*?s?L&(l&Tr;BU9xrgTg@4IHMO!2{x0elx%m9pQ{I4h x+Z9DwKR%Z-d;Qw7b6>pH-s-P=GQWT2|1SP;-?j+8i@;pP;OXk;vd$@?2>|ox*6;uT literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png new file mode 100644 index 0000000000000000000000000000000000000000..5553f8178f02a3f9db58392f1345fa9f639e9c13 GIT binary patch literal 1376 zcmZ{kX*8P&6ox;dClyPAYC1$)RZ*=a_AEoRWb8|AUFIv&h!%}WzC;a5#Zgq%GDI;- zQOq0~v@--_4N457w54TEPuhqIMOP!1PWorgnIHXe?{nVwp7-8=_qxBYn+8G;0RVu8 zhr4Tlg1(B%RDmhFCk^^y9HhAi_y9m6764My0YI*>QeOi=91Z|v;Q&A^0RS|!yyN7z ziorgA?;whTYHG-jd_aIa@UE-i)50{GoUJf-^L~OWpu`FA4FsH}Gm&4!c`r9tAZwR_ zyF$P@9zGPf435&!H)hZ9zAQ54P)>6qs2n{TuV>J#vU~tg+2i4QJm|Er zw&eTV2AsMx6^_Sknee2Q-^s|@=bUR7l}kcfJ{yDNmndR-K$%vVy(7MrM#`%`{hRik znW?O#LFFLIVkmp)(1m<)x2{hY@quj+cB_Q*DZ!lq(=MW+(}ICJBitn2qc|)9ntW*wtH!r&Vn-!jX=&X zQZ4yft9>(8-IX8Kr^y~;+JTN0u*eZR7dWW&t4b`imKK;)8_6YYT+8)J7}wjI@lmuX zpN?bZAFDEGPyIP7Vf7LTdo~z6TwjnKi(2S5830K>BbVNY1u6$aNXsWY@dI@apD97s zI9u{o%9~P{SUsgQ<*g12HNaqgjF;ebuSQow7nkW}{%7*$Y`aIqO@0{P(jF8R(pUOYV>x9M&TZg;6{Ec^e$2?yn$Y=`^;rwetG5a7Mh2X?nNXnd zd+JiUkP@1l}Zp8DyDNR>o6$B6+|rF&-N z4Q;oe4OsoU7Ef56bV zBGY7r)^G?t;Z`qA*HqOpXgBr9LYZ64mX<}3qv&SQE28tY_S*a!G)>x3TBs{C*DP6@ znNg#uPS0`Xs_%NYdPpq@ytYc16whu)MGVbPc-q`Ts`W2sZ|$GfzKjU=3t@{UGV7-> zjQKb*V>{583+EK=NQl>}=?+q@8Ku9~&7kP^Cwn+Mqx2Hs)Pf zMP;3zya6eAEu8Q(nT)1~7sU;B^_si)ZG31RsxlDcJI15t52OpRJ=*IIsH{#z-KMZ( zlqs}-I9U9FhA6cE)O6h?=`#bO%euL9fsL(2OHnpwuMFuf3f^yZ1DRCI_)!R3oEGV| z|J*dky{9?vtK}t&mL?-xeQ#tnRJ&DS!vX*Oh-FJndf_ C6j(X{ literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json new file mode 100644 index 0000000..6b468fe --- /dev/null +++ b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json @@ -0,0 +1,23 @@ +{ + "images" : [ + { + "filename" : "pixel-queen.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "filename" : "pixel-queen@2x.png", + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "pixel-queen@3x.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png new file mode 100644 index 0000000000000000000000000000000000000000..3658db049ae1260eb4cd82e85d3b51faa3217692 GIT binary patch literal 645 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@*L4h^_D=lv{JOY(*A9MpprGsZ z#84o`SQ6wH%;50sMjD8d21szhW2Q<cptHiBg&&una zKn)sj8%i>BQ;SOya|=-P7+V=wTA7$YEXn%G0CY8@rKgKyh{k!}Nsf6(6nI=0hVDMr zmk{gTRI0q{X82wQ;m$RudmjCkUnp-hnR9o${l9G+4J}gURLZF>6z?+I26c7 zW37ZuT=cjLLj?|6<~FTzm#9jLTko=ojdkr+agk?JrFEmkTfBDho!cXNRV?buw1*Ae zEq_++oXm0Q*OKYdvov%hm@^JpH=j#eG-Fln+o=5}&#%t>XFS8I&8ap{VNTD_L?_={ zoXo4NIe9-+`Tx~^m$G`dcl(^%Ml$@%FZ%_QrO&eC-{kSw@5pjq$63=q=S_V3g@0$a WHILe=1F^t>XYh3Ob6Mw<&;$UQt>;z% literal 0 HcmV?d00001 diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..75f56382fd8d511fd8f1cdb2ff5724bf90ae5bcd GIT binary patch literal 1106 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^q7Sh~Kq1u<*NBpo z#FA92eSzUgV3@cmHl6Mv@f_@w!XD6@!c7fDfM#Jt*gJ*70L$) z6gs_?wbVb@y?&L`hjXD5I~cCc5paxf7HYk4@~(%x{HZLCO^yHFD=z)-8p^sgz*dyk zUU}Qb{#~>7>#9whzEEE9x74QSKsD}*f`u`=l@~?cnb@=X)GzK=|GJOqKJmNjzH{Nt z2`A+1Bqq*bJUT<^rk}in<6GvVOi{*!+6ekr%`8;;|S5>KL#$wa{#3bZ|CA;UeaEVECwmGv(8>_9dGX zms&?%G5MsWJn^Xa%ifAi&9yhRqjrQpn-X`Au{5kl_+T8@rlnIvW@afB8Nc~C(Zf$v zz~%iO){`#XE0-wT;;y){tSRZr(gr5m2hN|C*sU;X$+_h0_<`~BxkrlCl1eryn{RBY zaN#)q=B?qs-ipT)zq5Y3Cw=tWNV# zQqFCwsxMQMLv+8@KK+o(>sadCtE9R1i*k}dBokA{jn9xK?6 q*!RC*Rd1k`^F(;kvrpT~bp9zYIziKnkC`zv+_N=_# z3Dlqgx1l66H?_DVF}DCkkFk}3rIm>Z#FDI^3_w@2aCo{nhD01*dm}b)v4H@4!4Ah` z=RWl*Kb6&aY8zG6;B}JmlGncff@S|68@8!5WKM6td+qPh)jvNYTsq=Kmee|@-pA9IrJD0_sXFd2H@D)u zsHmrVJ^V$-y5qUwm*>9Tomw7I)M1@tohP%uuw_|nv2zK3;n_sRHM_D`y{~Oux$FL} zFV^YrZ=O~7`EG*UMA?}_*Y{ZTJ+pqhDD%#XjKiku&sCLwiZykLT9NdC_FQ||bDCnTu-ey;>7{>WesBo=_&(;{;pCR3d>4;gO{vy1mzq-l;h)eCWBmoK!JLcyW8>~W}?`)anKF|HnzB^~{9nOp>=R4GK+o<^4-t0-M zOH+QmFxh>^>BY{{Kk}+=fot?!p5JL@G?TTcdMxMd7uNmyPVdgm1&X`RD@}h?H1Fuq zJB7z1b?$Z|<^@Tjiei~0*PEBmO>zCmEZBlT} zBUX(CmeX!)eEzgITU_>V=DUENpxa3eO6MhR#D5Ai@ibf}#Fgs4Z`$rva8`=G?GO9T`}xOh%Afg0t#GNpnI(+e;=!$@8&5p_ z=9y4Z>;7`)HTRA8zgfIVlt@>!_KNM?k(r&Fa`m%h5qtM*kMfhX>yCx|p0=}1rTG6L z%ljvH7*F0jkl8JNl&;0q2vZZ=s$cMkZbLCclTzS~;c!ABs zBoVnv(S;=~hdEQ$uihfA(RaTheB!4HyIGdDS!X1!r!M=xGALa2I)~4~4HqK(S=YrD z)FjP6dNj}2{;O8ah4~LZ*X^%A7FEzCt*^0}>*13VqTS(lLe<{t$J%yU`#iGS?=EXD zZR5Vz?$}48^WGbUElLj`R}@{pyR|xZ;2NMp?J9auKVY=GyRjVK0d(d$$uDt#64!v7t zcej3&nwd9gf9a0EjYpe4+Rw3Ro9=&Xw%`7xe = -2.0...2.0 + static let pixelStyleScale = 0.25 + + static func displayAngle(for storedAngle: Double, style: CardStyle) -> Double { + style == .pixel ? storedAngle * pixelStyleScale : storedAngle + } } /// Selection-dependent corner, border, and shadow treatment for card styles @@ -251,22 +256,25 @@ struct CardView: View { } .onChange(of: cardTilts[card.id]) { _, newTilt in guard isCardTiltEnabled, let newTilt else { return } - animateTilt(to: newTilt) + animateTilt(to: CardTilt.displayAngle(for: newTilt, style: cardStyle)) } .onChange(of: isCardTiltEnabled) { _, enabled in animateTilt(to: enabled ? resolvedTilt() : 0) } + .onChange(of: cardStyle) { + animateTilt(to: isCardTiltEnabled ? resolvedTilt() : 0) + } } // Returns the card's stored tilt, assigning a new random one on first use // so a card keeps the same lean for as long as it stays in play. private func resolvedTilt() -> Double { if let existing = cardTilts[card.id] { - return existing + return CardTilt.displayAngle(for: existing, style: cardStyle) } let newTilt = Double.random(in: CardTilt.angleRange) cardTilts[card.id] = newTilt - return newTilt + return CardTilt.displayAngle(for: newTilt, style: cardStyle) } private func animateTilt(to target: Double) { diff --git a/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift b/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift index dc9601d..cac9513 100644 --- a/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift +++ b/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift @@ -9,7 +9,11 @@ enum PixelCardStyle { enum PixelPalette { // Card face static let cardFace = Color(red: 0.97, green: 0.96, blue: 0.93) + static let cardFaceHighlight = Color(red: 1.00, green: 0.99, blue: 0.96) + static let cardFaceShadow = Color(red: 0.82, green: 0.78, blue: 0.70) static let outline = Color(red: 0.13, green: 0.12, blue: 0.15) + static let dropShadow = Color.black.opacity(0.32) + static let selection = Color(red: 1.00, green: 0.78, blue: 0.16) // Suit inks static let red = Color(red: 0.80, green: 0.14, blue: 0.16) @@ -85,17 +89,55 @@ extension PixelPalette { } } +// MARK: - Pixel Grid + +/// Converts virtual card cells into display-pixel-aligned rectangles. Card +/// widths are responsive, so a virtual cell is not always an integral number +/// of points; snapping both edges keeps every filled run crisp without gaps. +nonisolated struct PixelGrid { + let unit: CGFloat + let displayScale: CGFloat + + private var resolvedDisplayScale: CGFloat { + max(1, displayScale) + } + + var cellLength: CGFloat { + max(1 / resolvedDisplayScale, snapped(unit)) + } + + func rect(x: CGFloat, y: CGFloat, width: CGFloat, height: CGFloat) -> CGRect { + let minX = snapped(x * unit) + let minY = snapped(y * unit) + let maxX = snapped((x + width) * unit) + let maxY = snapped((y + height) * unit) + + return CGRect( + x: minX, + y: minY, + width: max(1 / resolvedDisplayScale, maxX - minX), + height: max(1 / resolvedDisplayScale, maxY - minY) + ) + } + + func snapped(_ value: CGFloat) -> CGFloat { + (value * resolvedDisplayScale).rounded() / resolvedDisplayScale + } +} + // MARK: - Card Silhouette (stepped pixel corners) struct PixelCardShape: InsettableShape { /// One virtual pixel unit (card width / PixelCardArt.gridWidth). let px: CGFloat + let displayScale: CGFloat var insetAmount: CGFloat = 0 func path(in rect: CGRect) -> Path { let insetRect = rect.insetBy(dx: insetAmount, dy: insetAmount) let maxCorner = min(insetRect.width, insetRect.height) / 2 - let step = max(1, min(px, floor(maxCorner / 3))) + let grid = PixelGrid(unit: px, displayScale: displayScale) + let step = min(grid.cellLength, floor(maxCorner / 3)) let corner = step * 3 var path = Path() @@ -171,6 +213,7 @@ struct PixelSprite { width = w height = cells.count } + } // MARK: - Sprite Art @@ -226,37 +269,42 @@ enum PixelSprites { } } - // Compact pip suits — 5x5, sized so pip rows and columns never collide. + // Compact pip suits use an even 6x6 footprint so their origins and centers + // stay on whole grid cells across the responsive card layout. static let spadePip = PixelSprite(""" - ..#.. - .###. - ##### - ##### - ..#.. + ..##.. + .####. + ###### + ###### + ##..## + ..##.. """) static let heartPip = PixelSprite(""" - .#.#. - ##### - ##### - .###. - ..#.. + .##.## + ###### + ###### + .####. + ..##.. + ..##.. """) static let diamondPip = PixelSprite(""" - ..#.. - .###. - ##### - .###. - ..#.. + ..##.. + .####. + ###### + ###### + .####. + ..##.. """) static let clubPip = PixelSprite(""" - .###. - ##### - ##### - ##.## - ..#.. + ..##.. + .####. + ###### + ..##.. + .####. + ..##.. """) static func pipSuit(_ suit: Suit) -> PixelSprite { @@ -520,6 +568,19 @@ enum PixelSprites { } } +enum PixelRoyalArtwork { + static let logicalSize = CGSize(width: 26, height: 45) + + static func assetName(for rank: Rank) -> String? { + switch rank { + case .jack: "PixelJack" + case .queen: "PixelQueen" + case .king: "PixelKing" + default: nil + } + } +} + // MARK: - Painter enum PixelCardArt { @@ -534,7 +595,7 @@ enum PixelCardArt { in context: GraphicsContext, x: CGFloat, y: CGFloat, - unit: CGFloat, + grid: PixelGrid, scale: CGFloat = 1, flipped: Bool = false, color: (PixelInk) -> Color? @@ -555,12 +616,12 @@ enum PixelCardArt { guard sprite.cells[srcRow][nextCol] == value else { break } run += 1 } - let rect = CGRect( - x: (x + CGFloat(col) * scale) * unit, - y: (y + CGFloat(row) * scale) * unit, - width: CGFloat(run) * scale * unit, - height: scale * unit - ).insetBy(dx: -0.2, dy: -0.2) + let rect = grid.rect( + x: x + CGFloat(col) * scale, + y: y + CGFloat(row) * scale, + width: CGFloat(run) * scale, + height: scale + ) context.fill(Path(rect), with: .color(fill)) col += run } @@ -570,18 +631,17 @@ enum PixelCardArt { static func fillCells( _ context: GraphicsContext, x: CGFloat, y: CGFloat, w: CGFloat, h: CGFloat, - unit: CGFloat, + grid: PixelGrid, color: Color ) { - let rect = CGRect(x: x * unit, y: y * unit, width: w * unit, height: h * unit) - .insetBy(dx: -0.2, dy: -0.2) + let rect = grid.rect(x: x, y: y, width: w, height: h) context.fill(Path(rect), with: .color(color)) } // MARK: Front - static func drawFront(card: Card, in context: GraphicsContext, size: CGSize, unit: CGFloat) { - let gridH = size.height / unit + static func drawFront(card: Card, in context: GraphicsContext, size: CGSize, grid: PixelGrid) { + let gridH = size.height / grid.unit let centerY = gridH / 2 let ink = PixelPalette.suitColor(for: card.suit) let suitSprite = PixelSprites.suit(card.suit) @@ -594,19 +654,19 @@ enum PixelCardArt { } } + drawFaceBevel(in: context, gridHeight: gridH, grid: grid) + // Corner indices: rank top-left / suit top-right, mirrored below. - draw(rankSprite, in: context, x: 3, y: 3, unit: unit, color: solid) - draw(suitSprite, in: context, x: gridWidth - 3 - 7, y: 3, unit: unit, color: solid) + draw(rankSprite, in: context, x: 3, y: 3, grid: grid, color: solid) + draw(suitSprite, in: context, x: gridWidth - 3 - 7, y: 3, grid: grid, color: solid) draw( rankSprite, in: context, x: gridWidth - 3 - CGFloat(rankSprite.width), y: gridH - 3 - 7, - unit: unit, flipped: true, color: solid + grid: grid, flipped: true, color: solid ) - draw(suitSprite, in: context, x: 3, y: gridH - 3 - 7, unit: unit, flipped: true, color: solid) + draw(suitSprite, in: context, x: 3, y: gridH - 3 - 7, grid: grid, flipped: true, color: solid) - if let portrait = PixelSprites.portrait(for: card.rank) { - drawPortrait(portrait, card: card, in: context, centerY: centerY, unit: unit) - } else if card.rank == .ace { + if card.rank == .ace { let shaded: (PixelInk) -> Color? = { pixelInk in switch pixelInk { case .ink: return ink @@ -617,32 +677,58 @@ enum PixelCardArt { draw( suitSprite, in: context, x: (gridWidth - 14) / 2, y: centerY - 7, - unit: unit, scale: 2, color: shaded + grid: grid, scale: 2, color: shaded ) - } else { + } else if card.rank.rawValue < Rank.jack.rawValue { let pipSprite = PixelSprites.pipSuit(card.suit) for pip in pipPlacements(count: card.rank.rawValue) { draw( pipSprite, in: context, - x: pip.x, y: centerY + pip.dy - 2.5, - unit: unit, flipped: pip.dy > 0, color: solid + x: pip.x, + y: centerY + pip.dy - CGFloat(pipSprite.height / 2), + grid: grid, + flipped: pip.dy > 0, + color: solid ) } } } + private static func drawFaceBevel( + in context: GraphicsContext, + gridHeight: CGFloat, + grid: PixelGrid + ) { + fillCells( + context, x: 4, y: 2, w: gridWidth - 8, h: 1, + grid: grid, color: PixelPalette.cardFaceHighlight + ) + fillCells( + context, x: 2, y: 4, w: 1, h: gridHeight - 8, + grid: grid, color: PixelPalette.cardFaceHighlight + ) + fillCells( + context, x: 4, y: gridHeight - 3, w: gridWidth - 8, h: 1, + grid: grid, color: PixelPalette.cardFaceShadow + ) + fillCells( + context, x: gridWidth - 3, y: 4, w: 1, h: gridHeight - 8, + grid: grid, color: PixelPalette.cardFaceShadow + ) + } + private static func drawPortrait( _ portrait: PixelSprite, card: Card, in context: GraphicsContext, centerY: CGFloat, - unit: CGFloat + grid: PixelGrid ) { let isRed = card.suit.isRed let originX = (gridWidth - CGFloat(portrait.width)) / 2 let originY = centerY - CGFloat(portrait.height) / 2 - draw(portrait, in: context, x: originX, y: originY, unit: unit) { pixelInk in + draw(portrait, in: context, x: originX, y: originY, grid: grid) { pixelInk in switch pixelInk { case .outlineDark: return PixelPalette.outline case .skin: return PixelPalette.skinTone @@ -666,9 +752,9 @@ enum PixelCardArt { let dy: CGFloat // sprite center offset from card center } - private static let leftCol: CGFloat = 10.5 - private static let midCol: CGFloat = 17.5 - private static let rightCol: CGFloat = 24.5 + private static let leftCol: CGFloat = 10 + private static let midCol: CGFloat = 17 + private static let rightCol: CGFloat = 24 static func pipPlacements(count: Int) -> [PipPlacement] { func cols(_ dys: [CGFloat]) -> [PipPlacement] { @@ -686,7 +772,7 @@ enum PixelCardArt { case 4: return cols([-11, 11]) case 5: return cols([-11, 11]) + mid([0]) case 6: return cols([-11, 0, 11]) - case 7: return cols([-11, 0, 11]) + mid([-5.5]) + case 7: return cols([-11, 0, 11]) + mid([-6]) case 8: return cols([-12, -4, 4, 12]) case 9: return cols([-12, -4, 4, 12]) + mid([0]) case 10: return cols([-12, -4, 4, 12]) + mid([-8, 8]) @@ -699,18 +785,18 @@ enum PixelCardArt { /// Woven-lattice card back: a bright single-pixel frame around a diagonal /// weave in the colorway's muted tone with mid-tone intersections. static func drawBack( - in context: GraphicsContext, size: CGSize, unit: CGFloat, + in context: GraphicsContext, size: CGSize, grid: PixelGrid, colorway: PixelBackColorway = .navy ) { - let gridH = size.height / unit + let gridH = size.height / grid.unit let lastRow = Int(gridH.rounded(.down)) - 3 // Inner bright frame, one unit thick, inset 2 from the edge. let frame = colorway.bright - fillCells(context, x: 2, y: 2, w: gridWidth - 4, h: 1, unit: unit, color: frame) - fillCells(context, x: 2, y: gridH - 3, w: gridWidth - 4, h: 1, unit: unit, color: frame) - fillCells(context, x: 2, y: 3, w: 1, h: gridH - 6, unit: unit, color: frame) - fillCells(context, x: gridWidth - 3, y: 3, w: 1, h: gridH - 6, unit: unit, color: frame) + fillCells(context, x: 2, y: 2, w: gridWidth - 4, h: 1, grid: grid, color: frame) + fillCells(context, x: 2, y: gridH - 3, w: gridWidth - 4, h: 1, grid: grid, color: frame) + fillCells(context, x: 2, y: 3, w: 1, h: gridH - 6, grid: grid, color: frame) + fillCells(context, x: gridWidth - 3, y: 3, w: 1, h: gridH - 6, grid: grid, color: frame) // Diagonal weave, phase-locked to the card center. let centerX = Int(gridWidth) / 2 @@ -724,16 +810,84 @@ enum PixelCardArt { if onSum && onDiff { fillCells( context, x: CGFloat(cx), y: CGFloat(cy), w: 1, h: 1, - unit: unit, color: colorway.mid + grid: grid, color: colorway.mid ) } else if onSum || onDiff { fillCells( context, x: CGFloat(cx), y: CGFloat(cy), w: 1, h: 1, - unit: unit, color: colorway.muted + grid: grid, color: colorway.muted ) } } } + + drawBackMedallion(in: context, gridHeight: gridH, grid: grid, colorway: colorway) + } + + private static func drawBackMedallion( + in context: GraphicsContext, + gridHeight: CGFloat, + grid: PixelGrid, + colorway: PixelBackColorway + ) { + let originX: CGFloat = 11 + let originY = (gridHeight - 24) / 2 + + fillCells(context, x: originX, y: originY, w: 18, h: 24, grid: grid, color: colorway.deep) + fillCells(context, x: originX, y: originY, w: 18, h: 1, grid: grid, color: colorway.bright) + fillCells(context, x: originX, y: originY + 23, w: 18, h: 1, grid: grid, color: colorway.bright) + fillCells(context, x: originX, y: originY + 1, w: 1, h: 22, grid: grid, color: colorway.bright) + fillCells(context, x: originX + 17, y: originY + 1, w: 1, h: 22, grid: grid, color: colorway.bright) + + let brightInk: (PixelInk) -> Color? = { ink in + ink == .none ? nil : colorway.bright + } + let mutedInk: (PixelInk) -> Color? = { ink in + ink == .none ? nil : colorway.muted + } + + draw(PixelSprites.spadePip, in: context, x: 13, y: originY + 4, grid: grid, color: brightInk) + draw(PixelSprites.heartPip, in: context, x: 21, y: originY + 4, grid: grid, color: mutedInk) + draw(PixelSprites.diamondPip, in: context, x: 13, y: originY + 14, grid: grid, color: mutedInk) + draw(PixelSprites.clubPip, in: context, x: 21, y: originY + 14, grid: grid, color: brightInk) + } +} + +// MARK: - Card Surface + +private struct PixelCardSurface: View { + let cardSize: CGSize + let displayScale: CGFloat + let fill: Color + let isSelected: Bool + @ViewBuilder let content: Content + + var body: some View { + let unit = cardSize.width / PixelCardArt.gridWidth + let grid = PixelGrid(unit: unit, displayScale: displayScale) + let shape = PixelCardShape(px: unit, displayScale: displayScale) + let borderWidth = isSelected ? grid.cellLength * 2 : grid.cellLength + let shadowOffset = grid.cellLength * (isSelected ? 2 : 1) + + ZStack { + shape + .fill(PixelPalette.dropShadow, style: FillStyle(antialiased: false)) + .offset(x: shadowOffset, y: shadowOffset) + + ZStack { + shape.fill(fill, style: FillStyle(antialiased: false)) + content + } + .clipShape(shape, style: FillStyle(antialiased: false)) + .overlay { + shape.strokeBorder( + isSelected ? PixelPalette.selection : PixelPalette.outline, + lineWidth: borderWidth, + antialiased: false + ) + } + } + .frame(width: cardSize.width, height: cardSize.height) } } @@ -743,30 +897,36 @@ struct PixelCardFrontView: View { let card: Card let cardSize: CGSize let isSelected: Bool + @Environment(\.displayScale) private var displayScale var body: some View { let unit = cardSize.width / PixelCardArt.gridWidth - let shape = PixelCardShape(px: unit) - let borderColor = isSelected ? Color.yellow.opacity(0.92) : PixelPalette.outline - let borderWidth = isSelected ? max(2, unit * 1.6) : max(0.8, unit) + let grid = PixelGrid(unit: unit, displayScale: displayScale) + + PixelCardSurface( + cardSize: cardSize, + displayScale: displayScale, + fill: PixelPalette.cardFace, + isSelected: isSelected + ) { + ZStack { + Canvas { context, size in + PixelCardArt.drawFront(card: card, in: context, size: size, grid: grid) + } - ZStack { - shape.fill(PixelPalette.cardFace, style: FillStyle(antialiased: false)) - Canvas { context, size in - PixelCardArt.drawFront(card: card, in: context, size: size, unit: unit) + if let royalAssetName = PixelRoyalArtwork.assetName(for: card.rank) { + Image(royalAssetName) + .resizable() + .interpolation(.none) + .frame( + width: PixelRoyalArtwork.logicalSize.width * unit, + height: PixelRoyalArtwork.logicalSize.height * unit + ) + .accessibilityHidden(true) + .allowsHitTesting(false) + } } } - .frame(width: cardSize.width, height: cardSize.height) - .clipShape(shape, style: FillStyle(antialiased: false)) - .overlay( - shape.strokeBorder(borderColor, lineWidth: borderWidth, antialiased: false) - ) - .shadow( - color: Color.black.opacity(isSelected ? 0.24 : 0.10), - radius: isSelected ? 7 : 2, - x: 0, - y: isSelected ? 4 : 1 - ) } } @@ -777,6 +937,7 @@ struct PixelCardBackView: View { let isSelected: Bool @AppStorage(SettingsKey.cardBackColor) private var cardBackColorRawValue = CardBackColor.defaultValue.id + @Environment(\.displayScale) private var displayScale init(cardSize: CGSize, isSelected: Bool = false) { self.cardSize = cardSize @@ -785,28 +946,19 @@ struct PixelCardBackView: View { var body: some View { let unit = cardSize.width / PixelCardArt.gridWidth - let shape = PixelCardShape(px: unit) - let borderColor = isSelected ? Color.yellow.opacity(0.88) : PixelPalette.outline - let borderWidth = isSelected ? max(2, unit * 1.6) : max(0.8, unit) + let grid = PixelGrid(unit: unit, displayScale: displayScale) let colorway = PixelBackColorway.matching(.from(rawValue: cardBackColorRawValue)) - ZStack { - shape.fill(colorway.deep, style: FillStyle(antialiased: false)) + PixelCardSurface( + cardSize: cardSize, + displayScale: displayScale, + fill: colorway.deep, + isSelected: isSelected + ) { Canvas { context, size in - PixelCardArt.drawBack(in: context, size: size, unit: unit, colorway: colorway) + PixelCardArt.drawBack(in: context, size: size, grid: grid, colorway: colorway) } } - .frame(width: cardSize.width, height: cardSize.height) - .clipShape(shape, style: FillStyle(antialiased: false)) - .overlay( - shape.strokeBorder(borderColor, lineWidth: borderWidth, antialiased: false) - ) - .shadow( - color: Color.black.opacity(isSelected ? 0.24 : 0.12), - radius: isSelected ? 7 : 2, - x: 0, - y: isSelected ? 4 : 1 - ) } } @@ -819,3 +971,31 @@ struct PixelStandaloneCardBackView: View { PixelCardBackView(cardSize: cardSize) } } + +#Preview("Pixel Deck") { + let cardSize = CGSize(width: 80, height: 116) + let cards = [ + Card(suit: .spades, rank: .ace, isFaceUp: true), + Card(suit: .hearts, rank: .seven, isFaceUp: true), + Card(suit: .clubs, rank: .ten, isFaceUp: true), + Card(suit: .diamonds, rank: .jack, isFaceUp: true), + Card(suit: .hearts, rank: .queen, isFaceUp: true), + Card(suit: .spades, rank: .king, isFaceUp: true), + ] + + VStack(spacing: 20) { + HStack(spacing: 12) { + ForEach(cards.prefix(3)) { card in + PixelCardFrontView(card: card, cardSize: cardSize, isSelected: false) + } + } + HStack(spacing: 12) { + ForEach(cards.suffix(3)) { card in + PixelCardFrontView(card: card, cardSize: cardSize, isSelected: false) + } + PixelCardBackView(cardSize: cardSize) + } + } + .padding(28) + .background(Color(red: 0.16, green: 0.38, blue: 0.32)) +} diff --git a/ComputerSolitaire/Views/SettingsView.swift b/ComputerSolitaire/Views/SettingsView.swift index 8e71f91..2b7c25e 100644 --- a/ComputerSolitaire/Views/SettingsView.swift +++ b/ComputerSolitaire/Views/SettingsView.swift @@ -196,10 +196,13 @@ struct SettingsView: View { // then scaled down; tiny layout sizes distort the art. CardStylePreview( style: selectedCardStyle, - cardSize: CGSize(width: 44, height: 64) + cardSize: CardStylePreview.recommendedCardSize ) - .frame(width: 44, height: 64) - .scaleEffect(24.0 / 64.0) + .frame( + width: CardStylePreview.recommendedCardSize.width, + height: CardStylePreview.recommendedCardSize.height + ) + .scaleEffect(24.0 / CardStylePreview.recommendedCardSize.height) .frame(width: 17, height: 24) } } diff --git a/ComputerSolitaireTests/Shared/CardStyleTests.swift b/ComputerSolitaireTests/Shared/CardStyleTests.swift index ab2ae18..231f08a 100644 --- a/ComputerSolitaireTests/Shared/CardStyleTests.swift +++ b/ComputerSolitaireTests/Shared/CardStyleTests.swift @@ -34,4 +34,54 @@ final class CardStyleTests: XCTestCase { func testUnknownCardBackColorFallsBackToDefault() { XCTAssertEqual(CardBackColor.from(rawValue: "unknown"), .defaultValue) } + + func testPixelGridSnapsEveryRectangleEdgeToTheDisplayScale() { + let displayScale: CGFloat = 2 + let grid = PixelGrid(unit: 1.1, displayScale: displayScale) + let rect = grid.rect(x: 3, y: 7, width: 5, height: 2) + + for edge in [rect.minX, rect.minY, rect.maxX, rect.maxY] { + XCTAssertEqual(edge * displayScale, (edge * displayScale).rounded()) + } + } + + func testPixelPipLayoutsStayOnWholeCells() { + for count in 2...10 { + let placements = PixelCardArt.pipPlacements(count: count) + XCTAssertEqual(placements.count, count) + + for placement in placements { + XCTAssertEqual(placement.x, placement.x.rounded()) + XCTAssertEqual(placement.dy, placement.dy.rounded()) + } + } + } + + func testPixelPipSpritesUseAnEvenFootprint() { + let sprites = [ + PixelSprites.spadePip, + PixelSprites.heartPip, + PixelSprites.diamondPip, + PixelSprites.clubPip, + ] + + for sprite in sprites { + XCTAssertEqual(sprite.width, 6) + XCTAssertEqual(sprite.height, 6) + } + } + + func testPixelRoyalArtworkMapsOnlyFaceCards() { + XCTAssertEqual(PixelRoyalArtwork.assetName(for: .jack), "PixelJack") + XCTAssertEqual(PixelRoyalArtwork.assetName(for: .queen), "PixelQueen") + XCTAssertEqual(PixelRoyalArtwork.assetName(for: .king), "PixelKing") + XCTAssertNil(PixelRoyalArtwork.assetName(for: .ten)) + XCTAssertEqual(PixelRoyalArtwork.logicalSize, CGSize(width: 26, height: 45)) + } + + func testPixelStyleReducesTiltWithoutDiscardingItsDirection() { + XCTAssertEqual(CardTilt.displayAngle(for: 2, style: .pixel), 0.5) + XCTAssertEqual(CardTilt.displayAngle(for: -2, style: .pixel), -0.5) + XCTAssertEqual(CardTilt.displayAngle(for: 2, style: .classic), 2) + } } From c71bd5690e28928d91aa616beac719d42e62c397 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Thu, 16 Jul 2026 20:50:24 -0700 Subject: [PATCH 2/5] replace pixel royal assets with swiftui sprites --- .../Pixel/PixelJack.imageset/Contents.json | 23 -- .../Pixel/PixelJack.imageset/pixel-jack.png | Bin 647 -> 0 bytes .../PixelJack.imageset/pixel-jack@2x.png | Bin 1170 -> 0 bytes .../PixelJack.imageset/pixel-jack@3x.png | Bin 1477 -> 0 bytes .../Pixel/PixelKing.imageset/Contents.json | 23 -- .../Pixel/PixelKing.imageset/pixel-king.png | Bin 641 -> 0 bytes .../PixelKing.imageset/pixel-king@2x.png | Bin 1122 -> 0 bytes .../PixelKing.imageset/pixel-king@3x.png | Bin 1376 -> 0 bytes .../Pixel/PixelQueen.imageset/Contents.json | 23 -- .../Pixel/PixelQueen.imageset/pixel-queen.png | Bin 645 -> 0 bytes .../PixelQueen.imageset/pixel-queen@2x.png | Bin 1106 -> 0 bytes .../PixelQueen.imageset/pixel-queen@3x.png | Bin 1364 -> 0 bytes .../Views/Cards/Styles/PixelCardViews.swift | 214 +++++++++++++++--- .../Shared/CardStyleTests.swift | 51 ++++- 14 files changed, 226 insertions(+), 108 deletions(-) delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@3x.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@3x.png diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json deleted file mode 100644 index 192fe2d..0000000 --- a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "pixel-jack.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "pixel-jack@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "pixel-jack@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack.png deleted file mode 100644 index cc8843b2809166a024c8b2549ea0975a2c702f01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 647 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@4u1Ks>lnoCyFTrm`0e?1pkV)I z*~vhPu_VYZn8D%MjWiG^$=lt9p@UV{1IS@7@$_|Nf5k4q%c)cJA=U^eq*~${QIe8a zl4_M)lnSI6j0}v-bPddP4b4IfjjT*8tc;Ac4GgRd49F|lWw8`)_ zYuAg;HI4i|R$+7Y&I$!3Vs1^CzCfi2^EbnMhZWN7n>i-2h+ez!@qP~-OVXxtDRR|o&Ai7^abb~cbKj|qUoo>yU5`up$VmdKI;Vst0A|GO&j0`b diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelJack.imageset/pixel-jack@2x.png deleted file mode 100644 index 16c6922455561133e1421fec7391df7c061a82c9..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1170 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^RkyUE;QM+bh1wv*FkEqULX9SBNc zc-fb6=bBbd(e?|Ab6&Y?`qtG2%|BK+b#ml6|G9EobJL~o&h6K@y(3pSaQ4@^{Tv%+ zu5tV~Z`pqZNwpZRn^jKJ6E9icTQNsvk2_&Um{rHxO3&(V z<#c2ZTyi2Rv-e_vXJ%W6BG1|&!OP9PqNO$Ka>RY|uYG(h`pJQl*Z1E!nImfyUnQR~ zEq~_le!hm-GnPQFsHtLic`hpH&Ulg7_G5yF(peR|=l?p3_9%b;kr+Cy^?!qe)Vhnx zzK5PCa%M1XVRz#&xx84*%lpH`=7w8KdenTTvAAS*`nTMEG4UO5h(t!^0?V$dmV$^J zW3l+=qo&j2pkFcpf%~>x=%X}-Rt_Y#HS^JF?q*gtty2x!U4X9t2MN_7HOT1 zJLM8M-}K;yRbHPLNQzX(Ph2_Y>O9`hF3S!j?tbgiy)x|FsR?Dby#tSQUVk__b9-%I z=!;jg9R1&Jb9QCl=)LRsqZPc(mh4=2g%ee;MHNOc`QH@W#-+dOuHh5h>AHXa|0~_fyI!R|E8qL}vHGQ2 zzvKQ|>4LsbOO|dq5yw2|%9N*}TO+k~fAancxnd{rxcB#v7$-``{S z)>O0R<3}yqy9d5x8m@o6;^g_AS1VHE=c_;e`FDv={~__;-VKK>STDNXw@5iZCFuOW zqII_hq#TcVD39JNIop^m?zC5||vLWOlz6XGaALMjq<*3qF_-8vfE zK@{y`XjO!whY$pt&g&FBVjrH`6-1T9gp$P~x z(1AihD3tRRfBpv`9FGkNiu-?{rHY#=1hoH4Aclko5{Loekv}j7Ak-8zH2gOtbO#V# zEfR--Uvl?+Q$6Y!T?RcYc9n5}qaKg-Z+~zO7!mb^3UpzAS-$={6DpC3R>oBp6&?FN zP!^JTtJFT{-fLHyYOSe9!@Au~EqR=Dqu0*@nf%)zDJ1^A@z@Nblz33@fl`>9vZ;lb zOT4!91{H0e;ax(-;3;FRI!rI^Mh8yzxh@`x_0*4O*smJRFW7y84*hh6?=k@2q(f2) zYi(zk-sQ&hGD~@tK(xSVlJYU5n0982!Xea#s(|EOf#-Yf(7My(9b5zfJ-0o7)INvG zA06BOc&N%1;fZ?T>>5k9Wae#2l7~C;#%rhVIq&5zc*RRUG(ZzWQicQCHgFXYoUKX9 zgm(z7r3Yb|KjF7`w4!b?;qEq`$*{mu=4(bFBN*KYhZAAxBT=6>qj@(ERu}D+@aopB zUNHtVA1n?A`99@^OImL0NnJNo=rbV?25>R_(`UTJjV`r5?)G7SnqBFJEbk7j&%BE= zR5zHUIj49hrENu)F0z#fF^=_X&9S^(`@EUvj)oLg-*l>1+YS^n?A%1)r74`Z?d)8& zLvC144vVF;{n}|J8)u)N?i1k}M^tuOBl?8I@X)s3mBUR!y{dJ(0g zDalQrlrkBeJ*EB!A=EBbyEB>aX8yHCOP;mzPgx%#y;jgD>-(vjsLJxR-9b(huIO>_&!uTI|G)K}Dy*~3oEF&P7RR~B`l zp5OYN5ZRbaNtB`K?xC3MfT%Ki7d=m~# zBP)x8?SqhJ=|%}6THhRkW;@FZcwtJq4VN|(b>yR9XB}=^v@7@=P$=}bEAi=3W~#c9 zk31=Xh|Nw0x!>*d0rRTr;hkcu)kbgyU3>&b5pk^M#`;jq1*|mgOGMvn zi7R!D{vmEKZI0SCv8wU3^3}#T$!E9sMp^u2=15^;y;gnk!-i#qno~s zE52FyP}bLzr*E{h(QrShkG_Pb#KN=roKtgzQT-_L?O;6KpM)sa{#XB@kCL($rt5zWzn{ OxB!F=5?*6pL diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json deleted file mode 100644 index 9b06f08..0000000 --- a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "pixel-king.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "pixel-king@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "pixel-king@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king.png deleted file mode 100644 index 66fcfcee31f2d2387f9b1127f559523acc4d833c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 641 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@*L4g#_~pMnzy4|OL~;8rpx~bG zOY49XV@Z%-FoVOh8)+a;lDE4HLkFv@2av;F;_2(k{)%0IS6a3B##E3&swJ)wB`Jv| zsaDBFsX&Us$iT=<*T7uY&@9Bz*viP<%Gg}nz`)ADz|fNUGm3`X{FKbJO57S8Qg-_T zHE6(XD9OxCEiOsSEx@fuyl~b8prcJaT^vI+&ih{SEoxB^U`;5PXu7_4P41c^*(JxM zpD=_RvdfzK-QG#=@%bL^wf?h~Ei_za$M>vG>U#IjT`A2hp*tmuRE`4T~Tl7xkxQpQf0YOF2{}$_SR0ba1 z@vHUcsq=drQm<9+7inKPx6EBN>(*A^&;=MMKw3;>%^~cpBjIR zQ>o5*YU%po%xNX89D!V#iW0$_hhgx<^3qf1PpWrPgg&e IbxsLQ00gM)a{vGU diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@2x.png deleted file mode 100644 index 5352ac84b68e56c8857b1e42477350e20ddb7096..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1122 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^9`V9i6Brnn#XMacLp+X`PCL79jsZ`rdVYg!g1quw+a<5$ zI?L*p?qe_xz4u?RDu-P{V%FKeKZ|mbt^MTsb6KnI=>4%@!hZMS1SthS^IHdaRg-QP zES(dPA#{k%E8WG@f_Gb^{wjse6qSQ)nd{CS5i?t&trYELpOiEGs6zZ($yu>KJ6GJj zAsT3{|9Gv=mM`|*iH)0=T1$VK;@EukoVs9dFFQwb8(-*|E|1jW@=u~kW!+m`H=btt z6t=&6h4tcJt9M#T*Hm0$()?ub&7x*{>b}^zxd9&g4qm7+s&K0G_?{6dky__{r7ubD zf?T15`74gZ4C{rre{^m6l6k4gQ*Qp&VxGXnEoql{ozGpm{4cigbl1HQ6;=P0Yrd`h zF@xPVRW^2C!-@~s)0;-la|^VpOEKlaYLhcy_l{|W4W{POfE&S63au*lRaHG zuNF9d`Swh{Ct9V;wg&l&i_XbhYcg@oUfvY;dr`Y5MC_l?_xj5IV`>RYZfU-{`ODQ` zqUW3btz%4eUCTR`sgyFcY~h{rVqRNqR?FWuj=z>OJdu_3wu4r#mwE;%~X%fBQA<_Jut)ciIz!f0o}p zaZTz$=Ejhl?3+&8+>P6?#C*?s?L&(l&Tr;BU9xrgTg@4IHMO!2{x0elx%m9pQ{I4h x+Z9DwKR%Z-d;Qw7b6>pH-s-P=GQWT2|1SP;-?j+8i@;pP;OXk;vd$@?2>|ox*6;uT diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelKing.imageset/pixel-king@3x.png deleted file mode 100644 index 5553f8178f02a3f9db58392f1345fa9f639e9c13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1376 zcmZ{kX*8P&6ox;dClyPAYC1$)RZ*=a_AEoRWb8|AUFIv&h!%}WzC;a5#Zgq%GDI;- zQOq0~v@--_4N457w54TEPuhqIMOP!1PWorgnIHXe?{nVwp7-8=_qxBYn+8G;0RVu8 zhr4Tlg1(B%RDmhFCk^^y9HhAi_y9m6764My0YI*>QeOi=91Z|v;Q&A^0RS|!yyN7z ziorgA?;whTYHG-jd_aIa@UE-i)50{GoUJf-^L~OWpu`FA4FsH}Gm&4!c`r9tAZwR_ zyF$P@9zGPf435&!H)hZ9zAQ54P)>6qs2n{TuV>J#vU~tg+2i4QJm|Er zw&eTV2AsMx6^_Sknee2Q-^s|@=bUR7l}kcfJ{yDNmndR-K$%vVy(7MrM#`%`{hRik znW?O#LFFLIVkmp)(1m<)x2{hY@quj+cB_Q*DZ!lq(=MW+(}ICJBitn2qc|)9ntW*wtH!r&Vn-!jX=&X zQZ4yft9>(8-IX8Kr^y~;+JTN0u*eZR7dWW&t4b`imKK;)8_6YYT+8)J7}wjI@lmuX zpN?bZAFDEGPyIP7Vf7LTdo~z6TwjnKi(2S5830K>BbVNY1u6$aNXsWY@dI@apD97s zI9u{o%9~P{SUsgQ<*g12HNaqgjF;ebuSQow7nkW}{%7*$Y`aIqO@0{P(jF8R(pUOYV>x9M&TZg;6{Ec^e$2?yn$Y=`^;rwetG5a7Mh2X?nNXnd zd+JiUkP@1l}Zp8DyDNR>o6$B6+|rF&-N z4Q;oe4OsoU7Ef56bV zBGY7r)^G?t;Z`qA*HqOpXgBr9LYZ64mX<}3qv&SQE28tY_S*a!G)>x3TBs{C*DP6@ znNg#uPS0`Xs_%NYdPpq@ytYc16whu)MGVbPc-q`Ts`W2sZ|$GfzKjU=3t@{UGV7-> zjQKb*V>{583+EK=NQl>}=?+q@8Ku9~&7kP^Cwn+Mqx2Hs)Pf zMP;3zya6eAEu8Q(nT)1~7sU;B^_si)ZG31RsxlDcJI15t52OpRJ=*IIsH{#z-KMZ( zlqs}-I9U9FhA6cE)O6h?=`#bO%euL9fsL(2OHnpwuMFuf3f^yZ1DRCI_)!R3oEGV| z|J*dky{9?vtK}t&mL?-xeQ#tnRJ&DS!vX*Oh-FJndf_ C6j(X{ diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json deleted file mode 100644 index 6b468fe..0000000 --- a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/Contents.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "images" : [ - { - "filename" : "pixel-queen.png", - "idiom" : "universal", - "scale" : "1x" - }, - { - "filename" : "pixel-queen@2x.png", - "idiom" : "universal", - "scale" : "2x" - }, - { - "filename" : "pixel-queen@3x.png", - "idiom" : "universal", - "scale" : "3x" - } - ], - "info" : { - "author" : "xcode", - "version" : 1 - } -} diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen.png deleted file mode 100644 index 3658db049ae1260eb4cd82e85d3b51faa3217692..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 645 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4TB!VDyxq|;siDTQQ@AYTTCDm4a%h86~fUqGP- zpm?bP1H-EX1_rAc3=HB0b9M#V03{>>d_r7-G&eW@*L4h^_D=lv{JOY(*A9MpprGsZ z#84o`SQ6wH%;50sMjD8d21szhW2Q<cptHiBg&&una zKn)sj8%i>BQ;SOya|=-P7+V=wTA7$YEXn%G0CY8@rKgKyh{k!}Nsf6(6nI=0hVDMr zmk{gTRI0q{X82wQ;m$RudmjCkUnp-hnR9o${l9G+4J}gURLZF>6z?+I26c7 zW37ZuT=cjLLj?|6<~FTzm#9jLTko=ojdkr+agk?JrFEmkTfBDho!cXNRV?buw1*Ae zEq_++oXm0Q*OKYdvov%hm@^JpH=j#eG-Fln+o=5}&#%t>XFS8I&8ap{VNTD_L?_={ zoXo4NIe9-+`Tx~^m$G`dcl(^%Ml$@%FZ%_QrO&eC-{kSw@5pjq$63=q=S_V3g@0$a WHILe=1F^t>XYh3Ob6Mw<&;$UQt>;z% diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/PixelQueen.imageset/pixel-queen@2x.png deleted file mode 100644 index 75f56382fd8d511fd8f1cdb2ff5724bf90ae5bcd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1106 zcmeAS@N?(olHy`uVBq!ia0vp^CO{m;!VDx?S7dwwQVPi)LB0$ORcZ_j4J`}|zkosw zFBlj~4Hy_+B``2p&0t^q7Sh~Kq1u<*NBpo z#FA92eSzUgV3@cmHl6Mv@f_@w!XD6@!c7fDfM#Jt*gJ*70L$) z6gs_?wbVb@y?&L`hjXD5I~cCc5paxf7HYk4@~(%x{HZLCO^yHFD=z)-8p^sgz*dyk zUU}Qb{#~>7>#9whzEEE9x74QSKsD}*f`u`=l@~?cnb@=X)GzK=|GJOqKJmNjzH{Nt z2`A+1Bqq*bJUT<^rk}in<6GvVOi{*!+6ekr%`8;;|S5>KL#$wa{#3bZ|CA;UeaEVECwmGv(8>_9dGX zms&?%G5MsWJn^Xa%ifAi&9yhRqjrQpn-X`Au{5kl_+T8@rlnIvW@afB8Nc~C(Zf$v zz~%iO){`#XE0-wT;;y){tSRZr(gr5m2hN|C*sU;X$+_h0_<`~BxkrlCl1eryn{RBY zaN#)q=B?qs-ipT)zq5Y3Cw=tWNV# zQqFCwsxMQMLv+8@KK+o(>sadCtE9R1i*k}dBokA{jn9xK?6 q*!RC*Rd1k`^F(;kvrpT~bp9zYIziKnkC`zv+_N=_# z3Dlqgx1l66H?_DVF}DCkkFk}3rIm>Z#FDI^3_w@2aCo{nhD01*dm}b)v4H@4!4Ah` z=RWl*Kb6&aY8zG6;B}JmlGncff@S|68@8!5WKM6td+qPh)jvNYTsq=Kmee|@-pA9IrJD0_sXFd2H@D)u zsHmrVJ^V$-y5qUwm*>9Tomw7I)M1@tohP%uuw_|nv2zK3;n_sRHM_D`y{~Oux$FL} zFV^YrZ=O~7`EG*UMA?}_*Y{ZTJ+pqhDD%#XjKiku&sCLwiZykLT9NdC_FQ||bDCnTu-ey;>7{>WesBo=_&(;{;pCR3d>4;gO{vy1mzq-l;h)eCWBmoK!JLcyW8>~W}?`)anKF|HnzB^~{9nOp>=R4GK+o<^4-t0-M zOH+QmFxh>^>BY{{Kk}+=fot?!p5JL@G?TTcdMxMd7uNmyPVdgm1&X`RD@}h?H1Fuq zJB7z1b?$Z|<^@Tjiei~0*PEBmO>zCmEZBlT} zBUX(CmeX!)eEzgITU_>V=DUENpxa3eO6MhR#D5Ai@ibf}#Fgs4Z`$rva8`=G?GO9T`}xOh%Afg0t#GNpnI(+e;=!$@8&5p_ z=9y4Z>;7`)HTRA8zgfIVlt@>!_KNM?k(r&Fa`m%h5qtM*kMfhX>yCx|p0=}1rTG6L z%ljvH7*F0jkl8JNl&;0q2vZZ=s$cMkZbLCclTzS~;c!ABs zBoVnv(S;=~hdEQ$uihfA(RaTheB!4HyIGdDS!X1!r!M=xGALa2I)~4~4HqK(S=YrD z)FjP6dNj}2{;O8ah4~LZ*X^%A7FEzCt*^0}>*13VqTS(lLe<{t$J%yU`#iGS?=EXD zZR5Vz?$}48^WGbUElLj`R}@{pyR|xZ;2NMp?J9auKVY=GyRjVK0d(d$$uDt#64!v7t zcej3&nwd9gf9a0EjYpe4+Rw3Ro9=&Xw%`7xe Color { suit.isRed ? red : black } @@ -83,6 +93,29 @@ struct PixelBackColorway { } } +enum PixelRoyalColorway: Equatable { + case redSuit + case blackSuit + + static func matching(_ suit: Suit) -> PixelRoyalColorway { + suit.isRed ? .redSuit : .blackSuit + } + + var tunic: Color { + switch self { + case .redSuit: PixelPalette.royalRed + case .blackSuit: PixelPalette.royalBlack + } + } + + var accent: Color { + switch self { + case .redSuit: PixelPalette.royalBlack + case .blackSuit: PixelPalette.royalRed + } + } +} + extension PixelPalette { static func suitHighlight(for suit: Suit) -> Color { suit.isRed ? redLight : blackLight @@ -190,7 +223,8 @@ enum PixelInk: UInt8 { case hair = 9 // "H" case white = 10 // "W" case accent = 11 // "A" - case altRobe = 12 // "B" — contrasting garment (blue on red suits, red on black) + case altRobe = 12 // "B" — primary garment color for the suit colorway + case steel = 13 // "T" — neutral metal that does not follow the suit palette } struct PixelSprite { @@ -201,7 +235,7 @@ struct PixelSprite { init(_ art: String) { let map: [Character: UInt8] = [ ".": 0, "#": 1, "+": 2, "K": 3, "S": 4, "s": 5, - "G": 6, "R": 7, "D": 8, "H": 9, "W": 10, "A": 11, "B": 12 + "G": 6, "R": 7, "D": 8, "H": 9, "W": 10, "A": 11, "B": 12, "T": 13 ] let lines = art.split(separator: "\n").map(String.init) let w = lines.map(\.count).max() ?? 0 @@ -443,7 +477,95 @@ enum PixelSprites { ranks[rank.label] ?? aceRank } - // Face card portraits — 28x35, outlined forms in the classic style. + // Two-way royal portraits. Each final row is horizontally symmetric so + // the upright and rotated copies can overlap without exposing a join. + static let twoWayJackHalf = PixelSprite(""" + .........KKKKKK........... + .......KRRRRRRRRKK.KK..... + ........GKKKRRRRRKBBK..... + ........KKKKGGKRKBKGK..... + ..KK....KSSKKKKGGKKK...... + ..GB....KKKWKKKKKKK....... + .KBBK..KSKSSKKKKKK........ + .GBBB..KSSSSSKKKK......... + ..BB....KSSSSKKKK......... + .KRRK...KSSSSSKKKK........ + .KKKK....KKKSSSKK......... + ..GG......KSSSKKGKK....... + ..KK....KGGKSKGGKGKKK..... + ..KK...KBBKBBBKKKKGRRGK... + ..KK..KRBBKKBKKKKBGRRGBK.. + .KSSKKRRBBKKKKKBBGRRGBBK.. + KSSSSKRRBBKKKKBBBGRRGBBBK. + KSSSSKRRBBBBKBBBGRRGRBBBK. + KSSSRRRBBBBKBBBBGRRGRBBBB. + KSKKRRRBBBBKBBBGRRGBRBBBBK + .KKKRRRBBBKBBBGRRGKBRRBBBK + KRRBRRBBBKBBGRRGKKBKRRBBBK + KRRBKRBBKBBGRRGBBKBBRKBRRK + """) + + static let twoWayQueenHalf = PixelSprite(""" + ........KK..K............. + ........KGKKGKKK.......... + ........KGGGKGGK.......... + ........KSKKKKGGK......... + ........SKSKKKKGKK........ + .......KWKSWKKKGKKK....... + .......KSSSSKBKKKKK....... + .......SSSSSSKKKKKK....... + .KR.....KSSSSKKKGKKK...... + KRGR....KSSKSKKKGKKK...... + KKRKK....KKSSKKKKKKKK..... + .KGGK...KKKSSKKKKKKBK..... + KKGGK...KKWSKGGGKGKKKK.... + KBB....KBKBBBKKKKBGRRG.... + ..BK..KRBBBKBKKKBBGRRGK... + .KSSKRRRBBKBKKKBBGRRGBB... + KSSSSRRRBBKBKKBBBGRRGBBK.. + KSSSSRRRBBKKKBBBGRRGRBBB.. + KSKKKRRRBBKKBBBBGRRGRBBB.. + KSKKRRRBBBKBBBBGRRGBRBBBK. + .KKBKRRBBKBBBBGRRGBBRRBBK. + KRRRKRBBKBBBGRRGKBBKRRBBK. + KKBBBKKKBBBGRRGBBBKKKBBBKK + """) + + static let twoWayKingHalf = PixelSprite(""" + ..........K.KK............ + .......GK.G.KG..R......... + .......KGGKGGGGKGKKG...... + ........GGGGRKGGKGGK...... + ........KKKKKKGGGGK....... + ..K.....KWWSKKKKKKK....... + .KWK....KKSSRKKBKKKK...... + .KWTK...SKSSSKKKKKK....... + .KWTK..KSSSSSKSKKKK....... + .KWTK...KKSSKKKKKKKK...... + .KWTK...KSKKKKKKKKKK...... + .KWTK...KKKKKSSKKKKK...... + .KWTK..KKKKKRSKGGKKK...... + .KWTK...KKKKKBBKKKGRRG.... + KGGGGGKKKKKBBKKKKBGRRGB... + .KSS.KRBBKBKBKKKBGRRGBBK.. + KSSSSRRBBKKKBKKBBGRRGBBB.. + KSSSSRRBBKKKBKBBGRRGRRBBK. + KSSSRRRBBKKKBBBBGRRGRRBBK. + KSKKRRRBBKKBBBBGRRGBRRBBB. + .BKKKRRBBKBBBBGRRGBBRRRBBK + KRBBRKRBBBKBGRRGKBBBRRRBBK + KRRRRKRBBKKGRRGKKBBRKRRRRK + """) + + static func twoWayRoyalHalf(for rank: Rank) -> PixelSprite? { + switch rank { + case .jack: twoWayJackHalf + case .queen: twoWayQueenHalf + case .king: twoWayKingHalf + default: nil + } + } + static let king = PixelSprite(""" ........G..G..G..G..G....... .......KGGGGGGGGGGGGGGK..... @@ -568,19 +690,6 @@ enum PixelSprites { } } -enum PixelRoyalArtwork { - static let logicalSize = CGSize(width: 26, height: 45) - - static func assetName(for rank: Rank) -> String? { - switch rank { - case .jack: "PixelJack" - case .queen: "PixelQueen" - case .king: "PixelKing" - default: nil - } - } -} - // MARK: - Painter enum PixelCardArt { @@ -598,6 +707,7 @@ enum PixelCardArt { grid: PixelGrid, scale: CGFloat = 1, flipped: Bool = false, + antialiased: Bool = true, color: (PixelInk) -> Color? ) { for row in 0.. Color? = { ink in + switch ink { + case .outlineDark: PixelPalette.royalOutline + case .skin: PixelPalette.royalSkin + case .gold: PixelPalette.royalGold + case .robe: PixelPalette.royalRed + case .altRobe: PixelPalette.royalBlue + case .steel: PixelPalette.royalSteel + case .white: PixelPalette.royalWhite + default: nil + } + } + + draw( + half, + in: context, + x: originX, + y: originY, + grid: grid, + antialiased: false, + color: royalInk + ) + draw( + half, + in: context, + x: originX, + y: originY + CGFloat(half.height - 1), + grid: grid, + flipped: true, + antialiased: false, + color: royalInk + ) + } + private static func drawFaceBevel( in context: GraphicsContext, gridHeight: CGFloat, @@ -740,6 +901,7 @@ enum PixelCardArt { case .white: return PixelPalette.ermine case .accent: return PixelPalette.accent case .altRobe: return isRed ? PixelPalette.robeBlue : PixelPalette.robeRed + case .steel: return PixelPalette.royalSteel case .ink, .inkHi, .none: return nil } } @@ -909,22 +1071,8 @@ struct PixelCardFrontView: View { fill: PixelPalette.cardFace, isSelected: isSelected ) { - ZStack { - Canvas { context, size in - PixelCardArt.drawFront(card: card, in: context, size: size, grid: grid) - } - - if let royalAssetName = PixelRoyalArtwork.assetName(for: card.rank) { - Image(royalAssetName) - .resizable() - .interpolation(.none) - .frame( - width: PixelRoyalArtwork.logicalSize.width * unit, - height: PixelRoyalArtwork.logicalSize.height * unit - ) - .accessibilityHidden(true) - .allowsHitTesting(false) - } + Canvas { context, size in + PixelCardArt.drawFront(card: card, in: context, size: size, grid: grid) } } } diff --git a/ComputerSolitaireTests/Shared/CardStyleTests.swift b/ComputerSolitaireTests/Shared/CardStyleTests.swift index 231f08a..cfc56d1 100644 --- a/ComputerSolitaireTests/Shared/CardStyleTests.swift +++ b/ComputerSolitaireTests/Shared/CardStyleTests.swift @@ -71,12 +71,51 @@ final class CardStyleTests: XCTestCase { } } - func testPixelRoyalArtworkMapsOnlyFaceCards() { - XCTAssertEqual(PixelRoyalArtwork.assetName(for: .jack), "PixelJack") - XCTAssertEqual(PixelRoyalArtwork.assetName(for: .queen), "PixelQueen") - XCTAssertEqual(PixelRoyalArtwork.assetName(for: .king), "PixelKing") - XCTAssertNil(PixelRoyalArtwork.assetName(for: .ten)) - XCTAssertEqual(PixelRoyalArtwork.logicalSize, CGSize(width: 26, height: 45)) + func testPixelRoyalSpritesMapOnlyFaceCards() { + XCTAssertNotNil(PixelSprites.twoWayRoyalHalf(for: .jack)) + XCTAssertNotNil(PixelSprites.twoWayRoyalHalf(for: .queen)) + XCTAssertNotNil(PixelSprites.twoWayRoyalHalf(for: .king)) + XCTAssertNil(PixelSprites.twoWayRoyalHalf(for: .ten)) + } + + func testPixelRoyalColorwaysFollowSuitColor() { + XCTAssertEqual(PixelRoyalColorway.matching(.hearts), .redSuit) + XCTAssertEqual(PixelRoyalColorway.matching(.diamonds), .redSuit) + XCTAssertEqual(PixelRoyalColorway.matching(.spades), .blackSuit) + XCTAssertEqual(PixelRoyalColorway.matching(.clubs), .blackSuit) + } + + func testTwoWayPixelRoyalsShareOneSymmetricCenterRow() throws { + for rank in [Rank.jack, .queen, .king] { + let half = try XCTUnwrap(PixelSprites.twoWayRoyalHalf(for: rank)) + let centerRow = half.cells[half.height - 1] + + XCTAssertEqual(half.width, 26) + XCTAssertEqual(half.height * 2 - 1, 45) + XCTAssertEqual(centerRow, Array(centerRow.reversed())) + } + } + + func testTwoWayPixelRoyalsUseTheSameSashBand() throws { + let sashStarts = [18, 18, 17, 17, 16, 16, 15, 14, 12, 11] + let sashBand = [ + PixelInk.gold.rawValue, + PixelInk.robe.rawValue, + PixelInk.robe.rawValue, + PixelInk.gold.rawValue, + ] + + for rank in [Rank.jack, .queen, .king] { + let half = try XCTUnwrap(PixelSprites.twoWayRoyalHalf(for: rank)) + + for (row, start) in sashStarts.enumerated() { + XCTAssertEqual( + Array(half.cells[row + 13][start..<(start + sashBand.count)]), + sashBand, + "\(rank) sash differs on row \(row + 13)" + ) + } + } } func testPixelStyleReducesTiltWithoutDiscardingItsDirection() { From 23e2cfa139bfd88dfd56c4898dc61dbc0bd670b5 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Fri, 17 Jul 2026 07:33:55 -0700 Subject: [PATCH 3/5] polish retro royal colorways --- .../Views/Cards/Styles/PixelCardViews.swift | 80 +++++++++++-------- .../Shared/CardStyleTests.swift | 54 ++++++++++++- 2 files changed, 97 insertions(+), 37 deletions(-) diff --git a/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift b/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift index 67aba69..cfc00da 100644 --- a/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift +++ b/ComputerSolitaire/Views/Cards/Styles/PixelCardViews.swift @@ -38,7 +38,7 @@ enum PixelPalette { static let royalSkin = Color(red: 242 / 255, green: 189 / 255, blue: 145 / 255) static let royalGold = Color(red: 245 / 255, green: 174 / 255, blue: 0) static let royalRed = Color(red: 184 / 255, green: 15 / 255, blue: 31 / 255) - static let royalBlue = Color(red: 23 / 255, green: 63 / 255, blue: 138 / 255) + static let royalBlue = Color(red: 31 / 255, green: 57 / 255, blue: 108 / 255) static let royalBlack = Color(red: 37 / 255, green: 39 / 255, blue: 46 / 255) static let royalSteel = Color(red: 180 / 255, green: 191 / 255, blue: 204 / 255) static let royalWhite = Color(red: 246 / 255, green: 231 / 255, blue: 215 / 255) @@ -114,6 +114,10 @@ enum PixelRoyalColorway: Equatable { case .blackSuit: PixelPalette.royalRed } } + + var sash: Color { + accent + } } extension PixelPalette { @@ -225,6 +229,7 @@ enum PixelInk: UInt8 { case accent = 11 // "A" case altRobe = 12 // "B" — primary garment color for the suit colorway case steel = 13 // "T" — neutral metal that does not follow the suit palette + case secondaryRobe = 14 // "U" — blue garment accent shared by both colorways } struct PixelSprite { @@ -235,7 +240,7 @@ struct PixelSprite { init(_ art: String) { let map: [Character: UInt8] = [ ".": 0, "#": 1, "+": 2, "K": 3, "S": 4, "s": 5, - "G": 6, "R": 7, "D": 8, "H": 9, "W": 10, "A": 11, "B": 12, "T": 13 + "G": 6, "R": 7, "D": 8, "H": 9, "W": 10, "A": 11, "B": 12, "T": 13, "U": 14 ] let lines = art.split(separator: "\n").map(String.init) let w = lines.map(\.count).max() ?? 0 @@ -493,16 +498,16 @@ enum PixelSprites { .KKKK....KKKSSSKK......... ..GG......KSSSKKGKK....... ..KK....KGGKSKGGKGKKK..... - ..KK...KBBKBBBKKKKGRRGK... - ..KK..KRBBKKBKKKKBGRRGBK.. - .KSSKKRRBBKKKKKBBGRRGBBK.. - KSSSSKRRBBKKKKBBBGRRGBBBK. - KSSSSKRRBBBBKBBBGRRGRBBBK. - KSSSRRRBBBBKBBBBGRRGRBBBB. - KSKKRRRBBBBKBBBGRRGBRBBBBK - .KKKRRRBBBKBBBGRRGKBRRBBBK - KRRBRRBBBKBBGRRGKKBKRRBBBK - KRRBKRBBKBBGRRGBBKBBRKBRRK + ..KK...KBBKBBBKKKKGAAGK... + ..KK..KRBBKKBKKKKBGAAGBK.. + .KSSKKRRUBKKKKKBBGAAGUUK.. + KSSSSKRRUBKKKKBBBGAAGUUBK. + KSSSSKRRUBBBKBBBGAAGRBUUK. + KSSSRRRBUBBKBBBBGAAGRBUUB. + KSKKRRRUUBBKBBBGAAGBRBUUBK + .KKKRRRUUBKBBBGAAGKBRRBUBK + KRRBRRBUBKBBGAAGKKBKRRBUBK + KRRBKRBBKBBGAAGBBKBBRKBRRK """) static let twoWayQueenHalf = PixelSprite(""" @@ -519,16 +524,16 @@ enum PixelSprites { KKRKK....KKSSKKKKKKKK..... .KGGK...KKKSSKKKKKKBK..... KKGGK...KKWSKGGGKGKKKK.... - KBB....KBKBBBKKKKBGRRG.... - ..BK..KRBBBKBKKKBBGRRGK... - .KSSKRRRBBKBKKKBBGRRGBB... - KSSSSRRRBBKBKKBBBGRRGBBK.. - KSSSSRRRBBKKKBBBGRRGRBBB.. - KSKKKRRRBBKKBBBBGRRGRBBB.. - KSKKRRRBBBKBBBBGRRGBRBBBK. - .KKBKRRBBKBBBBGRRGBBRRBBK. - KRRRKRBBKBBBGRRGKBBKRRBBK. - KKBBBKKKBBBGRRGBBBKKKBBBKK + KBB....KBKBBBKKKKBGAAG.... + ..BK..KRBBBKBKKKBBGAAGK... + .KSSKRRRUBKBKKKBBGAAGUU... + KSSSSRRRUBKBKKBBBGAAGUUK.. + KSSSSRRRUBKKKBBBGAAGRBUU.. + KSKKKRRRUBKKBBBBGAAGRBUU.. + KSKKRRRUUBKBBBBGAAGBRBUUK. + .KKBKRRUUKBBBBGAAGBBRRBUK. + KRRRKRBUKBBBGAAGKBBKRRBUK. + KKBBBKKKBBBGAAGBBBKKKBBBKK """) static let twoWayKingHalf = PixelSprite(""" @@ -545,16 +550,16 @@ enum PixelSprites { .KWTK...KSKKKKKKKKKK...... .KWTK...KKKKKSSKKKKK...... .KWTK..KKKKKRSKGGKKK...... - .KWTK...KKKKKBBKKKGRRG.... - KGGGGGKKKKKBBKKKKBGRRGB... - .KSS.KRBBKBKBKKKBGRRGBBK.. - KSSSSRRBBKKKBKKBBGRRGBBB.. - KSSSSRRBBKKKBKBBGRRGRRBBK. - KSSSRRRBBKKKBBBBGRRGRRBBK. - KSKKRRRBBKKBBBBGRRGBRRBBB. - .BKKKRRBBKBBBBGRRGBBRRRBBK - KRBBRKRBBBKBGRRGKBBBRRRBBK - KRRRRKRBBKKGRRGKKBBRKRRRRK + .KWTK...KKKKKBBKKKGAAG.... + KGGGGGKKKKKBBKKKKBGAAGB... + .KSS.KRBUKBKBKKKBGAAGUUK.. + KSSSSRRBUKKKBKKBBGAAGUUB.. + KSSSSRRBUKKKBKBBGAAGRRUUK. + KSSSRRRBUKKKBBBBGAAGRRUUK. + KSKKRRRUUKKBBBBGAAGBRRUUB. + .BKKKRRUUKBBBBGAAGBBRRRUBK + KRBBRKRUBBKBGAAGKBBBRRRUBK + KRRRRKRBBKKGAAGKKBBRKRRRRK """) static func twoWayRoyalHalf(for rank: Rank) -> PixelSprite? { @@ -806,12 +811,13 @@ enum PixelCardArt { ) } } else if let half = PixelSprites.twoWayRoyalHalf(for: card.rank) { - drawTwoWayRoyal(half, in: context, size: size, grid: grid) + drawTwoWayRoyal(half, suit: card.suit, in: context, size: size, grid: grid) } } private static func drawTwoWayRoyal( _ half: PixelSprite, + suit: Suit, in context: GraphicsContext, size: CGSize, grid: PixelGrid @@ -821,13 +827,16 @@ enum PixelCardArt { let gridHeight = size.height / grid.unit let originX = (gridWidth - artworkWidth) / 2 let originY = (gridHeight - artworkHeight) / 2 + let colorway = PixelRoyalColorway.matching(suit) let royalInk: (PixelInk) -> Color? = { ink in switch ink { case .outlineDark: PixelPalette.royalOutline case .skin: PixelPalette.royalSkin case .gold: PixelPalette.royalGold - case .robe: PixelPalette.royalRed - case .altRobe: PixelPalette.royalBlue + case .robe: colorway.tunic + case .accent: colorway.sash + case .altRobe: colorway.tunic + case .secondaryRobe: PixelPalette.royalBlue case .steel: PixelPalette.royalSteel case .white: PixelPalette.royalWhite default: nil @@ -902,6 +911,7 @@ enum PixelCardArt { case .accent: return PixelPalette.accent case .altRobe: return isRed ? PixelPalette.robeBlue : PixelPalette.robeRed case .steel: return PixelPalette.royalSteel + case .secondaryRobe: return PixelPalette.royalBlue case .ink, .inkHi, .none: return nil } } diff --git a/ComputerSolitaireTests/Shared/CardStyleTests.swift b/ComputerSolitaireTests/Shared/CardStyleTests.swift index cfc56d1..df971ae 100644 --- a/ComputerSolitaireTests/Shared/CardStyleTests.swift +++ b/ComputerSolitaireTests/Shared/CardStyleTests.swift @@ -83,6 +83,12 @@ final class CardStyleTests: XCTestCase { XCTAssertEqual(PixelRoyalColorway.matching(.diamonds), .redSuit) XCTAssertEqual(PixelRoyalColorway.matching(.spades), .blackSuit) XCTAssertEqual(PixelRoyalColorway.matching(.clubs), .blackSuit) + XCTAssertEqual(PixelRoyalColorway.redSuit.tunic, PixelPalette.royalRed) + XCTAssertEqual(PixelRoyalColorway.blackSuit.tunic, PixelPalette.royalBlack) + XCTAssertEqual(PixelRoyalColorway.redSuit.accent, PixelPalette.royalBlack) + XCTAssertEqual(PixelRoyalColorway.blackSuit.accent, PixelPalette.royalRed) + XCTAssertEqual(PixelRoyalColorway.redSuit.sash, PixelPalette.royalBlack) + XCTAssertEqual(PixelRoyalColorway.blackSuit.sash, PixelPalette.royalRed) } func testTwoWayPixelRoyalsShareOneSymmetricCenterRow() throws { @@ -100,8 +106,8 @@ final class CardStyleTests: XCTestCase { let sashStarts = [18, 18, 17, 17, 16, 16, 15, 14, 12, 11] let sashBand = [ PixelInk.gold.rawValue, - PixelInk.robe.rawValue, - PixelInk.robe.rawValue, + PixelInk.accent.rawValue, + PixelInk.accent.rawValue, PixelInk.gold.rawValue, ] @@ -118,6 +124,50 @@ final class CardStyleTests: XCTestCase { } } + func testTwoWayPixelRoyalsReserveAccentInkForTheSash() throws { + let sashStarts = [18, 18, 17, 17, 16, 16, 15, 14, 12, 11] + var expectedCells: [Int] = [] + for (row, start) in sashStarts.enumerated() { + let rowOffset = (row + 13) * 26 + expectedCells.append(rowOffset + start + 1) + expectedCells.append(rowOffset + start + 2) + } + + for rank in [Rank.jack, .queen, .king] { + let half = try XCTUnwrap(PixelSprites.twoWayRoyalHalf(for: rank)) + let actualCells = half.cells.enumerated().flatMap { row, cells in + cells.enumerated().compactMap { column, ink in + ink == PixelInk.accent.rawValue ? row * half.width + column : nil + } + } + + XCTAssertEqual(actualCells, expectedCells, "\(rank) has sash ink outside the sash") + } + } + + func testTwoWayPixelRoyalsUseTheSameBlueAccentMask() throws { + let expectedCells = [ + 15 * 26 + 8, 15 * 26 + 21, 15 * 26 + 22, + 16 * 26 + 8, 16 * 26 + 21, 16 * 26 + 22, + 17 * 26 + 8, 17 * 26 + 22, 17 * 26 + 23, + 18 * 26 + 8, 18 * 26 + 22, 18 * 26 + 23, + 19 * 26 + 7, 19 * 26 + 8, 19 * 26 + 22, 19 * 26 + 23, + 20 * 26 + 7, 20 * 26 + 8, 20 * 26 + 23, + 21 * 26 + 7, 21 * 26 + 23, + ] + + for rank in [Rank.jack, .queen, .king] { + let half = try XCTUnwrap(PixelSprites.twoWayRoyalHalf(for: rank)) + let actualCells = half.cells.enumerated().flatMap { row, cells in + cells.enumerated().compactMap { column, ink in + ink == PixelInk.secondaryRobe.rawValue ? row * half.width + column : nil + } + } + + XCTAssertEqual(actualCells, expectedCells, "\(rank) uses a different blue accent mask") + } + } + func testPixelStyleReducesTiltWithoutDiscardingItsDirection() { XCTAssertEqual(CardTilt.displayAngle(for: 2, style: .pixel), 0.5) XCTAssertEqual(CardTilt.displayAngle(for: -2, style: .pixel), -0.5) From b12f2eed90c352ec4241fe182ddbd7e6575ae83e Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Sat, 18 Jul 2026 23:36:20 -0700 Subject: [PATCH 4/5] fix tilt for retro cards --- ComputerSolitaire/Views/Cards/CardView.swift | 14 +++----------- ComputerSolitaireTests/Shared/CardStyleTests.swift | 5 ----- 2 files changed, 3 insertions(+), 16 deletions(-) diff --git a/ComputerSolitaire/Views/Cards/CardView.swift b/ComputerSolitaire/Views/Cards/CardView.swift index 14367f3..1e5c571 100644 --- a/ComputerSolitaire/Views/Cards/CardView.swift +++ b/ComputerSolitaire/Views/Cards/CardView.swift @@ -81,11 +81,6 @@ extension EnvironmentValues { enum CardTilt { static let angleRange: ClosedRange = -2.0...2.0 - static let pixelStyleScale = 0.25 - - static func displayAngle(for storedAngle: Double, style: CardStyle) -> Double { - style == .pixel ? storedAngle * pixelStyleScale : storedAngle - } } /// Selection-dependent corner, border, and shadow treatment for card styles @@ -256,25 +251,22 @@ struct CardView: View { } .onChange(of: cardTilts[card.id]) { _, newTilt in guard isCardTiltEnabled, let newTilt else { return } - animateTilt(to: CardTilt.displayAngle(for: newTilt, style: cardStyle)) + animateTilt(to: newTilt) } .onChange(of: isCardTiltEnabled) { _, enabled in animateTilt(to: enabled ? resolvedTilt() : 0) } - .onChange(of: cardStyle) { - animateTilt(to: isCardTiltEnabled ? resolvedTilt() : 0) - } } // Returns the card's stored tilt, assigning a new random one on first use // so a card keeps the same lean for as long as it stays in play. private func resolvedTilt() -> Double { if let existing = cardTilts[card.id] { - return CardTilt.displayAngle(for: existing, style: cardStyle) + return existing } let newTilt = Double.random(in: CardTilt.angleRange) cardTilts[card.id] = newTilt - return CardTilt.displayAngle(for: newTilt, style: cardStyle) + return newTilt } private func animateTilt(to target: Double) { diff --git a/ComputerSolitaireTests/Shared/CardStyleTests.swift b/ComputerSolitaireTests/Shared/CardStyleTests.swift index df971ae..207970e 100644 --- a/ComputerSolitaireTests/Shared/CardStyleTests.swift +++ b/ComputerSolitaireTests/Shared/CardStyleTests.swift @@ -168,9 +168,4 @@ final class CardStyleTests: XCTestCase { } } - func testPixelStyleReducesTiltWithoutDiscardingItsDirection() { - XCTAssertEqual(CardTilt.displayAngle(for: 2, style: .pixel), 0.5) - XCTAssertEqual(CardTilt.displayAngle(for: -2, style: .pixel), -0.5) - XCTAssertEqual(CardTilt.displayAngle(for: 2, style: .classic), 2) - } } From 48e28de494445d625cc63192dee0f592daf07b60 Mon Sep 17 00:00:00 2001 From: Austin Smith Date: Sat, 18 Jul 2026 23:40:54 -0700 Subject: [PATCH 5/5] delete now-unused contents.json --- .../Assets.xcassets/CardArt/Pixel/Contents.json | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json diff --git a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json b/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json deleted file mode 100644 index 73c0059..0000000 --- a/ComputerSolitaire/Assets.xcassets/CardArt/Pixel/Contents.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "info" : { - "author" : "xcode", - "version" : 1 - } -}