/* ---------------------------------------------------------------- Le Shed — Paysage de formes élémentaires Système décoratif indépendant de objets.js. Place des formes SVG individuelles en strates horizontales (ciel → horizon → lointain → moyen plan → premier plan) avec parallax custom (pas Rellax) et répartition organique (Perlin). Les formes vivent dans un wrapper position:fixed + overflow:hidden pour ne jamais influencer le scrollHeight du document. ---------------------------------------------------------------- */ (function () { /* ============================================================ PERLIN NOISE 2D (simplifié, 2 octaves) ============================================================ */ var PERLIN_SIZE = 256; var perm = []; function initPerlin(seed) { var p = []; for (var i = 0; i < PERLIN_SIZE; i++) p[i] = i; var s = seed || 42; for (var i = PERLIN_SIZE - 1; i > 0; i--) { s = (s * 16807 + 0) % 2147483647; var j = s % (i + 1); var tmp = p[i]; p[i] = p[j]; p[j] = tmp; } perm = []; for (var i = 0; i < 512; i++) perm[i] = p[i & 255]; } function fade(t) { return t * t * t * (t * (t * 6 - 15) + 10); } function lerp(a, b, t) { return a + t * (b - a); } var grad3 = [[1,1],[-1,1],[1,-1],[-1,-1],[1,0],[-1,0],[0,1],[0,-1]]; function dot2(g, x, y) { return g[0] * x + g[1] * y; } function perlin2(x, y) { var X = Math.floor(x) & 255, Y = Math.floor(y) & 255; x -= Math.floor(x); y -= Math.floor(y); var u = fade(x), v = fade(y); var aa = perm[perm[X] + Y], ab = perm[perm[X] + Y + 1]; var ba = perm[perm[X + 1] + Y], bb = perm[perm[X + 1] + Y + 1]; return lerp( lerp(dot2(grad3[aa & 7], x, y), dot2(grad3[ba & 7], x - 1, y), u), lerp(dot2(grad3[ab & 7], x, y - 1), dot2(grad3[bb & 7], x - 1, y - 1), u), v ); } function perlinOctaves(x, y, octaves) { var val = 0, amp = 1, freq = 1, max = 0; for (var i = 0; i < octaves; i++) { val += perlin2(x * freq, y * freq) * amp; max += amp; amp *= 0.5; freq *= 2; } return (val / max + 1) / 2; } /* ============================================================ SEEDED RANDOM ============================================================ */ var rngState = 12345; function seedRng(s) { rngState = s; } function rng() { rngState = (rngState * 16807 + 0) % 2147483647; return (rngState - 1) / 2147483646; } function rngInt(min, max) { return Math.floor(rng() * (max - min + 1)) + min; } function rngFloat(min, max) { return rng() * (max - min) + min; } function rngPick(arr) { return arr[rngInt(0, arr.length - 1)]; } /* ============================================================ VOCABULAIRE PAR REGISTRE ============================================================ */ var REGISTRES = { nuages: ['Rectangle-6', 'Rectangle-8', 'Rectangle-9', 'Union'], ciel_legers: ['Path-10', 'Union-2', 'Union'], horizon: ['Path-2', 'Path-4', 'Rectangle-7'], lointain: ['Path-3', 'Path-9', 'Rectangle', 'Path-2'], verticales: ['Path-3', 'Path-8', 'Path-9', 'Rectangle', 'Rectangle-2'], obliques_moyennes: ['Path-5', 'Path-6', 'Rectangle-3', 'Rectangle-4'], obliques_longues: ['Path-7', 'Rectangle-5', 'Path'], masses: ['Path', 'Rectangle-3', 'Rectangle-9'], details_sol: ['Path-10', 'Path-3', 'Rectangle'] }; /* ============================================================ COLLISION DETECTION ============================================================ */ var MARGIN = 0.8; var placed = []; function collides(x, y, w, h) { var ax1 = x - MARGIN, ay1 = y - MARGIN; var ax2 = x + w + MARGIN, ay2 = y + h + MARGIN; for (var i = 0; i < placed.length; i++) { var b = placed[i]; if (ax1 < b.x2 && ax2 > b.x1 && ay1 < b.y2 && ay2 > b.y1) return true; } return false; } function addPlaced(x, y, w, h) { placed.push({ x1: x, x2: x + w, y1: y, y2: y + h }); } /* ============================================================ SVG ELEMENT CREATION (no data-rellax-speed — parallax is manual) ============================================================ */ // All placed shapes for manual parallax var allShapes = []; function createSvgShape(shapeName, x, y, scale, speed) { var data = window.LeShedFormesElementaires[shapeName]; if (!data) return null; var ns = 'http://www.w3.org/2000/svg'; var svg = document.createElementNS(ns, 'svg'); var w = data.vbW * scale; var h = data.vbH * scale; svg.setAttribute('viewBox', data.vbX + ' ' + data.vbY + ' ' + data.vbW + ' ' + data.vbH); svg.setAttribute('width', w); svg.setAttribute('height', h); svg.setAttribute('fill', 'none'); var path = document.createElementNS(ns, 'path'); path.setAttribute('d', data.d); path.setAttribute('fill', 'black'); svg.appendChild(path); svg.style.position = 'absolute'; svg.style.left = x + 'px'; svg.style.pointerEvents = 'none'; svg.style.userSelect = 'none'; svg.style.willChange = 'transform'; // Store for parallax loop allShapes.push({ el: svg, baseY: y, speed: speed }); return { el: svg, w: w, h: h }; } /* ============================================================ PLACEMENT STRATEGIES ============================================================ */ function tryPlace(shapeName, x, y, scale, speed, layerEl) { var data = window.LeShedFormesElementaires[shapeName]; if (!data) return false; var w = data.vbW * scale; var h = data.vbH * scale; if (collides(x, y, w, h)) return false; var result = createSvgShape(shapeName, x, y, scale, speed); if (!result) return false; layerEl.appendChild(result.el); addPlaced(x, y, w, h); return true; } function placeCluster(shapes, cx, cy, radius, count, scale, speedRange, layerEl, pageW) { var placedCount = 0; var attempts = 0; while (placedCount < count && attempts < count * 8) { attempts++; var angle = rng() * Math.PI * 2; var dist = rng() * radius; var x = cx + Math.cos(angle) * dist; var y = cy + Math.sin(angle) * dist; if (x < 0 || x > pageW) continue; var shape = rngPick(shapes); var speed = rngFloat(speedRange[0], speedRange[1]); if (tryPlace(shape, x, y, scale, Math.round(speed), layerEl)) { placedCount++; } } return placedCount; } /* ============================================================ PARALLAX SCROLL LOOP ============================================================ */ function startParallaxLoop() { var ticking = false; function update() { var scrollY = window.scrollY; for (var i = 0; i < allShapes.length; i++) { var s = allShapes[i]; // Position dans le viewport sans parallax : baseY - scrollY // Parallax : décalage doux proportionnel au scroll, comme Rellax // Rellax fait environ speed * scrollY / 10 var basePos = s.baseY - scrollY; var parallaxOffset = s.speed * scrollY * 0.1; s.el.style.transform = 'translateY(' + Math.round(basePos + parallaxOffset) + 'px)'; } ticking = false; } window.addEventListener('scroll', function () { if (!ticking) { ticking = true; requestAnimationFrame(update); } }, { passive: true }); // Initial position update(); } /* ============================================================ MAIN INJECTION ============================================================ */ function injectPaysage() { if (!window.LeShedFormesElementaires) { console.warn('LeShedPaysage: formes-elementaires-data.js not loaded'); return Promise.resolve(); } var seed = Math.floor(Math.random() * 100000); seedRng(seed); initPerlin(seed); placed = []; allShapes = []; var pageW = window.innerWidth; // Hauteur basée sur la plus courte colonne (la page ne scrolle pas au-delà) var colLeft = document.querySelector('.col--left'); var colRight = document.querySelector('.col--right'); var layoutEl = document.querySelector('.layout'); var minColH = Math.min( colLeft ? colLeft.offsetHeight : 2000, colRight ? colRight.offsetHeight : 2000 ); var docH = (layoutEl ? layoutEl.offsetTop : 100) + minColH; var scale = Math.max(0.12, pageW / 3000); // Horizon line varies per load var horizonRatio = rngFloat(0.38, 0.55); var horizonY = docH * horizonRatio; // Strate boundaries var SAFE_TOP = 120; var cielTop = SAFE_TOP; var cielBottom = horizonY - 30; var horizonTop = horizonY - 15; var horizonBottom = horizonY + 15; var lointainTop = horizonY + 15; var lointainBottom = horizonY + 55; var moyenTop = horizonY + 55; var moyenBottom = horizonY + 120; var premierTop = horizonY + 120; var premierBottom = docH - 100; // Deux wrappers fixed séparés pour que les z-index s'intercalent // avec le contenu (z-index 5 pour .layout) var layerUnder = document.createElement('div'); layerUnder.className = 'paysage-layer paysage-layer--under'; layerUnder.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100vh;pointer-events:none;overflow:hidden;z-index:-1'; var layerOver = document.createElement('div'); layerOver.className = 'paysage-layer paysage-layer--over'; layerOver.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100vh;pointer-events:none;overflow:hidden;z-index:30'; document.body.appendChild(layerUnder); document.body.appendChild(layerOver); function pickLayer(band) { var underChance; if (band === 'ciel' || band === 'lointain') underChance = 0.7; else if (band === 'premier') underChance = 0.3; else underChance = 0.5; return rng() < underChance ? layerUnder : layerOver; } function speedForBand(band) { if (band === 'ciel') return rngFloat(1, 4); if (band === 'horizon') return rngFloat(0, 3); if (band === 'lointain') return rngFloat(0, 3); if (band === 'moyen') return rngFloat(-2, 2); return rngFloat(-4, -1); } /* ---- CIEL ---- */ var cloudClusters = rngInt(2, 4); for (var i = 0; i < cloudClusters; i++) { var cx = rngFloat(pageW * 0.1, pageW * 0.9); var cy = rngFloat(cielTop, cielBottom * 0.6); placeCluster(REGISTRES.nuages, cx, cy, rngFloat(40, 100) * scale * 10, rngInt(2, 4), scale, [1, 4], pickLayer('ciel'), pageW); } var cielIsolated = rngInt(3, 7); for (var i = 0; i < cielIsolated; i++) { var x = rngFloat(0, pageW); var y = rngFloat(cielTop, cielBottom); tryPlace(rngPick(REGISTRES.ciel_legers), x, y, scale, Math.round(speedForBand('ciel')), pickLayer('ciel')); } /* ---- HORIZON ---- */ var horizonCount = rngInt(1, 3); for (var i = 0; i < horizonCount; i++) { tryPlace(rngPick(REGISTRES.horizon), rngFloat(0, pageW * 0.7), rngFloat(horizonTop, horizonBottom), scale, Math.round(speedForBand('horizon')), pickLayer('horizon')); } /* ---- LOINTAIN ---- */ var perlinScale = 0.02 / scale; for (var i = 0; i < 80; i++) { var x = rngFloat(0, pageW); var y = rngFloat(lointainTop, lointainBottom); if (perlinOctaves(x * perlinScale, y * perlinScale, 2) < 0.4) continue; tryPlace(rngPick(REGISTRES.lointain), x, y, scale, Math.round(speedForBand('lointain')), pickLayer('lointain')); } /* ---- MOYEN PLAN ---- */ for (var i = 0; i < rngInt(3, 6); i++) { placeCluster(REGISTRES.verticales, rngFloat(pageW * 0.05, pageW * 0.95), rngFloat(moyenTop, moyenBottom), rngFloat(12, 22) * scale * 10, rngInt(4, 9), scale, [-2, 2], pickLayer('moyen'), pageW); } for (var i = 0; i < rngInt(2, 5); i++) { placeCluster(REGISTRES.obliques_moyennes, rngFloat(pageW * 0.1, pageW * 0.9), rngFloat(moyenTop, moyenBottom), rngFloat(15, 25) * scale * 10, rngInt(2, 4), scale, [-2, 2], pickLayer('moyen'), pageW); } for (var i = 0; i < rngInt(1, 3); i++) { tryPlace(rngPick(REGISTRES.nuages), rngFloat(0, pageW * 0.8), rngFloat(moyenTop, moyenBottom), scale, Math.round(rngFloat(-1, 2)), pickLayer('moyen')); } /* ---- PREMIER PLAN ---- */ for (var i = 0; i < rngInt(1, 3); i++) { tryPlace(rngPick(REGISTRES.obliques_longues), rngFloat(0, pageW * 0.6), rngFloat(premierTop, premierBottom * 0.8), scale, Math.round(speedForBand('premier')), pickLayer('premier')); } for (var i = 0; i < rngInt(6, 11); i++) { var shapes = rng() > 0.5 ? REGISTRES.verticales : REGISTRES.details_sol; placeCluster(shapes, rngFloat(pageW * 0.02, pageW * 0.98), rngFloat(premierTop, premierBottom * 0.95), rngFloat(10, 20) * scale * 10, rngInt(5, 12), scale, [-4, -1], pickLayer('premier'), pageW); } for (var i = 0; i < rngInt(2, 4); i++) { placeCluster(REGISTRES.masses, rngFloat(pageW * 0.05, pageW * 0.95), rngFloat(premierTop, premierBottom * 0.9), rngFloat(20, 35) * scale * 10, rngInt(2, 4), scale, [-4, -1], pickLayer('premier'), pageW); } for (var i = 0; i < 150; i++) { var x = rngFloat(0, pageW); var y = rngFloat(premierTop, premierBottom * 0.95); if (perlinOctaves(x * perlinScale, y * perlinScale, 2) < 0.35) continue; var allPremier = REGISTRES.verticales.concat(REGISTRES.details_sol, REGISTRES.masses); tryPlace(rngPick(allPremier), x, y, scale, Math.round(speedForBand('premier')), pickLayer('premier')); } // Start manual parallax loop startParallaxLoop(); return Promise.resolve(); } window.LeShedPaysage = { inject: injectPaysage }; })();