feat: 新增思想实验室模块与配套功能
- 新增5个物理仿真实验室:黑洞模拟、日地轨道、3D太阳系、π推导、拉格朗日点 - 新增思想实验室后端路由与鉴权逻辑 - 首页新增思想实验室页签与前端渲染逻辑 - 修复中韩半导体监控标的配置错误 - 新增市场技能推送白名单过滤逻辑与单元测试
This commit is contained in:
@@ -0,0 +1,826 @@
|
||||
<!-- ============================================================
|
||||
思想实验室 · 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">理论 π = 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θ (ρ 为记录角度的经验分布)<br>
|
||||
π̂<sub>MC</sub> = 2 / E[sin θ<sub>cut</sub>] (均匀随机测量角,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[θ] 重现该恒等式——因此主估计器在采样下即确定性地稳定到机器精度(>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>
|
||||
Reference in New Issue
Block a user