From df8b5ff18bdd27930934fa84b13334b0aabad88b Mon Sep 17 00:00:00 2001 From: Maxim Dolgolyov Date: Thu, 28 May 2026 21:31:01 +0300 Subject: [PATCH] =?UTF-8?q?fix(geom8=20ch1):=205=20drag-=D0=B8=D0=BD=D1=82?= =?UTF-8?q?=D0=B5=D1=80=D0=B0=D0=BA=D1=82=D0=B8=D0=B2=D0=BE=D0=B2=20?= =?UTF-8?q?=E2=80=94=20=D1=84=D0=B8=D0=BA=D1=81=20stale=20closure=20=D0=BF?= =?UTF-8?q?=D0=BE=D1=81=D0=BB=D0=B5=20innerHTML=20replace?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Корневая причина: каждый 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) --- frontend/textbooks/geometry_8_ch1.html | 467 ++++++++++++++----------- 1 file changed, 267 insertions(+), 200 deletions(-) diff --git a/frontend/textbooks/geometry_8_ch1.html b/frontend/textbooks/geometry_8_ch1.html index efda53e..3970685 100644 --- a/frontend/textbooks/geometry_8_ch1.html +++ b/frontend/textbooks/geometry_8_ch1.html @@ -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=''; + 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=''; s+=''; s+=''; s+=''; - 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+=''+dist(p1,p2).toFixed(1)+''; + }); pts.forEach((v,i)=>{ - const movable=(i===1||i===3); - s+=''; - s+=''; - 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+=''; + s+=''; + } else { + s+=''; + } s+=''+labels[i]+''; }); - 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+=''+d.toFixed(1)+''; - }); s+=''; - 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=`
AB = CD
${ab.toFixed(1)}
BC = DA
${bc.toFixed(1)}
∠A = ∠C
${angA.toFixed(1)}°
∠A + ∠B
${(angA+angB).toFixed(1)}°
`; } + // 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=''; + 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=''; s+=''; s+=''; s+=''; s+=''; s+='O'; - const movable=['B','D']; pts.forEach((v,i)=>{ - const m=movable.includes(labels[i]); - s+=''; - s+=''; - 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+=''; + s+=''; + } else { + s+=''; + } s+=''+labels[i]+''; }); s+=''; - 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=`
AB = CD
${ab.toFixed(1)}
BC = AD
${bc.toFixed(1)}
@@ -2419,6 +2427,33 @@ function buildP5(){
∠A = ∠C
${angA.toFixed(1)}°
∠A + ∠B
${(angA+angB).toFixed(1)}°
`; } + 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 ''; } + 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=''; - // 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=''; s+=''; - // diagonals (dashed, two colors to emphasize AC=BD) s+=''; s+=''; - // 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+=''; s+=''; - // 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+=''; s+=''; - // diagonal length labels near midpoints - s+='AC='+ac.toFixed(1)+''; - s+='BD='+bd.toFixed(1)+''; - // right-angle marks at all 4 corners (L-shape pointing INSIDE) - const sq=10; - // A = bottom-left: right goes +x, up goes -y + s+='AC='+ac.toFixed(1)+''; + s+='BD='+bd.toFixed(1)+''; 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+='a='+ab.toFixed(1)+''; s+='b='+bc.toFixed(1)+''; - // 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+=''; - s+=''; - // drag hint arrow - s+='тащи'; + s+=''; + s+=''; + s+='тащи'; } else { s+=''; } s+=''+lbl+''; }); s+=''; - 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=`
Сторона AB = CD
${ab.toFixed(1)}
Сторона BC = DA
${bc.toFixed(1)}
@@ -3074,6 +3065,31 @@ function buildP7(){
Диагонали AC = BD
AC = ${ac.toFixed(2)} = BD = ${bd.toFixed(2)}
Все 4 угла
90°
`; } + 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=''; + let s=''; s+=''; s+=''; s+=''; + const diagMx=(A.x+C.x)/2,diagMy=(A.y+C.y)/2; + s+='AC='+ac.toFixed(1)+''; + s+='BD='+bd.toFixed(1)+''; pts.forEach((v,i)=>{ const m=(i===3); - s+=''; - s+=''; - 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+=''; + s+=''; + } else { + s+=''; + } s+=''+labels[i]+''; }); s+=''; - 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=`
Диагонали равны: ${eq?'ДА':'НЕТ'} (AC=${ac.toFixed(1)}, BD=${bd.toFixed(1)})
Прямоугольник: ${eq?'ДА':'НЕТ'}
`; } + 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=''; + const sq=8; + let s=''; s+=''; s+=''; s+=''; - // right angle mark at O - const sq=8; - s+=''; - s+=''; + s+=''; + s+=''; + // 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+=''; + }); const labels=['A','B','C','D']; const pts=[A,B,C,D]; pts.forEach((v,i)=>{ - const m=(i===1||i===3); - s+=''; - s+=''; - 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+=''; + s+=''; s+=''+labels[i]+''; }); s+=''; - 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=` -
Сторона AB=BC=CD=DA
${side2.toFixed(1)}
+
Сторона AB=BC=CD=DA
${side.toFixed(1)}
Диагональ AC
${(half1*2).toFixed(1)}
Диагональ BD
${(half2*2).toFixed(1)}
Угол между диаг.
90°
-
Площадь S = d₁d₂/2
${S.toFixed(1)}
`; +
Площадь S = d1·d2/2
${S.toFixed(1)}
`; + } + 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(); })();