/* ============================================================ 维特尔液冷 — 首屏等距堆叠动画 Canvas 2D · 零依赖 · 线稿等距工程图 四层:冷板·芯片 → 机柜与 CDU → 数据中心 → 能源与设施 动效:逐层垂直落位(前层落定,后层再落),落地辉光脉冲; 待机呼吸浮动 + 辉光轮巡 + 鼠标视差;滚回视口或点击可重播。 ============================================================ */ (function () { "use strict"; var wrap = document.getElementById("stackWrap"); var canvas = document.getElementById("stackCanvas"); if (!wrap || !canvas) return; var ctx = canvas.getContext("2d"); /* 只在用户明确开启“减少动态效果”时呈现静态终态。 常规模式保留组装与流动动画,并统一限制在约 30fps。 */ var REDUCED = window.matchMedia("(prefers-reduced-motion: reduce)").matches; var LIGHT = window.matchMedia("(max-width: 720px)").matches; var FRAME_INTERVAL = 1000 / 30; /* ---------- 世界参数 ---------- */ var S = 360; // 层板边长 var T = 12; // 层板厚度 var GAP = 195; // 层间距 var CI = 0.866, DI = 0.43; // 压平的等距角 var TOP_ROOM = 84; /* ---------- 循环时间线 ---------- 逐层落位(图例同步高亮) → 热路径巡览 01→04 → 整体下坠淡出 → 重播 */ var DROP_H = 210; // 下落高度(px) var DROP_DUR = 1.6; // 单层时长(s),缓缓放下 var DROP_STAGGER = 1.1; // 前层落定,后层再落 var DROP_DELAY = 0.4; var ASSEMBLE_END = DROP_DELAY + 3 * DROP_STAGGER + DROP_DUR; var TOUR_DELAY = 0.7; // 落齐后到巡览开始 var TOUR_STEP = 0.95; // 巡览每层停留 var EXIT_HOLD = 0.9; // 巡完到整体下坠 var EXIT_DUR = 1.35; // 整体下坠时长(坠入地面) var EXIT_STAG = 0.06; // 下坠时上层略滞后 var LOOP_PAUSE = 0.7; // 空场停顿后重播 var EXIT_AT = ASSEMBLE_END + TOUR_DELAY + 3 * TOUR_STEP + EXIT_HOLD; var BG_TOP = "rgba(12,20,36,0.94)"; var CYAN = "111,212,255"; var COLD = "77,163,255"; // 冷却液·供 var HOT = "255,77,74"; // 冷却液·回 var TEAL = "62,207,178"; // 设施水(一次侧/空调) var AMBER = "255,184,92"; // 电力 var HEAT = "255,110,64"; // 芯片热源 function LN(a) { return "rgba(206,226,255," + a + ")"; } /* 层序即编号:01 能源与设施(先落) → 04 冷板·芯片 */ var LAYERS = [ { z: 0, draw: drawFacility }, { z: GAP, draw: drawDataCenter }, { z: GAP * 2, draw: drawRackCDU }, { z: GAP * 3, draw: drawColdPlate } ]; var dpr = 1, cw = 0, ch = 0, k = 1, cx = 0, cy = 0; var groundY = 0, exitDrop = 0; var offs = [], flows = [], rims = [], topPaths = [], topEnds = []; var hoverL = null; function px(x, y) { return cx + (x - y) * CI * k; } function py(x, y, z) { return cy + (x + y) * DI * k - z * k; } function pt(x, y, z) { return [px(x, y), py(x, y, z)]; } /* ---------- 基础绘制 ---------- */ function poly(c, pts, z, o) { c.beginPath(); pts.forEach(function (p, i) { var q = pt(p[0], p[1], z); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); }); if (o.close !== false) c.closePath(); if (o.fill) { c.fillStyle = o.fill; c.fill(); } if (o.a) { c.strokeStyle = LN(o.a); c.lineWidth = o.lw || 1; c.stroke(); } } function line(c, x1, y1, x2, y2, z, a, lw, z2) { c.beginPath(); var p1 = pt(x1, y1, z), p2 = pt(x2, y2, z2 == null ? z : z2); c.moveTo(p1[0], p1[1]); c.lineTo(p2[0], p2[1]); c.strokeStyle = LN(a); c.lineWidth = lw || 1; c.stroke(); } function cline(c, x1, y1, z1, x2, y2, z2, style, lw) { c.beginPath(); var p1 = pt(x1, y1, z1), p2 = pt(x2, y2, z2); c.moveTo(p1[0], p1[1]); c.lineTo(p2[0], p2[1]); c.strokeStyle = style; c.lineWidth = lw || 1; c.stroke(); } function face(c, x1, y1, x2, y2, zt, zb, o) { var a = pt(x1, y1, zt), b = pt(x2, y2, zt), d = pt(x2, y2, zb), e = pt(x1, y1, zb); c.beginPath(); c.moveTo(a[0], a[1]); c.lineTo(b[0], b[1]); c.lineTo(d[0], d[1]); c.lineTo(e[0], e[1]); c.closePath(); if (o.fill) { c.fillStyle = o.fill; c.fill(); } if (o.a) { c.strokeStyle = LN(o.a); c.lineWidth = o.lw || 1; c.stroke(); } } /* 地面接触阴影(贴地椭圆) */ function contactShadow(c, xc, yc, rx, z, alpha) { c.beginPath(); for (var i = 0; i <= 20; i++) { var th = (i / 20) * Math.PI * 2; var q = pt(xc + rx * Math.cos(th), yc + rx * Math.sin(th), z); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); } c.closePath(); c.fillStyle = "rgba(0,0,0," + (alpha == null ? 0.35 : alpha) + ")"; c.fill(); } function box(c, x, y, w, d, h, z, o) { o = o || {}; var zt = z + h, aa = o.a == null ? 0.55 : o.a; if (o.shadow !== false) contactShadow(c, x + w / 2, y + d / 2, Math.max(w, d) * 0.62, z, 0.3); face(c, x + w, y, x + w, y + d, zt, z, { fill: o.fill || "rgba(4,7,13,0.9)", a: aa * 0.8 }); face(c, x, y + d, x + w, y + d, zt, z, { fill: o.fill || "rgba(7,11,20,0.9)", a: aa * 0.8 }); poly(c, [[x, y], [x + w, y], [x + w, y + d], [x, y + d]], zt, { fill: o.top || BG_TOP, a: aa }); } function ringTop(c, xc, yc, r, z, a, lw) { c.beginPath(); for (var i = 0; i <= 24; i++) { var th = (i / 24) * Math.PI * 2; var q = pt(xc + r * Math.cos(th), yc + r * Math.sin(th), z); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); } c.strokeStyle = LN(a); c.lineWidth = lw || 1; c.stroke(); } function ringColor(c, xc, yc, r, z, stroke) { c.beginPath(); for (var i = 0; i <= 16; i++) { var th = (i / 16) * Math.PI * 2; var q = pt(xc + r * Math.cos(th), yc + r * Math.sin(th), z); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); } c.strokeStyle = stroke; c.lineWidth = 1; c.stroke(); } function ringFace(c, xc, yFace, zc, r, a, lw) { c.beginPath(); for (var i = 0; i <= 20; i++) { var th = (i / 20) * Math.PI * 2; var q = pt(xc + r * Math.cos(th), yFace, zc + r * Math.sin(th)); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); } c.strokeStyle = LN(a); c.lineWidth = lw || 1; c.stroke(); } function dot(c, x, y, z, r, style) { var q = pt(x, y, z); c.beginPath(); c.arc(q[0], q[1], r, 0, Math.PI * 2); c.fillStyle = style; c.fill(); } /* ---------- 层板 ---------- */ function slab(c, z) { var zt = z + T; var gr = c.createLinearGradient(0, py(S, S, zt), 0, py(S, S, z) + 6); gr.addColorStop(0, "rgba(" + CYAN + ",0.42)"); gr.addColorStop(1, "rgba(" + CYAN + ",0.02)"); face(c, S, 0, S, S, zt, z, { fill: gr, a: 0.1 }); face(c, 0, S, S, S, zt, z, { fill: gr, a: 0.1 }); var gt = c.createLinearGradient(px(0, 0), py(0, 0, zt), px(S, S), py(S, S, zt)); gt.addColorStop(0, "rgba(13,21,38,0.95)"); gt.addColorStop(1, "rgba(6,10,19,0.95)"); poly(c, [[0, 0], [S, 0], [S, S], [0, S]], zt, { fill: gt, a: 0.34 }); /* 板角蓝图标记 */ var m = 16, L2 = 15; [[m, m, 1, 1], [S - m, m, -1, 1], [S - m, S - m, -1, -1], [m, S - m, 1, -1]].forEach(function (cn) { line(c, cn[0], cn[1], cn[0] + L2 * cn[2], cn[1], zt, 0.22, 0.8); line(c, cn[0], cn[1], cn[0], cn[1] + L2 * cn[3], zt, 0.22, 0.8); }); } function rimPath(z) { var zt = z + T; return [pt(S, 0, zt), pt(S, S, zt), pt(0, S, zt)]; } /* ---------- L4 冷板 · 芯片(顶层) ---------- */ function drawColdPlate(c, z) { var zt = z + T; var xs = [52, 150, 248], ys = [74, 190]; /* 冷却液母线 */ line(c, 8, 152, 352, 152, zt, 0.15); line(c, 8, 166, 352, 166, zt, 0.13); ys.forEach(function (gy) { xs.forEach(function (gx) { box(c, gx, gy, 64, 64, 8, zt, { a: 0.5 }); var dz = zt + 8; /* 封装螺钉 */ [[gx + 5, gy + 5], [gx + 59, gy + 5], [gx + 59, gy + 59], [gx + 5, gy + 59]].forEach(function (s2) { dot(c, s2[0], s2[1], dz, 1.1, LN(0.3)); }); /* 芯片 die(中心一点热源暖光) */ var dc = pt(gx + 32, gy + 28, dz); var hg = c.createRadialGradient(dc[0], dc[1], 0, dc[0], dc[1], 28 * k); hg.addColorStop(0, "rgba(" + HEAT + ",0.2)"); hg.addColorStop(1, "rgba(" + HEAT + ",0)"); c.fillStyle = hg; c.beginPath(); c.arc(dc[0], dc[1], 28 * k, 0, Math.PI * 2); c.fill(); poly(c, [[gx + 20, gy + 12], [gx + 44, gy + 12], [gx + 44, gy + 44], [gx + 20, gy + 44]], dz, { a: 0.32 }); /* HBM 颗粒(die 两侧) */ [0, 1, 2].forEach(function (h2) { poly(c, [[gx + 8, gy + 14 + h2 * 11], [gx + 16, gy + 14 + h2 * 11], [gx + 16, gy + 22 + h2 * 11], [gx + 8, gy + 22 + h2 * 11]], dz, { a: 0.2 }); poly(c, [[gx + 48, gy + 14 + h2 * 11], [gx + 56, gy + 14 + h2 * 11], [gx + 56, gy + 22 + h2 * 11], [gx + 48, gy + 22 + h2 * 11]], dz, { a: 0.2 }); }); /* 微通道蛇形流道 */ c.beginPath(); for (var p2 = 0; p2 < 4; p2++) { var yy = gy + 16 + p2 * 8; var a2 = pt(gx + 22, yy, dz), b2 = pt(gx + 42, yy, dz); if (p2 % 2 === 0) { c.moveTo(a2[0], a2[1]); c.lineTo(b2[0], b2[1]); } else { c.moveTo(b2[0], b2[1]); c.lineTo(a2[0], a2[1]); } if (p2 < 3) { var xe = (p2 % 2 === 0) ? gx + 42 : gx + 22; var e1 = pt(xe, yy, dz), e2 = pt(xe, yy + 8, dz); c.moveTo(e1[0], e1[1]); c.lineTo(e2[0], e2[1]); } } c.strokeStyle = LN(0.24); c.lineWidth = 0.8; c.stroke(); /* 快接口 + 引管 + 汇点 */ ringColor(c, gx + 18, gy + 55, 3.4, dz, "rgba(" + COLD + ",0.78)"); ringColor(c, gx + 46, gy + 55, 3.4, dz, "rgba(" + HOT + ",0.72)"); var busB = gy < 150 ? 152 : 166, busR = gy < 150 ? 166 : 152; line(c, gx + 18, gy + 58, gx + 18, busB, zt, 0.17, 0.8); line(c, gx + 46, gy + 58, gx + 46, busR, zt, 0.14, 0.8); dot(c, gx + 18, busB, zt, 1.4, "rgba(" + COLD + ",0.55)"); dot(c, gx + 46, busR, zt, 1.4, "rgba(" + HOT + ",0.5)"); }); }); /* 悬浮分解的冷板(装配示意) */ var ex = 150, ey = 74, ez = zt + 40; c.save(); c.setLineDash([3, 5]); [[ex + 4, ey + 4], [ex + 60, ey + 4], [ex + 60, ey + 60], [ex + 4, ey + 60]].forEach(function (q) { line(c, q[0], q[1], q[0], q[1], zt + 8, 0.2, 0.8, ez); }); c.restore(); box(c, ex, ey, 64, 64, 6, ez, { a: 0.55, top: "rgba(13,22,40,0.85)", shadow: false }); /* 冷板底面流道纹 */ for (var f3 = 0; f3 < 3; f3++) { line(c, ex + 12, ey + 18 + f3 * 14, ex + 52, ey + 18 + f3 * 14, ez + 6, 0.18, 0.8); } ringColor(c, ex + 18, ey + 55, 3.4, ez + 6, "rgba(" + COLD + ",0.8)"); ringColor(c, ex + 46, ey + 55, 3.4, ez + 6, "rgba(" + HOT + ",0.75)"); flows.push({ pts: [[8, 152], [352, 152]], z: zt, rgb: COLD, speed: 30 }); flows.push({ pts: [[352, 166], [8, 166]], z: zt, rgb: HOT, speed: 30 }); } /* ---------- L3 机柜与 CDU ---------- */ function drawRackCDU(c, z) { var zt = z + T; var rx = [36, 120, 204], ry = 168, rw = 62, rd = 74, rh = 106; rx.forEach(function (x) { box(c, x, ry, rw, rd, rh, zt, { a: 0.6 }); /* 前面板槽位 + 两侧导轨 */ for (var s2 = 0; s2 < 7; s2++) { var zz = zt + 12 + s2 * 12; line(c, x + 8, ry + rd, x + rw - 8, ry + rd, zz, 0.3, 0.8); } cline(c, x + 4, ry + rd, zt + 6, x + 4, ry + rd, zt + rh - 6, LN(0.2), 0.8); cline(c, x + rw - 4, ry + rd, zt + 6, x + rw - 4, ry + rd, zt + rh - 6, LN(0.2), 0.8); /* 顶部通风孔 */ for (var v2 = 0; v2 < 3; v2++) { line(c, x + 10, ry + 14 + v2 * 18, x + rw - 10, ry + 14 + v2 * 18, zt + rh, 0.16, 0.8); } /* 状态灯 */ var q = pt(x + rw - 11, ry + rd, zt + rh - 9); c.fillStyle = "rgba(96,230,150,0.85)"; c.fillRect(q[0] - 1, q[1] - 1, 2, 2); }); /* CDU */ var cxx = 290, cyy = 168, cdd = 74, chh = 90; box(c, cxx, cyy, 64, cdd, chh, zt, { a: 0.65 }); ringFace(c, cxx + 32, cyy + cdd, zt + 32, 15, 0.5, 1); ringFace(c, cxx + 32, cyy + cdd, zt + 32, 4, 0.35, 1); /* 泵叶轮示意(三叶) */ for (var b2 = 0; b2 < 3; b2++) { var an = (b2 / 3) * Math.PI * 2 + 0.6; var q1 = pt(cxx + 32 + 4 * Math.cos(an), cyy + cdd, zt + 32 + 4 * Math.sin(an)); var q2 = pt(cxx + 32 + 12 * Math.cos(an + 0.6), cyy + cdd, zt + 32 + 12 * Math.sin(an + 0.6)); c.beginPath(); c.moveTo(q1[0], q1[1]); c.lineTo(q2[0], q2[1]); c.strokeStyle = LN(0.35); c.lineWidth = 0.9; c.stroke(); } /* 屏幕 */ face(c, cxx + 10, cyy + cdd, cxx + 54, cyy + cdd, zt + 76, zt + 62, { fill: "rgba(" + CYAN + ",0.09)", a: 0.35 }); /* 歧管:蓝供红回 + 垂管 + CDU 接管 */ line(c, 30, 154, 322, 154, zt + 114, 0.16); line(c, 30, 144, 322, 144, zt + 120, 0.14); rx.forEach(function (x) { cline(c, x + 30, 154, zt + 114, x + 30, 168, zt + 106, LN(0.2), 0.8); cline(c, x + 40, 144, zt + 120, x + 40, 168, zt + 106, LN(0.16), 0.8); }); cline(c, 322, 154, zt + 114, cxx + 28, 176, zt + chh, "rgba(" + COLD + ",0.4)", 1); cline(c, 322, 144, zt + 120, cxx + 40, 172, zt + chh, "rgba(" + HOT + ",0.35)", 1); flows.push({ pts: [[30, 154], [322, 154]], z: zt + 114, rgb: COLD, speed: 26 }); flows.push({ pts: [[322, 144], [30, 144]], z: zt + 120, rgb: HOT, speed: 26 }); } /* ---------- L2 数据中心 ---------- */ function drawDataCenter(c, z) { var zt = z + T; /* 周界内衬线 */ poly(c, [[14, 14], [S - 14, 14], [S - 14, S - 14], [14, S - 14]], zt, { a: 0.12 }); /* 地板网格 */ c.save(); c.setLineDash([2, 6]); for (var g = 60; g < 340; g += 60) { line(c, g, 20, g, 340, zt, 0.07, 0.8); line(c, 20, g, 340, g, zt, 0.07, 0.8); } c.restore(); /* 冷通道封闭带(两列机柜之间,冷空气语义的淡蓝) */ poly(c, [[166, 96], [196, 96], [196, 326], [166, 326]], zt, { fill: "rgba(" + COLD + ",0.06)", a: 0 }); c.save(); c.setLineDash([4, 5]); c.strokeStyle = "rgba(" + COLD + ",0.22)"; c.lineWidth = 0.8; [[166], [196]].forEach(function (gx2) { var a2 = pt(gx2[0], 96, zt), b2 = pt(gx2[0], 326, zt); c.beginPath(); c.moveTo(a2[0], a2[1]); c.lineTo(b2[0], b2[1]); c.stroke(); }); c.restore(); /* 机柜排(两列四排) */ [44, 200].forEach(function (colX) { for (var r2 = 0; r2 < 4; r2++) { var yy = 96 + r2 * 62; box(c, colX, yy, 118, 20, 24, zt, { a: 0.5 }); line(c, colX + 6, yy + 20, colX + 112, yy + 20, zt + 17, 0.2, 0.8); /* 排端小门 */ line(c, colX, yy + 4, colX, yy + 16, zt + 20, 0.2, 0.8); line(c, colX + 118, yy + 4, colX + 118, yy + 16, zt + 20, 0.2, 0.8); } }); /* 空调/配电单元(后缘,冷冻水接管为青绿) */ [70, 170, 270].forEach(function (ux) { box(c, ux, 44, 44, 16, 34, zt, { a: 0.45 }); line(c, ux + 6, 44, ux + 38, 44, zt + 26, 0.2, 0.8); cline(c, ux + 12, 60, zt, ux + 12, 60, zt + 8, "rgba(" + TEAL + ",0.4)", 1); cline(c, ux + 32, 60, zt, ux + 32, 60, zt + 8, "rgba(" + TEAL + ",0.3)", 1); }); /* 机柜排状态灯(极小的绿点) */ [44, 200].forEach(function (colX) { for (var l3 = 0; l3 < 4; l3++) { var q3 = pt(colX + 112, 96 + l3 * 62 + 20, zt + 20); c.fillStyle = "rgba(96,230,150,0.7)"; c.fillRect(q3[0] - 0.8, q3[1] - 0.8, 1.6, 1.6); } }); } /* ---------- L1 能源与设施(底层) ---------- */ function drawFacility(c, z) { var zt = z + T; /* 冷却塔 ×2 */ [[80, 168], [80, 262]].forEach(function (tp) { cylinder(c, tp[0], tp[1], 30, 48, zt); }); /* 干冷器 */ box(c, 176, 130, 146, 56, 26, zt, { a: 0.55 }); for (var f2 = 0; f2 < 3; f2++) { var fx = 200 + f2 * 48; ringTop(c, fx, 158, 17, zt + 26, 0.45, 1); /* 弧形扇叶 */ for (var b2 = 0; b2 < 3; b2++) { var an = (b2 / 3) * Math.PI * 2 + f2 * 0.7; var pA = pt(fx + 3 * Math.cos(an), 158 + 3 * Math.sin(an), zt + 26); var pC = pt(fx + 9 * Math.cos(an + 0.7), 158 + 9 * Math.sin(an + 0.7), zt + 26); var pB = pt(fx + 14 * Math.cos(an + 1.1), 158 + 14 * Math.sin(an + 1.1), zt + 26); c.beginPath(); c.moveTo(pA[0], pA[1]); c.quadraticCurveTo(pC[0], pC[1], pB[0], pB[1]); c.strokeStyle = LN(0.32); c.lineWidth = 0.9; c.stroke(); } dot(c, fx, 158, zt + 26, 1.4, LN(0.4)); } /* 变压器 + 输电塔(电力语义:琥珀) */ box(c, 246, 148, 26, 20, 16, zt, { a: 0.45 }); cline(c, 252, 158, zt + 16, 252, 158, zt + 22, "rgba(" + AMBER + ",0.5)", 1); cline(c, 262, 154, zt + 16, 262, 154, zt + 22, "rgba(" + AMBER + ",0.4)", 1); var bx = 286, by = 262, tz = zt + 64; line(c, bx - 12, by + 8, bx, by, zt, 0.45, 1, tz); line(c, bx + 12, by - 8, bx, by, zt, 0.45, 1, tz); line(c, bx - 13, by + 2, bx + 13, by - 2, zt + 42, 0.4, 1); line(c, bx - 9, by + 1, bx + 9, by - 1, zt + 54, 0.4, 1); [[bx - 13, by + 2], [bx + 13, by - 2]].forEach(function (ar) { cline(c, ar[0], ar[1], zt + 42, ar[0], ar[1], zt + 36, "rgba(" + AMBER + ",0.45)", 0.8); dot(c, ar[0], ar[1], zt + 36, 1, "rgba(" + AMBER + ",0.55)"); }); /* 变压器→输电塔连线(输电) */ cline(c, 262, 154, zt + 22, bx - 4, by - 4, zt + 40, "rgba(" + AMBER + ",0.2)", 0.8); /* 厂区管路(设施水:青绿,缓速流动) */ c.strokeStyle = "rgba(" + TEAL + ",0.3)"; c.lineWidth = 1; [[[112, 176], [176, 176]], [[112, 262], [148, 262]], [[148, 262], [148, 180]]].forEach(function (sg) { var a3 = pt(sg[0][0], sg[0][1], zt + 8), b3 = pt(sg[1][0], sg[1][1], zt + 8); c.beginPath(); c.moveTo(a3[0], a3[1]); c.lineTo(b3[0], b3[1]); c.stroke(); }); flows.push({ pts: [[112, 262], [148, 262], [148, 176], [176, 176]], z: zt + 8, rgb: TEAL, speed: 16 }); } function cylinder(c, xc, yc, r, h, z) { var zt = z + h; contactShadow(c, xc, yc, r * 1.15, z, 0.3); var thA = -Math.PI / 4, thB = (3 * Math.PI) / 4; var pA = [xc + r * Math.cos(thA), yc + r * Math.sin(thA)]; var pB = [xc + r * Math.cos(thB), yc + r * Math.sin(thB)]; c.beginPath(); for (var i = 0; i <= 14; i++) { var th = thA + (i / 14) * Math.PI; var q = pt(xc + r * Math.cos(th), yc + r * Math.sin(th), z); i === 0 ? c.moveTo(q[0], q[1]) : c.lineTo(q[0], q[1]); } for (var j = 14; j >= 0; j--) { var th2 = thA + (j / 14) * Math.PI; var q2 = pt(xc + r * Math.cos(th2), yc + r * Math.sin(th2), zt); c.lineTo(q2[0], q2[1]); } c.closePath(); c.fillStyle = "rgba(6,10,18,0.92)"; c.fill(); c.strokeStyle = LN(0.14); c.lineWidth = 1; c.stroke(); line(c, pA[0], pA[1], pA[0], pA[1], z, 0.5, 1, zt); line(c, pB[0], pB[1], pB[0], pB[1], z, 0.5, 1, zt); /* 集水盘 */ ringTop(c, xc, yc, r + 5, z + 3, 0.2, 0.8); ringTop(c, xc, yc, r, zt, 0.55, 1); ringTop(c, xc, yc, r * 0.62, zt, 0.25, 0.8); /* 弧形扇叶 */ for (var s2 = 0; s2 < 3; s2++) { var a3 = (s2 / 3) * Math.PI * 2 + 0.5; var qA = pt(xc + r * 0.1 * Math.cos(a3), yc + r * 0.1 * Math.sin(a3), zt); var qC = pt(xc + r * 0.36 * Math.cos(a3 + 0.5), yc + r * 0.36 * Math.sin(a3 + 0.5), zt); var qB = pt(xc + r * 0.56 * Math.cos(a3 + 0.85), yc + r * 0.56 * Math.sin(a3 + 0.85), zt); c.beginPath(); c.moveTo(qA[0], qA[1]); c.quadraticCurveTo(qC[0], qC[1], qB[0], qB[1]); c.strokeStyle = LN(0.35); c.lineWidth = 0.9; c.stroke(); } dot(c, xc, yc, zt, 1.4, LN(0.4)); /* 百叶 */ c.save(); c.setLineDash([4, 4]); for (var l2 = 1; l2 <= 2; l2++) { c.beginPath(); for (var m2 = 0; m2 <= 10; m2++) { var th3 = -Math.PI / 6 + (m2 / 10) * (Math.PI * 0.8); var q3 = pt(xc + r * Math.cos(th3), yc + r * Math.sin(th3), z + l2 * 13); m2 === 0 ? c.moveTo(q3[0], q3[1]) : c.lineTo(q3[0], q3[1]); } c.strokeStyle = LN(0.13); c.lineWidth = 0.8; c.stroke(); } c.restore(); } /* ---------- 布局与预渲染 ---------- */ function layout() { var rect = wrap.getBoundingClientRect(); cw = Math.max(1, Math.round(rect.width)); ch = Math.max(1, Math.round(rect.height)); dpr = (REDUCED || LIGHT) ? 1 : Math.min(window.devicePixelRatio || 1, 1.25); canvas.width = cw * dpr; canvas.height = ch * dpr; var rhombH = 2 * S * DI; var totalH = rhombH + GAP * 3 + T + TOP_ROOM; k = Math.min((cw * 0.94) / (2 * CI * S), (ch * 0.92) / totalH); cx = cw / 2; cy = ch - Math.max(18, (ch - totalH * k) / 2) - rhombH * k; /* 地平线(底座最低点):整体下坠时在此被吞没 */ groundY = py(S, S, 0) + T * k + 2; /* 让最高一层也能完全沉入地面 */ exitDrop = (totalH + T) * k + 60; flows = []; rims = []; topPaths = []; topEnds = []; offs = LAYERS.map(function (L) { var off = document.createElement("canvas"); off.width = canvas.width; off.height = canvas.height; var c = off.getContext("2d"); c.setTransform(dpr, 0, 0, dpr, 0, 0); c.lineJoin = "round"; c.lineCap = "round"; slab(c, L.z); L.draw(c, L.z); rims.push(rimPath(L.z)); /* 顶面路径与对角端点(供扫描光裁切/渐变) */ var zt = L.z + T; var tp2 = new Path2D(); [[0, 0], [S, 0], [S, S], [0, S]].forEach(function (q, i2) { var s3 = pt(q[0], q[1], zt); i2 === 0 ? tp2.moveTo(s3[0], s3[1]) : tp2.lineTo(s3[0], s3[1]); }); tp2.closePath(); topPaths.push(tp2); var e1 = pt(0, 0, zt), e2 = pt(S, S, zt); topEnds.push([e1[0], e1[1], e2[0], e2[1]]); return off; }); flows.forEach(function (f) { var p = new Path2D(); f.pts.forEach(function (q, i) { var s2 = pt(q[0], q[1], f.z); i === 0 ? p.moveTo(s2[0], s2[1]) : p.lineTo(s2[0], s2[1]); }); f.path = p; var best = 0, bd = 1e9; LAYERS.forEach(function (L, i) { var dist = Math.abs(f.z - L.z); if (dist < bd) { bd = dist; best = i; } }); f.layer = best; }); } /* ---------- 动态状态 ---------- */ var st = LAYERS.map(function () { return { dx: 0, dy: 0, alpha: REDUCED ? 1 : 0, glow: 0, pulse: 0, landed: false, am: 1, scanT: null }; }); var active = 3, pauseUntil = 0; var t = 0, lastNow = 0, lastFrame = 0, running = false; var mx = 0; var wasOut = false; var pathBtns = [].slice.call(document.querySelectorAll("#stackPath .sp-item")); var pathWrap = document.getElementById("stackPath"); var tagsEl = document.getElementById("stackTags"); function setActive(idx, manual) { if (idx !== active) st[idx].scanT = t; /* 新激活层做一次扫描光 */ active = idx; pathBtns.forEach(function (b) { b.classList.toggle("is-active", parseInt(b.dataset.layer, 10) === idx); }); if (manual) pauseUntil = t + 8; if (REDUCED) renderFrame(); } pathBtns.forEach(function (b) { var idx = parseInt(b.dataset.layer, 10); b.addEventListener("click", function () { /* 点击:暂停循环并显示该层工程标签 */ setActive(idx, true); pauseUntil = t + 14; if (tagsEl) tagsEl.dataset.show = idx; }); b.addEventListener("mouseenter", function () { hoverL = idx; setActive(idx, true); }); }); if (pathWrap) { pathWrap.addEventListener("mouseleave", function () { hoverL = null; if (REDUCED) renderFrame(); }); } function resetAnim() { t = 0; pauseUntil = 0; lastNow = performance.now(); st.forEach(function (s2) { s2.alpha = 0; s2.glow = 0; s2.pulse = 0; s2.landed = false; s2.dy = -DROP_H; s2.am = 1; s2.scanT = null; }); if (tagsEl) delete tagsEl.dataset.show; setActive(0); /* 第一个落下的是 01 能源与设施 */ } /* 落位缓动:缓起缓收的"放下"感,到位即停,无回弹 */ var LAND_AT = 0.96; // 接近落定时触发辉光脉冲 function dropOffset(p) { if (p <= 0) return 1; if (p >= 1) return 0; var e = p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; // easeInOutCubic return 1 - e; } function easeInCubic(x) { return x * x * x; } /* ---------- 渲染 ---------- */ function renderFrame() { ctx.setTransform(dpr, 0, 0, dpr, 0, 0); ctx.clearRect(0, 0, cw, ch); /* 地面光影 */ var gx = px(S / 2, S / 2), gy = py(S / 2, S / 2, 0) + T * k + 14; var gr = ctx.createRadialGradient(gx, gy, 0, gx, gy, S * k * 0.9); gr.addColorStop(0, "rgba(0,0,0,0.5)"); gr.addColorStop(0.5, "rgba(" + CYAN + ",0.03)"); gr.addColorStop(1, "rgba(0,0,0,0)"); ctx.save(); ctx.translate(gx, gy); ctx.scale(1, 0.22); ctx.translate(-gx, -gy); ctx.fillStyle = gr; ctx.beginPath(); ctx.arc(gx, gy, S * k * 0.9, 0, Math.PI * 2); ctx.fill(); ctx.restore(); /* 地平线裁切:下坠的层在底座平面处被吞没 */ ctx.save(); ctx.beginPath(); ctx.rect(0, 0, cw, groundY); ctx.clip(); /* 层间装配导引虚线(右角、左角) */ ctx.save(); ctx.setLineDash([2, 7]); ctx.lineWidth = 0.8; for (var g2 = 0; g2 < 3; g2++) { var lo = st[g2], hi = st[g2 + 1]; var a2 = Math.min(lo.alpha, hi.alpha) * 0.35; if (a2 <= 0.02) continue; ctx.strokeStyle = LN(a2.toFixed(3)); [[S, 0], [0, S]].forEach(function (cn) { var pTop = pt(cn[0], cn[1], LAYERS[g2 + 1].z); var pBot = pt(cn[0], cn[1], LAYERS[g2].z + T); ctx.beginPath(); ctx.moveTo(pTop[0] + hi.dx, pTop[1] + hi.dy); ctx.lineTo(pBot[0] + lo.dx, pBot[1] + lo.dy); ctx.stroke(); }); } ctx.restore(); for (var i = 0; i < LAYERS.length; i++) { var s2 = st[i]; if (s2.alpha <= 0.01) continue; /* 图例悬停:非目标层压暗 */ var amT = (hoverL === null || hoverL === i) ? 1 : 0.4; if (REDUCED) s2.am = amT; ctx.globalAlpha = s2.alpha * s2.am; ctx.drawImage(offs[i], s2.dx, s2.dy, cw, ch); /* 辉光棱线(叠加落地脉冲) */ ctx.save(); ctx.translate(s2.dx, s2.dy); var rim = rims[i]; var glowA = 0.3 + 0.6 * s2.glow + 0.5 * s2.pulse; ctx.beginPath(); ctx.moveTo(rim[0][0], rim[0][1]); ctx.lineTo(rim[1][0], rim[1][1]); ctx.lineTo(rim[2][0], rim[2][1]); ctx.strokeStyle = "rgba(" + CYAN + "," + Math.min(glowA, 0.95) + ")"; ctx.lineWidth = 1.4; ctx.shadowColor = "rgba(" + CYAN + ",0.9)"; ctx.shadowBlur = 16 * s2.glow + 26 * s2.pulse; ctx.stroke(); ctx.shadowBlur = 0; /* 内侧白色发丝线,板缘更锐 */ ctx.beginPath(); ctx.moveTo(rim[0][0], rim[0][1] - 1.2); ctx.lineTo(rim[1][0], rim[1][1] - 1.2); ctx.lineTo(rim[2][0], rim[2][1] - 1.2); ctx.strokeStyle = LN(0.3 + 0.25 * s2.glow); ctx.lineWidth = 0.7; ctx.stroke(); /* 激活层一次性扫描光(沿对角扫过顶面,克制) */ var sc = s2.scanT == null ? -1 : t - s2.scanT; if (!REDUCED && sc > 0 && sc < 1.0) { var q2 = sc / 1.0; var e0 = topEnds[i]; var gr2 = ctx.createLinearGradient(e0[0], e0[1], e0[2], e0[3]); var lo = Math.max(0, q2 - 0.11), hi = Math.min(1, q2 + 0.11); gr2.addColorStop(lo, "rgba(111,212,255,0)"); gr2.addColorStop(q2, "rgba(185,232,255,0.13)"); gr2.addColorStop(hi, "rgba(111,212,255,0)"); ctx.save(); ctx.clip(topPaths[i]); ctx.fillStyle = gr2; ctx.fillRect(-40, -40, cw + 80, ch + 80); ctx.restore(); } /* 该层流线 */ for (var f2 = 0; f2 < flows.length; f2++) { var fl = flows[f2]; if (fl.layer !== i) continue; ctx.setLineDash([9, 42]); ctx.lineDashOffset = -t * fl.speed * (REDUCED ? 0 : 1); ctx.strokeStyle = "rgba(" + fl.rgb + "," + (0.5 + 0.35 * s2.glow) + ")"; ctx.lineWidth = 1.6; ctx.stroke(fl.path); ctx.setLineDash([]); } ctx.restore(); ctx.globalAlpha = 1; } ctx.restore(); /* 解除地平线裁切 */ } function tick(now) { if (!running) return; if (lastFrame && now - lastFrame < FRAME_INTERVAL) { requestAnimationFrame(tick); return; } lastFrame = now; var dt = Math.min((now - lastNow) / 1000, 0.08); lastNow = now; t += dt; /* 用户交互时顺延整体下坠 */ var exitStart = Math.max(EXIT_AT, pauseUntil + 1.5); /* 一轮播完:空场停顿后重播 */ if (t > exitStart + EXIT_DUR + EXIT_STAG * 3 + LOOP_PAUSE) { resetAnim(); } /* 图例与画面严格同步:第 n 层落下即亮 0n;落齐后按同一顺序 01→04 巡览 */ if (t >= pauseUntil) { var target; if (t < ASSEMBLE_END + TOUR_DELAY) { target = 0; for (var n2 = 1; n2 < 4; n2++) { if (t >= DROP_DELAY + n2 * DROP_STAGGER) target = n2; } } else { target = Math.min(3, Math.floor((t - ASSEMBLE_END - TOUR_DELAY) / TOUR_STEP)); } if (target !== active) setActive(target); } for (var i = 0; i < st.length; i++) { var s2 = st[i]; var t0 = DROP_DELAY + i * DROP_STAGGER; var p = (t - t0) / DROP_DUR; var off = dropOffset(p); /* 落地瞬间:辉光脉冲 */ if (!s2.landed && p >= LAND_AT) { s2.landed = true; s2.pulse = 1; } s2.pulse = Math.max(0, s2.pulse - dt * 2.4); s2.glow += ((i === active ? 1 : 0) - s2.glow) * Math.min(1, dt * 4); /* 整体下坠,沉入地平线(上层微滞后);不做透明淡出 */ var pe = (t - (exitStart + (3 - i) * EXIT_STAG)) / EXIT_DUR; var dyExit = pe > 0 ? exitDrop * easeInCubic(Math.min(pe, 1)) : 0; s2.alpha = Math.min(1, Math.max(0, p / 0.2)); /* 落定即静止:不加呼吸浮动、不随辉光抬升 */ s2.dy = -DROP_H * off + dyExit; /* 视差 + 悬停压暗过渡 */ var dxT = LIGHT ? 0 : mx * (2 + i * 2.6); s2.dx += (dxT - s2.dx) * 0.06; var amT2 = (hoverL === null || hoverL === i) ? 1 : 0.4; s2.am += (amT2 - s2.am) * Math.min(1, dt * 8); } /* 标签随暂停结束收起 */ if (tagsEl && tagsEl.dataset.show != null && t > pauseUntil) { delete tagsEl.dataset.show; } renderFrame(); requestAnimationFrame(tick); } function start() { if (running || REDUCED) return; running = true; lastNow = performance.now(); lastFrame = 0; requestAnimationFrame(tick); } function stop() { running = false; } /* ---------- 事件 ---------- */ var ro = new ResizeObserver(function () { layout(); if (REDUCED) { st.forEach(function (s2, i) { s2.alpha = 1; s2.dy = 0; s2.glow = i === active ? 1 : 0; }); renderFrame(); } }); ro.observe(wrap); if (!REDUCED) { var io = new IntersectionObserver(function (entries) { entries.forEach(function (en) { if (en.intersectionRatio === 0) { wasOut = true; stop(); } else { if (wasOut) { wasOut = false; resetAnim(); } start(); } }); }, { threshold: [0, 0.15] }); io.observe(wrap); document.addEventListener("visibilitychange", function () { if (document.hidden) stop(); else if (wrap.getBoundingClientRect().top < window.innerHeight) start(); }); } layout(); if (REDUCED) { st.forEach(function (s2, i) { s2.alpha = 1; s2.dy = 0; s2.glow = i === 3 ? 1 : 0; }); renderFrame(); } else { /* 架构图进入视口后再开播,避免首屏 CAD 动画与下方 Canvas 同时占用资源 */ resetAnim(); renderFrame(); var initialRect = wrap.getBoundingClientRect(); if (initialRect.top < window.innerHeight && initialRect.bottom > 0) start(); } })();