// Copyright (C) 2019 The Android Open Source Project // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import {hsl} from 'color-convert'; import {hash} from '../base/hash'; import {featureFlags} from '../core/feature_flags'; import {Color, HSLColor, HSLuvColor} from '../base/color'; import {ColorScheme} from '../base/color_scheme'; import {RandState, pseudoRand} from '../base/rand'; // 128 would provide equal weighting between dark and light text. // However, we want to prefer light text for stylistic reasons. // A higher value means color must be brighter before switching to dark text. const PERCEIVED_BRIGHTNESS_LIMIT = 180; // This file defines some opinionated colors and provides functions to access // random but predictable colors based on a seed, as well as standardized ways // to access colors for core objects such as slices and thread states. // We have, over the years, accumulated a number of different color palettes // which are used for different parts of the UI. // It would be nice to combine these into a single palette in the future, but // changing colors is difficult especially for slice colors, as folks get used // to certain slices being certain colors and are resistant to change. // However we do it, we should make it possible for folks to switch back the a // previous palette, or define their own. const USE_CONSISTENT_COLORS = featureFlags.register({ id: 'useConsistentColors', name: 'Use common color palette for timeline elements', description: 'Use the same color palette for all timeline elements.', defaultValue: false, }); const randColourState: RandState = {seed: 0}; const MD_PALETTE_RAW: Color[] = [ new HSLColor({h: 4, s: 90, l: 58}), new HSLColor({h: 340, s: 82, l: 52}), new HSLColor({h: 291, s: 64, l: 42}), new HSLColor({h: 262, s: 52, l: 47}), new HSLColor({h: 231, s: 48, l: 48}), new HSLColor({h: 207, s: 90, l: 54}), new HSLColor({h: 199, s: 98, l: 48}), new HSLColor({h: 187, s: 100, l: 42}), new HSLColor({h: 174, s: 100, l: 29}), new HSLColor({h: 122, s: 39, l: 49}), new HSLColor({h: 88, s: 50, l: 53}), new HSLColor({h: 66, s: 70, l: 54}), new HSLColor({h: 45, s: 100, l: 51}), new HSLColor({h: 36, s: 100, l: 50}), new HSLColor({h: 14, s: 100, l: 57}), new HSLColor({h: 16, s: 25, l: 38}), new HSLColor({h: 200, s: 18, l: 46}), new HSLColor({h: 54, s: 100, l: 62}), ]; const WHITE_COLOR = new HSLColor([0, 0, 100]); const BLACK_COLOR = new HSLColor([0, 0, 0]); const GRAY_COLOR = new HSLColor([0, 0, 90]); const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => { const base = color.lighten(10, 60).desaturate(20); const variant = base.lighten(30, 80).desaturate(20); return { base, variant, disabled: GRAY_COLOR, textBase: WHITE_COLOR, // White text suits MD colors quite well textVariant: WHITE_COLOR, textDisabled: WHITE_COLOR, // Low contrast is on purpose }; }); // Create a color scheme based on a single color, which defines the variant // color as a slightly darker and more saturated version of the base color. export function makeColorScheme(base: Color, variant?: Color): ColorScheme { variant = variant ?? base.darken(15).saturate(15); return { base, variant, disabled: GRAY_COLOR, textBase: base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT ? BLACK_COLOR : WHITE_COLOR, textVariant: variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT ? BLACK_COLOR : WHITE_COLOR, textDisabled: WHITE_COLOR, // Low contrast is on purpose }; } const GRAY = makeColorScheme(new HSLColor([0, 0, 62])); const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49])); const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34])); const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47])); const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55)); const ORANGE = makeColorScheme(new HSLColor([36, 100, 50])); const INDIGO = makeColorScheme(new HSLColor([231, 48, 48])); // A piece of wisdom from a long forgotten blog post: "Don't make // colors you want to change something normal like grey." export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70])); // Selects a predictable color scheme from a palette of material design colors, // based on a string seed. export function materialColorScheme(seed: string): ColorScheme { const colorIdx = hash(seed, MD_PALETTE.length); return MD_PALETTE[colorIdx]; } const proceduralColorCache = new Map(); // Procedurally generates a predictable color scheme based on a string seed. function proceduralColorScheme(seed: string): ColorScheme { const colorScheme = proceduralColorCache.get(seed); if (colorScheme) { return colorScheme; } else { const hue = hash(seed, 360); // Saturation 100 would give the most differentiation between colors, but // it's garish. const saturation = 80; // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This // is because this function chooses hue/lightness uniform at random, but HSL // is not perceptually uniform. // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/. const base = new HSLuvColor({ h: hue, s: saturation, l: hash(seed + 'x', 40) + 40, }); const variant = new HSLuvColor({h: hue, s: saturation, l: 30}); const colorScheme = makeColorScheme(base, variant); proceduralColorCache.set(seed, colorScheme); return colorScheme; } } export function colorForState(state: string): ColorScheme { if (state === 'Running') { return DARK_GREEN; } else if (state.startsWith('Runnable')) { return LIME_GREEN; } else if (state.includes('Uninterruptible Sleep')) { if (state.includes('non-IO')) { return DESAT_RED; } return ORANGE; } else if (state.includes('Dead')) { return GRAY; } else if (state.includes('Sleeping') || state.includes('Idle')) { return TRANSPARENT_WHITE; } return INDIGO; } export function colorForTid(tid: number): ColorScheme { return materialColorScheme(tid.toString()); } export function colorForThread(thread?: { pid?: number; tid: number; }): ColorScheme { if (thread === undefined) { return GRAY; } const tid = thread.pid ?? thread.tid; return colorForTid(tid); } export function colorForCpu(cpu: number): Color { if (USE_CONSISTENT_COLORS.get()) { return materialColorScheme(cpu.toString()).base; } else { const hue = (128 + 32 * cpu) % 256; return new HSLColor({h: hue, s: 50, l: 50}); } } export function randomColor(): string { const rand = pseudoRand(randColourState); if (USE_CONSISTENT_COLORS.get()) { return materialColorScheme(rand.toString()).base.cssString; } else { // 40 different random hues 9 degrees apart. const hue = Math.floor(rand * 40) * 9; return '#' + hsl.hex([hue, 90, 30]); } } export function getColorForSlice(sliceName: string): ColorScheme { const name = sliceName.replace(/( )?\d+/g, ''); if (USE_CONSISTENT_COLORS.get()) { return materialColorScheme(name); } else { return proceduralColorScheme(name); } } export function getColorForSample(callsiteId: number): ColorScheme { if (USE_CONSISTENT_COLORS.get()) { return materialColorScheme(String(callsiteId)); } else { return proceduralColorScheme(String(callsiteId)); } }