UNPKG

3dmol

Version:

JavaScript/TypeScript molecular visualization library

439 lines (400 loc) 12.8 kB
import { CC, Color, ColorSpec } from "./colors"; export abstract class GradientType { gradient?: string; abstract valueToHex(value: number, range?: number[]): number; abstract range(): number[] | null; } export function normalizeValue( lo: number, hi: number, val: number ): { lo: number; hi: number; val: number } { if (hi >= lo) { if (val < lo) val = lo; if (val > hi) val = hi; return { lo: lo, hi: hi, val: val }; } else { if (val > lo) val = lo; if (val < hi) val = hi; //flip the meaning of val, lo, hi val = lo - val + hi; return { lo: hi, hi: lo, val: val }; } } /** * Gradient specification. * @see builtinGradients */ export type GradientSpec = { /** Kind of gradient. E.g. RWB, ROYGB, sinebow. Can also specify linear[_color]* as a * shorthand for CustomLinear and passing a colors array. */ gradient?: string; /** Lower range of gradient */ min?: number; /** Upper range of gradient */ max?: number; /** {AtomSpec} property to use for gradient calculation. E.g., 'b' for temperature factors of a PDB. */ prop?: string; /** mid point value for gradient (for rwb) */ mid?: number; /** Custom colors for gradient (for {@link CustomLinear}) */ colors?: Array<ColorSpec>; /** map of a certain {@link AtomSpec} property to a color of the form `{'prop': 'elem', map:elementColors.greenCarbon}` Allows the user to provide a mapping of elements to colors to the colorscheme. This can be done with any properties, and not just 'elem'. */ map?: Record<string, unknown> }; //return a Gradient object, even if what is specified is descriptive export function getGradient(grad: GradientSpec|GradientType): GradientType { if (grad instanceof GradientType) { return grad; } else if (grad.gradient !== undefined && builtinGradients[grad.gradient] ) { let min = grad.min === undefined ? -1 : grad.min; let max = grad.max === undefined ? 1 : grad.max; if (grad.mid === undefined) { if (grad.colors === undefined) { return new builtinGradients[grad.gradient](min, max); } else { return new builtinGradients[grad.gradient](min, max, grad.colors); } } else { return new builtinGradients[grad.gradient](min, max, grad.mid); } } else if(typeof(grad.gradient) == "string" && grad.gradient.startsWith('linear_')) { let colors = grad.gradient.split('_'); colors.shift(); let min = grad.min === undefined ? -1 : grad.min; let max = grad.max === undefined ? 1 : grad.max; return new CustomLinear(min,max,colors); } return grad as GradientType; } /** * Color scheme red to white to blue, for charges * Reverse gradients are supported when min>max so that the colors are displayed in reverse order. * @subcategory Gradients */ export class RWB extends GradientType { gradient = "RWB"; min: number; max: number; mid?: number; mult: number; constructor(min?: number | [number, number], max?: number, mid?: number) { super(); this.mult = 1.0; this.mid = mid; this.min = min as number; this.max = max as number; if (typeof max == "undefined" && Array.isArray(min) && min.length >= 2) { //we were passed a single range this.max = min[1]; this.min = min[0]; } else if (!!min && !!max && !Array.isArray(min)) { this.min = min; this.max = max; } } //return range used for color mapping, null if none set range() { if (typeof this.min != "undefined" && typeof this.max != "undefined") { return [this.min, this.max] as [number, number]; } return null; } //map value to hex color, range is provided valueToHex(val: number, range?: number[]) { var lo: number, hi: number; val = this.mult * val; //reverse if necessary if (range) { lo = range[0]; hi = range[1]; } else { lo = this.min; hi = this.max; } if (val === undefined) return 0xffffff; var norm = normalizeValue(lo, hi, val); lo = norm.lo; hi = norm.hi; val = norm.val; var middle = (hi + lo) / 2; if (range && typeof range[2] != "undefined") middle = range[2]; else if (typeof this.mid != "undefined") middle = this.mid; //allow user to specify midpoint else middle = (lo + hi) / 2; var scale: number, color: number; //scale bottom from red to white if (val < middle) { scale = Math.floor(255 * Math.sqrt((val - lo) / (middle - lo))); color = 0xff0000 + 0x100 * scale + scale; return color; } else if (val > middle) { //form white to blue scale = Math.floor(255 * Math.sqrt(1 - (val - middle) / (hi - middle))); color = 0x10000 * scale + 0x100 * scale + 0xff; return color; } else { //val == middle return 0xffffff; } } } /** * rainbow gradient, but without purple to match jmol * Reverse gradients are supported when min>max so that the colors are displayed in reverse order. * @subcategory Gradients */ export class ROYGB extends GradientType { gradient = "ROYGB"; mult: number; max?: number; min?: number; constructor(min?: number, max?: number) { super(); this.mult = 1.0; this.min = min; this.max = max; if (typeof max == "undefined" && Array.isArray(min) && min.length >= 2) { //we were passed a single range this.max = min[1]; this.min = min[0]; } else if (!!min && !!max && !Array.isArray(min)) { this.min = min; this.max = max; } }; //map value to hex color, range is provided valueToHex(val: number, range?: any[]) { var lo: number, hi: number; val = this.mult * val; if (range) { lo = range[0]; hi = range[1]; } else { lo = this.min!; hi = this.max!; } if (typeof val == "undefined") return 0xffffff; var norm = normalizeValue(lo, hi, val); lo = norm.lo; hi = norm.hi; val = norm.val; var mid = (lo + hi) / 2; var q1 = (lo + mid) / 2; var q3 = (mid + hi) / 2; var scale: number, color: number; if (val < q1) { //scale green up, red up, blue down scale = Math.floor(255 * Math.sqrt((val - lo) / (q1 - lo))); color = 0xff0000 + 0x100 * scale + 0; return color; } else if (val < mid) { //scale red down, green up, blue down scale = Math.floor(255 * Math.sqrt(1 - (val - q1) / (mid - q1))); color = 0x010000 * scale + 0xff00 + 0x0; return color; } else if (val < q3) { //scale blue up, red down, green up scale = Math.floor(255 * Math.sqrt((val - mid) / (q3 - mid))); color = 0x000000 + 0xff00 + 0x1 * scale; return color; } else { //scale green down, blue up, red down scale = Math.floor(255 * Math.sqrt(1 - (val - q3) / (hi - q3))); color = 0x000000 + 0x0100 * scale + 0xff; return color; } }; //return range used for color mapping, null if none set range() { if (typeof this.min != "undefined" && typeof this.max != "undefined") { return [this.min, this.max] as [number, number]; } return null; }; } /** * rainbow gradient with constant saturation, all the way to purple! * Reverse gradients are supported when min>max so that the colors are displayed in reverse order. * @subcategory Gradients * * @example $.get('data/1fas.pqr', function(data){ viewer.addModel(data, "pqr"); $.get("data/1fas.cube",function(volumedata){ viewer.addSurface($3Dmol.SurfaceType.VDW, { opacity:0.85, voldata: new $3Dmol.VolumeData(volumedata, "cube"), volscheme: new $3Dmol.Gradient.Sinebow(2,0,1) },{}); viewer.render(); }); viewer.zoomTo(); }); */ export class Sinebow extends GradientType { gradient = "Sinebow"; mult: number; max: number; min: number; constructor(min: number, max: number) { super(); this.mult = 1.0; this.min = min; this.max = max; if (typeof max == "undefined" && Array.isArray(min) && min.length >= 2) { //we were passed a single range this.max = min[1]; this.min = min[0]; } if (max < min) { //reverse the order this.mult = -1.0; this.min *= -1.0; this.max *= -1.0; } }; //map value to hex color, range is provided valueToHex(val: number, range?: any[]) { var lo: number, hi: number; val = this.mult * val; if (range) { lo = range[0]; hi = range[1]; } else { lo = this.min; hi = this.max; } if (typeof val == "undefined") return 0xffffff; var norm = Gradient.normalizeValue(lo, hi, val); lo = norm.lo; hi = norm.hi; val = norm.val; var scale = (val - lo) / (hi - lo); var h = (5 * scale) / 6.0 + 0.5; var r = Math.sin(Math.PI * h); r *= r * 255; var g = Math.sin(Math.PI * (h + 1 / 3.0)); g *= g * 255; var b = Math.sin(Math.PI * (h + 2 / 3.0)); b *= b * 255; return ( 0x10000 * Math.floor(r) + 0x100 * Math.floor(b) + 0x1 * Math.floor(g) ); }; //return range used for color mapping, null if none set range() { if (typeof this.min != "undefined" && typeof this.max != "undefined") { return [this.min, this.max] as [number, number]; } return null; }; } /** * Custom linear gradient using user supplied colors. * Reverse gradients are supported when min>max so that the colors are displayed in reverse order. * Midpoints are not supported - color map should be specified to get desired middle color. * * @param {number} min * @param {number} max * @param {Array} colors Array of colors that will be linearly interpolated between from min to max values. * @subcategory Gradients * * @example $3Dmol.get('../test_structs/af.pdb', function(data){ viewer.addModel(data); viewer.setStyle({cartoon:{colorscheme:{prop: 'b', gradient:'linear', min: 70, max: 100, colors: ["blue","yellow","green"]}}}); viewer.zoomTo(); viewer.render(); }); */ export class CustomLinear extends GradientType { gradient = "linear"; min: number; max: number; colors = new Array<Color>(); constructor(min: any, max: any, colors?: any) { super(); var carr: Array<any>; if (Array.isArray(min) && min.length >= 2) { //we were passed a single range this.max = min[1] as number; this.min = min[0] as number; carr = max; } else { this.min = min as number; this.max = max as number; carr = colors; } //convert colors if (carr) { for (let c of carr) { this.colors.push(CC.color(c)); } } else { this.colors.push(CC.color(0)); } } //return range used for color mapping, null if none set range() { if (typeof this.min != "undefined" && typeof this.max != "undefined") { return [this.min, this.max] as [number, number]; } return null; } //map value to hex color, range is provided valueToHex(val: number, range?: any[]) { var lo: number, hi: number; if (range) { lo = range[0]; hi = range[1]; } else { lo = this.min; hi = this.max; } if (val === undefined) return 0xffffff; var norm = normalizeValue(lo, hi, val); lo = norm.lo; hi = norm.hi; val = norm.val; let nsteps = this.colors.length; let stepsize = (hi - lo) / nsteps; let startpos = Math.min(Math.floor((val - lo) / stepsize), nsteps - 1); let endpos = Math.min(startpos + 1, nsteps - 1); let frac = (val - lo - (startpos * stepsize)) / stepsize; let startcol = this.colors[startpos]; let endcol = this.colors[endpos]; let col = new Color(startcol.r + frac * (endcol.r - startcol.r), startcol.g + frac * (endcol.g - startcol.g), startcol.b + frac * (endcol.b - startcol.b)); return col.getHex(); } } /** * built in gradient schemes * The user can pass these strings directly as the gradient * @prop rwb - red/white/blue, supports setting a mid point for white * @prop roygb - rainbow * @prop sinebow - rainbow with better saturation properties * @prop linear - linearly maps between provided colors * */ export const builtinGradients = { "rwb": RWB, "RWB": RWB, "roygb": ROYGB, "ROYGB": ROYGB, "sinebow": Sinebow, "linear": CustomLinear }; export class Gradient extends GradientType { static RWB = RWB; static ROYGB = ROYGB; static Sinebow = Sinebow; static CustomLinear = CustomLinear; static builtinGradients = builtinGradients; static normalizeValue = normalizeValue; static getGradient = getGradient; valueToHex(_value: number, _range?: number[]): number { return 0; } range(): [number, number] | null { return null; } }