UNPKG

chrome-devtools-frontend

Version:
194 lines (164 loc) 6.95 kB
// 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(' '); } }