Devlog #16 — Building the Bridge Designer: FEM in the Browser
No external library, no WebAssembly, no server — just JavaScript
assembling a global stiffness matrix, solving it with Gaussian
elimination, and rendering stress as colour in real time. Here is
exactly how it works.
What is the Finite Element Method?
FEM breaks a continuous structure into simple elements. For a 2D truss
(the type used in bridges), each element is a bar that can only carry
axial load — tension or compression. The analysis finds how much each
bar deforms under external loads, then computes the internal stress.
For a truss with $n$ nodes, the system of equations is: $\mathbf{K}
\cdot \mathbf{u} = \mathbf{f}$, where $\mathbf{K}$ is the global
stiffness matrix ($2n \times 2n$), $\mathbf{u}$ is the displacement
vector, and $\mathbf{f}$ is the applied force vector.
Step 1: Building the Element Stiffness Matrix
Each truss bar contributes a 4×4 local stiffness matrix. It depends on
the bar's orientation angle $\theta$, its cross-sectional area $A$,
Young's modulus $E$, and length $L$:
functionelementStiffness(nodeA, nodeB, E, A) {
const dx = nodeB.x - nodeA.x;
const dy = nodeB.y - nodeA.y;
const L = Math.hypot(dx, dy); const c = dx / L;
// cos θconst s = dy
/ L; // sin θconst k
= E * A / L;
// Local 4×4 matrix [ux_i, uy_i, ux_j, uy_j]return [ [ c*c, c*s, -c*c, -c*s], [ c*s, s*s,
-c*s, -s*s], [-c*c, -c*s, c*c, c*s], [-c*s, -s*s, c*s, s*s], ].map(row
=> row.map(v => v * k)); }
Step 2: Assembling the Global Stiffness Matrix
Each element's 4×4 matrix is "stamped" into the correct positions in
the global $2n \times 2n$ matrix. Node $i$ occupies DOFs $[2i, 2i+1]$
(x and y):
functionassembleK(nodes, elements, E, A) {
const n = nodes.length;
const dof = n * 2;
const K = Array.from({
length: dof }, () => newFloat64Array(dof));
for (const el
of elements) {
const ke =
elementStiffness(nodes[el.i], nodes[el.j], E,
A); const idx = [el.i*2, el.i*2+1,
el.j*2, el.j*2+1];
for (let r =
0; r < 4; r++)
for (let c =
0; c < 4; c++)
K[idx[r]][idx[c]] += ke[r][c]; } return K; }
Step 3: Applying Boundary Conditions
Fixed nodes (supports) have known displacement (zero). We enforce this
by zeroing the row and column for each fixed DOF and setting the
diagonal to 1:
for (const dof
of fixedDofs) {
for (let i =
0; i < K.length; i++) { K[dof][i] =
0; K[i][dof] = 0; }
K[dof][dof] = 1; f[dof] =
0;
// no displacement at support
}
Step 4: Gaussian Elimination Solver
With boundary conditions applied, solve $\mathbf{K} \cdot \mathbf{u} =
\mathbf{f}$ using Gaussian elimination with partial pivoting. For a
typical bridge (50 nodes), this runs in under 1 ms in JavaScript:
functiongaussianElimination(A, b) {
const n = b.length;
// Forward elimination with partial pivotfor (let col =
0; col < n; col++) {
// Find row with max absolute value in this columnlet maxRow = col;
for (let r = col+1; r < n; r++) if (Math.abs(A[r][col]) > Math.abs(A[maxRow][col]))
maxRow = r; [A[col], A[maxRow]] = [A[maxRow], A[col]]; [b[col],
b[maxRow]] = [b[maxRow], b[col]];
for (let r = col+1; r < n; r++) { const factor = A[r][col] /
A[col][col]; b[r] -= factor * b[col];
for (let c = col; c <
n; c++) A[r][c] -= factor * A[col][c]; } }
// Back substitutionconst x = newFloat64Array(n);
for (let i = n-1; i >= 0; i--) { x[i] = b[i];
for (let j = i+1; j < n; j++) x[i] -= A[i][j] * x[j]; x[i] /= A[i][i]; }
return x; }
Step 5: Computing Member Stress
With displacement $\mathbf{u}$ known, the axial strain in each bar is:
$\varepsilon = (du_x \cdot \cos\theta + du_y \cdot \sin\theta) / L$,
and stress is $\sigma = E \cdot \varepsilon$. Positive = tension (bar
is being stretched), negative = compression.
functionmemberStress(el, nodes, u, E) {
const { i, j } = el;
const dx = nodes[j].x - nodes[i].x;
const dy = nodes[j].y - nodes[i].y;
const L = Math.hypot(dx, dy); const c = dx / L, s = dy / L;
const du = (u[j*2] -
u[i*2]) * c + (u[j*2+1] - u[i*2+1]) * s; return E * du / L;
// σ = E·ε
}
Step 6: Colour-Coding Stress in Three.js
Each bar's stress value is mapped to a colour: blue (max compression)
→ white (zero load) → red (max tension).
Real steel bridge deflections under load are millimetres — invisible
at normal scale. We apply a configurable amplification factor (default
50×) to the node displacements so the deformation is visible without
misrepresenting the geometry. The bar stays connected to the (scaled)
displaced nodes:
Alternating diagonal members at 60°. Efficient use of material,
equal distribution of stress.
Most even stress distribution
Pratt Truss
Vertical members in compression, diagonal members in tension.
Preferred for long spans.
Great for heavy centre loads
Howe Truss
Opposite to Pratt — diagonals in compression. Less efficient for
steel but suited for wood.
Classic 19th century form
Accuracy note: Our implementation handles 2D truss
elements only. Real bridges need 3D beam elements (Euler-Bernoulli)
to capture bending moments. For the educational purpose —
demonstrating stiffness assembly and FEM solving — 2D truss analysis
is accurate and fast in the browser.
Try it now
Open the Bridge Designer, select a
preset, apply loads at different nodes and watch the colour-coded
stress and deformation update in real time. The safety factor panel
turns red when any member exceeds its yield stress.