205 lines
8.8 KiB
JavaScript
205 lines
8.8 KiB
JavaScript
/**
|
|
* Counting Worksheet Generator
|
|
* Generates randomized counting exercises with themed objects
|
|
*/
|
|
|
|
// Simple themed object SVG shapes
|
|
const themedObjects = {
|
|
star: `<polygon points='12,0 15,8 24,9 17,15 19,24 12,19 5,24 7,15 0,9 9,8' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
heart: `<path d='M12,21 L2,11 C-2,7 2,0 8,0 C10,0 12,2 12,4 C12,2 14,0 16,0 C22,0 26,7 22,11 Z' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
circle: `<circle cx='12' cy='12' r='10' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
apple: `<path d='M12,4 Q12,0 16,0 M8,6 Q0,6 0,14 Q0,24 12,24 Q24,24 24,14 Q24,6 16,6 Q12,2 8,6 Z' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
fish: `<path d='M0,12 Q6,0 18,6 L24,0 L24,24 L18,18 Q6,24 0,12 Z M16,10 A1,1 0 1,1 16,11' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
flower: `<circle cx='12' cy='12' r='4' fill='none' stroke='#000000' stroke-width='2'/><circle cx='12' cy='4' r='4' fill='none' stroke='#000000' stroke-width='1.5'/><circle cx='19' cy='8' r='4' fill='none' stroke='#000000' stroke-width='1.5'/><circle cx='19' cy='16' r='4' fill='none' stroke='#000000' stroke-width='1.5'/><circle cx='12' cy='20' r='4' fill='none' stroke='#000000' stroke-width='1.5'/><circle cx='5' cy='16' r='4' fill='none' stroke='#000000' stroke-width='1.5'/><circle cx='5' cy='8' r='4' fill='none' stroke='#000000' stroke-width='1.5'/>`,
|
|
butterfly: `<ellipse cx='6' cy='8' rx='6' ry='8' fill='none' stroke='#000000' stroke-width='2'/><ellipse cx='18' cy='8' rx='6' ry='8' fill='none' stroke='#000000' stroke-width='2'/><ellipse cx='6' cy='20' rx='5' ry='6' fill='none' stroke='#000000' stroke-width='2'/><ellipse cx='18' cy='20' rx='5' ry='6' fill='none' stroke='#000000' stroke-width='2'/><line x1='12' y1='2' x2='12' y2='26' stroke='#000000' stroke-width='2'/>`,
|
|
ball: `<circle cx='12' cy='12' r='11' fill='none' stroke='#000000' stroke-width='2'/><path d='M1,12 Q12,8 23,12' fill='none' stroke='#000000' stroke-width='1'/><path d='M1,12 Q12,16 23,12' fill='none' stroke='#000000' stroke-width='1'/>`,
|
|
moon: `<path d='M18,2 A14,14 0 1,0 18,22 A10,10 0 1,1 18,2' fill='none' stroke='#000000' stroke-width='2'/>`,
|
|
cloud: `<path d='M8,18 A6,6 0 0,1 8,6 A8,8 0 0,1 20,8 A5,5 0 0,1 20,18 Z' fill='none' stroke='#000000' stroke-width='2'/>`
|
|
};
|
|
|
|
const objectNames = Object.keys(themedObjects);
|
|
|
|
/**
|
|
* Generate random quantities that are all different
|
|
*/
|
|
function generateRandomQuantities(count, maxValue) {
|
|
const quantities = [];
|
|
const used = new Set();
|
|
|
|
while (quantities.length < count) {
|
|
const num = Math.floor(Math.random() * maxValue) + 1;
|
|
if (!used.has(num)) {
|
|
used.add(num);
|
|
quantities.push(num);
|
|
}
|
|
}
|
|
|
|
// Shuffle the array
|
|
return quantities.sort(() => Math.random() - 0.5);
|
|
}
|
|
|
|
/**
|
|
* Pick object type based on theme keywords with randomization
|
|
*/
|
|
function pickObjectForTheme(theme, llmSuggestion) {
|
|
// Try to use LLM suggestion first if valid
|
|
if (llmSuggestion && themedObjects[llmSuggestion]) {
|
|
return llmSuggestion;
|
|
}
|
|
|
|
if (!theme) {
|
|
return objectNames[Math.floor(Math.random() * objectNames.length)];
|
|
}
|
|
|
|
const themeLower = theme.toLowerCase();
|
|
|
|
// Direct matches
|
|
for (const name of objectNames) {
|
|
if (themeLower.includes(name)) {
|
|
return name;
|
|
}
|
|
}
|
|
|
|
// Build list of ALL matching shapes, then pick randomly
|
|
const associations = {
|
|
star: ['space', 'night', 'sky', 'twinkle', 'magic', 'galaxy', 'astronaut', 'king', 'crown', 'royal'],
|
|
heart: ['love', 'valentine', 'romance', 'caring', 'friendship', 'kind', 'help'],
|
|
circle: ['round', 'bubble', 'dot'],
|
|
apple: ['fruit', 'food', 'tree', 'garden', 'healthy', 'snack', 'farm', 'autumn', 'fall'],
|
|
fish: ['ocean', 'sea', 'water', 'aquarium', 'swim', 'underwater', 'beach', 'marine', 'animal'],
|
|
flower: ['garden', 'plant', 'nature', 'spring', 'rose', 'bloom', 'petal', 'beautiful'],
|
|
butterfly: ['insect', 'bug', 'garden', 'spring', 'colorful', 'wings', 'animal', 'fly'],
|
|
ball: ['sport', 'play', 'game', 'soccer', 'football', 'basketball', 'tennis', 'cricket', 'kick'],
|
|
moon: ['night', 'sleep', 'bedtime', 'dream', 'space', 'lunar', 'dark'],
|
|
cloud: ['sky', 'weather', 'rain', 'sunny', 'day', 'fluffy', 'soft']
|
|
};
|
|
|
|
// Collect ALL matching shapes
|
|
const matchingShapes = [];
|
|
for (const [objectType, keywords] of Object.entries(associations)) {
|
|
if (keywords.some(keyword => themeLower.includes(keyword))) {
|
|
matchingShapes.push(objectType);
|
|
}
|
|
}
|
|
|
|
// If we found matches, pick one randomly for variety
|
|
if (matchingShapes.length > 0) {
|
|
return matchingShapes[Math.floor(Math.random() * matchingShapes.length)];
|
|
}
|
|
|
|
// Random fallback
|
|
return objectNames[Math.floor(Math.random() * objectNames.length)];
|
|
}
|
|
|
|
/**
|
|
* Generate a group of objects at a position
|
|
*/
|
|
function generateObjectGroup(objectType, count, x, y, groupWidth) {
|
|
const objectSvg = themedObjects[objectType] || themedObjects.star;
|
|
const objectSize = 22;
|
|
const maxPerRow = 5;
|
|
|
|
let svg = '';
|
|
|
|
if (count <= maxPerRow) {
|
|
// Single row
|
|
const spacing = Math.min(28, (groupWidth - count * objectSize) / (count + 1));
|
|
let startX = x + spacing;
|
|
|
|
for (let i = 0; i < count; i++) {
|
|
svg += `<g transform='translate(${startX + i * (objectSize + spacing)}, ${y})'>${objectSvg}</g>`;
|
|
}
|
|
} else {
|
|
// Two rows for larger counts
|
|
const topRowCount = Math.ceil(count / 2);
|
|
const bottomRowCount = count - topRowCount;
|
|
const spacing = Math.min(26, (groupWidth - topRowCount * objectSize) / (topRowCount + 1));
|
|
|
|
// Top row
|
|
let startX = x + spacing;
|
|
for (let i = 0; i < topRowCount; i++) {
|
|
svg += `<g transform='translate(${startX + i * (objectSize + spacing)}, ${y - 12})'>${objectSvg}</g>`;
|
|
}
|
|
|
|
// Bottom row
|
|
startX = x + spacing + (topRowCount - bottomRowCount) * (objectSize + spacing) / 2;
|
|
for (let i = 0; i < bottomRowCount; i++) {
|
|
svg += `<g transform='translate(${startX + i * (objectSize + spacing)}, ${y + 14})'>${objectSvg}</g>`;
|
|
}
|
|
}
|
|
|
|
return svg;
|
|
}
|
|
|
|
/**
|
|
* Generate an empty answer box
|
|
*/
|
|
function generateAnswerBox(x, y, size = 40) {
|
|
return `<rect x='${x}' y='${y}' width='${size}' height='${size}' fill='#FFFFFF' stroke='#333333' stroke-width='2' rx='5'/>`;
|
|
}
|
|
|
|
/**
|
|
* Generate complete counting worksheet
|
|
* @param {Object} params
|
|
* @param {string} params.theme - Theme name (optional)
|
|
* @param {string} params.shape - Shape chosen by LLM (e.g. 'fish', 'star')
|
|
* @param {number[]} params.quantities - Array of quantities from LLM
|
|
* @param {number} params.age - Child's age
|
|
*/
|
|
export function generateCountingWorksheet({ theme, shape, quantities, age }) {
|
|
// Use hybrid approach: LLM suggestion + programmatic randomization from matching shapes
|
|
const objectType = pickObjectForTheme(theme, shape);
|
|
|
|
// ALWAYS generate random quantities for variety (LLM is deterministic)
|
|
const maxValue = age <= 4 ? 5 : 10;
|
|
const rowCount = 8;
|
|
const finalQuantities = generateRandomQuantities(rowCount, maxValue);
|
|
|
|
const rowHeight = 62; // 500 / 8 = 62.5, fill entire height
|
|
const startY = 0; // Start at absolute top
|
|
const objectAreaWidth = 320;
|
|
const answerBoxX = 440;
|
|
const answerBoxSize = 42;
|
|
|
|
let objectRows = '';
|
|
|
|
for (let i = 0; i < rowCount; i++) {
|
|
const rowY = startY + i * rowHeight;
|
|
const quantity = finalQuantities[i];
|
|
|
|
// Dotted separator line between rows (except first row)
|
|
if (i > 0) {
|
|
objectRows += `<line x1='15' y1='${rowY}' x2='485' y2='${rowY}' stroke='#CCCCCC' stroke-width='1' stroke-dasharray='5,5'/>`;
|
|
}
|
|
|
|
// Row number indicator - vertically centered in row
|
|
objectRows += `<text x='30' y='${rowY + 38}' font-family='Arial, sans-serif' font-size='14' font-weight='bold' fill='#AAAAAA'>${i + 1}.</text>`;
|
|
|
|
// Objects to count - positioned in middle of row
|
|
objectRows += generateObjectGroup(objectType, quantity, 50, rowY + 20, objectAreaWidth);
|
|
|
|
// Equals sign
|
|
objectRows += `<text x='405' y='${rowY + 40}' font-family='Arial, sans-serif' font-size='20' font-weight='bold' fill='#666666'>=</text>`;
|
|
|
|
// Answer box
|
|
objectRows += generateAnswerBox(answerBoxX, rowY + 10, answerBoxSize);
|
|
}
|
|
|
|
const svg = `<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 500 500'>
|
|
<!-- Counting rows -->
|
|
${objectRows}
|
|
</svg>`;
|
|
|
|
return {
|
|
title: `Count the ${objectType.charAt(0).toUpperCase() + objectType.slice(1)}s!`,
|
|
subtitle: 'Count each group and write the number',
|
|
sections: [{
|
|
id: 'counting-main',
|
|
type: 'counting',
|
|
content_svg: svg
|
|
}]
|
|
};
|
|
}
|
|
|
|
export default { generateCountingWorksheet };
|