Skill: function-transformations
Transformation Playground
Sweep the sliders and watch y = a(x − h)² + k morph live. Build the reflex: see the equation, picture the graph.
Your parabola vs. the parent
y = (x − 0)² + 0
your y = a(x−h)²+k
mystery target
Sliders
Challenges
'use strict'; /* === PURE FUNCTIONS (extracted by tests/test_lab.js; keep DOM-free) === */ function ftY(a, h, k, x){ return a * (x - h) * (x - h) + k; } /* sample points of y = a(x-h)^2 + k over [x0,x1] (graph coords) */ function ftSample(a, h, k, x0, x1, n){ var pts = []; for (var i = 0; i <= n; i++){ var x = x0 + (x1 - x0) * i / n; pts.push([x, ftY(a, h, k, x)]); } return pts; } function ftVertex(h, k){ return { x: h, y: k }; } function ftOpens(a){ return a > 0 ? 'upward' : 'downward'; } function ftShape(a){ var m = Math.abs(a); if (m === 1) return a < 0 ? 'reflected' : 'parent'; return (a < 0 ? 'reflected, ' : '') + (m > 1 ? 'stretched' : 'compressed'); } function ftEq(a, h, k){ var sa = a === 1 ? '' : a === -1 ? '\u2212' : String(a).replace('-', '\u2212'); var sh = h === 0 ? '(x)' : h > 0 ? '(x \u2212 ' + h + ')' : '(x + ' + Math.abs(h) + ')'; var sk = k === 0 ? '' : k > 0 ? ' + ' + k : ' \u2212 ' + Math.abs(k); return 'y = ' + sa + sh + '\u00B2' + sk; } /* deterministic pseudo-random target for the mystery challenge */ function ftMystery(seed){ var a = seed >>> 0; function rnd(){ a |= 0; a = (a + 0x6D2B79F5) | 0; var t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; } var av = [-2, -1.5, -1, 1, 1.5, 2, 2.5]; return { a: av[Math.floor(rnd() * av.length)], h: Math.floor(rnd() * 9) - 4, k: Math.floor(rnd() * 9) - 4 }; } var FT_CHALLENGES = [ { id: 'c1', text: 'Sweep h from \u22125 to 5. Does the graph move the way you first expected?', hint: 'Hint: watch the vertex, and remember the inside shift goes opposite.', check: function(s){ var ok = !(!(s.h === 3) || !(s.k === 2) || !(s.a === 1)); return { pass: ok, message: ok ? 'Correct \u2014 h = 3, k = 2 gives y = (x \u2212 3)\u00B2 + 2, vertex (3, 2).' : 'Not yet \u2014 set h = 3, k = 2, a = 1 to match y = (x \u2212 3)\u00B2 + 2.' }; } }, { id: 'c2', text: 'Sweep a from \u22123 to 3. At what a-value does the flip happen?', hint: 'Hint: the parabola opens downward exactly when a goes negative.', check: function(s){ var ok = s.a < 0; return { pass: ok, message: ok ? 'Correct \u2014 the flip happens at a = 0: any negative a opens downward.' : 'Not yet \u2014 drag a below 0 to flip the parabola.' }; } }, { id: 'c3', text: 'Match the mystery parabola (red) with your sliders.', hint: 'Hint: read its vertex first (that gives h and k), then match the width and flip (that gives a).', check: function(s){ var t = s.mystery; var ok = !(!(s.a === t.a) || !(s.h === t.h) || !(s.k === t.k)); return { pass: ok, message: ok ? 'Matched in ' + s.guesses + ' guess(es)! The mystery was ' + ftEq(t.a, t.h, t.k) + '.' : 'Not yet \u2014 keep adjusting. Vertex first, then shape.' }; } } ]; function ftCheckChallenge(id, s){ for (var i = 0; i < FT_CHALLENGES.length; i++){ if (FT_CHALLENGES[i].id === id){ return FT_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; } var state = { a: 1, h: 0, k: 0, mystery: ftMystery(20261001), guesses: 0, showMystery: false }; var CX = 220, CY = 180, SC = 26; function PX(x){ return CX + x * SC; } function PY(y){ return CY - y * SC; } function pathFrom(pts){ return 'M ' + pts.map(function (p){ return PX(p[0]).toFixed(1) + ',' + PY(p[1]).toFixed(1); }).join(' L '); } function draw(){ var svg = document.getElementById('ftSvg'); while (svg.firstChild) svg.removeChild(svg.firstChild); /* grid */ for (var gx = -8; gx <= 8; gx++){ mk('line', { x1: PX(gx), y1: 10, x2: PX(gx), y2: 350, stroke: '#e2e8f0', 'stroke-width': 1 }, svg); } for (var gy = -6; gy <= 6; gy++){ mk('line', { x1: 10, y1: PY(gy), x2: 430, y2: PY(gy), stroke: '#e2e8f0', 'stroke-width': 1 }, svg); } mk('line', { x1: 10, y1: CY, x2: 430, y2: CY, stroke: '#94a3b8', 'stroke-width': 1.5 }, svg); mk('line', { x1: CX, y1: 10, x2: CX, y2: 350, stroke: '#94a3b8', 'stroke-width': 1.5 }, svg); /* parent */ mk('path', { d: pathFrom(ftSample(1, 0, 0, -2.6, 2.6, 60)), fill: 'none', stroke: '#94a3b8', 'stroke-width': 2, 'stroke-dasharray': '7 5' }, svg); /* mystery */ if (state.showMystery){ var m = state.mystery; mk('path', { d: pathFrom(ftSample(m.a, m.h, m.k, m.h - 3, m.h + 3, 60)), fill: 'none', stroke: '#DC2626', 'stroke-width': 2.5 }, svg); } /* user curve */ var s = state; mk('path', { d: pathFrom(ftSample(s.a, s.h, s.k, s.h - 3.2, s.h + 3.2, 72)), fill: 'none', stroke: '#2563EB', 'stroke-width': 3 }, svg); /* vertex dot */ var v = ftVertex(s.h, s.k); mk('circle', { cx: PX(v.x), cy: PY(v.y), r: 6, fill: '#F4B740', stroke: '#172033', 'stroke-width': 2 }, svg); var t = mk('text', { x: PX(v.x) + 12, y: PY(v.y) - 10, 'font-size': 13, 'font-weight': 700, fill: '#172033' }, svg); t.textContent = '(' + v.x + ', ' + v.y + ')'; /* readouts */ document.getElementById('eqDisplay').textContent = ftEq(s.a, s.h, s.k); document.getElementById('roVertex').textContent = '(' + v.x + ', ' + v.y + ')'; document.getElementById('roOpens').textContent = ftOpens(s.a); document.getElementById('roShape').textContent = ftShape(s.a); document.getElementById('hVal').textContent = s.h; document.getElementById('kVal').textContent = s.k; document.getElementById('aVal').textContent = s.a; } function bumpGuesses(){ state.guesses++; } ['hSlider', 'kSlider', 'aSlider'].forEach(function (id){ document.getElementById(id).addEventListener('input', function(){ state.h = +document.getElementById('hSlider').value; state.k = +document.getElementById('kSlider').value; state.a = +document.getElementById('aSlider').value; bumpGuesses(); draw(); }); }); document.getElementById('resetBtn').addEventListener('click', function(){ state.a = 1; state.h = 0; state.k = 0; state.guesses = 0; document.getElementById('hSlider').value = '0'; document.getElementById('kSlider').value = '0'; document.getElementById('aSlider').value = '1'; draw(); }); var challengeList = document.getElementById('challengeList'); FT_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; if (ch.id === 'c3'){ var my = document.createElement('div'); my.className = 'mystery'; my.textContent = 'Mystery target shown in red on the graph.'; div.appendChild(my); } var btn = document.createElement('button'); btn.className = '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(){ if (!(!(ch.id === 'c3') || !(!state.showMystery))){ state.showMystery = true; draw(); } var r = ftCheckChallenge(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); }); draw();