dancinlife Claude Opus 4.7 (1M context) commited on
Commit
bdaaaa3
·
1 Parent(s): 049f532

revert from Pyodide → vanilla JS (faster) + 3 fixes

Browse files

User feedback: Pyodide too slow to load, prefers JS.
- Drop Pyodide entirely (no numpy WASM, no 5s cold load).
- Switch coupling formula: shared sin(t) + per-stream noise (no XOR).
High coupling now produces clean diagonal scatter alignment instead of
the zigzag pattern the XOR was creating.
- Slow tick from rAF (60+ fps) to setInterval(33ms) — ~30 fps, plenty for
the visualization, less GC pressure.
- EMERGENT badge: opacity 0 + visibility hidden when MI ≤ 0.30 (was
opacity 0.15, looked always-on faintly).

Rolling 250-sample buffer for stable MI estimates; metrics start updating
after 50 samples (~1.7 s warmup).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

Files changed (2) hide show
  1. README.md +7 -5
  2. index.html +157 -148
README.md CHANGED
@@ -25,11 +25,13 @@ emergence = H(L) + H(R) − H(L, R) (bits)
25
 
26
  ## Tech
27
 
28
- - Static HF Space (`sdk: static`) — no cold start.
29
- - **Pyodide**runs `byte_emergence_demo.py` 100% verbatim in the browser via WebAssembly. numpy included.
30
- - First paint after Pyodide load (~5 s download, then cached): metrics tick at ~20-30 fps.
31
- - 16-bin histogram entropy, 256-sample window per tick.
32
- - Reproducible noise: `np.random.default_rng(t)` seeded by frame index same as the Python original.
 
 
33
 
34
  ## Sister
35
 
 
25
 
26
  ## Tech
27
 
28
+ - Static HF Space (`sdk: static`) — first paint < 1 s, no cold start, no Python runtime.
29
+ - Vanilla JS — `setInterval` 30 fps tick over a 250-sample rolling buffer.
30
+ - Engine: shared `sin(t)` + individual gaussian-ish noise per stream.
31
+ `L = (1−c)·noiseL + c·sin(t)`, `R = (1−c)·noiseR + c·sin(t)`. High coupling
32
+ collapses both streams onto the diagonal — the scatter aligns cleanly.
33
+ - 12-bin histogram entropy over `[−1.5, +1.5]`. EMERGENT badge shows only
34
+ when MI > 0.30 (`opacity:0` + `visibility:hidden` otherwise — fully gone).
35
 
36
  ## Sister
37
 
index.html CHANGED
@@ -2,7 +2,7 @@
2
  <html lang="ko">
3
  <head>
4
  <meta charset="utf-8">
5
- <title>Anima Emergence — byte-level mutual information</title>
6
  <style>
7
  :root {
8
  --bg: #1a1d24;
@@ -72,17 +72,8 @@
72
  .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; }
73
  .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; }
74
  .reset-btn:hover { background: var(--border-bright); }
75
- .reset-btn:disabled { opacity: 0.4; cursor: not-allowed; }
76
  .footnote { color: var(--muted); font-size: 12px; text-align: center; margin-top: 18px; line-height: 1.6; }
77
  .footnote code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 4px; }
78
- .loading-bar {
79
- height: 3px;
80
- background: linear-gradient(90deg, var(--blue), var(--green));
81
- width: 0%;
82
- transition: width 0.3s;
83
- border-radius: 2px;
84
- margin-top: 4px;
85
- }
86
  </style>
87
  </head>
88
  <body>
@@ -90,14 +81,13 @@
90
  <div class="header">
91
  <div>
92
  <h2 class="title">데이터 스트림 결합 및 창발성 시각화</h2>
93
- <div class="status" id="status">Pyodide 로딩 중...</div>
94
- <div class="loading-bar" id="loading-bar"></div>
95
  </div>
96
  <div class="metrics">
97
- <div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="m-hl"></div></div>
98
- <div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="m-hr"></div></div>
99
- <div class="metric"><div class="metric-label">H(L,R)</div><div class="metric-value" id="m-hj"></div></div>
100
- <div class="metric"><div class="metric-label">창발성 (MI)</div><div class="metric-value" id="m-mi" style="color:#c8f7cd"></div></div>
101
  </div>
102
  </div>
103
 
@@ -122,84 +112,106 @@
122
 
123
  <div class="controls">
124
  <span class="label">결합 강도 (Coupling)</span>
