Skip to content

Instantly share code, notes, and snippets.

@Lysak
Last active March 30, 2026 07:26
Show Gist options
  • Select an option

  • Save Lysak/83ec1112ed616e53851cbfc466ded85f to your computer and use it in GitHub Desktop.

Select an option

Save Lysak/83ec1112ed616e53851cbfc466ded85f to your computer and use it in GitHub Desktop.
smoke-script.js
/* 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);
}
})();
@Lysak
Copy link
Copy Markdown
Author

Lysak commented Mar 30, 2026

Smoke-Element-white.png
Smoke-Element-white

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment