diff --git a/app/assets/images/kloofendal_48d_partly_cloudy_puresky_2k.hdr b/app/assets/images/kloofendal_48d_partly_cloudy_puresky_2k.hdr new file mode 100644 index 0000000..31843fc Binary files /dev/null and b/app/assets/images/kloofendal_48d_partly_cloudy_puresky_2k.hdr differ diff --git a/app/assets/sfx/intro.js b/app/assets/sfx/intro.js index 1dcf7bb..dcd3910 100644 --- a/app/assets/sfx/intro.js +++ b/app/assets/sfx/intro.js @@ -3,211 +3,224 @@ 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 +class Intro{ -// === 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 + #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 + } -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 -}) + 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 -// === Load background as a plane that fits the viewport === -const loader = new THREE.TextureLoader() -let plane -let planeDistance = 4.4 + hdrTexture.mapping = THREE.EquirectangularReflectionMapping + this.backgroundTexture.colorSpace = THREE.SRGBColorSpace + } else { + console.error('Loading error:', hdrResult, texResult) + return + } -loader.load('/app/assets/images/logop42-intro_three.png', texture => { + this.scene = new THREE.Scene() - 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) + 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 * camera.aspect + const viewWidth = viewHeight * this.camera.aspect let planeWidth, planeHeight - if (viewWidth / viewHeight > imageAspect) { + if (viewWidth / viewHeight > this.imageAspect) { planeHeight = viewHeight - planeWidth = planeHeight * imageAspect + planeWidth = planeHeight * this.imageAspect } else { planeWidth = viewWidth - planeHeight = planeWidth / imageAspect + planeHeight = planeWidth / this.imageAspect } - if (!plane) { + if (!this.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) + 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 { - plane.geometry.dispose() - plane.geometry = new THREE.PlaneGeometry(planeWidth, planeHeight) + this.plane.geometry.dispose() + this.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, - envMapIntensity: 2.5 -}) + 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 + } -// === 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) - + 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 + } + } -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.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 + } } - } - if(lettersZ>4.5) { - lettersZ = -1 - for(const [i, letter] of glassLetters.entries()){ - letter.rotation.y = 0 - letter.rotation.x = 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) + this.renderer.render(this.scene, this.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()) -}) +new Intro() + + diff --git a/app/assets/styles/intro-old.css b/app/assets/styles/intro-old.css deleted file mode 100644 index cb4b446..0000000 --- a/app/assets/styles/intro-old.css +++ /dev/null @@ -1,228 +0,0 @@ -body { - display: flex; - justify-content: center; - align-items: center; - width: 100%; - min-height: 100vh; - background: #161616; - overflow: hidden; - font-family: sans; -} - - -canvas.intro3d{ - position:absolute; - top:0; - left:0; - width:100%; - height:100%; - background:transparent; - pointer-events: none; - z-index:99; -} -.glowing { - position: absolute; - width: 60vw; - height: 100vh; - transform-origin: right; - animation: colorChange 5s linear infinite; - animation: fadeaway 30s forwards; - transform: translateX(-50%); -} -.glowing.lt { - left: 0%; - transform: rotate(-15deg); - top: -15%; -} -.glowing.ct { - left: 50%; -} -.glowing.rt { - left: 50%; - transform: rotate(15deg) -} - - -.glowing:nth-child(even) { - transform-origin: left; -} - -@keyframes colorChange { - 0% { filter: hue-rotate(0deg); } - 100% { filter: hue-rotate(360deg); } -} - -.glowing span { - position: absolute; - top: calc(80px * var(--i)); - left: calc(80px * var(--i)); - bottom: calc(80px * var(--i)); - right: calc(80px * var(--i)); - filter: blur(2px); -} - -.glowing span::before { - content: ""; - position: absolute; - top: 50%; - left: -8px; - width: 10px; - height: 10px; - background: #f00; - border-radius: 50%; -} - -.glowing span:nth-child(3n + 1)::before { - background: rgba(134,255,0,1); - box-shadow: 0 0 20px rgba(134,255,0,1), - 0 0 40px rgba(134,255,0,1), - 0 0 60px rgba(134,255,0,1), - 0 0 80px rgba(134,255,0,1), - 0 0 0 8px rgba(134,255,0,.1); -} - -.glowing span:nth-child(3n + 2)::before { - background: rgba(0,255,183,1); - box-shadow: 0 0 20px rgba(0,255,183,1), - 0 0 40px rgba(0,255,183,1), - 0 0 60px rgba(0,255,183,1), - 0 0 80px rgba(0,255,183,1), - 0 0 0 8px rgba(0,255,183,.1); -} - -.glowing span:nth-child(3n + 3)::before { - background: rgba(0,226,255,1); - box-shadow: 0 0 20px rgba(0,226,255,1), - 0 0 40px rgba(0,226,255,1), - 0 0 60px rgba(0,226,255,1), - 0 0 80px rgba(0,226,255,1), - 0 0 0 8px rgba(0,226,255,.1); -} - -.glowing span:nth-child(3n + 1) { - animation: rotatedots1 15s alternate infinite; -} - -.glowing span:nth-child(3n + 2) { - animation: rotatedots-reverse 9s alternate infinite; -} - -.glowing span:nth-child(3n + 3) { - animation: rotatedots1 15s alternate infinite; -} - -@keyframes fadeaway { - 0% { opacity: 1; } - 15% { opacity: 1; } - 100% { opacity: 0; } -} - -@keyframes rotatedots1 { - 0% { transform: rotate(calc(180deg * var(--i))); scale:20%;} - 25% { transform: rotate(calc(90deg * var(--i))); scale:100%;} - 50% { transform: rotate(calc(0deg * var(--i))); scale:80%;} - 100% { transform: rotate(calc(360deg * var(--i))); scale:10%;} -} - - -@keyframes rotatedots-reverse { - 0% { transform: rotate(calc(135deg * var(--i))); scale:20%;} - 25% { transform: rotate(calc(45deg * var(--i))); scale:100%;} - 50% { transform: rotate(calc(-45deg * var(--i))); scale:80%;} - 100% { transform: rotate(calc(315deg * var(--i))); scale:10%;} -} - - - - -.logointro{ - filter: blur(14px); - opacity: 0; - animation: logoanim 5s ease-out forwards; /* runs once */ - will-change: filter, opacity; - color: #FFF; - position: absolute; - text-shadow: 3px -3px 5px #C5F7FF; -} - -@keyframes logoanim{ - 0% { filter: blur(14px); opacity: 0; transform: scale(0.1) } - 100% { filter: blur(0); opacity: 1; transform: scale(0.7) } -} - -@keyframes startbtnanim{ - 0% { opacity: 0; } - 40% { opacity: 0.8; } - 100% { opacity: 1; } -} - -#startbtn{ - cursor: default; - position: absolute; - bottom: 15vh; - color: aliceblue; - font-style: italic; - font-size: 1.5rem; - border-radius: 100px; - padding: 5px 40px; - background: radial-gradient(#82cc50, #0a8200); - box-shadow: 2px 2px 10px #BAFFEF, -2px -2px 10px #BAFFEF, -2px 0 10px #BAFFEF, 0 -2px 10px #BAFFEF; - animation: startbtnanim 5s ease-out forwards; - z-index: 99; -} - -#startbtn[disabled]{ - filter: brightness(.3); -} - -#login-dialog{ - width: 22rem; - z-index: 99; - border-radius: 1rem; - box-shadow: 2px 2px 10px #BAFFEF, -2px -2px 10px #BAFFEF, -2px 0 10px #BAFFEF, 0 -2px 10px #BAFFEF; - background: radial-gradient(#82cc50, #0a8200); - padding: 2rem 3rem 1rem 3rem; - margin-top: -68vh; - opacity: 0; - visibility: hidden; - transition: opacity 0.5s ease; - font-size: 1.3rem; - transform: perspective(200px) rotateY(0deg) rotateX(6deg) rotateZ(0deg); - transform-style: preserve-3d; -} -#login-dialog.show { - opacity: 1; - visibility: visible; -} -#login-dialog > div.cols2{ - display: grid !important; - grid-gap: 10px; - grid-template-columns: 7rem 15rem; -} -#login-dialog > div{ - margin-bottom: 1rem; -} -#login-dialog input{ - line-height: 2rem; - width: 15rem; - border-radius: 5px; - border: none; - font-size: 1rem; -} -#login-dialog button{ - justify-self: end; - font-size: 1rem; - float: right; -} -#login-dialog div.loginerr{ - background-color: #326A1E; - font-size: 1rem; - color: #FFF; - padding: .1rem .5rem; - border-radius: 5px; - text-align: center; - display:none; -} -#login-dialog div.loginerr.show{ - display:block!important; -} \ No newline at end of file diff --git a/app/assets/styles/intro.css b/app/assets/styles/intro.css index e255223..92458e0 100644 --- a/app/assets/styles/intro.css +++ b/app/assets/styles/intro.css @@ -19,6 +19,9 @@ canvas.intro3d{ background:transparent; pointer-events: none; z-index:99; +} + +canvas.intro3d.ready{ animation: logoanim 3.5s ease-out forwards; } @@ -32,6 +35,15 @@ canvas.intro3d{ 40% { opacity: 0.8; } 100% { opacity: 1; } } +.progress{ + position: absolute; + width: 100px; + height: 2px; + background: white; + top: 50%; + left: 50%; + transform: translate(-50%, -50%); +} #startbtn{ cursor: default; diff --git a/index.html b/index.html index 85eb8ad..03fd732 100644 --- a/index.html +++ b/index.html @@ -20,6 +20,7 @@ +