Community project

Animated Pixel Eyes

Photo of Animated Pixel Eyes
Generated with AI

Filippo Baratti

Published November 8, 2026

Animated Pixel Eyes brings a character to life on a small display, complete with expressive animations, blinking, and interactive reactions. Built around an ESP32 microcontroller and ST7789 LCD screen, this project creates a playful digital companion that responds to user input and cycles through different moods and visual effects.

This guide provides everything needed to assemble and program the project: a wiring diagram showing how to connect the display to the ESP32, a complete parts list, the Arduino firmware with animation logic and color palettes, and step-by-step assembly instructions. Makers will learn how to drive a small LCD display, implement frame-based animation, and add interactivity through button input.

Wiring diagram

Loading diagram…

Assemble it in 3 steps

1. Prepara la scheda

Usa la Waveshare ESP32-C6-LCD-1.47 senza touch. Lo schermo è già collegato: non servono fili, componenti esterni o microSD. Appoggiala su una superficie non metallica.

  • Tieni lo schermo in orizzontale per vedere i due occhi affiancati.

2. Collega il cavo USB

Collega la porta USB-C della scheda al computer con un cavo USB che trasporti anche dati.

  • Non appoggiare la scheda su metallo: può provocare un cortocircuito.
  • Non aumentare la luminosità oltre il 50%: il produttore avverte del rischio di surriscaldamento e ombre sul display.

3. Gioca con gli occhi

Menu semplificato: tieni BOOT per 0,7 secondi e rilascia. Quattro sezioni: OCCHI, GIOCHI, RELAX e STILE. Clic per passare alla scheda successiva; tieni premuto finché appare RILASCIA PER SCEGLIERE, poi rilascia per confermare. GIOCHI contiene tutti i sette giochi e INDIETRO. STILE permette di cambiare colore, attivare o fermare il cambio automatico, sorteggiare una faccia e tornare indietro. Nei giochi la pressione lunga torna alla relativa scheda: clic per riprovare se perdi. Le descrizioni sulle schede spiegano il comando di ogni gioco. Negli occhi: clic cambia espressione, doppio clic rabbia seguita da offesa, tre clic offrono un lecca-lecca. Restano 80 espressioni animate. RELAX offre quattro atmosfere animate: cielo stellato, oceano calmo, bosco magico e tramonto. Un clic cambia atmosfera senza interrompere il respiro; tieni BOOT e rilascia per tornare al menu. La faccina e il cerchio guidano quattro secondi di inspirazione e sei di espirazione, senza trattenere il respiro. Dopo il primo ciclo compaiono durata e cicli completati. Respira senza sforzo: non devi seguire il ritmo se non è confortevole; se senti fastidio, interrompi. Nessun audio e nessuna funzione medica. I record dei giochi restano salvati. Non tenere BOOT premuto all'accensione.

  • Non tenere BOOT premuto mentre colleghi USB o premi RESET, altrimenti la scheda entra nella modalità di caricamento.

Deploy the firmware

