@@ -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