├── demo.gif
├── num2math1.png
├── num2math2.png
├── .gitignore
├── README.md
├── src
├── utils
│ ├── constants.js
│ ├── mathHelpers.js
│ └── urlSharing.js
├── download-png.js
├── services
│ ├── equationService.js
│ └── decompositionService.js
├── generators
│ └── latexGenerators.js
├── script.js
└── styles.css
├── package.json
├── examples
└── test-example.js
└── index.html
/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enjeck/num2math/HEAD/demo.gif
--------------------------------------------------------------------------------
/num2math1.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enjeck/num2math/HEAD/num2math1.png
--------------------------------------------------------------------------------
/num2math2.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/enjeck/num2math/HEAD/num2math2.png
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | dist/
2 | package-lock.json
3 | node_modules/
4 | .cache/
5 | .parcel-cache/
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # num2math
2 |
3 | A simple web app that generates a complicated math expression that results in a number. If you wanna do this for some reason.
4 |
5 | Try it: https://enjeck.com/num2math/
6 |
7 | 
8 |
9 | ## Run the app
10 | To run the app locally:
11 |
12 | ```bash
13 | git clone https://github.com/enjeck/num2math.git
14 | cd num2math
15 | npm install
16 | npm run dev
17 | ```
18 |
--------------------------------------------------------------------------------
/src/utils/constants.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Application constants
3 | */
4 |
5 | export const INPUT_LIMITS = {
6 | MIN: 0,
7 | MAX: 1000,
8 | };
9 |
10 | export const RANDOM_MULTIPLIER_MAX = 5;
11 | export const RANDOM_COEFFICIENT_MAX = 10;
12 | export const RANDOM_OFFSET_MAX = 30;
13 | export const LARGE_NUMBER_THRESHOLD = 70;
14 | export const SMALL_NUMBER_THRESHOLD = 10;
15 | export const MEDIUM_NUMBER_THRESHOLD = 100;
16 |
17 | export const SIGNS = ['-', '+'];
18 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "homepage": "http://enjeck.github.io/num2math",
3 | "name": "num2math",
4 | "version": "2.0.0",
5 | "description": "A complicated math expression generator. What is a really complicated math equation that equals xxx? Input a solution and get a math expression. Equations can be copied.",
6 | "type": "module",
7 | "scripts": {
8 | "start": "parcel index.html --open",
9 | "build": "parcel build index.html --public-url ./",
10 | "predeploy": "npm run build",
11 | "deploy": "gh-pages -d dist"
12 | },
13 | "dependencies": {},
14 | "devDependencies": {
15 | "parcel": "^2.12.0",
16 | "gh-pages": "^6.1.1"
17 | },
18 | "keywords": [
19 | "math",
20 | "latex",
21 | "math equations",
22 | "equation generator"
23 | ]
24 | }
25 |
--------------------------------------------------------------------------------
/src/download-png.js:
--------------------------------------------------------------------------------
1 | /**
2 | * PNG download functionality for SVG equations
3 | */
4 |
5 | document.addEventListener('DOMContentLoaded', function () {
6 | const downloadBtn = document.getElementById('download-img');
7 | downloadBtn.addEventListener('click', downloadPNG);
8 |
9 | /**
10 | * Initiate download of blob
11 | * @param {string} filename - Name of the file to download
12 | * @param {Blob} blob - Blob data to download
13 | */
14 | function download(filename, blob) {
15 | if (window.navigator.msSaveOrOpenBlob) {
16 | window.navigator.msSaveBlob(blob, filename);
17 | } else {
18 | const elem = window.document.createElement('a');
19 | elem.href = window.URL.createObjectURL(blob);
20 | elem.download = filename;
21 | elem.click();
22 | // Clean up the URL object
23 | window.URL.revokeObjectURL(elem.href);
24 | }
25 | }
26 |
27 | /**
28 | * Download the rendered equation as PNG
29 | */
30 | function downloadPNG() {
31 | const svg = document.querySelector('svg');
32 | if (!svg) {
33 | console.error('No SVG found to download');
34 | return;
35 | }
36 |
37 | const number = document.getElementById('input').value;
38 |
39 | const w = parseInt(svg.getAttribute('width')) * 3;
40 | const h = parseInt(svg.getAttribute('height')) * 3;
41 |
42 | const clonedSvg = svg.cloneNode(true);
43 | clonedSvg.setAttribute('width', `${w}ex`);
44 | clonedSvg.setAttribute('height', `${h}ex`);
45 |
46 | const data = new XMLSerializer().serializeToString(clonedSvg);
47 |
48 | const canvas = document.createElement('canvas');
49 | canvg(canvas, data, {
50 | renderCallback: function () {
51 | canvas.toBlob(function (blob) {
52 | download(`complicated-equation-that-equals-${number}.png`, blob);
53 | });
54 | },
55 | });
56 | }
57 | });
58 |
59 |
--------------------------------------------------------------------------------
/src/utils/mathHelpers.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Mathematical utility functions
3 | */
4 |
5 | /**
6 | * Check if a number is odd
7 | * @param {number} n - The number to check
8 | * @returns {boolean} True if odd, false otherwise
9 | */
10 | export function isOdd(n) {
11 | return n % 2 !== 0;
12 | }
13 |
14 | /**
15 | * Check if a number is a perfect square
16 | * @param {number} n - The number to check
17 | * @returns {boolean} True if perfect square, false otherwise
18 | */
19 | export function isSquare(n) {
20 | return Number.isInteger(Math.sqrt(n));
21 | }
22 |
23 | /**
24 | * Check if a number is prime
25 | * @param {number} n - The number to check
26 | * @returns {boolean} True if prime, false otherwise
27 | */
28 | export function isPrime(n) {
29 | if (n <= 1) return false;
30 | if (n % 2 === 0 && n > 2) return false;
31 | const s = Math.sqrt(n);
32 | for (let i = 3; i <= s; i += 2) {
33 | if (n % i === 0) return false;
34 | }
35 | return true;
36 | }
37 |
38 | /**
39 | * Get all factors of a number
40 | * @param {number} n - The number to factorize
41 | * @returns {number[]} Array of factors
42 | */
43 | export function getFactors(n) {
44 | return [...Array(n + 1).keys()].filter((i) => n % i === 0);
45 | }
46 |
47 | /**
48 | * Check if a number can be formed using factorial
49 | * @param {number} n - The number to check
50 | * @returns {number|false} The factorial base or false
51 | */
52 | export function isFactorial(n) {
53 | const factorialMap = {
54 | 2: 2,
55 | 6: 3,
56 | 24: 4,
57 | 120: 5,
58 | 720: 6,
59 | };
60 | return factorialMap[n] || false;
61 | }
62 |
63 | /**
64 | * Generate a random integer between min and max (inclusive)
65 | * @param {number} min - Minimum value
66 | * @param {number} max - Maximum value
67 | * @returns {number} Random integer
68 | */
69 | export function randomInt(min, max) {
70 | return Math.floor(Math.random() * (max - min + 1)) + min;
71 | }
72 |
73 | /**
74 | * Select a random item from an array
75 | * @param {Array} arr - The array to select from
76 | * @returns {*} Random item from array
77 | */
78 | export function randomChoice(arr) {
79 | return arr[Math.floor(Math.random() * arr.length)];
80 | }
81 |
--------------------------------------------------------------------------------
/examples/test-example.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Example test file showing how to test the refactored modules
3 | * These are examples - actual testing framework (Jest, Vitest, etc.) required
4 | */
5 |
6 | import {
7 | isOdd,
8 | isSquare,
9 | isPrime,
10 | getFactors,
11 | isFactorial
12 | } from '../src/utils/mathHelpers.js';
13 |
14 | // Example tests for mathHelpers
15 | console.log('Testing mathHelpers...');
16 |
17 | // Test isOdd
18 | console.assert(isOdd(3) === true, 'isOdd(3) should be true');
19 | console.assert(isOdd(4) === false, 'isOdd(4) should be false');
20 |
21 | // Test isSquare
22 | console.assert(isSquare(9) === true, 'isSquare(9) should be true');
23 | console.assert(isSquare(10) === false, 'isSquare(10) should be false');
24 |
25 | // Test isPrime
26 | console.assert(isPrime(7) === true, 'isPrime(7) should be true');
27 | console.assert(isPrime(4) === false, 'isPrime(4) should be false');
28 | console.assert(isPrime(2) === true, 'isPrime(2) should be true');
29 | console.assert(isPrime(1) === false, 'isPrime(1) should be false');
30 |
31 | // Test getFactors
32 | const factors6 = getFactors(6);
33 | console.assert(
34 | JSON.stringify(factors6) === JSON.stringify([0, 1, 2, 3, 6]),
35 | 'getFactors(6) should return [0, 1, 2, 3, 6]'
36 | );
37 |
38 | // Test isFactorial
39 | console.assert(isFactorial(6) === 3, 'isFactorial(6) should return 3');
40 | console.assert(isFactorial(24) === 4, 'isFactorial(24) should return 4');
41 | console.assert(isFactorial(7) === false, 'isFactorial(7) should return false');
42 |
43 | console.log('All tests passed! ✓');
44 |
45 | /**
46 | * To run these tests with a proper testing framework:
47 | *
48 | * 1. Install a test runner:
49 | * npm install --save-dev vitest
50 | *
51 | * 2. Update package.json:
52 | * "scripts": {
53 | * "test": "vitest"
54 | * }
55 | *
56 | * 3. Rename this file to mathHelpers.test.js
57 | *
58 | * 4. Rewrite tests using framework syntax:
59 | *
60 | * import { describe, it, expect } from 'vitest';
61 | *
62 | * describe('mathHelpers', () => {
63 | * it('should identify odd numbers', () => {
64 | * expect(isOdd(3)).toBe(true);
65 | * expect(isOdd(4)).toBe(false);
66 | * });
67 | * });
68 | */
69 |
--------------------------------------------------------------------------------
/src/utils/urlSharing.js:
--------------------------------------------------------------------------------
1 | /**
2 | * URL Sharing utilities - encode/decode application state for sharing
3 | */
4 |
5 | /**
6 | * Encodes the current state (number and config) into a URL-safe string
7 | * @param {number} number - The input number
8 | * @param {Object} config - The checkbox configuration
9 | * @returns {string} Base64-encoded state
10 | */
11 | export function encodeState(number, config) {
12 | const state = {
13 | n: number, // Use short keys to minimize URL length
14 | g: config.gammaFunction,
15 | e: config.eulersIdentity,
16 | le: config.limitExponential,
17 | lp: config.limitPolynomial,
18 | t: config.trig,
19 | gs: config.geometricSeries,
20 | };
21 |
22 | const json = JSON.stringify(state);
23 | // Convert to base64 and make URL-safe
24 | return btoa(json)
25 | .replace(/\+/g, '-')
26 | .replace(/\//g, '_')
27 | .replace(/=+$/, ''); // Remove padding
28 | }
29 |
30 | /**
31 | * Decodes a URL-safe string back into state
32 | * @param {string} encoded - The encoded state string
33 | * @returns {Object|null} The decoded state or null if invalid
34 | */
35 | export function decodeState(encoded) {
36 | try {
37 | // Restore base64 format
38 | let base64 = encoded
39 | .replace(/-/g, '+')
40 | .replace(/_/g, '/');
41 |
42 | // Add padding if needed
43 | while (base64.length % 4) {
44 | base64 += '=';
45 | }
46 |
47 | const json = atob(base64);
48 | const state = JSON.parse(json);
49 |
50 | // Expand to full config object
51 | return {
52 | number: state.n,
53 | config: {
54 | gammaFunction: state.g ?? true,
55 | eulersIdentity: state.e ?? true,
56 | limitExponential: state.le ?? true,
57 | limitPolynomial: state.lp ?? true,
58 | trig: state.t ?? true,
59 | geometricSeries: state.gs ?? true,
60 | }
61 | };
62 | } catch (error) {
63 | console.error('Failed to decode state:', error);
64 | return null;
65 | }
66 | }
67 |
68 | /**
69 | * Gets the current state from URL parameters
70 | * @returns {Object|null} The decoded state or null if no valid state in URL
71 | */
72 | export function getStateFromURL() {
73 | const params = new URLSearchParams(window.location.search);
74 | const encoded = params.get('s');
75 |
76 | if (!encoded) {
77 | return null;
78 | }
79 |
80 | return decodeState(encoded);
81 | }
82 |
83 | /**
84 | * Generates a shareable URL with the current state
85 | * @param {number} number - The input number
86 | * @param {Object} config - The checkbox configuration
87 | * @returns {string} The full shareable URL
88 | */
89 | export function generateShareableURL(number, config) {
90 | const encoded = encodeState(number, config);
91 | const url = new URL(window.location.href);
92 | url.search = `?s=${encoded}`;
93 | return url.toString();
94 | }
95 |
96 | /**
97 | * Updates the current URL with the state (without page reload)
98 | * @param {number} number - The input number
99 | * @param {Object} config - The checkbox configuration
100 | */
101 | export function updateURL(number, config) {
102 | const encoded = encodeState(number, config);
103 | const url = new URL(window.location.href);
104 | url.search = `?s=${encoded}`;
105 | window.history.replaceState({}, '', url);
106 | }
107 |
--------------------------------------------------------------------------------
/src/services/equationService.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main equation generation service
3 | * Coordinates generator functions to create complex mathematical expressions
4 | */
5 |
6 | import {
7 | factorial,
8 | limDiffTwoSquares,
9 | limitNaturalLog,
10 | limitExponential,
11 | limitPolynomial,
12 | eulersIdentity,
13 | infiniteGeometricSeries,
14 | trigIdentity,
15 | } from '../generators/latexGenerators.js';
16 | import { decompose } from './decompositionService.js';
17 | import { randomChoice } from '../utils/mathHelpers.js';
18 |
19 | /**
20 | * Configuration for equation generation
21 | * @typedef {Object} EquationConfig
22 | * @property {boolean} gammaFunction - Use Gamma function for factorials
23 | * @property {boolean} eulersIdentity - Use Euler's identity
24 | * @property {boolean} limitExponential - Use exponential limits
25 | * @property {boolean} limitPolynomial - Use polynomial limits
26 | * @property {boolean} trig - Use trigonometric identities
27 | * @property {boolean} geometricSeries - Use geometric series
28 | */
29 |
30 | /**
31 | * Generate a complex mathematical expression that equals the target number
32 | * @param {number} number - Target number (0-1000)
33 | * @param {EquationConfig} config - Configuration options
34 | * @returns {string} LaTeX expression
35 | */
36 | export function generateEquation(number, config) {
37 | // Build list of available generator functions based on config
38 | const possibleOptions = [];
39 |
40 | if (config.eulersIdentity) {
41 | possibleOptions.push((n) => eulersIdentity(n, config.gammaFunction));
42 | }
43 |
44 | if (config.limitExponential) {
45 | possibleOptions.push(limitNaturalLog);
46 | possibleOptions.push(limitExponential);
47 | }
48 |
49 | if (config.limitPolynomial) {
50 | possibleOptions.push((n) => limDiffTwoSquares(n, config.gammaFunction));
51 | possibleOptions.push((n) => limitPolynomial(n, config.gammaFunction));
52 | }
53 |
54 | if (config.geometricSeries) {
55 | possibleOptions.push((n) => infiniteGeometricSeries(n, config.gammaFunction));
56 | }
57 |
58 | // Special handling for trig identity - needs to wrap other generators
59 | if (config.trig) {
60 | const originalOptions = [...possibleOptions];
61 |
62 | // Create trig-wrapped generator
63 | const trigGenerator = (n) => {
64 | // Filter out some options for trig wrapping
65 | const optionsForTrig = originalOptions.filter(
66 | (opt) => opt !== limitNaturalLog && opt !== trigGenerator
67 | );
68 |
69 | let randOption;
70 | if (optionsForTrig.length > 0) {
71 | randOption = (num) => randomChoice(optionsForTrig)(num);
72 | } else {
73 | randOption = sameNumber;
74 | }
75 |
76 | return trigIdentity(n, randOption);
77 | };
78 |
79 | possibleOptions.push(trigGenerator);
80 | }
81 |
82 | // Fallback function if all options are disabled
83 | const sameNumber = (n) => n;
84 |
85 | // Generate the expression
86 | return decompose(number, possibleOptions, sameNumber);
87 | }
88 |
89 | /**
90 | * Validate input number
91 | * @param {*} value - Input value to validate
92 | * @returns {{valid: boolean, error?: string}} Validation result
93 | */
94 | export function validateInput(value) {
95 | const number = Number(value);
96 |
97 | if (isNaN(number)) {
98 | return { valid: false, error: 'Please enter a valid number' };
99 | }
100 |
101 | if (value === '' || value === null || value === undefined) {
102 | return { valid: false, error: 'Please enter a number' };
103 | }
104 |
105 | if (number < 0 || number > 1000) {
106 | return { valid: false, error: 'Number must be between 0 and 1000' };
107 | }
108 |
109 | if (!Number.isInteger(number)) {
110 | return { valid: false, error: 'Please enter an integer' };
111 | }
112 |
113 | return { valid: true };
114 | }
115 |
--------------------------------------------------------------------------------
/src/generators/latexGenerators.js:
--------------------------------------------------------------------------------
1 | /**
2 | * LaTeX expression generators for various mathematical concepts
3 | */
4 |
5 | import { isFactorial, randomInt } from '../utils/mathHelpers.js';
6 | import { SIGNS } from '../utils/constants.js';
7 |
8 | /**
9 | * Generate factorial notation using Gamma function or Pi product
10 | * @param {number} n - The factorial base
11 | * @param {boolean} useGammaFunction - Whether to use Gamma function
12 | * @returns {string} LaTeX string
13 | */
14 | export function factorial(n, useGammaFunction) {
15 | if (Math.random() < 0.5 && useGammaFunction) {
16 | return `{\\Gamma (${n + 1})}`;
17 | }
18 | return `{\\prod_{k=1}^{${n}} k}`;
19 | }
20 |
21 | /**
22 | * Generate limit using difference of two squares
23 | * @param {number} n - Target number
24 | * @param {boolean} useGammaFunction - Whether gamma function is enabled
25 | * @returns {string} LaTeX string
26 | */
27 | export function limDiffTwoSquares(n, useGammaFunction) {
28 | const fac = isFactorial(n);
29 | if (fac && Math.random() < 0.5) {
30 | return factorial(fac, useGammaFunction);
31 | }
32 |
33 | const r = randomInt(1, 10);
34 | if (Math.random() < 0.5) {
35 | return `{\\lim_{x \\to ${n - r}} {{x^2 - ${r ** 2}} \\over {x - ${r}}}}`;
36 | }
37 | return `{\\lim_{x \\to ${n + r}} {{x^2 - ${r ** 2}} \\over {x + ${r}}}}`;
38 | }
39 |
40 | /**
41 | * Generate natural logarithm limit
42 | * @param {number} n - Target number
43 | * @returns {string} LaTeX string
44 | */
45 | export function limitNaturalLog(n) {
46 | if (n === 0) {
47 | return `{\\lim_{x \\to \\infty}{ \\ln(x) \\over {x} }}`;
48 | }
49 | if (n === 1) {
50 | return `{\\lim_{x \\to 1} { {\\ln(x)} \\over {x - 1} }}`;
51 | }
52 | return `{\\lim_{x \\to 0}{ {-\\ln(1 + ${n}(e^{-x} - 1))} \\over {x} }}`;
53 | }
54 |
55 | /**
56 | * Generate exponential limit
57 | * @param {number} n - Target number
58 | * @returns {string} LaTeX string
59 | */
60 | export function limitExponential(n) {
61 | if (n === 0) {
62 | return `{\\lim_{x \\to \\infty}{xe^{-x}}}`;
63 | }
64 | if (n === 1) {
65 | return `{\\lim_{x \\to 0}{ {e^x - 1} \\over {x} }}`;
66 | }
67 | return `{\\lim_{x \\to 0}{ {e^{${n}x} - 1} \\over {x} }}`;
68 | }
69 |
70 | /**
71 | * Generate polynomial limit
72 | * @param {number} n - Target number
73 | * @param {boolean} useGammaFunction - Whether gamma function is enabled
74 | * @returns {string} LaTeX string
75 | */
76 | export function limitPolynomial(n, useGammaFunction) {
77 | const fac = isFactorial(n);
78 | if (fac && Math.random() < 0.5) {
79 | return factorial(fac, useGammaFunction);
80 | }
81 |
82 | if (n === 0) {
83 | const r = randomInt(0, 19);
84 | return `{\\lim_{x \\to \\infty}{${r}x^{-1}}}`;
85 | }
86 |
87 | if (n === 1) {
88 | return `{\\lim_{x \\to \\infty}{x^{1/x}}}`;
89 | }
90 |
91 | const m = randomInt(1, 5);
92 | const highestPower = randomInt(2, 4);
93 | const numeratorLength = randomInt(1, highestPower - 1);
94 | const denominatorLength = randomInt(1, highestPower - 1);
95 |
96 | let numerator = `${m * n}x^{${highestPower}} `;
97 | let denominator = `${m}x^{${highestPower}} `;
98 |
99 | for (let i = numeratorLength; i > 0; i--) {
100 | const coef = randomInt(2, 11);
101 | const power = i < 2 ? '' : `^{${i}}`;
102 | numerator += `${SIGNS[Math.floor(Math.random() * 2)]} ${coef}x${power} `;
103 | }
104 |
105 | for (let i = denominatorLength; i > 0; i--) {
106 | const coef = randomInt(2, 11);
107 | const power = i < 2 ? '' : `^{${i}}`;
108 | denominator += `${SIGNS[Math.floor(Math.random() * 2)]} ${coef}x${power} `;
109 | }
110 |
111 | return `{\\lim_{x \\to \\infty}{{ ${numerator} } \\over {{ ${denominator} }}}}`;
112 | }
113 |
114 | /**
115 | * Generate Euler's identity expression
116 | * @param {number} n - Target number
117 | * @param {boolean} useGammaFunction - Whether gamma function is enabled
118 | * @returns {string} LaTeX string
119 | */
120 | export function eulersIdentity(n, useGammaFunction) {
121 | const fac = isFactorial(n);
122 | if (fac && Math.random() < 0.5) {
123 | return factorial(fac, useGammaFunction);
124 | }
125 |
126 | if (n !== 0) {
127 | return `{-${n}e^{\\pi i}}`;
128 | }
129 | return `{(e^{\\pi i} + 1)}`;
130 | }
131 |
132 | /**
133 | * Generate infinite geometric series
134 | * @param {number} n - Target number
135 | * @param {boolean} useGammaFunction - Whether gamma function is enabled
136 | * @returns {string} LaTeX string
137 | */
138 | export function infiniteGeometricSeries(n, useGammaFunction) {
139 | const fac = isFactorial(n);
140 | if (fac && Math.random() < 0.5) {
141 | return factorial(fac, useGammaFunction);
142 | }
143 |
144 | if (n === 0) {
145 | const r = randomInt(3, 12);
146 | return `{\\sum\\limits_{k=0}^{${r - 1}} {\\sin \\left({ {2 \\pi k} \\over {${r}} } \\right)}}`;
147 | }
148 |
149 | if (n === 1) {
150 | return `{\\lim_{\\epsilon \\to 0}{ \\epsilon \\zeta(1 + \\epsilon) }}`;
151 | }
152 |
153 | return `{\\sum\\limits_{k=0}^\\infty {\\left({${n - 1} \\over {${n}}}\\right)^{k}}}`;
154 | }
155 |
156 | /**
157 | * Generate trigonometric identity expression
158 | * @param {number} n - Target number
159 | * @param {Function} randOption - Random generator function
160 | * @returns {string} LaTeX string
161 | */
162 | export function trigIdentity(n, randOption) {
163 | if (n > 0) {
164 | const randomValue = Math.random();
165 | if (randomValue < 0.25) {
166 | return `\\left({${randOption(n)} \\over {(\\cos^2x + \\sin^2x)}}\\right)`;
167 | }
168 | if (randomValue < 0.5) {
169 | return `\\left({${randOption(n)} \\times (\\cos^2x + \\sin^2x)}\\right)`;
170 | }
171 | return `\\left({${randOption(n + 1)} - (\\cos^2x + \\sin^2x)}\\right)`;
172 | }
173 | return `\\left({${randOption(n + 1)} - (\\cos^2x + \\sin^2x)}\\right)`;
174 | }
175 |
--------------------------------------------------------------------------------
/src/script.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Main application script - handles UI interactions and MathJax rendering
3 | */
4 |
5 | import { generateEquation, validateInput } from './services/equationService.js';
6 | import { getStateFromURL, updateURL, generateShareableURL } from './utils/urlSharing.js';
7 |
8 | document.addEventListener('DOMContentLoaded', function () {
9 | const form = document.getElementById('form');
10 | const displayCheckbox = document.getElementById('display');
11 | const closeButton = document.querySelector('.close');
12 | const inputField = document.getElementById('input');
13 | const outputDiv = document.getElementById('output');
14 | const renderButton = document.getElementById('render');
15 | const downloadBtn = document.getElementById('download-img');
16 | const shareBtn = document.getElementById('share-btn');
17 |
18 | form.addEventListener('submit', handleFormSubmit);
19 | displayCheckbox.addEventListener('change', convert);
20 | closeButton.addEventListener('click', () => {
21 | document.getElementById('mobile-notice').style.display = 'none';
22 | });
23 | shareBtn.addEventListener('click', handleShare);
24 |
25 | loadStateFromURL();
26 |
27 | function loadStateFromURL() {
28 | const state = getStateFromURL();
29 | if (state) {
30 | inputField.value = state.number;
31 |
32 | document.getElementById('gamma-function').checked = state.config.gammaFunction;
33 | document.getElementById('eulers-identity').checked = state.config.eulersIdentity;
34 | document.getElementById('limits-exponential').checked = state.config.limitExponential;
35 | document.getElementById('limits-polynomial').checked = state.config.limitPolynomial;
36 | document.getElementById('trig').checked = state.config.trig;
37 | document.getElementById('geometric-series').checked = state.config.geometricSeries;
38 |
39 | if (window.MathJax && window.MathJax.startup) {
40 | MathJax.startup.promise.then(() => {
41 | convert();
42 | });
43 | } else {
44 | window.addEventListener('load', () => {
45 | if (window.MathJax && window.MathJax.startup) {
46 | MathJax.startup.promise.then(() => {
47 | convert();
48 | });
49 | }
50 | });
51 | }
52 | }
53 | }
54 |
55 | function handleFormSubmit(event) {
56 | event.preventDefault();
57 | convert();
58 | }
59 |
60 | function convert() {
61 | const number = inputField.value;
62 |
63 | const validation = validateInput(number);
64 | if (!validation.valid) {
65 | showError(validation.error);
66 | return;
67 | }
68 |
69 | const config = {
70 | gammaFunction: document.getElementById('gamma-function').checked,
71 | eulersIdentity: document.getElementById('eulers-identity').checked,
72 | limitExponential: document.getElementById('limits-exponential').checked,
73 | limitPolynomial: document.getElementById('limits-polynomial').checked,
74 | trig: document.getElementById('trig').checked,
75 | geometricSeries: document.getElementById('geometric-series').checked,
76 | };
77 |
78 | const input = generateEquation(Number(number), config);
79 |
80 | renderEquation(input);
81 |
82 | updateURL(Number(number), config);
83 | }
84 |
85 | function handleShare() {
86 | const number = inputField.value;
87 | const validation = validateInput(number);
88 |
89 | if (!validation.valid) {
90 | showError('Please generate an equation first before sharing!');
91 | return;
92 | }
93 |
94 | const config = {
95 | gammaFunction: document.getElementById('gamma-function').checked,
96 | eulersIdentity: document.getElementById('eulers-identity').checked,
97 | limitExponential: document.getElementById('limits-exponential').checked,
98 | limitPolynomial: document.getElementById('limits-polynomial').checked,
99 | trig: document.getElementById('trig').checked,
100 | geometricSeries: document.getElementById('geometric-series').checked,
101 | };
102 |
103 | const shareableURL = generateShareableURL(Number(number), config);
104 |
105 | // Try to use native share API if available (mobile devices)
106 | if (navigator.share) {
107 | navigator.share({
108 | title: `num2math - Equation for ${number}`,
109 | text: `Check out this complicated math equation that equals ${number}!`,
110 | url: shareableURL,
111 | }).catch((error) => {
112 | if (error.name !== 'AbortError') {
113 | copyToClipboard(shareableURL);
114 | }
115 | });
116 | } else {
117 | copyToClipboard(shareableURL);
118 | }
119 | }
120 |
121 | function copyToClipboard(text) {
122 | navigator.clipboard.writeText(text).then(() => {
123 | const originalText = shareBtn.textContent;
124 | shareBtn.textContent = '✓ Link copied!';
125 | shareBtn.style.backgroundColor = '#28a745';
126 |
127 | setTimeout(() => {
128 | shareBtn.textContent = originalText;
129 | shareBtn.style.backgroundColor = '';
130 | }, 2000);
131 | }).catch(() => {
132 | const textarea = document.createElement('textarea');
133 | textarea.value = text;
134 | textarea.style.position = 'fixed';
135 | textarea.style.opacity = '0';
136 | document.body.appendChild(textarea);
137 | textarea.select();
138 | document.execCommand('copy');
139 | document.body.removeChild(textarea);
140 |
141 | const originalText = shareBtn.textContent;
142 | shareBtn.textContent = '✓ Link copied!';
143 | setTimeout(() => {
144 | shareBtn.textContent = originalText;
145 | }, 2000);
146 | });
147 | }
148 |
149 | function showError(message) {
150 | outputDiv.innerHTML = `
${message}
`;
151 | }
152 |
153 | function renderEquation(latexExpression) {
154 | renderButton.disabled = displayCheckbox.disabled = true;
155 |
156 | outputDiv.innerHTML = '';
157 |
158 | MathJax.texReset();
159 | const options = MathJax.getMetricsFor(outputDiv);
160 | options.display = displayCheckbox.checked;
161 | MathJax.tex2svgPromise(latexExpression, options)
162 | .then((node) => {
163 | outputDiv.appendChild(node);
164 | MathJax.startup.document.clear();
165 | MathJax.startup.document.updateDocument();
166 | downloadBtn.style.display = 'block';
167 | shareBtn.style.display = 'inline-block';
168 | })
169 | .catch((err) => {
170 | const pre = document.createElement('pre');
171 | pre.textContent = err.message;
172 | outputDiv.appendChild(pre);
173 | })
174 | .finally(() => {
175 | renderButton.disabled = displayCheckbox.disabled = false;
176 | });
177 | }
178 | });
179 |
180 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | * {
2 | box-sizing: border-box;
3 | }
4 |
5 | body {
6 | margin: 0;
7 | padding: 0;
8 | font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
9 | background: #fff;
10 | min-height: 100vh;
11 | color: #000;
12 | line-height: 1.5;
13 | }
14 |
15 | .menu {
16 | width: 100%;
17 | text-align: right;
18 | padding: 20px 40px;
19 | background: #fff;
20 | border-bottom: 1px solid #e5e5e5;
21 | }
22 |
23 | .menu a {
24 | padding: 8px 16px;
25 | font-weight: 400;
26 | color: #000;
27 | text-decoration: none;
28 | transition: opacity 0.2s ease;
29 | font-size: 14px;
30 | }
31 |
32 | .menu a:hover {
33 | opacity: 0.6;
34 | }
35 |
36 | #frame {
37 | max-width: 720px;
38 | margin: 0 auto;
39 | padding: 80px 24px;
40 | }
41 |
42 | .intro {
43 | text-align: center;
44 | max-width: 540px;
45 | margin: 0 auto 80px;
46 | }
47 |
48 | .intro h1 {
49 | font-size: 56px;
50 | font-weight: 600;
51 | color: #000;
52 | margin: 0 0 24px;
53 | letter-spacing: -0.03em;
54 | line-height: 1.1;
55 | }
56 |
57 | .intro h2 {
58 | font-size: 18px;
59 | font-weight: 400;
60 | color: #666;
61 | line-height: 1.6;
62 | margin: 0;
63 | }
64 |
65 | form {
66 | background: #fff;
67 | border: 1px solid #e5e5e5;
68 | padding: 32px;
69 | margin-bottom: 24px;
70 | }
71 |
72 | fieldset {
73 | border: none;
74 | padding: 0;
75 | margin: 0;
76 | }
77 |
78 | legend {
79 | display: none;
80 | }
81 |
82 | label[for="input"] {
83 | display: block;
84 | font-size: 14px;
85 | font-weight: 500;
86 | color: #000;
87 | margin-bottom: 8px;
88 | }
89 |
90 | #input {
91 | border: 1px solid #d4d4d4;
92 | padding: 12px 16px;
93 | width: 100%;
94 | font-size: 16px;
95 | font-family: inherit;
96 | transition: border-color 0.15s ease;
97 | background: #fff;
98 | color: #000;
99 | }
100 |
101 | #input:hover {
102 | border-color: #a3a3a3;
103 | }
104 |
105 | #input:focus {
106 | outline: none;
107 | border-color: #000;
108 | }
109 |
110 | .lr {
111 | display: flex;
112 | justify-content: space-between;
113 | align-items: center;
114 | margin-top: 20px;
115 | }
116 |
117 | .left {
118 | display: flex;
119 | align-items: center;
120 | gap: 8px;
121 | }
122 |
123 | .left input[type="checkbox"] {
124 | width: 16px;
125 | height: 16px;
126 | cursor: pointer;
127 | accent-color: #000;
128 | }
129 |
130 | .left label {
131 | font-size: 14px;
132 | color: #666;
133 | cursor: pointer;
134 | user-select: none;
135 | }
136 |
137 | #render {
138 | padding: 12px 24px;
139 | font-size: 14px;
140 | font-weight: 500;
141 | border: 1px solid #000;
142 | background: #000;
143 | color: #fff;
144 | cursor: pointer;
145 | transition: all 0.15s ease;
146 | }
147 |
148 | #render:hover {
149 | background: #333;
150 | border-color: #333;
151 | }
152 |
153 | #render:active {
154 | background: #000;
155 | border-color: #000;
156 | }
157 |
158 | .options {
159 | background: #fff;
160 | border: 1px solid #e5e5e5;
161 | padding: 32px;
162 | display: grid;
163 | grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
164 | gap: 16px;
165 | margin-bottom: 24px;
166 | }
167 |
168 | .options > div {
169 | display: flex;
170 | align-items: center;
171 | gap: 8px;
172 | }
173 |
174 | .options input[type="checkbox"] {
175 | width: 16px;
176 | height: 16px;
177 | cursor: pointer;
178 | accent-color: #000;
179 | }
180 |
181 | .options label {
182 | font-size: 14px;
183 | color: #666;
184 | cursor: pointer;
185 | user-select: none;
186 | }
187 |
188 | #output {
189 | background: #fff;
190 | padding: 32px;
191 | border: 1px solid #e5e5e5;
192 | min-height: 120px;
193 | font-size: 18px;
194 | overflow-x: auto;
195 | display: none;
196 | }
197 |
198 | #output:not(:empty) {
199 | display: block;
200 | }
201 |
202 | #output > pre {
203 | margin: 0;
204 | }
205 |
206 | .action-buttons {
207 | margin-top: 16px;
208 | display: flex;
209 | gap: 12px;
210 | flex-wrap: wrap;
211 | }
212 |
213 | #download-img,
214 | #share-btn {
215 | padding: 12px 24px;
216 | font-size: 14px;
217 | font-weight: 500;
218 | border: 1px solid #000;
219 | background-color: transparent;
220 | color: #000;
221 | cursor: pointer;
222 | transition: all 0.15s ease;
223 | font-family: inherit;
224 | }
225 |
226 | #download-img {
227 | display: none;
228 | }
229 |
230 | #share-btn {
231 | display: none;
232 | }
233 |
234 | #download-img:hover,
235 | #share-btn:hover {
236 | background-color: #000;
237 | color: #fff;
238 | }
239 |
240 | #mobile-notice {
241 | padding: 24px;
242 | margin: 0 0 32px;
243 | background: #fef2f2;
244 | border: 1px solid #fca5a5;
245 | display: none;
246 | position: relative;
247 | }
248 |
249 | #mobile-notice h3 {
250 | margin-top: 0;
251 | color: #991b1b;
252 | font-size: 18px;
253 | font-weight: 600;
254 | }
255 |
256 | #mobile-notice p {
257 | color: #7f1d1d;
258 | line-height: 1.6;
259 | margin-bottom: 0;
260 | font-size: 14px;
261 | }
262 |
263 | .close {
264 | position: absolute;
265 | top: 16px;
266 | right: 16px;
267 | width: 24px;
268 | height: 24px;
269 | display: flex;
270 | align-items: center;
271 | justify-content: center;
272 | background: transparent;
273 | color: #991b1b;
274 | cursor: pointer;
275 | font-size: 20px;
276 | font-weight: normal;
277 | transition: opacity 0.15s ease;
278 | border: none;
279 | padding: 0;
280 | }
281 |
282 | .close:hover {
283 | opacity: 0.6;
284 | }
285 |
286 | .tips {
287 | background: #fff;
288 | border: 1px solid #e5e5e5;
289 | padding: 32px;
290 | margin-top: 24px;
291 | }
292 |
293 | .tips h3 {
294 | margin-top: 0;
295 | color: #000;
296 | font-size: 18px;
297 | font-weight: 600;
298 | }
299 |
300 | .tips ul {
301 | margin: 16px 0 0;
302 | padding-left: 24px;
303 | }
304 |
305 | .tips li {
306 | font-size: 14px;
307 | color: #666;
308 | margin-bottom: 12px;
309 | line-height: 1.6;
310 | font-style: normal;
311 | }
312 |
313 | .tips li:last-child {
314 | margin-bottom: 0;
315 | }
316 |
317 | .visually-hidden {
318 | position: absolute;
319 | width: 1px;
320 | height: 1px;
321 | padding: 0;
322 | margin: -1px;
323 | overflow: hidden;
324 | clip: rect(0, 0, 0, 0);
325 | white-space: nowrap;
326 | border: 0;
327 | }
328 |
329 | @media(max-width: 768px) {
330 | #frame {
331 | padding: 60px 16px;
332 | }
333 |
334 | .intro {
335 | margin-bottom: 60px;
336 | }
337 |
338 | .intro h1 {
339 | font-size: 40px;
340 | }
341 |
342 | .intro h2 {
343 | font-size: 16px;
344 | }
345 |
346 | form, .options, #output, .tips {
347 | padding: 24px;
348 | }
349 |
350 | .menu {
351 | padding: 16px 20px;
352 | }
353 |
354 | .options {
355 | grid-template-columns: 1fr;
356 | }
357 | }
358 |
359 | @media(max-width: 550px) {
360 | #mobile-notice {
361 | display: block;
362 | }
363 |
364 | .lr {
365 | flex-direction: column;
366 | gap: 16px;
367 | align-items: stretch;
368 | }
369 |
370 | .left {
371 | justify-content: center;
372 | }
373 |
374 | #render {
375 | width: 100%;
376 | }
377 | }
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 | num2math - Complicated Math Equation Generator | Create Complex Equations
10 |
14 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
41 |
50 |
51 |
52 |
53 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
76 |
77 |
78 |
79 |
80 |
x
81 |
Mobile Notice
82 |
83 | You appear to be on a device with a narrow screen width (i.e you are
84 | probably on a mobile device). Due to the nature of mathematics on this
85 | site, it best views in landscape mode. On narrow screens, some math
86 | expressions may run off the side of the screen (you should be able to
87 | scroll to see them)
88 |
89 |
90 |
91 | num2math
92 |
93 | A math expression generator. What is a complicated
94 | math equation that equals x? Let's find out!
95 |
96 |
97 |
98 |
128 |
129 |
130 | Equation Type Options
131 |
132 |
133 | Gamma function
134 |
135 |
136 |
137 | Euler's identity
138 |
139 |
140 |
141 | Exponential Limits
142 |
143 |
144 |
145 | Polynomial Limits
146 |
147 |
148 |
149 | Trig Identity
150 |
151 |
152 |
153 | Geometric Series
154 |
155 |
156 |
157 |
158 |
159 | 🔗 Share
160 | Download as PNG
161 |
162 |
163 | Tips
164 |
165 |
166 | There is an endless number of math expressions for every number.
167 | Click "Generate" several times for a different output.
168 |
169 |
170 | Click the "Share" button to get a link that includes your equation
171 | and settings - perfect for sharing with friends or saving for later!
172 |
173 |
174 | Right click (or long press if on a mobile device) on an equation to
175 | copy the TeX commands or MathML code.
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
--------------------------------------------------------------------------------
/src/services/decompositionService.js:
--------------------------------------------------------------------------------
1 | /**
2 | * Number decomposition strategies for generating complex expressions
3 | */
4 |
5 | import {
6 | isOdd,
7 | isSquare,
8 | isPrime,
9 | getFactors,
10 | randomInt,
11 | randomChoice,
12 | } from '../utils/mathHelpers.js';
13 | import {
14 | LARGE_NUMBER_THRESHOLD,
15 | SMALL_NUMBER_THRESHOLD,
16 | MEDIUM_NUMBER_THRESHOLD,
17 | RANDOM_OFFSET_MAX,
18 | } from '../utils/constants.js';
19 |
20 | /**
21 | * Decompose a number using exponential representation
22 | * @param {number} n - Target number
23 | * @param {Function} randomOption1 - First random generator
24 | * @param {Function} randomOption2 - Second random generator
25 | * @returns {string} LaTeX string
26 | */
27 | function decompose2(n, randomOption1, randomOption2) {
28 | const randNum = randomInt(6, 8);
29 | const logValue = Math.log(n) / Math.log(randNum);
30 | const floorLog = Math.floor(logValue);
31 | const ceilLog = Math.ceil(logValue);
32 | const exp1 = randNum ** floorLog;
33 | const exp2 = randNum ** ceilLog;
34 | const diff1 = Math.abs(n - exp1);
35 | const diff2 = Math.abs(n - exp2);
36 |
37 | if (diff1 < diff2) {
38 | const power = floorLog < 2 ? '' : `^{${floorLog}}`;
39 | return `{ \\left({${randomOption1(randNum)}}\\right)${power} + {${randomOption2(diff1)}}}`;
40 | }
41 |
42 | const power = ceilLog < 2 ? '' : `^{${ceilLog}}`;
43 | return `{ \\left({${randomOption1(randNum)}}\\right) ${power} - {${randomOption2(diff2)}}}`;
44 | }
45 |
46 | /**
47 | * Find minimal exponential representation for large numbers
48 | * @param {number} n - Target number
49 | * @param {Function} randomOption3 - Random generator function
50 | * @returns {string} LaTeX string
51 | */
52 | function minExp(n, randomOption3, decompose2Fn) {
53 | let oldDiff, newDiff, exp, exponent;
54 | const valObj = {};
55 |
56 | // Start with base 3
57 | let logValue = Math.log(n) / Math.log(3);
58 | let floorLog = Math.floor(logValue);
59 | let ceilLog = Math.ceil(logValue);
60 | let exp1 = 3 ** floorLog;
61 | let exp2 = 3 ** ceilLog;
62 | let diff1 = Math.abs(n - exp1);
63 | let diff2 = Math.abs(n - exp2);
64 |
65 | if (diff1 < diff2) {
66 | oldDiff = diff1;
67 | exp = [exp1, `\\left({${randomOption3(3)}}\\right)^{${floorLog}}`];
68 | valObj[oldDiff] = exp;
69 | } else {
70 | oldDiff = diff2;
71 | exp = [exp2, `\\left({${randomOption3(3)}}\\right)^{${ceilLog}}`];
72 | valObj[oldDiff] = exp;
73 | }
74 |
75 | // Try bases 4-8
76 | for (let i = 4; i < 9; i++) {
77 | logValue = Math.log(n) / Math.log(i);
78 | floorLog = Math.floor(logValue);
79 | ceilLog = Math.ceil(logValue);
80 | exp1 = i ** floorLog;
81 | exp2 = i ** ceilLog;
82 | diff1 = Math.abs(n - exp1);
83 | diff2 = Math.abs(n - exp2);
84 |
85 | if (diff1 < diff2) {
86 | exp = [exp1, `\\left({${randomOption3(i)}}\\right)^{${floorLog}}`];
87 | newDiff = diff1;
88 | valObj[newDiff] = exp;
89 | } else {
90 | exp = [exp2, `\\left({${randomOption3(i)}}\\right)^{${ceilLog}}`];
91 | newDiff = diff2;
92 | valObj[newDiff] = exp;
93 | }
94 | newDiff = Math.min(newDiff, oldDiff);
95 | oldDiff = newDiff;
96 | exponent = valObj[newDiff];
97 | }
98 |
99 | if (exponent[0] < n) {
100 | return `${exponent[1]} + \\left({${decompose2Fn(newDiff)}}\\right)`;
101 | }
102 | return `${exponent[1]} - \\left({${decompose2Fn(newDiff)}}\\right)`;
103 | }
104 |
105 | /**
106 | * Decompose a number into a complex mathematical expression
107 | * @param {number} n - Target number
108 | * @param {Function[]} possibleOptions - Array of generator functions
109 | * @param {Function} sameNumber - Fallback function
110 | * @returns {string} LaTeX expression
111 | */
112 | export function decompose(n, possibleOptions, sameNumber) {
113 | // Select random generator functions
114 | function moreRandomOptions(num) {
115 | return randomChoice(possibleOptions)(num);
116 | }
117 |
118 | let randomOption1 = moreRandomOptions;
119 | let randomOption2 = moreRandomOptions;
120 | let randomOption3 = moreRandomOptions;
121 |
122 | if (possibleOptions.length < 1) {
123 | randomOption1 = sameNumber;
124 | randomOption2 = sameNumber;
125 | randomOption3 = sameNumber;
126 | } else if (possibleOptions.length >= 3) {
127 | // Select three different options
128 | const options1 = [...possibleOptions];
129 | const randIndex1 = randomInt(0, options1.length - 1);
130 | randomOption1 = options1[randIndex1];
131 |
132 | const options2 = [...options1];
133 | options2.splice(randIndex1, 1);
134 | const randIndex2 = randomInt(0, options2.length - 1);
135 | randomOption2 = options2[randIndex2];
136 |
137 | const options3 = [...options2];
138 | options3.splice(randIndex2, 1);
139 | const randIndex3 = randomInt(0, options3.length - 1);
140 | randomOption3 = options3[randIndex3];
141 | }
142 |
143 | // Wrapper for decompose2 with bound functions
144 | const decompose2Fn = (num) => decompose2(num, randomOption1, randomOption2);
145 |
146 | // Handle large numbers with exponential representation
147 | if (n > LARGE_NUMBER_THRESHOLD) {
148 | return minExp(n, randomOption3, decompose2Fn);
149 | }
150 |
151 | const randomValue = Math.random();
152 |
153 | // Strategy 1: Factor decomposition - ab = (a - c)(b + c) + c(b - a + c)
154 | if (n < MEDIUM_NUMBER_THRESHOLD && (n === 2 || !isPrime(n)) && randomValue < 0.25) {
155 | const factors = getFactors(parseInt(n));
156 | const randomIndex = randomInt(0, factors.length - 1);
157 | const a = factors[randomIndex];
158 | const b = n / a;
159 | const c = randomInt(1, RANDOM_OFFSET_MAX);
160 |
161 | if (Math.random() < 0.2) {
162 | return `{{\\left({${randomOption1(a)}}\\right)}{\\left({${randomOption2(b)}}\\right)}}`;
163 | }
164 |
165 | return `{ \\left({${randomOption1(a)} - ${randomOption2(c)}}\\right) \\left({${randomOption3(b)} + ${moreRandomOptions(c)}}\\right) + {${moreRandomOptions(c)}}{\\left({${moreRandomOptions(b)} - ${moreRandomOptions(a)} + ${moreRandomOptions(c)}} \\right)} }`;
166 | }
167 |
168 | // Strategy 2: Consecutive squares - n = (d + 1)^2 - d^2 = 2d + 1
169 | if (n > 2 && isOdd(n) && randomValue < 0.40) {
170 | const d = Math.floor(n / 2);
171 | if (Math.random() < 0.5) {
172 | return `{${randomOption1(2)} \\left({${randomOption2(d)}}\\right) + ${randomOption3(1)}}`;
173 | }
174 | return `{\\left({${randomOption1(d)} + ${randomOption2(1)}}\\right)^2 - \\left({${randomOption3(d)}}\\right)^2}`;
175 | }
176 |
177 | // Strategy 3: Square root for small numbers
178 | if (randomValue < 0.55 && n < SMALL_NUMBER_THRESHOLD) {
179 | const square = n ** 2;
180 | return `{\\sqrt{${randomOption1(square)}}}`;
181 | }
182 |
183 | // Strategy 4: Sum of odd numbers = n^2
184 | if (n > 1 && isSquare(n) && randomValue < 0.6) {
185 | const squareroot = Math.sqrt(n);
186 |
187 | if (Math.random() < 0.2) {
188 | let sum = `${randomOption1(1)}`;
189 | let oddVal = 1;
190 | for (let i = 0; i < squareroot - 1; i++) {
191 | const randomOption = randomChoice(possibleOptions);
192 | oddVal += 2;
193 | sum += `+ ${randomOption(oddVal)}`;
194 | }
195 | return `{ ${sum} }`;
196 | }
197 |
198 | // (a + b)^2 expansion
199 | const a = randomInt(1, squareroot - 1);
200 | const b = squareroot - a;
201 |
202 | if (Math.random() < 0.5) {
203 | return `{ {\\left(${randomOption1(a)} + ${randomOption2(b)}\\right)}^2}`;
204 | }
205 | return `{ {\\left(${randomOption1(a)}\\right)}^2 + {${moreRandomOptions(2)}}{\\left(${randomOption2(a)}\\right)}{\\left(${randomOption3(b)}\\right)} + {\\left(${moreRandomOptions(b)}\\right)}^2}`;
206 | }
207 |
208 | // Strategy 5: Difference of consecutive squares
209 | if (isOdd(n) && randomValue < 0.7) {
210 | const a = Math.floor(n / 2);
211 | const b = n - a;
212 | if (n < 22) {
213 | return `{${randomOption1(b ** 2)} - ${randomOption2(a ** 2)}}`;
214 | }
215 | return `{ \\left({${randomOption1(b)}}\\right)^2 - \\left({${randomOption2(a)}}\\right)^2}`;
216 | }
217 |
218 | // Strategy 6: Pythagorean triples using Fibonacci's method
219 | if (randomValue < 0.8 && isOdd(n) && n < SMALL_NUMBER_THRESHOLD) {
220 | const a = n;
221 | const aSquare = a ** 2;
222 | const position = (aSquare + 1) / 2;
223 | let bSquare = 0;
224 | let odd = -1;
225 | for (let i = 1; i < position; i++) {
226 | odd += 2;
227 | bSquare += odd;
228 | }
229 | const b = Math.sqrt(bSquare);
230 | const cSquare = odd + 2 + bSquare;
231 | const c = Math.sqrt(cSquare);
232 | return `{\\sqrt{\\left({${randomOption1(c)}}\\right)^2 - \\left({${randomOption2(b)}}\\right)^2}}`;
233 | }
234 |
235 | // Strategy 7: Multiplication and addition
236 | if (randomValue < 0.90) {
237 | const randNum = randomInt(1, n + 1);
238 | const r = n % randNum;
239 | const a = Math.floor(n / randNum);
240 | return `${randomOption1(a)} \\times {${randomOption2(randNum)}} + ${randomOption3(r)}`;
241 | }
242 |
243 | // Strategy 8: Multiply and divide
244 | const r = randomInt(1, 5);
245 | return `${randomOption1(n * r)} \\over {${randomOption2(r)}}`;
246 | }
247 |
--------------------------------------------------------------------------------