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

Marcin Wichary
Marcin Wichary

Hello, (strano) mondo

La home page di Google è un ambiente affascinante al fine di programmare. È caratterizzato da molte limitazioni complesse: un'attenzione particolare a velocità e latenza, la necessità di soddisfare tutti i tipi di browser e di lavorare in varie circostanze, e... sì, sorprenderti e divertirti.

Mi riferisco ai doodle di Google, ovvero delle illustrazioni speciali che occasionalmente sostituiscono il nostro logo. Nonostante il mio rapporto con le penne e i pennelli abbia da tempo il sapore distintivo di un ordine restrittivo, spesso contribuisco a quelli interattivi.

Tutti i doodle interattivi che ho codificato (Pac-Man, Jules Verne, World’s Fair) e molti con cui ho aiutato erano in parti uguali futuristici e anacronici: ottime opportunità per applicazioni pie-in-the-sky di funzionalità web all'avanguardia... e pragmatismo crudo della compatibilità cross-browser.

Apprendiamo molto da ogni doodle interattivo e il recente mini-gioco Stanisław Lem non ha fatto eccezione, con le sue 17.000 righe di codice JavaScript che ha messo alla prova molte cose per la prima volta nella storia dei doodle. Oggi voglio condividere con te quel codice (forse troverai qualcosa di interessante o far notare i miei errori) e parlarne un po'.

Visualizza il codice del doodle di Stanisław Lem »

È importante tenere a mente che la home page di Google non è un luogo per le demo tecnologiche. Con i nostri doodle, vogliamo celebrare persone ed eventi specifici e vogliamo farlo utilizzando le migliori opere d'arte e le migliori tecnologie che possiamo evocare, senza mai celebrare la tecnologia nell'interesse della tecnologia. Ciò significa esaminare attentamente qualsiasi parte dell'HTML5 ampiamente compreso tra quelle disponibili e capire se ci aiuta a migliorare il doodle senza distrarlo o aggirarlo.

Esaminiamo alcune delle moderne tecnologie web che hanno trovato il loro posto, e altre no, nel doodle di Stanisław Lem.

Grafica tramite DOM e canvas

Canvas è uno strumento potente e creato per soddisfare esattamente il tipo di attività che volevamo realizzare 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 metodo diverso.

Ho creato un motore grafico che astrae le primitive grafiche chiamate "retti" e poi le esegue il rendering utilizzando una tela, il DOM se la canvas non è disponibile.

Questo approccio presenta alcune sfide interessanti, ad esempio lo spostamento o la modifica di un oggetto nel DOM ha conseguenze immediate, mentre per la tela c'è un momento specifico in cui tutto è disegnato nello stesso momento. (Ho deciso di usare una sola tela, cancellarla e disegnarci sopra ogni inquadratura. Troppe parti, letteralmente, mobili da una parte, e dall'altra non sufficientemente complessità per giustificare la suddivisione in più quadri sovrapposti e l'aggiornamento selettivo.)

Sfortunatamente, passare a canvas non è semplice come il mirroring degli sfondi CSS con drawImage(): perderai una serie di funzionalità senza costi quando si mettono insieme i prodotti tramite DOM, soprattutto la sovrapposizione di z-index ed eventi del mouse.

Ho già astrato lo z-index con un concetto chiamato "aerei". Il doodle definiva un numero di piani, dal cielo molto indietro, al puntatore del mouse davanti a tutto, e ogni attore all'interno del doodle doveva decidere a quale apparteneva (piccole correzioni più/meno all'interno di un piano utilizzando planeCorrection).

Nel rendering tramite DOM, i piani vengono semplicemente tradotti in z-index. Se invece eseguiamo il rendering tramite canvas, dobbiamo ordinare i rettili in base ai loro piani prima di disegnarli. Poiché questa operazione è costosa ogni volta, l'ordine viene ricalcolato solo quando viene aggiunto un attore o quando si passa su un altro piano.

