chrome-devtools-frontend
Version:
Chrome DevTools UI
194 lines (164 loc) • 6.95 kB
text/typescript
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as Root from '../root/root.js';
import * as UI from '../ui/ui.js';
import {ContrastInfo, Events} from './ContrastInfo.js'; // eslint-disable-line no-unused-vars
export class ContrastOverlay {
_contrastInfo: ContrastInfo;
_visible: boolean;
_contrastRatioSVG: Element;
_contrastRatioLines: Map<string, Element>;
_width: number;
_height: number;
_contrastRatioLineBuilder: ContrastRatioLineBuilder;
_contrastRatioLinesThrottler: Common.Throttler.Throttler;
_drawContrastRatioLinesBound: () => Promise<void>;
constructor(contrastInfo: ContrastInfo, colorElement: Element) {
this._contrastInfo = contrastInfo;
this._visible = false;
this._contrastRatioSVG = UI.UIUtils.createSVGChild(colorElement, 'svg', 'spectrum-contrast-container fill');
this._contrastRatioLines = new Map();
if (Root.Runtime.experiments.isEnabled('APCA')) {
this._contrastRatioLines.set(
'APCA', UI.UIUtils.createSVGChild(this._contrastRatioSVG, 'path', 'spectrum-contrast-line'));
} else {
this._contrastRatioLines.set(
'aa', UI.UIUtils.createSVGChild(this._contrastRatioSVG, 'path', 'spectrum-contrast-line'));
this._contrastRatioLines.set(
'aaa', UI.UIUtils.createSVGChild(this._contrastRatioSVG, 'path', 'spectrum-contrast-line'));
}
this._width = 0;
this._height = 0;
this._contrastRatioLineBuilder = new ContrastRatioLineBuilder(this._contrastInfo);
this._contrastRatioLinesThrottler = new Common.Throttler.Throttler(0);
this._drawContrastRatioLinesBound = this._drawContrastRatioLines.bind(this);
this._contrastInfo.addEventListener(Events.ContrastInfoUpdated, this._update.bind(this));
}
_update(): void {
if (!this._visible || this._contrastInfo.isNull()) {
return;
}
if (Root.Runtime.experiments.isEnabled('APCA') && this._contrastInfo.contrastRatioAPCA() === null) {
return;
}
if (!this._contrastInfo.contrastRatio()) {
return;
}
this._contrastRatioLinesThrottler.schedule(this._drawContrastRatioLinesBound);
}
setDimensions(width: number, height: number): void {
this._width = width;
this._height = height;
this._update();
}
setVisible(visible: boolean): void {
this._visible = visible;
this._contrastRatioSVG.classList.toggle('hidden', !visible);
this._update();
}
async _drawContrastRatioLines(): Promise<void> {
for (const [level, element] of this._contrastRatioLines) {
const path = this._contrastRatioLineBuilder.drawContrastRatioLine(this._width, this._height, level as string);
if (path) {
element.setAttribute('d', path);
} else {
element.removeAttribute('d');
}
}
}
}
export class ContrastRatioLineBuilder {
_contrastInfo: ContrastInfo;
constructor(contrastInfo: ContrastInfo) {
this._contrastInfo = contrastInfo;
}
drawContrastRatioLine(width: number, height: number, level: string): string|null {
const isAPCA = Root.Runtime.experiments.isEnabled('APCA');
const requiredContrast =
isAPCA ? this._contrastInfo.contrastRatioAPCAThreshold() : this._contrastInfo.contrastRatioThreshold(level);
if (!width || !height || requiredContrast === null) {
return null;
}
const dS = 0.02;
const H = 0;
const S = 1;
const V = 2;
const A = 3;
const color = this._contrastInfo.color();
const bgColor = this._contrastInfo.bgColor();
if (!color || !bgColor) {
return null;
}
const fgRGBA = color.rgba();
const fgHSVA = color.hsva();
const bgRGBA = bgColor.rgba();
const bgLuminance = Common.ColorUtils.luminance(bgRGBA);
let blendedRGBA: number[] = Common.ColorUtils.blendColors(fgRGBA, bgRGBA);
const fgLuminance = Common.ColorUtils.luminance(blendedRGBA);
const fgIsLighter = fgLuminance > bgLuminance;
const desiredLuminance = isAPCA ?
Common.ColorUtils.desiredLuminanceAPCA(bgLuminance, requiredContrast, fgIsLighter) :
Common.Color.Color.desiredLuminance(bgLuminance, requiredContrast, fgIsLighter);
if (isAPCA &&
Math.abs(Math.round(Common.ColorUtils.contrastRatioByLuminanceAPCA(desiredLuminance, bgLuminance))) <
requiredContrast) {
return null;
}
let lastV: number = fgHSVA[V];
let currentSlope = 0;
const candidateHSVA = [fgHSVA[H], 0, 0, fgHSVA[A]];
let pathBuilder: string[] = [];
const candidateRGBA: number[] = [];
Common.Color.Color.hsva2rgba(candidateHSVA, candidateRGBA);
blendedRGBA = Common.ColorUtils.blendColors(candidateRGBA, bgRGBA);
let candidateLuminance: ((candidateHSVA: number[]) => number)|((candidateHSVA: number[]) => number) =
(candidateHSVA: number[]): number => {
return Common.ColorUtils.luminance(
Common.ColorUtils.blendColors(Common.Color.Color.fromHSVA(candidateHSVA).rgba(), bgRGBA));
};
if (Root.Runtime.experiments.isEnabled('APCA')) {
candidateLuminance = (candidateHSVA: number[]): number => {
return Common.ColorUtils.luminanceAPCA(
Common.ColorUtils.blendColors(Common.Color.Color.fromHSVA(candidateHSVA).rgba(), bgRGBA));
};
}
// Plot V for values of S such that the computed luminance approximates
// `desiredLuminance`, until no suitable value for V can be found, or the
// current value of S goes of out bounds.
let s;
for (s = 0; s < 1 + dS; s += dS) {
s = Math.min(1, s);
candidateHSVA[S] = s;
// Extrapolate the approximate next value for `v` using the approximate
// gradient of the curve.
candidateHSVA[V] = lastV + currentSlope * dS;
const v = Common.Color.Color.approachColorValue(candidateHSVA, bgRGBA, V, desiredLuminance, candidateLuminance);
if (v === null) {
break;
}
// Approximate the current gradient of the curve.
currentSlope = s === 0 ? 0 : (v - lastV) / dS;
lastV = v;
pathBuilder.push(pathBuilder.length ? 'L' : 'M');
pathBuilder.push((s * width).toFixed(2));
pathBuilder.push(((1 - v) * height).toFixed(2));
}
// If no suitable V value for an in-bounds S value was found, find the value
// of S such that V === 1 and add that to the path.
if (s < 1 + dS) {
s -= dS;
candidateHSVA[V] = 1;
s = Common.Color.Color.approachColorValue(candidateHSVA, bgRGBA, S, desiredLuminance, candidateLuminance);
if (s !== null) {
pathBuilder = pathBuilder.concat(['L', (s * width).toFixed(2), '-0.1']);
}
}
if (pathBuilder.length === 0) {
return null;
}
return pathBuilder.join(' ');
}
}