<p class="hint">Slide and rotate the <b>ligand</b> (colored blob) into the protein <b>pocket</b> (dark notch). The closer it sits to the pocket's center and the better its angle matches, the higher the <b>binding score</b>. Scoring a pose is instant — finding the best pose is the hard search.</p>
<canvas id="scene" width="320" height="220"></canvas>
<div class="score" id="score">Binding score: 0</div>
<div class="controls">
<label>Slide X <input id="x" type="range" min="0" max="320" value="40"></label>
<label>Slide Y <input id="y" type="range" min="0" max="220" value="40"></label>
<label>Rotate <input id="a" type="range" min="0" max="360" value="0"></label>
</div>
<div class="btns">
<button id="auto" type="button">Auto-dock</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { background: #f1f4f7; border: 1px solid #cdd9e3; border-radius: 8px; display: block; }
.score { font-size: 1.05rem; font-weight: 700; margin: .5rem 0; color: #1d3557; min-height: 1.4em; }
.controls { display: flex; flex-direction: column; gap: .35rem; margin: .3rem 0 .6rem; }
.controls label { font-size: .85rem; display: flex; align-items: center; gap: .5rem; }
.controls input { flex: 1; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Molecular docking, in miniature. A pose is (x, y, angle). The "binding score" rewards being near the pocket center at the ideal angle and penalizes distance and angle mismatch. Scoring a pose is a quick formula; finding the best pose means searching the (x, y, angle) space — and with many rotatable bonds that space explodes, which is why real docking (flexible-ligand) is NP-hard and tools use heuristic random/genetic search rather than exact optimization.
const cv = document.getElementById('scene'), ctx = cv.getContext('2d');
const TARGET = { x: 210, y: 120, a: 40 }; // ideal pose: snug in the pocket
const pose = { x: 40, y: 40, a: 0 };
// Score in 0..100: 100 at the perfect pose, falling off with distance & angle gap.
function score(p){
const dx = p.x - TARGET.x, dy = p.y - TARGET.y;
const dist = Math.sqrt(dx*dx + dy*dy); // pixels off-center
let da = Math.abs(((p.a - TARGET.a) % 360 + 360) % 360); // angle gap 0..360
if (da > 180) da = 360 - da; // wrap to 0..180
const distTerm = Math.max(0, 1 - dist / 200); // 1 when centered
const angTerm = Math.max(0, 1 - da / 90); // 1 when aligned
return Math.round(100 * distTerm * angTerm);
}
function draw(){
ctx.clearRect(0,0,cv.width,cv.height);
// protein body with a pocket (notch) around TARGET
ctx.fillStyle = '#9fb2c8';
ctx.beginPath();
ctx.moveTo(150,20); ctx.lineTo(300,20); ctx.lineTo(300,200); ctx.lineTo(150,200);
ctx.arc(TARGET.x, TARGET.y, 34, Math.PI*0.5, Math.PI*1.5, true);
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#3a4a5e';
ctx.beginPath(); ctx.arc(TARGET.x, TARGET.y, 30, 0, 2*Math.PI); ctx.fill();
// ligand: a blob with an orientation marker
const s = score(pose);
ctx.save();
ctx.translate(pose.x, pose.y); ctx.rotate(pose.a*Math.PI/180);
ctx.fillStyle = s > 70 ? '#0a7d33' : (s > 30 ? '#e9a23b' : '#e63946');
ctx.beginPath(); ctx.ellipse(0,0,24,16,0,0,2*Math.PI); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(22,0); ctx.stroke();
ctx.restore();
document.getElementById('score').textContent = fmt('Binding score: %s%', { s: s });
}
function syncInputs(){
document.getElementById('x').value = Math.round(pose.x);
document.getElementById('y').value = Math.round(pose.y);
document.getElementById('a').value = Math.round(((pose.a%360)+360)%360);
}
['x','y','a'].forEach(id => document.getElementById(id).addEventListener('input', e => {
pose[id] = +e.target.value; draw();
}));
// Auto-dock: random search (a simple heuristic), keep the best pose found.
document.getElementById('auto').addEventListener('click', () => {
let best = { ...pose }, bestS = score(pose), tries = 0;
for (let i = 0; i < 4000; i++){ tries++;
const cand = { x: Math.random()*320, y: Math.random()*220, a: Math.random()*360 };
const s = score(cand);
if (s > bestS){ bestS = s; best = cand; }
}
Object.assign(pose, best); syncInputs(); draw();
console.log('Auto-dock: best score', bestS, 'after', tries, 'random poses');
});
document.getElementById('reset').addEventListener('click', () => {
pose.x = 40; pose.y = 40; pose.a = 0; syncInputs(); draw();
});
syncInputs(); draw();
<p class="hint">Slide and rotate the <b>ligand</b> (colored blob) into the protein <b>pocket</b> (dark notch). The closer it sits to the pocket's center and the better its angle matches, the higher the <b>binding score</b>. Scoring a pose is instant — finding the best pose is the hard search.</p>
<canvas id="scene" width="320" height="220"></canvas>
<div class="score" id="score">Binding score: 0</div>
<div class="controls">
<label>Slide X <input id="x" type="range" min="0" max="320" value="40"></label>
<label>Slide Y <input id="y" type="range" min="0" max="220" value="40"></label>
<label>Rotate <input id="a" type="range" min="0" max="360" value="0"></label>
</div>
<div class="btns">
<button id="auto" type="button">Auto-dock</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
canvas { background: #f1f4f7; border: 1px solid #cdd9e3; border-radius: 8px; display: block; }
.score { font-size: 1.05rem; font-weight: 700; margin: .5rem 0; color: #1d3557; min-height: 1.4em; }
.controls { display: flex; flex-direction: column; gap: .35rem; margin: .3rem 0 .6rem; }
.controls label { font-size: .85rem; display: flex; align-items: center; gap: .5rem; }
.controls input { flex: 1; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Molecular docking, in miniature. A pose is (x, y, angle). The "binding score" rewards being near the pocket center at the ideal angle and penalizes distance and angle mismatch. Scoring a pose is a quick formula; finding the best pose means searching the (x, y, angle) space — and with many rotatable bonds that space explodes, which is why real docking (flexible-ligand) is NP-hard and tools use heuristic random/genetic search rather than exact optimization.
const cv = document.getElementById('scene'), ctx = cv.getContext('2d');
const TARGET = { x: 210, y: 120, a: 40 }; // ideal pose: snug in the pocket
const pose = { x: 40, y: 40, a: 0 };
// Score in 0..100: 100 at the perfect pose, falling off with distance & angle gap.
function score(p){
const dx = p.x - TARGET.x, dy = p.y - TARGET.y;
const dist = Math.sqrt(dx*dx + dy*dy); // pixels off-center
let da = Math.abs(((p.a - TARGET.a) % 360 + 360) % 360); // angle gap 0..360
if (da > 180) da = 360 - da; // wrap to 0..180
const distTerm = Math.max(0, 1 - dist / 200); // 1 when centered
const angTerm = Math.max(0, 1 - da / 90); // 1 when aligned
return Math.round(100 * distTerm * angTerm);
}
function draw(){
ctx.clearRect(0,0,cv.width,cv.height);
// protein body with a pocket (notch) around TARGET
ctx.fillStyle = '#9fb2c8';
ctx.beginPath();
ctx.moveTo(150,20); ctx.lineTo(300,20); ctx.lineTo(300,200); ctx.lineTo(150,200);
ctx.arc(TARGET.x, TARGET.y, 34, Math.PI*0.5, Math.PI*1.5, true);
ctx.closePath(); ctx.fill();
ctx.fillStyle = '#3a4a5e';
ctx.beginPath(); ctx.arc(TARGET.x, TARGET.y, 30, 0, 2*Math.PI); ctx.fill();
// ligand: a blob with an orientation marker
const s = score(pose);
ctx.save();
ctx.translate(pose.x, pose.y); ctx.rotate(pose.a*Math.PI/180);
ctx.fillStyle = s > 70 ? '#0a7d33' : (s > 30 ? '#e9a23b' : '#e63946');
ctx.beginPath(); ctx.ellipse(0,0,24,16,0,0,2*Math.PI); ctx.fill();
ctx.strokeStyle = '#fff'; ctx.lineWidth = 3;
ctx.beginPath(); ctx.moveTo(0,0); ctx.lineTo(22,0); ctx.stroke();
ctx.restore();
document.getElementById('score').textContent = fmt('Binding score: %s%', { s: s });
}
function syncInputs(){
document.getElementById('x').value = Math.round(pose.x);
document.getElementById('y').value = Math.round(pose.y);
document.getElementById('a').value = Math.round(((pose.a%360)+360)%360);
}
['x','y','a'].forEach(id => document.getElementById(id).addEventListener('input', e => {
pose[id] = +e.target.value; draw();
}));
// Auto-dock: random search (a simple heuristic), keep the best pose found.
document.getElementById('auto').addEventListener('click', () => {
let best = { ...pose }, bestS = score(pose), tries = 0;
for (let i = 0; i < 4000; i++){ tries++;
const cand = { x: Math.random()*320, y: Math.random()*220, a: Math.random()*360 };
const s = score(cand);
if (s > bestS){ bestS = s; best = cand; }
}
Object.assign(pose, best); syncInputs(); draw();
console.log('Auto-dock: best score', bestS, 'after', tries, 'random poses');
});
document.getElementById('reset').addEventListener('click', () => {
pose.x = 40; pose.y = 40; pose.a = 0; syncInputs(); draw();
});
syncInputs(); draw();
Comments
Loading comments...