SOURCE

console 命令行工具 X clear

                    
>
console
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<title>从第28名到第1名 - 四年高科技版</title>
<style>
  html, body {
    margin: 0; padding: 0;
    background: #03060d;
    overflow: hidden;
  }
  body {
    display: flex; align-items: center; justify-content: center;
    height: 100vh;
  }
  canvas {
    width: 100vw; height: auto; max-height: 100vh;
    display: block; background: #03060d;
  }
</style>
</head>
<body>
<canvas id="stage" width="1920" height="1080"></canvas>
<script>
const canvas = document.getElementById('stage');
const ctx = canvas.getContext('2d');
const W = 1920, H = 1080;

// ================= 数据 =================
const data = [
  { label: '入学考',        date: '2022.09', rank: 28, classRank: 3 },
  { label: '初一上期中',    date: '2022.11', rank: 46, classRank: 3 },
  { label: '初一上期末',    date: '2023.01', rank: 22, classRank: 1 },
  { label: '初一下期中',    date: '2023.05', rank: 11, classRank: 1 },
  { label: '初一下期末',    date: '2023.07', rank: 2,  classRank: 1 },
  { label: '初二上期中',    date: '2023.11', rank: 1,  classRank: 1 },
  { label: '初二上期末',    date: '2024.01', rank: 7,  classRank: 1 },
  { label: '初二下三月',    date: '2024.04', rank: 9,  classRank: 1 },
  { label: '初二下期中',    date: '2024.05', rank: 1,  classRank: 1 },
  { label: '初二下期末',    date: '2024.07', rank: 3,  classRank: 1 },
  { label: '初三上九月',    date: '2024.09', rank: 2,  classRank: 1 },
  { label: '初三上期中',    date: '2024.11', rank: 1,  classRank: 1 },
  { label: '初三上期末',    date: '2025.01', rank: 2,  classRank: 1 },
  { label: '初三下三月',    date: '2025.04', rank: 1,  classRank: 1 },
  { label: '初三下期中',    date: '2025.05', rank: 1,  classRank: 1 },
  { label: '初三下期末',    date: '2025.07', rank: 1,  classRank: 1 },
  { label: '初四九月月考',  date: '2025.10', rank: 2,  classRank: 1 },
  { label: '初四统考',      date: '2025.11', rank: 1,  classRank: 1 },
  { label: '12月月考',      date: '2025.12', rank: 1,  classRank: 1 },
  { label: '初四下期末',    date: '2026.01', rank: 3,  classRank: 1 },
  { label: '三月份月考',    date: '2026.03', rank: 3,  classRank: 1 },
  { label: '南岗一模',      date: '2026.03', rank: 16, classRank: 1 },
  { label: '南岗二模',      date: '2026.04', rank: 21, classRank: 2 },
  { label: '南岗三模',      date: '2026.06', rank: 13, classRank: 1 },
  { label: '中考',          date: '2026.06', rank: 1,  classRank: 1 }
];

const timeMarks = [
  { i: 0,  text: '2022.09' },
  { i: 2,  text: '2023.01' },
  { i: 4,  text: '2023.07' },
  { i: 6,  text: '2024.01' },
  { i: 9,  text: '2024.07' },
  { i: 12, text: '2025.01' },
  { i: 15, text: '2025.07' },
  { i: 19, text: '2026.01' },
  { i: 24, text: '2026.06' }
];

const TOTAL = 60;
const CURVE_START = 2;
const CURVE_END = 38;
const CARD_START = 38;
const FINAL_START = 43;

const LEFT = 170, RIGHT = 1750;
const TOP = 250, BOTTOM = 860;
const CHART_W = RIGHT - LEFT;
const CHART_H = BOTTOM - TOP;

// ================= 工具 =================
function clamp01(x){ return Math.max(0, Math.min(1, x)); }
function easeOutCubic(x){ return 1 - Math.pow(1 - x, 3); }
function easeOutBack(x){
  const c1 = 1.70158, c3 = c1 + 1;
  return 1 + c3 * Math.pow(x - 1, 3) + c1 * Math.pow(x - 1, 2);
}
function roundRect(x, y, w, h, r){
  ctx.beginPath();
  ctx.moveTo(x + r, y);
  ctx.arcTo(x + w, y, x + w, y + h, r);
  ctx.arcTo(x + w, y + h, x, y + h, r);
  ctx.arcTo(x, y + h, x, y, r);
  ctx.arcTo(x, y, x + w, y, r);
  ctx.closePath();
}
function rankToY(rank){
  const r = Math.max(1, Math.min(50, rank));
  return TOP + ((r - 1) / 49) * CHART_H;
}
function classRankToY(rank){
  const r = Math.max(1, Math.min(3, rank));
  const top = TOP + 40, bottom = TOP + 150;
  return top + ((r - 1) / 2) * (bottom - top);
}
function indexToX(i){ return LEFT + (i / (data.length - 1)) * CHART_W; }
function getPos(t){
  const p = clamp01((t - CURVE_START) / (CURVE_END - CURVE_START));
  return p * (data.length - 1);
}

// ================= 背景 =================
const stars = [];
for (let i = 0; i < 240; i++) {
  stars.push({
    x: Math.random() * W,
    y: Math.random() * H,
    r: Math.random() * 2.2 + 0.3,
    speed: Math.random() * 12 + 2,
    phase: Math.random() * Math.PI * 2,
    amp: Math.random() * 28 + 5,
    hue: Math.random() < 0.7 ? 200 : 40
  });
}

const nebulas = [
  { x: 380,  y: 280, r: 520, hue: 200, speed: 0.13, phase: 0 },
  { x: 1540, y: 740, r: 600, hue: 215, speed: 0.09, phase: 2 },
  { x: 960,  y: 180, r: 460, hue: 45,  speed: 0.07, phase: 4 }
];

