1// Copyright (C) 2019 The Android Open Source Project 2// 3// Licensed under the Apache License, Version 2.0 (the "License"); 4// you may not use this file except in compliance with the License. 5// You may obtain a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, 11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12// See the License for the specific language governing permissions and 13// limitations under the License. 14 15import {hsl} from 'color-convert'; 16import {hash} from '../base/hash'; 17import {featureFlags} from '../core/feature_flags'; 18import {Color, HSLColor, HSLuvColor} from '../public/color'; 19import {ColorScheme} from '../public/color_scheme'; 20import {RandState, pseudoRand} from '../base/rand'; 21 22// 128 would provide equal weighting between dark and light text. 23// However, we want to prefer light text for stylistic reasons. 24// A higher value means color must be brighter before switching to dark text. 25const PERCEIVED_BRIGHTNESS_LIMIT = 180; 26 27// This file defines some opinionated colors and provides functions to access 28// random but predictable colors based on a seed, as well as standardized ways 29// to access colors for core objects such as slices and thread states. 30 31// We have, over the years, accumulated a number of different color palettes 32// which are used for different parts of the UI. 33// It would be nice to combine these into a single palette in the future, but 34// changing colors is difficult especially for slice colors, as folks get used 35// to certain slices being certain colors and are resistant to change. 36// However we do it, we should make it possible for folks to switch back the a 37// previous palette, or define their own. 38 39const USE_CONSISTENT_COLORS = featureFlags.register({ 40 id: 'useConsistentColors', 41 name: 'Use common color palette for timeline elements', 42 description: 'Use the same color palette for all timeline elements.', 43 defaultValue: false, 44}); 45 46const randColourState: RandState = {seed: 0}; 47 48const MD_PALETTE_RAW: Color[] = [ 49 new HSLColor({h: 4, s: 90, l: 58}), 50 new HSLColor({h: 340, s: 82, l: 52}), 51 new HSLColor({h: 291, s: 64, l: 42}), 52 new HSLColor({h: 262, s: 52, l: 47}), 53 new HSLColor({h: 231, s: 48, l: 48}), 54 new HSLColor({h: 207, s: 90, l: 54}), 55 new HSLColor({h: 199, s: 98, l: 48}), 56 new HSLColor({h: 187, s: 100, l: 42}), 57 new HSLColor({h: 174, s: 100, l: 29}), 58 new HSLColor({h: 122, s: 39, l: 49}), 59 new HSLColor({h: 88, s: 50, l: 53}), 60 new HSLColor({h: 66, s: 70, l: 54}), 61 new HSLColor({h: 45, s: 100, l: 51}), 62 new HSLColor({h: 36, s: 100, l: 50}), 63 new HSLColor({h: 14, s: 100, l: 57}), 64 new HSLColor({h: 16, s: 25, l: 38}), 65 new HSLColor({h: 200, s: 18, l: 46}), 66 new HSLColor({h: 54, s: 100, l: 62}), 67]; 68 69const WHITE_COLOR = new HSLColor([0, 0, 100]); 70const BLACK_COLOR = new HSLColor([0, 0, 0]); 71const GRAY_COLOR = new HSLColor([0, 0, 90]); 72 73const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => { 74 const base = color.lighten(10, 60).desaturate(20); 75 const variant = base.lighten(30, 80).desaturate(20); 76 77 return { 78 base, 79 variant, 80 disabled: GRAY_COLOR, 81 textBase: WHITE_COLOR, // White text suits MD colors quite well 82 textVariant: WHITE_COLOR, 83 textDisabled: WHITE_COLOR, // Low contrast is on purpose 84 }; 85}); 86 87// Create a color scheme based on a single color, which defines the variant 88// color as a slightly darker and more saturated version of the base color. 89export function makeColorScheme(base: Color, variant?: Color): ColorScheme { 90 variant = variant ?? base.darken(15).saturate(15); 91 92 return { 93 base, 94 variant, 95 disabled: GRAY_COLOR, 96 textBase: 97 base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT 98 ? BLACK_COLOR 99 : WHITE_COLOR, 100 textVariant: 101 variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT 102 ? BLACK_COLOR 103 : WHITE_COLOR, 104 textDisabled: WHITE_COLOR, // Low contrast is on purpose 105 }; 106} 107 108const GRAY = makeColorScheme(new HSLColor([0, 0, 62])); 109const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49])); 110const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34])); 111const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47])); 112const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55)); 113const ORANGE = makeColorScheme(new HSLColor([36, 100, 50])); 114const INDIGO = makeColorScheme(new HSLColor([231, 48, 48])); 115 116// A piece of wisdom from a long forgotten blog post: "Don't make 117// colors you want to change something normal like grey." 118export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70])); 119 120// Selects a predictable color scheme from a palette of material design colors, 121// based on a string seed. 122function materialColorScheme(seed: string): ColorScheme { 123 const colorIdx = hash(seed, MD_PALETTE.length); 124 return MD_PALETTE[colorIdx]; 125} 126 127const proceduralColorCache = new Map<string, ColorScheme>(); 128 129// Procedurally generates a predictable color scheme based on a string seed. 130function proceduralColorScheme(seed: string): ColorScheme { 131 const colorScheme = proceduralColorCache.get(seed); 132 if (colorScheme) { 133 return colorScheme; 134 } else { 135 const hue = hash(seed, 360); 136 // Saturation 100 would give the most differentiation between colors, but 137 // it's garish. 138 const saturation = 80; 139 140 // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This 141 // is because this function chooses hue/lightness uniform at random, but HSL 142 // is not perceptually uniform. 143 // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/. 144 const base = new HSLuvColor({ 145 h: hue, 146 s: saturation, 147 l: hash(seed + 'x', 40) + 40, 148 }); 149 const variant = new HSLuvColor({h: hue, s: saturation, l: 30}); 150 const colorScheme = makeColorScheme(base, variant); 151 152 proceduralColorCache.set(seed, colorScheme); 153 154 return colorScheme; 155 } 156} 157 158export function colorForState(state: string): ColorScheme { 159 if (state === 'Running') { 160 return DARK_GREEN; 161 } else if (state.startsWith('Runnable')) { 162 return LIME_GREEN; 163 } else if (state.includes('Uninterruptible Sleep')) { 164 if (state.includes('non-IO')) { 165 return DESAT_RED; 166 } 167 return ORANGE; 168 } else if (state.includes('Dead')) { 169 return GRAY; 170 } else if (state.includes('Sleeping') || state.includes('Idle')) { 171 return TRANSPARENT_WHITE; 172 } 173 return INDIGO; 174} 175 176export function colorForTid(tid: number): ColorScheme { 177 return materialColorScheme(tid.toString()); 178} 179 180export function colorForThread(thread?: { 181 pid?: number; 182 tid: number; 183}): ColorScheme { 184 if (thread === undefined) { 185 return GRAY; 186 } 187 const tid = thread.pid ?? thread.tid; 188 return colorForTid(tid); 189} 190 191export function colorForCpu(cpu: number): Color { 192 if (USE_CONSISTENT_COLORS.get()) { 193 return materialColorScheme(cpu.toString()).base; 194 } else { 195 const hue = (128 + 32 * cpu) % 256; 196 return new HSLColor({h: hue, s: 50, l: 50}); 197 } 198} 199 200export function randomColor(): string { 201 const rand = pseudoRand(randColourState); 202 if (USE_CONSISTENT_COLORS.get()) { 203 return materialColorScheme(rand.toString()).base.cssString; 204 } else { 205 // 40 different random hues 9 degrees apart. 206 const hue = Math.floor(rand * 40) * 9; 207 return '#' + hsl.hex([hue, 90, 30]); 208 } 209} 210 211export function getColorForSlice(sliceName: string): ColorScheme { 212 const name = sliceName.replace(/( )?\d+/g, ''); 213 if (USE_CONSISTENT_COLORS.get()) { 214 return materialColorScheme(name); 215 } else { 216 return proceduralColorScheme(name); 217 } 218} 219 220export function colorForFtrace(name: string): ColorScheme { 221 return materialColorScheme(name); 222} 223 224export function getColorForSample(callsiteId: number): ColorScheme { 225 if (USE_CONSISTENT_COLORS.get()) { 226 return materialColorScheme(String(callsiteId)); 227 } else { 228 return proceduralColorScheme(String(callsiteId)); 229 } 230} 231