Skip to content

Commit 5ce95a1

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Add smooth gradient transitions to Custom coloring mode
1 parent ca5f345 commit 5ce95a1

1 file changed

Lines changed: 56 additions & 21 deletions

File tree

src/components/ProteinViewer.tsx

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1831,20 +1831,22 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18311831
name: 'charge_neutral'
18321832
});
18331833
} else if (currentColoring === 'custom') {
1834-
// --- CUSTOM COLORING MODE: Smooth Single-Mesh Rendering ---
1835-
// This mode uses a custom NGL color scheme for smooth transitions
1834+
// --- CUSTOM COLORING MODE: Smooth Gradient Rendering ---
1835+
// This mode uses residue-level color processing with gradient blending
18361836

18371837
const schemeId = 'custom_smooth_coloring';
18381838
const atomColorMap = new Map<number, number>();
1839+
const residueColorMap = new Map<string, number>();
18391840

1840-
// Build the color map from custom color rules
1841+
// Build residue-level color map from custom rules
18411842
if (hasValidCustomRules) {
18421843
customColors.forEach(rule => {
18431844
if (rule.color && rule.target) {
18441845
const colorHex = new window.NGL.Color(rule.color).getHex();
18451846
try {
1846-
component.structure.eachAtom((atom: any) => {
1847-
atomColorMap.set(atom.index, colorHex);
1847+
component.structure.eachResidue((residue: any) => {
1848+
const resKey = `${residue.chainname}:${residue.resno}`;
1849+
residueColorMap.set(resKey, colorHex);
18481850
}, new window.NGL.Selection(rule.target));
18491851
} catch (e) {
18501852
console.warn("Invalid custom color selection:", rule.target, e);
@@ -1853,30 +1855,63 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18531855
});
18541856
}
18551857

1856-
// Register the custom color scheme
1857-
window.NGL.ColormakerRegistry.addScheme(function (this: any, params: any) {
1858-
this.atomColor = function (atom: any) {
1859-
// Check if this atom has a custom color
1860-
const customColor = atomColorMap.get(atom.index);
1861-
if (customColor !== undefined) {
1862-
return customColor;
1858+
// Helper: blend two colors
1859+
const blendColors = (c1: number, c2: number, factor: number) => {
1860+
const r = Math.round(((c1 >> 16) & 0xFF) + (((c2 >> 16) & 0xFF) - ((c1 >> 16) & 0xFF)) * factor);
1861+
const g = Math.round(((c1 >> 8) & 0xFF) + (((c2 >> 8) & 0xFF) - ((c1 >> 8) & 0xFF)) * factor);
1862+
const b = Math.round((c1 & 0xFF) + ((c2 & 0xFF) - (c1 & 0xFF)) * factor);
1863+
return (r << 16) | (g << 8) | b;
1864+
};
1865+
1866+
// Apply gradient transitions at boundaries
1867+
const gradWidth = 2; // Number of residues to blend on each side
1868+
component.structure.eachResidue((res: any) => {
1869+
const key = `${res.chainname}:${res.resno}`;
1870+
const customCol = residueColorMap.get(key);
1871+
1872+
// Get element color for this residue
1873+
const firstAtom = Array.from(res.iterateAtom())[0];
1874+
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1875+
const baseCol = ElementScheme ? new ElementScheme({}).atomColor(firstAtom) : 0xCCCCCC;
1876+
1877+
let finalCol = customCol !== undefined ? customCol : baseCol;
1878+
1879+
// If not custom colored, check for nearby custom residues for gradient
1880+
if (customCol === undefined) {
1881+
let nearestCustom = null;
1882+
let nearestDist = gradWidth + 1;
1883+
1884+
for (let d = -gradWidth; d <= gradWidth; d++) {
1885+
if (d === 0) continue;
1886+
const nKey = `${res.chainname}:${res.resno + d}`;
1887+
const nCol = residueColorMap.get(nKey);
1888+
if (nCol !== undefined && Math.abs(d) < nearestDist) {
1889+
nearestDist = Math.abs(d);
1890+
nearestCustom = nCol;
1891+
}
18631892
}
18641893

1865-
// Fallback to element coloring for undefined regions
1866-
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1867-
if (ElementScheme) {
1868-
const elementScheme = new ElementScheme(params);
1869-
return elementScheme.atomColor(atom);
1894+
if (nearestCustom !== null) {
1895+
const blend = 1 - (nearestDist / (gradWidth + 1));
1896+
finalCol = blendColors(baseCol, nearestCustom, blend);
18701897
}
1898+
}
18711899

1872-
// Last resort: light gray
1873-
return 0xCCCCCC;
1900+
// Apply color to all atoms in residue
1901+
res.eachAtom((atom: any) => {
1902+
atomColorMap.set(atom.index, finalCol);
1903+
});
1904+
});
1905+
1906+
// Register the custom color scheme
1907+
window.NGL.ColormakerRegistry.addScheme(function (this: any, params: any) {
1908+
this.atomColor = function (atom: any) {
1909+
return atomColorMap.get(atom.index) || 0xCCCCCC;
18741910
};
18751911
}, schemeId);
18761912

18771913
// Create single representation with custom scheme
18781914
if (repType === 'cartoon') {
1879-
// Force recalculation for cartoons
18801915
try {
18811916
component.structure.eachModel((m: any) => {
18821917
if (m.calculateSecondaryStructure) m.calculateSecondaryStructure();
@@ -1895,7 +1930,7 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18951930
});
18961931
}
18971932

1898-
console.log(`Custom coloring mode: ${atomColorMap.size} atoms with custom colors`);
1933+
console.log(`Custom coloring: ${residueColorMap.size} residues colored, ${atomColorMap.size} atoms total`);
18991934
} else {
19001935
// Standard Coloring for other modes (sstruc, element, etc.) -> Robust Native NGL
19011936
// REVERTED to use 'color' property as previously working.

0 commit comments

Comments
 (0)