Files
le-shed_proto_formes/paysage.js
2026-05-18 23:07:31 +02:00

391 lines
15 KiB
JavaScript

/* ----------------------------------------------------------------
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 };
})();