Simulazione di carenze della visione dei colori in Blink Renderer

Questo articolo descrive perché e come abbiamo implementato la simulazione della carenza di visione dei colori in DevTools e nel renderer Blink.

Sfondo: contrasto di colore scadente

Il testo a basso contrasto è il problema di accessibilità rilevabile automaticamente più comune sul web.

Un elenco di problemi di accessibilità comuni sul web. Il testo a basso contrasto è senza dubbio il problema più comune.

Secondo l'analisi dell'accessibilità di WebAIM relativa ai principali 1 milione di siti web, oltre l'86% delle home page ha un basso contrasto. In media, ogni home page presenta 36 istanze distinte di testo a basso contrasto.

Utilizzo di DevTools per individuare, comprendere e risolvere i problemi di contrasto

Chrome DevTools può aiutare sviluppatori e designer a migliorare il contrasto e a scegliere combinazioni di colori più accessibili per le app web:

Di recente abbiamo aggiunto un nuovo strumento a questo elenco, che è leggermente diverso dagli altri. Gli strumenti descritti sopra si concentrano principalmente sulla visualizzazione delle informazioni sul rapporto di contrasto e sulla fornitura di opzioni per correggerlo. Ci siamo resi conto che a DevTools gli sviluppatori mancavano ancora un modo per avere una comprensione più approfondita di questo ambito di problemi. Per risolvere questo problema, abbiamo implementato la simulazione di deficit visivo nella scheda Rendering di DevTools.

In Puppeteer, la nuova API page.emulateVisionDeficiency(type) ti consente di abilitare queste simulazioni in modo programmatico.

Carenze di visione dei colori

Circa 1 persona su 20 soffre di discromatopsia (nota anche come il termine meno preciso "daltonismo"). In questi casi è più difficile distinguere i diversi colori, il che può amplificare i problemi di contrasto.

Un'immagine colorata di pastelli sciolti, senza carenze di visione dei colori simulata
. Un'immagine colorata di pastelli sciolti, senza carenze di visione dei colori simulata.
di Gemini Advanced.
.
. ALT_TEXT_HERE
L'impatto della simulazione di acromatopsia su un'immagine colorata di pastelli sciolti.
di Gemini Advanced.
.
. L'impatto della simulazione della deuteranopia su un'immagine colorata di pastelli sciolti.
L'impatto della simulazione della deuteranopia su un'immagine colorata di pastelli sciolti.
di Gemini Advanced.
.
. L'impatto della simulazione di protanopia su un'immagine colorata di pastelli sciolti.
L'impatto della simulazione di protanopia su un'immagine colorata di pastelli sciolti.
di Gemini Advanced.
.
. L'impatto della simulazione della tritanopia su un'immagine colorata di pastelli sciolti.
L'impatto della simulazione della tritanopia su un'immagine colorata di pastelli sciolti.

In qualità di sviluppatore con visione regolare, DevTools potrebbe mostrare un rapporto di contrasto errato per le coppie di colori che sembrano corrette. Ciò accade perché le formule del rapporto di contrasto tengono conto di queste carenze di visione dei colori. In alcuni casi potresti comunque essere in grado di leggere il testo a basso contrasto, ma le persone con disabilità visiva non dispongono di questo privilegio.

Permettendo a designer e sviluppatori di simulare l'effetto di queste carenze visive sulle proprie app web, miriamo a fornire l'elemento mancante: non solo DevTools può aiutarti a individuare e risolvere i problemi di contrasto, ma ora puoi anche comprendere questi problemi.

Simulazione di carenze di visione dei colori con HTML, CSS, SVG e C++

Prima di approfondire l'implementazione di Blink Renderer della nostra funzionalità, è utile capire come implementare una funzionalità equivalente utilizzando la tecnologia web.