125
- <input type="range" id="coup" min="0" max="100" value="80" disabled>
126
  <span class="coup-val" id="coup-val">0.80</span>
127
- <button class="reset-btn" id="reset" disabled>초기화</button>
128
  </div>
129
 
130
  <div class="footnote">
131
  emergence = H(L) + H(R) − H(L,R) — 두 스트림이 독립이면 0, 결합하면 양수로 떠오릅니다.<br>
132
- 원본: <code>anima/byte_emergence_demo.py</code> Pyodide 브라우저에서 100% 그대로 실행니다 (numpy 포함).<br>
133
- zigzag 산점도는 <code>w ^ (L &gt;&gt; 1)</code> XOR 결합의 정상 동작 — 비트 순서가 뒤집히는 지점마다 점이 다른 위치로 매핑됩니다.
134
  </div>
135
  </div>
136
 
137
- <script src="https://cdn.jsdelivr.net/pyodide/v0.26.4/full/pyodide.js"></script>
138
  <script>
139
  "use strict";
140
 
141
  // ─────────────────────────────────────────────────────────────────────────
142
- // Original Python — verbatim from anima/byte_emergence_demo.py
143
- // (only the print loop is replaced with a per-tick step() helper for JS)
144
  // ─────────────────────────────────────────────────────────────────────────
145
- const PY_SOURCE = `
146
- import numpy as np
147
-
148
- VOCAB, BINS = 256, 16
149
-
150
- def H(x):
151
- h, _ = np.histogram(x, bins=BINS, range=(0, VOCAB))
152
- p = h / max(h.sum(), 1); p = p[p > 0]
153
- return float(-(p * np.log2(p)).sum())
154
-
155
- def H_joint(L, R):
156
- jh, *_ = np.histogram2d(L, R, bins=BINS, range=[[0, VOCAB], [0, VOCAB]])
157
- p = jh / max(jh.sum(), 1); p = p[p > 0]
158
- return float(-(p * np.log2(p)).sum())
159
-
160
- def emergence(L, R):
161
- return max(0.0, H(L) + H(R) - H_joint(L, R))
162
 
163
- def engine_step(t, N=1024, coupling=0.8):
164
- """Tension wave couples L and R — integration at byte level."""
165
- rng = np.random.default_rng(t)
166
- L = rng.integers(0, VOCAB, N)
167
- R = rng.integers(0, VOCAB, N)
168
- w = ((np.sin(np.linspace(0, 4 * np.pi, N) + t * 0.2) + 1) * 127).astype(int)
169
- L = ((1 - coupling) * L + coupling * w).astype(int) % VOCAB
170
- R = ((1 - coupling) * R + coupling * (w ^ (L >> 1))).astype(int) % VOCAB
171
- return L, R
172
-
173
- # ── thin adapter for the JS layer ────────────────────────────────────────
174
- def step(t, coupling, N=256):
175
- L, R = engine_step(t, N=N, coupling=coupling)
176
- return {
177
- "L": L.astype(np.uint8).tobytes(),
178
- "R": R.astype(np.uint8).tobytes(),
179
- "hL": H(L),
180
- "hR": H(R),
181
- "hJ": H_joint(L, R),
182
- "mi": emergence(L, R),
183
- }
184
- `;
185
 
 
 
 
186
  const $ = id => document.getElementById(id);
187
  const cvL = $("cv-l"), cvR = $("cv-r"), cvS = $("cv-s");
188
  const ctxL = cvL.getContext("2d"), ctxR = cvR.getContext("2d"), ctxS = cvS.getContext("2d");
189
  const slider = $("coup"), coupVal = $("coup-val"), resetBtn = $("reset");
190
  const elHL = $("m-hl"), elHR = $("m-hr"), elHJ = $("m-hj"), elMI = $("m-mi");
191
- const elBadge = $("badge"), elStatus = $("status"), elLoadBar = $("loading-bar");
192
 
193
- let pyodide = null;
194
- let t = 0;
195
- let running = false;
196
- let dimsL, dimsR;
 
 
 
 
 
 
 
 
197
 
198
- function setLoading(pct, label) {
199
- elLoadBar.style.width = pct + "%";
200
- if (label) elStatus.textContent = label;
 
 
 
 
 
201
  }
