Mô phỏng tình trạng thiếu thị lực màu trong Trình kết xuất Blink

Bài viết này mô tả lý do và cách chúng tôi triển khai tính năng mô phỏng khiếm thị màu trong DevTools và Trình kết xuất Blink.

Nền: độ tương phản màu kém

Văn bản có độ tương phản thấp là vấn đề về khả năng hỗ trợ tiếp cận phổ biến nhất có thể tự động phát hiện được trên web.

Danh sách các vấn đề phổ biến về hỗ trợ tiếp cận trên web. Văn bản có độ tương phản thấp là vấn đề thường gặp nhất.

Theo dữ liệu phân tích về khả năng tiếp cận của WebAIM đối với 1 triệu trang web hàng đầu, hơn 86% số trang chủ có độ tương phản thấp. Trung bình, mỗi trang chủ có 36 trường hợp riêng biệt của văn bản có độ tương phản thấp.

Sử dụng Công cụ dành cho nhà phát triển để tìm, hiểu và khắc phục các vấn đề về độ tương phản

Công cụ của Chrome cho nhà phát triển có thể giúp nhà phát triển và nhà thiết kế cải thiện độ tương phản cũng như chọn bảng phối màu dễ tiếp cận hơn cho ứng dụng web:

Gần đây, chúng tôi đã thêm một công cụ mới vào danh sách này và công cụ này hơi khác so với những công cụ khác. Các công cụ trên chủ yếu tập trung vào việc hiển thị thông tin về tỷ lệ tương phản và cung cấp cho bạn các lựa chọn để khắc phục vấn đề này. Chúng tôi nhận thấy rằng DevTools vẫn thiếu một cách để nhà phát triển hiểu rõ hơn về không gian vấn đề này. Để giải quyết vấn đề này, chúng tôi đã triển khai mô phỏng tình trạng khiếm thị trong thẻ Kết xuất trong Công cụ cho nhà phát triển.

Trong Puppeteer, API page.emulateVisionDeficiency(type) mới cho phép bạn bật các hoạt động mô phỏng này theo phương thức lập trình.

Khiếm khuyết về thị lực màu

Khoảng 1/20 người bị hội chứng thiếu thị lực màu (còn gọi là "mù màu" theo cách gọi không chính xác). Những khiếm khuyết như vậy khiến khó phân biệt các màu sắc khác nhau, điều này có thể làm trầm trọng thêm các vấn đề về độ tương phản.

Hình ảnh đầy màu sắc của bút chì màu đã tan chảy, không có tình trạng thiếu hụt thị lực màu được mô phỏng
Hình ảnh đầy màu sắc của bút sáp nến đã tan chảy, không có hội chứng mù màu được mô phỏng.
ALT_TEXT_HERE
Ảnh hưởng của việc mô phỏng chứng mù màu đối với một bức ảnh đầy màu sắc về bút sáp màu đã tan chảy.
Ảnh hưởng của việc mô phỏng chứng mù màu xanh lục đối với một bức ảnh đầy màu sắc về bút sáp màu đã tan chảy.
Tác động của việc mô phỏng chứng mù màu xanh lục trên một bức tranh nhiều màu sắc của bút chì màu tan chảy.
Tác động của việc mô phỏng chứng mù màu đỏ trên một bức ảnh đầy màu sắc của bút sáp màu tan chảy.
Ảnh hưởng của việc mô phỏng chứng mù màu đỏ nhẹ đối với một bức ảnh đầy màu sắc về bút sáp màu đã tan chảy.
Ảnh hưởng của việc mô phỏng chứng mù màu lam vàng nhẹ đối với một bức ảnh đầy màu sắc về bút sáp màu đã tan chảy.
Ảnh hưởng của việc mô phỏng chứng mù màu xanh lam đối với một bức ảnh đầy màu sắc về bút sáp màu đã tan chảy.

Là một nhà phát triển có thị lực bình thường, bạn có thể thấy DevTools hiển thị tỷ lệ tương phản không tốt cho các cặp màu trông ổn với bạn. Điều này xảy ra vì các công thức tỷ lệ tương phản tính đến những khiếm khuyết về thị lực màu này! Bạn vẫn có thể đọc được văn bản có độ tương phản thấp trong một số trường hợp, nhưng người khiếm thị thì không.

Bằng cách cho phép nhà thiết kế và nhà phát triển mô phỏng hiệu ứng của những khiếm khuyết thị giác này trên ứng dụng web của riêng họ, chúng tôi muốn cung cấp phần còn thiếu: giờ đây, DevTools không chỉ giúp bạn tìmkhắc phục các vấn đề về độ tương phản, mà bạn còn có thể hiểu rõ các vấn đề đó!

Mô phỏng khiếm khuyết về thị giác màu với HTML, CSS, SVG và C++

Trước khi tìm hiểu kỹ hơn về cách triển khai tính năng Blink Renderer (Trình kết xuất Blink) cho tính năng này, chúng tôi muốn tìm hiểu cách bạn sẽ triển khai chức năng tương đương bằng công nghệ web.

Bạn có thể coi mỗi mô phỏng khiếm thị màu này là một lớp phủ bao phủ toàn bộ trang. Nền tảng web có một cách để làm điều đó: bộ lọc CSS! Với thuộc tính filter của CSS, bạn có thể sử dụng một số hàm bộ lọc định sẵn, chẳng hạn như blur, contrast, grayscale, hue-rotate và nhiều hàm khác. Để kiểm soát nhiều hơn nữa, thuộc tính filter cũng chấp nhận một URL có thể trỏ đến định nghĩa bộ lọc SVG tuỳ chỉnh:

<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>

Ví dụ trên sử dụng định nghĩa bộ lọc tuỳ chỉnh dựa trên ma trận màu. Về mặt lý thuyết, giá trị màu [Red, Green, Blue, Alpha] của mỗi pixel được nhân với ma trận để tạo ra một màu mới [R′, G′, B′, A′].

Mỗi hàng trong ma trận chứa 5 giá trị: hệ số cho (từ trái sang phải) R, G, B và A, cũng như giá trị thứ năm cho giá trị dịch không đổi. Có 4 hàng: hàng đầu tiên của ma trận được dùng để tính giá trị Đỏ mới, hàng thứ hai Xanh lục, hàng thứ ba Xanh dương và hàng cuối cùng Alpha.

Bạn có thể thắc mắc những con số chính xác trong ví dụ của chúng tôi đến từ đâu. Điều gì khiến ma trận màu này gần đúng với chứng mù màu xanh lục? Câu trả lời là: khoa học! Các giá trị này dựa trên mô hình mô phỏng thiếu hụt thị lực màu chính xác về mặt sinh lý của Machado, Oliveira và Fernandes.

Dù sao, chúng ta đã có bộ lọc SVG này và giờ đây có thể áp dụng bộ lọc đó cho các phần tử tuỳ ý trên trang bằng CSS. Chúng ta có thể lặp lại cùng một mẫu cho các khiếm khuyết thị giác khác. Sau đây là bản minh hoạ về cách thực hiện:

Nếu muốn, chúng ta có thể xây dựng tính năng DevTools như sau: khi người dùng mô phỏng tình trạng khiếm thị trong giao diện người dùng DevTools, chúng ta sẽ chèn bộ lọc SVG vào tài liệu đã kiểm tra, sau đó áp dụng kiểu bộ lọc trên phần tử gốc. Tuy nhiên, phương pháp đó có một số vấn đề:

  • Trang có thể đã có một bộ lọc trên phần tử gốc mà mã của chúng ta sau đó có thể ghi đè.
  • Trang có thể đã có một phần tử với id="deuteranopia", xung đột với định nghĩa bộ lọc của chúng ta.
  • Trang có thể dựa vào một cấu trúc DOM nhất định và bằng cách chèn <svg> vào DOM, chúng ta có thể vi phạm các giả định này.

Ngoài các trường hợp hiếm gặp, vấn đề chính với phương pháp này là chúng ta sẽ thực hiện các thay đổi có thể quan sát được theo phương thức lập trình đối với trang. Nếu người dùng DevTools kiểm tra DOM, họ có thể đột nhiên thấy một phần tử <svg> mà họ chưa bao giờ thêm hoặc một filter CSS mà họ chưa bao giờ viết. Điều đó sẽ gây nhầm lẫn! Để triển khai chức năng này trong DevTools, chúng ta cần một giải pháp không có những hạn chế này.

Hãy xem cách chúng ta có thể làm cho thông báo này ít gây phiền toái hơn. Có hai phần trong giải pháp này mà chúng ta cần ẩn: 1) kiểu CSS có thuộc tính filter và 2) định nghĩa bộ lọc SVG, hiện là một phần của 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>

Tránh phần phụ thuộc SVG trong tài liệu

Hãy bắt đầu với phần 2: làm cách nào để tránh thêm SVG vào DOM? Một ý tưởng là di chuyển tệp đó sang một tệp SVG riêng. Chúng ta có thể sao chép <svg>…</svg> từ HTML ở trên và lưu dưới dạng filter.svg, nhưng trước tiên chúng ta cần thực hiện một số thay đổi! SVG nội tuyến trong HTML tuân theo các quy tắc phân tích cú pháp HTML. Điều đó có nghĩa là bạn có thể bỏ qua những việc như bỏ qua dấu ngoặc kép xung quanh giá trị thuộc tính trong một số trường hợp. Tuy nhiên, SVG trong các tệp riêng biệt phải là XML hợp lệ và việc phân tích cú pháp XML nghiêm ngặt hơn nhiều so với HTML. Dưới đây là đoạn mã SVG trong HTML của chúng tôi:

<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>

Để tạo SVG độc lập hợp lệ này (và do đó là XML), chúng ta cần thực hiện một số thay đổi. Bạn có đoán được đó là sản phẩm nào không?

<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>

Thay đổi đầu tiên là khai báo không gian tên XML ở trên cùng. Phần bổ sung thứ hai là dấu gạch chéo được gọi là "solidus", cho biết thẻ <feColorMatrix> vừa mở vừa đóng phần tử. Thay đổi cuối cùng này thực sự không cần thiết (chúng ta chỉ cần sử dụng thẻ đóng </feColorMatrix> rõ ràng), nhưng vì cả XML và SVG-in-HTML đều hỗ trợ viết tắt /> này, nên chúng ta cũng có thể sử dụng nó.

Cuối cùng, với những thay đổi đó, chúng ta có thể lưu tệp này dưới dạng tệp SVG hợp lệ và trỏ đến tệp đó từ giá trị thuộc tính filter CSS trong tài liệu HTML:

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

Rất tiếc, chúng ta không còn phải chèn SVG vào tài liệu nữa! Như vậy đã tốt hơn nhiều. Nhưng… giờ đây, chúng ta phụ thuộc vào một tệp riêng biệt. Đó vẫn là một phần phụ thuộc. Chúng ta có thể loại bỏ vấn đề này bằng cách nào không?

Hóa ra, chúng ta không thực sự cần tệp. Chúng ta có thể mã hoá toàn bộ tệp trong một URL bằng cách sử dụng URL dữ liệu. Để thực hiện việc này, chúng ta sẽ lấy nội dung của tệp SVG mà chúng ta có trước đó, thêm tiền tố data:, định cấu hình loại MIME thích hợp và chúng ta đã có một URL dữ liệu hợp lệ đại diện cho chính tệp 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>

Lợi ích là giờ đây, chúng ta không còn phải lưu trữ tệp ở bất cứ đâu, hoặc tải tệp từ ổ đĩa hoặc qua mạng mà chỉ cần sử dụng tệp trong tài liệu HTML. Vì vậy, thay vì tham chiếu đến tên tệp như trước đây, giờ đây chúng ta có thể trỏ đến URL dữ liệu:

<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>

Ở cuối URL, chúng ta vẫn ghi rõ mã nhận dạng của bộ lọc mà mình muốn sử dụng, giống như trước đó. Xin lưu ý rằng bạn không cần mã hoá Base64 cho tài liệu SVG trong URL. Việc này chỉ làm giảm khả năng đọc và tăng kích thước tệp. Chúng tôi đã thêm dấu gạch chéo ngược vào cuối mỗi dòng để đảm bảo các ký tự dòng mới trong URL dữ liệu không kết thúc chuỗi cố định CSS.

Cho đến nay, chúng ta chỉ mới bàn về cách mô phỏng các khiếm khuyết về thị giác bằng công nghệ web. Điều thú vị là cách triển khai cuối cùng của chúng ta trong Trình kết xuất Blink thực sự khá giống nhau. Sau đây là tiện ích trợ giúp C++ mà chúng tôi đã thêm vào để tạo URL dữ liệu với định nghĩa bộ lọc cho trước, dựa trên cùng một kỹ thuật:

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;
}

Sau đây là cách chúng ta sử dụng lớp này để tạo tất cả bộ lọc cần thiết:

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 "";
  }
}

Lưu ý rằng kỹ thuật này cho phép chúng ta tiếp cận toàn bộ sức mạnh của bộ lọc SVG mà không cần phải triển khai lại bất kỳ thứ gì hoặc sáng chế lại bất kỳ bánh xe nào. Chúng tôi đang triển khai tính năng Trình kết xuất Blink, nhưng chúng tôi sẽ triển khai bằng cách tận dụng Nền tảng web.

Vậy là chúng ta đã tìm được cách tạo bộ lọc SVG và chuyển chúng thành các URL dữ liệu để sử dụng trong giá trị thuộc tính filter CSS. Bạn có nghĩ ra vấn đề nào với kỹ thuật này không? Hoá ra là chúng ta không thể dựa vào URL dữ liệu được tải trong mọi trường hợp, vì trang đích có thể có Content-Security-Policy chặn URL dữ liệu. Quy trình triển khai cấp Blink cuối cùng của chúng tôi cần đặc biệt chú ý để bỏ qua CSP cho các URL dữ liệu “nội bộ” này trong quá trình tải.

Ngoài các trường hợp hiếm gặp, chúng tôi đã đạt được một số tiến bộ đáng kể. Vì không còn phụ thuộc vào <svg> nội tuyến có trong cùng một tài liệu, nên chúng ta đã giảm hiệu quả giải pháp của mình xuống chỉ còn một định nghĩa thuộc tính filter CSS độc lập. Tuyệt vời! Bây giờ, hãy loại bỏ cả phần đó.

Tránh phần phụ thuộc CSS trong tài liệu

Tóm lại, chúng ta đã đạt được những thành tựu sau:

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

Chúng ta vẫn phụ thuộc vào thuộc tính filter CSS này. Thuộc tính này có thể ghi đè filter trong tài liệu thực và làm hỏng mọi thứ. Lỗi này cũng sẽ xuất hiện khi kiểm tra các kiểu đã tính toán trong DevTools, gây nhầm lẫn. Làm cách nào để tránh những vấn đề này? Chúng ta cần tìm cách thêm bộ lọc vào tài liệu mà nhà phát triển không thể quan sát được bằng cách lập trình.

Một ý tưởng được đưa ra là tạo một thuộc tính CSS nội bộ mới của Chrome hoạt động giống như filter, nhưng có tên khác, chẳng hạn như --internal-devtools-filter. Sau đó, chúng ta có thể thêm logic đặc biệt để đảm bảo thuộc tính này không bao giờ xuất hiện trong DevTools hoặc trong các kiểu được tính toán trong DOM. Thậm chí, chúng tôi còn có thể đảm bảo công cụ này chỉ hoạt động trên một phần tử mà chúng tôi cần: phần tử gốc. Tuy nhiên, giải pháp này không phải là lý tưởng: chúng ta sẽ sao chép chức năng đã có với filter và ngay cả khi chúng ta cố gắng ẩn thuộc tính không chuẩn này, các nhà phát triển web vẫn có thể tìm hiểu và bắt đầu sử dụng thuộc tính này, điều này sẽ gây bất lợi cho Nền tảng web. Chúng ta cần một số cách khác để áp dụng kiểu CSS mà không cần quan sát được trong DOM. Bạn có đề xuất nào không?

