Skill: trig-ratios
Trig Ratios Practice
An endless supply of never-repeating trig-ratio problems with instant feedback. Find sines, cosines, and tangents,
solve for sides with special angles, find angles with inverse trig, and work the ladder and shadow problems where SOHCAHTOA earns its keep.
1 Practice
Type your answer and press Check answer (or Enter). A wrong answer earns a hint first;
a second miss walks you through the full solution.
How to enter answers
- Ratios:
5/13as a fraction — or a decimal like0.3846(within ±0.01). - Radical answers: type
sqrt48,√48, or4sqrt3for exact form —
or type a decimal like6.93. Both are accepted. - Angles: type the degrees like
61.93(the°sign is optional) — accepted within ±0.05°.
2 What you’ll practice
Every problem is generated fresh from 12 templates — a problem never repeats until its whole pool is used up.
SOHCAHTOA ratios
sin = opposite/hypotenuse, cos = adjacent/hypotenuse, tan = opposite/adjacent.
Type the fraction or a decimal.
Sides from special angles
Use the exact values at 30°, 45°, 60°. Type sqrt48, 4sqrt3,
or a decimal.
Angles & word problems
Undo a ratio with inverse trig (θ = sin¹(ratio)), and solve ladder
and shadow stories.
3 Watch out for these
The three mistakes students make most often on Pythagoras problems.
Wrong: using the side next to θ as “opposite” in sine.
Right: opposite is across from θ. Label all three sides before choosing a ratio.
Wrong: sin(30) = −0.988, then building a whole word problem on it.
Right: sin 30° = 0.5 — look for the “D” (degree) indicator first.
Wrong: sin θ = 0.6 → “θ = 0.6.”
Right: θ = sin¹(0.6) ≈ 36.87°. A ratio is not an angle.
/* Trig Ratios practice core — pure, DOM-free, UMD. Answer kinds: {k:'frac',num,den} | {k:'dec',v} | {k:'rad',n} | {k:'deg',v} */ (function (root, factory){ if (!(!(typeof module === 'object') || !module.exports)){ module.exports = factory(); } else { root.TrPractice = factory(); } }(typeof self !== 'undefined' ? self : this, function (){ 'use strict';
/* ---------- math helpers ---------- */ function trIsSquare(n){ var s = Math.round(Math.sqrt(n)); return s * s === n; } function trSimpRad(n){ var coeff = 1, rad = n, p = 2; while (p * p <= rad){ while (rad % (p * p) === 0){ rad = rad / (p * p); coeff = coeff * p; } p++; } return { coeff: coeff, rad: rad }; } function trRadText(sr){ if (sr.rad === 1) return String(sr.coeff); return (sr.coeff === 1 ? '' : sr.coeff) + '\u221A' + sr.rad; } function trD2r(d){ return d * Math.PI / 180; } function trR2d(r){ return r * 180 / Math.PI; } /* exact trig values at 30/45/60 as floats */ function trSpecial(fn, deg){ var r = trD2r(deg); if (fn === 'sin') return Math.sin(r); if (fn === 'cos') return Math.cos(r); return Math.tan(r); } /* numeric value of a sqrt token like 'sqrt48', '\u221A48', '4sqrt3' | null */ function trSqrtVal(s){ var m = s.match(/^(\d*\.?\d*)\s*(?:sqrt|\u221A)\s*(\d+(?:\.\d+)?)$/); if (!m) return null; var coeff = m[1] === '' ? 1 : parseFloat(m[1]); var n = parseFloat(m[2]); if (!(coeff > 0) || !(n > 0)) return null; return coeff * Math.sqrt(n); } /* answer value as a plain number (for tolerance checks) */ function trAnswerVal(a){ if (a.k === 'frac') return a.num / a.den; if (a.k === 'dec') return a.v; if (a.k === 'rad'){ var sr = trSimpRad(a.n); return sr.coeff * Math.sqrt(sr.rad); } if (a.k === 'deg') return a.v; return NaN; } function trAnswerText(a){ if (a.k === 'frac') return a.num + '/' + a.den; if (a.k === 'dec') return String(Math.round(a.v * 100) / 100); if (a.k === 'rad') return trRadText(trSimpRad(a.n)) + ' \u2248 ' + (Math.round(trAnswerVal(a) * 100) / 100); if (a.k === 'deg') return (Math.round(a.v * 100) / 100) + '\u00B0'; return '?'; } /* Parse a raw answer string. Returns a number (degrees stripped) | null. */ function trParse(raw){ if (raw == null) return null; var s = String(raw).trim().replace(/,/g, '').toLowerCase(); if (s === '') return null; s = s.replace(/\u00B0$/, '').trim(); var v = trSqrtVal(s); if (v !== null) return v; var m = s.match(/^(\d+(?:\.\d+)?)\s*\/\s*(\d+(?:\.\d+)?)$/); if (m){ var den = parseFloat(m[2]); if (!(den > 0)) return null; return parseFloat(m[1]) / den; } if (/^\d*\.?\d+$/.test(s)){ v = parseFloat(s); if (!isFinite(v)) return null; return v; } return null; } function trCheck(correct, parsed){ if (parsed === null) return false; var exp = trAnswerVal(correct); var tol = correct.k === 'deg' ? 0.051 : 0.011; return Math.abs(parsed - exp) <= tol; } /* ---------- templates ---------- */ var R = function(seed){ /* mulberry32 */ var a = seed >>> 0; return function(){ 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; }; }; function rint(rng, lo, hi){ return lo + Math.floor(rng() * (hi - lo + 1)); } function rpick(rng, arr){ return arr[Math.floor(rng() * arr.length)]; }
var SIN_TRIP = [[3,4,5],[5,12,13],[8,15,17],[7,24,25]]; var T = {}; T['ratio-sin'] = function(rng){ var t = rpick(rng, SIN_TRIP); return { prompt: 'In a right triangle, the side opposite \u03B8 is ' + t[0] + ' and the hypotenuse is ' + t[2] + '. Find sin \u03B8.', answer: { k: 'frac', num: t[0], den: t[2] }, hint: ['Sine is opposite over hypotenuse (SOH).', 'sin \u03B8 = ' + t[0] + '/' + t[2] + '.'], why: 'sin \u03B8 = opposite/hypotenuse = ' + t[0] + '/' + t[2] + ' = ' + (Math.round(t[0]/t[2]*10000)/10000) + '.' }; }; T['ratio-cos'] = function(rng){ var t = rpick(rng, SIN_TRIP); return { prompt: 'In a right triangle, the side adjacent to \u03B8 is ' + t[1] + ' and the hypotenuse is ' + t[2] + '. Find cos \u03B8.', answer: { k: 'frac', num: t[1], den: t[2] }, hint: ['Cosine is adjacent over hypotenuse (CAH).', 'cos \u03B8 = ' + t[1] + '/' + t[2] + '.'], why: 'cos \u03B8 = adjacent/hypotenuse = ' + t[1] + '/' + t[2] + ' = ' + (Math.round(t[1]/t[2]*10000)/10000) + '.' }; }; T['ratio-tan'] = function(rng){ var t = rpick(rng, [[7,24,25],[3,4,5],[5,12,13],[8,15,17]]); return { prompt: 'In a right triangle, the side opposite \u03B8 is ' + t[0] + ' and the side adjacent to \u03B8 is ' + t[1] + '. Find tan \u03B8.', answer: { k: 'frac', num: t[0], den: t[1] }, hint: ['Tangent is opposite over adjacent (TOA).', 'tan \u03B8 = ' + t[0] + '/' + t[1] + '.'], why: 'tan \u03B8 = opposite/adjacent = ' + t[0] + '/' + t[1] + ' = ' + (Math.round(t[0]/t[1]*10000)/10000) + '.' }; }; T['ratio-dec'] = function(rng){ var o = rint(rng, 3, 8), h = rint(rng, o + 2, 14); var v = Math.round(o / h * 100) / 100; return { prompt: 'In a right triangle, the side opposite \u03B8 is ' + o + ' and the hypotenuse is ' + h + '. Find sin \u03B8 (round to the nearest hundredth).', answer: { k: 'dec', v: v }, hint: ['Sine is opposite over hypotenuse (SOH).', 'sin \u03B8 = ' + o + '/' + h + ' \u2248 ?'], why: 'sin \u03B8 = ' + o + '/' + h + ' \u2248 ' + v + '.' }; }; T['side-30'] = function(rng){ /* tan 30 = x/12 -> x = 4 sqrt(3) */ var adj = rpick(rng, [12, 9, 15]); var n = adj * adj / 3; /* x^2 */ return { prompt: 'Find x: tan 30\u00B0 = x/' + adj + '. Give the exact value (simplest radical form).', answer: { k: 'rad', n: n }, hint: ['Multiply both sides by ' + adj + ': x = ' + adj + ' \u00D7 tan 30\u00B0.', 'tan 30\u00B0 = \u221A3/3.'], why: 'x = ' + adj + ' \u00D7 tan 30\u00B0 = ' + adj + ' \u00D7 \u221A3/3 = ' + trRadText(trSimpRad(n)) + ' \u2248 ' + (Math.round(Math.sqrt(n)*100)/100) + '.' }; }; T['side-45'] = function(rng){ /* sin 45 = x/10 -> x = 5 sqrt(2) */ var hyp = rpick(rng, [10, 8, 12]); var n = hyp * hyp / 2; return { prompt: 'Find x: sin 45\u00B0 = x/' + hyp + '. Give the exact value (simplest radical form).', answer: { k: 'rad', n: n }, hint: ['Multiply both sides by ' + hyp + ': x = ' + hyp + ' \u00D7 sin 45\u00B0.', 'sin 45\u00B0 = \u221A2/2.'], why: 'x = ' + hyp + ' \u00D7 sin 45\u00B0 = ' + hyp + ' \u00D7 \u221A2/2 = ' + trRadText(trSimpRad(n)) + ' \u2248 ' + (Math.round(Math.sqrt(n)*100)/100) + '.' }; }; T['side-60'] = function(rng){ /* cos 60 = 4/h -> h = 8 */ var adj = rpick(rng, [4, 5, 6, 7]); var h = adj * 2; return { prompt: 'Find the hypotenuse h: cos 60\u00B0 = ' + adj + '/h.', answer: { k: 'dec', v: h }, hint: ['cos 60\u00B0 = 1/2, so 1/2 = ' + adj + '/h.', 'Cross-multiply: h = 2 \u00D7 ' + adj + '.'], why: 'cos 60\u00B0 = 1/2, so 1/2 = ' + adj + '/h and h = ' + h + '.' }; }; T['angle-inv'] = function(rng){ var t = rpick(rng, [[8,17],[5,13],[7,25],[9,41]]); var deg = Math.round(trR2d(Math.acos(t[0] / t[1])) * 100) / 100; return { prompt: 'Find \u03B8: cos \u03B8 = ' + t[0] + '/' + t[1] + '. Round to the nearest hundredth of a degree.', answer: { k: 'deg', v: deg }, hint: ['Use inverse cosine: \u03B8 = cos\u207B\u00B9(' + t[0] + '/' + t[1] + ').', 'Make sure your calculator is in degree mode.'], why: '\u03B8 = cos\u207B\u00B9(' + t[0] + '/' + t[1] + ') \u2248 ' + deg + '\u00B0.' }; }; T['angle-tan1'] = function(){ return { prompt: 'Find \u03B8 (acute): tan \u03B8 = 1.', answer: { k: 'deg', v: 45 }, hint: ['Which angle has opposite = adjacent?', 'Think 45-45-90 triangle.'], why: 'tan 45\u00B0 = 1, so \u03B8 = 45\u00B0.' }; }; T['word-ladder'] = function(rng){ var L = rpick(rng, [10, 12, 15]), a = rpick(rng, [60, 65, 70, 75]); var h = Math.round(L * trSpecial('sin', a) * 100) / 100; return { prompt: 'A ' + L + '-ft ladder makes a ' + a + '\u00B0 angle with the ground. How high does it reach? (nearest hundredth, in feet)', answer: { k: 'dec', v: h }, hint: ['Height is opposite the ' + a + '\u00B0 angle; the ladder is the hypotenuse.', 'sin ' + a + '\u00B0 = h/' + L + '.'], why: 'h = ' + L + ' \u00D7 sin ' + a + '\u00B0 \u2248 ' + h + ' ft.' }; }; T['word-shadow'] = function(rng){ var b = rpick(rng, [40, 45, 50, 60]), a = rpick(rng, [30, 35, 40]); var s = Math.round(b / trSpecial('tan', a) * 100) / 100; return { prompt: 'A ' + b + '-ft building casts a shadow when the sun\u2019s angle of elevation is ' + a + '\u00B0. How long is the shadow? (nearest hundredth, in feet)', answer: { k: 'dec', v: s }, hint: ['Building is opposite the elevation angle; shadow is adjacent.', 'tan ' + a + '\u00B0 = ' + b + '/s.'], why: 's = ' + b + ' \u00F7 tan ' + a + '\u00B0 \u2248 ' + s + ' ft.' }; }; T['hyp-from-py'] = function(rng){ /* combine: given opp/adj, find hyp via triple (manuscript try-it style) */ var t = rpick(rng, [[7,24,25],[6,8,10],[9,12,15]]); return { prompt: 'A right triangle has opposite = ' + t[0] + ' and adjacent = ' + t[1] + ' (relative to \u03B8). Find the hypotenuse.', answer: { k: 'dec', v: t[2] }, hint: ['First find tan \u03B8 if you want; but the hypotenuse needs Pythagoras.', 'c\u00B2 = ' + t[0] + '\u00B2 + ' + t[1] + '\u00B2.'], why: 'c = \u221A(' + t[0] + '\u00B2 + ' + t[1] + '\u00B2) = \u221A' + (t[2]*t[2]) + ' = ' + t[2] + ' (a ' + t[0] + '-' + t[1] + '-' + t[2] + ' triple).' }; };
var ORDER = ['ratio-sin','ratio-cos','ratio-tan','ratio-dec','side-30','side-45','side-60','angle-inv','angle-tan1','word-ladder','word-shadow','hyp-from-py'];
function buildQuestion(rng, templateId){ var q = T[templateId](rng); q.id = templateId; return q; }
return { buildQuestion: buildQuestion, templates: ORDER, trParse: trParse, trCheck: trCheck, trAnswerVal: trAnswerVal, trAnswerText: trAnswerText, trSimpRad: trSimpRad, trRadText: trRadText, trSqrtVal: trSqrtVal, trSpecial: trSpecial, mulberry: R }; }));
/* ===== page glue: wires the tested TR core to the DOM ===== */ (function () { 'use strict'; var TR = window.TrPractice; var TAGS = { 'ratio-sin': 'Ratios', 'ratio-cos': 'Ratios', 'ratio-tan': 'Ratios', 'ratio-dec': 'Ratios', 'side-30': 'Missing sides', 'side-45': 'Missing sides', 'side-60': 'Missing sides', 'angle-inv': 'Find the angle', 'angle-tan1': 'Find the angle', 'word-ladder': 'Word problems', 'word-shadow': 'Word problems', 'hyp-from-py': 'Mixed' };
var seedBase = (Date.now() % 2000000000) + 7; var order = TR.templates.slice(); var rng0 = TR.mulberry(seedBase); for (var i = order.length - 1; i > 0; i--){ var j = Math.floor(rng0() * (i + 1)); var t = order[i]; order[i] = order[j]; order[j] = t; } var session = { orderIdx: 0, perTemplate: {}, score: 0, streak: 0, problemNum: 0, current: null, wrongCount: 0, revealed: false, firstTry: true };
function el(id) { return document.getElementById(id); } var input = el('ansInput'), checkBtn = el('checkBtn'), anotherBtn = el('anotherBtn'), showSolBtn = el('showSolBtn'), promptTag = el('promptTag'), promptEl = el('promptEl'), fb = el('fb'), hintBox = el('hintBox'), whyBox = el('whyBox'), statsLine = el('statsLine');
function updateStats() { statsLine.innerHTML = 'Score ' + session.score + ' · Streak ' + session.streak + ' · Problem ' + session.problemNum + ''; } function clearPanels() { fb.className = 'fb'; fb.innerHTML = ''; hintBox.className = 'hintbox'; hintBox.innerHTML = ''; whyBox.className = 'whybox'; whyBox.innerHTML = ''; } function showFb(kind, html) { fb.className = 'fb show ' + kind; fb.innerHTML = html; }
function escHtml(str){ return String(str).replace(/[<>&"]/g, function (c){ return { '<': '<', '>': '>', '&': '&', '"': '"' }[c]; }); }
function newProblem() { var tid = order[session.orderIdx % order.length]; session.orderIdx++; var n = (session.perTemplate[tid] || 0) + 1; session.perTemplate[tid] = n; var rng = TR.mulberry((seedBase + session.orderIdx * 1013904223 + n * 97) >>> 0); var q = TR.buildQuestion(rng, tid); session.current = q; session.wrongCount = 0; session.revealed = false; session.firstTry = true; session.problemNum++; promptTag.textContent = TAGS[tid] || 'Trig ratios'; promptEl.textContent = q.prompt; input.value = ''; clearPanels(); showSolBtn.style.display = ''; updateStats(); input.focus(); }
function showWhy() { whyBox.className = 'whybox show'; whyBox.innerHTML = '
' + '
'; showSolBtn.style.display = 'none'; }
checkBtn.addEventListener('click', function () { var q = session.current; if (!q) return; var parsed = TR.trParse(input.value); if (parsed === null) { showFb('warn', 'Could not read that. Type a fraction like 5/13, a radical like 4sqrt3, a decimal like 9.40, or degrees like 61.93.'); return; } if (TR.trCheck(q.answer, parsed)) { session.streak++; var first = session.firstTry; if (first) session.score++; if (first) { showFb('ok', 'Correct! +1 point. The answer is ' + escHtml(TR.trAnswerText(q.answer)) + ''); } else if (session.revealed) { showFb('ok', 'That is right. You already saw the solution, so the score is unchanged. ' + 'Hit Try another for a fresh problem.'); } else { showFb('ok', 'Correct! The answer is ' + escHtml(TR.trAnswerText(q.answer)) + ' ' + 'Solved after a hint, so no point this time — but the streak grows.'); } showSolBtn.style.display = 'none'; updateStats(); return; } session.wrongCount++; session.firstTry = false; session.streak = 0; if (session.wrongCount === 1) { showFb('no', 'Not quite. Try again — here is a hint.'); hintBox.className = 'hintbox show'; hintBox.textContent = q.hint[0]; input.focus(); input.select(); } else { showFb('no', 'Not quite. Let us walk through it step by step.'); session.revealed = true; showWhy(); } updateStats(); }); anotherBtn.addEventListener('click', newProblem); showSolBtn.addEventListener('click', function () { session.revealed = true; showWhy(); updateStats(); }); input.addEventListener('keydown', function (e) { if (e.key === 'Enter') checkBtn.click(); }); newProblem(); })();