"Where ink becomes voice, and voice becomes echo."
InkEcho is a cinematic, mood-adaptive poetry anthology built as a single-page web application. It treats reading as an immersive, atmospheric experience rather than a static page: the environment, lighting, particles, and motion all shift to match the emotional resonance of the poems on screen.
This repository contains two things:
- The React application (
src/,index.html) - the main, production InkEcho experience. - The original HTML prototype (
Poems/) - the earlier static single-file version that inspired the React build. It is preserved here as a historical reference and is linked from the app's nav as "archive".
- Mood-adaptive atmosphere - Selecting a mood (Ether, Pulse, Shadow, Astral, Bloom, Tide) re-themes the entire page: background gradient, accent color, WebGL ink shader hue, and decorative particles all respond.
- Procedural WebGL ink shader - A custom GLSL fragment shader (
InkShader.tsx) renders flowing, domain-warped "ink in water" aurora bands that drift behind the content and lerp smoothly between mood colors. - Cinematic loading screen - A word-by-word reveal of the tagline ("ink becomes voice becomes echo") with a glowing radial bloom and an animated progress bar.
- 3D tilt poem cards - Each card reacts to the cursor with a perspective tilt and a moving glare, plus tactile "tape" strips and a gradient underline on the title (inspired by the original
Poems/wall). - WhisperReader - Opens a poem in a full-screen modal where the text reveals character-by-character with a staggered animation (powered by anime.js), dims the world around it, and tracks reading progress with a top progress bar.
- Lenis smooth scrolling - Buttery, inertia-based smooth scrolling on the page, with native scrolling preserved inside the modal via
data-lenis-prevent. - Animated mesh + noise overlays - A subtle moving grid (
mesh-overlay) and a film-grain noise layer give the whole site a tactile, filmic texture. - Custom cursor - A difference-blend cursor that grows on interactive elements (desktop only; auto-disabled on touch devices).
- Floating meteors - Ambient drifting accent particles in the background.
- Liquid-glass nav - The
InkEchowordmark sits in a frosted, animated-shine glass chip; the "archive" link opens the original HTML prototype. - Responsive & accessible - Mobile-aware touch handling,
prefers-reduced-motionsupport, and semantic structure.
| Layer | Technology |
|---|---|
| Framework | React 18 + TypeScript |
| Build tool | Vite 5 |
| Animation | Framer Motion, GSAP (+ ScrollTrigger), anime.js |
| 3D / Shader | React Three Fiber, Three.js, custom GLSL |
| Smooth scroll | Lenis |
| Styling | Tailwind CSS 3, PostCSS, Autoprefixer |
| Icons | Lucide React |
| Fonts | Playfair Display (display), Cormorant Garamond (body), Inter (UI), Allura (script) |
ink-echo/
βββ .github/
β βββ workflows/
β βββ deploy.yml # GitHub Actions: builds and deploys to GitHub Pages (gh-pages)
βββ Poems/ # Original static HTML prototype (reference + "archive" link)
β βββ index.html # The earlier single-file version
β βββ digital_cage.html # Standalone poem page from the prototype
β βββ inkechov2.html # Alternate prototype iteration
β βββ script.js # JS for the prototype (theme toggle, floating particles)
β βββ styles.css # CSS for the prototype
βββ src/
β βββ main.tsx # React entry point (mounts <App/> in StrictMode)
β βββ App.tsx # Root composition: layout, loading, modal, mood, Lenis init
β βββ index.css # Global styles, Tailwind layers, glass utils, decor keyframes
β βββ types.ts # TypeScript interfaces: Poem, ThemeConfig, MoodOption, Emotion
β βββ components/
β β βββ Nav.tsx # Fixed top nav: liquid-glass InkEcho logo + archive link
β β βββ Hero.tsx # Full-screen hero: GSAP char animation, parallax, shimmer divider
β β βββ LoadingScreen.tsx # Cinematic intro with word reveal + progress bar
β β βββ InkShader.tsx # WebGL ink-in-water background (React Three Fiber + GLSL)
β β βββ Meteors.tsx # Ambient drifting meteor particles
β β βββ CustomCursor.tsx # Difference-blend custom cursor (desktop only)
β β βββ MoodSelector.tsx # Mood filter buttons (Ether/Pulse/Shadow/Astral/Bloom/Tide)
β β βββ PoemCard.tsx # 3D tilt card with tape strips + gradient title underline
β β βββ PoemDecor.tsx # Per-emotion decorative SVG/motifs + floating motes
β β βββ PoemModal.tsx # Immersive reading modal (progress bar, copy, WhisperReader)
β β βββ WhisperReader.tsx # Character-by-character text reveal engine (anime.js)
β βββ data/
β β βββ poems.ts # All poem content + MOODS + THEMES config + getTheme()
β βββ lib/
β βββ smoothScroll.ts # Lenis singleton wrapper (init/get/destroy)
βββ index.html # Vite HTML entry, fonts, meta/OG tags
βββ vite.config.ts # Vite config (base: './' for GitHub Pages)
βββ tailwind.config.js # Tailwind theme: fonts, colors, keyframes
βββ postcss.config.js # PostCSS + Tailwind + Autoprefixer
βββ tsconfig.json # TypeScript config
βββ package.json # Scripts and dependencies
βββ README.md # This file
index.html- The Vite HTML shell. Loads Google Fonts (Playfair Display, Cormorant Garamond, Inter, Allura), sets the page title, meta description, theme color, and Open Graph tags for social sharing. Mounts#rootwhere React renders.src/main.tsx- React entry point. CallscreateRoot(document.getElementById('root'))and renders<App />wrapped in<React.StrictMode>. Imports the globalindex.css.vite.config.ts- Vite configuration. Uses@vitejs/plugin-react. Setsbase: './'so built asset paths are relative (required for GitHub Pages subpath hosting). Output goes todist/.tailwind.config.js- Extends Tailwind with the project's font families (serif,sans,display,script), custom colors (ink,gold), thenoisebackground image, and keyframes/animations (shimmer,flow,aurora,floaty,expandWidth).postcss.config.js- Wires Tailwind and Autoprefixer into the build pipeline.tsconfig.json- Strict TypeScript config (target ES2020, bundler resolution, React JSX transform).noUnusedLocals/noUnusedParametersare relaxed to avoid build breaks on intentional placeholders.
-
src/App.tsx- The root component. Responsibilities:- Manages
loading,activeMood,selected(poem for modal), andshowTop(back-to-top button) state. - Renders the layered background stack:
InkShader(WebGL),Meteors,mesh-overlay,noise-overlay,CustomCursor,Nav. - Shows
LoadingScreenviaAnimatePresenceuntil the timed intro completes. - Renders
Hero, the featured poem block, the#wallanthology section (withMoodSelector+ grid ofPoemCards), and the footer. - Initializes Lenis smooth scroll on mount (with StrictMode-safe cleanup that nulls the singleton so the second dev mount re-inits cleanly).
- Provides
scrollToWall()(smooth-scrolls to the anthology) andscrollTop()(back to top) using the Lenis instance. - Renders
PoemModaland the floating back-to-top button viaAnimatePresence.
- Manages
-
src/types.ts- Type definitions:Emotion- union of all mood/emotion tags.Poem- a poem'sid,title,preview,body,category,date,emotion, optionalfeaturedandauthor.ThemeConfig- per-mood visual config (base,accent,background,text,secondary,gradient,particleColor,rgb).MoodOption-{ id, label, description }for the mood selector.
-
src/index.css- Global stylesheet. Contains:- Tailwind base/components/utilities layers.
html/bodyreset withoverflow-x: clip(deliberately NOThidden, which would break Lenis window scrolling).- Lenis baseline classes (
.lenis,.lenis-smooth,.lenis-stopped, etc.). .mesh-overlay(animated grid) +@keyframes meshMove..noise-overlay(film grain).- Glass utilities
.glass-panel/.glass-card. .liquid-logo+.liquid-logo-shine(nav wordmark glass chip)..poem-card-title(gradient underline),.text-glow,.perspective-1000,.hide-scrollbar..custom-cursorstyles.- Decor keyframes:
decor-float,decor-twinkle,decor-pulse,decor-drift,decor-wave,shimmer,gradient-shift,meteor-fall,liquid-shine. prefers-reduced-motionblock that disables animations for accessibility.
-
src/components/Nav.tsx- Fixed top navigation. Left: amotion.aliquid-glass chip containing theInkEchoscript wordmark (links to/Poems/index.html). Right: an "archive" pill link (also to/Poems/index.html, opens in a new tab) with a small status dot. -
src/components/Hero.tsx- Full-viewport hero. Uses GSAP to animate each title character up into place (gsap.from('.hero-title .char', ...)) and to stagger in the subtitle/button lines. Mouse movement drives a parallax tilt on the title via Framer Motion motion values + springs. Includes the shimmer divider under the title and the "Enter the anthology" CTA that callsonExplore(scrolls to the wall). -
src/components/LoadingScreen.tsx- Full-screen intro shown on first load. Reveals the tagline words one at a time, animates theInkEchowordmark in, shows a radial bloom, and fills a gradient progress bar (0 to 100%) as words appear. CallsonDoneafter the sequence to dismiss. -
src/components/InkShader.tsx- React Three Fiber<Canvas>rendering a full-screen quad with a custom GLSL fragment shader. The shader produces flowing, domain-warped fbm "ink" with aurora bands, a vignette, and film grain. TheuColoruniform lerps toward the active mood's RGB each frame, so the background color transitions smoothly when the mood changes. -
src/components/Meteors.tsx- Renders a set of absolutely-positioned.meteorspans that fall diagonally on a loop. Count is reduced on mobile (touch devices). Purely decorative,pointer-events: none. -
src/components/CustomCursor.tsx- A fixed div that follows the mouse with easing and toggles anis-hoverclass when over links/buttons/[data-cursor="hover"]. Usesmix-blend-mode: difference. Disabled on coarse-pointer (touch) devices. -
src/components/MoodSelector.tsx- Row of mood buttons built fromMOODS. The active mood gets a highlighted border, accent-colored label, and a colored glow. Clicking callsonChange(id)to re-filter the wall. -
src/components/PoemCard.tsx- Amotion.buttonrepresenting one poem. On hover (non-touch) it tilts in 3D (rotateX/rotateYsprings) and shows a moving glare. Contains "tape" strips and aPoemDecorlayer. UseslayoutId={poem-${poem.id}}so it can smoothly expand into the modal (shared-layout transition). Title uses the.poem-card-titlegradient underline. -
src/components/PoemDecor.tsx- Per-emotion decorative SVG/motifs rendered behind the text inside both cards and the modal. Every emotion also gets a set of floating "motes" (small glowing dots) for a living feel. TheLongingcase uses a seamless tiling wave. -
src/components/PoemModal.tsx- Full-screen immersive reader. Renders a dark scrim, a top reading-progress bar (scaleXdriven by scroll), the poem header (category, date, reading time, title, copy + close buttons), a divider, and a scrollable body (data-lenis-preventso native scroll works while Lenis is paused). Body containsPoemDecor+ a readability scrim +WhisperReader. On open it pauses Lenis and locksbodyoverflow; on close it restores them. Supports Escape to close. -
src/components/WhisperReader.tsx- Splits the poem body into lines and characters, wrapping each character in a.whisper-charspan. On mount, anime.js staggers their opacity/translateY into view. Accent color is applied per character.
-
src/data/poems.ts- The content layer. Exports:POEMS- the array of all poem objects (including thefeaturedDigital Cage piece by "Div").MOODS- the mood filter options (Ether/All, Pulse/Love, Shadow/Melancholy, Astral/Dreams, Bloom/Nature, Tide/Longing).THEMES- theRecord<string, ThemeConfig>mapping each mood to its visual palette.getTheme(emotion)- helper that returns the matchingThemeConfig(falls back to theAlltheme).
-
src/lib/smoothScroll.ts- Lenis wrapper.initSmoothScroll()creates a single Lenis instance (duration 1.1, custom easing, smooth wheel) driven by arequestAnimationFrameloop, and returns it (idempotent).getLenis()returns the current instance.destroySmoothScroll()destroys it and nulls the singleton (StrictMode-safe).
Poems/index.html- The earlier static, single-file InkEcho (light/dark theme toggle, poem wall rendered byscript.js, modal reader). Linked from the app as "archive".Poems/styles.css- Styles for the prototype (mesh background, shimmer header rule, tape strips, gradient underlines, floating particles) - the visual language that inspired several React ported elements.Poems/script.js- Prototype logic: theme toggle, dynamic poem rendering, modal open/close, advanced floating elements.Poems/digital_cage.html- A standalone poem page from the prototype.Poems/inkechov2.html- An alternate prototype iteration.
Prerequisites: Node.js 18+ (Node 20 recommended).
-
Clone the repository
git clone https://github.com/divicoded/InkEcho.git cd InkEcho -
Install dependencies
npm install
-
Start the development server
npm run dev
Vite will print a local URL (default
http://localhost:5173). -
Open your browser at the printed URL.
npm run build- Type-check (tsc -b) and produce a production build indist/.npm run preview- Serve the productiondist/build locally for testing.
This repo is configured for automatic deployment to GitHub Pages via the workflow in .github/workflows/deploy.yml:
- The workflow triggers on every push to the
mainbranch. - It checks out the code, sets up Node 20, runs
npm install, thennpm run build. - It deploys the
dist/folder to thegh-pagesbranch usingJamesIves/github-pages-deploy-action. - In the repository Settings > Pages, set the source to the
gh-pagesbranch (root). The site will then be available athttps://divicoded.github.io/InkEcho/.
Note:
vite.config.tsusesbase: './'so the built assets use relative paths and work correctly under the GitHub Pages subpath.
- Reduced motion: users with
prefers-reduced-motion: reduceget animations disabled and instant scroll behavior. - Touch devices: the custom cursor and 3D tilt are disabled; meteor count is reduced.
- Scroll: Lenis provides smooth scrolling on the page; inside the poem modal,
data-lenis-preventrestores native scrolling so long poems are readable. - Performance: the WebGL shader uses a single full-screen quad with
antialias: falseand capped DPR; meteors and decor are CSS-driven andpointer-events: none.
Designed & developed by Divi Coded (Div). A digital sanctuary for words left unspoken.
This project is provided as-is for personal and educational use. The poems and artwork are the property of their respective authors.