391 lines
15 KiB
JavaScript
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 };
|
|
|
|
})();
|