Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 7 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Generate PDF worksheets of simple math problems in the browser.

Inspired by the worksheets my dad printed off for my siblings and I during the summer breaks. I'm recreating this purely by memory; all I remember is that it was a grid of math problems, printed on a dot matrix printer. Not sure what he created his sheets in (likely QBasic or VisualBasic as this was late 1990s), so my attempt is to recreate this as close as possible, using modern web technologies.
Inspired by the worksheets my dad printed off for my siblings and I during the summer breaks. I'm recreating this purely by memory; all I remember is that it was a grid of math problems, printed on a dot matrix printer. Not sure how he created those sheets (likely Basic/QBasic or VisualBasic as this was late 1990s), so my attempt is to recreate this as close as possible, using modern web technologies.

Built with HTML, CSS and Typescript and Vite in (more than) a few hours. I'm also using this as an exercise to learn newer web technologies (like TS and Vite). Is this overkill? Yes. Was it worth it? TBD (but likely yes).

Expand All @@ -15,21 +15,21 @@ For generating the problems, there's several configuration options:
- The two operand min/max values can be set independently
- For division problems, the second operand is regenerated if it's 0 to avoid divide by zero problems
- Set the number of problems to generate
- Currently I'm creating the PDF so that it fits 24 problems (4x6) per page, which may change
- Currently I'm creating the PDF so that it fits 20 problems (4x5) per page, which may change
- Set the operand order (highest or lowest first)
- Option to not have negative answers, which overrides operand order to highest first (helpful with subtraction problems)
- Option to use long division notation in displaying problems
- Option to make answers be integers only (for division problems)
- If unselected, then answers are rounded to 3 decimal places where needed
- Configuration options and resulting problems can be saved by using the same URL
- When the problems are generated, you can click the `Save Config` button to rewrite the current URL with parameters that specify which options are set and their respective values
- When returning to the site with the same URL, the problems generated will be the same, handled by the `seed` value in the form. A different seed will generate different problems
- When returning to the site with the same URL and parameters, the problems generated will be the same, handled by the `seed` value in the form. A different seed will generate different problems
- Throwback wallpaper!
- Wallpaper is randomly chosen on first visit and reloads
- The Select menu below the windows can change the background, saving the choice in localStorage for future visits


For generating the PDFs, there's a couple options to change:
When generating the PDFs, there's a couple options to change:

- Set the font used in the generated PDF
- If something fails, this will fall back to using the default Courier font
Expand All @@ -39,17 +39,13 @@ For generating the PDFs, there's a couple options to change:

## Status

It's at the MVP stage right now with basic functionality available.
The web app is at the MVP stage right now, with basic functionality available.

There are a couple features still in-progress:

- Allow multiple operators, instead of one or all

