Simulazione di carenze della visione dei colori in Blink Renderer

Mathias Bynens
Mathias Bynens

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

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 è di gran lunga il problema più comune.

Secondo l'analisi dell'accessibilità di WebAIM sul milione di siti web principali, oltre l'86% delle home page ha un basso contrasto. In media, ogni home page ha 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 ed è un po' diverso dagli altri. Gli strumenti sopra citati si concentrano principalmente sulla visualizzazione delle informazioni sul rapporto di contrasto e offrono opzioni per correggerle. Ci siamo resi conto che DevTools mancava ancora un modo per consentire agli sviluppatori di ottenere una understanding più approfondita di questo spazio problematico. Per risolvere il problema, abbiamo implementato la simulazione dei difetti di vista nella scheda Rendering DevTools.

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

Carenze nella visione dei colori

Circa una persona su 20 soffre di un deficit della visione dal colore (noto anche come il termine meno preciso "daltonismo"). Tali problemi rendono più difficile distinguere i diversi colori, il che può amplificare i problemi di contrasto.

Un'immagine colorata di pastelli fondenti, senza difetti di visione cromatica simulata
Un'immagine colorata di pastelli fondenti, senza anomalie nella visione dei colori.
ALT_TEXT_HERE
L'impatto della simulazione di acromatopsia su un'immagine colorata di pastelli fusi.
L'impatto della simulazione della deuteranopia su un'immagine colorata di pastelli fondenti.
L'impatto della simulazione di deuteranopia su un'immagine colorata di pastelli fondenti.
L'impatto della simulazione di protanopia su un'immagine colorata di pastelli fondenti.
L'impatto della simulazione di protanopia su un'immagine colorata di pastelli fondenti.
L'impatto della simulazione di tritanopia su un'immagine colorata di pastelli fondenti.
L'impatto della simulazione di tritanopia su un'immagine colorata di pastelli fondenti.

In qualità di sviluppatore che ha una visione regolare, potresti notare che DevTools mostra un rapporto di contrasto non valido per coppie di colori che sembrano a posto. Ciò accade perché le formule del rapporto di contrasto tengono conto di queste carenze della visione dei colori. In alcuni casi potresti comunque essere in grado di leggere il testo a basso contrasto, ma le persone con problemi di vista non hanno questo privilegio.

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

Simulazione di carenze nella 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 del deficit della visione cromatica 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, come 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 riportato sopra utilizza una definizione di filtro personalizzata basata su una matrice dei colori. Concettualmente, il valore di colore [Red, Green, Blue, Alpha] di ogni pixel viene moltiplicato per matrice per creare un nuovo colore [R′, G′, B′, A′].

Ogni riga della matrice contiene 5 valori: un moltiplicatore per (da sinistra a destra) R, G, B e A, 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 di rosso, la seconda di verde, la terza di blu e l'ultima di alfa.

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

Ad ogni modo, abbiamo questo filtro SVG e ora possiamo applicarlo a elementi arbitrari sulla pagina utilizzando CSS. Possiamo ripetere lo stesso schema per altre carenze della vista. Ecco una demo di ciò che appare:

Se volessimo, potremmo creare la nostra funzionalità DevTools come segue: quando l'utente emula un difetto di visione nell'interfaccia utente di DevTools, inseriamo il filtro SVG nel documento controllato e applichiamo lo stile del filtro all'elemento principale. Tuttavia, questo approccio presenta diversi problemi:

  • La pagina potrebbe avere già un filtro sull'elemento principale, che il nostro codice potrebbe sostituire.
  • La pagina potrebbe avere già un elemento con id="deuteranopia", in conflitto con la nostra definizione del 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 apportiamo modifiche osservabili in modo programmatico alla pagina. 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. Non sarebbe chiaro. Per implementare questa funzionalità in DevTools, è necessaria una soluzione che non presenti questi svantaggi.

Vediamo come possiamo renderlo meno invadente. Questa soluzione è composta da due parti che dobbiamo nascondere: 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 SVG nel documento

Iniziamo con la parte 2: come possiamo evitare di aggiungere il formato SVG al DOM? Potresti spostarlo in un file SVG separato. Possiamo copiare il <svg>…</svg> dal codice HTML riportato sopra e salvarlo come filter.svg, ma prima dobbiamo apportare alcune modifiche. Il file SVG incorporato in HTML segue le regole di analisi HTML. Ciò significa che è possibile risolvere problemi come l'omissione delle virgolette per i valori degli attributi. Tuttavia, i file SVG in file separati dovrebbero essere in formato XML valido, mentre l'analisi XML è molto più rigorosa 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 rendere questo file SVG autonomo (e quindi XML) valido, 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 in realtà non è necessaria (potremmo solo attenerti al tag di chiusura esplicito </feColorMatrix>), ma poiché sia XML che SVG-in-HTML supportano questa scorciatoia />, potremmo farne uso.

In ogni caso, con queste modifiche possiamo finalmente salvarlo come file SVG valido e indirizzarlo dal valore della proprietà filter CSS nel nostro documento HTML:

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

