From b1f8cde32992db160799ed1424951618c6f3ea47 Mon Sep 17 00:00:00 2001 From: David Buzatto Date: Wed, 3 Dec 2025 05:44:18 -0300 Subject: [PATCH] [examples] Added: `text_strings_management` (#5379) * new shapes example - penrose tile * stack cleanup * proper use of strnlen, strncat and strncpy * typo correction * update screenshot of shapes_penrose_tile example * new example for strings management * Improved structure for text_strings_management --- examples/text/text_strings_management.c | 400 ++++++++++++++++++++++ examples/text/text_strings_management.png | Bin 0 -> 18431 bytes 2 files changed, 400 insertions(+) create mode 100644 examples/text/text_strings_management.c create mode 100644 examples/text/text_strings_management.png diff --git a/examples/text/text_strings_management.c b/examples/text/text_strings_management.c new file mode 100644 index 000000000..d6b4aeb57 --- /dev/null +++ b/examples/text/text_strings_management.c @@ -0,0 +1,400 @@ +/******************************************************************************************* +* +* raylib [text] example - strings management +* +* Example complexity rating: [★★★☆] 3/4 +* +* Example originally created with raylib 5.6-dev, last time updated with raylib 5.6-dev +* +* Example contributed by David Buzatto (@davidbuzatto) and reviewed by Ramon Santamaria (@raysan5) +* +* Example licensed under an unmodified zlib/libpng license, which is an OSI-certified, +* BSD-like license that allows static linking with closed source software +* +* Copyright (c) 2025 David Buzatto (@davidbuzatto) +* +********************************************************************************************/ + +#include "raylib.h" + +#include + +#define MAX_TEXT_LENGTH 100 +#define MAX_TEXT_PARTICLES 100 +#define FONT_SIZE 30 + +//---------------------------------------------------------------------------------- +// Types and Structures Definition +//---------------------------------------------------------------------------------- +typedef struct TextParticle { + char text[MAX_TEXT_LENGTH]; + Rectangle rect; // Boundary + Vector2 vel; // Velocity + Vector2 ppos; // Previous position + float padding; + float borderWidth; + float friction; + float elasticity; + Color color; + bool grabbed; +} TextParticle; + +//---------------------------------------------------------------------------------- +// Module Functions Declaration +//---------------------------------------------------------------------------------- +void PrepareFirstTextParticle(const char* text, TextParticle *tps, int *particleCount); +TextParticle CreateTextParticle(const char *text, float x, float y, Color color); +void SliceTextParticle(TextParticle *tp, int particlePos, int sliceLength, TextParticle *tps, int *particleCount); +void SliceTextParticleByChar(TextParticle *tp, char charToSlice, TextParticle *tps, int *particleCount); +void ShatterTextParticle(TextParticle *tp, int particlePos, TextParticle *tps, int *particleCount); +void GlueTextParticles(TextParticle *grabbed, TextParticle *target, TextParticle *tps, int *particleCount); +void RealocateTextParticles(TextParticle *tps, int particlePos, int *particleCount); + +//------------------------------------------------------------------------------------ +// Program main entry point +//------------------------------------------------------------------------------------ +int main(void) +{ + // Initialization + //-------------------------------------------------------------------------------------- + const int screenWidth = 800; + const int screenHeight = 450; + + InitWindow(screenWidth, screenHeight, "raylib [shapes] example - strings management"); + + TextParticle textParticles[MAX_TEXT_PARTICLES] = { 0 }; + int particleCount = 0; + TextParticle *grabbedTextParticle = NULL; + Vector2 pressOffset = {0}; + + PrepareFirstTextParticle("raylib => fun videogames programming!", textParticles, &particleCount); + + SetTargetFPS(60); // Set our game to run at 60 frames-per-second + //--------------------------------------------------------------------------------------- + + // Main game loop + while (!WindowShouldClose()) // Detect window close button or ESC key + { + // Update + //---------------------------------------------------------------------------------- + float delta = GetFrameTime(); + Vector2 mousePos = GetMousePosition(); + + // Checks if a text particle was grabbed + if (IsMouseButtonPressed(MOUSE_BUTTON_LEFT)) + { + for (int i = particleCount - 1; i >= 0; i--) + { + TextParticle *tp = &textParticles[i]; + pressOffset.x = mousePos.x - tp->rect.x; + pressOffset.y = mousePos.y - tp->rect.y; + if (CheckCollisionPointRec(mousePos, tp->rect)) + { + tp->grabbed = true; + grabbedTextParticle = tp; + break; + } + } + } + + // Releases any text particle the was grabbed + if (IsMouseButtonReleased(MOUSE_BUTTON_LEFT)) + { + if (grabbedTextParticle != NULL) + { + grabbedTextParticle->grabbed = false; + grabbedTextParticle = NULL; + } + } + + // Slice os shatter a text particle + if (IsMouseButtonPressed(MOUSE_BUTTON_RIGHT)) + { + for (int i = particleCount - 1; i >= 0; i--) + { + TextParticle *tp = &textParticles[i]; + if (CheckCollisionPointRec(mousePos, tp->rect)) + { + if (IsKeyDown(KEY_LEFT_SHIFT)) + { + ShatterTextParticle(tp, i, textParticles, &particleCount); + } + else + { + SliceTextParticle(tp, i, TextLength(tp->text)/2, textParticles, &particleCount); + } + break; + } + } + } + + // Shake text particles + if (IsMouseButtonPressed(MOUSE_BUTTON_MIDDLE)) + { + for (int i = 0; i < particleCount; i++) + { + if (!textParticles[i].grabbed) textParticles[i].vel = (Vector2){ GetRandomValue(-2000, 2000), GetRandomValue(-2000, 2000) }; + } + } + + // Reset using TextTo* functions + if (IsKeyPressed(KEY_ONE)) PrepareFirstTextParticle("raylib => fun videogames programming!", textParticles, &particleCount); + if (IsKeyPressed(KEY_TWO)) PrepareFirstTextParticle(TextToUpper("raylib => fun videogames programming!"), textParticles, &particleCount); + if (IsKeyPressed(KEY_THREE)) PrepareFirstTextParticle(TextToLower("raylib => fun videogames programming!"), textParticles, &particleCount); + if (IsKeyPressed(KEY_FOUR)) PrepareFirstTextParticle(TextToPascal("raylib_fun_videogames_programming"), textParticles, &particleCount); + if (IsKeyPressed(KEY_FIVE)) PrepareFirstTextParticle(TextToSnake("RaylibFunVideogamesProgramming"), textParticles, &particleCount); + if (IsKeyPressed(KEY_SIX)) PrepareFirstTextParticle(TextToCamel("raylib_fun_videogames_programming"), textParticles, &particleCount); + + // Slice by char pressed only when we have one text particle + char charPressed = GetCharPressed(); + if ((charPressed >= 'A') && (charPressed <= 'z') && (particleCount == 1)) + { + SliceTextParticleByChar(&textParticles[0], charPressed, textParticles, &particleCount); + } + + // Updates each text particle state + for (int i = 0; i < particleCount; i++) + { + TextParticle *tp = &textParticles[i]; + + // The text particle is not grabbed + if (!tp->grabbed) + { + // text particle repositioning using the velocity + tp->rect.x += tp->vel.x * delta; + tp->rect.y += tp->vel.y * delta; + + // Does the text particle hit the screen right boundary? + if ((tp->rect.x + tp->rect.width) >= screenWidth) + { + tp->rect.x = screenWidth - tp->rect.width; // Text particle repositioning + tp->vel.x = -tp->vel.x*tp->elasticity; // Elasticity makes the text particle lose 10% of its velocity on hit + } + // Does the text particle hit the screen left boundary? + else if (tp->rect.x <= 0) + { + tp->rect.x = 0.0f; + tp->vel.x = -tp->vel.x*tp->elasticity; + } + + // The same for y axis + if ((tp->rect.y + tp->rect.height) >= screenHeight) + { + tp->rect.y = screenHeight - tp->rect.height; + tp->vel.y = -tp->vel.y*tp->elasticity; + } + else if (tp->rect.y <= 0) + { + tp->rect.y = 0.0f; + tp->vel.y = -tp->vel.y*tp->elasticity; + } + + // Friction makes the text particle lose 1% of its velocity each frame + tp->vel.x = tp->vel.x*tp->friction; + tp->vel.y = tp->vel.y*tp->friction; + } + else + { + // Text particle repositioning using the mouse position + tp->rect.x = mousePos.x - pressOffset.x; + tp->rect.y = mousePos.y - pressOffset.y; + + // While the text particle is grabbed, recalculates its velocity + tp->vel.x = (tp->rect.x - tp->ppos.x)/delta; + tp->vel.y = (tp->rect.y - tp->ppos.y)/delta; + tp->ppos.x = tp->rect.x; + tp->ppos.y = tp->rect.y; + + // Glue text particles when dragging and pressing left ctrl + if (IsKeyDown(KEY_LEFT_CONTROL)) + { + for (int i = 0; i < particleCount; i++) + { + if (&textParticles[i] != grabbedTextParticle && grabbedTextParticle->grabbed) + { + if (CheckCollisionRecs(grabbedTextParticle->rect, textParticles[i].rect)) + { + GlueTextParticles(grabbedTextParticle, &textParticles[i], textParticles, &particleCount); + grabbedTextParticle = &textParticles[particleCount-1]; + } + } + } + } + } + } + //---------------------------------------------------------------------------------- + + // Draw + //---------------------------------------------------------------------------------- + BeginDrawing(); + + ClearBackground(RAYWHITE); + + for (int i = 0; i < particleCount; i++) + { + TextParticle *tp = &textParticles[i]; + DrawRectangle(tp->rect.x-tp->borderWidth, tp->rect.y-tp->borderWidth, tp->rect.width+tp->borderWidth*2, tp->rect.height+tp->borderWidth*2, BLACK); + DrawRectangleRec(tp->rect, tp->color); + DrawText(tp->text, tp->rect.x+tp->padding, tp->rect.y+tp->padding, FONT_SIZE, BLACK); + } + + DrawText("grab a text particle by pressing with the mouse and throw it by releasing", 10, 10, 10, DARKGRAY); + DrawText("slice a text particle by pressing it with the mouse right button", 10, 30, 10, DARKGRAY); + DrawText("shatter a text particle keeping left shift pressed and pressing it with the mouse right button", 10, 50, 10, DARKGRAY); + DrawText("glue text particles by grabbing than and keeping left control pressed", 10, 70, 10, DARKGRAY); + DrawText("1 to 6 to reset", 10, 90, 10, DARKGRAY); + DrawText("when you have only one text particle, you can slice it by pressing a char", 10, 110, 10, DARKGRAY); + DrawText(TextFormat("TEXT PARTICLE COUNT: %d", particleCount), 10, GetScreenHeight() - 30, 20, BLACK); + + EndDrawing(); + //---------------------------------------------------------------------------------- + } + + // De-Initialization + //-------------------------------------------------------------------------------------- + CloseWindow(); // Close window and OpenGL context + //-------------------------------------------------------------------------------------- + + return 0; +} + +//---------------------------------------------------------------------------------- +// Module Functions Definition +//---------------------------------------------------------------------------------- +void PrepareFirstTextParticle(const char* text, TextParticle *tps, int *particleCount) +{ + tps[0] = CreateTextParticle( + text, + GetScreenWidth()/2, + GetScreenHeight()/2, + RAYWHITE + ); + *particleCount = 1; +} + +TextParticle CreateTextParticle(const char *text, float x, float y, Color color) +{ + TextParticle tp = { + .text = "", + .rect = { x, y, 30, 30 }, + .vel = { GetRandomValue(-200, 200), GetRandomValue(-200, 200) }, + .ppos = { 0 }, + .padding = 5.0f, + .borderWidth = 5.0f, + .friction = 0.99, + .elasticity = 0.9, + .color = color, + .grabbed = false + }; + + TextCopy(tp.text, text); + tp.rect.width = MeasureText(tp.text, FONT_SIZE)+tp.padding*2; + tp.rect.height = FONT_SIZE+tp.padding*2; + return tp; +} + +void SliceTextParticle(TextParticle *tp, int particlePos, int sliceLength, TextParticle *tps, int *particleCount) +{ + int length = TextLength(tp->text); + + if((length > 1) && ((*particleCount+length) < MAX_TEXT_PARTICLES)) + { + for (int i = 0; i < length; i += sliceLength) + { + const char *text = sliceLength == 1 ? TextFormat("%c", tp->text[i]) : TextSubtext(tp->text, i, sliceLength); + tps[(*particleCount)++] = CreateTextParticle( + text, + tp->rect.x + i * tp->rect.width/length, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + RealocateTextParticles(tps, particlePos, particleCount); + } +} + +void SliceTextParticleByChar(TextParticle *tp, char charToSlice, TextParticle *tps, int *particleCount) +{ + int tokenCount = 0; + const char **tokens = TextSplit(tp->text, charToSlice, &tokenCount); + + if (tokenCount > 1) + { + int textLength = TextLength(tp->text); + for (int i = 0; i < textLength; i++) + { + if (tp->text[i] == charToSlice) + { + tps[(*particleCount)++] = CreateTextParticle( + TextFormat("%c", charToSlice), + tp->rect.x, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + } + for (int i = 0; i < tokenCount; i++) + { + int tokenLength = TextLength(tokens[i]); + tps[(*particleCount)++] = CreateTextParticle( + TextFormat("%s", tokens[i]), + tp->rect.x + i * tp->rect.width/tokenLength, + tp->rect.y, + (Color) { GetRandomValue(0, 255), GetRandomValue(0, 255), GetRandomValue(0, 255), 255 } + ); + } + if (tokenCount) + { + RealocateTextParticles(tps, 0, particleCount); + } + } +} + +void ShatterTextParticle(TextParticle *tp, int particlePos, TextParticle *tps, int *particleCount) +{ + SliceTextParticle(tp, particlePos, 1, tps, particleCount); +} + +void GlueTextParticles(TextParticle *grabbed, TextParticle *target, TextParticle *tps, int *particleCount) +{ + int p1 = -1; + int p2 = -1; + + for (int i = 0; i < *particleCount; i++) + { + if (&tps[i] == grabbed) p1 = i; + if (&tps[i] == target) p2 = i; + } + + if ((p1 != -1) && (p2 != -1)) + { + TextParticle tp = CreateTextParticle( + TextFormat( "%s%s", grabbed->text, target->text), + grabbed->rect.x, + grabbed->rect.y, + RAYWHITE + ); + tp.grabbed = true; + tps[(*particleCount)++] = tp; + grabbed->grabbed = false; + if (p1 < p2) + { + RealocateTextParticles(tps, p2, particleCount); + RealocateTextParticles(tps, p1, particleCount); + } + else + { + RealocateTextParticles(tps, p1, particleCount); + RealocateTextParticles(tps, p2, particleCount); + } + } +} + +void RealocateTextParticles(TextParticle *tps, int particlePos, int *particleCount) +{ + for (int i = particlePos+1; i < *particleCount; i++) + { + tps[i-1] = tps[i]; + } + (*particleCount)--; +} \ No newline at end of file diff --git a/examples/text/text_strings_management.png b/examples/text/text_strings_management.png new file mode 100644 index 0000000000000000000000000000000000000000..d9b6cc4ed83dfd2eadef9b2b06725a4ccae864cc GIT binary patch literal 18431 zcmeHPc{tR2+aLSb$Ie)%ImE#jj3pYo8B3O?g@c4jGm{FDk}|TF36U)o%@Caug@#g= z5i-i)NJUSUCL{-`)G56`Bvek%_4K^wdavvC&+qz~F~7OL_wxDP_x-uQ6YuPZ7372R zfj}TZTN^7J2*jZT0(2Um7#J@hLUAvm=U3U8TkMa#^yxW_`fNFz`ulSf`U++&{)A&1us>?6zINE zu)mrD6wx28TEKZ_H@sOXV#3I9-1!h%`fgg^USyQaEgCjaD)hOcTbIGcW)ky#IEoWv z={0+qime_LXjF>p&$CZI!P3KnTa7d@b)f?)Wyz9nDTKqNuJQ}^x&*7XxKvk6&X4%j z5La*hCig|n=JkC#Ql}4j6TMka_laHIq7*zB>c8nx>6q!&o~c^VhxZ4f_}7^-ggm^> z63`udJQe{~j+GW6ruyNc$M*TOsRTkX*Et>v((kC1uVoXhyP)_`5&1EygYsP7k8lP$ z-b~`CW~dl2zn3JnV$)_Zm{_(Y3%w$qqQVTJmmr>Az>H zP50JVw0g;CPDrQ%;Zze&bfZQR@eD6C;ed^2h83Go7_(BLl9$48GAgu9mz;b{m6}P#ZrXQPj(f; zRjQuyOW}?Q`*bHZBf@$yM5BghzJYD+I2~hVRPE8MJuu~NwRk7ogrUBH^#&xrEE=Rs zTcR|2EU5p%m93vr#Q1WK?HU3bRS{_KrP6Q591F*~8Odp0<8c8CT$nV-O2_Pb@C6@0 zRTiZ48;UU$!eDf@v*Ng%e2IKZFoz6#o!Un-E01{-wEz^(FtaxTjri2%13v~-2t$U7 z^y0H*7WBUKF2@}_3sH~1i0}6$sLUq~-L@<$-oYG~rN1?2QJNyTK zJBaGW9IsxA_iPqGwD>{1WgV>c`jI+FbpJx-I|50E;x@%zl4MDS?T6oxVr};G&57kL zoUVC(Bt!1%q48*$tL}}wQW-}w$b>~-+_(BCXe%|GP!Y`yD&KKtJX1L=1piu@ZSVrdilp1o1U9le2K+HbTIj((-BqcAfZ59fnMWwqXQ>y#Y{)`|9}1?Ev# za&3vBGI88ZUb{Ja1Eq>kmgG`Y_k=-Je_9t!MiYzuyuW<$FH#oFEQp8maA|7y9nlha z8}$Th6%QZ6*z1fialeIX!0I(36+ua(*J@Y@4?>I^fk2bsnoMvH3n&f>(t3c`)Ln?y znLD#ONDf{Y%-GTeolHFuCib|-d^683L+@*{56OeNur7v~@IH@GSQ$L#J|fIJ=KkKU zHlDoEJo;9px7D>7;@3q|?iI>BX&jQGtEez+c4v#Fl(+V<^>6Tz3Uu{(C3VZ6eo9Ej zhO;z5J#b=m`PwM;t>3C9D|!x>5@WQcAs{_!On(6q5iTh4N$9}=nG^2K~hhM4F z6O);9$}g6}2B>Hb4P=)M#UOqlrz-xboC=6WjIk0W ze)ktME!nGpB%zc;CyRSNS@`C^Vzz=4?g5Z9ymXZd2*mcolW}GT`brY^xWU zm1`+@V`%6|U~Qt@TjnFD4xMuz`&3I5(>=p{g|W%h`B04t_Z~57TO55`uv<=Mw(uO1xoo#X~x9*N1c z*ZK?d3bEUQZL`%0l!_&xVLi2W~S&{~Yz~$@Z!UPdM9dH*>ys5tUfi0KlDJ~wMe;7$=eiejYI(v(8q;- z!fa7l-BLG_n}4{&&bR}~<31(qyRXUNT8GW)9Di6|SoP*|hzrxL3&XiBlOU;eERLrhPhMXh)kCEQS30ydT_!nY9Uk zi|z_mwQ_NRE8PZaB~Gu2qJxJTHph!xM0!uqACWzps(&>M^99nz&}SSQ>=R7W`!dH} z?dX2s@~PVNxbb%q>4)M)8YK5q2kThAxDObjE9^0rDDBfMMx`mHy9ZQ6O24L0#Y%^Z zo_^32?{4=2OYpQv)U|74;a2TbW4?kG8LmnV89RmUxErEmTGMTRycnjRD8lFN(*{v~ zZI2RZPAaKLPLj^s0jpD;-$c#j*nbH&Yvp$f;|fQ`AF?ct7E$(fCITs8<0*t*-zjLX z)kWjDGO>Ii^x1$RMeyyMzs@8*PCH5SAg7`dM8HN&XU_5ypRA0j6#$uIV}9(;ty}oPk8n6ozD{=-JL_9&O4B%<9tKRf?$}O zMUNA78NjGtRq%M9E_Ad?wIfKj@;54qF(@I~HB$3zPuTQtMx=w}EbCv&YPbF3;xWl+ zC|#FAAt%n;LNKC6mRE!fSG(2yVv?e_*z}iv9 z$?i-8l5ZYZGGaa>=^eyNOVCDMg#6J9o7O{hIwlO1S*^w+O=Wwj`63pb@Ch#ZTPA(M&afn7+QPvwn*-OC@%+fWbMsk#jMW8koJyY-CFUl>K zLIGJEk{t7TDHtzFmt-(Ys(oh&y9Ol}+XHc}3>v~Gi30XT zp5H2(yVI3oK*IJPEd3J0pT(_~WVZ@2D<&7^8X(PxN%dY~K9jX_JRA6RdD1;S_D3+x z4LExGrx;c){DC|sZ@2B!f}sbGSeK``R?!%wp$19eNiYwP@bw8lJWRdAQO=p5S;TCq zm&&K-I}oWdKF5?S4#voZIM;n@+|wZCYhMynT~aTi%}P!&DSn2Z#3D7O)ByKz{-&{p7)>bL6{ z)=mwywvS%;Zeic+g84pYQ$LW4BrDoWf=Vq7+zu@i6ao~8QX$9Dy(r-dk!Z~@fAUD~ z@AaF9eAytXSh4vw#`Lfa>?<25I!PNKgEc$PXxR=LO_HB-L zJxr(TX=qT?vU|^OcI`dp9yTPMrf-J26=d!B(8|uW|L*fCXyd>D*5Vm2A{+-tlNi;{ z6z$qUw?2^X`w<)7;WbSbC!|1GnXvWtk&jM$C_4&H-0j`vCD1Tnb19r>7cHR_c+v+O zTUX(?2cQnIlv7Kv#d$TSsu9;NlqmO3FvQpnD1Hxl#_^Wa3>WogJi*q5mWqE>?B#@j zlh7PG&`tCC!D2Dy(+whkVcZW_FbpV`e-gt000A&e#!UKjvwyr;KYvC=BdY9WPB0$V z_la4$#0M4(0$1a7je9);cXJ3m62NydxA1}Vgdr3Fq@-+(r zHqyh`zarYLF9&HA@)*xrw%<@Kz;-d%tT-5vR6o{oHmA9FxWL(mN85MRjYzj)tXR)- z<_E3fbmg}6f;?WI8|@z7^Wf-S-<>G)Wm_K*DRvDwItScxkyIO-k(tcoH299o&9(&0 z3^@yK@o;WeF1*Gym9`Uhh|L{K0>Jbr`{N0%z_jD!{W#3EW!fVUA+|eLw%(pr6fF35 zxB0T~qog_my!bTM{=dMswE zqOSk%B9QuKI?)n+x_{-RhYVy63i2N@4DyNs0O^)}7Ag}JuS$pf;lBn5U2I+2ZB409=O z44Y&cBpLa^1E=WBo``0XFjilnqW-nydT@o*=?Yr6c_SieZAmXli;?^irgtem*tk(B z!_8GXNo;!1*BOeL>P~E_0y?&pkHLCK!YN0B#05GX&2XYtvTapRN##yzhce_`Z;GT` zDU|RW$gsRTaU!D>yY3NI*wl}8632~P+9b&d1Q#a}6+$EVsftw*Al;OW0X{uPn?39% zDF!G?ysI1Lk}+OFsBt=PlbqKs(za{-6(w<;KeR3~Zfw~{ z&lmP!iSZpuGV$;}s`U|}?7cZI!(T=33Z(yEc%8KUqA5H!ExNRhZLc_#-^rn~o8nFV z-t_B+3=NJbE!oyHAYQq7WlT$aIC#w-vVzZFTa)^;M6dU^@e^CUUgVrSeTREh7|)?> zT;1V8-=ubk{9S@#nnJ)kHkT=9c(?yEF1P6C{&5$WcNXPfWBhDc`^{n=fn|$Ctellk zTn<2|Vj*e)#)v9fZTv~U$Xy#VH~$u(N>@|%wzz9!d<*GmB!IbJGW$^V?l{2>#Qk!E zXJRq3d<$*>^m0@=ZH7tJzgqjKW9_g4@$$J*2T!)y$)+FiJ0qNQhWs#7X!ZF25t|~&#(+rc0R7nHWdRjAY3Vh&s+PHN?Q&@LLsIL% zk;wl&pst88W1JN&tY~4L|6cKh6<=8Kg(X}3k7h4cwyC+Q=jSU{4T`tJC&*J4OF{6> zcPc5iRTfWQJh(#XMFF!9GhD6hLThDlnFA!<{|GfRs4{7OvI}G2N@l(PO59`xb_&ny zgaq-KR-;?zvK8t-CFl2xO0#r`*^})ip`iPX4nZ^YI@i6$>EvRa{(GgPeKAH%w#Sl` zG34Sw#9Y{e71Pk1;fIK*!zc3Z&*~_GFM>I-6ZKLsZ;m9qvN+b3u=7Db8X1_GT0qo5 z9sDA7IAhwIq)X3AV)f5uG|HiaIdxMpb=ymo=6dDBFr?cFa(-{F`0NnDQ1CZ>?au}A zkB10mgi>~mG%QK3EaiTv-ngNp6d9XQ=xLXxkQ|DW)MnZ=O`?>Wv;FJ2H1nHn^-?+v zYxB;!5fVB^Z1~@9Q2$fuuN}zeJ8*T`ojaQS1BI1`4x&7jV92Z|V;4AmhSO^{PtVq9 zpqSZazs|C4>>rM7%vdalw-aLPm#Xm~cl&Smyo3}0j=iZv_v*h2%RG*qZ7$*db#zch zYjP4^_CYV3k!$_XVx0m^Wox@`HX&Jv=k#B(K9%(nT9(Q^LLDbvLRnXsZ>Af8eg-hA z)?xY|XZ16@>Y(T)k)*DIG6BLb&+Tz_kO$(Xg%|Xbp)6Xr?PcZdlpVUyV94x=ey=zD zW!;#|H%+rbxShZWo$-{y02C*;$y}eK_otoJ)kn5ywK&rVs`gv0)r%c2jYkv-_LptY z+5}|OeSb2gW^=i-=)F=yWkTr|T|iOehT{qW;__JgbOQ?VnaeS{L{5c4?2A zbxjl0+`yAf&Y61@hF8}?uE`xgIbzb~xCzQQcn~T6duq0)p7cbmS94;dX4QIiGWl$r zwWkeEbkC$m+S*6wq;Jmg8b*J|Yy7Yr^5R6ePnklWObW93ZAh;h-x$XvY<^Guo$0Tj zS%a5nrx5&5TJ5^&+_Rk6XAJeRNJni8K)?3{yBqO@FJ_|p9jjuQlJT@17)v+ zb7Edi;O+8vf)nzuGXuAw8ptuA&^g{d{wu`&^5?(D+dI`jNp$_t+B&?@qG`0bX5CD2 zyBkhU*r+|j6aDp{>+`?SubWgzn%QLT+bXua{vK4qVRNmhcR#25pY1yu@uD_odSrK8zBr952(Zc+c^@=a7_`-@WEauBr zB4Q;XRw800BEFdt|0fP0tfa3$ZtblA^a?<)0Q3q#uK@ItX}!-5v4V~(=(vK8E9kg_ zjw|RmgM|N?7Jvhsl0LipSS*$1^}yi)t{3o4rj;hris^7KR?03#hKI$Q>W=lpias1E zZoOFIfXLN!&v84#0kMg%Y{|2xKA%|s>Wdd>dtka?(1gQcIPTYyU9CLC%SIJ&deo%I zq0mkncZ#XeDq%I*LA02To60wH6ri%XXKOA)=FCl403=#p{O*E;DB;(cSvAAcS5UV? zwozM($(`sWf&wSn=GfAW%Z|3s%$)^Y@Tv(jsq8#giZc5>6Eaz)O2g0z!55`Mjg&eS zaP9Y%u7S{7dzrUk2-EDe|2CbF^UL7anloB=77UKnsH4{!y