first commit
This commit is contained in:
+390
@@ -0,0 +1,390 @@
|
||||
/* ----------------------------------------------------------------
|
||||
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 };
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user