Quy cách CSS có một phần giới thiệu về mô hình định dạng hình ảnh mà nó sử dụng và một trong những khái niệm chính ở đó là khung nhìn. Đây là chế độ xem trực quan mà qua đó người dùng tham khảo trang web. Một khái niệm liên quan chặt chẽ là khối chứa tên viết tắt, giống như một khung nhìn có thể tạo kiểu <div> chỉ tồn tại ở cấp độ thông số kỹ thuật. Thông số kỹ thuật đề cập đến khái niệm "khung nhìn" này ở khắp nơi. Ví dụ: bạn có biết trình duyệt hiển thị thanh cuộn như thế nào khi nội dung không vừa không? Tất cả đều được xác định trong quy cách CSS, dựa trên "cửa sổ xem" này.

viewport này cũng tồn tại trong Trình kết xuất Blink, dưới dạng chi tiết triển khai. Sau đây là mã áp dụng các kiểu khung nhìn mặc định theo thông số kỹ thuật:

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;
}

Bạn không cần phải hiểu về C++ hoặc các chi tiết phức tạp của công cụ Style (Kiểu của Blink) để đảm bảo rằng mã này xử lý z-index, display, positionoverflow của khung nhìn (hay chính xác hơn là khối ban đầu chứa khối). Đó là tất cả các khái niệm mà bạn có thể đã quen thuộc với CSS! Có một số tính năng kỳ diệu khác liên quan đến ngữ cảnh xếp chồng, không trực tiếp chuyển đổi thành thuộc tính CSS, nhưng nhìn chung, bạn có thể coi đối tượng viewport này là một đối tượng có thể được tạo kiểu bằng CSS từ bên trong Blink, giống như một phần tử DOM, ngoại trừ đối tượng này không phải là một phần của DOM.

Điều này giúp chúng ta có được chính xác những gì mình muốn! Chúng ta có thể áp dụng kiểu filter cho đối tượng viewport. Kiểu này ảnh hưởng trực quan đến quá trình kết xuất mà không can thiệp vào kiểu trang có thể quan sát được hoặc DOM theo bất kỳ cách nào.

Kết luận

Để tóm tắt hành trình nhỏ của chúng ta tại đây, chúng ta bắt đầu bằng cách tạo một nguyên mẫu bằng công nghệ web thay vì C++, sau đó bắt đầu di chuyển các phần của nguyên mẫu đó sang Trình kết xuất Blink.

  • Trước tiên, chúng tôi đã tạo bản nguyên mẫu độc lập hơn bằng cách nội tuyến các URL dữ liệu.
  • Sau đó, chúng tôi đã tạo các URL dữ liệu nội bộ đó thân thiện với CSP bằng cách tải các URL đó theo cách đặc biệt.
  • Chúng tôi đã triển khai DOM không phân biệt và không thể quan sát được theo phương thức lập trình bằng cách di chuyển các kiểu sang viewport nội bộ của Blink.

Điều độc đáo về cách triển khai này là nguyên mẫu HTML/CSS/SVG của chúng tôi đã ảnh hưởng đến thiết kế kỹ thuật cuối cùng. Chúng tôi đã tìm ra cách sử dụng Nền tảng web, ngay cả trong Trình kết xuất Blink!

Để biết thêm thông tin cơ bản, hãy xem đề xuất thiết kế của chúng tôi hoặc lỗi theo dõi Chromium tham chiếu đến tất cả các bản vá liên quan.

Tải kênh xem trước xuống

Hãy cân nhắc sử dụng Chrome Canary, Dev hoặc Beta làm trình duyệt phát triển mặc định. Các kênh xem trước này cho phép bạn sử dụng các tính năng mới nhất của DevTools, kiểm thử các API nền tảng web tiên tiến và giúp bạn tìm thấy vấn đề trên trang web của mình trước khi người dùng phát hiện ra!

Liên hệ với nhóm Công cụ của Chrome cho nhà phát triển

Hãy sử dụng các lựa chọn sau để thảo luận về các tính năng, bản cập nhật mới hoặc bất kỳ nội dung nào khác liên quan đến Công cụ cho nhà phát triển.