function drawNebula(t){
  ctx.save();
  ctx.globalCompositeOperation = 'screen';
  for (const n of nebulas) {
    const cx = n.x + Math.sin(t * n.speed + n.phase) * 90;
    const cy = n.y + Math.cos(t * n.speed * 0.8 + n.phase) * 60;
    const g = ctx.createRadialGradient(cx, cy, 20, cx, cy, n.r);
    if (n.hue === 45) {
      g.addColorStop(0, 'rgba(251,191,36,0.07)');
      g.addColorStop(0.5, 'rgba(251,191,36,0.022)');
      g.addColorStop(1, 'rgba(251,191,36,0)');
    } else {
      g.addColorStop(0, 'rgba(56,189,248,0.085)');
      g.addColorStop(0.5, 'rgba(56,189,248,0.028)');
      g.addColorStop(1, 'rgba(56,189,248,0)');
    }
    ctx.fillStyle = g;
    ctx.fillRect(0, 0, W, H);
  }
  ctx.restore();
}

function drawStars(t){
  for (const s of stars) {
    const x = (s.x + t * s.speed) % W;
    const y = s.y + Math.sin(t * 0.6 + s.phase) * s.amp;
    const twinkle = 0.5 + 0.5 * Math.sin(t * 2.2 + s.phase);
    const a = 0.12 + 0.28 * twinkle;
    ctx.beginPath();
    ctx.arc(x, y, s.r, 0, Math.PI * 2);
    ctx.fillStyle = s.hue === 45
      ? `rgba(253,224,140,${a})`
      : `rgba(147,197,253,${a})`;
    ctx.fill();
  }
}

function drawDotMatrix(t){
  ctx.save();
  const spacing = 64;
  for (let row = 0; row * spacing < H + spacing; row++) {
    for (let col = 0; col * spacing < W + spacing; col++) {
      const x = col * spacing + (row % 2) * spacing / 2;
      const y = row * spacing;
      const phase = (x + y) * 0.006 + t * 0.5;
      const a = 0.035 + 0.055 * (0.5 + 0.5 * Math.sin(phase));
      ctx.beginPath();
      ctx.arc(x, y, 1.4, 0, Math.PI * 2);
      ctx.fillStyle = `rgba(56,189,248,${a})`;
      ctx.fill();
    }
  }
  ctx.restore();
}

function drawRadar(t){
  ctx.save();
  ctx.strokeStyle = 'rgba(56,189,248,0.05)';
  ctx.lineWidth = 1;
  for (let r = 200; r <= 800; r += 150) {
    ctx.beginPath();
    ctx.arc(960, 540, r, 0, Math.PI * 2);
    ctx.stroke();
  }
  const ang = t * 0.35;
  const g = ctx.createLinearGradient(960, 540,
    960 + Math.cos(ang) * 820, 540 + Math.sin(ang) * 820);
  g.addColorStop(0, 'rgba(56,189,248,0.10)');
  g.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.strokeStyle = g;
  ctx.lineWidth = 2;
  ctx.beginPath();
  ctx.moveTo(960, 540);
  ctx.lineTo(960 + Math.cos(ang) * 820, 540 + Math.sin(ang) * 820);
  ctx.stroke();
  ctx.restore();
}

function drawPerspectiveGrid(t){
  const horizon = 800;
  ctx.save();
  ctx.globalAlpha = 0.14;
  ctx.strokeStyle = 'rgba(56,189,248,0.7)';
  ctx.lineWidth = 1.1;

  for (let i = 0; i < 16; i++) {
    const p = ((i + (t * 0.22) % 1) / 16);
    const y = horizon + Math.pow(p, 2.2) * (H - horizon + 220);
    if (y > H + 50) continue;
    ctx.beginPath();
    ctx.moveTo(0, y);
    ctx.lineTo(W, y);
    ctx.stroke();
  }

  const vpX = W / 2;
  for (let i = -12; i <= 12; i++) {
    ctx.beginPath();
    ctx.moveTo(vpX + i * 26, horizon);
    ctx.lineTo(vpX + i * 220, H + 60);
    ctx.stroke();
  }
  ctx.restore();
}

function drawBackground(t){
  const bg = ctx.createLinearGradient(0, 0, 0, H);
  bg.addColorStop(0, '#03060d');
  bg.addColorStop(0.5, '#060b17');
  bg.addColorStop(1, '#0a1324');
  ctx.fillStyle = bg;
  ctx.fillRect(0, 0, W, H);

  drawDotMatrix(t);
  drawNebula(t);
  drawStars(t);
  drawRadar(t);
  drawPerspectiveGrid(t);

  const vg = ctx.createRadialGradient(W/2, H/2, 220, W/2, H/2, 1100);
  vg.addColorStop(0, 'rgba(0,0,0,0)');
  vg.addColorStop(1, 'rgba(0,0,0,0.55)');
  ctx.fillStyle = vg;
  ctx.fillRect(0, 0, W, H);
}

// ================= HUD =================
function drawHUD(t){
  const m = 28, len = 52;
  ctx.save();
  ctx.strokeStyle = 'rgba(56,189,248,0.45)';
  ctx.lineWidth = 2;
  ctx.shadowColor = 'rgba(56,189,248,0.5)';
  ctx.shadowBlur = 10;

  const corners = [
    [m, m + len, m, m, m + len, m],
    [W - m - len, m, W - m, m, W - m, m + len],
    [m, H - m - len, m, H - m, m + len, H - m],
    [W - m - len, H - m, W - m, H - m, W - m, H - m - len]
  ];
  corners.forEach(c => {
    ctx.beginPath();
    ctx.moveTo(c[0], c[1]);
    ctx.lineTo(c[2], c[3]);
    ctx.lineTo(c[4], c[5]);
    ctx.stroke();
  });
  ctx.restore();

  ctx.save();
  ctx.font = '19px "Consolas", "Microsoft YaHei", monospace';
  const blink = Math.sin(t * 4) > 0;

  ctx.fillStyle = blink ? '#4ade80' : '#14532d';
  ctx.shadowColor = '#4ade80';
  ctx.shadowBlur = blink ? 12 : 0;
  ctx.beginPath();
  ctx.arc(92, 42, 5, 0, Math.PI * 2);
  ctx.fill();
  ctx.shadowBlur = 0;

  ctx.fillStyle = 'rgba(148,197,253,0.78)';
  ctx.textAlign = 'left';
  ctx.fillText('SYSTEM ONLINE', 110, 48);

  const tc = `T+${String(Math.floor(t)).padStart(2,'0')}:${String(Math.floor((t % 1) * 60)).padStart(2,'0')}`;
  ctx.textAlign = 'right';
  ctx.fillText(tc, W - 92, 48);

  ctx.textAlign = 'left';
  ctx.fillText('DATA STREAM ACTIVE', 92, H - 36);
  ctx.textAlign = 'right';
  ctx.fillText('RANK TRACKER v4.0', W - 92, H - 36);
  ctx.restore();
}

