AVIF q60 par défaut et sélecteur de mode de fusion par couche
This commit is contained in:
@ -212,6 +212,12 @@ DEFAULT_LAYERS = ('positive_openness', 'aspect')
|
||||
# Opacité par défaut des couches allumées (0–1) ; les absentes restent à 1.
|
||||
DEFAULT_OPACITY = {'aspect': 0.6}
|
||||
|
||||
# Mode de fusion (mix-blend-mode CSS) par couche : les couches d'ombrage en
|
||||
# niveaux de gris fondent en « multiply » — elles assombrissent le relief sans
|
||||
# délaver la rampe de couleurs en dessous, contrairement à la transparence.
|
||||
# Les couches absentes restent en « normal » (simple transparence).
|
||||
DEFAULT_BLEND = {'hillshade_multi': 'multiply', 'solar': 'multiply', 'svf': 'multiply'}
|
||||
|
||||
# Couche « principale » (mise en avant ; index_tiles.json + affichage en
|
||||
# direct de la webapp).
|
||||
DEFAULT_VIZ = DEFAULT_LAYERS[0]
|
||||
@ -1123,6 +1129,7 @@ def build_index(output_dir, output_format='avif'):
|
||||
'stats': {'default_viz': default_viz,
|
||||
'default_layers': default_layers_present(all_viz_keys),
|
||||
'default_opacity': dict(DEFAULT_OPACITY),
|
||||
'default_blend': dict(DEFAULT_BLEND),
|
||||
'n_tiles': len(display_tiles)},
|
||||
}, ensure_ascii=False), encoding='utf-8')
|
||||
|
||||
@ -1239,6 +1246,7 @@ def _render_html(tiles, global_bbox, all_viz_keys, output_format):
|
||||
{'default_viz': default_viz,
|
||||
'default_layers': default_layers_present(all_viz_keys),
|
||||
'default_opacity': dict(DEFAULT_OPACITY),
|
||||
'default_blend': dict(DEFAULT_BLEND),
|
||||
'n_tiles': len(tiles)},
|
||||
)
|
||||
|
||||
@ -1589,6 +1597,15 @@ select, input[type="checkbox"] { accent-color: var(--accent); }
|
||||
padding: 5px 6px; border-radius: 6px;
|
||||
user-select: none;
|
||||
}
|
||||
/* Ligne allumée : un sélecteur de mode de fusion s'insère avant le slider
|
||||
(les lignes éteintes gardent la grille large pour un nom lisible). */
|
||||
.layer-row.on { grid-template-columns: 16px 24px minmax(0, 1fr) 64px 62px 24px; }
|
||||
.layer-blend {
|
||||
font-size: 10.5px; color: var(--muted); background: rgba(255, 255, 255, 0.05);
|
||||
border: 1px solid var(--panel-border); border-radius: 4px;
|
||||
padding: 2px 2px; min-width: 0; width: 100%; height: 22px;
|
||||
}
|
||||
.layer-blend:hover { color: var(--text); }
|
||||
#osmRow { grid-template-columns: 24px minmax(0, 1fr) 80px 30px; }
|
||||
.layer-row:hover { background: rgba(255, 255, 255, 0.05); }
|
||||
.layer-row.dragging { opacity: 0.45; }
|
||||
@ -1667,6 +1684,10 @@ input[type="range"]::-moz-range-thumb {
|
||||
/* Fond transparent obligatoire : l'opacité des couches doit laisser voir
|
||||
les couches du dessous et le fond de carte, pas un rectangle noir. */
|
||||
background: transparent;
|
||||
/* Les modes de fusion (mix-blend-mode) des <img> se cantonnent à la pile de
|
||||
couches de la tuile : sans isolation, « multiply » assombrirait aussi le
|
||||
fond de carte OSM sous les zones sans données. */
|
||||
isolation: isolate;
|
||||
/* Layout et peinture des tuiles hors écran skippés par le navigateur */
|
||||
content-visibility: auto;
|
||||
}
|
||||
@ -1998,8 +2019,28 @@ L.control.scale({ metric: true, imperial: false, position: 'bottomleft' }).addTo
|
||||
// État des couches (persisté en localStorage)
|
||||
// ============================================================
|
||||
// order : pile de visualisations, index 0 = bas de pile.
|
||||
// blend : mode de fusion CSS (mix-blend-mode) de chaque couche — les couches
|
||||
// se fondent entre elles au sein de la tuile (isolation: isolate sur .ltile)
|
||||
// au lieu de simplement se superposer en transparence.
|
||||
const LS_KEY = 'lidarLayers_v1';
|
||||
|
||||
// Modes de fusion proposés : [valeur CSS, libellé]. « multiply » assombrit le
|
||||
// relief (ombrage par-dessus une rampe), « overlay » / « soft-light » creusent
|
||||
// le contraste local, « screen » éclaircit (openness négative sur fond sombre).
|
||||
const BLEND_MODES = [
|
||||
['normal', 'Normal'],
|
||||
['multiply', 'Produit'],
|
||||
['overlay', 'Superposé'],
|
||||
['soft-light', 'Doux'],
|
||||
['screen', 'Écran'],
|
||||
];
|
||||
const BLEND_KEYS = BLEND_MODES.map(m => m[0]);
|
||||
const blendLabel = v => {
|
||||
const m = BLEND_MODES.find(x => x[0] === v);
|
||||
return m ? m[1] : 'Normal';
|
||||
};
|
||||
const validBlend = v => BLEND_KEYS.includes(v) ? v : null;
|
||||
|
||||
function defaultLayers() {
|
||||
const order = Object.keys(VIZ_META);
|
||||
// Couches activées par défaut : seules celles-ci sont allumées, toutes les
|
||||
@ -2023,14 +2064,18 @@ function defaultLayers() {
|
||||
// Opacités par défaut (STATS.default_opacity) ; absentes → 1.
|
||||
const defOp = (STATS.default_opacity && typeof STATS.default_opacity === 'object')
|
||||
? STATS.default_opacity : {};
|
||||
// Modes de fusion par défaut (STATS.default_blend) ; absents → normal.
|
||||
const defBl = (STATS.default_blend && typeof STATS.default_blend === 'object')
|
||||
? STATS.default_blend : {};
|
||||
const onSet = new Set(defOrder);
|
||||
const on = {}, opacity = {};
|
||||
const on = {}, opacity = {}, blend = {};
|
||||
for (const k of order) {
|
||||
on[k] = onSet.has(k);
|
||||
const op = Number(defOp[k]);
|
||||
opacity[k] = Number.isNaN(op) ? 1 : Math.min(1, Math.max(0, op));
|
||||
blend[k] = validBlend(defBl[k]) || 'normal';
|
||||
}
|
||||
return { order, on, opacity, osm: { on: true, opacity: 0.85, dark: true } };
|
||||
return { order, on, opacity, blend, osm: { on: true, opacity: 0.85, dark: true } };
|
||||
}
|
||||
|
||||
function loadLayers() {
|
||||
@ -2045,19 +2090,28 @@ function loadLayers() {
|
||||
for (const k of Object.keys(VIZ_META)) {
|
||||
if (!order.includes(k)) order.push(k);
|
||||
}
|
||||
const on = {}, opacity = {};
|
||||
const on = {}, opacity = {}, blend = {};
|
||||
const savedBlend = (saved.blend && typeof saved.blend === 'object') ? saved.blend : {};
|
||||
for (const k of order) {
|
||||
const op = saved.opacity ? saved.opacity[k] : undefined;
|
||||
on[k] = saved.on && saved.on[k] === true;
|
||||
opacity[k] = (op === undefined || op === null || Number.isNaN(Number(op)))
|
||||
? 1 : Math.min(1, Math.max(0, Number(op)));
|
||||
blend[k] = validBlend(savedBlend[k]) || def.blend[k] || 'normal';
|
||||
}
|
||||
st = { order, on, opacity, osm: Object.assign({}, def.osm, saved.osm || {}) };
|
||||
st = { order, on, opacity, blend, osm: Object.assign({}, def.osm, saved.osm || {}) };
|
||||
}
|
||||
} catch (e) { st = def; }
|
||||
// Lien partagé (vue + couches + transparences) : il prime sur l'état local.
|
||||
// Le lien ne transporte pas le mode de fusion : repli sur les défauts.
|
||||
if (sharedHash) {
|
||||
if (sharedHash.layers) st = sharedHash.layers;
|
||||
if (sharedHash.layers) {
|
||||
st = sharedHash.layers;
|
||||
if (!st.blend || typeof st.blend !== 'object') st.blend = {};
|
||||
for (const k of st.order) {
|
||||
if (!validBlend(st.blend[k])) st.blend[k] = def.blend[k] || 'normal';
|
||||
}
|
||||
}
|
||||
if (sharedHash.osm) st = Object.assign({}, st, { osm: sharedHash.osm });
|
||||
}
|
||||
return st;
|
||||
@ -2216,6 +2270,7 @@ function syncTileLayers() {
|
||||
}
|
||||
img.style.zIndex = String(i + 1);
|
||||
img.style.opacity = String(state.layers.opacity[k]);
|
||||
img.style.mixBlendMode = state.layers.blend[k] || 'normal';
|
||||
});
|
||||
}
|
||||
updateImages();
|
||||
@ -2357,11 +2412,21 @@ function renderLayerPanel() {
|
||||
row.draggable = !!(e.target.closest && e.target.closest('.layer-grip'));
|
||||
});
|
||||
row.title = meta.label;
|
||||
const curBlend = state.layers.blend[key] || 'normal';
|
||||
// Sélecteur de mode de fusion seulement sur les couches allumées :
|
||||
// les lignes éteintes gardent la grille large (nom lisible).
|
||||
const blendSel =
|
||||
'<select class="layer-blend" title="Mode de fusion avec les couches en dessous">' +
|
||||
BLEND_MODES.map(m =>
|
||||
'<option value="' + m[0] + '"' + (m[0] === curBlend ? ' selected' : '') + '>' +
|
||||
m[1] + '</option>').join('') +
|
||||
'</select>';
|
||||
row.innerHTML =
|
||||
'<span class="layer-grip" title="Glisser pour réordonner">⠿</span>' +
|
||||
'<button class="eye' + (state.layers.on[key] ? ' on' : '') + '" title="Afficher / masquer">' +
|
||||
(state.layers.on[key] ? SVG_EYE : SVG_EYE_OFF) + '</button>' +
|
||||
'<span class="layer-name">' + escapeHtml(meta.label) + '</span>' +
|
||||
(state.layers.on[key] ? blendSel : '') +
|
||||
'<input type="range" min="0" max="100" step="1" value="' +
|
||||
Math.round(state.layers.opacity[key] * 100) + '" title="Opacité">' +
|
||||
'<span class="layer-pct">' + Math.round(state.layers.opacity[key] * 100) + '%</span>';
|
||||
@ -2391,6 +2456,17 @@ function renderLayerPanel() {
|
||||
});
|
||||
range.addEventListener('change', saveLayers);
|
||||
|
||||
const sel = row.querySelector('.layer-blend');
|
||||
if (sel) sel.addEventListener('change', () => {
|
||||
state.layers.blend[key] = sel.value;
|
||||
saveLayers();
|
||||
// Applique directement sans reconstruire le DOM
|
||||
for (const entry of markers.values()) {
|
||||
const img = entry.imgs[key];
|
||||
if (img) img.style.mixBlendMode = sel.value;
|
||||
}
|
||||
});
|
||||
|
||||
row.addEventListener('dragstart', () => { dragKey = key; row.classList.add('dragging'); });
|
||||
row.addEventListener('dragend', () => { dragKey = null; row.classList.remove('dragging'); row.draggable = false; });
|
||||
row.addEventListener('dragover', e => e.preventDefault());
|
||||
@ -2488,7 +2564,9 @@ const BUILTIN_PRESETS = [
|
||||
function presetDetail(p) {
|
||||
const parts = (p.on || []).filter(k => VIZ_META[k])
|
||||
.map(k => (VIZ_META[k].label || k) + ' ' +
|
||||
Math.round(((p.opacity && p.opacity[k] != null) ? Number(p.opacity[k]) : 1) * 100) + '%');
|
||||
Math.round(((p.opacity && p.opacity[k] != null) ? Number(p.opacity[k]) : 1) * 100) + '%' +
|
||||
((p.blend && validBlend(p.blend[k]) && p.blend[k] !== 'normal')
|
||||
? ' (' + blendLabel(p.blend[k]) + ')' : ''));
|
||||
if (p.osm) parts.push('Fond ' + (p.osm.on
|
||||
? Math.round((p.osm.opacity != null ? Number(p.osm.opacity) : 0.85) * 100) + '%'
|
||||
: 'masqué'));
|
||||
@ -2504,7 +2582,10 @@ function applyPreset(p) {
|
||||
for (const k of L.order) {
|
||||
const on = want.includes(k);
|
||||
L.on[k] = on;
|
||||
if (on) L.opacity[k] = (p.opacity && k in p.opacity) ? Number(p.opacity[k]) : 1;
|
||||
if (on) {
|
||||
L.opacity[k] = (p.opacity && k in p.opacity) ? Number(p.opacity[k]) : 1;
|
||||
if (p.blend && validBlend(p.blend[k])) L.blend[k] = p.blend[k];
|
||||
}
|
||||
}
|
||||
// Niveaux de transparence du fond de carte (visibilité, opacité, sombre)
|
||||
if (p.osm) {
|
||||
@ -2569,6 +2650,7 @@ presetSaveEl.addEventListener('click', () => {
|
||||
label: name.trim(),
|
||||
on: enabled,
|
||||
opacity: Object.fromEntries(enabled.map(k => [k, state.layers.opacity[k]])),
|
||||
blend: Object.fromEntries(enabled.map(k => [k, state.layers.blend[k] || 'normal'])),
|
||||
// Niveaux de transparence du fond de carte
|
||||
osm: { on: osm.on, opacity: osm.opacity, dark: osm.dark },
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user