.chicco-hero-bloom {
position: relative;
overflow: hidden;
}
/* Il canvas non deve mai entrare nel flusso della pagina */
.chicco-hero-bloom > .chicco-leaf-canvas {
position: absolute !important;
inset: 0 !important;
width: 100% !important;
height: 100% !important;
max-width: none !important;
margin: 0 !important;
display: block;
pointer-events: none;
z-index: 3;
}
/* Solo i contenuti vanno sopra le foglie: overlay e sfondi di Elementor restano come sono */
.chicco-hero-bloom > .e-con-inner,
.chicco-hero-bloom > .elementor-container,
.chicco-hero-bloom > .elementor-element {
position: relative;
z-index: 4;
}
(() => {
/* Stessa classe della versione primaverile: basta sostituire il codice del widget */
const HERO_CLASS = 'chicco-hero-bloom';
const CONFIG = {
leavesDesktop: 40, // foglie in volo contemporaneamente (desktop)
leavesMobile: 18, // idem sotto i 768px
maxResting: 24, // foglie ferme a terra al massimo
restSeconds: [8, 15], // quanto restano a terra prima di svanire
beanChance: 0.05, // ogni tanto un chicco di caffe tra le foglie
minSize: 12,
maxSize: 25,
windDirection: -1, // -1 soffia verso sinistra (dall'albero), 1 verso destra
baseWind: 0.8,
gustEvery: [5, 10], // secondi tra una raffica e l'altra
gustStrength: [2.2, 3.8],
gravity: 0.035,
sweepRadius: 150, // raggio d'azione del mouse
sweepForce: 0.14, // forza della "scopata" del mouse
clickRadius: 260, // raggio del soffio al click
windStreaks: true, // sottili scie d'aria durante le raffiche
leafShadow: true, // ombra morbida che stacca le foglie dalla foto
leafColours: ['#c2410c', '#d9571a', '#e8801f', '#b3301c', '#8f2d14', '#d19a0a', '#f1bf00', '#a0522d', '#7a3b12'],
beanColour: '#4a230e'
};
const TAU = Math.PI * 2;
const rand = (a, b) => Math.random() * (b - a) + a;
const pick = arr => arr[Math.floor(Math.random() * arr.length)];
const clamp = (v, a, b) => Math.max(a, Math.min(b, v));
const smooth = t => t * t * (3 - 2 * t);
function shade(hex, k) {
const n = parseInt(hex.slice(1), 16);
const r = Math.round(((n >> 16) & 255) * k);
const g = Math.round(((n >> 8) & 255) * k);
const b = Math.round((n & 255) * k);
return 'rgb(' + r + ',' + g + ',' + b + ')';
}
/* Forme delle foglie in coordinate unitarie (punta in alto, gambo in basso) */
const SHAPES = (() => {
const shapes = {};
// Acero
const half = [[0, 0.55], [0.12, 0.35], [0.55, 0.45], [0.45, 0.28], [0.95, 0.05], [0.78, -0.02],
[0.88, -0.3], [0.55, -0.22], [0.5, -0.38], [0.3, -0.2], [0.35, -0.65], [0.18, -0.58], [0, -1]];
const pts = half.concat(half.slice(1, -1).reverse().map(([x, y]) => [-x, y]));
const maple = new Path2D();
maple.moveTo(pts[0][0], pts[0][1]);
for (let i = 1; i {
mapleVeins.moveTo(0, y0); mapleVeins.lineTo(x1, y1);
mapleVeins.moveTo(0, y0); mapleVeins.lineTo(-x1, y1);
});
shapes.maple = { body: maple, veins: mapleVeins };
// Quercia
const oak = new Path2D();
const right = [];
const N = 28;
for (let i = 0; i oak.lineTo(x, y));
for (let i = N; i >= 0; i--) oak.lineTo(-right[i][0], right[i][1]);
oak.closePath();
const oakVeins = new Path2D();
oakVeins.moveTo(0, 0.95); oakVeins.lineTo(0, -0.85);
[[-0.35, 0.28, -0.6], [0.05, 0.3, -0.2]].forEach(([y0, x1, y1]) => {
oakVeins.moveTo(0, y0); oakVeins.lineTo(x1, y1);
oakVeins.moveTo(0, y0); oakVeins.lineTo(-x1, y1);
});
shapes.oak = { body: oak, veins: oakVeins };
// Foglia ovale (betulla / faggio)
const oval = new Path2D();
oval.moveTo(0, -1);
oval.bezierCurveTo(0.62, -0.62, 0.58, 0.32, 0, 0.72);
oval.bezierCurveTo(-0.58, 0.32, -0.62, -0.62, 0, -1);
oval.closePath();
const ovalVeins = new Path2D();
ovalVeins.moveTo(0, 0.98); ovalVeins.lineTo(0, -0.88);
[[-0.2, 0.3, -0.48], [0.15, 0.32, -0.12]].forEach(([y0, x1, y1]) => {
ovalVeins.moveTo(0, y0); ovalVeins.lineTo(x1, y1);
ovalVeins.moveTo(0, y0); ovalVeins.lineTo(-x1, y1);
});
shapes.oval = { body: oval, veins: ovalVeins };
// Chicco di caffe
const bean = new Path2D();
bean.ellipse(0, 0, 0.42, 0.72, 0, 0, TAU);
const crease = new Path2D();
crease.moveTo(0, -0.5);
crease.bezierCurveTo(0.16, -0.18, -0.16, 0.18, 0, 0.5);
shapes.bean = { body: bean, veins: crease };
return shapes;
})();
function initAutumnHero(hero) {
if (!hero || hero.dataset.leavesReady === 'true') return;
hero.dataset.leavesReady = 'true';
if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return;
const canvas = document.createElement('canvas');
canvas.className = 'chicco-leaf-canvas';
hero.prepend(canvas);
const ctx = canvas.getContext('2d');
const dir = CONFIG.windDirection < 0 ? -1 : 1;
let width = 0;
let height = 0;
let target = CONFIG.leavesDesktop;
let sizeScale = 1;
let leaves = [];
let streaks = [];
let simT = 0;
let gust = { start: -99, hold: 1, peak: 0 };
let nextGust = rand(2.5, 4.5);
const mouse = { x: 0, y: 0, vx: 0, vy: 0, t: 0, active: false };
function resize() {
// misura la hero senza trasformazioni e ignora variazioni sotto il pixel
const w = hero.clientWidth;
const h = hero.clientHeight;
if (Math.abs(w - width) < 1 && Math.abs(h - height) < 1) return;
const dpr = Math.min(window.devicePixelRatio || 1, 2);
width = w;
height = h;
canvas.width = Math.round(width * dpr);
canvas.height = Math.round(height * dpr);
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
const small = width < 768;
target = small ? CONFIG.leavesMobile : CONFIG.leavesDesktop;
sizeScale = small ? 0.85 : 1;
}
/* ---------- vento e raffiche ---------- */
function gustLevel() {
const k = simT - gust.start;
const attack = 0.9;
const release = 2.4;
if (k < 0) return 0;
if (k < attack) return smooth(k / attack);
if (k < attack + gust.hold) return 1;
if (k = nextGust) {
gust = { start: simT, hold: rand(0.8, 2.2), peak: rand(CONFIG.gustStrength[0], CONFIG.gustStrength[1]) };
nextGust = simT + 3.3 + gust.hold + rand(CONFIG.gustEvery[0], CONFIG.gustEvery[1]);
}
}
/* ---------- foglie ---------- */
function makeLeaf(x, y, opts) {
opts = opts || {};
const bean = !opts.noBean && Math.random() < CONFIG.beanChance;
const depth = rand(0.65, 1.25);
const colour = pick(CONFIG.leafColours);
return {
x, y,
vx: opts.vx !== undefined ? opts.vx : rand(-0.3, 0.3),
vy: opts.vy !== undefined ? opts.vy : rand(0.2, 0.8),
depth,
size: rand(CONFIG.minSize, CONFIG.maxSize) * depth * sizeScale * (bean ? 0.55 : 1),
bean,
shape: bean ? 'bean' : pick(['maple', 'maple', 'oak', 'oval']),
colour,
back: shade(colour, 0.72),
vein: shade(colour, 0.48),
rot: rand(0, TAU),
rotSpeed: rand(-0.03, 0.03),
flip: rand(0, TAU),
flipSpeed: rand(0.03, 0.085) * (Math.random() < 0.5 ? -1 : 1),
swayPhase: rand(0, TAU),
swayFreq: rand(0.35, 0.8),
drag: bean ? 0.3 : rand(0.75, 1.2) * depth,
groundOffset: Math.random(),
state: 'fly',
lay: 0,
restSx: 1,
restFor: 0,
alpha: opts.fadeIn ? 0 : 1,
fadeIn: !!opts.fadeIn,
dying: false
};
}
function spawnAmbient(prefill) {
let x;
let y;
if (prefill) {
x = rand(0, width);
y = rand(-20, height * 0.7);
} else if (Math.random() < 0.7) {
// dall'alto, sbilanciato verso il lato da cui arriva il vento (l'albero)
x = width * (dir < 0 ? rand(0.25, 1.1) : rand(-0.1, 0.75));
y = rand(-40, -15);
} else {
x = dir = 0 ? 1 : -1) * rand(0.7, 1);
}
function applyPointer(p, dt, speed) {
const R = CONFIG.sweepRadius;
const dx = p.x - mouse.x;
const dy = p.y - mouse.y;
const d2 = dx * dx + dy * dy;
if (d2 > R * R) return;
const d = Math.sqrt(d2) || 1;
const f = 1 - d / R;
const f2 = f * f;
const k = CONFIG.sweepForce * f2 * (p.bean ? 0.45 : 1) * dt;
p.vx += mouse.vx * k + (dx / d) * speed * k * 0.35;
p.vy += mouse.vy * k + (dy / d) * speed * k * 0.35;
p.rotSpeed = clamp(p.rotSpeed + mouse.vx * 0.0008 * f2, -0.14, 0.14);
if (p.state === 'rest' && speed * f > 1.2) wake(p, -rand(1, 2.5) * f - speed * 0.08);
}
function burst(x, y) {
const R = CONFIG.clickRadius;
for (const p of leaves) {
const dx = p.x - x;
const dy = p.y - y;
const d = Math.sqrt(dx * dx + dy * dy) || 1;
if (d > R) continue;
const f = 1 - d / R;
if (p.state === 'rest') wake(p, -rand(2, 4) * f);
p.vx += (dx / d) * 7 * f;
p.vy += (dy / d) * 7 * f - 2 * f;
p.rotSpeed = clamp(p.rotSpeed + rand(-0.08, 0.08), -0.14, 0.14);
}
for (let i = 0; i 0.3;
let flying = 0;
let resting = 0;
for (const p of leaves) {
if (p.fadeIn) {
p.alpha = Math.min(1, p.alpha + 0.03 * dt);
if (p.alpha >= 1) p.fadeIn = false;
}
if (p.state === 'fly') {
flying++;
const sway = Math.sin(simT * p.swayFreq * TAU + p.swayPhase) * 0.6 * (p.bean ? 0.2 : 1);
const airVx = wind * p.drag + sway;
p.vx += (airVx - p.vx) * 0.035 * dt;
p.vy += CONFIG.gravity * dt;
const terminal = p.bean ? 2.8 : (0.65 + 0.85 * (1 - Math.abs(Math.cos(p.flip)))) * p.depth;
const lift = p.bean ? 0 : g * 0.9 * p.drag;
const targetVy = terminal - lift;
if (p.vy > targetVy) p.vy += (targetVy - p.vy) * 0.08 * dt;
p.vx += Math.cos(p.flip) * 0.012 * dt;
if (pointerOn) applyPointer(p, dt, speed);
p.vx = clamp(p.vx, -14, 14);
p.vy = clamp(p.vy, -14, 14);
p.x += p.vx * dt;
p.y += p.vy * dt;
p.rot += (p.rotSpeed + p.vx * 0.004) * dt;
p.flip += p.flipSpeed * (1 + Math.abs(p.vx) * 0.25) * dt;
p.lay = Math.max(0, p.lay - 0.08 * dt);
const ground = height - p.size * 0.35 - p.groundOffset * height * 0.07;
if (p.y >= ground && p.vy > 0) {
p.y = ground;
land(p);
}
} else {
resting++;
p.lay = Math.min(1, p.lay + 0.05 * dt);
p.vx *= Math.pow(0.9, dt);
p.x += p.vx * dt;
p.restFor -= dt / 60;
if (pointerOn) applyPointer(p, dt, speed);
if (g > 0.55 && !p.bean && Math.random() < 0.012 * g * dt) wake(p, -rand(0.8, 1.8));
if (p.restFor CONFIG.maxResting) {
let extra = resting - CONFIG.maxResting;
for (const p of leaves) {
if (extra p.alpha > 0 && p.x > -80 && p.x -140 && p.y < height + 80);
const want = target * (1 + g * 0.6);
if (flying < want && Math.random() 0.3 && streaks.length < 6 && Math.random() < 0.05 * g * dt) {
streaks.push({
x: dir s.life < 1);
}
mouse.vx *= Math.pow(0.82, dt);
mouse.vy *= Math.pow(0.82, dt);
}
/* ---------- disegno ---------- */
function drawLeaf(p) {
const shape = SHAPES[p.shape];
const s = p.size;
const flipX = Math.cos(p.flip);
ctx.save();
ctx.translate(p.x, p.y);
ctx.rotate(p.rot);
if (p.bean) {
ctx.globalAlpha = Math.max(p.alpha, 0);
ctx.scale(s * Math.max(Math.abs(flipX), 0.35), s * (1 - p.lay * 0.3));
ctx.fillStyle = CONFIG.beanColour;
ctx.fill(shape.body);
ctx.globalAlpha = Math.max(p.alpha * 0.6, 0);
ctx.strokeStyle = 'rgba(255, 210, 90, 0.8)';
ctx.lineWidth = 1 / s;
ctx.stroke(shape.veins);
ctx.restore();
return;
}
let sx = flipX + (p.restSx - flipX) * p.lay;
if (Math.abs(sx) < 0.05) sx = sx = 0 ? p.colour : p.back;
ctx.fill(shape.body);
const shadeAmt = 1 - Math.abs(sx);
if (shadeAmt > 0.05) {
ctx.globalAlpha = Math.max(p.alpha * shadeAmt * 0.35, 0);
ctx.fillStyle = '#2a1206';
ctx.fill(shape.body);
}
ctx.globalAlpha = Math.max(p.alpha * 0.55, 0);
ctx.strokeStyle = p.vein;
ctx.lineWidth = 1.1 / s;
ctx.lineCap = 'round';
ctx.stroke(shape.veins);
ctx.restore();
}
function drawStreak(st) {
const a = Math.sin(st.life * Math.PI) * 0.16;
if (a {
const pos = pointerPosition(event);
mouse.x = pos.x;
mouse.y = pos.y;
mouse.vx = 0;
mouse.vy = 0;
mouse.t = performance.now();
mouse.active = true;
}, { passive: true });
hero.addEventListener('pointermove', event => {
const now = performance.now();
const pos = pointerPosition(event);
if (mouse.active && mouse.t) {
const k = 16.667 / Math.max(now - mouse.t, 8);
mouse.vx = clamp(mouse.vx * 0.4 + (pos.x - mouse.x) * k * 0.6, -45, 45);
mouse.vy = clamp(mouse.vy * 0.4 + (pos.y - mouse.y) * k * 0.6, -45, 45);
}
mouse.x = pos.x;
mouse.y = pos.y;
mouse.t = now;
mouse.active = true;
}, { passive: true });
const release = () => { mouse.active = false; mouse.vx = 0; mouse.vy = 0; };
hero.addEventListener('pointerleave', release, { passive: true });
hero.addEventListener('pointercancel', release, { passive: true });
hero.addEventListener('pointerup', event => {
if (event.pointerType !== 'mouse') release();
}, { passive: true });
hero.addEventListener('click', event => {
if (event.target.closest && event.target.closest('a, button')) return;
const pos = pointerPosition(event);
burst(pos.x, pos.y);
}, { passive: true });
/* ---------- avvio ---------- */
resize();
if ('ResizeObserver' in window) new ResizeObserver(resize).observe(hero);
else window.addEventListener('resize', resize, { passive: true });
if ('IntersectionObserver' in window) {
new IntersectionObserver(entries => {
inView = entries[0].isIntersecting;
evaluate();
}).observe(hero);
}
document.addEventListener('visibilitychange', evaluate);
for (let i = 0; i < Math.round(target * 0.6); i++) spawnAmbient(true);
evaluate();
}
function boot() {
document.querySelectorAll('.' + HERO_CLASS).forEach(initAutumnHero);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', boot);
} else {
boot();
}
window.addEventListener('elementor/frontend/init', boot);
})();
Le vent d’automne,
sait toujours quoi faire.
Il balaie les feuilles mortes. Et porte avec lui l’arôme de notre café.