molstar
Version:
A comprehensive macromolecular library.
156 lines (155 loc) • 6.9 kB
JavaScript
"use strict";
/**
* Copyright (c) 2019-2023 mol* contributors, licensed under MIT, See LICENSE file for more info.
*
* @author Alexander Rose <alexander.rose@weirdbyte.de>
* @author David Sehnal <david.sehnal@gmail.com>
*
* adapted from https://github.com/internalfx/distinct-colors (ISC License Copyright (c) 2015, InternalFX Inc.)
* which is heavily inspired by http://tools.medialab.sciences-po.fr/iwanthue/
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.DistinctColorsParams = void 0;
exports.distinctColors = distinctColors;
const lab_1 = require("./spaces/lab");
const hcl_1 = require("./spaces/hcl");
const object_1 = require("../../mol-util/object");
const mol_util_1 = require("../../mol-util");
const param_definition_1 = require("../../mol-util/param-definition");
const names_1 = require("./names");
exports.DistinctColorsParams = {
hue: param_definition_1.ParamDefinition.Interval([1, 360], { min: 0, max: 360, step: 1 }),
chroma: param_definition_1.ParamDefinition.Interval([40, 70], { min: 0, max: 100, step: 1 }),
luminance: param_definition_1.ParamDefinition.Interval([15, 85], { min: 0, max: 100, step: 1 }),
sort: param_definition_1.ParamDefinition.Select('contrast', param_definition_1.ParamDefinition.arrayToOptions(['none', 'contrast']), { description: 'no sorting leaves colors approximately ordered by hue' }),
clusteringStepCount: param_definition_1.ParamDefinition.Numeric(50, { min: 10, max: 200, step: 1 }, { isHidden: true }),
minSampleCount: param_definition_1.ParamDefinition.Numeric(800, { min: 100, max: 5000, step: 100 }, { isHidden: true }),
sampleCountFactor: param_definition_1.ParamDefinition.Numeric(5, { min: 1, max: 100, step: 1 }, { isHidden: true }),
};
const LabTolerance = 2;
const tmpCheckColorHcl = [0, 0, 0];
const tmpCheckColorLab = [0, 0, 0];
function checkColor(lab, props) {
lab_1.Lab.toHcl(tmpCheckColorHcl, lab);
// roundtrip to RGB for conversion tolerance testing
lab_1.Lab.fromColor(tmpCheckColorLab, lab_1.Lab.toColor(lab));
return (tmpCheckColorHcl[0] >= props.hue[0] &&
tmpCheckColorHcl[0] <= props.hue[1] &&
tmpCheckColorHcl[1] >= props.chroma[0] &&
tmpCheckColorHcl[1] <= props.chroma[1] &&
tmpCheckColorHcl[2] >= props.luminance[0] &&
tmpCheckColorHcl[2] <= props.luminance[1] &&
tmpCheckColorLab[0] >= (lab[0] - LabTolerance) &&
tmpCheckColorLab[0] <= (lab[0] + LabTolerance) &&
tmpCheckColorLab[1] >= (lab[1] - LabTolerance) &&
tmpCheckColorLab[1] <= (lab[1] + LabTolerance) &&
tmpCheckColorLab[2] >= (lab[2] - LabTolerance) &&
tmpCheckColorLab[2] <= (lab[2] + LabTolerance));
}
function sortByContrast(colors) {
const unsortedColors = colors.slice(0);
const sortedColors = [unsortedColors.shift()];
while (unsortedColors.length > 0) {
const lastColor = sortedColors[sortedColors.length - 1];
let nearest = 0;
let maxDist = Number.NEGATIVE_INFINITY;
for (let i = 0; i < unsortedColors.length; ++i) {
const dist = lab_1.Lab.distance(lastColor, unsortedColors[i]);
if (dist > maxDist) {
maxDist = dist;
nearest = i;
}
}
sortedColors.push(unsortedColors.splice(nearest, 1)[0]);
}
return sortedColors;
}
function getSamples(count, p) {
const samples = new Map();
const rangeDivider = Math.ceil(Math.cbrt(count));
const hcl = (0, hcl_1.Hcl)();
const hStep = Math.max((p.hue[1] - p.hue[0]) / rangeDivider, 1);
const cStep = Math.max((p.chroma[1] - p.chroma[0]) / rangeDivider, 1);
const lStep = Math.max((p.luminance[1] - p.luminance[0]) / rangeDivider, 1);
for (let h = p.hue[0] + hStep / 2; h <= p.hue[1]; h += hStep) {
for (let c = p.chroma[0] + cStep / 2; c <= p.chroma[1]; c += cStep) {
for (let l = p.luminance[0] + lStep / 2; l <= p.luminance[1]; l += lStep) {
const lab = lab_1.Lab.fromHcl((0, lab_1.Lab)(), hcl_1.Hcl.set(hcl, h, c, l));
if (checkColor(lab, p))
samples.set(lab_1.Lab.toColor(lab), lab);
}
}
}
return Array.from(samples.values());
}
function getClosestIndex(colors, color) {
let minDist = Infinity;
let nearest = 0;
for (let j = 0; j < colors.length; j++) {
const dist = lab_1.Lab.distance(color, colors[j]);
if (dist < minDist) {
minDist = dist;
nearest = j;
}
}
return nearest;
}
/**
* Create a list of visually distinct colors
*/
function distinctColors(count, props = {}) {
const p = { ...param_definition_1.ParamDefinition.getDefaultValues(exports.DistinctColorsParams), ...props };
if (count <= 0)
return [];
const samples = getSamples(Math.max(p.minSampleCount, count * p.sampleCountFactor), p);
if (samples.length < count) {
console.warn('Not enough samples to generate distinct colors, increase sample count.');
return (new Array(count)).fill(names_1.ColorNames.lightgrey);
}
const colors = [];
const zonesProto = [];
const sliceSize = Math.floor(samples.length / count);
for (let i = 0; i < samples.length; i += sliceSize) {
colors.push(samples[i]);
zonesProto.push([]);
if (colors.length >= count)
break;
}
for (let step = 1; step <= p.clusteringStepCount; ++step) {
const zones = (0, object_1.deepClone)(zonesProto);
const sampleList = (0, object_1.deepClone)(samples); // Immediately add the closest sample for each color
// Find closest color for each sample
for (let i = 0; i < colors.length; ++i) {
const idx = getClosestIndex(sampleList, colors[i]);
zones[i].push(samples[idx]);
sampleList.splice(idx, 1);
}
for (let i = 0; i < sampleList.length; ++i) {
const nearest = getClosestIndex(colors, samples[i]);
zones[nearest].push(samples[i]);
}
const lastColors = (0, object_1.deepClone)(colors);
for (let i = 0; i < zones.length; ++i) {
const zone = zones[i];
const size = zone.length;
if (size === 0)
continue;
let Ls = 0;
let As = 0;
let Bs = 0;
for (const sample of zone) {
Ls += sample[0];
As += sample[1];
Bs += sample[2];
}
const lAvg = Ls / size;
const aAvg = As / size;
const bAvg = Bs / size;
colors[i] = [lAvg, aAvg, bAvg];
}
if ((0, mol_util_1.deepEqual)(lastColors, colors))
break;
}
const sorted = p.sort === 'contrast' ? sortByContrast(colors) : colors;
return sorted.map(c => lab_1.Lab.toColor(c));
}