• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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