diff --git a/rse/src/input.ts b/rse/src/input.ts new file mode 100644 index 0000000..f3e72a8 --- /dev/null +++ b/rse/src/input.ts @@ -0,0 +1,28 @@ +export interface InputState { + targetX: number + targetY: number + smoothX: number + smoothY: number +} + +export function createInputState(): InputState { + return { targetX: 0, targetY: 0, smoothX: 0, smoothY: 0 } +} + +export function setupInputListeners(state: InputState): void { + document.addEventListener('mousemove', (e) => { + state.targetX = (e.clientX / window.innerWidth - 0.5) * 2 + state.targetY = (e.clientY / window.innerHeight - 0.5) * 2 + }) + + document.addEventListener('touchmove', (e) => { + const t = e.touches[0] + state.targetX = (t.clientX / window.innerWidth - 0.5) * 2 + state.targetY = (t.clientY / window.innerHeight - 0.5) * 2 + }, { passive: true }) +} + +export function smoothInput(state: InputState): void { + state.smoothX += (state.targetX - state.smoothX) * 0.025 + state.smoothY += (state.targetY - state.smoothY) * 0.025 +} diff --git a/rse/src/language.ts b/rse/src/language.ts new file mode 100644 index 0000000..073e64a --- /dev/null +++ b/rse/src/language.ts @@ -0,0 +1,38 @@ +function applyLang(lang: string): void { + document.querySelectorAll('[data-lang-en]').forEach(el => { + const text = el.getAttribute(`data-lang-${lang}`) + if (text) el.innerHTML = text + }) + + document.getElementById('lang-dropdown') + ?.querySelectorAll('.lang-option') + .forEach(opt => { + opt.classList.toggle('selected', (opt as HTMLElement).dataset.lang === lang) + }) +} + +export function setupLanguageSelector(): void { + let currentLang = localStorage.getItem('preferred-lang') || 'en' + + const langBtn = document.getElementById('lang-tab') + const langDropdown = document.getElementById('lang-dropdown') + const menuDropdown = document.getElementById('menu-dropdown') + + langBtn?.addEventListener('click', (e) => { + e.stopPropagation() + langDropdown?.classList.toggle('show') + menuDropdown?.classList.remove('show') + }) + + langDropdown?.querySelectorAll('.lang-option').forEach(opt => { + opt.addEventListener('click', (e) => { + e.stopPropagation() + currentLang = (opt as HTMLElement).dataset.lang || 'en' + localStorage.setItem('preferred-lang', currentLang) + applyLang(currentLang) + langDropdown?.classList.remove('show') + }) + }) + + applyLang(currentLang) +} diff --git a/rse/src/main.ts b/rse/src/main.ts index c8e3eff..b169b74 100644 --- a/rse/src/main.ts +++ b/rse/src/main.ts @@ -1,41 +1,30 @@ import './style.css' import * as THREE from 'three' import { createScene, updateScene } from './scene' +import { createInputState, setupInputListeners, smoothInput } from './input' +import { setupNavigation } from './navigation' +import { setupLanguageSelector } from './language' +import { setupMenu } from './menu' -// Scene setup +// Scene const canvas = document.getElementById('space-canvas') as HTMLCanvasElement const sceneObjs = createScene(canvas) -// Mouse / Touch -let targetX = 0 -let targetY = 0 -let smoothX = 0 -let smoothY = 0 - -document.addEventListener('mousemove', (e) => { - targetX = (e.clientX / window.innerWidth - 0.5) * 2 - targetY = (e.clientY / window.innerHeight - 0.5) * 2 -}) - -document.addEventListener('touchmove', (e) => { - const t = e.touches[0] - targetX = (t.clientX / window.innerWidth - 0.5) * 2 - targetY = (t.clientY / window.innerHeight - 0.5) * 2 -}, { passive: true }) +// Input +const input = createInputState() +setupInputListeners(input) // Animation loop -let time = 0 const clock = new THREE.Clock() +let time = 0 -function animate() { +function animate(): void { requestAnimationFrame(animate) const dt = clock.getDelta() time += dt - smoothX += (targetX - smoothX) * 0.025 - smoothY += (targetY - smoothY) * 0.025 - - updateScene(sceneObjs, time, dt, smoothX, smoothY) + smoothInput(input) + updateScene(sceneObjs, time, dt, input.smoothX, input.smoothY) sceneObjs.renderer.render(sceneObjs.scene, sceneObjs.camera) } @@ -48,99 +37,9 @@ window.addEventListener('resize', () => { sceneObjs.renderer.setSize(window.innerWidth, window.innerHeight) }) -// Page navigation -const pageVideo = document.getElementById('page-video') -const pageLogo = document.getElementById('page-logo') -const pageMessage = document.getElementById('page-message') -const pageMessage2 = document.getElementById('page-message2') -const pageAbout = document.getElementById('page-about') -const pageTitle = document.getElementById('page-title') -const menuBtn = document.getElementById('menu-btn') -const menuDropdown = document.getElementById('menu-dropdown') +// UI +setupNavigation() +setupLanguageSelector() +setupMenu() -let currentPage: HTMLElement | null = null - -const siteHeader = document.getElementById('site-header') -const siteFooter = document.getElementById('site-footer') -const subpages = [pageAbout] -const fullpages = [pageTitle] - -function showPage(show: HTMLElement | null, hide: HTMLElement | null) { - if (hide) { - hide.classList.remove('visible') - hide.classList.add('page-hidden') - } - if (show) { - show.classList.remove('page-hidden') - show.classList.add('visible') - } - currentPage = show - const isFull = fullpages.includes(show) - if (siteHeader) siteHeader.style.display = isFull ? 'none' : '' - if (siteFooter) { - siteFooter.style.display = isFull ? 'none' : '' - siteFooter.classList.toggle('footer-solid', subpages.includes(show)) - } -} - -// Main page navigation -document.getElementById('btn-next')?.addEventListener('click', () => showPage(pageLogo, pageVideo)) -document.getElementById('btn-logo-back')?.addEventListener('click', () => showPage(pageVideo, pageLogo)) -document.getElementById('btn-logo-next')?.addEventListener('click', () => showPage(pageMessage, pageLogo)) -document.getElementById('btn-msg-back')?.addEventListener('click', () => showPage(pageLogo, pageMessage)) -document.getElementById('btn-msg-next')?.addEventListener('click', () => showPage(pageMessage2, pageMessage)) -document.getElementById('btn-msg2-back')?.addEventListener('click', () => showPage(pageMessage, pageMessage2)) -document.getElementById('btn-msg2-next')?.addEventListener('click', () => showPage(pageAbout, pageMessage2)) -document.getElementById('btn-about-back')?.addEventListener('click', () => showPage(pageMessage2, pageAbout)) -document.getElementById('btn-about-next')?.addEventListener('click', () => showPage(pageTitle, pageAbout)) -pageTitle?.addEventListener('click', () => showPage(pageAbout, pageTitle)) - -// Menu dropdown -menuBtn?.addEventListener('click', (e) => { - e.stopPropagation() - menuDropdown?.classList.toggle('show') - langDropdown?.classList.remove('show') -}) - -// Language selector -let currentLang = localStorage.getItem('preferred-lang') || 'en' -const langBtn = document.getElementById('lang-tab') -const langDropdown = document.getElementById('lang-dropdown') - -function applyLang(lang: string) { - document.querySelectorAll('[data-lang-en]').forEach(el => { - const text = el.getAttribute(`data-lang-${lang}`) - if (text) el.innerHTML = text - }) - langDropdown?.querySelectorAll('.lang-option').forEach(opt => { - opt.classList.toggle('selected', (opt as HTMLElement).dataset.lang === lang) - }) -} - -langBtn?.addEventListener('click', (e) => { - e.stopPropagation() - langDropdown?.classList.toggle('show') - menuDropdown?.classList.remove('show') -}) - -langDropdown?.querySelectorAll('.lang-option').forEach(opt => { - opt.addEventListener('click', (e) => { - e.stopPropagation() - currentLang = (opt as HTMLElement).dataset.lang || 'en' - localStorage.setItem('preferred-lang', currentLang) - applyLang(currentLang) - langDropdown?.classList.remove('show') - }) -}) - -document.addEventListener('click', () => { - langDropdown?.classList.remove('show') - menuDropdown?.classList.remove('show') -}) - -applyLang(currentLang) - -// Show first page immediately -pageVideo?.classList.add('visible') -currentPage = pageVideo document.body.style.opacity = '1' diff --git a/rse/src/menu.ts b/rse/src/menu.ts new file mode 100644 index 0000000..e77e15e --- /dev/null +++ b/rse/src/menu.ts @@ -0,0 +1,16 @@ +export function setupMenu(): void { + const menuBtn = document.getElementById('menu-btn') + const menuDropdown = document.getElementById('menu-dropdown') + const langDropdown = document.getElementById('lang-dropdown') + + menuBtn?.addEventListener('click', (e) => { + e.stopPropagation() + menuDropdown?.classList.toggle('show') + langDropdown?.classList.remove('show') + }) + + document.addEventListener('click', () => { + langDropdown?.classList.remove('show') + menuDropdown?.classList.remove('show') + }) +} diff --git a/rse/src/navigation.ts b/rse/src/navigation.ts new file mode 100644 index 0000000..595e5af --- /dev/null +++ b/rse/src/navigation.ts @@ -0,0 +1,76 @@ +interface NavigationElements { + siteHeader: HTMLElement | null + siteFooter: HTMLElement | null +} + +type PageId = 'page-video' | 'page-logo' | 'page-message' | 'page-message2' | 'page-about' | 'page-title' + +interface PageRoute { + btnId: string + from: PageId + to: PageId +} + +const routes: PageRoute[] = [ + { btnId: 'btn-next', from: 'page-video', to: 'page-logo' }, + { btnId: 'btn-logo-back', from: 'page-logo', to: 'page-video' }, + { btnId: 'btn-logo-next', from: 'page-logo', to: 'page-message' }, + { btnId: 'btn-msg-back', from: 'page-message', to: 'page-logo' }, + { btnId: 'btn-msg-next', from: 'page-message', to: 'page-message2' }, + { btnId: 'btn-msg2-back', from: 'page-message2', to: 'page-message' }, + { btnId: 'btn-msg2-next', from: 'page-message2', to: 'page-about' }, + { btnId: 'btn-about-back', from: 'page-about', to: 'page-message2' }, + { btnId: 'btn-about-next', from: 'page-about', to: 'page-title' }, +] + +const subpageIds: PageId[] = ['page-about'] +const fullpageIds: PageId[] = ['page-title'] + +function getPage(id: PageId): HTMLElement | null { + return document.getElementById(id) +} + +function showPage( + show: HTMLElement | null, + hide: HTMLElement | null, + showId: PageId, + nav: NavigationElements, +): void { + if (hide) { + hide.classList.remove('visible') + hide.classList.add('page-hidden') + } + if (show) { + show.classList.remove('page-hidden') + show.classList.add('visible') + } + + const isFull = fullpageIds.includes(showId) + if (nav.siteHeader) nav.siteHeader.style.display = isFull ? 'none' : '' + if (nav.siteFooter) { + nav.siteFooter.style.display = isFull ? 'none' : '' + nav.siteFooter.classList.toggle('footer-solid', subpageIds.includes(showId)) + } +} + +export function setupNavigation(): void { + const nav: NavigationElements = { + siteHeader: document.getElementById('site-header'), + siteFooter: document.getElementById('site-footer'), + } + + for (const route of routes) { + document.getElementById(route.btnId)?.addEventListener('click', () => { + showPage(getPage(route.to), getPage(route.from), route.to, nav) + }) + } + + // Title page click returns to about + getPage('page-title')?.addEventListener('click', () => { + showPage(getPage('page-about'), getPage('page-title'), 'page-about', nav) + }) + + // Show first page + const firstPage = getPage('page-video') + firstPage?.classList.add('visible') +}