705 lines
24 KiB
HTML
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>
|
|
|
|
|
|
|
|
|