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' class Intro{ #lettersZ=-1 #onResizeCallbacks = [] constructor(){ this.canvas = document.querySelector('.intro3d') const loadHDR = () => new Promise((resolve, reject) => { new RGBELoader() .setPath('/app/assets/images/') .load('kloofendal_48d_partly_cloudy_puresky_2k.hdr', resolve, this.hdrProgress, reject) }) const loadTexture = () => new Promise((resolve, reject) => { new THREE.TextureLoader() .setPath('/app/assets/images/') .load('logop42-intro_three.png', resolve, undefined, reject) }) Promise.allSettled([loadHDR(), loadTexture()]).then(this.initScene.bind(this)) this.Xcorrection = -0.02 } hdrProgress(xhr){ const pb = document.querySelector('div.progress') if (xhr.lengthComputable) { const percent = (xhr.loaded / xhr.total) * 100 pb.style.width = percent.toFixed(0) if(percent.toFixed(0)>=99) pb.style.display = 'none' } } initScene(LoadersResults){ let hdrTexture const [hdrResult, texResult] = LoadersResults if (hdrResult.status === 'fulfilled' && texResult.status === 'fulfilled') { hdrTexture = hdrResult.value this.backgroundTexture = texResult.value hdrTexture.mapping = THREE.EquirectangularReflectionMapping this.backgroundTexture.colorSpace = THREE.SRGBColorSpace } else { console.error('Loading error:', hdrResult, texResult) return } this.scene = new THREE.Scene() this.camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 100) this.camera.position.set(0.1, 0.02, 4) this.renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true, canvas: this.canvas }) hdrTexture.mapping = THREE.EquirectangularReflectionMapping this.scene.environment = hdrTexture this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) this.renderer.setSize(window.innerWidth, window.innerHeight) this.renderer.setClearColor(0x000000, 0) this.renderer.outputColorSpace = THREE.SRGBColorSpace this.backgroundTexture.colorSpace = THREE.SRGBColorSpace this.backgroundTexture.wrapS = THREE.ClampToEdgeWrapping this.backgroundTexture.wrapT = THREE.ClampToEdgeWrapping this.backgroundTexture.generateMipmaps = false this.backgroundTexture.minFilter = THREE.LinearFilter this.backgroundTexture.magFilter = THREE.LinearFilter this.imageAspect = this.backgroundTexture.image.width / this.backgroundTexture.image.height this.makeOrResizePlane() this.#onResizeCallbacks.push(this.makeOrResizePlane.bind(this)) // === Lights === const l1 = new THREE.DirectionalLight(0xffbb33, 1) l1.position.set(-1, 1.5, 6) this.scene.add(l1) const l2 = new THREE.DirectionalLight(0xffbb33, 1) l2.position.set(2, -1.5, 6) this.scene.add(l2) this.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 } }, ] this.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.05, roughness: 0.02, transmission: 1, thickness: 0.8, ior: 1.45, envMapIntensity: 1.5, attenuationColor: new THREE.Color(0x88ffcc), attenuationDistance: 2, color: 0x88ffcc, }) 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(this.lettersPosScale[i].position.x + this.Xcorrection, this.lettersPosScale[i].position.y, this.lettersPosScale[i].position.z) mesh.scale.set(this.lettersPosScale[i].scale.x, this.lettersPosScale[i].scale.y, this.lettersPosScale[i].scale.z) this.scene.add(mesh) this.glassLetters.push(mesh) } }) // === Resize handling === window.addEventListener('resize', () => { this.camera.aspect = window.innerWidth / window.innerHeight this.camera.updateProjectionMatrix() this.renderer.setSize(window.innerWidth, window.innerHeight) this.#onResizeCallbacks.forEach(fn => fn()) }) this.canvas.classList.add('ready') this.nextAnim = Date.now()+2000 this.animate() } makeOrResizePlane() { const planeDistance = 4.4 const fovRad = THREE.MathUtils.degToRad(this.camera.fov) const viewHeight = 2 * Math.tan(fovRad / 2) * planeDistance const viewWidth = viewHeight * this.camera.aspect let planeWidth, planeHeight if (viewWidth / viewHeight > this.imageAspect) { planeHeight = viewHeight planeWidth = planeHeight * this.imageAspect } else { planeWidth = viewWidth planeHeight = planeWidth / this.imageAspect } if (!this.plane) { const geo = new THREE.PlaneGeometry(planeWidth, planeHeight) const mat = new THREE.MeshBasicMaterial({ map: this.backgroundTexture }) this.plane = new THREE.Mesh(geo, mat) this.plane.scale.set(1.44, 1.5, 1) this.plane.position.x = this.Xcorrection this.plane.position.z = 0 this.plane.renderOrder = 0 this.scene.add(this.plane) } else { this.plane.geometry.dispose() this.plane.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) } } animate() { requestAnimationFrame(this.animate.bind(this)) if(Date.now()>this.nextAnim){ this.#lettersZ += .005 for(const letter of this.glassLetters){ letter.position.z += 0.005 } if(this.#lettersZ>1.5) { for(const [i, letter] of this.glassLetters.entries()){ letter.rotation.x += Math.abs(i-4)/-1000 letter.rotation.y += (i-4)/2000 letter.position.x += (i-4)/4000 } } if(this.#lettersZ>4.5) { this.#lettersZ = -1 for(const [i, letter] of this.glassLetters.entries()){ letter.rotation.y = 0 letter.rotation.x = 0 letter.position.x = this.lettersPosScale[i].position.x + this.Xcorrection letter.position.z = this.lettersPosScale[i].position.z } this.nextAnim = Date.now()+10000 } } this.renderer.render(this.scene, this.camera) } } new Intro()