Area Lab
Move the sliders and watch the geometry respond. Dissect a triangle out of its rectangle, shear a parallelogram
without changing its area, and grow a circle — the formulas stop being memorization and start being obvious.
1 · The triangle is half its rectangle
Drag the apex along the top edge. The blue triangle always fills exactly half of the dashed rectangle.
2 · Shear the parallelogram
Slide it sideways. The base and the vertical height never change — so the area never changes either.
3 · Grow the circle
Grow the radius and watch the area grow with the square of the radius — that is why the formula is πr², not πr.
Challenges
Set the sliders, then press Check.
/* === PURE FUNCTIONS (extracted by tests/test_lab.js; keep DOM-free) === */ function avTriArea(b, h){ return b * h / 2; } function avParArea(b, h){ return b * h; } function avCircArea(r){ return Math.PI * r * r; } function avParPoints(shear){ /* base 8, height 5, drawn in a 360x240 box; returns corner coords */ var x0 = 180 - 80 - shear / 2, y1 = 190, y0 = 90; return { bl: [x0, y1], br: [x0 + 160, y1], tr: [x0 + 160 + shear, y0], tl: [x0 + shear, y0] }; } var AV_CHALLENGES = [ { id: 'shear', text: 'Shear the parallelogram as far left as it goes, then confirm the area is unchanged.', hint: 'Hint: drag the shear slider all the way left. Area = base \u00D7 height never moves.', check: function(s){ var ok = s.shear === -70; return { pass: ok, message: ok ? 'Correct \u2014 even fully sheared, the area is still 8 \u00D7 5 = 40.' : 'Not yet \u2014 move the shear slider all the way to \u221270 first.' }; } }, { id: 'circle', text: 'Set the radius to 5 and read the exact area.', hint: 'Hint: drag the radius slider to 5. Exact area = \u03C0r\u00B2.', check: function(s){ var ok = Math.abs(s.radius - 5) < 0.01; return { pass: ok, message: ok ? 'Correct \u2014 A = \u03C0 \u00D7 5\u00B2 = 25\u03C0 \u2248 78.54.' : 'Not yet \u2014 the radius is ' + s.radius + ', but it needs to be 5.' }; } }, { id: 'half', text: 'Explain in your own words why every triangle in panel 1 has area 30.', hint: 'Hint: no slider needed \u2014 think about what the dashed rectangle measures.', check: function(s){ var ok = Math.abs(avTriArea(10, 6) - 30) < 1e-9; return { pass: ok, message: ok ? 'The rectangle is 10 \u00D7 6 = 60, and the triangle always fills exactly half of it: 60 \u00F7 2 = 30, wherever the apex sits.' : 'Something is off in the area computation.' }; } } ]; function avCheckChallenge(id, s){ for (var i = 0; i < AV_CHALLENGES.length; i++){ if (AV_CHALLENGES[i].id === id){ return AV_CHALLENGES[i].check(s); } } return { pass: false, message: 'Unknown challenge.' }; } /* === END PURE FUNCTIONS === */ /* ---------- rendering ---------- */ var NS = 'http://www.w3.org/2000/svg'; function mk(tag, attrs, parent){ var e = document.createElementNS(NS, tag); for (var k in attrs){ e.setAttribute(k, attrs[k]); } parent.appendChild(e); return e; } function txt(parent, x, y, str, attrs){ var t = mk('text', attrs || {}, parent); t.setAttribute('x', x); t.setAttribute('y', y); t.textContent = str; return t; } var state = { apex: 50, shear: 0, radius: 3 }; function drawTri(){ var svg = document.getElementById('triSvg'); while (svg.firstChild) svg.removeChild(svg.firstChild); var rx = 60, ry = 40, rw = 240, rh = 144; /* rectangle 10 x 6 in 24px units */ mk('rect', { x: rx, y: ry, width: rw, height: rh, fill: 'none', stroke: '#94A3B8', 'stroke-width': 2, 'stroke-dasharray': '7 5' }, svg); var ax = rx + (state.apex / 100) * rw; var pts = rx + ',' + (ry + rh) + ' ' + (rx + rw) + ',' + (ry + rh) + ' ' + ax + ',' + ry; mk('polygon', { points: pts, fill: 'rgba(37,99,235,0.16)', stroke: '#2563EB', 'stroke-width': 2.5 }, svg); mk('circle', { cx: ax, cy: ry, r: 6, fill: '#14213D' }, svg); mk('circle', { cx: rx, cy: ry + rh, r: 5, fill: '#14213D' }, svg); mk('circle', { cx: rx + rw, cy: ry + rh, r: 5, fill: '#14213D' }, svg); txt(svg, rx + rw / 2, ry + rh + 22, 'base = 10', { 'text-anchor': 'middle', 'font-size': 14, 'font-weight': 700, fill: '#0d9488' }); txt(svg, rx - 12, ry + rh / 2, 'h = 6', { 'text-anchor': 'middle', 'font-size': 14, 'font-weight': 700, fill: '#b45309', transform: 'rotate(-90 ' + (rx - 12) + ' ' + (ry + rh / 2) + ')' }); document.getElementById('triRect').textContent = '60'; document.getElementById('triTri').textContent = avTriArea(10, 6); document.getElementById('triRatio').textContent = '1/2'; document.getElementById('apexVal').textContent = state.apex === 50 ? 'middle' : state.apex + '%'; } function drawPar(){ var svg = document.getElementById('parSvg'); while (svg.firstChild) svg.removeChild(svg.firstChild); var p = avParPoints(state.shear); function P(pt){ return pt[0] + ',' + pt[1]; } /* height guide */ mk('line', { x1: 300, y1: p.tl[1], x2: 300, y2: p.bl[1], stroke: '#F4B740', 'stroke-width': 2.5, 'stroke-dasharray': '6 4' }, svg); txt(svg, 312, (p.tl[1] + p.bl[1]) / 2, 'h = 5', { 'font-size': 13, 'font-weight': 700, fill: '#b45309' }); mk('polygon', { points: P(p.bl) + ' ' + P(p.br) + ' ' + P(p.tr) + ' ' + P(p.tl), fill: 'rgba(20,184,166,0.18)', stroke: '#14B8A6', 'stroke-width': 2.5 }, svg); txt(svg, (p.bl[0] + p.br[0]) / 2, p.bl[1] + 22, 'base = 8', { 'text-anchor': 'middle', 'font-size': 14, 'font-weight': 700, fill: '#0d9488' }); document.getElementById('parArea').textContent = avParArea(8, 5); document.getElementById('shearVal').textContent = state.shear; } function drawCirc(){ var svg = document.getElementById('circSvg'); while (svg.firstChild) svg.removeChild(svg.firstChild); var cx = 180, cy = 120, scale = 17; var rpx = state.radius * scale; mk('circle', { cx: cx, cy: cy, r: rpx, fill: 'rgba(37,99,235,0.12)', stroke: '#2563EB', 'stroke-width': 2.5 }, svg); mk('line', { x1: cx, y1: cy, x2: cx + rpx, y2: cy, stroke: '#F4B740', 'stroke-width': 3 }, svg); mk('circle', { cx: cx, cy: cy, r: 5, fill: '#14213D' }, svg); txt(svg, cx + rpx / 2, cy - 10, 'r = ' + state.radius, { 'text-anchor': 'middle', 'font-size': 14, 'font-weight': 700, fill: '#b45309' }); var rsq = state.radius * state.radius; document.getElementById('circR').textContent = state.radius; document.getElementById('circExact').textContent = (rsq === 1 ? '\u03C0' : (Number.isInteger(rsq) ? rsq : rsq.toFixed(2)) + '\u03C0'); document.getElementById('circDec').textContent = avCircArea(state.radius).toFixed(2); document.getElementById('radVal').textContent = state.radius; } function drawAll(){ drawTri(); drawPar(); drawCirc(); } document.getElementById('apexSlider').addEventListener('input', function(){ state.apex = +this.value; drawTri(); }); document.getElementById('shearSlider').addEventListener('input', function(){ state.shear = +this.value; drawPar(); }); document.getElementById('radSlider').addEventListener('input', function(){ state.radius = +this.value; drawCirc(); }); document.getElementById('resetBtn').addEventListener('click', function(){ state.apex = 50; state.shear = 0; state.radius = 3; document.getElementById('apexSlider').value = '50'; document.getElementById('shearSlider').value = '0'; document.getElementById('radSlider').value = '3'; drawAll(); }); /* challenges */ var challengeList = document.getElementById('challengeList'); AV_CHALLENGES.forEach(function(ch, idx){ var div = document.createElement('div'); div.className = 'challenge'; var t = document.createElement('p'); t.className = 'c-text'; t.textContent = (idx + 1) + '. ' + ch.text; var h = document.createElement('p'); h.className = 'c-hint'; h.textContent = ch.hint; var btn = document.createElement('button'); btn.className = 'btn btn-primary'; btn.type = 'button'; btn.textContent = 'Check'; var res = document.createElement('p'); res.className = 'c-result'; res.setAttribute('aria-live', 'polite'); btn.addEventListener('click', function(){ var r = avCheckChallenge(ch.id, state); res.textContent = r.message; res.className = 'c-result ' + (r.pass ? 'pass' : 'fail'); }); div.appendChild(t); div.appendChild(h); div.appendChild(btn); div.appendChild(res); challengeList.appendChild(div); }); drawAll();