Per gli eventi del mouse, l'ho astratti anche... in un certo senso. Sia per DOM che per canvas, ho utilizzato altri elementi DOM mobili completamente trasparenti con z-index elevato, la cui funzione è solo di reagire a mouseover/out, clic e tocchi.

Una delle cose che volevamo provare con questo doodle era rompere la quarta parete. Il motore di cui sopra ci ha permesso di combinare attori basati su canvas con attori basati su DOM. Ad esempio, le esplosioni nel finale sono sia in canvas per gli oggetti nell'universo sia nel DOM per il resto della home page di Google. L'uccello, che di solito vola intorno e tagliato dalla nostra maschera frastagliata come qualsiasi altro attore, decide di non andare dai guai durante il livello di tiro e siede sul pulsante Mi sento fortunato. Per questo motivo, l'uccello abbandona la tela e diventa un elemento DOM (e viceversa in seguito), con la speranza che sia completamente trasparente per i visitatori.

La frequenza fotogrammi

Conoscere la frequenza fotogrammi attuale e reagire a quando è troppo lenta (e troppo veloce) è un aspetto importante del nostro motore. Poiché i browser non segnalano la frequenza fotogrammi, dobbiamo calcolarla da soli.

Ho iniziato a utilizzare requestAnimationFrame, tornando al vecchio setTimeout se il primo non era disponibile. requestAnimationFrame risparmia abilmente la CPU in alcune situazioni, anche se stiamo svolgendo alcune di queste operazioni autonomamente, come spiegheremo di seguito, ma ci consente anche di ottenere una frequenza fotogrammi più elevata rispetto a setTimeout.

Il calcolo della frequenza fotogrammi corrente è semplice, ma è soggetto a cambiamenti drastici, ad esempio può interrompersi rapidamente quando un'altra applicazione perde il tempo per controllare il computer. Di conseguenza, calcoliamo la frequenza fotogrammi "rullante" (media) solo ogni 100 tick fisici e prendiamo le decisioni in base a questa metrica.

Che tipo di decisioni?

  • Se la frequenza fotogrammi è superiore a 60 f/s, viene limitata. Attualmente, requestAnimationFrame su alcune versioni di Firefox non ha un limite superiore per la frequenza fotogrammi e non ha senso sprecare la CPU. Tieni presente che in realtà il limite è di 65 f/s, a causa degli errori di arrotondamento che rendono la frequenza fotogrammi di poco superiore a 60 f/s su altri browser. Non vogliamo iniziare a limitarla per sbaglio.

  • Se la frequenza fotogrammi è inferiore a 10 f/s, rallentiamo semplicemente il motore anziché perdere i frame. È una proposta di perdita di dati, ma ho pensato che saltare i frame sarebbe stato troppo complicato rispetto a un gioco più lento (ma comunque coerente). C'è un altro effetto collaterale: se il sistema diventa lento temporaneamente, l'utente non sperimenterà un buffo salto avanti, dato che il motore sta disperatamente recuperare il passo. (L'ho fatto in modo leggermente diverso per Pac-Man, ma la frequenza fotogrammi minima è un approccio migliore.)

  • Infine, possiamo pensare a semplificare la grafica quando la frequenza fotogrammi è pericolosamente bassa. Non lo stiamo facendo per il doodle di Lem, ad eccezione del puntatore del mouse (vedi di seguito), ma ipoteticamente potremmo perdere alcune animazioni estranee solo per far sì che il doodle sembri fluido anche sui computer più lenti.

Abbiamo anche il concetto di un segno di spunta fisico e di un segno di spunta logico. Il primo proviene da requestAnimationFrame/setTimeout. Il rapporto nel gameplay normale è 1:1, ma per l'avanzamento veloce aggiungiamo semplicemente segni più logici per ogni segno di spunta fisico (fino a 1:5). In questo modo possiamo fare tutti i calcoli necessari per ogni segno di spunta logico, ma indicare solo che l'ultimo è quello che aggiorna la schermata.

Analisi comparativa

Si può (e fin da subito) basarsi sul presupposto che la tela sarà più veloce del DOM ogni volta che sarà disponibile. Non è sempre vero. Durante il test, abbiamo scoperto che Opera 10.0-10.1 su Mac e Firefox su Linux sono in realtà più veloci quando si spostano elementi DOM.

Nel mondo perfetto, il doodle analizzava silenziosamente diverse tecniche grafiche: elementi DOM spostati utilizzando style.left e style.top, disegnava su canvas e forse anche elementi DOM spostati con trasformazioni CSS3

e poi passa a quella con la frequenza fotogrammi più elevata. Ho iniziato a scrivere un codice per questo, ma ho scoperto che almeno il mio metodo di benchmarking era piuttosto inaffidabile e richiedeva molto tempo. Tempo che non abbiamo sulla nostra home page: abbiamo molto a cuore la velocità e vogliamo che il doodle venga visualizzato all'istante 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 si deve fare. Ho guardato dietro le mie spalle per assicurarmi che nessuno stesse guardando e poi ho semplicemente codificato Opera 10 e Firefox. Nella prossima vita, tornerò come tag <marquee>.

Risparmio di CPU

Hai presente quel amico che arriva a casa tua, guarda il finale di stagione di Breaking Bad, ti rovina e poi lo elimina dal tuo DVR? Non vuoi essere quella persona, vero?

Quindi sì, l'analogia peggiore che abbia mai visto. Ma non vogliamo che il nostro doodle sia neanche tale: il fatto di poter accedere alla scheda del browser di un utente è un privilegio e accumulare cicli della CPU o distrarre l'utente ci renderebbe un ospite sgradevole. Pertanto, se nessuno sta giocando con il doodle (senza tocchi, clic del mouse, movimenti del mouse o pressione dei tasti), vogliamo che alla fine vada a dormire.

Quando?

  • dopo 18 secondi sulla home page (i giochi arcade si chiamano modalità Attira)
  • dopo 180 secondi se la scheda è attiva
  • dopo 30 secondi se la scheda non è più attiva (ad esempio l'utente è passato a un'altra finestra, ma forse sta ancora guardando il doodle ora in una scheda non attiva)
  • immediatamente se la scheda diventa invisibile (ad es.l'utente è passato a un'altra scheda nella stessa finestra; non c'è motivo di sprecare cicli se non siamo visibili).

Come faccio a sapere che la scheda è attualmente attiva? Ci colleghiamo a window.focus e window.blur. Come faccio a sapere che la scheda è visibile? Stiamo utilizzando la nuova API Page Visibilità e reagiamo all'evento appropriato.

I timeout sopra riportati sono più tolleranti del solito per noi. Le ho adattate a questo particolare doodle, che contiene molte animazioni ambientali (soprattutto il cielo e l'uccello). Idealmente, i timeout sarebbero limitati all'interazione in-game. Ad esempio, subito dopo l'atterraggio, l'uccello potrebbe riferire al doodle che può andare a dormire ora, ma io non l'ho implementato alla fine.

Poiché il cielo è sempre in movimento, quando ti addormenti e ti svegli il doodle non si limita o si avvia, ma rallenta prima di essere messo in pausa e viceversa per riprendere, aumentare o diminuire i segni logici per un segno di spunta fisico, se necessario.

Transizioni, trasformazioni, eventi

Uno dei vantaggi dell'HTML è sempre stato il fatto di essere in grado di migliorarlo: se qualcosa non è abbastanza buono nel normale portafoglio di HTML e CSS, puoi utilizzare JavaScript per estenderlo. Purtroppo, spesso significa dover ricominciare da zero. Le transizioni CSS3 sono ottime, ma non puoi aggiungere un nuovo tipo di transizione o utilizzare le transizioni per fare qualcosa di diverso rispetto agli elementi di stile. Un altro esempio: le trasformazioni CSS3 sono perfette per il DOM, ma quando si passa alla tela, improvvisamente non ci riesci.

Questi problemi, e non solo, sono il motivo per cui Lem Doodle ha un proprio motore di transizione e trasformazione. Sì, lo so, come gli anni '2000. Le funzionalità che ho integrato non sono così 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 azione (eventi), una sequenza temporale che attiva gli eventi in futuro senza utilizzare setTimeout, dato che in qualsiasi momento l'orario del doodle può diventare divorziato dal momento fisico man mano che l'azione diventa più veloce (avanzata veloce), più lenta (frequenza fotogrammi bassa o si addormenta per risparmiare CPU) o si interrompe del tutto (attendi che le immagini terminino il caricamento).

Le transizioni sono solo un altro tipo di azioni. Oltre ai movimenti e alle rotazioni di base, sono supportati anche movimenti relativi (ad es. spostare un elemento di 10 pixel a destra), elementi personalizzati come i brividi e animazioni con fotogrammi chiave.

Ho citato le rotazioni, anche queste vengono eseguite manualmente: abbiamo sprite per vari angoli per gli oggetti che devono essere ruotati. Il motivo principale è che sia le rotazione CSS3 che quelle del canvas stavano introducendo artefatti visivi che ritenevamo inaccettabili e, per di più, questi artefatti erano diversi a seconda della piattaforma.

Dato che alcuni oggetti che ruotano sono attaccati ad altri oggetti in rotazione, ad esempio la mano di un robot collegata al braccio inferiore, che a sua volta è agganciata a un braccio superiore rotante, ho dovuto anche creare l'origine della trasformazione di un uomo povero sotto forma di pivot.

Si tratta di una notevole quantità di lavoro che in definitiva copre gli aspetti già gestiti da HTML5, ma a volte il supporto nativo non è sufficiente ed è arrivato il momento di reinventarsi tutto.

Gestione di immagini e sprite

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

Lo sprite è una tecnica molto nota che usiamo anche noi per i doodle. Ci permette di risparmiare byte e di ridurre i tempi di caricamento, oltre a semplificare il precaricamento. Tuttavia, rende anche più difficoltoso lo sviluppo: ogni modifica alle immagini richiederebbe respeting (in gran parte automatizzati, ma comunque ingombranti). Di conseguenza, il motore supporta l'esecuzione su immagini non elaborate per lo sviluppo e sprite per la produzione tramite engine.useSprites: entrambi sono inclusi nel codice sorgente.

doodle di Pac-Man
Sprites utilizzati dal doodle di Pac-Man.

Supportiamo anche il precaricamento delle immagini, mentre procediamo con l'interruzione del doodle se le immagini non vengono caricate in tempo, con tanto di barra di avanzamento finta. (Falso perché, sfortunatamente, nemmeno HTML5 è in grado di indicarci la quantità di file immagine già caricata.)

Uno screenshot del caricamento dell&#39;immagine con la barra di avanzamento manipolata.
Uno screenshot del caricamento dell'immagine con la barra di avanzamento manipolata.

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

Dove si inserisce HTML5 in tutto questo? Non c'è molto di cui sopra, ma lo strumento che ho scritto per lo spriting/ritaglio era una tecnologia web completamente nuova: canvas, blobs, a[download]. Uno degli aspetti più entusiasmanti dell'HTML è che riassume lentamente le attività che in precedenza dovevano essere eseguite al di fuori del browser; l'unica parte che dovevamo fare è stata l'ottimizzazione dei file PNG.

Salvataggio dello stato tra una partita e l'altra

I mondi di Lem sembravano sempre grandi, vivi e realistici. Le sue storie di solito iniziavano senza molte spiegazioni, la prima pagina iniziava in media res, con il lettore che doveva orientarsi.

La Cyberiad non faceva eccezione e volevamo replicare questo sentimento per il doodle. Iniziamo cercando di non spiegare troppo la storia. Un'altra parte importante è la randomizzazione, che ci è sembrata appropriata per la natura meccanica dell'universo del libro. Abbiamo una serie di funzioni helper che trattano la casualità che utilizziamo in molti casi.

Volevamo aumentare la rigiocabilità anche in altri modi. Per farlo, dovevamo sapere quante volte il doodle era stato finito prima. 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 sul web, una soluzione banale di utilizzo, che ci permette di salvare e richiamare il conteggio generale delle riproduzioni e l'ultima scena eseguita dall'utente, con molto più tolleranza di quanto non consentirebbe i cookie.

Come utilizziamo queste informazioni?

  • mostriamo un pulsante per andare avanti veloce che consente di scorrere i filmati che l'utente ha già visto in precedenza.
  • mostriamo N elementi diversi durante il finale
  • aumentiamo leggermente la difficoltà del livello di tiro
  • mostriamo un piccolo drago con la probabilità di un easter egg di una storia diversa per la tua terza opera e le successive

Esiste una serie di parametri di debug che controllano questo:

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

Dispositivi touch

Volevamo che il doodle risultasse a casa sui dispositivi touch. I più moderni erano sufficientemente potenti da far funzionare il doodle molto bene, mentre sperimentare il gioco tramite tocco è molto più divertente che con un clic.

È stato necessario apportare alcune modifiche iniziali all'esperienza utente. In origine, il puntatore del mouse era l'unico punto da cui veniva comunicata l'esecuzione di un filmato/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 puntatore del mouse (considerato che non esistono sui dispositivi touch).

Normale Occupato Cliccabile Selezionato
Elaborazione in corso
Puntatore normale in corso
Puntatore occupato in corso
Puntatore cliccabile in corso
Puntatore selezionato
Finale
Puntatore normale finalev
Puntatore finale occupato
Puntatore cliccabile finale
Puntatore su cui è stato fatto clic finale
Puntatori del mouse durante lo sviluppo e equivalenti finali.

La maggior parte delle cose funzionava subito. Tuttavia, i rapidi test di usabilità estemporanei della nostra esperienza tattile hanno evidenziato due problemi: alcuni dei target erano troppo difficili da premere e i tocchi rapidi sono stati ignorati perché abbiamo appena sostituito gli eventi di clic del mouse.

L'utilizzo di elementi DOM trasparenti cliccabili separati era molto utile, dato che potevo ridimensionarli indipendentemente dalle immagini. Ho introdotto una spaziatura interna extra di 15 pixel per i dispositivi touch e l'ho utilizzata ogni volta che venivano creati elementi cliccabili. (Ho aggiunto una spaziatura interna di 5 pixel anche per gli ambienti del mouse, solo per soddisfare le esigenze di Mr. Fitts).

Per quanto riguarda l'altro problema, mi sono assicurato di collegare e testare i gestori di inizio e fine tocco, anziché fare clic sul mouse.

Stiamo anche usando proprietà di stile più moderne per rimuovere alcune funzionalità touch aggiunte per impostazione predefinita dai browser WebKit (tocca evidenziazione, tocca callout).

E come facciamo a capire se il dispositivo su cui è in esecuzione il doodle supporta il tocco? Pigramente. Invece di capire a priori, abbiamo usato le nostre QI combinate per dedurre che il dispositivo supporta il tocco... dopo aver ricevuto il primo evento di avvio tocco.

Personalizzazione del puntatore del mouse

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

Come si personalizza un puntatore del mouse? Alcuni browser consentono di cambiare il cursore del mouse collegandolo a un file immagine personalizzato. Tuttavia, ciò non è supportato bene ed è anche un po' restrittivo.

Se non è così, cosa succede? Dunque, perché non rendere un puntatore del mouse come un altro attore del doodle? Funziona, ma richiede una serie di avvertenze, principalmente:

  • devi poter rimuovere il puntatore nativo del mouse
  • devi essere bravo a tenere il puntatore del mouse sincronizzato con quello "reale"

La prima è difficile. CSS3 consente cursor: none, ma non è supportato in alcuni browser. Abbiamo dovuto ricorrere a un po' di ginnastica: utilizzare un file .cur vuoto come riserva, specificare un comportamento concreto per alcuni browser e persino eseguire l'hard-coding di altri partendo dall'esperienza.

L'altro è relativamente banale all'aspetto, ma il puntatore del mouse è solo un'altra parte dell'universo del doodle, pertanto erediterà anche tutti i suoi problemi. Il più grande? Se la frequenza fotogrammi del doodle è bassa, anche quella del puntatore del mouse sarà bassa, con conseguenze gravi poiché il puntatore del mouse, essendo un'estensione naturale della mano, deve essere reattivo in ogni caso. (Le persone che hanno usato il Commodore Amiga in passato ora acclamano con decisione.)

Una soluzione piuttosto complessa a questo problema è disaccoppiare il puntatore del mouse dal regolare loop di aggiornamento. L'abbiamo fatto esattamente, in un universo alternativo in cui non ho bisogno di dormire. Una soluzione più semplice? Basta tornare al puntatore nativo del mouse se la frequenza fotogrammi mobile scende al di sotto dei 20 f/s. È qui che torna utile la frequenza framework mobile. Se reagissimo alla frequenza fotogrammi corrente e oscillasse di circa 20 f/s, l'utente vedrebbe il puntatore del mouse personalizzato nascosto e mostrato sempre. Questo ci porta a:

Intervallo frequenza fotogrammi Comportamento
> 10 f/s Rallenta il gioco per non far cadere più frame.
10-20 f/s Usa il puntatore del mouse nativo anziché personalizzato.
20-60 f/s Funzionamento normale.
> 60 f/s Limita in modo che la frequenza fotogrammi non superi questo valore.
Riepilogo del comportamento dipendente dalla frequenza fotogrammi.

Ah, il puntatore del mouse è scuro su Mac, ma bianco su PC. Come mai? Perché le guerre tra piattaforme hanno bisogno di energia anche negli universi fittizi.

Conclusione

Questo non è un motore perfetto, ma non cerca di esserlo. È stato sviluppato insieme al doodle su Lem ed è molto specifico. Va bene. "L'ottimizzazione precoce è la radice di tutti i mali", come ha affermato Don Knuth, e non credo che prima scrivere un motore in modo isolato abbia senso solo in seguito. La pratica informa la teoria tanto quanto la teoria informa la pratica. Nel mio caso, il codice veniva buttato via, diverse parti riscritte più volte e molte parti comuni venivano notate nel post anziché nell'ante factum. In fin dei conti, ciò che 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 possibile.

Spero che quanto riportato sopra faccia luce su alcune scelte di design e compromessi che dovevamo fare e su come abbiamo usato HTML5 in uno scenario specifico e reale. A questo punto, prova a utilizzare il codice sorgente e facci sapere cosa ne pensi.

L'ho fatto io. Questo è stato pubblicato negli ultimi giorni, con il conto alla rovescia fino alle prime ore del 23 novembre 2011 in Russia, che è stato il primo fuso orario in cui è stato visto il doodle di Lem. Una cosa buffa, forse, ma proprio come i doodle, le cose che sembrano insignificanti a volte hanno un significato più profondo. Questo contatore è stato davvero un buon "test di stress" per il motore.

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

Questo è un modo di osservare la vita di un doodle di Google: mesi di lavoro, settimane di test, 48 ore di preparazione, il tutto per qualcosa che le persone giocano per cinque minuti. Ognuna di queste migliaia di righe JavaScript spera che quei 5 minuti siano tempo ben spesi. Buon divertimento.