// ================= 标题 =================
function drawTitle(t){
  const a = clamp01(t / 1.3);
  ctx.save();
  ctx.globalAlpha = a;
  ctx.textAlign = 'center';

  const bandW = 800 * easeOutCubic(a);
  const bandGrad = ctx.createLinearGradient(W/2 - bandW, 0, W/2 + bandW, 0);
  bandGrad.addColorStop(0, 'rgba(56,189,248,0)');
  bandGrad.addColorStop(0.5, 'rgba(56,189,248,0.10)');
  bandGrad.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.fillStyle = bandGrad;
  ctx.fillRect(W/2 - bandW, 42, bandW * 2, 116);

  const grad = ctx.createLinearGradient(W/2 - 420, 0, W/2 + 420, 0);
  grad.addColorStop(0, '#7dd3fc');
  grad.addColorStop(0.45, '#e0f2fe');
  grad.addColorStop(0.55, '#fef3c7');
  grad.addColorStop(1, '#fbbf24');

  ctx.shadowColor = 'rgba(56,189,248,0.7)';
  ctx.shadowBlur = 30;
  ctx.fillStyle = grad;
  ctx.font = 'bold 64px "Microsoft YaHei", "PingFang SC", sans-serif';
  ctx.fillText('从第28名到第1名', W / 2, 112);

  ctx.shadowBlur = 0;
  ctx.fillStyle = '#94a3b8';
  ctx.font = '32px "Microsoft YaHei", "PingFang SC", sans-serif';
  ctx.fillText('一条用四年画出的上升曲线', W / 2, 168);

  const lineW = 560 * easeOutCubic(a);
  const lg = ctx.createLinearGradient(W/2 - lineW, 0, W/2 + lineW, 0);
  lg.addColorStop(0, 'rgba(56,189,248,0)');
  lg.addColorStop(0.5, 'rgba(56,189,248,0.75)');
  lg.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.fillStyle = lg;
  ctx.fillRect(W/2 - lineW, 186, lineW * 2, 2);
  ctx.restore();
}

// ================= 网格 + 横轴 =================
function drawGrid(t){
  ctx.save();

  ctx.strokeStyle = 'rgba(148,163,184,0.10)';
  ctx.lineWidth = 1.2;
  ctx.fillStyle = 'rgba(148,163,184,0.58)';
  ctx.font = '22px "Consolas", "Microsoft YaHei", monospace';
  ctx.textAlign = 'right';
  ctx.textBaseline = 'middle';

  [1, 10, 20, 30, 40, 50].forEach(r => {
    const y = rankToY(r);

    if (r === 1) {
      const g = ctx.createLinearGradient(LEFT, 0, RIGHT, 0);
      g.addColorStop(0, 'rgba(251,191,36,0)');
      g.addColorStop(0.12, 'rgba(251,191,36,0.80)');
      g.addColorStop(0.5, 'rgba(254,243,199,1)');
      g.addColorStop(0.88, 'rgba(251,191,36,0.80)');
      g.addColorStop(1, 'rgba(251,191,36,0)');

      ctx.save();
      ctx.strokeStyle = 'rgba(251,191,36,0.30)';
      ctx.lineWidth = 10;
      ctx.shadowColor = '#fbbf24';
      ctx.shadowBlur = 26;
      ctx.beginPath();
      ctx.moveTo(LEFT, y);
      ctx.lineTo(RIGHT, y);
      ctx.stroke();
      ctx.restore();

      ctx.save();
      ctx.strokeStyle = g;
      ctx.lineWidth = 3;
      ctx.shadowColor = '#fbbf24';
      ctx.shadowBlur = 16;
      ctx.beginPath();
      ctx.moveTo(LEFT, y);
      ctx.lineTo(RIGHT, y);
      ctx.stroke();
      ctx.restore();

      ctx.strokeStyle = 'rgba(251,191,36,0.9)';
      ctx.lineWidth = 3;
      ctx.beginPath();
      ctx.moveTo(LEFT - 14, y);
      ctx.lineTo(LEFT, y);
      ctx.stroke();

      ctx.save();
      ctx.fillStyle = '#fbbf24';
      ctx.shadowColor = '#fbbf24';
      ctx.shadowBlur = 14;
      ctx.font = 'bold 23px "Consolas", "Microsoft YaHei", monospace';
      ctx.fillText('第1名', LEFT - 24, y);
      ctx.restore();

      ctx.save();
      ctx.fillStyle = '#fbbf24';
      ctx.shadowColor = '#fbbf24';
      ctx.shadowBlur = 10;
      ctx.beginPath();
      ctx.moveTo(LEFT - 22, y - 7);
      ctx.lineTo(LEFT - 14, y);
      ctx.lineTo(LEFT - 22, y + 7);
      ctx.closePath();
      ctx.fill();
      ctx.restore();

      return;
    }

    ctx.strokeStyle = 'rgba(148,163,184,0.10)';
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.moveTo(LEFT, y);
    ctx.lineTo(RIGHT, y);
    ctx.stroke();

    ctx.strokeStyle = 'rgba(56,189,248,0.35)';
    ctx.beginPath();
    ctx.moveTo(LEFT - 10, y);
    ctx.lineTo(LEFT, y);
    ctx.stroke();

    ctx.fillStyle = 'rgba(148,163,184,0.58)';
    ctx.fillText('第' + r + '名', LEFT - 24, y);
  });

  for (let i = 0; i < data.length; i++) {
    const x = indexToX(i);
    ctx.strokeStyle = 'rgba(56,189,248,0.045)';
    ctx.beginPath();
    ctx.moveTo(x, TOP);
    ctx.lineTo(x, BOTTOM);
    ctx.stroke();
  }

  const scanY = TOP + ((Math.sin(t * 0.7) * 0.5 + 0.5) * CHART_H);
  const scanGrad = ctx.createLinearGradient(LEFT, scanY - 24, LEFT, scanY + 24);
  scanGrad.addColorStop(0, 'rgba(56,189,248,0)');
  scanGrad.addColorStop(0.5, 'rgba(56,189,248,0.12)');
  scanGrad.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.fillStyle = scanGrad;
  ctx.fillRect(LEFT, scanY - 24, CHART_W, 48);

  ctx.strokeStyle = 'rgba(56,189,248,0.32)';
  ctx.lineWidth = 1.5;
  ctx.beginPath();
  ctx.moveTo(LEFT, TOP);
  ctx.lineTo(LEFT, BOTTOM);
  ctx.stroke();

  ctx.strokeStyle = 'rgba(56,189,248,0.38)';
  ctx.beginPath();
  ctx.moveTo(LEFT, BOTTOM);
  ctx.lineTo(RIGHT, BOTTOM);
  ctx.stroke();

  for (let i = 0; i < data.length; i++) {
    const x = indexToX(i);
    ctx.strokeStyle = 'rgba(56,189,248,0.30)';
    ctx.lineWidth = 1.2;
    ctx.beginPath();
    ctx.moveTo(x, BOTTOM);
    ctx.lineTo(x, BOTTOM + 7);
    ctx.stroke();
  }

  ctx.textAlign = 'center';
  ctx.textBaseline = 'alphabetic';
  timeMarks.forEach(m => {
    const x = indexToX(m.i);

    ctx.strokeStyle = 'rgba(56,189,248,0.75)';
    ctx.lineWidth = 2;
    ctx.shadowColor = 'rgba(56,189,248,0.6)';
    ctx.shadowBlur = 8;
    ctx.beginPath();
    ctx.moveTo(x, BOTTOM);
    ctx.lineTo(x, BOTTOM + 16);
    ctx.stroke();
    ctx.shadowBlur = 0;

    ctx.fillStyle = 'rgba(165,215,255,0.92)';
    ctx.font = 'bold 21px "Consolas", monospace';
    ctx.shadowColor = 'rgba(56,189,248,0.5)';
    ctx.shadowBlur = 10;
    ctx.fillText(m.text, x, BOTTOM + 46);
    ctx.shadowBlur = 0;
  });

  ctx.textAlign = 'left';
  ctx.textBaseline = 'middle';
  ctx.font = '21px "Consolas", "Microsoft YaHei", monospace';
  [1, 2, 3].forEach(r => {
    const y = classRankToY(r);
    ctx.strokeStyle = 'rgba(251,191,36,0.22)';
    ctx.beginPath();
    ctx.moveTo(RIGHT, y);
    ctx.lineTo(RIGHT + 14, y);
    ctx.stroke();
    ctx.fillStyle = 'rgba(251,191,36,0.85)';
    ctx.fillText('班级第' + r, RIGHT + 22, y);
  });

  ctx.restore();
}

function drawTopLine(t){
  const y = rankToY(1);
  const appear = clamp01((t - 1.2) / 1.0);
  if (appear <= 0) return;

  ctx.save();
  ctx.globalAlpha = appear;

  const flow = (t * 0.45) % 1;
  const flowX = LEFT + flow * CHART_W;
  const flowGrad = ctx.createLinearGradient(flowX - 180, 0, flowX + 180, 0);
  flowGrad.addColorStop(0, 'rgba(251,191,36,0)');
  flowGrad.addColorStop(0.5, 'rgba(255,251,235,0.75)');
  flowGrad.addColorStop(1, 'rgba(251,191,36,0)');
  ctx.fillStyle = flowGrad;
  ctx.fillRect(flowX - 180, y - 2, 360, 4);

  for (let k = 0; k < 4; k++) {
    const p = ((t * 0.30 + k / 4) % 1);
    const px = LEFT + p * CHART_W;
    ctx.beginPath();
    ctx.arc(px, y, 3.5, 0, Math.PI * 2);
    ctx.fillStyle = 'rgba(255,251,235,0.95)';
    ctx.shadowColor = '#fbbf24';
    ctx.shadowBlur = 16;
    ctx.fill();
  }
  ctx.restore();
}

function getDrawnPoints(t){
  const pos = getPos(t);
  if (pos <= 0) return [];
  const fullIndex = Math.floor(pos);
  const frac = pos - fullIndex;
  const pts = [];
  for (let i = 0; i <= fullIndex; i++) {
    pts.push({ x: indexToX(i), y: rankToY(data[i].rank) });
  }
  if (fullIndex < data.length - 1 && frac > 0) {
    const x1 = indexToX(fullIndex), y1 = rankToY(data[fullIndex].rank);
    const x2 = indexToX(fullIndex + 1), y2 = rankToY(data[fullIndex + 1].rank);
    pts.push({ x: x1 + (x2 - x1) * frac, y: y1 + (y2 - y1) * frac });
  }
  return pts;
}

function pointsLength(pts){
  let len = 0;
  for (let i = 1; i < pts.length; i++) {
    len += Math.hypot(pts[i].x - pts[i-1].x, pts[i].y - pts[i-1].y);
  }
  return len;
}

