diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc194d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.DS_Store +*.log +.env +dist/ +tmp/ diff --git a/CNAME b/CNAME new file mode 100644 index 0000000..87b8e6d --- /dev/null +++ b/CNAME @@ -0,0 +1 @@ +dev.discoverygubbio.com \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..5ce92fb --- /dev/null +++ b/README.md @@ -0,0 +1,88 @@ +# InsideGubbio AR + +Piattaforma di realtà aumentata per esplorare i monumenti di Gubbio. + +## Funzionalità + +- **Geolocalizzazione** — Rileva automaticamente la posizione dell'utente e identifica i monumenti vicini +- **Fotocamera AR** — Inquadra i monumenti per attivare l'esperienza in realtà aumentata +- **Audio guide** — Ascolta le storie di ogni monumento con il toggle audio +- **Pannello informativo** — Leggi descrizioni dettagliate che compaiono dal basso +- **Personaggi storici** — Incontra personaggi legati a ogni monumento +- **Decorazioni AR** — Particelle ed effetti visivi unici per ogni monumento +- **Percorsi guidati** — Scegli tra diversi itinerari tematici + +## Monumenti + +| Monumento | Epoca | Categoria | +|-----------|-------|-----------| +| Palazzo dei Consoli | XIV secolo | Palazzo | +| Basilica di Sant'Ubaldo | XVI secolo | Chiesa | +| Teatro Romano | I secolo a.C. | Archeologico | +| Palazzo Ducale | XV secolo | Palazzo | +| Fontana dei Matti | XIV secolo | Fontana | +| Chiesa di San Francesco | XIII secolo | Chiesa | + +## Struttura + +``` +├── index.html # Pagina principale (SPA) +├── css/ +│ └── style.css # Stili dell'applicazione +├── js/ +│ ├── app.js # Logica principale e navigazione +│ ├── ar.js # Engine AR (fotocamera, overlay, particelle) +│ └── monuments.js # Servizio dati monumenti +├── api/ +│ └── monuments.json # Dati dei monumenti e percorsi +└── assets/ + ├── audio/ # File audio delle guide (da caricare) + └── images/ # Immagini (da caricare) +``` + +## Utilizzo + +1. Apri `index.html` in un browser (o servila con un server HTTP) +2. Dalla home, tocca **Avvia AR** per la modalità automatica con GPS +3. Oppure tocca **Esplora monumenti** per scegliere un monumento specifico +4. Nella vista AR: + - **Audio** — Attiva/disattiva la guida audio + - **Info** — Mostra il pannello con la descrizione completa + - **Personaggio** — Mostra/nascondi il personaggio storico + +## Aggiungere contenuti + +### Audio +Carica i file audio MP3 nella cartella `assets/audio/` con i nomi specificati in `api/monuments.json`. + +### Testi +Modifica le descrizioni in `api/monuments.json` per aggiornare i testi di ogni monumento. + +## Requisiti + +- Browser moderno con supporto a: + - Geolocation API + - MediaDevices API (fotocamera) + - CSS backdrop-filter +- Connessione HTTPS (richiesta per fotocamera e geolocalizzazione) + +## Sviluppo + +Servire i file con un server HTTP locale: + +```bash +# Con Python +python3 -m http.server 8000 + +# Con Node.js +npx serve . +``` + +## Design + +Il design è ispirato a [insidegubbio.framer.website](https://insidegubbio.framer.website), con: +- Palette scura ed elegante +- Accenti dorati e colori per categoria +- Tipografia serif per i titoli (Playfair Display) +- Animazioni fluide e transizioni morbide +- UI ottimizzata per dispositivi mobili diff --git a/api/monuments.json b/api/monuments.json new file mode 100644 index 0000000..68ff386 --- /dev/null +++ b/api/monuments.json @@ -0,0 +1,144 @@ +{ + "monuments": [ + { + "id": "palazzo-dei-consoli", + "name": "Palazzo dei Consoli", + "subtitle": "Il simbolo di Gubbio", + "description": "Il Palazzo dei Consoli è uno dei più imponenti edifici pubblici medievali d'Italia. Costruito tra il 1332 e il 1349 su progetto attribuito a Angelo da Orvieto, domina Piazza Grande con la sua maestosa facciata in pietra bianca. Ospita il Museo Civico con le celebri Tavole Iguvine, sette lastre di bronzo con iscrizioni in lingua umbra risalenti al III-I secolo a.C.", + "shortDescription": "Imponente palazzo medievale del XIV secolo, simbolo della città e sede del Museo Civico.", + "lat": 43.35150, + "lng": 12.57740, + "radius": 100, + "audioFile": "assets/audio/palazzo-consoli.mp3", + "character": { + "name": "Angelo da Orvieto", + "role": "Architetto", + "greeting": "Benvenuto! Sono Angelo da Orvieto, l'architetto di questo magnifico palazzo. Lascia che ti racconti la sua storia..." + }, + "decorations": ["medieval-banner", "stone-particles", "golden-glow"], + "epoch": "XIV secolo", + "category": "palazzo" + }, + { + "id": "basilica-sant-ubaldo", + "name": "Basilica di Sant'Ubaldo", + "subtitle": "Il protettore di Gubbio", + "description": "La Basilica di Sant'Ubaldo si trova sulla cima del Monte Ingino, raggiungibile con una suggestiva funivia. Custodisce le spoglie del santo patrono di Gubbio, Sant'Ubaldo, e i tre Ceri, le grandi strutture di legno protagoniste della celebre Corsa dei Ceri che si tiene ogni 15 maggio. La chiesa attuale risale al XVI secolo.", + "shortDescription": "Basilica sul Monte Ingino che custodisce le spoglie del patrono e i famosi Ceri.", + "lat": 43.35570, + "lng": 12.58110, + "radius": 150, + "audioFile": "assets/audio/basilica-ubaldo.mp3", + "character": { + "name": "Sant'Ubaldo", + "role": "Patrono di Gubbio", + "greeting": "Pace a te, visitatore. Sono Ubaldo, vescovo e protettore di questa città. Ti svelerò i segreti di questo luogo sacro..." + }, + "decorations": ["candlelight", "sacred-glow", "dove-particles"], + "epoch": "XVI secolo", + "category": "chiesa" + }, + { + "id": "teatro-romano", + "name": "Teatro Romano", + "subtitle": "L'eredità di Roma", + "description": "Il Teatro Romano di Gubbio, costruito nel I secolo a.C., è uno dei teatri romani meglio conservati dell'Umbria. Con un diametro di circa 70 metri, poteva ospitare fino a 6.000 spettatori. Ancora oggi viene utilizzato per spettacoli estivi, mantenendo viva una tradizione che dura da oltre duemila anni.", + "shortDescription": "Antico teatro del I secolo a.C., ancora oggi utilizzato per spettacoli dal vivo.", + "lat": 43.34860, + "lng": 12.57970, + "radius": 120, + "audioFile": "assets/audio/teatro-romano.mp3", + "character": { + "name": "Centurione Marco", + "role": "Centurione Romano", + "greeting": "Ave! Sono il Centurione Marco. Questo teatro ha visto duemila anni di storia. Lascia che ti guidi..." + }, + "decorations": ["roman-columns", "laurel-particles", "marble-glow"], + "epoch": "I secolo a.C.", + "category": "archeologico" + }, + { + "id": "palazzo-ducale", + "name": "Palazzo Ducale", + "subtitle": "La corte dei Montefeltro", + "description": "Il Palazzo Ducale di Gubbio fu fatto costruire da Federico da Montefeltro nel 1470, ispirandosi al più celebre Palazzo Ducale di Urbino. Il cortile rinascimentale è un gioiello architettonico. Il palazzo ospita oggi un museo con arredi e opere d'arte che testimoniano lo splendore della corte dei Montefeltro.", + "shortDescription": "Elegante palazzo rinascimentale voluto da Federico da Montefeltro nel 1470.", + "lat": 43.35270, + "lng": 12.57820, + "radius": 80, + "audioFile": "assets/audio/palazzo-ducale.mp3", + "character": { + "name": "Federico da Montefeltro", + "role": "Duca di Urbino", + "greeting": "Benvenuto nella mia dimora eugubina! Sono Federico da Montefeltro. Scopri con me le meraviglie di questo palazzo..." + }, + "decorations": ["renaissance-frame", "gold-particles", "noble-glow"], + "epoch": "XV secolo", + "category": "palazzo" + }, + { + "id": "fontana-dei-matti", + "name": "Fontana dei Matti", + "subtitle": "La tradizione più pazza", + "description": "La Fontana del Bargello, nota come Fontana dei Matti, è una piccola fontana situata in Via dei Consoli. Secondo la tradizione, chi gira tre volte intorno alla fontana riceve la 'patente da matto', un documento goliardico rilasciato dal Comune. È uno dei simboli più amati e divertenti di Gubbio.", + "shortDescription": "La celebre fontana dove si ottiene la 'patente da matto' girando tre volte intorno.", + "lat": 43.35190, + "lng": 12.57710, + "radius": 60, + "audioFile": "assets/audio/fontana-matti.mp3", + "character": { + "name": "Il Matto di Gubbio", + "role": "Giullare", + "greeting": "Ahah! Benvenuto, amico! Sei pronto per diventare matto? Gira tre volte e la patente sarà tua!" + }, + "decorations": ["water-splash", "confetti-particles", "rainbow-glow"], + "epoch": "XIV secolo", + "category": "fontana" + }, + { + "id": "chiesa-san-francesco", + "name": "Chiesa di San Francesco", + "subtitle": "Il luogo del lupo", + "description": "La Chiesa di San Francesco sorge nel luogo dove, secondo la tradizione, San Francesco ammansì il lupo di Gubbio. Costruita nel XIII secolo in stile gotico, conserva affreschi di Ottaviano Nelli e altri pittori eugubini. La chiesa è un importante testimonianza del legame tra Gubbio e il santo di Assisi.", + "shortDescription": "Chiesa gotica del XIII secolo legata alla leggenda di San Francesco e il lupo.", + "lat": 43.34980, + "lng": 12.57580, + "radius": 90, + "audioFile": "assets/audio/chiesa-francesco.mp3", + "character": { + "name": "San Francesco", + "role": "Santo", + "greeting": "Pace e bene, fratello! Qui ammansii il lupo che terrorizzava Gubbio. Lascia che ti racconti questa storia..." + }, + "decorations": ["wolf-shadow", "leaf-particles", "holy-glow"], + "epoch": "XIII secolo", + "category": "chiesa" + } + ], + "routes": [ + { + "id": "percorso-medievale", + "name": "Gubbio Medievale", + "description": "Scopri i tesori del Medioevo eugubino", + "monuments": ["palazzo-dei-consoli", "fontana-dei-matti", "palazzo-ducale"], + "duration": "45 min", + "distance": "1.2 km" + }, + { + "id": "percorso-completo", + "name": "Grand Tour di Gubbio", + "description": "Il percorso completo attraverso tutti i monumenti", + "monuments": ["teatro-romano", "chiesa-san-francesco", "fontana-dei-matti", "palazzo-dei-consoli", "palazzo-ducale", "basilica-sant-ubaldo"], + "duration": "2 ore", + "distance": "4.5 km" + }, + { + "id": "percorso-spirituale", + "name": "Gubbio Sacra", + "description": "Un viaggio tra fede e storia", + "monuments": ["chiesa-san-francesco", "basilica-sant-ubaldo"], + "duration": "1.5 ore", + "distance": "3.2 km" + } + ] +} diff --git a/css/style.css b/css/style.css new file mode 100644 index 0000000..58da6cd --- /dev/null +++ b/css/style.css @@ -0,0 +1,1299 @@ +/* =================================================== + InsideGubbio AR — Stylesheet + Design inspired by insidegubbio.framer.website + =================================================== */ + +/* ---------- CSS Variables ---------- */ +:root { + --bg: #0a0a0f; + --bg-card: #12121a; + --bg-surface: #1a1a26; + --text: #f0ede6; + --text-dim: #8a8698; + --text-muted: #5a5670; + --accent: #d4a04a; + --accent-glow: rgba(212, 160, 74, 0.3); + --cat-palazzo: #d4a04a; + --cat-chiesa: #a08cd4; + --cat-archeologico: #7cb4a0; + --cat-fontana: #5b9bd5; + --radius: 16px; + --radius-sm: 10px; + --radius-xs: 6px; + --font-sans: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif; + --font-serif: 'Playfair Display', Georgia, serif; + --safe-bottom: env(safe-area-inset-bottom, 0px); + --safe-top: env(safe-area-inset-top, 0px); +} + +/* ---------- Reset ---------- */ +*, *::before, *::after { + margin: 0; + padding: 0; + box-sizing: border-box; +} + +html { + font-size: 16px; + -webkit-text-size-adjust: 100%; + scroll-behavior: smooth; +} + +body { + font-family: var(--font-sans); + background: var(--bg); + color: var(--text); + overflow-x: hidden; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +/* ---------- Views ---------- */ +.view { + display: none; + min-height: 100vh; + min-height: 100dvh; +} + +.view.active { + display: block; +} + +/* =================================================== + HOME PAGE + =================================================== */ + +.home-hero { + position: relative; + min-height: 100vh; + min-height: 100dvh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + padding: 2rem 1.5rem; + overflow: hidden; +} + +.home-bg-gradient { + position: absolute; + inset: 0; + background: + radial-gradient(ellipse 80% 60% at 50% 0%, rgba(212, 160, 74, 0.08) 0%, transparent 60%), + radial-gradient(ellipse 60% 40% at 80% 80%, rgba(160, 140, 212, 0.06) 0%, transparent 50%), + var(--bg); + z-index: 0; +} + +.home-particles { + position: absolute; + inset: 0; + z-index: 1; + pointer-events: none; +} + +/* Nav */ +.home-nav { + position: absolute; + top: 0; + left: 0; + right: 0; + padding: 1.2rem 1.5rem; + padding-top: calc(1.2rem + var(--safe-top)); + z-index: 10; +} + +.home-logo { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.logo-icon { + color: var(--accent); + font-size: 1.1rem; +} + +.logo-text { + font-family: var(--font-serif); + font-weight: 600; + font-size: 1.1rem; + letter-spacing: -0.02em; +} + +.logo-badge { + background: var(--accent); + color: var(--bg); + font-size: 0.6rem; + font-weight: 700; + padding: 0.15rem 0.4rem; + border-radius: 4px; + text-transform: uppercase; + letter-spacing: 0.08em; +} + +/* Hero Content */ +.home-content { + position: relative; + z-index: 2; + text-align: center; + max-width: 600px; +} + +.home-eyebrow { + font-size: 0.75rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.2em; + color: var(--accent); + margin-bottom: 1.2rem; +} + +.home-title { + font-family: var(--font-serif); + font-size: clamp(2.4rem, 8vw, 4rem); + font-weight: 700; + line-height: 1.1; + letter-spacing: -0.02em; + margin-bottom: 1.2rem; +} + +.home-title em { + font-style: italic; + color: var(--accent); +} + +.home-subtitle { + font-size: 1rem; + line-height: 1.7; + color: var(--text-dim); + margin-bottom: 2.5rem; + max-width: 440px; + margin-left: auto; + margin-right: auto; +} + +/* Buttons */ +.home-actions { + display: flex; + gap: 0.8rem; + justify-content: center; + flex-wrap: wrap; +} + +.btn-primary { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.85rem 1.8rem; + background: var(--accent); + color: var(--bg); + border: none; + border-radius: 50px; + font-family: var(--font-sans); + font-size: 0.9rem; + font-weight: 600; + cursor: pointer; + transition: all 0.3s ease; + letter-spacing: -0.01em; +} + +.btn-primary:hover { + transform: translateY(-2px); + box-shadow: 0 8px 30px var(--accent-glow); +} + +.btn-primary:active { + transform: translateY(0); +} + +.btn-icon { + font-size: 1.1rem; +} + +.btn-secondary { + display: inline-flex; + align-items: center; + gap: 0.5rem; + padding: 0.85rem 1.8rem; + background: transparent; + color: var(--text); + border: 1px solid rgba(240, 237, 230, 0.15); + border-radius: 50px; + font-family: var(--font-sans); + font-size: 0.9rem; + font-weight: 500; + cursor: pointer; + transition: all 0.3s ease; +} + +.btn-secondary:hover { + border-color: var(--accent); + color: var(--accent); +} + +/* Scroll hint */ +.home-scroll-hint { + position: absolute; + bottom: 2rem; + left: 50%; + transform: translateX(-50%); + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + z-index: 2; +} + +.home-scroll-hint span { + font-size: 0.7rem; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--text-muted); +} + +.scroll-arrow { + width: 20px; + height: 20px; + border-right: 1.5px solid var(--text-muted); + border-bottom: 1.5px solid var(--text-muted); + transform: rotate(45deg); + animation: scrollBounce 2s ease infinite; +} + +@keyframes scrollBounce { + 0%, 100% { transform: rotate(45deg) translateY(0); opacity: 0.5; } + 50% { transform: rotate(45deg) translateY(6px); opacity: 1; } +} + +/* ===== HOME SECTIONS ===== */ +.home-section { + padding: 5rem 1.5rem; +} + +.home-section-dark { + background: var(--bg-card); +} + +.section-container { + max-width: 900px; + margin: 0 auto; +} + +.section-eyebrow { + font-size: 0.7rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.2em; + color: var(--accent); + margin-bottom: 0.8rem; +} + +.section-title { + font-family: var(--font-serif); + font-size: clamp(1.8rem, 5vw, 2.4rem); + font-weight: 700; + letter-spacing: -0.02em; + margin-bottom: 0.6rem; +} + +.section-subtitle { + color: var(--text-dim); + font-size: 0.95rem; + line-height: 1.6; + margin-bottom: 2.5rem; +} + +.section-cta { + text-align: center; + margin-top: 2.5rem; +} + +/* Steps */ +.steps-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 1.5rem; + margin-top: 2.5rem; +} + +.step-card { + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius); + padding: 2rem 1.5rem; + position: relative; + transition: border-color 0.3s; +} + +.step-card:hover { + border-color: rgba(212, 160, 74, 0.2); +} + +.step-number { + font-size: 0.7rem; + font-weight: 700; + color: var(--accent); + letter-spacing: 0.1em; + margin-bottom: 1rem; +} + +.step-icon { + font-size: 2rem; + margin-bottom: 1rem; +} + +.step-card h3 { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.step-card p { + font-size: 0.85rem; + line-height: 1.6; + color: var(--text-dim); +} + +/* Home monument preview */ +.home-monument-preview { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); + gap: 1rem; + margin-top: 2rem; +} + +.home-monument-item { + background: var(--bg-surface); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius-sm); + padding: 1.5rem 1rem; + text-align: center; + cursor: pointer; + transition: all 0.3s ease; +} + +.home-monument-item:hover { + border-color: var(--accent); + transform: translateY(-3px); +} + +.home-monument-icon { + font-size: 2rem; + margin-bottom: 0.8rem; + filter: drop-shadow(0 0 10px var(--accent)); +} + +.home-monument-item h4 { + font-size: 0.85rem; + font-weight: 600; + margin-bottom: 0.3rem; +} + +.home-monument-item span { + font-size: 0.7rem; + color: var(--text-muted); + text-transform: uppercase; + letter-spacing: 0.08em; +} + +/* Footer */ +.home-footer { + padding: 3rem 1.5rem; + border-top: 1px solid rgba(255, 255, 255, 0.05); +} + +.footer-content { + max-width: 900px; + margin: 0 auto; + text-align: center; +} + +.footer-content .home-logo { + justify-content: center; + margin-bottom: 0.8rem; +} + +.footer-tagline { + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 0.5rem; +} + +.footer-copy { + font-size: 0.7rem; + color: var(--text-muted); +} + +/* =================================================== + EXPLORE PAGE + =================================================== */ + +.explore-nav { + position: sticky; + top: 0; + z-index: 100; + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.8rem 1rem; + padding-top: calc(0.8rem + var(--safe-top)); + background: rgba(10, 10, 15, 0.85); + backdrop-filter: blur(20px); + -webkit-backdrop-filter: blur(20px); + border-bottom: 1px solid rgba(255, 255, 255, 0.05); +} + +.nav-back, .nav-ar { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + background: rgba(255, 255, 255, 0.06); + border-radius: 50%; + color: var(--text); + cursor: pointer; + transition: background 0.3s; +} + +.nav-back:hover, .nav-ar:hover { + background: rgba(212, 160, 74, 0.15); + color: var(--accent); +} + +.explore-content { + padding: 1.5rem; + padding-bottom: calc(2rem + var(--safe-bottom)); + max-width: 900px; + margin: 0 auto; +} + +.explore-section { + margin-bottom: 3rem; +} + +.explore-section-title { + font-family: var(--font-serif); + font-size: 1.6rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.explore-section-desc { + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 1.5rem; +} + +/* Monument cards */ +.monument-grid { + display: grid; + gap: 1rem; +} + +.monument-card { + display: flex; + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius); + overflow: hidden; + cursor: pointer; + transition: all 0.3s ease; +} + +.monument-card:hover { + border-color: rgba(212, 160, 74, 0.2); + transform: translateY(-2px); + box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3); +} + +.monument-card-accent { + width: 4px; + flex-shrink: 0; +} + +.monument-card-body { + padding: 1.2rem; + flex: 1; +} + +.monument-card-epoch { + font-size: 0.65rem; + font-weight: 600; + color: var(--accent); + text-transform: uppercase; + letter-spacing: 0.12em; + margin-bottom: 0.4rem; +} + +.monument-card-title { + font-family: var(--font-serif); + font-size: 1.15rem; + font-weight: 600; + margin-bottom: 0.4rem; +} + +.monument-card-desc { + font-size: 0.8rem; + line-height: 1.5; + color: var(--text-dim); + margin-bottom: 0.8rem; +} + +.monument-card-footer { + display: flex; + justify-content: space-between; + align-items: center; +} + +.monument-card-category { + font-size: 0.65rem; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--text-muted); + background: rgba(255, 255, 255, 0.05); + padding: 0.2rem 0.6rem; + border-radius: 20px; +} + +.monument-card-cta { + font-size: 0.75rem; + font-weight: 600; + color: var(--accent); +} + +/* Route cards */ +.route-grid { + display: grid; + gap: 1rem; +} + +.route-card { + background: var(--bg-card); + border: 1px solid rgba(255, 255, 255, 0.05); + border-radius: var(--radius); + padding: 1.5rem; + transition: border-color 0.3s; +} + +.route-card:hover { + border-color: rgba(212, 160, 74, 0.15); +} + +.route-card-header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 0.6rem; +} + +.route-card-header h3 { + font-family: var(--font-serif); + font-size: 1.1rem; + font-weight: 600; +} + +.route-card-meta { + display: flex; + gap: 0.8rem; + font-size: 0.75rem; + color: var(--text-muted); +} + +.route-card-desc { + font-size: 0.85rem; + color: var(--text-dim); + line-height: 1.5; + margin-bottom: 1rem; +} + +.route-card-stops { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 0.4rem; +} + +.route-stop { + font-size: 0.7rem; + font-weight: 500; + color: var(--accent); + background: rgba(212, 160, 74, 0.1); + padding: 0.25rem 0.6rem; + border-radius: 20px; +} + +.route-connector { + font-size: 0.65rem; + color: var(--text-muted); +} + +/* =================================================== + AR VIEW + =================================================== */ + +.ar-container { + position: fixed; + inset: 0; + background: #000; + overflow: hidden; +} + +#ar-camera { + width: 100%; + height: 100%; + object-fit: cover; +} + +#ar-particles { + position: absolute; + inset: 0; + pointer-events: none; + z-index: 2; +} + +.ar-overlay { + position: absolute; + inset: 0; + z-index: 3; + pointer-events: none; +} + +.ar-overlay > * { + pointer-events: auto; +} + +/* AR Error */ +.ar-error { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + padding: 2rem; +} + +.ar-error-icon { + font-size: 3rem; + margin-bottom: 1rem; + opacity: 0.6; +} + +.ar-error h3 { + font-size: 1.2rem; + margin-bottom: 0.5rem; +} + +.ar-error p { + font-size: 0.85rem; + color: var(--text-dim); + margin-bottom: 1.5rem; +} + +.btn-retry { + padding: 0.6rem 1.5rem; + background: var(--accent); + color: var(--bg); + border: none; + border-radius: 50px; + font-family: var(--font-sans); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; +} + +/* AR HUD */ +.ar-monument-hud { + position: absolute; + inset: 0; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +/* AR Top Bar */ +.ar-top-bar { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.8rem 1rem; + padding-top: calc(0.8rem + var(--safe-top)); + background: linear-gradient(to bottom, rgba(0,0,0,0.6), transparent); +} + +.ar-btn-back { + display: flex; + align-items: center; + justify-content: center; + width: 40px; + height: 40px; + border: none; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-radius: 50%; + color: #fff; + cursor: pointer; + transition: background 0.3s; +} + +.ar-btn-back:active { + background: rgba(255, 255, 255, 0.2); +} + +.ar-monument-badge { + display: flex; + flex-direction: column; + align-items: center; + opacity: 0; + transform: translateY(-10px); + transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); +} + +.ar-monument-badge.visible { + opacity: 1; + transform: translateY(0); +} + +.ar-badge-epoch { + font-size: 0.55rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.15em; + color: var(--accent, #d4a04a); + margin-bottom: 0.15rem; +} + +.ar-badge-name { + font-family: var(--font-serif); + font-size: 1rem; + font-weight: 600; + color: #fff; + text-shadow: 0 1px 4px rgba(0,0,0,0.5); +} + +.ar-distance-indicator { + display: flex; + align-items: center; + justify-content: center; + min-width: 40px; + height: 40px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(10px); + -webkit-backdrop-filter: blur(10px); + border-radius: 20px; + padding: 0 0.8rem; +} + +.ar-distance-value { + font-size: 0.7rem; + font-weight: 600; + color: #fff; +} + +/* AR Scan Frame */ +.ar-scan-frame { + position: absolute; + top: 50%; + left: 50%; + width: 65vw; + max-width: 300px; + height: 65vw; + max-height: 300px; + transform: translate(-50%, -50%) scale(0.8); + opacity: 0; + transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); +} + +.ar-scan-frame.visible { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.ar-scan-corner { + position: absolute; + width: 24px; + height: 24px; + border-color: var(--accent, #d4a04a); + border-style: solid; + border-width: 0; +} + +.ar-scan-corner.tl { top: 0; left: 0; border-top-width: 2px; border-left-width: 2px; border-radius: 4px 0 0 0; } +.ar-scan-corner.tr { top: 0; right: 0; border-top-width: 2px; border-right-width: 2px; border-radius: 0 4px 0 0; } +.ar-scan-corner.bl { bottom: 0; left: 0; border-bottom-width: 2px; border-left-width: 2px; border-radius: 0 0 0 4px; } +.ar-scan-corner.br { bottom: 0; right: 0; border-bottom-width: 2px; border-right-width: 2px; border-radius: 0 0 4px 0; } + +.ar-scan-line { + position: absolute; + left: 10%; + right: 10%; + height: 1px; + background: var(--accent, #d4a04a); + opacity: 0.5; + top: 0; + animation: scanLine 3s ease-in-out infinite; +} + +@keyframes scanLine { + 0%, 100% { top: 10%; opacity: 0.3; } + 50% { top: 90%; opacity: 0.6; } +} + +/* AR Character Figure — Full-body overlay */ +.ar-character-figure { + position: absolute; + bottom: 18%; + right: 5%; + width: 120px; + display: flex; + flex-direction: column; + align-items: center; + opacity: 0; + transform: translateY(40px) scale(0.8); + transition: all 0.8s cubic-bezier(0.16, 1, 0.3, 1); + pointer-events: none; + z-index: 4; +} + +.ar-character-figure.visible { + opacity: 1; + transform: translateY(0) scale(1); + pointer-events: auto; +} + +.ar-figure-body { + position: relative; + width: 120px; + height: 240px; + display: flex; + flex-direction: column; + align-items: center; +} + +.ar-figure-glow { + position: absolute; + inset: -20px; + border-radius: 50%; + animation: figureFloat 4s ease-in-out infinite; + pointer-events: none; +} + +.ar-figure-svg { + width: 100%; + height: 200px; + filter: drop-shadow(0 0 12px rgba(255,255,255,0.15)); + animation: figureFloat 4s ease-in-out infinite; +} + +.ar-figure-label { + display: flex; + flex-direction: column; + align-items: center; + margin-top: 0.3rem; +} + +.ar-figure-label strong { + font-size: 0.7rem; + font-weight: 600; + color: #fff; + text-shadow: 0 1px 6px rgba(0,0,0,0.8); + text-align: center; +} + +.ar-figure-label span { + font-size: 0.55rem; + color: rgba(255,255,255,0.6); + text-transform: uppercase; + letter-spacing: 0.08em; + text-shadow: 0 1px 4px rgba(0,0,0,0.8); +} + +.ar-figure-speech { + position: absolute; + bottom: 100%; + right: 0; + width: 200px; + margin-bottom: 0.5rem; + background: rgba(0, 0, 0, 0.65); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.12); + border-radius: var(--radius-sm); + padding: 0.7rem 0.9rem; + opacity: 0; + transform: translateY(10px); + transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1); + pointer-events: none; +} + +.ar-figure-speech.visible { + opacity: 1; + transform: translateY(0); + pointer-events: auto; +} + +.ar-figure-speech p { + font-size: 0.72rem; + line-height: 1.45; + color: rgba(255, 255, 255, 0.9); + margin: 0; + padding-right: 1rem; +} + +.ar-figure-speech-close { + position: absolute; + top: 0.3rem; + right: 0.3rem; + background: none; + border: none; + color: rgba(255,255,255,0.5); + font-size: 0.7rem; + cursor: pointer; + padding: 0.2rem; + line-height: 1; +} + +.ar-figure-speech::after { + content: ''; + position: absolute; + bottom: -6px; + right: 30px; + width: 12px; + height: 12px; + background: rgba(0, 0, 0, 0.65); + border-right: 1px solid rgba(255, 255, 255, 0.12); + border-bottom: 1px solid rgba(255, 255, 255, 0.12); + transform: rotate(45deg); +} + +@keyframes figureFloat { + 0%, 100% { transform: translateY(0); } + 50% { transform: translateY(-8px); } +} + +/* AR Controls */ +.ar-controls { + display: flex; + justify-content: center; + gap: 1rem; + padding: 1rem; + padding-bottom: calc(1.5rem + var(--safe-bottom)); + background: linear-gradient(to top, rgba(0,0,0,0.6), transparent); + opacity: 0; + transform: translateY(20px); + transition: all 0.5s cubic-bezier(0.16, 1, 0.3, 1) 0.2s; +} + +.ar-controls.visible { + opacity: 1; + transform: translateY(0); +} + +.ar-control-btn { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.35rem; + padding: 0.8rem 1.2rem; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: var(--radius-sm); + color: #fff; + cursor: pointer; + transition: all 0.3s ease; + font-family: var(--font-sans); + min-width: 80px; +} + +.ar-control-btn span { + font-size: 0.65rem; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.06em; +} + +.ar-control-btn:active { + transform: scale(0.95); +} + +.ar-control-btn.active { + background: var(--accent, #d4a04a); + border-color: var(--accent, #d4a04a); + color: #000; +} + +.ar-control-btn.active svg { + stroke: #000; +} + +/* AR Text Panel Backdrop */ +.ar-text-backdrop { + position: absolute; + inset: 0; + background: rgba(0, 0, 0, 0.4); + opacity: 0; + pointer-events: none; + transition: opacity 0.4s ease; + z-index: 9; +} + +.ar-text-backdrop.visible { + opacity: 1; + pointer-events: auto; +} + +/* AR Text Panel */ +.ar-text-panel { + position: absolute; + bottom: 0; + left: 0; + right: 0; + max-height: 55vh; + background: rgba(10, 10, 15, 0.94); + backdrop-filter: blur(24px); + -webkit-backdrop-filter: blur(24px); + border-top: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 20px 20px 0 0; + transform: translateY(100%); + transition: transform 0.5s cubic-bezier(0.16, 1, 0.3, 1); + overflow-y: auto; + -webkit-overflow-scrolling: touch; + z-index: 10; +} + +.ar-text-panel.visible { + transform: translateY(0); +} + +.ar-text-header { + display: flex; + align-items: center; + justify-content: center; + padding: 0.6rem 1rem; + position: sticky; + top: 0; + background: rgba(10, 10, 15, 0.94); + z-index: 1; +} + +.ar-text-handle { + width: 40px; + height: 5px; + background: rgba(255, 255, 255, 0.25); + border-radius: 3px; + cursor: pointer; +} + +.ar-text-close { + position: absolute; + right: 0.8rem; + top: 50%; + transform: translateY(-50%); + display: flex; + align-items: center; + justify-content: center; + width: 32px; + height: 32px; + background: rgba(255, 255, 255, 0.08); + border: none; + border-radius: 50%; + color: rgba(255, 255, 255, 0.6); + cursor: pointer; + transition: all 0.2s; +} + +.ar-text-close:active { + background: rgba(255, 255, 255, 0.15); + color: #fff; +} + +.ar-text-content { + padding: 0 1.5rem 2rem; + padding-bottom: calc(2rem + var(--safe-bottom)); +} + +.ar-text-content h2 { + font-family: var(--font-serif); + font-size: 1.6rem; + font-weight: 700; + margin-bottom: 0.3rem; +} + +.ar-text-subtitle { + font-size: 0.85rem; + color: var(--text-dim); + font-style: italic; + margin-bottom: 1rem; +} + +.ar-text-divider { + width: 40px; + height: 2px; + border-radius: 1px; + margin-bottom: 1rem; +} + +.ar-text-body { + font-size: 0.9rem; + line-height: 1.8; + color: rgba(255, 255, 255, 0.85); + margin-bottom: 1.2rem; +} + +.ar-text-meta { + display: flex; + gap: 0.5rem; +} + +.ar-meta-tag { + font-size: 0.65rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.1em; + color: var(--accent, #d4a04a); + background: rgba(212, 160, 74, 0.1); + padding: 0.3rem 0.8rem; + border-radius: 20px; +} + +/* Nearby hint */ +.ar-nearby-hint { + position: absolute; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); + text-align: center; + padding: 2rem; +} + +.ar-nearby-hint p { + font-size: 1.1rem; + font-weight: 600; + margin-bottom: 0.5rem; +} + +.ar-hint-sub { + font-size: 0.85rem !important; + font-weight: 400 !important; + color: var(--text-dim); + margin-bottom: 1.5rem !important; +} + +.btn-explore { + padding: 0.7rem 1.5rem; + background: var(--accent); + color: var(--bg); + border: none; + border-radius: 50px; + font-family: var(--font-sans); + font-size: 0.85rem; + font-weight: 600; + cursor: pointer; +} + +/* Toast */ +.ar-toast { + position: fixed; + bottom: 5rem; + left: 50%; + transform: translateX(-50%) translateY(20px); + background: rgba(30, 30, 40, 0.9); + backdrop-filter: blur(16px); + -webkit-backdrop-filter: blur(16px); + border: 1px solid rgba(255, 255, 255, 0.08); + border-radius: 50px; + padding: 0.6rem 1.2rem; + font-size: 0.8rem; + color: var(--text); + opacity: 0; + transition: all 0.4s ease; + z-index: 1000; + pointer-events: none; +} + +.ar-toast.visible { + opacity: 1; + transform: translateX(-50%) translateY(0); +} + +/* =================================================== + ANIMATIONS + =================================================== */ + +@keyframes fadeInUp { + from { opacity: 0; transform: translateY(20px); } + to { opacity: 1; transform: translateY(0); } +} + +@keyframes fadeIn { + from { opacity: 0; } + to { opacity: 1; } +} + +.home-content { + animation: fadeInUp 0.8s ease 0.2s both; +} + +.home-nav { + animation: fadeIn 0.6s ease 0.1s both; +} + +.home-scroll-hint { + animation: fadeIn 1s ease 1s both; +} + +/* =================================================== + RESPONSIVE + =================================================== */ + +@media (min-width: 768px) { + .monument-grid { + grid-template-columns: repeat(2, 1fr); + } + + .route-grid { + grid-template-columns: repeat(2, 1fr); + } + + .ar-character-figure { + width: 160px; + right: 8%; + } + + .ar-figure-body { + width: 160px; + height: 320px; + } + + .ar-figure-svg { + height: 270px; + } + + .ar-figure-speech { + width: 260px; + } +} + +@media (min-width: 1024px) { + .home-hero { + padding: 4rem 2rem; + } + + .explore-content { + padding: 2rem 2rem; + } +} + +/* Prefers reduced motion */ +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + } +} diff --git a/index.html b/index.html new file mode 100644 index 0000000..7af17a4 --- /dev/null +++ b/index.html @@ -0,0 +1,185 @@ + + + + + + + + + + InsideGubbio AR + + + + + + + + +
+
+
+
+ +
+

