Skip to content

Commit a816e8a

Browse files
committed
Add area and perimeter problem pages, including Count Area with Unit Squares, Rectangle Area & Perimeter, Composite Figure Area, and Perimeter on Grid Shapes. Implement corresponding JavaScript logic and visual rendering. Update site catalog and styles for new problem types.
1 parent ef09415 commit a816e8a

13 files changed

Lines changed: 897 additions & 1 deletion

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Visit the live site and pick a topic from the homepage. Each problem page shows
1515
- **Customary Units of Length:** Using 3 One or Two Digit Numbers, Units Up to 100, Appropriate Metric Unit of Length
1616
- **Word Problems:** Length Word Problems
1717
- **Basic Operations:** Addition & Subtraction (0–20), Flashcards: Add & Subtract (0–20), Multiplication (0–10)
18+
- **Area & Perimeter:** Count Area with Unit Squares, Rectangle Area & Perimeter, Composite Figure Area, Perimeter on Grid Shapes
1819
- **Time:** Telling Time Problems, Read Clock Time (Minute Tick Marks)
1920

2021
## Current Stack

area-count-unit-squares.html

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8">
5+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
6+
<title>Count Area with Unit Squares</title>
7+
<link rel="stylesheet" href="styles.css">
8+
</head>
9+
<body>
10+
<div class="main-content">
11+
<div class="problem-area">
12+
<div class="header-container">
13+
<a href="index.html" class="home-button">🏠 Home</a>
14+
<h1>🟨 Count Area with Unit Squares</h1>
15+
<button class="toggle-scratchpad" id="toggle-scratchpad">📝 Open Scratchpad</button>
16+
</div>
17+
<div class="container">
18+
<div class="problem" id="problem-text">Loading problem...</div>
19+
<div class="options" id="options-container"></div>
20+
<div class="navigation">
21+
<button class="nav-button" id="prev-problem">← Previous</button>
22+
<button class="nav-button" id="next-problem">Next →</button>
23+
</div>
24+
</div>
25+
</div>
26+
<div class="scratchpad-area" id="scratchpad-area">
27+
<div class="scratchpad-controls">
28+
<button class="scratchpad-button" id="undo-btn">↩️ Undo</button>
29+
<button class="scratchpad-button" id="redo-btn">↪️ Redo</button>
30+
<button class="scratchpad-button clear" id="clear-btn">🗑️ Clear</button>
31+
<button class="scratchpad-button" id="close-scratchpad">❌ Close</button>
32+
</div>
33+
<canvas class="scratchpad-canvas" id="scratchpad"></canvas>
34+
</div>
35+
</div>
36+
<div class="problem-type">
37+
Count Area with Unit Squares
38+
</div>
39+
<div class="stars" id="stars-container"></div>
40+
<script src="area-count-unit-squares.js" type="module" defer></script>
41+
</body>
42+
</html>

area-count-unit-squares.js

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import { pickRandom } from './shared.js';
2+
import { createNumberOptions, randomInt, startMultipleChoicePage } from './generated-multiple-choice.js';
3+
import { calculateColumnShapeArea, renderColumnShapeSvg } from './area-visuals.js';
4+
5+
const units = [
6+
{ singular: 'inch', plural: 'inches' },
7+
{ singular: 'foot', plural: 'feet' },
8+
{ singular: 'centimeter', plural: 'centimeters' },
9+
];
10+
11+
function formatAreaLabel(value, unit) {
12+
return `${value} square ${value === 1 ? unit.singular : unit.plural}`;
13+
}
14+
15+
function formatSingleSquare(unit) {
16+
return `1 square ${unit.singular}`;
17+
}
18+
19+
function generateColumns() {
20+
const width = randomInt(3, 5);
21+
const columns = Array.from({ length: width }, () => randomInt(1, 3));
22+
23+
if (columns.every((height) => height === columns[0])) {
24+
const index = randomInt(0, width - 1);
25+
columns[index] = Math.min(columns[index] + 1, 4);
26+
}
27+
28+
return columns;
29+
}
30+
31+
function generateProblem() {
32+
const unit = pickRandom(units);
33+
const columns = generateColumns();
34+
const answer = calculateColumnShapeArea(columns);
35+
36+
return {
37+
promptHtml: `
38+
<div class="visual-problem">
39+
<p class="problem-question">What is the area of the figure?</p>
40+
<div class="diagram-panel">
41+
${renderColumnShapeSvg({ columns })}
42+
</div>
43+
<p class="diagram-caption">Each small square covers ${formatSingleSquare(unit)}.</p>
44+
</div>
45+
`,
46+
options: createNumberOptions(answer, (value) => formatAreaLabel(value, unit), {
47+
min: 1,
48+
max: answer + 8,
49+
offsets: [-4, -3, -2, -1, 1, 2, 3, 4],
50+
}),
51+
correctValue: String(answer),
52+
};
53+
}
54+
55+
startMultipleChoicePage({ generateProblem });

area-visuals.js

Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
function arrowDefs(prefix) {
2+
return `
3+
<defs>
4+
<marker id="${prefix}-arrow-end" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
5+
<path d="M 0 0 L 10 5 L 0 10 z" fill="#35556a"></path>
6+
</marker>
7+
</defs>
8+
`;
9+
}
10+
11+
export function getColumnShapeCells(columns) {
12+
const width = columns.length;
13+
const height = Math.max(...columns);
14+
const cells = [];
15+
16+
columns.forEach((columnHeight, columnIndex) => {
17+
for (let level = 0; level < columnHeight; level += 1) {
18+
cells.push({
19+
x: columnIndex,
20+
y: height - level - 1,
21+
});
22+
}
23+
});
24+
25+
return {
26+
cells,
27+
width,
28+
height,
29+
};
30+
}
31+
32+
export function calculateColumnShapeArea(columns) {
33+
return columns.reduce((sum, value) => sum + value, 0);
34+
}
35+
36+
export function calculateColumnShapePerimeter(columns) {
37+
const { cells } = getColumnShapeCells(columns);
38+
const occupied = new Set(cells.map((cell) => `${cell.x},${cell.y}`));
39+
let perimeter = 0;
40+
41+
for (const cell of cells) {
42+
const neighbors = [
43+
[cell.x + 1, cell.y],
44+
[cell.x - 1, cell.y],
45+
[cell.x, cell.y + 1],
46+
[cell.x, cell.y - 1],
47+
];
48+
49+
neighbors.forEach(([x, y]) => {
50+
if (!occupied.has(`${x},${y}`)) {
51+
perimeter += 1;
52+
}
53+
});
54+
}
55+
56+
return perimeter;
57+
}
58+
59+
export function renderColumnShapeSvg({ columns }) {
60+
const { cells, width, height } = getColumnShapeCells(columns);
61+
const cellSize = 52;
62+
const padding = 16;
63+
const svgWidth = width * cellSize + padding * 2;
64+
const svgHeight = height * cellSize + padding * 2;
65+
66+
const outlineRects = [];
67+
for (let row = 0; row < height; row += 1) {
68+
for (let column = 0; column < width; column += 1) {
69+
outlineRects.push(`
70+
<rect
71+
class="grid-outline"
72+
x="${padding + column * cellSize}"
73+
y="${padding + row * cellSize}"
74+
width="${cellSize}"
75+
height="${cellSize}"
76+
></rect>
77+
`);
78+
}
79+
}
80+
81+
const filledRects = cells
82+
.map(
83+
(cell) => `
84+
<rect
85+
class="shape-cell"
86+
x="${padding + cell.x * cellSize}"
87+
y="${padding + cell.y * cellSize}"
88+
width="${cellSize}"
89+
height="${cellSize}"
90+
></rect>
91+
`
92+
)
93+
.join('');
94+
95+
return `
96+
<svg class="shape-svg" viewBox="0 0 ${svgWidth} ${svgHeight}" aria-hidden="true" role="img">
97+
${outlineRects.join('')}
98+
${filledRects}
99+
</svg>
100+
`;
101+
}
102+
103+
export function renderRectangleDiagram({ width, height, unitLabel }) {
104+
const scale = Math.max(26, Math.min(52, 420 / width, 240 / height));
105+
const x = 92;
106+
const y = 76;
107+
const rectWidth = width * scale;
108+
const rectHeight = height * scale;
109+
const svgWidth = x + rectWidth + 72;
110+
const svgHeight = y + rectHeight + 60;
111+
const defsPrefix = 'rect-dim';
112+
113+
return `
114+
<svg class="shape-svg" viewBox="0 0 ${svgWidth} ${svgHeight}" aria-hidden="true" role="img">
115+
${arrowDefs(defsPrefix)}
116+
<rect class="rectangle-fill" x="${x}" y="${y}" width="${rectWidth}" height="${rectHeight}" rx="12"></rect>
117+
118+
<line class="dimension-cap" x1="${x}" y1="${y}" x2="${x}" y2="${y - 20}"></line>
119+
<line class="dimension-cap" x1="${x + rectWidth}" y1="${y}" x2="${x + rectWidth}" y2="${y - 20}"></line>
120+
<line
121+
class="dimension-line"
122+
x1="${x}"
123+
y1="${y - 28}"
124+
x2="${x + rectWidth}"
125+
y2="${y - 28}"
126+
marker-start="url(#${defsPrefix}-arrow-end)"
127+
marker-end="url(#${defsPrefix}-arrow-end)"
128+
></line>
129+
<text class="dimension-text" x="${x + rectWidth / 2}" y="${y - 38}" text-anchor="middle">${width} ${unitLabel}</text>
130+
131+
<line class="dimension-cap" x1="${x}" y1="${y}" x2="${x - 20}" y2="${y}"></line>
132+
<line class="dimension-cap" x1="${x}" y1="${y + rectHeight}" x2="${x - 20}" y2="${y + rectHeight}"></line>
133+
<line
134+
class="dimension-line"
135+
x1="${x - 28}"
136+
y1="${y}"
137+
x2="${x - 28}"
138+
y2="${y + rectHeight}"
139+
marker-start="url(#${defsPrefix}-arrow-end)"
140+
marker-end="url(#${defsPrefix}-arrow-end)"
141+
></line>
142+
<text
143+
class="dimension-text"
144+
x="${x - 40}"
145+
y="${y + rectHeight / 2}"
146+
text-anchor="middle"
147+
dominant-baseline="middle"
148+
transform="rotate(-90 ${x - 40} ${y + rectHeight / 2})"
149+
>${height} ${unitLabel}</text>
150+
</svg>
151+
`;
152+
}
153+
154+
export function renderLShapeDiagram({ totalWidth, topHeight, legWidth, extraHeight, unitLabel }) {
155+
const scale = Math.max(26, Math.min(38, 420 / totalWidth, 250 / (topHeight + extraHeight)));
156+
const x = 92;
157+
const y = 70;
158+
const totalHeight = topHeight + extraHeight;
159+
const rightWidth = totalWidth - legWidth;
160+
const shapePath = [
161+
`M ${x} ${y}`,
162+
`L ${x + totalWidth * scale} ${y}`,
163+
`L ${x + totalWidth * scale} ${y + topHeight * scale}`,
164+
`L ${x + legWidth * scale} ${y + topHeight * scale}`,
165+
`L ${x + legWidth * scale} ${y + totalHeight * scale}`,
166+
`L ${x} ${y + totalHeight * scale}`,
167+
'Z',
168+
].join(' ');
169+
const defsPrefix = 'composite-dim';
170+
const svgWidth = x + totalWidth * scale + 84;
171+
const svgHeight = y + totalHeight * scale + 84;
172+
173+
return `
174+
<svg class="shape-svg" viewBox="0 0 ${svgWidth} ${svgHeight}" aria-hidden="true" role="img">
175+
${arrowDefs(defsPrefix)}
176+
<path class="composite-fill" d="${shapePath}"></path>
177+
178+
<line class="dimension-cap" x1="${x}" y1="${y}" x2="${x}" y2="${y - 20}"></line>
179+
<line class="dimension-cap" x1="${x + totalWidth * scale}" y1="${y}" x2="${x + totalWidth * scale}" y2="${y - 20}"></line>
180+
<line
181+
class="dimension-line"
182+
x1="${x}"
183+
y1="${y - 28}"
184+
x2="${x + totalWidth * scale}"
185+
y2="${y - 28}"
186+
marker-start="url(#${defsPrefix}-arrow-end)"
187+
marker-end="url(#${defsPrefix}-arrow-end)"
188+
></line>
189+
<text class="dimension-text" x="${x + (totalWidth * scale) / 2}" y="${y - 38}" text-anchor="middle">${totalWidth} ${unitLabel}</text>
190+
191+
<line class="dimension-cap" x1="${x}" y1="${y}" x2="${x - 20}" y2="${y}"></line>
192+
<line class="dimension-cap" x1="${x}" y1="${y + totalHeight * scale}" x2="${x - 20}" y2="${y + totalHeight * scale}"></line>
193+
<line
194+
class="dimension-line"
195+
x1="${x - 28}"
196+
y1="${y}"
197+
x2="${x - 28}"
198+
y2="${y + totalHeight * scale}"
199+
marker-start="url(#${defsPrefix}-arrow-end)"
200+
marker-end="url(#${defsPrefix}-arrow-end)"
201+
></line>
202+
<text
203+
class="dimension-text"
204+
x="${x - 40}"
205+
y="${y + (totalHeight * scale) / 2}"
206+
text-anchor="middle"
207+
dominant-baseline="middle"
208+
transform="rotate(-90 ${x - 40} ${y + (totalHeight * scale) / 2})"
209+
>${totalHeight} ${unitLabel}</text>
210+
211+
<line class="dimension-cap" x1="${x + legWidth * scale}" y1="${y + topHeight * scale}" x2="${x + legWidth * scale}" y2="${y + totalHeight * scale + 18}"></line>
212+
<line class="dimension-cap" x1="${x + totalWidth * scale}" y1="${y + topHeight * scale}" x2="${x + totalWidth * scale}" y2="${y + totalHeight * scale + 18}"></line>
213+
<line
214+
class="dimension-line"
215+
x1="${x + legWidth * scale}"
216+
y1="${y + totalHeight * scale + 26}"
217+
x2="${x + totalWidth * scale}"
218+
y2="${y + totalHeight * scale + 26}"
219+
marker-start="url(#${defsPrefix}-arrow-end)"
220+
marker-end="url(#${defsPrefix}-arrow-end)"
221+
></line>
222+
<text
223+
class="dimension-text"
224+
x="${x + legWidth * scale + (rightWidth * scale) / 2}"
225+
y="${y + totalHeight * scale + 44}"
226+
text-anchor="middle"
227+
>${rightWidth} ${unitLabel}</text>
228+
229+
<line class="dimension-cap" x1="${x + legWidth * scale}" y1="${y + topHeight * scale}" x2="${x + legWidth * scale + 18}" y2="${y + topHeight * scale}"></line>
230+
<line class="dimension-cap" x1="${x + legWidth * scale}" y1="${y + totalHeight * scale}" x2="${x + legWidth * scale + 18}" y2="${y + totalHeight * scale}"></line>
231+
<line
232+
class="dimension-line"
233+
x1="${x + legWidth * scale + 26}"
234+
y1="${y + topHeight * scale}"
235+
x2="${x + legWidth * scale + 26}"
236+
y2="${y + totalHeight * scale}"
237+
marker-start="url(#${defsPrefix}-arrow-end)"
238+
marker-end="url(#${defsPrefix}-arrow-end)"
239+
></line>
240+
<text
241+
class="dimension-text"
242+
x="${x + legWidth * scale + 40}"
243+
y="${y + topHeight * scale + (extraHeight * scale) / 2}"
244+
text-anchor="middle"
245+
dominant-baseline="middle"
246+
transform="rotate(-90 ${x + legWidth * scale + 40} ${y + topHeight * scale + (extraHeight * scale) / 2})"
247+
>${extraHeight} ${unitLabel}</text>
248+
</svg>
249+
`;
250+
}

0 commit comments

Comments
 (0)