<div class="controls">
<button id="btn-coarse" type="button">Coarse mesh (4ร4)</button>
<button id="btn-medium" type="button" class="active">Medium mesh (8ร8)</button>
<button id="btn-fine" type="button">Fine mesh (16ร16)</button>
</div>
<canvas id="canvas" width="480" height="260"></canvas>
<div class="info" id="info">Hover over a node to see its temperature.</div>
<div class="legend">
<span class="leg-hot">Hot (T=1)</span>
<span class="leg-bar"></span>
<span class="leg-cold">Cold (T=0)</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fa; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 7px; cursor: pointer; transition: background .15s; }
button.active { background: #1d3557; color: #fff; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #fff; max-width: 100%; }
.info { font-size: .85rem; color: #444; margin: .4rem 0; min-height: 1.2em; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .8rem; }
.leg-hot { color: #c0392b; font-weight: 700; }
.leg-cold { color: #2980b9; font-weight: 700; }
.leg-bar { flex: 1; max-width: 160px; height: 10px; border-radius: 5px;
background: linear-gradient(to right, #c0392b, #f39c12, #f1c40f, #2ecc71, #2980b9); }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// FEM solver for 2D steady-state heat conduction on a regular triangular mesh.
// Domain: unit square [0,1] x [0,1]
// BCs: T=1 on left edge (x=0), T=0 on right edge (x=1), insulated top/bottom.
// Each rectangular cell is split into 2 triangles.
// The stiffness matrix K is assembled from element conductivity matrices,
// then boundary conditions are applied and the system K u = f is solved
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const info = document.getElementById('info');
// โโ Mesh generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildMesh(N) {
// N = number of divisions along each axis โ (N+1)ยฒ nodes, 2Nยฒ triangles
const nodes = [];
for (let j = 0; j <= N; j++)
for (let i = 0; i <= N; i++)
nodes.push([i / N, j / N]); // [x, y]
const idx = (i, j) => j * (N + 1) + i;
const tris = [];
for (let j = 0; j < N; j++) {
for (let i = 0; i < N; i++) {
// lower-left triangle
tris.push([idx(i,j), idx(i+1,j), idx(i+1,j+1)]);
// upper-right triangle
tris.push([idx(i,j), idx(i+1,j+1), idx(i,j+1)]);
}
}
return { nodes, tris, N };
}
// โโ FEM assembly (constant-strain triangles, k=1) โโโโโโโโโโโโโโโโโโโโโ
function solveFEM(mesh) {
const { nodes, tris } = mesh;
const n = nodes.length;
// Sparse storage as flat array (small enough for n โค 289)
const K = new Float64Array(n * n);
const f = new Float64Array(n);
for (const [a, b, c] of tris) {
const [x0,y0] = nodes[a], [x1,y1] = nodes[b], [x2,y2] = nodes[c];
// Area
const area = 0.5 * Math.abs((x1-x0)*(y2-y0) - (x2-x0)*(y1-y0));
if (area < 1e-14) continue;
// Gradient of shape functions (constant inside element)
const bv = [y1-y2, y2-y0, y0-y1];
const cv = [x2-x1, x0-x2, x1-x0];
// Element stiffness: ke[i][j] = (bi*bj + ci*cj)/(4*area)
const ids = [a, b, c];
for (let i = 0; i < 3; i++)
for (let j = 0; j < 3; j++)
K[ids[i]*n + ids[j]] += (bv[i]*bv[j] + cv[i]*cv[j]) / (4*area);
}
// Apply Dirichlet BCs: T=1 at x=0 nodes, T=0 at x=1 nodes
const u = new Float64Array(n);
const fixed = new Uint8Array(n);
nodes.forEach(([x], i) => {
if (x < 1e-9) { u[i] = 1; fixed[i] = 1; }
if (x > 1-1e-9) { u[i] = 0; fixed[i] = 1; }
});
// Move known BCs to RHS and zero rows/cols (penalty alternative: just Gauss-Seidel with fixed)
// Gauss-Seidel iteration โ simple, correct for SPD systems
const ITER = 800;
for (let it = 0; it < ITER; it++) {
for (let i = 0; i < n; i++) {
if (fixed[i]) continue;
let sigma = f[i];
for (let j = 0; j < n; j++)
if (j !== i) sigma -= K[i*n+j] * u[j];
const kii = K[i*n+i];
if (Math.abs(kii) > 1e-14) u[i] = sigma / kii;
}
}
return u;
}
// โโ Color mapping (blueโgreenโyellowโred) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function tempColor(t) {
t = Math.max(0, Math.min(1, t));
// Blue (0) โ Cyan (0.25) โ Green (0.5) โ Yellow (0.75) โ Red (1)
let r, g, b;
if (t < 0.25) { const s=t/0.25; r=0; g=Math.round(255*s); b=255; }
else if (t < 0.5) { const s=(t-0.25)/0.25; r=0; g=255; b=Math.round(255*(1-s)); }
else if (t < 0.75) { const s=(t-0.5)/0.25; r=Math.round(255*s); g=255; b=0; }
else { const s=(t-0.75)/0.25; r=255; g=Math.round(255*(1-s)); b=0; }
return `rgb(${r},${g},${b})`;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PAD = 18;
function toCanvas(x, y) {
const W = canvas.width - 2*PAD;
const H = canvas.height - 2*PAD;
return [PAD + x*W, PAD + (1-y)*H];
}
function draw(mesh, u) {
const { nodes, tris } = mesh;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Fill each triangle with averaged color
for (const [a, b, c] of tris) {
const tAvg = (u[a] + u[b] + u[c]) / 3;
ctx.beginPath();
const [ax,ay] = toCanvas(...nodes[a]);
const [bx,by] = toCanvas(...nodes[b]);
const [cx,cy] = toCanvas(...nodes[c]);
ctx.moveTo(ax,ay); ctx.lineTo(bx,by); ctx.lineTo(cx,cy); ctx.closePath();
ctx.fillStyle = tempColor(tAvg);
ctx.fill();
// Mesh edges
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// Nodes as small dots
nodes.forEach(([x,y], i) => {
const [px,py] = toCanvas(x,y);
ctx.beginPath();
ctx.arc(px, py, mesh.N <= 4 ? 4 : mesh.N <= 8 ? 2.5 : 1.5, 0, 2*Math.PI);
ctx.fillStyle = tempColor(u[i]);
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = 0.5;
ctx.fill();
ctx.stroke();
});
// BC labels
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillStyle = '#c0392b';
ctx.fillText('T=1 (hot)', PAD, 12);
ctx.fillStyle = '#2980b9';
ctx.fillText('T=0 (cold)', canvas.width - PAD, 12);
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let currentMesh, currentU;
function run(N) {
currentMesh = buildMesh(N);
currentU = solveFEM(currentMesh);
draw(currentMesh, currentU);
info.textContent = fmt('Mesh: %N%ร%N% = %length% triangles, %length2% nodes. Solved by Gauss-Seidel.', { N: N, length: currentMesh.tris.length, length2: currentMesh.nodes.length });
}
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (!currentMesh) return;
// Find nearest node
let best = -1, bestD = Infinity;
currentMesh.nodes.forEach(([x,y], i) => {
const [px,py] = toCanvas(x,y);
const d = (px-mx)**2 + (py-my)**2;
if (d < bestD) { bestD = d; best = i; }
});
if (best >= 0 && bestD < 400) {
const [x,y] = currentMesh.nodes[best];
info.textContent = fmt('Node (%x%, %y%) โ T = %v%', { x: x.toFixed(3), y: y.toFixed(3), v: currentU[best].toFixed(4) });
} else {
info.textContent = fmt('Mesh: %N%ร%N% = %length% triangles. Hover a node.', { N: currentMesh.N, length: currentMesh.tris.length });
}
});
document.getElementById('btn-coarse').addEventListener('click', () => {
setActive('btn-coarse'); run(4);
});
document.getElementById('btn-medium').addEventListener('click', () => {
setActive('btn-medium'); run(8);
});
document.getElementById('btn-fine').addEventListener('click', () => {
setActive('btn-fine'); run(16);
});
function setActive(id) {
['btn-coarse','btn-medium','btn-fine'].forEach(b => {
document.getElementById(b).classList.toggle('active', b === id);
});
}
// Start with medium mesh
run(8);
// โโ Console verification output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Verify: at x=0.5 (midpoint), by symmetry T โ 0.5
// Also check boundary nodes are exactly T=1 and T=0
setTimeout(() => {
const { nodes } = currentMesh;
const midNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>Math.abs(n[0]-0.5)<0.01);
const midTemps = midNodes.map(({i})=>currentU[i]);
const midAvg = midTemps.reduce((a,b)=>a+b,0)/midTemps.length;
console.log('FEM verification:');
console.log(' ' + fmt('Mid-plane (x=0.5) average T = %midAvg% (expected โ 0.500 by symmetry)', { midAvg: midAvg.toFixed(4) }));
const leftNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>n[0]<1e-9);
const rightNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>n[0]>1-1e-9);
const leftOk = leftNodes.every(({i})=>Math.abs(currentU[i]-1)<1e-9);
const rightOk = rightNodes.every(({i})=>Math.abs(currentU[i])<1e-9);
console.log(' ' + fmt('Left BC T=1: %v%', { v: leftOk ? 'PASS' : 'FAIL' }));
console.log(' ' + fmt('Right BC T=0: %v%', { v: rightOk ? 'PASS' : 'FAIL' }));
console.log(' ' + fmt('Nodes: %length% | Triangles: %length2%', { length: nodes.length, length2: currentMesh.tris.length }));
}, 100);
<div class="controls">
<button id="btn-coarse" type="button">Coarse mesh (4ร4)</button>
<button id="btn-medium" type="button" class="active">Medium mesh (8ร8)</button>
<button id="btn-fine" type="button">Fine mesh (16ร16)</button>
</div>
<canvas id="canvas" width="480" height="260"></canvas>
<div class="info" id="info">Hover over a node to see its temperature.</div>
<div class="legend">
<span class="leg-hot">Hot (T=1)</span>
<span class="leg-bar"></span>
<span class="leg-cold">Cold (T=0)</span>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f5f7fa; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #fff; color: #1d3557; border-radius: 7px; cursor: pointer; transition: background .15s; }
button.active { background: #1d3557; color: #fff; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #fff; max-width: 100%; }
.info { font-size: .85rem; color: #444; margin: .4rem 0; min-height: 1.2em; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .8rem; }
.leg-hot { color: #c0392b; font-weight: 700; }
.leg-cold { color: #2980b9; font-weight: 700; }
.leg-bar { flex: 1; max-width: 160px; height: 10px; border-radius: 5px;
background: linear-gradient(to right, #c0392b, #f39c12, #f1c40f, #2ecc71, #2980b9); }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// FEM solver for 2D steady-state heat conduction on a regular triangular mesh.
// Domain: unit square [0,1] x [0,1]
// BCs: T=1 on left edge (x=0), T=0 on right edge (x=1), insulated top/bottom.
// Each rectangular cell is split into 2 triangles.
// The stiffness matrix K is assembled from element conductivity matrices,
// then boundary conditions are applied and the system K u = f is solved
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const info = document.getElementById('info');
// โโ Mesh generation โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildMesh(N) {
// N = number of divisions along each axis โ (N+1)ยฒ nodes, 2Nยฒ triangles
const nodes = [];
for (let j = 0; j <= N; j++)
for (let i = 0; i <= N; i++)
nodes.push([i / N, j / N]); // [x, y]
const idx = (i, j) => j * (N + 1) + i;
const tris = [];
for (let j = 0; j < N; j++) {
for (let i = 0; i < N; i++) {
// lower-left triangle
tris.push([idx(i,j), idx(i+1,j), idx(i+1,j+1)]);
// upper-right triangle
tris.push([idx(i,j), idx(i+1,j+1), idx(i,j+1)]);
}
}
return { nodes, tris, N };
}
// โโ FEM assembly (constant-strain triangles, k=1) โโโโโโโโโโโโโโโโโโโโโ
function solveFEM(mesh) {
const { nodes, tris } = mesh;
const n = nodes.length;
// Sparse storage as flat array (small enough for n โค 289)
const K = new Float64Array(n * n);
const f = new Float64Array(n);
for (const [a, b, c] of tris) {
const [x0,y0] = nodes[a], [x1,y1] = nodes[b], [x2,y2] = nodes[c];
// Area
const area = 0.5 * Math.abs((x1-x0)*(y2-y0) - (x2-x0)*(y1-y0));
if (area < 1e-14) continue;
// Gradient of shape functions (constant inside element)
const bv = [y1-y2, y2-y0, y0-y1];
const cv = [x2-x1, x0-x2, x1-x0];
// Element stiffness: ke[i][j] = (bi*bj + ci*cj)/(4*area)
const ids = [a, b, c];
for (let i = 0; i < 3; i++)
for (let j = 0; j < 3; j++)
K[ids[i]*n + ids[j]] += (bv[i]*bv[j] + cv[i]*cv[j]) / (4*area);
}
// Apply Dirichlet BCs: T=1 at x=0 nodes, T=0 at x=1 nodes
const u = new Float64Array(n);
const fixed = new Uint8Array(n);
nodes.forEach(([x], i) => {
if (x < 1e-9) { u[i] = 1; fixed[i] = 1; }
if (x > 1-1e-9) { u[i] = 0; fixed[i] = 1; }
});
// Move known BCs to RHS and zero rows/cols (penalty alternative: just Gauss-Seidel with fixed)
// Gauss-Seidel iteration โ simple, correct for SPD systems
const ITER = 800;
for (let it = 0; it < ITER; it++) {
for (let i = 0; i < n; i++) {
if (fixed[i]) continue;
let sigma = f[i];
for (let j = 0; j < n; j++)
if (j !== i) sigma -= K[i*n+j] * u[j];
const kii = K[i*n+i];
if (Math.abs(kii) > 1e-14) u[i] = sigma / kii;
}
}
return u;
}
// โโ Color mapping (blueโgreenโyellowโred) โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function tempColor(t) {
t = Math.max(0, Math.min(1, t));
// Blue (0) โ Cyan (0.25) โ Green (0.5) โ Yellow (0.75) โ Red (1)
let r, g, b;
if (t < 0.25) { const s=t/0.25; r=0; g=Math.round(255*s); b=255; }
else if (t < 0.5) { const s=(t-0.25)/0.25; r=0; g=255; b=Math.round(255*(1-s)); }
else if (t < 0.75) { const s=(t-0.5)/0.25; r=Math.round(255*s); g=255; b=0; }
else { const s=(t-0.75)/0.25; r=255; g=Math.round(255*(1-s)); b=0; }
return `rgb(${r},${g},${b})`;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const PAD = 18;
function toCanvas(x, y) {
const W = canvas.width - 2*PAD;
const H = canvas.height - 2*PAD;
return [PAD + x*W, PAD + (1-y)*H];
}
function draw(mesh, u) {
const { nodes, tris } = mesh;
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Fill each triangle with averaged color
for (const [a, b, c] of tris) {
const tAvg = (u[a] + u[b] + u[c]) / 3;
ctx.beginPath();
const [ax,ay] = toCanvas(...nodes[a]);
const [bx,by] = toCanvas(...nodes[b]);
const [cx,cy] = toCanvas(...nodes[c]);
ctx.moveTo(ax,ay); ctx.lineTo(bx,by); ctx.lineTo(cx,cy); ctx.closePath();
ctx.fillStyle = tempColor(tAvg);
ctx.fill();
// Mesh edges
ctx.strokeStyle = 'rgba(0,0,0,0.08)';
ctx.lineWidth = 0.5;
ctx.stroke();
}
// Nodes as small dots
nodes.forEach(([x,y], i) => {
const [px,py] = toCanvas(x,y);
ctx.beginPath();
ctx.arc(px, py, mesh.N <= 4 ? 4 : mesh.N <= 8 ? 2.5 : 1.5, 0, 2*Math.PI);
ctx.fillStyle = tempColor(u[i]);
ctx.strokeStyle = 'rgba(0,0,0,0.25)';
ctx.lineWidth = 0.5;
ctx.fill();
ctx.stroke();
});
// BC labels
ctx.font = 'bold 11px system-ui';
ctx.textAlign = 'center';
ctx.fillStyle = '#c0392b';
ctx.fillText('T=1 (hot)', PAD, 12);
ctx.fillStyle = '#2980b9';
ctx.fillText('T=0 (cold)', canvas.width - PAD, 12);
}
// โโ Interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let currentMesh, currentU;
function run(N) {
currentMesh = buildMesh(N);
currentU = solveFEM(currentMesh);
draw(currentMesh, currentU);
info.textContent = fmt('Mesh: %N%ร%N% = %length% triangles, %length2% nodes. Solved by Gauss-Seidel.', { N: N, length: currentMesh.tris.length, length2: currentMesh.nodes.length });
}
canvas.addEventListener('mousemove', e => {
const rect = canvas.getBoundingClientRect();
const mx = e.clientX - rect.left;
const my = e.clientY - rect.top;
if (!currentMesh) return;
// Find nearest node
let best = -1, bestD = Infinity;
currentMesh.nodes.forEach(([x,y], i) => {
const [px,py] = toCanvas(x,y);
const d = (px-mx)**2 + (py-my)**2;
if (d < bestD) { bestD = d; best = i; }
});
if (best >= 0 && bestD < 400) {
const [x,y] = currentMesh.nodes[best];
info.textContent = fmt('Node (%x%, %y%) โ T = %v%', { x: x.toFixed(3), y: y.toFixed(3), v: currentU[best].toFixed(4) });
} else {
info.textContent = fmt('Mesh: %N%ร%N% = %length% triangles. Hover a node.', { N: currentMesh.N, length: currentMesh.tris.length });
}
});
document.getElementById('btn-coarse').addEventListener('click', () => {
setActive('btn-coarse'); run(4);
});
document.getElementById('btn-medium').addEventListener('click', () => {
setActive('btn-medium'); run(8);
});
document.getElementById('btn-fine').addEventListener('click', () => {
setActive('btn-fine'); run(16);
});
function setActive(id) {
['btn-coarse','btn-medium','btn-fine'].forEach(b => {
document.getElementById(b).classList.toggle('active', b === id);
});
}
// Start with medium mesh
run(8);
// โโ Console verification output โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Verify: at x=0.5 (midpoint), by symmetry T โ 0.5
// Also check boundary nodes are exactly T=1 and T=0
setTimeout(() => {
const { nodes } = currentMesh;
const midNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>Math.abs(n[0]-0.5)<0.01);
const midTemps = midNodes.map(({i})=>currentU[i]);
const midAvg = midTemps.reduce((a,b)=>a+b,0)/midTemps.length;
console.log('FEM verification:');
console.log(' ' + fmt('Mid-plane (x=0.5) average T = %midAvg% (expected โ 0.500 by symmetry)', { midAvg: midAvg.toFixed(4) }));
const leftNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>n[0]<1e-9);
const rightNodes = nodes.map((n,i)=>({n,i})).filter(({n})=>n[0]>1-1e-9);
const leftOk = leftNodes.every(({i})=>Math.abs(currentU[i]-1)<1e-9);
const rightOk = rightNodes.every(({i})=>Math.abs(currentU[i])<1e-9);
console.log(' ' + fmt('Left BC T=1: %v%', { v: leftOk ? 'PASS' : 'FAIL' }));
console.log(' ' + fmt('Right BC T=0: %v%', { v: rightOk ? 'PASS' : 'FAIL' }));
console.log(' ' + fmt('Nodes: %length% | Triangles: %length2%', { length: nodes.length, length2: currentMesh.tris.length }));
}, 100);
Comments
Loading comments...