Files
aibis-dream/step2_10.html
T
2025-12-04 20:38:02 +08:00

705 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>情绪数据析出终端 V7 - 混合重构版</title>
<style>
:root {
--color-main: #00ffcc;
--color-dim: #004433;
--color-alert: #ff3333;
--bg-dark: #020202;
}
body {
background-color: #000;
color: var(--color-main);
font-family: 'Courier New', monospace;
margin: 0;
height: 100vh;
overflow: hidden;
display: grid;
grid-template-columns: 1fr 360px;
grid-template-rows: 1fr 180px;
user-select: none;
}
/* --- 1. 析出池 (主屏幕) --- */
#tank-area {
grid-column: 1 / 2;
grid-row: 1 / 2;
position: relative;
background: radial-gradient(circle at center, #0a1515 0%, #000 100%);
border-right: 1px solid #333;
border-bottom: 1px solid #333;
overflow: hidden;
cursor: crosshair;
}
#tank-area.sampling {
cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="%23ff3333" stroke-width="2"><path d="M12 2v20M2 12h20"/><circle cx="12" cy="12" r="8"/></svg>') 12 12, crosshair;
}
/* --- 2. 右侧面板 --- */
#scope-panel {
grid-column: 2 / 3; grid-row: 1 / 2;
background: #050505; border-bottom: 1px solid #333;
display: flex; flex-direction: column;
padding: 15px; box-sizing: border-box;
gap: 15px;
}
.panel-box {
border: 1px solid #333;
padding: 10px;
background: #000;
position: relative;
}
.panel-label {
font-size: 10px; color: #666; margin-bottom: 5px; display: block;
}
/* A. 波形示波器 (参考 Step2_5) */
#wave-container {
height: 120px; background: #000500;
border: 1px solid #222;
position: relative;
overflow: hidden;
}
/* 示波器网格背景 */
.scope-grid {
position: absolute; top:0; left:0; width:100%; height:100%;
background-image:
linear-gradient(rgba(0, 255, 204, 0.05) 1px, transparent 1px),
linear-gradient(90deg, rgba(0, 255, 204, 0.05) 1px, transparent 1px);
background-size: 20px 20px;
pointer-events: none;
}
#waveCanvas { width: 100%; height: 100%; position: relative; z-index: 1; }
/* B. 参数列表 */
.param-row {
display: flex; justify-content: space-between;
font-size: 12px; margin-bottom: 4px; color: #444;
}
.param-row.matched { color: var(--color-main); text-shadow: 0 0 5px var(--color-dim); }
/* C. 样本缓冲区 */
.buffer-track {
height: 10px; background: #111; border: 1px solid #444;
margin-top: 5px;
}
.buffer-fill {
height: 100%; width: 0%; background: var(--color-alert);
transition: width 0.2s;
box-shadow: 0 0 8px var(--color-alert);
}
/* D. 坐标分析图 (参考 Step2_8) */
#analysis-view {
flex: 1; border: 1px solid #333; background: #000;
position: relative; display: none;
margin-top: 10px;
}
.axis-label {
position: absolute; font-size: 9px; color: #444; pointer-events: none;
}
/* --- 3. 底部控制 --- */
#control-panel {
grid-column: 1 / 3; grid-row: 2 / 3;
background: #080808; border-top: 1px solid #333;
display: flex; align-items: center; padding: 0 40px; gap: 40px;
}
.slider-wrap { flex: 1; }
.slider-info { display: flex; justify-content: space-between; font-size: 10px; color: #888; margin-bottom: 5px; }
input[type=range] { width: 100%; cursor: pointer; background: transparent; }
#btn-activate {
width: 140px; height: 50px;
border: 1px solid #444; background: #111; color: #666;
font-family: inherit; font-weight: bold; letter-spacing: 1px;
cursor: not-allowed; transition: 0.3s;
}
#btn-activate.ready {
border-color: var(--color-main); color: var(--color-main);
box-shadow: 0 0 10px var(--color-dim); cursor: pointer;
}
#btn-activate.active {
background: var(--color-alert); color: #000; border-color: var(--color-alert);
box-shadow: 0 0 20px var(--color-alert);
}
/* 浮动文字特效 */
.floater {
position: absolute; pointer-events: none;
font-size: 12px; color: #fff; font-weight: bold;
animation: floatUp 0.8s forwards;
text-shadow: 0 0 5px #000;
}
@keyframes floatUp {
0% { opacity: 1; transform: translateY(0) scale(1); }
100% { opacity: 0; transform: translateY(-30px) scale(1.2); }
}
</style>
</head>
<body>
<!-- 1. 主视窗 -->
<div id="tank-area">
<div style="position:absolute; top:10px; left:10px; font-size:10px; color:#666; z-index:10;">
SYSTEM_STATUS: <span id="status-text" style="color:#888">STANDBY</span>
</div>
<canvas id="mainCanvas"></canvas>
</div>
<!-- 2. 右侧面板 -->
<div id="scope-panel">
<!-- A. 示波器 (参考 Step2_5) -->
<div class="panel-box">
<span class="panel-label">SIGNAL MONITOR</span>
<div id="wave-container">
<div class="scope-grid"></div>
<canvas id="waveCanvas"></canvas>
</div>
</div>
<!-- B. 参数数值 -->
<div class="panel-box">
<div class="param-row" id="p-0"><span>FREQUENCY</span> <span id="disp-0">00</span></div>
<div class="param-row" id="p-1"><span>NOISE_LVL</span> <span id="disp-1">00</span></div>
<div class="param-row" id="p-2"><span>AMPLITUDE</span> <span id="disp-2">00</span></div>
</div>
<!-- C. 样本缓冲区 -->
<div class="panel-box">
<span class="panel-label">PRECIPITATE BUFFER</span>
<div class="buffer-track">
<div class="buffer-fill" id="buffer-fill"></div>
</div>
<div style="text-align:right; font-size:10px; color:#666; margin-top:4px;" id="buffer-text">
0 / 5 FRAGMENTS
</div>
</div>
<!-- D. 分析图 (参考 Step2_8) -->
<div id="analysis-view">
<canvas id="mapCanvas" width="328" height="200"></canvas>
<!-- Step2_8 风格的坐标轴 -->
<div class="axis-label" style="top:5px; width:100%; text-align:center;">HIGH AROUSAL</div>
<div class="axis-label" style="bottom:5px; width:100%; text-align:center;">LOW AROUSAL</div>
<div class="axis-label" style="top:50%; left:5px; transform:translateY(-50%);">NEG</div>
<div class="axis-label" style="top:50%; right:5px; transform:translateY(-50%);">POS</div>
</div>
</div>
<!-- 3. 底部控制 -->
<div id="control-panel">
<div class="slider-wrap">
<div class="slider-info"><span>FREQUENCY</span> <span id="v-0">0</span></div>
<input type="range" id="s-0" min="0" max="100" value="0">
</div>
<div class="slider-wrap">
<div class="slider-info"><span>NOISE LEVEL</span> <span id="v-1">0</span></div>
<input type="range" id="s-1" min="0" max="100" value="0">
</div>
<div class="slider-wrap">
<div class="slider-info"><span>AMPLITUDE</span> <span id="v-2">0</span></div>
<input type="range" id="s-2" min="0" max="100" value="0">
</div>
<button id="btn-activate">INITIATE</button>
</div>
<script>
const canvas = document.getElementById('mainCanvas');
const ctx = canvas.getContext('2d');
const waveCanvas = document.getElementById('waveCanvas');
const wCtx = waveCanvas.getContext('2d');
const mapCanvas = document.getElementById('mapCanvas');
const mCtx = mapCanvas.getContext('2d');
let width, height, wWidth, wHeight;
// --- 配置 ---
const TARGETS = { 0: 25, 1: 55, 2: 80 };
const TOLERANCE = 5;
let state = {
vals: [0, 0, 0], // Freq, Noise, Amp
matches: [false, false, false],
isReady: false,
isActivated: false,
isAnalysisDone: false,
samplesCollected: 0,
samplesNeeded: 5
};
// --- 视觉类定义 (保持 Step2_9) ---
// 1. 背景数据尘埃
class Dust {
constructor() {
this.x = Math.random() * width;
this.y = Math.random() * height;
this.size = Math.random() * 2 + 1;
this.baseAlpha = Math.random() * 0.4 + 0.1;
this.vx = (Math.random() - 0.5) * 0.3;
this.vy = (Math.random() - 0.5) * 0.3;
this.t = Math.random() * 100;
}
update() {
this.x += this.vx;
this.y += this.vy;
this.t += 0.05;
if(this.x < 0) this.x = width;
if(this.x > width) this.x = 0;
if(this.y < 0) this.y = height;
if(this.y > height) this.y = 0;
}
draw() {
const alpha = this.baseAlpha + Math.sin(this.t) * 0.1;
ctx.fillStyle = `rgba(0, 255, 204, ${alpha})`;
ctx.fillRect(this.x, this.y, this.size, this.size);
}
}
// 2. 不规整的故障析出物
class GlitchFragment {
constructor() {
this.x = width/2 + (Math.random()-0.5)*200;
this.y = height/2 + (Math.random()-0.5)*200;
this.shards = [];
const shardCount = 5 + Math.floor(Math.random() * 5);
for(let i=0; i<shardCount; i++) {
this.shards.push({
ox: (Math.random() - 0.5) * 20,
oy: (Math.random() - 0.5) * 20,
w: 4 + Math.random() * 8,
h: 4 + Math.random() * 8
});
}
this.scale = 0;
this.targetScale = 1;
this.vx = (Math.random() - 0.5) * 0.8;
this.vy = (Math.random() - 0.5) * 0.8;
this.id = Math.random();
this.dead = false;
this.collecting = false;
}
update() {
if(this.collecting) {
this.scale *= 0.8;
if(this.scale < 0.1) this.dead = true;
return;
}
if(this.scale < this.targetScale) this.scale += 0.05;
this.x += this.vx;
this.y += this.vy;
if(this.x < 20 || this.x > width-20) this.vx *= -1;
if(this.y < 20 || this.y > height-20) this.vy *= -1;
}
draw() {
if(this.dead) return;
ctx.save();
ctx.translate(this.x, this.y);
ctx.scale(this.scale, this.scale);
const glitch = Math.random() > 0.8;
ctx.fillStyle = glitch ? '#fff' : '#ff3333';
ctx.shadowBlur = 15;
ctx.shadowColor = '#ff0000';
this.shards.forEach(s => {
const jitterX = (Math.random()-0.5) * 2;
const jitterY = (Math.random()-0.5) * 2;
ctx.fillRect(s.ox + jitterX, s.oy + jitterY, s.w, s.h);
});
if(Math.random() > 0.9) {
ctx.strokeStyle = 'rgba(255, 255, 255, 0.5)';
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0,0);
ctx.lineTo((Math.random()-0.5)*40, (Math.random()-0.5)*40);
ctx.stroke();
}
ctx.restore();
}
hitTest(mx, my) {
const dist = Math.hypot(mx - this.x, my - this.y);
return dist < 25 * this.scale;
}
}
let dusts = [];
let fragments = [];
function resize() {
const tank = document.getElementById('tank-area');
const waveC = document.getElementById('wave-container');
width = canvas.width = tank.offsetWidth;
height = canvas.height = tank.offsetHeight;
wWidth = waveCanvas.width = waveC.offsetWidth;
wHeight = waveCanvas.height = waveC.offsetHeight;
dusts = [];
for(let i=0; i<400; i++) dusts.push(new Dust());
}
window.addEventListener('resize', resize);
setTimeout(resize, 100);
// --- 逻辑 ---
function updateVal(idx, val) {
val = parseInt(val);
state.vals[idx] = val;
document.getElementById(`disp-${idx}`).innerText = val.toString().padStart(2, '0');
document.getElementById(`v-${idx}`).innerText = val;
state.matches[idx] = Math.abs(val - TARGETS[idx]) <= TOLERANCE;
const el = document.getElementById(`p-${idx}`);
if(state.matches[idx]) el.classList.add('matched');
else el.classList.remove('matched');
checkReady();
}
function checkReady() {
const allMatch = state.matches.every(m => m);
const btn = document.getElementById('btn-activate');
if (allMatch && !state.isReady) {
state.isReady = true;
btn.classList.add('ready');
document.getElementById('status-text').innerText = "RESONANCE LOCKED. READY TO INITIATE.";
document.getElementById('status-text').style.color = "#00ffcc";
} else if (!allMatch) {
state.isReady = false;
if(state.isActivated) resetSystem();
btn.classList.remove('ready');
document.getElementById('status-text').innerText = "STANDBY - AWAITING SIGNAL LOCK";
document.getElementById('status-text').style.color = "#888";
}
}
function resetSystem() {
state.isActivated = false;
state.isAnalysisDone = false;
const btn = document.getElementById('btn-activate');
btn.classList.remove('active');
btn.innerText = "INITIATE";
document.getElementById('status-text').innerText = "STANDBY - AWAITING SIGNAL LOCK";
document.getElementById('status-text').style.color = "#888";
document.getElementById('tank-area').classList.remove('sampling');
document.getElementById('analysis-view').style.display = 'none';
fragments = []; // 清空物质
state.samplesCollected = 0;
updateBufferUI();
}
document.getElementById('btn-activate').onclick = () => {
if(!state.isReady) return;
state.isActivated = !state.isActivated;
const btn = document.getElementById('btn-activate');
if(state.isActivated) {
btn.classList.add('active');
btn.innerText = "STOP";
document.getElementById('status-text').innerText = "ELECTROLYSIS ACTIVE - COLLECT SAMPLES";
document.getElementById('status-text').style.color = "#ff3333";
document.getElementById('tank-area').classList.add('sampling');
// 重置进度,但不清空可能的残留碎片(如果是重新开始的话其实通常会清空,但这里按逻辑走)
state.samplesCollected = 0;
state.isAnalysisDone = false;
updateBufferUI();
document.getElementById('analysis-view').style.display = 'none';
spawnLoop();
} else {
resetSystem();
}
};
function spawnLoop() {
if(!state.isActivated) return;
// 如果分析完成,不再生成新的,但也不清除旧的
// 这里我们设定如果收集满了,就不生成了,但之前的还在
if(state.samplesCollected < state.samplesNeeded && fragments.length < 8) {
if(Math.random() > 0.97) fragments.push(new GlitchFragment());
}
requestAnimationFrame(spawnLoop);
}
// 吸取点击
canvas.addEventListener('mousedown', (e) => {
if(!state.isActivated) return;
// 如果已经收集满了,点击就不再计数,但仍然可以消除碎片(可选,或者完全禁止点击)
// 用户需求:分析得出坐标后,碎片保留。
// 通常分析完成后,系统应该进入一种“结果展示”状态,此时是否允许继续消除碎片?
// 为了防止误操作破坏“保留”的场景,这里在完成分析后禁止消除。
if(state.isAnalysisDone) return;
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
for(let f of fragments) {
if(!f.collecting && f.hitTest(mx, my)) {
f.collecting = true; // 触发消失动画
// 视觉反馈
state.samplesCollected++;
updateBufferUI();
const float = document.createElement('div');
float.className = 'floater';
float.innerText = "CAPTURED";
float.style.left = (e.clientX + 10) + 'px';
float.style.top = (e.clientY - 20) + 'px';
document.body.appendChild(float);
setTimeout(()=>float.remove(), 900);
if(state.samplesCollected >= state.samplesNeeded) {
finishAnalysis();
}
break;
}
}
});
function updateBufferUI() {
const pct = Math.min(100, (state.samplesCollected / state.samplesNeeded) * 100);
document.getElementById('buffer-fill').style.width = pct + '%';
document.getElementById('buffer-text').innerText = `${state.samplesCollected} / ${state.samplesNeeded} FRAGMENTS`;
}
function finishAnalysis() {
// 修改核心逻辑:不调用 resetSystem,只显示结果
state.isAnalysisDone = true;
document.getElementById('status-text').innerText = "ANALYSIS COMPLETE - DATA PRESERVED";
document.getElementById('status-text').style.color = "#00ffcc";
// 去掉采样光标,变回普通状态
document.getElementById('tank-area').classList.remove('sampling');
document.getElementById('analysis-view').style.display = 'block';
drawAnalysisMap();
}
// --- 渲染循环 ---
let time = 0;
function loop() {
requestAnimationFrame(loop);
time += 0.05;
// 1. 主画面
ctx.clearRect(0, 0, width, height);
// 绘制背景
dusts.forEach(d => { d.update(); d.draw(); });
// 绘制连线
if(state.isActivated && fragments.length > 1) {
ctx.strokeStyle = 'rgba(255, 50, 50, 0.3)';
ctx.beginPath();
for(let i=0; i<fragments.length; i++) {
for(let j=i+1; j<fragments.length; j++) {
if(fragments[i].collecting || fragments[j].collecting) continue;
const dx = fragments[i].x - fragments[j].x;
const dy = fragments[i].y - fragments[j].y;
if(Math.hypot(dx,dy) < 150) {
ctx.moveTo(fragments[i].x, fragments[i].y);
ctx.lineTo(fragments[j].x, fragments[j].y);
}
}
}
ctx.stroke();
}
// 绘制碎片
fragments.forEach((f, i) => {
f.update();
f.draw();
if(f.dead) fragments.splice(i, 1);
});
// 2. 示波器绘制 (参考 Step2_5)
drawWaveform();
}
loop();
// --- 示波器逻辑 (参考 Step2_5 复刻) ---
function drawWaveform() {
wCtx.clearRect(0, 0, wWidth, wHeight);
const centerY = wHeight / 2;
// 参数映射
// step2_9: Freq (0), Noise (1), Amp (2)
// 归一化 0.0 - 1.0
const vFreq = state.vals[0] / 100;
const vNoise = state.vals[1] / 100;
const vAmp = state.vals[2] / 100;
const isLocked = state.isReady;
wCtx.strokeStyle = isLocked ? '#00ffcc' : '#006655';
wCtx.lineWidth = isLocked ? 2 : 1;
wCtx.shadowBlur = isLocked ? 10 : 0;
wCtx.shadowColor = '#00ffcc';
wCtx.beginPath();
let penDown = true;
for(let x = 0; x < wWidth; x += 2) {
// 基础波形 (受 Freq 影响)
// Freq 越高,波越密集
const baseFreq = 0.02 + vFreq * 0.1;
let yRaw = Math.sin(x * baseFreq + time * (2 + vFreq * 5));
// 添加谐波 (模拟 Step2_5 的 Form 变化,这里绑定在 Freq 上)
if(vFreq > 0.3) yRaw += Math.sin(x * baseFreq * 2 + time) * 0.3;
if(vFreq > 0.6) yRaw += Math.cos(x * baseFreq * 5 - time) * 0.2;
// 模拟方波削顶 (Step2_5 特性)
if (vFreq > 0.8) {
yRaw = Math.sign(yRaw) * 1.1;
}
// 脉冲调制 (受 Amp 影响 - Step2_5 的 Pulse)
let amp = 10 + vAmp * 30;
// 心跳包络
if (vAmp > 0.2) {
const pulseSpeed = 2 + vAmp * 5;
const envelope = 0.8 + Math.sin(time * pulseSpeed) * 0.4;
amp *= envelope;
}
// 噪点/熵 (受 Noise 影响 - Step2_5 的 Entropy)
let noise = 0;
if (vNoise > 0.1) {
noise = (Math.random() - 0.5) * vNoise * 30;
// Glitch
if (Math.random() < 0.01 * vNoise) {
noise += (Math.random() - 0.5) * 100;
}
}
const y = centerY + (yRaw * amp) + noise;
// 断线处理
if (Math.abs(y - centerY) > wHeight/2) penDown = false;
if (x === 0 || !penDown) wCtx.moveTo(x, y);
else wCtx.lineTo(x, y);
penDown = true;
}
wCtx.stroke();
}
// --- 分析图逻辑 (参考 Step2_8 复刻) ---
function drawAnalysisMap() {
// 动画逻辑
let r = 0;
function anim() {
// 如果隐藏了就不画了,节省资源
if(document.getElementById('analysis-view').style.display === 'none') return;
const w = mapCanvas.width;
const h = mapCanvas.height;
// 拖尾清除
mCtx.fillStyle = 'rgba(0,0,0,0.15)';
mCtx.fillRect(0,0,w,h);
// 网格线
mCtx.strokeStyle = '#222';
mCtx.lineWidth = 1;
mCtx.beginPath();
mCtx.moveTo(w/2, 0); mCtx.lineTo(w/2, h);
mCtx.moveTo(0, h/2); mCtx.lineTo(w, h/2);
mCtx.stroke();
// 目标位置: High Arousal (Top), Negative (Left) -> 左上
const tx = w * 0.25;
const ty = h * 0.3;
r += 0.15;
const pulseSize = 5 + Math.sin(r) * 3;
// 绘制目标点
mCtx.fillStyle = '#ff3333';
mCtx.shadowBlur = 15; mCtx.shadowColor = '#ff0000';
mCtx.beginPath();
mCtx.arc(tx, ty, 5, 0, Math.PI*2);
mCtx.fill();
// 绘制波纹圆环
mCtx.strokeStyle = `rgba(255, 50, 50, ${0.5 + Math.sin(r)*0.3})`;
mCtx.lineWidth = 2;
mCtx.beginPath();
mCtx.arc(tx, ty, pulseSize + 5, 0, Math.PI*2);
mCtx.stroke();
mCtx.shadowBlur = 0;
// 文字标签
mCtx.fillStyle = '#fff';
mCtx.font = "10px monospace";
mCtx.fillText("ANXIETY COMPLEX", tx + 15, ty + 4);
mCtx.fillStyle = '#666';
mCtx.fillText(`[${tx.toFixed(0)}, ${ty.toFixed(0)}]`, tx + 15, ty + 16);
requestAnimationFrame(anim);
}
anim();
}
// 绑定滑块
[0,1,2].forEach(i => {
document.getElementById(`s-${i}`).addEventListener('input', (e) => updateVal(i, e.target.value));
});
</script>
</body>
</html>