Test axonométrique du 27/09 : pipeline de fabrication et prototype
pipeline/axo : extraction du Rhino, recalage sur la planche (1:640), maisons, arbres (positions réelles des blocs, couleurs de la planche), scène en mètres, sols depuis le PDF Illustrator vectoriel (calques pilotés), bulles de la page 15, planche de l'eau nettoyée. prototypes/axo-test : la page de test et son assembleur. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a9e0143536
commit
731531dbe2
@@ -0,0 +1,21 @@
|
||||
"""Assemble la maquette autonome (une seule page HTML, images intégrées) depuis template.html.
|
||||
|
||||
À lancer dans le dossier où le pipeline a écrit ses sorties (scene.json, eau_crop.json,
|
||||
sol2035.jpg, solauj.jpg, eau_crop.jpg, bulles/). C'est la maquette de test publiée le 27/09 ;
|
||||
la version de production (LOT Q2+) vit dans src/ et ne passe pas par ce fichier.
|
||||
"""
|
||||
import base64, json, os, sys
|
||||
|
||||
ici = os.path.dirname(os.path.abspath(__file__))
|
||||
b64 = lambda f, m: "data:%s;base64,%s" % (m, base64.b64encode(open(f, "rb").read()).decode())
|
||||
t = open(os.path.join(ici, "template.html")).read()
|
||||
bulles = {f[:-4]: b64("bulles/" + f, "image/png") for f in sorted(os.listdir("bulles"))}
|
||||
t = (t.replace("__SCENE__", open("scene.json").read())
|
||||
.replace("__BULLES__", json.dumps(bulles))
|
||||
.replace("__EAUPTS__", open("eau_crop.json").read())
|
||||
.replace("__SOL2035__", b64("sol2035.jpg", "image/jpeg"))
|
||||
.replace("__SOLAUJ__", b64("solauj.jpg", "image/jpeg"))
|
||||
.replace("__EAU__", b64("eau_crop.jpg", "image/jpeg")))
|
||||
sortie = sys.argv[1] if len(sys.argv) > 1 else "quelque-part-en-2035.html"
|
||||
open(sortie, "w").write(t)
|
||||
print(sortie, "%.2f Mo" % (len(t.encode()) / 1e6))
|
||||
@@ -0,0 +1 @@
|
||||
[{"id": "film", "x": 880, "y": 95, "r": 30, "w": [8.62, -79.67]}, {"id": "marche", "x": 388, "y": 238, "r": 38, "w": [-71.38, -56.42]}, {"id": "ballons", "x": 775, "y": 240, "r": 37, "w": [-8.46, -56.1]}, {"id": "barbecue", "x": 856, "y": 238, "r": 37, "w": [4.72, -56.42]}, {"id": "enfant", "x": 937, "y": 240, "r": 37, "w": [17.89, -56.1]}, {"id": "confiture", "x": 1153, "y": 203, "r": 37, "w": [53.01, -62.11]}, {"id": "bouteille", "x": 1233, "y": 196, "r": 37, "w": [66.02, -63.25]}, {"id": "pomme", "x": 1287, "y": 292, "r": 35, "w": [74.8, -47.64]}, {"id": "miel", "x": 1494, "y": 300, "r": 38, "w": [108.46, -46.34]}, {"id": "abeille", "x": 1403, "y": 360, "r": 38, "w": [93.66, -36.59]}, {"id": "fleur", "x": 1310, "y": 418, "r": 38, "w": [78.54, -27.15]}, {"id": "arbre", "x": 1376, "y": 503, "r": 37, "w": [89.27, -13.33]}, {"id": "biere", "x": 157, "y": 383, "r": 38, "w": [-108.94, -32.85]}, {"id": "couverts", "x": 157, "y": 463, "r": 38, "w": [-108.94, -19.84]}, {"id": "outils", "x": 157, "y": 543, "r": 38, "w": [-108.94, -6.83]}, {"id": "musique", "x": 573, "y": 513, "r": 37, "w": [-41.3, -11.71]}, {"id": "oiseaux", "x": 684, "y": 578, "r": 27, "w": [-23.25, -1.14]}, {"id": "promeneurs", "x": 857, "y": 388, "r": 38, "w": [4.88, -32.03]}, {"id": "bols", "x": 1036, "y": 525, "r": 28, "w": [33.98, -9.76]}, {"id": "soin", "x": 963, "y": 782, "r": 28, "w": [22.11, 32.03]}, {"id": "entraide", "x": 1259, "y": 780, "r": 28, "w": [70.24, 31.71]}, {"id": "reemploi", "x": 225, "y": 880, "r": 28, "w": [-97.89, 47.97]}, {"id": "garage", "x": 1233, "y": 992, "r": 28, "w": [66.02, 66.18]}, {"id": "apaise1", "x": 293, "y": 352, "r": 17, "w": [-86.83, -37.89]}, {"id": "apaise2", "x": 857, "y": 860, "r": 17, "w": [4.88, 44.72]}]
|
||||
@@ -0,0 +1 @@
|
||||
{"W": 1615, "H": 860, "pts": {"h1": [410, 660], "h2": [590, 720], "h3": [617, 482], "h4": [820, 530], "h5": [835, 670], "h6": [1110, 570], "h7": [1195, 660], "h8": [1358, 495], "h9": [1300, 650], "h10": [1235, 750], "h11": [1085, 745], "h1b": [610, 170], "h5b": [1005, 242]}, "flux": {"usees": [[572, 692], [620, 742], [680, 795], [740, 818], [810, 820], [865, 805], [920, 765], [980, 720], [1030, 675], [1070, 635]], "pluie": [[565, 618], [610, 620], [670, 628]]}}
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,802 @@
|
||||
<title>Quelque part en 2035</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Barlow:wght@400;500;600;700&family=Barlow+Condensed:wght@500;600&display=swap">
|
||||
<style>
|
||||
/* Une seule ambiance, volontairement : le papier des planches TMIP. Couleurs posées explicitement. */
|
||||
:root {
|
||||
--papier: #f1ece0; --papier-2: #fbf9f3; --encre: #2a2925; --encre-2: #66615a; --trait: #dcd4c3;
|
||||
--eau: #2d86bf; --vivant: #5a9837; --chaleur: #c4850f; --quartier: #d4662a; --neutre: #6c665b;
|
||||
--ombre: 0 10px 30px rgba(42, 41, 37, .16);
|
||||
color-scheme: light;
|
||||
}
|
||||
[hidden] { display: none !important; }
|
||||
html, body { height: 100%; }
|
||||
body { margin: 0; overflow: hidden; background: var(--papier); color: var(--encre);
|
||||
font: 15px/1.45 Barlow, "Segoe UI", system-ui, -apple-system, sans-serif; -webkit-font-smoothing: antialiased; }
|
||||
button { font: inherit; color: inherit; cursor: pointer; }
|
||||
button:focus-visible, [tabindex]:focus-visible { outline: 2px solid var(--eau); outline-offset: 2px; }
|
||||
#scene { position: fixed; inset: 0; width: 100%; height: 100%; display: block; touch-action: none; cursor: grab; }
|
||||
#scene.tire { cursor: grabbing; }
|
||||
#repli { position: fixed; inset: 0; background: var(--papier) center / cover no-repeat; }
|
||||
|
||||
/* ---------- barre du haut ---------- */
|
||||
.barre { position: fixed; z-index: 5; top: calc(env(safe-area-inset-top, 0px) + 10px); left: 12px; right: 12px;
|
||||
display: flex; justify-content: space-between; align-items: flex-start; gap: 8px; flex-wrap: wrap; pointer-events: none; }
|
||||
.barre > * { pointer-events: auto; }
|
||||
.titre { background: rgba(251, 249, 243, .92); border: 1px solid var(--trait); border-radius: 12px; padding: 7px 12px; box-shadow: var(--ombre); }
|
||||
.titre h1 { margin: 0; font: 600 20px/1.1 "Barlow Condensed", Barlow, sans-serif; letter-spacing: .01em; }
|
||||
.titre p { margin: 1px 0 0; font-size: 12.5px; color: var(--encre-2); }
|
||||
.commandes { display: flex; gap: 6px; flex-wrap: wrap; justify-content: flex-end; }
|
||||
.groupe { display: flex; background: rgba(251, 249, 243, .92); border: 1px solid var(--trait); border-radius: 12px; padding: 3px; box-shadow: var(--ombre); }
|
||||
.groupe button { border: 0; background: transparent; border-radius: 9px; padding: 0 11px; min-height: 38px; min-width: 40px; font-weight: 500; font-size: 14px; }
|
||||
.groupe button[aria-pressed="true"] { background: var(--encre); color: var(--papier-2); }
|
||||
.groupe svg { width: 20px; height: 20px; display: block; margin: auto; }
|
||||
@media (max-width: 520px) { .titre p { display: none; } .titre h1 { font-size: 18px; } }
|
||||
|
||||
/* ---------- fiches ---------- */
|
||||
#fiches { position: fixed; z-index: 6; left: 0; right: 0; bottom: 0; background: var(--papier-2); border-top: 1px solid var(--trait);
|
||||
border-radius: 16px 16px 0 0; box-shadow: 0 -8px 28px rgba(42, 41, 37, .14); padding: 10px 14px calc(env(safe-area-inset-bottom, 0px) + 10px);
|
||||
display: flex; flex-direction: column; gap: 8px; max-height: 50%; }
|
||||
@media (min-width: 900px) { #fiches { left: 16px; right: auto; bottom: 16px; width: 430px; border: 1px solid var(--trait); border-radius: 16px; max-height: calc(100% - 110px); } }
|
||||
.onglets { display: flex; gap: 6px; overflow-x: auto; scrollbar-width: none; flex: none; }
|
||||
.onglets::-webkit-scrollbar { display: none; }
|
||||
.onglets button { flex: none; display: flex; align-items: center; gap: 6px; border: 1px solid var(--trait); background: #fff; border-radius: 999px; padding: 5px 11px; min-height: 34px; font-size: 13.5px; font-weight: 500; }
|
||||
.onglets button i { width: 9px; height: 9px; border-radius: 50%; background: var(--c, var(--neutre)); }
|
||||
.onglets button[aria-pressed="true"] { background: var(--c, var(--encre)); border-color: var(--c, var(--encre)); color: #fff; }
|
||||
.onglets button[aria-pressed="true"] i { background: #fff; }
|
||||
.fiche { display: grid; grid-template-columns: 56px 1fr; gap: 12px; overflow-y: auto; min-height: 0; }
|
||||
.fiche .icone { width: 56px; height: 56px; border-radius: 50%; display: block; background: #fff; box-shadow: 0 2px 8px rgba(0,0,0,.08); }
|
||||
.fiche .icone svg, .fiche .icone img { width: 56px; height: 56px; display: block; border-radius: 50%; }
|
||||
.couche { font: 600 12.5px/1.2 "Barlow Condensed", Barlow, sans-serif; text-transform: uppercase; letter-spacing: .07em; color: var(--c, var(--neutre)); }
|
||||
.fiche h2 { margin: 2px 0 5px; font-size: 19px; line-height: 1.22; font-weight: 600; text-wrap: balance; }
|
||||
.fiche p { margin: 0 0 8px; }
|
||||
.chiffres { display: flex; flex-wrap: wrap; gap: 8px 16px; margin: 2px 0 6px; }
|
||||
.chiffres div { font-variant-numeric: tabular-nums; }
|
||||
.chiffres b { display: block; font-size: 21px; font-weight: 700; line-height: 1.1; color: var(--c, var(--encre)); }
|
||||
.chiffres small { display: block; font-size: 12.5px; line-height: 1.3; color: var(--encre-2); max-width: 22ch; }
|
||||
.source { font-size: 11.5px; color: var(--encre-2); }
|
||||
.action { margin-top: 8px; border: 0; background: var(--eau); color: #fff; font-weight: 600; border-radius: 10px; padding: 10px 14px; min-height: 42px; }
|
||||
.pied { display: flex; align-items: center; gap: 8px; flex: none; }
|
||||
.barre-temps { flex: 1; height: 3px; background: var(--trait); border-radius: 2px; overflow: hidden; }
|
||||
.barre-temps span { display: block; height: 100%; width: 0; background: var(--c, var(--encre)); }
|
||||
.pied button { border: 1px solid var(--trait); background: #fff; border-radius: 10px; width: 40px; height: 40px; display: grid; place-items: center; }
|
||||
.pied svg { width: 18px; height: 18px; }
|
||||
.compteur { font-size: 12.5px; color: var(--encre-2); font-variant-numeric: tabular-nums; min-width: 42px; text-align: right; }
|
||||
.maquette { font-size: 11px; color: var(--encre-2); }
|
||||
|
||||
/* ---------- le cycle de l'eau (planche 13) ---------- */
|
||||
#cycle { position: fixed; inset: 0; z-index: 20; background: var(--papier); display: grid; grid-template-rows: auto 1fr auto; }
|
||||
#cycle header { display: flex; align-items: center; gap: 10px; padding: calc(env(safe-area-inset-top, 0px) + 10px) 14px 8px; }
|
||||
#cycle header button { border: 1px solid var(--trait); background: #fff; border-radius: 10px; min-height: 40px; padding: 0 12px; font-weight: 500; }
|
||||
#cycle header h2 { margin: 0; font: 600 20px/1.1 "Barlow Condensed", Barlow, sans-serif; }
|
||||
#cycle header p { margin: 0; font-size: 12.5px; color: var(--encre-2); }
|
||||
.vue-planche { position: relative; overflow: hidden; touch-action: none; cursor: grab; }
|
||||
.plateau { position: absolute; left: 0; top: 0; width: 1615px; height: 860px; transform-origin: 0 0; will-change: transform; }
|
||||
.plateau img { position: absolute; inset: 0; width: 100%; height: 100%; user-select: none; -webkit-user-drag: none; pointer-events: none; }
|
||||
.plateau svg { position: absolute; inset: 0; width: 100%; height: 100%; overflow: visible; }
|
||||
.flux { fill: none; stroke: var(--eau); stroke-width: 7; stroke-linecap: round; stroke-dasharray: 2 16; opacity: .25; transition: opacity .5s; }
|
||||
.flux.actif { opacity: .95; animation: couler 1.1s linear infinite; }
|
||||
@keyframes couler { to { stroke-dashoffset: -18; } }
|
||||
.pe { cursor: pointer; }
|
||||
.pe .halo { fill: var(--eau); opacity: 0; }
|
||||
.pe .coeur { fill: #fff; stroke: var(--eau); stroke-width: 3; }
|
||||
.pe text { font: 700 15px Barlow, sans-serif; fill: var(--eau); text-anchor: middle; dominant-baseline: central; pointer-events: none; }
|
||||
.pe.actif .coeur { fill: var(--eau); }
|
||||
.pe.actif text { fill: #fff; }
|
||||
.pe.actif .halo { animation: halo 1.6s ease-out infinite; }
|
||||
@keyframes halo { 0% { opacity: .45; r: 16; } 100% { opacity: 0; r: 46; } }
|
||||
#cycle .bas { background: var(--papier-2); border-top: 1px solid var(--trait); padding: 10px 14px calc(env(safe-area-inset-bottom, 0px) + 10px); display: flex; flex-direction: column; gap: 8px; }
|
||||
@media (min-width: 900px) { #cycle .bas { position: absolute; left: 16px; bottom: 16px; width: 430px; border: 1px solid var(--trait); border-radius: 16px; box-shadow: var(--ombre); } #cycle { grid-template-rows: auto 1fr; } }
|
||||
#cycle .etape h3 { margin: 2px 0 5px; font-size: 18px; font-weight: 600; }
|
||||
#cycle .etape p { margin: 0 0 6px; }
|
||||
@media (prefers-reduced-motion: reduce) { .flux.actif, .pe.actif .halo { animation: none; } .pe.actif .halo { opacity: .25; } }
|
||||
</style>
|
||||
|
||||
<canvas id="scene" aria-label="Maquette en trois dimensions du quartier type TMIP en 2035"></canvas>
|
||||
<div id="repli" hidden></div>
|
||||
|
||||
<div class="barre">
|
||||
<div class="titre">
|
||||
<h1>Quelque part en 2035</h1>
|
||||
<p>un quartier pavillonnaire, un jour de marché</p>
|
||||
</div>
|
||||
<div class="commandes">
|
||||
<div class="groupe" role="group" aria-label="Époque">
|
||||
<button type="button" id="b-auj" aria-pressed="false">Aujourd'hui</button>
|
||||
<button type="button" id="b-2035" aria-pressed="true">2035</button>
|
||||
</div>
|
||||
<div class="groupe" role="group" aria-label="Vue">
|
||||
<button type="button" id="b-axo" aria-pressed="true">Axo</button>
|
||||
<button type="button" id="b-plan" aria-pressed="false">Plan</button>
|
||||
</div>
|
||||
<div class="groupe">
|
||||
<button type="button" id="b-tourner" aria-label="Tourner d'un quart de tour"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 11a8 8 0 1 0-2.3 5.7"/><path d="M20 4v7h-7"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="fiches" aria-label="Fiches du quartier">
|
||||
<div class="onglets" id="onglets"></div>
|
||||
<article class="fiche" id="fiche" aria-live="polite"></article>
|
||||
<div class="pied">
|
||||
<div class="barre-temps" aria-hidden="true"><span id="temps"></span></div>
|
||||
<span class="compteur" id="compteur"></span>
|
||||
<button type="button" id="b-prec" aria-label="Fiche précédente"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg></button>
|
||||
<button type="button" id="b-lecture" aria-label="Pause"><svg viewBox="0 0 24 24" fill="currentColor" id="i-lecture"><rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/></svg></button>
|
||||
<button type="button" id="b-suiv" aria-label="Fiche suivante"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg></button>
|
||||
</div>
|
||||
<div class="maquette">Maquette de test du 27/09 · plan et dessins : Jules Nény, concours Habiter Demain 2020 et TMIP 2024</div>
|
||||
</section>
|
||||
|
||||
<section id="cycle" hidden aria-label="Le cycle de l'eau">
|
||||
<header>
|
||||
<button type="button" id="b-retour">← Retour au quartier</button>
|
||||
<div><h2>Le cycle de l'eau</h2><p>de la gouttière à la parcelle, en onze étapes</p></div>
|
||||
</header>
|
||||
<div class="vue-planche" id="vue-planche">
|
||||
<div class="plateau" id="plateau">
|
||||
<img id="img-eau" src="__EAU__" alt="Planche du réseau hydrique alternatif : récupération, assainissement, maîtrise hydraulique, phytoépuration, production de biogaz. Dessins de Jules Nény.">
|
||||
<svg viewBox="0 0 1615 860" id="svg-eau"></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div class="bas">
|
||||
<article class="etape" id="etape" aria-live="polite"></article>
|
||||
<div class="pied">
|
||||
<div class="barre-temps" aria-hidden="true"><span id="temps-eau" style="background: var(--eau)"></span></div>
|
||||
<span class="compteur" id="compteur-eau"></span>
|
||||
<button type="button" id="e-prec" aria-label="Étape précédente"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 5l-7 7 7 7"/></svg></button>
|
||||
<button type="button" id="e-suiv" aria-label="Étape suivante"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 5l7 7-7 7"/></svg></button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script src="https://cdn.jsdelivr.net/npm/three@0.158.0/build/three.min.js"></script>
|
||||
<script>
|
||||
(() => {
|
||||
'use strict';
|
||||
const D = __SCENE__;
|
||||
const BULLES = __BULLES__;
|
||||
const EAU = __EAUPTS__;
|
||||
const IMG = { sol2035: '__SOL2035__', solauj: '__SOLAUJ__' };
|
||||
const reduit = matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const COUL = { eau: '#2d86bf', vivant: '#5a9837', chaleur: '#c4850f', quartier: '#d4662a' };
|
||||
const NOM = { eau: "L'eau", vivant: 'Le vivant nourricier', chaleur: "L'enveloppe et la chaleur", quartier: 'La vie de quartier' };
|
||||
// coordonnées : pixels du plan à 100 dpi -> mètres monde (x vers l'est, z vers le sud)
|
||||
const P = (x, y) => ({ x: +((x - D.W / 2) / D.s).toFixed(1), z: +((y - D.H / 2) / D.s).toFixed(1) });
|
||||
|
||||
// ---------------------------------------------------------------- icônes des fiches (même style que les bulles)
|
||||
const svgIcone = (coul, corps) => `<svg viewBox="0 0 56 56" role="img" aria-hidden="true"><circle cx="28" cy="28" r="25" fill="#fff" stroke="${coul}" stroke-width="2.5"/>${corps}</svg>`;
|
||||
const ICONES = {
|
||||
toit: svgIcone(COUL.eau, '<path d="M14 27l12-10 12 10" fill="none" stroke="#2d86bf" stroke-width="2.6" stroke-linejoin="round"/><rect x="33" y="31" width="9" height="12" rx="2" fill="#9fd0ec" stroke="#2d86bf" stroke-width="2"/><path d="M38 22c2 3 3 4.5 3 6a3 3 0 0 1-6 0c0-1.5 1-3 3-6z" fill="#2d86bf"/><path d="M17 30v12h12V30" fill="none" stroke="#2d86bf" stroke-width="2.2"/>'),
|
||||
rigole: svgIcone(COUL.eau, '<path d="M13 22h9l6 6h8l7 7" fill="none" stroke="#2d86bf" stroke-width="2.6" stroke-dasharray="3 3" stroke-linecap="round"/><path d="M39 31l4 4-5 1" fill="none" stroke="#2d86bf" stroke-width="2.6" stroke-linecap="round" stroke-linejoin="round"/><path d="M18 36c2 3 3 4.5 3 6a3 3 0 0 1-6 0c0-1.5 1-3 3-6z" fill="#2d86bf"/>'),
|
||||
noue: svgIcone(COUL.eau, '<path d="M11 34c4 5 9 7 17 7s13-2 17-7" fill="#bfe0f2" stroke="#2d86bf" stroke-width="2.4"/><path d="M20 33v-9M24 33v-12M32 33v-10M36 33v-7" stroke="#5a9837" stroke-width="2.4" stroke-linecap="round"/>'),
|
||||
bassin: svgIcone(COUL.eau, '<path d="M11 30h34" stroke="#8a847a" stroke-width="2"/><path d="M13 31c3 8 8 11 15 11s12-3 15-11z" fill="#bfe0f2" stroke="#2d86bf" stroke-width="2.4"/><path d="M19 36c3-2 6 2 9 0s6-2 9 0" fill="none" stroke="#2d86bf" stroke-width="2"/><path d="M34 29v-9M38 29v-6" stroke="#5a9837" stroke-width="2.4" stroke-linecap="round"/>'),
|
||||
paille: svgIcone(COUL.chaleur, '<path d="M14 28l14-12 14 12" fill="none" stroke="#c4850f" stroke-width="2.6" stroke-linejoin="round"/><rect x="17" y="27" width="22" height="15" fill="#f1cf6b" stroke="#c4850f" stroke-width="2.2"/><path d="M21 31h6M29 35h6M21 39h6" stroke="#c4850f" stroke-width="1.6"/><path d="M34 14v7" stroke="#8a847a" stroke-width="3"/>'),
|
||||
quartier: svgIcone(COUL.vivant, '<path d="M9 36l7-6 7 6v7H9zM24 33l8-7 8 7v10H24zM37 37l5-4 5 4v6H37z" fill="#fff" stroke="#6c665b" stroke-width="2" stroke-linejoin="round"/><circle cx="17" cy="21" r="4" fill="#9dc07a"/><circle cx="41" cy="24" r="3.5" fill="#f0a860"/>'),
|
||||
liens: svgIcone(COUL.quartier, '<path d="M28 12l-8 31M28 12l8 31M22 30h12M24 22h8" fill="none" stroke="#6c665b" stroke-width="2.4" stroke-linecap="round"/><path d="M10 44c6-4 12-4 18 0s12 4 18 0" fill="none" stroke="#d4662a" stroke-width="2.2" stroke-dasharray="3 3"/>'),
|
||||
route: svgIcone(COUL.quartier, '<rect x="13" y="26" width="30" height="10" rx="4" fill="#fff" stroke="#6c665b" stroke-width="2.4"/><path d="M18 26l4-6h12l4 6" fill="none" stroke="#6c665b" stroke-width="2.4" stroke-linejoin="round"/><circle cx="20" cy="37" r="3" fill="#6c665b"/><circle cx="36" cy="37" r="3" fill="#6c665b"/>'),
|
||||
};
|
||||
const icone = (k) => ICONES[k] ? ICONES[k] : (BULLES[k] ? `<img src="${BULLES[k]}" alt="">` : ICONES.quartier);
|
||||
|
||||
// ---------------------------------------------------------------- les fiches
|
||||
const C2035 = [
|
||||
{ id: 'intro', ic: 'quartier', t: 'Quelque part en 2035', p: "Un quartier pavillonnaire ordinaire, vingt ans après. Les maisons sont isolées, les jardins nourrissent, l'eau reste sur place et la place a retrouvé un marché. Chaque couleur suit un système.", ch: [], s: 'TMIP 2024, p. 15 et 20', cam: { ...P(827, 560), w: 210 } },
|
||||
{ id: 'toits', c: 'eau', ic: 'toit', t: 'Les toits récoltent la pluie', p: "Chaque maison transformée capte l'eau de son toit : gouttière, filtre, cuve. Cette eau suffit pour les WC, le lave-linge et le jardin.", ch: [['+80 %', "d'eau de pluie récupérée par maison"], ['98 %', "des usages n'ont pas besoin d'eau potable"]], s: 'TMIP 2024, p. 10-11', cam: { ...P(898, 712), w: 62 }, fx: 'pluie' },
|
||||
{ id: 'rigoles', c: 'eau', ic: 'rigole', t: 'De petites rigoles vers les noues', p: "L'eau grise de la maison et le trop-plein des cuves partent à l'air libre, par une rigole, vers la noue plantée au fond du jardin.", ch: [['82 %', 'des rejets de la maison sont des eaux grises']], s: 'TMIP 2024, p. 13', cam: { ...P(1330, 790), w: 95 }, fx: 'liaisons' },
|
||||
{ id: 'noues', c: 'eau', ic: 'noue', t: 'Les noues épurent et arrosent', p: "Une noue est un fossé planté. Les racines filtrent l'eau, la ralentissent et arrosent les haies fruitières qui la bordent.", ch: [['−80 %', "d'eau rejetée à l'égout"], ['−250 €', 'par an et par maison']], s: 'TMIP 2024, p. 11 et 13', cam: { ...P(560, 400), w: 150 }, fx: 'noues' },
|
||||
{ id: 'mare', c: 'eau', ic: 'oiseaux', t: "L'eau se rassemble dans la mare", p: "Les noues descendent en pente douce jusqu'à la mare de la place. Elle rafraîchit l'été, accueille oiseaux et libellules, et retient l'orage.", ch: [], s: 'TMIP 2024, p. 13 et 15', cam: { ...P(700, 450), w: 64 }, fx: 'mare' },
|
||||
{ id: 'bassins', c: 'eau', ic: 'bassin', t: 'Des bassins à ciel ouvert', p: "Le long de la route, un bassin planté recueille les grosses pluies. Il fait le travail d'un bassin d'orage, et il épure, rafraîchit, nourrit la vie.", ch: [], s: 'TMIP 2024, p. 13 §4-5', cam: { ...P(720, 760), w: 82 }, fx: 'bassins', action: "Entrer dans le cycle de l'eau" },
|
||||
{ id: 'haies', c: 'vivant', ic: 'pomme', t: 'Les limites deviennent des haies fruitières', p: "Sur trois côtés du jardin, une haie de pommiers, poiriers et petits fruits. Après cinq ans, elle produit plus que la famille ne consomme.", ch: [['+12', 'personnes nourries en fruits, par maison'], ['85 m', "de haie autour d'un jardin de 750 m²"]], s: 'TMIP 2024, p. 11', cam: { ...P(270, 760), w: 95 }, fx: 'haies' },
|
||||
{ id: 'sentiers', c: 'vivant', ic: 'promeneurs', t: "Le sentier d'entretien s'ouvre aux voisins", p: "La loi impose deux mètres entre un arbre et la limite voisine. Cette bande devient un sentier, ouvert par un droit d'usage : on y passe, on y cueille.", ch: [['2 m', 'de recul obligatoire, devenus un chemin']], s: 'TMIP 2024, p. 14', cam: { ...P(1330, 800), w: 110 }, fx: 'sentiers' },
|
||||
{ id: 'foret', c: 'vivant', ic: 'abeille', t: 'La forêt nourricière', p: "À l'est, une forêt de fruitiers, de mellifères et d'essences qui dépolluent l'air et le sol. Des ruches, du miel, un chemin qui la traverse.", ch: [], s: 'TMIP 2024, p. 14-15', cam: { ...P(1470, 330), w: 120 }, fx: 'foret' },
|
||||
{ id: 'maison', c: 'chaleur', ic: 'paille', t: 'La maison se couvre de paille', p: "Isolation par l'extérieur en bottes de paille et enduit terre, une serre au sud, un poêle à bois d'appoint. La facture de chauffage fond.", ch: [['−85 %', "de consommation d'énergie"], ['1 000 à 5 000 €', "d'économies par an"]], s: 'TMIP 2024, p. 11-12', cam: { ...P(1180, 712), w: 58 }, fx: 'maisons' },
|
||||
{ id: 'chantier', c: 'chaleur', ic: 'outils', t: 'On isole ensemble', p: "Un chantier-école par maison : les voisins apprennent en faisant. En autoconstruction accompagnée, l'isolation coûte deux à trois fois moins cher.", ch: [['11 700 à 23 400 €', 'en autoconstruction accompagnée'], ['39 900 à 56 100 €', 'par une entreprise']], s: 'TMIP 2024, p. 16 et 28', cam: { ...P(760, 100), w: 58 }, fx: 'maisons' },
|
||||
{ id: 'marche', c: 'quartier', ic: 'marche', t: 'Le parking devient le marché', p: "Sur la place, les emplacements de stationnement accueillent les étals : fruits des haies, conserves, pain, miel.", ch: [], s: 'TMIP 2024, p. 15 et 20', cam: { ...P(505, 300), w: 56 }, fx: 'marche' },
|
||||
{ id: 'communs', c: 'quartier', ic: 'biere', t: 'Le grand bâtiment devient un lieu commun', p: "Brasserie, cantine, atelier de réparation, potagers devant la porte. Les activités qu'on allait chercher en zone commerciale reviennent dans le quartier.", ch: [], s: 'TMIP 2024, p. 14-15', cam: { ...P(330, 540), w: 62 }, fx: 'communs' },
|
||||
{ id: 'mobilite', c: 'quartier', ic: 'enfant', t: 'On se déplace à pied et à vélo', p: "Les sentiers relient les jardins, la forêt et la place. La route se calme, les enfants jouent dehors.", ch: [['80 km', "de voiture par jour et par habitant, aujourd'hui"]], s: 'concours Habiter Demain 2020, planche 1', cam: { ...P(720, 520), w: 130 }, fx: 'mobilite' },
|
||||
{ id: 'travail', c: 'quartier', ic: 'route', t: 'Ce qui ne change pas : le travail', p: "La voiture du matin vers le bassin d'emploi reste. TMIP ne prétend pas y répondre ; le montrer rend le reste crédible.", ch: [], s: 'proposition de la maquette, à valider', cam: { ...P(230, 300), w: 340 }, fx: 'travail' },
|
||||
{ id: 'horschamp', ic: 'liens', t: "Ce dont le quartier s'est libéré", p: "Autour, les grandes infrastructures dont il dépendait : centrale et gaz, supermarché, station d'épuration, gravière, agriculture lointaine. Leurs liens se sont détendus. Celui du travail reste tendu.", ch: [['850 km', 'parcourus par le contenu des rayons'], ['3 %', "des fruits mangés en Île-de-France y poussent"]], s: 'TMIP 2024, p. 10', cam: { x: -20, z: 0, w: 900 }, fx: 'flux' },
|
||||
];
|
||||
const CAUJ = [
|
||||
{ id: 'auj1', ic: 'quartier', t: "Aujourd'hui : un quartier ordinaire", p: "Pelouses, haies de thuyas, une voiture par adulte. La pluie part à l'égout, la chaleur vient du gaz, les courses du supermarché.", ch: [['23 %', "du bâti français est de l'habitat individuel périurbain"], ['750 m²', 'de jardin en moyenne']], s: 'TMIP 2024, p. 7', cam: { ...P(827, 560), w: 210 } },
|
||||
{ id: 'auj2', ic: 'liens', t: 'Tout vient de loin', p: "Eau, énergie, nourriture, travail : chaque besoin tire un fil vers une grande infrastructure. Le ménage paie tous les fils.", ch: [['80 km', 'de voiture par jour et par habitant'], ['1/3', 'des revenus au crédit, sur 30 ans']], s: 'concours Habiter Demain 2020, planches 1 et 2', cam: { x: -20, z: 0, w: 900 }, fx: 'flux' },
|
||||
{ id: 'auj3', ic: 'route', t: 'Sur la place, un parking', p: "La place sert à garer les voitures. Pour croiser ses voisins, il reste le supermarché.", ch: [], s: 'plan du quartier actuel, concours 2020', cam: { ...P(505, 300), w: 56 }, fx: 'parking' },
|
||||
];
|
||||
// bulles de la planche 2030 : fiche liée, ou une ligne quand il n'y en a pas
|
||||
const SIGNES = {
|
||||
marche: ['marche'], pomme: ['haies'], abeille: ['foret'], miel: ['foret'], fleur: ['foret', 'Des plantes mellifères et médicinales en lisière.'], arbre: ['foret'],
|
||||
biere: ['communs'], couverts: ['communs'], outils: ['communs'], oiseaux: ['mare'], promeneurs: ['sentiers'], enfant: ['mobilite'],
|
||||
film: [null, 'Un ciné-club dans un garage ouvert.', 'quartier'], ballons: [null, 'Les délaissés deviennent terrains de jeu.', 'quartier'],
|
||||
barbecue: [null, 'Un barbecue partagé au bord de la place.', 'quartier'], confiture: [null, 'Une conserverie : le surplus des haies en bocaux.', 'quartier'],
|
||||
bouteille: [null, 'Jus et cidre des vergers du quartier.', 'quartier'], musique: [null, 'Un bal sur la place, au bord de la mare.', 'quartier'],
|
||||
bols: [null, 'Un atelier de céramique, celui dont rêvait Sophia en 2020.', 'quartier'], soin: [null, 'Un cabinet de soin, une herboristerie.', 'quartier'],
|
||||
entraide: [null, "Garde d'enfants partagée, entraide entre voisins.", 'quartier'], reemploi: [null, 'Une recyclerie et un compost de quartier.', 'quartier'],
|
||||
garage: [null, 'Un garage associatif de réparation.', 'quartier'], apaise1: ['mobilite'], apaise2: ['mobilite'],
|
||||
};
|
||||
const VERT = new Set(['pomme', 'abeille', 'fleur', 'arbre', 'oiseaux']);
|
||||
|
||||
// ---------------------------------------------------------------- moteur des fiches
|
||||
const E = { epoque: '2035', filtre: 'tout', i: 0, lecture: !reduit, t0: performance.now(), pause: 0, mini: null };
|
||||
const DUREE = 10000;
|
||||
const liste = () => (E.epoque === 'auj' ? CAUJ : C2035).filter((c) => E.filtre === 'tout' || c.c === E.filtre);
|
||||
function onglets() {
|
||||
const o = $('onglets');
|
||||
if (E.epoque === 'auj') { o.innerHTML = ''; o.hidden = true; return; }
|
||||
o.hidden = false;
|
||||
o.innerHTML = [['tout', 'Tout'], ['eau', 'Eau'], ['vivant', 'Vivant'], ['chaleur', 'Chaleur'], ['quartier', 'Quartier']]
|
||||
.map(([k, n]) => `<button type="button" data-f="${k}" aria-pressed="${E.filtre === k}" style="${COUL[k] ? '--c:' + COUL[k] : ''}"><i></i>${n}</button>`).join('');
|
||||
o.querySelectorAll('button').forEach((b) => b.addEventListener('click', () => { E.filtre = b.dataset.f; E.i = 0; onglets(); montrer(); }));
|
||||
}
|
||||
function montrer(voler = true) {
|
||||
const L = liste(); if (!L.length) return;
|
||||
E.i = (E.i + L.length) % L.length;
|
||||
const c = E.mini || L[E.i];
|
||||
const f = $('fiches'); f.style.setProperty('--c', c.c ? COUL[c.c] : 'var(--encre)');
|
||||
$('fiche').innerHTML = `<div class="icone">${icone(c.ic)}</div><div>
|
||||
<div class="couche">${c.c ? NOM[c.c] : (E.epoque === 'auj' ? "Aujourd'hui" : '2035')}</div>
|
||||
<h2>${c.t}</h2><p>${c.p}</p>
|
||||
${c.ch && c.ch.length ? `<div class="chiffres">${c.ch.map(([v, l]) => `<div><b>${v}</b><small>${l}</small></div>`).join('')}</div>` : ''}
|
||||
<div class="source">${c.ch && c.ch.length ? 'Chiffres pour une maison · ' : ''}source : ${c.s}</div>
|
||||
${c.action ? `<button type="button" class="action" id="b-cycle">${c.action} →</button>` : ''}</div>`;
|
||||
const b = $('b-cycle'); if (b) b.addEventListener('click', ouvrirCycle);
|
||||
$('compteur').textContent = E.mini ? '' : `${E.i + 1} / ${L.length}`;
|
||||
E.t0 = performance.now();
|
||||
if (T3) T3.activer(c, voler);
|
||||
}
|
||||
function aller(d) { E.mini = null; E.i += d; montrer(); }
|
||||
function suspendre(ms = 25000) { E.pause = performance.now() + ms; }
|
||||
$('b-prec').addEventListener('click', () => { suspendre(); aller(-1); });
|
||||
$('b-suiv').addEventListener('click', () => { suspendre(); aller(1); });
|
||||
$('b-lecture').addEventListener('click', () => { E.lecture = !E.lecture; E.pause = 0; E.t0 = performance.now(); majLecture(); });
|
||||
function majLecture() {
|
||||
$('b-lecture').setAttribute('aria-label', E.lecture ? 'Pause' : 'Lecture');
|
||||
$('i-lecture').innerHTML = E.lecture ? '<rect x="6" y="5" width="4" height="14" rx="1"/><rect x="14" y="5" width="4" height="14" rx="1"/>' : '<path d="M7 5l12 7-12 7z"/>';
|
||||
}
|
||||
function tic(now) {
|
||||
if (!$('cycle').hidden) return;
|
||||
const actif = E.lecture && now > E.pause && !E.mini;
|
||||
if (!actif) { if (now <= E.pause) E.t0 = now - (DUREE * (parseFloat($('temps').style.width) || 0)) / 100; return; }
|
||||
const k = (now - E.t0) / DUREE;
|
||||
$('temps').style.width = Math.min(100, k * 100) + '%';
|
||||
if (k >= 1) aller(1);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- la scène 3D
|
||||
let T3 = null;
|
||||
try { T3 = window.THREE ? scene3D() : null; } catch (e) { console.error(e); T3 = null; }
|
||||
if (!T3) { $('scene').hidden = true; const r = $('repli'); r.hidden = false; r.style.backgroundImage = `url(${IMG.sol2035})`; }
|
||||
|
||||
function scene3D() {
|
||||
const canvas = $('scene');
|
||||
const renderer = new THREE.WebGLRenderer({ canvas, antialias: true, powerPreference: 'high-performance' });
|
||||
if (!renderer.getContext()) return null;
|
||||
renderer.setPixelRatio(Math.min(devicePixelRatio || 1, 2));
|
||||
renderer.outputColorSpace = THREE.SRGBColorSpace;
|
||||
renderer.useLegacyLights = true;
|
||||
renderer.shadowMap.enabled = true; renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
const scene = new THREE.Scene();
|
||||
scene.background = new THREE.Color('#ece6d8');
|
||||
const cam = new THREE.OrthographicCamera(-1, 1, 1, -1, 1, 4000);
|
||||
const sRGB = (h) => new THREE.Color(h);
|
||||
const rnd = (i) => { const x = Math.sin(i * 12.9898 + 78.233) * 43758.5453; return x - Math.floor(x); };
|
||||
|
||||
// lumière : ciel chaud + soleil bas du sud-ouest
|
||||
scene.add(new THREE.HemisphereLight(0xfffbf2, 0xd8d0bd, 0.78));
|
||||
const soleil = new THREE.DirectionalLight(0xfff0d8, 0.36);
|
||||
soleil.position.set(-110, 170, 120); soleil.target.position.set(0, 0, 0);
|
||||
soleil.castShadow = true; soleil.shadow.mapSize.set(2048, 2048);
|
||||
Object.assign(soleil.shadow.camera, { left: -170, right: 170, top: 130, bottom: -130, near: 20, far: 520 });
|
||||
soleil.shadow.bias = -0.0006; soleil.shadow.normalBias = 0.35;
|
||||
scene.add(soleil, soleil.target);
|
||||
|
||||
// sol : le plan vectoriel, en deux états
|
||||
const chargeur = new THREE.TextureLoader();
|
||||
const tex = (url) => { const t = chargeur.load(url, () => (besoin = true)); t.colorSpace = THREE.SRGBColorSpace; t.anisotropy = renderer.capabilities.getMaxAnisotropy(); return t; };
|
||||
const papier = new THREE.Mesh(new THREE.PlaneGeometry(5000, 5000).rotateX(-Math.PI / 2), new THREE.MeshLambertMaterial({ color: '#e6dfcf' }));
|
||||
papier.position.y = -2.3; papier.receiveShadow = true; scene.add(papier);
|
||||
// socle de maquette : le plan posé sur une épaisseur
|
||||
const socle = new THREE.Mesh(new THREE.BoxGeometry(D.Wm, 2.2, D.Hm), new THREE.MeshLambertMaterial({ color: '#d9cfba' }));
|
||||
socle.position.y = -1.13; socle.receiveShadow = true; scene.add(socle);
|
||||
const geoSol = new THREE.PlaneGeometry(D.Wm, D.Hm).rotateX(-Math.PI / 2);
|
||||
const sol35 = new THREE.Mesh(geoSol, new THREE.MeshLambertMaterial({ map: tex(IMG.sol2035) }));
|
||||
sol35.receiveShadow = true; scene.add(sol35);
|
||||
const matAuj = new THREE.MeshLambertMaterial({ map: tex(IMG.solauj), transparent: true, opacity: 0, depthWrite: false, polygonOffset: true, polygonOffsetFactor: -2, polygonOffsetUnits: -2 });
|
||||
const solAuj = new THREE.Mesh(geoSol, matAuj); solAuj.receiveShadow = true; solAuj.position.y = 0.02; scene.add(solAuj);
|
||||
|
||||
const G = { v2035: new THREE.Group(), vAuj: new THREE.Group() };
|
||||
scene.add(G.v2035, G.vAuj);
|
||||
const surv = { maisons: [], haies: null, foret: null, etals: [], grand: null, noues: [], bassins: [], liaisons: [], sentiers: [], chemins: [], flux: {}, bulles: [], pluie: [], ondes: [], fumee: [] };
|
||||
const cliquables = [];
|
||||
|
||||
// ---------- maisons
|
||||
const mat = {
|
||||
mur: new THREE.MeshLambertMaterial({ color: '#f8f5ee' }), toit: new THREE.MeshLambertMaterial({ color: '#cfc8ba', side: THREE.DoubleSide }),
|
||||
paille: new THREE.MeshLambertMaterial({ color: '#efc85a', emissive: '#000000' }), toitT: new THREE.MeshLambertMaterial({ color: '#c9bfab', side: THREE.DoubleSide }),
|
||||
verre: new THREE.MeshLambertMaterial({ color: '#cde9e5', transparent: true, opacity: 0.55, depthWrite: false }), bord: new THREE.LineBasicMaterial({ color: '#86ada7' }),
|
||||
chem: new THREE.MeshLambertMaterial({ color: '#a19a8e' }), cuve: new THREE.MeshLambertMaterial({ color: '#6fb0d8' }),
|
||||
grandMur: new THREE.MeshLambertMaterial({ color: '#f6f1e7' }), grandToit: new THREE.MeshLambertMaterial({ color: '#d7a07e', side: THREE.DoubleSide, emissive: '#000000' }),
|
||||
};
|
||||
function toit(Lu, Lv, eave, rh, o) {
|
||||
const hu = Lu / 2 + o, hv = Lv / 2 + o, ry = eave + rh, yb = eave - rh * (o / (Lv / 2));
|
||||
const g = new THREE.BufferGeometry();
|
||||
g.setAttribute('position', new THREE.Float32BufferAttribute([
|
||||
-hu, yb, -hv, hu, yb, -hv, hu, ry, 0, -hu, yb, -hv, hu, ry, 0, -hu, ry, 0,
|
||||
-hu, yb, hv, -hu, ry, 0, hu, ry, 0, -hu, yb, hv, hu, ry, 0, hu, yb, hv], 3));
|
||||
g.computeVertexNormals(); return g;
|
||||
}
|
||||
function pignons(Lu, Lv, eave, rh) {
|
||||
const g = new THREE.BufferGeometry(), x = Lu / 2, v = Lv / 2, ry = eave + rh;
|
||||
g.setAttribute('position', new THREE.Float32BufferAttribute([-x, eave, -v, -x, eave, v, -x, ry, 0, x, eave, v, x, eave, -v, x, ry, 0], 3));
|
||||
g.computeVertexNormals(); return g;
|
||||
}
|
||||
function maison(h, variante) {
|
||||
const p = h.p.map(([x, z]) => new THREE.Vector2(x, z));
|
||||
const c = p.reduce((a, b) => a.add(b), new THREE.Vector2()).multiplyScalar(0.25);
|
||||
const e1 = p[1].clone().sub(p[0]), e2 = p[2].clone().sub(p[1]);
|
||||
const fw = new THREE.Vector2(Math.cos(h.f), -Math.sin(h.f));
|
||||
let u, Lu, Lv;
|
||||
if (Math.abs(e1.clone().normalize().dot(fw)) >= Math.abs(e2.clone().normalize().dot(fw))) { u = e1.clone().normalize(); Lu = e1.length(); Lv = e2.length(); }
|
||||
else { u = e2.clone().normalize(); Lu = e2.length(); Lv = e1.length(); }
|
||||
const g = new THREE.Group(); g.position.set(c.x, 0, c.y); g.rotation.y = Math.atan2(-u.y, u.x);
|
||||
const grand = h.t === 'grand', transfo = h.t === 'transformee' && variante === '2035';
|
||||
const eave = grand ? 7.4 : 5.4, rh = Math.min(grand ? 5 : 3.4, Lv * (grand ? 0.27 : 0.31)), ite = transfo ? 0.7 : 0;
|
||||
const mMur = grand ? mat.grandMur : transfo ? mat.paille : mat.mur, mToit = grand ? mat.grandToit : transfo ? mat.toitT : mat.toit;
|
||||
const murs = new THREE.Mesh(new THREE.BoxGeometry(Lu + ite, eave, Lv + ite), mMur); murs.position.y = eave / 2;
|
||||
const t = new THREE.Mesh(toit(Lu + ite, Lv + ite, eave, rh, 0.55), mToit);
|
||||
const pg = new THREE.Mesh(pignons(Lu + ite, Lv + ite, eave, rh), new THREE.MeshLambertMaterial({ color: mMur.color, side: THREE.DoubleSide }));
|
||||
if (transfo) pg.material = mat.paille;
|
||||
[murs, t, pg].forEach((m) => { m.castShadow = true; m.receiveShadow = true; g.add(m); });
|
||||
if (transfo) {
|
||||
// serre sur la face la plus au sud
|
||||
const th = g.rotation.y, faces = [[1, 0, Math.cos(th), -Math.sin(th)], [-1, 0, -Math.cos(th), Math.sin(th)], [0, 1, Math.sin(th), Math.cos(th)], [0, -1, -Math.sin(th), -Math.cos(th)]];
|
||||
const f = faces.reduce((a, b) => (b[3] > a[3] ? b : a));
|
||||
const sh = 3.3, d = 2.4, cote = f[0] !== 0 ? Lv : Lu;
|
||||
const serre = new THREE.Mesh(new THREE.BoxGeometry(f[0] !== 0 ? d : cote * 0.7, sh, f[0] !== 0 ? cote * 0.7 : d), mat.verre);
|
||||
serre.position.set(f[0] * ((Lu + ite) / 2 + d / 2), sh / 2, f[1] * ((Lv + ite) / 2 + d / 2)); serre.renderOrder = 1; g.add(serre);
|
||||
const ar = new THREE.LineSegments(new THREE.EdgesGeometry(serre.geometry), mat.bord); ar.position.copy(serre.position); g.add(ar);
|
||||
const ch = new THREE.Mesh(new THREE.BoxGeometry(0.7, 2.6, 0.7), mat.chem); ch.position.set(Lu * 0.2, eave + rh * 0.62 + 0.7, -f[1] * Lv * 0.12); ch.castShadow = true; g.add(ch);
|
||||
// la cuve, au pied de la descente de gouttière, à l'opposé de la serre
|
||||
const bout = (Lu + ite) / 2 + 1.1;
|
||||
const cu = new THREE.Mesh(new THREE.CylinderGeometry(0.85, 0.85, 1.8, 16), mat.cuve);
|
||||
if (f[0] !== 0) cu.position.set(-f[0] * bout, 0.9, Lv / 2 - 1); else cu.position.set(bout, 0.9, -f[1] * (Lv / 2 - 1));
|
||||
cu.castShadow = true; g.add(cu);
|
||||
g.updateMatrixWorld(true);
|
||||
surv.maisons.push({ g, chem: ch.getWorldPosition(new THREE.Vector3()).add(new THREE.Vector3(0, 1.4, 0)), cuve: cu.getWorldPosition(new THREE.Vector3()), eave, c });
|
||||
}
|
||||
if (grand) surv.grand = g;
|
||||
g.userData = { t: h.t };
|
||||
return g;
|
||||
}
|
||||
for (const h of D.maisons) {
|
||||
if (h.t === 'transformee') { G.v2035.add(maison(h, '2035')); G.vAuj.add(maison(h, 'auj')); }
|
||||
else scene.add(maison(h, '2035'));
|
||||
}
|
||||
G.v2035.children.forEach((g) => cliquables.push({ o: g, carte: 'maison' }));
|
||||
if (surv.grand) cliquables.push({ o: surv.grand, carte: 'communs' });
|
||||
|
||||
// ---------- arbres (positions et couleurs tirées du plan)
|
||||
const geoC = new THREE.IcosahedronGeometry(1, 1), geoT = new THREE.CylinderGeometry(0.1, 0.17, 1, 5).translate(0, 0.5, 0);
|
||||
const TYPES = [{ base: 1.3, rmin: 1.1, sy: 1.05, tronc: true, g: 'v2035' }, { base: 2.6, rmin: 1.5, sy: 1.2, tronc: true, g: 'v2035' }, { base: 3.0, rmin: 2.0, sy: 1.15, tronc: true, g: 'tous' }, { base: 0, rmin: 0.75, sy: 0.8, tronc: false, g: 'tous' }];
|
||||
const matTronc = new THREE.MeshLambertMaterial({ color: '#8a7258' });
|
||||
const couleurs = D.pal.map(([r, g, b]) => new THREE.Color().setRGB(r / 255, g / 255, b / 255, THREE.SRGBColorSpace));
|
||||
TYPES.forEach((T, k) => {
|
||||
const A = D.arbres.filter((a) => a[4] === k); if (!A.length) return;
|
||||
const m = new THREE.MeshLambertMaterial({ color: '#ffffff', emissive: '#000000' });
|
||||
const im = new THREE.InstancedMesh(geoC, m, A.length); im.castShadow = true;
|
||||
const it = T.tronc ? new THREE.InstancedMesh(geoT, matTronc, A.length) : null;
|
||||
const M = new THREE.Matrix4(), Q = new THREE.Quaternion(), S = new THREE.Vector3(), V = new THREE.Vector3();
|
||||
A.forEach((a, i) => {
|
||||
const j = 0.88 + rnd(i + k * 7919) * 0.24, r = Math.max(T.rmin, a[2] * (k === 1 ? 1.25 : 1)) * j;
|
||||
V.set(a[0], T.base + r * T.sy * 0.85, a[1]); S.set(r, r * T.sy, r); Q.setFromAxisAngle(new THREE.Vector3(0, 1, 0), rnd(i) * 6.28);
|
||||
im.setMatrixAt(i, M.compose(V, Q, S)); im.setColorAt(i, couleurs[a[3]]);
|
||||
if (it) { V.set(a[0], 0, a[1]); S.set(1, T.base + r * 0.4, 1); it.setMatrixAt(i, M.compose(V, new THREE.Quaternion(), S)); }
|
||||
});
|
||||
im.instanceColor.needsUpdate = true;
|
||||
const cible = T.g === 'v2035' ? G.v2035 : scene; cible.add(im); if (it) cible.add(it);
|
||||
if (k === 0) surv.haies = m; if (k === 1) surv.foret = m;
|
||||
});
|
||||
|
||||
// ---------- personnages
|
||||
const geoP = new THREE.CapsuleGeometry(0.28, 0.95, 3, 8);
|
||||
const PAL_P = ['#d86a3a', '#3d6fa8', '#e3b23c', '#6b8f3a', '#b0556b', '#4a4a4a', '#c9c2b0', '#8a5a9e'].map(sRGB);
|
||||
const imP = new THREE.InstancedMesh(geoP, new THREE.MeshLambertMaterial({ color: '#ffffff' }), D.persos.length); imP.castShadow = true;
|
||||
D.persos.forEach(([x, z], i) => { imP.setMatrixAt(i, new THREE.Matrix4().makeTranslation(x, 0.76, z)); imP.setColorAt(i, PAL_P[i % PAL_P.length]); });
|
||||
imP.instanceColor.needsUpdate = true; G.v2035.add(imP);
|
||||
|
||||
// ---------- étals du marché (2035) / voitures garées (aujourd'hui)
|
||||
const PAL_E = ['#e98a4a', '#f2c14e', '#e36d5a', '#7fb069', '#5aa2d6'];
|
||||
const matEtal = PAL_E.map((c) => new THREE.MeshLambertMaterial({ color: c, emissive: '#000000' }));
|
||||
const matPoteau = new THREE.MeshLambertMaterial({ color: '#7b6a55' }), matTable = new THREE.MeshLambertMaterial({ color: '#c49a64' });
|
||||
function axes(pts) {
|
||||
const p = pts.map(([x, z]) => new THREE.Vector2(x, z)), c = p.reduce((a, b) => a.add(b), new THREE.Vector2()).multiplyScalar(0.25);
|
||||
const e1 = p[1].clone().sub(p[0]), e2 = p[2].clone().sub(p[1]); const [u, L, W] = e1.length() >= e2.length() ? [e1.clone().normalize(), e1.length(), e2.length()] : [e2.clone().normalize(), e2.length(), e1.length()];
|
||||
return { c, rot: Math.atan2(-u.y, u.x), L, W };
|
||||
}
|
||||
const PAL_V = ['#8f98a3', '#b24a3b', '#3e5f8a', '#d9d4c7', '#565a5e', '#7a8b5a'].map(sRGB);
|
||||
function voiture(k) {
|
||||
const g = new THREE.Group(), m = new THREE.MeshLambertMaterial({ color: PAL_V[k % PAL_V.length] });
|
||||
const a = new THREE.Mesh(new THREE.BoxGeometry(4.2, 0.9, 1.8), m); a.position.y = 0.65;
|
||||
const b = new THREE.Mesh(new THREE.BoxGeometry(2.3, 0.75, 1.62), m); b.position.set(-0.25, 1.45, 0);
|
||||
[a, b].forEach((x) => { x.castShadow = true; g.add(x); }); return g;
|
||||
}
|
||||
D.etals.forEach((e, i) => {
|
||||
const a = axes(e.p), g = new THREE.Group(); g.position.set(a.c.x, 0, a.c.y); g.rotation.y = a.rot;
|
||||
const L = Math.min(a.L, 4.5), W = Math.min(a.W, 3);
|
||||
const toile = new THREE.Mesh(new THREE.BoxGeometry(L + 0.4, 0.16, W + 0.4), matEtal[i % matEtal.length]); toile.position.y = 2.45; toile.castShadow = true; g.add(toile);
|
||||
for (const [sx, sz] of [[1, 1], [1, -1], [-1, 1], [-1, -1]]) { const po = new THREE.Mesh(new THREE.CylinderGeometry(0.06, 0.06, 2.45, 5), matPoteau); po.position.set(sx * L / 2, 1.22, sz * W / 2); g.add(po); }
|
||||
const tb = new THREE.Mesh(new THREE.BoxGeometry(L * 0.9, 0.85, W * 0.45), matTable); tb.position.set(0, 0.42, W * 0.18); tb.castShadow = true; g.add(tb);
|
||||
G.v2035.add(g); surv.etals.push(g); cliquables.push({ o: g, carte: 'marche' });
|
||||
const v = voiture(i); v.position.set(a.c.x, 0, a.c.y); v.rotation.y = a.rot + Math.PI / 2; G.vAuj.add(v);
|
||||
});
|
||||
|
||||
// ---------- eau, sentiers : surfaces qui s'allument
|
||||
const aire = (pts) => Math.abs(pts.reduce((s, p, i) => { const q = pts[(i + 1) % pts.length]; return s + p[0] * q[1] - q[0] * p[1]; }, 0)) / 2;
|
||||
function surface(pts, couleur, y) {
|
||||
const sh = new THREE.Shape(pts.map(([x, z]) => new THREE.Vector2(x, -z)));
|
||||
const g = new THREE.ShapeGeometry(sh).rotateX(-Math.PI / 2);
|
||||
const m = new THREE.Mesh(g, new THREE.MeshBasicMaterial({ color: couleur, transparent: true, opacity: 0, depthWrite: false }));
|
||||
m.position.y = y; m.renderOrder = 2; G.v2035.add(m); return m;
|
||||
}
|
||||
D.noues.forEach((pts) => { const m = surface(pts, '#1f7fc0', 0.08); surv.noues.push(m); cliquables.push({ o: m, carte: 'noues' }); });
|
||||
D.plantes.forEach((pts) => { if (pts.length > 3 && aire(pts) > 60) { const m = surface(pts, '#1c9aa8', 0.08); surv.bassins.push(m); cliquables.push({ o: m, carte: 'bassins' }); } });
|
||||
D.sentiers.forEach((pts) => surv.sentiers.push(surface(pts, '#e3a92c', 0.07)));
|
||||
|
||||
// rubans en pointillés qui coulent (liaisons, chemins, liens du hors-champ)
|
||||
const vs = 'varying vec2 vUv; void main(){ vUv = uv; gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); }';
|
||||
const fs = 'uniform float uT; uniform float uA; uniform float uV; uniform float uP; uniform vec3 uC; varying vec2 vUv; void main(){ float d = fract(vUv.x / uP - uT * uV); float t = smoothstep(0.0, 0.08, d) * (1.0 - smoothstep(0.45, 0.55, d)); float e = 1.0 - smoothstep(0.3, 0.5, abs(vUv.y - 0.5)); gl_FragColor = vec4(uC, uA * (0.22 + 0.78 * t) * e); }';
|
||||
const hex3 = (h) => { const c = parseInt(h.slice(1), 16); return new THREE.Vector3((c >> 16 & 255) / 255, (c >> 8 & 255) / 255, (c & 255) / 255); };
|
||||
function ruban(pts, largeur, couleur, vitesse, pas, y, groupe) {
|
||||
const pos = [], uv = [], idx = []; let L = 0;
|
||||
for (let i = 0; i < pts.length; i++) {
|
||||
const [x, z] = pts[i], a = pts[Math.max(0, i - 1)], b = pts[Math.min(pts.length - 1, i + 1)];
|
||||
let dx = b[0] - a[0], dz = b[1] - a[1]; const n = Math.hypot(dx, dz) || 1; dx /= n; dz /= n;
|
||||
const rx = -dz * largeur / 2, rz = dx * largeur / 2;
|
||||
if (i > 0) L += Math.hypot(x - pts[i - 1][0], z - pts[i - 1][1]);
|
||||
pos.push(x - rx, 0, z - rz, x + rx, 0, z + rz); uv.push(L, 0, L, 1);
|
||||
if (i > 0) { const k = 2 * i; idx.push(k - 2, k - 1, k, k - 1, k + 1, k); }
|
||||
}
|
||||
const g = new THREE.BufferGeometry(); g.setAttribute('position', new THREE.Float32BufferAttribute(pos, 3)); g.setAttribute('uv', new THREE.Float32BufferAttribute(uv, 2)); g.setIndex(idx);
|
||||
const m = new THREE.ShaderMaterial({ vertexShader: vs, fragmentShader: fs, transparent: true, depthWrite: false, side: THREE.DoubleSide,
|
||||
uniforms: { uT: { value: 0 }, uA: { value: 0 }, uV: { value: vitesse }, uP: { value: pas }, uC: { value: hex3(couleur) } } });
|
||||
const mesh = new THREE.Mesh(g, m); mesh.position.y = y; mesh.renderOrder = 3; (groupe || G.v2035).add(mesh); return mesh;
|
||||
}
|
||||
const sommetsNoues = D.noues.flat();
|
||||
const dNoue = ([x, z]) => Math.min(...sommetsNoues.map(([a, b]) => (a - x) ** 2 + (b - z) ** 2));
|
||||
D.liaisons.forEach((l) => { const pts = dNoue(l[0]) < dNoue(l[l.length - 1]) ? l.slice().reverse() : l; surv.liaisons.push(ruban(pts, 1.0, '#1f7fc0', 0.9, 2.2, 0.14)); });
|
||||
D.chemins.forEach((l) => { let L = 0; for (let i = 1; i < l.length; i++) L += Math.hypot(l[i][0] - l[i - 1][0], l[i][1] - l[i - 1][1]); if (L > 12) surv.chemins.push(ruban(l, 1.1, '#d0901f', 0.35, 3.0, 0.13)); });
|
||||
|
||||
// pluie des toits vers les cuves, ondes sur la mare, fumée des poêles
|
||||
const disque = (() => { const c = document.createElement('canvas'); c.width = c.height = 64; const x = c.getContext('2d'); const g = x.createRadialGradient(32, 32, 2, 32, 32, 30); g.addColorStop(0, 'rgba(255,255,255,1)'); g.addColorStop(1, 'rgba(255,255,255,0)'); x.fillStyle = g; x.fillRect(0, 0, 64, 64); const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace; return t; })();
|
||||
surv.maisons.forEach((m, k) => {
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: disque, color: '#2d86bf', transparent: true, opacity: 0, depthWrite: false }));
|
||||
s.scale.set(0.9, 0.9, 1); s.userData = { m, ph: i / 7 }; G.v2035.add(s); surv.pluie.push(s);
|
||||
}
|
||||
for (let i = 0; i < 6; i++) {
|
||||
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: disque, color: '#b9b2a6', transparent: true, opacity: 0.5, depthWrite: false }));
|
||||
s.userData = { m, ph: (i + rnd(k * 13 + i)) / 6 }; G.v2035.add(s); surv.fumee.push(s);
|
||||
}
|
||||
});
|
||||
const MARE = P(700, 468);
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const o = new THREE.Mesh(new THREE.RingGeometry(0.85, 1.15, 48).rotateX(-Math.PI / 2), new THREE.MeshBasicMaterial({ color: '#1f7fc0', transparent: true, opacity: 0, depthWrite: false }));
|
||||
o.position.set(MARE.x, 0.16, MARE.z); o.userData = { ph: i / 3 }; G.v2035.add(o); surv.ondes.push(o);
|
||||
}
|
||||
|
||||
// ---------- véhicules et marcheurs le long des voies
|
||||
function trajet(pts) { const c = [0]; for (let i = 1; i < pts.length; i++) c.push(c[i - 1] + Math.hypot(pts[i][0] - pts[i - 1][0], pts[i][1] - pts[i - 1][1])); return { pts, c, L: c[c.length - 1] }; }
|
||||
function surTrajet(t, s) {
|
||||
s = Math.max(0, Math.min(t.L, s)); let i = 1; while (i < t.c.length - 1 && t.c[i] < s) i++;
|
||||
const a = t.pts[i - 1], b = t.pts[i], k = (s - t.c[i - 1]) / ((t.c[i] - t.c[i - 1]) || 1);
|
||||
return { x: a[0] + (b[0] - a[0]) * k, z: a[1] + (b[1] - a[1]) * k, dx: b[0] - a[0], dz: b[1] - a[1] };
|
||||
}
|
||||
function velo(k) {
|
||||
const g = new THREE.Group(), cadre = new THREE.Mesh(new THREE.BoxGeometry(1.7, 0.12, 0.14), new THREE.MeshLambertMaterial({ color: '#3b3a36' })); cadre.position.y = 0.62;
|
||||
const r = new THREE.Mesh(new THREE.CapsuleGeometry(0.26, 0.7, 3, 8), new THREE.MeshLambertMaterial({ color: PAL_P[(k + 2) % PAL_P.length] })); r.position.y = 1.35;
|
||||
[cadre, r].forEach((x) => { x.castShadow = true; g.add(x); }); return g;
|
||||
}
|
||||
const mobiles = [];
|
||||
const R = D.routes; const tr = {}; for (const k in R) tr[k] = trajet(R[k]);
|
||||
const cheminsLongs = D.chemins.map(trajet).filter((t) => t.L > 40).sort((a, b) => b.L - a.L);
|
||||
const ajoute = (obj, t, vitesse, decal, depart, groupe) => { groupe.add(obj); mobiles.push({ obj, t, v: vitesse, d: decal, s: depart * t.L, sens: 1 }); };
|
||||
ajoute(velo(0), tr.principale, 4.2, 2.8, 0.1, G.v2035); ajoute(velo(1), tr.principale, 3.8, 2.8, 0.65, G.v2035);
|
||||
ajoute(velo(2), tr.verticale, 4.0, 2.2, 0.4, G.v2035); ajoute(velo(3), tr.anneau, 3.5, 2.0, 0.2, G.v2035);
|
||||
ajoute(voiture(0), tr.principale, 8, 1.8, 0.45, G.v2035); ajoute(voiture(2), tr.verticale, 7, 1.6, 0.8, G.v2035);
|
||||
if (cheminsLongs[0]) ajoute(velo(4), cheminsLongs[0], 3.2, 0, 0.3, G.v2035);
|
||||
cheminsLongs.slice(0, 5).forEach((t, i) => { const p = new THREE.Mesh(geoP, new THREE.MeshLambertMaterial({ color: PAL_P[(i + 3) % PAL_P.length] })); p.castShadow = true; const g = new THREE.Group(); p.position.y = 0.76; g.add(p); ajoute(g, t, 1.3, 0, rnd(i + 3), G.v2035); });
|
||||
[['principale', 0.05], ['principale', 0.35], ['principale', 0.7], ['verticale', 0.2], ['verticale', 0.6], ['liaison_anneau', 0.5], ['anneau', 0.3], ['droite', 0.5]]
|
||||
.forEach(([k, d], i) => ajoute(voiture(i + 1), tr[k], 9, 1.8, d, G.vAuj));
|
||||
|
||||
// ---------- bulles de la planche 2030, plantées comme des panneaux
|
||||
const matTige = { v: new THREE.MeshLambertMaterial({ color: '#8fb76a' }), q: new THREE.MeshLambertMaterial({ color: '#e1a35c' }) };
|
||||
const geoTige = new THREE.CylinderGeometry(0.1, 0.1, 1, 6).translate(0, 0.5, 0);
|
||||
D.bulles.forEach((b, i) => {
|
||||
const [x, z] = b.w, vert = VERT.has(b.id);
|
||||
const tige = new THREE.Mesh(geoTige, vert ? matTige.v : matTige.q); tige.position.set(x, 0, z); tige.scale.y = 8.4; G.v2035.add(tige);
|
||||
const t = chargeur.load(BULLES[b.id], () => (besoin = true)); t.colorSpace = THREE.SRGBColorSpace;
|
||||
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t, transparent: true, depthTest: false }));
|
||||
const petit = b.id.startsWith('apaise'); s.scale.set(petit ? 3.4 : 6.4, petit ? 3.4 : 6.4, 1); s.position.set(x, petit ? 7.4 : 10.4, z); s.renderOrder = 10;
|
||||
s.userData = { id: b.id, base: s.position.y, ph: rnd(i) * 6.28, taille: s.scale.x }; G.v2035.add(s); surv.bulles.push(s);
|
||||
});
|
||||
|
||||
// ---------- hors-champ : les grandes infrastructures et leurs liens
|
||||
const matI = (c) => new THREE.MeshLambertMaterial({ color: c });
|
||||
function etiquette(texte, couleur) {
|
||||
const c = document.createElement('canvas'); c.width = 640; c.height = 120; const x = c.getContext('2d');
|
||||
x.fillStyle = 'rgba(251,249,243,0.94)'; x.beginPath(); if (x.roundRect) x.roundRect(4, 4, 632, 112, 22); else x.rect(4, 4, 632, 112); x.fill(); x.strokeStyle = couleur; x.lineWidth = 5; x.stroke();
|
||||
x.fillStyle = '#2a2925'; x.font = '600 50px "Barlow Condensed", Barlow, sans-serif'; x.textAlign = 'center'; x.textBaseline = 'middle'; x.fillText(texte, 320, 62);
|
||||
const t = new THREE.CanvasTexture(c); t.colorSpace = THREE.SRGBColorSpace;
|
||||
const s = new THREE.Sprite(new THREE.SpriteMaterial({ map: t, transparent: true, depthTest: false })); s.scale.set(64, 12, 1); s.renderOrder = 9; return s;
|
||||
}
|
||||
const INFRA = [
|
||||
{ k: 'centrale', n: 'Centrale, gaz importé', c: '#c4850f', x: -130, z: -330, sys: 'chaleur' },
|
||||
{ k: 'potab', n: 'Puisement, potabilisation', c: '#2d86bf', x: 170, z: -300, sys: 'eau' },
|
||||
{ k: 'agri', n: 'Agriculture industrielle · 850 km', c: '#5a9837', x: 360, z: -40, sys: 'vivant' },
|
||||
{ k: 'step', n: "Station d'épuration", c: '#2d86bf', x: 110, z: 300, sys: 'eau' },
|
||||
{ k: 'super', n: 'Supermarché', c: '#d4662a', x: -230, z: 260, sys: 'quartier' },
|
||||
{ k: 'ciment', n: 'Gravière, cimenterie', c: '#c4850f', x: -380, z: 70, sys: 'chaleur' },
|
||||
{ k: 'emploi', n: "Bassin d'emploi", c: '#6c665b', x: -400, z: -200, sys: 'travail' },
|
||||
];
|
||||
const HC = new THREE.Group(); HC.position.y = -2.3; scene.add(HC); // posé sur le papier, au pied du socle
|
||||
for (const f of INFRA) {
|
||||
const g = new THREE.Group(); g.position.set(f.x, 0, f.z);
|
||||
const add = (geo, m, x, y, z) => { const o = new THREE.Mesh(geo, m); o.position.set(x, y, z); o.castShadow = true; g.add(o); return o; };
|
||||
if (f.k === 'centrale') { add(new THREE.CylinderGeometry(8, 12, 34, 24), matI('#dcd6ca'), -12, 17, 0); add(new THREE.CylinderGeometry(8, 12, 34, 24), matI('#dcd6ca'), 14, 17, 8); add(new THREE.BoxGeometry(16, 14, 16), matI('#c9c3b7'), 2, 7, -22); }
|
||||
if (f.k === 'potab') { add(new THREE.CylinderGeometry(2.5, 2.5, 26, 12), matI('#d3cec4'), 0, 13, 0); add(new THREE.CylinderGeometry(9, 6, 8, 20), matI('#bfd6e2'), 0, 30, 0); }
|
||||
if (f.k === 'agri') { for (let i = 0; i < 6; i++) add(new THREE.BoxGeometry(120, 0.3, 16), matI(['#dcd49a', '#c8d49a', '#e4d08c'][i % 3]), 0, 0.15, -48 + i * 17); for (let i = 0; i < 3; i++) add(new THREE.CylinderGeometry(4, 4, 22, 14), matI('#cdc7bb'), -70, 11, -12 + i * 10); }
|
||||
if (f.k === 'step') { for (let i = 0; i < 3; i++) { add(new THREE.CylinderGeometry(12, 12, 2.4, 32), matI('#c9c5bd'), -30 + i * 28, 1.2, 0); add(new THREE.CylinderGeometry(11, 11, 0.2, 32), matI('#9fc9dd'), -30 + i * 28, 2.45, 0); } }
|
||||
if (f.k === 'super') { add(new THREE.BoxGeometry(72, 9, 44), matI('#e2dbcd'), 0, 4.5, 0); add(new THREE.BoxGeometry(90, 0.2, 42), matI('#d3cfc6'), 0, 0.1, 48); }
|
||||
if (f.k === 'ciment') { add(new THREE.ConeGeometry(14, 11, 18), matI('#cfbd9d'), -14, 5.5, 8); add(new THREE.ConeGeometry(10, 8, 18), matI('#c7b48f'), 14, 4, 16); add(new THREE.CylinderGeometry(4, 4, 34, 14), matI('#bdb6aa'), 6, 17, -14); }
|
||||
if (f.k === 'emploi') { add(new THREE.BoxGeometry(16, 44, 16), matI('#cfd3d8'), -12, 22, 0); add(new THREE.BoxGeometry(14, 30, 14), matI('#c3c8ce'), 12, 15, 10); add(new THREE.BoxGeometry(12, 22, 12), matI('#d8dbe0'), 6, 11, -16); }
|
||||
const e = etiquette(f.n, f.c); e.position.set(0, 58, 0); g.add(e);
|
||||
HC.add(g);
|
||||
// le lien : une courbe vers le cœur du quartier
|
||||
const pts = []; const cx = f.x * 0.45 + (f.z > 0 ? 30 : -30), cz = f.z * 0.45;
|
||||
for (let i = 0; i <= 40; i++) { const t = i / 40, a = (1 - t) * (1 - t), b = 2 * (1 - t) * t, c = t * t; pts.push([a * f.x + b * cx + c * MARE.x, a * f.z + b * cz + c * MARE.z]); }
|
||||
surv.flux[f.k] = { r: ruban(pts, 4, f.c, 0.35, 14, 0.2, HC), sys: f.sys };
|
||||
}
|
||||
|
||||
// ---------- caméra : axonométrie orthographique, pilotable au doigt
|
||||
const vue = { x: 0, z: 0, az: 0, el: 0.62, w: 210 };
|
||||
let vol = null, besoin = true;
|
||||
const taille = () => ({ W: canvas.clientWidth, H: canvas.clientHeight });
|
||||
function place() {
|
||||
const { W, H } = taille(), a = W / H, vh = vue.w / a;
|
||||
cam.left = -vue.w / 2; cam.right = vue.w / 2; cam.top = vh / 2; cam.bottom = -vh / 2; cam.updateProjectionMatrix();
|
||||
const d = 1500; cam.position.set(vue.x + Math.sin(vue.az) * Math.cos(vue.el) * d, Math.sin(vue.el) * d, vue.z + Math.cos(vue.az) * Math.cos(vue.el) * d);
|
||||
cam.lookAt(vue.x, 0, vue.z);
|
||||
}
|
||||
function decalagePanneau(w) {
|
||||
const { W, H } = taille(); const f = $('fiches').getBoundingClientRect(); const haut = 64;
|
||||
const bas = W >= 900 ? 0 : Math.max(0, H - f.top);
|
||||
return ((bas - haut) / 2) * (w / W) / Math.sin(vue.el);
|
||||
}
|
||||
function versCible(c0, duree = 1500) {
|
||||
// en portrait, on resserre le cadrage : la largeur de l'écran est la dimension qui manque
|
||||
const { W, H } = taille(), c = { ...c0, w: W < H && c0.w < 400 ? c0.w * 0.72 : c0.w };
|
||||
const avant = { ...vue }, off = decalagePanneau(c.w);
|
||||
const fx = -Math.sin(vue.az), fz = -Math.cos(vue.az);
|
||||
const apres = { x: c.x - fx * off, z: c.z - fz * off, az: vue.az, el: vue.el, w: c.w };
|
||||
if (reduit || duree === 0) { Object.assign(vue, apres); besoin = true; return; }
|
||||
vol = { avant, apres, t0: performance.now(), d: duree };
|
||||
}
|
||||
const bornes = () => { vue.w = Math.max(18, Math.min(1000, vue.w)); vue.x = Math.max(-450, Math.min(450, vue.x)); vue.z = Math.max(-450, Math.min(450, vue.z)); };
|
||||
function solSous(px, py) {
|
||||
const { W, H } = taille(), wpp = vue.w / W, sx = px - W / 2, sy = py - H / 2;
|
||||
const rx = Math.cos(vue.az), rz = -Math.sin(vue.az), fx = -Math.sin(vue.az), fz = -Math.cos(vue.az), k = wpp / Math.sin(vue.el);
|
||||
return { x: vue.x + rx * sx * wpp - fx * sy * k, z: vue.z + rz * sx * wpp - fz * sy * k };
|
||||
}
|
||||
function zoomVers(px, py, f) { const p = solSous(px, py); vue.w *= f; vue.x = p.x + (vue.x - p.x) * f; vue.z = p.z + (vue.z - p.z) * f; bornes(); besoin = true; }
|
||||
// gestes
|
||||
const ptrs = new Map(); let geste = null;
|
||||
canvas.addEventListener('pointerdown', (e) => { canvas.setPointerCapture(e.pointerId); ptrs.set(e.pointerId, { x: e.clientX, y: e.clientY }); geste = { t: performance.now(), bouge: 0, x: e.clientX, y: e.clientY }; vol = null; canvas.classList.add('tire'); suspendre(); });
|
||||
canvas.addEventListener('pointermove', (e) => {
|
||||
if (!ptrs.has(e.pointerId)) return;
|
||||
const avant = [...ptrs.values()].map((p) => ({ ...p })); ptrs.set(e.pointerId, { x: e.clientX, y: e.clientY }); const apres = [...ptrs.values()];
|
||||
const { W } = taille(), wpp = vue.w / W;
|
||||
const cA = avant.reduce((a, p) => ({ x: a.x + p.x / avant.length, y: a.y + p.y / avant.length }), { x: 0, y: 0 });
|
||||
const cB = apres.reduce((a, p) => ({ x: a.x + p.x / apres.length, y: a.y + p.y / apres.length }), { x: 0, y: 0 });
|
||||
const dx = cB.x - cA.x, dy = cB.y - cA.y; if (geste) geste.bouge += Math.abs(dx) + Math.abs(dy);
|
||||
const rx = Math.cos(vue.az), rz = -Math.sin(vue.az), fx = -Math.sin(vue.az), fz = -Math.cos(vue.az), k = wpp / Math.sin(vue.el);
|
||||
vue.x -= rx * dx * wpp - fx * dy * k; vue.z -= rz * dx * wpp - fz * dy * k;
|
||||
if (apres.length === 2 && avant.length === 2) {
|
||||
const dA = Math.hypot(avant[0].x - avant[1].x, avant[0].y - avant[1].y), dB = Math.hypot(apres[0].x - apres[1].x, apres[0].y - apres[1].y);
|
||||
if (dA > 0 && dB > 0) zoomVers(cB.x, cB.y, dA / dB);
|
||||
const aA = Math.atan2(avant[1].y - avant[0].y, avant[1].x - avant[0].x), aB = Math.atan2(apres[1].y - apres[0].y, apres[1].x - apres[0].x);
|
||||
let da = aB - aA; if (da > Math.PI) da -= 2 * Math.PI; if (da < -Math.PI) da += 2 * Math.PI; vue.az -= da;
|
||||
}
|
||||
bornes(); besoin = true;
|
||||
});
|
||||
const fin = (e) => {
|
||||
ptrs.delete(e.pointerId); if (!ptrs.size) canvas.classList.remove('tire');
|
||||
if (geste && !ptrs.size && geste.bouge < 8 && performance.now() - geste.t < 450) toucher(e.clientX, e.clientY);
|
||||
if (!ptrs.size) geste = null;
|
||||
};
|
||||
canvas.addEventListener('pointerup', fin); canvas.addEventListener('pointercancel', fin);
|
||||
canvas.addEventListener('wheel', (e) => { e.preventDefault(); vol = null; zoomVers(e.clientX, e.clientY, e.deltaY > 0 ? 1.12 : 1 / 1.12); suspendre(); }, { passive: false });
|
||||
const ray = new THREE.Raycaster();
|
||||
function toucher(px, py) {
|
||||
const { W, H } = taille(); ray.setFromCamera(new THREE.Vector2((px / W) * 2 - 1, -(py / H) * 2 + 1), cam);
|
||||
if (E.epoque === '2035') {
|
||||
const hb = ray.intersectObjects(surv.bulles, false)[0];
|
||||
if (hb) { const id = hb.object.userData.id, s = SIGNES[id] || [null, '', 'quartier']; return s[0] ? allerA(s[0]) : mini(id, s[1], s[2]); }
|
||||
}
|
||||
const cand = cliquables.filter((c) => c.o.parent && c.o.parent.visible !== false && (c.o.parent === scene || c.o.parent.visible));
|
||||
const hits = ray.intersectObjects(cand.map((c) => c.o), true);
|
||||
if (hits.length) { let o = hits[0].object; const trouve = cand.find((c) => { let x = o; while (x) { if (x === c.o) return true; x = x.parent; } return false; }); if (trouve && E.epoque === '2035') allerA(trouve.carte); }
|
||||
}
|
||||
function allerA(id) { const L = liste(); let i = L.findIndex((c) => c.id === id); if (i < 0) { E.filtre = 'tout'; onglets(); i = liste().findIndex((c) => c.id === id); } if (i >= 0) { E.mini = null; E.i = i; montrer(); } }
|
||||
function mini(id, texte, couche) {
|
||||
const b = D.bulles.find((x) => x.id === id);
|
||||
E.mini = { id: 'mini', c: couche, ic: id, t: texte.split(/[.:]/)[0], p: texte, ch: [], s: 'bulle de la planche « le quartier en 2030 », TMIP 2024, p. 15', cam: { x: b.w[0], z: b.w[1], w: 50 } };
|
||||
montrer();
|
||||
}
|
||||
|
||||
// ---------- état : époque, vue, surlignage
|
||||
const etat = { epoque: '2035', fx: null, auj: 0, cibleAuj: 0 };
|
||||
function epoque(e) {
|
||||
etat.epoque = e; etat.cibleAuj = e === 'auj' ? 1 : 0;
|
||||
G.v2035.visible = e === '2035'; G.vAuj.visible = e === 'auj'; besoin = true;
|
||||
}
|
||||
function activer(c, voler) { etat.fx = c.fx || null; if (voler && c.cam) versCible(c.cam); besoin = true; }
|
||||
function vueAxo(axo) { vol = { avant: { ...vue }, apres: { ...vue, el: axo ? 0.62 : 1.54 }, t0: performance.now(), d: reduit ? 0 : 900 }; }
|
||||
function tourner() { vol = { avant: { ...vue }, apres: { ...vue, az: vue.az + Math.PI / 2 }, t0: performance.now(), d: reduit ? 0 : 900 }; }
|
||||
|
||||
// ---------- boucle
|
||||
const horloge = new THREE.Clock(); let t = 0;
|
||||
const ease = (k) => (k < 0.5 ? 4 * k * k * k : 1 - Math.pow(-2 * k + 2, 3) / 2);
|
||||
function redim() { const { W, H } = taille(); renderer.setSize(W, H, false); besoin = true; }
|
||||
addEventListener('resize', redim); redim();
|
||||
function boucle(now) {
|
||||
requestAnimationFrame(boucle);
|
||||
tic(now);
|
||||
if (document.hidden) return;
|
||||
const dt = Math.min(0.05, horloge.getDelta()); t += dt;
|
||||
if (vol) { const k = Math.min(1, (now - vol.t0) / (vol.d || 1)), e = ease(k); for (const key of ['x', 'z', 'az', 'el', 'w']) vue[key] = vol.avant[key] + (vol.apres[key] - vol.avant[key]) * e; if (k >= 1) vol = null; }
|
||||
// fondu des sols
|
||||
etat.auj += (etat.cibleAuj - etat.auj) * Math.min(1, dt * 4); matAuj.opacity = etat.auj;
|
||||
const puls = 0.5 + 0.5 * Math.sin(t * 2.6), fx = etat.fx, eau = E.epoque === '2035';
|
||||
surv.noues.forEach((m) => (m.material.opacity = fx === 'noues' ? 0.3 + 0.3 * puls : fx === 'mare' || fx === 'liaisons' ? 0.22 : 0));
|
||||
surv.bassins.forEach((m) => (m.material.opacity = fx === 'bassins' ? 0.3 + 0.3 * puls : 0));
|
||||
surv.sentiers.forEach((m) => (m.material.opacity = fx === 'sentiers' ? 0.3 + 0.35 * puls : 0));
|
||||
surv.liaisons.forEach((m) => { m.material.uniforms.uT.value = t; m.material.uniforms.uA.value = fx === 'liaisons' || fx === 'pluie' ? 0.95 : fx === 'noues' ? 0.45 : 0; });
|
||||
surv.chemins.forEach((m) => { m.material.uniforms.uT.value = t; m.material.uniforms.uA.value = fx === 'sentiers' || fx === 'mobilite' ? 0.9 : fx === 'foret' ? 0.5 : 0; });
|
||||
for (const k in surv.flux) {
|
||||
const f = surv.flux[k], u = f.r.material.uniforms; u.uT.value = t;
|
||||
const tendu = etat.epoque === 'auj' || f.sys === 'travail';
|
||||
const vise = fx === 'flux' || (fx === 'travail' && f.sys === 'travail');
|
||||
u.uA.value = tendu ? (vise ? 0.95 : 0.7) : (vise ? 0.22 : 0); u.uV.value = tendu ? 0.45 : 0.05;
|
||||
}
|
||||
if (surv.haies) surv.haies.emissive.setRGB(0.12 * puls * (fx === 'haies'), 0.26 * puls * (fx === 'haies'), 0.02 * (fx === 'haies'));
|
||||
if (surv.foret) surv.foret.emissive.setRGB(0.08 * puls * (fx === 'foret'), 0.2 * puls * (fx === 'foret'), 0.04 * puls * (fx === 'foret'));
|
||||
mat.paille.emissive.setRGB(fx === 'maisons' ? 0.28 * puls : 0, fx === 'maisons' ? 0.17 * puls : 0, 0);
|
||||
mat.grandToit.emissive.setRGB(fx === 'communs' ? 0.35 * puls : 0, fx === 'communs' ? 0.12 * puls : 0, 0);
|
||||
matEtal.forEach((m) => m.emissive.setRGB(fx === 'marche' ? 0.25 * puls : 0, fx === 'marche' ? 0.14 * puls : 0, 0));
|
||||
surv.pluie.forEach((s) => {
|
||||
const on = fx === 'pluie'; s.material.opacity = on ? 0.85 : 0; if (!on) return;
|
||||
const k = (t * 0.55 + s.userData.ph) % 1, m = s.userData.m; s.position.set(m.cuve.x, 0.2 + (m.eave + 0.6) * (1 - k), m.cuve.z);
|
||||
});
|
||||
surv.fumee.forEach((s) => {
|
||||
const k = (t * 0.12 + s.userData.ph) % 1, m = s.userData.m; s.position.set(m.chem.x - k * 5, m.chem.y + k * 9, m.chem.z - k * 2.5);
|
||||
const sc = 1.2 + k * 4.5; s.scale.set(sc, sc, 1); s.material.opacity = (fx === 'maisons' ? 0.75 : 0.5) * (1 - k) * Math.min(1, k * 6);
|
||||
});
|
||||
surv.ondes.forEach((o) => { const k = (t * 0.35 + o.userData.ph) % 1, on = fx === 'mare' || fx === 'noues'; o.scale.setScalar(1 + k * 11); o.material.opacity = on ? 0.6 * (1 - k) : 0; });
|
||||
const lieesA = { marche: ['marche', 'musique'], haies: ['pomme', 'confiture', 'bouteille'], foret: ['abeille', 'miel', 'fleur', 'arbre'], communs: ['biere', 'couverts', 'outils'], mare: ['oiseaux', 'musique'], sentiers: ['promeneurs'], mobilite: ['enfant', 'promeneurs', 'apaise1', 'apaise2', 'ballons'] }[fx] || [];
|
||||
surv.bulles.forEach((s) => { const u = s.userData, on = lieesA.includes(u.id), k = on ? 1.35 : 1; s.position.y = u.base + Math.sin(t * 1.4 + u.ph) * 0.35; s.scale.set(u.taille * k, u.taille * k, 1); s.material.opacity = lieesA.length && !on ? 0.55 : 1; });
|
||||
mobiles.forEach((m) => {
|
||||
if (!m.obj.parent.visible) return; m.s += m.v * dt * m.sens; if (m.s > m.t.L) { m.s = m.t.L; m.sens = -1; } if (m.s < 0) { m.s = 0; m.sens = 1; }
|
||||
const p = surTrajet(m.t, m.s); let dx = p.dx * m.sens, dz = p.dz * m.sens; const n = Math.hypot(dx, dz) || 1; dx /= n; dz /= n;
|
||||
m.obj.position.set(p.x - dz * m.d, 0, p.z + dx * m.d); m.obj.rotation.y = Math.atan2(-dz, dx);
|
||||
});
|
||||
place(); renderer.render(scene, cam); besoin = false;
|
||||
}
|
||||
place(); requestAnimationFrame(boucle);
|
||||
return { activer, epoque, vueAxo, tourner, versCible };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- commandes
|
||||
function setEpoque(e) {
|
||||
E.epoque = e; E.filtre = 'tout'; E.i = 0; E.mini = null;
|
||||
$('b-auj').setAttribute('aria-pressed', e === 'auj'); $('b-2035').setAttribute('aria-pressed', e === '2035');
|
||||
if (T3) T3.epoque(e); onglets(); montrer();
|
||||
}
|
||||
$('b-auj').addEventListener('click', () => setEpoque('auj'));
|
||||
$('b-2035').addEventListener('click', () => setEpoque('2035'));
|
||||
$('b-axo').addEventListener('click', () => { $('b-axo').setAttribute('aria-pressed', 'true'); $('b-plan').setAttribute('aria-pressed', 'false'); if (T3) T3.vueAxo(true); });
|
||||
$('b-plan').addEventListener('click', () => { $('b-plan').setAttribute('aria-pressed', 'true'); $('b-axo').setAttribute('aria-pressed', 'false'); if (T3) T3.vueAxo(false); });
|
||||
$('b-tourner').addEventListener('click', () => { if (T3) T3.tourner(); });
|
||||
|
||||
// ---------------------------------------------------------------- le cycle de l'eau
|
||||
const ETAPES = [
|
||||
{ n: 1, h: ['h1', 'h1b'], t: 'Potentiels', p: "La maison dispose des ressources pour être autonome en eau : un toit, un jardin, de la place.", ch: [['+80 %', "d'eau récupérée"], ['98 %', "des usages sans eau potable"]], f: ['pluie'] },
|
||||
{ n: 2, h: ['h2'], t: 'Séparer les flux', p: "Les eaux grises partent vers une noue à l'air libre. Les eaux noires vont au centre de phytoépuration.", ch: [['82 %', 'des rejets sont des eaux grises']], f: ['usees'] },
|
||||
{ n: 3, h: ['h3'], t: 'Phytoépuration', p: "Des plantes aquifères épurent les eaux grises : graisses, sucres, amidon.", ch: [], f: ['usees'] },
|
||||
{ n: 4, h: ['h4'], t: 'Maîtriser les pluies', p: "Noues et bassins absorbent l'essentiel des précipitations. Les stations d'épuration ne débordent plus à l'orage.", ch: [['−21 %', "de charge sur le réseau national, si la moitié des maisons le faisaient (estimation, p. 29)"]], f: ['pluie'] },
|
||||
{ n: 5, h: ['h5', 'h5b'], t: 'Un écosystème', p: "Même rôle qu'un maillage de bassins d'orage, plus rentable à long terme : il épure, accueille la biodiversité, soutient l'agriculture.", ch: [['−250 €', 'par an et par maison']], f: ['pluie'] },
|
||||
{ n: 6, h: ['h6'], t: 'Cuve de décantation', p: "Les eaux vannes décantent ; la matière se dépose et part vers le biométhaniseur.", ch: [], f: ['usees'] },
|
||||
{ n: 7, h: ['h7'], t: 'Filtre vertical', p: "L'essentiel de l'épuration : les bactéries des rhizomes changent la boue en terreau. Une ligne curée chaque année donne des tonnes de compost.", ch: [], f: ['usees'] },
|
||||
{ n: 8, h: ['h8'], t: 'Biométhanisation', p: "Le biogaz produit reste modeste : au mieux, le gaz de la cuisine.", ch: [], f: ['usees'] },
|
||||
{ n: 9, h: ['h9'], t: 'Filtre horizontal', p: "Entre les rhizomes, les plantes traitent ce qui reste : métaux, composés chimiques, médicaments.", ch: [], f: ['usees'] },
|
||||
{ n: 10, h: ['h10'], t: 'Lagunage', p: "Dernière étape : l'eau s'oxygène, les UV éliminent bactéries et microbes.", ch: [], f: ['usees'] },
|
||||
{ n: 11, h: ['h11'], t: 'Rejet à la parcelle', p: "L'eau épurée n'est pas encore reconnue conforme par l'administration : on l'absorbe sur place.", ch: [], f: ['usees'] },
|
||||
];
|
||||
const svg = $('svg-eau'), NS = 'http://www.w3.org/2000/svg';
|
||||
for (const [k, l] of Object.entries(EAU.flux)) { const p = document.createElementNS(NS, 'path'); p.setAttribute('d', 'M' + l.map((q) => q.join(' ')).join(' L')); p.setAttribute('class', 'flux'); p.dataset.f = k; svg.appendChild(p); }
|
||||
const etapeDe = {}; ETAPES.forEach((e, i) => e.h.forEach((h) => (etapeDe[h] = i)));
|
||||
for (const [k, [x, y]] of Object.entries(EAU.pts)) {
|
||||
const g = document.createElementNS(NS, 'g'); g.setAttribute('class', 'pe'); g.setAttribute('transform', `translate(${x} ${y})`); g.dataset.h = k;
|
||||
g.setAttribute('tabindex', '0'); g.setAttribute('role', 'button'); g.setAttribute('aria-label', 'Étape ' + ETAPES[etapeDe[k]].n + ' : ' + ETAPES[etapeDe[k]].t);
|
||||
g.innerHTML = `<circle class="halo" r="16"/><circle class="coeur" r="14"/><text>${ETAPES[etapeDe[k]].n}</text>`;
|
||||
const go = () => { C.i = etapeDe[k]; C.pause = performance.now() + 20000; etape(); };
|
||||
g.addEventListener('click', go); g.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); go(); } });
|
||||
svg.appendChild(g);
|
||||
}
|
||||
const C = { i: 0, t0: 0, pause: 0, s: 1, x: 0, y: 0, vol: null };
|
||||
const plateau = $('plateau'), vp = $('vue-planche');
|
||||
const appliqueP = () => (plateau.style.transform = `translate(${C.x}px, ${C.y}px) scale(${C.s})`);
|
||||
function ajuste() { const r = vp.getBoundingClientRect(); return Math.min(r.width / 1615, r.height / 860); }
|
||||
function borneP(o, r) {
|
||||
// la planche couvre la vue quand elle est assez grande ; sinon elle reste centrée
|
||||
const w = 1615 * o.s, h = 860 * o.s;
|
||||
o.x = w >= r.width ? Math.min(0, Math.max(r.width - w, o.x)) : (r.width - w) / 2;
|
||||
o.y = h >= r.height ? Math.min(0, Math.max(r.height - h, o.y)) : (r.height - h) / 2;
|
||||
return o;
|
||||
}
|
||||
function viser(x, y, s) {
|
||||
const r = vp.getBoundingClientRect(), fin = borneP({ s, x: r.width / 2 - x * s, y: r.height / 2 - y * s }, r);
|
||||
if (reduit) { Object.assign(C, fin); appliqueP(); return; }
|
||||
const debut = { s: C.s, x: C.x, y: C.y }, t0 = performance.now();
|
||||
const pas = (now) => { const k = Math.min(1, (now - t0) / 900), e = k < 0.5 ? 2 * k * k : 1 - Math.pow(-2 * k + 2, 2) / 2; C.s = debut.s + (fin.s - debut.s) * e; C.x = debut.x + (fin.x - debut.x) * e; C.y = debut.y + (fin.y - debut.y) * e; appliqueP(); if (k < 1) C.vol = requestAnimationFrame(pas); };
|
||||
cancelAnimationFrame(C.vol); C.vol = requestAnimationFrame(pas);
|
||||
}
|
||||
function etape() {
|
||||
const e = ETAPES[C.i];
|
||||
$('etape').innerHTML = `<div class="couche" style="--c: var(--eau)">Étape ${e.n} sur 11</div><h3>${e.t}</h3><p>${e.p}</p>${e.ch.length ? `<div class="chiffres" style="--c: var(--eau)">${e.ch.map(([v, l]) => `<div><b>${v}</b><small>${l}</small></div>`).join('')}</div>` : ''}<div class="source">source : TMIP 2024, p. 13 ; concours Habiter Demain 2020, planche 6</div>`;
|
||||
$('compteur-eau').textContent = `${e.n} / 11`;
|
||||
svg.querySelectorAll('.pe').forEach((g) => g.classList.toggle('actif', e.h.includes(g.dataset.h)));
|
||||
svg.querySelectorAll('.flux').forEach((p) => p.classList.toggle('actif', e.f.includes(p.dataset.f)));
|
||||
const [x, y] = EAU.pts[e.h[0]], sf = ajuste(), r = vp.getBoundingClientRect();
|
||||
// en portrait, on zoome assez pour que le dessin remplisse la hauteur
|
||||
const zoom = r.width < r.height ? Math.max(sf * 2.3, (r.height * 0.98) / 860) : sf * 2.3;
|
||||
viser(x, y, Math.min(Math.max(zoom, sf), 1.25));
|
||||
C.t0 = performance.now();
|
||||
}
|
||||
function ouvrirCycle() { $('cycle').hidden = false; C.i = 0; C.pause = 0; const sf = ajuste(); C.s = sf; C.x = 0; C.y = 0; appliqueP(); etape(); $('b-retour').focus(); }
|
||||
function fermerCycle() { $('cycle').hidden = true; E.t0 = performance.now(); }
|
||||
$('b-retour').addEventListener('click', fermerCycle);
|
||||
addEventListener('keydown', (e) => { if (e.key === 'Escape' && !$('cycle').hidden) fermerCycle(); });
|
||||
$('e-prec').addEventListener('click', () => { C.i = (C.i + 10) % 11; C.pause = performance.now() + 20000; etape(); });
|
||||
$('e-suiv').addEventListener('click', () => { C.i = (C.i + 1) % 11; C.pause = performance.now() + 20000; etape(); });
|
||||
// pan et pince sur la planche
|
||||
const pp = new Map();
|
||||
vp.addEventListener('pointerdown', (e) => { if (e.target.closest('.pe')) return; vp.setPointerCapture(e.pointerId); pp.set(e.pointerId, [e.clientX, e.clientY]); C.pause = performance.now() + 20000; cancelAnimationFrame(C.vol); });
|
||||
vp.addEventListener('pointermove', (e) => {
|
||||
if (!pp.has(e.pointerId)) return; const av = [...pp.values()].map((p) => [...p]); pp.set(e.pointerId, [e.clientX, e.clientY]); const ap = [...pp.values()];
|
||||
const c = (L) => [L.reduce((s, p) => s + p[0], 0) / L.length, L.reduce((s, p) => s + p[1], 0) / L.length], a = c(av), b = c(ap);
|
||||
C.x += b[0] - a[0]; C.y += b[1] - a[1];
|
||||
if (av.length === 2 && ap.length === 2) { const r = vp.getBoundingClientRect(), dA = Math.hypot(av[0][0] - av[1][0], av[0][1] - av[1][1]), dB = Math.hypot(ap[0][0] - ap[1][0], ap[0][1] - ap[1][1]); const k = dB / dA, ns = Math.max(ajuste() * 0.9, Math.min(2.5, C.s * k)), kk = ns / C.s, px = b[0] - r.left, py = b[1] - r.top; C.x = px - (px - C.x) * kk; C.y = py - (py - C.y) * kk; C.s = ns; }
|
||||
appliqueP();
|
||||
});
|
||||
const finP = (e) => pp.delete(e.pointerId); vp.addEventListener('pointerup', finP); vp.addEventListener('pointercancel', finP);
|
||||
vp.addEventListener('wheel', (e) => { e.preventDefault(); const r = vp.getBoundingClientRect(), ns = Math.max(ajuste() * 0.9, Math.min(2.5, C.s * (e.deltaY > 0 ? 0.9 : 1.11))), kk = ns / C.s, px = e.clientX - r.left, py = e.clientY - r.top; C.x = px - (px - C.x) * kk; C.y = py - (py - C.y) * kk; C.s = ns; appliqueP(); C.pause = performance.now() + 20000; }, { passive: false });
|
||||
(function ticEau(now) {
|
||||
requestAnimationFrame(ticEau);
|
||||
if ($('cycle').hidden) return;
|
||||
if (now < C.pause) { C.t0 = now; $('temps-eau').style.width = '0%'; return; }
|
||||
const k = (now - C.t0) / 8000; $('temps-eau').style.width = Math.min(100, k * 100) + '%';
|
||||
if (k >= 1 && !reduit) { C.i = (C.i + 1) % 11; etape(); }
|
||||
})(performance.now());
|
||||
|
||||
// ---------------------------------------------------------------- départ
|
||||
majLecture(); onglets(); montrer(false);
|
||||
if (!T3) { /* sans WebGL : les fiches restent lisibles sur l'image du plan */ }
|
||||
document.fonts && document.fonts.ready.then(() => {});
|
||||
if (!T3) (function boucleSans(now) { requestAnimationFrame(boucleSans); tic(now); })(performance.now());
|
||||
// démarrage : un premier survol sur la première fiche ; liens directs #aujourdhui et #cycle
|
||||
if (location.hash === '#aujourdhui') setEpoque('auj');
|
||||
if (T3) setTimeout(() => montrer(true), 400);
|
||||
if (location.hash === '#cycle') setTimeout(ouvrirCycle, 300);
|
||||
})();
|
||||
</script>
|
||||
@@ -0,0 +1 @@
|
||||
{"dpi": 100, "s": 6.149999999999984, "x0": 348.346, "y1": 542.703, "dx": -1, "dy": -1}
|
||||
Reference in New Issue
Block a user