From 1e893db2467dc407d2dff467b2001f3e5a9378d2 Mon Sep 17 00:00:00 2001 From: strdst7 Date: Mon, 14 Sep 2026 23:34:36 +0800 Subject: [PATCH] Upload files to "/" --- App.tsx | 193 +++++++++++++++++++++++++++++++++++++++++ audio.ts | 250 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ index.css | 80 +++++++++++++++++ main.tsx | 10 +++ speech.ts | 98 +++++++++++++++++++++ 5 files changed, 631 insertions(+) create mode 100644 App.tsx create mode 100644 audio.ts create mode 100644 index.css create mode 100644 main.tsx create mode 100644 speech.ts diff --git a/App.tsx b/App.tsx new file mode 100644 index 0000000..8042584 --- /dev/null +++ b/App.tsx @@ -0,0 +1,193 @@ +import { useState, useEffect } from 'react'; +import { Navbar, NavTab, AgeGroup } from './components/Navbar'; +import { StoryReader } from './components/StoryReader'; +import { StoryboardView } from './components/StoryboardView'; +import { ArtDirectionView } from './components/ArtDirectionView'; +import { CharacterVault } from './components/CharacterVault'; +import { AdaptabilityView } from './components/AdaptabilityView'; +import { FormatStrategyView } from './components/FormatStrategyView'; +import { ActivityCorner } from './components/ActivityCorner'; +import { PitchDeckModal } from './components/PitchDeckModal'; +import { BOOK_METADATA } from './data/storyData'; +import { soundManager } from './utils/audio'; +import { Heart } from 'lucide-react'; + +export function App() { + const [activeTab, setActiveTab] = useState('reader'); + const [ageGroup, setAgeGroup] = useState('pictureBook'); + const [isBedtimeMode, setIsBedtimeMode] = useState(false); + const [soundEnabled, setSoundEnabled] = useState(true); + const [isPitchDeckOpen, setIsPitchDeckOpen] = useState(false); + const [storyboardSceneId, setStoryboardSceneId] = useState(1); + + // Sync global .dark class on with Bedtime Mode so all dark: variants apply + useEffect(() => { + const root = document.documentElement; + if (isBedtimeMode) { + root.classList.add('dark'); + } else { + root.classList.remove('dark'); + } + }, [isBedtimeMode]); + + const handleExploreStoryboard = (sceneId: number) => { + soundManager.playPop(); + setStoryboardSceneId(sceneId); + setActiveTab('storyboard'); + }; + + return ( +
+ {/* Top Navigation */} + setIsPitchDeckOpen(true)} + /> + + {/* Main View Router */} +
+ {activeTab === 'reader' && ( + + )} + + {activeTab === 'storyboard' && ( + + )} + + {activeTab === 'art' && ( + + )} + + {activeTab === 'characters' && ( + + )} + + {activeTab === 'adaptability' && ( + + )} + + {activeTab === 'publishing' && ( + setIsPitchDeckOpen(true)} + /> + )} + + {activeTab === 'activities' && ( + + )} +
+ + {/* Footer */} +
+
+
+ 🦔 +
+

+ {BOOK_METADATA.title} +

+

+ Children's Storybook Concept & Interactive Story Experience +

