|
| 1 | +<!DOCTYPE html> |
| 2 | +<html lang="en"> |
| 3 | +<head> |
| 4 | +<meta charset="utf-8" /> |
| 5 | +<meta name="viewport" content="width=device-width,initial-scale=1" /> |
| 6 | +<title>Math Worksheet & Answer Sheet Generator</title> |
| 7 | +<style> |
| 8 | + :root { |
| 9 | + --bg: #0b0b0b; |
| 10 | + --panel: #121212; |
| 11 | + --fg: #f3f3f3; |
| 12 | + --muted: #bdbdbd; |
| 13 | + --accent: #4aa3ff; |
| 14 | + --border: #2a2a2a; |
| 15 | + } |
| 16 | + * { box-sizing: border-box; } |
| 17 | + body { |
| 18 | + margin: 0; background: var(--bg); color: var(--fg); |
| 19 | + font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial; |
| 20 | + } |
| 21 | + header { padding: 16px 20px 8px; } |
| 22 | + h1 { margin: 0 0 4px; font-size: 20px; } |
| 23 | + .sub { color: var(--muted); font-size: 13px; } |
| 24 | + |
| 25 | + #controls { |
| 26 | + padding: 12px 20px 18px; display: grid; gap: 10px; |
| 27 | + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); |
| 28 | + background: var(--panel); border-top: 1px solid var(--border); border-bottom: 1px solid var(--border); |
| 29 | + } |
| 30 | + .field { display: grid; gap: 6px; } |
| 31 | + .field label { font-size: 13px; color: var(--muted); } |
| 32 | + input[type="number"], select { |
| 33 | + width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--border); |
| 34 | + background: #161616; color: var(--fg); font-size: 14px; |
| 35 | + } |
| 36 | + .row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; } |
| 37 | + |
| 38 | + .buttons { |
| 39 | + display: flex; flex-wrap: wrap; gap: 10px; padding: 14px 20px 18px; |
| 40 | + align-items: center; |
| 41 | + } |
| 42 | + button { |
| 43 | + background: var(--accent); border: none; color: white; padding: 10px 14px; |
| 44 | + border-radius: 12px; font-size: 14px; cursor: pointer; font-weight: 600; |
| 45 | + } |
| 46 | + button.secondary { background: #2e2e2e; color: var(--fg); } |
| 47 | + |
| 48 | + .gallery { |
| 49 | + display: grid; gap: 16px; padding: 16px 20px 40px; |
| 50 | + grid-template-columns: 1fr; max-width: 1100px; margin: 0 auto; |
| 51 | + } |
| 52 | + .imgCard { |
| 53 | + background: var(--panel); border: 1px solid var(--border); border-radius: 14px; |
| 54 | + padding: 14px; |
| 55 | + } |
| 56 | + .imgCard h3 { margin: 0 0 8px; font-size: 16px; } |
| 57 | + .imgWrap { |
| 58 | + background: #fff; border-radius: 8px; overflow: hidden; display: flex; |
| 59 | + justify-content: center; align-items: center; |
| 60 | + } |
| 61 | + .imgWrap img { width: 100%; height: auto; display: block; } |
| 62 | + |
| 63 | + a.dl { |
| 64 | + text-decoration: none; background: #2e2e2e; color: var(--fg); |
| 65 | + padding: 8px 10px; border-radius: 10px; font-size: 13px; |
| 66 | + } |
| 67 | + |
| 68 | + @media (min-width: 1000px) { .gallery { grid-template-columns: 1fr 1fr; } } |
| 69 | +</style> |
| 70 | +</head> |
| 71 | +<body> |
| 72 | + <header> |
| 73 | + <h1>Math Worksheet & Answer Sheet Generator</h1> |
| 74 | + <div class="sub">Generate printable vertical-format problems. Exports to PDF.</div> |
| 75 | + </header> |
| 76 | + |
| 77 | + <section id="controls"> |
| 78 | + <div class="field"> |
| 79 | + <label>Operation</label> |
| 80 | + <select id="op"> |
| 81 | + <option value="+">Addition (+)</option> |
| 82 | + <option value="-">Subtraction (−)</option> |
| 83 | + <option value="×">Multiplication (×)</option> |
| 84 | + </select> |
| 85 | + </div> |
| 86 | + |
| 87 | + <div class="field"> |
| 88 | + <label>Columns</label> |
| 89 | + <input id="cols" type="number" min="1" max="6" value="3" /> |
| 90 | + </div> |
| 91 | + |
| 92 | + <div class="field"> |
| 93 | + <label>Problems per column</label> |
| 94 | + <input id="rows" type="number" min="1" max="30" value="10" /> |
| 95 | + </div> |
| 96 | + |
| 97 | + <div class="field"> |
| 98 | + <label>Font size (px)</label> |
| 99 | + <input id="fontSize" type="number" min="12" max="72" value="32" /> |
| 100 | + </div> |
| 101 | + |
| 102 | + <div class="field"> |
| 103 | + <label>Top number range (min / max)</label> |
| 104 | + <div class="row"> |
| 105 | + <input id="topMin" type="number" value="10" /> |
| 106 | + <input id="topMax" type="number" value="99" /> |
| 107 | + </div> |
| 108 | + </div> |
| 109 | + |
| 110 | + <div class="field"> |
| 111 | + <label>Bottom number range (min / max)</label> |
| 112 | + <div class="row"> |
| 113 | + <input id="botMin" type="number" value="10" /> |
| 114 | + <input id="botMax" type="number" value="99" /> |
| 115 | + </div> |
| 116 | + </div> |
| 117 | + |
| 118 | + <div class="field"> |
| 119 | + <label>Title (optional)</label> |
| 120 | + <input id="title" type="text" placeholder="e.g., Addition Practice" /> |
| 121 | + </div> |
| 122 | + |
| 123 | + <div class="field"> |
| 124 | + <label>Paper Size</label> |
| 125 | + <select id="paper"> |
| 126 | + <option value="letter" selected>US Letter (8.5×11)</option> |
| 127 | + <option value="a4">A4 (210×297 mm)</option> |
| 128 | + </select> |
| 129 | + </div> |
| 130 | + </section> |
| 131 | + |
| 132 | + <div class="buttons"> |
| 133 | + <button id="genBtn">Generate Worksheet</button> |
| 134 | + <button id="pdfWorksheet" class="secondary">Download PDF — Worksheet Only</button> |
| 135 | + <button id="pdfBoth" class="secondary">Download PDF — Worksheet + Answers</button> |
| 136 | + <span id="status" style="color:var(--muted); font-size:12px;"></span> |
| 137 | + </div> |
| 138 | + |
| 139 | + <section class="gallery"> |
| 140 | + <div class="imgCard"> |
| 141 | + <h3>Worksheet Preview</h3> |
| 142 | + <div class="imgWrap"><img id="sheetImg" alt="Worksheet preview" /></div> |
| 143 | + <div style="margin-top:8px; display:flex; gap:10px;"> |
| 144 | + <a id="dlSheetPng" class="dl" download="worksheet.png" href="#">Download PNG</a> |
| 145 | + </div> |
| 146 | + </div> |
| 147 | + |
| 148 | + <div class="imgCard"> |
| 149 | + <h3>Answer Sheet Preview</h3> |
| 150 | + <div class="imgWrap"><img id="answersImg" alt="Answer sheet preview" /></div> |
| 151 | + <div style="margin-top:8px; display:flex; gap:10px;"> |
| 152 | + <a id="dlAnswersPng" class="dl" download="answers.png" href="#">Download PNG</a> |
| 153 | + </div> |
| 154 | + </div> |
| 155 | + </section> |
| 156 | + |
| 157 | +<script src="https://cdn.jsdelivr.net/npm/jspdf@2.5.1/dist/jspdf.umd.min.js"></script> |
| 158 | +<script> |
| 159 | +(() => { |
| 160 | + const $ = s => document.querySelector(s); |
| 161 | + |
| 162 | + const opEl = $('#op'), colsEl = $('#cols'), rowsEl = $('#rows'), |
| 163 | + topMinEl = $('#topMin'), topMaxEl = $('#topMax'), |
| 164 | + botMinEl = $('#botMin'), botMaxEl = $('#botMax'), |
| 165 | + paperEl = $('#paper'), titleEl = $('#title'), fontSizeEl = $('#fontSize'); |
| 166 | + |
| 167 | + const genBtn = $('#genBtn'), pdfWorksheetBtn = $('#pdfWorksheet'), |
| 168 | + pdfBothBtn = $('#pdfBoth'), statusEl = $('#status'); |
| 169 | + |
| 170 | + const sheetImg = $('#sheetImg'), answersImg = $('#answersImg'); |
| 171 | + const dlSheetPng = $('#dlSheetPng'), dlAnswersPng = $('#dlAnswersPng'); |
| 172 | + |
| 173 | + const PAPER = { |
| 174 | + letter: { wPx:2550, hPx:3300, wPt:612, hPt:792, key:'letter' }, |
| 175 | + a4: { wPx:2480, hPx:3508, wPt:595.28, hPt:841.89, key:'a4' } |
| 176 | + }; |
| 177 | + |
| 178 | + let lastSheet = '', lastAns = '', lastProblems = []; |
| 179 | + |
| 180 | + genBtn.onclick = generateAll; |
| 181 | + pdfWorksheetBtn.onclick = () => downloadPDF(false); |
| 182 | + pdfBothBtn.onclick = () => downloadPDF(true); |
| 183 | + |
| 184 | + function readCfg() { |
| 185 | + return { |
| 186 | + op: opEl.value, |
| 187 | + cols: clamp(int(colsEl.value,3),1,6), |
| 188 | + rows: clamp(int(rowsEl.value,10),1,30), |
| 189 | + fontSize: clamp(int(fontSizeEl.value,32),12,72), |
| 190 | + topMin: int(topMinEl.value,10), |
| 191 | + topMax: int(topMaxEl.value,99), |
| 192 | + botMin: int(botMinEl.value,10), |
| 193 | + botMax: int(botMaxEl.value,99), |
| 194 | + title: titleEl.value.trim(), |
| 195 | + paper: PAPER[paperEl.value] || PAPER.letter, |
| 196 | + nonNegSub: true |
| 197 | + }; |
| 198 | + } |
| 199 | + |
| 200 | + function generateAll() { |
| 201 | + status(''); |
| 202 | + const cfg = readCfg(); |
| 203 | + if (cfg.topMin > cfg.topMax || cfg.botMin > cfg.botMax) { |
| 204 | + status('Min cannot exceed Max.', true); return; |
| 205 | + } |
| 206 | + const total = cfg.cols * cfg.rows; |
| 207 | + lastProblems = makeProblems(total, cfg); |
| 208 | + const {canvas:ws} = renderPage(lastProblems, cfg, false); |
| 209 | + const {canvas:as} = renderPage(lastProblems, cfg, true); |
| 210 | + |
| 211 | + lastSheet = ws.toDataURL('image/png'); |
| 212 | + lastAns = as.toDataURL('image/png'); |
| 213 | + |
| 214 | + sheetImg.src = lastSheet; |
| 215 | + answersImg.src = lastAns; |
| 216 | + dlSheetPng.href = lastSheet; |
| 217 | + dlAnswersPng.href = lastAns; |
| 218 | + } |
| 219 | + |
| 220 | + function makeProblems(total, cfg) { |
| 221 | + const {op, topMin, topMax, botMin, botMax, nonNegSub} = cfg; |
| 222 | + const seen = new Set(), arr = []; |
| 223 | + let attempts = 0, maxA = total*50; |
| 224 | + |
| 225 | + while(arr.length < total && attempts < maxA) { |
| 226 | + attempts++; |
| 227 | + let a = r(topMin, topMax), b = r(botMin, botMax); |
| 228 | + |
| 229 | + if(op==='-' && nonNegSub && a < b) [a,b] = [b,a]; |
| 230 | + |
| 231 | + let key; |
| 232 | + if(op==='+' || op==='×') { |
| 233 | + const lo=Math.min(a,b), hi=Math.max(a,b); |
| 234 | + key = `${op}:${lo}|${hi}`; |
| 235 | + } else key = `${op}:${a}|${b}`; |
| 236 | + |
| 237 | + if(seen.has(key)) continue; |
| 238 | + seen.add(key); |
| 239 | + |
| 240 | + arr.push({top:a, bot:b, op, ans:calc(a,b,op)}); |
| 241 | + } |
| 242 | + |
| 243 | + while(arr.length < total) { |
| 244 | + let a=r(topMin,topMax), b=r(botMin,botMax); |
| 245 | + if(op==='-' && nonNegSub && a<b) [a,b]=[b,a]; |
| 246 | + arr.push({top:a,bot:b,op,ans:calc(a,b,op)}); |
| 247 | + } |
| 248 | + return arr; |
| 249 | + } |
| 250 | + |
| 251 | + function calc(a,b,op){ return op==='+'?a+b:op==='-'?a-b:a*b; } |
| 252 | + |
| 253 | + function renderPage(probs,cfg,withAns){ |
| 254 | + const {paper,cols,rows,fontSize,title} = cfg; |
| 255 | + const W=paper.wPx, H=paper.hPx; |
| 256 | + const c=document.createElement('canvas'); c.width=W; c.height=H; |
| 257 | + const g=c.getContext('2d'); |
| 258 | + |
| 259 | + g.fillStyle='#fff'; g.fillRect(0,0,W,H); |
| 260 | + g.fillStyle='#000'; |
| 261 | + |
| 262 | + const margin = Math.round(W*0.06); |
| 263 | + const headerH = Math.round(H*0.11); |
| 264 | + |
| 265 | + g.font = `${Math.round(headerH*0.18)}px Arial`; |
| 266 | + let yLine = margin + Math.round(headerH*0.55); |
| 267 | + g.fillText('Name: ____________________', margin, yLine); |
| 268 | + const dt='Date: ____________________'; |
| 269 | + g.fillText(dt, W-margin-g.measureText(dt).width, yLine); |
| 270 | + |
| 271 | + g.font = `${Math.round(headerH*0.25)}px Arial`; |
| 272 | + const ttl = title || (withAns?'Answer Sheet':'Math Worksheet'); |
| 273 | + g.fillText(ttl,(W-g.measureText(ttl).width)/2, margin+Math.round(headerH*0.2)); |
| 274 | + |
| 275 | + const areaX=margin, areaY=margin+headerH, areaW=W-2*margin, areaH=H-areaY-margin; |
| 276 | + const colW=Math.floor(areaW/cols), rowH=Math.floor(areaH/rows); |
| 277 | + |
| 278 | + const numFS = fontSize; |
| 279 | + const symFS = fontSize; |
| 280 | + const ansFS = Math.floor(fontSize*0.8); |
| 281 | + const mono='Courier New, Courier, monospace'; |
| 282 | + |
| 283 | + let i=0; |
| 284 | + for(let cIdx=0;cIdx<cols;cIdx++){ |
| 285 | + for(let rIdx=0;rIdx<rows;rIdx++){ |
| 286 | + if(i>=probs.length) break; |
| 287 | + const p=probs[i++]; |
| 288 | + |
| 289 | + const x0=areaX + cIdx*colW; |
| 290 | + const y0=areaY + rIdx*rowH; |
| 291 | + |
| 292 | + // Compute max width of the problem block for centering |
| 293 | + g.font = `${numFS}px ${mono}`; |
| 294 | + const topW = g.measureText(String(p.top)).width; |
| 295 | + const botW = g.measureText(String(p.bot)).width; |
| 296 | + const maxW = Math.max(topW, botW) * 1.15; |
| 297 | + const rightX = x0 + (colW / 2) + (maxW / 2); // center alignment |
| 298 | + let currY = y0 + fontSize*1.1; // top aligned |
| 299 | + |
| 300 | + // Top number |
| 301 | + g.font = `${numFS}px ${mono}`; |
| 302 | + g.textAlign='right'; |
| 303 | + g.fillText(String(p.top), rightX, currY); |
| 304 | + |
| 305 | + // Bottom number width |
| 306 | + g.font = `${numFS}px ${mono}`; |
| 307 | + const botStr = String(p.bot); |
| 308 | + const botWidth = g.measureText(botStr).width; |
| 309 | + |
| 310 | + // Symbol just left of bottom number |
| 311 | + const botY = currY + fontSize*1.2; |
| 312 | + const gap = fontSize * 0.25; |
| 313 | + g.font = `${symFS}px ${mono}`; |
| 314 | + g.textAlign='left'; |
| 315 | + const symX = rightX - botWidth - gap - g.measureText(p.op).width; |
| 316 | + g.fillText(p.op, symX, botY); |
| 317 | + |
| 318 | + // Bottom number |
| 319 | + g.font = `${numFS}px ${mono}`; |
| 320 | + g.textAlign='right'; |
| 321 | + g.fillText(botStr, rightX, botY); |
| 322 | + |
| 323 | + // Line |
| 324 | + const lineY = botY + fontSize*0.25; |
| 325 | + const lineLeft = rightX - botWidth - gap; |
| 326 | + g.lineWidth = Math.max(2,Math.floor(fontSize*0.08)); |
| 327 | + g.beginPath(); |
| 328 | + g.moveTo(lineLeft, lineY); |
| 329 | + g.lineTo(rightX, lineY); |
| 330 | + g.stroke(); |
| 331 | + |
| 332 | + // Answer |
| 333 | + if(withAns){ |
| 334 | + g.font = `${ansFS}px ${mono}`; |
| 335 | + g.textAlign='right'; |
| 336 | + g.fillText(String(p.ans), rightX, lineY + ansFS*1.2); |
| 337 | + } |
| 338 | + } |
| 339 | + } |
| 340 | + return {canvas:c}; |
| 341 | + } |
| 342 | + |
| 343 | + function downloadPDF(includeAns){ |
| 344 | + if(!lastSheet){ status('Generate first.',true); return; } |
| 345 | + const cfg=readCfg(); const {jsPDF}=window.jspdf; |
| 346 | + const pdf=new jsPDF({orientation:'portrait',unit:'pt',format:cfg.paper.key}); |
| 347 | + const w=pdf.internal.pageSize.getWidth(), h=pdf.internal.pageSize.getHeight(); |
| 348 | + pdf.addImage(lastSheet,'PNG',0,0,w,h,'','FAST'); |
| 349 | + if(includeAns){ |
| 350 | + pdf.addPage(cfg.paper.key,'portrait'); |
| 351 | + pdf.addImage(lastAns,'PNG',0,0,w,h,'','FAST'); |
| 352 | + } |
| 353 | + const base=(cfg.title||'worksheet').toLowerCase().replace(/\s+/g,'-'); |
| 354 | + pdf.save(includeAns?`${base}-with-answers.pdf`:`${base}.pdf`); |
| 355 | + } |
| 356 | + |
| 357 | + function int(v,d){const n=parseInt(v,10);return Number.isFinite(n)?n:d;} |
| 358 | + function clamp(v,lo,hi){return Math.max(lo,Math.min(hi,v));} |
| 359 | + function r(a,b){return Math.floor(Math.random()*(b-a+1))+a;} |
| 360 | + function status(m,e){statusEl.textContent=m||'';statusEl.style.color=e?'#ff7b7b':'var(--muted)';} |
| 361 | + |
| 362 | + generateAll(); |
| 363 | +})(); |
| 364 | +</script> |
| 365 | +</body> |
| 366 | +</html> |
0 commit comments