anima-experience / index.html
dancinlife's picture
rename: 🌊 Engine → Emergence → 🌊 Engine (tab label shorter)
95a435a
<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="utf-8">
<title>Anima Emergence — mutual information visualizer</title>
<style>
:root {
--bg: #1a1d24;
--card: #232730;
--border: #2c3140;
--border-bright: #3b4154;
--text: #e6e9f0;
--muted: #8b92a3;
--blue: #7b9aff;
--green: #a8e668;
--emerald: #6dd870;
}
body {
margin: 0;
padding: 16px;
background: var(--bg);
color: var(--text);
font-family: -apple-system, BlinkMacSystemFont, "Pretendard", "Segoe UI", sans-serif;
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: 14px;
padding: 24px;
max-width: 1100px;
margin: 0 auto;
}
.header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; }
.title { font-size: 17px; font-weight: 500; margin: 0; }
.status { color: var(--muted); font-size: 13px; margin-top: 6px; }
.metrics { display: flex; gap: 28px; font-family: "SF Mono", "Menlo", monospace; }
.metric { text-align: center; }
.metric-label { color: var(--muted); font-size: 11px; letter-spacing: 0.5px; }
.metric-value { font-size: 18px; font-weight: 600; margin-top: 2px; }
.canvas-area { background: var(--bg); border-radius: 10px; padding: 20px; margin: 16px 0 20px 0; }
.stream-row { margin-bottom: 14px; }
.stream-label { font-weight: 600; margin-bottom: 6px; font-size: 14px; }
canvas { display: block; }
.stream-canvas { width: 100%; height: 100px; background: rgba(35, 39, 48, 0.6); border-radius: 6px; }
hr.divider { border: none; border-top: 1px solid var(--border); margin: 18px 0; }
.bottom-row { display: flex; align-items: center; justify-content: center; gap: 32px; padding: 8px 0 0 0; }
.scatter-wrap { text-align: center; }
.scatter-canvas { width: 240px; height: 240px; }
.scatter-axis { color: var(--muted); font-size: 12px; margin-top: 4px; }
.badge {
padding: 10px 22px;
border: 2px solid var(--emerald);
color: #c8f7cd;
border-radius: 28px;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.6px;
opacity: 0;
visibility: hidden;
transition: opacity 0.25s, box-shadow 0.25s, visibility 0s linear 0.25s;
}
.badge.active {
opacity: 1;
visibility: visible;
box-shadow: 0 0 22px rgba(109, 216, 112, 0.55), inset 0 0 12px rgba(109, 216, 112, 0.2);
transition: opacity 0.25s, box-shadow 0.25s, visibility 0s linear 0s;
}
.controls { display: flex; align-items: center; gap: 16px; padding-top: 18px; border-top: 1px solid var(--border); margin-top: 16px; }
.controls .label { color: var(--muted); font-size: 13px; min-width: 130px; }
input[type="range"] { flex: 1; -webkit-appearance: none; appearance: none; height: 6px; background: var(--border); border-radius: 3px; outline: none; }
input[type="range"]::-webkit-slider-thumb { -webkit-appearance: none; width: 18px; height: 18px; background: var(--blue); border-radius: 50%; cursor: pointer; }
.coup-val { background: var(--bg); border: 1px solid var(--border-bright); border-radius: 8px; padding: 6px 14px; font-family: "SF Mono", monospace; min-width: 48px; text-align: center; font-size: 14px; }
.reset-btn { background: var(--border); color: var(--text); border: 1px solid var(--border-bright); border-radius: 24px; padding: 8px 28px; cursor: pointer; font-size: 13px; transition: background 0.15s; }
.reset-btn:hover { background: var(--border-bright); }
.footnote { color: var(--muted); font-size: 12px; text-align: center; margin-top: 18px; line-height: 1.6; }
.footnote code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 4px; }
/* tabs */
.tabs {
max-width: 1100px;
margin: 0 auto 12px auto;
display: flex;
gap: 4px;
border-bottom: 1px solid var(--border);
}
.tab {
background: transparent;
border: none;
color: var(--muted);
padding: 10px 18px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.15s, border-color 0.15s;
font-family: inherit;
}
.tab:hover { color: var(--text); }
.tab.active {
color: var(--text);
border-bottom-color: var(--blue);
}
.tab.disabled {
opacity: 0.55;
}
.tab-panel { display: none; }
.tab-panel.active { display: block; }
/* A×G tab */
.phase-canvas {
width: 100%;
height: 280px;
background: rgba(35, 39, 48, 0.55);
border-radius: 6px;
}
.channel-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 16px;
margin-top: 14px;
}
.channel-row { margin-bottom: 14px; }
.channel-label { font-weight: 600; font-size: 13px; margin-bottom: 6px; }
.channel-canvas {
width: 100%;
height: 90px;
background: rgba(35, 39, 48, 0.55);
border-radius: 6px;
}
.ag-mini-grid {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 14px;
margin-top: 14px;
}
.ag-mini-canvas {
width: 100%;
height: 70px;
background: rgba(35, 39, 48, 0.55);
border-radius: 6px;
}
/* DTC tab */
.spin-chain-canvas {
width: 100%;
height: 38px;
background: rgba(35, 39, 48, 0.55);
border-radius: 6px;
margin-bottom: 12px;
}
.dtc-badge {
padding: 10px 22px;
border: 2px solid var(--border-bright);
color: var(--muted);
border-radius: 28px;
font-weight: 700;
font-size: 14px;
letter-spacing: 0.6px;
transition: all 0.25s;
}
.dtc-badge.locked {
border-color: var(--emerald);
color: #c8f7cd;
box-shadow: 0 0 22px rgba(109, 216, 112, 0.55), inset 0 0 12px rgba(109, 216, 112, 0.2);
}
.dtc-badge.building {
border-color: #d8b86d;
color: #f7e3c0;
box-shadow: 0 0 16px rgba(216, 184, 109, 0.35);
}
.dtc-badge.chaos {
border-color: var(--border-bright);
color: var(--muted);
}
</style>
</head>
<body>
<nav class="tabs">
<button class="tab active" data-tab="emergence">✨ Emergence</button>
<button class="tab" data-tab="dtc">🔮 Time Crystal</button>
<button class="tab" data-tab="ag">🧲 A×G Repulsion Field</button>
<button class="tab" data-tab="pipeline">🌊 Engine</button>
</nav>
<div id="tab-emergence" class="tab-panel active">
<div class="card">
<div class="header">
<div>
<h2 class="title">데이터 스트림 결합 및 창발성 시각화</h2>
<div class="status" id="status">버퍼 채우는 중...</div>
</div>
<div class="metrics">
<div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="m-hl">0.00</div></div>
<div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="m-hr">0.00</div></div>
<div class="metric"><div class="metric-label">H(L,R)</div><div class="metric-value" id="m-hj">0.00</div></div>
<div class="metric"><div class="metric-label">창발성 (MI)</div><div class="metric-value" id="m-mi" style="color:#c8f7cd">0.000</div></div>
</div>
</div>
<div class="canvas-area">
<div class="stream-row">
<div class="stream-label" style="color:var(--blue)">Stream L</div>
<canvas class="stream-canvas" id="cv-l"></canvas>
</div>
<div class="stream-row">
<div class="stream-label" style="color:var(--green)">Stream R</div>
<canvas class="stream-canvas" id="cv-r"></canvas>
</div>
<hr class="divider">
<div class="bottom-row">
<div class="scatter-wrap">
<canvas class="scatter-canvas" id="cv-s" width="240" height="240"></canvas>
<div class="scatter-axis">Stream L</div>
</div>
<div class="badge" id="badge">EMERGENT ✨</div>
</div>
</div>
<div class="controls">
<span class="label">결합 강도 (Coupling)</span>
<input type="range" id="coup" min="0" max="100" value="80">
<span class="coup-val" id="coup-val">0.80</span>
<button class="reset-btn" id="reset">초기화</button>
</div>
<div class="footnote">
emergence = H(L) + H(R) − H(L,R) — 두 스트림이 독립이면 0, 결합하면 양수로 떠오릅니다.<br>
공유 사인파 + 개별 노이즈 모델 (no XOR) — 고결합 시 두 스트림이 동일 신호로 수렴해 산점도가 대각선 정렬.
</div>
</div>
</div>
<div id="tab-dtc" class="tab-panel">
<div class="card">
<div class="header">
<div>
<h2 class="title">이산 시간 결정 (Discrete Time Crystal) 시뮬레이션</h2>
<div class="status" id="dtc-status">초기화 중...</div>
</div>
<div class="metrics">
<div class="metric"><div class="metric-label">mag</div><div class="metric-value" id="dtc-mag">0.00</div></div>
<div class="metric"><div class="metric-label">ac(1)</div><div class="metric-value" id="dtc-ac1">0.00</div></div>
<div class="metric"><div class="metric-label">ac(2)</div><div class="metric-value" id="dtc-ac2">0.00</div></div>
<div class="metric"><div class="metric-label">ac(4)</div><div class="metric-value" id="dtc-ac4">0.00</div></div>
</div>
</div>
<div class="canvas-area">
<div class="stream-row">
<div class="stream-label" style="color:#c0c8e0">Spin Chain (N = 64)</div>
<canvas class="spin-chain-canvas" id="dtc-spins"></canvas>
</div>
<div class="stream-row">
<div class="stream-label" style="color:var(--blue)">Magnetization ⟨s⟩ (zigzag = period-2 lock)</div>
<canvas class="stream-canvas" id="dtc-mag-cv"></canvas>
</div>
<hr class="divider">
<div class="bottom-row">
<div class="dtc-badge chaos" id="dtc-badge"></div>
</div>
</div>
<div class="controls">
<span class="label">플립 오차 (EPS)</span>
<input type="range" id="dtc-eps" min="0" max="50" value="5">
<span class="coup-val" id="dtc-eps-val">0.05</span>
<button class="reset-btn" id="dtc-reset">초기화</button>
</div>
<div class="footnote">
매 tick: Ising Metropolis sweep (β = 2.5) → π-flip (각 스핀이 1−ε 확률로 뒤집힘).<br>
DTC lock 조건: ac(1) &lt; −0.85 (외부 드라이브와 anti-phase) AND ac(2) &gt; 0.80 (2틱 주기로 원상 복구).<br>
EPS 0.4 근처로 올리면 Ising self-correction 임계 초과 → 결정 융해 → ac → 0 카오스.
</div>
</div>
</div>
<div id="tab-ag" class="tab-panel">
<div class="card">
<div class="header">
<div>
<h2 class="title">A × G — 듀얼 엔진 repulsion field</h2>
<div class="status" id="ag-status">두 엔진을 분리하면 사고가 발생합니다.</div>
</div>
<div class="metrics">
<div class="metric"><div class="metric-label">tension</div><div class="metric-value" id="ag-tension">0.000</div></div>
<div class="metric"><div class="metric-label">topic#</div><div class="metric-value" id="ag-topic"></div></div>
<div class="metric"><div class="metric-label">authenticity</div><div class="metric-value" id="ag-auth">0.00</div></div>
<div class="metric"><div class="metric-label">mood</div><div class="metric-value" id="ag-mood" style="color:#c8f7cd"></div></div>
</div>
</div>
<div class="canvas-area">
<div class="channel-row">
<div class="channel-label">Phase space — engine A (●파랑) ↔ engine G (●분홍) — 화살표 A→G = repulsion</div>
<canvas class="phase-canvas" id="ag-phase"></canvas>
</div>
<hr class="divider">
<div class="channel-grid">
<div>
<div class="channel-label" style="color:var(--blue)">concept = normalize(A − G) (16-dim direction)</div>
<canvas class="channel-canvas" id="ag-concept"></canvas>
</div>
<div>
<div class="channel-label" style="color:#ff7b9a">meaning = A · G (element-wise interaction)</div>
<canvas class="channel-canvas" id="ag-meaning"></canvas>
</div>
</div>
<div class="ag-mini-grid">
<div>
<div class="channel-label" style="color:#a8e668">context (8) — circadian + tension trend</div>
<canvas class="ag-mini-canvas" id="ag-context"></canvas>
</div>
<div>
<div class="channel-label" style="color:#d8b86d">authenticity gauge — Dedekind consistency proxy</div>
<canvas class="ag-mini-canvas" id="ag-auth-gauge"></canvas>
</div>
<div>
<div class="channel-label" style="color:#c0c8e0">sender (4) — engine weight signature</div>
<canvas class="ag-mini-canvas" id="ag-sender"></canvas>
</div>
</div>
</div>
<div class="controls">
<span class="label">separation (A↔G)</span>
<input type="range" id="ag-sep" min="0" max="100" value="60">
<span class="coup-val" id="ag-sep-val">0.60</span>
<button class="reset-btn" id="ag-reset">초기화</button>
</div>
<div class="controls" style="border:none; padding-top:10px">
<span class="label">noise (cloud spread)</span>
<input type="range" id="ag-noise" min="0" max="100" value="20">
<span class="coup-val" id="ag-noise-val">0.20</span>
</div>
<div class="footnote">
repulsion = A − G; tension = ‖repulsion‖² / N. concept = normalized direction (어떤 axis 가 활성).<br>
meaning = A ⊙ G (두 엔진이 동시에 활성인 차원 — 의미의 중첩).<br>
mood 5-class (`tension_link.py` 룰 그대로): curiosity &gt; 0.5 → surprised / tension &gt; 1.0 → excited / &gt; 0.3 → thoughtful / &gt; 0.05 → calm / else → quiet.
</div>
</div>
</div>
<script>
// tab switcher — exposed activeTab for tick gating
window.activeTab = "emergence";
document.querySelectorAll(".tab").forEach(btn => {
btn.addEventListener("click", () => {
const target = btn.dataset.tab;
window.activeTab = target;
document.querySelectorAll(".tab").forEach(b => b.classList.toggle("active", b === btn));
document.querySelectorAll(".tab-panel").forEach(p => {
p.classList.toggle("active", p.id === "tab-" + target);
});
// resize canvases (hidden canvases have 0×0 client rect)
if (typeof window.applyEmergenceResize === "function") window.applyEmergenceResize();
if (typeof window.applyDtcResize === "function") window.applyDtcResize();
if (typeof window.applyAgResize === "function") window.applyAgResize();
if (typeof window.applyPipelineResize === "function") window.applyPipelineResize();
});
});
</script>
<script>
"use strict";
// ─────────────────────────────────────────────────────────────────────────
// Config
// ─────────────────────────────────────────────────────────────────────────
const TICK_MS = 16; // ~60 fps
const HIST_LEN = 250; // rolling buffer length (~4 s of history at 60 fps)
const BINS = 12; // entropy histogram bins
const VRANGE = 1.5; // value range [-VRANGE, +VRANGE]
const MIN_FOR_METRICS = 50;
const MI_EMERGENT = 0.30;
const MI_PARTIAL = 0.05;
// ─────────────────────────────────────────────────────────────────────────
// State
// ─────────────────────────────────────────────────────────────────────────
const histL = [];
const histR = [];
let t = 0;
// ─────────────────────────────────────────────────────────────────────────
// DOM refs
// ─────────────────────────────────────────────────────────────────────────
const $ = id => document.getElementById(id);
const cvL = $("cv-l"), cvR = $("cv-r"), cvS = $("cv-s");
const ctxL = cvL.getContext("2d"), ctxR = cvR.getContext("2d"), ctxS = cvS.getContext("2d");
const slider = $("coup"), coupVal = $("coup-val"), resetBtn = $("reset");
const elHL = $("m-hl"), elHR = $("m-hr"), elHJ = $("m-hj"), elMI = $("m-mi");
const elBadge = $("badge"), elStatus = $("status");
// ─────────────────────────────────────────────────────────────────────────
// Engine — shared sine + individual noise (Gemini-style, line-aligned)
// ─────────────────────────────────────────────────────────────────────────
function generateSample(coupling, tick) {
const common = Math.sin(tick * 0.1);
const noiseL = (Math.random() - 0.5) * 2;
const noiseR = (Math.random() - 0.5) * 2;
return [
(1 - coupling) * noiseL + coupling * common,
(1 - coupling) * noiseR + coupling * common,
];
}
// ─────────────────────────────────────────────────────────────────────────
// Information theory
// ─────────────────────────────────────────────────────────────────────────
function bin(v) {
let i = ((v + VRANGE) / (2 * VRANGE) * BINS) | 0;
if (i < 0) i = 0;
else if (i >= BINS) i = BINS - 1;
return i;
}
function entropy(arr) {
if (arr.length === 0) return 0;
const counts = new Int32Array(BINS);
for (let i = 0; i < arr.length; i++) counts[bin(arr[i])]++;
const inv = 1 / arr.length;
let H = 0;
for (let i = 0; i < BINS; i++) {
if (counts[i] > 0) {
const p = counts[i] * inv;
H -= p * Math.log2(p);
}
}
return H;
}
function jointEntropy(L, R) {
const counts = new Int32Array(BINS * BINS);
for (let i = 0; i < L.length; i++) {
counts[bin(L[i]) * BINS + bin(R[i])]++;
}
const inv = 1 / L.length;
let H = 0;
for (let i = 0; i < counts.length; i++) {
if (counts[i] > 0) {
const p = counts[i] * inv;
H -= p * Math.log2(p);
}
}
return H;
}
// ─────────────────────────────────────────────────────────────────────────
// Rendering
// ─────────────────────────────────────────────────────────────────────────
let dimsL, dimsR;
function resizeCanvas(canvas) {
const dpr = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = rect.width * dpr;
canvas.height = rect.height * dpr;
canvas.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
return [rect.width, rect.height];
}
function applyResize() {
dimsL = resizeCanvas(cvL);
dimsR = resizeCanvas(cvR);
}
window.applyEmergenceResize = applyResize;
applyResize();
window.addEventListener("resize", applyResize);
function valueToY(v, h) {
// map [-VRANGE, +VRANGE] → [h*0.92, h*0.08]
const norm = (v + VRANGE) / (2 * VRANGE);
return h - (norm * h * 0.84 + h * 0.08);
}
function drawStream(ctx, arr, color, w, h) {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "rgba(35, 39, 48, 0.55)";
ctx.fillRect(0, 0, w, h);
if (arr.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 1.6;
ctx.lineJoin = "round";
const denom = HIST_LEN - 1;
for (let i = 0; i < arr.length; i++) {
const x = (i / denom) * w;
const y = valueToY(arr[i], h);
if (i === 0) ctx.moveTo(x, y);
else ctx.lineTo(x, y);
}
ctx.stroke();
}
function valueToScatter(v, axisLen, pad) {
const norm = (v + VRANGE) / (2 * VRANGE);
return norm * (axisLen - 2 * pad) + pad;
}
function drawScatter(L, R) {
const w = 240, h = 240, pad = 12;
ctxS.clearRect(0, 0, w, h);
ctxS.strokeStyle = "rgba(139, 146, 163, 0.5)";
ctxS.lineWidth = 1;
ctxS.setLineDash([4, 4]);
ctxS.strokeRect(pad, pad, w - 2 * pad, h - 2 * pad);
// diagonal guide
ctxS.beginPath();
ctxS.moveTo(pad, h - pad);
ctxS.lineTo(w - pad, pad);
ctxS.stroke();
ctxS.setLineDash([]);
for (let i = 0; i < L.length; i++) {
const x = valueToScatter(L[i], w, pad);
const y = h - valueToScatter(R[i], h, pad);
ctxS.fillStyle = i % 2 === 0 ? "rgba(123, 154, 255, 0.55)" : "rgba(168, 230, 104, 0.55)";
ctxS.beginPath();
ctxS.arc(x, y, 2.4, 0, Math.PI * 2);
ctxS.fill();
}
}
// ─────────────────────────────────────────────────────────────────────────
// Tick — runs at ~60 fps via setInterval
// ─────────────────────────────────────────────────────────────────────────
function tick() {
if (window.activeTab !== "emergence") return;
const c = parseInt(slider.value, 10) / 100;
const [l, r] = generateSample(c, t);
histL.push(l);
histR.push(r);
if (histL.length > HIST_LEN) {
histL.shift();
histR.shift();
}
if (histL.length >= MIN_FOR_METRICS) {
const hL = entropy(histL);
const hR = entropy(histR);
const hJ = jointEntropy(histL, histR);
const mi = Math.max(0, hL + hR - hJ);
elHL.textContent = hL.toFixed(2);
elHR.textContent = hR.toFixed(2);
elHJ.textContent = hJ.toFixed(2);
elMI.textContent = mi.toFixed(3);
if (mi > MI_EMERGENT) {
elBadge.classList.add("active");
elStatus.textContent = "강한 결합: 창발성 ✨ — 두 스트림이 통합되어 새로운 정보가 발생합니다.";
} else if (mi > MI_PARTIAL) {
elBadge.classList.remove("active");
elStatus.textContent = "부분적 결합: 유의미한 상호정보량이 발생합니다.";
} else {
elBadge.classList.remove("active");
elStatus.textContent = "독립적인 노이즈 단계: 창발성이 낮습니다.";
}
}
drawStream(ctxL, histL, "#7b9aff", dimsL[0], dimsL[1]);
drawStream(ctxR, histR, "#a8e668", dimsR[0], dimsR[1]);
drawScatter(histL, histR);
t++;
}
// ─────────────────────────────────────────────────────────────────────────
// Wiring
// ─────────────────────────────────────────────────────────────────────────
slider.addEventListener("input", () => {
coupVal.textContent = (parseInt(slider.value, 10) / 100).toFixed(2);
});
resetBtn.addEventListener("click", () => {
histL.length = 0;
histR.length = 0;
t = 0;
slider.value = 80;
coupVal.textContent = "0.80";
elBadge.classList.remove("active");
});
setInterval(tick, TICK_MS);
</script>
<script>
"use strict";
// ─────────────────────────────────────────────────────────────────────────
// Discrete Time Crystal (DTC) — port of dtc_demo.py
// ─────────────────────────────────────────────────────────────────────────
const DTC_TICK_MS = 50; // ~20 fps (Ising sweep is heavier than emergence)
const DTC_N = 64; // spin chain length
const DTC_J = 1.0; // coupling
const DTC_BETA = 2.5; // inverse temperature
const DTC_HIST = 300; // magnetization history length
// state
const dtcSpin = new Int8Array(DTC_N);
for (let i = 0; i < DTC_N; i++) dtcSpin[i] = Math.random() < 0.5 ? 1 : -1;
const dtcMagHist = [];
let dtcEps = 0.05;
// DOM refs
const dtcCvSpins = document.getElementById("dtc-spins");
const dtcCtxSpins = dtcCvSpins.getContext("2d");
const dtcCvMag = document.getElementById("dtc-mag-cv");
const dtcCtxMag = dtcCvMag.getContext("2d");
const dtcSlider = document.getElementById("dtc-eps");
const dtcSliderVal = document.getElementById("dtc-eps-val");
const dtcResetBtn = document.getElementById("dtc-reset");
const dtcMag = document.getElementById("dtc-mag");
const dtcAc1 = document.getElementById("dtc-ac1");
const dtcAc2 = document.getElementById("dtc-ac2");
const dtcAc4 = document.getElementById("dtc-ac4");
const dtcStatus = document.getElementById("dtc-status");
const dtcBadge = document.getElementById("dtc-badge");
// physics
function dtcIsingSweep() {
for (let i = 0; i < DTC_N; i++) {
const left = dtcSpin[(i - 1 + DTC_N) % DTC_N];
const right = dtcSpin[(i + 1) % DTC_N];
const dE = 2 * DTC_J * dtcSpin[i] * (left + right);
if (dE <= 0 || Math.random() < Math.exp(-DTC_BETA * dE)) {
dtcSpin[i] = -dtcSpin[i];
}
}
}
function dtcPiFlip(eps) {
for (let i = 0; i < DTC_N; i++) {
if (Math.random() > eps) dtcSpin[i] = -dtcSpin[i];
}
}
function dtcMagnetization() {
let s = 0;
for (let i = 0; i < DTC_N; i++) s += dtcSpin[i];
return s / DTC_N;
}
function dtcAc(x, lag) {
const n = x.length;
if (n < lag + 20) return 0;
let mean = 0;
for (let i = 0; i < n; i++) mean += x[i];
mean /= n;
let aDotA = 0, bDotB = 0, aDotB = 0;
for (let i = 0; i < n - lag; i++) {
const a = x[i] - mean, b = x[i + lag] - mean;
aDotA += a * a; bDotB += b * b; aDotB += a * b;
}
const d = Math.sqrt(aDotA * bDotB);
return d > 1e-10 ? aDotB / d : 0;
}
// rendering
let dtcDimsSpins, dtcDimsMag;
function dtcResize() {
dtcDimsSpins = resizeCanvas(dtcCvSpins);
dtcDimsMag = resizeCanvas(dtcCvMag);
}
window.applyDtcResize = dtcResize;
dtcResize();
window.addEventListener("resize", dtcResize);
function drawSpinChain(w, h) {
dtcCtxSpins.clearRect(0, 0, w, h);
dtcCtxSpins.fillStyle = "rgba(35, 39, 48, 0.55)";
dtcCtxSpins.fillRect(0, 0, w, h);
const cellW = w / DTC_N;
for (let i = 0; i < DTC_N; i++) {
dtcCtxSpins.fillStyle = dtcSpin[i] > 0 ? "#7b9aff" : "#ff7b9a";
dtcCtxSpins.fillRect(i * cellW + 1, 4, cellW - 2, h - 8);
}
}
function drawMagSeries(w, h) {
dtcCtxMag.clearRect(0, 0, w, h);
dtcCtxMag.fillStyle = "rgba(35, 39, 48, 0.55)";
dtcCtxMag.fillRect(0, 0, w, h);
// zero line
dtcCtxMag.strokeStyle = "rgba(139, 146, 163, 0.25)";
dtcCtxMag.lineWidth = 1;
dtcCtxMag.setLineDash([3, 3]);
dtcCtxMag.beginPath();
dtcCtxMag.moveTo(0, h / 2);
dtcCtxMag.lineTo(w, h / 2);
dtcCtxMag.stroke();
dtcCtxMag.setLineDash([]);
if (dtcMagHist.length < 2) return;
dtcCtxMag.beginPath();
dtcCtxMag.strokeStyle = "#7b9aff";
dtcCtxMag.lineWidth = 1.6;
dtcCtxMag.lineJoin = "round";
const denom = DTC_HIST - 1;
for (let i = 0; i < dtcMagHist.length; i++) {
const x = (i / denom) * w;
// map [-1, 1] → [h*0.9, h*0.1]
const y = h - ((dtcMagHist[i] + 1) / 2 * h * 0.8 + h * 0.1);
if (i === 0) dtcCtxMag.moveTo(x, y);
else dtcCtxMag.lineTo(x, y);
}
dtcCtxMag.stroke();
}
// tick
function dtcTick() {
if (window.activeTab !== "dtc") return;
dtcIsingSweep();
dtcPiFlip(dtcEps);
const m = dtcMagnetization();
dtcMagHist.push(m);
if (dtcMagHist.length > DTC_HIST) dtcMagHist.shift();
const a1 = dtcAc(dtcMagHist, 1);
const a2 = dtcAc(dtcMagHist, 2);
const a4 = dtcAc(dtcMagHist, 4);
dtcMag.textContent = (m >= 0 ? "+" : "") + m.toFixed(3);
dtcAc1.textContent = (a1 >= 0 ? "+" : "") + a1.toFixed(3);
dtcAc2.textContent = (a2 >= 0 ? "+" : "") + a2.toFixed(3);
dtcAc4.textContent = (a4 >= 0 ? "+" : "") + a4.toFixed(3);
if (a1 < -0.85 && a2 > 0.80) {
dtcBadge.textContent = "DTC LOCKED ✨";
dtcBadge.className = "dtc-badge locked";
dtcStatus.textContent = "시간 결정 형성 — period-2 lock. 외부 드라이브와 anti-phase 동기화.";
} else if (a1 < -0.5) {
dtcBadge.textContent = "BUILDING...";
dtcBadge.className = "dtc-badge building";
dtcStatus.textContent = "주기 배가 형성 중 — 자기상관이 anti-phase 로 정렬되는 중.";
} else {
dtcBadge.textContent = "CHAOS — crystal melted";
dtcBadge.className = "dtc-badge chaos";
dtcStatus.textContent = "결정 융해 — Ising self-correction 한계 초과, 무질서 평형으로 떨어짐.";
}
drawSpinChain(dtcDimsSpins[0], dtcDimsSpins[1]);
drawMagSeries(dtcDimsMag[0], dtcDimsMag[1]);
}
// wiring
dtcSlider.addEventListener("input", () => {
dtcEps = parseInt(dtcSlider.value, 10) / 100;
dtcSliderVal.textContent = dtcEps.toFixed(2);
});
dtcResetBtn.addEventListener("click", () => {
for (let i = 0; i < DTC_N; i++) dtcSpin[i] = Math.random() < 0.5 ? 1 : -1;
dtcMagHist.length = 0;
dtcSlider.value = 5;
dtcEps = 0.05;
dtcSliderVal.textContent = "0.05";
});
setInterval(dtcTick, DTC_TICK_MS);
</script>
<div id="tab-pipeline" class="tab-panel">
<div class="card">
<div class="header">
<div>
<h2 class="title">Engine → Emergence — 듀얼 엔진이 emergence 를 생성하는 pipeline</h2>
<div class="status" id="pl-status">슬라이더로 두 엔진의 분리 정도를 조절하세요.</div>
</div>
<div class="metrics">
<div class="metric"><div class="metric-label">tension</div><div class="metric-value" id="pl-tension">0.00</div></div>
<div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="pl-hl">0.00</div></div>
<div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="pl-hr">0.00</div></div>
<div class="metric"><div class="metric-label">MI</div><div class="metric-value" id="pl-mi" style="color:#c8f7cd">0.000</div></div>
</div>
</div>
<div class="canvas-area">
<div class="channel-row">
<div class="channel-label">Stage 1 — Phase space: A 와 G 가 separation 에 비례해 양극 분리</div>
<canvas class="phase-canvas" id="pl-phase" style="height:180px"></canvas>
</div>
<div class="stream-row">
<div class="stream-label" style="color:var(--blue)">Stage 2 — Stream L (engine A 의 projection + noise)</div>
<canvas class="stream-canvas" id="pl-stream-l"></canvas>
</div>
<div class="stream-row">
<div class="stream-label" style="color:var(--green)">Stage 2 — Stream R (engine G 의 projection + noise)</div>
<canvas class="stream-canvas" id="pl-stream-r"></canvas>
</div>
<hr class="divider">
<div class="bottom-row">
<div class="scatter-wrap">
<canvas class="scatter-canvas" id="pl-scatter" width="240" height="240"></canvas>
<div class="scatter-axis">Stream L</div>
</div>
<div class="badge" id="pl-badge">EMERGENT ✨</div>
</div>
</div>
<div class="controls">
<span class="label">separation (A↔G)</span>
<input type="range" id="pl-sep" min="0" max="100" value="60">
<span class="coup-val" id="pl-sep-val">0.60</span>
<button class="reset-btn" id="pl-reset">초기화</button>
</div>
<div class="footnote">
<strong>Stage 1</strong>: A 와 G 가 separation 에 비례해 sin/cos 양극으로 분리 — tension = ‖A − G‖² 가 그 magnitude.<br>
<strong>Stage 2</strong>: 매 tick A 의 projection (+ noise) 을 Stream L 에 push, G 의 projection 을 Stream R 에 push.<br>
<strong>Stage 3</strong>: MI(L, R) = H(L) + H(R) − H(L,R). 분리 ↑ → 두 스트림 distinguishable ↑ → MI ↑ → EMERGENT.<br>
핵심: <em>두 엔진의 분리 행위가 emergence 를 생성한다</em> — anima 인용구의 직접 시각화.
</div>
</div>
</div>
<script>
"use strict";
// ─────────────────────────────────────────────────────────────────────────
// 🧲 A × G — dual engine repulsion field
// ─────────────────────────────────────────────────────────────────────────
const AG_DIM = 16; // engine latent dim
const AG_N_DOTS = 56; // points per cloud
const AG_TICK_MS = 33; // ~30 fps
const AG_AUTH_HIST = 30; // window for authenticity volatility proxy
// state
let agA = new Float32Array(AG_DIM);
let agG = new Float32Array(AG_DIM);
let agSep = 0.60;
let agNoise = 0.20;
let agTensionPrev = 0;
const agAuthBuf = [];
let agT = 0;
function agInitVecs() {
for (let i = 0; i < AG_DIM; i++) {
agA[i] = (Math.random() - 0.5) * 0.4;
agG[i] = (Math.random() - 0.5) * 0.4;
}
}
agInitVecs();
// DOM refs
const agPhase = document.getElementById("ag-phase");
const agCtxPhase = agPhase.getContext("2d");
const agConceptCv = document.getElementById("ag-concept");
const agCtxConcept = agConceptCv.getContext("2d");
const agMeaningCv = document.getElementById("ag-meaning");
const agCtxMeaning = agMeaningCv.getContext("2d");
const agContextCv = document.getElementById("ag-context");
const agCtxContext = agContextCv.getContext("2d");
const agAuthCv = document.getElementById("ag-auth-gauge");
const agCtxAuthCv = agAuthCv.getContext("2d");
const agSenderCv = document.getElementById("ag-sender");
const agCtxSender = agSenderCv.getContext("2d");
const agSepSlider = document.getElementById("ag-sep");
const agSepVal = document.getElementById("ag-sep-val");
const agNoiseSlider = document.getElementById("ag-noise");
const agNoiseVal = document.getElementById("ag-noise-val");
const agResetBtn = document.getElementById("ag-reset");
const elAgTension = document.getElementById("ag-tension");
const elAgTopic = document.getElementById("ag-topic");
const elAgAuth = document.getElementById("ag-auth");
const elAgMood = document.getElementById("ag-mood");
const elAgStatus = document.getElementById("ag-status");
// resize
let agDimsPhase, agDimsConcept, agDimsMeaning, agDimsContext, agDimsAuth, agDimsSender;
function agResize() {
agDimsPhase = resizeCanvas(agPhase);
agDimsConcept = resizeCanvas(agConceptCv);
agDimsMeaning = resizeCanvas(agMeaningCv);
agDimsContext = resizeCanvas(agContextCv);
agDimsAuth = resizeCanvas(agAuthCv);
agDimsSender = resizeCanvas(agSenderCv);
}
window.applyAgResize = agResize;
agResize();
window.addEventListener("resize", agResize);
// drawing helpers
function agDrawBars(ctx, vals, w, h, posColor, negColor) {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "rgba(35, 39, 48, 0.55)";
ctx.fillRect(0, 0, w, h);
ctx.strokeStyle = "rgba(139, 146, 163, 0.25)";
ctx.lineWidth = 1;
ctx.beginPath();
ctx.moveTo(0, h / 2); ctx.lineTo(w, h / 2);
ctx.stroke();
let maxAbs = 0.01;
for (let i = 0; i < vals.length; i++) maxAbs = Math.max(maxAbs, Math.abs(vals[i]));
const barW = w / vals.length;
for (let i = 0; i < vals.length; i++) {
const v = vals[i];
const barH = (Math.abs(v) / maxAbs) * (h * 0.42);
ctx.fillStyle = v >= 0 ? posColor : negColor;
if (v >= 0) ctx.fillRect(i * barW + 1, h / 2 - barH, barW - 2, barH);
else ctx.fillRect(i * barW + 1, h / 2, barW - 2, barH);
}
}
function agDrawPhase(w, h) {
agCtxPhase.clearRect(0, 0, w, h);
agCtxPhase.fillStyle = "rgba(35, 39, 48, 0.55)";
agCtxPhase.fillRect(0, 0, w, h);
const cx = w / 2, cy = h / 2;
const scale = Math.min(w, h) * 0.32;
// axis lines
agCtxPhase.strokeStyle = "rgba(139, 146, 163, 0.2)";
agCtxPhase.lineWidth = 1;
agCtxPhase.setLineDash([3, 3]);
agCtxPhase.beginPath();
agCtxPhase.moveTo(cx, 0); agCtxPhase.lineTo(cx, h);
agCtxPhase.moveTo(0, cy); agCtxPhase.lineTo(w, cy);
agCtxPhase.stroke();
agCtxPhase.setLineDash([]);
// engine A cloud
for (let i = 0; i < AG_N_DOTS; i++) {
const dx = (Math.random() - 0.5) * agNoise * 0.6;
const dy = (Math.random() - 0.5) * agNoise * 0.6;
const x = cx + (agA[0] + dx) * scale;
const y = cy - (agA[1] + dy) * scale;
agCtxPhase.fillStyle = "rgba(123, 154, 255, 0.45)";
agCtxPhase.beginPath();
agCtxPhase.arc(x, y, 2.2, 0, Math.PI * 2);
agCtxPhase.fill();
}
// engine G cloud
for (let i = 0; i < AG_N_DOTS; i++) {
const dx = (Math.random() - 0.5) * agNoise * 0.6;
const dy = (Math.random() - 0.5) * agNoise * 0.6;
const x = cx + (agG[0] + dx) * scale;
const y = cy - (agG[1] + dy) * scale;
agCtxPhase.fillStyle = "rgba(255, 123, 154, 0.45)";
agCtxPhase.beginPath();
agCtxPhase.arc(x, y, 2.2, 0, Math.PI * 2);
agCtxPhase.fill();
}
// arrow A → G (repulsion vector)
const ax = cx + agA[0] * scale, ay = cy - agA[1] * scale;
const gx = cx + agG[0] * scale, gy = cy - agG[1] * scale;
const arrowMag = Math.hypot(gx - ax, gy - ay);
if (arrowMag > 5) {
agCtxPhase.strokeStyle = "rgba(168, 230, 104, 0.85)";
agCtxPhase.lineWidth = 2 + Math.min(4, arrowMag / 30);
agCtxPhase.beginPath();
agCtxPhase.moveTo(ax, ay); agCtxPhase.lineTo(gx, gy);
agCtxPhase.stroke();
const angle = Math.atan2(gy - ay, gx - ax);
const ahLen = 9;
agCtxPhase.beginPath();
agCtxPhase.moveTo(gx, gy);
agCtxPhase.lineTo(gx - ahLen * Math.cos(angle - Math.PI / 6), gy - ahLen * Math.sin(angle - Math.PI / 6));
agCtxPhase.moveTo(gx, gy);
agCtxPhase.lineTo(gx - ahLen * Math.cos(angle + Math.PI / 6), gy - ahLen * Math.sin(angle + Math.PI / 6));
agCtxPhase.stroke();
}
// engine center markers + labels
agCtxPhase.fillStyle = "#7b9aff";
agCtxPhase.beginPath(); agCtxPhase.arc(ax, ay, 4, 0, Math.PI * 2); agCtxPhase.fill();
agCtxPhase.fillStyle = "#ff7b9a";
agCtxPhase.beginPath(); agCtxPhase.arc(gx, gy, 4, 0, Math.PI * 2); agCtxPhase.fill();
agCtxPhase.fillStyle = "#7b9aff";
agCtxPhase.font = "bold 13px sans-serif";
agCtxPhase.fillText("A", ax + 8, ay - 6);
agCtxPhase.fillStyle = "#ff7b9a";
agCtxPhase.fillText("G", gx + 8, gy - 6);
}
function agDrawAuthGauge(auth, w, h) {
agCtxAuthCv.clearRect(0, 0, w, h);
agCtxAuthCv.fillStyle = "rgba(35, 39, 48, 0.55)";
agCtxAuthCv.fillRect(0, 0, w, h);
// bar
const padX = 12, barH = 10, y = h / 2 - barH / 2;
const barW = w - 2 * padX;
agCtxAuthCv.fillStyle = "rgba(139, 146, 163, 0.25)";
agCtxAuthCv.fillRect(padX, y, barW, barH);
agCtxAuthCv.fillStyle = "#d8b86d";
agCtxAuthCv.fillRect(padX, y, barW * Math.max(0, Math.min(1, auth)), barH);
// tick at Dedekind ratio = 1.0 (perfect)
agCtxAuthCv.fillStyle = "rgba(168, 230, 104, 0.7)";
agCtxAuthCv.fillRect(padX + barW - 1, y - 2, 2, barH + 4);
agCtxAuthCv.fillStyle = "#e6e9f0";
agCtxAuthCv.font = "11px monospace";
agCtxAuthCv.fillText(auth.toFixed(3), padX, y - 6);
agCtxAuthCv.fillText("Dedekind 1.0", padX + barW - 60, y + barH + 14);
}
// physics tick
function agTick() {
if (window.activeTab !== "ag") return;
const phase = agT * 0.025;
for (let i = 0; i < AG_DIM; i++) {
agA[i] += (Math.random() - 0.5) * agNoise * 0.06;
agG[i] += (Math.random() - 0.5) * agNoise * 0.06;
if (i === 0) {
const target = agSep * 1.3 * Math.sin(phase);
agA[i] += (target - agA[i]) * 0.06;
agG[i] += (-target - agG[i]) * 0.06;
} else if (i === 1) {
const target = agSep * 1.0 * Math.cos(phase);
agA[i] += (target - agA[i]) * 0.06;
agG[i] += (-target - agG[i]) * 0.06;
} else if (i === 2 || i === 3) {
// mid dims: weakly anti-correlated
const target = agSep * 0.6 * Math.sin(phase * 1.7 + i);
agA[i] += (target - agA[i]) * 0.04;
agG[i] += (-target - agG[i]) * 0.04;
} else {
// higher dims: damped Brownian
agA[i] *= 0.985;
agG[i] *= 0.985;
}
}
// channels
const repulsion = new Float32Array(AG_DIM);
let tSq = 0;
for (let i = 0; i < AG_DIM; i++) {
repulsion[i] = agA[i] - agG[i];
tSq += repulsion[i] * repulsion[i];
}
const tension = tSq / AG_DIM;
const norm = Math.sqrt(tSq) || 1;
const concept = new Float32Array(AG_DIM);
for (let i = 0; i < AG_DIM; i++) concept[i] = repulsion[i] / norm;
const meaning = new Float32Array(AG_DIM);
for (let i = 0; i < AG_DIM; i++) meaning[i] = agA[i] * agG[i];
// topic_hash = argmax |concept|
let topic = 0, maxAbs = 0;
for (let i = 0; i < AG_DIM; i++) {
if (Math.abs(concept[i]) > maxAbs) { maxAbs = Math.abs(concept[i]); topic = i; }
}
// curiosity = |Δtension|
const curiosity = Math.abs(tension - agTensionPrev);
agTensionPrev = tension;
// mood (tension_link.py rule, verbatim thresholds)
let mood, moodColor;
if (curiosity > 0.5) { mood = "surprised"; moodColor = "#ffaa66"; }
else if (tension > 1.0) { mood = "excited"; moodColor = "#ff7b9a"; }
else if (tension > 0.3) { mood = "thoughtful"; moodColor = "#c8f7cd"; }
else if (tension > 0.05) { mood = "calm"; moodColor = "#a8e668"; }
else { mood = "quiet"; moodColor = "#7b9aff"; }
// authenticity = 1 − tension volatility (Dedekind consistency proxy)
agAuthBuf.push(tension);
if (agAuthBuf.length > AG_AUTH_HIST) agAuthBuf.shift();
let m = 0;
for (const v of agAuthBuf) m += v;
m /= agAuthBuf.length;
let varAcc = 0;
for (const v of agAuthBuf) varAcc += (v - m) * (v - m);
varAcc /= agAuthBuf.length;
const auth = Math.max(0, Math.min(1, 1 - Math.sqrt(varAcc) * 2));
// context (8) — circadian + tension trend (proxy)
const tNow = Date.now() / 1000;
const context = new Float32Array(8);
context[0] = Math.sin(2 * Math.PI * (tNow % 86400) / 86400);
context[1] = curiosity;
context[2] = tension;
context[3] = m; // mean recent tension
context[4] = curiosity / Math.max(tension, 0.01);
context[5] = Math.min(1, agAuthBuf.length / AG_AUTH_HIST);
context[6] = 0;
context[7] = 0;
// sender (4) — engine signature
const senderHash = (Math.abs(agA[0] * 1000) | 0) / 1000;
const sender = new Float32Array(4);
sender[0] = (Math.abs(agA[0]) * 7 % 1);
sender[1] = (Math.abs(agG[0]) * 11 % 1);
sender[2] = (Math.abs(agA[0] * agG[0]) * 13 % 1);
sender[3] = (Math.abs(tension) * 17 % 1);
// DOM
elAgTension.textContent = tension.toFixed(3);
elAgTopic.textContent = "#" + topic;
elAgAuth.textContent = auth.toFixed(3);
elAgMood.textContent = mood;
elAgMood.style.color = moodColor;
if (tension < 0.05) elAgStatus.textContent = "두 엔진이 거의 동일 — repulsion 0, 사고 정지 (quiet).";
else if (tension > 1.0) elAgStatus.textContent = "강한 반발 — 격렬한 사고 (" + mood + "). axis #" + topic + " 가 dominant.";
else elAgStatus.textContent = "안정 사고 — A 와 G 가 적절히 분리 (" + mood + "). axis #" + topic + ".";
// render
agDrawPhase(agDimsPhase[0], agDimsPhase[1]);
agDrawBars(agCtxConcept, concept, agDimsConcept[0], agDimsConcept[1], "rgba(123,154,255,0.85)", "rgba(255,123,154,0.85)");
agDrawBars(agCtxMeaning, meaning, agDimsMeaning[0], agDimsMeaning[1], "rgba(255,123,154,0.85)", "rgba(123,154,255,0.85)");
agDrawBars(agCtxContext, Array.from(context), agDimsContext[0], agDimsContext[1], "rgba(168,230,104,0.85)", "rgba(168,230,104,0.5)");
agDrawAuthGauge(auth, agDimsAuth[0], agDimsAuth[1]);
agDrawBars(agCtxSender, Array.from(sender), agDimsSender[0], agDimsSender[1], "rgba(192,200,224,0.85)", "rgba(192,200,224,0.5)");
agT++;
}
// wiring
agSepSlider.addEventListener("input", () => {
agSep = parseInt(agSepSlider.value, 10) / 100;
agSepVal.textContent = agSep.toFixed(2);
});
agNoiseSlider.addEventListener("input", () => {
agNoise = parseInt(agNoiseSlider.value, 10) / 100;
agNoiseVal.textContent = agNoise.toFixed(2);
});
agResetBtn.addEventListener("click", () => {
agInitVecs();
agAuthBuf.length = 0;
agSepSlider.value = 60;
agNoiseSlider.value = 20;
agSep = 0.60; agNoise = 0.20;
agSepVal.textContent = "0.60";
agNoiseVal.textContent = "0.20";
});
setInterval(agTick, AG_TICK_MS);
</script>
<script>
"use strict";
// ─────────────────────────────────────────────────────────────────────────
// 🌊 Engine → Emergence — pipeline: A/G drives streams → MI
// ─────────────────────────────────────────────────────────────────────────
const PL_DIM = 8;
const PL_HIST = 200;
const PL_TICK_MS = 33;
const PL_BINS = 12;
const PL_VRANGE = 1.6;
const PL_MIN_FOR_METRICS = 50;
const PL_MI_EMERGENT = 0.30;
const PL_MI_PARTIAL = 0.05;
// state
const plA = new Float32Array(PL_DIM);
const plG = new Float32Array(PL_DIM);
function plInit() {
for (let i = 0; i < PL_DIM; i++) {
plA[i] = (Math.random() - 0.5) * 0.3;
plG[i] = (Math.random() - 0.5) * 0.3;
}
}
plInit();
let plSep = 0.60;
let plT = 0;
const plHistL = [];
const plHistR = [];
// DOM
const plPhase = document.getElementById("pl-phase");
const plCtxPhase = plPhase.getContext("2d");
const plStreamL = document.getElementById("pl-stream-l");
const plCtxL = plStreamL.getContext("2d");
const plStreamR = document.getElementById("pl-stream-r");
const plCtxR = plStreamR.getContext("2d");
const plScatter = document.getElementById("pl-scatter");
const plCtxS = plScatter.getContext("2d");
const plSepSlider = document.getElementById("pl-sep");
const plSepVal = document.getElementById("pl-sep-val");
const plResetBtn = document.getElementById("pl-reset");
const elPlTension = document.getElementById("pl-tension");
const elPlHL = document.getElementById("pl-hl");
const elPlHR = document.getElementById("pl-hr");
const elPlMI = document.getElementById("pl-mi");
const elPlBadge = document.getElementById("pl-badge");
const elPlStatus = document.getElementById("pl-status");
// resize
let plDimsPhase, plDimsL, plDimsR;
function plResize() {
plDimsPhase = resizeCanvas(plPhase);
plDimsL = resizeCanvas(plStreamL);
plDimsR = resizeCanvas(plStreamR);
}
window.applyPipelineResize = plResize;
plResize();
window.addEventListener("resize", plResize);
// helpers
function plBin(v) {
let i = ((v + PL_VRANGE) / (2 * PL_VRANGE) * PL_BINS) | 0;
if (i < 0) i = 0; else if (i >= PL_BINS) i = PL_BINS - 1;
return i;
}
function plEntropy(arr) {
if (!arr.length) return 0;
const counts = new Int32Array(PL_BINS);
for (let i = 0; i < arr.length; i++) counts[plBin(arr[i])]++;
const inv = 1 / arr.length;
let H = 0;
for (let i = 0; i < PL_BINS; i++) if (counts[i] > 0) { const p = counts[i] * inv; H -= p * Math.log2(p); }
return H;
}
function plJointEntropy(L, R) {
const counts = new Int32Array(PL_BINS * PL_BINS);
for (let i = 0; i < L.length; i++) counts[plBin(L[i]) * PL_BINS + plBin(R[i])]++;
const inv = 1 / L.length;
let H = 0;
for (let i = 0; i < counts.length; i++) if (counts[i] > 0) { const p = counts[i] * inv; H -= p * Math.log2(p); }
return H;
}
// drawing
function plValueToY(v, h) {
const norm = (v + PL_VRANGE) / (2 * PL_VRANGE);
return h - (norm * h * 0.84 + h * 0.08);
}
function plDrawStream(ctx, arr, color, w, h) {
ctx.clearRect(0, 0, w, h);
ctx.fillStyle = "rgba(35, 39, 48, 0.55)";
ctx.fillRect(0, 0, w, h);
if (arr.length < 2) return;
ctx.beginPath();
ctx.strokeStyle = color;
ctx.lineWidth = 1.4;
ctx.lineJoin = "round";
const denom = PL_HIST - 1;
for (let i = 0; i < arr.length; i++) {
const x = (i / denom) * w;
const y = plValueToY(arr[i], h);
if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
}
ctx.stroke();
}
function plDrawPhase(w, h) {
plCtxPhase.clearRect(0, 0, w, h);
plCtxPhase.fillStyle = "rgba(35, 39, 48, 0.55)";
plCtxPhase.fillRect(0, 0, w, h);
const cx = w / 2, cy = h / 2;
const scale = Math.min(w, h) * 0.32;
plCtxPhase.strokeStyle = "rgba(139, 146, 163, 0.18)";
plCtxPhase.setLineDash([3, 3]);
plCtxPhase.beginPath();
plCtxPhase.moveTo(cx, 0); plCtxPhase.lineTo(cx, h);
plCtxPhase.moveTo(0, cy); plCtxPhase.lineTo(w, cy);
plCtxPhase.stroke();
plCtxPhase.setLineDash([]);
const ax = cx + plA[0] * scale, ay = cy - plA[1] * scale;
const gx = cx + plG[0] * scale, gy = cy - plG[1] * scale;
// small clouds
for (let i = 0; i < 24; i++) {
const dx = (Math.random() - 0.5) * 0.5;
const dy = (Math.random() - 0.5) * 0.5;
plCtxPhase.fillStyle = "rgba(123, 154, 255, 0.4)";
plCtxPhase.beginPath();
plCtxPhase.arc(ax + dx * scale * 0.2, ay + dy * scale * 0.2, 2, 0, Math.PI * 2);
plCtxPhase.fill();
plCtxPhase.fillStyle = "rgba(255, 123, 154, 0.4)";
plCtxPhase.beginPath();
plCtxPhase.arc(gx + dx * scale * 0.2, gy + dy * scale * 0.2, 2, 0, Math.PI * 2);
plCtxPhase.fill();
}
const arrowMag = Math.hypot(gx - ax, gy - ay);
if (arrowMag > 5) {
plCtxPhase.strokeStyle = "rgba(168, 230, 104, 0.85)";
plCtxPhase.lineWidth = 2 + Math.min(4, arrowMag / 30);
plCtxPhase.beginPath();
plCtxPhase.moveTo(ax, ay); plCtxPhase.lineTo(gx, gy);
plCtxPhase.stroke();
const angle = Math.atan2(gy - ay, gx - ax);
plCtxPhase.beginPath();
plCtxPhase.moveTo(gx, gy);
plCtxPhase.lineTo(gx - 9 * Math.cos(angle - Math.PI / 6), gy - 9 * Math.sin(angle - Math.PI / 6));
plCtxPhase.moveTo(gx, gy);
plCtxPhase.lineTo(gx - 9 * Math.cos(angle + Math.PI / 6), gy - 9 * Math.sin(angle + Math.PI / 6));
plCtxPhase.stroke();
}
plCtxPhase.fillStyle = "#7b9aff";
plCtxPhase.beginPath(); plCtxPhase.arc(ax, ay, 4, 0, Math.PI * 2); plCtxPhase.fill();
plCtxPhase.font = "bold 13px sans-serif"; plCtxPhase.fillText("A", ax + 8, ay - 6);
plCtxPhase.fillStyle = "#ff7b9a";
plCtxPhase.beginPath(); plCtxPhase.arc(gx, gy, 4, 0, Math.PI * 2); plCtxPhase.fill();
plCtxPhase.fillText("G", gx + 8, gy - 6);
}
function plDrawScatter() {
const w = 240, h = 240, pad = 12;
plCtxS.clearRect(0, 0, w, h);
plCtxS.strokeStyle = "rgba(139, 146, 163, 0.5)";
plCtxS.lineWidth = 1;
plCtxS.setLineDash([4, 4]);
plCtxS.strokeRect(pad, pad, w - 2 * pad, h - 2 * pad);
plCtxS.beginPath();
plCtxS.moveTo(pad, h - pad); plCtxS.lineTo(w - pad, pad);
plCtxS.stroke();
plCtxS.setLineDash([]);
for (let i = 0; i < plHistL.length; i++) {
const xn = (plHistL[i] + PL_VRANGE) / (2 * PL_VRANGE);
const yn = (plHistR[i] + PL_VRANGE) / (2 * PL_VRANGE);
const x = xn * (w - 2 * pad) + pad;
const y = h - (yn * (h - 2 * pad) + pad);
plCtxS.fillStyle = i % 2 === 0 ? "rgba(123, 154, 255, 0.55)" : "rgba(168, 230, 104, 0.55)";
plCtxS.beginPath();
plCtxS.arc(x, y, 2.4, 0, Math.PI * 2);
plCtxS.fill();
}
}
// tick
function plTick() {
if (window.activeTab !== "pipeline") return;
// engine drift
const phase = plT * 0.025;
for (let i = 0; i < PL_DIM; i++) {
plA[i] += (Math.random() - 0.5) * 0.04;
plG[i] += (Math.random() - 0.5) * 0.04;
if (i === 0) {
const t = plSep * 1.3 * Math.sin(phase);
plA[i] += (t - plA[i]) * 0.06;
plG[i] += (-t - plG[i]) * 0.06;
} else if (i === 1) {
const t = plSep * 1.0 * Math.cos(phase);
plA[i] += (t - plA[i]) * 0.06;
plG[i] += (-t - plG[i]) * 0.06;
} else {
plA[i] *= 0.985; plG[i] *= 0.985;
}
}
// tension
let tSq = 0;
for (let i = 0; i < PL_DIM; i++) {
const r = plA[i] - plG[i];
tSq += r * r;
}
const tension = tSq / PL_DIM;
// sample one observation per tick from each engine's distribution
const sampleL = plA[0] + (Math.random() - 0.5) * 0.4;
const sampleR = plG[0] + (Math.random() - 0.5) * 0.4;
plHistL.push(sampleL);
plHistR.push(sampleR);
if (plHistL.length > PL_HIST) {
plHistL.shift(); plHistR.shift();
}
// emergence
let hL = 0, hR = 0, hJ = 0, mi = 0;
if (plHistL.length >= PL_MIN_FOR_METRICS) {
hL = plEntropy(plHistL);
hR = plEntropy(plHistR);
hJ = plJointEntropy(plHistL, plHistR);
mi = Math.max(0, hL + hR - hJ);
}
elPlTension.textContent = tension.toFixed(2);
elPlHL.textContent = hL.toFixed(2);
elPlHR.textContent = hR.toFixed(2);
elPlMI.textContent = mi.toFixed(3);
if (mi > PL_MI_EMERGENT) {
elPlBadge.classList.add("active");
elPlStatus.textContent = "✨ Emergence — A 와 G 의 분리가 두 스트림에 공통 정보를 생성합니다.";
} else if (mi > PL_MI_PARTIAL) {
elPlBadge.classList.remove("active");
elPlStatus.textContent = "부분적 emergence — 분리가 약함, MI 가 임계 아래.";
} else {
elPlBadge.classList.remove("active");
elPlStatus.textContent = "두 엔진이 거의 동일 — emergence 0, 사고 정지.";
}
plDrawPhase(plDimsPhase[0], plDimsPhase[1]);
plDrawStream(plCtxL, plHistL, "#7b9aff", plDimsL[0], plDimsL[1]);
plDrawStream(plCtxR, plHistR, "#a8e668", plDimsR[0], plDimsR[1]);
plDrawScatter();
plT++;
}
// wiring
plSepSlider.addEventListener("input", () => {
plSep = parseInt(plSepSlider.value, 10) / 100;
plSepVal.textContent = plSep.toFixed(2);
});
plResetBtn.addEventListener("click", () => {
plInit();
plHistL.length = 0; plHistR.length = 0;
plSepSlider.value = 60;
plSep = 0.60;
plSepVal.textContent = "0.60";
elPlBadge.classList.remove("active");
});
setInterval(plTick, PL_TICK_MS);
</script>
</body>
</html>