init: 导入团队知识库内容
This commit is contained in:
@@ -0,0 +1,326 @@
|
||||
<!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>
|
||||
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||
body {
|
||||
background: #1a1a2e;
|
||||
min-height: 100vh;
|
||||
display: flex; flex-direction: column;
|
||||
align-items: center; justify-content: center;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto,
|
||||
'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', sans-serif;
|
||||
color: #ccc; overflow: hidden;
|
||||
}
|
||||
h2 { font-size: 16px; font-weight: 600; margin-bottom: 8px; color: #fff; }
|
||||
.subtitle { font-size: 13px; color: #888; margin-bottom: 20px; }
|
||||
canvas { cursor: grab; border-radius: 8px; }
|
||||
canvas:active { cursor: grabbing; }
|
||||
.controls { display: flex; gap: 12px; align-items: center; margin-top: 16px; }
|
||||
.btn {
|
||||
padding: 10px 28px; border: 1px solid rgba(255,255,255,0.2);
|
||||
border-radius: 8px; background: rgba(255,255,255,0.08);
|
||||
color: #fff; font-size: 14px; cursor: pointer; transition: all 0.2s;
|
||||
}
|
||||
.btn:hover { background: rgba(255,255,255,0.15); border-color: rgba(255,255,255,0.4); }
|
||||
.btn-active { background: rgba(22,119,255,0.4); border-color: #1677FF; }
|
||||
.info { position: fixed; bottom: 16px; font-size: 12px; color: #555; }
|
||||
#debug {
|
||||
position: fixed; top: 10px; right: 10px; font-size: 11px; color: #aaa;
|
||||
font-family: monospace; background: rgba(0,0,0,0.6); padding: 10px 14px;
|
||||
border-radius: 6px; line-height: 1.6; max-width: 300px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>正四面体旋转动画</h2>
|
||||
<p class="subtitle">鼠标拖拽旋转 · 铰链展开</p>
|
||||
<canvas id="canvas" width="500" height="500"></canvas>
|
||||
<div class="controls">
|
||||
<button class="btn" id="toggleBtn" onclick="toggleUnfold()">展开</button>
|
||||
<button class="btn" id="autoBtn" onclick="toggleAutoRotate()">自动旋转</button>
|
||||
</div>
|
||||
<div id="debug">点击展开查看各面边长</div>
|
||||
<div class="info">正四面体 — 铰链展开为平面展开图</div>
|
||||
|
||||
<script>
|
||||
const canvas = document.getElementById('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const W = canvas.width, H = canvas.height;
|
||||
const cx = W/2, cy = H/2;
|
||||
const debugEl = document.getElementById('debug');
|
||||
|
||||
const SC = 90;
|
||||
const V = [
|
||||
[ 0, 2/Math.sqrt(3), 0], // 0
|
||||
[-1, -1/Math.sqrt(3), 0], // 1
|
||||
[ 1, -1/Math.sqrt(3), 0], // 2
|
||||
[ 0, 0, 2*Math.sqrt(2/3)], // 3
|
||||
].map(v => v.map(c => c * SC));
|
||||
|
||||
const EDGE = 2 * SC;
|
||||
const FACE_H = EDGE * Math.sqrt(3) / 2;
|
||||
|
||||
const FACES = [
|
||||
[0, 2, 1], // 0: 底面
|
||||
[0, 1, 3], // 1
|
||||
[1, 2, 3], // 2
|
||||
[2, 0, 3], // 3
|
||||
];
|
||||
|
||||
// ===== 铰链展开预计算 =====
|
||||
const hingeData = FACES.map((fv, fi) => {
|
||||
if (fi === 0) return { base: true, unfoldedVerts: FACES[0].map(i => [...V[i]]) };
|
||||
const f0v = FACES[0];
|
||||
const shared = fv.filter(v => f0v.includes(v));
|
||||
const apexIdx = fv.find(v => !f0v.includes(v));
|
||||
const eA = V[shared[0]], eB = V[shared[1]];
|
||||
const apex = V[apexIdx];
|
||||
|
||||
const hx = eB[0]-eA[0], hy = eB[1]-eA[1], hz = eB[2]-eA[2];
|
||||
const hl = Math.sqrt(hx*hx+hy*hy+hz*hz);
|
||||
const axis = [hx/hl, hy/hl, hz/hl];
|
||||
|
||||
const mx = (eA[0]+eB[0])/2, my = (eA[1]+eB[1])/2;
|
||||
const dx = eB[0]-eA[0], dy = eB[1]-eA[1];
|
||||
const dl = Math.sqrt(dx*dx+dy*dy);
|
||||
const perp = [-dy/dl, dx/dl];
|
||||
const uApex1 = [mx + perp[0]*FACE_H, my + perp[1]*FACE_H, 0];
|
||||
const uApex2 = [mx - perp[0]*FACE_H, my - perp[1]*FACE_H, 0];
|
||||
const d1 = uApex1[0]*uApex1[0]+uApex1[1]*uApex1[1];
|
||||
const d2 = uApex2[0]*uApex2[0]+uApex2[1]*uApex2[1];
|
||||
const unfoldedApex = d1 > d2 ? uApex1 : uApex2;
|
||||
|
||||
// 验证展开后三边长度
|
||||
const ua = unfoldedApex;
|
||||
const s0 = V[shared[0]], s1 = V[shared[1]];
|
||||
const e0 = Math.sqrt((ua[0]-s0[0])**2+(ua[1]-s0[1])**2);
|
||||
const e1 = Math.sqrt((ua[1]-s0[1])**2+(ua[1]-s1[1])**2);
|
||||
const edgeLen = Math.sqrt((s1[0]-s0[0])**2+(s1[1]-s0[1])**2);
|
||||
|
||||
const vFrom = [apex[0]-eA[0], apex[1]-eA[1], apex[2]-eA[2]];
|
||||
const vTo = [unfoldedApex[0]-eA[0], unfoldedApex[1]-eA[1], 0];
|
||||
const crX = vFrom[1]*vTo[2]-vFrom[2]*vTo[1];
|
||||
const crY = vFrom[2]*vTo[0]-vFrom[0]*vTo[2];
|
||||
const crZ = vFrom[0]*vTo[1]-vFrom[1]*vTo[0];
|
||||
const crL = Math.sqrt(crX*crX+crY*crY+crZ*crZ);
|
||||
const dotP = vFrom[0]*vTo[0]+vFrom[1]*vTo[1]+vFrom[2]*vTo[2];
|
||||
let angle = Math.atan2(crL, dotP);
|
||||
if (crX*axis[0]+crY*axis[1]+crZ*axis[2] < 0) angle = -angle;
|
||||
|
||||
const unfoldedVerts = fv.map(vi => {
|
||||
if (shared.includes(vi)) return [...V[vi]];
|
||||
return [...unfoldedApex];
|
||||
});
|
||||
|
||||
return {
|
||||
base: false, shared, apexIdx, hingePt: eA, axis, unfoldedApex, angle, unfoldedVerts,
|
||||
verify: `e0=${e0.toFixed(1)} e1=${e1.toFixed(1)} base=${edgeLen.toFixed(1)}`
|
||||
};
|
||||
});
|
||||
|
||||
function rodrigues(v, axis, angle) {
|
||||
const [x,y,z] = v;
|
||||
const [u,v2,w] = axis;
|
||||
const c = Math.cos(angle), s = Math.sin(angle), t = 1-c;
|
||||
return [
|
||||
(t*u*u+c)*x+(t*u*v2-w*s)*y+(t*u*w+v2*s)*z,
|
||||
(t*u*v2+w*s)*x+(t*v2*v2+c)*y+(t*v2*w-u*s)*z,
|
||||
(t*u*w-v2*s)*x+(t*v2*w+u*s)*y+(t*w*w+c)*z
|
||||
];
|
||||
}
|
||||
|
||||
function faceVerts(fi, t) {
|
||||
const hd = hingeData[fi];
|
||||
const fv = FACES[fi];
|
||||
if (hd.base) return fv.map(i => [...V[i]]);
|
||||
if (t >= 0.999) return hd.unfoldedVerts.map(v => [...v]);
|
||||
if (t === 0) return fv.map(i => [...V[i]]);
|
||||
return fv.map(vi => {
|
||||
if (hd.shared.includes(vi)) return [...V[vi]];
|
||||
const v = V[vi];
|
||||
const hp = hd.hingePt;
|
||||
const translated = [v[0]-hp[0], v[1]-hp[1], v[2]-hp[2]];
|
||||
const rotated = rodrigues(translated, hd.axis, t * hd.angle);
|
||||
return [rotated[0]+hp[0], rotated[1]+hp[1], rotated[2]+hp[2]];
|
||||
});
|
||||
}
|
||||
|
||||
function proj(x, y, z) {
|
||||
const cY = Math.cos(rY), sY = Math.sin(rY);
|
||||
const cX = Math.cos(rX), sX = Math.sin(rX);
|
||||
let x1 = x*cY+z*sY, y1 = y, z1 = -x*sY+z*cY;
|
||||
let y2 = y1*cX-z1*sX, z2 = y1*sX+z1*cX;
|
||||
const fov = 500;
|
||||
const sc = fov/(fov+z2);
|
||||
return [cx+x1*sc, cy+y2*sc, z2];
|
||||
}
|
||||
|
||||
function dist(a, b) {
|
||||
return Math.sqrt((a[0]-b[0])**2+(a[1]-b[1])**2);
|
||||
}
|
||||
|
||||
let rX = -0.5, rY = 0.6;
|
||||
let rot2d = 0;
|
||||
let unfoldT = 0, unfoldTarget = 0;
|
||||
let isUnfolded = false, autoRotate = true;
|
||||
let dragging = false, lastMX, lastMY;
|
||||
|
||||
const COLORS = [
|
||||
{ fill:'rgba(239,68,68,0.8)', stroke:'rgba(255,255,255,0.7)', label:'1' },
|
||||
{ fill:'rgba(59,130,246,0.8)', stroke:'rgba(255,255,255,0.7)', label:'2' },
|
||||
{ fill:'rgba(34,197,94,0.8)', stroke:'rgba(255,255,255,0.7)', label:'3' },
|
||||
{ fill:'rgba(234,179,8,0.8)', stroke:'rgba(255,255,255,0.7)', label:'4' },
|
||||
];
|
||||
|
||||
function drawFace(pv, col, showEdges) {
|
||||
const [p0,p1,p2] = pv;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p0[0],p0[1]); ctx.lineTo(p1[0],p1[1]); ctx.lineTo(p2[0],p2[1]);
|
||||
ctx.closePath();
|
||||
ctx.fillStyle = col.fill; ctx.fill();
|
||||
ctx.strokeStyle = col.stroke; ctx.lineWidth = 2; ctx.stroke();
|
||||
const mx = (p0[0]+p1[0]+p2[0])/3, my = (p0[1]+p1[1]+p2[1])/3;
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.9)';
|
||||
ctx.font = 'bold 22px sans-serif';
|
||||
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
||||
ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 4;
|
||||
ctx.fillText(col.label, mx, my);
|
||||
ctx.shadowBlur = 0;
|
||||
|
||||
if (showEdges) {
|
||||
ctx.font = '11px monospace';
|
||||
ctx.fillStyle = 'rgba(255,255,255,0.6)';
|
||||
const e1 = dist(p0,p1).toFixed(0);
|
||||
const e2 = dist(p1,p2).toFixed(0);
|
||||
const e3 = dist(p2,p0).toFixed(0);
|
||||
ctx.fillText(e1, (p0[0]+p1[0])/2, (p0[1]+p1[1])/2);
|
||||
ctx.fillText(e2, (p1[0]+p2[0])/2, (p1[1]+p2[1])/2);
|
||||
ctx.fillText(e3, (p2[0]+p0[0])/2, (p2[1]+p0[1])/2);
|
||||
}
|
||||
}
|
||||
|
||||
function draw() {
|
||||
ctx.clearRect(0,0,W,H);
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.03)';
|
||||
ctx.lineWidth = 0.5;
|
||||
for (let x = 0; x < W; x += 40) { ctx.beginPath(); ctx.moveTo(x,0); ctx.lineTo(x,H); ctx.stroke(); }
|
||||
for (let y = 0; y < H; y += 40) { ctx.beginPath(); ctx.moveTo(0,y); ctx.lineTo(W,y); ctx.stroke(); }
|
||||
|
||||
let debugInfo = '';
|
||||
|
||||
if (unfoldT < 0.5) {
|
||||
const faceData = FACES.map((fv, fi) => {
|
||||
const v3d = faceVerts(fi, unfoldT * 2);
|
||||
const pv = v3d.map(([x,y,z]) => proj(x,y,z));
|
||||
return { fi, pv, avgZ: (pv[0][2]+pv[1][2]+pv[2][2])/3 };
|
||||
});
|
||||
faceData.sort((a,b) => b.avgZ - a.avgZ);
|
||||
for (const fd of faceData) drawFace(fd.pv, COLORS[fd.fi], false);
|
||||
|
||||
if (unfoldT < 0.3) {
|
||||
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
|
||||
ctx.lineWidth = 1;
|
||||
const ap = V.map(([x,y,z]) => proj(x,y,z));
|
||||
for (const [a,b] of [[0,1],[0,2],[0,3],[1,2],[1,3],[2,3]]) {
|
||||
ctx.beginPath(); ctx.moveTo(ap[a][0],ap[a][1]); ctx.lineTo(ap[b][0],ap[b][1]); ctx.stroke();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const c = Math.cos(rot2d), s = Math.sin(rot2d);
|
||||
const faceData = FACES.map((fv, fi) => {
|
||||
const pv = hingeData[fi].unfoldedVerts.map(([x,y,z]) => {
|
||||
return [cx + x*c - y*s, cy + x*s + y*c, 0];
|
||||
});
|
||||
return { fi, pv };
|
||||
});
|
||||
for (const fd of faceData) drawFace(fd.pv, COLORS[fd.fi], true);
|
||||
|
||||
// 几何验证
|
||||
hingeData.forEach((hd, fi) => {
|
||||
if (fi === 0) {
|
||||
const fv = FACES[0];
|
||||
const e = dist(V[fv[0]], V[fv[1]]).toFixed(0);
|
||||
debugInfo += `面${fi+1}: ${e} ${e} ${e}\n`;
|
||||
} else {
|
||||
debugInfo += `面${fi+1}: ${hd.verify}\n`;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
debugEl.textContent = debugInfo || '点击展开后显示各面3D边长';
|
||||
}
|
||||
|
||||
function animate() {
|
||||
if (Math.abs(unfoldT - unfoldTarget) > 0.001) {
|
||||
unfoldT += (unfoldTarget - unfoldT) * 0.075;
|
||||
if (Math.abs(unfoldT - unfoldTarget) < 0.001) unfoldT = unfoldTarget;
|
||||
}
|
||||
if (autoRotate) {
|
||||
if (!isUnfolded && !dragging && unfoldT < 0.5) {
|
||||
rY += 0.008;
|
||||
rX = -0.5 + Math.sin(Date.now()/4000)*0.2;
|
||||
}
|
||||
if (isUnfolded && !dragging && unfoldT > 0.5) {
|
||||
rot2d += 0.005;
|
||||
}
|
||||
}
|
||||
draw();
|
||||
requestAnimationFrame(animate);
|
||||
}
|
||||
animate();
|
||||
|
||||
canvas.addEventListener('mousedown', e => { dragging=true; lastMX=e.clientX; lastMY=e.clientY; });
|
||||
document.addEventListener('mousemove', e => {
|
||||
if (!dragging) return;
|
||||
if (unfoldT > 0.5) {
|
||||
rot2d += (e.clientX-lastMX)*0.008;
|
||||
} else {
|
||||
rY += (e.clientX-lastMX)*0.008;
|
||||
rX -= (e.clientY-lastMY)*0.008;
|
||||
rX = Math.max(-1.5,Math.min(1.5,rX));
|
||||
}
|
||||
lastMX=e.clientX; lastMY=e.clientY;
|
||||
});
|
||||
document.addEventListener('mouseup', () => { dragging=false; });
|
||||
canvas.addEventListener('touchstart', e => {
|
||||
e.preventDefault(); dragging=true;
|
||||
lastMX=e.touches[0].clientX; lastMY=e.touches[0].clientY;
|
||||
}, {passive:false});
|
||||
canvas.addEventListener('touchmove', e => {
|
||||
e.preventDefault(); if(!dragging) return;
|
||||
if (unfoldT > 0.5) {
|
||||
rot2d += (e.touches[0].clientX-lastMX)*0.008;
|
||||
} else {
|
||||
rY += (e.touches[0].clientX-lastMX)*0.008;
|
||||
rX -= (e.touches[0].clientY-lastMY)*0.008;
|
||||
rX = Math.max(-1.5,Math.min(1.5,rX));
|
||||
}
|
||||
lastMX=e.touches[0].clientX; lastMY=e.touches[0].clientY;
|
||||
}, {passive:false});
|
||||
canvas.addEventListener('touchend', () => { dragging=false; });
|
||||
|
||||
function toggleUnfold() {
|
||||
isUnfolded = !isUnfolded;
|
||||
const btn = document.getElementById('toggleBtn');
|
||||
if (isUnfolded) {
|
||||
btn.textContent = '收起'; btn.classList.add('btn-active');
|
||||
unfoldTarget = 1;
|
||||
} else {
|
||||
btn.textContent = '展开'; btn.classList.remove('btn-active');
|
||||
unfoldTarget = 0;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleAutoRotate() {
|
||||
autoRotate = !autoRotate;
|
||||
const btn = document.getElementById('autoBtn');
|
||||
btn.textContent = autoRotate ? '停止旋转' : '自动旋转';
|
||||
btn.classList.toggle('btn-active', autoRotate);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user