Skip to content

Commit 7b1a23a

Browse files
Create index.html
1 parent fe2679b commit 7b1a23a

1 file changed

Lines changed: 366 additions & 0 deletions

File tree

index.html

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
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

Comments
 (0)