function pointAtLength(pts, dist){
  if (pts.length < 2) return pts[0] || {x:0,y:0};
  let acc = 0;
  for (let i = 1; i < pts.length; i++) {
    const seg = Math.hypot(pts[i].x - pts[i-1].x, pts[i].y - pts[i-1].y);
    if (acc + seg >= dist) {
      const r = (dist - acc) / seg;
      return {
        x: pts[i-1].x + (pts[i].x - pts[i-1].x) * r,
        y: pts[i-1].y + (pts[i].y - pts[i-1].y) * r
      };
    }
    acc += seg;
  }
  return pts[pts.length - 1];
}

// ================= 班级曲线(大幅增强) =================
function drawClassCurve(t){
  const pos = getPos(t);
  if (pos <= 0) return;
  const fullIndex = Math.floor(pos);
  const frac = pos - fullIndex;
  const pts = [];
  for (let i = 0; i <= fullIndex; i++) {
    pts.push({ x: indexToX(i), y: classRankToY(data[i].classRank) });
  }
  if (fullIndex < data.length - 1 && frac > 0) {
    const x1 = indexToX(fullIndex), y1 = classRankToY(data[fullIndex].classRank);
    const x2 = indexToX(fullIndex + 1), y2 = classRankToY(data[fullIndex + 1].classRank);
    pts.push({ x: x1 + (x2 - x1) * frac, y: y1 + (y2 - y1) * frac });
  }

  // 阴影层
  ctx.save();
  ctx.setLineDash([24, 14]);
  ctx.strokeStyle = 'rgba(251,191,36,0.3)';
  ctx.lineWidth = 14;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.shadowColor = '#fbbf24';
  ctx.shadowBlur = 30;
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
  ctx.restore();

  // 主实心虚线
  ctx.save();
  ctx.setLineDash([24, 14]);
  ctx.strokeStyle = '#fbbf24';
  ctx.lineWidth = 7;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.shadowColor = '#fcd34d';
  ctx.shadowBlur = 20;
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
  ctx.restore();

  // 数据节点放大
  ctx.save();
  for (let i = 0; i <= fullIndex; i++) {
    const x = indexToX(i), y = classRankToY(data[i].classRank);
    ctx.beginPath();
    ctx.arc(x, y, 7.5, 0, Math.PI * 2);
    ctx.fillStyle = '#fbbf24';
    ctx.fill();
    ctx.strokeStyle = '#ffffff';
    ctx.lineWidth = 2.5;
    ctx.stroke();
  }
  ctx.restore();

  const totalLen = pointsLength(pts);
  if (totalLen > 10) {
    const count = 6;
    for (let k = 0; k < count; k++) {
      const ratio = ((t * 0.16 + k / count) % 1);
      const pt = pointAtLength(pts, ratio * totalLen);
      ctx.save();
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, 5, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(255,255,255,1)';
      ctx.shadowColor = '#fbbf24';
      ctx.shadowBlur = 20;
      ctx.fill();
      ctx.restore();
    }
  }
}

// ================= 菱形数据点 =================
function drawDiamond(x, y, size, fill, stroke){
  ctx.beginPath();
  ctx.moveTo(x, y - size);
  ctx.lineTo(x + size, y);
  ctx.lineTo(x, y + size);
  ctx.lineTo(x - size, y);
  ctx.closePath();
  ctx.fillStyle = fill;
  ctx.fill();
  ctx.strokeStyle = stroke;
  ctx.lineWidth = 2.5;
  ctx.stroke();
}

function drawElectricPulse(pts, t){
  if (pts.length < 2) return;
  const totalLen = pointsLength(pts);
  const pulseLen = 90;
  const pulsePos = ((t * 0.55) % 1) * (totalLen + pulseLen);
  const startD = Math.max(0, pulsePos - pulseLen);
  const endD = Math.min(totalLen, pulsePos);
  if (endD <= startD) return;

  ctx.save();
  ctx.strokeStyle = 'rgba(240,249,255,0.95)';
  ctx.lineWidth = 5;
  ctx.lineCap = 'round';
  ctx.shadowColor = '#7dd3fc';
  ctx.shadowBlur = 32;
  ctx.beginPath();
  const steps = 24;
  for (let s = 0; s <= steps; s++) {
    const d = startD + (endD - startD) * (s / steps);
    const pt = pointAtLength(pts, d);
    if (s === 0) ctx.moveTo(pt.x, pt.y);
    else ctx.lineTo(pt.x, pt.y);
  }
  ctx.stroke();
  ctx.restore();
}

const trailParticles = [];

function spawnTrail(x, y){
  for (let i = 0; i < 2; i++) {
    trailParticles.push({
      x: x + (Math.random() - 0.5) * 8,
      y: y + (Math.random() - 0.5) * 8,
      vx: (Math.random() - 0.5) * 40,
      vy: (Math.random() - 0.5) * 40 - 10,
      life: 0.9 + Math.random() * 0.5,
      age: 0,
      size: 2 + Math.random() * 3,
      hue: Math.random() < 0.3 ? 45 : 200
    });
  }
}

function updateTrail(dt){
  for (let i = trailParticles.length - 1; i >= 0; i--) {
    const p = trailParticles[i];
    p.age += dt;
    p.x += p.vx * dt;
    p.y += p.vy * dt;
    p.vy += 30 * dt;
    if (p.age > p.life) trailParticles.splice(i, 1);
  }
}

function drawTrail(){
  ctx.save();
  ctx.globalCompositeOperation = 'lighter';
  for (const p of trailParticles) {
    const a = 1 - p.age / p.life;
    ctx.beginPath();
    ctx.arc(p.x, p.y, p.size * a, 0, Math.PI * 2);
    ctx.fillStyle = p.hue === 45
      ? `rgba(253,224,140,${a * 0.8})`
      : `rgba(147,197,253,${a * 0.85})`;
    ctx.fill();
  }
  ctx.restore();
}

let lastHead = null;

