Skip to content

Instantly share code, notes, and snippets.

@jbdamask
Created February 22, 2026 04:54
Show Gist options
  • Select an option

  • Save jbdamask/f2f577864aecba720179c3884848dd1f to your computer and use it in GitHub Desktop.

Select an option

Save jbdamask/f2f577864aecba720179c3884848dd1f to your computer and use it in GitHub Desktop.
NowIGetIt: jay-eigen.pdf
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Consistently Oriented Eigenvectors — Interactive Explainer</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700;800;900&family=JetBrains+Mono:wght@400;500&display=swap');
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
--bg: #0a0a0f;
--bg2: #111118;
--bg3: #1a1a24;
--accent: #6366f1;
--accent2: #818cf8;
--accent3: #a5b4fc;
--green: #34d399;
--red: #f87171;
--orange: #fb923c;
--yellow: #fbbf24;
--cyan: #22d3ee;
--pink: #f472b6;
--text: #e2e8f0;
--text2: #94a3b8;
--text3: #64748b;
--border: #1e293b;
}
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: 'Inter', sans-serif;
line-height: 1.7;
overflow-x: hidden;
}
/* Hero */
.hero {
min-height: 100vh;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
position: relative;
padding: 2rem;
overflow: hidden;
}
.hero-bg {
position: absolute;
inset: 0;
z-index: 0;
}
.hero-bg canvas {
width: 100%;
height: 100%;
}
.hero-content {
position: relative;
z-index: 1;
max-width: 800px;
}
.hero h1 {
font-size: clamp(2.2rem, 5vw, 4rem);
font-weight: 900;
background: linear-gradient(135deg, var(--accent3), var(--cyan), var(--green));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
line-height: 1.15;
margin-bottom: 1.5rem;
}
.hero p {
font-size: 1.15rem;
color: var(--text2);
max-width: 600px;
margin: 0 auto 2rem;
}
.hero .author {
font-size: 0.9rem;
color: var(--text3);
margin-bottom: 2.5rem;
}
.scroll-hint {
animation: bounce 2s ease infinite;
color: var(--accent2);
font-size: 1.5rem;
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(10px); }
}
/* Sections */
section {
max-width: 1100px;
margin: 0 auto;
padding: 5rem 2rem;
}
.section-label {
font-size: 0.75rem;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.15em;
color: var(--accent2);
margin-bottom: 0.75rem;
}
h2 {
font-size: clamp(1.8rem, 3.5vw, 2.5rem);
font-weight: 800;
margin-bottom: 1.5rem;
line-height: 1.2;
}
h3 {
font-size: 1.3rem;
font-weight: 700;
margin-bottom: 1rem;
color: var(--accent3);
}
p { margin-bottom: 1rem; color: var(--text2); }
.highlight { color: var(--accent3); font-weight: 600; }
.green { color: var(--green); }
.red { color: var(--red); }
.orange { color: var(--orange); }
.cyan { color: var(--cyan); }
/* Cards */
.card-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
margin: 2rem 0;
}
.card {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
transition: transform 0.3s, border-color 0.3s;
}
.card:hover {
transform: translateY(-4px);
border-color: var(--accent);
}
.card .icon {
font-size: 2rem;
margin-bottom: 1rem;
}
.card h3 { color: var(--text); }
/* Interactive demos */
.demo-container {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 16px;
padding: 2rem;
margin: 2rem 0;
position: relative;
overflow: hidden;
}
.demo-container canvas {
display: block;
margin: 0 auto;
cursor: grab;
}
.demo-container canvas:active { cursor: grabbing; }
.demo-controls {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
margin-top: 1.5rem;
justify-content: center;
}
.btn {
padding: 0.6rem 1.4rem;
border-radius: 10px;
border: 1px solid var(--border);
background: var(--bg3);
color: var(--text);
font-family: 'Inter', sans-serif;
font-size: 0.85rem;
font-weight: 600;
cursor: pointer;
transition: all 0.2s;
}
.btn:hover {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.btn.active {
background: var(--accent);
border-color: var(--accent);
color: #fff;
}
.slider-group {
display: flex;
align-items: center;
gap: 0.75rem;
}
.slider-group label {
font-size: 0.85rem;
color: var(--text2);
font-weight: 500;
white-space: nowrap;
}
input[type="range"] {
-webkit-appearance: none;
appearance: none;
height: 6px;
background: var(--bg3);
border-radius: 3px;
outline: none;
width: 150px;
}
input[type="range"]::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
border-radius: 50%;
background: var(--accent);
cursor: pointer;
}
/* Comparison */
.comparison {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin: 2rem 0;
}
@media (max-width: 700px) {
.comparison { grid-template-columns: 1fr; }
}
.comparison-side {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 16px;
padding: 1.5rem;
text-align: center;
}
.comparison-side canvas {
display: block;
margin: 0 auto 1rem;
}
/* Timeline */
.timeline {
position: relative;
margin: 3rem 0;
}
.timeline::before {
content: '';
position: absolute;
left: 24px;
top: 0;
bottom: 0;
width: 2px;
background: var(--border);
}
.timeline-item {
display: flex;
gap: 2rem;
margin-bottom: 2.5rem;
position: relative;
}
.timeline-dot {
width: 12px;
height: 12px;
border-radius: 50%;
background: var(--accent);
flex-shrink: 0;
margin-top: 0.5rem;
position: relative;
left: 19px;
z-index: 1;
}
.timeline-content {
background: var(--bg2);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.5rem;
flex: 1;
margin-left: 1rem;
}
.timeline-content h3 {
color: var(--text);
font-size: 1.1rem;
margin-bottom: 0.5rem;
}
/* Polar plot */
.polar-demo {
text-align: center;
}
/* Key insight boxes */
.insight {
background: linear-gradient(135deg, rgba(99,102,241,0.1), rgba(34,211,238,0.05));
border-left: 3px solid var(--accent);
border-radius: 0 12px 12px 0;
padding: 1.5rem 2rem;
margin: 2rem 0;
}
.insight strong { color: var(--accent3); }
/* Code */
code {
font-family: 'JetBrains Mono', monospace;
background: var(--bg3);
padding: 0.15em 0.4em;
border-radius: 4px;
font-size: 0.85em;
color: var(--cyan);
}
/* Footer */
footer {
text-align: center;
padding: 3rem 2rem;
color: var(--text3);
font-size: 0.85rem;
border-top: 1px solid var(--border);
}
footer a { color: var(--accent2); text-decoration: none; }
/* Fade in animation */
.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* Nav */
nav {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
background: rgba(10,10,15,0.85);
backdrop-filter: blur(20px);
border-bottom: 1px solid var(--border);
padding: 0.75rem 2rem;
display: flex;
justify-content: center;
gap: 2rem;
flex-wrap: wrap;
font-size: 0.82rem;
font-weight: 500;
}
nav a {
color: var(--text2);
text-decoration: none;
transition: color 0.2s;
}
nav a:hover { color: var(--accent3); }
/* Tooltip for math */
.math {
font-family: 'JetBrains Mono', monospace;
color: var(--yellow);
font-size: 0.92em;
}
.eq-block {
background: var(--bg3);
border-radius: 12px;
padding: 1.5rem 2rem;
margin: 1.5rem 0;
font-family: 'JetBrains Mono', monospace;
font-size: 0.9rem;
color: var(--yellow);
overflow-x: auto;
text-align: center;
line-height: 2;
}
</style>
</head>
<body>
<nav>
<a href="#problem">The Problem</a>
<a href="#eigenvectors">Eigenvectors 101</a>
<a href="#original">Original Method</a>
<a href="#new">New Method</a>
<a href="#demo2d">2D Demo</a>
<a href="#demo3d">3D Demo</a>
<a href="#rmt">Random Matrix Theory</a>
<a href="#stabilization">Stabilization</a>
</nav>
<!-- HERO -->
<div class="hero">
<div class="hero-bg"><canvas id="heroBg"></canvas></div>
<div class="hero-content">
<h1>Fixing the Compass of Data</h1>
<p>How a new algorithm gives eigenvectors a consistent sense of direction — and why it matters for understanding complex systems.</p>
<div class="author">Based on the paper by <strong>Jay Damask</strong> · arXiv:2402.08139 · 2024</div>
<div class="scroll-hint">↓</div>
</div>
</div>
<!-- THE PROBLEM -->
<section id="problem" class="fade-in">
<div class="section-label">The Problem</div>
<h2>Eigenvectors don't know which way to point</h2>
<p>When you ask a computer to find the <span class="highlight">principal directions</span> in your data (the "eigenvectors"), the software returns vectors that are mathematically correct — but their <span class="red">signs are arbitrary</span>. Run the same analysis twice and a vector might flip from pointing "north" to pointing "south."</p>
<p>This is like having a compass that randomly reverses polarity. You can still measure distances, but you can't reliably say <em>which way</em> things are moving over time.</p>
<div class="card-grid">
<div class="card">
<div class="icon">🔀</div>
<h3>Random Sign Flips</h3>
<p>Software routines like <code>svd</code> and <code>eig</code> return eigenvectors with arbitrary signs. Consecutive runs on similar data can produce opposite-pointing vectors.</p>
</div>
<div class="card">
<div class="icon">📊</div>
<h3>Breaks Tracking Over Time</h3>
<p>If you track how your system's principal directions evolve — like in financial markets or sensor arrays — sign flips create artificial jumps.</p>
</div>
<div class="card">
<div class="icon">🧭</div>
<h3>Limited Angular Range</h3>
<p>Even with the original fix (arcsin method), angles were limited to a half-circle (π interval). Vectors near the boundary could appear to "wrap around."</p>
</div>
</div>
</section>
<!-- EIGENVECTORS 101 -->
<section id="eigenvectors" class="fade-in">
<div class="section-label">Background</div>
<h2>Eigenvectors in 60 Seconds</h2>
<p>Imagine you have a cloud of data points in multiple dimensions. <span class="highlight">Eigenvectors</span> are the natural "axes" of that cloud — they point in the directions where the data varies the most.</p>
<div class="demo-container">
<canvas id="eigenCloud" width="700" height="400"></canvas>
<div class="demo-controls">
<button class="btn" id="reshuffleCloud">🎲 New Data Cloud</button>
<button class="btn" id="toggleEigen">Show Eigenvectors</button>
</div>
<p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">The colored arrows are the eigenvectors — the principal directions of variation in the data.</p>
</div>
<div class="insight">
<strong>Key point:</strong> Eigenvectors come in pairs of opposite directions (like a line, not an arrow). The computer picks one direction arbitrarily. This paper's algorithm makes that choice <em>consistent</em>.
</div>
</section>
<!-- ORIGINAL METHOD -->
<section id="original" class="fade-in">
<div class="section-label">Original Algorithm</div>
<h2>The First Fix: arcsin Method</h2>
<p>The original paper (2020) solved the sign problem by checking each eigenvector's direction: if it pointed "backward" relative to a reference axis, the algorithm <span class="highlight">reflected</span> (flipped) it. The rotation angles were computed using <code>arcsin</code>.</p>
<div class="timeline">
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h3>Step 1: Sort Eigenvectors</h3>
<p>Order them by eigenvalue magnitude (largest first). This ensures a consistent labeling.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h3>Step 2: Check Hemisphere</h3>
<p>For each eigenvector, check if it points in the "front" hemisphere relative to its reference axis. If not, <span class="red">flip its sign</span>.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h3>Step 3: Rotate to Align</h3>
<p>Use Givens rotations to swing the eigenvector onto its target axis. Record the rotation angles.</p>
</div>
</div>
<div class="timeline-item">
<div class="timeline-dot"></div>
<div class="timeline-content">
<h3>Step 4: Move to Next Subspace</h3>
<p>Drop down one dimension and repeat for the next eigenvector, until done.</p>
</div>
</div>
</div>
<div class="insight">
<strong>Limitation:</strong> Because <code>arcsin</code> returns angles only in [−π/2, π/2], each eigenvector's direction is tracked over only a <span class="orange">half-circle</span>. Vectors near the edge can appear to jump from one side to the other — a "wrap-around" artifact.
</div>
</section>
<!-- NEW METHOD -->
<section id="new" class="fade-in">
<div class="section-label">The Breakthrough</div>
<h2>The New Fix: Modified arctan2 Method</h2>
<p>The key insight is that a reflection (sign flip) in a <em>reducible</em> subspace can be replaced by a <span class="green">rotation through a major angle</span> (more than 90°). This means:</p>
<div class="card-grid">
<div class="card">
<div class="icon">🔄</div>
<h3>Full-Circle Tracking</h3>
<p>The primary rotation for each mode now spans a full <span class="math">2π</span> circle instead of just <span class="math">π</span>. No more wrap-around.</p>
</div>
<div class="card">
<div class="icon">🪞</div>
<h3>Minimal Reflections</h3>
<p>Reflections are deferred to the very last subspace (which is "irreducible" — no rotation can fix it). At most <em>one</em> reflection is needed.</p>
</div>
<div class="card">
<div class="icon">📐</div>
<h3>Clean Statistics</h3>
<p>With full-circle angles, you can compute proper directional statistics: means, variances, and detect outliers without artifacts.</p>
</div>
</div>
<div class="eq-block">
θ<sub>1,2</sub> = arctan2(a₂, a₁) &nbsp;&nbsp;&nbsp;← full 2π range<br>
θ<sub>1,3</sub> = arctan2(a₃, |a₂ csc θ₂|) &nbsp;&nbsp;&nbsp;← minor angle<br>
θ<sub>1,4</sub> = arctan2(a₄, |a₃ csc θ₃|) &nbsp;&nbsp;&nbsp;← minor angle
</div>
<p>Only the <span class="green">first rotation</span> upon entering each new subspace uses the full <span class="math">2π</span> range. Subsequent rotations within that subspace are naturally limited to <span class="math">π</span> — and that's perfectly fine.</p>
</section>
<!-- 2D INTERACTIVE DEMO -->
<section id="demo2d" class="fade-in">
<div class="section-label">Interactive Demo</div>
<h2>See It in Action: 2D Rotation</h2>
<p>Drag the vector around. Watch how the two methods track the angle differently. The <span class="orange">arcsin method</span> is limited to a half-circle, while the <span class="green">arctan2 method</span> tracks the full rotation.</p>
<div class="comparison">
<div class="comparison-side">
<h3 style="color:var(--orange);">arcsin Method</h3>
<canvas id="arcsinDemo" width="300" height="300"></canvas>
<p id="arcsinAngle" style="font-family:'JetBrains Mono';font-size:1.1rem;color:var(--orange);">θ = 0°</p>
<p style="font-size:0.8rem;">Range: −90° to +90°</p>
</div>
<div class="comparison-side">
<h3 style="color:var(--green);">arctan2 Method</h3>
<canvas id="arctan2Demo" width="300" height="300"></canvas>
<p id="arctan2Angle" style="font-family:'JetBrains Mono';font-size:1.1rem;color:var(--green);">θ = 0°</p>
<p style="font-size:0.8rem;">Range: −180° to +180°</p>
</div>
</div>
<p style="text-align:center;font-size:0.85rem;color:var(--text3);">Click and drag inside either circle to rotate the vector. Notice the wrap-around in the arcsin version!</p>
</section>
<!-- 3D GIVENS ROTATION DEMO -->
<section id="demo3d" class="fade-in">
<div class="section-label">Deeper Dive</div>
<h2>Givens Rotations: Building Blocks</h2>
<p>The algorithm uses <span class="highlight">Givens rotations</span> — simple rotations within a 2D plane embedded in higher-dimensional space. They annihilate one component at a time, like zeroing out entries in a matrix.</p>
<div class="demo-container">
<canvas id="givensDemo" width="700" height="450"></canvas>
<div class="demo-controls">
<button class="btn active" id="givensStep0">Original Vector</button>
<button class="btn" id="givensStep1">Rotate in (x,y)</button>
<button class="btn" id="givensStep2">Rotate in (x,z)</button>
<button class="btn" id="givensStep3">Aligned!</button>
<button class="btn" id="givensAuto">▶ Auto-Play</button>
</div>
<p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">A 3D vector is progressively rotated onto the x-axis using two Givens rotations.</p>
</div>
</section>
<!-- POLAR PLOT / RMT -->
<section id="rmt" class="fade-in">
<div class="section-label">Application</div>
<h2>Signal vs. Noise: Random Matrix Theory</h2>
<p>When the algorithm is applied to real financial market data (foreign exchange quotes & trades), something beautiful emerges:</p>
<div class="card-grid">
<div class="card" style="border-color: var(--green);">
<div class="icon">🎯</div>
<h3><span class="green">Informative Modes</span></h3>
<p>Eigenvectors carrying real information point in <em>consistent directions</em> over time. On the polar plot, they cluster tightly.</p>
</div>
<div class="card" style="border-color: var(--red);">
<div class="icon">🎲</div>
<h3><span class="red">Noise Modes</span></h3>
<p>Eigenvectors corrupted by sample noise point in <em>random directions</em>. On the polar plot, they scatter uniformly around the circle.</p>
</div>
</div>
<div class="demo-container polar-demo">
<canvas id="polarPlot" width="700" height="400"></canvas>
<div class="demo-controls">
<div class="slider-group">
<label>Signal Strength:</label>
<input type="range" id="signalStrength" min="0" max="100" value="70">
</div>
<div class="slider-group">
<label>Noise Level:</label>
<input type="range" id="noiseLevel" min="0" max="100" value="30">
</div>
<button class="btn" id="regeneratePolar">🔄 Regenerate</button>
</div>
<p style="margin-top:1rem;font-size:0.85rem;color:var(--text3);text-align:center;">
<span class="green">Green dots</span> = informative mode (clustered). <span class="red">Red dots</span> = noise mode (scattered). Adjust the sliders to see how signal and noise separate.
</p>
</div>
<div class="insight">
<strong>The punchline:</strong> This distinction between clustered (informative) and scattered (noisy) modes matches perfectly with what the <em>eigenvalue</em> analysis predicts via the Marčenko–Pastur distribution. The angular analysis provides <strong>independent confirmation</strong> of which modes carry real information.
</div>
</section>
<!-- STABILIZATION -->
<section id="stabilization" class="fade-in">
<div class="section-label">Practical Impact</div>
<h2>Stabilizing the Compass Over Time</h2>
<p>Once informative and noisy modes are identified, two stabilization strategies can be applied:</p>
<div class="comparison">
<div class="comparison-side">
<h3 style="color:var(--cyan);">Dynamic Stabilization</h3>
<canvas id="dynamicStab" width="300" height="250"></canvas>
<p style="font-size:0.85rem;">For <span class="green">informative</span> modes: filter the eigenvectors over time to reduce wobble. Like smoothing a noisy compass reading.</p>
</div>
<div class="comparison-side">
<h3 style="color:var(--pink);">Static Stabilization</h3>
<canvas id="staticStab" width="300" height="250"></canvas>
<p style="font-size:0.85rem;">For <span class="red">noise</span> modes: fix their angles to zero (point them at identity). Trading variance for bias — they were random anyway!</p>
</div>
</div>
<div class="demo-container">
<h3 style="text-align:center;margin-bottom:1rem;">Correlation Matrix Cleaning</h3>
<canvas id="corrMatrix" width="700" height="240"></canvas>
<div class="demo-controls">
<button class="btn active" id="corrRaw">Raw</button>
<button class="btn" id="corrDynamic">+ Dynamic</button>
<button class="btn" id="corrStatic">+ Static</button>
</div>
<p style="text-align:center;margin-top:1rem;font-size:0.85rem;color:var(--text3);">Watch the correlation matrix become cleaner and more stable as stabilization is applied.</p>
</div>
</section>
<!-- SUMMARY -->
<section class="fade-in">
<div class="section-label">Summary</div>
<h2>What This All Means</h2>
<div class="card-grid">
<div class="card">
<div class="icon">✅</div>
<h3>Consistent Directions</h3>
<p>Eigenvectors now have a reliable, consistent orientation that doesn't randomly flip between computations.</p>
</div>
<div class="card">
<div class="icon">📏</div>
<h3>Full Angular Range</h3>
<p>Primary rotation angles span the full 360° circle, eliminating wrap-around artifacts and enabling proper directional statistics.</p>
</div>
<div class="card">
<div class="icon">🔍</div>
<h3>Signal Detection</h3>
<p>The angular analysis independently confirms which modes carry real information vs. noise — cross-validating eigenvalue-based methods.</p>
</div>
<div class="card">
<div class="icon">🧹</div>
<h3>Matrix Cleaning</h3>
<p>Dynamic and static stabilization of eigenvectors leads to cleaner, more stable correlation matrices for downstream use.</p>
</div>
</div>
<div class="eq-block">
R<sup>T</sup> V S = I &nbsp;&nbsp;&nbsp; ← &nbsp; The central equation: orient V so it's a pure rotation away from identity.
</div>
<p style="text-align:center;color:var(--text3);">Available as the <code>thucyd</code> Python package on PyPI and Conda-Forge.</p>
</section>
<footer>
<p>Interactive explainer based on <a href="https://arxiv.org/abs/2402.08139" target="_blank">"A Consistently Oriented Basis for Eigenanalysis: Improved Directional Statistics"</a> by Jay Damask (2024).</p>
<p style="margin-top:0.5rem;">Built for educational purposes. Not affiliated with the author.</p>
</footer>
<script>
// ============================================================
// UTILITY FUNCTIONS
// ============================================================
function lerp(a, b, t) { return a + (b - a) * t; }
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)); }
function randRange(lo, hi) { return lo + Math.random() * (hi - lo); }
function gaussRand() {
let u = 0, v = 0;
while (u === 0) u = Math.random();
while (v === 0) v = Math.random();
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
}
// ============================================================
// HERO BACKGROUND ANIMATION
// ============================================================
(function() {
const c = document.getElementById('heroBg');
const ctx = c.getContext('2d');
let W, H;
const particles = [];
function resize() {
W = c.width = c.parentElement.clientWidth;
H = c.height = c.parentElement.clientHeight;
}
resize();
window.addEventListener('resize', resize);
for (let i = 0; i < 80; i++) {
particles.push({
x: Math.random() * 2000,
y: Math.random() * 1200,
vx: (Math.random() - 0.5) * 0.4,
vy: (Math.random() - 0.5) * 0.4,
r: Math.random() * 2 + 1,
hue: Math.random() * 60 + 220
});
}
function drawHero() {
ctx.clearRect(0, 0, W, H);
for (const p of particles) {
p.x += p.vx;
p.y += p.vy;
if (p.x < 0) p.x += W;
if (p.x > W) p.x -= W;
if (p.y < 0) p.y += H;
if (p.y > H) p.y -= H;
ctx.beginPath();
ctx.arc(p.x, p.y, p.r, 0, Math.PI * 2);
ctx.fillStyle = `hsla(${p.hue}, 80%, 70%, 0.5)`;
ctx.fill();
}
// connect nearby
for (let i = 0; i < particles.length; i++) {
for (let j = i + 1; j < particles.length; j++) {
const dx = particles[i].x - particles[j].x;
const dy = particles[i].y - particles[j].y;
const d = Math.sqrt(dx * dx + dy * dy);
if (d < 120) {
ctx.beginPath();
ctx.moveTo(particles[i].x, particles[i].y);
ctx.lineTo(particles[j].x, particles[j].y);
ctx.strokeStyle = `rgba(99,102,241,${0.15 * (1 - d / 120)})`;
ctx.lineWidth = 0.5;
ctx.stroke();
}
}
}
requestAnimationFrame(drawHero);
}
drawHero();
})();
// ============================================================
// SCROLL FADE-IN
// ============================================================
const observer = new IntersectionObserver((entries) => {
entries.forEach(e => {
if (e.isIntersecting) e.target.classList.add('visible');
});
}, { threshold: 0.1 });
document.querySelectorAll('.fade-in').forEach(el => observer.observe(el));
// ============================================================
// EIGEN CLOUD DEMO
// ============================================================
(function() {
const c = document.getElementById('eigenCloud');
const ctx = c.getContext('2d');
let showEigen = false;
let points = [];
let eigenAngle = 0.4;
let eigenRatio = 0.3;
function generate() {
points = [];
eigenAngle = randRange(-0.8, 0.8);
eigenRatio = randRange(0.15, 0.45);
const s1 = 120, s2 = s1 * eigenRatio;
for (let i = 0; i < 200; i++) {
const u = gaussRand(), v = gaussRand();
const x = u * s1 * Math.cos(eigenAngle) - v * s2 * Math.sin(eigenAngle);
const y = u * s1 * Math.sin(eigenAngle) + v * s2 * Math.cos(eigenAngle);
points.push({ x, y });
}
}
generate();
function draw() {
ctx.clearRect(0, 0, c.width, c.height);
const cx = c.width / 2, cy = c.height / 2;
// grid
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
ctx.lineWidth = 1;
for (let x = 0; x < c.width; x += 50) {
ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, c.height); ctx.stroke();
}
for (let y = 0; y < c.height; y += 50) {
ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(c.width, y); ctx.stroke();
}
// points
for (const p of points) {
ctx.beginPath();
ctx.arc(cx + p.x, cy + p.y, 2.5, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(148, 163, 184, 0.5)';
ctx.fill();
}
if (showEigen) {
const s1 = 120, s2 = s1 * eigenRatio;
// eigenvector 1
const dx1 = Math.cos(eigenAngle) * 180;
const dy1 = Math.sin(eigenAngle) * 180;
drawArrow(ctx, cx, cy, cx + dx1, cy + dy1, '#6366f1', 3);
drawArrow(ctx, cx, cy, cx - dx1, cy - dy1, '#6366f1', 3);
// eigenvector 2
const dx2 = -Math.sin(eigenAngle) * 100;
const dy2 = Math.cos(eigenAngle) * 100;
drawArrow(ctx, cx, cy, cx + dx2, cy + dy2, '#22d3ee', 3);
drawArrow(ctx, cx, cy, cx - dx2, cy - dy2, '#22d3ee', 3);
ctx.fillStyle = '#6366f1';
ctx.font = '600 14px Inter';
ctx.fillText('v₁ (most variance)', cx + dx1 + 10, cy + dy1 - 10);
ctx.fillStyle = '#22d3ee';
ctx.fillText('v₂ (least variance)', cx + dx2 + 10, cy + dy2 - 10);
}
}
function drawArrow(ctx, x0, y0, x1, y1, color, width) {
const angle = Math.atan2(y1 - y0, x1 - x0);
const headLen = 12;
ctx.beginPath();
ctx.moveTo(x0, y0);
ctx.lineTo(x1, y1);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x1 - headLen * Math.cos(angle - 0.4), y1 - headLen * Math.sin(angle - 0.4));
ctx.lineTo(x1 - headLen * Math.cos(angle + 0.4), y1 - headLen * Math.sin(angle + 0.4));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
draw();
document.getElementById('reshuffleCloud').onclick = () => { generate(); draw(); };
document.getElementById('toggleEigen').onclick = function() {
showEigen = !showEigen;
this.classList.toggle('active', showEigen);
this.textContent = showEigen ? 'Hide Eigenvectors' : 'Show Eigenvectors';
draw();
};
})();
// ============================================================
// 2D ARCSIN vs ARCTAN2 COMPARISON
// ============================================================
(function() {
const cSin = document.getElementById('arcsinDemo');
const cTan = document.getElementById('arctan2Demo');
const ctxSin = cSin.getContext('2d');
const ctxTan = cTan.getContext('2d');
let angle = 0.5;
let dragging = false;
function drawComparison(ctx, canvas, method) {
const W = canvas.width, H = canvas.height;
const cx = W / 2, cy = H / 2, R = W / 2 - 30;
ctx.clearRect(0, 0, W, H);
// background circle
ctx.beginPath();
ctx.arc(cx, cy, R, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.1)';
ctx.lineWidth = 1;
ctx.stroke();
// axes
ctx.strokeStyle = 'rgba(255,255,255,0.07)';
ctx.beginPath(); ctx.moveTo(cx - R, cy); ctx.lineTo(cx + R, cy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(cx, cy - R); ctx.lineTo(cx, cy + R); ctx.stroke();
// valid range shading
if (method === 'arcsin') {
ctx.beginPath();
ctx.arc(cx, cy, R, -Math.PI / 2, Math.PI / 2);
ctx.lineTo(cx, cy);
ctx.closePath();
ctx.fillStyle = 'rgba(251, 146, 60, 0.07)';
ctx.fill();
// shade the restricted zone
ctx.beginPath();
ctx.arc(cx, cy, R, Math.PI / 2, Math.PI * 1.5);
ctx.lineTo(cx, cy);
ctx.closePath();
ctx.fillStyle = 'rgba(248, 113, 113, 0.05)';
ctx.fill();
} else {
ctx.beginPath();
ctx.arc(cx, cy, R, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(52, 211, 153, 0.05)';
ctx.fill();
}
// vector
const vx = Math.cos(angle);
const vy = Math.sin(angle);
let displayAngle;
if (method === 'arcsin') {
displayAngle = Math.asin(vy);
} else {
displayAngle = Math.atan2(vy, vx);
}
// Draw the vector the method "sees"
let seenVx, seenVy;
if (method === 'arcsin') {
seenVx = Math.cos(displayAngle);
seenVy = Math.sin(displayAngle);
} else {
seenVx = vx;
seenVy = vy;
}
// actual vector (faded)
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + vx * R * 0.85, cy - vy * R * 0.85);
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 2;
ctx.stroke();
// seen vector
const color = method === 'arcsin' ? '#fb923c' : '#34d399';
ctx.beginPath();
ctx.moveTo(cx, cy);
const ex = cx + seenVx * R * 0.85;
const ey = cy - seenVy * R * 0.85;
ctx.lineTo(ex, ey);
ctx.strokeStyle = color;
ctx.lineWidth = 3;
ctx.stroke();
// arrow head
const ha = Math.atan2(-(seenVy), seenVx);
const hl = 12;
ctx.beginPath();
ctx.moveTo(ex, ey);
ctx.lineTo(ex - hl * Math.cos(ha - 0.4), ey - hl * Math.sin(ha - 0.4));
ctx.lineTo(ex - hl * Math.cos(ha + 0.4), ey - hl * Math.sin(ha + 0.4));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
// dot at tip
ctx.beginPath();
ctx.arc(ex, ey, 5, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.fill();
// arc showing angle
ctx.beginPath();
if (displayAngle >= 0) {
ctx.arc(cx, cy, 35, 0, -displayAngle, true);
} else {
ctx.arc(cx, cy, 35, 0, -displayAngle, false);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2;
ctx.stroke();
// labels
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '11px Inter';
ctx.fillText('π₁', cx + R + 5, cy + 4);
ctx.fillText('π₂', cx - 5, cy - R - 8);
// update text
const deg = (displayAngle * 180 / Math.PI).toFixed(1);
const el = method === 'arcsin' ? document.getElementById('arcsinAngle') : document.getElementById('arctan2Angle');
el.textContent = `θ = ${deg}°`;
}
function draw() {
drawComparison(ctxSin, cSin, 'arcsin');
drawComparison(ctxTan, cTan, 'arctan2');
}
draw();
function handleMouse(e, canvas) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const scaleY = canvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX - canvas.width / 2;
const my = -((e.clientY - rect.top) * scaleY - canvas.height / 2);
angle = Math.atan2(my, mx);
draw();
}
[cSin, cTan].forEach(canvas => {
canvas.addEventListener('mousedown', (e) => { dragging = true; handleMouse(e, canvas); });
canvas.addEventListener('mousemove', (e) => { if (dragging) handleMouse(e, canvas); });
canvas.addEventListener('touchstart', (e) => { e.preventDefault(); dragging = true; handleMouse(e.touches[0], canvas); });
canvas.addEventListener('touchmove', (e) => { e.preventDefault(); if (dragging) handleMouse(e.touches[0], canvas); });
});
document.addEventListener('mouseup', () => dragging = false);
document.addEventListener('touchend', () => dragging = false);
})();
// ============================================================
// GIVENS ROTATION 3D DEMO
// ============================================================
(function() {
const c = document.getElementById('givensDemo');
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
let step = 0;
let animT = 0;
let autoPlay = false;
let autoTimer = null;
// Original vector
const orig = { x: 0.5, y: 0.7, z: 0.5 };
const len = Math.sqrt(orig.x * orig.x + orig.y * orig.y + orig.z * orig.z);
orig.x /= len; orig.y /= len; orig.z /= len;
function getVec(step, t) {
const cx = orig.x, cy = orig.y, cz = orig.z;
// Step 1: rotate in (x,y) to eliminate y
const angle1 = Math.atan2(cy, cx);
// Step 2: rotate in (x,z) to eliminate z
const r1 = Math.sqrt(cx * cx + cy * cy);
const angle2 = Math.atan2(cz, r1);
if (step === 0) return { x: cx, y: cy, z: cz };
// After step 1
const a1 = step >= 1 ? angle1 * Math.min(t, 1) : 0;
let vx = cx * Math.cos(a1) + cy * Math.sin(a1);
let vy = -cx * Math.sin(a1) + cy * Math.cos(a1);
let vz = cz;
if (step === 1) return { x: vx, y: vy, z: vz };
// After step 2
const a2 = step >= 2 ? angle2 * Math.min(t, 1) : 0;
const vx2 = vx * Math.cos(a2) + vz * Math.sin(a2);
const vy2 = vy;
const vz2 = -vx * Math.sin(a2) + vz * Math.cos(a2);
if (step === 2) return { x: vx2, y: vy2, z: vz2 };
return { x: vx2, y: vy2, z: vz2 };
}
function project(x, y, z) {
const scale = 150;
const angleY = 0.5;
const angleX = 0.3;
// rotate for view
const c1 = Math.cos(angleY), s1 = Math.sin(angleY);
const rx = x * c1 + z * s1;
const rz = -x * s1 + z * c1;
const c2 = Math.cos(angleX), s2 = Math.sin(angleX);
const ry = y * c2 - rz * s2;
const rz2 = y * s2 + rz * c2;
return {
px: W / 2 + rx * scale,
py: H / 2 - ry * scale,
depth: rz2
};
}
function drawArrow3D(x0, y0, z0, x1, y1, z1, color, width) {
const p0 = project(x0, y0, z0);
const p1 = project(x1, y1, z1);
ctx.beginPath();
ctx.moveTo(p0.px, p0.py);
ctx.lineTo(p1.px, p1.py);
ctx.strokeStyle = color;
ctx.lineWidth = width;
ctx.stroke();
// arrowhead
const a = Math.atan2(p1.py - p0.py, p1.px - p0.px);
const hl = 10;
ctx.beginPath();
ctx.moveTo(p1.px, p1.py);
ctx.lineTo(p1.px - hl * Math.cos(a - 0.4), p1.py - hl * Math.sin(a - 0.4));
ctx.lineTo(p1.px - hl * Math.cos(a + 0.4), p1.py - hl * Math.sin(a + 0.4));
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw axes
drawArrow3D(0,0,0, 1.4,0,0, 'rgba(255,255,255,0.15)', 1);
drawArrow3D(0,0,0, 0,1.4,0, 'rgba(255,255,255,0.15)', 1);
drawArrow3D(0,0,0, 0,0,1.4, 'rgba(255,255,255,0.15)', 1);
// axis labels
const lx = project(1.5, 0, 0);
const ly = project(0, 1.5, 0);
const lz = project(0, 0, 1.5);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '13px Inter';
ctx.fillText('x (π₁)', lx.px, lx.py);
ctx.fillText('y (π₂)', ly.px, ly.py);
ctx.fillText('z (π₃)', lz.px, lz.py);
// ghost of original
if (step > 0) {
drawArrow3D(0, 0, 0, orig.x, orig.y, orig.z, 'rgba(255,255,255,0.1)', 1);
}
// current vector
const v = getVec(step, 1);
const colors = ['#6366f1', '#34d399', '#22d3ee', '#fbbf24'];
drawArrow3D(0, 0, 0, v.x, v.y, v.z, colors[step], 3);
// dot
const p = project(v.x, v.y, v.z);
ctx.beginPath();
ctx.arc(p.px, p.py, 6, 0, Math.PI * 2);
ctx.fillStyle = colors[step];
ctx.fill();
// info
ctx.fillStyle = '#fff';
ctx.font = '600 14px Inter';
const labels = [
`Original: (${v.x.toFixed(2)}, ${v.y.toFixed(2)}, ${v.z.toFixed(2)})`,
`After R(x,y): y → 0 | (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(2)})`,
`After R(x,z): z → 0 | (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(3)})`,
`Aligned to x-axis! (${v.x.toFixed(2)}, ${v.y.toFixed(3)}, ${v.z.toFixed(3)})`
];
ctx.fillText(labels[step], 20, 30);
// components bars
const barX = W - 160, barY = 30, barW = 120, barH = 14;
const comps = [
{ label: 'x', val: Math.abs(v.x), color: '#f87171' },
{ label: 'y', val: Math.abs(v.y), color: '#34d399' },
{ label: 'z', val: Math.abs(v.z), color: '#60a5fa' },
];
comps.forEach((comp, i) => {
const y = barY + i * 24;
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillRect(barX, y, barW, barH);
ctx.fillStyle = comp.color;
ctx.fillRect(barX, y, barW * comp.val, barH);
ctx.fillStyle = '#fff';
ctx.font = '11px JetBrains Mono';
ctx.fillText(comp.label, barX - 15, y + 11);
});
}
draw();
function setStep(s) {
step = s;
draw();
['givensStep0','givensStep1','givensStep2','givensStep3'].forEach((id, i) => {
document.getElementById(id).classList.toggle('active', i === s);
});
}
document.getElementById('givensStep0').onclick = () => setStep(0);
document.getElementById('givensStep1').onclick = () => setStep(1);
document.getElementById('givensStep2').onclick = () => setStep(2);
document.getElementById('givensStep3').onclick = () => setStep(3);
document.getElementById('givensAuto').onclick = function() {
autoPlay = !autoPlay;
this.textContent = autoPlay ? '⏸ Pause' : '▶ Auto-Play';
if (autoPlay) {
let s = 0;
setStep(s);
autoTimer = setInterval(() => {
s = (s + 1) % 4;
setStep(s);
}, 1200);
} else {
clearInterval(autoTimer);
}
};
})();
// ============================================================
// POLAR PLOT DEMO
// ============================================================
(function() {
const c = document.getElementById('polarPlot');
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
let signalData = [];
let noiseData = [];
function generate() {
const strength = document.getElementById('signalStrength').value / 100;
const noise = document.getElementById('noiseLevel').value / 100;
signalData = [];
noiseData = [];
const baseAngle = 0.7;
for (let i = 0; i < 23; i++) {
const a = baseAngle + gaussRand() * (1 - strength) * 0.8;
const r = 0.5 + strength * 0.4 + gaussRand() * 0.08;
signalData.push({ angle: a, r: clamp(r, 0.1, 1) });
}
for (let i = 0; i < 23; i++) {
const a = Math.random() * Math.PI * 2;
const r = 0.15 + Math.random() * 0.35;
noiseData.push({ angle: a, r: r });
}
}
generate();
function draw() {
ctx.clearRect(0, 0, W, H);
// Two polar plots side by side
const plots = [
{ cx: W * 0.28, cy: H / 2, R: Math.min(W * 0.22, H * 0.4), data: signalData, color: '#34d399', label: 'Mode 1 (Signal)' },
{ cx: W * 0.72, cy: H / 2, R: Math.min(W * 0.22, H * 0.4), data: noiseData, color: '#f87171', label: 'Mode 4 (Noise)' },
];
plots.forEach(plot => {
const { cx, cy, R, data, color, label } = plot;
// concentric circles
for (let i = 1; i <= 4; i++) {
ctx.beginPath();
ctx.arc(cx, cy, R * i / 4, 0, Math.PI * 2);
ctx.strokeStyle = 'rgba(255,255,255,0.06)';
ctx.lineWidth = 1;
ctx.stroke();
}
// radial lines
for (let a = 0; a < Math.PI * 2; a += Math.PI / 6) {
ctx.beginPath();
ctx.moveTo(cx, cy);
ctx.lineTo(cx + Math.cos(a) * R, cy - Math.sin(a) * R);
ctx.strokeStyle = 'rgba(255,255,255,0.04)';
ctx.stroke();
}
// points
data.forEach(d => {
const px = cx + Math.cos(d.angle) * d.r * R;
const py = cy - Math.sin(d.angle) * d.r * R;
ctx.beginPath();
ctx.arc(px, py, 5, 0, Math.PI * 2);
ctx.fillStyle = color;
ctx.globalAlpha = 0.7;
ctx.fill();
ctx.globalAlpha = 1;
});
// label
ctx.fillStyle = color;
ctx.font = '600 14px Inter';
ctx.textAlign = 'center';
ctx.fillText(label, cx, cy + R + 30);
ctx.textAlign = 'left';
// degree labels
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.font = '10px Inter';
ctx.textAlign = 'center';
ctx.fillText('0°', cx + R + 12, cy + 4);
ctx.fillText('90°', cx, cy - R - 6);
ctx.fillText('180°', cx - R - 16, cy + 4);
ctx.fillText('270°', cx, cy + R + 14);
ctx.textAlign = 'left';
});
}
draw();
document.getElementById('signalStrength').oninput = () => { generate(); draw(); };
document.getElementById('noiseLevel').oninput = () => { generate(); draw(); };
document.getElementById('regeneratePolar').onclick = () => { generate(); draw(); };
})();
// ============================================================
// DYNAMIC STABILIZATION DEMO
// ============================================================
(function() {
const c = document.getElementById('dynamicStab');
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
const N = 20;
const baseAngle = 0.6;
const raw = [];
const filtered = [];
for (let i = 0; i < N; i++) {
raw.push(baseAngle + gaussRand() * 0.3);
}
// simple 5-point filter
for (let i = 0; i < N; i++) {
let sum = 0, wt = 0;
for (let j = Math.max(0, i - 2); j <= i; j++) {
const w = 1;
sum += raw[j] * w;
wt += w;
}
filtered.push(sum / wt);
}
function draw() {
ctx.clearRect(0, 0, W, H);
const padL = 40, padR = 20, padT = 20, padB = 40;
const gw = W - padL - padR;
const gh = H - padT - padB;
// y range
const allVals = [...raw, ...filtered];
const ymin = Math.min(...allVals) - 0.2;
const ymax = Math.max(...allVals) + 0.2;
function toX(i) { return padL + (i / (N - 1)) * gw; }
function toY(v) { return padT + (1 - (v - ymin) / (ymax - ymin)) * gh; }
// grid
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
for (let i = 0; i < N; i++) {
const x = toX(i);
ctx.beginPath(); ctx.moveTo(x, padT); ctx.lineTo(x, padT + gh); ctx.stroke();
}
// raw
ctx.beginPath();
raw.forEach((v, i) => {
if (i === 0) ctx.moveTo(toX(i), toY(v));
else ctx.lineTo(toX(i), toY(v));
});
ctx.strokeStyle = 'rgba(255,255,255,0.2)';
ctx.lineWidth = 1;
ctx.stroke();
raw.forEach((v, i) => {
ctx.beginPath();
ctx.arc(toX(i), toY(v), 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.fill();
});
// filtered
ctx.beginPath();
filtered.forEach((v, i) => {
if (i === 0) ctx.moveTo(toX(i), toY(v));
else ctx.lineTo(toX(i), toY(v));
});
ctx.strokeStyle = '#22d3ee';
ctx.lineWidth = 2;
ctx.stroke();
filtered.forEach((v, i) => {
ctx.beginPath();
ctx.arc(toX(i), toY(v), 4, 0, Math.PI * 2);
ctx.fillStyle = '#22d3ee';
ctx.fill();
});
// labels
ctx.fillStyle = 'rgba(255,255,255,0.3)';
ctx.font = '10px Inter';
ctx.fillText('Raw', padL, padT - 5);
ctx.fillStyle = '#22d3ee';
ctx.fillText('Filtered', padL + 40, padT - 5);
ctx.fillStyle = 'var(--text3)';
ctx.font = '10px Inter';
ctx.fillText('time →', W / 2 - 15, H - 5);
ctx.save();
ctx.translate(10, H / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.fillText('θ (angle)', 0, 0);
ctx.restore();
}
draw();
})();
// ============================================================
// STATIC STABILIZATION DEMO
// ============================================================
(function() {
const c = document.getElementById('staticStab');
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
const N = 20;
const raw = [];
for (let i = 0; i < N; i++) {
raw.push(Math.random() * Math.PI * 2 - Math.PI);
}
function draw() {
ctx.clearRect(0, 0, W, H);
const padL = 40, padR = 20, padT = 20, padB = 40;
const gw = W - padL - padR;
const gh = H - padT - padB;
const ymin = -Math.PI - 0.3;
const ymax = Math.PI + 0.3;
function toX(i) { return padL + (i / (N - 1)) * gw; }
function toY(v) { return padT + (1 - (v - ymin) / (ymax - ymin)) * gh; }
// grid
ctx.strokeStyle = 'rgba(255,255,255,0.05)';
for (let i = 0; i < N; i++) {
const x = toX(i);
ctx.beginPath(); ctx.moveTo(x, padT); ctx.lineTo(x, padT + gh); ctx.stroke();
}
// zero line
ctx.beginPath();
ctx.moveTo(padL, toY(0));
ctx.lineTo(padL + gw, toY(0));
ctx.strokeStyle = '#f472b6';
ctx.lineWidth = 2;
ctx.setLineDash([5, 5]);
ctx.stroke();
ctx.setLineDash([]);
// raw (scattered)
ctx.beginPath();
raw.forEach((v, i) => {
if (i === 0) ctx.moveTo(toX(i), toY(v));
else ctx.lineTo(toX(i), toY(v));
});
ctx.strokeStyle = 'rgba(255,255,255,0.15)';
ctx.lineWidth = 1;
ctx.stroke();
raw.forEach((v, i) => {
ctx.beginPath();
ctx.arc(toX(i), toY(v), 3, 0, Math.PI * 2);
ctx.fillStyle = 'rgba(248,113,113,0.5)';
ctx.fill();
});
// labels
ctx.fillStyle = 'rgba(248,113,113,0.5)';
ctx.font = '10px Inter';
ctx.fillText('Random (noise)', padL, padT - 5);
ctx.fillStyle = '#f472b6';
ctx.fillText('Fixed to θ=0', padL + 100, padT - 5);
ctx.fillStyle = 'rgba(255,255,255,0.2)';
ctx.font = '10px Inter';
ctx.fillText('time →', W / 2 - 15, H - 5);
ctx.save();
ctx.translate(10, H / 2);
ctx.rotate(-Math.PI / 2);
ctx.fillText('θ (angle)', 0, 0);
ctx.restore();
}
draw();
})();
// ============================================================
// CORRELATION MATRIX DEMO
// ============================================================
(function() {
const c = document.getElementById('corrMatrix');
const ctx = c.getContext('2d');
const W = c.width, H = c.height;
const N = 7;
// generate 3 correlation matrices
function genCorrMatrix(noiseLevel) {
const mat = [];
for (let i = 0; i < N; i++) {
mat.push([]);
for (let j = 0; j < N; j++) {
if (i === j) mat[i].push(1);
else if (j > i) {
let base = 0.3 + 0.4 * Math.exp(-Math.abs(i - j) * 0.5);
base += (Math.random() - 0.5) * noiseLevel;
base = clamp(base, -0.3, 0.95);
mat[i].push(base);
} else {
mat[i].push(0);
}
}
}
// symmetrize
for (let i = 0; i < N; i++)
for (let j = 0; j < i; j++)
mat[i][j] = mat[j][i];
return mat;
}
const rawMat = genCorrMatrix(0.4);
const dynMat = genCorrMatrix(0.2);
const statMat = genCorrMatrix(0.08);
let currentMat = rawMat;
function drawMatrix(mat, ox, title) {
const cellSize = Math.min(28, (H - 60) / N);
const totalW = cellSize * N;
const startX = ox;
const startY = 40;
ctx.fillStyle = '#fff';
ctx.font = '600 12px Inter';
ctx.textAlign = 'center';
ctx.fillText(title, startX + totalW / 2, 25);
for (let i = 0; i < N; i++) {
for (let j = 0; j < N; j++) {
const v = mat[i][j];
const x = startX + j * cellSize;
const y = startY + i * cellSize;
// color mapping
let r, g, b;
if (v >= 0) {
r = Math.round(30 + 69 * v);
g = Math.round(30 + 181 * v);
b = Math.round(50 + 200 * v);
} else {
r = Math.round(30 - 218 * v);
g = Math.round(30 + 80 * v);
b = Math.round(50 + 80 * v);
}
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fillRect(x + 1, y + 1, cellSize - 2, cellSize - 2);
}
}
ctx.textAlign = 'left';
}
function draw() {
ctx.clearRect(0, 0, W, H);
const spacing = (W - 3 * N * 28) / 4;
drawMatrix(rawMat, spacing, 'Raw');
drawMatrix(dynMat, spacing * 2 + N * 28, '+ Dynamic Filter');
drawMatrix(statMat, spacing * 3 + N * 28 * 2, '+ Static Stabilize');
}
draw();
// Highlight the active one
function setMode(mode) {
['corrRaw', 'corrDynamic', 'corrStatic'].forEach(id => {
document.getElementById(id).classList.remove('active');
});
document.getElementById('corr' + mode).classList.add('active');
draw();
// add highlight
const spacing = (W - 3 * N * 28) / 4;
const cellSize = 28;
let ox;
if (mode === 'Raw') ox = spacing;
else if (mode === 'Dynamic') ox = spacing * 2 + N * 28;
else ox = spacing * 3 + N * 28 * 2;
ctx.strokeStyle = '#6366f1';
ctx.lineWidth = 3;
ctx.strokeRect(ox - 2, 38, N * cellSize + 4, N * cellSize + 4);
}
document.getElementById('corrRaw').onclick = () => setMode('Raw');
document.getElementById('corrDynamic').onclick = () => setMode('Dynamic');
document.getElementById('corrStatic').onclick = () => setMode('Static');
setMode('Raw');
})();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment