fixed typographic traitment, moved js alog to header defer script for speed, no more flickering

This commit is contained in:
2026-09-11 21:36:42 +02:00
parent 4efafd0e22
commit 9dc1272631
10 changed files with 268 additions and 182 deletions
+1 -168
View File
@@ -3,8 +3,6 @@ import '../scss/main.scss'
import "splitting/dist/splitting.css";
import "splitting/dist/splitting-cells.css";
import Splitting from "splitting";
import { hyphenateSync } from "hyphen/fr";
import { gsap, Power1 } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
import Lenis from 'lenis';
@@ -53,7 +51,6 @@ gsap.registerPlugin(ScrollTrigger);
console.log('LeShedTheme init()');
console.log('deploy test to delete');
initTitles();
initBurgerMenu();
initSmoothScroll();
initHomeColumnScrollSync();
@@ -71,170 +68,6 @@ gsap.registerPlugin(ScrollTrigger);
})
}
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
// Rendu typographique des titres. Police Epilogue = axe variable `wght`
// (100900) uniquement ; l'italique est un fichier séparé (font-style).
const TITLE_STYLE = {
// 3 groupes de graisse : thin = majorité (défaut), medium = quelques,
// bold = les pics. Les fines ne sont pas comptées (c'est le reste).
wghtThin: { min: 100, max: 200 }, // majorité (défaut)
wghtMedium: { min: 200, max: 400 }, // quelques
wghtBold: { min: 800, max: 900 }, // pics
boldRatio: 0.28, // proportion de pics gras par mot
mediumRatio: 0.12, // proportion de medium par mot
italicOnBold: 0.6, // proba d'italique sur un pic
italicOnMedium: 0.4, // proba d'italique sur un medium
italicOnThin: 0.30, // proba d'italique sur une fine
};
function initTitles() {
const titles = document.querySelectorAll('\
#block-leshed-identitedusite>a, \
article.node-type-projet>header>h2,\
article.node-type-projet>.cols>.left>h2,\
article.node-type-projet.view-mode-full h2.node-title,\
article.node-type-evenement>header>h2,\
article.node-type-evenement>header>.field_dates>time,\
article.node-type-evenement.view-mode-full h2.node-title,\
article.node-type-evenement.view-mode-full .field_dates.digit-dates span.date-part\
');
for (const txt of titles) {
txt.classList.add("variable-title");
const splitted = Splitting({ target: txt, by: 'chars' });
for (const word of splitted[0].words) {
const chars = Array.from(word.getElementsByClassName('char'));
styleWordChars(chars);
hyphenateWord(chars);
}
}
}
/**
* Tire k positions réparties régulièrement dans [0, n[ : le mot est découpé
* en k cases contiguës, une position au hasard par case (aléa contrôlé, pas
* de paquets). Les positions déjà prises par un autre groupe sont évitées.
*/
function spreadPositions(n, k, exclude = new Set()) {
const set = new Set();
for (let i = 0; i < k; i++) {
const start = Math.floor((i * n) / k);
const end = Math.floor(((i + 1) * n) / k); // borne exclue
const span = Math.max(1, end - start);
let pos = randomInt(start, start + span);
let guard = 0;
while ((exclude.has(pos) || set.has(pos)) && guard < span) {
pos = start + ((pos - start + 1) % span);
guard++;
}
set.add(pos);
}
return set;
}
/**
* Applique graisse + italique caractère par caractère.
*
* Majorité de fines ; quelques medium et quelques pics gras, chacun réparti
* régulièrement dans le mot. L'italique est corrélé à la graisse.
*/
function styleWordChars(chars) {
const n = chars.length;
if (!n) return;
// Accents répartis régulièrement : d'abord les pics gras, puis les medium
// (en évitant les positions déjà grasses). Le reste sera fin.
const bold = spreadPositions(n, Math.round(n * TITLE_STYLE.boldRatio));
const medium = spreadPositions(n, Math.round(n * TITLE_STYLE.mediumRatio), bold);
// 1) Décide graisse + italique pour chaque caractère.
const decisions = chars.map((char, i) => {
let range, pItal;
if (bold.has(i)) {
range = TITLE_STYLE.wghtBold;
pItal = TITLE_STYLE.italicOnBold;
}
else if (medium.has(i)) {
range = TITLE_STYLE.wghtMedium;
pItal = TITLE_STYLE.italicOnMedium;
}
else {
range = TITLE_STYLE.wghtThin;
pItal = TITLE_STYLE.italicOnThin;
}
return {
char,
wght: randomInt(range.min, range.max + 1),
ital: Math.random() < pItal,
};
});
// 2) Au moins une italique par mot : si aucune, on en force une, de
// préférence sur un pic gras ou un medium, sinon sur une lettre au hasard.
if (!decisions.some((d) => d.ital)) {
const accents = [...bold, ...medium];
const idx = accents.length ? accents[randomInt(0, accents.length)] : randomInt(0, n);
decisions[idx].ital = true;
}
// 3) Pas plus de 2 italiques consécutives : on casse les séries de 3+.
let run = 0;
for (const d of decisions) {
if (d.ital) {
run += 1;
if (run > 2) {
d.ital = false;
run = 0;
}
}
else {
run = 0;
}
}
// 4) Applique.
for (const { char, wght, ital } of decisions) {
char.style.fontVariationSettings = `'wght' ${wght}`;
char.style.fontStyle = ital ? 'italic' : 'normal';
char.classList.toggle('italic', ital);
char.classList.toggle('normal', !ital);
}
}
/**
* Insère des césures logicielles (U+00AD) aux points de coupure valides.
*
* Comme chaque lettre est un span (display:inline) sans « run » de texte
* continu, `hyphens: auto` est inopérant : on calcule donc les points de
* césure (motifs FR) et on insère un soft hyphen — en nœud texte, entre les
* lettres concernées — qui n'autorise une coupure (avec tiret) qu'à ces
* endroits. Les mots trop longs coupent proprement au lieu de casser
* n'importe où.
*/
function hyphenateWord(chars) {
if (chars.length < 5) return;
const SOFT = '\u00AD';
const text = chars.map((c) => c.textContent).join('');
const hyphenated = hyphenateSync(text);
if (!hyphenated.includes(SOFT)) return;
let letter = 0;
for (const ch of hyphenated) {
if (ch === SOFT) {
const target = chars[letter];
if (letter > 0 && target && target.parentNode) {
target.parentNode.insertBefore(document.createTextNode(SOFT), target);
}
}
else {
letter++;
}
}
}
function initBurgerMenu() {
let header_right = document.getElementById('burger-btn');
header_right.parentElement.addEventListener('click', function(e){
@@ -373,7 +206,7 @@ gsap.registerPlugin(ScrollTrigger);
const leftFreezeStart = () => {
const overflow = leftColumn.offsetHeight - scroller.clientHeight;
if (overflow > 0) return overflow;
return pageTitle ? pageTitle.offsetHeight * (1 - titleScaleTarget) : 0;
return initheaderH ? initheaderH.offsetHeight * (1 - titleScaleTarget) : 0;
};
gsap_mm.add('(min-width: 768px)', () => {
@@ -0,0 +1,173 @@
// Chargé en defer dans le <head> : s'exécute avant main.js (defer, fin de body).
import Splitting from "splitting";
import { hyphenateSync } from "hyphen/fr";
// À garder identique à $variable-title-selectors (main.scss), qui masque ces
// titres jusqu'à `is-ready`.
const TITLE_SELECTORS = '\
#block-leshed-identitedusite>a, \
article.node-type-projet>header>h2,\
article.node-type-projet>.cols>.left>h2,\
article.node-type-projet.view-mode-full h2.node-title,\
article.node-type-evenement>header>h2,\
article.node-type-evenement>header>.field_dates>time,\
article.node-type-evenement.view-mode-full h2.node-title,\
article.node-type-evenement.view-mode-full .field_dates.digit-dates span.date-part\
';
// Police Epilogue = axe variable `wght` (100900) uniquement ; l'italique est
// un fichier séparé (font-style).
const TITLE_STYLE = {
// 3 groupes de graisse : thin = majorité (défaut), medium = quelques,
// bold = les pics. Les fines ne sont pas comptées (c'est le reste).
wghtThin: { min: 100, max: 200 }, // majorité (défaut)
wghtMedium: { min: 200, max: 400 }, // quelques
wghtBold: { min: 800, max: 900 }, // pics
boldRatio: 0.28, // proportion de pics gras par mot
mediumRatio: 0.12, // proportion de medium par mot
italicOnBold: 0.6, // proba d'italique sur un pic
italicOnMedium: 0.4, // proba d'italique sur un medium
italicOnThin: 0.30, // proba d'italique sur une fine
};
function randomInt(min, max) {
return Math.floor(Math.random() * (max - min)) + min;
}
function initTitles() {
const titles = document.querySelectorAll(TITLE_SELECTORS);
for (const txt of titles) {
txt.classList.add("variable-title");
const splitted = Splitting({ target: txt, by: 'chars' });
for (const word of splitted[0].words) {
const chars = Array.from(word.getElementsByClassName('char'));
styleWordChars(chars);
hyphenateWord(chars);
}
txt.classList.add("is-ready");
}
}
/**
* Tire k positions réparties régulièrement dans [0, n[ : le mot est découpé
* en k cases contiguës, une position au hasard par case (aléa contrôlé, pas
* de paquets). Les positions déjà prises par un autre groupe sont évitées.
*/
function spreadPositions(n, k, exclude = new Set()) {
const set = new Set();
for (let i = 0; i < k; i++) {
const start = Math.floor((i * n) / k);
const end = Math.floor(((i + 1) * n) / k); // borne exclue
const span = Math.max(1, end - start);
let pos = randomInt(start, start + span);
let guard = 0;
while ((exclude.has(pos) || set.has(pos)) && guard < span) {
pos = start + ((pos - start + 1) % span);
guard++;
}
set.add(pos);
}
return set;
}
/**
* Applique graisse + italique caractère par caractère.
*
* Majorité de fines ; quelques medium et quelques pics gras, chacun réparti
* régulièrement dans le mot. L'italique est corrélé à la graisse.
*/
function styleWordChars(chars) {
const n = chars.length;
if (!n) return;
// Accents répartis régulièrement : d'abord les pics gras, puis les medium
// (en évitant les positions déjà grasses). Le reste sera fin.
const bold = spreadPositions(n, Math.round(n * TITLE_STYLE.boldRatio));
const medium = spreadPositions(n, Math.round(n * TITLE_STYLE.mediumRatio), bold);
// 1) Décide graisse + italique pour chaque caractère.
const decisions = chars.map((char, i) => {
let range, pItal;
if (bold.has(i)) {
range = TITLE_STYLE.wghtBold;
pItal = TITLE_STYLE.italicOnBold;
}
else if (medium.has(i)) {
range = TITLE_STYLE.wghtMedium;
pItal = TITLE_STYLE.italicOnMedium;
}
else {
range = TITLE_STYLE.wghtThin;
pItal = TITLE_STYLE.italicOnThin;
}
return {
char,
wght: randomInt(range.min, range.max + 1),
ital: Math.random() < pItal,
};
});
// 2) Au moins une italique par mot : si aucune, on en force une, de
// préférence sur un pic gras ou un medium, sinon sur une lettre au hasard.
if (!decisions.some((d) => d.ital)) {
const accents = [...bold, ...medium];
const idx = accents.length ? accents[randomInt(0, accents.length)] : randomInt(0, n);
decisions[idx].ital = true;
}
// 3) Pas plus de 2 italiques consécutives : on casse les séries de 3+.
let run = 0;
for (const d of decisions) {
if (d.ital) {
run += 1;
if (run > 2) {
d.ital = false;
run = 0;
}
}
else {
run = 0;
}
}
// 4) Applique.
for (const { char, wght, ital } of decisions) {
char.style.fontVariationSettings = `'wght' ${wght}`;
char.style.fontStyle = ital ? 'italic' : 'normal';
char.classList.toggle('italic', ital);
char.classList.toggle('normal', !ital);
}
}
/**
* Insère des césures logicielles (U+00AD) aux points de coupure valides.
*
* Comme chaque lettre est un span (display:inline) sans « run » de texte
* continu, `hyphens: auto` est inopérant : on calcule donc les points de
* césure (motifs FR) et on insère un soft hyphen — en nœud texte, entre les
* lettres concernées — qui n'autorise une coupure (avec tiret) qu'à ces
* endroits. Les mots trop longs coupent proprement au lieu de casser
* n'importe où.
*/
function hyphenateWord(chars) {
if (chars.length < 5) return;
const SOFT = '\u00AD';
const text = chars.map((c) => c.textContent).join('');
const hyphenated = hyphenateSync(text);
if (!hyphenated.includes(SOFT)) return;
let letter = 0;
for (const ch of hyphenated) {
if (ch === SOFT) {
const target = chars[letter];
if (letter > 0 && target && target.parentNode) {
target.parentNode.insertBefore(document.createTextNode(SOFT), target);
}
}
else {
letter++;
}
}
}
initTitles();