diff --git a/tv-player/src/core/animate.js b/tv-player/src/core/animate.js index 04b7e3b..6bcd0a8 100644 --- a/tv-player/src/core/animate.js +++ b/tv-player/src/core/animate.js @@ -88,6 +88,65 @@ function updateVcr() { } } +function updateBooks() { + const LEVITATE_CHANCE = 0.001; // Chance for a resting book to start levitating per frame + const LEVITATE_DURATION_MIN = 100; // frames + const LEVITATE_DURATION_MAX = 300; // frames + const LEVITATE_AMPLITUDE = 0.02; // Max vertical displacement + const LEVITATE_SPEED_FACTOR = 0.03; // Speed of oscillation + const START_RATE = 0.05; // How quickly a book starts to levitate + const RETURN_RATE = 0.1; // How quickly a book returns to original position + const START_DURATION = 120; // frames for the starting transition + const levitation = state.bookLevitation; + + // Manage the global levitation state + if (levitation.state === 'resting') { + if (Math.random() < LEVITATE_CHANCE) { + levitation.state = 'starting'; + levitation.timer = START_DURATION; + } + } else if (levitation.state === 'starting') { + levitation.timer--; + if (levitation.timer <= 0) { + levitation.state = 'levitating'; + levitation.timer = LEVITATE_DURATION_MIN + Math.random() * (LEVITATE_DURATION_MAX - LEVITATE_DURATION_MIN); + } + } else if (levitation.state === 'levitating') { + levitation.timer--; + if (levitation.timer <= 0) { + levitation.state = 'returning'; + } + } + + // Animate books based on the global state + let allBooksReturned = true; + state.books.forEach(book => { + const data = book.userData; + + if (levitation.state === 'starting') { + allBooksReturned = false; + book.position.y = THREE.MathUtils.lerp(book.position.y, data.originalY + LEVITATE_AMPLITUDE/2, START_RATE); + data.oscillationTime = 0; + } else if (levitation.state === 'levitating') { + allBooksReturned = false; + data.oscillationTime += LEVITATE_SPEED_FACTOR; + data.levitateOffset = Math.sin(data.oscillationTime) * LEVITATE_AMPLITUDE; + book.position.y = data.originalY + data.levitateOffset + LEVITATE_AMPLITUDE/2; + } else if (levitation.state === 'returning') { + book.position.y = THREE.MathUtils.lerp(book.position.y, data.originalY, RETURN_RATE); + data.levitateOffset = book.position.y - data.originalY; + + if (Math.abs(data.levitateOffset) > 0.001) { + allBooksReturned = false; + } + } + }); + + if (levitation.state === 'returning' && allBooksReturned) { + levitation.state = 'resting'; + } +} + // --- Animation Loop --- export function animate() { requestAnimationFrame(animate); @@ -98,6 +157,7 @@ export function animate() { updateScreenLight(); updateVideo(); updateVcr(); + updateBooks(); updateDoor(); // RENDER! diff --git a/tv-player/src/scene/bookshelf.js b/tv-player/src/scene/bookshelf.js index 03d29be..099057b 100644 --- a/tv-player/src/scene/bookshelf.js +++ b/tv-player/src/scene/bookshelf.js @@ -102,8 +102,16 @@ export function createBookshelf(x, z, rotationY, uniqueSeed) { book.castShadow = true; book.receiveShadow = true; + // Store original Y position and animation data + book.userData.originalY = book.position.y; + book.userData.levitateOffset = 0; + book.userData.oscillationTime = Math.random() * Math.PI * 2; // Start at random phase shelfGroup.add(book); + if (Math.random() > 0.8) { + state.books.push(book); + } + currentBookX += bookWidth + 0.002; // Tiny gap between books if (seededRandom() > 0.92) { diff --git a/tv-player/src/state.js b/tv-player/src/state.js index 59c2066..a750a86 100644 --- a/tv-player/src/state.js +++ b/tv-player/src/state.js @@ -44,6 +44,11 @@ export function initState() { // Utilities loader: new THREE.TextureLoader(), landingSurfaces: [], + bookLevitation: { + state: 'resting', // 'resting', 'levitating', 'returning' + timer: 0, + }, + books: [], // Array to hold all individual book meshes for animation raycaster: new THREE.Raycaster(), seed: 12345, };