// ================= 年级曲线(大幅增强) =================
function drawCurve(t, dt){
  const pts = getDrawnPoints(t);
  if (pts.length < 2) return;

  ctx.save();
  const areaGrad = ctx.createLinearGradient(0, TOP, 0, BOTTOM);
  areaGrad.addColorStop(0, 'rgba(56,189,248,0.22)');
  areaGrad.addColorStop(0.6, 'rgba(56,189,248,0.06)');
  areaGrad.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.lineTo(pts[pts.length - 1].x, BOTTOM);
  ctx.lineTo(pts[0].x, BOTTOM);
  ctx.closePath();
  ctx.fillStyle = areaGrad;
  ctx.fill();
  ctx.restore();

  // 超粗底板
  ctx.save();
  ctx.strokeStyle = 'rgba(56,189,248,0.20)';
  ctx.lineWidth = 24;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.shadowColor = 'rgba(56,189,248,0.8)';
  ctx.shadowBlur = 50;
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
  ctx.restore();

  // 主色层
  ctx.save();
  ctx.strokeStyle = '#38bdf8';
  ctx.lineWidth = 12;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.shadowColor = 'rgba(56,189,248,0.9)';
  ctx.shadowBlur = 30;
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
  ctx.restore();

  // 纯白高光层
  ctx.save();
  ctx.strokeStyle = 'rgba(240,249,255,0.95)';
  ctx.lineWidth = 4;
  ctx.lineJoin = 'round';
  ctx.lineCap = 'round';
  ctx.beginPath();
  ctx.moveTo(pts[0].x, pts[0].y);
  for (let i = 1; i < pts.length; i++) ctx.lineTo(pts[i].x, pts[i].y);
  ctx.stroke();
  ctx.restore();

  if (t > CURVE_START + 1 && t < CURVE_END + 2) {
    drawElectricPulse(pts, t);
  }

  const head = pts[pts.length - 1];

  const scanGrad = ctx.createLinearGradient(head.x - 26, 0, head.x + 26, 0);
  scanGrad.addColorStop(0, 'rgba(56,189,248,0)');
  scanGrad.addColorStop(0.5, 'rgba(56,189,248,0.24)');
  scanGrad.addColorStop(1, 'rgba(56,189,248,0)');
  ctx.save();
  ctx.fillStyle = scanGrad;
  ctx.fillRect(head.x - 26, TOP, 52, BOTTOM - TOP);
  ctx.restore();

  const totalLen = pointsLength(pts);
  if (totalLen > 10) {
    const count = 8;
    for (let k = 0; k < count; k++) {
      const ratio = ((t * 0.22 + k / count) % 1);
      const pt = pointAtLength(pts, ratio * totalLen);
      ctx.save();
      ctx.beginPath();
      ctx.arc(pt.x, pt.y, 5.5, 0, Math.PI * 2);
      ctx.fillStyle = 'rgba(240,249,255,1)';
      ctx.shadowColor = '#38bdf8';
      ctx.shadowBlur = 22;
      ctx.fill();
      ctx.restore();
    }
  }

  const fullIndex = Math.floor(getPos(t));
  for (let i = 0; i <= fullIndex; i++) {
    const x = indexToX(i), y = rankToY(data[i].rank);
    drawDiamond(x, y, 9, '#0ea5e9', '#ffffff');
  }

  ctx.save();
  ctx.beginPath();
  ctx.arc(head.x, head.y, 30, 0, Math.PI * 2);
  ctx.fillStyle = 'rgba(56,189,248,0.18)';
  ctx.fill();
  ctx.beginPath();
  ctx.arc(head.x, head.y, 10, 0, Math.PI * 2);
  ctx.fillStyle = '#f0f9ff';
  ctx.shadowColor = '#38bdf8';
  ctx.shadowBlur = 36;
  ctx.fill();
  ctx.restore();

  if (t > CURVE_START && t < CURVE_END + 1.5) {
    ctx.save();
    ctx.strokeStyle = 'rgba(240,249,255,0.85)';
    ctx.lineWidth = 1.5;
    const r = 20;
    for (let k = 0; k < 4; k++) {
      const ang = t * 2.5 + k * Math.PI / 2;
      ctx.beginPath();
      ctx.arc(head.x, head.y, r, ang, ang + Math.PI / 3);
      ctx.stroke();
    }
    ctx.beginPath();
    ctx.moveTo(head.x - r - 9, head.y);
    ctx.lineTo(head.x - r - 2, head.y);
    ctx.moveTo(head.x + r + 2, head.y);
    ctx.lineTo(head.x + r + 9, head.y);
    ctx.moveTo(head.x, head.y - r - 9);
    ctx.lineTo(head.x, head.y - r - 2);
    ctx.moveTo(head.x, head.y + r + 2);
    ctx.lineTo(head.x, head.y + r + 9);
    ctx.stroke();
    ctx.restore();
  }

  if (t > CURVE_START && t < CURVE_END) {
    if (lastHead) {
      const dist = Math.hypot(head.x - lastHead.x, head.y - lastHead.y);
      if (dist > 2) spawnTrail(head.x, head.y);
    }
    lastHead = { x: head.x, y: head.y };
  }
  updateTrail(dt);
  drawTrail();

  drawKeyLabels(t);
}

function drawKeyLabels(t){
  const keys = [
    { i: 1,  text: '跌至46名', color: '#f87171' },
    { i: 21, text: '一模16名', color: '#fbbf24' },
    { i: 22, text: '二模21名', color: '#fbbf24' },
    { i: 23, text: '三模13名', color: '#fbbf24' },
    { i: 24, text: '中考第1名', color: '#fbbf24' }
  ];

  keys.forEach(k => {
    const showTime = CURVE_START + (k.i / (data.length - 1)) * (CURVE_END - CURVE_START);
    if (t < showTime + 0.15) return;

    const x = indexToX(k.i);
    const y = rankToY(data[k.i].rank);
    const alpha = clamp01((t - showTime - 0.15) / 0.55);

    const sinceShow = t - showTime;
    if (sinceShow < 1.4) {
      const p = sinceShow / 1.4;
      ctx.save();
      ctx.globalAlpha = (1 - p) * 0.7;
      ctx.beginPath();
      ctx.arc(x, y, 12 + p * 90, 0, Math.PI * 2);
      ctx.strokeStyle = k.color;
      ctx.lineWidth = 4 * (1 - p) + 1;
      ctx.shadowColor = k.color;
      ctx.shadowBlur = 26;
      ctx.stroke();
      ctx.restore();

      const burstCount = 14;
      for (let b = 0; b < burstCount; b++) {
        const ang = (b / burstCount) * Math.PI * 2 + p * 1.5;
        const dist = 20 + p * 95;
        const px = x + Math.cos(ang) * dist;
        const py = y + Math.sin(ang) * dist;
        ctx.save();
        ctx.globalAlpha = (1 - p) * 0.9;
        ctx.beginPath();
        ctx.arc(px, py, 3 * (1 - p) + 0.6, 0, Math.PI * 2);
        ctx.fillStyle = k.color;
        ctx.shadowColor = k.color;
        ctx.shadowBlur = 14;
        ctx.fill();
        ctx.restore();
      }
    }

    const pulse = ((t - showTime) % 1.4) / 1.4;
    ctx.save();
    ctx.globalAlpha = (1 - pulse) * 0.45;
    ctx.beginPath();
    ctx.arc(x, y, 16 + pulse * 44, 0, Math.PI * 2);
    ctx.strokeStyle = k.color;
    ctx.lineWidth = 2.5;
    ctx.shadowColor = k.color;
    ctx.shadowBlur = 18;
    ctx.stroke();
    ctx.restore();

    ctx.save();
    ctx.globalAlpha = alpha;
    const scale = 0.8 + 0.2 * easeOutBack(alpha);
    ctx.translate(x, y - 64);
    ctx.scale(scale, scale);
    ctx.translate(-x, -(y - 64));

    ctx.font = 'bold 25px "Microsoft YaHei", sans-serif';
    ctx.textAlign = 'center';
    ctx.textBaseline = 'bottom';

    const tw = ctx.measureText(k.text).width;
    const pad = 14;
    const boxW = tw + pad * 2;
    const boxH = 40;
    const boxX = x - boxW / 2;
    const boxY = y - 64;

    ctx.fillStyle = 'rgba(5,10,20,0.9)';
    roundRect(boxX, boxY, boxW, boxH, 10);
    ctx.fill();
    ctx.strokeStyle = k.color;
    ctx.lineWidth = 2;
    ctx.shadowColor = k.color;
    ctx.shadowBlur = 16;
    ctx.stroke();

    ctx.fillStyle = k.color;
    ctx.fillText(k.text, x, boxY + 29);
    ctx.restore();
  });
}

function drawCurrentInfo(t){
  if (t < CURVE_START || t > CURVE_END) return;
  const pos = getPos(t);
  const idx = Math.min(data.length - 1, Math.max(0, Math.round(pos)));
  const item = data[idx];
  const alpha = clamp01((t - CURVE_START) / 0.6);

  ctx.save();
  ctx.globalAlpha = alpha;
  ctx.textAlign = 'left';

  ctx.fillStyle = '#94a3b8';
  ctx.font = '29px "Microsoft YaHei", sans-serif';
  ctx.fillText('当前考试:' + item.label + '  ·  ' + item.date, 170, 205);

  ctx.save();
  ctx.shadowColor = 'rgba(56,189,248,0.7)';
  ctx.shadowBlur = 26;
  ctx.fillStyle = '#f8fafc';
  ctx.font = 'bold 74px "Microsoft YaHei", sans-serif';
  ctx.fillText('第 ' + item.rank + ' 名', 170, 292);
  ctx.restore();

  ctx.save();
  ctx.shadowColor = 'rgba(251,191,36,0.7)';
  ctx.shadowBlur = 18;
  ctx.fillStyle = 'rgba(251,191,36,0.98)';
  ctx.font = 'bold 27px "Microsoft YaHei", sans-serif';
  ctx.fillText('班级第 ' + item.classRank + ' 名', 170, 344);
  ctx.restore();

  ctx.restore();
}

// ================= 图例(加粗增强) =================
function drawLegend(t){
  if (t < 1.0) return;
  const a = clamp01((t - 1.0) / 0.8);
  ctx.save();
  ctx.globalAlpha = a;
  ctx.textAlign = 'left';
  ctx.textBaseline = 'middle';
  ctx.font = 'bold 23px "Microsoft YaHei", sans-serif';

  const x = RIGHT - 470, y = 205;

  // 蓝色实线
  ctx.strokeStyle = '#38bdf8';
  ctx.lineWidth = 8;
  ctx.shadowColor = '#38bdf8';
  ctx.shadowBlur = 15;
  ctx.beginPath();
  ctx.moveTo(x, y);
  ctx.lineTo(x + 46, y);
  ctx.stroke();
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#cbd5e1';
  ctx.fillText('年级名次', x + 58, y);

  // 黄色虚线
  ctx.strokeStyle = '#fbbf24';
  ctx.lineWidth = 6;
  ctx.setLineDash([10, 6]);
  ctx.shadowColor = '#fbbf24';
  ctx.shadowBlur = 15;
  ctx.beginPath();
  ctx.moveTo(x + 210, y);
  ctx.lineTo(x + 256, y);
  ctx.stroke();
  ctx.setLineDash([]);
  ctx.shadowBlur = 0;
  ctx.fillStyle = '#cbd5e1';
  ctx.fillText('班级名次', x + 268, y);

  ctx.restore();
}