Puoi pensare a ciascuna di queste simulazioni di deficit di visione dei colori come una sovrapposizione che copre l'intera pagina. La piattaforma web ha un modo per farlo: i filtri CSS. Con la proprietà CSS filter, puoi utilizzare alcune funzioni di filtro predefinite, ad esempio blur, contrast, grayscale, hue-rotate e molte altre. Per un controllo ancora maggiore, la proprietà filter accetta anche un URL che può indirizzare a una definizione di filtro SVG personalizzato:

<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

L'esempio precedente utilizza una definizione di filtro personalizzato basata su una matrice di colori. Concettualmente, il valore di colore [Red, Green, Blue, Alpha] di ogni pixel viene moltiplicato per creare un nuovo colore [R′, G′, B′, A′].

Ogni riga della matrice contiene 5 valori: un moltiplicatore per R, G, B e A (da sinistra a destra), nonché un quinto valore per un valore di spostamento costante. Ci sono 4 righe: la prima riga della matrice viene utilizzata per calcolare il nuovo valore del rosso, la seconda riga Verde, la terza riga Blu e l'ultima riga Alpha.

Forse ti starai chiedendo da dove provengono i numeri esatti nel nostro esempio. Cosa rende questa matrice cromatica una buona approssimazione della deuteranopia? La risposta è: scienza! I valori si basano su un modello di simulazione di deficit di visione dei colori fisiologicamente accurato di Machado, Oliveira e Fernandes.

Comunque, abbiamo questo filtro SVG e ora possiamo applicarlo a elementi arbitrari sulla pagina utilizzando CSS. Possiamo ripetere lo stesso schema per altre carenze di vista. Ecco una demo:

Se volessimo, potremmo creare la nostra funzionalità DevTools nel seguente modo: quando l'utente emula un deficit visivo nella UI di DevTools, iniettiamo il filtro SVG nel documento ispezionato e poi applichiamo lo stile del filtro all'elemento principale. Tuttavia, questo approccio presenta diversi problemi:

  • La pagina potrebbe già avere un filtro nell'elemento principale, che il nostro codice potrebbe quindi sostituire.
  • La pagina potrebbe già avere un elemento con id="deuteranopia", in conflitto con la definizione del nostro filtro.
  • La pagina potrebbe basarsi su una determinata struttura DOM e l'inserimento di <svg> nel DOM potrebbe violare queste ipotesi.

A parte i casi limite, il problema principale di questo approccio è che apporteremo modifiche alla pagina osservabili in modo programmatico. Se un utente DevTools esamina il DOM, potrebbe improvvisamente vedere un elemento <svg> che non ha mai aggiunto o un filter CSS che non ha mai scritto. Che confusione! Per implementare questa funzionalità in DevTools, abbiamo bisogno di una soluzione che non presenti questi svantaggi.

Vediamo come possiamo renderlo meno invadente. Questa soluzione è composta da due parti: 1) lo stile CSS con la proprietà filter e 2) la definizione del filtro SVG, che attualmente fa parte del DOM.

