Skip to content

Instantly share code, notes, and snippets.

@lucashmorais
Created September 21, 2025 20:16
Show Gist options
  • Select an option

  • Save lucashmorais/6648ab1ada34714b96453ecebdc9cc92 to your computer and use it in GitHub Desktop.

Select an option

Save lucashmorais/6648ab1ada34714b96453ecebdc9cc92 to your computer and use it in GitHub Desktop.
Animation of 3x3 multiplication using a systolic array
<!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