// ================= 中考卡片 =================
function drawFinal(t){
  if (t < CARD_START) return;
  const a = clamp01((t - CARD_START) / 1.0);

  ctx.save();
  ctx.globalAlpha = a * 0.45;
  const rot = t * 0.6;
  for (let ring = 0; ring < 3; ring++) {
    const r = 210 + ring * 36;
    const segments = 24;
    ctx.strokeStyle = ring === 1 ? 'rgba(251,191,36,0.7)' : 'rgba(56,189,248,0.35)';
    ctx.lineWidth = ring === 1 ? 3 : 2;
    ctx.shadowColor = ring === 1 ? '#fbbf24' : '#38bdf8';
    ctx.shadowBlur = 18;
    for (let i = 0; i < segments; i++) {
      const ang1 = rot + (i / segments) * Math.PI * 2;
      const ang2 = ang1 + (Math.PI * 2 / segments) * 0.6;
      ctx.beginPath();
      ctx.arc(W/2, 540, r, ang1, ang2);
      ctx.stroke();
    }
  }
  ctx.restore();

  ctx.save();
  for (let k = 0; k < 4; k++) {
    const p = ((t - CARD_START + k * 0.45) % 1.8) / 1.8;
    ctx.globalAlpha = (1 - p) * 0.3 * a;
    ctx.beginPath();
    ctx.arc(W/2, 540, 60 + p * 300, 0, Math.PI * 2);
    ctx.strokeStyle = '#fbbf24';
    ctx.lineWidth = 3;
    ctx.shadowColor = '#fbbf24';
    ctx.shadowBlur = 26;
    ctx.stroke();
  }
  ctx.restore();

  const cardW = 720, cardH = 240;
  const cardX = W / 2 - cardW / 2, cardY = 400;

  ctx.save();
  ctx.globalAlpha = a;
  const scale = 0.82 + 0.18 * easeOutBack(a);
  ctx.translate(W/2, cardY + cardH/2);
  ctx.scale(scale, scale);
  ctx.translate(-W/2, -(cardY + cardH/2));

  ctx.save();
  ctx.shadowColor = 'rgba(251,191,36,0.6)';
  ctx.shadowBlur = 60;
  ctx.fillStyle = 'rgba(8,14,26,0.95)';
  roundRect(cardX, cardY, cardW, cardH, 26);
  ctx.fill();
  ctx.restore();

  const borderGrad = ctx.createLinearGradient(cardX, cardY, cardX + cardW, cardY + cardH);
  borderGrad.addColorStop(0, 'rgba(56,189,248,0.7)');
  borderGrad.addColorStop(0.5, 'rgba(251,191,36,0.95)');
  borderGrad.addColorStop(1, 'rgba(56,189,248,0.7)');
  ctx.strokeStyle = borderGrad;
  ctx.lineWidth = 3;
  ctx.shadowColor = 'rgba(251,191,36,0.7)';
  ctx.shadowBlur = 30;
  roundRect(cardX, cardY, cardW, cardH, 26);
  ctx.stroke();
  ctx.shadowBlur = 0;

  ctx.strokeStyle = 'rgba(251,191,36,0.5)';
  ctx.lineWidth = 1.5;
  for (let i = 0; i < 12; i++) {
    const tx = cardX + 30 + i * (cardW - 60) / 11;
    ctx.beginPath();
    ctx.moveTo(tx, cardY + 12);
    ctx.lineTo(tx, cardY + 20);
    ctx.stroke();
  }

  ctx.textAlign = 'center';
  ctx.fillStyle = '#fbbf24';
  ctx.font = 'bold 34px "Microsoft YaHei", sans-serif';
  ctx.fillText('中 考 总 分', W / 2, cardY + 56);

  // 修正总分 841
  const numA = clamp01((t - CARD_START - 0.2) / 1.4);
  const displayNum = (841 * easeOutCubic(numA)).toFixed(0);
  ctx.save();
  ctx.shadowColor = 'rgba(251,191,36,0.85)';
  ctx.shadowBlur = 36;
  ctx.fillStyle = '#f8fafc';
  ctx.font = 'bold 80px "Consolas", "Microsoft YaHei", monospace';
  ctx.fillText(numA >= 1 ? '841' : displayNum, W / 2, cardY + 146);
  ctx.restore();

  ctx.fillStyle = '#cbd5e1';
  ctx.font = 'bold 28px "Microsoft YaHei", sans-serif';
  ctx.fillText('全市第2    年级第1    班级第1', W / 2, cardY + 206);
  ctx.restore();

  if (t > FINAL_START) {
    const a2 = clamp01((t - FINAL_START) / 0.9);

    ctx.save();
    ctx.globalAlpha = a2;
    ctx.textAlign = 'center';
    ctx.fillStyle = '#cbd5e1';
    ctx.font = '28px "Microsoft YaHei", sans-serif';
    // 修正语文分数 114
    ctx.fillText('单科成绩:数学 120 · 英语 119 · 语文 114 · 物理 79 · 化学 69', W / 2, 690);
    ctx.fillText('政治 70 · 历史 70 · 地理 50 · 生物 50', W / 2, 734);
    ctx.restore();

    ctx.save();
    ctx.globalAlpha = a2;
    ctx.textAlign = 'center';
    ctx.shadowColor = 'rgba(56,189,248,0.65)';
    ctx.shadowBlur = 24;
    ctx.fillStyle = '#e2e8f0';
    ctx.font = 'bold 42px "Microsoft YaHei", sans-serif';
    ctx.fillText('不是一次爆发,是四年坚持', W / 2, 800);
    ctx.restore();
  }
}

function applyCamera(t){
  const breath = 1 + 0.006 * Math.sin(t * 0.8);
  ctx.translate(W/2, H/2);
  ctx.scale(breath, breath);
  ctx.translate(-W/2, -H/2);
}

let prevT = 0;

function draw(t){
  const dt = Math.min(0.05, t - prevT);
  prevT = t;

  ctx.clearRect(0, 0, W, H);
  drawBackground(t);
  drawHUD(t);

  ctx.save();
  applyCamera(t);
  drawTitle(t);
  drawGrid(t);
  drawClassCurve(t); // 先画班级曲线
  drawCurve(t, dt);  // 再画年级曲线
  drawTopLine(t);    
  drawCurrentInfo(t);
  drawLegend(t);
  drawFinal(t);
  ctx.restore();
}

const startTime = performance.now();
function loop(now){
  const t = (now - startTime) / 1000;
  draw(Math.min(t, TOTAL));
  requestAnimationFrame(loop);
}
requestAnimationFrame(loop);
</script>
</body>
</html>