+
+
+ +
+ + + + + + + +
+ +
+ Crafted with + + by + aimirah + | + mi4inc +
+
+
+ + {/* Editorial Pitch Deck Modal */} + setIsPitchDeckOpen(false)} + /> +
+ ); +} + +export default App; diff --git a/audio.ts b/audio.ts new file mode 100644 index 0000000..a5e5092 --- /dev/null +++ b/audio.ts @@ -0,0 +1,250 @@ +// Web Audio API Synthesizer for Children's Storybook + +class SoundEffectsManager { + private ctx: AudioContext | null = null; + public enabled: boolean = true; + + private initCtx() { + if (!this.ctx && typeof window !== 'undefined') { + const AudioCtx = window.AudioContext || (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext; + if (AudioCtx) { + this.ctx = new AudioCtx(); + } + } + if (this.ctx && this.ctx.state === 'suspended') { + this.ctx.resume(); + } + } + + // Soft cheerful pop + playPop() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'sine'; + const now = this.ctx.currentTime; + osc.frequency.setValueAtTime(400, now); + osc.frequency.exponentialRampToValueAtTime(800, now + 0.08); + + gain.gain.setValueAtTime(0.18, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.09); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.1); + } + + // Pentatonic magical chime + playChime() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const notes = [523.25, 659.25, 783.99, 1046.5]; // C5, E5, G5, C6 + notes.forEach((freq, idx) => { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + osc.type = 'triangle'; + const now = this.ctx.currentTime + idx * 0.07; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.12, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.35); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.4); + }); + } + + // Soft gentle raindrop plop + playRain() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const drops = [700, 1100, 850]; + drops.forEach((freq, i) => { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + const now = this.ctx.currentTime + i * 0.08; + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + osc.frequency.exponentialRampToValueAtTime(freq * 0.5, now + 0.07); + + gain.gain.setValueAtTime(0.14, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.12); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.15); + }); + } + + // Harp strum + playHarp() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const notes = [392.00, 523.25, 659.25, 783.99, 1046.5]; // G4, C5, E5, G5, C6 + notes.forEach((freq, i) => { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + const now = this.ctx.currentTime + i * 0.05; + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.1, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.6); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.65); + }); + } + + // Flutter / whoosh + playFlutter() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + const now = this.ctx.currentTime; + osc.type = 'sine'; + osc.frequency.setValueAtTime(220, now); + osc.frequency.linearRampToValueAtTime(440, now + 0.1); + osc.frequency.linearRampToValueAtTime(260, now + 0.2); + + gain.gain.setValueAtTime(0.08, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.22); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.25); + } + + // Playful giggle / chirp + playGiggle() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const notes = [587.33, 739.99, 587.33, 880.00]; + notes.forEach((freq, i) => { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + const now = this.ctx.currentTime + i * 0.06; + osc.type = 'triangle'; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.09, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.1); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.12); + }); + } + + // Page turn sound + playPageTurn() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const bufferSize = this.ctx.sampleRate * 0.15; + const buffer = this.ctx.createBuffer(1, bufferSize, this.ctx.sampleRate); + const data = buffer.getChannelData(0); + + for (let i = 0; i < bufferSize; i++) { + data[i] = (Math.random() * 2 - 1) * Math.exp(-i / (bufferSize * 0.3)); + } + + const noise = this.ctx.createBufferSource(); + noise.buffer = buffer; + + const filter = this.ctx.createBiquadFilter(); + filter.type = 'lowpass'; + filter.frequency.value = 1200; + + const gain = this.ctx.createGain(); + const now = this.ctx.currentTime; + gain.gain.setValueAtTime(0.12, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.14); + + noise.connect(filter); + filter.connect(gain); + gain.connect(this.ctx.destination); + + noise.start(now); + } + + // Star Celebration fanfare + playStarCelebration() { + if (!this.enabled) return; + this.initCtx(); + if (!this.ctx) return; + + const notes = [523.25, 659.25, 783.99, 1046.50, 1318.51]; + notes.forEach((freq, i) => { + if (!this.ctx) return; + const osc = this.ctx.createOscillator(); + const gain = this.ctx.createGain(); + + const now = this.ctx.currentTime + i * 0.08; + osc.type = 'sine'; + osc.frequency.setValueAtTime(freq, now); + + gain.gain.setValueAtTime(0.15, now); + gain.gain.exponentialRampToValueAtTime(0.001, now + 0.45); + + osc.connect(gain); + gain.connect(this.ctx.destination); + + osc.start(now); + osc.stop(now + 0.5); + }); + } + + playSound(type: 'pop' | 'chime' | 'rain' | 'harp' | 'flutter' | 'giggle') { + switch (type) { + case 'pop': this.playPop(); break; + case 'chime': this.playChime(); break; + case 'rain': this.playRain(); break; + case 'harp': this.playHarp(); break; + case 'flutter': this.playFlutter(); break; + case 'giggle': this.playGiggle(); break; + default: this.playPop(); + } + } +} + +export const soundManager = new SoundEffectsManager(); diff --git a/index.css b/index.css new file mode 100644 index 0000000..2bea987 --- /dev/null +++ b/index.css @@ -0,0 +1,80 @@ +@import "tailwindcss"; + +/* Class-based dark variant so Bedtime Mode toggle drives every dark: style */ +@custom-variant dark (&:where(.dark, .dark *)); + +@layer base { + body { + font-family: 'Nunito', -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; + background-color: #fbf8f2; + color: #2c2523; + overflow-x: hidden; + } + + h1, h2, h3, .font-heading { + font-family: 'Fredoka', 'Quicksand', sans-serif; + } + + .font-hand { + font-family: 'Patrick Hand', cursive; + } + + .font-reading { + font-family: 'Quicksand', 'Nunito', sans-serif; + } +} + +@layer components { + /* Custom whimsical storybook effects */ + .book-shadow { + box-shadow: 0 20px 40px -15px rgba(45, 25, 20, 0.22), + 0 0 0 1px rgba(0, 0, 0, 0.05), + 0 1px 3px rgba(0, 0, 0, 0.1); + } + + .book-spine { + box-shadow: inset 18px 0 30px -10px rgba(0, 0, 0, 0.15), + inset -4px 0 8px rgba(0, 0, 0, 0.05); + } + + .paper-texture { + background-color: #fdfaf5; + background-image: radial-gradient(#ece4d4 0.75px, transparent 0.75px), radial-gradient(#ece4d4 0.75px, #fdfaf5 0.75px); + background-size: 30px 30px; + background-position: 0 0, 15px 15px; + } + + /* Dark paper texture for Bedtime Mode */ + .dark .paper-texture { + background-color: #0f172a; + background-image: radial-gradient(rgba(99, 102, 241, 0.25) 0.75px, transparent 0.75px), radial-gradient(rgba(99, 102, 241, 0.18) 0.75px, #0f172a 0.75px); + } + + .animate-float-slow { + animation: floatSlow 5s ease-in-out infinite; + } + + .animate-pulse-gentle { + animation: gentlePulse 3.5s ease-in-out infinite; + } + + .animate-sparkle { + animation: shimmerSparkle 3s ease-in-out infinite; + } +} + +@keyframes floatSlow { + 0%, 100% { transform: translateY(0px) rotate(0deg); } + 50% { transform: translateY(-8px) rotate(1.5deg); } +} + +@keyframes gentlePulse { + 0%, 100% { opacity: 0.9; transform: scale(1); } + 50% { opacity: 1; transform: scale(1.03); } +} + +@keyframes shimmerSparkle { + 0% { transform: scale(0.8) rotate(0deg); opacity: 0.4; } + 50% { transform: scale(1.15) rotate(90deg); opacity: 1; } + 100% { transform: scale(0.8) rotate(180deg); opacity: 0.4; } +} diff --git a/main.tsx b/main.tsx new file mode 100644 index 0000000..12fa35b --- /dev/null +++ b/main.tsx @@ -0,0 +1,10 @@ +import { StrictMode } from "react"; +import { createRoot } from "react-dom/client"; +import "./index.css"; +import App from "./App"; + +createRoot(document.getElementById("root")!).render( + + + +); diff --git a/speech.ts b/speech.ts new file mode 100644 index 0000000..2e881f4 --- /dev/null +++ b/speech.ts @@ -0,0 +1,98 @@ +export class StorySpeechNarrator { + private synth: SpeechSynthesis | null = null; + private currentUtterance: SpeechSynthesisUtterance | null = null; + private onEndCallback: (() => void) | null = null; + private onWordCallback: ((wordIndex: number) => void) | null = null; + + constructor() { + if (typeof window !== 'undefined' && 'speechSynthesis' in window) { + this.synth = window.speechSynthesis; + } + } + + public isAvailable(): boolean { + return this.synth !== null; + } + + public speak( + text: string, + options?: { + rate?: number; + pitch?: number; + onEnd?: () => void; + onWord?: (index: number) => void; + } + ) { + if (!this.synth) return; + this.stop(); + + const utterance = new SpeechSynthesisUtterance(text); + utterance.rate = options?.rate ?? 0.9; // Slightly slower for children's story cadence + utterance.pitch = options?.pitch ?? 1.05; // Slightly warmer/friendly pitch + + // Find warm friendly voice + const voices = this.synth.getVoices(); + const naturalVoice = voices.find(v => + v.lang.startsWith('en') && + (v.name.includes('Natural') || v.name.includes('Samantha') || v.name.includes('Karen') || v.name.includes('Daniel') || v.name.includes('Google')) + ) || voices.find(v => v.lang.startsWith('en')); + + if (naturalVoice) { + utterance.voice = naturalVoice; + } + + this.onEndCallback = options?.onEnd || null; + this.onWordCallback = options?.onWord || null; + + utterance.onend = () => { + this.currentUtterance = null; + if (this.onEndCallback) this.onEndCallback(); + }; + + utterance.onerror = () => { + this.currentUtterance = null; + if (this.onEndCallback) this.onEndCallback(); + }; + + let wordCount = 0; + utterance.onboundary = (e) => { + if (e.name === 'word') { + if (this.onWordCallback) { + this.onWordCallback(wordCount); + } + wordCount++; + } + }; + + this.currentUtterance = utterance; + this.synth.speak(utterance); + } + + public stop() { + if (!this.synth) return; + this.synth.cancel(); + this.currentUtterance = null; + } + + public pause() { + if (this.synth) { + this.synth.pause(); + } + } + + public resume() { + if (this.synth) { + this.synth.resume(); + } + } + + public isSpeaking(): boolean { + return this.synth ? this.synth.speaking : false; + } + + public getCurrentUtterance(): SpeechSynthesisUtterance | null { + return this.currentUtterance; + } +} + +export const speechNarrator = new StorySpeechNarrator();