diff --git a/3js/audio/stalker.mp3 b/3js/audio/stalker.mp3 new file mode 100644 index 0000000..ab3434f Binary files /dev/null and b/3js/audio/stalker.mp3 differ diff --git a/3js/background.js b/3js/background.js new file mode 100644 index 0000000..7546c2a --- /dev/null +++ b/3js/background.js @@ -0,0 +1,552 @@ +import * as THREE from "three"; +import { FBXLoader } from "three/addons/loaders/FBXLoader.js"; +import { TGALoader } from "three/addons/loaders/TGALoader.js"; + +(() => { + if (window.__threePS1Campfire) return; + window.__threePS1Campfire = true; + + const MODEL_URL = "https://seppjm.com/3js/models/scene.fbx"; + const NORMALIZE_SCALE = false; + const USE_FOG = true; + + const PS1_SCALE = 3; + const COLOR_LEVELS = 32; + const USE_DITHER = true; + const VERTEX_SNAP_PIXELS = 0.75; + + const FIRE_POS = new THREE.Vector3( + 2885.090654499771, + 5.937671631541306, + -2843.489246932181 + ); + const CAM_POS = new THREE.Vector3( + 3280.98691276581, + 386.84586301208896, + -2012.4527013816644 + ); + const CAM_ROT_DEG = { yaw: 395.51276597880525, pitch: 2.7272791048226543 }; + const CAM_FOV = 90; + + const FIRE_SPRITE_SIZE = 150.0; + const FIRE_HEIGHT_OFFSET = 0.25; + const FIRE_MAIN_INTENSITY = 2.2; + const FIRE_SUB_INTENSITY = 1.0; + const FIRE_MAIN_DISTANCE = 12000; + const FIRE_SUB_DISTANCE = 6000; + const FIRE_DECAY = 1.2; + + const footer = document.querySelector("footer"); + const canvas = document.createElement("canvas"); + canvas.id = "three-bg"; + Object.assign(canvas.style, { + position: "fixed", + inset: "0", + width: "100vw", + height: "100vh", + display: "block", + zIndex: "0", + pointerEvents: "none", + clipPath: "inset(0 0 var(--bg-clip-bottom, 0px) 0)", + WebkitClipPath: "inset(0 0 var(--bg-clip-bottom, 0px) 0)", + opacity: "0", + transition: "opacity 1400ms ease", + imageRendering: "pixelated", + }); + document.body.prepend(canvas); + + function updateFooterClip() { + const fadeLen = 140; + const rect = footer?.getBoundingClientRect(); + if (!rect) { + canvas.style.setProperty("--bg-clip-bottom", "0px"); + canvas.style.maskImage = "none"; + canvas.style.WebkitMaskImage = "none"; + return; + } + const overlap = Math.max(0, innerHeight - rect.top); + const clip = Math.max(0, overlap - fadeLen); + canvas.style.setProperty("--bg-clip-bottom", `${clip}px`); + if (overlap > 0) { + const grad = `linear-gradient(to bottom, black 0%, black calc(100% - ${fadeLen}px), transparent 100%)`; + canvas.style.maskImage = grad; + canvas.style.WebkitMaskImage = grad; + } else { + canvas.style.maskImage = "none"; + canvas.style.WebkitMaskImage = "none"; + } + } + addEventListener("scroll", updateFooterClip, { passive: true }); + addEventListener("resize", updateFooterClip); + new ResizeObserver(updateFooterClip).observe(document.body); + if (footer) new ResizeObserver(updateFooterClip).observe(footer); + updateFooterClip(); + + const renderer = new THREE.WebGLRenderer({ + canvas, + antialias: false, + alpha: true, + powerPreference: "high-performance", + }); + renderer.outputColorSpace = THREE.SRGBColorSpace; + renderer.toneMapping = THREE.NoToneMapping; + renderer.shadowMap.enabled = false; + renderer.setPixelRatio(1); + renderer.setClearColor(0x000000, 0); + + let lowW = 0, + lowH = 0, + rt = null; + + const postScene = new THREE.Scene(); + const postCam = new THREE.OrthographicCamera(-1, 1, 1, -1, 0, 1); + const postMat = new THREE.ShaderMaterial({ + uniforms: { + tDiffuse: { value: null }, + uLowRes: { value: new THREE.Vector2(1, 1) }, + uLevels: { value: COLOR_LEVELS }, + uDither: { value: USE_DITHER ? 1 : 0 }, + uTime: { value: 0.0 }, + }, + vertexShader: ` + varying vec2 vUv; + void main() { + vUv = (position.xy + 1.0) * 0.5; + gl_Position = vec4(position.xy, 0.0, 1.0); + } + `, + fragmentShader: ` + precision mediump float; + uniform sampler2D tDiffuse; + uniform vec2 uLowRes; + uniform int uLevels; + uniform int uDither; + uniform float uTime; + varying vec2 vUv; + + float hash(vec2 p) { + p = fract(p * vec2(123.34, 345.45)); + p += dot(p, p + 34.345); + return fract(p.x * p.y); + } + vec3 quantize(vec3 c, vec2 pix) { + float L = float(uLevels); + float t = (uDither == 1) ? (hash(pix + uTime) - 0.5) : 0.0; + vec3 q = floor(clamp(c + t / L, 0.0, 1.0) * (L - 1.0) + 0.5) / (L - 1.0); + return q; + } + void main() { + vec2 p = vUv * uLowRes; + vec2 uv = (floor(p) + 0.5) / uLowRes; + vec3 col = texture2D(tDiffuse, uv).rgb; + col = quantize(col, p); + gl_FragColor = vec4(col, 1.0); + } + `, + depthTest: false, + depthWrite: false, + }); + postScene.add(new THREE.Mesh(new THREE.PlaneGeometry(2, 2), postMat)); + + function resizeRenderer() { + const w = innerWidth, + h = innerHeight; + lowW = Math.max(1, Math.floor(w / PS1_SCALE)); + lowH = Math.max(1, Math.floor(h / PS1_SCALE)); + renderer.setSize(w, h, false); + if (rt) rt.dispose(); + rt = new THREE.WebGLRenderTarget(lowW, lowH, { + minFilter: THREE.NearestFilter, + magFilter: THREE.NearestFilter, + depthBuffer: true, + stencilBuffer: false, + type: THREE.UnsignedByteType, + samples: 0, + }); + postMat.uniforms.uLowRes.value.set(lowW, lowH); + } + resizeRenderer(); + + const scene = new THREE.Scene(); + scene.environment = null; + scene.fog = USE_FOG ? new THREE.FogExp2(0x0b1020, 0.02) : null; + + const camera = new THREE.PerspectiveCamera( + CAM_FOV, + innerWidth / innerHeight, + 0.005, + 5000 + ); + camera.position.copy(CAM_POS); + + const BASE_YAW = THREE.MathUtils.degToRad( + ((CAM_ROT_DEG.yaw % 360) + 360) % 360 + ); + const BASE_PITCH = THREE.MathUtils.degToRad(CAM_ROT_DEG.pitch); + camera.rotation.set(BASE_PITCH, BASE_YAW, 0, "YXZ"); + camera.updateProjectionMatrix(); + + const MAX_YAW_DELTA = THREE.MathUtils.degToRad(6); + const MAX_PITCH_DELTA = THREE.MathUtils.degToRad(4); + let aimYaw = BASE_YAW, + aimPitch = BASE_PITCH; + let curYaw = BASE_YAW, + curPitch = BASE_PITCH; + + addEventListener( + "pointermove", + (e) => { + const nx = (e.clientX / innerWidth) * 2 - 1; + const ny = (e.clientY / innerHeight) * 2 - 1; + aimYaw = BASE_YAW - nx * MAX_YAW_DELTA; + aimPitch = BASE_PITCH + ny * MAX_PITCH_DELTA; + }, + { passive: true } + ); + + function updateCameraHover(dt) { + const smooth = 1.0 - Math.pow(0.2, dt * 60); + curYaw += (aimYaw - curYaw) * smooth; + curPitch += (aimPitch - curPitch) * smooth; + camera.rotation.set(curPitch, curYaw, 0, "YXZ"); + } + + scene.add(new THREE.HemisphereLight(0xb0c8ff, 0x0b1020, 0.28)); + const sun = new THREE.DirectionalLight(0xffffff, 0.6); + sun.position.set(3, 5, 4); + scene.add(sun); + + const basePath = MODEL_URL.slice(0, MODEL_URL.lastIndexOf("/") + 1) || "/"; + const manager = new THREE.LoadingManager(); + manager.addHandler(/\.tga$/i, new TGALoader(manager)); + const loader = new FBXLoader(manager); + loader.setResourcePath(basePath); + + let model, + mixer, + sceneRadius = 100; + const patchedMaterials = new Set(); + const perFrame = []; + + function ps1ifyMaterials(root) { + root.traverse((n) => { + if (!n.isMesh) return; + const src = n.material; + const mats = (Array.isArray(src) ? src : [src]).filter(Boolean); + + const newMats = mats.map((m) => { + const p = { + color: + m.color && m.color.isColor + ? m.color.clone() + : new THREE.Color(0xffffff), + map: m.map || null, + transparent: false, + alphaTest: + (m.alphaTest ?? 0.0) > 0 + ? m.alphaTest + : m.map?.format === THREE.RGBAFormat + ? 0.5 + : 0.0, + side: THREE.DoubleSide, + }; + const lm = new THREE.MeshLambertMaterial(p); + + ["map", "emissiveMap", "aoMap", "specularMap"].forEach((k) => { + const tex = lm[k]; + if (tex) { + tex.generateMipmaps = false; + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + tex.anisotropy = 0; + tex.needsUpdate = true; + } + }); + + lm.depthWrite = true; + lm.depthTest = true; + lm.dithering = false; + lm.polygonOffset = true; + lm.polygonOffsetFactor = -0.5; + lm.polygonOffsetUnits = 1.0; + + lm.onBeforeCompile = (shader) => { + shader.uniforms.uResolution = { + value: new THREE.Vector2(innerWidth, innerHeight), + }; + shader.uniforms.uSnapPixels = { value: VERTEX_SNAP_PIXELS }; + shader.vertexShader = shader.vertexShader.replace( + /void\s+main\s*\(\)\s*\{/, + "uniform vec2 uResolution;\nuniform float uSnapPixels;\nvoid main(){" + ); + if (VERTEX_SNAP_PIXELS > 0) { + shader.vertexShader = shader.vertexShader.replace( + "#include ", + ` + #include + vec2 ndc = gl_Position.xy / gl_Position.w; + vec2 pix = (ndc * 0.5 + 0.5) * uResolution; + pix = floor(pix / uSnapPixels) * uSnapPixels; + vec2 ndc2 = (pix / uResolution) * 2.0 - 1.0; + gl_Position.xy = ndc2 * gl_Position.w; + ` + ); + } + lm.userData._ps1Shader = shader; + patchedMaterials.add(lm); + }; + + return lm; + }); + + n.material = Array.isArray(src) ? newMats : newMats[0]; + }); + } + + function autoFogFor(target) { + if (!USE_FOG) return; + const box = new THREE.Box3().setFromObject(target); + const size = box.getSize(new THREE.Vector3()); + sceneRadius = Math.max(size.x, size.y, size.z) * 0.5 || 100; + const k = 0.03; + const density = Math.min(0.06, Math.max(0.000005, k / (sceneRadius || 1))); + scene.fog = new THREE.FogExp2(0x0b1020, density); + } + + loader.load( + MODEL_URL, + (fbx) => { + model = fbx; + if (NORMALIZE_SCALE) { + const box = new THREE.Box3().setFromObject(model); + const size = box.getSize(new THREE.Vector3()); + const maxDim = Math.max(size.x, size.y, size.z) || 1; + const scale = 20.0 / maxDim; + model.scale.setScalar(scale); + const center = box.getCenter(new THREE.Vector3()).multiplyScalar(scale); + model.position.sub(center); + } + ps1ifyMaterials(model); + scene.add(model); + + if (fbx.animations?.length) { + mixer = new THREE.AnimationMixer(model); + mixer.clipAction(fbx.animations[0]).play(); + } + + autoFogFor(model); + addCampfire(FIRE_POS); + }, + undefined, + () => { + addCampfire(FIRE_POS); + } + ); + + let fireGroup = null, + fireLight = null, + fireLight2 = null, + fireSprite = null; + + function makeFlameTexture(size = 128) { + const c = document.createElement("canvas"); + c.width = c.height = size; + const g = c.getContext("2d"); + const grd = g.createRadialGradient( + size * 0.5, + size * 0.6, + size * 0.05, + size * 0.5, + size * 0.6, + size * 0.5 + ); + grd.addColorStop(0.0, "rgba(255,255,255,1)"); + grd.addColorStop(0.25, "rgba(255,200,80,0.95)"); + grd.addColorStop(0.55, "rgba(255,120,30,0.65)"); + grd.addColorStop(0.85, "rgba(200,40,10,0.25)"); + grd.addColorStop(1.0, "rgba(0,0,0,0)"); + g.fillStyle = grd; + g.fillRect(0, 0, size, size); + const tex = new THREE.CanvasTexture(c); + tex.generateMipmaps = false; + tex.minFilter = THREE.NearestFilter; + tex.magFilter = THREE.NearestFilter; + tex.anisotropy = 0; + return tex; + } + + function addCampfire(pos) { + fireGroup = new THREE.Group(); + fireGroup.position.copy(pos); + scene.add(fireGroup); + + fireLight = new THREE.PointLight( + 0xff7a2a, + FIRE_MAIN_INTENSITY, + FIRE_MAIN_DISTANCE, + FIRE_DECAY + ); + fireLight.position.set(0, FIRE_HEIGHT_OFFSET + 0.6, 0); + fireGroup.add(fireLight); + + fireLight2 = new THREE.PointLight( + 0xff3300, + FIRE_SUB_INTENSITY, + FIRE_SUB_DISTANCE, + FIRE_DECAY + ); + fireLight2.position.set(0.35, FIRE_HEIGHT_OFFSET + 0.4, -0.2); + fireGroup.add(fireLight2); + + const flameTex = makeFlameTexture(128); + const mat = new THREE.SpriteMaterial({ + map: flameTex, + color: 0xffffff, + transparent: true, + depthWrite: false, + blending: THREE.AdditiveBlending, + depthTest: true, + }); + fireSprite = new THREE.Sprite(mat); + fireSprite.position.set(0, FIRE_HEIGHT_OFFSET + 0.25, 0); + fireSprite.scale.set(FIRE_SPRITE_SIZE, FIRE_SPRITE_SIZE * 1.35, 1); + fireGroup.add(fireSprite); + + const base1 = FIRE_MAIN_INTENSITY, + base2 = FIRE_SUB_INTENSITY; + let t = 0; + perFrame.push((dt) => { + t += dt; + const pulse = 0.45 * Math.sin(t * 5.4) + 0.28 * Math.sin(t * 8.7 + 1.1); + fireLight.intensity = base1 + pulse; + fireLight2.intensity = base2 + pulse * 0.75; + fireLight.color.setHSL(0.06 + Math.sin(t * 1.6) * 0.01, 1.0, 0.55); + fireLight2.color.setHSL( + 0.03 + Math.sin(t * 1.9 + 0.8) * 0.012, + 1.0, + 0.47 + ); + fireSprite.material.rotation += dt * 0.5; + const j = 1.0 + Math.sin(t * 6.0) * 0.06; + fireSprite.scale.set( + FIRE_SPRITE_SIZE * j, + FIRE_SPRITE_SIZE * 1.35 * j, + 1 + ); + }); + } + + function animate() { + if (!active) return; + const dt = clock.getDelta(); + if (mixer) mixer.update(dt); + updateCameraHover(dt); + perFrame.forEach((fn) => fn(dt)); + + renderer.setRenderTarget(rt); + postMat.uniforms.tDiffuse.value = null; + renderer.clear(); + renderer.render(scene, camera); + + renderer.setRenderTarget(null); + postMat.uniforms.tDiffuse.value = rt.texture; + postMat.uniforms.uTime.value += dt; + renderer.render(postScene, postCam); + + requestAnimationFrame(animate); + } + + const clock = new THREE.Clock(); + let active = true; + addEventListener("visibilitychange", () => { + active = document.visibilityState === "visible"; + if (active) requestAnimationFrame(animate); + }); + const fadeIn = () => requestAnimationFrame(() => (canvas.style.opacity = "1")); + requestAnimationFrame(() => { + animate(); + requestAnimationFrame(fadeIn); + }); + + addEventListener("resize", () => { + camera.aspect = innerWidth / innerHeight; + camera.updateProjectionMatrix(); + resizeRenderer(); + updateFooterClip(); + patchedMaterials.forEach((m) => { + const s = m.userData._ps1Shader; + if (s) s.uniforms.uResolution.value.set(innerWidth, innerHeight); + }); + }); + + const audio = new Audio("https://seppjm.com/3js/audio/stalker.mp3"); + audio.loop = true; + audio.preload = "auto"; + audio.crossOrigin = "anonymous"; + audio.volume = 0.5; + + const btn = document.createElement("button"); + btn.type = "button"; + btn.textContent = "Play music"; + btn.setAttribute("aria-pressed", "false"); + btn.ariaLabel = "Toggle background music"; + Object.assign(btn.style, { + position: "fixed", + right: "16px", + bottom: "16px", + zIndex: "3", + padding: "10px 14px", + borderRadius: "9999px", + border: "1px solid rgba(255,255,255,0.25)", + background: "rgba(0,0,0,0.45)", + color: "white", + font: + "500 14px/1.1 system-ui, -apple-system, Segoe UI, Roboto, Inter, sans-serif", + backdropFilter: "blur(6px)", + WebkitBackdropFilter: "blur(6px)", + cursor: "pointer", + userSelect: "none", + }); + document.body.appendChild(btn); + + let isPlaying = false, + fadeRAF = null; + function fadeTo(target, ms = 700) { + cancelAnimationFrame(fadeRAF); + const start = audio.volume, + delta = target - start, + t0 = performance.now(); + const tick = (now) => { + const p = Math.min(1, (now - t0) / ms); + audio.volume = Math.max(0, Math.min(1, start + delta * p)); + if (p < 1) fadeRAF = requestAnimationFrame(tick); + }; + fadeRAF = requestAnimationFrame(tick); + } + async function playAudio() { + try { + await audio.play(); + isPlaying = true; + btn.textContent = "Pause music"; + btn.setAttribute("aria-pressed", "true"); + fadeTo(0.5, 600); + } catch {} + } + function pauseAudio() { + fadeTo(0.0, 400); + setTimeout(() => audio.pause(), 420); + isPlaying = false; + btn.textContent = "Play music"; + btn.setAttribute("aria-pressed", "false"); + } + btn.addEventListener("click", () => + isPlaying ? pauseAudio() : playAudio() + ); + const startOnGesture = () => { + if (!isPlaying) playAudio(); + removeEventListener("pointerdown", startOnGesture); + removeEventListener("keydown", startOnGesture); + }; + addEventListener("pointerdown", startOnGesture, { once: true }); + addEventListener("keydown", startOnGesture, { once: true }); +})(); diff --git a/3js/index.html b/3js/index.html new file mode 100644 index 0000000..20d33fd --- /dev/null +++ b/3js/index.html @@ -0,0 +1,239 @@ + + + + + + + + + + SEPPJM.COM + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+

Welcome

+

Welcome to my webpage, Where can i take you today?

+
+
+ About me + my BRAiN + my CV +

+

---


+

I might add more links & content to this website soon. You can find more links under the "Links" dropdown menu.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

About me

+

Hi i'm Sepp! While I might add more content to my website at some point, writing about more interesting topics is what truly excites me. After all, who's really interested in this page, right?

+

I made this website to save you from a 404 or 403.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + + + + + + + diff --git a/3js/models/scene.fbx b/3js/models/scene.fbx new file mode 100644 index 0000000..99ae89c Binary files /dev/null and b/3js/models/scene.fbx differ diff --git a/index.html b/index.html index a776e93..20d33fd 100644 --- a/index.html +++ b/index.html @@ -220,6 +220,18 @@ + + + + + diff --git a/oldindex.html b/oldindex.html new file mode 100644 index 0000000..a776e93 --- /dev/null +++ b/oldindex.html @@ -0,0 +1,227 @@ + + + + + + + + + + SEPPJM.COM + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+

Welcome

+

Welcome to my webpage, Where can i take you today?

+
+
+ About me + my BRAiN + my CV +

+

---


+

I might add more links & content to this website soon. You can find more links under the "Links" dropdown menu.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+

About me

+

Hi i'm Sepp! While I might add more content to my website at some point, writing about more interesting topics is what truly excites me. After all, who's really interested in this page, right?

+

I made this website to save you from a 404 or 403.

+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ + + + + + + +