ai-time-machine / static /js /tte_animation_reference.js
manikandanj's picture
Prepare AI Time Machine hackathon Space
5862322 verified
Raw
History Blame Contribute Delete
16.9 kB
/**
* TRANS-TEMPORAL EXPRESS β€” Animation Timeline Reference
*
* This is a REFERENCE implementation showing the developer how to orchestrate
* the visual and audio sequences. It's pseudocode-quality β€” meant to be
* adapted into the existing cockpit.js structure.
*
* The existing codebase uses:
* - A 750ms polling interval reading from #tm-immersive-payload
* - Web Audio API for procedural ambient
* - CSS class toggling for state transitions
*
* This reference extends that pattern with the new experience flow.
*/
// ═══════════════════════════════════════════════════════════════
// ASSET PATHS
// ═══════════════════════════════════════════════════════════════
const ASSETS = {
images: {
cockpit_frame: '/file=static/img/cockpit/cockpit_frame.png',
throttle_neutral: '/file=static/img/cockpit/chrono_throttle_neutral.png',
comm_screen: '/file=static/img/cockpit/comm_screen.png',
gauge_cluster: '/file=static/img/cockpit/gauge_cluster.png',
idle_viewport: '/file=static/img/cockpit/idle_viewport.png',
travel_future: '/file=static/img/travel/travel_future.png',
travel_past: '/file=static/img/travel/travel_past.png',
steam_cloud: '/file=static/img/overlays/steam_cloud.png',
energy_lines: '/file=static/img/overlays/energy_lines.png',
speed_lines: '/file=static/img/overlays/speed_lines.png',
// Intro assets
temporal_ticket: '/file=static/img/intro/temporal_ticket.png',
conductor_box: '/file=static/img/intro/conductor_box.png',
train_materialization: '/file=static/img/intro/train_materialization.png',
},
audio: {
sfx: {
ticket_clang: '/file=static/audio/sfx/ticket_clang.mp3',
steam_burst: '/file=static/audio/sfx/steam_burst.mp3',
brake_screech: '/file=static/audio/sfx/brake_screech.mp3',
gears_grinding: '/file=static/audio/sfx/gears_grinding.mp3',
train_chug: '/file=static/audio/sfx/train_chug.mp3',
train_whistle: '/file=static/audio/sfx/train_whistle.mp3',
materialize: '/file=static/audio/sfx/materialize.mp3',
time_warp: '/file=static/audio/sfx/time_warp.mp3',
charge_up: '/file=static/audio/sfx/charge_up.mp3',
shockwave: '/file=static/audio/sfx/shockwave.mp3',
chime: '/file=static/audio/sfx/chime.mp3',
lever_pull: '/file=static/audio/sfx/lever_pull.mp3',
button_click: '/file=static/audio/sfx/button_click.mp3',
},
ambient: {
temporal_storm: '/file=static/audio/ambient/temporal_storm.mp3',
rain: '/file=static/audio/ambient/rain.mp3',
marketplace: '/file=static/audio/ambient/marketplace.mp3',
fire_hearth: '/file=static/audio/ambient/fire_hearth.mp3',
wind: '/file=static/audio/ambient/wind.mp3',
machinery: '/file=static/audio/ambient/machinery.mp3',
ocean: '/file=static/audio/ambient/ocean.mp3',
night_insects: '/file=static/audio/ambient/night_insects.mp3',
desert: '/file=static/audio/ambient/desert.mp3',
scifi_hum: '/file=static/audio/ambient/scifi_hum.mp3',
}
}
};
// Era sign data β€” signs that fly past during travel
const ERA_SIGNS = {
// Past-bound (shown in reverse chronological order from present)
past: [
{ text: 'THE SPACE AGE: 1960s', year: 1960 },
{ text: 'WORLD WARS: 1940s', year: 1940 },
{ text: 'THE JAZZ AGE: 1920s', year: 1920 },
{ text: 'THE INDUSTRIAL AGE: 1800s', year: 1800 },
{ text: 'AGE OF SAIL: 1700s', year: 1700 },
{ text: 'THE RENAISSANCE: 1500s', year: 1500 },
{ text: 'THE MIDDLE AGES: 1200s', year: 1200 },
{ text: 'ANCIENT EMPIRES: 500 BC', year: -500 },
{ text: 'DAWN OF CIVILIZATION: 3000 BC', year: -3000 },
],
// Future-bound (shown in forward chronological order from present)
future: [
{ text: 'NEAR FUTURE: 2050s', year: 2050 },
{ text: 'THE SINGULARITY: 2100s', year: 2100 },
{ text: 'INTERPLANETARY ERA: 2200s', year: 2200 },
{ text: 'THE FEDERATION: 2400s', year: 2400 },
{ text: 'POST-SCARCITY: 3000s', year: 3000 },
{ text: 'DEEP FUTURE: 5000+', year: 5000 },
]
};
// ═══════════════════════════════════════════════════════════════
// AUDIO MANAGER (extends existing Web Audio setup)
// ═══════════════════════════════════════════════════════════════
class AudioManager {
constructor() {
this.ctx = null; // AudioContext β€” create on first user interaction
this.sfxGain = null;
this.ambientGain = null;
this.masterGain = null;
this.currentAmbient = null;
this.preloaded = {};
}
init() {
if (this.ctx) return;
this.ctx = new (window.AudioContext || window.webkitAudioContext)();
this.masterGain = this.ctx.createGain();
this.sfxGain = this.ctx.createGain();
this.ambientGain = this.ctx.createGain();
this.sfxGain.connect(this.masterGain);
this.ambientGain.connect(this.masterGain);
this.masterGain.connect(this.ctx.destination);
this.sfxGain.gain.value = 0.8;
this.ambientGain.gain.value = 0.3;
}
async preload(key, url) {
try {
const response = await fetch(url);
const arrayBuffer = await response.arrayBuffer();
this.preloaded[key] = await this.ctx.decodeAudioData(arrayBuffer);
} catch (e) {
console.warn(`Failed to preload audio: ${key}`, e);
}
}
playSFX(key) {
if (!this.preloaded[key]) return;
const source = this.ctx.createBufferSource();
source.buffer = this.preloaded[key];
source.connect(this.sfxGain);
source.start();
return source;
}
playAmbient(key, fadeInMs = 2000) {
if (this.currentAmbient) {
// Crossfade: fade out current
this.currentAmbient.gain.linearRampToValueAtTime(
0, this.ctx.currentTime + fadeInMs / 1000
);
}
if (!this.preloaded[key]) return;
const source = this.ctx.createBufferSource();
const gain = this.ctx.createGain();
source.buffer = this.preloaded[key];
source.loop = true;
source.connect(gain);
gain.connect(this.ambientGain);
// Fade in
gain.gain.setValueAtTime(0, this.ctx.currentTime);
gain.gain.linearRampToValueAtTime(1, this.ctx.currentTime + fadeInMs / 1000);
source.start();
this.currentAmbient = gain;
return source;
}
// Duck ambient when character speaks
duckAmbient(level = 0.15) {
if (!this.ambientGain) return;
this.ambientGain.gain.linearRampToValueAtTime(level, this.ctx.currentTime + 0.3);
}
unduckAmbient(level = 0.3) {
if (!this.ambientGain) return;
this.ambientGain.gain.linearRampToValueAtTime(level, this.ctx.currentTime + 0.5);
}
}
// ═══════════════════════════════════════════════════════════════
// INTRO SEQUENCE
// ═══════════════════════════════════════════════════════════════
/**
* Orchestrates the intro: ticket β†’ conductor box β†’ materialization β†’ cockpit
* Replaces the existing 32s facility walkthrough.
* Total duration: ~20 seconds
*/
async function playIntroSequence(audioManager) {
const container = document.getElementById('tte-intro-container');
// Preload intro sounds
await Promise.all([
audioManager.preload('ticket_clang', ASSETS.audio.sfx.ticket_clang),
audioManager.preload('charge_up', ASSETS.audio.sfx.charge_up),
audioManager.preload('gears_grinding', ASSETS.audio.sfx.gears_grinding),
audioManager.preload('shockwave', ASSETS.audio.sfx.shockwave),
audioManager.preload('materialize', ASSETS.audio.sfx.materialize),
audioManager.preload('train_whistle', ASSETS.audio.sfx.train_whistle),
audioManager.preload('steam_burst', ASSETS.audio.sfx.steam_burst),
]);
// BEAT 1: Ticket appears (0-6s)
const ticket = container.querySelector('.tte-intro-ticket');
ticket.style.opacity = '1';
ticket.style.animation = 'tte-ticket-float 6s ease-in-out forwards';
// Start low procedural hum (reuse existing Web Audio oscillator pattern)
await sleep(6000);
// BEAT 2: Conductor box + ticket punch (6-11s)
const box = container.querySelector('.tte-intro-conductor-box');
box.style.opacity = '1';
box.style.transform = 'scale(1)';
await sleep(500);
audioManager.playSFX('ticket_clang');
// Screen flash
container.querySelector('.tte-intro-flash').style.opacity = '0.8';
await sleep(100);
container.querySelector('.tte-intro-flash').style.opacity = '0';
await sleep(1000);
audioManager.playSFX('charge_up');
await sleep(2000);
audioManager.playSFX('gears_grinding');
// Energy lines begin
container.querySelector('.tte-energy-lines').classList.add('active');
await sleep(1500);
// BEAT 3: Materialization (11-18s)
audioManager.playSFX('shockwave');
audioManager.playSFX('materialize');
// Screen shake
container.style.animation = 'tte-screen-shake 0.3s ease-in-out 3';
await sleep(500);
audioManager.playSFX('train_whistle');
// Train fades in
const train = container.querySelector('.tte-intro-train');
train.style.opacity = '1';
train.style.transform = 'scale(1)';
train.style.filter = 'brightness(1)';
await sleep(3000);
audioManager.playSFX('steam_burst');
await sleep(3500);
// BEAT 4: Transition to cockpit (18-22s)
// Flash to white, then fade to cockpit
container.querySelector('.tte-intro-flash').style.opacity = '1';
container.querySelector('.tte-intro-flash').style.background = 'white';
await sleep(500);
// Redirect to cockpit view (same pattern as current intro.js)
window.location.href = '/app';
}
// ═══════════════════════════════════════════════════════════════
// TRAVEL SEQUENCE β€” ERA SIGNS
// ═══════════════════════════════════════════════════════════════
/**
* Spawns era signs that fly across the viewport during travel.
* Signs accelerate as the train picks up speed.
*
* @param {string} direction - 'future' or 'past'
* @param {number} targetYear - destination year
* @param {number} durationMs - total travel duration
*/
function spawnEraSigns(direction, targetYear, durationMs) {
const viewport = document.querySelector('.tte-viewport');
const signs = ERA_SIGNS[direction] || ERA_SIGNS.past;
// Filter signs to only show those between current year and target
const currentYear = new Date().getFullYear();
const relevantSigns = signs.filter(s => {
if (direction === 'past') return s.year < currentYear && s.year >= targetYear;
return s.year > currentYear && s.year <= targetYear;
});
const signCount = Math.min(relevantSigns.length, 8); // Max 8 signs
const interval = durationMs / (signCount + 1);
relevantSigns.slice(0, signCount).forEach((sign, i) => {
setTimeout(() => {
const el = document.createElement('div');
el.className = `tte-era-sign ${direction}`;
el.textContent = sign.text;
// Signs get faster as travel progresses
const speed = Math.max(0.3, 1.5 - (i * 0.15));
el.style.setProperty('--sign-speed', `${speed}s`);
// Vary vertical position for depth
el.style.top = `${30 + Math.random() * 30}%`;
viewport.appendChild(el);
// Remove after animation
el.addEventListener('animationend', () => el.remove());
}, interval * (i + 1));
});
}
// ═══════════════════════════════════════════════════════════════
// YEAR COUNTER ANIMATION
// ═══════════════════════════════════════════════════════════════
/**
* Animates the year counter from current year to target year.
* Uses eased counting for dramatic effect.
*
* @param {number} from - starting year
* @param {number} to - target year
* @param {number} durationMs - animation duration
*/
function animateYearCounter(from, to, durationMs) {
const counterEl = document.querySelector('.tte-year-counter .tte-year-value');
if (!counterEl) return;
const startTime = performance.now();
const diff = to - from;
function update(currentTime) {
const elapsed = currentTime - startTime;
const progress = Math.min(elapsed / durationMs, 1);
// Cubic ease-in-out for dramatic feel
const eased = progress < 0.5
? 4 * progress * progress * progress
: 1 - Math.pow(-2 * progress + 2, 3) / 2;
const currentYear = Math.round(from + diff * eased);
// Format: negative years as "XXX BC"
if (currentYear < 0) {
counterEl.textContent = `${Math.abs(currentYear)} BC`;
} else {
counterEl.textContent = currentYear.toString();
}
if (progress < 1) {
requestAnimationFrame(update);
}
}
requestAnimationFrame(update);
}
// ═══════════════════════════════════════════════════════════════
// LANDING / STEAM REVEAL
// ═══════════════════════════════════════════════════════════════
/**
* Orchestrates the landing sequence:
* 1. Brake screech + violent shake
* 2. Steam cloud covers viewport
* 3. AI world image loads behind steam
* 4. Steam clears to reveal destination
* 5. Chime plays, narration starts
*/
async function playLandingSequence(audioManager, worldImageB64, ambientKey) {
const cockpit = document.querySelector('.tte-cockpit');
const steamCloud = document.querySelector('.tte-steam-cloud');
const viewport = document.querySelector('.tte-viewport-content');
// 1. Braking (0-3s)
cockpit.classList.add('tte-state-braking');
audioManager.playSFX('brake_screech');
await sleep(2000);
// 2. Steam burst (2-3s)
audioManager.playSFX('steam_burst');
steamCloud.classList.add('active');
cockpit.classList.remove('tte-state-braking');
cockpit.classList.remove('tte-state-traveling');
await sleep(1000);
// 3. Load world image behind steam (3-5s)
if (worldImageB64) {
viewport.src = `data:image/png;base64,${worldImageB64}`;
}
// Start era ambient
if (ambientKey && ASSETS.audio.ambient[ambientKey]) {
audioManager.playAmbient(ambientKey, 3000);
}
await sleep(2000);
// 4. Steam clears (5-8s)
steamCloud.classList.remove('active');
await sleep(1000);
// 5. Arrival chime
audioManager.playSFX('chime');
cockpit.classList.add('tte-state-conversation');
}
// ═══════════════════════════════════════════════════════════════
// UTILITY
// ═══════════════════════════════════════════════════════════════
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
/**
* Preload all critical images to avoid flicker during transitions
*/
function preloadImages(urls) {
return Promise.all(urls.map(url => {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = resolve;
img.onerror = reject;
img.src = url;
});
}));
}
// Export for use in cockpit.js integration
// window.TTE = { AudioManager, playIntroSequence, spawnEraSigns, animateYearCounter, playLandingSequence, ASSETS, ERA_SIGNS };