See [ISSUES](https://github.com/sphars/math-sheets/issues) for details about upcoming features.
See [ISSUES](https://github.com/sphars/math-sheets/issues) for details about potential features.

## Development

This site is built with Typescript and Vite. Only requirement is Node v22+. To run locally, clone the repo, then
This site is built with Typescript and Vite. Node v22+ is required. To run locally, clone the repo, then

```
cd math-sheets
Expand Down
63 changes: 32 additions & 31 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,10 @@
in the late 90s/early 00s (hence the Windows 98 theme). Thanks Dad!
</p>
<p>
This website is open-source and is licensed under the MIT license. Have any comments or bugs to report? File
an issue on <a href="https://github.com/sphars/math-sheets" target="_blank">GitHub</a>.
This website is open-source and is licensed under the MIT license.
<a href="https://github.com/sphars/math-sheets" target="_blank">Source code</a>. Have any comments or bugs
to report? File an issue on
<a href="https://github.com/sphars/math-sheets/issues" target="_blank">GitHub</a>.
</p>

<noscript>
Expand All @@ -62,7 +64,7 @@
<div class="main-wrapper">
<div class="window">
<div class="title-bar">
<div class="title-bar-text">Generator Options</div>
<div class="title-bar-text">Generate Problems</div>
<div class="title-bar-controls">
<button aria-label="Minimize"></button>
<button aria-label="Maximize"></button>
Expand All @@ -78,25 +80,21 @@
<fieldset>
<legend>Operator</legend>
<div class="field-row">
<input type="radio" name="operator" id="operator-add" value="+" checked />
<input type="checkbox" name="operator" id="operator-add" value="+" />
<label for="operator-add">Addition (&plus;)</label>
</div>
<div class="field-row">
<input type="radio" name="operator" id="operator-subtract" value="-" />
<input type="checkbox" name="operator" id="operator-subtract" value="-" />
<label for="operator-subtract">Subtraction (&minus;)</label>
</div>
<div class="field-row">
<input type="radio" name="operator" id="operator-multiply" value="*" />
<input type="checkbox" name="operator" id="operator-multiply" value="*" />
<label for="operator-multiply">Multiplication (&times;)</label>
</div>
<div class="field-row">
<input type="radio" name="operator" id="operator-divide" value="/" />
<input type="checkbox" name="operator" id="operator-divide" value="/" />
<label for="operator-divide">Division (&divide;)</label>
</div>
<div class="field-row">
<input type="radio" name="operator" id="operator-mix" value="mix" />
<label for="operator-mix">Mix</label>
</div>
</fieldset>

<fieldset class="operands-fields">
Expand Down Expand Up @@ -165,7 +163,7 @@
</fieldset>

<fieldset>
<legend>Options</legend>
<legend>Generator Options</legend>
<div class="field-row">
<label for="num-problems">Number of problems</label>
<input required type="number" id="num-problems" name="num-problems" min="1" max="1000" step="1" />
Expand All @@ -183,24 +181,41 @@

<div class="field-row">
<input type="checkbox" id="ints-only" name="ints-only" />
<label for="ints-only">Integer (whole number) answers (Division only)</label>
<label for="ints-only">Integer (whole number) answers</label>
</div>

<div class="field-row">
<input type="checkbox" id="long-div-notation" name="long-div-notation" />
<label for="long-div-notation">Long division notation (Division only)</label>
<label for="long-div-notation">Long division notation</label>
</div>

<div class="field-row">
<label for="seed">Seed</label>
<label for="seed">RNG Seed</label>
<input required type="number" id="seed" name="seed" min="0" max="100000000" step="1" value="" />
<button type="button" id="reseed">New Seed</button>
</div>
</fieldset>

<fieldset>
<legend>Print Options</legend>
<div class="field-row">
<label for="font-select">Font</label>
<select name="font-select" id="font-select"></select>
</div>
<div class="field-row">
<input type="checkbox" name="with-header" id="with-header" />
<label for="with-header">Add Header</label>
</div>
<div class="field-row">
<input type="checkbox" name="with-answers" id="with-answers" />
<label for="with-answers">Include Answers</label>
</div>
</fieldset>
</form>
<section class="field-row window-actions">
<button type="submit" id="form-submit" form="input-form">Generate</button>
<button id="save-config">Save Config</button>
<button type="reset" form="input-form">Reset</button>
<button type="submit" id="form-submit" form="input-form">Generate</button>
</section>
</div>
</div>
Expand All @@ -221,20 +236,6 @@
Due to differences in HTML vs PDF rendering, the PDF will not look quite the same as what you see below.
But it'll be pretty close.
</p>
<div class="fields">
<div class="field-row">
<label for="font-select">Font</label>
<select name="font-select" id="font-select"></select>
</div>
<div class="field-row">
<input type="checkbox" name="with-header" id="with-header" />
<label for="with-header">Show header</label>
</div>
<div class="field-row">
<input type="checkbox" name="with-answers" id="with-answers" />
<label for="with-answers">With answers</label>
</div>
</div>

<!-- TODO: rename to print-preview -->
<div class="text-area" id="page-wrapper">
Expand All @@ -249,7 +250,6 @@
</div>

<section class="field-row window-actions">
<button id="save-config">Save Config</button>
<button id="pdf-button" disabled>Print</button>
</section>
</div>
Expand Down Expand Up @@ -294,6 +294,7 @@
<a href="https://windowswallpaper.miraheze.org/wiki/Windows_95" target="_blank">Windows Wallpaper Wiki</a>
(and Microsoft, I guess) for the Windows 98 background tiles
</li>
<li><a href="https://fonts.google.com" target="_blank">Google Fonts</a> for the various fonts used</li>
</ul>

<section class="window-actions">
Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "math-sheets",
"version": "0.7.0",
"version": "0.8.0",
"private": true,
"description": "Generate PDF worksheets of simple math problems in the browser.",
"scripts": {
Expand Down
2 changes: 1 addition & 1 deletion src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export interface Problem {

export interface GeneratorOptions {
seed: number;
operator: string;
operators: string[];
upperMin: number;
upperMax: number;
lowerMin: number;
Expand Down
47 changes: 32 additions & 15 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ inputForm.addEventListener("submit", (e) => {

const options: GeneratorOptions = {
seed: inputData.get("seed") as unknown as number, // TODO: generate a new seed if not present
operator: inputData.get("operator") as string,
operators: inputData.getAll("operator") as string[],
upperMin: inputData.get("upper-min") as unknown as number,
upperMax: inputData.get("upper-max") as unknown as number,
lowerMin: inputData.get("lower-min") as unknown as number,
Expand Down Expand Up @@ -221,6 +221,18 @@ function setFormValues(options: GeneratorOptions) {
break;
}
}

// mark checkboxes checked for selected operators
if (key === "operators") {
const operatorCheckboxes = Array.from(
inputForm.elements.namedItem("operator") as RadioNodeList
) as HTMLInputElement[];
value.forEach((operator: string) => {
operatorCheckboxes.forEach((opCB) => {
if (opCB.value === operator) opCB.checked = true;
});
});
}
}
}

Expand Down Expand Up @@ -275,6 +287,11 @@ function updatePagesNote() {
function validateForm(options: GeneratorOptions): boolean {
let isFormValid = true;

if (!options.operators || options.operators.length < 1) {
console.error("At least one operator must be selected");
isFormValid = false;
}

// minimum operands have to be less than maximum
if (parseInt(options.upperMin.toString()) > parseInt(options.upperMax.toString())) {
console.error("Minimum upper operand values must be greater than maximum upper operand values");
Expand All @@ -286,7 +303,7 @@ function validateForm(options: GeneratorOptions): boolean {
}

// for division problems
if (options.operator === "/") {
if (options.operators.includes("/")) {
// integer answers are only possible if upper/dividend is greater than lower/divisor
if (options.intsOnly) {
if (parseInt(options.upperMax.toString()) < parseInt(options.lowerMin.toString())) {
Expand Down Expand Up @@ -329,7 +346,7 @@ function getAnswer(left: number, right: number, operator: string) {
return answer;
}

function getOperands(options: GeneratorOptions, rng: SeededRNG) {
function getOperands(options: GeneratorOptions, rng: SeededRNG, currOperator: string) {
const upperOperand = rng.nextInt(options.upperMin, options.upperMax);
const lowerOperand = rng.nextInt(options.lowerMin, options.lowerMax);

Expand All @@ -341,7 +358,7 @@ function getOperands(options: GeneratorOptions, rng: SeededRNG) {
}

// avoid divide by zero
if (options.operator === "/" && operands[1] === 0) {
if (currOperator === "/" && operands[1] === 0) {
operands[1] = rng.nextInt(1, options.lowerMax);
}

Expand All @@ -352,26 +369,26 @@ function generateMathProblems(options: GeneratorOptions): Problem[] {
const rng = new SeededRNG(options.seed);

for (let i = 0; i < options.numProblems; i++) {
// copy the form options
const optionsCopy = JSON.parse(JSON.stringify(options)) as GeneratorOptions;

// if operator = mix, need to randomize which operator to use
optionsCopy.operator = options.operator === "mix" ? operators[rng.nextInt(0, 3)] : options.operator;
// if operators has more than one selection, randomize which one to use
const currOperator =
options.operators.length > 1
? options.operators[rng.nextInt(0, options.operators.length - 1)]
: options.operators[0];

let operands = getOperands(optionsCopy, rng);
let answer = getAnswer(operands[0], operands[1], optionsCopy.operator);
let operands = getOperands(options, rng, currOperator);
let answer = getAnswer(operands[0], operands[1], currOperator);

if (options.intsOnly) {
do {
operands = getOperands(options, rng);
answer = getAnswer(operands[0], operands[1], optionsCopy.operator);
operands = getOperands(options, rng, currOperator);
answer = getAnswer(operands[0], operands[1], currOperator);
} while (!Number.isInteger(answer));
}

generatedProblems.push({
left: operands[0],
right: operands[1],
operator: optionsCopy.operator,
operator: currOperator,
answer: answer
});
}
Expand Down Expand Up @@ -571,7 +588,7 @@ function getOptionsFromURL(): GeneratorOptions {
// return a GeneratorOptions object with either the values from the params or default values
return {
seed: parseInt(params.get("seed") || generateRandomSeed().toString(), 10),
operator: params.get("operator") || "+",
operators: params.getAll("operator").length > 0 ? params.getAll("operator") : ["+"],
upperMin: parseInt(params.get("upper-min") || "0"),
upperMax: parseInt(params.get("lef-max") || "100"),
lowerMin: parseInt(params.get("lower-min") || "0"),
Expand Down