<!-- Part 1: the CSS style with the filter property -->
<style>
  :root {
    filter: url(#deuteranopia);
  }
</style>
<!-- Part 2: the SVG filter definition -->
<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

Evitare la dipendenza da SVG nel documento

Iniziamo con la seconda parte: come possiamo evitare di aggiungere il file SVG al DOM? Un'idea è quella di spostarli in un file SVG separato. Possiamo copiare il file <svg>…</svg> dal codice HTML riportato sopra e salvarlo come filter.svg, ma prima dobbiamo apportare alcune modifiche. Il file SVG incorporato nell'HTML segue le regole di analisi HTML. Ciò significa che è possibile risolvere attività come in alcuni casi omettere le virgolette per i valori degli attributi. Tuttavia, i file SVG in file separati dovrebbero essere XML valido e l'analisi XML è molto più rigida dell'HTML. Ecco di nuovo il nostro snippet SVG in HTML:

<svg>
  <filter id="deuteranopia">
    <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000">
    </feColorMatrix>
  </filter>
</svg>

Per creare questo SVG autonomo valido (e quindi il file XML), dobbiamo apportare alcune modifiche. Riesci a indovinare quale?

<svg xmlns="http://www.w3.org/2000/svg">
 
<filter id="deuteranopia">
   
<feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                           0.280  0.673  0.047  0.000  0.000
                          -0.012  0.043  0.969  0.000  0.000
                           0.000  0.000  0.000  1.000  0.000"
/>
 
</filter>
</svg>

La prima modifica riguarda la dichiarazione dello spazio dei nomi XML in alto. La seconda aggiunta è il cosiddetto "solidus", la barra che indica che il tag <feColorMatrix> apre e chiude l'elemento. Quest'ultima modifica non è effettivamente necessaria (potremmo semplicemente attenerci al tag di chiusura </feColorMatrix> esplicito), ma poiché sia XML che SVG in HTML supportano questa forma abbreviata di />, potremmo farne uso.

Comunque, con queste modifiche, possiamo salvare il file come un file SVG valido e puntare a quest'ultimo dal valore della proprietà filter CSS nel nostro documento HTML:

<style>
  :root {
    filter: url(filters.svg#deuteranopia);
  }
</style>

Evviva, non dobbiamo più inserire SVG nel documento. Questo è già molto meglio. Ma... ora ci affidiamo a un file separato. Si tratta comunque di una dipendenza. Possiamo in qualche modo eliminarlo?

A quanto pare, non abbiamo effettivamente bisogno di un file. Possiamo codificare l'intero file all'interno di un URL utilizzando un URL di dati. A questo scopo, prendiamo letteralmente i contenuti del file SVG che avevamo prima, aggiungiamo il prefisso data:, configuriamo il tipo MIME corretto e ci proponiamo un URL di dati valido che rappresenta lo stesso file SVG:

data:image/svg+xml,
  <svg xmlns="http://www.w3.org/2000/svg">
    <filter id="deuteranopia">
      <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000
                             0.280  0.673  0.047  0.000  0.000
                            -0.012  0.043  0.969  0.000  0.000
                             0.000  0.000  0.000  1.000  0.000" />
    </filter>
  </svg>

Il vantaggio è che ora non dobbiamo più archiviare il file da nessuna parte o caricarlo dal disco o sulla rete solo per utilizzarlo nel nostro documento HTML. Quindi, invece di fare riferimento al nome file come facevamo prima, possiamo puntare all'URL dei dati:

<style>
  :root {
    filter: url('data:image/svg+xml,\
      <svg xmlns="http://www.w3.org/2000/svg">\
        <filter id="deuteranopia">\
          <feColorMatrix values="0.367  0.861 -0.228  0.000  0.000\
                                 0.280  0.673  0.047  0.000  0.000\
                                -0.012  0.043  0.969  0.000  0.000\
                                 0.000  0.000  0.000  1.000  0.000" />\
        </filter>\
      </svg>#deuteranopia');
  }
</style>

Alla fine dell'URL, specifichiamo comunque l'ID del filtro da utilizzare, come prima. Tieni presente che non è necessario codificare il documento SVG nell'URL utilizzando la codifica Base64, altrimenti questa operazione potrebbe solo compromettere la leggibilità e aumentare le dimensioni del file. Abbiamo aggiunto barre rovesciate alla fine di ogni riga per assicurarci che i caratteri di nuova riga nell'URL dei dati non terminino il valore letterale della stringa CSS.

Finora, abbiamo parlato solo di come simulare difetti alla vista utilizzando la tecnologia web. È interessante notare che la nostra implementazione finale nel renderer Blink è in realtà molto simile. Ecco un'utilità helper C++ che abbiamo aggiunto per creare un URL di dati con una determinata definizione di filtro, basata sulla stessa tecnica:

AtomicString CreateFilterDataUrl(const char* piece) {
  AtomicString url =
      "data:image/svg+xml,"
        "<svg xmlns=\"http://www.w3.org/2000/svg\">"
          "<filter id=\"f\">" +
            StringView(piece) +
          "</filter>"
        "</svg>"
      "#f";
  return url;
}

Ed ecco come lo utilizziamo per creare tutti i filtri di cui abbiamo bisogno:

AtomicString CreateVisionDeficiencyFilterUrl(VisionDeficiency vision_deficiency) {
  switch (vision_deficiency) {
    case VisionDeficiency::kAchromatopsia:
      return CreateFilterDataUrl("");
    case VisionDeficiency::kBlurredVision:
      return CreateFilterDataUrl("<feGaussianBlur stdDeviation=\"2\"/>");
    case VisionDeficiency::kDeuteranopia:
      return CreateFilterDataUrl(
          "<feColorMatrix values=\""
          " 0.367  0.861 -0.228  0.000  0.000 "
          " 0.280  0.673  0.047  0.000  0.000 "
          "-0.012  0.043  0.969  0.000  0.000 "
          " 0.000  0.000  0.000  1.000  0.000 "
          "\"/>");
    case VisionDeficiency::kProtanopia:
      return CreateFilterDataUrl("");
    case VisionDeficiency::kTritanopia:
      return CreateFilterDataUrl("");
    case VisionDeficiency::kNoVisionDeficiency:
      NOTREACHED();
      return "";
  }
}

Tieni presente che questa tecnica ci permette di accedere a tutta la potenza dei filtri SVG senza dover implementare di nuovo nulla o inventare nulla. Stiamo implementando una funzionalità Blink Renderer, ma lo stiamo facendo sfruttando la piattaforma web.

Abbiamo capito come creare filtri SVG e trasformarli in URL di dati da utilizzare all'interno del valore della proprietà filter CSS. Riesci a pensare a un problema con questa tecnica? A quanto pare, non possiamo affidarsi sempre sull'URL di dati caricato, poiché la pagina di destinazione potrebbe avere un Content-Security-Policy che blocca gli URL di dati. La nostra implementazione finale a livello di Blink presta particolare attenzione a bypassare CSP per questi URL di dati "interni" durante il caricamento.

A parte i casi limite, abbiamo fatto ottimi progressi. Poiché non dipendiamo più dalla presenza di <svg> in linea nello stesso documento, abbiamo ridotto in modo efficace la nostra soluzione a una sola definizione di proprietà filter CSS indipendente. Bene. Ora eliminiamo anche questo.

Evitare la dipendenza da CSS nel documento

Ricapitolando, ecco dove siamo arrivati finora:

<style>
  :root {
    filter: url('data:…');
  }
</style>

Dipendiamo comunque da questa proprietà filter CSS, che potrebbe sostituire un filter nel documento reale e interrompere le cose. Viene visualizzato anche durante l'ispezione degli stili calcolati in DevTools, il che genera confusione. Come possiamo evitare questi problemi? Dobbiamo trovare un modo per aggiungere un filtro al documento senza che sia osservabile in modo programmatico per gli sviluppatori.

Un'idea è stata quella di creare una nuova proprietà CSS interna di Chrome che si comporta come filter, ma con un nome diverso, ad esempio --internal-devtools-filter. Potremmo quindi aggiungere una logica speciale per garantire che questa proprietà non venga mai visualizzata in DevTools o negli stili calcolati nel DOM. Potremmo anche assicurarci che funzioni solo sull'elemento per cui abbiamo bisogno: l'elemento principale. Tuttavia, questa soluzione non sarebbe ideale: duplicando le funzionalità già esistenti con filter e, anche se cerchiamo di nascondere questa proprietà non standard, gli sviluppatori web potrebbero comunque scoprirla e iniziare a utilizzarla, il che sarebbe negativo per la piattaforma web. Abbiamo bisogno di un altro modo per applicare uno stile CSS senza che sia osservabile nel DOM. Qualche idea?

La specifica CSS comprende una sezione che introduce il modello di formattazione visiva utilizzato e uno dei concetti chiave è l'area visibile. Si tratta della visualizzazione visiva attraverso la quale gli utenti consultano la pagina web. Un concetto strettamente correlato è l'iniziale contenente il blocco, che è un po' come un'area visibile <div> con stile che esiste solo a livello di specifica. Le specifiche fanno riferimento a questo concetto di "area visibile" ovunque. Ad esempio, sai in che modo il browser mostra le barre di scorrimento quando i contenuti non sono adatti? Tutto questo è definito nelle specifiche CSS, in base all'"area visibile".

Questo viewport esiste anche nel renderer di Blink, come dettaglio di implementazione. Ecco il codice che applica gli stili predefiniti dell'area visibile in base alle specifiche:

scoped_refptr<ComputedStyle> StyleResolver::StyleForViewport() {
  scoped_refptr<ComputedStyle> viewport_style =
      InitialStyleForElement(GetDocument());
  viewport_style->SetZIndex(0);
  viewport_style->SetIsStackingContextWithoutContainment(true);
  viewport_style->SetDisplay(EDisplay::kBlock);
  viewport_style->SetPosition(EPosition::kAbsolute);
  viewport_style->SetOverflowX(EOverflow::kAuto);
  viewport_style->SetOverflowY(EOverflow::kAuto);
  // …
  return viewport_style;
}

Non è necessario conoscere C++ o le complessità del motore di stile di Blink per vedere che questo codice gestisce i valori z-index, display, position e overflow dell'area visibile (o, in modo più accurato, il blocco iniziale contenitore). Questi sono tutti concetti che potresti già conoscere in CSS. Esistono altri elementi magici relativi ai contesti di sovrapposizione, che non si traducono direttamente in una proprietà CSS, ma nel complesso potresti considerare questo oggetto viewport come qualcosa che può essere definito con CSS da Blink, proprio come un elemento DOM, tranne che non fa parte del DOM.

In questo modo possiamo ottenere esattamente quello che vogliamo. Possiamo applicare gli stili filter all'oggetto viewport, il che influisce visivamente sul rendering, senza interferire in alcun modo con gli stili di pagina osservabili o con il DOM.

Conclusione

Per ricapitolare il nostro piccolo viaggio qui, abbiamo iniziato creando un prototipo utilizzando la tecnologia web anziché C++, quindi abbiamo iniziato a lavorare allo spostamento di alcune parti di quest'ultimo su Blink Renderer.

  • Inizialmente abbiamo reso il nostro prototipo più indipendente integrando gli URL di dati.
  • Abbiamo quindi reso questi URL di dati interni ottimizzati per i CSP, inserendo il loro caricamento in caratteri speciali.
  • Abbiamo reso la nostra implementazione indipendente dal DOM e non osservabile programmaticamente spostando gli stili all'interno di Blink viewport.

L'aspetto unico di questa implementazione è che il nostro prototipo HTML/CSS/SVG ha finito per influenzare il design tecnico finale. Abbiamo trovato un modo per utilizzare la piattaforma web, anche all'interno del renderer di Blink.

Per maggiori informazioni, leggi la nostra proposta di progettazione o il bug di monitoraggio di Chromium che fa riferimento a tutte le patch correlate.

Scaricare i canali in anteprima

Prendi in considerazione l'utilizzo di Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, testare le API delle piattaforme web all'avanguardia e individuare i problemi sul tuo sito prima che lo facciano gli utenti.

Contattare il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere delle nuove funzionalità e modifiche nel post o di qualsiasi altra informazione relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools utilizzando Altre opzioni   Altro > Guida > Segnala un problema di DevTools in DevTools.
  • Invia un tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sulle novità nei video di YouTube di DevTools o nei video di YouTube dei suggerimenti di DevTools.