📚 AI Storybook Generator

Core Source Code & Interactive Storytelling Implementation

React 19.1+ TypeScript Gemini 2.5 Flash Imagen 4.0 Canvas API

🔍 About This Code Showcase

This curated code snippet demonstrates how the AI Storybook Generator creates interactive, multi-page storybooks with AI-generated narratives and coloring book illustrations.

Full deployment scripts, API integrations, and proprietary details are omitted for clarity and security. This showcase highlights the core story generation, image creation, and interactive canvas algorithms.

📖 Core Algorithm: Interactive Storybook Engine

The foundation of the AI Storybook Generator is its ability to create cohesive, multi-page narratives with matching illustrations, while providing interactive coloring and text editing capabilities:

📚 App.tsx - Main Storybook Controller
import React, { useState, useCallback } from 'react'; import { StoryPage } from './types'; import { generateStoryPage } from './services/geminiService'; import ColoringCanvas from './components/ColoringCanvas'; const MAX_PAGES = 8; // Maximum pages per storybook function App() { const [theme, setTheme] = useState(''); const [pages, setPages] = useState<StoryPage[]>([]); const [currentPageIndex, setCurrentPageIndex] = useState(0); const [isLoading, setIsLoading] = useState(false); // Generate the first page of a new storybook const handleGenerateFirstPage = async (e: React.FormEvent) => { e.preventDefault(); if (!inputTheme.trim()) return; setIsLoading(true); setError(null); setPages([]); setTheme(inputTheme); try { // Generate story text and illustration for first page const { storyText, imageBase64 } = await generateStoryPage(inputTheme, 1); const newPage: StoryPage = { id: `page-${Date.now()}`, pageNumber: 1, storyText, imageBase64, textItems: [], // Empty array for drag-and-drop text elements }; setPages([newPage]); setCurrentPageIndex(0); } catch (err) { setError('Oh no! Our storybook magic failed. Please try again.'); console.error(err); } finally { setIsLoading(false); } }; // Generate sequential pages with narrative continuity const handleGenerateNextPage = useCallback(async () => { if (pages.length >= MAX_PAGES) return; setIsLoading(true); setError(null); // Pass previous story context for narrative continuity const previousStory = pages.map(p => p.storyText); try { // Generate next page with story context const { storyText, imageBase64 } = await generateStoryPage( theme, pages.length + 1, previousStory ); const newPage: StoryPage = { id: `page-${Date.now()}`, pageNumber: pages.length + 1, storyText, imageBase64, textItems: [], }; const updatedPages = [...pages, newPage]; setPages(updatedPages); setCurrentPageIndex(updatedPages.length - 1); // Navigate to new page } catch (err) { setError('Could not create the next page. Please try again.'); console.error(err); } finally { setIsLoading(false); } }, [pages, theme]); // Save page modifications (coloring, text positions) const handleSavePage = useCallback((pageData: Partial<StoryPage>) => { setPages(currentPages => currentPages.map((p, index) => index === currentPageIndex ? { ...p, ...pageData } : p ) ); }, [currentPageIndex]); return ( <div className="storybook-app"> {pages.length > 0 && ( <ColoringCanvas page={pages[currentPageIndex]} onSavePage={handleSavePage} /> )} </div> ); }

🤖 Two-Stage AI Generation Pipeline

The storybook generator uses a sophisticated two-stage AI pipeline that creates coherent narratives with matching visual illustrations:

🧠 geminiService.ts - AI Story & Image Generation
import { GoogleGenAI } from "@google/genai"; const ai = new GoogleGenAI({ apiKey: process.env.API_KEY }); // Stage 1: Generate contextual story text with narrative continuity async function generateStoryText( theme: string, pageNumber: number, previousStory?: string[] ): Promise<string> { let prompt = `You are a creative storyteller for children aged 3-6. Your task is to write a single page for a storybook. The main theme of the story is "${theme}". This is page ${pageNumber}. Each page must be a complete, self-contained story that doesn't end on a cliffhanger, but should logically follow previous pages.`; // Add story context for narrative continuity if (previousStory && previousStory.length > 0) { prompt += `\n\nHere is the story so far:\n`; previousStory.forEach((pageText, index) => { prompt += `Page ${index + 1}: ${pageText}\n`; }); prompt += `\nNow, write the text for Page ${pageNumber}. Continue the adventure based on the theme and story so far, but make sure this new page also works as a standalone story.`; } else { prompt += `\nThis is the first page. Please start the story.`; } prompt += `\n\nThe story for this new page should be very short and simple, just one or two sentences.`; try { const response = await ai.models.generateContent({ model: 'gemini-2.5-flash', contents: prompt, }); return response.text.trim(); } catch (error) { console.error("Error generating story text:", error); throw new Error("Failed to generate story text from AI."); } } // Stage 2: Generate coloring book style illustration async function generateStoryImage(storyText: string): Promise<string> { const prompt = `Create a black and white, simple, bold outline coloring book illustration for a child. The style should be very clean with thick lines and large, easy-to-color areas, suitable for flood-fill coloring. Do not include any shading or complex details. The scene should depict: "${storyText}"`; try { const response = await ai.models.generateImages({ model: 'imagen-4.0-generate-001', prompt: prompt, config: { numberOfImages: 1, outputMimeType: 'image/png', aspectRatio: '1:1', // Square format for consistent layout safetyFilterLevel: 'BLOCK_MOST', // Child-safe content }, }); // Convert to base64 for canvas rendering const imageBuffer = response.images[0].data; const base64Image = btoa( new Uint8Array(imageBuffer).reduce( (data, byte) => data + String.fromCharCode(byte), '' ) ); return `data:image/png;base64,${base64Image}`; } catch (error) { console.error("Error generating story image:", error); throw new Error("Failed to generate story image from AI."); } } // Combined function: Generate complete story page export async function generateStoryPage( theme: string, pageNumber: number, previousStory?: string[] ): Promise<{ storyText: string; imageBase64: string; }> { // Step 1: Generate contextual story text const storyText = await generateStoryText(theme, pageNumber, previousStory); // Step 2: Generate matching coloring book illustration const imageBase64 = await generateStoryImage(storyText); return { storyText, imageBase64 }; }