realtà aumentata

+

+ Scopri Gubbio
+ come mai prima +

+

+ Inquadra i monumenti e lasciati guidare dalla storia. + Audio, racconti e personaggi prendono vita davanti ai tuoi occhi. +

+
+ + +
+
+
+ scorri per scoprire +
+
+
+ +
+
+

come funziona

+

Tre semplici passi

+
+
+
01
+
📍
+

Posizionati

+

Avvicinati a un monumento di Gubbio. L'app rileva la tua posizione automaticamente.

+
+
+
02
+
📱
+

Inquadra

+

Punta la fotocamera verso il monumento e guarda la magia prendere forma.

+
+
+
03
+
+

Esplora

+

Ascolta audio, leggi storie e incontra personaggi storici in realtà aumentata.

+
+
+
+
+ +
+
+

monumenti

+

Sei luoghi straordinari

+

Dai palazzi medievali ai teatri romani, ogni angolo di Gubbio ha una storia da raccontare.

+
+
+ +
+
+
+ + +
+ + +
+ + +
+
+

Monumenti

+

Tocca un monumento per esplorarlo in realtà aumentata.

+
+
+ +
+

Percorsi

+

Scegli un percorso guidato per scoprire Gubbio.

+
+
+
+
+ + +
+
+ + +
+
+
+ + +
+ + + + + + + diff --git a/js/app.js b/js/app.js new file mode 100644 index 0000000..eec6ece --- /dev/null +++ b/js/app.js @@ -0,0 +1,196 @@ +/** + * InsideGubbio AR - Main Application + * Handles page navigation, monument list, AR view management + */ + +const ARApp = (() => { + let currentView = 'home'; + let selectedMonument = null; + let userLat = null; + let userLng = null; + let demoMode = false; + + async function init() { + await MonumentService.loadData(); + setupNavigation(); + checkView(); + window.addEventListener('popstate', checkView); + window.addEventListener('resize', handleResize); + } + + function checkView() { + const hash = window.location.hash.replace('#', '') || 'home'; + if (hash.startsWith('ar/')) { + const id = hash.replace('ar/', ''); + openARForMonument(id); + } else if (hash === 'explore') { + showExplore(); + } else if (hash === 'ar') { + showARScan(); + } else { + showHome(); + } + } + + function setupNavigation() { + document.querySelectorAll('[data-navigate]').forEach((el) => { + el.addEventListener('click', (e) => { + e.preventDefault(); + const target = el.getAttribute('data-navigate'); + window.location.hash = target; + }); + }); + } + + function showView(viewId) { + document.querySelectorAll('.view').forEach((v) => v.classList.remove('active')); + const view = document.getElementById(viewId); + if (view) { + view.classList.add('active'); + currentView = viewId; + } + } + + function showHome() { + showView('view-home'); + AREngine.cleanup(); + } + + function showExplore() { + showView('view-explore'); + AREngine.cleanup(); + renderMonumentList(); + renderRouteList(); + } + + function renderMonumentList() { + const container = document.getElementById('monument-list'); + if (!container) return; + const monuments = MonumentService.getAllMonuments(); + + container.innerHTML = monuments + .map( + (m) => ` +
+
+
+
${m.epoch}
+

${m.name}

+

${m.shortDescription}

+ +
+
` + ) + .join(''); + } + + function renderRouteList() { + const container = document.getElementById('route-list'); + if (!container) return; + const routes = MonumentService.getRoutes(); + const monuments = MonumentService.getAllMonuments(); + + container.innerHTML = routes + .map((r) => { + const routeMonuments = r.monuments + .map((id) => monuments.find((m) => m.id === id)) + .filter(Boolean); + return ` +
+
+

${r.name}

+
+ ⏱ ${r.duration} + 📍 ${r.distance} +
+
+

${r.description}

+
+ ${routeMonuments.map((m) => `${m.name}`).join('')} +
+
`; + }) + .join(''); + } + + function showARScan() { + showView('view-ar'); + startAR(); + } + + async function openARForMonument(id) { + const monument = MonumentService.getMonumentById(id); + if (!monument) return; + + selectedMonument = monument; + demoMode = true; + showView('view-ar'); + + const cameraOk = await AREngine.initCamera(); + if (cameraOk) { + monument.distance = 0; + AREngine.setActiveMonument(monument); + } + } + + async function startAR() { + const cameraOk = await AREngine.initCamera(); + if (!cameraOk) return; + + AREngine.startGeolocation((lat, lng, accuracy) => { + userLat = lat; + userLng = lng; + + const closest = MonumentService.getClosestMonument(lat, lng); + if (closest) { + AREngine.setActiveMonument(closest); + } else { + AREngine.clearActiveMonument(); + showNearbyHint(); + } + }); + } + + function showNearbyHint() { + const overlay = document.getElementById('ar-overlay'); + if (!overlay || overlay.querySelector('.ar-monument-hud')) return; + const all = MonumentService.getAllMonuments(); + if (!all.length) return; + + let hint = '
'; + hint += '

Nessun monumento nelle vicinanze

'; + hint += '

Avvicinati a uno dei monumenti di Gubbio o scegli dalla lista

'; + hint += ``; + hint += '
'; + overlay.innerHTML = hint; + } + + function goBack() { + AREngine.cleanup(); + selectedMonument = null; + demoMode = false; + window.location.hash = 'home'; + } + + function handleResize() { + const canvas = document.getElementById('ar-particles'); + if (canvas) { + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + } + } + + return { + init, + showHome, + showExplore, + showARScan, + openARForMonument, + goBack, + }; +})(); + +document.addEventListener('DOMContentLoaded', () => ARApp.init()); diff --git a/js/ar.js b/js/ar.js new file mode 100644 index 0000000..d465d89 --- /dev/null +++ b/js/ar.js @@ -0,0 +1,566 @@ +/** + * InsideGubbio AR - AR Camera & Overlay Engine + * Handles camera stream, overlays, audio, text panels, and decorations + */ + +const AREngine = (() => { + let videoStream = null; + let currentMonument = null; + let isAudioPlaying = false; + let audioElement = null; + let watchId = null; + let animationFrame = null; + let particles = []; + + const CATEGORY_COLORS = { + palazzo: '#d4a04a', + chiesa: '#a08cd4', + archeologico: '#7cb4a0', + fontana: '#5b9bd5', + }; + + async function initCamera() { + const video = document.getElementById('ar-camera'); + if (!video) return false; + try { + videoStream = await navigator.mediaDevices.getUserMedia({ + video: { facingMode: 'environment', width: { ideal: 1920 }, height: { ideal: 1080 } }, + audio: false, + }); + video.srcObject = videoStream; + await video.play(); + return true; + } catch (err) { + console.error('AREngine: Camera access denied', err); + showCameraError(); + return false; + } + } + + function stopCamera() { + if (videoStream) { + videoStream.getTracks().forEach((t) => t.stop()); + videoStream = null; + } + } + + function showCameraError() { + const overlay = document.getElementById('ar-overlay'); + if (overlay) { + overlay.innerHTML = ` +
+
📷
+

Fotocamera non disponibile

+

Consenti l'accesso alla fotocamera per vivere l'esperienza AR.

+ +
`; + } + } + + function startGeolocation(callback) { + if (!navigator.geolocation) { + console.error('Geolocation not supported'); + return; + } + watchId = navigator.geolocation.watchPosition( + (pos) => callback(pos.coords.latitude, pos.coords.longitude, pos.coords.accuracy), + (err) => console.error('Geolocation error:', err), + { enableHighAccuracy: true, maximumAge: 5000, timeout: 15000 } + ); + } + + function stopGeolocation() { + if (watchId !== null) { + navigator.geolocation.clearWatch(watchId); + watchId = null; + } + } + + function setActiveMonument(monument) { + if (currentMonument && currentMonument.id === monument.id) return; + currentMonument = monument; + renderMonumentOverlay(monument); + startDecorations(monument); + } + + function clearActiveMonument() { + currentMonument = null; + stopAudio(); + stopDecorations(); + const overlay = document.getElementById('ar-overlay'); + if (overlay) overlay.innerHTML = ''; + const panel = document.getElementById('ar-text-panel'); + if (panel) panel.classList.remove('visible'); + const backdrop = document.getElementById('ar-text-backdrop'); + if (backdrop) backdrop.classList.remove('visible'); + const figure = document.getElementById('ar-character-figure'); + if (figure) figure.classList.remove('visible'); + } + + function renderMonumentOverlay(monument) { + const overlay = document.getElementById('ar-overlay'); + if (!overlay) return; + + const color = CATEGORY_COLORS[monument.category] || '#d4a04a'; + + overlay.innerHTML = ` +
+
+ +
+ ${monument.epoch} + ${monument.name} +
+
+ ${Math.round(monument.distance)}m +
+
+ +
+
+
+ + ${getCharacterSVG(monument.character.role, color)} + +
+ ${monument.character.name} + ${monument.character.role} +
+
+
+

${monument.character.greeting}

+ +
+
+ +
+
+
+
+
+
+
+ +
+ + + +
+
+ +
+
+
+
+ +
+
+

${monument.name}

+

${monument.subtitle}

+
+

${monument.description}

+
+ ${monument.epoch} + ${monument.category} +
+
+
+ `; + + // Setup swipe-down to close text panel + setupTextPanelSwipe(); + + setTimeout(() => { + const badge = overlay.querySelector('.ar-monument-badge'); + if (badge) badge.classList.add('visible'); + const scanFrame = overlay.querySelector('.ar-scan-frame'); + if (scanFrame) scanFrame.classList.add('visible'); + const controls = overlay.querySelector('.ar-controls'); + if (controls) controls.classList.add('visible'); + }, 300); + + // Character figure appears after a short delay with animation + setTimeout(() => { + const figure = document.getElementById('ar-character-figure'); + if (figure) figure.classList.add('visible'); + }, 1200); + + // Speech bubble appears after character + setTimeout(() => { + const speech = document.getElementById('ar-figure-speech'); + if (speech) speech.classList.add('visible'); + }, 2200); + } + + function getCharacterSVG(role, color) { + // Full-body character silhouettes with role-specific details + const base = ` + + + + + + + + + + + + + + `; + + const roleDetails = { + Architetto: ` + + + + + + + + + + + + + + `, + 'Patrono di Gubbio': ` + + + + + + + + + + + + + + `, + 'Centurione Romano': ` + + + + + + + + + + + + + + + + + `, + 'Duca di Urbino': ` + + + + + + + + + + + + + `, + Giullare: ` + + + + + + + + + + + + + + + + + + `, + Santo: ` + + + + + + + + + + + + + + + + + + `, + }; + + return base + (roleDetails[role] || roleDetails['Architetto']); + } + + function toggleAudio() { + if (!currentMonument) return; + const btn = document.getElementById('btn-audio'); + if (isAudioPlaying) { + stopAudio(); + if (btn) btn.classList.remove('active'); + } else { + playAudio(currentMonument.audioFile); + if (btn) btn.classList.add('active'); + } + } + + function playAudio(src) { + if (!audioElement) { + audioElement = new Audio(); + audioElement.addEventListener('ended', () => { + isAudioPlaying = false; + const btn = document.getElementById('btn-audio'); + if (btn) btn.classList.remove('active'); + }); + } + audioElement.src = src; + audioElement.play().catch((err) => { + console.warn('Audio playback failed:', err); + showToast('Audio non ancora disponibile per questo monumento'); + }); + isAudioPlaying = true; + } + + function stopAudio() { + if (audioElement) { + audioElement.pause(); + audioElement.currentTime = 0; + } + isAudioPlaying = false; + } + + function toggleTextPanel() { + const panel = document.getElementById('ar-text-panel'); + const backdrop = document.getElementById('ar-text-backdrop'); + if (panel) { + const isOpen = panel.classList.toggle('visible'); + if (backdrop) { + if (isOpen) backdrop.classList.add('visible'); + else backdrop.classList.remove('visible'); + } + } + } + + function closeTextPanel() { + const panel = document.getElementById('ar-text-panel'); + const backdrop = document.getElementById('ar-text-backdrop'); + if (panel) panel.classList.remove('visible'); + if (backdrop) backdrop.classList.remove('visible'); + } + + function toggleCharacter() { + const figure = document.getElementById('ar-character-figure'); + if (figure) figure.classList.toggle('visible'); + } + + function dismissSpeech() { + const speech = document.getElementById('ar-figure-speech'); + if (speech) speech.classList.remove('visible'); + } + + function setupTextPanelSwipe() { + const panel = document.getElementById('ar-text-panel'); + if (!panel) return; + let startY = 0; + let currentY = 0; + let isDragging = false; + + panel.addEventListener('touchstart', (e) => { + startY = e.touches[0].clientY; + currentY = startY; + isDragging = true; + panel.style.transition = 'none'; + }, { passive: true }); + + panel.addEventListener('touchmove', (e) => { + if (!isDragging) return; + currentY = e.touches[0].clientY; + const diff = currentY - startY; + if (diff > 0) { + panel.style.transform = `translateY(${diff}px)`; + } + }, { passive: true }); + + panel.addEventListener('touchend', () => { + if (!isDragging) return; + isDragging = false; + panel.style.transition = ''; + const diff = currentY - startY; + if (diff > 80) { + closeTextPanel(); + } + panel.style.transform = ''; + }, { passive: true }); + } + + function showToast(message) { + let toast = document.getElementById('ar-toast'); + if (!toast) { + toast = document.createElement('div'); + toast.id = 'ar-toast'; + toast.className = 'ar-toast'; + document.body.appendChild(toast); + } + toast.textContent = message; + toast.classList.add('visible'); + setTimeout(() => toast.classList.remove('visible'), 3000); + } + + // --- Particle Decoration System --- + function startDecorations(monument) { + stopDecorations(); + const canvas = document.getElementById('ar-particles'); + if (!canvas) return; + const ctx = canvas.getContext('2d'); + canvas.width = window.innerWidth; + canvas.height = window.innerHeight; + + const color = CATEGORY_COLORS[monument.category] || '#d4a04a'; + particles = []; + for (let i = 0; i < 30; i++) { + particles.push(createParticle(canvas, color, monument.category)); + } + animateParticles(ctx, canvas); + } + + function createParticle(canvas, color, category) { + const shapes = { palazzo: 'diamond', chiesa: 'circle', archeologico: 'square', fontana: 'circle' }; + return { + x: Math.random() * canvas.width, + y: Math.random() * canvas.height, + size: Math.random() * 4 + 2, + speedX: (Math.random() - 0.5) * 0.8, + speedY: -Math.random() * 1.2 - 0.3, + opacity: Math.random() * 0.6 + 0.2, + color, + shape: shapes[category] || 'circle', + life: Math.random() * 200 + 100, + maxLife: 300, + }; + } + + function animateParticles(ctx, canvas) { + ctx.clearRect(0, 0, canvas.width, canvas.height); + particles.forEach((p, i) => { + p.x += p.speedX; + p.y += p.speedY; + p.life--; + p.opacity = Math.max(0, (p.life / p.maxLife) * 0.6); + + if (p.life <= 0) { + const prevShape = p.shape; + const prevColor = p.color; + particles[i] = createParticle(canvas, prevColor, null); + particles[i].shape = prevShape; + particles[i].color = prevColor; + } + + ctx.globalAlpha = p.opacity; + ctx.fillStyle = p.color; + ctx.beginPath(); + if (p.shape === 'diamond') { + ctx.save(); + ctx.translate(p.x, p.y); + ctx.rotate(Math.PI / 4); + ctx.fillRect(-p.size / 2, -p.size / 2, p.size, p.size); + ctx.restore(); + } else if (p.shape === 'square') { + ctx.fillRect(p.x - p.size / 2, p.y - p.size / 2, p.size, p.size); + } else { + ctx.arc(p.x, p.y, p.size, 0, Math.PI * 2); + ctx.fill(); + } + }); + ctx.globalAlpha = 1; + animationFrame = requestAnimationFrame(() => animateParticles(ctx, canvas)); + } + + function stopDecorations() { + if (animationFrame) { + cancelAnimationFrame(animationFrame); + animationFrame = null; + } + particles = []; + const canvas = document.getElementById('ar-particles'); + if (canvas) { + const ctx = canvas.getContext('2d'); + ctx.clearRect(0, 0, canvas.width, canvas.height); + } + } + + function cleanup() { + stopCamera(); + stopGeolocation(); + stopAudio(); + stopDecorations(); + } + + return { + initCamera, + stopCamera, + startGeolocation, + stopGeolocation, + setActiveMonument, + clearActiveMonument, + toggleAudio, + toggleTextPanel, + closeTextPanel, + toggleCharacter, + dismissSpeech, + showToast, + cleanup, + }; +})(); diff --git a/js/monuments.js b/js/monuments.js new file mode 100644 index 0000000..e5b3f83 --- /dev/null +++ b/js/monuments.js @@ -0,0 +1,83 @@ +/** + * InsideGubbio AR - Monument Data Service + * Handles loading monument data and proximity detection + */ + +const MonumentService = (() => { + let monuments = []; + let routes = []; + let loaded = false; + + async function loadData() { + if (loaded) return { monuments, routes }; + try { + const response = await fetch('api/monuments.json'); + if (!response.ok) throw new Error('Failed to load monument data'); + const data = await response.json(); + monuments = data.monuments || []; + routes = data.routes || []; + loaded = true; + return { monuments, routes }; + } catch (err) { + console.error('MonumentService: Error loading data', err); + return { monuments: [], routes: [] }; + } + } + + function getDistance(lat1, lng1, lat2, lng2) { + const R = 6371e3; + const toRad = (deg) => (deg * Math.PI) / 180; + const dLat = toRad(lat2 - lat1); + const dLng = toRad(lng2 - lng1); + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(toRad(lat1)) * + Math.cos(toRad(lat2)) * + Math.sin(dLng / 2) * + Math.sin(dLng / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + return R * c; + } + + function getNearbyMonuments(userLat, userLng) { + return monuments + .map((m) => ({ + ...m, + distance: getDistance(userLat, userLng, m.lat, m.lng), + })) + .filter((m) => m.distance <= m.radius) + .sort((a, b) => a.distance - b.distance); + } + + function getClosestMonument(userLat, userLng) { + const nearby = getNearbyMonuments(userLat, userLng); + return nearby.length > 0 ? nearby[0] : null; + } + + function getAllMonuments() { + return monuments; + } + + function getMonumentById(id) { + return monuments.find((m) => m.id === id) || null; + } + + function getRoutes() { + return routes; + } + + function getRouteById(id) { + return routes.find((r) => r.id === id) || null; + } + + return { + loadData, + getDistance, + getNearbyMonuments, + getClosestMonument, + getAllMonuments, + getMonumentById, + getRoutes, + getRouteById, + }; +})();