#include <Arduino.h>
#include <SPI.h>
#include <Adafruit_GFX.h>
#include <Adafruit_ST7789.h>
#include <esp_system.h>
#include <math.h>
#include <Preferences.h>
Adafruit_ST7789 lcd(&SPI,14,15,21);
// Raster a 2x: trasferimento delle sole righe modificate, ritmo a 60 Hz.
constexpr int W=160,H=86,COUNT=80;
uint16_t frame[W*H],previous[W*H];
// Shared 32-colour palette: eyes, mouth, party effects and arcade games.
// Keep the original colours first so existing selections remain unchanged.
const uint16_t colors[]={
 0x07FF,0x07E0,0xFFE0,0xF81F,0xFFFF,0xFD20,0x867F,0xF9B0,0xF800,
 0x051F, // electric blue
 0x03FF, // azure
 0x05D5, // turquoise
 0x07EF, // spring green
 0x87F0, // mint
 0xBFE0, // lime
 0xDFF0, // lemon cream
 0xFEA0, // golden yellow
 0xFC00, // vivid orange
 0xFB08, // coral
 0xFAAA, // salmon
 0xFCB2, // peach
 0xF813, // raspberry
 0xF819, // hot pink
 0xFBB7, // rose
 0xD81F, // violet
 0xA21F, // purple
 0xB51F, // lavender
 0xC65F, // lilac
 0xAEFF, // ice blue
 0x7FF8, // aquamarine
 0xC7F9, // pale mint
 0xFF58  // warm cream
};
constexpr int COLOR_COUNT=sizeof(colors)/sizeof(colors[0]);
int mood=0,palette=0; uint32_t changeAt,blinkAt=2200,blinkStart=0,nextFrame=0;
bool rawDown=false,down=false,autoPlay=true;
uint32_t edgeAt=0,pressedAt=0,clickAt=0,reactionUntil=0,gazeAt=0;
uint32_t partyStarted=0,partyUntil=0;
uint32_t treatStarted=0,treatUntil=0;
uint32_t offendedUntil=0, offenseStarted=0;
int moodBeforeOffense=0;
int clicks=0,shuffleBag[COUNT],bagIndex=COUNT;
float gazeX=0,gazeY=0,targetX=0,targetY=0,smoothRX=21,smoothRY=25;
uint32_t transitionAt=0;
constexpr uint32_t FRAME_MS=16;
constexpr float GAME_STEP=16.f/33.f;
void selectMood(int next,uint32_t now){mood=next;transitionAt=now;changeAt=now+5500+esp_random()%3500;}
int randomMood(){
 if(bagIndex>=COUNT){for(int i=0;i<COUNT;i++)shuffleBag[i]=i;
 for(int i=COUNT-1;i>0;i--){int j=esp_random()%(i+1);int a=shuffleBag[i];shuffleBag[i]=shuffleBag[j];shuffleBag[j]=a;}bagIndex=0;}
 return shuffleBag[bagIndex++];
}
void pixel(int x,int y,uint16_t c){if(x>=0&&x<W&&y>=0&&y<H)frame[y*W+x]=c;}
void ellipse(int cx,int cy,int rx,int ry,uint16_t c){rx=max(1,rx);ry=max(1,ry);for(int y=-ry;y<=ry;y++){int span=(int)(rx*sqrtf(max(0.f,1.f-float(y*y)/float(ry*ry))));for(int x=-span;x<=span;x++)pixel(cx+x,cy+y,c);}}
void line(int x,int y,int xx,int yy,int r,uint16_t c){int n=max(abs(xx-x),abs(yy-y));for(int i=0;i<=n;i++)ellipse(x+(xx-x)*i/max(1,n),y+(yy-y)*i/max(1,n),r,r,c);}
void heart(int cx,int cy,uint16_t c){for(int y=-14;y<=15;y++)for(int x=-16;x<=16;x++){float a=x/16.f,b=-y/16.f;float q=a*a+b*b-1;if(q*q*q-a*a*b*b*b<=0)pixel(cx+x,cy+y,c);}}
void eye(int cx,int cy,int rx,int ry,int type,bool right,uint16_t c,float t){
 if(type>=64){
  float a=t*2.4f+(right?.55f:0);
  uint16_t accent=colors[(type+int(t*.6f))%COLOR_COUNT];
  switch(type){
   case 64: // Pianeti con anello inclinato.
    ellipse(cx,cy,14,14,c);
    for(int i=0;i<80;i++){float q=i*PI/40;pixel(cx+cosf(q)*24,cy+sinf(q)*7+cosf(q)*5,accent);}break;
   case 65: // Fuochi d'artificio a cicli sfalsati.
    for(int i=0;i<10;i++){float q=i*PI/5;float r=4+fmodf(a*8,21.f);line(cx+cosf(q)*r*.65f,cy+sinf(q)*r*.65f,cx+cosf(q)*r,cy+sinf(q)*r,1,i%2?c:accent);}break;
   case 66: // Doppia elica.
    for(int y=-22;y<=22;y+=3){int x=sin(a+y*.18f)*16;ellipse(cx+x,cy+y,2,2,c);ellipse(cx-x,cy+y,2,2,accent);if(y%2==0)line(cx-x,cy+y,cx+x,cy+y,0,0x3186);}break;
   case 67: // Onde concentriche.
    for(int y=-18;y<=18;y+=6)for(int x=-19;x<=19;x++)pixel(cx+x,cy+y+int(sinf(x*.18f+a+y*.1f)*3),y%12?accent:c);break;
   case 68: // Gemma sfaccettata.
    for(int i=0;i<6;i++){float q=i*PI/3+a*.2f;int x=cx+cosf(q)*20,y=cy+sinf(q)*22;line(cx,cy,x,y,1,i%2?c:accent);float b=q+PI/3;line(x,y,cx+cosf(b)*20,cy+sinf(b)*22,1,c);}break;
   case 69: // Sole con raggi mobili.
    ellipse(cx,cy,11,11,c);for(int i=0;i<8;i++){float q=a*.4f+i*PI/4;line(cx+cosf(q)*16,cy+sinf(q)*16,cx+cosf(q)*22,cy+sinf(q)*22,1,accent);}break;
   case 70: // Fiocchi di neve.
    for(int i=0;i<6;i++){float q=i*PI/3+a*.12f;int x=cx+cosf(q)*20,y=cy+sinf(q)*20;line(cx,cy,x,y,1,c);for(int k=-1;k<=1;k+=2)line(cx+cosf(q)*13,cy+sinf(q)*13,cx+cosf(q)*13+cosf(q+k*.8f)*7,cy+sinf(q)*13+sinf(q+k*.8f)*7,0,accent);}break;
   case 71: // Satellite e orbita.
    ellipse(cx,cy,19,22,c);ellipse(cx,cy,17,20,0);ellipse(cx+cosf(a)*19,cy+sinf(a)*22,4,4,accent);ellipse(cx,cy,5,6,c);break;
   case 72: // Occhi acquatici.
    ellipse(cx,cy,rx,ry,c);for(int x=-rx;x<=rx;x++)for(int y=-ry;y<int(sinf(x*.2f+a)*4);y++)pixel(cx+x,cy+y,0);break;
   case 73: // Pixel danzanti.
    for(int i=0;i<9;i++){int x=(i%3-1)*13,y=(i/3-1)*14+sin(a+i)*4;ellipse(cx+x,cy+y,2+int((sin(a+i)+1)*2),3,i%2?c:accent);}break;
   case 74: // Occhio fotografico.
    ellipse(cx,cy,22,24,c);ellipse(cx,cy,18,20,0);ellipse(cx,cy,10+sin(a)*4,12+sin(a)*4,accent);ellipse(cx,cy,5,6,0);ellipse(cx-5,cy-8,2,2,0xFFFF);break;
   case 75: // Nastri luminosi.
    for(int i=0;i<100;i++){float q=i*PI/50;pixel(cx+sin(q)*21,cy+sin(q*3+a)*18,c);pixel(cx+sin(q)*19,cy+cos(q*2+a)*16,accent);}break;
   case 76: // Occhi timidi.
    ellipse(cx,cy,rx,18,c);ellipse(cx+(right?-7:7),cy+5,8,10,0);ellipse(cx-6,cy-7,3,3,0xFFFF);break;
   case 77: // Occhi a molla.
    for(int x=-21;x<=21;x++)ellipse(cx+x,cy+sin(x*.3f+a)*14,1,2,c);break;
   case 78: // Petali doppi.
    for(int i=0;i<8;i++){float q=i*PI/4+a*.25f;ellipse(cx+cosf(q)*14,cy+sinf(q)*17,5,6,i%2?c:accent);}ellipse(cx,cy,5,5,0xFFE0);break;
   default: // Galassia a due bracci.
    for(int i=0;i<100;i++){float q=i*.13f+a,r=i*.22f;pixel(cx+cosf(q)*r,cy+sinf(q)*r,c);pixel(cx-cosf(q)*r,cy-sinf(q)*r,accent);}break;
  }
  return;
 }
 if(type>=48){
  float phase=t*3+(right?.7f:0);
  switch(type){
  case 48: // Fiore che respira.
   for(int i=0;i<6;i++){float a=phase*.4f+i*PI/3;ellipse(cx+cosf(a)*13,cy+sinf(a)*13,7,8,c);}
   ellipse(cx,cy,6,6,0);ellipse(cx,cy,3,3,c);break;
  case 49: // Radar con raggio rotante.
   ellipse(cx,cy,20,20,c);ellipse(cx,cy,17,17,0);
   line(cx,cy,cx+cosf(phase)*16,cy+sinf(phase)*16,1,c);ellipse(cx,cy,2,2,c);break;
  case 50: // Diamanti pulsanti.
   for(int i=0;i<4;i++){float a=i*PI/2+phase*.2f,b=a+PI/2;line(cx+cosf(a)*rx,cy+sinf(a)*ry,cx+cosf(b)*rx,cy+sinf(b)*ry,2,c);}break;
  case 51: // Farfalle.
   {int wing=8+int((sin(phase*2)+1)*4);ellipse(cx-wing/2,cy-7,wing,11,c);ellipse(cx+wing/2,cy-7,wing,11,c);ellipse(cx-6,cy+8,7,8,c);ellipse(cx+6,cy+8,7,8,c);line(cx,cy-12,cx,cy+14,1,0);}break;
  case 52: // Equalizzatore.
   for(int i=-2;i<=2;i++){int h=4+int((sinf(phase+i*.9f)+1)*9);line(cx+i*8,cy-h,cx+i*8,cy+h,2,c);}break;
  case 53: // Occhi robot con scansione.
   for(int y=-18;y<=18;y+=6)line(cx-17,cy+y,cx+17,cy+y,1,c);
   line(cx-18,cy+int(sinf(phase)*17),cx+18,cy+int(sinf(phase)*17),2,0xFFFF);break;
  case 54: // Comete orbitanti.
   for(int i=0;i<9;i++){float a=phase-i*.18f;ellipse(cx+cosf(a)*18,cy+sinf(a)*20,max(1,4-i/3),max(1,4-i/3),c);}break;
  case 55: // Mezzelune gemelle.
   ellipse(cx,cy,20,24,c);ellipse(cx+(right?-8:8),cy-5,18,22,0);break;
  case 56: // Risata con guance sollevate.
   for(int x=-20;x<=20;x++)ellipse(cx+x,cy-5+int(x*x*.025f),2,3,c);break;
  case 57: // Occhi curiosi con pupille mobili.
   ellipse(cx,cy,rx,ry,c);ellipse(cx+cosf(phase)*9,cy+sinf(phase)*10,8,10,0);ellipse(cx+cosf(phase)*9-2,cy+sinf(phase)*10-3,2,2,0xFFFF);break;
  case 58: // Portale pulsante.
   for(int i=0;i<3;i++){int r=5+(int(t*12)+i*7)%21;ellipse(cx,cy,r,r,c);ellipse(cx,cy,max(1,r-2),max(1,r-2),0);}break;
  case 59: // Pioggia digitale dentro gli occhi.
   for(int i=-2;i<=2;i++){int y=(int(t*22)+i*11+60)%35-17;line(cx+i*7,cy+y-5,cx+i*7,cy+y,1,c);}break;
  case 60: // Occhiali da sole.
   ellipse(cx,cy,24,16,c);ellipse(cx,cy,20,12,0);line(cx-11,cy+5,cx,cy-6,1,c);line(cx-3,cy+6,cx+8,cy-5,1,c);break;
  case 61: // Ventaglio di stelle.
   for(int i=0;i<8;i++){float a=phase*.5f+i*PI/4;line(cx+cosf(a)*7,cy+sinf(a)*7,cx+cosf(a)*22,cy+sinf(a)*22,2,c);}break;
  case 62: // Saltelli alternati.
   ellipse(cx,cy,rx,ry,c);ellipse(cx-5,cy-8,5,7,0);break;
  default: // Infinito in movimento.
   for(int i=0;i<72;i++){float a=i*PI/36+phase*.4f;pixel(cx+sin(a)*22,cy+sin(a*2)*13,c);pixel(cx+sin(a)*22,cy+sin(a*2)*13+1,c);}break;
  }
  return;
 }
 if(type==32){for(int i=0;i<10;i++){float a=i*PI/5-PI/2,b=(i+1)*PI/5-PI/2;int r=i%2?9:22,r2=i%2?22:9;line(cx+cosf(a)*r,cy+sinf(a)*r,cx+cosf(b)*r2,cy+sinf(b)*r2,2,c);}return;}
 if(type==33){ellipse(cx,cy,rx,ry,c);ellipse(cx+8,cy-6,rx-3,ry-2,0);return;}
 if(type==34){line(cx-13,cy-15,cx+9,cy-2,3,c);line(cx+9,cy-2,cx-8,cy+4,3,c);line(cx-8,cy+4,cx+12,cy+17,3,c);return;}
 if(type==35){ellipse(cx,cy,rx,ry,c);ellipse(cx+sin(t*3)*6,cy+cos(t*3)*8,rx-6,ry-6,0);return;}
 if(type==36){line(cx-12,cy-14,cx+12,cy,3,c);line(cx+12,cy,cx-12,cy+14,3,c);return;}
 if(type==37){for(int i=-2;i<=2;i++)line(cx+i*8,cy-12,cx+i*8,cy+12,2,c);return;}
 if(type==38){ellipse(cx,cy,rx,ry,c);ellipse(cx,cy,rx-4,ry-4,0);ellipse(cx+sin(t*4)*8,cy,4,8,c);return;}
 if(type==7){heart(cx,cy,c);return;}
 if(type==8){line(cx-12,cy-12,cx+12,cy+12,3,c);line(cx-12,cy+12,cx+12,cy-12,3,c);return;}
 if(type==9){for(int i=0;i<180;i++){float a=i*.095f,r=i*.085f;ellipse(cx+cosf(a+t*4)*r,cy+sinf(a+t*4)*r,1,1,c);}return;}
 if(type==10){line(cx-16,cy,cx+16,cy,2,c);line(cx,cy-19,cx,cy+19,2,c);line(cx-10,cy-10,cx+10,cy+10,1,c);line(cx-10,cy+10,cx+10,cy-10,1,c);return;}
 if(type==1||type==13||type==20){for(int x=-rx;x<=rx;x++){int y=(int)(float(x*x)/max(1,rx)*.55f);ellipse(cx+x,cy-6+y,2,3,c);}return;}
 if(type==6&&right){line(cx-rx,cy,cx+rx,cy-3,3,c);return;}
 ellipse(cx,cy,rx,ry,c);
 if(type==11||type==24)ellipse(cx,cy,max(1,rx-5),max(1,ry-5),0);
 if(type==2||type==3||type==17||type==18){for(int x=-rx;x<=rx;x++){int cut=(right?x:-x)/2;if(type==3||type==18)cut=-cut;for(int y=-ry;y<cut-ry/2;y++)pixel(cx+x,cy+y,0);}}
 if(type==4||type==16)for(int y=-ry;y<0;y++)for(int x=-rx;x<=rx;x++)pixel(cx+x,cy+y,0);
 if(type==21||type==22){ellipse(cx+(int)(sinf(t*2)*7),cy,5,ry-4,0);}
 if(type==25){line(cx-rx,cy,cx+rx,cy,2,0);line(cx,cy-ry,cx,cy+ry,2,0);}
}
// Bocca nel margine inferiore: stesso raster e orientamento degli occhi.
void mouth(int type,int dx,int dy,uint16_t c,float t){
 const int cx=80+dx/3,cy=75+constrain(dy/4,-2,2);
 // Ogni espressione ha una posa; il movimento resta continuo.
 const uint8_t poses[COUNT]={
  0,1,2,3,4,5,6,1,7,8,5,5,8,1,1,0,
  4,2,3,5,1,6,6,8,5,7,4,1,8,3,6,5,
  1,0,2,8,6,7,5,1,3,4,0,1,2,1,6,8,
  1,5,0,1,8,7,5,4,1,5,5,8,6,1,1,8,
  5,1,8,0,5,1,4,5,0,8,5,8,6,8,1,5};
 int pose=poses[type%COUNT];
 if(pose==5){ // O di sorpresa, pulsante.
  int r=3+int((sinf(t*4)+1)*1.3f);
  ellipse(cx,cy,4,r,c);ellipse(cx,cy,2,max(1,r-2),0);return;
 }
 if(pose==7){ // Espressione KO / denti serrati.
  line(cx-7,cy-2,cx+7,cy-2,1,c);line(cx-7,cy+2,cx+7,cy+2,1,c);
  line(cx-7,cy-2,cx-7,cy+2,1,c);line(cx+7,cy-2,cx+7,cy+2,1,c);
  for(int x=-3;x<=3;x+=3)line(cx+x,cy-1,cx+x,cy+1,0,c);return;
 }
 if(pose==8){ // Bocca ondulata per vertigini e facce buffe.
  for(int x=-8;x<=8;x++)ellipse(cx+x,cy+int(sinf(x*.55f+t*6)*2),1,1,c);return;
 }
 if(pose==4){ // Respiro lento durante sonno e stanchezza.
  ellipse(cx,cy,4,1+int((sinf(t*2)+1)*.6f),c);return;
 }
 for(int x=-8;x<=8;x++){
  float u=x/8.f;int y=0;
  if(pose==0)y=int(2*(1-u*u));
  else if(pose==1)y=int(4*(1-u*u))-2;
  else if(pose==2||pose==3)y=-int(3*(1-u*u))+1;
  else if(pose==6)y=x/3;
  ellipse(cx+x,cy+y,1,1,c);
 }
 if(pose==1){ // Piccolo sorriso aperto, animato.
  int opening=2+int((sinf(t*5)+1)*.6f);
  ellipse(cx,cy,5,opening,c);ellipse(cx,cy-1,3,1,0);
 }
}
// Timeline dedicata: movimenti continui, senza casualità fra due fotogrammi.
float easeEmotion(float v){v=constrain(v,0.f,1.f);return v*v*(3.f-2.f*v);}
uint16_t emotionColor(uint16_t a,uint16_t b,float f){
 f=constrain(f,0.f,1.f);
 int r=int(((a>>11)&31)*(1-f)+((b>>11)&31)*f);
 int g=int(((a>>5)&63)*(1-f)+((b>>5)&63)*f);
 int bl=int((a&31)*(1-f)+(b&31)*f);
 return (r<<11)|(g<<5)|bl;
}
void drawOffense(float age){
 if(age<1.2f){
  float rise=easeEmotion(age/.22f),settle=1-easeEmotion((age-.96f)/.24f);
  float energy=rise*settle;
  int dx=int(sinf(age*57)*2.5f*energy),dy=int(sinf(age*29)*1.6f*energy);
  uint16_t hot=emotionColor(0xFD20,0xF800,(.5f+.5f*sinf(age*12))*rise);
  // Bagliore scuro e scintille centrifughe, non lampi a tutto schermo.
  for(int side=-1;side<=1;side+=2){
   int cx=80+side*33+dx,cy=40+dy;
   int rx=22+int(2*energy),ry=23-int(7*rise)+int(sinf(age*16)*energy);
   ellipse(cx,cy,rx+3,ry+3,0x4000);
   ellipse(cx,cy,rx,ry,hot);
   // Taglio inclinato verso il naso e pupilla incandescente.
   for(int x=-rx-2;x<=rx+2;x++){
    int top=cy-ry+7-int(side*x*.37f*rise);
    for(int y=cy-ry-4;y<top;y++)pixel(cx+x,y,0);
   }
   ellipse(cx-side*3,cy+2,4,7,0x2000);
   ellipse(cx-side*3-1,cy,1,3,0xFFE0);
   line(cx-19,cy-24+side*6,cx+19,cy-24-side*6,2,hot);
   ellipse(cx+side*21,cy+17,5,2,0x8800);
   for(int i=0;i<3;i++){
    float q=fmodf(age*1.7f+i*.33f,1.f);
    int x=cx+side*int(26+q*11),y=cy-12-int(q*17)+i*8;
    line(x,y,x+side*3,y-3,0,emotionColor(0xFD20,0x4000,q));
   }
  }
  int mx=80+dx,my=72+dy;
  // Denti davvero serrati: contorno, smalto e separazioni scure.
  line(mx-10,my-3,mx+10,my-3,1,hot);
  line(mx-10,my+3,mx+10,my+3,1,hot);
  for(int y=-1;y<=1;y++)line(mx-9,my+y,mx+9,my+y,0,0xFF58);
  for(int x=-6;x<=6;x+=4)line(mx+x,my-2,mx+x,my+2,0,0x6000);
  int r=3+int(2*energy),cy=12+int(sinf(age*18)*energy);
  for(int side=-1;side<=1;side+=2){
   line(80+side*2,cy-2,80+side*(r+3),cy-r-3,1,hot);
   line(80+side*2,cy+2,80+side*(r+3),cy+r+3,1,hot);
  }
  return;
 }
 float t=age-1.2f;
 float turn=easeEmotion(t/1.25f);
 float back=easeEmotion((t-5.1f)/.9f);
 float retreat=turn*(1-back);
 float scale=1-.47f*retreat;
 float breath=sinf(t*2.5f);
 int cx=80+int(31*retreat),cy=42-int(8*retreat)+int(breath*1.5f);
 uint16_t c=emotionColor(0xFB08,0xB51F,easeEmotion(t/.8f));
 c=emotionColor(c,colors[palette],back);
 int spread=int(33*scale),width=max(6,int(20*scale));
 for(int side=-1;side<=1;side+=2){
  int ex=cx+side*spread;
  // Palpebre sempre sigillate: nessuna pupilla, neppure nel rientro.
  for(int x=-width;x<=width;x++){
   float u=float(x)/width;
   int y=cy+int((3*(1-u*u)+side*u*3*retreat)*scale);
   ellipse(ex+x,y,1,1,c);
  }
  line(ex-width,cy-int(13*scale)+side,ex+width,cy-int(16*scale)-side,1,c);
  // Guance discrete e ciglia rivolte lontano dal centro.
  ellipse(ex+side*int(8*scale),cy+int(12*scale),max(2,int(5*scale)),1,0x780C);
  line(ex+side*width,cy,ex+side*(width+3),cy-3,0,c);
 }
 int mx=cx+int(5*retreat),my=cy+int(29*scale);
 for(int x=-int(8*scale);x<=int(8*scale);x++){
  float u=x/(8*scale);
  ellipse(mx+x,my-int(3*(1-u*u)*scale),1,1,c);
 }
 ellipse(mx+int(3*scale),my+int(4*scale),max(2,int(4*scale)),1,0xF9B0);
 // Due sbuffi disegnati come piccoli anelli in dissolvenza.
 for(int i=0;i<2;i++){
  float q=(t-(.25f+i*.48f))/1.05f;
  if(q>0&&q<1){
   int x=cx-int(42*scale)-int(q*18),y=cy+14-int(q*11),r=2+int(q*4);
   uint16_t steam=emotionColor(0xC65F,0,q);
   ellipse(x,y,r,max(2,r-1),steam);ellipse(x,y,max(1,r-2),max(1,r-3),0);
  }
 }
 // Puntini di silenzio: compaiono uno alla volta, poi svaniscono.
 float dots=easeEmotion((t-1.8f)/.5f)*(1-easeEmotion((t-4.6f)/.5f));
 for(int i=0;i<3;i++)if(t>1.8f+i*.3f)
  ellipse(28+i*8,25+int(sinf(t*2-i*.6f)),1,1,emotionColor(0,0x867F,dots));
}
void present();
void arcadeFace(uint32_t now);
void draw(uint32_t now){
 static uint32_t lastDraw=0;
 const float dt=lastDraw?min(uint32_t(50),now-lastDraw)*.001f:.016f;
 lastDraw=now;
 memset(frame,0,sizeof(frame));float t=now*.001f;
 if((int32_t)(now-gazeAt)>=0){targetX=(int)(esp_random()%25)-12;targetY=(int)(esp_random()%13)-6;gazeAt=now+700+esp_random()%1800;}
 gazeX+=(targetX-gazeX)*(1.f-expf(-6.38f*dt));gazeY+=(targetY-gazeY)*(1.f-expf(-5.28f*dt));
 const bool sweet=treatUntil && (int32_t)(treatUntil-now)>0;
 const float treatTime=(now-treatStarted)*.001f;
 const bool offended=offendedUntil && (int32_t)(offendedUntil-now)>0;
 const uint32_t emotionAge=now-offenseStarted;
 const bool angry=offended && emotionAge<1200;
 const float emotionTime=emotionAge*.001f;
 int dx=gazeX,dy=gazeY+sin(t*2)*1.5f,rx=21,ry=25;
 switch(mood){
 case 48:dy=sinf(t*2)*3;break;
 case 49:dx=0;dy=0;break;
 case 50:rx=18+sinf(t*4)*4;ry=23+cosf(t*4)*3;break;
 case 51:dy=sinf(t*5)*5;break;
 case 52:dx=0;dy=0;break;
 case 53:dx=sinf(t)*8;break;
 case 54:dy=cosf(t*2)*4;break;
 case 55:dy=5;break;
 case 56:dy=-abs(int(sinf(t*6)*7));break;
 case 57:rx=24;ry=27;break;
 case 58:dx=0;dy=0;break;
 case 59:dy=sinf(t)*3;break;
 case 60:dx=sinf(t*2)*8;ry=16;break;
 case 61:dy=sinf(t*3)*4;break;
 case 62:ry=18+sin(t*6)*7;dy=-abs(int(sin(t*6)*8));break;
 case 63:dx=0;dy=sin(t*2)*4;break;
 case 4:ry=18;break;case 5:rx=15;ry=31;break;
 case 12:dx=sinf(t*12)*14;dy=cosf(t*9)*5;break;
 case 13:dy=8;break;case 14:rx=26;ry=14;dy=sinf(t*8)*7;break;
 case 15:dx=sinf(t*2)*21;ry=20;break;
 case 16:dy=7+sinf(t*2)*3;ry=14;break;
 case 17:dx=sinf(t*16)*3;break;case 18:dy=4;break;
 case 19:rx=18+sinf(t*5)*5;ry=24+cosf(t*5)*6;break;
 case 20:dy=-abs((int)(sinf(t*6)*12));break;
 case 21:dx=-12;break;case 22:dx=12;break;
 case 23:dx=cosf(t*3)*15;dy=sinf(t*3)*12;break;
 case 24:ry=29;rx=18;break;
 case 26:rx=25;ry=8;dy=6;break;
 case 27:dy=sinf(t*10)*9;break;
 case 28:dx=sinf(t*7)*16;dy=cosf(t*7)*8;break;
 case 29:rx=17;ry=18;dy=10;break;
 case 30:ry=22+sinf(t*4)*8;break;
 case 39:rx=24;ry=18;dy=sin(t*5)*6;break;
 case 40:rx=15;ry=30;dx=sin(t*9)*5;break;
 case 41:rx=27;ry=10;break;
 case 42:dx=sin(t*1.4f)*23;dy=cos(t*1.4f)*10;break;
 case 43:rx=20+sin(t*8)*5;ry=23+sin(t*8)*7;break;
 case 44:dx=sin(t*16)*2;ry=29;break;
 case 45:rx=25;ry=21;dy=sin(t*7)*7;break;
 case 46:rx=18;ry=18;dx=sin(t*4)*18;break;
 case 47:ry=12+abs((int)(sin(t*3)*18));dx=cos(t*3)*10;break;
 case 31:dx=0;ry=4+abs((int)(sinf(t*4)*25));break;
 }
 if(offended){
  if(angry){dx=int(sinf(emotionTime*48)*3);dy=int(sinf(emotionTime*24)*2);rx=23;ry=18+int(sinf(emotionTime*14)*3);}
  else { // Distoglie lo sguardo, respira e stringe lentamente le palpebre.
   dx=10+int(sinf(emotionTime*1.8f)*2);dy=3+int(sinf(emotionTime*3)*2);
   rx=22;ry=12+int(sinf(emotionTime*2.5f)*2);
  }
 }
 const float shapeBlend=1.f-expf(-7.92f*dt);
 smoothRX+=(rx-smoothRX)*shapeBlend;smoothRY+=(ry-smoothRY)*shapeBlend;
 rx=smoothRX;ry=smoothRY;
 uint32_t tr=now-transitionAt;
 if(tr<240)ry=max(2,(int)(ry*fabsf((float)tr-120)/120));
 if(blinkStart){uint32_t b=now-blinkStart;if(b<180)ry=max(2,(int)(ry*fabsf((float)b-90)/90));else blinkStart=0;}
 const bool party=(int32_t)(partyUntil-now)>0;
 uint16_t c=offended?(angry?(sinf(emotionTime*18)>0?0xF800:0xFA00):0xB51F):colors[party?((now-partyStarted)/240)%COLOR_COUNT:palette];
 if(mood>=64&&!offended&&!sweet&&!party)c=emotionColor(c,colors[(palette+5)%COLOR_COUNT],(.5f+.5f*sinf(t*.7f))*.45f);
 int shape=mood;
 if(mood==39||mood==45)shape=1;
 if(mood==40)shape=11;
 if(mood==41)shape=4;
 if(mood==43)shape=7;
 if(mood==44)shape=2;
 if(mood==46)shape=21;
 if(offended)shape=angry?2:4;
 if(sweet){
  // Stesso stile luminoso delle altre facce: occhi da risata e saltelli felici.
  dx=int(sinf(treatTime*2)*3);
  dy=-int(fabsf(sinf(treatTime*5))*3);
  eye(47+dx,42+dy,22,25,1,false,c,t);
  eye(113+dx,42+dy,22,25,1,true,c,t);
  ellipse(25+dx,60+dy,7,3,0xF9B0);
  ellipse(135+dx,60+dy,7,3,0xF9B0);
  if(treatTime>=1.f && treatTime<4.6f){
   // La bocca mastica in sincronia con i piccoli morsi.
   int opening=3+int((sinf((treatTime-1)*PI*5)+1)*2);
   ellipse(80,74,10,opening,c);
   ellipse(80,74,7,max(1,opening-2),0);
   ellipse(80,76,4,1,0xF9B0);
  }else mouth(1,dx,dy,c,t);
 }else if(!offended){
 eye(47+dx,42+dy,rx,ry,shape,false,c,t);
 eye(113+dx,42+(mood==62?-dy:dy),rx,mood==30?max(2,44-ry):ry,shape,true,c,t);
 if(mood==39||mood==45){ellipse(24+dx,64+dy,8,3,0xF9B0);ellipse(136+dx,64+dy,8,3,0xF9B0);}
 if(mood==76){ellipse(24+dx,63+dy,7,2,0xF9B0);ellipse(136+dx,63+dy,7,2,0xF9B0);}
 if(mood==40){ellipse(140,20+(now/70)%18,3,6,0x049F);}
 }
 if(sweet){}
 else if(!offended)mouth(mood,dx,dy,c,t);
 else drawOffense(emotionTime);

 if(sweet){
  // Il lecca-lecca raggiunge la bocca; ogni morso ne riduce la caramella.
  if(treatTime<5.3f){
   float enter=constrain(treatTime/1.f,0.f,1.f);
   enter=enter*enter*(3.f-2.f*enter);
   int bites=treatTime<1.f?0:min(6,int((treatTime-1.f)/.6f));
   int radius=max(0,11-bites*2);
   float exit=constrain((treatTime-4.6f)/.7f,0.f,1.f);
   int cx=109-int(enter*23),cy=104-int(enter*33)+int(exit*35);
   if(treatTime>=1.f&&treatTime<4.6f)cy+=int(sinf((treatTime-1)*PI*5)*2);
   line(cx+2,cy+4,cx+7,cy+24,1,0xFFFF);
   if(radius>0){
    ellipse(cx,cy,radius,radius,0xFFE0);
    for(int i=0;i<100;i++){
     float a=i*.19f+treatTime*2,r=i*.0095f*max(0,radius-1);
     ellipse(cx+int(cosf(a)*r),cy+int(sinf(a)*r),0,0,colors[(i/17+22)%COLOR_COUNT]);
    }
    // Impronta del morso sul lato rivolto alla bocca.
    if(bites>0)ellipse(cx-radius,cy-2,3,3,0);
    if(radius>4)ellipse(cx-2,cy-3,1,1,0xFFFF);
   }
  }
  // Cuoricini che salgono lungo i bordi senza coprire gli occhi.
  for(int i=0;i<4;i++){
   int x=i%2?148:12;
   int y=76-int(fmodf(treatTime*13+i*21,72.f));
   ellipse(x-2,y-1,2,2,0xF819);ellipse(x+2,y-1,2,2,0xF819);
   line(x-3,y,x,y+4,0,0xF819);line(x+3,y,x,y+4,0,0xF819);
  }
 }
 // Coriandoli deterministici: nessuna allocazione o attesa nel disegno.
 if(party){
  uint32_t elapsed=now-partyStarted;
  for(int i=0;i<24;i++){
   float speed=12.f+(i%7)*3.f;
   int y=int(elapsed*.001f*speed+i*17)%104-9;
   int x=(i*47+int(sinf(t*2+i)*5)+W)%W;
   // Lascia leggibili occhi e bocca: coriandoli sui bordi.
   if(y>15&&y<81&&x>17&&x<143)continue;
   uint16_t confetti=colors[i%COLOR_COUNT];
   line(x,y,x+int(sinf(t*5+i)*2),y+2,0,confetti);
  }
 }

 if((int32_t)(reactionUntil-now)>0){for(int i=0;i<9;i++){float a=i*6.283f/9+t;int r=25+(now/20+i*7)%55;int x=80+cosf(a)*r,y=43+sinf(a)*r*.55f;line(x-2,y,x+2,y,1,colors[i%COLOR_COUNT]);line(x,y-2,x,y+2,1,colors[i%COLOR_COUNT]);}}
 if(!autoPlay){ellipse(77,82,1,1,c);ellipse(82,82,1,1,c);}
 if(mood==3||mood==18){int drop=now/50%23;ellipse(69,51+drop,3,5,0x049F);ellipse(136,51+drop,3,5,0x049F);}
 if(mood==16){line(133,16,145,16,1,c);line(145,16,133,27,1,c);line(133,27,145,27,1,c);}
 if(mood==27||mood==28){for(int i=0;i<6;i++){int x=(i*29+now/35)%160;int y=(i*19+now/65)%86;ellipse(x,y,1,1,colors[(i+palette)%COLOR_COUNT]);}}
 present();
}

