/
45case
/
ptable
Обзор
Документация
Войти
/
45case
/
ptable
Код
Запросы
3
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/entities/element/lib/propertyColorScale.ts
645 строк
16 KB
Olya
Страница Электроны + сайдбар с характеритстиками (Степень окисления, Конфигурация, Развёрнутая, Уровни энергии, ВЗМО)
24 июл 2026, 11:54
24 июл 2026, 11:54
3cf1d48
Код
Авторство
О чём код?
export type PropertyScaleKind = "lin" | "log"; export type PropertyColorScheme = { start: string; end: string; unknown: string; zero?: string; scale: PropertyScaleKind; }; export type NumericRange = { min: number; max: number; }; /** Linear normalization used for numeric home-property heatmaps (ptable-style). */ export function normalizeLinear(value: number, min: number, max: number): number { if (!Number.isFinite(value) || !Number.isFinite(min) || !Number.isFinite(max)) { return 0; } if (max === min) { return 0; } return (value - min) / (max - min); } /** * Ptable scale transform: values are mapped with lin/log/exp before min–max normalize. * `Math.log(0) === -Infinity` is clamped to the transformed minimum (ptable). */ export function transformPropertyValue(value: number, scale: PropertyScaleKind): number { if (scale === "log") { return Math.log(value); } return value; } export function getTransformedNumericRange( values: readonly (number | null | undefined)[], scale: PropertyScaleKind, ): NumericRange | null { let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const value of values) { if (value == null || Number.isNaN(value)) { continue; } const transformed = transformPropertyValue(value, scale); if (!Number.isFinite(transformed)) { // Match ptable: -Infinity from log(0) is ignored in min/max via isFinite → 0 fallback // for extremes; usable positive masses never hit this. continue; } if (transformed < min) { min = transformed; } if (transformed > max) { max = transformed; } } if (!Number.isFinite(min) || !Number.isFinite(max)) { return null; } return { min, max }; } export function normalizePropertyValue( value: number, range: NumericRange, scale: PropertyScaleKind, ): number { let transformed = transformPropertyValue(value, scale); // ptable: n(e) === -Infinity ? o : n(e) if (transformed === Number.NEGATIVE_INFINITY) { transformed = range.min; } if (!Number.isFinite(transformed)) { return 0; } return normalizeLinear(transformed, range.min, range.max); } export function getNumericRange(values: readonly (number | null | undefined)[]): NumericRange | null { let min = Number.POSITIVE_INFINITY; let max = Number.NEGATIVE_INFINITY; for (const value of values) { if (value == null || Number.isNaN(value)) { continue; } if (value < min) { min = value; } if (value > max) { max = value; } } if (!Number.isFinite(min) || !Number.isFinite(max)) { return null; } return { min, max }; } type Rgb = { r: number; g: number; b: number }; function clampByte(value: number): number { return Math.round(Math.min(255, Math.max(0, value))); } export function parseCssColorToRgb(color: string): Rgb | null { const hex = color.trim(); const short = /^#([0-9a-f]{3})$/i.exec(hex); if (short) { const [r, g, b] = short[1].split("").map((ch) => Number.parseInt(ch + ch, 16)); return { r, g, b }; } const full = /^#([0-9a-f]{6})$/i.exec(hex); if (full) { const raw = full[1]; return { r: Number.parseInt(raw.slice(0, 2), 16), g: Number.parseInt(raw.slice(2, 4), 16), b: Number.parseInt(raw.slice(4, 6), 16), }; } const rgb = /^rgba?\(\s*([\d.]+)\s*,\s*([\d.]+)\s*,\s*([\d.]+)/i.exec(hex); if (rgb) { return { r: clampByte(Number(rgb[1])), g: clampByte(Number(rgb[2])), b: clampByte(Number(rgb[3])), }; } const hsl = /^hsla?\(\s*([\d.]+)\s*,\s*([\d.]+)%\s*,\s*([\d.]+)%/i.exec(hex); if (hsl) { return hslToRgb(Number(hsl[1]), Number(hsl[2]) / 100, Number(hsl[3]) / 100); } return null; } function hslToRgb(h: number, s: number, l: number): Rgb { const hue = ((h % 360) + 360) % 360; const c = (1 - Math.abs(2 * l - 1)) * s; const x = c * (1 - Math.abs(((hue / 60) % 2) - 1)); const m = l - c / 2; let r = 0; let g = 0; let b = 0; if (hue < 60) { r = c; g = x; } else if (hue < 120) { r = x; g = c; } else if (hue < 180) { g = c; b = x; } else if (hue < 240) { g = x; b = c; } else if (hue < 300) { r = x; b = c; } else { r = c; b = x; } return { r: clampByte((r + m) * 255), g: clampByte((g + m) * 255), b: clampByte((b + m) * 255), }; } export function rgbToHex({ r, g, b }: Rgb): string { return `#${[r, g, b].map((v) => clampByte(v).toString(16).padStart(2, "0")).join("")}`; } export function mixRgb(a: Rgb, b: Rgb, t: number): Rgb { const clamped = Math.min(1, Math.max(0, t)); return { r: a.r + (b.r - a.r) * clamped, g: a.g + (b.g - a.g) * clamped, b: a.b + (b.b - a.b) * clamped, }; } export function mixCssColors(start: string, end: string, t: number): string { const a = parseCssColorToRgb(start) ?? { r: 255, g: 255, b: 255 }; const b = parseCssColorToRgb(end) ?? { r: 240, g: 0, b: 0 }; return rgbToHex(mixRgb(a, b, t)); } /** * Color for a numeric property value using an explicit scheme (ptable PropertyKey). * Pass raw values; range is computed in the transformed (lin/log) space. * * When `scheme.zero` and `options.zeroPoint` are set (melt/boil), colors diverge * from the pivot: start→zero below, zero→end above (ptable Temperature slider). */ export function getPropertySchemeColor( value: number | null | undefined, values: readonly (number | null | undefined)[], scheme: PropertyColorScheme, options?: { zeroPoint?: number | null }, ): string { if (value == null || Number.isNaN(value)) { return scheme.unknown; } const range = getTransformedNumericRange(values, scheme.scale); if (range == null) { return scheme.unknown; } if (range.min === range.max) { return scheme.start; } const zeroPoint = options?.zeroPoint; if ( scheme.zero != null && zeroPoint != null && Number.isFinite(zeroPoint) ) { // Melt/boil: ptable keeps the temperature pivot in raw units (lin only). const transformed = normalizePropertyValueTransformed(value, range, scheme.scale); const pivot = zeroPoint; if (transformed < pivot) { if (pivot === range.min) { return scheme.zero; } const t = normalizeLinear(transformed, range.min, pivot); return mixCssColors(scheme.start, scheme.zero, t); } if (range.max === pivot) { return scheme.zero; } const t = normalizeLinear(transformed, pivot, range.max); return mixCssColors(scheme.zero, scheme.end, t); } const normalized = normalizePropertyValue(value, range, scheme.scale); return mixCssColors(scheme.start, scheme.end, normalized); } /** Transformed value clamped like ptable before min–max / pivot blends. */ function normalizePropertyValueTransformed( value: number, range: NumericRange, scale: PropertyScaleKind, ): number { let transformed = transformPropertyValue(value, scale); if (transformed === Number.NEGATIVE_INFINITY) { transformed = range.min; } return Number.isFinite(transformed) ? transformed : range.min; } /** Ptable Weight defaults (light / dark). */ export const atomicMassSchemeDefaults = { light: { start: "#ffffff", end: "#f00000", unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#191919", end: "#e60000", unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Energy levels (`--electrons-start` / `--electrons-end`). * Colored by outer-shell electron count. */ export const energyLevelsSchemeDefaults = { light: { start: "#f5fbe9", // hsl(80, 70%, 95%) end: "#638128", // hsl(80, 53%, 33%) unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#3b3b3b", // hsl(20, 0%, 23%) end: "#617e25", // hsl(80, 55%, 32%) unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Electronegativity (`--electroneg-start` / `--electroneg-end`). * Default scale is logarithmic. */ export const electronegativitySchemeDefaults = { light: { start: "#ffff00", // hsl(60, 100%, 50%) end: "#c75300", // hsl(25, 100%, 39%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#02149c", // hsl(233, 97%, 31%) end: "#85730f", // hsl(51, 80%, 29%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Electron affinity (`--blocks` → `--affinity-end`). * Default scale is logarithmic. */ export const electronAffinitySchemeDefaults = { light: { start: "#ffffff", // --blocks end: "#d600cf", // hsl(302, 100%, 42%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#191919", // --blocks end: "#d100ca", // hsl(302, 100%, 41%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Ionization (`--ionize-start` / `--ionize-end`). * Default scale is logarithmic; order 1–30 via property select. */ export const ionizationEnergySchemeDefaults = { light: { start: "#00ff00", // hsl(120, 100%, 50%) end: "#6161ff", // hsl(240, 100%, 69%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#60006b", // hsl(294, 100%, 21%) end: "#6a5d0c", // hsl(52, 80%, 23%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Radius (`--radius-start` / `--radius-end`). * Default scale is linear; kind via property select. */ export const atomicRadiusSchemeDefaults = { light: { start: "#e0ffff", // hsl(180, 100%, 94%) end: "#318181", // hsl(180, 45%, 35%) unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#333333", // hsl(0, 0%, 20%) end: "#2b7d7d", // hsl(180, 49%, 33%) unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Hardness (`--blocks` → `--hardness-end`). * Default scale is logarithmic; scale kind via property select. */ export const hardnessSchemeDefaults = { light: { start: "#ffffff", // --blocks end: "#ad33ff", // hsl(276, 100%, 60%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#191919", // --blocks end: "#ab2eff", // hsl(276, 100%, 59%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Modulus (`--modulus-start` / `--modulus-end`). * Default scale is linear; kind via property select. */ export const modulusSchemeDefaults = { light: { start: "#fff8eb", // hsl(38, 100%, 96%) end: "#e08e00", // hsl(38, 100%, 44%) unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#4f4f4f", // hsl(0, 0%, 31%) end: "#a36700", // hsl(38, 100%, 32%) unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Density (`--density-start` / `--density-end`). * Default scale is linear; STP/liquid + kg/m³|g/cm³ via selects. */ export const densitySchemeDefaults = { light: { start: "#d1fff4", // hsl(165, 100%, 91%) end: "#008a67", // hsl(165, 100%, 27%) unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#2b2b2b", // hsl(0, 0%, 17%) end: "#008060", // hsl(165, 100%, 25%) unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Conductivity (`--conduct-start` / `--conduct-end`). * Default scale is linear; thermal/electric via property select. */ export const conductivitySchemeDefaults = { light: { start: "#e6e6fa", // hsl(240, 66%, 94%) end: "#6161ff", // hsl(240, 100%, 69%) unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#424242", // hsl(0, 0%, 26%) end: "#5c5cff", // hsl(240, 100%, 68%) unknown: "#333333", scale: "lin" as const, }, } as const; /** * Ptable Heat (`--heat-start` / `--heat-end`). * Specific defaults to log; vaporization/fusion use lin. */ export const heatSchemeDefaults = { light: { start: "#ffff00", // hsl(60, 100%, 50%) end: "#ff0000", // hsl(0, 100%, 50%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#b3b300", // hsl(60, 100%, 35%) end: "#ff1a1a", // hsl(0, 100%, 55%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Abundance (`--blocks` → `--abundance-end`). * Default scale is logarithmic; environment via property select. */ export const abundanceSchemeDefaults = { light: { start: "#ffffff", // --blocks end: "#f70303", // hsl(0, 97%, 49%) unknown: "#e6e6e6", scale: "log" as const, }, dark: { start: "#383333", // hsl(0, 5%, 21%) end: "#bf4040", // hsl(0, 50%, 50%) unknown: "#333333", scale: "log" as const, }, } as const; /** * Ptable Melting point (`--meltboil-*` + `--blocks` as zero). * Colors relative to the current environment temperature (zero = selected T). */ export const meltingPointSchemeDefaults = { light: { start: "#8080ff", // hsl(240, 100%, 75%) end: "#f00000", // hsl(0, 100%, 47%) zero: "#ffffff", // --blocks unknown: "#e6e6e6", scale: "lin" as const, }, dark: { start: "#5c5cff", // hsl(240, 100%, 68%) end: "#eb0000", // hsl(0, 100%, 46%) zero: "#191919", // --blocks unknown: "#333333", scale: "lin" as const, }, } as const; /** * Outer energy level electron count used for ptable Energy levels heatmap. * Ptable: take the last Bohr shell; if it has more than 8 electrons (only Pd: * `[2,8,18,18]`), treat as `0` so the filled 4d¹⁰ shell is not scaled as “18”. */ export function getOuterShellElectronCount( shells: readonly number[] | null | undefined, ): number | null { if (!shells?.length) { return null; } const outer = shells[shells.length - 1]; if (outer == null || Number.isNaN(outer)) { return null; } return outer > 8 ? 0 : outer; } /** @deprecated Prefer getPropertySchemeColor with an explicit scheme. */ export function getLinearPropertyColor( value: number | null | undefined, range: NumericRange | null, cssVars: { start: string; end: string; unknown: string; }, ): string { if (value == null || Number.isNaN(value) || range == null) { return `var(${cssVars.unknown})`; } const normalized = normalizeLinear(value, range.min, range.max); const pct = Math.round(Math.min(1, Math.max(0, normalized)) * 1000) / 10; return `color-mix(in srgb, var(${cssVars.end}) ${pct}%, var(${cssVars.start}))`; } export const atomicMassColorVars = { start: "--property-weight-start", end: "--property-weight-end", unknown: "--property-weight-unknown", } as const; export const energyLevelsColorVars = { start: "--property-electrons-start", end: "--property-electrons-end", unknown: "--property-electrons-unknown", } as const; export const electronegativityColorVars = { start: "--property-electroneg-start", end: "--property-electroneg-end", unknown: "--property-electroneg-unknown", } as const; export const meltingPointColorVars = { start: "--property-meltboil-start", end: "--property-meltboil-end", zero: "--property-meltboil-zero", unknown: "--property-meltboil-unknown", } as const; export const electronAffinityColorVars = { start: "--property-affinity-start", end: "--property-affinity-end", unknown: "--property-affinity-unknown", } as const; export const ionizationEnergyColorVars = { start: "--property-ionize-start", end: "--property-ionize-end", unknown: "--property-ionize-unknown", } as const; export const atomicRadiusColorVars = { start: "--property-radius-start", end: "--property-radius-end", unknown: "--property-radius-unknown", } as const; export const hardnessColorVars = { start: "--property-hardness-start", end: "--property-hardness-end", unknown: "--property-hardness-unknown", } as const; export const modulusColorVars = { start: "--property-modulus-start", end: "--property-modulus-end", unknown: "--property-modulus-unknown", } as const; export const densityColorVars = { start: "--property-density-start", end: "--property-density-end", unknown: "--property-density-unknown", } as const; export const conductivityColorVars = { start: "--property-conduct-start", end: "--property-conduct-end", unknown: "--property-conduct-unknown", } as const; export const heatColorVars = { start: "--property-heat-start", end: "--property-heat-end", unknown: "--property-heat-unknown", } as const; export const abundanceColorVars = { start: "--property-abundance-start", end: "--property-abundance-end", unknown: "--property-abundance-unknown", } as const;