New project: Magic mirror
This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
import { DustEffect } from './dust.js';
|
||||
import { FliesEffect } from './flies.js';
|
||||
import { SpiderEffect } from './spider.js';
|
||||
|
||||
export class EffectsManager {
|
||||
constructor(scene) {
|
||||
this.effects = [];
|
||||
this._initializeEffects(scene);
|
||||
}
|
||||
|
||||
_initializeEffects(scene) {
|
||||
// Add all desired effects here.
|
||||
// This is now the single place to manage which effects are active.
|
||||
this.addEffect(new DustEffect(scene));
|
||||
this.addEffect(new FliesEffect(scene));
|
||||
this.addEffect(new SpiderEffect(scene));
|
||||
}
|
||||
|
||||
addEffect(effect) {
|
||||
this.effects.push(effect);
|
||||
}
|
||||
|
||||
update() {
|
||||
this.effects.forEach(effect => effect.update());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
import * as THREE from 'three';
|
||||
|
||||
export class DustEffect {
|
||||
constructor(scene) {
|
||||
this.dust = null;
|
||||
this._create(scene);
|
||||
}
|
||||
|
||||
_create(scene) {
|
||||
const particleCount = 2000;
|
||||
const particlesGeometry = new THREE.BufferGeometry();
|
||||
const positions = [];
|
||||
|
||||
for (let i = 0; i < particleCount; i++) {
|
||||
positions.push(
|
||||
(Math.random() - 0.5) * 15,
|
||||
Math.random() * 10,
|
||||
(Math.random() - 0.5) * 15
|
||||
);
|
||||
}
|
||||
particlesGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
||||
|
||||
const particleMaterial = new THREE.PointsMaterial({
|
||||
color: 0xffffff,
|
||||
size: 0.015,
|
||||
transparent: true,
|
||||
opacity: 0.08,
|
||||
blending: THREE.AdditiveBlending
|
||||
});
|
||||
|
||||
this.dust = new THREE.Points(particlesGeometry, particleMaterial);
|
||||
scene.add(this.dust);
|
||||
}
|
||||
|
||||
update() {
|
||||
if (this.dust) {
|
||||
const positions = this.dust.geometry.attributes.position.array;
|
||||
for (let i = 1; i < positions.length; i += 3) {
|
||||
positions[i] -= 0.001;
|
||||
if (positions[i] < -2) {
|
||||
positions[i] = 8;
|
||||
}
|
||||
}
|
||||
this.dust.geometry.attributes.position.needsUpdate = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
import { degToRad } from '../utils.js';
|
||||
|
||||
const FLIES_COUNT = 2;
|
||||
|
||||
// --- Configuration ---
|
||||
const FLIGHT_HEIGHT_MIN = 0.5; // Min height for flying
|
||||
const FLIGHT_HEIGHT_MAX = 2;//state.roomHeight * 0.9; // Max height for flying
|
||||
const FLY_FLIGHT_SPEED_FACTOR = 0.01; // How quickly 't' increases per frame
|
||||
const FLY_WAIT_BASE = 1000;
|
||||
const FLY_LAND_CHANCE = 0.3;
|
||||
|
||||
export class FliesEffect {
|
||||
constructor(scene) {
|
||||
this.flies = [];
|
||||
this._setupFlies(scene);
|
||||
}
|
||||
|
||||
_randomFlyTarget() {
|
||||
return new THREE.Vector3(
|
||||
(Math.random() - 0.5) * (state.roomSize - 1),
|
||||
FLIGHT_HEIGHT_MIN + Math.random() * (FLIGHT_HEIGHT_MAX - FLIGHT_HEIGHT_MIN),
|
||||
(Math.random() - 0.5) * (state.roomSize - 1)
|
||||
);
|
||||
}
|
||||
|
||||
_createFlyMesh() {
|
||||
const flyGroup = new THREE.Group();
|
||||
const flyMaterial = new THREE.MeshPhongMaterial({ color: 0x111111, shininess: 50 });
|
||||
const bodyGeometry = new THREE.ConeGeometry(0.01, 0.02, 3);
|
||||
const body = new THREE.Mesh(bodyGeometry, flyMaterial);
|
||||
body.rotation.x = degToRad(90);
|
||||
body.castShadow = true;
|
||||
body.receiveShadow = true;
|
||||
flyGroup.add(body);
|
||||
|
||||
flyGroup.userData = {
|
||||
state: 'flying',
|
||||
landTimer: 0,
|
||||
t: 0,
|
||||
speed: FLY_FLIGHT_SPEED_FACTOR + Math.random() * 0.01,
|
||||
curve: null,
|
||||
landCheckTimer: 0,
|
||||
oscillationTime: Math.random() * 100,
|
||||
};
|
||||
|
||||
flyGroup.position.copy(this._randomFlyTarget());
|
||||
return flyGroup;
|
||||
}
|
||||
|
||||
_createFlyCurve(fly, endPoint) {
|
||||
const startPoint = fly.position.clone();
|
||||
const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5);
|
||||
const offsetMagnitude = startPoint.distanceTo(endPoint) * 0.5;
|
||||
const offsetAngle = Math.random() * Math.PI * 2;
|
||||
|
||||
const controlPoint = new THREE.Vector3(
|
||||
midPoint.x + Math.cos(offsetAngle) * offsetMagnitude * 0.5,
|
||||
midPoint.y + Math.random() * 0.5 + 0.5,
|
||||
midPoint.z + Math.sin(offsetAngle) * offsetMagnitude * 0.5
|
||||
);
|
||||
|
||||
fly.userData.curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint);
|
||||
fly.userData.t = 0;
|
||||
fly.userData.landCheckTimer = 50 + Math.random() * 50;
|
||||
}
|
||||
|
||||
_setupFlies(scene) {
|
||||
for (let i = 0; i < FLIES_COUNT; i++) {
|
||||
const fly = this._createFlyMesh();
|
||||
scene.add(fly);
|
||||
this.flies.push(fly);
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.flies.forEach(fly => {
|
||||
const data = fly.userData;
|
||||
|
||||
if (data.state === 'flying' || data.state === 'landing') {
|
||||
if (!data.curve) {
|
||||
const newTargetPos = this._randomFlyTarget();
|
||||
this._createFlyCurve(fly, newTargetPos);
|
||||
data.t = 0;
|
||||
}
|
||||
|
||||
data.t += data.speed;
|
||||
data.landCheckTimer--;
|
||||
|
||||
if (data.t >= 1) {
|
||||
if (data.state === 'landing') {
|
||||
data.state = 'landed';
|
||||
data.landTimer = FLY_WAIT_BASE + Math.random() * 1000;
|
||||
data.t = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.landCheckTimer <= 0 && Math.random() > FLY_LAND_CHANCE) {
|
||||
state.raycaster.set(fly.position, new THREE.Vector3(0, -1, 0));
|
||||
const intersects = state.raycaster.intersectObjects(state.landingSurfaces, false);
|
||||
|
||||
if (intersects.length > 0) {
|
||||
const intersect = intersects[0];
|
||||
data.state = 'landing';
|
||||
let newTargetPos = new THREE.Vector3(
|
||||
intersect.point.x,
|
||||
intersect.point.y + 0.05,
|
||||
intersect.point.z
|
||||
);
|
||||
this._createFlyCurve(fly, newTargetPos);
|
||||
data.t = 0;
|
||||
}
|
||||
}
|
||||
|
||||
if (data.state !== 'landing') {
|
||||
const newTargetPos = this._randomFlyTarget();
|
||||
this._createFlyCurve(fly, newTargetPos);
|
||||
data.t = 0;
|
||||
}
|
||||
}
|
||||
|
||||
fly.position.copy(data.curve.getPoint(Math.min(data.t, 1)));
|
||||
const tangent = data.curve.getTangent(Math.min(data.t, 1)).normalize();
|
||||
fly.rotation.y = Math.atan2(tangent.x, tangent.z);
|
||||
data.oscillationTime += 0.1;
|
||||
fly.position.y += Math.sin(data.oscillationTime * 4) * 0.01;
|
||||
|
||||
} else if (data.state === 'landed') {
|
||||
data.landTimer--;
|
||||
if (data.landTimer <= 0) {
|
||||
data.state = 'flying';
|
||||
const newTargetPos = this._randomFlyTarget();
|
||||
this._createFlyCurve(fly, newTargetPos);
|
||||
data.t = 0;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,131 @@
|
||||
import * as THREE from 'three';
|
||||
import { state } from '../state.js';
|
||||
|
||||
const SPIDER_COUNT = 5;
|
||||
const SPIDER_SPEED = 0.0001;
|
||||
const SPIDER_TURN_SPEED = 0.02;
|
||||
const SPIDER_WAIT_MIN = 200; // frames
|
||||
const SPIDER_WAIT_MAX = 500; // frames
|
||||
|
||||
export class SpiderEffect {
|
||||
constructor(scene) {
|
||||
this.spiders = [];
|
||||
this._setupSpiders(scene);
|
||||
}
|
||||
|
||||
_getRandomPointOnWall(wall) {
|
||||
const position = new THREE.Vector3();
|
||||
const width = wall.geometry.parameters.width;
|
||||
const height = wall.geometry.parameters.height;
|
||||
|
||||
position.x = (Math.random() - 0.5) * width;
|
||||
position.y = (Math.random() - 0.5) * height;
|
||||
position.z = 0; // Local z is 0 for a plane
|
||||
|
||||
// Convert local position to world position
|
||||
return wall.localToWorld(position);
|
||||
}
|
||||
|
||||
_createSpiderMesh() {
|
||||
const spiderGroup = new THREE.Group();
|
||||
const spiderMaterial = new THREE.MeshPhongMaterial({ color: 0x919191, shininess: 50 });
|
||||
|
||||
// Body
|
||||
const bodyGeometry = new THREE.SphereGeometry(0.01, 6, 5);
|
||||
const body = new THREE.Mesh(bodyGeometry, spiderMaterial);
|
||||
body.scale.z = 0.6; // Flatten the sphere
|
||||
body.castShadow = true;
|
||||
spiderGroup.add(body);
|
||||
|
||||
// Head
|
||||
const headGeometry = new THREE.SphereGeometry(0.005, 5, 4);
|
||||
const head = new THREE.Mesh(headGeometry, spiderMaterial);
|
||||
head.position.y = 0.015;
|
||||
head.castShadow = true;
|
||||
spiderGroup.add(head);
|
||||
|
||||
spiderGroup.userData = {
|
||||
state: 'crawling', // 'crawling', 'waiting'
|
||||
waitTimer: 0,
|
||||
t: 0,
|
||||
curve: null,
|
||||
currentWall: null,
|
||||
};
|
||||
|
||||
return spiderGroup;
|
||||
}
|
||||
|
||||
_findNewTarget(spider) {
|
||||
if (!spider.userData.currentWall) {
|
||||
// First time, pick a random wall
|
||||
const walls = state.crawlSurfaces;
|
||||
if (walls.length === 0) return;
|
||||
spider.userData.currentWall = walls[Math.floor(Math.random() * walls.length)];
|
||||
spider.position.copy(this._getRandomPointOnWall(spider.userData.currentWall));
|
||||
}
|
||||
|
||||
const startPoint = spider.position.clone();
|
||||
const endPoint = this._getRandomPointOnWall(spider.userData.currentWall);
|
||||
|
||||
// Create a curved path on the wall
|
||||
const midPoint = new THREE.Vector3().lerpVectors(startPoint, endPoint, 0.5);
|
||||
const direction = new THREE.Vector3().subVectors(endPoint, startPoint).normalize();
|
||||
const wallNormal = spider.userData.currentWall.getWorldDirection(new THREE.Vector3()).negate();
|
||||
|
||||
// Get a perpendicular vector on the plane of the wall
|
||||
const perpendicular = new THREE.Vector3().crossVectors(direction, wallNormal).normalize();
|
||||
const offsetMagnitude = startPoint.distanceTo(endPoint) * (Math.random() * 0.4 - 0.2); // Random offset left or right
|
||||
|
||||
const controlPoint = midPoint.clone().add(perpendicular.multiplyScalar(offsetMagnitude));
|
||||
|
||||
spider.userData.curve = new THREE.QuadraticBezierCurve3(startPoint, controlPoint, endPoint);
|
||||
spider.userData.t = 0;
|
||||
spider.userData.state = 'crawling';
|
||||
}
|
||||
|
||||
_setupSpiders(scene) {
|
||||
for (let i = 0; i < SPIDER_COUNT; i++) {
|
||||
const spider = this._createSpiderMesh();
|
||||
scene.add(spider);
|
||||
this.spiders.push(spider);
|
||||
this._findNewTarget(spider); // Initial placement
|
||||
}
|
||||
}
|
||||
|
||||
update() {
|
||||
this.spiders.forEach(spider => {
|
||||
const data = spider.userData;
|
||||
|
||||
if (data.state === 'crawling') {
|
||||
if (!data.curve) {
|
||||
this._findNewTarget(spider);
|
||||
return;
|
||||
}
|
||||
|
||||
data.t += SPIDER_SPEED;
|
||||
if (data.t >= 1) {
|
||||
spider.position.copy(data.curve.v2);
|
||||
data.state = 'waiting';
|
||||
data.waitTimer = SPIDER_WAIT_MIN + Math.random() * (SPIDER_WAIT_MAX - SPIDER_WAIT_MIN);
|
||||
} else {
|
||||
spider.position.copy(data.curve.getPoint(data.t));
|
||||
|
||||
// Smoothly turn the spider towards the tangent of the curve
|
||||
const tangent = data.curve.getTangent(data.t);
|
||||
const up = data.currentWall.getWorldDirection(new THREE.Vector3()).negate();
|
||||
|
||||
const targetQuaternion = new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 1, 0), tangent).multiply(
|
||||
new THREE.Quaternion().setFromUnitVectors(new THREE.Vector3(0, 0, 1), up)
|
||||
);
|
||||
|
||||
spider.quaternion.slerp(targetQuaternion, SPIDER_TURN_SPEED);
|
||||
}
|
||||
} else if (data.state === 'waiting') {
|
||||
data.waitTimer--;
|
||||
if (data.waitTimer <= 0) {
|
||||
this._findNewTarget(spider);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user