// Interfaccia e giochi sullo stesso raster degli occhi.
class FrameText: public Adafruit_GFX {
 public: FrameText():Adafruit_GFX(W,H){} void drawPixel(int16_t x,int16_t y,uint16_t c) override {pixel(x,y,c);}
} text;
enum Screen {EYES,MENU,SNAKE,DINO,FLAPPY,PADDLE,RELAX,METEORS,NEON,RPS};
Screen screen=EYES;
int menuIndex=0,score=0,bestSnake=0,bestDino=0,bestFlappy=0,bestPaddle=0;
bool dead=false,dirty=true,turnQueued=false;
uint32_t gameAt=0,gameStarted=0,renderAt=0;
int sx[240],sy[240],length=4,direction=0,foodX=0,foodY=0;
float dinoY=0,velocity=0,obstacleX=170;
float birdY=40,birdV=0,pipeX=165;
int gapY=40; bool pipePassed=false;
float paddleX=65,ballX=80,ballY=40,ballVX=1.5f,ballVY=1.6f;
int paddleDir=1;
Preferences records;
int jumps=0,obstacleHeight=17,obstacleWidth=9,combo=0;
bool bricks[3][10];
int stage=1; uint32_t readyUntil=0,flashUntil=0; bool savedDeath=false;
struct Spark {float x,y,vx,vy; int life; uint16_t color;};
Spark sparks[28];
void burst(float x,float y,uint16_t color){
 for(int i=0;i<28;i++){float a=i*6.283185f/28;float v=.5f+(esp_random()%20)*.08f;sparks[i]={x,y,cosf(a)*v,sinf(a)*v,24+int(esp_random()%20),color};}
}
void newBricks(){for(int y=0;y<3;y++)for(int x=0;x<10;x++)bricks[y][x]=true;}
void saveRecords(){records.putUInt("snake",bestSnake);records.putUInt("dino",bestDino);records.putUInt("fly",bestFlappy);records.putUInt("paddle",bestPaddle);}

