diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 11a76df..582a705 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -47,7 +47,8 @@ "Bash(mv freighter-science1.png freighter12.png)", "Bash(sort -t'\\(' -k2 -n)", "Bash(grep -o 'scale\\(3.50\\).\\\\{0,200\\\\}')", - "Bash(python3 -c ':*)" + "Bash(python3 -c ':*)", + "Bash(grep -r \"margin.*-\\\\|negative\" tasks/*/CLAUDE.md)" ] } } diff --git a/assets/items/asteroid-shapes/asteroid1.png b/assets/items/asteroid-shapes/asteroid1.png new file mode 100644 index 0000000..422e67e Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid1.png differ diff --git a/assets/items/asteroid-shapes/asteroid10.png b/assets/items/asteroid-shapes/asteroid10.png new file mode 100644 index 0000000..61363e0 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid10.png differ diff --git a/assets/items/asteroid-shapes/asteroid11.png b/assets/items/asteroid-shapes/asteroid11.png new file mode 100644 index 0000000..90c643c Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid11.png differ diff --git a/assets/items/asteroid-shapes/asteroid12.png b/assets/items/asteroid-shapes/asteroid12.png new file mode 100644 index 0000000..f47083c Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid12.png differ diff --git a/assets/items/asteroid-shapes/asteroid13.png b/assets/items/asteroid-shapes/asteroid13.png new file mode 100644 index 0000000..e12d869 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid13.png differ diff --git a/assets/items/asteroid-shapes/asteroid14.png b/assets/items/asteroid-shapes/asteroid14.png new file mode 100644 index 0000000..0f944d4 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid14.png differ diff --git a/assets/items/asteroid-shapes/asteroid15.png b/assets/items/asteroid-shapes/asteroid15.png new file mode 100644 index 0000000..bc0db6f Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid15.png differ diff --git a/assets/items/asteroid-shapes/asteroid16.png b/assets/items/asteroid-shapes/asteroid16.png new file mode 100644 index 0000000..3e68e48 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid16.png differ diff --git a/assets/items/asteroid-shapes/asteroid2.png b/assets/items/asteroid-shapes/asteroid2.png new file mode 100644 index 0000000..5aa4077 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid2.png differ diff --git a/assets/items/asteroid-shapes/asteroid3.png b/assets/items/asteroid-shapes/asteroid3.png new file mode 100644 index 0000000..601d454 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid3.png differ diff --git a/assets/items/asteroid-shapes/asteroid4.png b/assets/items/asteroid-shapes/asteroid4.png new file mode 100644 index 0000000..0fd861f Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid4.png differ diff --git a/assets/items/asteroid-shapes/asteroid5.png b/assets/items/asteroid-shapes/asteroid5.png new file mode 100644 index 0000000..fd74186 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid5.png differ diff --git a/assets/items/asteroid-shapes/asteroid6.png b/assets/items/asteroid-shapes/asteroid6.png new file mode 100644 index 0000000..8da4c7e Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid6.png differ diff --git a/assets/items/asteroid-shapes/asteroid7.png b/assets/items/asteroid-shapes/asteroid7.png new file mode 100644 index 0000000..5beaaec Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid7.png differ diff --git a/assets/items/asteroid-shapes/asteroid8.png b/assets/items/asteroid-shapes/asteroid8.png new file mode 100644 index 0000000..2cd4573 Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid8.png differ diff --git a/assets/items/asteroid-shapes/asteroid9.png b/assets/items/asteroid-shapes/asteroid9.png new file mode 100644 index 0000000..a0fa24d Binary files /dev/null and b/assets/items/asteroid-shapes/asteroid9.png differ diff --git a/package.json b/package.json index fa40578..c660371 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "pdf": "node src/scripts/generate-pdf.mjs", "dev": "concurrently \"pnpm build:css:watch\" \"pnpm preview\"", "split-sprites": "node src/scripts/split-sprites.mjs", - "remove-bg": "node src/scripts/remove-bg.mjs" + "remove-bg": "node src/scripts/remove-bg.mjs", + "create-shapes": "node src/scripts/create-asteroid-shapes.mjs" }, "keywords": [], "author": "", diff --git a/src/scripts/create-asteroid-shapes.mjs b/src/scripts/create-asteroid-shapes.mjs new file mode 100644 index 0000000..5f12193 --- /dev/null +++ b/src/scripts/create-asteroid-shapes.mjs @@ -0,0 +1,159 @@ +import sharp from 'sharp'; +import { resolve, basename, join } from 'path'; +import { existsSync, mkdirSync, readdirSync } from 'fs'; +import { fileURLToPath } from 'url'; + +const PROJECT_ROOT = resolve(fileURLToPath(import.meta.url), '../../..'); +const DEFAULT_INPUT = resolve(PROJECT_ROOT, 'assets/icons/pack3-asteroids'); +const DEFAULT_OUTPUT = resolve(PROJECT_ROOT, 'assets/items/asteroid-shapes'); + +// Tunable constants +const OUTLINE_WIDTH = 5; // px thickness of boundary outline +const OUTLINE_COLOR = 40; // darkness of outline (0=black, 255=white) +const EDGE_THRESHOLD = 30; // min gradient magnitude to count as an edge +const EDGE_DARKNESS = 180; // how dark internal edge lines are (0=black, 255=white) +const ALPHA_THRESHOLD = 128; // min alpha to consider a pixel "opaque" + +async function createAsteroidShape(filePath, outputDir) { + // Step 1: Read source raw RGBA + const { data: srcData, info } = await sharp(filePath) + .ensureAlpha() + .raw() + .toBuffer({ resolveWithObject: true }); + const { width, height } = info; + + // Step 2: Gradient-based edge detection on greyscale + const greyResult = await sharp(filePath) + .greyscale() + .blur(1.0) + .raw() + .toBuffer({ resolveWithObject: true }); + const grey = greyResult.data; + + // Compute Sobel gradient magnitude per pixel + const gradients = new Float32Array(width * height); + for (let y = 1; y < height - 1; y++) { + for (let x = 1; x < width - 1; x++) { + const idx = y * width + x; + // Only compute for opaque pixels + if (srcData[idx * 4 + 3] < ALPHA_THRESHOLD) continue; + // Sobel X + const gx = + -grey[(y - 1) * width + (x - 1)] + grey[(y - 1) * width + (x + 1)] + - 2 * grey[y * width + (x - 1)] + 2 * grey[y * width + (x + 1)] + - grey[(y + 1) * width + (x - 1)] + grey[(y + 1) * width + (x + 1)]; + // Sobel Y + const gy = + -grey[(y - 1) * width + (x - 1)] - 2 * grey[(y - 1) * width + x] - grey[(y - 1) * width + (x + 1)] + + grey[(y + 1) * width + (x - 1)] + 2 * grey[(y + 1) * width + x] + grey[(y + 1) * width + (x + 1)]; + gradients[idx] = Math.sqrt(gx * gx + gy * gy); + } + } + + // Build base layer: white with faint gray edge lines where gradient is strong + const baseLayer = Buffer.alloc(width * height * 4); + for (let i = 0; i < width * height; i++) { + const alpha = srcData[i * 4 + 3]; + if (alpha < ALPHA_THRESHOLD) { + // Transparent pixel — keep transparent + baseLayer[i * 4 + 3] = 0; + continue; + } + const grad = gradients[i]; + let v = 255; // default white + if (grad > EDGE_THRESHOLD) { + // Map gradient above threshold to gray line darkness + const intensity = Math.min(1, (grad - EDGE_THRESHOLD) / 200); + v = 255 - Math.round(intensity * (255 - EDGE_DARKNESS)); + } + baseLayer[i * 4 + 0] = v; + baseLayer[i * 4 + 1] = v; + baseLayer[i * 4 + 2] = v; + baseLayer[i * 4 + 3] = alpha; + } + + // Step 3: Outline layer — boundary detection via neighbor check + const outlineLayer = Buffer.alloc(width * height * 4, 0); + + for (let y = 0; y < height; y++) { + for (let x = 0; x < width; x++) { + const idx = y * width + x; + if (srcData[idx * 4 + 3] < ALPHA_THRESHOLD) continue; + + let isBorder = false; + outer: + for (let dy = -OUTLINE_WIDTH; dy <= OUTLINE_WIDTH; dy++) { + for (let dx = -OUTLINE_WIDTH; dx <= OUTLINE_WIDTH; dx++) { + if (dx === 0 && dy === 0) continue; + const nx = x + dx; + const ny = y + dy; + if (nx < 0 || nx >= width || ny < 0 || ny >= height) { + isBorder = true; + break outer; + } + const nIdx = ny * width + nx; + if (srcData[nIdx * 4 + 3] < ALPHA_THRESHOLD) { + isBorder = true; + break outer; + } + } + } + + if (isBorder) { + outlineLayer[idx * 4 + 0] = OUTLINE_COLOR; + outlineLayer[idx * 4 + 1] = OUTLINE_COLOR; + outlineLayer[idx * 4 + 2] = OUTLINE_COLOR; + outlineLayer[idx * 4 + 3] = 255; + } + } + } + + // Step 4: Composite — base (white + faint edges) + outline on top + const raw = { width, height, channels: 4 }; + + await sharp(baseLayer, { raw }) + .composite([ + { + input: outlineLayer, + raw, + blend: 'over', + }, + ]) + .png() + .toFile(join(outputDir, basename(filePath))); +} + +async function main() { + const inputDir = resolve(process.argv[2] || DEFAULT_INPUT); + const outputDir = resolve(process.argv[3] || DEFAULT_OUTPUT); + + if (!existsSync(inputDir)) { + console.error(`Not found: ${inputDir}`); + process.exit(1); + } + + mkdirSync(outputDir, { recursive: true }); + + const files = readdirSync(inputDir) + .filter(f => /\.png$/i.test(f)) + .sort() + .map(f => join(inputDir, f)); + + if (files.length === 0) { + console.error(`No PNG files found in: ${inputDir}`); + process.exit(1); + } + + console.log(`Creating ${files.length} asteroid shape(s)...`); + + await Promise.all( + files.map(async (filePath) => { + await createAsteroidShape(filePath, outputDir); + console.log(` ${basename(filePath)} -> done`); + }) + ); + + console.log(`Done. ${files.length} shapes saved to ${outputDir}`); +} + +main(); diff --git a/tasks/cargo-filling/CLAUDE.md b/tasks/cargo-filling/CLAUDE.md new file mode 100644 index 0000000..12b01f6 --- /dev/null +++ b/tasks/cargo-filling/CLAUDE.md @@ -0,0 +1,124 @@ +# Cargo Filling Task Type + +Split an asteroid to fill a spaceship cargo bay to exactly 10. Combines asteroid splitting with cargo bay loading. Teaches addition crossing the tens boundary. + +## Math Concept + +Given: +- Cargo bay capacity: always **10** +- Asteroid A already in cargo (1-9) +- Asteroid B floating in space, B > (10 - A), so it doesn't fit whole +- Child splits B = N + M where A + N = 10 (N fills remaining space) +- Full expression: `A + B = (A + N) + M = C` +- Under `(A + N)` a curly brace with "10" + +Answer is always: N = 10 - A, M = B - N, C = A + B. + +## Layout + +- **Page:** A4 (210mm x 297mm), white background +- **Header:** Hero splitter image + title + subtitle +- **Footer:** Cabin interior (28mm, `object-cover`) +- **Content:** 6 task cards per page in 2 columns x 3 rows grid +- **Dividers:** Dark solid lines between cards (`border-bottom: 1.5px solid #334155`, odd cards have `border-right`) +- **Card overflow:** `hidden` — ships get cropped at card boundary + +## Card Layout (tuned values) + +Each card has two zones: +- **Visual area** (flex: 1, relative) — asteroid, ship, arrows, remnant +- **Formula area** (flex-shrink: 0, bottom) — label + formula + brace + +### Element positions & sizes (CSS) + +| Element | Position | Size | +|---------|----------|------| +| `.space-asteroid` | `left: -7.5mm; top: 18.5mm` | `28mm × 28mm` | +| `.cargo-ship` | `right: -12.5mm; top: -5mm` | `90mm × 42mm` | +| `.badge-10` | `left: 68%; top: 39%` (of ship) | `22px × 22px` | +| `.inner-ast` | `left: 86%; top: 73%` (of ship) | `14mm × 14mm` | +| `.remnant-shape` | `left: 61%; bottom: 3mm` | `16mm × 16mm` shape | + +### SVG arrows (viewBox `0 0 100 55`) + +Both arrows start from asteroid center and curve outward: +- **Arrow to ship:** `M7,30 Q30,15 52,25` +- **Arrow to remnant:** `M7,30 Q24,57 61,47` + +Stroke: `#6366f1`, width `0.5`, marker-end arrowhead. + +### Visual elements + +- **Big asteroid:** `pack3-asteroids/asteroidN.png` — colored, with white number B overlay (1.8rem) +- **Ship:** `pack4-cargobay/cargo-bayN.png` — flipped with `scaleX(-1)` so nose points right +- **Inner asteroid:** `items/asteroid-shapes/asteroidN.png` — line-art outline, white number A (1.1rem) +- **Remnant:** `items/asteroid-shapes/asteroidN.png` — line-art outline (opacity 0.5), blank underline, "?" label +- **Badge "10":** indigo circle (`#4f46e5`) with white "10" +- **Asteroid z-index: 6** (above arrows z-index: 4) + +### Formula + +``` +запиши формулу для пилота +A + B = (A + __) + __ = __ + ⏟ + 10 +``` + +- Label: uppercase, `#818cf8`, 0.45rem +- Formula: single line, 0.85rem bold `#1e1b4b` +- Brace: SVG path (width 90% of brace-group), margin-top 1.5mm +- "10": `#6366f1`, 0.6rem bold +- Brace gap from formula: 2mm (configurable via `braceGapMm`) + +## Asset Conventions + +- **Cargo bays:** `assets/icons/pack4-cargobay/cargo-bay{1-9}.png` — 6 unique per page +- **Asteroids (colored):** `assets/icons/pack3-asteroids/asteroid{1-16}.png` — for big asteroid in space +- **Asteroids (outline):** `assets/items/asteroid-shapes/asteroid{1-16}.png` — for inner asteroid & remnant +- **Hero images:** `assets/hero-images/splitters/splitter{1-9}.png` +- **Footer images:** `assets/footers/cabin{1-9}.jpeg` + +## Problem Range + +- A: 2-9 (value already in cargo) +- B: must be > (10-A) and ≤ 9 +- Practical combinations: + - A=8,9: B from 3-9 (easiest — small split) + - A=5,6,7: B from 4-9 (medium) + - A=2,3,4: B from 7-9 (hardest — large split) + +## Difficulty Progression + +- Page 1: A=8,9 with smaller B values (easy) +- Page 2: A=5,6,7 (medium) +- Page 3: A=2,3,4 with larger B (hard) + +## Color Palette + +Same as project-wide indigo theme: +- Title: `text-indigo-950` +- Subtitle: `text-indigo-400` +- Number overlay (big asteroid): white with text-shadow +- Number overlay (inner asteroid): white with text-shadow +- Cargo "10" badge: `bg-indigo-600` circle with white text +- Formula text: `text-indigo-950` +- Input blanks: `border-indigo-300` underlines +- Brace + "10" label: `#6366f1` +- Arrows: `#6366f1` + +## Title & Subtitle + +- **Title:** "Заполни Трюм" +- **Subtitle:** "На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?" + +## Scripts + +- `scripts/generate.mjs` — template + data → output pipeline (standard, no transforms yet) + +## Editor + +- `editor-temp.html` — temporary drag-and-drop tuner for card element positions/sizes +- Loads/saves state from `docs/cargo-filling-1.data.json` +- Export button outputs JSON with CSS values and SVG arrow paths +- Convention: edit only card 1, then apply values to all cards in template diff --git a/tasks/cargo-filling/docs/cargo-filling-1.data.json b/tasks/cargo-filling/docs/cargo-filling-1.data.json new file mode 100644 index 0000000..11570a7 --- /dev/null +++ b/tasks/cargo-filling/docs/cargo-filling-1.data.json @@ -0,0 +1,31 @@ +{ + "pages": [ + { + "page": 1, + "cards": [ + { + "index": 0, + "asteroidScale": 0.85, + "ship": { + "top": -7 + }, + "innerAst": { + "left": 57, + "top": 70 + } + }, + { + "index": 1, + "ship": { + "right": -26.5, + "top": -7 + }, + "innerAst": { + "left": 40, + "top": 68 + } + } + ] + } + ] +} \ No newline at end of file diff --git a/tasks/cargo-filling/docs/cargo-filling-1.md b/tasks/cargo-filling/docs/cargo-filling-1.md new file mode 100644 index 0000000..844009a --- /dev/null +++ b/tasks/cargo-filling/docs/cargo-filling-1.md @@ -0,0 +1,55 @@ +# Cargo Filling — Document 1 + +Addition crossing the tens boundary. 3 pages, 6 cards per page, 18 problems total. +Difficulty progression: easy → medium → hard. + +## Page 1 — Easy (A=8,9) + +Hero: splitter3 (right), Footer: cabin1 + +| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img | +|---|---|---|--------|-------|-------|--------------|---------------| +| 1 | 8 | 3 | 2 | 1 | 11 | asteroid1 | cargo-bay1 | +| 2 | 9 | 4 | 1 | 3 | 13 | asteroid2 | cargo-bay2 | +| 3 | 8 | 5 | 2 | 3 | 13 | asteroid3 | cargo-bay3 | +| 4 | 9 | 6 | 1 | 5 | 15 | asteroid4 | cargo-bay1 | +| 5 | 8 | 7 | 2 | 5 | 15 | asteroid5 | cargo-bay2 | +| 6 | 9 | 8 | 1 | 7 | 17 | asteroid6 | cargo-bay3 | + +## Page 2 — Medium (A=5,6,7) + +Hero: splitter6 (left), Footer: cabin3 + +| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img | +|---|---|---|--------|-------|-------|--------------|---------------| +| 1 | 7 | 4 | 3 | 1 | 11 | asteroid7 | cargo-bay4 | +| 2 | 6 | 5 | 4 | 1 | 11 | asteroid8 | cargo-bay5 | +| 3 | 7 | 6 | 3 | 3 | 13 | asteroid9 | cargo-bay6 | +| 4 | 5 | 7 | 5 | 2 | 12 | asteroid10 | cargo-bay4 | +| 5 | 6 | 8 | 4 | 4 | 14 | asteroid11 | cargo-bay5 | +| 6 | 7 | 9 | 3 | 6 | 16 | asteroid12 | cargo-bay6 | + +## Page 3 — Hard (A=2,3,4) + +Hero: splitter9 (right), Footer: cabin5 + +| # | A | B | N=10-A | M=B-N | C=A+B | Asteroid img | Cargo bay img | +|---|---|---|--------|-------|-------|--------------|---------------| +| 1 | 4 | 7 | 6 | 1 | 11 | asteroid13 | cargo-bay7 | +| 2 | 3 | 8 | 7 | 1 | 11 | asteroid14 | cargo-bay8 | +| 3 | 4 | 8 | 6 | 2 | 12 | asteroid15 | cargo-bay9 | +| 4 | 3 | 9 | 7 | 2 | 12 | asteroid16 | cargo-bay7 | +| 5 | 4 | 9 | 6 | 3 | 13 | asteroid1 | cargo-bay8 | +| 6 | 2 | 9 | 8 | 1 | 11 | asteroid2 | cargo-bay9 | + +## Hero Orientation + +- Page 1: splitter3, right side (`flex-row-reverse`) +- Page 2: splitter6, left side (default `flex-row`) +- Page 3: splitter9, right side (`flex-row-reverse`) + +## Footer Images + +- Page 1: cabin1.jpeg +- Page 2: cabin3.jpeg +- Page 3: cabin5.jpeg diff --git a/tasks/cargo-filling/docs/cargo-filling-1.template.html b/tasks/cargo-filling/docs/cargo-filling-1.template.html new file mode 100644 index 0000000..14fbff4 --- /dev/null +++ b/tasks/cargo-filling/docs/cargo-filling-1.template.html @@ -0,0 +1,1179 @@ + + + + + + + + + + Заполни Трюм + + + + + + + + + +
+
+ +
+
+ +
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
+ + 3 +
+
+ +
10
+
+ + 8 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 8+3= +
+ (8+ ) + + 10 +
+ + =  +
+
+
+ + +
+
+
+ + 4 +
+
+ +
10
+
+ + 9 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 9+4= +
+ (9+ ) + + 10 +
+ + =  +
+
+
+ + +
+
+
+ + 5 +
+
+ +
10
+
+ + 8 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 8+5= +
+ (8+ ) + + 10 +
+ + =  +
+
+
+ + +
+
+
+ + 6 +
+
+ +
10
+
+ + 9 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 9+6= +
+ (9+ ) + + 10 +
+ + =  +
+
+
+ + +
+
+
+ + 7 +
+
+ +
10
+
+ + 8 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 8+7= +
+ (8+ ) + + 10 +
+ + =  +
+
+
+ + +
+
+
+ + 8 +
+
+ +
10
+
+ + 9 +
+
+ + + + +
 
 
?
+
+
+
запиши формулу для пилота
+
+ 9+8= +
+ (9+ ) + + 10 +
+ + =  +
+
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
4
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+4=
(7+ )10
+ = 
+
+ + +
+
+
5
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+5=
(6+ )10
+ = 
+
+ + +
+
+
6
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+6=
(7+ )10
+ = 
+
+ + +
+
+
7
+
10
5
+ +
 
 
?
+
+
запиши формулу для пилота
5+7=
(5+ )10
+ = 
+
+ + +
+
+
8
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+8=
(6+ )10
+ = 
+
+ + +
+
+
9
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+9=
(7+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
7
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+7=
(4+ )10
+ = 
+
+ + +
+
+
8
+
10
3
+ +
 
 
?
+
+
запиши формулу для пилота
3+8=
(3+ )10
+ = 
+
+ + +
+
+
8
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+8=
(4+ )10
+ = 
+
+ + +
+
+
9
+
10
3
+ +
 
 
?
+
+
запиши формулу для пилота
3+9=
(3+ )10
+ = 
+
+ + +
+
+
9
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+9=
(4+ )10
+ = 
+
+ + +
+
+
9
+
10
2
+ +
 
 
?
+
+
запиши формулу для пилота
2+9=
(2+ )10
+ = 
+
+ +
+
+
+ + + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
2
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+2=
(9+ )10
+ = 
+
+ + +
+
+
4
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+4=
(8+ )10
+ = 
+
+ + +
+
+
3
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+3=
(9+ )10
+ = 
+
+ + +
+
+
6
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+6=
(8+ )10
+ = 
+
+ + +
+
+
5
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+5=
(9+ )10
+ = 
+
+ + +
+
+
8
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+8=
(8+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
5
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+5=
(7+ )10
+ = 
+
+ + +
+
+
6
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+6=
(6+ )10
+ = 
+
+ + +
+
+
6
+
10
5
+ +
 
 
?
+
+
запиши формулу для пилота
5+6=
(5+ )10
+ = 
+
+ + +
+
+
7
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+7=
(6+ )10
+ = 
+
+ + +
+
+
7
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+7=
(7+ )10
+ = 
+
+ + +
+
+
8
+
10
5
+ +
 
 
?
+
+
запиши формулу для пилота
5+8=
(5+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
7
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+7=
(9+ )10
+ = 
+
+ + +
+
+
9
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+9=
(8+ )10
+ = 
+
+ + +
+
+
9
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+9=
(9+ )10
+ = 
+
+ + +
+
+
8
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+8=
(7+ )10
+ = 
+
+ + +
+
+
9
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+9=
(6+ )10
+ = 
+
+ + +
+
+
9
+
10
5
+ +
 
 
?
+
+
запиши формулу для пилота
5+9=
(5+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
2
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+2=
(9+ )10
+ = 
+
+ + +
+
+
3
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+3=
(8+ )10
+ = 
+
+ + +
+
+
4
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+4=
(9+ )10
+ = 
+
+ + +
+
+
5
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+5=
(8+ )10
+ = 
+
+ + +
+
+
6
+
10
9
+ +
 
 
?
+
+
запиши формулу для пилота
9+6=
(9+ )10
+ = 
+
+ + +
+
+
7
+
10
8
+ +
 
 
?
+
+
запиши формулу для пилота
8+7=
(8+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
4
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+4=
(7+ )10
+ = 
+
+ + +
+
+
5
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+5=
(6+ )10
+ = 
+
+ + +
+
+
7
+
10
5
+ +
 
 
?
+
+
запиши формулу для пилота
5+7=
(5+ )10
+ = 
+
+ + +
+
+
6
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+6=
(7+ )10
+ = 
+
+ + +
+
+
8
+
10
6
+ +
 
 
?
+
+
запиши формулу для пилота
6+8=
(6+ )10
+ = 
+
+ + +
+
+
9
+
10
7
+ +
 
 
?
+
+
запиши формулу для пилота
7+9=
(7+ )10
+ = 
+
+ +
+
+
+ + +
+
+ +
+
+
+ +
+

Заполни Трюм

+

На какие части нужно расщепить астероид, чтобы заполнить свободное место в трюме? Какой кусочек останется в космосе? Каков общий объём?

+
+
+ +
+ + +
+
+
7
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+7=
(4+ )10
+ = 
+
+ + +
+
+
8
+
10
3
+ +
 
 
?
+
+
запиши формулу для пилота
3+8=
(3+ )10
+ = 
+
+ + +
+
+
9
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+9=
(4+ )10
+ = 
+
+ + +
+
+
9
+
10
3
+ +
 
 
?
+
+
запиши формулу для пилота
3+9=
(3+ )10
+ = 
+
+ + +
+
+
9
+
10
2
+ +
 
 
?
+
+
запиши формулу для пилота
2+9=
(2+ )10
+ = 
+
+ + +
+
+
8
+
10
4
+ +
 
 
?
+
+
запиши формулу для пилота
4+8=
(4+ )10
+ = 
+
+ +
+
+
+ + + diff --git a/tasks/cargo-filling/editor.html b/tasks/cargo-filling/editor.html new file mode 100644 index 0000000..6765f63 --- /dev/null +++ b/tasks/cargo-filling/editor.html @@ -0,0 +1,560 @@ + + + + + + Cargo Filling Editor + + + +
+ + Page 1 / ? + +
+ Loading... +
+ + + + +
+
+
+ Keys: +/- scale/resize • arrows move • 0 reset • Esc deselect +
+
+
+ + + + + diff --git a/tasks/cargo-filling/index.html b/tasks/cargo-filling/index.html new file mode 100644 index 0000000..1bd1b1b --- /dev/null +++ b/tasks/cargo-filling/index.html @@ -0,0 +1,182 @@ + + + + + + Fill the Cargo Bay — Space Math Adventures + + + + + + +
+
+ ← All Categories + + +

Split an asteroid to fill the remaining space in a cargo bay! Each card shows a ship with cargo already inside, and an asteroid that's too big to fit whole. The child splits the asteroid so one piece fills the bay to exactly 10. Practices addition crossing the tens boundary.

+ +
+ +
+
Preview
+
+

Cargo Filling 1

+
+ 3 pages + Easy → Hard
+ 6 cards per page • Split asteroid to fill cargo bay to 10 • Addition through 10 +
+
+ Preview + PDF + Editor +
+
+
+ +
+
+
+ + + + + diff --git a/tasks/cargo-filling/scripts/generate.mjs b/tasks/cargo-filling/scripts/generate.mjs new file mode 100644 index 0000000..6f331e0 --- /dev/null +++ b/tasks/cargo-filling/scripts/generate.mjs @@ -0,0 +1,192 @@ +#!/usr/bin/env node + +/** + * Generate output HTML from template + data for cargo-filling documents. + * + * Usage: node generate.mjs + * Example: node generate.mjs cargo-filling-1 + * + * Reads: docs/.template.html + * Reads: docs/.data.json (optional) + * Writes: docs/.output.html + * + * data.json format: + * { + * pages: [{ + * page: 1, + * cards: [{ + * index: 0, + * asteroidScale: 1.15, + * ship: { right: -10, top: -3, width: 85, height: 40 }, + * innerAst: { left: 60, top: 70 } + * }] + * }] + * } + * + * Transforms: + * - asteroidScale: scale on .space-asteroid img, .remnant-shape .shape-container, .inner-ast + * - ship: inline right/top/width/height on .cargo-ship + * - innerAst: inline left/top on .inner-ast (composed with translate and optional scale) + */ + +import { readFileSync, writeFileSync, existsSync } from 'fs'; +import { join, dirname } from 'path'; +import { fileURLToPath } from 'url'; +import { postGenerate } from '../../../src/scripts/post-generate.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const docsDir = join(__dirname, '..', 'docs'); + +const docId = process.argv[2]; +if (!docId) { + console.error('Usage: node generate.mjs '); + process.exit(1); +} + +const templatePath = join(docsDir, `${docId}.template.html`); +const dataPath = join(docsDir, `${docId}.data.json`); +const outputPath = join(docsDir, `${docId}.output.html`); + +if (!existsSync(templatePath)) { + console.error(`Template not found: ${templatePath}`); + process.exit(1); +} + +let html = readFileSync(templatePath, 'utf-8'); + +if (existsSync(dataPath)) { + const data = JSON.parse(readFileSync(dataPath, 'utf-8')); + html = applyData(html, data); + console.log(`Applied data from ${data.pages?.length || 0} pages`); +} + +writeFileSync(outputPath, html); +console.log(`Generated: ${outputPath}`); +await postGenerate(outputPath); + +function applyData(html, data) { + if (!data.pages) return html; + + // Split HTML into pages by w-[210mm] h-[297mm] divs + const pageRegex = /
= 0; i--) { + const cardData = cards[i]; + const cardIdx = cardData.index; + if (cardIdx >= cardStarts.length) continue; + + const cardStart = cardStarts[cardIdx]; + const cardEnd = cardIdx + 1 < cardStarts.length + ? cardStarts[cardIdx + 1] + : pageHtml.length; + + let cardHtml = pageHtml.slice(cardStart, cardEnd); + + const scale = cardData.asteroidScale; + const ship = cardData.ship; + const innerAst = cardData.innerAst; + + // Apply asteroidScale to .space-asteroid img + if (scale != null && scale !== 1) { + // Match
then find the [\s]*]*?)(>)/, + function(full, before, close) { + // Remove any existing style, then add new one + const cleaned = before.replace(/ style="[^"]*"/, ''); + return cleaned + ' style="transform: scale(' + scale + ')"' + close; + } + ); + } + + // Apply asteroidScale to .remnant-shape .shape-container + if (scale != null && scale !== 1) { + cardHtml = cardHtml.replace( + /
/, + '
' + ); + } + + // Apply ship position/size to .cargo-ship + if (ship) { + const shipRight = ship.right != null ? ship.right : -12.5; + const shipTop = ship.top != null ? ship.top : -5; + const shipWidth = ship.width != null ? ship.width : 90; + const shipHeight = ship.height != null ? ship.height : 42; + const shipStyle = 'right: ' + shipRight + 'mm; top: ' + shipTop + 'mm; width: ' + shipWidth + 'mm; height: ' + shipHeight + 'mm'; + + cardHtml = cardHtml.replace( + /(
])/, + function(full, tag, after) { + return tag + ' style="' + shipStyle + '"' + after; + } + ); + } + + // Apply innerAst position (and compose with asteroidScale if present) + if (innerAst || (scale != null && scale !== 1)) { + const innerLeft = innerAst && innerAst.left != null ? innerAst.left : 58; + const innerTop = innerAst && innerAst.top != null ? innerAst.top : 73; + + let transformPart = 'translate(-50%, -50%)'; + if (scale != null && scale !== 1) { + transformPart = 'translate(-50%, -50%) scale(' + scale + ')'; + } + + let innerStyle = 'left: ' + innerLeft + '%; top: ' + innerTop + '%; transform: ' + transformPart; + + // Only apply if we actually have something to change + const needsInnerChange = (innerAst && (innerAst.left != null || innerAst.top != null)) || (scale != null && scale !== 1); + if (needsInnerChange) { + cardHtml = cardHtml.replace( + /(
])/, + function(full, tag, after) { + return tag + ' style="' + innerStyle + '"' + after; + } + ); + } + } + + pageHtml = pageHtml.slice(0, cardStart) + cardHtml + pageHtml.slice(cardEnd); + } + + return pageHtml; +} diff --git a/tasks/index.html b/tasks/index.html index 6e75737..e9779bb 100644 --- a/tasks/index.html +++ b/tasks/index.html @@ -217,6 +217,20 @@
+ + +
+ Fill the Cargo Bay +
+
+

Fill the Cargo Bay

+

Split an asteroid to fill a ship’s cargo bay! The bay holds 10, but there’s already cargo inside. Break the asteroid so one piece fills the gap. Practices addition crossing 10.

+
+ 1 worksheet + Addition Through 10 +
+
+