blob: ab7e5326730dab9dd76683bb41153ea3dc1c718a [file]
// 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<string, ColorScheme>();
// 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));
}
}