// Poche scelte per pagina; tutti i giochi rimangono disponibili.
enum MenuPage {HOME_PAGE,GAMES_PAGE,STYLE_PAGE};
MenuPage menuPage=HOME_PAGE;
uint32_t menuFeedbackUntil=0;
int menuCount(){return menuPage==HOME_PAGE?4:menuPage==GAMES_PAGE?8:4;}
const char* homeNames[]={"OCCHI","GIOCHI","RELAX","STILE"};
const char* homeHelp[]={"80 facce animate","7 giochi da scoprire","Respira con la faccia","Colori e automatismo"};
const char* gameNames[]={"SNAKE","DINOSAURO","VOLATORE","RACCHETTA","METEORE","NEON RUN","SASSO CARTA","INDIETRO"};
const char* gameHelp[]={"Clic: svolta a destra","Clic: salto / doppio","Clic: batti le ali","Clic: inverti direzione","Clic: inverti la nave","Clic: cambia corsia","1 sasso 2 carta 3 forbici","Torna alle 4 sezioni"};
const char* styleNames[]={"COLORE","AUTOMATICO","CASUALE","INDIETRO"};
uint32_t relaxStarted=0;
int relaxScene=0;
uint32_t relaxSceneAt=0;
struct SpaceRock {float x,y,speed;int radius;bool gem;};
SpaceRock rocks[9];
float shipX=80; int shipDir=1,spaceBest=0,shield=1;
uint32_t spaceHit=0;
void resetSpace(){shipX=80;shipDir=1;shield=1;spaceHit=0;
 for(int i=0;i<9;i++)rocks[i]={float(8+esp_random()%144),float(-i*15-12),.4f+float(esp_random()%60)/100,3+int(esp_random()%3),i%4==0};
}

