Simulazione di carenze della visione dei colori in Blink Renderer

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

Sfondo: scarso contrasto di colore

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

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

Secondo l'analisi di accessibilità di WebAIM del primo milione di siti web, oltre l'86% delle home page ha un basso contrasto. In media, ogni home page contiene 36 istanze distinte di testo a basso contrasto.

Utilizzare DevTools per trovare, 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 è un po' diverso dagli altri. Gli strumenti riportati sopra si concentrano principalmente sulla visualizzazione di informazioni sul rapporto di contrasto e offrono opzioni per correggerlo. Ci siamo resi conto che in DevTools mancava ancora un modo per consentire agli sviluppatori di comprendere più a fondo questo spazio di problemi. Per risolvere il problema, abbiamo implementato la simulazione di difetti alla vista nella scheda Rendering di DevTools.

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

Deficit della visione dei colori

Circa una persona su 20 soffre di una discromatopsia (nota anche con il termine meno preciso "daltonismo"). Questi disturbi rendono più difficile distinguere i diversi colori, il che può amplificare i problemi di contrasto.

Un'immagine colorata di pastelli fusi, senza simulazione di deficienze nella visione dei colori
Un'immagine colorata di pastelli sciolti, senza simulazione di deficienze nella visione dei colori.
ALT_TEXT_HERE
L'impatto della simulazione dell'acromatopsia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della deuteranopia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della deuteranopia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della protanopia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della protanopia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della tritanopia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della tritanopia su un'immagine colorata di pastelli fusi.

In qualità di sviluppatore con una vista normale, potresti vedere che DevTools mostra un rapporto di contrasto errato per coppie di colori che visivamente sembrano corrette. Questo accade perché le formule del rapporto di contrasto prendono in considerazione queste carenze nella visione dei colori. Tu potresti riuscire a leggere il testo a basso contrasto in alcuni casi, ma le persone con disabilità visive non hanno questo privilegio.

Consentendo a designer e sviluppatori di simulare l'effetto di queste carenze visive sulle proprie app web, il nostro obiettivo è fornire il tassello mancante: non solo DevTools può aiutarti a trovare e correggere i problemi di contrasto, ma ora puoi anche capirli.

Simulazione di discromatopsie con HTML, CSS, SVG e C++

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

Puoi considerare ciascuna di queste simulazioni di deficienza della visione dei colori come un overlay 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, come blur, contrast, grayscale, hue-rotate e molte altre. Per un controllo ancora maggiore, la proprietà filter accetta anche un URL che può puntare 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 riportato sopra utilizza una definizione di filtro personalizzato basata su una matrice di colori. In linea di principio, il valore di colore [Red, Green, Blue, Alpha] di ogni pixel viene moltiplicato per una matrice 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) e un quinto valore per uno spostamento costante. Esistono 4 righe: la prima riga della matrice viene utilizzata per calcolare il nuovo valore Rosso, la seconda riga Verde, la terza riga Blu e l'ultima riga Alfa.

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

Ad ogni modo, abbiamo questo filtro SVG e ora possiamo applicarlo a elementi arbitrari della pagina utilizzando il CSS. Possiamo ripetere lo stesso schema per altre carenze visive. Ecco una demo di come funziona:

Se volessimo, potremmo creare la nostra funzionalità di DevTools nel seguente modo: quando l'utente emula una deficienza visiva nell'interfaccia utente 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 sull'elemento principale, che il nostro codice potrebbe quindi sostituire.
  • La pagina potrebbe già avere un elemento con id="deuteranopia", in conflitto con la nostra definizione di filtro.
  • La pagina potrebbe basarsi su una determinata struttura DOM e, inserendo <svg> nel DOM, potremmo violare queste ipotesi.

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

Vediamo come possiamo renderlo meno invadente. Questa soluzione prevede due parti che dobbiamo nascondere: 1) lo stile CSS con la proprietà filter e 2) la definizione del filtro SVG, che al momento 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 SVG all'interno del documento

Iniziamo con la parte 2: come possiamo evitare di aggiungere l'SVG al DOM? Un'idea è spostarlo in un file SVG separato. Possiamo copiare <svg>…</svg> dal codice HTML riportato sopra e salvarlo come filter.svg, ma prima dobbiamo apportare alcune modifiche. SVG in linea in HTML segue le regole di analisi dell'HTML. Ciò significa che in alcuni casi puoi omettere le virgolette intorno ai valori degli attributi. Tuttavia, gli SVG in file separati dovrebbero essere XML validi e l'analisi XML è molto più rigorosa di quella 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 un SVG autonomo valido (e quindi 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 è 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 è in realtà necessaria (potremmo semplicemente attenerci al tag di chiusura </feColorMatrix> esplicito), ma poiché sia XML che SVG in HTML supportano questa abbreviazione </feColorMatrix>, potremmo anche utilizzarla./>

Ad ogni modo, con queste modifiche possiamo finalmente salvarlo come file SVG valido e fare riferimento al valore della proprietà CSS filter nel nostro documento HTML:

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

Evviva, non dobbiamo più iniettare SVG nel documento. Va già molto meglio. Ma… ora abbiamo bisogno di un file separato. Si tratta comunque di una dipendenza. Possiamo sbarazzarcene in qualche modo?

A quanto pare, non abbiamo bisogno di un file. Possiamo codificare l'intero file all'interno di un URL utilizzando un URL di dati. Per farlo, prendiamo letteralmente i contenuti del file SVG che avevamo prima, aggiungiamo il prefisso data:, configuriamo il tipo MIME appropriato e otteniamo un URL dei 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 è più necessario archiviare il file da nessuna parte o caricarlo dal disco o dalla rete solo per utilizzarlo nel documento HTML. Pertanto, anziché fare riferimento al nome del file come facevamo in precedenza, ora possiamo indicare l'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 ancora l'ID del filtro che vogliamo utilizzare, come prima. Tieni presente che non è necessario codificare in base64 il documento SVG nell'URL, perché ciò peggiorerebbe la leggibilità e aumenterebbe le dimensioni del file. Abbiamo aggiunto barre oblique inverse alla fine di ogni riga per assicurarci che i caratteri di a capo nell'URL dei dati non terminino la stringa letterale CSS.

Finora abbiamo parlato solo di come simulare le deficienze visive utilizzando la tecnologia web. È interessante notare che la nostra implementazione finale in Blink Renderer è in realtà molto simile. Ecco un'utilità di supporto C++ che abbiamo aggiunto per creare un URL dati con una determinata definizione di filtro, in base alla 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;
}

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 consente di accedere a tutte le funzionalità dei filtri SVG senza dover riimplementare nulla o reinventare la ruota. Stiamo implementando una funzionalità del Renderer Blink, ma lo stiamo facendo sfruttando la piattaforma web.

