Last active
March 30, 2026 07:26
-
-
Save Lysak/83ec1112ed616e53851cbfc466ded85f to your computer and use it in GitHub Desktop.
smoke-script.js
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* smoke-script.js | |
| * – plynulý kouř, fade-in/fade-out | |
| * – pozastaví se, když HERO2 není v záběru (IntersectionObserver) | |
| * – probuzení záložky/telefonu už neodskakuje | |
| */ | |
| (function () { | |
| const smokeURL = './images/Smoke-Element-white.png'; | |
| /* ---------- laditelné parametry ---------- */ | |
| const smokeSpeed = 1.4; | |
| const riseSpeed = 30.0; | |
| const clearRadius = 250; | |
| const spawnRadius = 750; | |
| const planeSize = 450; | |
| const spriteCount = 90; | |
| const baseOpacity = 0.1; | |
| const fadeInTime = 1.2; | |
| const edgeFadeLen = 120; | |
| /* ----------------------------------------- */ | |
| const clamp = (v, a, b) => (v < a ? a : v > b ? b : v); | |
| let container, renderer, scene, camera, clock; | |
| const smokeParticles = []; | |
| /* ————— init po onload ————— */ | |
| window.addEventListener('load', () => { | |
| container = document.getElementById('smoke-container'); | |
| if (!container) return; | |
| /* renderer – canvas přes celý kontejner */ | |
| renderer = new THREE.WebGLRenderer({ alpha: true, antialias: true }); | |
| renderer.setPixelRatio(window.devicePixelRatio || 1); | |
| Object.assign(renderer.domElement.style, { | |
| position: 'absolute', | |
| inset : '0', | |
| width : '100%', | |
| height : '100%', | |
| pointerEvents: 'none' | |
| }); | |
| container.appendChild(renderer.domElement); | |
| /* scene & camera */ | |
| scene = new THREE.Scene(); | |
| camera = new THREE.PerspectiveCamera(75, 1, 1, 10000); | |
| camera.position.z = 1000; | |
| scene.add(camera); | |
| /* textury + částice */ | |
| new THREE.TextureLoader().load(smokeURL, tex => { | |
| tex.wrapS = tex.wrapT = THREE.ClampToEdgeWrapping; | |
| tex.minFilter = THREE.LinearFilter; | |
| const templateMat = new THREE.MeshBasicMaterial({ | |
| map : tex, | |
| transparent: true, | |
| depthWrite : false, | |
| blending : THREE.AdditiveBlending | |
| }); | |
| const geo = new THREE.PlaneGeometry(planeSize, planeSize); | |
| for (let i = 0; i < spriteCount; i++) { | |
| const mat = templateMat.clone(); | |
| const p = new THREE.Mesh(geo, mat); | |
| /* spawn v „donutu“ */ | |
| const ang = Math.random() * Math.PI * 2; | |
| const rad = clearRadius + Math.random() * (spawnRadius - clearRadius); | |
| p.position.set( | |
| Math.cos(ang) * rad, | |
| Math.sin(ang) * rad, | |
| Math.random() * 600 - 300 | |
| ); | |
| p.scale.multiplyScalar(0.8 + Math.random() * 0.4); | |
| p.rotation.z = Math.random() * Math.PI * 2; | |
| p.userData.vy = riseSpeed * (0.6 + Math.random() * 0.8); | |
| p.userData.fade = Math.random(); // rychlejší start | |
| mat.opacity = baseOpacity * p.userData.fade; | |
| scene.add(p); | |
| smokeParticles.push(p); | |
| } | |
| clock = new THREE.Clock(); | |
| /* resize → vždy podle rect kontejneru */ | |
| const onResize = () => { | |
| const { width, height } = container.getBoundingClientRect(); | |
| if (width && height) { | |
| renderer.setSize(width, height, false); | |
| camera.aspect = width / height; | |
| camera.updateProjectionMatrix(); | |
| } | |
| }; | |
| new ResizeObserver(onResize).observe(container); | |
| window.addEventListener('resize', onResize); | |
| onResize(); | |
| /* page-visibility → žádné obří dt */ | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.visibilityState === 'hidden') clock.stop(); | |
| else clock.start(); | |
| }); | |
| /* start animace jen když je HERO2 vidět */ | |
| setupIntersectionPause(); | |
| }); | |
| }); | |
| /* ————— IntersectionObserver = pauza mimo viewport ————— */ | |
| function setupIntersectionPause() { | |
| let rafId = null; | |
| const resume = () => { | |
| if (rafId === null) { | |
| clock.start(); // vynuluj dt | |
| animate(); // rozjeď smyčku | |
| } | |
| }; | |
| const pause = () => { | |
| if (rafId !== null) { | |
| cancelAnimationFrame(rafId); | |
| rafId = null; | |
| clock.stop(); | |
| } | |
| }; | |
| function animate() { | |
| rafId = requestAnimationFrame(animate); | |
| /* limit dt na 0.05 s, jinak může částice „skočit“ */ | |
| const dt = Math.min(clock.getDelta(), 0.05); | |
| smokeParticles.forEach(p => { | |
| p.rotation.z += dt * 0.2 * smokeSpeed; | |
| p.position.y += dt * p.userData.vy; | |
| if (p.userData.fade < 1) { | |
| p.userData.fade = Math.min(1, p.userData.fade + dt / fadeInTime); | |
| } | |
| let edge = 1; | |
| if (p.position.y > spawnRadius - edgeFadeLen) { | |
| edge = (spawnRadius - p.position.y) / edgeFadeLen; | |
| } else if (p.position.y < -spawnRadius + edgeFadeLen) { | |
| edge = (p.position.y + spawnRadius) / edgeFadeLen; | |
| } | |
| p.material.opacity = | |
| baseOpacity * | |
| p.userData.fade * | |
| clamp(edge, 0, 1); | |
| if (p.position.y > spawnRadius) { | |
| p.position.y = -spawnRadius; | |
| p.userData.fade = 0; | |
| } | |
| }); | |
| renderer.render(scene, camera); | |
| } | |
| /* sleduj hero-sekci (předpoklad <section id="hero2">) */ | |
| const hero = document.getElementById('hero2') || container; | |
| const io = new IntersectionObserver( | |
| ([entry]) => (entry.isIntersecting ? resume() : pause()), | |
| { threshold: 0.1 } | |
| ); | |
| io.observe(hero); | |
| } | |
| })(); |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Smoke-Element-white.png