Evviva, non dobbiamo più inserire il formato SVG nel documento. Questo è già molto meglio. Ma... ora dipendiamo da un file separato. Questa è ancora una dipendenza. Possiamo in qualche modo liberarci?

In realtà 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 in precedenza, aggiungiamo il prefisso data:, configuriamo il tipo MIME corretto e ci occuperemo di ottenere 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, né caricarlo dal disco o sulla rete solo per utilizzarlo nel nostro documento HTML. Quindi, invece di fare riferimento al nome file come abbiamo fatto prima, ora 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 abbiamo fatto prima. Tieni presente che non è necessario codificare in Base64 il documento SVG nell'URL, altrimenti comprometterebbe la leggibilità e aumentare le dimensioni del file. Abbiamo aggiunto barre rovesciate alla fine di ogni riga per garantire 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 di vista utilizzando la tecnologia web. È interessante notare che la nostra implementazione finale in Blink Renderer è in realtà abbastanza simile. Ecco un'utilità di supporto per 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;
}

Ecco come li 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 dà accesso a tutta la potenza dei filtri SVG senza dover implementare nulla o reinventare qualsiasi ruota. Stiamo implementando una funzionalità Blink Renderer, ma lo stiamo facendo sfruttando la piattaforma web.

Bene, abbiamo capito come creare i filtri SVG e convertirli in URL di dati da utilizzare nel valore della proprietà filter CSS. Vi viene in mente un problema con questa tecnica? In realtà non possiamo fare affidamento sull'URL dei dati caricato in tutti i casi, 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 ignorare il 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> incorporato nello stesso documento, abbiamo ridotto la nostra soluzione a un'unica definizione di proprietà filter CSS autonoma. Bene. Ora eliminiamoci anche questo.

Evitare la dipendenza CSS nel documento

Per ricapitolare, ecco a che punto siamo arrivati finora:

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

Dipendiamo ancora da questa proprietà filter CSS, che potrebbe sostituire un valore filter nel documento reale e causare errori. Verrà visualizzato anche durante l'ispezione degli stili elaborati in DevTools, il che non sarebbe chiaro. Come possiamo evitare questi problemi? Dobbiamo trovare un modo per aggiungere un filtro al documento senza che sia osservabile in modo programmatico dagli sviluppatori.

Un'idea è nata per creare una nuova proprietà CSS interna a Chrome che funzioni come filter, ma abbia 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 ne abbiamo bisogno: l'elemento principale. Tuttavia, questa soluzione non sarebbe l'ideale: duplicare le funzionalità già esistenti con filter e, anche se facciamo del possibile per 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 che utilizza 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 è il blocco contenitore iniziale, che assomiglia a un'area visibile con stile <div> che esiste solo a livello di specifica. Le specifiche fanno riferimento a questo concetto di "area visibile" in tutto il luogo. Ad esempio, sai come il browser mostra le barre di scorrimento quando i contenuti non sono adatti? Tutto questo è definito nelle specifiche CSS, in base a questa "area visibile".

Questo viewport esiste anche all'interno del renderer 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 il C++ o le complessità del motore Stile di Blink per vedere che questo codice gestisce i valori z-index, display, position e overflow dell'area visibile (o più precisamente del blocco contenitore iniziale). Questi sono tutti concetti che potresti conoscere con i CSS. C'è un'altra magia legata allo stack dei contesti, che non si traduce direttamente in una proprietà CSS, ma nel complesso potresti pensare a questo oggetto viewport come a qualcosa a cui puoi assegnare uno stile utilizzando CSS all'interno di Blink, proprio come un elemento DOM, tranne per il fatto che non fa parte del DOM.

È esattamente quello che vogliamo. Possiamo applicare i nostri 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 riassumere il nostro piccolo viaggio qui, abbiamo iniziato creando un prototipo utilizzando la tecnologia web anziché il C++, quindi abbiamo iniziato a spostare parti di tale processo in Blink Renderer.

  • Innanzitutto, abbiamo reso il nostro prototipo più autonomo integrando gli URL di dati.
  • Abbiamo quindi reso gli URL di dati interni ottimizzati per CSP, utilizzando le maiuscole e le minuscole in modo speciale.
  • Abbiamo reso la nostra implementazione indipendente dal DOM e non osservabile in modo programmatico spostando gli stili su viewport interno a Blink.

La caratteristica unica di questa implementazione è che il nostro prototipo HTML/CSS/SVG ha finito per influenzare il progetto tecnico finale. Abbiamo trovato un modo per usare la piattaforma web, anche all'interno di Blink Renderer.

Per saperne di più, dai un'occhiata alla nostra proposta di progettazione o al 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 in anteprima ti consentono di accedere alle ultime funzionalità DevTools, testare le API delle piattaforme web all'avanguardia e individuare eventuali 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 qualsiasi altra informazione relativa a DevTools.

  • Inviaci un suggerimento o un feedback tramite crbug.com.
  • Segnala un problema di DevTools utilizzando Altre opzioni   Altre   > Guida > Segnala i problemi di DevTools in DevTools.
  • Tweet all'indirizzo @ChromeDevTools.
  • Lascia commenti sui video di YouTube o sui suggerimenti di DevTools sui video di YouTube di DevTools.