202
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  function resizeCanvas(canvas) {
204
  const dpr = window.devicePixelRatio || 1;
205
  const rect = canvas.getBoundingClientRect();
@@ -208,22 +220,43 @@ function resizeCanvas(canvas) {
208
  canvas.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
209
  return [rect.width, rect.height];
210
  }
 
 
 
 
 
 
 
 
 
 
 
 
211
 
212
  function drawStream(ctx, arr, color, w, h) {
213
  ctx.clearRect(0, 0, w, h);
214
- ctx.fillStyle = "rgba(35, 39, 48, 0.5)";
215
  ctx.fillRect(0, 0, w, h);
 
216
  ctx.beginPath();
217
  ctx.strokeStyle = color;
218
- ctx.lineWidth = 1.4;
 
 
219
  for (let i = 0; i < arr.length; i++) {
220
- const x = (i / (arr.length - 1)) * w;
221
- const y = h - (arr[i] / 255) * h * 0.85 - h * 0.075;
222
- if (i === 0) ctx.moveTo(x, y); else ctx.lineTo(x, y);
 
223
  }
224
  ctx.stroke();
225
  }
226
 
 
 
 
 
 
227
  function drawScatter(L, R) {
228
  const w = 240, h = 240, pad = 12;
229
  ctxS.clearRect(0, 0, w, h);
@@ -231,10 +264,15 @@ function drawScatter(L, R) {
231
  ctxS.lineWidth = 1;
232
  ctxS.setLineDash([4, 4]);
233
  ctxS.strokeRect(pad, pad, w - 2 * pad, h - 2 * pad);
 
 
 
 
 
234
  ctxS.setLineDash([]);
235
  for (let i = 0; i < L.length; i++) {
236
- const x = (L[i] / 255) * (w - 2 * pad) + pad;
237
- const y = h - ((R[i] / 255) * (h - 2 * pad) + pad);
238
  ctxS.fillStyle = i % 2 === 0 ? "rgba(123, 154, 255, 0.55)" : "rgba(168, 230, 104, 0.55)";
239
  ctxS.beginPath();
240
  ctxS.arc(x, y, 2.4, 0, Math.PI * 2);
@@ -242,94 +280,65 @@ function drawScatter(L, R) {
242
  }
243
  }
244
 
245
- function applyResize() {
246
- dimsL = resizeCanvas(cvL);
247
- dimsR = resizeCanvas(cvR);
248
- }
249
- applyResize();
250
- window.addEventListener("resize", applyResize);
251
-
252
- async function init() {
253
- setLoading(15, "Pyodide 다운로드 중...");
254
- pyodide = await loadPyodide({
255
- indexURL: "https://cdn.jsdelivr.net/pyodide/v0.26.4/full/",
256
- });
257
-
258
- setLoading(50, "numpy 로드 중... (~7MB)");
259
- await pyodide.loadPackage("numpy");
260
-
261
- setLoading(85, "byte_emergence_demo.py 실행 중...");
262
- pyodide.runPython(PY_SOURCE);
263
-
264
- setLoading(100, "준비 완료. 슬라이더를 움직여보세요.");
265
- slider.disabled = false;
266
- resetBtn.disabled = false;
267
- setTimeout(() => { elLoadBar.style.opacity = 0; }, 600);
268
-
269
- running = true;
270
- loop();
271
- }
272
-
273
- function tickOnce() {
274
  const c = parseInt(slider.value, 10) / 100;
275
- const proxy = pyodide.runPython(`step(${t}, ${c}, N=256)`);
276
- const result = proxy.toJs({dict_converter: Object.fromEntries});
277
- proxy.destroy();
278
-
279
- const L = new Uint8Array(result.L);
280
- const R = new Uint8Array(result.R);
281
- const hL = result.hL, hR = result.hR, hJ = result.hJ, mi = result.mi;
282
-
283
- elHL.textContent = hL.toFixed(2);
284
- elHR.textContent = hR.toFixed(2);
285
- elHJ.textContent = hJ.toFixed(2);
286
- elMI.textContent = mi.toFixed(3);
287
 
288
- if (mi > 0.30) {
289
- elBadge.classList.add("active");
290
- elStatus.textContent = "강한 결합: 창발성 ✨ — 두 스트림이 통합되어 새로운 정보가 발생합니다.";
291
- } else if (mi > 0.05) {
292
- elBadge.classList.remove("active");
293
- elStatus.textContent = "부분적 결합: 유의미한 상호정보량이 발생합니다.";
294
- } else {
295
- elBadge.classList.remove("active");
296
- elStatus.textContent = "독립적인 노이즈 단계: 창발성이 낮습니다.";
 
 
 
 
 
 
 
 
 
 
 
 
297
  }
298
 
299
- drawStream(ctxL, L, "#7b9aff", dimsL[0], dimsL[1]);
300
- drawStream(ctxR, R, "#a8e668", dimsR[0], dimsR[1]);
301
- drawScatter(L, R);
302
 
303
  t++;
304
  }
305
 
306
- function loop() {
307
- if (!running) return;
308
- try {
309
- tickOnce();
310
- } catch (e) {
311
- elStatus.textContent = "런타임 에러: " + e.message;
312
- console.error(e);
313
- running = false;
314
- return;
315
- }
316
- // Pyodide 기준 ~20-30fps; 너무 빠르면 GC 부담.
317
- setTimeout(() => requestAnimationFrame(loop), 30);
318
- }
319
-
320
  slider.addEventListener("input", () => {
321
  coupVal.textContent = (parseInt(slider.value, 10) / 100).toFixed(2);
322
  });
323
  resetBtn.addEventListener("click", () => {
 
 
324
  t = 0;
325
  slider.value = 80;
326
  coupVal.textContent = "0.80";
 
327
  });
328
 
329
- init().catch(err => {
330
- elStatus.textContent = "Pyodide 로딩 실패: " + err.message;
331
- console.error(err);
332
- });
333
  </script>
334
  </body>
335
  </html>
 
2
  <html lang="ko">
3
  <head>
4
  <meta charset="utf-8">
5
+ <title>Anima Emergence — mutual information visualizer</title>
6
  <style>
7
  :root {
8
  --bg: #1a1d24;
 
72
  .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; }
73
  .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; }
74
  .reset-btn:hover { background: var(--border-bright); }
 
75
  .footnote { color: var(--muted); font-size: 12px; text-align: center; margin-top: 18px; line-height: 1.6; }
76
  .footnote code { background: rgba(255,255,255,0.06); padding: 1px 6px; border-radius: 4px; }
 
 
 
 
 
 
 
 
77
  </style>
78
  </head>
79
  <body>
 
81
  <div class="header">
82
  <div>
83
  <h2 class="title">데이터 스트림 결합 및 창발성 시각화</h2>
84
+ <div class="status" id="status">버퍼 채우는 중...</div>
 
85
  </div>
86
  <div class="metrics">
87
+ <div class="metric"><div class="metric-label">H(L)</div><div class="metric-value" id="m-hl">0.00</div></div>
88
+ <div class="metric"><div class="metric-label">H(R)</div><div class="metric-value" id="m-hr">0.00</div></div>
89
+ <div class="metric"><div class="metric-label">H(L,R)</div><div class="metric-value" id="m-hj">0.00</div></div>
90
+ <div class="metric"><div class="metric-label">창발성 (MI)</div><div class="metric-value" id="m-mi" style="color:#c8f7cd">0.000</div></div>
91
  </div>
92
  </div>
93
 
 
112
 
113
  <div class="controls">
114
  <span class="label">결합 강도 (Coupling)</span>
115
+ <input type="range" id="coup" min="0" max="100" value="80">
116
  <span class="coup-val" id="coup-val">0.80</span>
117
+ <button class="reset-btn" id="reset">초기화</button>
118
  </div>
119
 
120
  <div class="footnote">
121
  emergence = H(L) + H(R) − H(L,R) — 두 스트림이 독립이면 0, 결합하면 양수로 떠오릅니다.<br>
122
+ 공유 사인파 + 개별 노이즈 모델 (no XOR) — 고결 두 스트림이 동일 신호로 수렴해 산점도가 대각선 정렬.
 
123
  </div>
124
  </div>
125
 
 
126
  <script>
127
  "use strict";
128
 
129
  // ─────────────────────────────────────────────────────────────────────────
130
+ // Config
 
131
  // ─────────────────────────────────────────────────────────────────────────
132
+ const TICK_MS = 33; // ~30 fps
133
+ const HIST_LEN = 250; // rolling buffer length (~8 s of history at 30 fps)
134
+ const BINS = 12; // entropy histogram bins
135
+ const VRANGE = 1.5; // value range [-VRANGE, +VRANGE]
136
+ const MIN_FOR_METRICS = 50;
137
+ const MI_EMERGENT = 0.30;
138
+ const MI_PARTIAL = 0.05;
 
 
 
 
 
 
 
 
 
 
139
 
140
+ // ─────────────────────────────────────────────────────────────────────────
141
+ // State
142
+ // ─────────────────────────────────────────────────────────────────────────
143
+ const histL = [];
144
+ const histR = [];
145
+ let t = 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
146
 
147
+ // ─────────────────────────────────────────────────────────────────────────
148
+ // DOM refs
149
+ // ─────────────────────────────────────────────────────────────────────────
150
  const $ = id => document.getElementById(id);
151
  const cvL = $("cv-l"), cvR = $("cv-r"), cvS = $("cv-s");
152
  const ctxL = cvL.getContext("2d"), ctxR = cvR.getContext("2d"), ctxS = cvS.getContext("2d");
153
  const slider = $("coup"), coupVal = $("coup-val"), resetBtn = $("reset");
154
  const elHL = $("m-hl"), elHR = $("m-hr"), elHJ = $("m-hj"), elMI = $("m-mi");
155
+ const elBadge = $("badge"), elStatus = $("status");
156
 
157
+ // ─────────────────────────────────────────────────────────────────────────
158
+ // Engine shared sine + individual noise (Gemini-style, line-aligned)
159
+ // ─────────────────────────────────────────────────────────────────────────
160
+ function generateSample(coupling, tick) {
161
+ const common = Math.sin(tick * 0.1);
162
+ const noiseL = (Math.random() - 0.5) * 2;
163
+ const noiseR = (Math.random() - 0.5) * 2;
164
+ return [
165
+ (1 - coupling) * noiseL + coupling * common,
166
+ (1 - coupling) * noiseR + coupling * common,
167
+ ];
168
+ }
169
 
170
+ // ─────────────────────────────────────────────────────────────────────────
171
+ // Information theory
172
+ // ─────────────────────────────────────────────────────────────────────────
173
+ function bin(v) {
174
+ let i = ((v + VRANGE) / (2 * VRANGE) * BINS) | 0;
175
+ if (i < 0) i = 0;
176
+ else if (i >= BINS) i = BINS - 1;
177
+ return i;
178
  }
179
 
180
+ function entropy(arr) {
181
+ if (arr.length === 0) return 0;
182
+ const counts = new Int32Array(BINS);
183
+ for (let i = 0; i < arr.length; i++) counts[bin(arr[i])]++;
184
+ const inv = 1 / arr.length;
185
+ let H = 0;
186
+ for (let i = 0; i < BINS; i++) {
187
+ if (counts[i] > 0) {
188
+ const p = counts[i] * inv;
189
+ H -= p * Math.log2(p);
190
+ }
191
+ }
192
+ return H;
193
+ }
194
+
195
+ function jointEntropy(L, R) {
196
+ const counts = new Int32Array(BINS * BINS);
197
+ for (let i = 0; i < L.length; i++) {
198
+ counts[bin(L[i]) * BINS + bin(R[i])]++;
199
+ }
200
+ const inv = 1 / L.length;
201
+ let H = 0;
202
+ for (let i = 0; i < counts.length; i++) {
203
+ if (counts[i] > 0) {
204
+ const p = counts[i] * inv;
205
+ H -= p * Math.log2(p);
206
+ }
207
+ }
208
+ return H;
209
+ }
210
+
211
+ // ─────────────────────────────────────────────────────────────────────────
212
+ // Rendering
213
+ // ────────────────���────────────────────────────────────────────────────────
214
+ let dimsL, dimsR;
215
  function resizeCanvas(canvas) {
216
  const dpr = window.devicePixelRatio || 1;
217
  const rect = canvas.getBoundingClientRect();
 
220
  canvas.getContext("2d").setTransform(dpr, 0, 0, dpr, 0, 0);
221
  return [rect.width, rect.height];
222
  }
223
+ function applyResize() {
224
+ dimsL = resizeCanvas(cvL);
225
+ dimsR = resizeCanvas(cvR);
226
+ }
227
+ applyResize();
228
+ window.addEventListener("resize", applyResize);
229
+
230
+ function valueToY(v, h) {
231
+ // map [-VRANGE, +VRANGE] → [h*0.92, h*0.08]
232
+ const norm = (v + VRANGE) / (2 * VRANGE);
233
+ return h - (norm * h * 0.84 + h * 0.08);
234
+ }
235
 
236
  function drawStream(ctx, arr, color, w, h) {
237
  ctx.clearRect(0, 0, w, h);
238
+ ctx.fillStyle = "rgba(35, 39, 48, 0.55)";
239
  ctx.fillRect(0, 0, w, h);
240
+ if (arr.length < 2) return;
241
  ctx.beginPath();
242
  ctx.strokeStyle = color;
243
+ ctx.lineWidth = 1.6;
244
+ ctx.lineJoin = "round";
245
+ const denom = HIST_LEN - 1;
246
  for (let i = 0; i < arr.length; i++) {
247
+ const x = (i / denom) * w;
248
+ const y = valueToY(arr[i], h);
249
+ if (i === 0) ctx.moveTo(x, y);
250
+ else ctx.lineTo(x, y);
251
  }
252
  ctx.stroke();
253
  }
254
 
255
+ function valueToScatter(v, axisLen, pad) {
256
+ const norm = (v + VRANGE) / (2 * VRANGE);
257
+ return norm * (axisLen - 2 * pad) + pad;
258
+ }
259
+
260
  function drawScatter(L, R) {
261
  const w = 240, h = 240, pad = 12;
262
  ctxS.clearRect(0, 0, w, h);
 
264
  ctxS.lineWidth = 1;
265
  ctxS.setLineDash([4, 4]);
266
  ctxS.strokeRect(pad, pad, w - 2 * pad, h - 2 * pad);
267
+ // diagonal guide
268
+ ctxS.beginPath();
269
+ ctxS.moveTo(pad, h - pad);
270
+ ctxS.lineTo(w - pad, pad);
271
+ ctxS.stroke();
272
  ctxS.setLineDash([]);
273
  for (let i = 0; i < L.length; i++) {
274
+ const x = valueToScatter(L[i], w, pad);
275
+ const y = h - valueToScatter(R[i], h, pad);
276
  ctxS.fillStyle = i % 2 === 0 ? "rgba(123, 154, 255, 0.55)" : "rgba(168, 230, 104, 0.55)";
277
  ctxS.beginPath();
278
  ctxS.arc(x, y, 2.4, 0, Math.PI * 2);
 
280
  }
281
  }
282
 
283
+ // ─────────────────────────────────────────────────────────────────────────
284
+ // Tick — runs at ~30 fps via setInterval
285
+ // ─────────────────────────────────────────────────────────────────────────
286
+ function tick() {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
287
  const c = parseInt(slider.value, 10) / 100;
288
+ const [l, r] = generateSample(c, t);
289
+ histL.push(l);
290
+ histR.push(r);
291
+ if (histL.length > HIST_LEN) {
292
+ histL.shift();
293
+ histR.shift();
294
+ }
 
 
 
 
 
295
 
296
+ if (histL.length >= MIN_FOR_METRICS) {
297
+ const hL = entropy(histL);
298
+ const hR = entropy(histR);
299
+ const hJ = jointEntropy(histL, histR);
300
+ const mi = Math.max(0, hL + hR - hJ);
301
+
302
+ elHL.textContent = hL.toFixed(2);
303
+ elHR.textContent = hR.toFixed(2);
304
+ elHJ.textContent = hJ.toFixed(2);
305
+ elMI.textContent = mi.toFixed(3);
306
+
307
+ if (mi > MI_EMERGENT) {
308
+ elBadge.classList.add("active");
309
+ elStatus.textContent = "강한 결합: 창발성 ✨ — 두 스트림이 통합되어 새로운 정보가 발생합니다.";
310
+ } else if (mi > MI_PARTIAL) {
311
+ elBadge.classList.remove("active");
312
+ elStatus.textContent = "부분적 결합: 유의미한 상호정보량이 발생합니다.";
313
+ } else {
314
+ elBadge.classList.remove("active");
315
+ elStatus.textContent = "독립적인 노이즈 단계: 창발성이 낮습니다.";
316
+ }
317
  }
318
 
319
+ drawStream(ctxL, histL, "#7b9aff", dimsL[0], dimsL[1]);
320
+ drawStream(ctxR, histR, "#a8e668", dimsR[0], dimsR[1]);
321
+ drawScatter(histL, histR);
322
 
323
  t++;
324
  }
325
 
326
+ // ─────────────────────────────────────────────────────────────────────────
327
+ // Wiring
328
+ // ─────────────────────────────────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
329
  slider.addEventListener("input", () => {
330
  coupVal.textContent = (parseInt(slider.value, 10) / 100).toFixed(2);
331
  });
332
  resetBtn.addEventListener("click", () => {
333
+ histL.length = 0;
334
+ histR.length = 0;
335
  t = 0;
336
  slider.value = 80;
337
  coupVal.textContent = "0.80";
338
+ elBadge.classList.remove("active");
339
  });
340
 
341
+ setInterval(tick, TICK_MS);
 
 
 
342
  </script>
343
  </body>
344
  </html>