Files
Toolbox/public/tools/thought_lab/labs/pi_derivation/index.html
T
yangxiangyuan ea379116cc feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点
- 新增思想实验室后端路由与鉴权逻辑
- 首页新增思想实验室页签与前端渲染逻辑
- 修复中韩半导体监控标的配置错误
- 新增市场技能推送白名单过滤逻辑与单元测试
2026-08-06 13:39:48 +08:00

827 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!-- ============================================================
思想实验室 · labs/pi_derivation · π 推导实验室
随机剖分采样三角形内角,用角度积分公式反推圆周率。
独立 lab:单文件自包含,不引用其他 lab 的任何资源。
服务端配置:src/server/thought_lab/labs/pi_derivation/config.json
============================================================ -->
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>π 推导实验室 · 随机角度统计</title>
<style>
:root{
--bg:#070a12; --panel:#0d1220; --panel2:#111a2e; --line:#1e2a44;
--cyan:#53d8ff; --green:#42e696; --orange:#ffb454; --pink:#ff6b9d;
--text:#dbe6ff; --dim:#7d8db1;
--mono:'Consolas','Cascadia Mono','JetBrains Mono',monospace;
}
*{margin:0;padding:0;box-sizing:border-box}
body{
background:radial-gradient(1200px 800px at 70% -10%, #101b36 0%, var(--bg) 55%);
color:var(--text); font-family:'Segoe UI','Microsoft YaHei',sans-serif;
min-height:100vh; padding:16px;
}
header{max-width:1440px;margin:0 auto 12px}
h1{font-size:21px;letter-spacing:1px}
h1 b{color:var(--cyan)}
.sub{color:var(--dim);font-size:12.5px;margin-top:4px;line-height:1.6}
main{max-width:1440px;margin:0 auto;display:flex;gap:14px;flex-wrap:wrap}
.panel{
background:linear-gradient(180deg,var(--panel2),var(--panel));
border:1px solid var(--line); border-radius:14px; padding:14px;
box-shadow:0 10px 30px rgba(0,0,0,.35);
}
#geoPanel{flex:1.25;min-width:580px}
#statsPanel{flex:1;min-width:440px}
.ptitle{
font-size:13px;color:var(--cyan);letter-spacing:3px;margin-bottom:10px;
display:flex;justify-content:space-between;align-items:center;
}
.ptitle small{color:var(--dim);letter-spacing:1px}
canvas{display:block;width:100%;border-radius:10px;background:#0a0f1c;border:1px solid var(--line)}
#geo{height:540px}
#hist{height:150px}
#spark{height:72px}
.controls{display:flex;gap:10px;align-items:center;margin-top:10px;flex-wrap:wrap}
button{
background:#16223c;border:1px solid #2b3f66;color:var(--text);
padding:7px 14px;border-radius:8px;cursor:pointer;font-size:13px;
}
button:hover{border-color:var(--cyan);color:var(--cyan)}
.speedbox{display:flex;align-items:center;gap:6px;color:var(--dim);font-size:12px}
input[type=range]{accent-color:var(--cyan);width:110px}
#statusLine{color:var(--dim);font-size:12px;margin-top:8px;font-family:var(--mono)}
/* ---- 统计面板 ---- */
.hero{background:#0a1122;border:1px solid var(--line);border-radius:10px;padding:12px 14px}
.hero .lab{font-size:12px;color:var(--dim);letter-spacing:2px}
.big{font-family:var(--mono);font-size:29px;margin-top:4px;letter-spacing:.5px}
.big .ok{color:var(--green)}
.big .bad{color:#ff5c7a}
.ref{font-family:var(--mono);font-size:13px;color:var(--dim);margin-top:4px}
.meta{display:flex;gap:14px;margin-top:8px;font-size:12px;font-family:var(--mono);flex-wrap:wrap}
.meta b{color:var(--cyan)}
.badge{padding:2px 10px;border-radius:20px;font-size:12px;font-weight:600}
.badge.ok{background:rgba(66,230,150,.15);color:var(--green);border:1px solid rgba(66,230,150,.4)}
.badge.no{background:rgba(255,180,84,.12);color:var(--orange);border:1px solid rgba(255,180,84,.35)}
.formula{
font-family:var(--mono);font-size:12.5px;color:#9fb4de;
background:#0a1122;border-left:3px solid var(--cyan);
padding:8px 12px;border-radius:6px;margin:10px 0;line-height:1.8;
}
.grid4{display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-bottom:10px}
.card{background:#0a1122;border:1px solid var(--line);border-radius:8px;padding:8px 10px}
.card .k{font-size:11px;color:var(--dim)}
.card .v{font-family:var(--mono);font-size:17px;margin-top:2px}
.card .s{font-size:11px;color:var(--dim);font-family:var(--mono)}
.acc{color:var(--cyan)} .obt{color:var(--orange)} .grn{color:var(--green)}
.secTitle{font-size:12px;color:var(--dim);letter-spacing:2px;margin:10px 0 6px}
.mcRow{display:flex;gap:10px;align-items:stretch}
.mcRow .card{flex:1}
table.log{width:100%;border-collapse:collapse;font-family:var(--mono);font-size:11.5px;margin-top:6px}
table.log th{color:var(--dim);font-weight:400;text-align:left;padding:3px 6px;border-bottom:1px solid var(--line)}
table.log td{padding:3px 6px;border-bottom:1px solid #141d33;color:#c4d3f5}
.foot{color:#5a6a8f;font-size:11px;margin-top:10px;line-height:1.6}
</style>
</head>
<body>
<header>
<h1>π 推导实验室 <b>Random-Angle Derivation Lab</b></h1>
<div class="sub">
基础流程:正方形 → 随机三角剖分 → 随机取走一块 → 剩余面积守恒重拼矩形(循环)。
每刀切割角度完全随机;记录每个被取走三角形的三个内角,统计锐角/钝角三角形的角度分布,用角度积分公式 π̂ = 3·∫₀<sup>π</sup> θ·ρ(θ)dθ 反推 π。
</div>
</header>
<main>
<!-- ============ 左:几何动画视图 ============ -->
<section class="panel" id="geoPanel">
<div class="ptitle">◤ 几何动画视图 <small>Canvas 实时模拟</small></div>
<canvas id="geo"></canvas>
<div class="controls">
<button id="btnRun">⏸ 暂停</button>
<button id="btnFast">⚡ 后台快进 ×5000</button>
<button id="btnReset">↺ 重置统计</button>
<div class="speedbox">速度 <input type="range" id="speed" min="0.5" max="4" step="0.1" value="1"><span id="speedVal">1.0×</span></div>
</div>
<div id="statusLine">初始化…</div>
</section>
<!-- ============ 右:概率统计计算面板 ============ -->
<section class="panel" id="statsPanel">
<div class="ptitle">◤ 概率统计计算面板 <small>后台实时统计</small></div>
<div class="hero">
<div class="lab">角度积分反推 π̂(主估计器)</div>
<div class="big" id="piHat">—</div>
<div class="ref">理论 π&nbsp;&nbsp;= 3.141592653589793238…</div>
<div class="meta">
<span>绝对误差 <b id="piErr">—</b></span>
<span>稳定小数位 <b id="piDigits">—</b></span>
<span id="badge" class="badge no">目标 ≥ 10 位</span>
</div>
</div>
<div class="formula">
π̂ = (1/N)·Σₖ(αₖ+βₖ+γₖ) = 3·∫₀^π θ·ρ(θ)dθ&nbsp;&nbsp;(ρ 为记录角度的经验分布)<br>
π̂<sub>MC</sub> = 2 / E[sin θ<sub>cut</sub>]&nbsp;&nbsp;(均匀随机测量角,Buffon 型对照)
</div>
<div class="grid4">
<div class="card"><div class="k">取走三角形 N</div><div class="v" id="cN">0</div></div>
<div class="card"><div class="k">角度样本 3N</div><div class="v" id="cAng">0</div></div>
<div class="card"><div class="k acc">锐角三角形</div><div class="v acc" id="cAcute">0</div><div class="s" id="pAcute">P = —</div></div>
<div class="card"><div class="k obt">钝角三角形</div><div class="v obt" id="cObtuse">0</div><div class="s" id="pObtuse">P = —</div></div>
</div>
<div class="secTitle">角度分布直方图(0°–180°,按三角形类型堆叠)</div>
<canvas id="hist"></canvas>
<div class="secTitle">独立蒙特卡洛对照(慢收敛 · 1/√N)</div>
<div class="mcRow">
<div class="card">
<div class="k">π̂<sub>MC</sub> = 2/⟨sinθ⟩</div>
<div class="v grn" id="piBuffon">—</div>
<div class="s">误差 <span id="buffErr">—</span></div>
<div class="s">测量角样本 <span id="buffN">0</span></div>
</div>
<div class="card"><canvas id="spark" style="height:100%;border:none;background:transparent"></canvas></div>
<div class="card">
<div class="k">直方图数值积分 π̂<sub>h</sub>(粗)</div>
<div class="v obt" id="piHistVal">—</div>
<div class="s">分箱 5°,演示离散化误差</div>
</div>
</div>
<div class="secTitle">最近取走样本(每行验证 α+β+γ = π)</div>
<table class="log">
<thead><tr><th>#</th><th>α</th><th>β</th><th>γ</th><th>类型</th><th>Σ (rad)</th></tr></thead>
<tbody id="logBody"></tbody>
</table>
<div class="foot">
原理:欧氏几何中任意三角形内角和恒等于 π。实验室用完全随机的切割反复生成三角形并采样其内角,
以经验分布 ρ(θ) 的积分 3·E[θ] 重现该恒等式——因此主估计器在采样下即确定性地稳定到机器精度(&gt;10 位小数);
右侧 Buffon 型估计器作为对照,展示普通蒙特卡洛只能以 1/√N 速度缓慢收敛。
</div>
</section>
</main>
<script>
'use strict';
/* =========================================================
* π 推导实验室 —— 随机角度统计推导圆周率
* 流程:矩形 → 随机点剖分(切割角随机) → 随机取走一块(记录内角)
* → 剩余块重拼为等面积新矩形 → 循环
* ========================================================= */
// ---------- DOM ----------
const geo = document.getElementById('geo'), g = geo.getContext('2d');
const histCv = document.getElementById('hist'), hc = histCv.getContext('2d');
const sparkCv = document.getElementById('spark'), sc = sparkCv.getContext('2d');
const $ = id => document.getElementById(id);
const el = {
piHat:$('piHat'), err:$('piErr'), digits:$('piDigits'), badge:$('badge'),
n:$('cN'), ang:$('cAng'), acute:$('cAcute'), pacute:$('pAcute'),
obtuse:$('cObtuse'), pobtuse:$('pObtuse'),
p2:$('piBuffon'), p2e:$('buffErr'), cutn:$('buffN'), ph:$('piHistVal'),
log:$('logBody'), status:$('statusLine'),
btnRun:$('btnRun'), btnFast:$('btnFast'), btnReset:$('btnReset'),
speed:$('speed'), speedVal:$('speedVal')
};
// ---------- 常量 ----------
const BINS = 36; // 角度直方图分箱数(0~180°,每箱 5°)
const DUR = {cut:1.15, flash:0.4, remove:1.35, fly:1.2, merge:0.8, settle:0.4, reset:0.85};
const PHASE_CN = {
idle:'等待', cut:'随机切割(切割角完全随机)', flash:'剖分完成',
remove:'随机取走一块 · 记录三个内角', fly:'剩余块重拼 → 新矩形',
merge:'面积守恒合并', settle:'本轮完成', reset:'面积过小 → 重置新正方形'
};
const MONO = "Consolas,'Cascadia Mono',monospace";
// ---------- 工具函数 ----------
const rand = (a,b)=>a+(b-a)*Math.random();
const clamp=(v,a,b)=>v<a?a:(v>b?b:v);
const lerp =(a,b,t)=>a+(b-a)*t;
const easeIO=t=>t<.5?4*t*t*t:1-Math.pow(-2*t+2,3)/2;
const sub=(p,q)=>({x:p.x-q.x,y:p.y-q.y});
const add=(p,q)=>({x:p.x+q.x,y:p.y+q.y});
const mul=(p,s)=>({x:p.x*s,y:p.y*s});
const dot=(p,q)=>p.x*q.x+p.y*q.y;
const len=p=>Math.hypot(p.x,p.y);
const dist=(p,q)=>len(sub(p,q));
const norm=p=>{const l=len(p)||1;return{x:p.x/l,y:p.y/l};};
const rotVec=(v,a)=>({x:v.x*Math.cos(a)-v.y*Math.sin(a), y:v.x*Math.sin(a)+v.y*Math.cos(a)});
const lerpP=(p,q,t)=>({x:lerp(p.x,q.x,t), y:lerp(p.y,q.y,t)});
function areaPoly(p){let a=0;for(let i=0;i<p.length;i++){const q=p[(i+1)%p.length];a+=p[i].x*q.y-q.x*p[i].y;}return Math.abs(a)/2;}
function centroidOf(p){let x=0,y=0;for(const q of p){x+=q.x;y+=q.y;}return{x:x/p.length,y:y/p.length};}
function anglesOf(poly){ // 三角形三个内角(弧度)
const ang=(U,V,Ww)=>{
const v1=sub(V,U), v2=sub(Ww,U);
return Math.acos(clamp(dot(v1,v2)/(len(v1)*len(v2)),-1,1));
};
const [A,B,C]=poly;
return [ang(A,B,C), ang(B,C,A), ang(C,A,B)];
}
function rr(c,x,y,w,h,r){c.beginPath();c.moveTo(x+r,y);c.arcTo(x+w,y,x+w,y+h,r);
c.arcTo(x+w,y+h,x,y+h,r);c.arcTo(x,y+h,x,y,r);c.arcTo(x,y,x+w,y,r);c.closePath();}
// ---------- 画布尺寸 ----------
let W=800,H=540,DPR=1;
function sizeCanvas(cv,ctx,hCss){
const w = cv.clientWidth || cv.parentElement.clientWidth;
const h = hCss || cv.clientHeight;
DPR = window.devicePixelRatio||1;
cv.width = Math.round(w*DPR); cv.height = Math.round(h*DPR);
ctx.setTransform(DPR,0,0,DPR,0,0);
return [w,h];
}
function sizeAll(){
[W,H] = sizeCanvas(geo,g,540);
sizeCanvas(histCv,hc,150);
sizeCanvas(sparkCv,sc,0);
if(S.rect){ S.rect.cx=W/2; S.rect.cy=H*0.47; }
}
// ---------- 全局状态 ----------
const S = {
running:true, speed:1, iter:0,
phase:'idle', t:0,
rect:null, area0:1,
P:null, tris:[], cutSegs:[], remIdx:-1,
remAngles:null, remArea:0, thetaM:0,
nextRect:null, resetNext:false,
flyData:null, RT:null,
recorded:false, recFlash:0,
fast:0
};
// ---------- 后台统计 ----------
const stats = {
nTri:0, sumS:0, // 取走三角形数 / 内角和累加
acute:0, obtuse:0, // 锐角 / 钝角三角形计数
binsA:new Float64Array(BINS), // 锐角三角形的角度直方图
binsO:new Float64Array(BINS), // 钝角三角形的角度直方图
cutN:0, cutSin:0, // 测量切割角样本(Buffon 估计器)
spark:[], sparkTick:1,
log:[]
};
function resetStats(){
stats.nTri=0; stats.sumS=0; stats.acute=0; stats.obtuse=0;
stats.binsA.fill(0); stats.binsO.fill(0);
stats.cutN=0; stats.cutSin=0; stats.spark.length=0; stats.sparkTick=1;
stats.log.length=0;
refreshStats(); drawHist(); drawSpark();
}
// ---------- 几何构建 ----------
function rectCorners(R){
const x=R.cx-R.w/2, y=R.cy-R.h/2;
return [{x,y},{x:x+R.w,y},{x:x+R.w,y:y+R.h},{x,y:y+R.h}];
}
function pieceColor(i){ return `hsla(${(S.iter*47+i*67)%360},66%,58%,0.92)`; }
function buildIteration(){
const R=S.rect, cs=rectCorners(R);
// 随机内点 P → 4 刀切割(P 均匀随机 ⇒ 切割角度完全随机)
const P={x:R.cx+(Math.random()-.5)*R.w*0.6, y:R.cy+(Math.random()-.5)*R.h*0.6};
const tris=[];
for(let i=0;i<4;i++){
tris.push({poly:[cs[i], cs[(i+1)%4], P], color:pieceColor(i)});
}
const segs=cs.map(c=>({a:c,b:P}));
// 以 60% 概率追加随机一刀(把某块再切成两片)
if(Math.random()<0.6){
const i=(Math.random()*tris.length)|0;
const t=tris[i];
const e=t.poly.filter(v=>v!==P); // 外侧边(矩形边)的两个端点
const u=rand(0.15,0.85);
const Q={x:lerp(e[0].x,e[1].x,u), y:lerp(e[0].y,e[1].y,u)};
tris.splice(i,1,
{poly:[e[0],Q,P], color:pieceColor(4)},
{poly:[Q,e[1],P], color:pieceColor(5)});
segs.push({a:P,b:Q});
}
return {P,tris,segs};
}
// ---------- 统计记录 ----------
function recordCore(angs){
const s = angs[0]+angs[1]+angs[2]; // 理论上恒等于 π
stats.nTri++; stats.sumS += s;
const acute = Math.max(angs[0],angs[1],angs[2]) < Math.PI/2;
if(acute) stats.acute++; else stats.obtuse++;
const bins = acute?stats.binsA:stats.binsO;
for(const a of angs){
const k=Math.min(BINS-1, Math.floor(a/Math.PI*BINS));
bins[k]++;
}
stats.log.unshift({n:stats.nTri, d:angs.map(r=>r*180/Math.PI), acute, s});
if(stats.log.length>9) stats.log.pop();
if(--stats.sparkTick<=0){ stats.sparkTick=4; pushSpark(); }
}
function pushSpark(){
if(stats.cutN<8) return;
const p2=2/(stats.cutSin/stats.cutN);
const d=Math.max(0,-Math.log10(Math.abs(p2-Math.PI)/Math.PI));
stats.spark.push(d);
if(stats.spark.length>240) stats.spark.shift();
}
function piBuffon(){ return stats.cutN>=2 ? 2/(stats.cutSin/stats.cutN) : NaN; }
function piHistCoarse(){
let tot=0,sx=0;
for(let k=0;k<BINS;k++){
const c=stats.binsA[k]+stats.binsO[k];
tot+=c; sx+=c*((k+0.5)/BINS*Math.PI);
}
return tot>0 ? 3*sx/tot : NaN;
}
// ---------- 迭代生命周期 ----------
function resetSquare(){
const side=Math.min(W,H)*0.6;
S.rect={cx:W/2, cy:H*0.47, w:side, h:side};
S.area0=side*side;
S.resetNext=false;
}
function startIteration(){
S.iter++;
const R=S.rect;
const b=buildIteration();
S.P=b.P; S.tris=b.tris; S.cutSegs=b.segs;
S.remIdx=(Math.random()*S.tris.length)|0;
// 均匀随机测量角 θ∈[0,π):Buffon 型估计器的独立样本
S.thetaM=Math.random()*Math.PI;
stats.cutN++; stats.cutSin+=Math.sin(S.thetaM);
const rem=S.tris[S.remIdx];
S.remAngles=anglesOf(rem.poly);
S.remArea=areaPoly(rem.poly);
// 新矩形:面积守恒 A' = A − ΔA,长宽比随机
const A2=R.w*R.h-S.remArea;
const asp=rand(0.8,1.5);
S.nextRect={cx:R.cx, cy:R.cy, w:Math.sqrt(A2*asp), h:Math.sqrt(A2/asp)};
S.resetNext = (A2 < 0.15*S.area0) || S.nextRect.w<64 || S.nextRect.h<64;
S.recorded=false;
setPhase('cut');
}
function setPhase(p){
S.phase=p; S.t=0;
if(p==='fly'){
const NR=S.nextRect;
const RT={x:NR.cx-NR.w/2, y:NR.cy-NR.h/2, w:NR.w, h:NR.h};
const pcs=S.tris.filter((_,i)=>i!==S.remIdx);
const slots=layoutSlots(pcs,RT);
S.flyData=pcs.map((pc,i)=>({pc,slot:slots[i]}));
S.RT=RT;
}
}
function nextPhase(){
switch(S.phase){
case 'cut': setPhase('flash'); break;
case 'flash': setPhase('remove'); break;
case 'remove':
if(S.resetNext){ setPhase('reset'); } else { setPhase('fly'); }
break;
case 'fly': setPhase('merge'); break;
case 'merge': setPhase('settle'); break;
case 'settle': S.rect=S.nextRect; S.phase='idle'; break;
case 'reset': resetSquare(); S.phase='idle'; break;
default: S.phase='idle';
}
}
// 快进模式:纯数学迭代(无动画),用于批量采样
function fastIteration(){
const R=S.rect;
const b=buildIteration();
const remI=(Math.random()*b.tris.length)|0;
const rem=b.tris[remI];
recordCore(anglesOf(rem.poly));
const th=Math.random()*Math.PI;
stats.cutN++; stats.cutSin+=Math.sin(th);
const A2=R.w*R.h-areaPoly(rem.poly);
if(A2 < 0.15*S.area0){ resetSquare(); S.iter++; return; }
const asp=rand(0.8,1.5);
const w=Math.sqrt(A2*asp), h=Math.sqrt(A2/asp);
if(w<64||h<64){ resetSquare(); S.iter++; return; }
S.rect={cx:R.cx, cy:R.cy, w, h};
S.iter++;
}
// ---------- 重拼布局:把三角形旋到底边水平,逐行摆入目标矩形 ----------
function layoutSlots(pcs, RT){
const slots=[], pad=8;
const x0=RT.x+pad;
let yBase=RT.y+RT.h-pad, x=x0, rowH=0;
for(const pc of pcs){
const [A,B,C]=pc.poly;
const Vs=[A,B,C];
// 选最长边作为底边
let bi=0, blen=0;
const edges=[[A,B],[B,C],[C,A]];
edges.forEach((e,i)=>{const L=dist(e[0],e[1]); if(L>blen){blen=L;bi=i;}});
const bA=edges[bi][0], bB=edges[bi][1];
const apex=Vs.find(v=>v!==bA&&v!==bB);
const ex=norm(sub(bB,bA));
const hAlt=2*areaPoly(pc.poly)/blen;
const apexProj=dot(sub(apex,bA),ex);
const spanMin=Math.min(0,apexProj), spanMax=Math.max(blen,apexProj);
const span=spanMax-spanMin;
if(x-x0+span>RT.w-2*pad && x>x0){ x=x0; yBase-=rowH+pad; rowH=0; }
// 目标顶点:底边水平、apex 朝上(屏幕 y 向下)
const tA={x:x-spanMin, y:yBase};
const tB={x:x-spanMin+blen, y:yBase};
const tC={x:x-spanMin+apexProj, y:yBase-hAlt};
// 旋转量:把底边转到 +x 方向,并保证 apex 在底边上方
const phi0=Math.atan2(bB.y-bA.y, bB.x-bA.x);
let dRot=-phi0;
if(rotVec(sub(apex,bA),dRot).y>0) dRot+=Math.PI;
const srcC=centroidOf(pc.poly);
const tgtC={x:(tA.x+tB.x+tC.x)/3, y:(tA.y+tB.y+tC.y)/3};
slots.push({dx:tgtC.x-srcC.x, dy:tgtC.y-srcC.y, rot:dRot});
x+=span+pad; rowH=Math.max(rowH,hAlt);
}
return slots;
}
// ---------- 主更新 ----------
function update(dt){
if(S.phase==='idle'){
if(S.fast>0){
const k=Math.min(800,S.fast);
for(let i=0;i<k;i++) fastIteration();
S.fast-=k;
refreshStats(); drawHist(); drawSpark();
}else if(S.running){
startIteration();
}
return;
}
if(!S.running) return;
S.t += dt*S.speed/DUR[S.phase];
if(S.phase==='remove' && !S.recorded && S.t>0.1){
recordCore(S.remAngles);
S.recFlash=1;
}
if(S.t>=1) nextPhase();
}
// ---------- 绘制:几何视图 ----------
function drawGrid(){
g.strokeStyle='rgba(80,120,200,0.06)'; g.lineWidth=1;
g.beginPath();
for(let x=0;x<W;x+=40){g.moveTo(x,0);g.lineTo(x,H);}
for(let y=0;y<H;y+=40){g.moveTo(0,y);g.lineTo(W,y);}
g.stroke();
}
function drawRectSolid(R,alpha,color){
g.save(); g.globalAlpha=alpha;
const x=R.cx-R.w/2, y=R.cy-R.h/2;
g.fillStyle=color||'#1c2a4a';
g.fillRect(x,y,R.w,R.h);
g.strokeStyle='#3f62a8'; g.lineWidth=1.5;
g.strokeRect(x,y,R.w,R.h);
g.fillStyle='rgba(160,190,255,.55)';
g.font='11px '+MONO;
g.fillText(`${R.w|0} × ${R.h|0}`, x, y+R.h+14);
g.restore();
}
function drawPiece(pc,dx,dy,rot,alpha,scale){
const c=pc.centroid||(pc.centroid=centroidOf(pc.poly));
g.save(); g.globalAlpha=alpha;
g.translate(c.x+dx, c.y+dy); g.rotate(rot); if(scale!==1)g.scale(scale,scale);
g.beginPath();
pc.poly.forEach((p,i)=> i?g.lineTo(p.x-c.x,p.y-c.y):g.moveTo(p.x-c.x,p.y-c.y));
g.closePath();
g.fillStyle=pc.color; g.fill();
g.strokeStyle='rgba(5,10,20,.5)'; g.lineWidth=1.5; g.stroke();
g.restore();
}
function collectorPos(){ return {x:W-96,y:36}; }
function drawCollector(){
const c=collectorPos();
g.save();
g.globalAlpha=0.92;
g.fillStyle= S.recFlash>0.05 ? `rgba(83,216,255,${0.12+0.35*S.recFlash})` : 'rgba(83,216,255,0.08)';
rr(g,c.x-84,c.y-22,168,44,8); g.fill();
g.strokeStyle='rgba(83,216,255,0.5)'; g.stroke();
g.fillStyle='#9fdcff'; g.font='12px '+MONO; g.textAlign='center';
g.fillText('∠ 角度记录器', c.x, c.y-5);
g.fillStyle='#e8f6ff';
g.fillText(`已记录 ${stats.nTri} 个三角形`, c.x, c.y+12);
g.textAlign='left';
g.restore();
S.recFlash*=0.93;
}
// 测量切割线:均匀随机角 θ(Buffon 样本的可视化)
function drawMeasureLine(R,P,theta,alpha){
g.save();
const x=R.cx-R.w/2, y=R.cy-R.h/2;
g.beginPath(); g.rect(x,y,R.w,R.h); g.clip();
const d={x:Math.cos(theta),y:Math.sin(theta)};
const L=Math.max(R.w,R.h)*1.2;
g.strokeStyle=`rgba(255,107,157,${alpha})`;
g.setLineDash([7,6]); g.lineWidth=1.4;
g.beginPath();
g.moveTo(P.x-d.x*L,P.y-d.y*L); g.lineTo(P.x+d.x*L,P.y+d.y*L);
g.stroke(); g.setLineDash([]);
g.restore();
g.save();
g.fillStyle=`rgba(255,107,157,${Math.min(1,alpha+0.2)})`;
g.font='11px '+MONO;
g.fillText(`测量切割线 θ = ${(theta*180/Math.PI).toFixed(1)}°(均匀随机)`, x+8, y-8);
g.restore();
}
function drawCutPhase(){
const R=S.rect, t=S.t;
drawRectSolid(R,1);
drawMeasureLine(R,S.P,S.thetaM, 0.25+0.5*t);
// 逐条绘制切割线(进度动画)
const n=S.cutSegs.length;
g.save();
g.strokeStyle='#7ee0ff'; g.lineWidth=2;
g.shadowColor='#53d8ff'; g.shadowBlur=8;
for(let i=0;i<n;i++){
const u=clamp(t*n-i,0,1);
if(u<=0) continue;
const s=S.cutSegs[i], e=lerpP(s.a,s.b,easeIO(u));
g.beginPath(); g.moveTo(s.a.x,s.a.y); g.lineTo(e.x,e.y); g.stroke();
}
g.restore();
if(t>0.15){
g.fillStyle='#fff';
g.beginPath(); g.arc(S.P.x,S.P.y,3.5,0,7); g.fill();
}
}
function drawAngleLabels(tri,alpha){
const Vs=tri.poly, degs=S.remAngles.map(r=>r*180/Math.PI);
g.save(); g.globalAlpha=alpha; g.font='12px '+MONO;
for(let i=0;i<3;i++){
const V=Vs[i], P1=Vs[(i+1)%3], P2=Vs[(i+2)%3];
const bis=norm(add(norm(sub(P1,V)),norm(sub(P2,V))));
g.fillStyle='#ffffff';
g.fillText(degs[i].toFixed(1)+'°', V.x+bis.x*26-14, V.y+bis.y*26+4);
}
g.restore();
}
function drawRemovePhase(){
const t=S.t;
const cp=collectorPos();
S.tris.forEach((pc,i)=>{
if(i===S.remIdx) return;
drawPiece(pc,0,0,0,1,1);
});
const rem=S.tris[S.remIdx];
if(t<0.45){
const pulse=1+0.04*Math.sin(t*40);
drawPiece(rem,0,0,0,1,pulse);
drawAngleLabels(rem, clamp(t/0.12,0,1));
// 高亮描边
const c=centroidOf(rem.poly);
g.save(); g.translate(c.x,c.y); g.scale(pulse,pulse);
g.beginPath();
rem.poly.forEach((p,j)=> j?g.lineTo(p.x-c.x,p.y-c.y):g.moveTo(p.x-c.x,p.y-c.y));
g.closePath(); g.strokeStyle='#ff5c7a'; g.lineWidth=2.5; g.stroke();
g.restore();
}else{
const u=easeIO((t-0.45)/0.55);
const c=centroidOf(rem.poly);
drawPiece(rem,(cp.x-c.x)*u,(cp.y-c.y)*u, u*1.5, 1-u, 1-0.75*u);
// 记录信息浮牌
const d=S.remAngles.map(r=>r*180/Math.PI);
g.save(); g.globalAlpha=clamp((t-0.5)/0.2,0,1);
const txt=`记录 △#${stats.nTri}: ${d[0].toFixed(1)}° + ${d[1].toFixed(1)}° + ${d[2].toFixed(1)}° = π`;
g.font='12px '+MONO;
const tw=g.measureText(txt).width;
g.fillStyle='rgba(10,17,34,.92)';
rr(g, cp.x-tw/2-10, cp.y+30, tw+20, 26, 6); g.fill();
g.strokeStyle='rgba(83,216,255,.5)'; g.stroke();
g.fillStyle='#bfeaff'; g.textAlign='center';
g.fillText(txt, cp.x, cp.y+47);
g.textAlign='left'; g.restore();
}
}
function drawFlyPhase(){
const RT=S.RT, t=S.t;
// 目标矩形虚线轮廓
g.save();
g.strokeStyle='rgba(83,216,255,.7)'; g.setLineDash([6,5]); g.lineWidth=1.5;
g.strokeRect(RT.x,RT.y,RT.w,RT.h); g.setLineDash([]);
g.fillStyle='rgba(83,216,255,.6)'; g.font='11px '+MONO;
g.fillText('目标矩形(面积守恒)', RT.x, RT.y-6);
g.restore();
S.flyData.forEach((fd,j)=>{
const u=clamp(t*1.35-j*0.12,0,1), e=easeIO(u);
drawPiece(fd.pc, fd.slot.dx*e, fd.slot.dy*e, fd.slot.rot*e, 1, 1);
});
}
function drawMergePhase(){
const t=easeIO(S.t);
S.flyData.forEach(fd=>{
drawPiece(fd.pc, fd.slot.dx, fd.slot.dy, fd.slot.rot, 1-t, 1);
});
drawRectSolid(S.nextRect, t);
// 面积守恒浮牌
const A=(S.rect.w*S.rect.h)|0, dA=S.remArea|0, A2=(S.nextRect.w*S.nextRect.h)|0;
g.save(); g.globalAlpha=t;
g.font='12px '+MONO; g.fillStyle='#bfeaff'; g.textAlign='center';
g.fillText(`面积守恒:${A} − ${dA} = ${A2} px² → 新矩形`, S.nextRect.cx, S.nextRect.cy);
g.textAlign='left'; g.restore();
}
function drawResetPhase(){
const t=S.t;
const side=Math.min(W,H)*0.6;
g.save();
g.strokeStyle=`rgba(255,180,84,${1-t})`; g.lineWidth=2;
g.beginPath(); g.arc(W/2,H*0.47, t*Math.max(W,H)*0.6, 0, 7); g.stroke();
g.globalAlpha=easeIO(clamp(t*1.4-0.3,0,1));
const s2=side*(0.6+0.4*easeIO(t));
g.fillStyle='#1c2a4a';
g.fillRect(W/2-s2/2, H*0.47-s2/2, s2, s2);
g.strokeStyle='#3f62a8'; g.strokeRect(W/2-s2/2, H*0.47-s2/2, s2, s2);
g.fillStyle='#ffd9a0'; g.font='13px '+MONO; g.textAlign='center';
g.fillText('面积过小 → 重置为新正方形(统计数据继续累积)', W/2, H*0.47);
g.textAlign='left'; g.restore();
}
function drawHUD(){
g.save();
g.font='12px '+MONO; g.fillStyle='rgba(160,190,255,.8)';
g.fillText(`迭代 #${S.iter}`, 14, 22);
if(S.rect) g.fillText(`矩形 ${S.rect.w|0} × ${S.rect.h|0} px · 面积 ${(S.rect.w*S.rect.h)|0}`, 14, 40);
g.fillStyle='rgba(125,141,177,.8)';
g.fillText(`切割角样本 ${stats.cutN} · 三角形样本 ${stats.nTri}`, 14, 58);
// 阶段说明(底部居中)
const cap=PHASE_CN[S.phase]||'';
g.font='13px "Microsoft YaHei",sans-serif';
const tw=g.measureText(cap).width;
g.fillStyle='rgba(10,17,34,.85)';
rr(g, W/2-tw/2-14, H-38, tw+28, 26, 13); g.fill();
g.strokeStyle='rgba(83,216,255,.3)'; g.stroke();
g.fillStyle='#cfe6ff'; g.textAlign='center';
g.fillText(cap, W/2, H-20);
g.textAlign='left';
g.restore();
}
function draw(){
g.clearRect(0,0,W,H);
drawGrid();
drawCollector();
switch(S.phase){
case 'idle': if(S.rect) drawRectSolid(S.rect,1); break;
case 'cut': drawCutPhase(); break;
case 'flash':
drawMeasureLine(S.rect,S.P,S.thetaM,0.6);
S.tris.forEach(pc=>drawPiece(pc,0,0,0,easeIO(S.t),1));
break;
case 'remove': drawRemovePhase(); break;
case 'fly': drawFlyPhase(); break;
case 'merge': drawMergePhase(); break;
case 'settle': drawRectSolid(S.nextRect,1); break;
case 'reset': drawResetPhase(); break;
}
drawHUD();
}
// ---------- 绘制:直方图 ----------
function drawHist(){
const w=histCv.clientWidth, h=150;
hc.clearRect(0,0,w,h);
let maxCol=1;
for(let k=0;k<BINS;k++) maxCol=Math.max(maxCol, stats.binsA[k]+stats.binsO[k]);
const bw=w/BINS;
for(let k=0;k<BINS;k++){
const a=stats.binsA[k], o=stats.binsO[k];
const hA=(h-22)*a/maxCol, hO=(h-22)*o/maxCol;
hc.fillStyle='rgba(255,180,84,.85)';
hc.fillRect(k*bw+1, h-18-hO, bw-2, hO);
hc.fillStyle='rgba(83,216,255,.85)';
hc.fillRect(k*bw+1, h-18-hO-hA, bw-2, hA);
}
// 90° 分界线
const x90=w*0.5;
hc.strokeStyle='rgba(255,92,122,.7)'; hc.setLineDash([4,4]);
hc.beginPath(); hc.moveTo(x90,4); hc.lineTo(x90,h-18); hc.stroke(); hc.setLineDash([]);
hc.fillStyle='#ff5c7a'; hc.font='10px '+MONO;
hc.fillText('90°', x90+3, 12);
// 坐标轴标签
hc.fillStyle='rgba(125,141,177,.9)';
[[0,'0°'],[0.25,'45°'],[0.5,'90°'],[0.75,'135°'],[1,'180°']].forEach(([f,t])=>{
hc.fillText(t, clamp(w*f-10,2,w-26), h-5);
});
// 图例
hc.fillStyle='rgba(83,216,255,.9)'; hc.fillRect(w-118,6,8,8);
hc.fillStyle='#9fb4de'; hc.fillText('锐角三角形角度', w-106, 13);
hc.fillStyle='rgba(255,180,84,.9)'; hc.fillRect(w-118,19,8,8);
hc.fillStyle='#9fb4de'; hc.fillText('钝角三角形角度', w-106, 26);
}
// ---------- 绘制:蒙特卡洛收敛火花线 ----------
function drawSpark(){
const w=sparkCv.clientWidth, h=sparkCv.clientHeight||72;
sc.clearRect(0,0,w,h);
sc.strokeStyle='rgba(30,42,68,.9)';
for(let i=1;i<=4;i++){
const y=h-(i/5)*h;
sc.beginPath(); sc.moveTo(0,y); sc.lineTo(w,y); sc.stroke();
}
const data=stats.spark;
if(data.length<2){
sc.fillStyle='rgba(125,141,177,.7)'; sc.font='11px '+MONO;
sc.fillText('等待样本累积…', 8, h/2);
return;
}
let maxD=3; for(const d of data) maxD=Math.max(maxD,d);
maxD+=0.8;
sc.beginPath();
data.forEach((d,i)=>{
const x=i/(data.length-1)*w, y=h-(d/maxD)*(h-8)-2;
i?sc.lineTo(x,y):sc.moveTo(x,y);
});
sc.strokeStyle='#42e696'; sc.lineWidth=1.5; sc.stroke();
sc.fillStyle='rgba(125,141,177,.9)'; sc.font='10px '+MONO;
sc.fillText(`MC 正确位数(纵轴) 峰值≈${maxD.toFixed(1)} · 斜率≈-½logN`, 6, 11);
}
// ---------- 统计面板刷新 ----------
function refreshStats(){
const N=stats.nTri;
if(N>0){
const p1=stats.sumS/N; // 主估计器:角度积分
const a=p1.toFixed(13), b=Math.PI.toFixed(13);
let m=0; while(m<a.length && a[m]===b[m]) m++;
const dec=Math.max(0,m-2); // 小数点后稳定位数
el.piHat.innerHTML=
`<span class="ok">${a.slice(0,m)}</span><span class="bad">${a.slice(m)}</span>`;
const err=Math.abs(p1-Math.PI);
el.err.textContent = err===0 ? '0(机器精度内)' : err.toExponential(2);
el.digits.textContent = dec+' 位';
el.badge.className = 'badge '+(dec>=10?'ok':'no');
el.badge.textContent = dec>=10 ? `✓ 已精确 ${dec} 位(≥10 达标)` : `收敛中 · ${dec} 位`;
}else{
el.piHat.textContent='—'; el.err.textContent='—';
el.digits.textContent='—';
el.badge.className='badge no'; el.badge.textContent='目标 ≥ 10 位';
}
el.n.textContent=N;
el.ang.textContent=N*3;
el.acute.textContent=stats.acute;
el.obtuse.textContent=stats.obtuse;
el.pacute.textContent = N?`P = ${(stats.acute/N).toFixed(4)}`:'P = —';
el.pobtuse.textContent = N?`P = ${(stats.obtuse/N).toFixed(4)}`:'P = —';
const p2=piBuffon();
if(isFinite(p2)){
el.p2.textContent=p2.toFixed(10);
el.p2e.textContent=Math.abs(p2-Math.PI).toExponential(2);
}else{ el.p2.textContent='—'; el.p2e.textContent='—'; }
el.cutn.textContent=stats.cutN;
const ph=piHistCoarse();
el.ph.textContent=isFinite(ph)?ph.toFixed(5):'—';
// 最近样本表
el.log.innerHTML=stats.log.map(r=>
`<tr><td>${r.n}</td>`+
r.d.map(x=>`<td>${x.toFixed(2)}°</td>`).join('')+
`<td class="${r.acute?'acc':'obt'}">${r.acute?'锐角':'钝角'}</td>`+
`<td>${r.s.toFixed(13)}</td></tr>`).join('');
// 状态行与按钮
el.status.textContent=
`第 ${S.iter} 轮 · ${PHASE_CN[S.phase]} · 面积 ${(S.rect?S.rect.w*S.rect.h:0)|0} px²`+
(S.fast>0?` · 快进队列 ${S.fast}`:'');
el.btnFast.textContent = `⚡ 后台快进 ×5000${S.fast>0?`(排队 ${S.fast})`:''}`;
}
// ---------- 控件 ----------
el.btnRun.onclick=()=>{
S.running=!S.running;
el.btnRun.textContent=S.running?'⏸ 暂停':'▶ 继续';
};
el.btnFast.onclick=()=>{ S.fast+=5000; };
el.btnReset.onclick=()=>{ resetStats(); };
el.speed.oninput=()=>{
S.speed=parseFloat(el.speed.value);
el.speedVal.textContent=S.speed.toFixed(1)+'×';
};
window.addEventListener('resize',()=>{ sizeAll(); });
// ---------- 主循环 ----------
let last=performance.now(), acc1=0, acc2=0;
function loop(now){
const dt=Math.min(0.05,(now-last)/1000); last=now;
update(dt);
draw();
acc1+=dt; acc2+=dt;
if(acc1>0.12){ acc1=0; refreshStats(); }
if(acc2>0.3){ acc2=0; drawHist(); drawSpark(); }
requestAnimationFrame(loop);
}
// ---------- 启动 ----------
sizeAll();
resetSquare();
refreshStats(); drawHist(); drawSpark();
requestAnimationFrame(loop);
</script>
</body>
</html>