312 lines
12 KiB
JavaScript
312 lines
12 KiB
JavaScript
/**
|
||
* Generate space-worksheet3.html — 9-page static HTML worksheet
|
||
* combining task patterns from worksheet1 and worksheet2.
|
||
*
|
||
* Usage: node src/scripts/generate-worksheet3.mjs
|
||
* Output: output/html/space-worksheet3.html
|
||
*/
|
||
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
|
||
const ROOT = process.cwd();
|
||
|
||
// ── Icon pools ──────────────────────────────────────────────────────────────
|
||
const pack1Icons = fs.readdirSync(path.join(ROOT, 'assets/icons/pack1'))
|
||
.filter(f => f.endsWith('.png'))
|
||
.map(f => `assets/icons/pack1/${f}`);
|
||
|
||
const pack2Icons = fs.readdirSync(path.join(ROOT, 'assets/icons/pack2'))
|
||
.filter(f => f.endsWith('.png'))
|
||
.map(f => `assets/icons/pack2/${f}`);
|
||
|
||
function shuffle(arr) {
|
||
const a = [...arr];
|
||
for (let i = a.length - 1; i > 0; i--) {
|
||
const j = Math.floor(Math.random() * (i + 1));
|
||
[a[i], a[j]] = [a[j], a[i]];
|
||
}
|
||
return a;
|
||
}
|
||
|
||
function randInt(min, max) {
|
||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||
}
|
||
|
||
function randSign() {
|
||
return Math.random() < 0.5 ? 1 : -1;
|
||
}
|
||
|
||
function pickAlignment() {
|
||
const opts = ['justify-start', 'justify-center', 'justify-end'];
|
||
return opts[Math.floor(Math.random() * opts.length)];
|
||
}
|
||
|
||
// ── Problem generators ──────────────────────────────────────────────────────
|
||
|
||
// Pattern 1: A + B ± C (A:8-16, B:4-8, C:1-3)
|
||
function genPattern1() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
while (problems.length < 20) {
|
||
const A = randInt(8, 16);
|
||
const B = randInt(4, 8);
|
||
const C = randInt(1, 3);
|
||
const sign = randSign();
|
||
const expr = sign > 0 ? `${A} + ${B} + ${C}` : `${A} + ${B} − ${C}`;
|
||
const result = A + B + sign * C;
|
||
if (result >= 0 && !seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
// Pattern 2: 5 × N ± C (N:1-5, C:1-6)
|
||
function genPattern2() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
while (problems.length < 20) {
|
||
const N = randInt(1, 5);
|
||
const C = randInt(1, 6);
|
||
const sign = randSign();
|
||
const expr = sign > 0 ? `5 × ${N} + ${C}` : `5 × ${N} − ${C}`;
|
||
const result = 5 * N + sign * C;
|
||
if (result >= 0 && !seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
// Pattern 3: A × B ± C (A,B:1-4, C:1-8)
|
||
function genPattern3() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
while (problems.length < 20) {
|
||
const A = randInt(1, 4);
|
||
const B = randInt(1, 4);
|
||
const C = randInt(1, 8);
|
||
const sign = randSign();
|
||
const expr = sign > 0 ? `${A} × ${B} + ${C}` : `${A} × ${B} − ${C}`;
|
||
const result = A * B + sign * C;
|
||
if (result >= 0 && !seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
// Pattern 4: A + B + C (A:12-24, B:±6-10, C:±2-5, result 0-40)
|
||
function genPattern4() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
while (problems.length < 20) {
|
||
const A = randInt(12, 24);
|
||
const signB = randSign();
|
||
const B = randInt(6, 10);
|
||
const signC = randSign();
|
||
const C = randInt(2, 5);
|
||
const result = A + signB * B + signC * C;
|
||
if (result >= 0 && result <= 40) {
|
||
const bPart = signB > 0 ? `+ ${B}` : `− ${B}`;
|
||
const cPart = signC > 0 ? `+ ${C}` : `− ${C}`;
|
||
const expr = `${A} ${bPart} ${cPart}`;
|
||
if (!seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
// Pattern 5: mixed fives (5+5+5, 5×N, 5×N±5)
|
||
function genPattern5() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
|
||
// Type A: 5+5+5 (2,3,4 fives)
|
||
for (const count of [2, 3, 4]) {
|
||
const expr = Array(count).fill('5').join(' + ');
|
||
problems.push(expr);
|
||
seen.add(expr);
|
||
}
|
||
|
||
// Type B: 5×N (N:1-12)
|
||
const ns = shuffle([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
||
for (const n of ns) {
|
||
if (problems.length >= 20) break;
|
||
const expr = `5 × ${n}`;
|
||
if (!seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
|
||
// Type C: 5×N±5 (fill remaining)
|
||
while (problems.length < 20) {
|
||
const N = randInt(2, 12);
|
||
const sign = randSign();
|
||
const expr = sign > 0 ? `5 × ${N} + 5` : `5 × ${N} − 5`;
|
||
if (!seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
|
||
return shuffle(problems);
|
||
}
|
||
|
||
// Pattern 6: A×B + C×D (A:2-4, B:1-3, C:2-3, D:1-2)
|
||
function genPattern6() {
|
||
const problems = [];
|
||
const seen = new Set();
|
||
while (problems.length < 20) {
|
||
const A = randInt(2, 4);
|
||
const B = randInt(1, 3);
|
||
const C = randInt(2, 3);
|
||
const D = randInt(1, 2);
|
||
const expr = `${A} × ${B} + ${C} × ${D}`;
|
||
if (!seen.has(expr)) {
|
||
seen.add(expr);
|
||
problems.push(expr);
|
||
}
|
||
}
|
||
return problems;
|
||
}
|
||
|
||
const generators = [genPattern1, genPattern2, genPattern3, genPattern4, genPattern5, genPattern6, genPattern1, genPattern2, genPattern3];
|
||
|
||
// ── Page config ─────────────────────────────────────────────────────────────
|
||
const pages = [
|
||
{ hero: 'assets/hero-images/spaceship1.jpeg', footer: 'assets/footers/planet1.jpeg', dir: 'flex-row-reverse', pack: 'pack1', gen: 0 },
|
||
{ hero: 'assets/hero-images/spaceship2.jpeg', footer: 'assets/footers/planet2.jpeg', dir: '', pack: 'pack1', gen: 1 },
|
||
{ hero: 'assets/hero-images/spaceship3.png', footer: 'assets/footers/planet3.jpeg', dir: 'flex-row-reverse', pack: 'pack1', gen: 2 },
|
||
{ hero: 'assets/hero-images/spaceship4.jpeg', footer: 'assets/footers/planet4.jpeg', dir: '', pack: 'pack2', gen: 3 },
|
||
{ hero: 'assets/hero-images/spaceship5.jpeg', footer: 'assets/footers/planet5.jpeg', dir: 'flex-row-reverse', pack: 'pack2', gen: 4, fivesHint: true },
|
||
{ hero: 'assets/hero-images/spaceship6.jpeg', footer: 'assets/footers/planet6.jpeg', dir: '', pack: 'pack2', gen: 5 },
|
||
{ hero: 'assets/hero-images/spaceship7.jpeg', footer: 'assets/footers/planet7.jpeg', dir: 'flex-row-reverse', pack: 'pack1', gen: 6 },
|
||
{ hero: 'assets/hero-images/spaceship8.jpeg', footer: 'assets/footers/planet8.jpeg', dir: '', pack: 'pack1', gen: 7 },
|
||
{ hero: 'assets/hero-images/spaceship9.jpeg', footer: 'assets/footers/planet9.jpeg', dir: 'flex-row-reverse', pack: 'pack1', gen: 8 },
|
||
];
|
||
|
||
// ── Assign icons per section (3 pages share a pool) ─────────────────────────
|
||
// Section 1 (pages 0-2): pack1 shuffled
|
||
// Section 2 (pages 3-5): pack2 shuffled
|
||
// Section 3 (pages 6-8): pack1 re-shuffled
|
||
const iconSections = [
|
||
shuffle(pack1Icons), // pages 0-2: 60 icons
|
||
shuffle(pack2Icons), // pages 3-5: 60 icons
|
||
shuffle(pack1Icons), // pages 6-8: 60 icons (reuse pack1, different shuffle)
|
||
];
|
||
|
||
// ── Build HTML ──────────────────────────────────────────────────────────────
|
||
function buildProblemCard(icon, expression, alignment, iconSize) {
|
||
return ` <div class="flex ${alignment}">
|
||
<div class="flex items-center gap-1">
|
||
<img src="../../${icon}" class="w-[${iconSize}px] h-[${iconSize}px] shrink-0 object-contain" alt="">
|
||
<div class="pl-3 pr-4 py-1 border-[1.5px] border-indigo-100 rounded-full bg-gradient-to-br from-white to-indigo-50/40 shadow-sm shadow-indigo-100/30">
|
||
<span class="text-[1.2rem] font-semibold text-indigo-950 whitespace-nowrap">${expression} = <span class="inline-block w-16 border-b-[2px] border-indigo-300"> </span></span>
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
function buildPage(pageIdx, isLast) {
|
||
const pg = pages[pageIdx];
|
||
const sectionIdx = Math.floor(pageIdx / 3);
|
||
const posInSection = pageIdx % 3;
|
||
const sectionIcons = iconSections[sectionIdx];
|
||
const pageIcons = sectionIcons.slice(posInSection * 20, posInSection * 20 + 20);
|
||
const iconSize = pg.pack === 'pack2' ? 58 : 58;
|
||
|
||
const problems = generators[pg.gen]();
|
||
const dirClass = pg.dir ? ` ${pg.dir}` : '';
|
||
const breakStyle = isLast ? '' : ' style="break-after: page;"';
|
||
|
||
const fivesHint = pg.fivesHint
|
||
? `\n <div class="font-semibold text-indigo-950 mb-3 text-center" style="font-size: 0.8rem; margin-top: -10px;">Посчитай пятёрками: <span style="font-size: 1rem; word-spacing: 0.25em;">5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60</span> для зарядки расщепителя</div>\n`
|
||
: '';
|
||
|
||
const cards = problems.map((expr, i) => {
|
||
return buildProblemCard(pageIcons[i], expr, pickAlignment(), iconSize);
|
||
}).join('\n\n');
|
||
|
||
return ` <!-- PAGE ${pageIdx + 1} -->
|
||
<div class="w-[210mm] h-[297mm] relative overflow-hidden mx-auto bg-white"${breakStyle}>
|
||
|
||
<div class="absolute bottom-0 left-0 right-0 h-[80mm] z-0">
|
||
<div class="absolute top-0 left-0 right-0 h-full z-10" style="background: linear-gradient(to bottom, white 0%, rgba(255,255,255,0.6) 25%, transparent 50%);"></div>
|
||
<img src="../../${pg.footer}" class="w-full h-full object-cover object-top" alt="">
|
||
</div>
|
||
|
||
<div class="absolute bottom-[12mm] left-0 right-0 z-20 flex justify-center">
|
||
<div style="background: rgba(255,255,255,0.85);" class="rounded-full px-6 py-1.5 border-[1.5px] border-indigo-200 shadow-md">
|
||
<span class="text-[0.95rem] font-bold text-indigo-950">Итого собрано на планете: <span class="inline-block w-14 border-b-2 border-indigo-400 text-center ml-1"> </span></span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="relative z-10 w-full h-full px-[12mm] pt-[4mm] pb-[65mm] flex flex-col">
|
||
|
||
<div class="flex items-center gap-4 mb-4${dirClass}">
|
||
<img src="../../${pg.hero}" class="w-[48%] shrink-0 object-contain" alt="">
|
||
<div class="flex-1 flex flex-col items-center justify-center text-center">
|
||
<h1 class="text-2xl font-extrabold leading-tight tracking-tight text-indigo-950">Исследуй Планету</h1>
|
||
<p class="text-sm font-medium text-indigo-400 mt-1">Собери ресурсы, решая примеры!</p>
|
||
</div>
|
||
</div>
|
||
${fivesHint}
|
||
<div class="grid grid-cols-2 gap-x-3 gap-y-[3px] flex-1 content-start">
|
||
|
||
${cards}
|
||
|
||
</div>
|
||
</div>
|
||
</div>`;
|
||
}
|
||
|
||
const htmlPages = pages.map((_, i) => buildPage(i, i === pages.length - 1)).join('\n\n');
|
||
|
||
const html = `<!DOCTYPE html>
|
||
<html lang="ru">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||
<link href="https://fonts.googleapis.com/css2?family=Nunito:wght@400;600;700;800;900&display=swap" rel="stylesheet">
|
||
<title>Исследуй Планету</title>
|
||
<script>
|
||
tailwind.config = {
|
||
theme: {
|
||
extend: {
|
||
fontFamily: { nunito: ['Nunito', 'sans-serif'] },
|
||
}
|
||
}
|
||
}
|
||
</script>
|
||
<style>
|
||
@page { size: A4; margin: 0; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gray-200 font-nunito">
|
||
|
||
${htmlPages}
|
||
|
||
</body>
|
||
</html>
|
||
`;
|
||
|
||
// ── Write output ────────────────────────────────────────────────────────────
|
||
const outDir = path.join(ROOT, 'output/html');
|
||
if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
|
||
|
||
const outPath = path.join(outDir, 'space-worksheet3.html');
|
||
fs.writeFileSync(outPath, html, 'utf-8');
|
||
console.log(`Generated: ${outPath} (${pages.length} pages, ${pages.length * 20} problems)`);
|