Feature: Lectern with spellbook flipping pages
This commit is contained in:
parent
a71fd38e9e
commit
bb923968e5
@ -5,6 +5,7 @@ import { updateScreenEffect } from '../scene/magic-mirror.js'
|
||||
import { updateCauldron } from '../scene/cauldron.js';
|
||||
import { updateFire } from '../scene/fireplace.js';
|
||||
import { updateRats } from '../scene/rat.js';
|
||||
import { updateLectern } from '../scene/lectern.js';
|
||||
|
||||
function updateCamera() {
|
||||
const globalTime = Date.now() * 0.00003;
|
||||
@ -160,6 +161,7 @@ export function animate() {
|
||||
updateScreenEffect();
|
||||
updateFire();
|
||||
updateCauldron();
|
||||
updateLectern();
|
||||
updateRats();
|
||||
|
||||
// RENDER!
|
||||
|
||||
213
magic-mirror/src/scene/lectern.js
Normal file
213
magic-mirror/src/scene/lectern.js
Normal file
@ -0,0 +1,213 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import woodTextureUrl from '/textures/wood.png';
|
||||
import spellbookSpritesheetUrl from '/textures/pages_sheet.png';
|
||||
|
||||
let lecternState = {
|
||||
isFlipping: false,
|
||||
flipProgress: 0,
|
||||
flipSpeed: 0.015,
|
||||
leftPageIndex: 0,
|
||||
rightPageIndex: 1,
|
||||
totalPages: 4, // 2x2 spritesheet
|
||||
pageFlipChance: 0.01,
|
||||
flippingPage: null,
|
||||
flippingPageMaterials: null,
|
||||
leftPage: null,
|
||||
rightPage: null,
|
||||
spritesheetTexture: null,
|
||||
};
|
||||
|
||||
const bookWidth = 0.7;
|
||||
const bookHeight = 0.45;
|
||||
|
||||
function setPageTexture(material, pageIndex) {
|
||||
const texture = material.map;
|
||||
if (!texture) return;
|
||||
|
||||
texture.wrapS = THREE.ClampToEdgeWrapping;
|
||||
texture.wrapT = THREE.ClampToEdgeWrapping;
|
||||
texture.repeat.set(0.5, 0.5);
|
||||
|
||||
const col = pageIndex % 2;
|
||||
const row = 1 - Math.floor(pageIndex / 2); // Invert row for THREE.js UVs (bottom-left origin)
|
||||
|
||||
texture.offset.set(col * 0.5, row * 0.5);
|
||||
}
|
||||
|
||||
export function createLectern(x, y, z, rotY) {
|
||||
const lecternGroup = new THREE.Group();
|
||||
|
||||
// --- Materials ---
|
||||
const woodMaterial = new THREE.MeshPhongMaterial({
|
||||
map: state.loader.load(woodTextureUrl),
|
||||
color: 0x6b4f3a,
|
||||
shininess: 20
|
||||
});
|
||||
|
||||
// 1. Lectern Stand
|
||||
const baseGeo = new THREE.BoxGeometry(0.6, 0.05, 0.6);
|
||||
const base = new THREE.Mesh(baseGeo, woodMaterial);
|
||||
base.castShadow = true;
|
||||
lecternGroup.add(base);
|
||||
|
||||
const poleGeo = new THREE.CylinderGeometry(0.08, 0.1, 1.0, 12);
|
||||
const pole = new THREE.Mesh(poleGeo, woodMaterial);
|
||||
pole.position.y = 0.5;
|
||||
pole.castShadow = true;
|
||||
lecternGroup.add(pole);
|
||||
|
||||
const topGeo = new THREE.BoxGeometry(0.8, 0.05, 0.5);
|
||||
const top = new THREE.Mesh(topGeo, woodMaterial);
|
||||
top.position.y = 1.1;
|
||||
top.rotation.x = Math.PI/2 + -Math.PI * 0.3; // Slanted top
|
||||
top.castShadow = true;
|
||||
lecternGroup.add(top);
|
||||
|
||||
// 2. Spellbook
|
||||
const bookGroup = new THREE.Group();
|
||||
bookGroup.position.y = 1.2;
|
||||
bookGroup.position.z = 0.01;
|
||||
bookGroup.rotation.x = -Math.PI * 0.3;
|
||||
|
||||
const pageGeo = new THREE.PlaneGeometry(bookWidth / 2, bookHeight);
|
||||
|
||||
// 2.6. Stack of Pages
|
||||
const pageStackHeight = 0.08;
|
||||
const pageStackGeo = new THREE.BoxGeometry(bookWidth, bookHeight, pageStackHeight);
|
||||
const pageStackMaterial = new THREE.MeshPhongMaterial({ color: 0xffeedd, shininess: 5 });
|
||||
const pageStack = new THREE.Mesh(pageStackGeo, pageStackMaterial);
|
||||
pageStack.position.x = 0.01;
|
||||
pageStack.position.z = -pageStackHeight/2; // Position it just behind the pages
|
||||
pageStack.position.y = 0.01; // Position it a bit lower
|
||||
pageStack.castShadow = true;
|
||||
bookGroup.add(pageStack);
|
||||
|
||||
|
||||
// 2.5. Book Cover
|
||||
const coverMaterial = new THREE.MeshPhongMaterial({ color: 0x8B0000, shininess: 15 }); // DarkRed
|
||||
const coverWidth = bookWidth + 0.04;
|
||||
const coverHeight = bookHeight + 0.04;
|
||||
const coverDepth = 0.01;
|
||||
const coverGeo = new THREE.BoxGeometry(coverWidth, coverHeight, coverDepth);
|
||||
const bookCover = new THREE.Mesh(coverGeo, coverMaterial);
|
||||
// Position it just behind the pages
|
||||
bookCover.position.x = 0.01;
|
||||
bookCover.position.z = -pageStackHeight/2 -coverDepth / 2 - 0.01;
|
||||
bookCover.castShadow = true;
|
||||
bookGroup.add(bookCover);
|
||||
|
||||
// Load single spritesheet texture
|
||||
lecternState.spritesheetTexture = state.loader.load(spellbookSpritesheetUrl);
|
||||
|
||||
// Create page materials
|
||||
// We clone the material so each page can have an independent texture offset
|
||||
const leftPageMat = new THREE.MeshPhongMaterial({ map: lecternState.spritesheetTexture.clone() });
|
||||
const rightPageMat = new THREE.MeshPhongMaterial({ map: lecternState.spritesheetTexture.clone() });
|
||||
|
||||
// Left and Right static pages
|
||||
setPageTexture(leftPageMat, lecternState.leftPageIndex);
|
||||
setPageTexture(rightPageMat, lecternState.rightPageIndex);
|
||||
|
||||
lecternState.leftPage = new THREE.Mesh(pageGeo, leftPageMat);
|
||||
lecternState.leftPage.position.x = -bookWidth / 4;
|
||||
lecternState.leftPage.position.z = 0.01;
|
||||
lecternState.leftPage.visible = true;
|
||||
lecternState.leftPage.receiveShadow = true;
|
||||
bookGroup.add(lecternState.leftPage);
|
||||
|
||||
lecternState.rightPage = new THREE.Mesh(pageGeo, rightPageMat);
|
||||
lecternState.rightPage.position.x = bookWidth / 4;
|
||||
lecternState.rightPage.position.z = 0.01;
|
||||
lecternState.rightPage.visible = true;
|
||||
lecternState.rightPage.receiveShadow = true;
|
||||
bookGroup.add(lecternState.rightPage);
|
||||
|
||||
|
||||
// The page that will animate
|
||||
const flippingPageMat = new THREE.MeshPhongMaterial({
|
||||
map: lecternState.spritesheetTexture.clone(),
|
||||
});
|
||||
const flippingPageBackMat = new THREE.MeshPhongMaterial({
|
||||
map: lecternState.spritesheetTexture.clone(),
|
||||
});
|
||||
// Assign a name to identify the back material in setPageTexture
|
||||
flippingPageBackMat.name = 'back';
|
||||
lecternState.flippingPageMaterials = [flippingPageMat, flippingPageBackMat];
|
||||
|
||||
// Create a group for the flipping page
|
||||
const flippingPageGroup = new THREE.Group();
|
||||
|
||||
const frontPage = new THREE.Mesh(pageGeo, flippingPageMat);
|
||||
const backPage = new THREE.Mesh(pageGeo, flippingPageBackMat);
|
||||
backPage.rotation.y = Math.PI; // Rotate the back page to face the other way
|
||||
|
||||
flippingPageGroup.add(frontPage);
|
||||
flippingPageGroup.add(backPage);
|
||||
|
||||
lecternState.flippingPage = flippingPageGroup; // Assign the group to the state
|
||||
lecternState.flippingPage.position.x = bookWidth / 4;
|
||||
lecternState.flippingPage.visible = false;
|
||||
lecternState.flippingPage.castShadow = true;
|
||||
bookGroup.add(lecternState.flippingPage);
|
||||
|
||||
lecternGroup.add(bookGroup);
|
||||
|
||||
// Position and add to scene
|
||||
lecternGroup.position.set(x, y, z);
|
||||
lecternGroup.rotation.y = rotY;
|
||||
state.scene.add(lecternGroup);
|
||||
}
|
||||
|
||||
export function updateLectern() {
|
||||
const { isFlipping, pageFlipChance, flippingPage, flippingPageMaterials, leftPage, rightPage } = lecternState;
|
||||
|
||||
// Randomly decide to start flipping a page
|
||||
if (!isFlipping && Math.random() < pageFlipChance) {
|
||||
lecternState.isFlipping = true;
|
||||
lecternState.flipProgress = 0;
|
||||
|
||||
const nextPageIndex = (lecternState.rightPageIndex + 1) % lecternState.totalPages;
|
||||
const nextNextPageIndex = (lecternState.rightPageIndex + 2) % lecternState.totalPages;
|
||||
|
||||
// Set the front of the flipping page to the current right page's content
|
||||
setPageTexture(flippingPageMaterials[0], lecternState.rightPageIndex);
|
||||
// Set the back of the flipping page to what will be the new right page's content
|
||||
setPageTexture(flippingPageMaterials[1], nextPageIndex);
|
||||
flippingPageMaterials[0].map.needsUpdate = true;
|
||||
flippingPageMaterials[1].map.needsUpdate = true;
|
||||
flippingPage.visible = true;
|
||||
flippingPage.position.x = rightPage.position.x;
|
||||
flippingPage.rotation.y = 0;
|
||||
|
||||
// Immediately update the static right page to show the *next* page, and keep it visible.
|
||||
setPageTexture(rightPage.material, nextNextPageIndex);
|
||||
rightPage.material.needsUpdate = true;
|
||||
}
|
||||
|
||||
if (isFlipping) {
|
||||
lecternState.flipProgress += lecternState.flipSpeed;
|
||||
const progress = lecternState.flipProgress;
|
||||
|
||||
// Animate the page turning from right to left (0 to PI)
|
||||
flippingPage.rotation.y = -progress * Math.PI;
|
||||
// Move it in an arc
|
||||
flippingPage.position.x = (rightPage.position.x) * Math.cos(progress * Math.PI);
|
||||
flippingPage.position.z = Math.sin(progress * Math.PI) * bookWidth/4 + 0.015;
|
||||
|
||||
// When flip is complete
|
||||
if (progress >= 1.0) {
|
||||
// The left page now shows the content of the page that just landed.
|
||||
lecternState.leftPageIndex = (lecternState.rightPageIndex + 1) % lecternState.totalPages;
|
||||
// The right page index officially becomes the next page's index.
|
||||
lecternState.rightPageIndex = (lecternState.rightPageIndex + 2) % lecternState.totalPages;
|
||||
|
||||
// Update the left page's texture to finalize the flip.
|
||||
setPageTexture(leftPage.material, lecternState.leftPageIndex);
|
||||
|
||||
// Reset state
|
||||
flippingPage.visible = false;
|
||||
lecternState.isFlipping = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ import { createFireplace } from './fireplace.js';
|
||||
import { createTable } from './table.js';
|
||||
import { createCauldron } from './cauldron.js';
|
||||
import { createRats } from './rat.js';
|
||||
import { createLectern } from './lectern.js';
|
||||
import { PictureFrame } from './PictureFrame.js';
|
||||
import painting1 from '/textures/painting1.jpg';
|
||||
import painting2 from '/textures/painting2.jpg';
|
||||
@ -142,6 +143,9 @@ export function createSceneObjects() {
|
||||
// --- 9. Fireplace ---
|
||||
createFireplace(state.roomSize / 2 - 0.5, -1, -Math.PI / 2);
|
||||
|
||||
// --- Lectern ---
|
||||
createLectern(-state.roomSize/2 + 0.4, 0, -0.1, Math.PI / 2.3);
|
||||
|
||||
createRats(state.roomSize/2 - 0.01, 0, 0.37, -Math.PI / 2);
|
||||
|
||||
//createBookshelf(-state.roomSize/2 + 0.2, state.roomSize/2*0.1, Math.PI/2, 0);
|
||||
|
||||
BIN
magic-mirror/textures/pages_sheet.png
Normal file
BIN
magic-mirror/textures/pages_sheet.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 488 KiB |
Loading…
Reference in New Issue
Block a user