void label(int x,int y,const char* str,uint16_t c=0xFFFF,int size=1){text.setTextSize(size);text.setTextColor(c);text.setCursor(x,y);text.print(str);}
void arcadeFace(uint32_t now){
 text.fillRect(143,77,17,9,0);
 uint16_t c=dead?0xFB08:colors[palette];
 bool blink=now%2900<130;
 for(int x=0;x<2;x++){
  int cx=147+x*8;
  if(dead){line(cx-1,78,cx+1,80,0,c);line(cx+1,78,cx-1,80,0,c);}
  else ellipse(cx,79,1,blink?1:2,c);
 }
 for(int x=-3;x<=3;x++)pixel(151+x,(dead?84:82)+(dead?-1:1)*int(1-x*x/9.f),c);
}
void present(){
 // Piccoli rettangoli compatti: niente trasferimenti delle fasce invariate.
 // Mantiene esattamente il verso e il raster 2x esistenti.
 constexpr int BAND=8;
 static uint16_t output[320*BAND*2];
 for(int y0=0;y0<H;y0+=BAND){
  const int y1=min(H,y0+BAND);
  int left=W,right=-1,first=y1,last=-1;
  for(int y=y0;y<y1;y++){
   const int base=y*W;
   if(memcmp(frame+base,previous+base,W*sizeof(uint16_t))==0)continue;
   first=min(first,y);last=y;
   for(int x=0;x<W;x++)if(frame[base+x]!=previous[base+x]){
    left=min(left,x);right=max(right,x);
   }
  }
  if(right<left)continue;
  const int width=(right-left+1)*2;
  const int rows=(last-first+1)*2;
  for(int y=last;y>=first;y--){
   int k=(last-y)*2*width;
   for(int x=right;x>=left;x--){
    const uint16_t c=frame[y*W+x];
    output[k]=c;output[k+1]=c;
    output[k+width]=c;output[k+width+1]=c;k+=2;
   }
  }
  lcd.drawRGBBitmap((W-1-right)*2,(H-1-last)*2,output,width,rows);
  for(int y=first;y<=last;y++)
   memcpy(previous+y*W+left,frame+y*W+left,(right-left+1)*sizeof(uint16_t));
 }
}

