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'; 16 17import {hash} from './hash'; 18import {featureFlags} from './feature_flags'; 19 20import {Color, HSLColor, HSLuvColor} from './color'; 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 46// |ColorScheme| defines a collection of colors which can be used for various UI 47// elements. In the future we would expand this interface to include light and 48// dark variants. 49export interface ColorScheme { 50 // The base color to be used for the bulk of the element. 51 readonly base: Color; 52 53 // A variant on the base color, commonly used for highlighting. 54 readonly variant: Color; 55 56 // Grayed out color to represent a disabled state. 57 readonly disabled: Color; 58 59 // Appropriate colors for text to be displayed on top of the above colors. 60 readonly textBase: Color; 61 readonly textVariant: Color; 62 readonly textDisabled: Color; 63} 64 65const MD_PALETTE_RAW: Color[] = [ 66 new HSLColor({h: 4, s: 90, l: 58}), 67 new HSLColor({h: 340, s: 82, l: 52}), 68 new HSLColor({h: 291, s: 64, l: 42}), 69 new HSLColor({h: 262, s: 52, l: 47}), 70 new HSLColor({h: 231, s: 48, l: 48}), 71 new HSLColor({h: 207, s: 90, l: 54}), 72 new HSLColor({h: 199, s: 98, l: 48}), 73 new HSLColor({h: 187, s: 100, l: 42}), 74 new HSLColor({h: 174, s: 100, l: 29}), 75 new HSLColor({h: 122, s: 39, l: 49}), 76 new HSLColor({h: 88, s: 50, l: 53}), 77 new HSLColor({h: 66, s: 70, l: 54}), 78 new HSLColor({h: 45, s: 100, l: 51}), 79 new HSLColor({h: 36, s: 100, l: 50}), 80 new HSLColor({h: 14, s: 100, l: 57}), 81 new HSLColor({h: 16, s: 25, l: 38}), 82 new HSLColor({h: 200, s: 18, l: 46}), 83 new HSLColor({h: 54, s: 100, l: 62}), 84]; 85 86const WHITE_COLOR = new HSLColor([0, 0, 100]); 87const BLACK_COLOR = new HSLColor([0, 0, 0]); 88const GRAY_COLOR = new HSLColor([0, 0, 90]); 89 90const MD_PALETTE: ColorScheme[] = MD_PALETTE_RAW.map((color): ColorScheme => { 91 const base = color.lighten(10, 60).desaturate(20); 92 const variant = base.lighten(30, 80).desaturate(20); 93 94 return { 95 base, 96 variant, 97 disabled: GRAY_COLOR, 98 textBase: WHITE_COLOR, // White text suits MD colors quite well 99 textVariant: WHITE_COLOR, 100 textDisabled: WHITE_COLOR, // Low contrast is on purpose 101 }; 102}); 103 104// Create a color scheme based on a single color, which defines the variant 105// color as a slightly darker and more saturated version of the base color. 106export function makeColorScheme(base: Color, variant?: Color): ColorScheme { 107 variant = variant ?? base.darken(15).saturate(15); 108 109 return { 110 base, 111 variant, 112 disabled: GRAY_COLOR, 113 textBase: 114 base.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT 115 ? BLACK_COLOR 116 : WHITE_COLOR, 117 textVariant: 118 variant.perceivedBrightness >= PERCEIVED_BRIGHTNESS_LIMIT 119 ? BLACK_COLOR 120 : WHITE_COLOR, 121 textDisabled: WHITE_COLOR, // Low contrast is on purpose 122 }; 123} 124 125const GRAY = makeColorScheme(new HSLColor([0, 0, 62])); 126const DESAT_RED = makeColorScheme(new HSLColor([3, 30, 49])); 127const DARK_GREEN = makeColorScheme(new HSLColor([120, 44, 34])); 128const LIME_GREEN = makeColorScheme(new HSLColor([75, 55, 47])); 129const TRANSPARENT_WHITE = makeColorScheme(new HSLColor([0, 1, 97], 0.55)); 130const ORANGE = makeColorScheme(new HSLColor([36, 100, 50])); 131const INDIGO = makeColorScheme(new HSLColor([231, 48, 48])); 132 133// A piece of wisdom from a long forgotten blog post: "Don't make 134// colors you want to change something normal like grey." 135export const UNEXPECTED_PINK = makeColorScheme(new HSLColor([330, 100, 70])); 136 137// Selects a predictable color scheme from a palette of material design colors, 138// based on a string seed. 139function materialColorScheme(seed: string): ColorScheme { 140 const colorIdx = hash(seed, MD_PALETTE.length); 141 return MD_PALETTE[colorIdx]; 142} 143 144const proceduralColorCache = new Map<string, ColorScheme>(); 145 146// Procedurally generates a predictable color scheme based on a string seed. 147function proceduralColorScheme(seed: string): ColorScheme { 148 const colorScheme = proceduralColorCache.get(seed); 149 if (colorScheme) { 150 return colorScheme; 151 } else { 152 const hue = hash(seed, 360); 153 // Saturation 100 would give the most differentiation between colors, but 154 // it's garish. 155 const saturation = 80; 156 157 // Prefer using HSLuv, not the browser's built-in vanilla HSL handling. This 158 // is because this function chooses hue/lightness uniform at random, but HSL 159 // is not perceptually uniform. 160 // See https://www.boronine.com/2012/03/26/Color-Spaces-for-Human-Beings/. 161 const base = new HSLuvColor({ 162 h: hue, 163 s: saturation, 164 l: hash(seed + 'x', 40) + 40, 165 }); 166 const variant = new HSLuvColor({h: hue, s: saturation, l: 30}); 167 const colorScheme = makeColorScheme(base, variant); 168 169 proceduralColorCache.set(seed, colorScheme); 170 171 return colorScheme; 172 } 173} 174 175export function colorForState(state: string): ColorScheme { 176 if (state === 'Running') { 177 return DARK_GREEN; 178 } else if (state.startsWith('Runnable')) { 179 return LIME_GREEN; 180 } else if (state.includes('Uninterruptible Sleep')) { 181 if (state.includes('non-IO')) { 182 return DESAT_RED; 183 } 184 return ORANGE; 185 } else if (state.includes('Dead')) { 186 return GRAY; 187 } else if (state.includes('Sleeping') || state.includes('Idle')) { 188 return TRANSPARENT_WHITE; 189 } 190 return INDIGO; 191} 192 193export function colorForTid(tid: number): ColorScheme { 194 return materialColorScheme(tid.toString()); 195} 196 197export function colorForThread(thread?: { 198 pid?: number; 199 tid: number; 200}): ColorScheme { 201 if (thread === undefined) { 202 return GRAY; 203 } 204 const tid = thread.pid ?? thread.tid; 205 return colorForTid(tid); 206} 207 208export function colorForCpu(cpu: number): Color { 209 if (USE_CONSISTENT_COLORS.get()) { 210 return materialColorScheme(cpu.toString()).base; 211 } else { 212 const hue = (128 + 32 * cpu) % 256; 213 return new HSLColor({h: hue, s: 50, l: 50}); 214 } 215} 216 217export function randomColor(): string { 218 const rand = Math.random(); 219 if (USE_CONSISTENT_COLORS.get()) { 220 return materialColorScheme(rand.toString()).base.cssString; 221 } else { 222 // 40 different random hues 9 degrees apart. 223 const hue = Math.floor(rand * 40) * 9; 224 return '#' + hsl.hex([hue, 90, 30]); 225 } 226} 227 228export function getColorForSlice(sliceName: string): ColorScheme { 229 const name = sliceName.replace(/( )?\d+/g, ''); 230 if (USE_CONSISTENT_COLORS.get()) { 231 return materialColorScheme(name); 232 } else { 233 return proceduralColorScheme(name); 234 } 235} 236 237export function colorForFtrace(name: string): ColorScheme { 238 return materialColorScheme(name); 239} 240 241export function colorForSample(callsiteId: number, isHovered: boolean): string { 242 let colorScheme; 243 if (USE_CONSISTENT_COLORS.get()) { 244 colorScheme = materialColorScheme(String(callsiteId)); 245 } else { 246 colorScheme = proceduralColorScheme(String(callsiteId)); 247 } 248 249 return isHovered ? colorScheme.variant.cssString : colorScheme.base.cssString; 250} 251