import * as THREE from 'three' import { FontLoader } from 'three/examples/jsm/loaders/FontLoader.js' import { TextGeometry } from 'three/examples/jsm/geometries/TextGeometry.js' import { RGBELoader } from 'three/examples/jsm/loaders/RGBELoader.js' // === Scene & Camera === const scene = new THREE.Scene() const camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 100) camera.position.set(0.1, 0.02, 4) //camera.position.z = 4 // === Renderer (transparent) === const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: document.querySelector('.intro3d') }) renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) renderer.setSize(window.innerWidth, window.innerHeight) renderer.setClearColor(0x000000, 0) renderer.outputColorSpace = THREE.SRGBColorSpace const rgbeLoader = new RGBELoader() rgbeLoader.load('/app/assets/images/satara_night_no_lamps_2k.hdr', hdrTexture => { hdrTexture.mapping = THREE.EquirectangularReflectionMapping scene.environment = hdrTexture scene.background = hdrTexture }) // === Load background as a plane that fits the viewport === const loader = new THREE.TextureLoader() let plane let planeDistance = 4.4 loader.load('/app/assets/images/logop42-intro_three.png', texture => { texture.colorSpace = THREE.SRGBColorSpace texture.wrapS = THREE.ClampToEdgeWrapping texture.wrapT = THREE.ClampToEdgeWrapping texture.minFilter = THREE.LinearFilter texture.magFilter = THREE.LinearFilter const imageAspect = texture.image.width / texture.image.height // Create plane sized to exactly fill the camera frustum at Z = -planeDistance function makeOrResizePlane() { const fovRad = THREE.MathUtils.degToRad(camera.fov) const viewHeight = 2 * Math.tan(fovRad / 2) * planeDistance const viewWidth = viewHeight * camera.aspect let planeWidth, planeHeight if (viewWidth / viewHeight > imageAspect) { planeHeight = viewHeight planeWidth = planeHeight * imageAspect } else { planeWidth = viewWidth planeHeight = planeWidth / imageAspect } if (!plane) { const geo = new THREE.PlaneGeometry(planeWidth, planeHeight) const mat = new THREE.MeshBasicMaterial({ map: texture }) plane = new THREE.Mesh(geo, mat) plane.scale.set(1.44, 1.5, 1) plane.position.z = 0 plane.renderOrder = 0 scene.add(plane) } else { plane.geometry.dispose() plane.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) } } makeOrResizePlane() onResizeCallbacks.push(makeOrResizePlane) }) const glassMat = new THREE.MeshPhysicalMaterial({ color: 0xffffff, metalness: .05, roughness: 0.05, //0.02, transmission: 1, //1, // true glass thickness: 1,//0.6, ior: 1.45, //1.5, envMapIntensity: 1.2, attenuationColor: new THREE.Color(0x88ffcc), attenuationDistance: 2, }) // === Lights === const l1 = new THREE.DirectionalLight(0xffbb33, 1) l1.position.set(-1, 1.5, 6) scene.add(l1) const l2 = new THREE.DirectionalLight(0xffbb33, 1) l2.position.set(2, -1.5, 6) scene.add(l2) const lettersPosScale =[ { position: { x: -0.98, y: 0.02, z: -1 }, scale: { x: 1, y: 1.12, z: 1 } }, { position: { x: -0.63, y: -0.04, z: -1.05 }, scale: { x: 1, y: 1.05, z: 1 } }, { position: { x: -0.28, y: -0.05, z: -1.1 }, scale: { x: 0.9, y: 1.15, z: 1, X: 0.8 } }, { position: { x: -0.1, y: -0.04, z: -1.15 }, scale: { x: 1, y: 1, z: 1 } }, { position: { x: 0.12, y: -0.06, z: -1.2 }, scale: { x: 0.65, y: 1, z: 1 } }, { position: { x: 0.355, y: -0.055, z: -1.25 }, scale: { x: 0.5, y: 0.9, z: 1, Y: 0.9 } }, { position: { x: 0.53, y: -0.01, z: -1.3 }, scale: { x: 1, y: 0.94, z: 1 } }, { position: { x: 0.84, y: -0.05, z: -1.35 }, scale: { x: 0.95, y: 1.04, z: 1 } }, { position: { x: 1.17, y: -0.05, z: -1.4 }, scale: { x: 0.7, y: 1.02, z: 1 } }, ] const glassLetters = [] const text = 'Project42' const fontLoader = new FontLoader() fontLoader.load('/app/thirdparty/Three/fonts/helvetiker_regular.typeface.json', font => { const mat = new THREE.MeshPhysicalMaterial({ metalness: 0.2, roughness: 0.04, transmission: 1, thickness: 0.8, ior: 1.45, envMapIntensity: 1.2, attenuationColor: new THREE.Color(0x88ffcc), attenuationDistance: 2, color: 0x88ffcc, emissive: 0x00CCFF, emissiveIntensity: 0.05, }) for (let i = 0; i < text.length; i++) { const char = text[i] const geo = new TextGeometry(char, { font, size: .5, height: 0.15, curveSegments: 32, bevelEnabled: true, bevelThickness: 0.03, bevelSize: 0.03, bevelSegments: 32 }) geo.center() const mesh = new THREE.Mesh(geo, mat) mesh.position.set(lettersPosScale[i].position.x, lettersPosScale[i].position.y, lettersPosScale[i].position.z) mesh.scale.set(lettersPosScale[i].scale.x, lettersPosScale[i].scale.y, lettersPosScale[i].scale.z) scene.add(mesh) glassLetters.push(mesh) } }) // Helper: update envMap (hide cube → capture → show cube) function updateEnvMap() { const wasVisible = cube.visible cube.visible = false cubeCam.update(renderer, scene) cube.visible = wasVisible glassMat.envMap = cubeRT.texture glassMat.needsUpdate = true } let t = 0 let lettersZ=-1 function animate() { requestAnimationFrame(animate) lettersZ += .005 for(const letter of glassLetters){ letter.position.z += 0.005 //= lettersZ } if(lettersZ>1.5) { for(const [i, letter] of glassLetters.entries()){ letter.rotation.y += (i-4)/2000 letter.position.x += (i-4)/4000 } } if(lettersZ>4.5) { lettersZ = -1 for(const [i, letter] of glassLetters.entries()){ letter.rotation.y = 0 letter.position.x = lettersPosScale[i].position.x letter.position.z = lettersPosScale[i].position.z } } // refresh envmap if anything moves around glass to update reflections //if ((t++ % 2) === 0) updateEnvMap() renderer.render(scene, camera) } animate() // === Resize handling === const onResizeCallbacks = [] window.addEventListener('resize', () => { camera.aspect = window.innerWidth / window.innerHeight camera.updateProjectionMatrix() renderer.setSize(window.innerWidth, window.innerHeight) onResizeCallbacks.forEach(fn => fn()) })