feat: Extend worksheet generation age range to 10 years, refine generator parameters, and add PDF watermarks.

This commit is contained in:
francy 2026-02-07 15:36:17 +01:00
parent ed9c118fef
commit 5630c6cee7
9 changed files with 172 additions and 160 deletions

File diff suppressed because one or more lines are too long

View File

@ -120,7 +120,7 @@ const illustrations = {
<circle cx='15' cy='30' r='2' fill='none' stroke='#000' stroke-width='1'/>
<path d='M10,40 Q15,45 20,42' fill='none' stroke='#000' stroke-width='1'/>
<path d='M25,75 L25,95' stroke='#000' stroke-width='3'/>
<path d='M40' y1='75' L40,95' stroke='#000' stroke-width='3'/>
<path d='M40,75 L40,95' stroke='#000' stroke-width='3'/>
<path d='M60,75 L60,95' stroke='#000' stroke-width='3'/>
<path d='M75,75 L75,95' stroke='#000' stroke-width='3'/>
<path d='M85,50 Q100,45 95,55 Q105,60 90,65' fill='none' stroke='#000' stroke-width='2'/>

View File

@ -19,6 +19,19 @@ const themedObjects = {
const objectNames = Object.keys(themedObjects);
/**
* Pluralize object names correctly
*/
function pluralize(word) {
const irregulars = {
'fish': 'Fish',
'butterfly': 'Butterflies',
'moon': 'Moons'
};
if (irregulars[word]) return irregulars[word];
return word.charAt(0).toUpperCase() + word.slice(1) + 's';
}
/**
* Generate random quantities that are all different
*/
@ -151,7 +164,16 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
const objectType = pickObjectForTheme(theme, shape);
// ALWAYS generate random quantities for variety (LLM is deterministic)
const maxValue = age <= 4 ? 5 : 10;
let maxValue;
if (age <= 4) {
maxValue = 5;
} else if (age <= 6) {
maxValue = 10;
} else if (age <= 8) {
maxValue = 15;
} else {
maxValue = 20;
}
const rowCount = 8;
const finalQuantities = generateRandomQuantities(rowCount, maxValue);
@ -169,7 +191,7 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
// 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'/>`;
objectRows += `<line x1='15' y1='${rowY}' x2='485' y2='${rowY}' stroke='#AAAAAA' stroke-width='1' stroke-dasharray='5,5'/>`;
}
// Row number indicator - vertically centered in row
@ -191,7 +213,7 @@ export function generateCountingWorksheet({ theme, shape, quantities, age }) {
</svg>`;
return {
title: `Count the ${objectType.charAt(0).toUpperCase() + objectType.slice(1)}s!`,
title: `Count the ${pluralize(objectType)}!`,
subtitle: 'Count each group and write the number',
sections: [{
id: 'counting-main',

View File

@ -44,7 +44,7 @@ function generateMazeGrid(rows, cols, startEdge, endEdge) {
startRow = 0; startCol = 0;
break;
case 'bottom':
startRow = rows - 1; startCol = rows - 1;
startRow = rows - 1; startCol = cols - 1;
break;
case 'left':
startRow = 0; startCol = 0;
@ -169,9 +169,15 @@ export function generateMazeWorksheet({ theme, difficulty = 1, age }) {
} else if (age <= 5 || difficulty === 2) {
rows = 5;
cols = 5;
} else {
} else if (age <= 7) {
rows = 6;
cols = 6;
} else if (age <= 9) {
rows = 7;
cols = 7;
} else {
rows = 8;
cols = 8;
}
// Randomly select start and end edges - favor opposite edges for better gameplay

View File

@ -96,7 +96,7 @@ function generateCharacterRow(text, y, baseScale = 1, color = '#000000') {
*/
function generateBaseline(y, width = 400) {
const startX = (500 - width) / 2;
return `<line x1='${startX}' y1='${y}' x2='${startX + width}' y2='${y}' stroke='#CCCCCC' stroke-width='1'/>`;
return `<line x1='${startX}' y1='${y}' x2='${startX + width}' y2='${y}' stroke='#AAAAAA' stroke-width='1'/>`;
}
/**

View File

@ -10,8 +10,7 @@ import { GoogleGenerativeAI } from '@google/generative-ai';
import {
generateWritingWorksheet,
generateCountingWorksheet,
generateMazeWorksheet,
generateColoringPage
generateMazeWorksheet
} from './generators/index.js';
dotenv.config();
@ -75,9 +74,9 @@ function addToGallery(item) {
const parameterPrompts = {
writing: (theme, age) => {
const randomSeed = Math.floor(Math.random() * 1000);
const wordLength = `${age} letters`;
const numberRange = age <= 4 ? '1-5' : '1-9';
const numberCount = age <= 4 ? '3-4' : '4-5';
const wordLength = age <= 4 ? '3-4 letters' : age <= 6 ? '4-5 letters' : age <= 8 ? '5-6 letters' : '6-8 letters';
const numberRange = age <= 4 ? '1-5' : age <= 6 ? '1-9' : age <= 8 ? '1-15' : '1-20';
const numberCount = age <= 4 ? '3-4' : age <= 6 ? '4-5' : '5-6';
return `
Create a children's tracing worksheet for theme "${theme}".
@ -109,7 +108,7 @@ CRITICAL: You MUST return JSON with BOTH "shape" AND "quantities":
{"shape": "star", "quantities": [3, 1, 5, 2, 7, 4, 8, 6]}
- "shape": Choose ONE shape that matches "${theme}". Use the random seed ${randomSeed} to vary your choice - don't always pick the same shape for this theme.
- "quantities": Array of exactly 8 different numbers (1-${age <= 4 ? 5 : 10})
- "quantities": Array of exactly 8 different numbers (1-${age <= 4 ? 5 : age <= 6 ? 10 : age <= 8 ? 15 : 20})
Return ONLY valid JSON. No explanation.
`;
@ -126,7 +125,9 @@ Return ONLY a JSON object:
Rules:
- difficulty 1 = easy (4x4 grid) for age 3-4
- difficulty 2 = medium (5x5 grid) for age 5-6
- difficulty 3 = hard (6x6 grid) for age 7+
- difficulty 3 = hard (6x6 grid) for age 7-8
- difficulty 4 = challenging (7x7 grid) for age 9
- difficulty 5 = expert (8x8 grid) for age 10
Return ONLY valid JSON, no explanation.
`,
@ -183,7 +184,7 @@ app.post('/api/surprise', async (req, res) => {
Current timestamp: ${timestamp}
Generate ONE creative, fun, specific theme for a children's worksheet (e.g. "Space-Rex Coding", "Underwater Hamster Tea Party").
Pick a RANDOM age between 3 and 7.
Pick a RANDOM age between 3 and 10.
Pick a RANDOM activity type with EQUAL PROBABILITY from these 4 options:
1. "coloring" - coloring pages

View File

@ -50,96 +50,69 @@ Age: ${age} years old
🎯 YOUR GOAL: Create a coloring page with ONE clear subject that a 3-year-old can immediately identify as "${theme}".
🚨 ABSOLUTE SVG REQUIREMENTS (CRITICAL - NON-NEGOTIABLE):
- EVERY single path MUST have fill="none"
- EVERY single path MUST have stroke="black" or stroke="#000000"
- Use stroke-width="2" or "3" for all outlines
- NO filled shapes allowed - children need empty white areas to color
- Example: <path d="M 100 100 L 200 100" fill="none" stroke="black" stroke-width="2"/>
🚨 ABSOLUTE QUALITY RULES (CRITICAL - NON-NEGOTIABLE):
1. USE WHITE FILLS: Every closed shape (head, body, legs, eyes) MUST have fill="#FFFFFF". This hides lines behind it.
2. DRAWING ORDER: Draw from BACK to FRONT. Background parts first (tail, back legs), then main body, then front details (eyes, nose).
3. CLOSED PATHS: All shapes must be fully closed paths so they can be filled with white.
4. NO OVERLAPPING MESS: White fills solve this. If you draw a leg over a body, the white fill of the leg must hide the body line behind it.
📐 STEP-BY-STEP SVG CONSTRUCTION METHOD:
🚨 SVG REQUIREMENTS (CRITICAL):
- Background: <rect width="500" height="500" fill="#FFFFFF"/> (Start with white canvas)
- Stroke: stroke="black" or stroke="#000000"
- Stroke Width: stroke-width="2" for main outlines, "2" for details
- Fill: fill="#FFFFFF" for all object parts (to block lines behind)
- Fill: fill="none" ONLY for open paths like whiskers or smiles
- Example Closed Shape: <path d="M..." fill="#FFFFFF" stroke="black" stroke-width="3"/>
- Example Open Line: <path d="M..." fill="none" stroke="black" stroke-width="2"/>
For ANIMALS (cat, dog, panda, unicorn, etc.):
1. HEAD: Draw a large circle or oval for head (100-150px diameter), centered around x=250, y=200
2. EARS: Add 2 rounded ears on top of head (circles, triangles, or ovals)
3. EYES: 2 circles (30-40px diameter) positioned symmetrically
4. NOSE: Small circle or triangle in center below eyes
5. MOUTH: Simple curved line (smile) below nose
6. BODY: Large oval or rounded rectangle below head (150-200px wide, 180-220px tall)
7. LEGS: 4 rounded rectangles or ovals extending down from body
8. TAIL: Curved path on one side
9. DETAILS: Add characteristic features (whiskers, spots, horn, etc.)
📐 STEP-BY-STEP CONSTRUCTION METHOD (Layer by Layer):
For OBJECTS (car, rocket, house, etc.):
1. MAIN BODY: Start with basic shape (rectangle for car/house, elongated triangle for rocket)
2. MAJOR COMPONENTS: Add 2-4 large recognizable parts (wheels, windows, door, roof)
3. DETAILS: Add smaller features that make it identifiable
For ANIMALS (Draw in this order):
1. BACK LEGS/TAIL (Furthest back): Draw simple shapes, fill="#FFFFFF", stroke="black"
2. BODY (Middle): Large oval/shape, fill="#FFFFFF", stroke="black" (Hides the top of back legs)
3. HEAD (Front): Large circle/shape, fill="#FFFFFF", stroke="black" (Hides neck/body connection)
4. FRONT LEGS (Front): Shapes overlapping body, fill="#FFFFFF", stroke="black"
5. FACE DETAILS (Topmost): Eyes, nose, mouth. Eyes can have black pupils.
For PLANTS (flower, tree, etc.):
1. CENTER/TRUNK: Main central element
2. PETALS/LEAVES: 5-8 symmetrical rounded shapes around center
3. STEM: Vertical line or path
4. BASE: Ground line or pot
For OBJECTS (Car, Rocket, etc):
1. MAIN CHASSIS/BODY: Large filled shape (fill="#FFFFFF")
2. WHEELS/WINGS: Draw on top or behind as needed
3. WINDOWS/DOORS: Draw on top of body (fill="#FFFFFF")
4. DETAILS: Lines, handles, bolts
🎨 SPECIFIC THEME EXAMPLES:
For PLANTS:
1. STEM/BRANCHES: Main lines
2. LEAVES BEHIND: Fill="#FFFFFF"
3. FLOWER CENTER: Fill="#FFFFFF" (Draw last so it's on top)
4. PETALS: Fill="#FFFFFF"
UNICORN:
- Circle head (cx="250" cy="180", r="70")
- 2 triangle ears
- HORN: Triangle pointing up from forehead (30-40px tall)
- 2 circle eyes
- Small nose circle
- Curved smile
- Oval body below head
- 4 rounded rectangle legs
- Flowing curved tail on right side
- Mane: 3-4 curved strokes on neck
🎨 SPECIFIC THEME GUIDES:
PANDA:
- Large circle head (r="80")
- 2 small circle ears on top
- 2 LARGE circle eye patches (r="25", OUTLINED not filled)
- 2 smaller circle eyes inside patches
- Small circle nose
- Smile below nose
- Large oval body
- 4 thick rounded legs
- Optional: bamboo stick to the side (2 parallel lines)
UNICORN:
- Body: Oval (fill="#FFFFFF")
- Legs: 4 rectangles (fill="#FFFFFF") - make sure they attach naturally
- Head: Oval/Horse shape (fill="#FFFFFF")
- START with separate shapes, but ensure they LOOK connected
- Horn: Triangle on forehead
- Mane/Tail: Flowing shapes
CAT:
- Circle head (r="60")
- 2 triangle ears pointing up
- 2 circle eyes
- Small triangle nose
- Whiskers (3 lines each side)
- Curved smile
- Oval body
- 4 legs
- Curved S-shaped tail
ROBOT:
- Use geometric shapes (rectangles, circles)
- Fill ALL shapes with #FFFFFF to look solid
- Add buttons/panels on top
FLOWER:
- Center circle (r="40")
- 6-8 rounded petal paths around center (use ellipses or bezier curves)
- Straight stem line down from center
- 2 leaf shapes on stem (ellipses at angle)
- Simple ground line
🔍 QUALITY CHECKLIST - YOUR SVG MUST:
Have 8-15 large closed shapes ready for coloring
Be IMMEDIATELY recognizable as "${theme}"
Use ONLY stroke outlines (fill="none" on ALL paths)
Have bold stroke-width="2" or "3"
Be 500x500 viewBox with subject centered
Include characteristic features (horn for unicorn, eye patches for panda, etc.)
Have simple, rounded, child-friendly shapes
Look like it came from a store-bought coloring book
🔍 QUALITY CHECKLIST:
Is the main subject clearly visible?
Are lines hidden where shapes overlap? (e.g. body line not visible through arm)
Is everything filled with white (except open lines)?
Is the stroke width consistent (bold 3px)?
Is the image centered in 500x500?
AVOID:
- Abstract or geometric art
- Filled/colored areas
- Tiny details (too hard for kids)
- Complex overlapping shapes
- Unrecognizable forms
- fill="none" on main shapes (causes messy overlaps)
- Translucent/opacity effects
- Tiny dust/specks
- Disconnected lines that should be closed
${baseRules}
${ageRules(age)}

View File

@ -110,7 +110,10 @@ function App() {
{/* Footer */}
<footer className="bg-white border-t border-slate-200 mt-20 py-12">
<div className="max-w-7xl mx-auto px-4 text-center text-slate-500 text-sm">
<p>© 2024 WonderSheets. All rights reserved.</p>
<p className="mb-2">
<a href="https://wondersheets.art" className="font-semibold text-primary-600 hover:text-primary-700">wondersheets.art</a>
</p>
<p>© {new Date().getFullYear()} WonderSheets. All rights reserved.</p>
</div>
</footer>
</div>

View File

@ -135,6 +135,13 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
const pdfHeight = (imgProperties.height * pdfWidth) / imgProperties.width;
pdf.addImage(data, 'PNG', 0, 0, pdfWidth, pdfHeight);
// Add watermark at bottom center
const pageHeight = pdf.internal.pageSize.getHeight();
pdf.setFontSize(10);
pdf.setTextColor(150, 150, 150); // Light gray
pdf.text('wondersheets.art', pdfWidth / 2, pageHeight - 5, { align: 'center' });
return pdf;
};
@ -190,19 +197,19 @@ export const Hero = ({ onGenerate, generatedSheet, isGenerating }) => {
<div className="space-y-2">
<div className="flex justify-between items-center px-1">
<label className="font-semibold text-slate-700">Age: {age}</label>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">3 to 7 years</span>
<span className="text-xs font-semibold text-slate-400 uppercase tracking-wider">3 to 10 years</span>
</div>
<input
type="range"
min="3"
max="7"
max="10"
value={age}
disabled={isGenerating}
onChange={(e) => setAge(parseInt(e.target.value))}
className="w-full h-3 bg-slate-100 rounded-lg appearance-none cursor-pointer accent-primary-600 disabled:opacity-50"
/>
<div className="flex justify-between text-xs text-slate-400 px-1">
<span>3</span><span>4</span><span>5</span><span>6</span><span>7</span>
<span>3</span><span>4</span><span>5</span><span>6</span><span>7</span><span>8</span><span>9</span><span>10</span>
</div>
</div>