Dhuni is a full-screen sacred listening landscape for Indian sound. Built by Samooh.
The browser window itself is the world:
- layered atmosphere and horizon depth
- one constant central dhuni fire (always burning)
- six pixel listener-characters gathered around the fire
- six ritu world-state shifts per station
- minimal floating overlays for metadata and controls
No framed stage. No dashboard shell. No visible YouTube UI.
This refactor preserves the proven architecture:
- static Vite build for GitHub Pages
- YouTube playlist-backed stations
- hidden iframe player abstraction
- local station config file
- keyboard/media controls and local storage preferences
- Vite
- React + TypeScript
- Plain CSS (layered world composition)
- YouTube Iframe Player API wrapper (
src/lib/youtubePlayer.ts)
npm install
npm run devBuild production assets:
npm run build
npm run previewsrc/components/DhuniScene.tsx: world composition and character interactionsrc/components/LandscapeLayers.tsx: sky/horizon/terrain/foreground depth layerssrc/components/FireSprite.tsx: hybrid sacred fire (flame layers, coals, embers, glow)src/components/CharacterSprite.tsx: pixel listener spritessrc/components/TopHud.tsx: minimal top metadata overlaysrc/components/ControlDock.tsx: minimal floating control dock
Edit src/data/stations.ts.
Each station maps to:
- one character
- one season state
- one playlist
- one world palette/camera profile
Important fields:
id,name,tagline,playlistId- optional:
startIndex(legacy override, usually not needed) season,mood,tradition,descriptionspriteType,environmentTypepalette(sky,horizon,terrain,fog,ember,hudtones)
If you want to update stations quickly, collect this per season:
- required:
playlistId(YouTube playlist ID only, not full URL) - optional:
startIndex(0-based station entry override; shuffle is enabled by default) - recommended:
nameandtagline - optional:
mood,tradition,description
Playback behavior:
- Dhuni enables playlist shuffle on station load.
- Dhuni keeps same-session memory per station and attempts to skip if a station immediately lands on the exact same video when you return.
Current seasonal worlds:
- Vasanta
- Grishma
- Varsha
- Sharad
- Hemanta
- Shishira
Space: play/pauseArrowLeft: previous stationArrowRight: next stationArrowUp: volume upArrowDown: volume downM: mute/unmuteHome: first stationEnd: last stationEnter: activate focused character
Selection, scene state, and playback stay synchronized.
- full-screen world remains intact
- all 6 characters remain visible and tappable
- top HUD condenses
- control dock stays minimal and reachable
- interaction does not depend on hover
Dhuni uses Media Session API where supported for:
- play/pause
- previous/next station
- metadata updates
Web limitation:
- browsers do not reliably expose OS hardware volume key interception to web apps.
- use in-app volume slider and ArrowUp/ArrowDown.
- some browsers may still force Picture-in-Picture behavior for backgrounded media; Dhuni applies best-effort iframe flags to reduce this, but full suppression is not guaranteed in every browser setting.
Browsers may block autoplay until a direct user interaction occurs.
Dhuni handles this with:
- interaction-led playback attempts
- graceful retry/error state when blocked
- GA4 (
gtag) is loaded fromindex.htmlfor analytics. - Dhuni attempts autoplay on initial load, so YouTube requests can begin on first paint.
- Dhuni uses
youtube-nocookie.comfor embed host, which can reduce cookie use in some cases. - YouTube may still call Google/DoubleClick endpoints for video delivery and ads. That behavior is controlled by YouTube.
- Metadata and schema live in
index.html. - Social/brand assets live in
public/:og-image.png(1200x630)favicon-32.pngapple-touch-icon.png
- To regenerate PNG assets:
python3 scripts/generate_assets.pyThis remains static-only and GitHub Pages compatible.
vite.config.ts:
const base = process.env.VITE_BASE_PATH ?? '/';- Custom domain (e.g.
dhuni.net): use/ - Project subpath deploy:
VITE_BASE_PATH=/repo/ npm run build
Current workflow deploys from main with VITE_BASE_PATH=/.