Created
September 21, 2025 20:16
-
-
Save lucashmorais/6648ab1ada34714b96453ecebdc9cc92 to your computer and use it in GitHub Desktop.
Animation of 3x3 multiplication using a systolic array
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta name="viewport" content="width=device-width, initial-scale=1" /> | |
| <title>Systolic Array: 3×3 Matrix Multiply Animation</title> | |
| <style> | |
| :root { | |
| --cell: 88px; | |
| --gap: 10px; | |
| --bg: #0f1320; | |
| --panel: #141a2a; | |
| --pe: #1b2440; | |
| --pe-border: #2b355a; | |
| --text: #e8ecff; | |
| --muted: #aab3d6; | |
| --a: #4f87ff; | |
| --b: #2ecc71; | |
| --pulse: #ffd166; | |
| --accent: #9b87ff; | |
| } | |
| * { box-sizing: border-box; } | |
| html, body { height: 100%; } | |
| body { | |
| margin: 0; | |
| font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, "Helvetica Neue", Arial, "Noto Sans", "Liberation Sans", sans-serif; | |
| color: var(--text); | |
| background: radial-gradient(1200px 700px at 30% 10%, #18203a 0%, var(--bg) 60%); | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .app { | |
| width: min(980px, 96vw); | |
| display: grid; | |
| grid-template-columns: 200px 1fr 200px; | |
| grid-template-rows: auto auto auto; | |
| grid-column-gap: 14px; | |
| grid-row-gap: 12px; | |
| align-items: start; | |
| } | |
| .panel { | |
| background: var(--panel); | |
| border: 1px solid #202846; | |
| border-radius: 10px; | |
| padding: 10px 12px; | |
| box-shadow: 0 10px 25px rgba(0,0,0,0.25); | |
| } | |
| .title { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: baseline; | |
| justify-content: space-between; | |
| } | |
| .title h1 { | |
| font-size: 18px; | |
| margin: 0; | |
| letter-spacing: 0.2px; | |
| } | |
| .subtitle { color: var(--muted); font-size: 12px; } | |
| /* Matrices A and B */ | |
| .matrix { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 6px; | |
| margin-top: 8px; | |
| } | |
| .matrix .cell { | |
| background: #0e1530; | |
| border: 1px solid #202846; | |
| border-radius: 6px; | |
| padding: 6px; | |
| text-align: center; | |
| color: var(--text); | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .matrix label { | |
| display: block; | |
| color: var(--muted); | |
| font-size: 12px; | |
| margin-bottom: 6px; | |
| letter-spacing: 0.2px; | |
| } | |
| /* Grid (PE array) */ | |
| .grid-wrap { | |
| grid-column: 2 / 3; | |
| background: linear-gradient(180deg, #121a33 0%, #101628 100%); | |
| border: 1px solid #202846; | |
| border-radius: 12px; | |
| padding: 14px; | |
| position: relative; | |
| box-shadow: inset 0 0 0 1px rgba(255,255,255,0.02); | |
| } | |
| .legend { | |
| display: flex; | |
| gap: 16px; | |
| align-items: center; | |
| color: var(--muted); | |
| font-size: 12px; | |
| margin-bottom: 10px; | |
| flex-wrap: wrap; | |
| } | |
| .legend .dot { width: 10px; height: 10px; border-radius: 50%; display: inline-block; margin-right: 6px; } | |
| .legend .a { background: var(--a); } | |
| .legend .b { background: var(--b); } | |
| .legend .pulse { background: var(--pulse); } | |
| .grid { | |
| position: relative; /* anchor for tokens overlay */ | |
| display: grid; | |
| grid-template-columns: repeat(3, var(--cell)); | |
| grid-template-rows: repeat(3, var(--cell)); | |
| gap: var(--gap); | |
| } | |
| .pe { | |
| position: relative; | |
| background: var(--pe); | |
| border: 1px solid var(--pe-border); | |
| border-radius: 10px; | |
| box-shadow: | |
| inset 0 -14px 18px rgba(0,0,0,0.3), | |
| inset 0 2px 2px rgba(255,255,255,0.06); | |
| overflow: hidden; | |
| } | |
| .pe .ij { | |
| position: absolute; top: 6px; left: 8px; | |
| font-size: 11px; color: var(--muted); | |
| } | |
| .pe .acc { | |
| position: absolute; inset: 0; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 20px; font-weight: 600; color: #f0f3ff; | |
| text-shadow: 0 1px 0 rgba(0,0,0,0.4); | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .pe .op { | |
| position: absolute; bottom: 6px; left: 6px; right: 6px; | |
| color: #e6e8ff; | |
| background: rgba(255, 209, 102, 0.06); | |
| border: 1px solid rgba(255, 209, 102, 0.3); | |
| border-radius: 6px; | |
| padding: 3px 6px; | |
| font-size: 12px; | |
| text-align: center; | |
| opacity: 0; | |
| transform: translateY(6px); | |
| transition: opacity 0.25s ease, transform 0.25s ease; | |
| font-variant-numeric: tabular-nums; | |
| } | |
| .pe.fire .op { opacity: 1; transform: translateY(0); } | |
| .pe.fire { animation: pulse 420ms ease; } | |
| @keyframes pulse { | |
| 0% { box-shadow: 0 0 0 0 rgba(255,209,102,0.45); } | |
| 100% { box-shadow: 0 0 0 14px rgba(255,209,102,0); } | |
| } | |
| /* Tokens overlay (now inside .grid) */ | |
| .tokens { | |
| pointer-events: none; | |
| position: absolute; | |
| inset: 0; /* perfectly align to the grid box */ | |
| z-index: 5; /* stay above the PEs and their content */ | |
| } | |
| .token { | |
| position: absolute; | |
| width: 26px; height: 26px; | |
| border-radius: 14px; | |
| color: white; | |
| display: flex; align-items: center; justify-content: center; | |
| font-size: 12px; font-weight: 600; | |
| text-shadow: 0 1px 0 rgba(0,0,0,0.4); | |
| box-shadow: 0 6px 12px rgba(0,0,0,0.35), inset 0 1px 0 rgba(255,255,255,0.12); | |
| transform: translate(-9999px, -9999px); | |
| transition: transform 0.72s ease, opacity 0.25s ease; | |
| opacity: 0; | |
| will-change: transform, opacity; | |
| } | |
| /* A tokens: row-encoded blue shades */ | |
| .token.a.r0 { background: linear-gradient(160deg, #74b0ff, #3f79ff); border: 1px solid #2e5ad9; } | |
| .token.a.r1 { background: linear-gradient(160deg, #5f97ff, #3266ff); border: 1px solid #284fd4; } | |
| .token.a.r2 { background: linear-gradient(160deg, #4a86ff, #2b5df3); border: 1px solid #254bcc; } | |
| /* B tokens: column-encoded green shades */ | |
| .token.b.c0 { background: linear-gradient(160deg, #5ee696, #22bd70); border: 1px solid #179a5a; } | |
| .token.b.c1 { background: linear-gradient(160deg, #46d681, #20b061); border: 1px solid #199255; } | |
| .token.b.c2 { background: linear-gradient(160deg, #34c874, #1aa357); border: 1px solid #148b49; } | |
| /* Controls */ | |
| .controls { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: center; | |
| gap: 10px; | |
| background: var(--panel); | |
| border: 1px solid #202846; | |
| border-radius: 10px; | |
| padding: 10px 12px; | |
| flex-wrap: wrap; | |
| } | |
| button, .slider { | |
| background: #182040; | |
| color: var(--text); | |
| border: 1px solid #283260; | |
| border-radius: 8px; | |
| padding: 8px 10px; | |
| cursor: pointer; | |
| transition: background 0.15s ease, transform 0.05s ease; | |
| } | |
| button:hover { background: #1b244a; } | |
| button:active { transform: translateY(1px); } | |
| .spacer { flex: 1; min-width: 12px; } | |
| .slider { | |
| padding: 6px 10px; | |
| display: inline-flex; | |
| gap: 8px; | |
| align-items: center; | |
| font-size: 12px; | |
| color: var(--muted); | |
| } | |
| input[type="range"] { | |
| width: 200px; | |
| accent-color: var(--accent); | |
| } | |
| .time { | |
| font-variant-numeric: tabular-nums; | |
| color: var(--muted); | |
| background: #141b36; | |
| border: 1px solid #25305c; | |
| border-radius: 8px; | |
| padding: 6px 8px; | |
| } | |
| .finalC { margin-top: 8px; } | |
| .note { color: var(--muted); font-size: 12px; margin-top: 6px; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="app"> | |
| <div class="panel title"> | |
| <h1>Systolic Array (3×3) — Matrix Multiply</h1> | |
| <div class="subtitle">Streams: A moves right, B moves down; each PE accumulates C = A×B</div> | |
| </div> | |
| <div class="panel" id="panelA"> | |
| <label>Matrix A (rows stream right)</label> | |
| <div class="matrix" id="matrixA"></div> | |
| <div class="note">Injected at t = k + i into row i, then shifts right each tick.</div> | |
| </div> | |
| <div class="grid-wrap panel"> | |
| <div class="legend"> | |
| <span><span class="dot a"></span>A token (blue, shade by row)</span> | |
| <span><span class="dot b"></span>B token (green, shade by col)</span> | |
| <span><span class="dot pulse"></span>MAC this tick</span> | |
| <span class="spacer"></span> | |
| <span class="time" id="timeLabel">t = 0</span> | |
| </div> | |
| <div class="grid" id="grid"> | |
| <div class="tokens" id="tokens"></div> | |
| </div> | |
| </div> | |
| <div class="panel" id="panelB"> | |
| <label>Matrix B (columns stream down)</label> | |
| <div class="matrix" id="matrixB"></div> | |
| <div class="note">Injected at t = k + j into column j, then shifts down each tick.</div> | |
| </div> | |
| <div class="panel finalC" id="panelC" style="grid-column: 1 / -1;"> | |
| <label>Result C = A×B (appears when each PE finishes)</label> | |
| <div class="matrix" id="matrixC"></div> | |
| </div> | |
| <div class="controls"> | |
| <button id="playBtn">▶ Play</button> | |
| <button id="pauseBtn">⏸ Pause</button> | |
| <button id="stepBtn">⏭ Step</button> | |
| <button id="resetBtn">⟲ Reset</button> | |
| <div class="slider"> | |
| Speed <input type="range" id="speed" min="0.05" max="3" step="0.05" value="1"> | |
| <span id="speedVal">1.00×</span> | |
| </div> | |
| <div class="spacer"></div> | |
| <div class="subtitle">Formula: C_ij = Σ A_ik · B_kj</div> | |
| </div> | |
| </div> | |
| <script> | |
| (function() { | |
| 'use strict'; | |
| // Problem size and data | |
| const N = 3; | |
| // Offsets to prevent overlap (define BEFORE first render/use) | |
| const OFFSETS = { A: { dx: -12, dy: 0 }, B: { dx: +12, dy: 0 } }; | |
| const A = [ | |
| [1, 2, 3], | |
| [4, 5, 6], | |
| [7, 8, 9], | |
| ]; | |
| const B = [ | |
| [9, 8, 7], | |
| [6, 5, 4], | |
| [3, 2, 1], | |
| ]; | |
| // Derived result for reference panel | |
| const Cfinal = mul3x3(A, B); | |
| // DOM references | |
| const grid = document.getElementById('grid'); | |
| const tokensLayer = document.getElementById('tokens'); | |
| const matrixAEl = document.getElementById('matrixA'); | |
| const matrixBEl = document.getElementById('matrixB'); | |
| const matrixCEl = document.getElementById('matrixC'); | |
| const timeLabel = document.getElementById('timeLabel'); | |
| const playBtn = document.getElementById('playBtn'); | |
| const pauseBtn = document.getElementById('pauseBtn'); | |
| const stepBtn = document.getElementById('stepBtn'); | |
| const resetBtn = document.getElementById('resetBtn'); | |
| const speed = document.getElementById('speed'); | |
| const speedVal = document.getElementById('speedVal'); | |
| // Build matrix panels | |
| renderMatrix(matrixAEl, A); | |
| renderMatrix(matrixBEl, B); | |
| renderMatrix(matrixCEl, [[ '', '', '' ],[ '', '', '' ],[ '', '', '' ]]); | |
| // Build grid cells | |
| const cells = []; | |
| for (let i = 0; i < N; i++) { | |
| cells[i] = []; | |
| for (let j = 0; j < N; j++) { | |
| const pe = document.createElement('div'); | |
| pe.className = 'pe'; | |
| pe.dataset.i = i; | |
| pe.dataset.j = j; | |
| pe.innerHTML = ` | |
| <div class="ij">[${i},${j}]</div> | |
| <div class="acc">0</div> | |
| <div class="op"></div> | |
| `; | |
| grid.appendChild(pe); | |
| cells[i][j] = { | |
| el: pe, | |
| accEl: pe.querySelector('.acc'), | |
| opEl: pe.querySelector('.op'), | |
| }; | |
| } | |
| } | |
| // Precompute cell centers (updated on resize as needed) | |
| let centers = computeCenters(); | |
| // Build moving tokens for A (rows) and B (columns) | |
| const tokensA = []; // each: {i,k,el} | |
| const tokensB = []; // each: {j,k,el} | |
| for (let i = 0; i < N; i++) { | |
| for (let k = 0; k < N; k++) { | |
| const t = document.createElement('div'); | |
| t.className = `token a r${i}`; | |
| t.textContent = A[i][k]; | |
| t.title = `A[${i},${k}]`; | |
| tokensLayer.appendChild(t); | |
| tokensA.push({ i, k, el: t }); | |
| } | |
| } | |
| for (let j = 0; j < N; j++) { | |
| for (let k = 0; k < N; k++) { | |
| const t = document.createElement('div'); | |
| t.className = `token b c${j}`; | |
| t.textContent = B[k][j]; | |
| t.title = `B[${k},${j}]`; | |
| tokensLayer.appendChild(t); | |
| tokensB.push({ j, k, el: t }); | |
| } | |
| } | |
| // Simulation time window | |
| const tLastMac = 3*N - 3; // 6 | |
| const tMax = tLastMac + 1; // 7 | |
| let t = 0; | |
| // Animation control | |
| let timer = null; | |
| let stepMs = 800; // base | |
| applySpeed(parseFloat(speed.value)); | |
| playBtn.addEventListener('click', play); | |
| pauseBtn.addEventListener('click', pause); | |
| stepBtn.addEventListener('click', stepOnce); | |
| resetBtn.addEventListener('click', reset); | |
| speed.addEventListener('input', e => { | |
| const v = parseFloat(e.target.value); | |
| applySpeed(v); | |
| }); | |
| window.addEventListener('resize', () => { | |
| centers = computeCenters(); | |
| render(); // keep tokens aligned after resize | |
| }); | |
| // Initial render | |
| render(); | |
| // Functions | |
| function renderMatrix(root, M) { | |
| root.innerHTML = ''; | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| const c = document.createElement('div'); | |
| c.className = 'cell'; | |
| c.textContent = M[i][j]; | |
| root.appendChild(c); | |
| } | |
| } | |
| } | |
| function mul3x3(A, B) { | |
| const C = [[0,0,0],[0,0,0],[0,0,0]]; | |
| for (let i = 0; i < 3; i++) { | |
| for (let j = 0; j < 3; j++) { | |
| let s = 0; | |
| for (let k = 0; k < 3; k++) s += A[i][k] * B[k][j]; | |
| C[i][j] = s; | |
| } | |
| } | |
| return C; | |
| } | |
| function computeCenters() { | |
| const centers = Array.from({length: N}, () => Array(N).fill(null)); | |
| const host = grid.getBoundingClientRect(); | |
| for (let i = 0; i < N; i++) { | |
| for (let j = 0; j < N; j++) { | |
| const r = cells[i][j].el.getBoundingClientRect(); | |
| const x = (r.left - host.left) + r.width / 2 - 13; // token radius ~13 | |
| const y = (r.top - host.top) + r.height / 2 - 13; | |
| centers[i][j] = { x, y }; | |
| } | |
| } | |
| return centers; | |
| } | |
| function currentAcc(i, j, t) { | |
| const maxK = Math.min(N-1, t - i - j); | |
| if (maxK < 0) return 0; | |
| let s = 0; | |
| for (let k = 0; k <= maxK; k++) s += A[i][k] * B[k][j]; | |
| return s; | |
| } | |
| function termAtTick(i, j, t) { | |
| const k = t - i - j; | |
| if (k >= 0 && k < N) return { k, a: A[i][k], b: B[k][j], prod: A[i][k] * B[k][j] }; | |
| return null; | |
| } | |
| function updateGrid() { | |
| timeLabel.textContent = 't = ' + t; | |
| for (let i = 0; i < N; i++) { | |
| for (let j = 0; j < N; j++) { | |
| const cell = cells[i][j]; | |
| const accVal = currentAcc(i, j, t); | |
| cell.accEl.textContent = accVal; | |
| const op = termAtTick(i, j, t); | |
| if (op) { | |
| cell.opEl.textContent = `${op.a} × ${op.b} → +${op.prod}`; | |
| cell.el.classList.add('fire'); | |
| setTimeout(() => cell.el.classList.remove('fire'), Math.max(300, stepMs * 0.5)); | |
| } else { | |
| cell.opEl.textContent = ''; | |
| } | |
| if (t >= i + j + (N-1)) { | |
| const idx = i*3 + j; | |
| matrixCEl.children[idx].textContent = Cfinal[i][j]; | |
| } | |
| } | |
| } | |
| } | |
| function updateTokens() { | |
| // A tokens: each A[i][k] at column j = t - (k + i) | |
| for (const tok of tokensA) { | |
| const j = t - (tok.k + tok.i); | |
| placeToken(tok.el, tok.i, j, 'A'); | |
| } | |
| // B tokens: each B[k][j] at row i = t - (k + j) | |
| for (const tok of tokensB) { | |
| const i = t - (tok.k + tok.j); | |
| placeToken(tok.el, i, tok.j, 'B'); | |
| } | |
| } | |
| function placeToken(el, i, j, kind) { | |
| if (i >= 0 && i < N && j >= 0 && j < N) { | |
| const c = centers[i][j]; | |
| const o = OFFSETS[kind] || { dx: 0, dy: 0 }; | |
| el.style.opacity = '1'; | |
| el.style.transform = `translate(${c.x + o.dx}px, ${c.y + o.dy}px)`; | |
| } else { | |
| el.style.opacity = '0'; | |
| } | |
| } | |
| function render() { | |
| updateGrid(); | |
| updateTokens(); | |
| } | |
| function play() { | |
| if (timer) return; | |
| timer = setInterval(() => { | |
| if (t >= tMax) { pause(); return; } | |
| t++; | |
| render(); | |
| }, stepMs); | |
| } | |
| function pause() { | |
| if (timer) { clearInterval(timer); timer = null; } | |
| } | |
| function stepOnce() { | |
| pause(); | |
| if (t < tMax) { t++; render(); } | |
| } | |
| function reset() { | |
| pause(); | |
| t = 0; | |
| renderMatrix(matrixCEl, [[ '', '', '' ],[ '', '', '' ],[ '', '', '' ]]); | |
| render(); | |
| } | |
| function applySpeed(mult) { | |
| speedVal.textContent = mult.toFixed(2) + '×'; | |
| stepMs = Math.round(800 / mult); | |
| const tokenAnimMs = Math.max(0.8 * stepMs, 120); | |
| for (const el of tokensLayer.querySelectorAll('.token')) { | |
| el.style.transitionDuration = tokenAnimMs + 'ms'; | |
| } | |
| if (timer) { pause(); play(); } | |
| } | |
| })(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment