Case study: creare il doodle di Google dedicato a Stanisław Lem

Marcin Wichary
Marcin Wichary

Ciao, (strano) mondo

La home page di Google è un ambiente affascinante per scrivere codice. Ha molte limitazioni impegnative: particolare attenzione a velocità e latenza, dover soddisfare tutti i tipi di browser e funzionare in varie circostanze e… sì, sorprendere e deliziare.

Mi riferisco ai doodle di Google, le illustrazioni speciali che a volte sostituiscono il nostro logo. Anche se il mio rapporto con matite e pennelli ha da tempo il sapore distintivo di un'ingiunzione di allontanamento, spesso contribuisco a quelli interattivi.

Ogni doodle interattivo che ho codificato (Pac-Man, Jules Verne, Fiera mondiale) e molti altri a cui ho collaborato erano allo stesso tempo futuristici e anacronistici: grandi opportunità per applicazioni utopiche di funzionalità web all'avanguardia e pragmatismo ostinato della compatibilità tra browser.

Impariamo molto da ogni doodle interattivo e il recente minigioco di Stanisław Lem non ha fatto eccezione, con le sue 17.000 righe di codice JavaScript che hanno provato molte cose per la prima volta nella storia dei doodle. Oggi voglio condividere questo codice con te, forse troverai qualcosa di interessante o mi farai notare i miei errori, e parlarne un po'.

Visualizza il codice del doodle di Stanisław Lem »

È importante ricordare che la home page di Google non è un luogo per dimostrazioni tecniche. Con i nostri doodle vogliamo celebrare persone e avvenimenti specifici e farlo utilizzando le migliori tecniche artistiche e le migliori tecnologie a nostra disposizione, ma senza mai celebrare la tecnologia per se stessa. Ciò significa esaminare attentamente la parte di HTML5 disponibile e capire se ci aiuta a migliorare il doodle senza distogliere l'attenzione o metterlo in ombra.

Vediamo alcune delle moderne tecnologie web che hanno trovato un loro posto nel doodle di Stanisław Lem, e altre che non ce l'hanno fatto.

Grafica tramite DOM e canvas

Canvas è un servizio potente, creato esattamente per il tipo di cose che volevamo fare in questo doodle. Tuttavia, alcuni dei browser meno recenti che ci interessavano non lo supportavano e, anche se condivido letteralmente un ufficio con la persona che ha creato un excanvas altrimenti eccellente, ho deciso di scegliere un altro metodo.

Ho creato un motore grafico che esegue l'astrazione delle primitive grafiche chiamate "rettangoli" e poi le esegue utilizzando canvas o DOM se canvas non è disponibile.

Questo approccio presenta alcune interessanti sfide: ad esempio, spostare o modificare un oggetto nel DOM ha conseguenze immediate, mentre per la tela c'è un momento specifico in cui tutto viene disegnato contemporaneamente. Ho deciso di avere un solo canvas, cancellarlo e disegnare da zero con ogni frame. Da un lato, troppi elementi in movimento e dall'altro, una complessità non sufficiente per giustificare la suddivisione in più canvas sovrapposti e il loro aggiornamento selettivo.

Purtroppo, il passaggio a canvas non è semplice come fare il mirroring degli sfondi CSS con drawImage(): perdi una serie di elementi che sono disponibili senza costi quando li metti insieme tramite DOM, in particolare il layering con gli indici z e gli eventi del mouse.

Ho già astratto l'indice z con un concetto chiamato "piani". Il scribble ha definito un numero di piani, dallo cielo lontano in fondo, al cursore del mouse davanti a tutto, e ogni attore all'interno del doodle doveva decidere a quale apparteneva (erano possibili piccole correzioni più/meno all'interno di un piano utilizzando planeCorrection).

Durante il rendering tramite DOM, i piani vengono semplicemente tradotti in z-index. Tuttavia, se eseguiamo il rendering tramite canvas, dobbiamo ordinare i rettangoli in base ai loro piani prima di disegnarli. Poiché è costoso eseguire questa operazione ogni volta, l'ordine viene ricalcolato solo quando viene aggiunto un attore o quando si sposta in un altro piano.

Ho astratto anche gli eventi del mouse… in un certo senso. Sia per DOM che per canvas, ho utilizzato elementi DOM aggiuntivi completamente trasparenti e in primo piano con un valore z-index elevato, la cui funzione è solo reagire al passaggio del mouse sopra/sotto, ai clic e ai tocchi.

Una delle cose che volevamo provare con questo doodle era rompere la quarta parete. Il motore sopra indicato ci ha permesso di combinare gli attori basati su canvas con gli attori basati su DOM. Ad esempio, le esplosioni nel finale sono sia nel canvas per gli oggetti nell'universo sia nel DOM per il resto della home page di Google. L'uccello, che normalmente vola e viene tagliato dalla nostra maschera frastagliata come qualsiasi altro attore, decide di stare lontano dai guai durante il livello di ripresa e si siede sul pulsante Fatti tentare dalla fortuna. Il modo in cui viene eseguito è che l'uccello esce dalla tela e diventa un elemento DOM (e viceversa in un secondo momento), il che speravo fosse completamente trasparente per i nostri visitatori.

La frequenza fotogrammi

Conoscere la frequenza fotogrammi corrente e reagire quando è troppo lenta (e anche troppo veloce) è stata una parte importante del nostro motore. Poiché i browser non riportano la frequenza frame, dobbiamo calcolarla noi stessi.

Ho iniziato a utilizzare requestAnimationFrame, ricorrendo al metodo antiquato setTimeout se il primo non era disponibile. requestAnimationFrame risparmia in modo intelligente la CPU in alcune situazioni, anche se lo facciamo anche noi, come spiegato di seguito, ma ci consente anche di ottenere una frequenza frame superiore a setTimeout.

Il calcolo della frequenza frame corrente è semplice, ma è soggetto a variazioni drastiche, ad esempio può diminuire rapidamente quando un'altra applicazione occupa il computer per un po' di tempo. Pertanto, calcoliamo una frequenza frame "cumulativa" (mediata) solo ogni 100 tick fisici e prendiamo decisioni in base a questo valore.

Di che decisioni si tratta?

  • Se la frequenza fotogrammi è superiore a 60 fps, la riduciamo. Al momento, requestAnimationFrame su alcune versioni di Firefox non ha un limite superiore alla frequenza frame e non ha senso sprecare la CPU. Tieni presente che il limite è in realtà di 65 fps, a causa degli errori di arrotondamento che fanno aumentare la frequenza fotogrammi di poco più di 60 fps su altri browser. Non vogliamo iniziare a limitare la frequenza fotogrammi per errore.

  • Se la frequenza fotogrammi è inferiore a 10 fps, rallentiamo semplicemente il motore anziché eliminare i fotogrammi. È una proposta che non fa bene a nessuno, ma ho ritenuto che saltare frame in modo eccessivo sarebbe stato più confuso che avere semplicemente un gioco più lento (ma comunque coerente). C'è un altro piacevole effetto collaterale: se il sistema rallenta temporaneamente, l'utente non noterà un brusco salto in avanti mentre il motore cerca disperatamente di recuperare. (Per Pac-Man ho proceduto in modo leggermente diverso, ma la frequenza fotogrammi minima è un approccio migliore).

  • Infine, possiamo pensare di semplificare la grafica quando la frequenza frame diventa pericolosamente bassa. Non lo faremo per il doodle di Lem, ad eccezione del cursore del mouse (di seguito sono riportate maggiori informazioni), ma ipotizziamo che potremmo perdere alcune animazioni estranee per rendere il doodle fluido anche su computer più lenti.

Abbiamo anche il concetto di un tick fisico e di un tick logico. Il primo proviene da requestAnimationFrame/setTimeout. Il rapporto nel gameplay normale è 1:1, ma per l'avanzamento veloce, basta aggiungere più tic logici per un tic fisico (fino a 1:5). In questo modo, possiamo eseguire tutti i calcoli necessari per ogni tick logico, ma designare solo l'ultimo come quello che aggiorna gli elementi sullo schermo.

Benchmarking

Si può presumere (e infatti all'inizio è stato così) che Canvas sia più veloce del DOM ogni volta che è disponibile. Non è sempre così. Durante il testing, abbiamo scoperto che Opera 10.0-10.1 su Mac e Firefox su Linux sono in realtà più veloci quando si spostano gli elementi DOM.

In un mondo perfetto, il doodle eseguirà in silenzio il benchmark di diverse tecniche grafiche: elementi DOM spostati utilizzando style.left e style.top, disegno su canvas e forse persino elementi DOM spostati utilizzando le trasformazioni CSS3.

e poi passa a quello che ha generato la frequenza frame più alta. Ho iniziato a scrivere codice per questo, ma ho scoperto che almeno il mio modo di eseguire il benchmarking era piuttosto inaffidabile e richiedeva molto tempo. Tempo che non abbiamo sulla nostra home page: ci preme molto la velocità e vogliamo che il doodle venga visualizzato immediatamente e che il gameplay inizi non appena fai clic o tocchi.

Alla fine, lo sviluppo web a volte si riduce a dover fare ciò che devi fare. Ho guardato dietro di me per assicurarmi che nessuno mi stesse guardando, poi ho semplicemente rimosso Opera 10 e Firefox dal canvas tramite codice rigido. Nella prossima vita, tornerò come <marquee> tag.

Risparmiare CPU

Conosci quell'amico che viene a casa tua, guarda la finale di stagione di Breaking Bad, te la rovina e poi la elimina dal tuo DVR? Non vuoi essere quel tipo, vero?

Quindi, sì, la peggiore analogia di sempre. Tuttavia, non vogliamo che il nostro doodle sia così: il fatto che ci sia consentito accedere alla scheda del browser di qualcuno è un privilegio e accumulare cicli della CPU o distrarre l'utente ci farebbe diventare degli ospiti sgradevoli. Pertanto, se nessuno gioca con il doodle (nessun tocco, clic del mouse, movimenti del mouse o pressioni dei tasti), vogliamo che infine entri in modalità di sospensione.

Quando?

  • dopo 18 secondi nella home page (nei giochi arcade questa fase è chiamata modalità Attract)
  • dopo 180 secondi se la scheda è attiva
  • dopo 30 secondi se la scheda non è attiva (ad es. l'utente è passato a un'altra finestra, ma forse sta ancora guardando il doodle in una scheda non attiva)
  • Immediatamente se la scheda diventa invisibile (ad es. l'utente è passato a un'altra scheda nella stessa finestra: non ha senso sprecare cicli se non possiamo essere visti)

Come faccio a sapere se la scheda è attualmente attiva? Ci colleghiamo a window.focus e window.blur. Come facciamo a sapere che la scheda è visibile? Utilizziamo la nuova API Page Visibility e reagiamo all'evento appropriato.

I timeout riportati sopra sono più permissivi del solito. Li ho adattati a questo particolare doodle, che ha molte animazioni ambientali (principalmente il cielo e l'uccello). Idealmente, i timeout dovrebbero essere basati sull'interazione in-game, ad esempio subito dopo l'atterraggio l'uccello potrebbe comunicare al doodle che può andare in sospensione, ma alla fine non l'ho implementato.

Poiché il cielo è sempre in movimento, quando ti addormenti e ti svegli il doodle non si ferma o si avvia, ma rallenta prima di mettere in pausa e viceversa per riprendere, aumentando o diminuendo il numero di tick logici per un tick fisico, se necessario.

Transizioni, trasformazioni, eventi

Uno dei punti di forza dell'HTML è sempre stato il fatto che puoi migliorarlo autonomamente: se qualcosa non è abbastanza buono nel normale portafoglio di HTML e CSS, puoi utilizzare JavaScript per estenderlo. Purtroppo, spesso significa dover ripartire da zero. Le transizioni CSS3 sono fantastiche, ma non puoi aggiungere un nuovo tipo di transizione o utilizzare le transizioni per fare qualcos'altro che applicare stili agli elementi. Un altro esempio: le trasformazioni CSS3 sono ottime per il DOM, ma quando passi a Canvas, improvvisamente sei da solo.

Per questi e altri motivi, Lem doodle ha il proprio motore di transizione e trasformazione. Sì, lo so, gli anni 2000 hanno chiamato e così via. Le funzionalità che ho integrato non sono neanche lontanamente potenti come CSS3, ma qualunque cosa faccia il motore, lo fa in modo coerente e ci offre molto più controllo.

Ho iniziato con un semplice sistema di azioni (eventi), una sequenza temporale che attiva gli eventi in futuro senza utilizzare setTimeout, poiché in qualsiasi momento il tempo del doodle può essere distinto dal tempo fisico man mano che diventa più veloce (avanzamento veloce), più lento (frequenza frame bassa o sospensione per risparmiare CPU) o si ferma del tutto (attesa del completamento del caricamento delle immagini).

Le transizioni sono solo un altro tipo di azioni. Oltre ai movimenti di base e alla rotazione, supportiamo anche i movimenti relativi (ad es. spostare un elemento di 10 pixel verso destra), elementi personalizzati come il tremolio e le animazioni di immagini con fotogrammi chiave.

Ho parlato delle rotazioni, che vengono eseguite anche manualmente: abbiamo sprite per vari angoli per gli oggetti che devono essere ruotati. Il motivo principale è che sia le rotazioni CSS3 sia quelle canvas introducevano artefatti visivi che abbiamo ritenuto inaccettabili. Inoltre, questi artefatti variavano in base alla piattaforma.

Dato che alcuni oggetti che ruotano sono collegati ad altri oggetti in rotazione, ad esempio la mano di un robot collegata al braccio inferiore, che a sua volta è collegato a un braccio superiore in rotazione, ho dovuto anche creare una sorta di origine trasformazione sotto forma di pivot.

Tutto questo richiede un notevole impegno che, in ultima analisi, copre le aree già gestite da HTML5, ma a volte il supporto nativo non è sufficiente ed è il momento di reinventare la ruota.

Utilizzo di immagini e sprite

Un motore non serve solo per eseguire il doodle, ma anche per lavorarci sopra. Ho condiviso alcuni parametri di debug sopra: puoi trovare il resto in engine.readDebugParams.

La pixel art è una tecnica ben nota che utilizziamo anche per i nostri scarabocchi. Ci consente di risparmiare byte e ridurre i tempi di caricamento, oltre a semplificare il pre-caricamento. Tuttavia, rende anche lo sviluppo più difficile: ogni modifica alle immagini richiederebbe un nuovo sprite (in gran parte automatizzato, ma comunque complicato). Pertanto, il motore supporta l'esecuzione su immagini non elaborate per lo sviluppo e su sprite per la produzione tramite engine.useSprites. Entrambi sono inclusi nel codice sorgente.

Doodle di Pac-Man
Sprite utilizzati dal doodle di Pac-Man.

Supportiamo anche il precaricamento delle immagini man mano che procediamo e l'interruzione del doodle se le immagini non si caricano in tempo – il tutto con una falsa barra di avanzamento. (Falso perché, purtroppo, nemmeno HTML5 può dirci quanto di un file immagine è già stato caricato.)

Uno screenshot della grafica di caricamento con la barra di avanzamento truccata.
Uno screenshot della grafica di caricamento con la barra di avanzamento truccata.

Per alcune scene, utilizziamo più di uno sprite non tanto per velocizzare il caricamento tramite connessioni parallele, ma semplicemente a causa della limitazione di 3/5 milioni di pixel per le immagini su iOS.

Che ruolo ha HTML5 in tutto questo? Non c'è molto sopra, ma lo strumento che ho scritto per lo spriting/il ritaglio era costituito da nuove tecnologie web: canvas, blob, a[download]. Uno degli aspetti interessanti dell'HTML è che gradualmente assorbe le operazioni che in precedenza dovevano essere eseguite al di fuori del browser. L'unica parte che dovevamo ottimizzare era costituita dai file PNG.

Salvataggio dello stato tra una partita e l'altra

I mondi di Lem sono sempre stati grandi, vividi e realistici. Le sue storie solitamente iniziavano senza molte spiegazioni, la prima pagina iniziava in medias res e il lettore doveva farsi strada da solo.

Il Cyberiad non faceva eccezione e volevamo replicare questa sensazione per il doodle. Per prima cosa, cerchiamo di non spiegare troppo la storia. Un'altra parte importante è la randomizzazione, che abbiamo ritenuto adatta alla natura meccanica dell'universo del libro. Abbiamo una serie di funzioni di supporto che si occupano della casualità e che utilizziamo in molti punti.

Volevamo anche aumentare la rigiocabilità in altri modi. Per farlo, dovevamo sapere quante volte il doodle era stato completato in precedenza. La soluzione tecnologica storicamente corretta è un cookie, ma non funziona per la home page di Google: ogni cookie aumenta il payload di ogni pagina e, di nuovo, ci preoccupiamo molto della velocità e della latenza.

Fortunatamente, HTML5 ci offre lo spazio di archiviazione web, di facile utilizzo, che ci consente di salvare e richiamare il conto delle riproduzioni generali e l'ultima scena riprodotta dall'utente, con molta più eleganza rispetto a quanto consentito dai cookie.

Che cosa facciamo con queste informazioni?

  • mostriamo un pulsante di avanzamento veloce, che consente di saltare le scene tagliate già viste dall'utente
  • mostriamo diversi elementi N durante la finale
  • aumentiamo leggermente la difficoltà del livello di scatto
  • mostriamo un piccolo drago di probabilità easter egg di una storia diversa alla terza e alle successive partite

Esistono diversi parametri di debug che controllano questo aspetto:

  • ?doodle-debug&doodle-first-run: fai finta che sia la prima esecuzione
  • ?doodle-debug&doodle-second-run: fai finta che sia una seconda esecuzione
  • ?doodle-debug&doodle-old-run – fai finta che sia una corsa vecchia

Dispositivi touch

Volevamo che il doodle fosse perfettamente a suo agio sui dispositivi touch. I più moderni sono abbastanza potenti da far funzionare il doodle alla grande e giocare tramite tocco è molto più divertente che con i clic.

Erano necessarie alcune modifiche preliminari all'esperienza utente. Inizialmente, il cursore del mouse era l'unico elemento che indicava che si stava svolgendo una cutscene o una parte non interattiva. In seguito abbiamo aggiunto un piccolo indicatore nell'angolo in basso a destra, in modo da non dover fare affidamento solo sul cursore del mouse (dato che non esistono sui dispositivi touch).

Normale Occupato Cliccabile Selezionato
In lavorazione
Puntatore normale in stato In lavorazione
Indicatore di attività in corso
Cursore cliccabile per i contenuti in lavorazione
Cursore che indica che un elemento è in fase di elaborazione
Finale
Puntatore normale finalev
Puntatore di stato occupato finale
Cursore cliccabile finale
Puntatore del clic finale
I cursori del mouse durante lo sviluppo e gli equivalenti finali.

La maggior parte delle funzionalità ha funzionato subito. Tuttavia, alcuni test di usabilità rapidi e improvvisati della nostra esperienza tocco hanno mostrato due problemi: alcuni obiettivi erano troppo difficili da premere e i tocchi rapidi venivano ignorati perché abbiamo semplicemente soprassediato gli eventi di clic del mouse.

Avere elementi DOM trasparenti separati e cliccabili è stato di grande aiuto, in quanto potevo ridimensionarli indipendentemente dalle immagini. Ho introdotto un padding aggiuntivo di 15 pixel per i dispositivi touch e l'ho utilizzato ogni volta che sono stati creati elementi cliccabili. Ho aggiunto anche un'area di a capo di 5 pixel per gli ambienti con mouse, solo per far felice il signor Fitts.

Per quanto riguarda l'altro problema, ho solo fatto in modo di collegare e testare i gestori di inizio e fine tocco appropriati, anziché fare affidamento sul clic del mouse.

Utilizziamo inoltre proprietà di stile più moderne per rimuovere alcune funzionalità touch aggiunte per impostazione predefinita dai browser WebKit (evidenziazione tocco, callout tocco).

E come facciamo a rilevare se un determinato dispositivo su cui è visualizzato il doodle supporta il tocco? Pigramente. Invece di capirlo a priori, abbiamo semplicemente utilizzato i nostri IQ combinati per dedurre che il dispositivo supporta il tocco dopo aver ricevuto il primo evento di inizio tocco.

Personalizzare il cursore del mouse

Ma non tutto è basato sul tocco. Uno dei nostri principi guida era inserire nel doodle il maggior numero possibile di elementi. La piccola UI della barra laterale (avanzamento veloce, punto interrogativo), la descrizione comando e persino il cursore del mouse.

Come personalizzare un cursore del mouse? Alcuni browser consentono di cambiare il cursore del mouse collegandolo a un file immagine personalizzato. Tuttavia, non è supportato bene ed è anche un po' limitativo.

Se non questo, cosa? Perché non fare del cursore del mouse un altro elemento del doodle? Questo metodo funziona, ma presenta una serie di limitazioni, tra cui:

  • devi essere in grado di rimuovere il cursore del mouse nativo
  • devi essere abbastanza bravo a mantenere il cursore del mouse sincronizzato con quello "reale"

La prima è complicata. CSS3 consente cursor: none, ma anche questo non è supportato in alcuni browser. Abbiamo dovuto ricorrere a qualche esercizio di ginnastica: abbiamo utilizzato un file .cur vuoto come opzione di riserva, specificato un comportamento concreto per alcuni browser e persino escluso altri dall'esperienza tramite il codice rigido.

L'altro è relativamente banale, ma poiché il cursore del mouse è solo un'altra parte dell'universo del doodle, erediterà anche tutti i suoi problemi. La più grande Se la frequenza frame del doodle è bassa, lo sarà anche quella del cursore del mouse e questo ha conseguenze gravi, poiché il cursore del mouse, essendo un'estensione naturale della mano, deve essere reattivo indipendentemente da tutto. (Chi ha usato Commodore Amiga in passato ora annuisce vigorosamente.)

Una soluzione un po' complessa a questo problema consiste nel disaccoppiare il cursore del mouse dal normale loop di aggiornamento. Abbiamo fatto proprio questo, in un universi alternato in cui non ho bisogno di dormire. C'è una soluzione più semplice per questo problema? Basta ripristinare il cursore del mouse nativo se la frequenza frame complessiva scende al di sotto di 20 fps. È qui che entra in gioco la frequenza di frame variabile. Se reagissimo alla frequenza frame corrente e se accadesse di oscillare intorno ai 20 fps, l'utente vedrebbe il cursore del mouse personalizzato nascondersi e mostrarsi continuamente. Questo ci porta a:

Intervallo di frequenza fotogrammi Comportamento
>10 f/s Rallenta il gioco in modo che non vengano persi altri frame.
10-20 fps Utilizza il cursore del mouse nativo anziché quello personalizzato.
20-60 fps Funzionamento normale.
> 60 f/s Riduci la frequenza in modo che non superi questo valore.
Riepilogo del comportamento dipendente dalla frequenza fotogrammi.

A proposito, il cursore del mouse è scuro su Mac, ma bianco su PC. Perché? Perché le guerre tra piattaforme hanno bisogno di carburante anche negli universi immaginari.

Conclusione

Non è un motore perfetto, ma non vuole esserlo. È stato sviluppato insieme al doodle di Lem ed è molto specifico per questo. Va bene. "L'ottimizzazione prematura è la radice di tutti i mali", come disse il celebre Don Knuth, e non credo che abbia senso scrivere prima un motore in isolamento e poi applicarlo solo in un secondo momento. La pratica informa la teoria, così come la teoria informa la pratica. Nel mio caso, il codice è stato eliminato, alcune parti sono state riscritte più volte e molti componenti comuni sono stati notati post, anziché ante factum. Ma alla fine, ciò che abbiamo ottenuto ci ha permesso di fare ciò che volevamo: celebrare la carriera di Stanisław Lem e i disegni di Daniel Mróz nel modo migliore che potevamo immaginare.

Spero che quanto sopra abbia chiarito alcune delle scelte di progettazione e dei compromessi che abbiamo dovuto fare e come abbiamo utilizzato HTML5 in uno scenario specifico e reale. Ora, gioca con il codice sorgente, provalo e facci sapere cosa ne pensi.

L'immagine qui sotto è stata pubblicata negli ultimi giorni, con il conto alla rovescia per le prime ore del 23 novembre 2011 in Russia, il primo fuso orario in cui è stato pubblicato il doodle di Lem. Forse una cosa sciocca, ma come i scarabocchi, le cose che sembrano insignificanti a volte hanno un significato più profondo. Questo contatore è stato davvero un bel "test di stress" per il motore.

Uno screenshot dell&#39;orologio del conto alla rovescia nell&#39;universo di Lem.
Uno screenshot dell'orologio del conto alla rovescia nell'universo di Lem.

Questo è uno dei modi per guardare alla vita di un doodle di Google: mesi di lavoro, settimane di test, 48 ore di messa in produzione, il tutto per qualcosa con cui le persone giocano per cinque minuti. Ogni riga di JavaScript spera che questi 5 minuti siano ben spesi. Buona visione.