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' import * as TWEEN from 'three/examples/jsm/libs/tween.module.js' class Intro{ #lettersZ=-1 #onResizeCallbacks = [] constructor(){ this.canvas = document.querySelector('.intro3d') // Order : // 1. Launch loading promises // 2. When they are all completed, initScene // 2.1 at the end of initScen, we add the class 'ready' to the canvas, which launches the CSS animation // 3. When that CS animation is finished, we can launch the 3D animation (rendering already started to have textures) this.canvas.addEventListener('animationend', this.launch3DAnim.bind(this)) const loadHDR = () => new Promise((resolve, reject) => { new RGBELoader() .setPath('/app/assets/images/') .load('kloofendal_48d_partly_cloudy_puresky_2k.hdr', resolve, null, reject) }) const loadTexture1 = () => new Promise((resolve, reject) => { new THREE.TextureLoader() .setPath('/app/assets/images/') .load('logop42-intro_three.png', resolve, undefined, reject) }) const loadTexture2 = () => new Promise((resolve, reject) => { new THREE.TextureLoader() .setPath('/app/assets/images/') .load('Logo-Emergence-bck.png', resolve, undefined, reject) }) Promise.allSettled([loadHDR(), loadTexture1(), loadTexture2()]).then(this.initScene.bind(this)) this.Xcorrection = -0.02 } initScene(LoadersResults){ let hdrTexture const [hdrResult, texResultP42, texResultEE] = LoadersResults if((hdrResult.status === 'fulfilled') && (texResultP42.status === 'fulfilled') && (texResultEE.status === 'fulfilled')) { hdrTexture = hdrResult.value this.backgroundTextureP42 = texResultP42.value this.backgroundTextureEE = texResultEE.value hdrTexture.mapping = THREE.EquirectangularReflectionMapping this.backgroundTextureP42.colorSpace = THREE.SRGBColorSpace this.backgroundTextureEE.colorSpace = THREE.SRGBColorSpace } else { console.error('Loading error:', hdrResult, texResultP42, texResultEE) return } this.scene = new THREE.Scene() this.camera = new THREE.PerspectiveCamera(50, window.innerWidth / window.innerHeight, 0.01, 100) this.camera.position.set(-2, -0.5, 4) //(0.1, 0.02, 4) this.camera.lookAt(0,0,0) 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.backgroundTextureP42.colorSpace = THREE.SRGBColorSpace this.backgroundTextureP42.wrapS = THREE.ClampToEdgeWrapping this.backgroundTextureP42.wrapT = THREE.ClampToEdgeWrapping this.backgroundTextureP42.generateMipmaps = false this.backgroundTextureP42.minFilter = THREE.LinearFilter this.backgroundTextureP42.magFilter = THREE.LinearFilter this.aspectRatioP42 = this.backgroundTextureP42.image.width / this.backgroundTextureP42.image.height this.backgroundTextureEE.colorSpace = THREE.SRGBColorSpace this.backgroundTextureEE.wrapS = THREE.ClampToEdgeWrapping this.backgroundTextureEE.wrapT = THREE.ClampToEdgeWrapping this.backgroundTextureEE.generateMipmaps = false this.backgroundTextureEE.minFilter = THREE.LinearFilter this.backgroundTextureEE.magFilter = THREE.LinearFilter this.aspectRatioEE = this.backgroundTextureEE.image.width / this.backgroundTextureEE.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, depthWrite: true, depthTest: true, transparent: true, }) 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, renderOrder: 10, }) 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.renderer.render(this.scene, this.camera) document.querySelector('div.loading').style.display = 'none' this.canvas.classList.add('ready') } launch3DAnim(){ console.log('CSs finished, launching 3D anim') document.getElementById('startbtn').style.display = 'block' this.nextAnim = Date.now() 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.aspectRatioP42) { planeHeight = viewHeight planeWidth = planeHeight * this.aspectRatioP42 } else { planeWidth = viewWidth planeHeight = planeWidth / this.aspectRatioP42 } if (!this.planeP42) { const geo = new THREE.PlaneGeometry(planeWidth, planeHeight) const mat = new THREE.MeshBasicMaterial({ map: this.backgroundTextureP42, transparent: false, depthWrite:true, depthTest:true }) this.planeP42 = new THREE.Mesh(geo, mat) this.planeP42.scale.set(1.44, 1.5, 1) this.planeP42.position.x = this.Xcorrection this.planeP42.position.z = 0 this.planeP42.renderOrder = 0 this.scene.add(this.planeP42) } else { this.planeP42.geometry.dispose() this.planeP42.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) } if (viewWidth / viewHeight > this.aspectRatioEE) { planeHeight = viewHeight planeWidth = planeHeight * this.aspectRatioEE } else { planeWidth = viewWidth planeHeight = planeWidth / this.aspectRatioEE } if (!this.planeEE) { const geo = new THREE.PlaneGeometry(planeWidth, planeHeight) this.matEE = new THREE.MeshBasicMaterial({ map: this.backgroundTextureEE, transparent: false, depthWrite:true, depthTest:true }) this.matEE.defines = this.matEE.defines || {} this.matEE.defines.USE_UV = "" this.matEE.onBeforeCompile = shader => { shader.uniforms.uReveal = { value: 0 } shader.uniforms.uTime = { value: 0 }; shader.fragmentShader = shader.fragmentShader .replace( `#include `, `#include uniform float uReveal; uniform float uTime; ` ) .replace( `#include `, `#include if ((uReveal-vUv.x) < (1.0-uReveal)){ float random = fract(sin(dot(gl_FragCoord.xy + vec2(uTime) , vec2(12.9898,78.233))) * 43758.5453); if(random > 0.1) discard; } ` ) this.matEE.userData.shader = shader } this.planeEE = new THREE.Mesh(geo, this.matEE) this.planeEE.scale.set(.2, .2, 1) this.planeEE.position.set(0,-0.8,.1) this.planeEE.renderOrder = 1 this.scene.add(this.planeEE) } else { this.planeEE.geometry.dispose() this.planeEE.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) } } animate() { TWEEN.update() this.camera.lookAt(0,0,0) requestAnimationFrame(this.animate.bind(this)) if (this.matEE.userData.shader) { if(this.matEE.userData.shader.uniforms.uReveal.value<1){ this.matEE.userData.shader.uniforms.uReveal.value += 0.01 this.matEE.userData.shader.uniforms.uTime.value = Math.floor(performance.now() / 200) } } if(Date.now()>this.nextAnim){ this.#lettersZ += .005 for(const letter of this.glassLetters){ letter.position.z += 0.005 } if(this.#lettersZ>0.5){ if(!this.camTween){ this.camTween = new TWEEN.Tween(this.camera.position) .to({ x: 0.1, y: 0.02, z: 4, }, 7000) .easing(TWEEN.Easing.Sinusoidal.InOut) .start() } } 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()