🎨 Interactive Coloring Canvas Engine

The coloring canvas provides advanced interactive features including flood-fill coloring, drag-and-drop text editing, and touch-optimized controls:

🖍️ ColoringCanvas.tsx - Interactive Canvas Implementation
import React, { useRef, useEffect, useState, useCallback } from 'react'; import { StoryPage, DraggableTextItem } from '../types'; // 16-color palette optimized for children const PALETTE_COLORS = [ '#FF5733', '#33FF57', '#3357FF', '#FF33A1', '#A133FF', '#33FFA1', '#FFFF33', '#FF8C33', '#8C33FF', '#33FFEC', '#EC33FF', '#FFC300', '#000000', '#FFFFFF', '#C7C7C7', '#8B4513' ]; type Tool = 'brush' | 'text'; const ColoringCanvas: React.FC<{ page: StoryPage; onSavePage: (pageData: Partial<StoryPage>) => void; }> = ({ page, onSavePage }) => { const canvasRef = useRef<HTMLCanvasElement>(null); const [currentTool, setCurrentTool] = useState<Tool>('brush'); const [selectedColor, setSelectedColor] = useState(PALETTE_COLORS[0]); const [textItems, setTextItems] = useState<DraggableTextItem[]>(page.textItems || []); // Advanced flood-fill algorithm for area coloring const floodFill = useCallback(( canvas: HTMLCanvasElement, x: number, y: number, fillColor: string ) => { const ctx = canvas.getContext('2d'); if (!ctx) return; const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height); const data = imageData.data; // Get target color at click position const targetColor = getColorAtPosition(data, x, y, canvas.width); const fillColorRgb = hexToRgb(fillColor); // Prevent unnecessary fills if (colorsEqual(targetColor, fillColorRgb)) return; // Optimized flood-fill using queue-based approach const stack = [{ x, y }]; const visited = new Set<string>(); while (stack.length > 0) { const { x: currentX, y: currentY } = stack.pop()!; const key = `${currentX},${currentY}`; if (visited.has(key)) continue; visited.add(key); // Boundary checks if (currentX < 0 || currentX >= canvas.width || currentY < 0 || currentY >= canvas.height) continue; const currentColor = getColorAtPosition(data, currentX, currentY, canvas.width); // Only fill if color matches target if (!colorsEqual(currentColor, targetColor)) continue; // Set new color setColorAtPosition(data, currentX, currentY, canvas.width, fillColorRgb); // Add neighboring pixels to stack stack.push( { x: currentX + 1, y: currentY }, { x: currentX - 1, y: currentY }, { x: currentX, y: currentY + 1 }, { x: currentX, y: currentY - 1 } ); } // Apply changes to canvas ctx.putImageData(imageData, 0, 0); saveCanvasState(); }, [selectedColor]); // Handle canvas interactions (touch and mouse) const handleCanvasInteraction = useCallback((e: React.MouseEvent<HTMLCanvasElement>) => { const canvas = canvasRef.current; if (!canvas) return; const rect = canvas.getBoundingClientRect(); const scaleX = canvas.width / rect.width; const scaleY = canvas.height / rect.height; const x = Math.floor((e.clientX - rect.left) * scaleX); const y = Math.floor((e.clientY - rect.top) * scaleY); if (currentTool === 'brush') { // Flood-fill coloring floodFill(canvas, x, y, selectedColor); } else if (currentTool === 'text') { // Add draggable text element addTextElement(x, y); } }, [currentTool, selectedColor, floodFill]); // Save canvas state and text positions const saveCanvasState = useCallback(() => { const canvas = canvasRef.current; if (!canvas) return; // Convert canvas to base64 for storage const coloredImageData = canvas.toDataURL('image/png'); onSavePage({ coloredImage: coloredImageData, textItems: textItems }); }, [textItems, onSavePage]); return ( <div className="coloring-canvas-container"> <canvas ref={canvasRef} onClick={handleCanvasInteraction} className="interactive-canvas" /> {/* Color Palette */} <div className="color-palette"> {PALETTE_COLORS.map(color => ( <button key={color} onClick={() => setSelectedColor(color)} style={{ backgroundColor: color }} className={selectedColor === color ? 'selected' : ''} /> ))} </div> {/* Draggable Text Elements */} {textItems.map(item => ( <DraggableText key={item.id} item={item} onUpdate={updateTextItem} onDelete={() => deleteTextItem(item.id)} /> ))} </div> ); };

⚙️ Technical Implementation Notes

Key Algorithms & Innovations

Why This Approach Works