fix(geom8 ch1): 5 drag-интерактивов — фикс stale closure после innerHTML replace

Корневая причина: каждый redraw() заменял SVG через innerHTML,
уничтожая элемент svgEl который onMove захватил в замыкании через
const svgEl = wrap.querySelector('svg'). На следующем pointermove
svgEl.getBoundingClientRect() возвращал {left:0,top:0,w:0,h:0} —
вершина прыгала в начало координат SVG, drag разваливался.

Применено к 5 интерактивам:
1. §4 Конструктор параллелограмма
2. §5 Живой параллелограмм — все свойства
3. §7 Живой прямоугольник — равенство диагоналей
4. §8 Признак прямоугольника — живая демонстрация
5. §9 Живой ромб

Что изменилось:
- Состояние (p4Active, p4Vname, p4OffX/Y и т.д.) вынесено на уровень
  модуля, ВНЕ redraw().
- Один pointerdown-listener на wrapper-div через делегирование событий
  (ev.target.closest('[data-v]')).
- clientToSvg() делает свежий document.getElementById(SVG_ID) на
  каждый вызов — не закрепляется на устаревшем DOM-узле.
- SVG получают стабильный id.
- viewBox.baseVal для точного coordinate scaling.
- Offset capture на pointerdown (нет snap-to-pointer).
- touch-action:none на SVG root.
- Hit area r=16 (visible r=8) — легче попасть на touch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-28 21:31:01 +03:00
parent e6b2d7f321
commit df8b5ff18b
+267 -200
View File
@@ -1991,76 +1991,89 @@ function buildP4(){
/* == INIT: SVG-конструктор параллелограмма == */
(function(){
const W=380, H=300;
let A={x:60,y:220}, B={x:180,y:220}, D={x:100,y:100};
const W=380, H=300, SVG_ID='p4-pgram-svg';
let A={x:60,y:220}, B={x:240,y:220}, D={x:120,y:100};
function getC(){ return {x:D.x+(B.x-A.x),y:D.y+(B.y-A.y)}; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function angle(O,P,Q){
function angDeg(O,P,Q){
const ax=P.x-O.x,ay=P.y-O.y,bx=Q.x-O.x,by=Q.y-O.y;
return Math.acos(Math.max(-1,Math.min(1,(ax*bx+ay*by)/(Math.hypot(ax,ay)*Math.hypot(bx,by)))))*180/Math.PI;
}
function svgToClient(svgEl,sx,sy){
const r=svgEl.getBoundingClientRect(), vb=svgEl.viewBox.baseVal;
return {x:(sx-vb.x)/vb.width*r.width+r.left, y:(sy-vb.y)/vb.height*r.height+r.top};
}
function clientToSvg(clientX,clientY){
const svgEl=document.getElementById(SVG_ID); if(!svgEl) return {x:0,y:0};
const r=svgEl.getBoundingClientRect(), vb=svgEl.viewBox.baseVal;
return {x:(clientX-r.left)/r.width*vb.width+vb.x, y:(clientY-r.top)/r.height*vb.height+vb.y};
}
function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); }
function redraw(){
const C=getC();
const vs={A,B,C,D};
const pts=[A,B,C,D];
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
const C=getC(); const pts=[A,B,C,D]; const labels=['A','B','C','D'];
const pcx=(A.x+B.x+C.x+D.x)/4, pcy=(A.y+B.y+C.y+D.y)/4;
let s='<svg id="'+SVG_ID+'" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<polygon points="'+pts.map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(220,38,38,.10)" stroke="#dc2626" stroke-width="2.5" stroke-linejoin="round"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5 3"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#94a3b8" stroke-width="1" stroke-dasharray="5 3"/>';
const labels=['A','B','C','D'];
const cx=(A.x+B.x+C.x+D.x)/4, cy=(A.y+B.y+C.y+D.y)/4;
const sides=[['AB',A,B],['BC',B,C],['CD',C,D],['DA',D,A]];
sides.forEach(([nm,p1,p2])=>{
const mx=(p1.x+p2.x)/2,my=(p1.y+p2.y)/2;
s+='<text x="'+mx+'" y="'+my+'" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace" dy="-8">'+dist(p1,p2).toFixed(1)+'</text>';
});
pts.forEach((v,i)=>{
const movable=(i===1||i===3);
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(movable?10:7)+'" fill="'+(movable?'#dc2626':'#dc2626')+'" opacity="'+(movable?.25:.15)+'" class="p4-vh" data-v="'+labels[i]+'"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(movable?6:5)+'" fill="#dc2626" stroke="#fff" stroke-width="2" '+(movable?'class="p4-vh" data-v="'+labels[i]+'"':'')+'style="cursor:'+(movable?'grab':'default')+'"/>';
const lx=v.x+(v.x-cx)*0.25,ly=v.y+(v.y-cy)*0.25;
const m=(i===1||i===3);
const lx=v.x+(v.x-pcx)*0.28, ly=v.y+(v.y-pcy)*0.28;
if(m){
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="16" fill="#dc2626" opacity=".12" data-v="'+labels[i]+'" style="cursor:grab"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="8" fill="#dc2626" stroke="#fff" stroke-width="2.5" data-v="'+labels[i]+'" style="cursor:grab"/>';
} else {
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#dc2626" stroke="#fff" stroke-width="2"/>';
}
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#b91c1c" font-family="Unbounded,sans-serif">'+labels[i]+'</text>';
});
const sides=[['AB',A,B],['BC',B,C],['CD',C,D],['DA',D,A]];
sides.forEach(([name,p1,p2])=>{
const mx=(p1.x+p2.x)/2,my=(p1.y+p2.y)/2;
const d=dist(p1,p2);
s+='<text x="'+mx+'" y="'+my+'" text-anchor="middle" dominant-baseline="middle" font-size="10" fill="#64748b" font-family="JetBrains Mono,monospace" dy="-7">'+d.toFixed(1)+'</text>';
});
s+='</svg>';
const svg=document.createElement('div');
svg.innerHTML=s;
const svgEl=svg.firstElementChild;
document.getElementById('p4-pgram-svg-wrap').innerHTML='';
document.getElementById('p4-pgram-svg-wrap').appendChild(svgEl);
svgEl.querySelectorAll('.p4-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
const vname=el.dataset.v;
function onMove(e){
e.preventDefault();
const rect=svgEl.getBoundingClientRect();
const sx=W/rect.width,sy=H/rect.height;
const nx=Math.max(10,Math.min(W-10,(e.clientX-rect.left)*sx));
const ny=Math.max(10,Math.min(H-10,(e.clientY-rect.top)*sy));
if(vname==='B') B={x:nx,y:ny};
else if(vname==='D') D={x:nx,y:ny};
redraw(); updateInfo();
}
function onUp(){ window.removeEventListener('pointermove',onMove);window.removeEventListener('pointerup',onUp);window.removeEventListener('pointercancel',onUp); }
window.addEventListener('pointermove',onMove,{passive:false});
window.addEventListener('pointerup',onUp);
window.addEventListener('pointercancel',onUp);
});
});
document.getElementById('p4-pgram-svg-wrap').innerHTML=s;
updateInfo();
}
function updateInfo(){
const C=getC();
const ab=dist(A,B),bc=dist(B,C),angA=angle(A,D,B),angB=angle(B,A,C);
const ab=dist(A,B),bc=dist(B,C),angA=angDeg(A,D,B),angB=angDeg(B,A,C);
document.getElementById('p4-pgram-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">AB = CD</div><b>${ab.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">BC = DA</div><b>${bc.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">∠A = ∠C</div><b>${angA.toFixed(1)}°</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.88rem"><div style="color:var(--muted);font-size:.72rem;font-weight:700;text-transform:uppercase;margin-bottom:4px">∠A + ∠B</div><b>${(angA+angB).toFixed(1)}°</b></div>`;
}
// Drag state: lives outside redraw so innerHTML replacement never resets it
let p4Active=false, p4Vname='', p4OffX=0, p4OffY=0;
document.getElementById('p4-pgram-svg-wrap').addEventListener('pointerdown',function(ev){
const el=ev.target.closest('[data-v]'); if(!el) return;
if(ev.button!==undefined&&ev.button!==0) return;
const vname=el.dataset.v; if(vname!=='B'&&vname!=='D') return;
ev.preventDefault();
p4Active=true; p4Vname=vname;
const cur=vname==='B'?B:D;
const sp=clientToSvg(ev.clientX,ev.clientY);
p4OffX=sp.x-cur.x; p4OffY=sp.y-cur.y;
el.style.cursor='grabbing';
window.addEventListener('pointermove',p4Move,{passive:false});
window.addEventListener('pointerup',p4Up);
window.addEventListener('pointercancel',p4Up);
});
function p4Move(e){
if(!p4Active) return; e.preventDefault();
const sp=clientToSvg(e.clientX,e.clientY);
const nx=clamp(sp.x-p4OffX,12,W-12), ny=clamp(sp.y-p4OffY,12,H-12);
if(p4Vname==='B') B={x:nx,y:ny}; else D={x:nx,y:ny};
redraw();
}
function p4Up(){
if(!p4Active) return; p4Active=false;
window.removeEventListener('pointermove',p4Move);
window.removeEventListener('pointerup',p4Up);
window.removeEventListener('pointercancel',p4Up);
}
redraw();
})();
@@ -2369,48 +2382,43 @@ function buildP5(){
/* == SVG-параллелограмм == */
(function(){
const W=380, H=280;
let A={x:55,y:210}, B={x:195,y:210}, D={x:110,y:80};
const W=380, H=280, SVG_ID='p5-pgram-svg';
let A={x:55,y:220}, B={x:235,y:220}, D={x:130,y:80};
function getC(){ return {x:D.x+(B.x-A.x), y:D.y+(B.y-A.y)}; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function angDeg(O,P,Q){ const ax=P.x-O.x,ay=P.y-O.y,bx=Q.x-O.x,by=Q.y-O.y; return Math.acos(Math.max(-1,Math.min(1,(ax*bx+ay*by)/(Math.hypot(ax,ay)*Math.hypot(bx,by)))))*180/Math.PI; }
function intersect(A,C,B,D){ const r={x:C.x-A.x,y:C.y-A.y},s={x:D.x-B.x,y:D.y-B.y}; const den=r.x*s.y-r.y*s.x; if(Math.abs(den)<1e-9) return {x:(A.x+C.x)/2,y:(A.y+C.y)/2}; const t=((B.x-A.x)*s.y-(B.y-A.y)*s.x)/den; return {x:A.x+t*r.x,y:A.y+t*r.y}; }
function intersectDiag(p1,p2,p3,p4){ const r={x:p2.x-p1.x,y:p2.y-p1.y},s={x:p4.x-p3.x,y:p4.y-p3.y}; const den=r.x*s.y-r.y*s.x; if(Math.abs(den)<1e-9) return {x:(p1.x+p2.x)/2,y:(p1.y+p2.y)/2}; const t=((p3.x-p1.x)*s.y-(p3.y-p1.y)*s.x)/den; return {x:p1.x+t*r.x,y:p1.y+t*r.y}; }
function clientToSvg(clientX,clientY){ const svgEl=document.getElementById(SVG_ID); if(!svgEl) return {x:0,y:0}; const r=svgEl.getBoundingClientRect(),vb=svgEl.viewBox.baseVal; return {x:(clientX-r.left)/r.width*vb.width+vb.x,y:(clientY-r.top)/r.height*vb.height+vb.y}; }
function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); }
function redraw(){
const C=getC(); const pts=[A,B,C,D]; const labels=['A','B','C','D'];
const cx=(A.x+B.x+C.x+D.x)/4, cy=(A.y+B.y+C.y+D.y)/4;
const O=intersect(A,C,B,D);
let s='<svg id="p5-svg" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
const pcx=(A.x+B.x+C.x+D.x)/4, pcy=(A.y+B.y+C.y+D.y)/4;
const O=intersectDiag(A,C,B,D);
let s='<svg id="'+SVG_ID+'" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<polygon points="'+pts.map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(225,29,72,.10)" stroke="#e11d48" stroke-width="2.5" stroke-linejoin="round"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#e11d48" stroke-width="1" stroke-dasharray="5 3" opacity=".6"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#e11d48" stroke-width="1" stroke-dasharray="5 3" opacity=".6"/>';
s+='<circle cx="'+O.x+'" cy="'+O.y+'" r="5" fill="#f59e0b" stroke="#fff" stroke-width="2"/>';
s+='<text x="'+(O.x+7)+'" y="'+(O.y-6)+'" font-size="11" font-weight="700" fill="#b45309" font-family="Unbounded,sans-serif">O</text>';
const movable=['B','D'];
pts.forEach((v,i)=>{
const m=movable.includes(labels[i]);
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(m?10:7)+'" fill="#e11d48" opacity="'+(m?.25:.12)+'" class="p5-vh" data-v="'+labels[i]+'"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(m?6:5)+'" fill="#e11d48" stroke="#fff" stroke-width="2" class="p5-vh" data-v="'+labels[i]+'"/>';
const lx=v.x+(v.x-cx)*0.28, ly=v.y+(v.y-cy)*0.28;
const m=(i===1||i===3);
const lx=v.x+(v.x-pcx)*0.28, ly=v.y+(v.y-pcy)*0.28;
if(m){
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="16" fill="#e11d48" opacity=".12" data-v="'+labels[i]+'" style="cursor:grab"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="8" fill="#e11d48" stroke="#fff" stroke-width="2.5" data-v="'+labels[i]+'" style="cursor:grab"/>';
} else {
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#e11d48" stroke="#fff" stroke-width="2"/>';
}
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#be123c" font-family="Unbounded,sans-serif">'+labels[i]+'</text>';
});
s+='</svg>';
const wrap=document.getElementById('p5-svg-wrap');
wrap.innerHTML=s;
const svgEl=wrap.querySelector('svg');
svgEl.querySelectorAll('.p5-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
const vname=el.dataset.v;
function onMove(e){ e.preventDefault(); const rect=svgEl.getBoundingClientRect(); const sx=W/rect.width,sy=H/rect.height; const nx=Math.max(10,Math.min(W-10,(e.clientX-rect.left)*sx)); const ny=Math.max(10,Math.min(H-10,(e.clientY-rect.top)*sy)); if(vname==='B') B={x:nx,y:ny}; else if(vname==='D') D={x:nx,y:ny}; redraw(); }
function onUp(){ window.removeEventListener('pointermove',onMove);window.removeEventListener('pointerup',onUp);window.removeEventListener('pointercancel',onUp); }
window.addEventListener('pointermove',onMove,{passive:false}); window.addEventListener('pointerup',onUp); window.addEventListener('pointercancel',onUp);
});
});
const C2=getC(); const O2=intersect(A,C2,B,D);
const ab=dist(A,B), bc=dist(B,C2), ao=dist(A,O2), bo=dist(B,O2);
const angA=angDeg(A,D,B), angB=angDeg(B,A,C2);
document.getElementById('p5-svg-wrap').innerHTML=s;
updateP5Info();
}
function updateP5Info(){
const C=getC(); const O=intersectDiag(A,C,B,D);
const ab=dist(A,B),bc=dist(B,C),ao=dist(A,O),bo=dist(B,O);
const angA=angDeg(A,D,B),angB=angDeg(B,A,C);
document.getElementById('p5-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">AB = CD</div><b>${ab.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">BC = AD</div><b>${bc.toFixed(1)}</b></div>
@@ -2419,6 +2427,33 @@ function buildP5(){
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">∠A = ∠C</div><b>${angA.toFixed(1)}°</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">∠A + ∠B</div><b>${(angA+angB).toFixed(1)}°</b></div>`;
}
let p5Active=false,p5Vname='',p5OffX=0,p5OffY=0;
document.getElementById('p5-svg-wrap').addEventListener('pointerdown',function(ev){
const el=ev.target.closest('[data-v]'); if(!el) return;
if(ev.button!==undefined&&ev.button!==0) return;
const vname=el.dataset.v; if(vname!=='B'&&vname!=='D') return;
ev.preventDefault();
p5Active=true; p5Vname=vname;
const cur=vname==='B'?B:D;
const sp=clientToSvg(ev.clientX,ev.clientY);
p5OffX=sp.x-cur.x; p5OffY=sp.y-cur.y;
window.addEventListener('pointermove',p5Move,{passive:false});
window.addEventListener('pointerup',p5Up);
window.addEventListener('pointercancel',p5Up);
});
function p5Move(e){
if(!p5Active) return; e.preventDefault();
const sp=clientToSvg(e.clientX,e.clientY);
const nx=clamp(sp.x-p5OffX,12,W-12),ny=clamp(sp.y-p5OffY,12,H-12);
if(p5Vname==='B') B={x:nx,y:ny}; else D={x:nx,y:ny};
redraw();
}
function p5Up(){
if(!p5Active) return; p5Active=false;
window.removeEventListener('pointermove',p5Move);
window.removeEventListener('pointerup',p5Up);
window.removeEventListener('pointercancel',p5Up);
}
redraw();
})();
@@ -2966,106 +3001,62 @@ function buildP7(){
/* == SVG-прямоугольник (redesigned) == */
(function(){
const W=400, H=290;
// A = bottom-left (fixed), B = bottom-right (draggable via handle at top-right = C position)
// Rectangle ABCD: A bottom-left, B bottom-right, C top-right, D top-left
// Only the top-right corner (C) is draggable; this changes both width and height.
const W=400, H=290, SVG_ID='p7-rect-svg-el';
const Ax=55, Ay=240;
let Cx=300, Cy=60; // draggable top-right corner
let Cx=310, Cy=55;
function getVerts(){ return { A:{x:Ax,y:Ay}, B:{x:Cx,y:Ay}, C:{x:Cx,y:Cy}, D:{x:Ax,y:Cy} }; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function sqMark(ox,oy,dx1,dy1,dx2,dy2,sz,col){
// L-shape corner mark: from corner (ox,oy) going into the rectangle
const ex1=ox+dx1*sz, ey1=oy+dy1*sz, ex2=ox+dx2*sz, ey2=oy+dy2*sz;
const mx=ex1+dx2*sz, my=ey1+dy2*sz;
const ex1=ox+dx1*sz,ey1=oy+dy1*sz,ex2=ox+dx2*sz,ey2=oy+dy2*sz;
const mx=ex1+dx2*sz,my=ey1+dy2*sz;
return '<polyline points="'+ex1+','+ey1+' '+mx+','+my+' '+ex2+','+ey2+'" fill="none" stroke="'+col+'" stroke-width="1.8" opacity=".85"/>';
}
function clientToSvg(clientX,clientY){
const svgEl=document.getElementById(SVG_ID); if(!svgEl) return {x:0,y:0};
const r=svgEl.getBoundingClientRect(),vb=svgEl.viewBox.baseVal;
return {x:(clientX-r.left)/r.width*vb.width+vb.x,y:(clientY-r.top)/r.height*vb.height+vb.y};
}
function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); }
function redraw(){
const v=getVerts();
const {A,B,C,D}=v;
const cxC=(A.x+B.x+C.x+D.x)/4, cyC=(A.y+B.y+C.y+D.y)/4;
const ab=dist(A,B), bc=dist(B,C), ac=dist(A,C), bd=dist(B,D);
const perimeter=2*(ab+bc), area=ab*bc;
let s='<svg viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:420px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
// rectangle fill
const {A,B,C,D}=getVerts();
const pcx=(A.x+B.x+C.x+D.x)/4, pcy=(A.y+B.y+C.y+D.y)/4;
const ab=dist(A,B),bc=dist(B,C),ac=dist(A,C),bd=dist(B,D);
const perimeter=2*(ab+bc),area=ab*bc;
const sq=10;
let s='<svg id="'+SVG_ID+'" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:420px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<rect x="'+Math.min(A.x,C.x)+'" y="'+Math.min(A.y,C.y)+'" width="'+Math.abs(C.x-A.x)+'" height="'+Math.abs(C.y-A.y)+'" fill="rgba(124,58,237,.09)" stroke="#7c3aed" stroke-width="2.5"/>';
// diagonals (dashed, two colors to emphasize AC=BD)
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#10b981" stroke-width="2" stroke-dasharray="7 4"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#f59e0b" stroke-width="2" stroke-dasharray="7 4"/>';
// equal-tick marks on each diagonal (two ticks each, near midpoint)
const acMx=(A.x+C.x)/2, acMy=(A.y+C.y)/2;
const bdMx=(B.x+D.x)/2, bdMy=(B.y+D.y)/2;
const tickLen=6;
// AC tick: perpendicular to AC direction
const acDx=C.x-A.x, acDy=C.y-A.y, acL=Math.hypot(acDx,acDy)||1;
const acPx=-acDy/acL*tickLen, acPy=acDx/acL*tickLen;
const acMx=(A.x+C.x)/2,acMy=(A.y+C.y)/2,bdMx=(B.x+D.x)/2,bdMy=(B.y+D.y)/2,tl=6;
const acDx=C.x-A.x,acDy=C.y-A.y,acL=Math.hypot(acDx,acDy)||1;
const acPx=-acDy/acL*tl,acPy=acDx/acL*tl;
s+='<line x1="'+(acMx+acPx-acDx/acL*5)+'" y1="'+(acMy+acPy-acDy/acL*5)+'" x2="'+(acMx-acPx-acDx/acL*5)+'" y2="'+(acMy-acPy-acDy/acL*5)+'" stroke="#10b981" stroke-width="2.2"/>';
s+='<line x1="'+(acMx+acPx+acDx/acL*5)+'" y1="'+(acMy+acPy+acDy/acL*5)+'" x2="'+(acMx-acPx+acDx/acL*5)+'" y2="'+(acMy-acPy+acDy/acL*5)+'" stroke="#10b981" stroke-width="2.2"/>';
// BD tick
const bdDx=D.x-B.x, bdDy=D.y-B.y, bdL=Math.hypot(bdDx,bdDy)||1;
const bdPx=-bdDy/bdL*tickLen, bdPy=bdDx/bdL*tickLen;
const bdDx=D.x-B.x,bdDy=D.y-B.y,bdL=Math.hypot(bdDx,bdDy)||1;
const bdPx=-bdDy/bdL*tl,bdPy=bdDx/bdL*tl;
s+='<line x1="'+(bdMx+bdPx-bdDx/bdL*5)+'" y1="'+(bdMy+bdPy-bdDy/bdL*5)+'" x2="'+(bdMx-bdPx-bdDx/bdL*5)+'" y2="'+(bdMy-bdPy-bdDy/bdL*5)+'" stroke="#f59e0b" stroke-width="2.2"/>';
s+='<line x1="'+(bdMx+bdPx+bdDx/bdL*5)+'" y1="'+(bdMy+bdPy+bdDy/bdL*5)+'" x2="'+(bdMx-bdPx+bdDx/bdL*5)+'" y2="'+(bdMy-bdPy+bdDy/bdL*5)+'" stroke="#f59e0b" stroke-width="2.2"/>';
// diagonal length labels near midpoints
s+='<text x="'+(acMx-22)+'" y="'+(acMy-6)+'" font-size="10" fill="#047857" font-weight="700" font-family="JetBrains Mono,monospace">AC='+ac.toFixed(1)+'</text>';
s+='<text x="'+(bdMx+6)+'" y="'+(bdMy-6)+'" font-size="10" fill="#b45309" font-weight="700" font-family="JetBrains Mono,monospace">BD='+bd.toFixed(1)+'</text>';
// right-angle marks at all 4 corners (L-shape pointing INSIDE)
const sq=10;
// A = bottom-left: right goes +x, up goes -y
s+='<text x="'+(acMx-24)+'" y="'+(acMy-7)+'" font-size="10" fill="#047857" font-weight="700" font-family="JetBrains Mono,monospace">AC='+ac.toFixed(1)+'</text>';
s+='<text x="'+(bdMx+6)+'" y="'+(bdMy-7)+'" font-size="10" fill="#b45309" font-weight="700" font-family="JetBrains Mono,monospace">BD='+bd.toFixed(1)+'</text>';
s+=sqMark(A.x,A.y,+1,0,0,-1,sq,'#7c3aed');
// B = bottom-right: left goes -x, up goes -y
s+=sqMark(B.x,B.y,-1,0,0,-1,sq,'#7c3aed');
// C = top-right: left goes -x, down goes +y
s+=sqMark(C.x,C.y,-1,0,0,+1,sq,'#7c3aed');
// D = top-left: right goes +x, down goes +y
s+=sqMark(D.x,D.y,+1,0,0,+1,sq,'#7c3aed');
// side length labels
s+='<text x="'+((A.x+B.x)/2)+'" y="'+(A.y+16)+'" text-anchor="middle" font-size="10" fill="#6d28d9" font-weight="700" font-family="JetBrains Mono,monospace">a='+ab.toFixed(1)+'</text>';
s+='<text x="'+(C.x+14)+'" y="'+((B.y+C.y)/2)+'" text-anchor="start" font-size="10" fill="#6d28d9" font-weight="700" font-family="JetBrains Mono,monospace" dominant-baseline="middle">b='+bc.toFixed(1)+'</text>';
// vertices: A, D, B are static; C is draggable
const verts=[{p:A,lbl:'A',drag:false},{p:B,lbl:'B',drag:false},{p:C,lbl:'C',drag:true},{p:D,lbl:'D',drag:false}];
verts.forEach(({p,lbl,drag})=>{
const lx=p.x+(p.x-cxC)*0.28, ly=p.y+(p.y-cyC)*0.28;
[{p:A,lbl:'A',drag:false},{p:B,lbl:'B',drag:false},{p:C,lbl:'C',drag:true},{p:D,lbl:'D',drag:false}].forEach(({p,lbl,drag})=>{
const lx=p.x+(p.x-pcx)*0.28,ly=p.y+(p.y-pcy)*0.28;
if(drag){
s+='<circle cx="'+p.x+'" cy="'+p.y+'" r="13" fill="#7c3aed" opacity=".18" class="p7-vh" style="cursor:grab"/>';
s+='<circle cx="'+p.x+'" cy="'+p.y+'" r="7" fill="#7c3aed" stroke="#fff" stroke-width="2.5" class="p7-vh" style="cursor:grab"/>';
// drag hint arrow
s+='<text x="'+p.x+'" y="'+(p.y-18)+'" text-anchor="middle" font-size="9" fill="#7c3aed" font-family="Inter,sans-serif" opacity=".7">тащи</text>';
s+='<circle cx="'+p.x+'" cy="'+p.y+'" r="16" fill="#7c3aed" opacity=".15" data-v="C" style="cursor:grab"/>';
s+='<circle cx="'+p.x+'" cy="'+p.y+'" r="8" fill="#7c3aed" stroke="#fff" stroke-width="2.5" data-v="C" style="cursor:grab"/>';
s+='<text x="'+p.x+'" y="'+(p.y-19)+'" text-anchor="middle" font-size="9" fill="#7c3aed" font-family="Inter,sans-serif" opacity=".7">тащи</text>';
} else {
s+='<circle cx="'+p.x+'" cy="'+p.y+'" r="5" fill="#7c3aed" stroke="#fff" stroke-width="2"/>';
}
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#6d28d9" font-family="Unbounded,sans-serif">'+lbl+'</text>';
});
s+='</svg>';
const wrap=document.getElementById('p7-rect-svg');
wrap.innerHTML=s;
const svgEl=wrap.querySelector('svg');
svgEl.querySelectorAll('.p7-vh').forEach(el=>{
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
let active=true;
function onMove(e){
if(!active) return;
e.preventDefault();
const rect=svgEl.getBoundingClientRect();
const sx=W/rect.width, sy=H/rect.height;
Cx=Math.max(Ax+40,Math.min(W-10,(e.clientX-rect.left)*sx));
Cy=Math.max(10,Math.min(Ay-40,(e.clientY-rect.top)*sy));
redraw();
}
function onUp(){
active=false;
window.removeEventListener('pointermove',onMove);
window.removeEventListener('pointerup',onUp);
window.removeEventListener('pointercancel',onUp);
}
window.addEventListener('pointermove',onMove,{passive:false});
window.addEventListener('pointerup',onUp);
window.addEventListener('pointercancel',onUp);
});
});
const eq=Math.abs(ac-bd)<0.1;
document.getElementById('p7-rect-svg').innerHTML=s;
document.getElementById('p7-rect-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Сторона AB = CD</div><b>${ab.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Сторона BC = DA</div><b>${bc.toFixed(1)}</b></div>
@@ -3074,6 +3065,31 @@ function buildP7(){
<div style="padding:8px 12px;background:#d1fae5;border-radius:8px;border:1.5px solid #10b981;font-size:.88rem;grid-column:span 2;text-align:center"><div style="color:#047857;font-size:.72rem;font-weight:800;text-transform:uppercase;margin-bottom:3px">Диагонали AC = BD</div><b style="color:#047857">AC = ${ac.toFixed(2)} = BD = ${bd.toFixed(2)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem;grid-column:span 2;text-align:center"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Все 4 угла</div><b>90°</b></div>`;
}
let p7Active=false,p7OffX=0,p7OffY=0;
document.getElementById('p7-rect-svg').addEventListener('pointerdown',function(ev){
const el=ev.target.closest('[data-v="C"]'); if(!el) return;
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
p7Active=true;
const sp=clientToSvg(ev.clientX,ev.clientY);
p7OffX=sp.x-Cx; p7OffY=sp.y-Cy;
window.addEventListener('pointermove',p7Move,{passive:false});
window.addEventListener('pointerup',p7Up);
window.addEventListener('pointercancel',p7Up);
});
function p7Move(e){
if(!p7Active) return; e.preventDefault();
const sp=clientToSvg(e.clientX,e.clientY);
Cx=clamp(sp.x-p7OffX,Ax+40,W-10);
Cy=clamp(sp.y-p7OffY,10,Ay-40);
redraw();
}
function p7Up(){
if(!p7Active) return; p7Active=false;
window.removeEventListener('pointermove',p7Move);
window.removeEventListener('pointerup',p7Up);
window.removeEventListener('pointercancel',p7Up);
}
redraw();
})();
@@ -3277,46 +3293,71 @@ function buildP8(){
/* == SVG-демонстрация признака == */
(function(){
const W=380, H=280;
let A={x:55,y:215}, B={x:215,y:215}, D={x:100,y:85};
const W=380, H=280, SVG_ID='p8-demo-svg-el';
let A={x:55,y:215}, B={x:225,y:215}, D={x:110,y:80};
function getC(){ return {x:D.x+(B.x-A.x),y:D.y+(B.y-A.y)}; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function clientToSvg(clientX,clientY){
const svgEl=document.getElementById(SVG_ID); if(!svgEl) return {x:0,y:0};
const r=svgEl.getBoundingClientRect(),vb=svgEl.viewBox.baseVal;
return {x:(clientX-r.left)/r.width*vb.width+vb.x,y:(clientY-r.top)/r.height*vb.height+vb.y};
}
function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); }
function redraw(){
const C=getC(); const pts=[A,B,C,D]; const labels=['A','B','C','D'];
const cx=(A.x+B.x+C.x+D.x)/4,cy=(A.y+B.y+C.y+D.y)/4;
const ac=dist(A,C), bd=dist(B,D);
const pcx=(A.x+B.x+C.x+D.x)/4,pcy=(A.y+B.y+C.y+D.y)/4;
const ac=dist(A,C),bd=dist(B,D);
const eq=Math.abs(ac-bd)<4;
const col=eq?'#10b981':'#2563eb';
let s='<svg id="p8-svg" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
let s='<svg id="'+SVG_ID+'" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:400px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<polygon points="'+pts.map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(37,99,235,.08)" stroke="'+col+'" stroke-width="2.5" stroke-linejoin="round"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#2563eb" stroke-width="1.5" stroke-dasharray="5 3"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#10b981" stroke-width="1.5" stroke-dasharray="5 3"/>';
const diagMx=(A.x+C.x)/2,diagMy=(A.y+C.y)/2;
s+='<text x="'+(diagMx-22)+'" y="'+(diagMy-7)+'" font-size="10" fill="#1d4ed8" font-weight="700" font-family="JetBrains Mono,monospace">AC='+ac.toFixed(1)+'</text>';
s+='<text x="'+((B.x+D.x)/2+5)+'" y="'+((B.y+D.y)/2-7)+'" font-size="10" fill="#047857" font-weight="700" font-family="JetBrains Mono,monospace">BD='+bd.toFixed(1)+'</text>';
pts.forEach((v,i)=>{
const m=(i===3);
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(m?10:6)+'" fill="'+col+'" opacity="'+(m?.25:.15)+'" '+(m?'class="p8-vh"':'')+'/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="'+col+'" stroke="#fff" stroke-width="2" '+(m?'class="p8-vh"':'')+'/>';
const lx=v.x+(v.x-cx)*0.28,ly=v.y+(v.y-cy)*0.28;
const lx=v.x+(v.x-pcx)*0.28,ly=v.y+(v.y-pcy)*0.28;
if(m){
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="16" fill="'+col+'" opacity=".15" data-v="D" style="cursor:grab"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="8" fill="'+col+'" stroke="#fff" stroke-width="2.5" data-v="D" style="cursor:grab"/>';
} else {
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="'+col+'" stroke="#fff" stroke-width="2"/>';
}
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#1d4ed8" font-family="Unbounded,sans-serif">'+labels[i]+'</text>';
});
s+='</svg>';
const wrap=document.getElementById('p8-demo-svg');
wrap.innerHTML=s;
const svgEl=wrap.querySelector('svg');
svgEl.querySelectorAll('.p8-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0)return;
ev.preventDefault();
function onMove(e){ e.preventDefault(); const rect=svgEl.getBoundingClientRect(); const sx=W/rect.width,sy=H/rect.height; D={x:Math.max(10,Math.min(W-10,(e.clientX-rect.left)*sx)),y:Math.max(10,Math.min(H-10,(e.clientY-rect.top)*sy))}; redraw(); }
function onUp(){ window.removeEventListener('pointermove',onMove);window.removeEventListener('pointerup',onUp);window.removeEventListener('pointercancel',onUp); }
window.addEventListener('pointermove',onMove,{passive:false}); window.addEventListener('pointerup',onUp); window.addEventListener('pointercancel',onUp);
});
});
document.getElementById('p8-demo-svg').innerHTML=s;
const okCol=eq?'#10b981':'#94a3b8';
document.getElementById('p8-demo-indicators').innerHTML=`
<div style="padding:10px 16px;border-radius:10px;border:2px solid ${eq?'#10b981':'#94a3b8'};background:${eq?'rgba(16,185,129,.12)':'var(--card)'};font-size:.88rem;font-weight:700;color:${okCol}">Диагонали равны: ${eq?'ДА':'НЕТ'} (AC=${ac.toFixed(1)}, BD=${bd.toFixed(1)})</div>
<div style="padding:10px 16px;border-radius:10px;border:2px solid ${eq?'#10b981':'#94a3b8'};background:${eq?'rgba(16,185,129,.12)':'var(--card)'};font-size:.88rem;font-weight:700;color:${okCol}">Прямоугольник: ${eq?'ДА':'НЕТ'}</div>`;
}
let p8Active=false,p8OffX=0,p8OffY=0;
document.getElementById('p8-demo-svg').addEventListener('pointerdown',function(ev){
const el=ev.target.closest('[data-v="D"]'); if(!el) return;
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
p8Active=true;
const sp=clientToSvg(ev.clientX,ev.clientY);
p8OffX=sp.x-D.x; p8OffY=sp.y-D.y;
window.addEventListener('pointermove',p8Move,{passive:false});
window.addEventListener('pointerup',p8Up);
window.addEventListener('pointercancel',p8Up);
});
function p8Move(e){
if(!p8Active) return; e.preventDefault();
const sp=clientToSvg(e.clientX,e.clientY);
D={x:clamp(sp.x-p8OffX,12,W-12),y:clamp(sp.y-p8OffY,12,H-12)};
redraw();
}
function p8Up(){
if(!p8Active) return; p8Active=false;
window.removeEventListener('pointermove',p8Move);
window.removeEventListener('pointerup',p8Up);
window.removeEventListener('pointercancel',p8Up);
}
redraw();
})();
@@ -3584,54 +3625,80 @@ function buildP9(){
/* == SVG-ромб == */
(function(){
const W=360, H=300, cx=180, cy=150;
let half1=110, half2=70; // half-diagonals
let dragV=null;
function getVerts(h1,h2){ return {A:{x:cx-h1,y:cy},B:{x:cx,y:cy-h2},C:{x:cx+h1,y:cy},D:{x:cx,y:cy+h2}}; }
const W=360, H=300, RCX=180, RCY=150, SVG_ID='p9-rhombus-svg-el';
let half1=110, half2=75;
function getVerts(h1,h2){ return {A:{x:RCX-h1,y:RCY},B:{x:RCX,y:RCY-h2},C:{x:RCX+h1,y:RCY},D:{x:RCX,y:RCY+h2}}; }
function dist(a,b){ return Math.hypot(b.x-a.x,b.y-a.y); }
function angDeg(O,P,Q){ const ax=P.x-O.x,ay=P.y-O.y,bx=Q.x-O.x,by=Q.y-O.y; return Math.acos(Math.max(-1,Math.min(1,(ax*bx+ay*by)/(Math.hypot(ax,ay)*Math.hypot(bx,by)))))*180/Math.PI; }
function clientToSvg(clientX,clientY){
const svgEl=document.getElementById(SVG_ID); if(!svgEl) return {x:0,y:0};
const r=svgEl.getBoundingClientRect(),vb=svgEl.viewBox.baseVal;
return {x:(clientX-r.left)/r.width*vb.width+vb.x,y:(clientY-r.top)/r.height*vb.height+vb.y};
}
function clamp(v,lo,hi){ return Math.max(lo,Math.min(hi,v)); }
function redraw(){
const {A,B,C,D}=getVerts(half1,half2);
const side=dist(A,B);
const angA=angDeg(A,D,B)*2;
let s='<svg id="p9-svg" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:380px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
const sq=8;
let s='<svg id="'+SVG_ID+'" viewBox="0 0 '+W+' '+H+'" style="width:100%;max-width:380px;background:var(--card);border:1px solid var(--border);border-radius:14px;touch-action:none">';
s+='<polygon points="'+[A,B,C,D].map(v=>v.x+','+v.y).join(' ')+'" fill="rgba(8,145,178,.10)" stroke="#0891b2" stroke-width="2.5" stroke-linejoin="round"/>';
s+='<line x1="'+A.x+'" y1="'+A.y+'" x2="'+C.x+'" y2="'+C.y+'" stroke="#0891b2" stroke-width="1.5" stroke-dasharray="5 3" opacity=".7"/>';
s+='<line x1="'+B.x+'" y1="'+B.y+'" x2="'+D.x+'" y2="'+D.y+'" stroke="#0891b2" stroke-width="1.5" stroke-dasharray="5 3" opacity=".7"/>';
// right angle mark at O
const sq=8;
s+='<polyline points="'+(cx+sq)+','+cy+' '+(cx+sq)+','+(cy-sq)+' '+cx+','+(cy-sq)+'" fill="none" stroke="#0891b2" stroke-width="1.5"/>';
s+='<circle cx="'+cx+'" cy="'+cy+'" r="4" fill="#f59e0b" stroke="#fff" stroke-width="1.5"/>';
s+='<polyline points="'+(RCX+sq)+','+RCY+' '+(RCX+sq)+','+(RCY-sq)+' '+RCX+','+(RCY-sq)+'" fill="none" stroke="#0891b2" stroke-width="1.5"/>';
s+='<circle cx="'+RCX+'" cy="'+RCY+'" r="4" fill="#f59e0b" stroke="#fff" stroke-width="1.5"/>';
// side-length tick marks
[[A,B],[B,C],[C,D],[D,A]].forEach(([p1,p2])=>{
const mx=(p1.x+p2.x)/2,my=(p1.y+p2.y)/2;
const dx=p2.x-p1.x,dy=p2.y-p1.y,ln=Math.hypot(dx,dy)||1;
const px=-dy/ln*5,py=dx/ln*5;
s+='<line x1="'+(mx+px)+'" y1="'+(my+py)+'" x2="'+(mx-px)+'" y2="'+(my-py)+'" stroke="#0891b2" stroke-width="2"/>';
});
const labels=['A','B','C','D']; const pts=[A,B,C,D];
pts.forEach((v,i)=>{
const m=(i===1||i===3);
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="'+(m?10:7)+'" fill="#0891b2" opacity="'+(m?.25:.15)+'" class="p9-vh" data-v="'+labels[i]+'"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="5" fill="#0891b2" stroke="#fff" stroke-width="2" class="p9-vh" data-v="'+labels[i]+'"/>';
const lx=v.x+(v.x-cx)*0.22,ly=v.y+(v.y-cy)*0.22;
const m=(i===0||i===1||i===2||i===3);
const lx=v.x+(v.x-RCX)*0.22,ly=v.y+(v.y-RCY)*0.22;
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="16" fill="#0891b2" opacity=".10" data-v="'+labels[i]+'" style="cursor:grab"/>';
s+='<circle cx="'+v.x+'" cy="'+v.y+'" r="8" fill="#0891b2" stroke="#fff" stroke-width="2.5" data-v="'+labels[i]+'" style="cursor:grab"/>';
s+='<text x="'+lx+'" y="'+ly+'" text-anchor="middle" dominant-baseline="middle" font-size="13" font-weight="700" fill="#0e7490" font-family="Unbounded,sans-serif">'+labels[i]+'</text>';
});
s+='</svg>';
const wrap=document.getElementById('p9-rhombus-svg'); wrap.innerHTML=s;
const svgEl=wrap.querySelector('svg');
svgEl.querySelectorAll('.p9-vh').forEach(el=>{
el.style.cursor='grab';
el.addEventListener('pointerdown',ev=>{
if(ev.button!==undefined&&ev.button!==0)return;
ev.preventDefault();
const vname=el.dataset.v;
function onMove(e){ e.preventDefault(); const rect=svgEl.getBoundingClientRect(); const sx=W/rect.width,sy=H/rect.height; const nx=(e.clientX-rect.left)*sx; const ny=(e.clientY-rect.top)*sy; if(vname==='B'){ half2=Math.max(20,Math.min(H/2-10,cy-ny)); } else if(vname==='D'){ half2=Math.max(20,Math.min(H/2-10,ny-cy)); } else if(vname==='A'){ half1=Math.max(20,Math.min(W/2-10,cx-nx)); } else if(vname==='C'){ half1=Math.max(20,Math.min(W/2-10,nx-cx)); } redraw(); }
function onUp(){ window.removeEventListener('pointermove',onMove);window.removeEventListener('pointerup',onUp);window.removeEventListener('pointercancel',onUp); }
window.addEventListener('pointermove',onMove,{passive:false}); window.addEventListener('pointerup',onUp); window.addEventListener('pointercancel',onUp);
});
});
const {A:A2,B:B2}=getVerts(half1,half2); const side2=dist(A2,B2);
document.getElementById('p9-rhombus-svg').innerHTML=s;
const S=half1*half2*2;
document.getElementById('p9-rhombus-info').innerHTML=`
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Сторона AB=BC=CD=DA</div><b>${side2.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Сторона AB=BC=CD=DA</div><b>${side.toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Диагональ AC</div><b>${(half1*2).toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Диагональ BD</div><b>${(half2*2).toFixed(1)}</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Угол между диаг.</div><b>90°</b></div>
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Площадь S = d₁d₂/2</div><b>${S.toFixed(1)}</b></div>`;
<div style="padding:8px 12px;background:var(--card);border-radius:8px;border:1px solid var(--border);font-size:.85rem"><div style="color:var(--muted);font-size:.7rem;font-weight:700;text-transform:uppercase;margin-bottom:3px">Площадь S = d1·d2/2</div><b>${S.toFixed(1)}</b></div>`;
}
let p9Active=false,p9Vname='',p9OffX=0,p9OffY=0;
document.getElementById('p9-rhombus-svg').addEventListener('pointerdown',function(ev){
const el=ev.target.closest('[data-v]'); if(!el) return;
if(ev.button!==undefined&&ev.button!==0) return;
ev.preventDefault();
p9Active=true; p9Vname=el.dataset.v;
const {A,B,C,D}=getVerts(half1,half2);
const cur={A,B,C,D}[p9Vname];
const sp=clientToSvg(ev.clientX,ev.clientY);
p9OffX=sp.x-cur.x; p9OffY=sp.y-cur.y;
window.addEventListener('pointermove',p9Move,{passive:false});
window.addEventListener('pointerup',p9Up);
window.addEventListener('pointercancel',p9Up);
});
function p9Move(e){
if(!p9Active) return; e.preventDefault();
const sp=clientToSvg(e.clientX,e.clientY);
const nx=sp.x-p9OffX,ny=sp.y-p9OffY;
if(p9Vname==='B') half2=clamp(RCY-ny,20,RCY-10);
else if(p9Vname==='D') half2=clamp(ny-RCY,20,RCY-10);
else if(p9Vname==='A') half1=clamp(RCX-nx,20,RCX-10);
else if(p9Vname==='C') half1=clamp(nx-RCX,20,W-RCX-10);
redraw();
}
function p9Up(){
if(!p9Active) return; p9Active=false;
window.removeEventListener('pointermove',p9Move);
window.removeEventListener('pointerup',p9Up);
window.removeEventListener('pointercancel',p9Up);
}
redraw();
})();