Skip to content

Commit 0bbf5da

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: Implement Global Chain Smoothing for properly blended custom gradients
1 parent 6fa082f commit 0bbf5da

1 file changed

Lines changed: 82 additions & 45 deletions

File tree

src/components/ProteinViewer.tsx

Lines changed: 82 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1870,63 +1870,100 @@ export const ProteinViewer = forwardRef<ProteinViewerRef, ProteinViewerProps>(({
18701870
}
18711871
});
18721872

1873-
// Helper: blend two colors
1874-
const blendColors = (c1: number, c2: number, factor: number) => {
1875-
const r = Math.round(((c1 >> 16) & 0xFF) + (((c2 >> 16) & 0xFF) - ((c1 >> 16) & 0xFF)) * factor);
1876-
const g = Math.round(((c1 >> 8) & 0xFF) + (((c2 >> 8) & 0xFF) - ((c1 >> 8) & 0xFF)) * factor);
1877-
const b = Math.round((c1 & 0xFF) + ((c2 & 0xFF) - (c1 & 0xFF)) * factor);
1878-
return (r << 16) | (g << 8) | b;
1879-
};
18801873

1881-
// Apply gradient transitions at boundaries
1882-
const gradWidth = 2; // Number of residues to blend on each side
1874+
// --- GLOBAL CHAIN SMOOTHING ALGORITHM ---
1875+
// 1. Linearize chain residues to array
1876+
// 2. Assign base vs custom colors
1877+
// 3. Apply floating-point moving average smoothing
1878+
// 4. Map back to atoms
18831879

18841880
try {
1885-
component.structure.eachResidue((res: any) => {
1886-
const key = `${res.chainname}:${res.resno}`;
1887-
const customCol = residueColorMap.get(key);
1888-
1889-
// Get element color for this residue
1890-
let baseCol = 0xCCCCCC;
1891-
try {
1892-
const firstAtom = Array.from(res.iterateAtom())[0];
1893-
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1894-
if (ElementScheme) {
1895-
const es = new ElementScheme({});
1896-
baseCol = es.atomColor(firstAtom);
1881+
component.structure.eachChain((chain: any) => {
1882+
// Collection phase
1883+
const residues: any[] = [];
1884+
const bgColors: number[] = []; // Base colors (element/custom)
1885+
1886+
chain.eachResidue((res: any) => {
1887+
residues.push(res);
1888+
const key = `${res.chainname}:${res.resno}`;
1889+
let col = residueColorMap.get(key);
1890+
1891+
if (col === undefined) {
1892+
// Default to element color
1893+
col = 0xCCCCCC;
1894+
try {
1895+
const firstAtom = Array.from(res.iterateAtom())[0];
1896+
const ElementScheme = window.NGL.ColormakerRegistry.getScheme('element');
1897+
if (ElementScheme) {
1898+
const es = new ElementScheme({});
1899+
col = es.atomColor(firstAtom);
1900+
}
1901+
} catch (e) { /* ignore */ }
18971902
}
1898-
} catch (e) { /* ignore */ }
1899-
1900-
let finalCol = customCol !== undefined ? customCol : baseCol;
1901-
1902-
// If not custom colored, check for nearby custom residues for gradient
1903-
if (customCol === undefined) {
1904-
let nearestCustom = null;
1905-
let nearestDist = gradWidth + 1;
1906-
1907-
for (let d = -gradWidth; d <= gradWidth; d++) {
1908-
if (d === 0) continue;
1909-
const nKey = `${res.chainname}:${res.resno + d}`;
1910-
const nCol = residueColorMap.get(nKey);
1911-
if (nCol !== undefined && Math.abs(d) < nearestDist) {
1912-
nearestDist = Math.abs(d);
1913-
nearestCustom = nCol;
1903+
bgColors.push(col || 0xCCCCCC);
1904+
});
1905+
1906+
// Smoothing Phase (RGB separation)
1907+
// We use a floating point buffer for precision accumulation
1908+
const len = bgColors.length;
1909+
let rBuffer = new Float32Array(len);
1910+
let gBuffer = new Float32Array(len);
1911+
let bBuffer = new Float32Array(len);
1912+
1913+
// Initialize
1914+
for (let i = 0; i < len; i++) {
1915+
const c = bgColors[i] || 0xCCCCCC;
1916+
rBuffer[i] = (c >> 16) & 0xFF;
1917+
gBuffer[i] = (c >> 8) & 0xFF;
1918+
bBuffer[i] = c & 0xFF;
1919+
}
1920+
1921+
// Multi-pass smoothing (3 passes of [0.25, 0.5, 0.25] kernel approx)
1922+
const passes = 3;
1923+
for (let p = 0; p < passes; p++) {
1924+
const newR = new Float32Array(len);
1925+
const newG = new Float32Array(len);
1926+
const newB = new Float32Array(len);
1927+
1928+
for (let i = 0; i < len; i++) {
1929+
let sumR = 0, sumG = 0, sumB = 0;
1930+
let count = 0;
1931+
1932+
// Previous
1933+
if (i > 0) {
1934+
sumR += rBuffer[i - 1]; sumG += gBuffer[i - 1]; sumB += bBuffer[i - 1];
1935+
count++;
1936+
}
1937+
// Current (weight x2 for stability)
1938+
sumR += rBuffer[i] * 2; sumG += gBuffer[i] * 2; sumB += bBuffer[i] * 2;
1939+
count += 2;
1940+
// Next
1941+
if (i < len - 1) {
1942+
sumR += rBuffer[i + 1]; sumG += gBuffer[i + 1]; sumB += bBuffer[i + 1];
1943+
count++;
19141944
}
1915-
}
19161945

1917-
if (nearestCustom !== null) {
1918-
const blend = 1 - (nearestDist / (gradWidth + 1));
1919-
finalCol = blendColors(baseCol, nearestCustom, blend);
1946+
newR[i] = sumR / count;
1947+
newG[i] = sumG / count;
1948+
newB[i] = sumB / count;
19201949
}
1950+
rBuffer = newR; gBuffer = newG; bBuffer = newB;
19211951
}
19221952

1923-
// Apply color to all atoms in residue
1924-
res.eachAtom((atom: any) => {
1925-
atomColorMap.set(atom.index, finalCol);
1953+
// Assignment Phase
1954+
residues.forEach((res, idx) => {
1955+
const r = Math.round(rBuffer[idx]);
1956+
const g = Math.round(gBuffer[idx]);
1957+
const b = Math.round(bBuffer[idx]);
1958+
const finalColor = (r << 16) | (g << 8) | b;
1959+
1960+
res.eachAtom((atom: any) => {
1961+
atomColorMap.set(atom.index, finalColor);
1962+
});
19261963
});
19271964
});
19281965
} catch (e) {
1929-
console.warn("Error in gradient processing", e);
1966+
console.warn("Global smoothing failed", e);
19301967
}
19311968

19321969
// Fallback: if atomColorMap is still empty (e.g. error above), force populate

0 commit comments

Comments
 (0)