void food(){
 bool occupied;
 do {foodX=esp_random()%26;foodY=esp_random()%10;occupied=false;
 for(int i=0;i<length;i++)if(sx[i]==foodX&&sy[i]==foodY)occupied=true;
 }while(occupied);
}
// NEON RUN: tre corsie, movimento graduale e simulazione indipendente dal rendering.
struct NeonItem {float y; int lane; bool gem;};
NeonItem neonItems[7];
int neonLane=1,neonBest=0,neonShield=1,neonChain=0;
float neonX=80; uint32_t neonTick=0,neonDamage=0;
void resetNeon(uint32_t now){
 neonLane=1;neonX=80;neonShield=1;neonChain=0;neonTick=now;neonDamage=0;
 for(int i=0;i<7;i++)neonItems[i]={float(-i*30-20),int(esp_random()%3),i%3==0};
}
void neonGame(uint32_t now){
 if(now-renderAt<FRAME_MS)return;
 float dt=min(uint32_t(40),now-neonTick)/1000.f;neonTick=now;renderAt=now;
 if(!dead&&now>=readyUntil){
  neonX+=(40+neonLane*40-neonX)*min(1.f,dt*18);
  for(auto &o:neonItems){
   o.y+=(24+min(34,score/2))*dt;
   if(o.y>61&&o.y<75&&fabsf(neonX-(40+o.lane*40))<11){
    if(o.gem){neonChain++;score+=3+min(7,neonChain);if(neonChain%4==0)neonShield=1;burst(neonX,65,0x07FF);}
    else if(now-neonDamage>1000){neonChain=0;neonDamage=now;if(neonShield){neonShield=0;burst(neonX,65,0xFFE0);}else{dead=true;burst(neonX,65,0xF800);}}
    o.y=-100; o.lane=esp_random()%3; o.gem=esp_random()%3==0;
   }
   if(o.y>89){if(!o.gem)score++;o.y=-100;o.lane=esp_random()%3;o.gem=esp_random()%3==0;}
  }
 }
 if(dead&&!savedDeath){savedDeath=true;neonBest=max(neonBest,score);records.putUInt("neon",neonBest);}
 memset(frame,0,sizeof(frame));
 for(int i=0;i<22;i++){int x=(i*73+11)%160;int y=14+(int(now/65)+i*17)%62;pixel(x,y,0x2104);}
 for(int lane=0;lane<4;lane++)for(int y=14;y<77;y+=8){int yy=14+(y-14+int(now/55))%63;line(20+lane*40,yy,20+lane*40,yy+3,0,0x3186);}
 for(auto &o:neonItems)if(o.y>17&&o.y<77){int x=40+o.lane*40,y=o.y;
  if(o.gem){text.fillTriangle(x,y-5,x-4,y,x,y+5,0x07FF);text.fillTriangle(x,y-5,x+4,y,x,y+5,0xFFFF);}
  else {text.fillRoundRect(x-8,y-4,16,8,2,0xF800);line(x-5,y-2,x+5,y-2,0,0xFD20);}
 }
 if(!dead&&(now-neonDamage>1000||now/90%2)){
  int x=neonX;uint16_t c=colors[palette];
  text.fillTriangle(x,59,x-7,72,x+7,72,c);ellipse(x-2,66,1,2,0);ellipse(x+2,66,1,2,0);
  line(x-3,74,x,76+now/70%3,1,0xFD20);
  if(neonShield)text.drawCircle(x,67,10,0x07FF);
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx;p.y+=p.vy;p.life--;}
 text.fillRect(0,0,W,13,0);char b[32];snprintf(b,sizeof(b),"NEON %d TOP %d",score,max(neonBest,score));label(2,2,b,colors[palette]);
 if(!dead&&now<readyUntil){label(46,31,"PRONTO?",0xFFFF);char n[4];snprintf(n,sizeof(n),"%d",int((readyUntil-now+499)/500));label(76,44,n,0xFFE0,2);}
 if(dead){text.fillRect(15,29,130,25,0);label(23,32,"PARTITA FINITA",0xF800);label(23,44,"CLIC: RIPROVA");}
 label(1,79,"CLIC:CORSIA LUNGO:MENU",0xBDF7);arcadeFace(now);present();
}
// Le sequenze di clic di questo gioco non attivano i gesti degli occhi.
int rpsClicks=0,rpsPlayer=-1,rpsBot=-1,rpsWins=0,rpsLosses=0,rpsOutcome=0;
uint32_t rpsClickAt=0,rpsRevealAt=0,rpsCountdownAt=0;
bool rpsCounting=false;
const char* rpsNames[]={"SASSO","CARTA","FORBICI"};
void resolveRps(uint32_t now){
 rpsCounting=false;
 // Rejection sampling: tre scelte equiprobabili, indipendenti dai clic.
 uint32_t n;do{n=esp_random();}while(n==UINT32_MAX);
 rpsBot=n%3;
 int result=(rpsPlayer-rpsBot+3)%3;
 rpsOutcome=result==0?0:result==1?1:-1;
 if(rpsOutcome>0)rpsWins++;else if(rpsOutcome<0)rpsLosses++;
 rpsRevealAt=now;
}
void rpsSymbol(int x,int y,int choice,uint16_t c){
 if(choice==0){text.fillRoundRect(x-9,y-6,18,13,3,c);for(int i=0;i<3;i++)line(x-5+i*5,y-5,x-5+i*5,y-1,0,0);}
 else if(choice==1){text.fillRect(x-8,y-9,16,19,c);for(int i=0;i<3;i++)line(x-5,y-5+i*5,x+5,y-5+i*5,0,0x4208);}
 else {text.drawCircle(x-5,y+5,4,c);text.drawCircle(x+5,y+5,4,c);line(x-3,y+2,x+7,y-9,1,c);line(x+3,y+2,x-7,y-9,1,c);}
}
void rpsGame(uint32_t now){
 if(rpsClicks&&!down&&now-rpsClickAt>550){
  rpsPlayer=rpsClicks-1;rpsClicks=0;rpsCounting=true;rpsCountdownAt=now;
 }
 if(rpsCounting&&now-rpsCountdownAt>=1800)resolveRps(now);
 if(now-renderAt<FRAME_MS)return;renderAt=now;
 memset(frame,0,sizeof(frame));
 bool result=rpsPlayer>=0&&!rpsClicks&&!rpsCounting;
 bool finished=rpsWins>=3||rpsLosses>=3;
 uint16_t c=result?(rpsOutcome>0?0x07E0:rpsOutcome<0?0xFD20:0x07FF):colors[palette];
 // Three illuminated gems make the match score readable at a glance.
 label(3,2,"TU",0xFFFF);label(72,2,"AMICO",c);
 for(int i=0;i<3;i++){
  ellipse(24+i*11,5,3,3,i<rpsWins?0x07E0:0x2104);
  ellipse(110+i*11,5,3,3,i<rpsLosses?0xFD20:0x2104);
 }
 bool blink=now%2800<110;int bob=int(sinf(now/220.f)*2);
 for(int x=120;x<=144;x+=24){
  if(result&&rpsOutcome>0)line(x-4,29+bob,x,26+bob,0,c),line(x,26+bob,x+4,29+bob,0,c);
  else {ellipse(x,28+bob,5,blink?1:7,c);if(!blink)ellipse(x-1,28+bob,2,3,0);}
 }
 for(int x=-7;x<=7;x++)pixel(132+x,43+bob+(result&&rpsOutcome<0?-1:1)*int((1-x*x/49.f)*3),c);
 if(rpsCounting){
  int beat=(now-rpsCountdownAt)/600;
  label(4,15,"SASSO CARTA FORBICI",0xBDF7);
  int bounce=int(sinf((now-rpsCountdownAt)%600/600.f*PI)*5);
  rpsSymbol(25,41-bounce,0,0xFFFF);rpsSymbol(78,41-bounce,0,c);
  char n[4];snprintf(n,sizeof(n),"%d",3-beat);label(48,32,n,0xFFE0,2);
  label(4,62,"PRONTO ALLA SFIDA?",c);
 }else if(result){
  label(5,15,"TU",0xBDF7);label(65,15,"AMICO",c);
  rpsSymbol(25,36,rpsPlayer,0xFFFF);rpsSymbol(79,36,rpsBot,c);
  label(2,50,rpsNames[rpsPlayer]);label(62,50,rpsNames[rpsBot],c);
  const char* reason=rpsOutcome==0?"STESSA SCELTA!":
    ((rpsOutcome>0?rpsPlayer:rpsBot)==0?"SASSO BATTE FORBICI":
     (rpsOutcome>0?rpsPlayer:rpsBot)==1?"CARTA BATTE SASSO":"FORBICI BATTE CARTA");
  label(2,62,finished?(rpsWins>=3?"SEI IL CAMPIONE!":"RIVINCITA?"):rpsOutcome>0?"PUNTO A TE!":rpsOutcome<0?"PUNTO ALL'AMICO!":"PAREGGIO!",c);
  label(1,78,now-rpsRevealAt<1400?reason:finished?"CLIC: NUOVA SFIDA":"CLIC: PROSSIMO TURNO",0xBDF7);
  if(rpsOutcome>0&&now-rpsRevealAt<1400){
   for(int i=0;i<12;i++){int x=(i*37+int((now-rpsRevealAt)/30))%158;
    int y=12+(i*13+int((now-rpsRevealAt)/20))%45;pixel(x,y,colors[i%COLOR_COUNT]);}
  }
 }else{
  label(2,14,"SCEGLI CON I CLIC",0xFFE0);
  for(int i=0;i<3;i++){
   int x=18+i*33;bool selected=rpsClicks==i+1;
   if(selected)text.drawRoundRect(x-14,24,29,29,3,c);
   rpsSymbol(x,38,i,selected?c:0xBDF7);
   char n[3];snprintf(n,sizeof(n),"%d",i+1);label(x-2,56,n,selected?c:0xBDF7);
  }
  if(rpsClicks){label(2,66,rpsNames[rpsClicks-1],c);
   text.fillRect(51,67,int(min(uint32_t(550),now-rpsClickAt)*56/550),3,c);}
  else label(2,66,"VINCE CHI FA 3 PUNTI",c);
  label(1,78,"1:SASSO 2:CARTA 3:FORBICI",0xBDF7);
 }
 // Same long-release gesture as the other games.
 if(down){text.fillRect(113,57,min(44,int((now-pressedAt)*44/700)),2,c);label(113,65,"MENU",0xBDF7);}
 present();
}
void startGame(Screen next,uint32_t now){
 if(next==METEORS)resetSpace();
 if(next==NEON)resetNeon(now);
 readyUntil=now+1500;savedDeath=false;stage=1;jumps=0;combo=0;newBricks();memset(sparks,0,sizeof(sparks));
 screen=next;score=0;dead=false;turnQueued=false;dirty=true;gameAt=now;gameStarted=now;
 if(next==SNAKE){length=4;direction=0;for(int i=0;i<length;i++){sx[i]=8-i;sy[i]=5;}food();}
 else if(next==PADDLE){paddleX=65;paddleDir=1;ballX=80;ballY=48;ballVX=1.5f;ballVY=1.6f;}
 else if(next==FLAPPY){birdY=40;birdV=-1.8f;pipeX=165;gapY=35+esp_random()%20;pipePassed=false;}
 else {dinoY=0;velocity=0;obstacleX=170;obstacleHeight=17;obstacleWidth=9;}
}
void shortPress(uint32_t now){
 if(screen==RPS){
  if(rpsCounting)return;
  if(rpsPlayer>=0&&!rpsClicks){
   if(now-rpsRevealAt<1400)return;
   if(rpsWins>=3||rpsLosses>=3){rpsWins=0;rpsLosses=0;}
   rpsPlayer=-1;rpsBot=-1;return;
  }
  rpsClicks=rpsClicks%3+1;rpsClickAt=now;
 }
 else if(screen==MENU){menuIndex=(menuIndex+1)%menuCount();dirty=true;}
 else if(screen==NEON){if(dead)startGame(NEON,now);else neonLane=(neonLane+1)%3;}
 else if(screen==METEORS){if(dead)startGame(METEORS,now);else shipDir=-shipDir;}
 else if(screen==RELAX){relaxScene=(relaxScene+1)%4;relaxSceneAt=now;}
 else if(screen==SNAKE){if(dead)startGame(SNAKE,now);else turnQueued=true;}
 else if(screen==PADDLE){if(dead)startGame(PADDLE,now);else paddleDir=-paddleDir;}
 else if(screen==DINO){if(dead)startGame(DINO,now);else if(jumps<2){velocity=jumps==0?5.5f:4.6f;jumps++;burst(25,65-dinoY,0xFFE0);}}
 else if(screen==FLAPPY){if(dead)startGame(FLAPPY,now);else birdV=-2.7f;}
 else {clicks++;clickAt=now;}
}
void longPress(uint32_t now){
 clicks=0;rpsClicks=0;
 if(screen!=MENU){
  Screen old=screen;screen=MENU;
  if(old==EYES){menuPage=HOME_PAGE;menuIndex=0;}
  else if(old==RELAX){menuPage=HOME_PAGE;menuIndex=2;}
  else {menuPage=GAMES_PAGE;menuIndex=old==SNAKE?0:old==DINO?1:old==FLAPPY?2:old==PADDLE?3:old==METEORS?4:old==NEON?5:6;}
  dirty=true;return;
 }
 if(menuPage==HOME_PAGE){
  if(menuIndex==0){screen=EYES;changeAt=now+6000;}
  else if(menuIndex==1){menuPage=GAMES_PAGE;menuIndex=0;}
  else if(menuIndex==2){screen=RELAX;relaxStarted=now;relaxSceneAt=now;nextFrame=now;}
  else {menuPage=STYLE_PAGE;menuIndex=0;}
 }else if(menuPage==GAMES_PAGE){
  if(menuIndex==7){menuPage=HOME_PAGE;menuIndex=1;}
  else if(menuIndex==6){screen=RPS;rpsCounting=false;rpsClicks=0;rpsPlayer=-1;rpsBot=-1;rpsWins=0;rpsLosses=0;rpsOutcome=0;}
  else {const Screen games[]={SNAKE,DINO,FLAPPY,PADDLE,METEORS,NEON};startGame(games[menuIndex],now);}
 }else{
  if(menuIndex==0)palette=(palette+1)%COLOR_COUNT;
  else if(menuIndex==1)autoPlay=!autoPlay;
  else if(menuIndex==2){palette=esp_random()%COLOR_COUNT;selectMood(randomMood(),now);}
  else {menuPage=HOME_PAGE;menuIndex=3;}
  menuFeedbackUntil=now+900;
 }
 dirty=true;
}
void button(uint32_t now){
 bool v=digitalRead(9)==LOW;
 if(v!=rawDown){rawDown=v;edgeAt=now;}
 if(now-edgeAt>=30&&down!=rawDown){down=rawDown;
 if(down)pressedAt=now;
 else if(now-pressedAt>=700)longPress(now);
 else shortPress(now);
 }
 if(screen==EYES&&clicks&&!down&&now-clickAt>280){
 if(clicks>=3){offendedUntil=0;partyUntil=0;reactionUntil=0;treatStarted=now;treatUntil=now+8400;selectMood(1,now);changeAt=treatUntil;}
 else if(clicks>=2){
  if(!offendedUntil)moodBeforeOffense=mood;
  treatUntil=0;offenseStarted=now;offendedUntil=now+1200+6000;partyUntil=0;reactionUntil=0;
  selectMood(2,now);changeAt=offendedUntil;
 }
 else if(!offendedUntil&&!treatUntil)selectMood((mood+1)%COUNT,now);
 clicks=0;
 }
}
void drawMenu(){
 memset(frame,0,sizeof(frame));
 uint32_t now=millis();uint16_t c=colors[palette];
 label(4,2,menuPage==HOME_PAGE?"SCEGLI":menuPage==GAMES_PAGE?"GIOCHI":"STILE",c);
 char counter[12];snprintf(counter,sizeof(counter),"%d / %d",menuIndex+1,menuCount());label(119,2,counter,0xBDF7);
 text.drawRoundRect(2,13,156,47,5,0x3186);
 // Faccia dedicata, lontana dal testo: resta animata in ogni sezione.
 int bob=int(sinf(now/270.f)*1.5f);bool blink=now%3100<120;
 for(int x=126;x<=144;x+=18){ellipse(x,28+bob,5,blink?1:6,c);if(!blink)ellipse(x+int(sinf(now/900.f)*2),28+bob,2,3,0);}
 for(int x=-5;x<=5;x++)pixel(135+x,41+bob+int(2*(1-x*x/25.f)),c);
 const char* name=menuPage==HOME_PAGE?homeNames[menuIndex]:menuPage==GAMES_PAGE?gameNames[menuIndex]:styleNames[menuIndex];
 label(8,21,name,c);
 const char* help=menuPage==HOME_PAGE?homeHelp[menuIndex]:menuPage==GAMES_PAGE?gameHelp[menuIndex]:menuIndex==0?"Conferma: nuovo colore":menuIndex==1?(autoPlay?"Attivo: cambia da solo":"In pausa: scegli tu"):menuIndex==2?"Sorteggia faccia e tinta":"Torna alle 4 sezioni";
 label(8,49,help,0xBDF7);
 for(int i=0;i<menuCount();i++)text.fillRoundRect(80-menuCount()*5+i*10,63,7,3,1,i==menuIndex?c:0x2104);
 bool ready=down&&now-pressedAt>=700;
 label(5,73,ready?"RILASCIA PER SCEGLIERE":now<menuFeedbackUntil?"FATTO! TIENI: RIPETI":"CLIC: AVANTI  TIENI: OK",ready?0x07E0:0xBDF7);
 if(down)text.fillRect(0,83,min(160,int((now-pressedAt)*160/700)),3,ready?0x07E0:c);
 present();dirty=false;
}
// Relax: ambienti silenziosi, senza lampeggi e senza pause del respiro.
uint16_t relaxMix(uint16_t a,uint16_t b,float u){
 u=constrain(u,0.f,1.f);
 int r=((a>>11)&31)*(1-u)+((b>>11)&31)*u;
 int g=((a>>5)&63)*(1-u)+((b>>5)&63)*u;
 int bl=(a&31)*(1-u)+(b&31)*u;
 return (r<<11)|(g<<5)|bl;
}
void drawRelax(uint32_t now){
 uint32_t elapsed=now-relaxStarted,phase=elapsed%10000;
 bool inhale=phase<4000;
 float u=inhale?phase/4000.f:1.f-(phase-4000)/6000.f;
 float ease=.5f-.5f*cosf(u*PI),t=elapsed/1000.f;
 const uint16_t sky[]={0x0804,0x0005,0x0023,0x1805};
 const uint16_t glow[]={0xC65F,0x7FF8,0xBFF0,0xFCB2};
 const uint16_t floorColor[]={0x210A,0x012A,0x1145,0x38C8};
 const char* names[]={"CIELO STELLATO","OCEANO CALMO","BOSCO MAGICO","TRAMONTO"};
 uint16_t c=glow[relaxScene],bg=sky[relaxScene];
 for(int y=0;y<H;y++){
  uint16_t shade=relaxMix(bg,floorColor[relaxScene],float(y)/H*.65f);
  for(int x=0;x<W;x++)frame[y*W+x]=shade;
 }
 // Piccoli punti di luce: pulsazioni lente, mai flash.
 for(int i=0;i<23;i++){
  int x=(i*47+11)%158+1,y=14+(i*19)%47;
  float shimmer=.18f+.22f*(.5f+.5f*sinf(t*.6f+i*2.f));
  if(relaxScene==2){x+=int(sinf(t*.22f+i)*3);y+=int(sinf(t*.35f+i)*3);}
  pixel(x,y,relaxMix(bg,c,shimmer));
 }
 if(relaxScene==0){
  ellipse(142,23,7,7,relaxMix(bg,c,.65f));ellipse(145,20,7,7,bg);
 }else if(relaxScene==3){
  ellipse(137,39,12,12,relaxMix(bg,c,.4f));
 }
 // Onde, colline e dune scivolano lentamente dietro alla faccia.
 for(int k=0;k<3;k++)for(int x=0;x<W;x++){
  int y=57+k*5+int(sinf(x*(relaxScene==2?.035f:.055f)+t*(.24f+k*.04f)+k*1.9f)*(2+k));
  pixel(x,y,relaxMix(bg,c,.12f+k*.07f));
 }
 // Un alone respira insieme alla faccina, con espansione sinusoidale.
 int cy=38-int(ease*2),radius=19+int(ease*7);
 for(int ring=0;ring<3;ring++){
  uint16_t rc=relaxMix(bg,c,.12f+ring*.12f);
  text.drawCircle(80,38,radius+6-ring*3,rc);
 }
 // Occhi chiusi, guance delicate e sorriso sincronizzato.
 uint16_t face=relaxMix(c,0xFFFF,.22f);
 int spread=11+int(ease*2);
 for(int side=-1;side<=1;side+=2){
  int cx=80+side*spread;
  for(int x=-5;x<=5;x++){
   int y=cy-3+int((1-x*x/25.f)*2);
   pixel(cx+x,y,face);pixel(cx+x,y+1,face);
  }
  ellipse(cx+side*2,cy+6,3,1,relaxMix(bg,0xF9B0,.38f));
 }
 for(int x=-6;x<=6;x++)pixel(80+x,cy+9+int((1-x*x/36.f)*(2+ease)),face);
 // Petali / gocce lungo il cerchio: indicano il movimento senza contatori urgenti.
 for(int i=0;i<8;i++){
  float a=i*PI/4-PI/2;
  int r=radius+5;
  ellipse(80+int(cosf(a)*r),38+int(sinf(a)*r),1,1,relaxMix(bg,c,.35f+.25f*ease));
 }
 label(inhale?49:52,2,inhale?"INSPIRA  4s":"ESPIRA  6s",face);
 // Barra che sale inspirando e scende espirando, non azzerata di colpo.
 text.fillRoundRect(48,67,64,3,1,relaxMix(bg,c,.15f));
 int width=int(ease*64);
 if(width)text.fillRoundRect(48,67,width,3,1,c);
 char info[28];
 if(now-relaxSceneAt<2400){
  snprintf(info,sizeof(info),"%d/4 %s",relaxScene+1,names[relaxScene]);
 }else if(elapsed<10000){snprintf(info,sizeof(info),"SENZA SFORZO, CON CALMA");}
 else {snprintf(info,sizeof(info),"%02lu:%02lu  %lu RESPIRI",(unsigned long)(elapsed/60000),(unsigned long)(elapsed/1000%60),(unsigned long)(elapsed/10000));}
 label((W-int(strlen(info))*6)/2,73,info,relaxMix(bg,c,.8f));
 // Aiuto iniziale, poi un invito discreto; tenere premuto mostra l'uscita.
 if(down){
  text.fillRect(0,73,W,13,bg);
  bool ready=now-pressedAt>=700;
  label(ready?23:32,75,ready?"RILASCIA: MENU":"TIENI PER USCIRE",face);
  text.fillRect(0,84,min(160,int((now-pressedAt)*160/700)),2,c);
 }else if(elapsed<5500){label(5,81,"CLIC:SCENA  TIENI:MENU",relaxMix(bg,c,.6f));}
 present();
}
// Gioco autonomo: passo a 16 ms, nessuna attesa o modifica al display.
void spaceGame(uint32_t now){
 if(down){gameAt=now;return;}
 if(now-gameAt<FRAME_MS)return;
 gameAt=now;
 if(!dead&&now>=readyUntil){
  shipX+=shipDir*1.15f;
  if(shipX<7){shipX=7;shipDir=1;}if(shipX>152){shipX=152;shipDir=-1;}
  for(auto &r:rocks){
   r.y+=r.speed+min(1.6f,score*.012f);
   if(fabsf(r.x-shipX)<r.radius+5&&fabsf(r.y-65)<r.radius+5){
    if(r.gem){score+=10;shield=1;burst(r.x,r.y,0x07FF);}
    else if(now-spaceHit>1300){
     burst(shipX,65,0xFD20);
     if(shield){shield=0;spaceHit=now;}else dead=true;
    }
    r.y=100;
   }
   if(r.y>85){if(!r.gem&&!dead)score++;r.x=8+esp_random()%144;r.y=-12-esp_random()%45;r.gem=esp_random()%5==0;}
  }
 }
 if(dead&&!savedDeath){savedDeath=true;spaceBest=max(score,spaceBest);records.putUInt("space",spaceBest);}
 memset(frame,0,sizeof(frame));
 for(int i=0;i<35;i++){int x=(i*47+13)%W,y=13+(i*23+now/(i%3?65:30))%62;pixel(x,y,i%3?0x4208:0xBDF7);}
 char info[32];snprintf(info,sizeof(info),"METEORE %d TOP %d",score,max(score,spaceBest));label(1,1,info,0x07FF);
 for(auto &r:rocks){int x=r.x,y=r.y;if(y<16||y>75)continue;
  if(r.gem){text.fillTriangle(x,y-4,x-4,y,x,y+4,0x07FF);text.fillTriangle(x,y-4,x+4,y,x,y+4,0x07FF);pixel(x,y-1,0xFFFF);}
  else{ellipse(x,y,r.radius,r.radius,0x8410);ellipse(x-1,y-1,2,1,0xBDF7);pixel(x+1,y+2,0x4208);}
 }
 int x=shipX;
 if(dead||now-spaceHit>1300||now/90%2){
 text.fillTriangle(x,58,x-6,71,x+6,71,colors[palette]);
 text.fillTriangle(x,61,x-2,66,x+2,66,0xFFFF);
 line(x-2,72,x,74+now/80%3,0,0xFD20);
 if(shield){text.drawCircle(x,65,10,0x03EF);}
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx*.5f;p.y+=p.vy*.5f;p.life--;}
 if(!dead&&now<readyUntil){label(28,33,"EVITA LE METEORE",0xFFE0);label(28,44,"PRENDI I DIAMANTI",0x07FF);}
 if(dead){text.fillRect(12,27,136,29,0);label(24,31,"PARTITA FINITA");label(20,44,"CLIC: RIPROVA");}
 label(1,78,"CLIC:GIRA LUNGO:MENU",0xBDF7);arcadeFace(now);present();
}
void games(uint32_t now){
 if(down){gameAt=now;return;} // Una pressione lunga non provoca mosse involontarie.
 uint32_t interval=screen==SNAKE?max(85,240-score*6):FRAME_MS;
 if(!dead&&now>=readyUntil&&now-gameAt>=interval){gameAt+=interval;if(now-gameAt>interval*3)gameAt=now;dirty=true;
 if(screen==SNAKE){
 if(turnQueued){direction=(direction+1)%4;turnQueued=false;}
 const int dx[]={1,0,-1,0},dy[]={0,1,0,-1};
 int x=sx[0]+dx[direction],y=sy[0]+dy[direction];
 bool eat=x==foodX&&y==foodY;
 if(x<0||x>=26||y<0||y>=10)dead=true;
 for(int i=0;i<length-(eat?0:1);i++)if(sx[i]==x&&sy[i]==y)dead=true;
 if(!dead){if(eat){length++;score++;bestSnake=max(score,bestSnake);burst(3+x*6,15+y*6,0xFFE0);flashUntil=now+130;}
 for(int i=length-1;i>0;i--){sx[i]=sx[i-1];sy[i]=sy[i-1];}sx[0]=x;sy[0]=y;
 if(length>=240)dead=true;else if(eat)food();}
 }else if(screen==PADDLE){
 paddleX=constrain(paddleX+paddleDir*2.5f*GAME_STEP,0.f,130.f);
 ballX+=ballVX*GAME_STEP;ballY+=ballVY*GAME_STEP;
 if(ballX<3){ballX=3;ballVX=fabsf(ballVX);}
 if(ballX>156){ballX=156;ballVX=-fabsf(ballVX);}
 if(ballY<15){ballY=15;ballVY=fabsf(ballVY);}
 bool hit=false;
 for(int r=0;r<3&&!hit;r++)for(int c=0;c<10&&!hit;c++)if(bricks[r][c]&&ballX+2>=c*16+1&&ballX-2<=c*16+14&&ballY+2>=18+r*7&&ballY-2<=23+r*7){
 bricks[r][c]=false;ballVY=-ballVY;score+=5;bestPaddle=max(score,bestPaddle);burst(ballX,ballY,colors[(r+stage)%COLOR_COUNT]);hit=true;}
 bool any=false;for(int r=0;r<3;r++)for(int c=0;c<10;c++)any|=bricks[r][c];
 if(!any){stage++;newBricks();ballX=80;ballY=49;ballVY=1.6f;burst(80,40,0xFFE0);}

 if(ballVY>0&&ballY>=67&&ballY<=73&&ballX>=paddleX-2&&ballX<=paddleX+32){
 ballY=67;score++;bestPaddle=max(score,bestPaddle);
 ballVY=-min(3.6f,1.6f+score*.1f);
 ballVX=constrain((ballX-(paddleX+15))*.17f,-2.7f,2.7f);
 }
 if(ballY>76)dead=true;
 }else if(screen==FLAPPY){
 birdV+=.19f*GAME_STEP;birdY+=birdV*GAME_STEP;
 pipeX-=(1.8f+min(1.2f,score*.06f))*GAME_STEP;
 if(!pipePassed&&pipeX+15<30){pipePassed=true;score++;bestFlappy=max(score,bestFlappy);burst(33,birdY,0xFFE0);}
 if(pipeX < -16){pipeX=165;gapY=32+esp_random()%24;pipePassed=false;}
 if(birdY-3<12||birdY+3>73)dead=true;
 if(pipeX<37&&pipeX+15>29&&(birdY-3<gapY-max(12,18-score/8)||birdY+3>gapY+max(12,18-score/8)))dead=true;
 }else{
 dinoY=max(0.f,dinoY+velocity*GAME_STEP);velocity-=.42f*GAME_STEP;if(dinoY==0){velocity=0;jumps=0;}
 obstacleX-=(2.4f+min(2.5f,score*.13f))*GAME_STEP;
 if(obstacleX < -12){obstacleX=165+esp_random()%65;obstacleHeight=12+esp_random()%12;obstacleWidth=7+esp_random()%6;score++;bestDino=max(score,bestDino);burst(25,55,0xFFE0);}
 if(obstacleX<34&&obstacleX+obstacleWidth>20&&dinoY<obstacleHeight-1)dead=true;
 }
 }
 if(!dead&&now<readyUntil)dirty=true;
 if(dead&&!savedDeath){savedDeath=true;saveRecords();burst(80,40,0xF800);}
 bool particles=false;for(auto &p:sparks)if(p.life>0)particles=true;
 if((particles||screen==SNAKE)&&!dead&&now-renderAt>=FRAME_MS)dirty=true;
 if(particles&&now-renderAt>=FRAME_MS)dirty=true;
 if(!dirty||now-renderAt<FRAME_MS)return;
 renderAt=now;
 memset(frame,0,sizeof(frame));char buf[32];snprintf(buf,sizeof(buf),"%s %d  TOP %d",screen==SNAKE?"SNAKE":screen==FLAPPY?"VOLO":screen==PADDLE?"RACC.":"DINO",score,screen==SNAKE?bestSnake:screen==FLAPPY?bestFlappy:screen==PADDLE?bestPaddle:bestDino);label(2,1,buf,colors[palette]);
 if(screen==SNAKE){
 text.drawRect(0,12,158,62,0x4208);
 for(int y=0;y<10;y++)for(int x=0;x<26;x++)if((x+y)%2==0)text.fillRect(1+x*6,13+y*6,6,6,0x0821);
 for(int i=length-1;i>=0;i--)text.fillRect(1+sx[i]*6,13+sy[i]*6,5,5,i==0?0xFFFF:colors[palette]);
 ellipse(3+foodX*6,15+foodY*6,2,2,now/200%2?0xF800:0xFD20);
 pixel(4+foodX*6,12+foodY*6,0x07E0);
 const int ex[]={1,0,-1,0},ey[]={0,1,0,-1};pixel(3+sx[0]*6+ex[direction],15+sy[0]*6+ey[direction],0);
 }else if(screen==PADDLE){
 text.drawRect(0,12,160,65,0x4208);
 for(int r=0;r<3;r++)for(int c=0;c<10;c++)if(bricks[r][c]){text.fillRoundRect(c*16+1,18+r*7,14,5,1,colors[(r+stage)%COLOR_COUNT]);text.drawFastHLine(c*16+3,18+r*7,9,0xFFFF);}
 line(ballX-ballVX*2,ballY-ballVY*2,ballX,ballY,0,0x8410);
 text.fillRoundRect((int)paddleX,70,30,4,2,colors[palette]);
 ellipse((int)ballX,(int)ballY,2,2,0xFFFF);
 }else if(screen==FLAPPY){
 text.drawFastHLine(0,12,160,0x4208);text.drawFastHLine(0,74,160,0x4208);
 for(int i=0;i<5;i++){int cx=(i*39+320-int(now/90)%195)%195-15;ellipse(cx,24+(i%3)*10,9,3,0x18C3);}
 int px=(int)pipeX; int gap=max(12,18-score/8);
 text.fillRect(px,13,15,max(0,gapY-gap-13),0x07E0);
 text.fillRect(px,gapY+max(12,18-score/8),15,max(0,74-gapY-gap),0x07E0);
 text.fillRect(px-2,gapY-gap-3,19,3,0x07E0);
 text.fillRect(px-2,gapY+max(12,18-score/8),19,3,0x07E0);
 ellipse(33,(int)birdY,4,3,colors[palette]);
 text.fillTriangle(37,birdY,41,birdY+1,37,birdY+2,0xFD20);pixel(35,(int)birdY-1,0);line(29,(int)birdY,31,(int)birdY+2,0,0xFFFF);
 }else{
 for(int i=0;i<4;i++){int cx=(i*51+320-int(now/120)%204)%204-18;ellipse(cx,24+i%2*9,10,3,0x2945);}
 line(0,68,159,68,0,0x7BEF);
 int y=66-(int)dinoY;
 text.fillRect(20,y-14,10,12,colors[palette]);text.fillRect(26,y-22,11,10,colors[palette]);pixel(33,y-20,0);
 line(20,y-9,15,y-13,1,colors[palette]);
 int stride=dead?0:(now/100)%2*3;
 text.fillRect(21,y-2,3,3+stride,colors[palette]);text.fillRect(28,y-2,3,6-stride,colors[palette]);
 text.fillRect((int)obstacleX,68-obstacleHeight,obstacleWidth,obstacleHeight,0x07E0);
 line(obstacleX-3,57,obstacleX+obstacleWidth+3,57,1,0x07E0);
 pixel(obstacleX+2,70-obstacleHeight,0xBFF7);
 for(int i=0;i<5;i++)pixel((i*37+320-(now/22)%160)%160,72,0x7BEF);
 }
 for(auto &p:sparks)if(p.life>0){pixel(p.x,p.y,p.color);p.x+=p.vx*GAME_STEP;p.y+=p.vy*GAME_STEP;p.vy+=.06f*GAME_STEP;p.life--;dirty=true;}
 if(!dead&&now<readyUntil){text.fillRoundRect(43,28,74,26,4,0);text.drawRoundRect(43,28,74,26,4,colors[palette]);char count[4];snprintf(count,sizeof(count),"%d",int((readyUntil-now+499)/500));label(75,33,count,0xFFE0,2);}
 if(dead){text.fillRect(12,27,136,29,0);text.drawRect(12,27,136,29,colors[palette]);label(24,31,"PARTITA FINITA");label(20,44,"CLIC: RIPROVA");}
 label(1,78,screen==SNAKE?"CLIC:DESTRA LUNGO:MENU":screen==PADDLE?"CLIC:GIRA LUNGO:MENU":screen==FLAPPY?"CLIC:VOLA LUNGO:MENU":"CLIC:2 SALTI LUNGO:MENU",0xBDF7);
 arcadeFace(now);present();dirty=false;
}

