Files
P42_UI/app/assets/sfx/intro.js
T
2025-11-15 20:10:31 +00:00

305 lines
12 KiB
JavaScript

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')
// 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, this.hdrProgress, 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.png', resolve, undefined, reject)
})
Promise.allSettled([loadHDR(), loadTexture1(), loadTexture2()]).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){ console.log('initScene')
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(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.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: false,
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)
this.canvas.classList.add('ready')
}
launch3DAnim(){ console.log('CSs finished, launching 3D anim')
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, alphaTest: 0.01, 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.fragmentShader = shader.fragmentShader
.replace(
`#include <uv_pars_fragment>`,
`#include <uv_pars_fragment>
uniform float uReveal;`
)
.replace(
`#include <envmap_fragment>`,
`#include <envmap_fragment>
if (vUv.x > uReveal) 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, 0)
this.planeEE.renderOrder = 1
this.scene.add(this.planeEE)
} else {
this.planeEE.geometry.dispose()
this.planeEE.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight)
}
}
animate() {
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
}
}
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()