feat: add fear

This commit is contained in:
2026-05-31 20:26:03 -03:00
parent 7a1c28cd19
commit 5de127449a
9 changed files with 340 additions and 1 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 799 KiB

Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.6 MiB

+4
View File
@@ -0,0 +1,4 @@
.canvas {
width: 100vw !important;
height: 100vh !important;
}
+291
View File
@@ -0,0 +1,291 @@
'use client';
import './page.css';
import { PointerLockControls, useTexture } from "@react-three/drei";
import { Canvas, useFrame, useThree } from "@react-three/fiber";
import { BrightnessContrast, EffectComposer, Noise, Pixelation, Vignette } from "@react-three/postprocessing";
import { Suspense, useEffect, useRef } from "react";
import * as THREE from "three";
import { AmbientSound } from './scene-components/ambient-sound';
import { MeshStandardNodeMaterial } from 'three/webgpu';
const HALLWAY_LENGTH = 40;
const HALLWAY_WIDTH = 6;
const HALLWAY_HEIGHT = 5;
const PLAYER_HEIGHT = 3;
function Player() {
const { camera } = useThree();
const controls = usePlayerControls();
const flashlightRef = useRef<THREE.SpotLight>(null);
const movementCounter = useRef<number>(0);
const frontVector = new THREE.Vector3();
const sideVector = new THREE.Vector3();
const direction = new THREE.Vector3();
const viewDirection = new THREE.Vector3();
useFrame((state, delta) => {
frontVector.set(0, 0, Number(controls.Backward) - Number(controls.Forward));
sideVector.set(Number(controls.Left) - Number(controls.Right), 0, 0);
direction
.subVectors(frontVector, sideVector)
.normalize()
.multiplyScalar(4 * delta)
.applyEuler(camera.rotation);
camera.position.x += direction.x;
camera.position.z += direction.z;
const isMoving = controls.Forward || controls.Backward || controls.Left || controls.Right;
if (isMoving) {
movementCounter.current += delta * 10;
camera.position.y = PLAYER_HEIGHT + Math.sin(movementCounter.current) * 0.08;
camera.position.x += Math.cos(movementCounter.current / 2) * 0.006;
} else
camera.position.y = THREE.MathUtils.lerp(camera.position.y, PLAYER_HEIGHT, 5 * delta);
if (flashlightRef.current) {
flashlightRef.current.position.copy(camera.position);
camera.getWorldDirection(viewDirection);
flashlightRef.current.target.position
.copy(camera.position)
.addScaledVector(viewDirection, 10);
flashlightRef.current.target.updateMatrixWorld();
flashlightRef.current.intensity = 5 + Math.sin(state.clock.elapsedTime * 30) * 0.3;
}
const wallBuffer = 0.6;
const HALLWAY_WIDTH = 6;
const HALLWAY_LENGTH = 40;
if (camera.position.x < -HALLWAY_WIDTH / 2 + wallBuffer) camera.position.x = -HALLWAY_WIDTH / 2 + wallBuffer;
if (camera.position.x > HALLWAY_WIDTH / 2 - wallBuffer) camera.position.x = HALLWAY_WIDTH / 2 - wallBuffer;
if (camera.position.z < -HALLWAY_LENGTH) camera.position.z += HALLWAY_LENGTH;
if (camera.position.z > 0) camera.position.z -= HALLWAY_LENGTH;
});
return (
<>
<PointerLockControls />
<spotLight
ref={flashlightRef}
distance={22}
angle={0.35}
penumbra={0.7}
intensity={5}
color="#fffaed"
/>
</>
);
}
function usePlayerControls() {
const keys = useRef({ Forward: false, Backward: false, Left: false, Right: false });
useEffect(() => {
const handleKeyDown = (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = true;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = true;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = true;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = true;
};
const handleKeyUp = (e) => {
if (e.code === 'KeyW' || e.code === 'ArrowUp') keys.current.Forward = false;
if (e.code === 'KeyS' || e.code === 'ArrowDown') keys.current.Backward = false;
if (e.code === 'KeyA' || e.code === 'ArrowLeft') keys.current.Left = false;
if (e.code === 'KeyD' || e.code === 'ArrowRight') keys.current.Right = false;
};
window.addEventListener('keydown', handleKeyDown);
window.addEventListener('keyup', handleKeyUp);
return () => {
window.removeEventListener('keydown', handleKeyDown);
window.removeEventListener('keyup', handleKeyUp);
};
}, []);
return keys.current;
}
interface DoorProps {
position: [number, number, number];
rotation: [number, number, number];
}
function Door({ position, rotation }: DoorProps) {
return (
<group position={position} rotation={rotation}>
<mesh position={[0, 2, -0.14]}>
<boxGeometry args={[2.4, 4.0, 0.2]} />
<meshStandardMaterial color="#8a8585" roughness={0.8} metalness={0.2} />
</mesh>
<mesh position={[0, 1.95, -0.08]}>
<boxGeometry args={[2.1, 3.8, 0.1]} />
<meshStandardMaterial color="#4e4b4b" roughness={0.7} metalness={0.2} />
</mesh>
<mesh position={[0.9, 1.8, 0.08]}>
<boxGeometry args={[0.08, 0.08, 0.15]} />
<meshStandardMaterial color="#4e4b4b" roughness={0.4} metalness={0.2} />
</mesh>
</group>
);
}
function Hallway() {
const [floorTex, wallTex] = useTexture([
'fear/img/concrete-floor.jpg',
'fear/img/concrete-wall.jpg'
]);
floorTex.wrapS = floorTex.wrapT = THREE.RepeatWrapping;
floorTex.repeat.set(1, 10);
floorTex.minFilter = THREE.NearestFilter;
floorTex.magFilter = THREE.NearestFilter;
wallTex.wrapS = wallTex.wrapT = THREE.RepeatWrapping;
wallTex.repeat.set(10, 1);
wallTex.minFilter = THREE.NearestFilter;
wallTex.magFilter = THREE.NearestFilter;
const segments = [0, -1, 1, -2, 2];
const lightRef1 = useRef<THREE.PointLight>(null);
const lightRef2 = useRef<THREE.PointLight>(null);
useFrame((state) => {
const time = state.clock.elapsedTime;
const flicker = Math.sin(time * 10) * Math.cos(time * 3) > 0.4 ? 0 : 0.8;
if (lightRef1.current) lightRef1.current.intensity = flicker;
if (lightRef2.current) lightRef2.current.intensity = flicker * 0.5;
});
return (
<>
{/* <ambientLight intensity={1} /> */}
<ambientLight intensity={0.02} />
{segments.map((i) => (
<group key={i} position={[0, 0, i * HALLWAY_LENGTH]}>
<pointLight ref={i === 0 ? lightRef1 : null} position={[0, HALLWAY_HEIGHT - 0.5, -HALLWAY_LENGTH / 4]} intensity={0.9} distance={15} color="#a8a1a1" />
<pointLight ref={i === 0 ? lightRef2 : null} position={[0, HALLWAY_HEIGHT - 0.5, -HALLWAY_LENGTH * 0.75]} intensity={0.9} distance={15} color="#a8a1a1" />
{/* floor */}
<mesh rotation={[-Math.PI / 2, 0, 0]} position={[0, 0, -HALLWAY_LENGTH / 2]}>
<planeGeometry args={[HALLWAY_WIDTH, HALLWAY_LENGTH]} />
<meshStandardMaterial map={floorTex} roughness={0.8} metalness={0.2} />
</mesh>
{/* roof */}
<mesh rotation={[Math.PI / 2, 0, 0]} position={[0, HALLWAY_HEIGHT, -HALLWAY_LENGTH / 2]}>
<planeGeometry args={[HALLWAY_WIDTH, HALLWAY_LENGTH]} />
<meshStandardMaterial map={floorTex} roughness={0.8} metalness={0.2} />
</mesh>
{/* l wall */}
<mesh rotation={[0, Math.PI / 2, 0]} position={[-HALLWAY_WIDTH / 2, HALLWAY_HEIGHT / 2, -HALLWAY_LENGTH / 2]}>
<planeGeometry args={[HALLWAY_LENGTH, HALLWAY_HEIGHT]} />
<meshStandardMaterial map={wallTex} roughness={0.7} metalness={0.1} />
</mesh>
{/* r wall */}
<mesh rotation={[0, -Math.PI / 2, 0]} position={[HALLWAY_WIDTH / 2, HALLWAY_HEIGHT / 2, -HALLWAY_LENGTH / 2]}>
<planeGeometry args={[HALLWAY_LENGTH, HALLWAY_HEIGHT]} />
<meshStandardMaterial map={wallTex} roughness={0.7} metalness={0.1} />
</mesh>
{/* doors */}
<Door
position={[-HALLWAY_WIDTH / 2 + 0.05, 0, -HALLWAY_LENGTH * 0.25]}
rotation={[0, Math.PI / 2, 0]}
/>
<Door
position={[HALLWAY_WIDTH / 2 - 0.05, 0, -HALLWAY_LENGTH * 0.65]}
rotation={[0, -Math.PI / 2, 0]}
/>
<Door
position={[-HALLWAY_WIDTH / 2 + 0.05, 0, -HALLWAY_LENGTH * 0.85]}
rotation={[0, Math.PI / 2, 0]}
/>
{/* pipes */}
{Array.from({ length: 3 }).map((_, idx) => (
<mesh
key={idx}
rotation={[Math.PI / 2, 0, 0]}
position={[-HALLWAY_WIDTH / 2 + 0.4 + (idx * 0.20), HALLWAY_HEIGHT - 0.2, -HALLWAY_LENGTH / 2]}
>
<cylinderGeometry args={[0.06, 0.06, HALLWAY_LENGTH, 4]} />
<meshStandardMaterial color="#a5aca8" roughness={0.0} metalness={0.4} />
</mesh>
))}
{Array.from({ length: 5 }).map((_, idx) => {
const zOffset = -(idx * 8 + 4);
return (
<mesh
key={`bracket-${idx}`}
position={[-HALLWAY_WIDTH / 2 + 0.6, HALLWAY_HEIGHT - 0.15, zOffset]}
>
<boxGeometry args={[0.7, 0.3, 0.15]} />
<meshStandardMaterial color="#b3adad" roughness={0.8} metalness={0.2} />
</mesh>
);
})}
</group>
))}
</>
);
}
function PostProcessing() {
return (<EffectComposer>
<Pixelation granularity={10} />
<Vignette />
<Noise opacity={0.005} />
<BrightnessContrast
brightness={-0.01}
contrast={0.05}
/>
</EffectComposer>)
}
export default function Fear() {
return (<>
<Canvas
shadows
gl={{ antialias: true }}
className='canvas'
camera={{ position: [0, 3, -5], fov: 65, far: 100 }}
>
<color attach="background" args={['#050505']} />
<fogExp2 attach='fog' args={[0x050505, 0.035]} />
<PostProcessing />
<Suspense fallback={null}>
<Hallway />
</Suspense>
<AmbientSound url='fear/snd/ambience.mp3' />
<Player />
</Canvas>
</>)
}
@@ -0,0 +1,43 @@
import { useEffect, useRef } from 'react'
interface AmbientSoundProps {
url: string
volume?: number
}
export function AmbientSound({ url, volume = 0.5 }: AmbientSoundProps) {
const audioRef = useRef<HTMLAudioElement | null>(null)
useEffect(() => {
const audio = new Audio(url)
audio.loop = true
audio.volume = volume
audioRef.current = audio
const startAudio = () => {
audio.play().catch((err) => {
console.warn('Autoplay blocked. Waiting for user interaction.', err)
})
}
startAudio()
window.addEventListener('click', startAudio, { once: true })
window.addEventListener('keydown', startAudio, { once: true })
return () => {
window.removeEventListener('click', startAudio)
window.removeEventListener('keydown', startAudio)
audio.pause()
audioRef.current = null
}
}, [url])
useEffect(() => {
if (audioRef.current) {
audioRef.current.volume = volume
}
}, [volume])
return null
}
@@ -8,6 +8,7 @@ float windSpeed = 1.5;
float windTime = uTime * windSpeed;
vec2 windSamplePos = (worldPos.xz * 0.05) - (mainWindDir * windTime * 0.2);
float windBase = fbm(windSamplePos * 0.8) * 0.4 + 0.2;
float gustNoise = fbm(windSamplePos * 0.4);
float gust = pow(gustNoise, 3.0) * 1.8;
+1 -1
View File
@@ -29,7 +29,7 @@ function Content() {
<>
<div className="main-frame">
<a href="/niko" className="decorative-sparkle" title="✧" style={{ left: '10px' }}></a>
<a href="/img/boom.gif" className="decorative-sparkle" title="✧" style={{ right: '10px' }}></a>
<a href="/fear" className="decorative-sparkle" title="✧" style={{ right: '10px' }}></a>
<header>
<h1>neru</h1>