SOURCE

console 命令行工具 X clear

                    
>
console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>希沃白板|互动旋转几何</title>
<style>
*{box-sizing:border-box}
body{
  margin:0;background:#f3f6fb;color:#1e293b;
  font-family:"Microsoft YaHei","PingFang SC",sans-serif;
  touch-action:manipulation;
}
main{max-width:1100px;margin:auto;padding:18px}
h1{font-size:26px;margin:0 0 8px}
h2{font-size:19px;margin:0 0 12px}
p{line-height:1.65}
.muted{color:#64748b;font-size:14px}
.panel{
  background:white;border:1px solid #dbe3ee;
  border-radius:14px;padding:15px;margin-bottom:14px;
}
.row{display:flex;gap:10px;flex-wrap:wrap;align-items:center}
button{
  font:inherit;font-size:16px;padding:11px 17px;
  border:1px solid #cbd5e1;border-radius:10px;
  background:white;color:#1e293b;cursor:pointer;
  touch-action:manipulation;user-select:none;
}
button:active{transform:scale(.98)}
button.active{background:#dbeafe;border:2px solid #2563eb;padding:10px 16px}
button.primary{background:#2563eb;color:white;border-color:#2563eb}
button:disabled{opacity:.45;cursor:default}
#status{
  font-size:17px;font-weight:700;line-height:1.65;
  background:#eff6ff;color:#1d4ed8;
  border-radius:10px;padding:12px;margin-top:12px;
}
#status.ok{background:#ecfdf5;color:#047857}
.stage{
  border:1px solid #dbe3ee;border-radius:12px;
  overflow:hidden;background:white;
}
svg{
  display:block;width:100%;height:auto;
  touch-action:manipulation;
  -webkit-user-select:none;user-select:none;
}
svg .hit-area{cursor:pointer;pointer-events:all}
svg .triangle-label{cursor:pointer;pointer-events:all}
svg .vertex-hit{cursor:pointer;pointer-events:all}
.legend{
  display:flex;gap:12px;flex-wrap:wrap;
  padding:11px;font-size:13px;color:#64748b;
  border-top:1px solid #e2e8f0;
}
.key{display:inline-block;width:22px;border-top:3px solid;margin-right:5px}
.info{
  padding:13px;background:#f8fafc;border:1px solid #e2e8f0;
  border-radius:10px;margin-top:12px;line-height:1.9;
}
.blue{color:#2563eb;font-weight:700}
.orange{color:#ea580c;font-weight:700}
@media(max-width:600px){
  main{padding:9px}
  h1{font-size:22px}
  .panel{padding:10px}
  button{padding:10px 12px}
}
</style>
</head>
<body>
<main>
  <h1>等边三角形:点击选择,旋转演示</h1>
  <p class="muted">
    操作顺序:①选择内部点或外部点 → ②点一个小三角形
    → ③点旋转顶点。适用于触摸屏和希沃白板笔。
  </p>

  <section class="panel">
    <h2>第一步:选择 P 的位置</h2>
    <div class="row">
      <button class="active" data-position="inside">P 在内部</button>
      <button data-position="outside">P 在外部</button>
    </div>
  </section>

  <section class="panel">
    <h2>第二步:直接点击图中的小三角形</h2>
    <p class="muted">
      点 △APB、△BPC 或 △CPA 的内部,也可以点击图中的三角形标签。
      选中后会高亮显示。
    </p>

    <div class="stage">
      <svg id="board" viewBox="0 0 900 690"
           aria-label="点击小三角形,再点击旋转中心"></svg>
      <div class="legend">
        <span><i class="key" style="border-color:#64748b"></i>原图形</span>
        <span><i class="key" style="border-color:#2563eb"></i>选中的小三角形</span>
        <span><i class="key" style="border-color:#ea580c;border-top-style:dashed"></i>旋转后的图形</span>
        <span style="color:#dc2626">● 原点 P</span>
        <span style="color:#ea580c">● 旋转后的点 P′</span>
      </div>
    </div>

    <div id="status" aria-live="polite">
      请先点击图中的一个小三角形。
    </div>
  </section>

  <section class="panel">
    <h2>第三步:点击旋转顶点</h2>
    <p id="centerHelp" class="muted">
      选择小三角形后,可以点击该三角形包含的 A、B 或 C 中的两个顶点。
      点 P 不是旋转中心。
    </p>
    <div class="row">
      <button id="pause" class="primary" disabled>暂停动画</button>
      <button id="replay" disabled>重新旋转 ↻</button>
      <button id="clear">重新选择</button>
    </div>

    <div class="info">
      <div>当前小三角形:<strong id="chosenTriangle">尚未选择</strong></div>
      <div>旋转中心:<strong id="chosenCenter">尚未选择</strong></div>
      <div>旋转方向:<strong id="chosenDirection">—</strong></div>
      <div class="blue" id="equalities">对应线段关系将在这里显示。</div>
      <div id="explain" class="muted">选好三角形和顶点后,自动播放旋转动画。</div>
    </div>
  </section>
</main>

<script>
(() => {
"use strict";

const $ = id => document.getElementById(id);
const svg = $("board");

const side = 460;
const h = side * Math.sqrt(3) / 2;

const V = {
  A: {x:430,y:95},
  B: {x:200,y:95+h},
  C: {x:660,y:95+h}
};

const P = {
  inside:  {x:430,y:290},
  outside: {x:730,y:290}
};

const triangles = {
  ABP: {
    names:["A","B","P"],
    centers:{
      A:{from:"B",to:"C",angle:-60,equality:"AP = AP′,BP = CP′"},
      B:{from:"A",to:"C",angle:60,equality:"BP = BP′,AP = CP′"}
    }
  },
  BPC: {
    names:["B","P","C"],
    centers:{
      B:{from:"C",to:"A",angle:-60,equality:"BP = BP′,CP = AP′"},
      C:{from:"B",to:"A",angle:60,equality:"CP = CP′,BP = AP′"}
    }
  },
  CPA: {
    names:["C","P","A"],
    centers:{
      C:{from:"A",to:"B",angle:-60,equality:"CP = CP′,AP = BP′"},
      A:{from:"C",to:"B",angle:60,equality:"AP = AP′,CP = BP′"}
    }
  }
};

const triangleColors = {
  ABP:"#60a5fa",
  BPC:"#34d399",
  CPA:"#c4b5fd"
};

let position = "inside";
let chosen = null;
let center = null;

let raf = 0;
let progress = 0;
let angleNow = 0;
let startTime = 0;
let paused = false;
let finished = false;
const duration = 1600;

const fmt = n => Number(n.toFixed(2));
const xy = p => `${fmt(p.x)},${fmt(p.y)}`;

function rotatePoint(p,o,degrees){
  const r = degrees*Math.PI/180;
  const x = p.x-o.x;
  const y = p.y-o.y;

  return {
    x:o.x+x*Math.cos(r)-y*Math.sin(r),
    y:o.y+x*Math.sin(r)+y*Math.cos(r)
  };
}

function poly(points,fill,stroke,width=2,extra=""){
  return `<polygon points="${points.map(xy).join(" ")}"
    fill="${fill}" stroke="${stroke}" stroke-width="${width}"
    stroke-linejoin="round" ${extra}/>`;
}

function line(a,b,color,width=2,dash=""){
  return `<line x1="${fmt(a.x)}" y1="${fmt(a.y)}"
    x2="${fmt(b.x)}" y2="${fmt(b.y)}"
    stroke="${color}" stroke-width="${width}"
    ${dash?`stroke-dasharray="${dash}"`:""}/>`;
}

function dot(p,r,color,extra=""){
  return `<circle cx="${fmt(p.x)}" cy="${fmt(p.y)}"
    r="${r}" fill="${color}" ${extra}/>`;
}

function text(p,s,color="#1e293b",size=20,anchor="middle",extra=""){
  return `<text x="${fmt(p.x)}" y="${fmt(p.y)}"
    fill="${color}" font-size="${size}" font-weight="700"
    text-anchor="${anchor}" paint-order="stroke"
    stroke="white" stroke-width="4" stroke-linejoin="round"
    ${extra}>${s}</text>`;
}

function vertexLabel(name,p){
  const offsets={
    A:{x:0,y:-19,anchor:"middle"},
    B:{x:-14,y:24,anchor:"end"},
    C:{x:15,y:24,anchor:"start"}
  };
  const o=offsets[name];
  return text(
    {x:p.x+o.x,y:p.y+o.y},
    name,"#1e293b",22,o.anchor
  );
}

function arcMarkup(o,p,angle){
  const p2=rotatePoint(p,o,angle);
  const radius=38;
  const a1=Math.atan2(p.y-o.y,p.x-o.x);
  const a2=Math.atan2(p2.y-o.y,p2.x-o.x);

  const s={
    x:o.x+radius*Math.cos(a1),
    y:o.y+radius*Math.sin(a1)
  };
  const e={
    x:o.x+radius*Math.cos(a2),
    y:o.y+radius*Math.sin(a2)
  };
  const mid=a1+angle*Math.PI/360;
  const labelP={
    x:o.x+54*Math.cos(mid),
    y:o.y+54*Math.sin(mid)
  };

  return `
    <path d="M ${xy(s)} A ${radius} ${radius} 0 0
      ${angle>0?1:0} ${xy(e)}"
      fill="none" stroke="#0f766e" stroke-width="3"
      marker-end="url(#arrow)"/>
    ${text(labelP,"60°","#0f766e",15)}
  `;
}

function cancelAnimation(){
  if(raf) cancelAnimationFrame(raf);
  raf=0;
  progress=0;
  angleNow=0;
  paused=false;
  finished=false;
}

function resetAll(){
  cancelAnimation();
  chosen=null;
  center=null;
  render();
  setStatus("请点击图中的一个小三角形。");
}

function setStatus(message,success=false){
  $("status").textContent=message;
  $("status").className=success?"ok":"";
}

function centroid(points){
  return {
    x:points.reduce((sum,p)=>sum+p.x,0)/points.length,
    y:points.reduce((sum,p)=>sum+p.y,0)/points.length
  };
}

function triangleLabel(key,pts){
  const c=centroid(pts);
  const w=76, h=31;
  const selected=chosen===key;

  return `
    <g class="triangle-label" data-triangle="${key}"
       role="button" tabindex="0"
       aria-label="选择三角形 ${key}">
      <rect x="${fmt(c.x-w/2)}" y="${fmt(c.y-h/2)}"
        width="${w}" height="${h}" rx="9"
        fill="${selected?"#2563eb":"white"}"
        fill-opacity="${selected?1:0.94}"
        stroke="${selected?"#1d4ed8":"#94a3b8"}"
        stroke-width="${selected?3:1.5}"/>
      <text x="${fmt(c.x)}" y="${fmt(c.y+6)}"
        font-size="17" font-weight="700"
        text-anchor="middle"
        fill="${selected?"white":"#334155"}"
        stroke="none">△${key}</text>
    </g>
  `;
}

function vertexHandle(name,p){
  const allowed=chosen && triangles[chosen].centers[name];
  const isCenter=center===name;
  const active=allowed && !center;
  const fill=isCenter?"#0f766e":active?"#10b981":"#64748b";
  const opacity=chosen && !allowed?0.65:1;

  return `
    <g class="vertex-hit" data-vertex="${name}"
       role="button" tabindex="0"
       aria-label="选择 ${name} 为旋转中心"
       style="cursor:pointer;opacity:${opacity}">
      <circle cx="${p.x}" cy="${p.y}" r="25"
        fill="transparent"/>
      <circle cx="${p.x}" cy="${p.y}" r="${isCenter?11:active?10:7}"
        fill="${fill}" stroke="white" stroke-width="2"/>
      ${active?`<circle cx="${p.x}" cy="${p.y}" r="17"
        fill="none" stroke="#10b981" stroke-width="2"
        stroke-dasharray="4 3"/>`:""}
      ${vertexLabel(name,p)}
    </g>
  `;
}

function render(){
  const p=P[position];
  const keys=["ABP","BPC","CPA"];

  let out=`
    <defs>
      <marker id="arrow" markerWidth="9" markerHeight="9"
        refX="7" refY="4" orient="auto"
        markerUnits="userSpaceOnUse">
        <path d="M0,0 L8,4 L0,8 Z" fill="#0f766e"/>
      </marker>
    </defs>

    <rect width="900" height="690" fill="white"/>

    <text x="22" y="30" font-size="18" font-weight="700" fill="#334155">
      ${position==="inside"?"P 在 △ABC 内部":"P 在 △ABC 外部"}
    </text>
    <text x="22" y="55" font-size="14" fill="#64748b">
      ${chosen?"已选 △"+chosen+":请点击高亮顶点作为旋转中心":"点击一个小三角形开始"}
    </text>

    ${poly([V.A,V.B,V.C],"#f8fafc","#64748b",2.5)}

    ${line(V.A,p,"#94a3b8",2)}
    ${line(V.B,p,"#94a3b8",2)}
    ${line(V.C,p,"#94a3b8",2)}
  `;

  // 原始的三个小三角形。
  for(const key of keys){
    const pts=triangles[key].names.map(n=>n==="P"?p:V[n]);
    const isSelected=chosen===key;
    out+=poly(
      pts,
      triangleColors[key],
      isSelected?"#2563eb":"#64748b",
      isSelected?3.5:1.7,
      `fill-opacity="${isSelected?0.48:0.20}"`
    );
  }

  // 透明的可点击区域。外部点时三角形可能重叠,
  // 因此也提供每个三角形的独立标签作为精准点击目标。
  for(const key of keys){
    const pts=triangles[key].names.map(n=>n==="P"?p:V[n]);
    out+=`
      <polygon class="hit-area" data-triangle="${key}"
        points="${pts.map(xy).join(" ")}"
        fill="transparent" stroke="transparent"
        stroke-width="8"/>
    `;
  }

  // 标签位于可点击区域上方,方便希沃笔精确选取。
  for(const key of keys){
    const pts=triangles[key].names.map(n=>n==="P"?p:V[n]);
    out+=triangleLabel(key,pts);
  }

  // 选定中心后,绘制旋转弧线和旋转后的三角形。
  if(chosen && center){
    const cfg=triangles[chosen].centers[center];
    const o=V[center];
    const f=V[cfg.from];

    out+=arcMarkup(o,p,cfg.angle);

    out+=`
      <g id="rotatedGroup"
         transform="rotate(${angleNow} ${o.x} ${o.y})">
        ${poly([o,f,p],"#fb923c","#ea580c",3,"stroke-dasharray='9 6' fill-opacity='0.20'")}
      </g>
      <circle id="primeDot" cx="${p.x}" cy="${p.y}" r="7"
        fill="#ea580c" stroke="white" stroke-width="2"
        visibility="hidden"/>
      <text id="primeLabel" x="${p.x+12}" y="${p.y-12}"
        font-size="21" font-weight="700" fill="#c2410c"
        paint-order="stroke" stroke="white" stroke-width="4"
        visibility="hidden">P′</text>
    `;
  }

  // 原点 P 保持可见。
  out+=dot(p,7,"#dc2626",'stroke="white" stroke-width="2"');
  out+=text({x:p.x+13,y:p.y-13},"P","#dc2626",21,"start");

  // 顶点交互层放在最上面,便于直接点击。
  for(const name of ["A","B","C"]){
    out+=vertexHandle(name,V[name]);
  }

  out+=`
    <text x="22" y="650" font-size="15" fill="#64748b">
      ${chosen && !center
        ?"绿色虚线圆圈:可选择的旋转中心"
        :center
          ?"绿色顶点:"+center+"(旋转中心保持不动)"
          :"A、B、C 为原三角形的三个顶点"}
    </text>
  `;

  svg.innerHTML=out;

  // 绑定三角形点击事件。
  svg.querySelectorAll("[data-triangle]").forEach(el=>{
    el.addEventListener("click",e=>{
      e.stopPropagation();
      selectTriangle(el.dataset.triangle);
    });
    el.addEventListener("keydown",e=>{
      if(e.key==="Enter"||e.key===" "){
        e.preventDefault();
        e.stopPropagation();
        selectTriangle(el.dataset.triangle);
      }
    });
  });

  // 绑定顶点点击事件。
  svg.querySelectorAll("[data-vertex]").forEach(el=>{
    el.addEventListener("click",e=>{
      e.stopPropagation();
      selectCenter(el.dataset.vertex);
    });
    el.addEventListener("keydown",e=>{
      if(e.key==="Enter"||e.key===" "){
        e.preventDefault();
        e.stopPropagation();
        selectCenter(el.dataset.vertex);
      }
    });
  });

  updateInfo();
}

function selectTriangle(key){
  cancelAnimation();
  chosen=key;
  center=null;
  render();

  const allowed=Object.keys(triangles[key].centers).join("、");
  setStatus(`已选中 △${key}。现在请直接点击顶点 ${allowed} 中的一个作为旋转中心。`);
}

function selectCenter(name){
  if(!chosen){
    setStatus("请先点击一个小三角形,再选择旋转中心。");
    return;
  }

  const cfg=triangles[chosen].centers[name];

  if(!cfg){
    const allowed=Object.keys(triangles[chosen].centers).join("、");
    setStatus(`△${chosen} 只能选择 ${allowed} 作为旋转中心,请点击绿色标记的顶点。`);
    return;
  }

  cancelAnimation();
  center=name;
  render();

  setStatus(
    `开始旋转:△${chosen} 绕 ${center} ${cfg.angle>0?"顺时针":"逆时针"}旋转 60°,${cfg.from} 将与 ${cfg.to} 重合。`
  );

  startAnimation();
}

function updateInfo(){
  $("chosenTriangle").textContent=chosen?"△"+chosen:"尚未选择";
  $("chosenCenter").textContent=center||"尚未选择";

  if(chosen && center){
    const cfg=triangles[chosen].centers[center];
    $("chosenDirection").textContent=
      `${cfg.angle>0?"顺时针":"逆时针"} 60°`;
    $("equalities").textContent=cfg.equality;
    $("explain").textContent=
      `旋转前的顶点 ${cfg.from} 与旋转后的顶点 ${cfg.to} 重合;中心 ${center} 不动,P 变为 P′。`;
  }else if(chosen){
    $("chosenDirection").textContent="等待选择";
    $("equalities").textContent="请先选旋转中心。";
    $("explain").textContent=
      `△${chosen} 可选的旋转中心为 ${Object.keys(triangles[chosen].centers).join("、")}。`;
  }else{
    $("chosenDirection").textContent="—";
    $("equalities").textContent="对应线段关系将在这里显示。";
    $("explain").textContent="选好三角形和顶点后,自动播放旋转动画。";
  }

  $("centerHelp").textContent=chosen
    ?`已选 △${chosen}。请点击绿色标记的 ${Object.keys(triangles[chosen].centers).join(" 或 ")}。`
    :"选择小三角形后,可以点击该三角形包含的两个原顶点作为旋转中心。点 P 不是旋转中心。";

  $("pause").disabled=!center;
  $("replay").disabled=!center;
}

function updateAnimation(){
  const cfg=triangles[chosen].centers[center];
  const o=V[center];
  const p=P[position];

  const group=$("rotatedGroup");
  const primeDot=$("primeDot");
  const primeLabel=$("primeLabel");

  if(!group||!primeDot||!primeLabel)return;

  group.setAttribute(
    "transform",`rotate(${angleNow} ${o.x} ${o.y})`
  );

  const q=rotatePoint(p,o,angleNow);
  primeDot.setAttribute("cx",q.x);
  primeDot.setAttribute("cy",q.y);
  primeDot.setAttribute(
    "visibility",progress>0.03||finished?"visible":"hidden"
  );

  const dx=q.x>790?-14:13;
  primeLabel.setAttribute("x",q.x+dx);
  primeLabel.setAttribute("y",q.y<75?q.y+24:q.y-13);
  primeLabel.setAttribute(
    "text-anchor",dx<0?"end":"start"
  );
  primeLabel.setAttribute(
    "visibility",progress>0.03||finished?"visible":"hidden"
  );
}

function startAnimation(){
  if(!chosen||!center)return;

  if(raf)cancelAnimationFrame(raf);
  raf=0;
  progress=0;
  angleNow=0;
  paused=false;
  finished=false;
  startTime=performance.now();

  $("pause").textContent="暂停动画";
  updateAnimation();

  raf=requestAnimationFrame(tick);
}

function tick(now){
  progress=Math.min(1,(now-startTime)/duration);

  // 缓入缓出,旋转更容易在大屏幕上观察。
  const eased=0.5-0.5*Math.cos(Math.PI*progress);
  const cfg=triangles[chosen].centers[center];

  angleNow=cfg.angle*eased;
  updateAnimation();

  if(progress<1){
    raf=requestAnimationFrame(tick);
  }else{
    raf=0;
    finished=true;
    angleNow=cfg.angle;
    updateAnimation();
    $("pause").textContent="继续 / 重播";
    setStatus(
      `旋转完成!△${chosen} 已绕 ${center} 旋转 60°,${cfg.from} 与 ${cfg.to} 重合。`,
      true
    );
  }
}

$("pause").addEventListener("click",()=>{
  if(!center)return;

  if(finished){
    startAnimation();
    return;
  }

  if(!paused){
    paused=true;
    if(raf)cancelAnimationFrame(raf);
    raf=0;
    $("pause").textContent="继续动画";
    setStatus("动画已暂停。点击“继续动画”即可接着播放。");
  }else{
    paused=false;
    startTime=performance.now()-progress*duration;
    $("pause").textContent="暂停动画";
    raf=requestAnimationFrame(tick);
  }
});

$("replay").addEventListener("click",()=>{
  if(center)startAnimation();
});

$("clear").addEventListener("click",resetAll);

document.querySelectorAll("[data-position]").forEach(btn=>{
  btn.addEventListener("click",()=>{
    position=btn.dataset.position;
    document.querySelectorAll("[data-position]").forEach(b=>{
      b.classList.toggle("active",b===btn);
    });
    resetAll();
  });
});

render();
})();
</script>
</body>
</html>