Bene, abbiamo capito come creare filtri SVG e trasformarli in URL di dati che possiamo utilizzare all'interno del valore della proprietà filter CSS. Riesci a pensare a un problema con questa tecnica? In realtà, non possiamo fare affidamento sul fatto che l'URL dei dati venga caricato in tutti i casi, poiché la pagina di destinazione potrebbe avere un Content-Security-Policy che blocca gli URL dei dati. La nostra implementazione finale a livello di Blink presta particolare attenzione a bypassare il CSP per questi URL dei dati "interni" durante il caricamento.

A parte i casi limite, abbiamo fatto buoni progressi. Poiché non dipendiamo più dalla presenza di <svg> in linea nello stesso documento, abbiamo ridotto efficacemente la nostra soluzione a una singola definizione di proprietà filter CSS autonomo. Bene. Ora sbarazzamoci anche di questo.

Evitare la dipendenza dal CSS all'interno del documento

Per ricapitolare, ecco dove siamo finora:

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

Dipendiamo ancora da questa proprietà CSS filter, che potrebbe sostituire un filter nel documento reale e causare dei problemi. Verrebbe visualizzato anche durante l'ispezione degli stili calcolati in DevTools, il che potrebbe creare confusione. Come possiamo evitare questi problemi? Dobbiamo trovare un modo per aggiungere un filtro al documento senza che sia osservabile dagli sviluppatori tramite programmazione.

Un'idea che è emersa è stata quella di creare una nuova proprietà CSS interna di Chrome che si comporti come filter, ma abbia un nome diverso, ad esempio --internal-devtools-filter. Potremmo quindi aggiungere una logica speciale per assicurarci che questa proprietà non venga mai visualizzata in DevTools o negli stili calcolati nel DOM. Potremmo anche assicurarci che funzioni solo sull'elemento di cui abbiamo bisogno: l'elemento principale. Tuttavia, questa soluzione non sarebbe ideale: duplicheremmo la funzionalità già esistente con filter e, anche se ci sforziamo 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 contiene una sezione che introduce il modello di formattazione visiva utilizzato e uno dei concetti chiave è il viewport. Si tratta della visualizzazione visiva attraverso la quale gli utenti consultano la pagina web. Un concetto strettamente correlato è il blocco contenitore iniziale, che è una sorta di viewport personalizzabile <div> esistente solo a livello di specifiche. La specifica fa riferimento a questo concetto di "viewport" ovunque. Ad esempio, sai come il browser mostra le barre di scorrimento quando i contenuti non si adattano? Tutto questo è definito nella specifica CSS, in base a questo "viewport".

Questo viewport esiste anche nel Renderer Blink, come dettaglio di implementazione. Ecco il codice che applica gli stili dell'area visibile predefiniti 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 capire che questo codice gestisce z-index, display, position e overflow del viewport (o, più precisamente, del blocco contenitore iniziale). Si tratta di concetti che potresti conoscere dal CSS. Esistono altri elementi magici relativi ai contesti di impilamento, che non si traducono direttamente in una proprietà CSS, ma in generale puoi considerare questo oggetto viewport come qualcosa a cui è possibile applicare uno stile utilizzando CSS da Blink, proprio come un elemento DOM, tranne per il fatto che non fa parte del DOM.

Ci dà esattamente ciò che vogliamo. Possiamo applicare i nostri stili filter all'oggetto viewport, 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, abbiamo iniziato creando un prototipo utilizzando la tecnologia web anziché C++, quindi abbiamo iniziato a spostarne parti nel Renderer Blink.

  • Innanzitutto, abbiamo reso il nostro prototipo più autonomo inserendo in linea gli URL dei dati.
  • Abbiamo poi reso questi URL dei dati interni compatibili con i CSP, trattando in modo speciale il loro caricamento.
  • Abbiamo reso la nostra implementazione indipendente dal DOM e non osservabile tramite programmazione spostando gli stili in viewport interno a Blink.

La particolarità 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 Blink.

Per ulteriori informazioni, consulta la nostra proposta di design o il bug di monitoraggio di Chromium che fa riferimento a tutte le patch correlate.

Scaricare i canali di anteprima

Valuta la possibilità di utilizzare Chrome Canary, Dev o Beta come browser di sviluppo predefinito. Questi canali di anteprima ti consentono di accedere alle funzionalità più recenti di DevTools, di testare API di piattaforme web all'avanguardia e di trovare i problemi sul tuo sito prima che lo facciano i tuoi utenti.

Contatta il team di Chrome DevTools

Utilizza le seguenti opzioni per discutere di nuove funzionalità, aggiornamenti o qualsiasi altro argomento relativo a DevTools.