Skip to content

Commit 43d6189

Browse files
AmirMohammad CheraghaliAmirMohammad Cheraghali
authored andcommitted
feat: add transparency controls and fix backbone style
1 parent 303f6c0 commit 43d6189

5 files changed

Lines changed: 262 additions & 29 deletions

File tree

src/App.tsx

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,7 @@ function App() {
287287
if (s.customColors !== undefined) ctrl.setCustomColors(s.customColors);
288288
if (s.measurements !== undefined) ctrl.setMeasurements(s.measurements);
289289
if (s.customBackgroundColor !== undefined) ctrl.setCustomBackgroundColor(s.customBackgroundColor);
290+
if (s.customTransparency !== undefined) ctrl.setCustomTransparency(s.customTransparency);
290291
if (s.hoveredResidue !== undefined) setRemoteHoveredResidue(s.hoveredResidue);
291292
if (s.annotations !== undefined) setAnnotations(s.annotations);
292293
}
@@ -356,6 +357,7 @@ function App() {
356357
// JSON stringify comparison for deep objects to avoid loops/unnecessary updates
357358
// if (vpState.customColors !== undefined && JSON.stringify(vpState.customColors) !== JSON.stringify(ctrl.customColors)) ctrl.setCustomColors(vpState.customColors);
358359
if (vpState.customColors !== undefined) ctrl.setCustomColors(vpState.customColors);
360+
if (vpState.customTransparency !== undefined) ctrl.setCustomTransparency(vpState.customTransparency);
359361
if (vpState.customBackgroundColor !== undefined && vpState.customBackgroundColor !== ctrl.customBackgroundColor) ctrl.setCustomBackgroundColor(vpState.customBackgroundColor);
360362

361363
// Analysis/Measurements
@@ -413,6 +415,7 @@ function App() {
413415
(received.isSpinning === undefined || received.isSpinning === ctrl.isSpinning) &&
414416
(received.highlightedResidue === undefined || deepEqual(received.highlightedResidue, ctrl.highlightedResidue)) &&
415417
(received.customColors === undefined || deepEqual(received.customColors, ctrl.customColors)) &&
418+
(received.customTransparency === undefined || deepEqual(received.customTransparency, ctrl.customTransparency)) &&
416419
(received.measurements === undefined || deepEqual(received.measurements, ctrl.measurements)) &&
417420
(received.customBackgroundColor === undefined || received.customBackgroundColor === ctrl.customBackgroundColor) &&
418421
(received.hoveredResidue === undefined || deepEqual(received.hoveredResidue, hoveredResidue));
@@ -429,6 +432,7 @@ function App() {
429432
isSpinning: ctrl.isSpinning,
430433
highlightedResidue: ctrl.highlightedResidue,
431434
customColors: ctrl.customColors,
435+
customTransparency: ctrl.customTransparency,
432436
measurements: ctrl.measurements,
433437
hoveredResidue: hoveredResidue,
434438
controllerId: controllerId, // Keep syncing the controller ID
@@ -443,6 +447,7 @@ function App() {
443447
controllers[0].isSpinning,
444448
controllers[0].highlightedResidue,
445449
controllers[0].customColors,
450+
controllers[0].customTransparency,
446451
controllers[0].measurements,
447452
controllers[0].customBackgroundColor,
448453
hoveredResidue,
@@ -585,6 +590,7 @@ function App() {
585590
customColors, setCustomColors,
586591
chainStyles, setChainStyle, // Destructure
587592
customStyles, setCustomStyles,
593+
customTransparency, setCustomTransparency,
588594
smoothSheetEnabled, setSmoothSheetEnabled,
589595
isSpinning, setIsSpinning,
590596
isRocking, setIsRocking,
@@ -667,6 +673,7 @@ function App() {
667673
if (vp.isSpinning !== undefined && vp.isSpinning !== ctrl.isSpinning) ctrl.setIsSpinning(vp.isSpinning);
668674
if (vp.customColors) ctrl.setCustomColors(vp.customColors);
669675
if (vp.customColors) ctrl.setCustomColors(vp.customColors);
676+
if (vp.customTransparency) ctrl.setCustomTransparency(vp.customTransparency);
670677

671678
// GUARD: Only update measurements if we haven't touched them locally recently (2s lock)
672679
if (vp.measurements && (Date.now() - lastLocalMeasurementUpdate.current > 2000)) {
@@ -2682,6 +2689,8 @@ function App() {
26822689
setChainStyle={setChainStyle}
26832690
customStyles={customStyles}
26842691
setCustomStyles={setCustomStyles}
2692+
customTransparency={customTransparency}
2693+
setCustomTransparency={setCustomTransparency}
26852694
smoothSheetEnabled={smoothSheetEnabled}
26862695
setSmoothSheetEnabled={setSmoothSheetEnabled}
26872696
onResetCamera={() => handleToolAction('reset')}
@@ -2986,6 +2995,7 @@ function App() {
29862995
customColors={ctrl.customColors}
29872996
chainStyles={ctrl.chainStyles}
29882997
customStyles={ctrl.customStyles}
2998+
customTransparency={ctrl.customTransparency}
29892999
smoothSheetEnabled={ctrl.smoothSheetEnabled}
29903000
palette={colorPalette}
29913001
backgroundColor={

src/components/Controls.tsx

Lines changed: 170 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ import {
3636
Wrench,
3737
X
3838
} from 'lucide-react';
39-
import type { RepresentationType, ColoringType, ChainInfo, Snapshot, Movie, ColorPalette, PDBMetadata, CustomColorRule, CustomStyleRule, Measurement } from '../types';
39+
import type { RepresentationType, ColoringType, ChainInfo, Snapshot, Movie, ColorPalette, PDBMetadata, CustomColorRule, CustomStyleRule, CustomTransparencyRule, Measurement } from '../types';
4040
import type { DataSource } from '../utils/pdbUtils';
4141
import type { HistoryItem } from '../hooks/useHistory';
4242
import { formatChemicalId } from '../utils/pdbUtils';
@@ -400,10 +400,14 @@ interface ControlsProps {
400400
onResetCamera?: () => void;
401401

402402
// Custom Residue Styles
403-
customStyles?: CustomStyleRule[];
404-
setCustomStyles?: (styles: CustomStyleRule[] | ((prev: CustomStyleRule[]) => CustomStyleRule[])) => void;
403+
customStyles: CustomStyleRule[];
404+
setCustomStyles: (styles: CustomStyleRule[] | ((prev: CustomStyleRule[]) => CustomStyleRule[])) => void;
405405

406-
smoothSheetEnabled?: boolean;
406+
// Transparency
407+
customTransparency: CustomTransparencyRule[];
408+
setCustomTransparency: (rules: CustomTransparencyRule[] | ((prev: CustomTransparencyRule[]) => CustomTransparencyRule[])) => void;
409+
410+
smoothSheetEnabled: boolean;
407411
setSmoothSheetEnabled?: (enabled: boolean | ((prev: boolean) => boolean)) => void;
408412
}
409413

@@ -426,7 +430,9 @@ export const Controls: React.FC<ControlsProps> = ({
426430
setChainStyle, // Destructure
427431
customStyles,
428432
setCustomStyles,
429-
433+
customTransparency,
434+
setCustomTransparency,
435+
smoothSheetEnabled,
430436
chains,
431437
ligands,
432438

@@ -449,7 +455,6 @@ export const Controls: React.FC<ControlsProps> = ({
449455
setShowLigands,
450456
showIons,
451457
setShowIons,
452-
smoothSheetEnabled,
453458
setSmoothSheetEnabled,
454459
onRecordMovie,
455460
isRecording,
@@ -577,6 +582,36 @@ export const Controls: React.FC<ControlsProps> = ({
577582
const [customColorValue, setCustomColorValue] = useState<string>("#ff0000");
578583
const [customColorMode, setCustomColorMode] = useState<'chain' | 'residue'>('chain');
579584

585+
// Transparency State
586+
const [transparencyMode, setTransparencyMode] = useState<'chain' | 'residue'>('chain');
587+
const [transparencyChain, setTransparencyChain] = useState<string>('');
588+
const [transparencyResidues, setTransparencyResidues] = useState('');
589+
const [transparencyValue, setTransparencyValue] = useState(50); // 0-100
590+
591+
// Handlers
592+
const handleAddTransparencyRule = () => {
593+
if (!transparencyChain && transparencyMode === 'chain') return;
594+
if (transparencyChain === 'Select' && transparencyMode === 'chain') return;
595+
596+
const newRule: CustomTransparencyRule = {
597+
id: crypto.randomUUID(),
598+
chain: transparencyChain || 'All',
599+
residues: transparencyMode === 'residue' ? transparencyResidues : undefined,
600+
opacity: transparencyValue / 100
601+
};
602+
603+
if (setCustomTransparency) {
604+
setCustomTransparency(prev => [...prev, newRule]);
605+
setTransparencyResidues('');
606+
}
607+
};
608+
609+
const handleRemoveTransparencyRule = (id: string) => {
610+
if (setCustomTransparency) {
611+
setCustomTransparency(prev => prev.filter(r => r.id !== id));
612+
}
613+
};
614+
580615
const handleAddCustomColor = () => {
581616
let selectionString = '';
582617
if (customChain) {
@@ -710,7 +745,7 @@ export const Controls: React.FC<ControlsProps> = ({
710745

711746
<div className={`
712747
absolute top-0 left-0 z-50
713-
h-[100dvh] w-full sm:w-[340px]
748+
h-[100dvh] w-full sm:w-[340px]
714749
backdrop-blur-xl border-r shadow-2xl
715750
transition-transform duration-300 ease-in-out
716751
flex flex-col
@@ -1783,6 +1818,134 @@ export const Controls: React.FC<ControlsProps> = ({
17831818

17841819
</div>
17851820

1821+
{/* Transparency Style */}
1822+
<div className={`p-2 rounded-lg border ${cardBg} mt-2`}>
1823+
<div className="flex items-center justify-between mb-2">
1824+
<h4 className={`text-[10px] font-bold uppercase tracking-wider ${subtleText}`}>Transparency</h4>
1825+
</div>
1826+
1827+
<div className="flex bg-black/10 dark:bg-white/5 rounded-lg p-0.5 mb-2">
1828+
<button
1829+
onClick={() => setTransparencyMode('chain')}
1830+
className={`flex-1 py-1 text-[10px] font-medium rounded-md transition-all ${transparencyMode === 'chain' ? 'bg-blue-500 text-white shadow-sm' : 'text-neutral-500 hover:text-neutral-300'}`}
1831+
>
1832+
Chain
1833+
</button>
1834+
<button
1835+
onClick={() => setTransparencyMode('residue')}
1836+
className={`flex-1 py-1 text-[10px] font-medium rounded-md transition-all ${transparencyMode === 'residue' ? 'bg-blue-500 text-white shadow-sm' : 'text-neutral-500 hover:text-neutral-300'}`}
1837+
>
1838+
Residue
1839+
</button>
1840+
</div>
1841+
1842+
{/* Chain Selection */}
1843+
{transparencyMode === 'chain' && (
1844+
<div className="space-y-2">
1845+
<label className={`text-[9px] font-bold uppercase tracking-wider block ${subtleText} opacity-70`}>Chain</label>
1846+
<div className={`relative flex items-center rounded-lg border transition-all ${inputBg}`}>
1847+
<select
1848+
value={transparencyChain}
1849+
onChange={(e) => setTransparencyChain(e.target.value)}
1850+
className="w-full appearance-none bg-transparent py-1.5 pl-2 pr-4 text-xs font-mono outline-none"
1851+
>
1852+
<option value="">Select Chain</option>
1853+
<option value="All">All Chains</option>
1854+
{chains.map(c => (
1855+
<option key={c.name} value={c.name}>Chain {c.name}</option>
1856+
))}
1857+
</select>
1858+
<ChevronDown className="absolute right-2 top-1/2 -translate-y-1/2 w-3 h-3 opacity-50 pointer-events-none" />
1859+
</div>
1860+
</div>
1861+
)}
1862+
1863+
{/* Residue Range Selection */}
1864+
{transparencyMode === 'residue' && (
1865+
<div className="flex gap-2">
1866+
<div className="w-[80px]">
1867+
<label className={`text-[9px] font-bold uppercase tracking-wider mb-1 block ${subtleText} opacity-70`}>Chain</label>
1868+
<div className={`relative flex items-center rounded-lg border transition-all ${inputBg}`}>
1869+
<select
1870+
value={transparencyChain}
1871+
onChange={(e) => setTransparencyChain(e.target.value)}
1872+
className="w-full appearance-none bg-transparent py-1.5 pl-2 pr-4 text-xs font-mono outline-none"
1873+
>
1874+
<option value="All">All</option>
1875+
{chains.map(c => (
1876+
<option key={c.name} value={c.name}>:{c.name}</option>
1877+
))}
1878+
</select>
1879+
<ChevronDown className="absolute right-1 top-1/2 -translate-y-1/2 w-3 h-3 opacity-50 pointer-events-none" />
1880+
</div>
1881+
</div>
1882+
<div className="flex-1">
1883+
<label className={`text-[9px] font-bold uppercase tracking-wider mb-1 block ${subtleText} opacity-70`}>Residues</label>
1884+
<input
1885+
type="text"
1886+
value={transparencyResidues}
1887+
onChange={(e) => setTransparencyResidues(e.target.value)}
1888+
placeholder="e.g. 50-60"
1889+
className={`w-full py-1.5 px-2 rounded-lg border text-xs outline-none transition-all ${inputBg}`}
1890+
/>
1891+
</div>
1892+
</div>
1893+
)}
1894+
1895+
{/* Opacity Slider */}
1896+
<div className="mt-2 space-y-1">
1897+
<div className="flex justify-between">
1898+
<label className={`text-[9px] font-bold uppercase tracking-wider block ${subtleText} opacity-70`}>Opacity</label>
1899+
<span className="text-[9px] font-mono">{transparencyValue}%</span>
1900+
</div>
1901+
<input
1902+
type="range"
1903+
min="0"
1904+
max="100"
1905+
value={transparencyValue}
1906+
onChange={(e) => setTransparencyValue(Number(e.target.value))}
1907+
className="w-full h-1.5 bg-neutral-200 dark:bg-neutral-700 rounded-lg appearance-none cursor-pointer"
1908+
/>
1909+
</div>
1910+
1911+
<button
1912+
onClick={handleAddTransparencyRule}
1913+
disabled={transparencyMode === 'chain' && !transparencyChain}
1914+
className="w-full mt-2 py-1.5 bg-blue-600 hover:bg-blue-500 disabled:opacity-50 disabled:cursor-not-allowed text-white rounded-lg text-xs font-bold transition-all shadow-sm active:scale-95 flex items-center justify-center"
1915+
>
1916+
<Plus size={14} className="mr-1" />
1917+
Add Rule
1918+
</button>
1919+
1920+
{/* Active Rules */}
1921+
{customTransparency && customTransparency.length > 0 && (
1922+
<div className="space-y-1 pt-2 mt-2 border-t border-dashed border-gray-200 dark:border-gray-700">
1923+
<div className={`text-[9px] font-bold uppercase tracking-wider ${subtleText} opacity-70`}>Active Rules</div>
1924+
<div className="space-y-1 max-h-[100px] overflow-y-auto custom-scrollbar">
1925+
{customTransparency.map((rule) => (
1926+
<div key={rule.id} className={`flex items-center justify-between p-2 rounded border ${isLightMode ? 'bg-white border-neutral-200' : 'bg-neutral-800 border-white/5'}`}>
1927+
<div className="flex flex-col gap-0.5">
1928+
<div className="flex items-center gap-2">
1929+
<span className={`px-1.5 py-0.5 rounded text-[9px] font-mono font-bold ${isLightMode ? 'bg-neutral-100 text-neutral-700' : 'bg-neutral-700 text-neutral-300'}`}>
1930+
{rule.chain === 'All' ? '*' : `:${rule.chain}`}
1931+
</span>
1932+
{rule.residues && <span className="text-[10px] opacity-70 font-mono">{rule.residues}</span>}
1933+
</div>
1934+
<span className="text-[9px] text-blue-500 font-medium">Opacity: {(rule.opacity * 100).toFixed(0)}%</span>
1935+
</div>
1936+
<button
1937+
onClick={() => handleRemoveTransparencyRule(rule.id)}
1938+
className="p-1 hover:bg-red-500/20 text-neutral-400 hover:text-red-500 rounded transition-colors"
1939+
>
1940+
<Trash2 size={12} />
1941+
</button>
1942+
</div>
1943+
))}
1944+
</div>
1945+
</div>
1946+
)}
1947+
</div>
1948+
17861949
{/* Custom Rules */}
17871950
</div>
17881951
</SidebarSection>

0 commit comments

Comments
 (0)