void setup(){
 // BOOT: ingresso soltanto, mai pilotare il pin di avvio.
 text.setTextWrap(false);
 records.begin("arcade",false);
 spaceBest=records.getUInt("space",0);
 neonBest=records.getUInt("neon",0);
 bestSnake=records.getUInt("snake",0);bestDino=records.getUInt("dino",0);bestFlappy=records.getUInt("fly",0);bestPaddle=records.getUInt("paddle",0);
 pinMode(9,INPUT_PULLUP);
 pinMode(22,OUTPUT);digitalWrite(22,LOW);
 pinMode(4,OUTPUT);digitalWrite(4,HIGH);
 SPI.begin(7,-1,6,14);
 lcd.init(172,320);lcd.setRotation(1);lcd.invertDisplay(true);lcd.fillScreen(0);
 memset(previous,0xFF,sizeof(previous));analogWrite(22,102);
 changeAt=millis()+3500;
}
void loop(){uint32_t now=millis();
 if(offendedUntil && (int32_t)(now-offendedUntil)>=0){offendedUntil=0;selectMood(moodBeforeOffense,now);}
 if(treatUntil && (int32_t)(now-treatUntil)>=0){treatUntil=0;selectMood(1,now);}
 button(now);
 if(screen==EYES){
 if(autoPlay&&!offendedUntil&&!treatUntil&&(int32_t)(now-changeAt)>=0&&!down&&!clicks)selectMood(randomMood(),now);
 if((int32_t)(now-blinkAt)>=0){blinkStart=now;blinkAt=now+2200+esp_random()%2600;}
 if((int32_t)(now-nextFrame)>=0){nextFrame+=FRAME_MS;if((int32_t)(now-nextFrame)>int32_t(FRAME_MS*3))nextFrame=now+FRAME_MS;draw(now);}
 }else if(screen==RELAX){if((int32_t)(now-nextFrame)>=0){nextFrame=now+FRAME_MS;drawRelax(now);}}
 else if(screen==MENU){if(dirty||now-renderAt>=FRAME_MS){renderAt=now;drawMenu();}}
 else if(screen==RPS)rpsGame(now);
 else if(screen==NEON)neonGame(now);
 else if(screen==METEORS)spaceGame(now);
 else games(now);
 delay(1);
}

Download project files

Remix this project

Make it yours in one click

Open a full copy of this project in your own Schematik workspace, diagram, code, parts, and assembly steps included. Swap the sensor, add features, or redesign the whole thing with AI. The author's original stays untouched.

Open in Schematik