(function () { const CHARS = { down: ['|', '¦', '¡', 'l', 'I', '1'], downRight: ['\\', '╲'], downLeft: ['/', '╱'], leaf: ['.', 'o', '*', "'", '"', '·', '°', '`', '¨', '+'], flower: ['✿', '❀', '✾', '❁', '⚘'], debris: ['°', '·', '¨', '`', ','], }; function pick(arr) { return arr[Math.floor(Math.random() * arr.length)]; } function charForAngle(angle) { const deg = angle * 180 / Math.PI; if (deg >= 60 && deg <= 120) return pick(CHARS.down); if (deg < 60) return pick(CHARS.downRight); return pick(CHARS.downLeft); } class Vine { constructor(container) { this.container = container; this.width = container.offsetWidth || window.innerWidth; this.height = container.offsetHeight || window.innerHeight; this.tips = []; this.elements = []; this.flowers = []; this.frameCount = 0; this.minStep = 24; this.flowerCount = 0; this.startVine(); this.startVine(); this.startVine(); this.startVine(); this.createHelpUI(); this.loop = this.loop.bind(this); requestAnimationFrame(this.loop); window.addEventListener('resize', () => { this.width = container.offsetWidth || window.innerWidth; this.height = container.offsetHeight || window.innerHeight; }); // Interactions this.container.addEventListener('click', (e) => this.handleClick(e)); this.container.addEventListener('dblclick', (e) => this.handleDoubleClick(e)); this.container.addEventListener('contextmenu', (e) => { e.preventDefault(); this.handleRightClick(e); }); } startVine() { if (this.tips.length >= 12) return; this.tips.push({ x: 40 + Math.random() * Math.max(0, this.width - 80), y: -5, angle: Math.PI / 2 + (Math.random() - 0.5) * 0.25, hue: Math.random() * 360, hueSpeed: (Math.random() - 0.5) * 0.3, generation: 0, age: 0, alive: true, nextBranch: 140 + Math.random() * 140, speed: 0.7 + Math.random() * 0.3, distSinceLastChar: 0, }); } createHelpUI() { const btn = document.createElement('button'); btn.id = 'vine-help-btn'; btn.textContent = '?'; this.container.appendChild(btn); const panel = document.createElement('div'); panel.id = 'vine-help-panel'; panel.style.display = 'none'; const close = document.createElement('button'); close.className = 'close'; close.innerHTML = '×'; close.setAttribute('aria-label', 'fermer'); panel.appendChild(close); const title = document.createElement('h3'); title.innerHTML = this.animText('jardinage'); panel.appendChild(title); const items = [ { k: 'clic', a: 'sur tige', d: 'coupe la liane' }, { k: 'double-clic', a: 'sur tige', d: 'plante une fleur' }, { k: 'clic droit', a: 'sur tige', d: 'plante une fleur' }, { k: 'clic', a: 'sur fleur', d: "l'attrape" }, { k: 'clic', a: 'sur fond', d: 'dévie la racine' }, ]; const ul = document.createElement('ul'); items.forEach((it) => { const li = document.createElement('li'); li.innerHTML = '' + it.k + ' ' + it.a + ' ' + it.d; ul.appendChild(li); }); panel.appendChild(ul); this.container.appendChild(panel); const counter = document.createElement('div'); counter.id = 'vine-flower-counter'; counter.textContent = '0'; this.counterEl = counter; this.container.appendChild(counter); btn.addEventListener('click', (e) => { e.stopPropagation(); panel.style.display = panel.style.display === 'none' ? 'block' : 'none'; }); close.addEventListener('click', (e) => { e.stopPropagation(); panel.style.display = 'none'; }); panel.addEventListener('click', (e) => e.stopPropagation()); } animText(text) { return text.split('').map((c, i) => { if (c === ' ') return ' '; const delayColor = (i * 0.3).toFixed(1); const delayLevite = (Math.random() * 4).toFixed(2); const duree = (3 + Math.random() * 3).toFixed(2); return `${c}`; }).join(''); } // ===== INTERACTIONS ===== handleClick(e) { const target = e.target; // 1. Clic sur une fleur vivante -> attraper if (target.classList.contains('vine-flower') && !target.classList.contains('falling-flower')) { this.catchFlower(target); return; } // 2. Clic sur caractère de tige (pas feuille) -> couper if (target.classList.contains('vine-char') && !target.classList.contains('vine-leaf')) { this.cutVine(parseFloat(target.style.left), parseFloat(target.style.top)); return; } // 3. Clic sur fond vide -> dévier la racine la plus proche if (target === this.container) { this.redirectVine(e.offsetX, e.offsetY); } } handleDoubleClick(e) { const target = e.target; // Double-clic sur caractère de tige -> planter une fleur if (target.classList.contains('vine-char')) { const x = parseFloat(target.style.left); const y = parseFloat(target.style.top); this.plantFlower(x, y); } } handleRightClick(e) { const target = e.target; // Clic droit sur caractère de tige -> planter une fleur if (target.classList.contains('vine-char')) { const x = parseFloat(target.style.left); const y = parseFloat(target.style.top); this.plantFlower(x, y); } } findClosestTip(x, y, onlyRoots = false) { let closest = null; let closestDist = Infinity; for (const tip of this.tips) { if (onlyRoots && tip.generation > 0) continue; const dx = tip.x - x; const dy = tip.y - y; const dist = Math.sqrt(dx * dx + dy * dy); if (dist < closestDist) { closestDist = dist; closest = tip; } } return closest; } cutVine(clickX, clickY) { const tip = this.findClosestTip(clickX, clickY); if (!tip) return; // Effet de débris visuels for (let i = 0; i < 4; i++) { this.createDebris(clickX + (Math.random() - 0.5) * 20, clickY + (Math.random() - 0.5) * 20, tip.hue); } // Créer deux nouvelles lianes qui divergent const hue = tip.hue; const hueSpeed = tip.hueSpeed; this.tips.push({ x: clickX, y: clickY, angle: Math.PI / 2 - 0.3 - Math.random() * 0.4, hue, hueSpeed: hueSpeed * (0.8 + Math.random() * 0.4), generation: 0, age: 0, alive: true, nextBranch: 60 + Math.random() * 80, speed: 0.9 + Math.random() * 0.4, distSinceLastChar: 0, }); this.tips.push({ x: clickX, y: clickY, angle: Math.PI / 2 + 0.3 + Math.random() * 0.4, hue, hueSpeed: hueSpeed * (0.8 + Math.random() * 0.4), generation: 0, age: 0, alive: true, nextBranch: 60 + Math.random() * 80, speed: 0.9 + Math.random() * 0.4, distSinceLastChar: 0, }); // Tuer l'ancienne liane tip.alive = false; } plantFlower(x, y) { const tip = this.findClosestTip(x, y); const hue = tip ? tip.hue : Math.random() * 360; this.createFlower(x, y, hue); } catchFlower(el) { const idx = this.flowers.findIndex((f) => f.el === el); if (idx === -1) return; const flower = this.flowers[idx]; if (flower.dying) return; flower.dying = true; flower.el.classList.remove('vine-flower'); flower.el.classList.add('falling-flower'); flower.el.style.animationDuration = '0.6s'; flower.el.style.animationTimingFunction = 'ease-out'; setTimeout(() => { if (flower.el.parentNode) flower.el.remove(); }, 700); this.flowers.splice(idx, 1); this.flowerCount++; if (this.counterEl) { this.counterEl.textContent = this.flowerCount; } } redirectVine(clickX, clickY) { const root = this.findClosestTip(clickX, clickY, true); if (!root) return; const dx = clickX - root.x; const dy = clickY - root.y; let targetAngle = Math.atan2(Math.abs(dy), dx); targetAngle = Math.max(0.3, Math.min(Math.PI - 0.3, targetAngle)); // Transition douce : on s'approche progressivement de l'angle cible const diff = targetAngle - root.angle; root.angle += diff * 0.4; root.speed = Math.min(root.speed * 1.3, 2.5); } createDebris(x, y, hue) { const color = this.getColor(hue, y, false); const el = document.createElement('span'); el.textContent = pick(CHARS.debris); el.style.cssText = `position:absolute;left:${x}px;top:${y}px;` + `font-family:"Courier New",Courier,monospace;` + `font-size:12px;line-height:1;color:${color};` + `pointer-events:none;user-select:none;` + `opacity:0.8;`; el.style.display = 'inline-block'; this.container.appendChild(el); this.elements.push({ el, born: this.frameCount, maxAge: 60 + Math.floor(Math.random() * 40), }); } // ===== LOGIQUE EXISTANTE ===== getColor(hue, y, isFlower) { const drifted = (hue + y * 0.04 + Math.random() * 6) % 360; const sat = isFlower ? 68 : 58; const lit = isFlower ? 74 : 68; return `hsl(${drifted}, ${sat}%, ${lit}%)`; } createChar(x, y, angle, hue, isLeaf) { const color = this.getColor(hue, y, false); const el = document.createElement('span'); el.textContent = isLeaf ? pick(CHARS.leaf) : charForAngle(angle); el.classList.add('vine-char'); if (isLeaf) el.classList.add('vine-leaf'); el.style.cssText = `position:absolute;left:${x}px;top:${y}px;` + `font-family:"Courier New",Courier,monospace;` + `font-size:17px;line-height:1;color:${color};` + `text-shadow:0 0 3px currentColor;` + `pointer-events:auto;user-select:none;` + `opacity:${isLeaf ? 0.55 : 0.85};`; const scale = 0.9 + Math.random() * 0.12; const rot = (Math.random() - 0.5) * 5; el.style.transform = `scale(${scale}) rotate(${rot}deg)`; el.style.display = 'inline-block'; this.container.appendChild(el); const lifeRatio = Math.min(1, y / Math.max(1, this.height)); const maxAge = Math.floor(1800 - lifeRatio * 1000 + Math.random() * 300); this.elements.push({ el, born: this.frameCount, maxAge, }); } createFlower(x, y, hue) { if (this.flowers.length >= 8) return; const color = this.getColor(hue, y, true); const el = document.createElement('span'); el.textContent = pick(CHARS.flower); el.classList.add('vine-flower'); el.style.cssText = `position:absolute;left:${x}px;top:${y}px;` + `font-family:"Courier New",Courier,monospace;` + `font-size:18px;line-height:1;color:${color};` + `text-shadow:0 0 5px currentColor;` + `pointer-events:auto;user-select:none;` + `opacity:0.9;display:inline-block;`; this.container.appendChild(el); this.flowers.push({ el, x, y, life: 30 + Math.random() * 40, dying: false, }); } updateFlowers() { for (let i = this.flowers.length - 1; i >= 0; i--) { const f = this.flowers[i]; if (f.dying) continue; f.life -= 1; if (f.life <= 0) { f.dying = true; f.el.classList.remove('vine-flower'); f.el.classList.add('falling-flower'); setTimeout(() => { if (f.el.parentNode) f.el.remove(); }, 2600); this.flowers.splice(i, 1); } } } ageElements() { if (this.frameCount % 10 !== 0) return; const now = this.frameCount; let pruneCount = 0; const maxPrune = 18; for (let i = 0; i < this.elements.length && pruneCount < maxPrune; i++) { const item = this.elements[i]; const age = now - item.born; if (age >= item.maxAge) { if (item.el.classList.contains('vine-leaf') && !item.falling) { item.falling = true; item.el.classList.add('falling-leaf'); setTimeout(() => { if (item.el.parentNode) item.el.remove(); }, 2100); } else { item.el.remove(); } this.elements.splice(i, 1); i--; pruneCount++; } else if (age > item.maxAge - 120 && !item.falling) { const fade = 1 - (age - (item.maxAge - 120)) / 120; item.el.style.opacity = Math.max(0, fade * 0.85); } } } update() { this.frameCount++; if (this.frameCount % 3 !== 0) return; this.updateFlowers(); this.ageElements(); const newTips = []; for (let i = 0; i < this.tips.length; i++) { const tip = this.tips[i]; tip.age++; tip.hue = (tip.hue + tip.hueSpeed) % 360; if (tip.hue < 0) tip.hue += 360; tip.angle += (Math.random() - 0.5) * 0.025; if (tip.angle < 0.3) tip.angle = 0.3; if (tip.angle > Math.PI - 0.3) tip.angle = Math.PI - 0.3; const dx = Math.cos(tip.angle); const dy = Math.sin(tip.angle); const step = tip.speed * 1.6; tip.x += dx * step; tip.y += dy * step; tip.distSinceLastChar += step; if (tip.distSinceLastChar >= this.minStep) { tip.distSinceLastChar = 0; this.createChar(tip.x, tip.y, tip.angle, tip.hue, false); } if (tip.generation > 0 && Math.random() < 0.015) { const lx = tip.x + (Math.random() - 0.5) * 14; const ly = tip.y + (Math.random() - 0.5) * 14; this.createChar(lx, ly, tip.angle, tip.hue, true); } if ( tip.age >= tip.nextBranch && tip.generation < 2 ) { tip.nextBranch = tip.age + 140 + Math.random() * 160; const side = Math.random() > 0.5 ? 1 : -1; const spread = 0.35 + Math.random() * 0.55; newTips.push({ x: tip.x, y: tip.y, angle: tip.angle + side * spread, hue: tip.hue, hueSpeed: tip.hueSpeed, generation: tip.generation + 1, age: 0, alive: true, nextBranch: 120 + Math.random() * 140, speed: tip.speed * (0.75 + Math.random() * 0.15), distSinceLastChar: 0, }); this.createFlower( tip.x + (Math.random() - 0.5) * 8, tip.y + (Math.random() - 0.5) * 8, tip.hue ); } if (tip.generation >= 1 && Math.random() < 0.008) { this.createFlower( tip.x + (Math.random() - 0.5) * 6, tip.y + (Math.random() - 0.5) * 6, tip.hue ); } // Branches : meurent quand sortent if (tip.generation > 0) { if ( tip.y > this.height + 15 || tip.x < -20 || tip.x > this.width + 20 ) { tip.alive = false; } } // Racines : respawn immédiat quand touchent le bas else if (tip.y > this.height + 15) { tip.y = -5; tip.x = 40 + Math.random() * Math.max(0, this.width - 80); tip.angle = Math.PI / 2 + (Math.random() - 0.5) * 0.25; tip.hue = Math.random() * 360; tip.hueSpeed = (Math.random() - 0.5) * 0.3; tip.age = 0; tip.nextBranch = 140 + Math.random() * 140; tip.distSinceLastChar = 0; // Chance de faire apparaître une nouvelle racine (max 6 racines) if (this.tips.filter((t) => t.generation === 0).length < 6 && Math.random() < 0.35) { this.startVine(); } } } this.tips = this.tips.filter((t) => t.alive).concat(newTips); // Respawn de secours si jamais il y a trop peu de tips if (this.tips.filter((t) => t.generation === 0).length < 3 && Math.random() < 0.5) { this.startVine(); } } loop() { this.update(); requestAnimationFrame(this.loop); } } document.addEventListener('DOMContentLoaded', () => { const container = document.getElementById('vine-container'); if (container) { new Vine(container); } }); })();