
Tech Visualizer
- 3 installs
- Updated February 9, 2026
- yfe404/tech-visualizer
Helps with ai & agent building tasks during AI-assisted development.
About
tech-visualizer is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- tech-visualizer
- AI & Agent Building
- AI-coding skill
Tech Visualizer by the numbers
- 3 all-time installs (skills.sh)
- Ranked #13,657 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Jul 28, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yfe404/tech-visualizer --skill tech-visualizerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 3 |
|---|---|
| Last updated | February 9, 2026 |
| Repository | yfe404/tech-visualizer ↗ |
What it does
Helps with ai & agent building tasks during AI-assisted development.
Files
Tech Visualizer
Create stunning, interactive visualizations that turn complex technical concepts into intuitive, explorable experiences. Every visualization should make the viewer think "now I finally get it."
When to Read Reference Files
Before building, read the appropriate reference:
references/patterns.md— Visualization component patterns, layout strategies, and interaction blueprints. Always read this first.
Design Philosophy
The "Aha Moment" Principle
Every visualization must have a clear aha moment — the single interaction or animation that makes the concept click. Identify it before writing any code.
Examples:
- HMAC-SHA: Seeing the inner and outer padding XOR with the key, then watching data
flow through the two hash rounds
- AES-CBC: Watching the XOR chain where each block's ciphertext feeds into the next
block's encryption — showing WHY changing one plaintext block cascades
- ORB keypoints: Seeing the FAST corner detector sweep across an image, lighting up
detected corners, then watching BRIEF descriptors form as binary comparison patterns
Visual Identity
Every visualization should feel like a premium interactive textbook illustration, not a generic flowchart. Think: 3Blue1Brown meets an interactive data dashboard.
Core aesthetic principles:
- Dark theme by default with vibrant, high-contrast accent colors for data flow
- Monospace fonts for data/hex values, clean sans-serif for labels
- Purposeful animation — every motion represents actual data transformation
- Depth through layering — use subtle shadows, glassmorphism, or gradients to
separate conceptual layers (e.g., application layer vs transport layer)
- Color encodes meaning — establish a color legend early: input data, keys,
intermediate state, output. Keep it consistent throughout
Output Format Decision
Choose based on complexity:
| Complexity | Format | When |
|---|---|---|
| Single algorithm, linear flow | HTML (.html) | HMAC, SHA-256, base64 encoding |
| Multi-stage with rich state | React (.jsx) | AES-CBC, TLS handshake, TCP state machine |
| Comparison / side-by-side | React (.jsx) | ECB vs CBC, RSA vs ECC, BFS vs DFS |
| Data structure with mutations | React (.jsx) | B-tree insertion, hash table collision |
When in doubt, use React — it handles state management for interactive controls more cleanly.
Building a Visualization
Step 1: Decompose the Concept
Break the technical concept into stages that can be individually visualized:
Concept → [ Stage 1 ] → [ Stage 2 ] → ... → [ Stage N ]
↓ ↓ ↓
Visual repr Visual repr Visual reprEach stage should have a clear input/output shown visually, transform data in a way that can be animated, and connect to the previous stage with a visible data flow line.
Step 2: Design the Interaction Model
Layer these interaction types (use ALL that apply):
1. Step-by-step controls: Play/pause, step forward/back, speed slider. This is the primary navigation. Use a prominent step indicator (e.g., "Step 3 of 7: XOR with round key").
2. Live input fields: Let users type their own plaintext, key, URL, etc. The entire visualization should reactively update. Use debounced inputs to avoid jank.
3. Hover/click inspection: Hovering over any data block, wire, or intermediate value should show a tooltip or panel with the raw data, hex representation, or explanation. Clicking can "pin" the inspection panel.
4. Side-by-side comparison: When the concept has variants (ECB vs CBC, HTTP/1.1 vs HTTP/2), show them simultaneously with synchronized step controls.
Step 3: Implement with Polish
Read references/patterns.md for detailed layout templates, animation patterns, and data representation strategies for each concept category.
Key principles:
- Top-to-bottom or left-to-right flow mirroring the algorithm's mental model
- Staggered reveals: Animate data blocks appearing 30-50ms apart
- Active element highlighting: Pulse/glow for current, dim for completed, base opacity for upcoming
- Data flow lines: Animate SVG paths with
stroke-dashoffsetfor "drawing" effects - Transition duration: 300-500ms for state changes, 150ms for hover effects
Step 4: Add Context & Learning
- Step descriptions: Each step must have a 1-2 sentence plain-English explanation
visible alongside the visualization (not just in tooltips)
- "Why does this matter?" callouts: At key stages, add a subtle info box explaining
the security/performance/correctness implication
- Edge case demonstrations: Add buttons like "What if the key is all zeros?" or
"What happens with identical plaintext blocks?" that demonstrate important properties
Step 5: Responsive & Accessible
- Works at 768px+ width (optimized for desktop, functional on tablet)
aria-labelon interactive elements- Keyboard navigation for step controls (arrow keys)
- Color is not the only differentiator (use shape/pattern too)
Quality Checklist
Before delivering, verify:
- [ ] Dark theme with consistent color coding throughout
- [ ] Step-by-step controls (forward, back, play/pause, reset)
- [ ] At least one live input field that reactively updates
- [ ] Hover inspection on data elements
- [ ] Step descriptions visible and accurate
- [ ] Smooth animations (no layout shifts or flicker)
- [ ] Color legend present
- [ ] The "aha moment" is clearly delivered
- [ ] Google Fonts loaded for typography
- [ ] No hardcoded magic numbers without comments
Anti-Patterns to Avoid
- ❌ Static flowcharts with no interactivity
- ❌ Walls of text explaining the algorithm — the visualization IS the explanation
- ❌ Generic bootstrap/material UI appearance
- ❌ Tooltips as the only information pathway — key details should be always visible
- ❌ Animations that don't map to real data transformations
- ❌ Light theme with muted pastels (dark + vibrant is the default)
- ❌ Skipping the decomposition step — always plan stages before coding
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>HMAC-SHA256 — Interactive Visualization</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=DM+Sans:wght@400;500;600;700&display=swap');
:root {
--bg-primary: #0f1117;
--bg-surface: #1a1d27;
--bg-deep: #12141c;
--bg-elevated: #252836;
--text-primary: #e4e7ec;
--text-secondary: #9ba3b5;
--text-muted: #5d6678;
--accent-input: #60a5fa;
--accent-key: #f59e0b;
--accent-output: #34d399;
--accent-intermediate: #22d3ee;
--accent-error: #f87171;
--accent-highlight: #a78bfa;
--border: #2a2d3a;
--border-active: #3d4155;
--font-mono: 'JetBrains Mono', monospace;
--font-sans: 'DM Sans', system-ui, sans-serif;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
background: var(--bg-primary);
color: var(--text-primary);
font-family: var(--font-sans);
min-height: 100vh;
overflow-x: hidden;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 24px 20px 60px;
}
/* Header */
.header {
text-align: center;
margin-bottom: 32px;
padding-top: 16px;
}
.header h1 {
font-size: 1.75rem;
font-weight: 700;
background: linear-gradient(135deg, var(--accent-input), var(--accent-highlight));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 6px;
}
.header p {
color: var(--text-secondary);
font-size: 0.9rem;
}
/* Controls */
.controls {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
padding: 14px 20px;
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 12px;
margin-bottom: 24px;
flex-wrap: wrap;
}
.controls button {
background: var(--bg-elevated);
border: 1px solid var(--border);
color: var(--text-primary);
padding: 8px 14px;
border-radius: 8px;
cursor: pointer;
font-family: var(--font-sans);
font-size: 0.85rem;
font-weight: 500;
transition: all 0.15s;
display: flex;
align-items: center;
gap: 6px;
}
.controls button:hover:not(:disabled) {
background: var(--border-active);
border-color: var(--accent-highlight);
}
.controls button:disabled {
opacity: 0.35;
cursor: not-allowed;
}
.controls button.play-btn {
background: var(--accent-highlight);
border-color: var(--accent-highlight);
color: #fff;
}
.controls button.play-btn:hover {
filter: brightness(1.15);
}
.step-indicator {
font-family: var(--font-mono);
font-size: 0.8rem;
color: var(--text-secondary);
min-width: 100px;
text-align: center;
}
.speed-control {
display: flex;
align-items: center;
gap: 6px;
}
.speed-control input[type="range"] {
width: 70px;
accent-color: var(--accent-highlight);
}
.speed-control span {
font-size: 0.7rem;
color: var(--text-muted);
font-family: var(--font-mono);
min-width: 28px;
}
/* Input Fields */
.inputs {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 14px;
margin-bottom: 28px;
}
.input-group {
display: flex;
flex-direction: column;
gap: 5px;
}
.input-group label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.06em;
}
.input-group.message label { color: var(--accent-input); }
.input-group.key label { color: var(--accent-key); }
.input-group input {
background: var(--bg-deep);
border: 1px solid var(--border);
border-radius: 8px;
padding: 10px 14px;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 0.85rem;
outline: none;
transition: border-color 0.15s;
}
.input-group input:focus {
border-color: var(--accent-input);
}
.input-group.key input:focus {
border-color: var(--accent-key);
}
/* Stage */
.stage {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: 14px;
padding: 22px;
margin-bottom: 16px;
position: relative;
transition: all 0.4s ease;
opacity: 0.3;
transform: translateY(4px);
}
.stage.active {
opacity: 1;
transform: translateY(0);
border-color: var(--accent-highlight);
box-shadow: 0 0 0 1px var(--accent-highlight),
0 0 30px rgba(167, 139, 250, 0.08),
inset 0 0 30px rgba(167, 139, 250, 0.02);
}
.stage.completed {
opacity: 0.65;
transform: translateY(0);
filter: saturate(0.8);
}
.stage.upcoming {
opacity: 0.25;
}
.stage-header {
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 14px;
}
.stage-number {
width: 28px;
height: 28px;
border-radius: 50%;
background: var(--bg-elevated);
display: flex;
align-items: center;
justify-content: center;
font-family: var(--font-mono);
font-size: 0.75rem;
font-weight: 600;
color: var(--text-muted);
flex-shrink: 0;
}
.stage.active .stage-number {
background: var(--accent-highlight);
color: #fff;
}
.stage.completed .stage-number {
background: var(--accent-output);
color: #000;
}
.stage-title {
font-size: 1rem;
font-weight: 600;
}
.stage-desc {
font-size: 0.82rem;
color: var(--text-secondary);
line-height: 1.5;
margin-bottom: 14px;
}
/* Data Display */
.data-row {
display: flex;
align-items: flex-start;
gap: 10px;
margin-bottom: 8px;
}
.data-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
min-width: 65px;
padding-top: 4px;
flex-shrink: 0;
}
.data-label.input-color { color: var(--accent-input); }
.data-label.key-color { color: var(--accent-key); }
.data-label.inter-color { color: var(--accent-intermediate); }
.data-label.output-color { color: var(--accent-output); }
.data-label.highlight-color { color: var(--accent-highlight); }
.byte-grid {
display: flex;
flex-wrap: wrap;
gap: 2px;
font-family: var(--font-mono);
font-size: 0.72rem;
}
.byte-cell {
padding: 3px 5px;
border-radius: 3px;
background: var(--bg-deep);
transition: all 0.25s ease;
cursor: default;
position: relative;
}
.byte-cell.highlight-key { background: rgba(245, 158, 11, 0.2); color: var(--accent-key); }
.byte-cell.highlight-input { background: rgba(96, 165, 250, 0.2); color: var(--accent-input); }
.byte-cell.highlight-inter { background: rgba(34, 211, 238, 0.2); color: var(--accent-intermediate); }
.byte-cell.highlight-output { background: rgba(52, 211, 153, 0.2); color: var(--accent-output); }
.byte-cell.highlight-active {
background: rgba(167, 139, 250, 0.3);
color: var(--accent-highlight);
animation: pulse 1.2s ease infinite;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(167, 139, 250, 0.3); }
50% { box-shadow: 0 0 8px 2px rgba(167, 139, 250, 0.15); }
}
/* XOR Visual */
.xor-visual {
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 12px;
background: var(--bg-deep);
border-radius: 10px;
margin: 10px 0;
}
.xor-row {
display: flex;
align-items: center;
gap: 3px;
font-family: var(--font-mono);
font-size: 0.7rem;
}
.xor-op {
font-size: 0.85rem;
font-weight: 700;
color: var(--accent-highlight);
padding: 0 8px;
}
.xor-divider {
width: 100%;
max-width: 400px;
height: 1px;
background: var(--border);
margin: 2px 0;
}
/* Flow Arrow */
.flow-arrow {
display: flex;
justify-content: center;
padding: 6px 0;
}
.flow-arrow svg {
opacity: 0.4;
transition: opacity 0.3s;
}
.flow-arrow.active svg {
opacity: 1;
}
/* Info Callout */
.callout {
background: rgba(167, 139, 250, 0.06);
border-left: 3px solid var(--accent-highlight);
border-radius: 0 8px 8px 0;
padding: 10px 14px;
margin-top: 12px;
font-size: 0.8rem;
color: var(--text-secondary);
line-height: 1.5;
}
.callout strong {
color: var(--accent-highlight);
}
/* Hash Box */
.hash-box {
background: var(--bg-deep);
border: 1px solid var(--border);
border-radius: 10px;
padding: 14px;
text-align: center;
margin: 10px 0;
transition: all 0.4s;
}
.hash-box.active-hash {
border-color: var(--accent-highlight);
box-shadow: 0 0 20px rgba(167, 139, 250, 0.1);
}
.hash-box .hash-label {
font-size: 0.7rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
margin-bottom: 6px;
}
.hash-box .hash-value {
font-family: var(--font-mono);
font-size: 0.72rem;
word-break: break-all;
line-height: 1.6;
}
/* Legend */
.legend {
display: flex;
gap: 16px;
justify-content: center;
padding: 12px;
flex-wrap: wrap;
margin-top: 8px;
}
.legend-item {
display: flex;
align-items: center;
gap: 6px;
font-size: 0.72rem;
color: var(--text-muted);
}
.legend-dot {
width: 10px;
height: 10px;
border-radius: 3px;
}
/* Tooltip */
.tooltip-wrap {
position: relative;
display: inline-block;
}
.tooltip-content {
display: none;
position: absolute;
bottom: calc(100% + 6px);
left: 50%;
transform: translateX(-50%);
background: var(--bg-elevated);
border: 1px solid var(--border);
border-radius: 8px;
padding: 8px 12px;
font-family: var(--font-mono);
font-size: 0.7rem;
white-space: nowrap;
z-index: 100;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.4);
pointer-events: none;
}
.tooltip-wrap:hover .tooltip-content {
display: block;
}
/* Final Output */
.final-output {
background: linear-gradient(135deg, rgba(52, 211, 153, 0.05), rgba(34, 211, 238, 0.05));
border: 1px solid var(--accent-output);
border-radius: 14px;
padding: 22px;
text-align: center;
transition: all 0.5s;
opacity: 0.2;
}
.final-output.revealed {
opacity: 1;
box-shadow: 0 0 40px rgba(52, 211, 153, 0.08);
}
.final-output h3 {
color: var(--accent-output);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.08em;
margin-bottom: 10px;
}
.final-output .hash-value {
font-family: var(--font-mono);
font-size: 0.78rem;
color: var(--accent-output);
word-break: break-all;
line-height: 1.7;
}
/* Responsive */
@media (max-width: 600px) {
.inputs { grid-template-columns: 1fr; }
.controls { gap: 8px; }
.byte-cell { font-size: 0.65rem; padding: 2px 4px; }
}
/* Presets */
.presets {
display: flex;
gap: 8px;
justify-content: center;
margin-bottom: 20px;
flex-wrap: wrap;
}
.presets button {
background: var(--bg-surface);
border: 1px solid var(--border);
color: var(--text-secondary);
padding: 6px 12px;
border-radius: 20px;
cursor: pointer;
font-family: var(--font-sans);
font-size: 0.75rem;
transition: all 0.15s;
}
.presets button:hover {
border-color: var(--accent-intermediate);
color: var(--accent-intermediate);
}
.badge {
display: inline-block;
font-size: 0.6rem;
font-family: var(--font-sans);
font-weight: 600;
padding: 2px 6px;
border-radius: 4px;
text-transform: uppercase;
letter-spacing: 0.04em;
margin-left: 4px;
vertical-align: middle;
}
.badge.security { background: rgba(248, 113, 113, 0.15); color: var(--accent-error); }
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>HMAC-SHA256</h1>
<p>Hash-based Message Authentication Code — step by step</p>
</div>
<div class="inputs">
<div class="input-group message">
<label>Message</label>
<input id="msgInput" type="text" value="Hello, World!" placeholder="Enter message..." />
</div>
<div class="input-group key">
<label>Secret Key</label>
<input id="keyInput" type="text" value="my-secret-key" placeholder="Enter key..." />
</div>
</div>
<div class="presets">
<button onclick="setPreset('Hello, World!','my-secret-key')">Default</button>
<button onclick="setPreset('','my-secret-key')">Empty message</button>
<button onclick="setPreset('Hello, World!','')">Empty key</button>
<button onclick="setPreset('Attack at dawn','supersecretkey123')">Secret ops</button>
</div>
<div class="controls">
<button id="resetBtn" onclick="goToStep(0)">⏮ Reset</button>
<button id="prevBtn" onclick="prevStep()">◀ Prev</button>
<button id="playBtn" class="play-btn" onclick="togglePlay()">▶ Play</button>
<button id="nextBtn" onclick="nextStep()">Next ▶</button>
<span class="step-indicator" id="stepIndicator">Step 0 / 6</span>
<div class="speed-control">
<span>⚡</span>
<input type="range" id="speedSlider" min="0.5" max="3" step="0.25" value="1" oninput="updateSpeed()" />
<span id="speedLabel">1×</span>
</div>
</div>
<div id="stages"></div>
<div class="legend">
<div class="legend-item"><div class="legend-dot" style="background:var(--accent-input)"></div>Message data</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent-key)"></div>Key / padding</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent-intermediate)"></div>Intermediate</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent-highlight)"></div>Active operation</div>
<div class="legend-item"><div class="legend-dot" style="background:var(--accent-output)"></div>Final HMAC</div>
</div>
</div>
<script>
// --- Minimal SHA-256 for demo (simplified, not production) ---
function sha256(message) {
// Use SubtleCrypto if available, but for sync demo we use a JS implementation
const utf8 = new TextEncoder().encode(message);
return sha256Bytes(utf8);
}
function sha256Bytes(bytes) {
const K = [
0x428a2f98,0x71374491,0xb5c0fbcf,0xe9b5dba5,0x3956c25b,0x59f111f1,0x923f82a4,0xab1c5ed5,
0xd807aa98,0x12835b01,0x243185be,0x550c7dc3,0x72be5d74,0x80deb1fe,0x9bdc06a7,0xc19bf174,
0xe49b69c1,0xefbe4786,0x0fc19dc6,0x240ca1cc,0x2de92c6f,0x4a7484aa,0x5cb0a9dc,0x76f988da,
0x983e5152,0xa831c66d,0xb00327c8,0xbf597fc7,0xc6e00bf3,0xd5a79147,0x06ca6351,0x14292967,
0x27b70a85,0x2e1b2138,0x4d2c6dfc,0x53380d13,0x650a7354,0x766a0abb,0x81c2c92e,0x92722c85,
0xa2bfe8a1,0xa81a664b,0xc24b8b70,0xc76c51a3,0xd192e819,0xd6990624,0xf40e3585,0x106aa070,
0x19a4c116,0x1e376c08,0x2748774c,0x34b0bcb5,0x391c0cb3,0x4ed8aa4a,0x5b9cca4f,0x682e6ff3,
0x748f82ee,0x78a5636f,0x84c87814,0x8cc70208,0x90befffa,0xa4506ceb,0xbef9a3f7,0xc67178f2
];
let H = [0x6a09e667,0xbb67ae85,0x3c6ef372,0xa54ff53a,0x510e527f,0x9b05688c,0x1f83d9ab,0x5be0cd19];
const l = bytes.length;
const padded = new Uint8Array(((l + 9 + 63) & ~63));
padded.set(bytes);
padded[l] = 0x80;
const dv = new DataView(padded.buffer);
dv.setUint32(padded.length - 4, l * 8, false);
for (let off = 0; off < padded.length; off += 64) {
const W = new Uint32Array(64);
for (let i = 0; i < 16; i++) W[i] = dv.getUint32(off + i * 4, false);
for (let i = 16; i < 64; i++) {
const s0 = (ror(W[i-15],7)^ror(W[i-15],18)^(W[i-15]>>>3))>>>0;
const s1 = (ror(W[i-2],17)^ror(W[i-2],19)^(W[i-2]>>>10))>>>0;
W[i] = (W[i-16]+s0+W[i-7]+s1)>>>0;
}
let [a,b,c,d,e,f,g,h] = H;
for (let i = 0; i < 64; i++) {
const S1 = (ror(e,6)^ror(e,11)^ror(e,25))>>>0;
const ch = ((e&f)^(~e&g))>>>0;
const t1 = (h+S1+ch+K[i]+W[i])>>>0;
const S0 = (ror(a,2)^ror(a,13)^ror(a,22))>>>0;
const maj = ((a&b)^(a&c)^(b&c))>>>0;
const t2 = (S0+maj)>>>0;
h=g; g=f; f=e; e=(d+t1)>>>0; d=c; c=b; b=a; a=(t1+t2)>>>0;
}
H = H.map((v,i)=>(v+[a,b,c,d,e,f,g,h][i])>>>0);
}
return H.map(v=>v.toString(16).padStart(8,'0')).join('');
}
function ror(n,b){return((n>>>b)|(n<<(32-b)))>>>0;}
function toHexBytes(str) {
return Array.from(new TextEncoder().encode(str)).map(b => b.toString(16).padStart(2, '0'));
}
function hexStringToBytes(hex) {
const bytes = [];
for (let i = 0; i < hex.length; i += 2) bytes.push(parseInt(hex.substr(i, 2), 16));
return bytes;
}
function xorArrays(a, b) {
return a.map((v, i) => (v ^ (b[i] || 0)));
}
function bytesToHex(arr) {
return arr.map(b => (b & 0xff).toString(16).padStart(2, '0'));
}
function computeHMAC(message, key) {
const BLOCK_SIZE = 64;
let keyBytes = Array.from(new TextEncoder().encode(key));
// Step 1: Key preparation
if (keyBytes.length > BLOCK_SIZE) {
const hashed = sha256Bytes(new Uint8Array(keyBytes));
keyBytes = hexStringToBytes(hashed);
}
const paddedKey = keyBytes.concat(new Array(BLOCK_SIZE - keyBytes.length).fill(0));
// Step 2: Inner padding (ipad = 0x36)
const ipad = new Array(BLOCK_SIZE).fill(0x36);
const innerKey = xorArrays(paddedKey, ipad);
// Step 3: Inner hash = SHA256(innerKey || message)
const msgBytes = Array.from(new TextEncoder().encode(message));
const innerData = innerKey.concat(msgBytes);
const innerHash = sha256Bytes(new Uint8Array(innerData));
// Step 4: Outer padding (opad = 0x5c)
const opad = new Array(BLOCK_SIZE).fill(0x5c);
const outerKey = xorArrays(paddedKey, opad);
// Step 5: Outer hash = SHA256(outerKey || innerHash)
const innerHashBytes = hexStringToBytes(innerHash);
const outerData = outerKey.concat(innerHashBytes);
const outerHash = sha256Bytes(new Uint8Array(outerData));
return {
keyBytes,
paddedKey: bytesToHex(paddedKey),
ipad: bytesToHex(ipad),
innerKey: bytesToHex(innerKey),
msgBytes: bytesToHex(msgBytes.map(b=>b&0xff)),
innerHash,
opad: bytesToHex(opad),
outerKey: bytesToHex(outerKey),
outerHash,
};
}
// --- State ---
let currentStep = 0;
const TOTAL_STEPS = 7;
let isPlaying = false;
let playInterval = null;
let speed = 1;
function getState() {
const msg = document.getElementById('msgInput').value;
const key = document.getElementById('keyInput').value;
return computeHMAC(msg, key);
}
function renderByteGrid(hexArr, highlightClass, maxShow = 32) {
const show = hexArr.slice(0, maxShow);
const truncated = hexArr.length > maxShow;
let html = show.map((h, i) => {
const sep = (i > 0 && i % 4 === 0) ? '<span style="width:4px;display:inline-block"></span>' : '';
return `${sep}<span class="tooltip-wrap"><span class="byte-cell ${highlightClass}">${h}</span><span class="tooltip-content">Byte ${i}: 0x${h} = ${parseInt(h,16)}</span></span>`;
}).join('');
if (truncated) html += `<span class="byte-cell" style="color:var(--text-muted)">… +${hexArr.length - maxShow}</span>`;
return `<div class="byte-grid">${html}</div>`;
}
function flowArrow(active) {
return `<div class="flow-arrow ${active ? 'active' : ''}">
<svg width="24" height="36" viewBox="0 0 24 36">
<path d="M12 2 L12 28 M6 22 L12 30 L18 22" stroke="${active ? '#a78bfa' : '#3d4155'}" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
</div>`;
}
function stageClass(stageIndex) {
if (stageIndex < currentStep) return 'completed';
if (stageIndex === currentStep) return 'active';
return 'upcoming';
}
function render() {
const s = getState();
const stages = [];
// Stage 0: Key Preparation
stages.push(`
<div class="stage ${stageClass(0)}">
<div class="stage-header">
<div class="stage-number">1</div>
<div class="stage-title">Key Preparation</div>
</div>
<div class="stage-desc">
The secret key is padded with zeros to match the hash block size (64 bytes / 512 bits).
If the key were longer than 64 bytes, it would first be hashed with SHA-256.
</div>
<div class="data-row">
<span class="data-label key-color">Key</span>
${renderByteGrid(toHexBytes(document.getElementById('keyInput').value), 'highlight-key')}
</div>
<div class="data-row" style="margin-top:8px">
<span class="data-label key-color">Padded</span>
${renderByteGrid(s.paddedKey, 'highlight-key', 64)}
</div>
<div class="callout">
<strong>Why pad?</strong> HMAC needs the key to be exactly one block (64 bytes) for the XOR operations that follow.
</div>
</div>
`);
// Stage 1: Inner Padding (ipad)
stages.push(`
<div class="stage ${stageClass(1)}">
<div class="stage-header">
<div class="stage-number">2</div>
<div class="stage-title">Inner Padding — XOR with ipad</div>
</div>
<div class="stage-desc">
The padded key is XORed byte-by-byte with the inner padding constant <code style="font-family:var(--font-mono);color:var(--accent-intermediate)">0x36</code> repeated 64 times.
</div>
<div class="xor-visual">
<div class="xor-row">${s.paddedKey.slice(0,8).map(h=>`<span class="byte-cell highlight-key">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (64 bytes)</span></div>
<div class="xor-op">⊕</div>
<div class="xor-row">${s.ipad.slice(0,8).map(h=>`<span class="byte-cell highlight-inter">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (ipad: 0x36 × 64)</span></div>
<div class="xor-divider"></div>
<div class="xor-row">${s.innerKey.slice(0,8).map(h=>`<span class="byte-cell highlight-active">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (inner key)</span></div>
</div>
</div>
`);
// Stage 2: Inner Hash
stages.push(`
<div class="stage ${stageClass(2)}">
<div class="stage-header">
<div class="stage-number">3</div>
<div class="stage-title">Inner Hash — SHA-256(innerKey ∥ message)</div>
</div>
<div class="stage-desc">
The inner key is concatenated with the message, then the combined data is fed through SHA-256.
</div>
<div class="data-row">
<span class="data-label highlight-color">Inner K</span>
${renderByteGrid(s.innerKey, 'highlight-active', 16)}
</div>
<div style="text-align:center;color:var(--text-muted);font-size:0.8rem;padding:4px 0">∥ (concatenate)</div>
<div class="data-row">
<span class="data-label input-color">Message</span>
${renderByteGrid(s.msgBytes, 'highlight-input')}
</div>
<div style="text-align:center;padding:8px 0">
<svg width="24" height="24" viewBox="0 0 24 24"><path d="M12 2 L12 18 M6 14 L12 20 L18 14" stroke="var(--accent-highlight)" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div>
<div class="hash-box ${currentStep === 2 ? 'active-hash' : ''}">
<div class="hash-label" style="color:var(--accent-intermediate)">SHA-256</div>
<div class="hash-value" style="color:var(--accent-intermediate)">${s.innerHash}</div>
</div>
</div>
`);
// Stage 3: Outer Padding (opad)
stages.push(`
<div class="stage ${stageClass(3)}">
<div class="stage-header">
<div class="stage-number">4</div>
<div class="stage-title">Outer Padding — XOR with opad</div>
</div>
<div class="stage-desc">
The same padded key is now XORed with the outer padding constant <code style="font-family:var(--font-mono);color:var(--accent-intermediate)">0x5C</code> repeated 64 times.
</div>
<div class="xor-visual">
<div class="xor-row">${s.paddedKey.slice(0,8).map(h=>`<span class="byte-cell highlight-key">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (64 bytes)</span></div>
<div class="xor-op">⊕</div>
<div class="xor-row">${s.opad.slice(0,8).map(h=>`<span class="byte-cell highlight-inter">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (opad: 0x5C × 64)</span></div>
<div class="xor-divider"></div>
<div class="xor-row">${s.outerKey.slice(0,8).map(h=>`<span class="byte-cell highlight-active">${h}</span>`).join('')} <span style="color:var(--text-muted);font-size:0.7rem">… (outer key)</span></div>
</div>
<div class="callout">
<strong>Why two different paddings?</strong> Using distinct constants (0x36 and 0x5C) for inner and outer hashes
prevents an attacker from exploiting the hash function's structure. This double-hash construction is what gives HMAC
its security proof. <span class="badge security">Security</span>
</div>
</div>
`);
// Stage 4: Outer Hash
stages.push(`
<div class="stage ${stageClass(4)}">
<div class="stage-header">
<div class="stage-number">5</div>
<div class="stage-title">Outer Hash — SHA-256(outerKey ∥ innerHash)</div>
</div>
<div class="stage-desc">
The outer key is concatenated with the inner hash result, then hashed again with SHA-256.
This "hash of a hash" is the core of HMAC's strength.
</div>
<div class="data-row">
<span class="data-label highlight-color">Outer K</span>
${renderByteGrid(s.outerKey, 'highlight-active', 16)}
</div>
<div style="text-align:center;color:var(--text-muted);font-size:0.8rem;padding:4px 0">∥ (concatenate)</div>
<div class="data-row">
<span class="data-label inter-color">Inner H</span>
<div class="byte-grid"><span class="byte-cell highlight-inter" style="font-size:0.68rem;letter-spacing:-0.02em">${s.innerHash}</span></div>
</div>
<div style="text-align:center;padding:8px 0">
<svg width="24" height="24" viewBox="0 0 24 24"><path d="M12 2 L12 18 M6 14 L12 20 L18 14" stroke="var(--accent-output)" stroke-width="2" fill="none" stroke-linecap="round"/></svg>
</div>
<div class="hash-box ${currentStep === 4 ? 'active-hash' : ''}">
<div class="hash-label" style="color:var(--accent-output)">SHA-256</div>
<div class="hash-value" style="color:var(--accent-output)">${s.outerHash}</div>
</div>
</div>
`);
// Stage 5: Final HMAC
stages.push(`
<div class="stage ${stageClass(5)}">
<div class="stage-header">
<div class="stage-number">6</div>
<div class="stage-title">HMAC Result</div>
</div>
<div class="stage-desc">
The output of the outer hash <em>is</em> the HMAC. This 256-bit value authenticates both the message content
and proves knowledge of the secret key.
</div>
<div class="final-output ${currentStep >= 5 ? 'revealed' : ''}">
<h3>HMAC-SHA256</h3>
<div class="hash-value">${s.outerHash}</div>
</div>
<div class="callout" style="margin-top:16px">
<strong>The formula:</strong><br>
HMAC(K, m) = SHA256( (K' ⊕ opad) ∥ SHA256( (K' ⊕ ipad) ∥ m ) )
</div>
</div>
`);
// Stage 6: Summary
stages.push(`
<div class="stage ${stageClass(6)}">
<div class="stage-header">
<div class="stage-number">✓</div>
<div class="stage-title">Complete — How HMAC Protects You</div>
</div>
<div class="stage-desc" style="line-height:1.7">
HMAC provides three guarantees: <strong style="color:var(--accent-output)">integrity</strong> (any change to the message changes the HMAC),
<strong style="color:var(--accent-key)">authentication</strong> (only someone with the key can produce a valid HMAC), and
<strong style="color:var(--accent-intermediate)">resistance to length-extension attacks</strong> (the double-hash construction prevents an attacker
from appending data and computing a valid MAC — a weakness of plain SHA-256).
</div>
<div class="callout">
<strong>Try it:</strong> Change a single character in the message or key above and watch the entire HMAC change completely.
This is the <em>avalanche effect</em> — a core property of cryptographic hash functions.
</div>
</div>
`);
// Join stages with flow arrows
let html = '';
stages.forEach((stg, i) => {
html += stg;
if (i < stages.length - 1) html += flowArrow(i < currentStep);
});
document.getElementById('stages').innerHTML = html;
document.getElementById('stepIndicator').textContent = `Step ${currentStep + 1} / ${TOTAL_STEPS}`;
document.getElementById('prevBtn').disabled = currentStep <= 0;
document.getElementById('nextBtn').disabled = currentStep >= TOTAL_STEPS - 1;
document.getElementById('playBtn').innerHTML = isPlaying ? '⏸ Pause' : '▶ Play';
}
function goToStep(n) {
currentStep = Math.max(0, Math.min(TOTAL_STEPS - 1, n));
render();
}
function nextStep() { goToStep(currentStep + 1); }
function prevStep() { goToStep(currentStep - 1); }
function togglePlay() {
isPlaying = !isPlaying;
if (isPlaying) {
if (currentStep >= TOTAL_STEPS - 1) currentStep = 0;
playInterval = setInterval(() => {
if (currentStep >= TOTAL_STEPS - 1) {
isPlaying = false;
clearInterval(playInterval);
render();
return;
}
nextStep();
}, 1500 / speed);
} else {
clearInterval(playInterval);
}
render();
}
function updateSpeed() {
speed = parseFloat(document.getElementById('speedSlider').value);
document.getElementById('speedLabel').textContent = speed + '×';
if (isPlaying) {
clearInterval(playInterval);
playInterval = setInterval(() => {
if (currentStep >= TOTAL_STEPS - 1) { isPlaying = false; clearInterval(playInterval); render(); return; }
nextStep();
}, 1500 / speed);
}
}
function setPreset(msg, key) {
document.getElementById('msgInput').value = msg;
document.getElementById('keyInput').value = key;
goToStep(0);
}
// Re-render on input change
document.getElementById('msgInput').addEventListener('input', render);
document.getElementById('keyInput').addEventListener('input', render);
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName === 'INPUT') return;
if (e.key === 'ArrowRight') nextStep();
else if (e.key === 'ArrowLeft') prevStep();
else if (e.key === ' ') { e.preventDefault(); togglePlay(); }
});
// Initial render
render();
</script>
</body>
</html>
{
"skill_name": "tech-visualizer",
"evals": [
{
"id": 1,
"prompt": "Show me how HMAC-SHA256 works with an interactive visualization",
"expected_output": "Interactive React or HTML artifact showing the HMAC double-hashing process with key padding, inner hash, and outer hash stages",
"files": [],
"assertions": [
"Output is a .jsx or .html file",
"Has step-by-step controls (forward, back, play/pause)",
"Has a live input field for the message or key",
"Uses dark theme with consistent color coding",
"Shows the inner hash and outer hash as distinct visual stages",
"Includes a color legend",
"Has hover inspection on intermediate values",
"Includes plain-English step descriptions"
]
},
{
"id": 2,
"prompt": "Visualize AES-CBC encryption vs ECB mode side by side",
"expected_output": "Side-by-side comparison visualization showing how identical plaintext blocks produce identical ciphertext in ECB but different ciphertext in CBC",
"files": [],
"assertions": [
"Shows ECB and CBC modes simultaneously",
"Has synchronized step controls",
"Demonstrates the chaining mechanism in CBC (XOR with previous ciphertext)",
"Highlights the security weakness of ECB with identical blocks",
"Has a shared input field that affects both sides",
"Uses dark theme with the specified color system",
"Includes the 'aha moment' about why CBC is more secure"
]
},
{
"id": 3,
"prompt": "Create an interactive TCP three-way handshake visualization",
"expected_output": "Two-party layout showing client and server exchanging SYN, SYN-ACK, ACK packets with state machine transitions",
"files": [],
"assertions": [
"Uses two-party layout with client and server",
"Shows state transitions for both parties",
"Animates packets flowing between parties",
"Has step-by-step controls",
"Includes sequence number tracking",
"Has hover inspection on packet contents",
"Uses dark theme"
]
}
]
}
Visualization Patterns Reference
Detailed implementation blueprints organized by concept category. Read the relevant section before building.
Table of Contents
1. Core Layout Templates 2. Interaction Components 3. Data Representation 4. Animation Patterns 5. Category: Cryptography 6. Category: Computer Vision 7. Category: Networking 8. Category: Data Structures 9. Category: ML/AI 10. Color System 11. Typography
---
Core Layout Templates
Linear Pipeline Layout
For algorithms with sequential stages (hash functions, encoding, encryption):
┌──────────────────────────────────────────┐
│ [Title] [◀ ▶] Step 3/7 [⚡▶] │
│ [Subtitle/desc] Speed: [━━○──] │
├──────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ INPUT │ ← editable │
│ │ "Hello..." │ │
│ └──────┬───────┘ │
│ │ ← animated flow │
│ ▼ │
│ ┌──────────────────┐ │
│ │ STAGE: Padding │ ← highlighted │
│ │ [visual repr] │ when active │
│ │ 48 65 6C 6C ... │ │
│ └──────┬───────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ OUTPUT │ │
│ │ a1 b2 c3 d4 ...│ │
│ └──────────────────┘ │
│ │
│ ┌──────────────────────────────────┐ │
│ │ 📝 Padding adds bytes so the │ │
│ │ message length is a multiple of │ │
│ │ the block size (512 bits). │ │
│ └──────────────────────────────────┘ │
│ │
│ [Legend: 🟦 Input 🟨 Key 🟩 Output] │
└──────────────────────────────────────────┘Two-Party Layout
For protocols with two participants (TLS, Diffie-Hellman, TCP):
┌──────────────────────────────────────────┐
│ [Title] [Controls] │
├──────────────────────────────────────────┤
│ │
│ ALICE (Client) │ BOB (Server) │
│ ┌──────────┐ │ ┌──────────┐ │
│ │ State: │ │ │ State: │ │
│ │ CONNECTED │ │ │ LISTENING │ │
│ └──────────┘ │ └──────────┘ │
│ │ │ ▲ │
│ └───── SYN ─────────────┘ │
│ ↕ │
│ ┌── SYN-ACK ──────────┐ │
│ ▼ │ │ │
│ ┌──────────┐ │ ┌──────────┐ │
│ │ SYN_SENT │ │ │ SYN_RCVD │ │
│ └──────────┘ │ └──────────┘ │
│ │ │
│ 📝 Client sends SYN with initial │
│ sequence number to begin handshake │
└──────────────────────────────────────────┘Side-by-Side Comparison Layout
For comparing variants (ECB vs CBC, HTTP/1.1 vs HTTP/2):
┌──────────────────────────────────────────┐
│ [Shared Input Field] [Sync Controls]│
├───────────────────┬──────────────────────┤
│ MODE A (ECB) │ MODE B (CBC) │
│ │ │
│ [visualization] │ [visualization] │
│ │ │
│ Block 1: ae 3f │ Block 1: ae 3f │
│ Block 2: ae 3f ⚠│ Block 2: 7c 19 ✓ │
│ Block 3: 12 b7 │ Block 3: 55 a2 │
│ │ │
│ ⚠ Identical │ ✓ All blocks │
│ blocks = same │ unique even with │
│ ciphertext! │ same plaintext │
└───────────────────┴──────────────────────┘Grid/Matrix Layout
For operations on 2D data (AES state matrix, convolution, attention):
┌──────────────────────────────────────────┐
│ 4x4 State Matrix Operation │
│ ┌────┬────┬────┬────┐ │
│ │ 63 │ 53 │ e0 │ 8c │ SubBytes: │
│ ├────┼────┼────┼────┤ Replace each │
│ │ 09 │ 60 │ e1 │ 04 │ byte using the │
│ ├────┼────┼────┼────┤ S-box lookup │
│ │ cd │ 70 │ b7 │ 51 │ table │
│ ├────┼────┼────┼────┤ │
│ │ 7a │ cd │ 1c │ 73 │ Current cell: │
│ └────┴────┴────┴────┘ 63 → [S-box] → │
│ fb │
│ [Highlight: active cell pulses amber] │
└──────────────────────────────────────────┘---
Interaction Components
Step Controller (React Pattern)
const StepController = ({ current, total, onStep, onPlay, isPlaying, speed, onSpeed }) => (
<div className="flex items-center gap-3 px-4 py-2 rounded-lg"
style={{ background: 'var(--bg-surface)' }}>
<button onClick={() => onStep(0)} aria-label="Reset">⏮</button>
<button onClick={() => onStep(current - 1)} disabled={current <= 0} aria-label="Previous">◀</button>
<button onClick={onPlay} aria-label={isPlaying ? 'Pause' : 'Play'}>
{isPlaying ? '⏸' : '▶'}
</button>
<button onClick={() => onStep(current + 1)} disabled={current >= total - 1} aria-label="Next">▶</button>
<span style={{ fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)' }}>
Step {current + 1} / {total}
</span>
<input type="range" min={0.25} max={3} step={0.25} value={speed}
onChange={e => onSpeed(+e.target.value)} aria-label="Speed" />
<span style={{ fontSize: '0.75rem', color: 'var(--text-muted)' }}>{speed}×</span>
</div>
);Live Input (React Pattern)
const LiveInput = ({ label, value, onChange, placeholder, mono = true }) => (
<div className="flex flex-col gap-1">
<label style={{ fontSize: '0.75rem', color: 'var(--accent-input)', fontWeight: 600,
textTransform: 'uppercase', letterSpacing: '0.05em' }}>
{label}
</label>
<input
value={value}
onChange={e => onChange(e.target.value)}
placeholder={placeholder}
style={{
background: 'var(--bg-deep)',
border: '1px solid var(--border)',
borderRadius: '6px',
padding: '8px 12px',
color: 'var(--text-primary)',
fontFamily: mono ? 'var(--font-mono)' : 'var(--font-sans)',
fontSize: '0.875rem',
outline: 'none',
transition: 'border-color 0.15s',
}}
onFocus={e => e.target.style.borderColor = 'var(--accent-input)'}
onBlur={e => e.target.style.borderColor = 'var(--border)'}
/>
</div>
);Hover Inspector (React Pattern)
const InspectableValue = ({ display, detail, color }) => {
const [hovered, setHovered] = useState(false);
return (
<span
onMouseEnter={() => setHovered(true)}
onMouseLeave={() => setHovered(false)}
style={{
position: 'relative',
cursor: 'pointer',
fontFamily: 'var(--font-mono)',
color: color || 'var(--text-primary)',
borderBottom: '1px dashed currentColor',
transition: 'opacity 0.15s',
}}
>
{display}
{hovered && (
<div style={{
position: 'absolute', bottom: '100%', left: '50%', transform: 'translateX(-50%)',
background: 'var(--bg-elevated)', border: '1px solid var(--border)',
borderRadius: '8px', padding: '8px 12px', whiteSpace: 'nowrap',
fontSize: '0.75rem', boxShadow: '0 4px 12px rgba(0,0,0,0.3)',
zIndex: 10, marginBottom: '4px',
}}>
{detail}
</div>
)}
</span>
);
};---
Data Representation
Byte Grid
Display bytes in a grid with grouped columns and alternating backgrounds:
const ByteGrid = ({ bytes, highlightIndex, groupSize = 4 }) => (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '2px', fontFamily: 'var(--font-mono)' }}>
{bytes.map((b, i) => (
<span key={i} style={{
padding: '2px 5px',
borderRadius: '3px',
fontSize: '0.8rem',
background: i === highlightIndex ? 'var(--accent-key)'
: Math.floor(i / groupSize) % 2 === 0 ? 'var(--bg-surface)' : 'var(--bg-deep)',
color: i === highlightIndex ? '#000' : 'var(--text-primary)',
transition: 'all 0.3s ease',
opacity: highlightIndex !== undefined && i > highlightIndex ? 0.3 : 1,
}}>
{b.toString(16).padStart(2, '0').toUpperCase()}
</span>
))}
</div>
);Bit Visualization
For XOR, bit shifts, and binary operations:
const BitRow = ({ bits, label, color }) => (
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
<span style={{ width: '60px', fontSize: '0.7rem', color: 'var(--text-muted)',
textAlign: 'right' }}>{label}</span>
<div style={{ display: 'flex', gap: '1px' }}>
{bits.map((bit, i) => (
<div key={i} style={{
width: '14px', height: '14px', borderRadius: '2px',
background: bit ? color || 'var(--accent-output)' : 'var(--bg-deep)',
opacity: bit ? 1 : 0.3,
transition: 'all 0.2s ease',
}} />
))}
</div>
</div>
);Animated Data Flow Arrow
SVG path that animates to show data flowing between stages:
<svg width="40" height="60">
<defs>
<linearGradient id="flowGrad" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="var(--accent-input)" />
<stop offset="100%" stop-color="var(--accent-intermediate)" />
</linearGradient>
</defs>
<path d="M20 0 L20 45 M12 37 L20 48 L28 37"
stroke="url(#flowGrad)" stroke-width="2" fill="none"
stroke-dasharray="60" stroke-dashoffset="60">
<animate attributeName="stroke-dashoffset" from="60" to="0"
dur="0.6s" fill="freeze" begin="0.2s" />
</path>
</svg>---
Animation Patterns
Staggered Reveal
Reveal a list of items one by one:
// CSS approach (HTML artifacts)
.byte-cell {
opacity: 0;
transform: translateY(8px);
animation: fadeInUp 0.3s ease forwards;
}
.byte-cell:nth-child(1) { animation-delay: 0ms; }
.byte-cell:nth-child(2) { animation-delay: 40ms; }
/* ... or use style={{animationDelay: `${i * 40}ms`}} in React */
@keyframes fadeInUp {
to { opacity: 1; transform: translateY(0); }
}Active Stage Glow
.stage-active {
box-shadow: 0 0 0 1px var(--accent-input),
0 0 20px rgba(96, 165, 250, 0.15),
inset 0 0 20px rgba(96, 165, 250, 0.05);
transition: box-shadow 0.3s ease;
}
.stage-completed {
opacity: 0.6;
filter: saturate(0.7);
transition: all 0.3s ease;
}
.stage-upcoming {
opacity: 0.4;
transition: all 0.3s ease;
}Pulse on Value Change
@keyframes pulse-highlight {
0% { background: var(--accent-key); transform: scale(1); }
50% { background: var(--accent-key); transform: scale(1.05); }
100% { background: transparent; transform: scale(1); }
}
.value-changed {
animation: pulse-highlight 0.5s ease;
}---
Category: Cryptography
Block Cipher (AES-CBC, AES-ECB, DES)
Aha moment: The chaining mechanism — how each block depends on the previous one (CBC) or doesn't (ECB).
Structure: Show multiple blocks in a horizontal/vertical chain. Each block is a card containing a 4×4 byte grid (for AES) with SubBytes, ShiftRows, MixColumns, AddRoundKey as sub-steps within each round.
Key interactions:
- Toggle between ECB/CBC to see the difference instantly
- Click any ciphertext block to highlight which plaintext blocks affect it
- Modify one plaintext byte and watch the cascade
Implementation tips:
- Implement simplified versions of actual operations (e.g., a real S-box lookup for SubBytes)
- Use arrow animations between blocks to show the XOR/chaining
- IV should be visually prominent in CBC mode
Hash Functions (SHA-256, MD5)
Aha moment: The compression function — seeing how a fixed-size state gets repeatedly modified by each message block.
Structure: Show message → padding → block splitting → compression rounds → digest.
Key interactions:
- Edit the input message and watch the entire hash change (avalanche effect)
- Step through compression rounds
- Hover over any intermediate hash state to see binary/hex
HMAC
Aha moment: The double hashing — inner hash with key⊕ipad, outer hash with key⊕opad.
Structure: Two vertical lanes — inner hash lane and outer hash lane — connected at the point where inner hash output feeds into outer hash input.
Key Exchange (Diffie-Hellman, ECDH)
Aha moment: Both parties arrive at the same shared secret without ever transmitting it.
Structure: Two-party layout with Alice and Bob. Show private values (hidden), public values (transmitted), and the computation arriving at the same shared secret.
---
Category: Computer Vision
Feature Detection (ORB, SIFT, Harris Corners)
Aha moment: Watching the detector sweep across an image and "light up" features.
Structure:
- Use a procedurally generated image (e.g., canvas with shapes, gradients, edges)
- Overlay a scanning window that moves across the image
- Detected features appear as colored circles with strength indicators
Implementation tips:
- Generate a simple 200×200 canvas image with rectangles, circles, and gradients
- Simulate detection by pre-computing feature locations for the sample image
- Show the descriptor extraction as a zoomed-in patch view with binary comparisons (BRIEF)
Convolution / Filtering
Aha moment: Seeing the kernel slide over the image and produce the output pixel-by-pixel.
Structure: Three side-by-side views: Input Image | Kernel (editable) | Output Image. The kernel position on the input highlights the receptive field, and the corresponding output pixel lights up.
Implementation tips:
- Use a small grid (8×8 to 16×16) for clarity
- Let users edit kernel values (preset options: blur, sharpen, edge detect, emboss)
- Animate the kernel sliding with the output building up
---
Category: Networking
TCP Handshake / Connection Lifecycle
Aha moment: The state machine transitions — seeing both sides change state as packets flow between them.
Structure: Two-party layout with state machines shown as node graphs for each party. The current state glows. Packets animate between the parties.
DNS Resolution
Aha moment: The recursive chain of queries — watching the resolver bounce between root, TLD, and authoritative servers.
Structure: Multiple server nodes (Resolver → Root → TLD → Authoritative) with animated query/response pairs bouncing between them.
TLS Handshake
Aha moment: The moment the session key is derived and both parties switch to encrypted communication (the visual transition from cleartext to encrypted).
Structure: Two-party layout with a clear visual shift (e.g., background color change) when encryption begins. Show certificate verification as a separate inspection panel.
---
Category: Data Structures
Tree Operations (BST, AVL, B-Tree, Red-Black)
Aha moment: Watching a rebalancing operation (rotation, split) in slow motion.
Structure: SVG-based tree rendering with animated node positions.
Implementation tips:
- Use absolute positioning or SVG for node placement
- Animate node positions with CSS transitions when the tree restructures
- Show comparison path highlighted during search/insert
- Display balance factors or colors on each node
Hash Table
Aha moment: A collision occurring and seeing the resolution strategy (chaining vs open addressing).
Structure: Array of buckets rendered as vertical slots. Items animate into their hash position. On collision, show the resolution strategy visually.
---
Category: ML/AI
Attention Mechanism
Aha moment: The attention heatmap showing which tokens attend to which.
Structure: Input tokens on both axes of a matrix, with cell intensity showing attention weights. Hovering a token highlights its row (what it attends to) and column (what attends to it).
Backpropagation
Aha moment: Watching gradients flow backward through the network, with edge thickness proportional to gradient magnitude.
Structure: Node-and-edge neural network diagram. Forward pass animates left-to-right with activations. Backward pass animates right-to-left with gradients.
Gradient Descent
Aha moment: The optimizer taking steps on a loss landscape, showing how learning rate affects convergence.
Structure: 2D contour plot of a loss surface (or simple 3D perspective). A dot representing the current parameters moves along the surface. Controls for learning rate and optimizer type.
---
Color System
Use CSS custom properties for a consistent dark palette:
:root {
/* Backgrounds */
--bg-primary: #0f1117;
--bg-surface: #1a1d27;
--bg-deep: #12141c;
--bg-elevated: #252836;
/* Text */
--text-primary: #e4e7ec;
--text-secondary: #9ba3b5;
--text-muted: #5d6678;
/* Semantic accent colors */
--accent-input: #60a5fa; /* Blue — input data, editable fields */
--accent-key: #f59e0b; /* Amber — keys, secrets, critical params */
--accent-output: #34d399; /* Emerald — final output, success */
--accent-intermediate: #22d3ee; /* Cyan — intermediate values */
--accent-error: #f87171; /* Red — errors, warnings, collisions */
--accent-highlight: #a78bfa; /* Violet — current focus, active element */
/* Borders */
--border: #2a2d3a;
--border-active: #3d4155;
}Color usage rules:
- Input/editable data is always
--accent-input(blue) - Keys and secrets are always
--accent-key(amber) - Final output is always
--accent-output(emerald) - The currently active/processing element uses
--accent-highlight(violet) with glow - Errors and warnings use
--accent-error(red)
---
Typography
Load from Google Fonts:
@import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=DM+Sans:wght@400;500;600;700&display=swap');
:root {
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--font-sans: 'DM Sans', 'Segoe UI', system-ui, sans-serif;
}Usage:
- Headings and labels: DM Sans, 600-700 weight
- Body text/descriptions: DM Sans, 400 weight
- Data values (hex, bytes, addresses): JetBrains Mono, 400 weight
- Active/important data: JetBrains Mono, 600 weight
- Step indicators: DM Sans, 500 weight, uppercase, letter-spacing 0.05em
Sizing:
- Title: 1.5rem
- Step label: 0.875rem
- Data values: 0.8rem
- Tooltips/secondary info: 0.75rem