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 { updateCauldron } from '../scene/cauldron.js';
|
||||||
import { updateFire } from '../scene/fireplace.js';
|
import { updateFire } from '../scene/fireplace.js';
|
||||||
import { updateRats } from '../scene/rat.js';
|
import { updateRats } from '../scene/rat.js';
|
||||||
|
import { updateLectern } from '../scene/lectern.js';
|
||||||
|
|
||||||
function updateCamera() {
|
function updateCamera() {
|
||||||
const globalTime = Date.now() * 0.00003;
|
const globalTime = Date.now() * 0.00003;
|
||||||
@ -160,6 +161,7 @@ export function animate() {
|
|||||||
updateScreenEffect();
|
updateScreenEffect();
|
||||||
updateFire();
|
updateFire();
|
||||||
updateCauldron();
|
updateCauldron();
|
||||||
|
updateLectern();
|
||||||
updateRats();
|
updateRats();
|
||||||
|
|
||||||
// RENDER!
|
// 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 { createTable } from './table.js';
|
||||||
import { createCauldron } from './cauldron.js';
|
import { createCauldron } from './cauldron.js';
|
||||||
import { createRats } from './rat.js';
|
import { createRats } from './rat.js';
|
||||||
|
import { createLectern } from './lectern.js';
|
||||||
import { PictureFrame } from './PictureFrame.js';
|
import { PictureFrame } from './PictureFrame.js';
|
||||||
import painting1 from '/textures/painting1.jpg';
|
import painting1 from '/textures/painting1.jpg';
|
||||||
import painting2 from '/textures/painting2.jpg';
|
import painting2 from '/textures/painting2.jpg';
|
||||||
@ -142,6 +143,9 @@ export function createSceneObjects() {
|
|||||||
// --- 9. Fireplace ---
|
// --- 9. Fireplace ---
|
||||||
createFireplace(state.roomSize / 2 - 0.5, -1, -Math.PI / 2);
|
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);
|
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);
|
//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