Mode clair / sombre et thèmes de saison
This commit is contained in:
parent
da05adede1
commit
17cf2c8380
19 changed files with 3563 additions and 0 deletions
322
assets/js/orb.js
Normal file
322
assets/js/orb.js
Normal file
|
|
@ -0,0 +1,322 @@
|
|||
/* orb.js : Orbe de particules persistante (vanilla JS)
|
||||
<canvas id="orb-canvas"></canvas> en position:fixed; inset:0; z-index:0; pointer-events:none
|
||||
YBOrb.init('#orb-canvas', { sections: { works:'#travaux', about:'#apropos', cta:'#cta' } });
|
||||
Sur les pages internes (sans sections), l'orbe reste dans son état "hero". */
|
||||
window.YBOrb = (function () {
|
||||
let cv, ctx, cw, ch, run = false, secs = {};
|
||||
const m = { x: -9999, y: -9999 };
|
||||
const pts = [], rx = [], ry = [];
|
||||
const N = 550;
|
||||
for (let i = 0; i < N; i++) {
|
||||
const y = 1 - (i / (N - 1)) * 2, r = Math.sqrt(1 - y * y), th = i * 2.39996323;
|
||||
pts.push([Math.cos(th) * r, y, Math.sin(th) * r]);
|
||||
rx.push(Math.sin(i * 12.9898) * 0.9 + Math.sin(i * 0.7) * 0.4);
|
||||
ry.push(Math.cos(i * 78.233) * 0.9 + Math.cos(i * 1.3) * 0.4);
|
||||
}
|
||||
// États [cx, cy, R, éclatement, alpha] (fractions du viewport)
|
||||
const A = [0.72, 0.42, 0.34, 0, 1]; // hero : grande, à droite
|
||||
const B = [0.11, 0.24, 0.16, 0, 0.55]; // travaux : petite lune en haut à gauche
|
||||
const C = [0.5, 0.48, 0.46, 1, 0.4]; // à propos : éclatée en champ d'étoiles
|
||||
const D = [0.76, 0.52, 0.3, 0, 0.95]; // CTA : reformée
|
||||
const c01 = v => Math.max(0, Math.min(1, v));
|
||||
const ease = k => k * k * (3 - 2 * k);
|
||||
const mix = (a, b, k) => a.map((v, i) => v + (b[i] - v) * k);
|
||||
const top = el => el ? el.getBoundingClientRect().top : null;
|
||||
/* Couleurs lues dans le thème (--lime-rgb / --ink-rgb), relues à chaque bascule clair/sombre */
|
||||
let LIME = '211,253,80', INK = '245,245,240', GOLD = '242,193,78';
|
||||
function readColors() {
|
||||
const cs = getComputedStyle(document.documentElement);
|
||||
LIME = cs.getPropertyValue('--lime-rgb').trim() || LIME;
|
||||
INK = cs.getPropertyValue('--ink-rgb').trim() || INK;
|
||||
GOLD = cs.getPropertyValue('--season-2-rgb').trim() || GOLD;
|
||||
}
|
||||
/* Halloween : visage de citrouille (repère de la sphère, y vers le bas, rayon 1) */
|
||||
const inTri = (x, y, a, b, c) => {
|
||||
const d = (p, q, r) => (p[0] - r[0]) * (q[1] - r[1]) - (q[0] - r[0]) * (p[1] - r[1]);
|
||||
const P = [x, y], d1 = d(P, a, b), d2 = d(P, b, c), d3 = d(P, c, a);
|
||||
return !((d1 < 0 || d2 < 0 || d3 < 0) && (d1 > 0 || d2 > 0 || d3 > 0));
|
||||
};
|
||||
const EYE_L = [[-0.55, -0.06], [-0.17, -0.06], [-0.36, -0.42]], EYE_R = [[0.17, -0.06], [0.55, -0.06], [0.36, -0.42]];
|
||||
const NOSE = [[-0.08, 0.13], [0.08, 0.13], [0, 0.01]];
|
||||
const smile = x => { const k = 1 - (x / 0.64) * (x / 0.64); return [0.2 + 0.13 * k, 0.27 + 0.3 * k]; };
|
||||
function inFace(x, y) {
|
||||
if (inTri(x, y, ...EYE_L) || inTri(x, y, ...EYE_R) || inTri(x, y, ...NOSE)) return true;
|
||||
if (Math.abs(x) > 0.64) return false;
|
||||
const [t, b] = smile(x);
|
||||
if (y < t || y > b) return false;
|
||||
if (Math.abs(Math.abs(x) - 0.22) < 0.07 && y < t + 0.09) return false; // dents du haut
|
||||
if (Math.abs(x) < 0.08 && y > b - 0.09) return false; // dent du bas
|
||||
return true;
|
||||
}
|
||||
/* Autres fêtes : forme de l'orbe [kx, ky], pulsation, couleur des points, décor dessous / dessus.
|
||||
point() renvoie [rgb, alpha, taille] ou rien (couleur par défaut). C = { LIME, INK, GOLD } */
|
||||
const tag = (c, x, y, w, h, txt, C, a) => { // étiquette de prix suspendue
|
||||
c.save(); c.translate(x, y); c.rotate(0.12);
|
||||
c.fillStyle = 'rgba(' + C.LIME + ',' + a + ')';
|
||||
c.beginPath(); c.moveTo(0, 0); c.lineTo(w, 0); c.lineTo(w, h); c.lineTo(0, h); c.lineTo(-h * 0.45, h / 2); c.closePath(); c.fill();
|
||||
c.fillStyle = 'rgba(0,0,0,' + a + ')'; c.font = '800 ' + Math.round(h * 0.46) + 'px "JetBrains Mono", monospace';
|
||||
c.textBaseline = 'middle'; c.fillText(txt, h * 0.12, h / 2 + 1);
|
||||
c.beginPath(); c.arc(-h * 0.12, h / 2, h * 0.09, 0, 6.283); c.fill(); c.restore();
|
||||
};
|
||||
const PASTEL = ['255,159,203', '142,230,194', '255,222,120', '190,160,255', '140,200,255'];
|
||||
const SEASON = {
|
||||
'nouvel-an': {
|
||||
point: (i, p, x, y, z, lime, a, t) => { const tw = Math.sin(t * 4 + i * 1.7); return tw > 0.75 ? ['255,255,255', Math.min(1, a + 0.5), 1.5] : [i % 2 ? GOLD_() : LIME, a * 1.1, 1]; },
|
||||
over: (c, cx, cy, R, t, k, C) => { // boule à facettes suspendue
|
||||
c.strokeStyle = 'rgba(' + C.INK + ',' + 0.3 * k + ')'; c.lineWidth = 1;
|
||||
c.beginPath(); c.moveTo(cx, 0); c.lineTo(cx, cy - R * 1.02); c.stroke();
|
||||
for (let j = 0; j < 4; j++) { const ang = t * 0.6 + j * 1.57, rr = R * 1.12; // reflets qui tournent
|
||||
c.fillStyle = 'rgba(255,255,255,' + 0.5 * k * (0.5 + 0.5 * Math.sin(t * 3 + j)) + ')';
|
||||
c.beginPath(); c.arc(cx + Math.cos(ang) * rr, cy + Math.sin(ang) * rr * 0.35, 2.2, 0, 6.283); c.fill(); }
|
||||
}
|
||||
},
|
||||
'juillet': {
|
||||
point: (i, p, x, y, z, lime, a) => [x < -0.33 ? '40,90,255' : x > 0.33 ? '255,60,80' : INK, a * 1.15, 1]
|
||||
},
|
||||
'st-patrick': {
|
||||
point: (i, p, x, y, z, lime, a, t) => i % 9 === 0 ? [GOLD, Math.min(1, a + 0.3 * (0.5 + 0.5 * Math.sin(t * 3 + i))), 1.3] : [i % 3 ? LIME : INK, a, 1],
|
||||
under: (c, cx, cy, R, t, k) => { // arc-en-ciel derrière l'orbe
|
||||
['255,80,80', '255,160,60', '255,225,80', '80,210,120', '80,150,255', '160,100,255'].forEach((col, j) => {
|
||||
c.strokeStyle = 'rgba(' + col + ',' + 0.16 * k + ')'; c.lineWidth = R * 0.035;
|
||||
c.beginPath(); c.arc(cx, cy, R * (1.42 - j * 0.04), Math.PI * 1.12, Math.PI * 1.88); c.stroke();
|
||||
});
|
||||
}
|
||||
},
|
||||
'paques': {
|
||||
shape: [0.84, 1.1],
|
||||
point: (i, p, x, y, z, lime, a) => { const band = Math.floor((p[1] + 1) * 4.5); return [band % 2 ? PASTEL[band % PASTEL.length] : INK, a * (band % 2 ? 1.25 : 0.8), band % 2 ? 1.2 : 1]; }
|
||||
},
|
||||
'poisson': {
|
||||
shape: [1.14, 0.8],
|
||||
point: (i, p, x, y, z, lime, a) => [(Math.round(x * 6) + Math.round(y * 6)) % 2 ? LIME : GOLD, a * 1.05, 1],
|
||||
over: (c, cx, cy, R, t, k, C) => {
|
||||
const w = Math.sin(t * 5) * R * 0.08;
|
||||
c.fillStyle = 'rgba(' + C.LIME + ',' + 0.55 * k + ')';
|
||||
c.beginPath(); c.moveTo(cx + R * 1.02, cy); c.lineTo(cx + R * 1.5, cy - R * 0.42 + w); c.lineTo(cx + R * 1.38, cy + w * 0.3); c.lineTo(cx + R * 1.5, cy + R * 0.42 + w); c.closePath(); c.fill(); // queue
|
||||
c.beginPath(); c.moveTo(cx - R * 0.2, cy - R * 0.74); c.quadraticCurveTo(cx + R * 0.2, cy - R * 1.12, cx + R * 0.5, cy - R * 0.66); c.closePath(); c.fill(); // nageoire
|
||||
c.fillStyle = 'rgba(' + C.INK + ',' + k + ')'; c.beginPath(); c.arc(cx - R * 0.62, cy - R * 0.12, R * 0.09, 0, 6.283); c.fill(); // œil
|
||||
c.fillStyle = 'rgba(' + C.LIME + ',' + k + ')'; c.beginPath(); c.arc(cx - R * 0.6, cy - R * 0.14, R * 0.035, 0, 6.283); c.fill();
|
||||
for (let j = 0; j < 3; j++) { const ph = (t * 0.5 + j / 3) % 1; // bulles
|
||||
c.strokeStyle = 'rgba(' + C.INK + ',' + 0.45 * k * (1 - ph) + ')'; c.lineWidth = 1.2;
|
||||
c.beginPath(); c.arc(cx - R * 1.1 - Math.sin(ph * 9) * 6, cy - R * 0.2 - ph * R * 0.9, 3 + j * 2, 0, 6.283); c.stroke(); }
|
||||
}
|
||||
},
|
||||
'musique': {
|
||||
pulse: t => { const b = (t * 2) % 1; return 1 + 0.035 * Math.exp(-b * 6); }, // 120 BPM
|
||||
point: (i, p, x, y, z, lime, a, t) => [i % 4 === 0 ? GOLD : lime ? LIME : INK, a, 1],
|
||||
under: (c, cx, cy, R, t, k, C) => { // vinyle : sillons + macaron
|
||||
c.save(); c.translate(cx, cy); c.rotate(t * 1.4);
|
||||
for (let r = 0.3; r < 0.98; r += 0.06) { c.strokeStyle = 'rgba(' + C.INK + ',' + 0.07 * k + ')'; c.lineWidth = 1; c.beginPath(); c.arc(0, 0, R * r, 0, 6.283); c.stroke(); }
|
||||
for (let j = 0; j < 2; j++) { c.strokeStyle = 'rgba(255,255,255,' + 0.1 * k + ')'; c.lineWidth = R * 0.012; c.beginPath(); c.arc(0, 0, R * (0.5 + j * 0.2), -0.3 + j * 3.14, 0.3 + j * 3.14); c.stroke(); }
|
||||
c.fillStyle = 'rgba(' + C.LIME + ',' + 0.7 * k + ')'; c.beginPath(); c.arc(0, 0, R * 0.22, 0, 6.283); c.fill();
|
||||
c.fillStyle = 'rgba(' + C.GOLD + ',' + 0.8 * k + ')'; c.fillRect(-R * 0.12, -R * 0.03, R * 0.24, R * 0.06);
|
||||
c.fillStyle = 'rgba(0,0,0,' + 0.8 * k + ')'; c.beginPath(); c.arc(0, 0, R * 0.025, 0, 6.283); c.fill();
|
||||
c.restore();
|
||||
}
|
||||
},
|
||||
'anniversaire': {
|
||||
shape: [0.9, 1.06],
|
||||
point: (i, p, x, y, z, lime, a) => [i % 5 === 0 ? GOLD : i % 5 === 1 ? '255,107,129' : lime || i % 2 ? LIME : INK, a * 1.1, 1],
|
||||
over: (c, cx, cy, R, t, k, C) => { // ballon : nœud + ficelle qui ondule
|
||||
const by = cy + R * 1.06;
|
||||
c.fillStyle = 'rgba(' + C.LIME + ',' + 0.8 * k + ')';
|
||||
c.beginPath(); c.moveTo(cx, by - R * 0.02); c.lineTo(cx - R * 0.07, by + R * 0.1); c.lineTo(cx + R * 0.07, by + R * 0.1); c.closePath(); c.fill();
|
||||
c.strokeStyle = 'rgba(' + C.INK + ',' + 0.4 * k + ')'; c.lineWidth = 1.2;
|
||||
c.beginPath(); c.moveTo(cx, by + R * 0.1);
|
||||
for (let j = 1; j <= 12; j++) c.lineTo(cx + Math.sin(t * 2 + j * 0.8) * R * 0.05 * (j / 12), by + R * 0.1 + j * R * 0.07);
|
||||
c.stroke();
|
||||
}
|
||||
},
|
||||
'algerie': {
|
||||
// drapeau : moitié verte / moitié blanche, croissant et étoile rouges au centre
|
||||
point: (i, p, x, y, z, lime, a) => [x < 0 ? '0,150,75' : INK, a * 1.2, 1],
|
||||
over: (c, cx, cy, R, t, k) => {
|
||||
c.save(); c.fillStyle = 'rgba(210,16,52,' + 0.92 * k + ')'; c.shadowColor = 'rgba(210,16,52,' + 0.6 * k + ')'; c.shadowBlur = 18;
|
||||
// croissant : arc extérieur (r1) puis arc intérieur (r2) entre leurs deux points d'intersection
|
||||
const x1 = cx + R * 0.02, r1 = R * 0.36, x2 = cx + R * 0.11, r2 = R * 0.29, d = x2 - x1;
|
||||
const ix = (d * d + r1 * r1 - r2 * r2) / (2 * d), iy = Math.sqrt(Math.max(0, r1 * r1 - ix * ix));
|
||||
const a1 = Math.atan2(iy, ix), a2 = Math.atan2(iy, ix - d);
|
||||
c.beginPath(); c.arc(x1, cy, r1, a1, 2 * Math.PI - a1, false); c.arc(x2, cy, r2, 2 * Math.PI - a2, a2, true); c.closePath(); c.fill();
|
||||
const sx = cx + R * 0.2, sr = R * 0.12; c.beginPath();
|
||||
for (let j = 0; j < 10; j++) { const ang = -Math.PI / 2 + j * Math.PI / 5, rr = j % 2 ? sr * 0.42 : sr; c.lineTo(sx + Math.cos(ang) * rr, cy + Math.sin(ang) * rr); }
|
||||
c.closePath(); c.fill(); c.restore();
|
||||
}
|
||||
},
|
||||
'japon': {
|
||||
// soleil levant derrière l'orbe, points rose sakura et encre
|
||||
point: (i, p, x, y, z, lime, a) => [i % 3 === 0 ? '255,183,197' : (lime ? LIME : INK), a * (i % 3 === 0 ? 1.3 : 1), i % 3 === 0 ? 1.2 : 1],
|
||||
under: (c, cx, cy, R, t, k) => {
|
||||
const g = c.createRadialGradient(cx, cy, R * 0.1, cx, cy, R * 0.62);
|
||||
g.addColorStop(0, 'rgba(220,40,40,' + 0.55 * k + ')'); g.addColorStop(0.85, 'rgba(200,30,30,' + 0.42 * k + ')'); g.addColorStop(1, 'rgba(200,30,30,0)');
|
||||
c.fillStyle = g; c.beginPath(); c.arc(cx, cy, R * 0.62, 0, 6.283); c.fill();
|
||||
}
|
||||
},
|
||||
'hack': {
|
||||
// radar : un balayage tourne, les points s'allument à son passage puis s'éteignent
|
||||
point: (i, p, x, y, z, lime, a, t) => {
|
||||
const sweep = (t * 1.4) % 6.283, ang = (Math.atan2(y, x) + 6.283) % 6.283;
|
||||
const d = (sweep - ang + 6.283) % 6.283, hit = d < 0.9 ? 1 - d / 0.9 : 0;
|
||||
return hit > 0 ? ['220,255,230', Math.min(1, a + hit * 0.8), 1 + hit * 0.9] : [LIME, a * 0.55, 1];
|
||||
},
|
||||
over: (c, cx, cy, R, t, k, C) => {
|
||||
const sweep = (t * 1.4) % 6.283;
|
||||
c.save();
|
||||
c.strokeStyle = 'rgba(' + C.LIME + ',' + 0.18 * k + ')'; c.lineWidth = 1;
|
||||
[0.35, 0.7, 1.02].forEach(r => { c.beginPath(); c.arc(cx, cy, R * r, 0, 6.283); c.stroke(); });
|
||||
c.beginPath(); c.moveTo(cx - R * 1.1, cy); c.lineTo(cx + R * 1.1, cy); c.moveTo(cx, cy - R * 1.1); c.lineTo(cx, cy + R * 1.1); c.stroke();
|
||||
const g = c.createConicGradient ? c.createConicGradient(sweep - 0.9, cx, cy) : null;
|
||||
if (g) {
|
||||
g.addColorStop(0, 'rgba(' + C.LIME + ',0)'); g.addColorStop(0.143, 'rgba(' + C.LIME + ',' + 0.22 * k + ')'); g.addColorStop(0.1431, 'rgba(' + C.LIME + ',0)'); g.addColorStop(1, 'rgba(' + C.LIME + ',0)');
|
||||
c.fillStyle = g; c.beginPath(); c.arc(cx, cy, R * 1.02, 0, 6.283); c.fill();
|
||||
}
|
||||
c.strokeStyle = 'rgba(' + C.LIME + ',' + 0.8 * k + ')'; c.lineWidth = 1.5;
|
||||
c.beginPath(); c.moveTo(cx, cy); c.lineTo(cx + Math.cos(sweep) * R * 1.02, cy + Math.sin(sweep) * R * 1.02); c.stroke();
|
||||
c.restore();
|
||||
}
|
||||
},
|
||||
'black-friday': {
|
||||
point: (i, p, x, y, z, lime, a) => [lime || i % 3 === 0 ? LIME : INK, a * 1.1, 1],
|
||||
over: (c, cx, cy, R, t, k, C) => {
|
||||
const ax = cx + R * 0.62, ay = cy - R * 0.8, sw = Math.sin(t * 1.6) * 0.08;
|
||||
c.strokeStyle = 'rgba(' + C.INK + ',' + 0.6 * k + ')'; c.lineWidth = 1.2;
|
||||
c.beginPath(); c.moveTo(ax, ay); c.lineTo(ax + R * 0.18, ay + R * 0.3); c.stroke();
|
||||
c.save(); c.translate(ax + R * 0.18, ay + R * 0.3); c.rotate(sw); tag(c, 0, 0, R * 0.62, R * 0.24, '-100 %', C, k); c.restore();
|
||||
}
|
||||
}
|
||||
};
|
||||
const GOLD_ = () => GOLD;
|
||||
function facePath(cx, cy, R) {
|
||||
const P = (x, y) => [cx + x * R, cy + y * R];
|
||||
ctx.beginPath();
|
||||
[EYE_L, EYE_R, NOSE].forEach(tri => { tri.forEach((v, i) => ctx[i ? 'lineTo' : 'moveTo'](...P(v[0], v[1]))); ctx.closePath(); });
|
||||
const top = [], bot = [];
|
||||
for (let k = 0; k <= 24; k++) { const x = -0.64 + k * 0.64 * 2 / 24, [t, b] = smile(x); top.push([x, t]); bot.push([x, b]); }
|
||||
// contour de la bouche avec deux dents en haut et une en bas
|
||||
ctx.moveTo(...P(top[0][0], top[0][1]));
|
||||
top.forEach(([x, t]) => {
|
||||
const tooth = Math.abs(Math.abs(x) - 0.22) < 0.07;
|
||||
ctx.lineTo(...P(x, tooth ? t + 0.09 : t));
|
||||
});
|
||||
bot.reverse().forEach(([x, b]) => ctx.lineTo(...P(x, Math.abs(x) < 0.08 ? b - 0.09 : b)));
|
||||
ctx.closePath();
|
||||
}
|
||||
|
||||
function size() {
|
||||
const dpr = Math.min(devicePixelRatio || 1, 2);
|
||||
cw = innerWidth; ch = innerHeight;
|
||||
cv.width = cw * dpr; cv.height = ch * dpr;
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
}
|
||||
function state() {
|
||||
const vh = innerHeight; let s = A, t;
|
||||
if ((t = top(secs.works)) !== null) s = mix(s, B, ease(c01(1 - t / vh)));
|
||||
if ((t = top(secs.about)) !== null) s = mix(s, C, ease(c01((vh - t) / (vh * 0.9))));
|
||||
if ((t = top(secs.cta)) !== null) s = mix(s, D, ease(c01((vh - t) / (vh * 0.9))));
|
||||
// Mobile : orbe recentrée et un peu plus petite
|
||||
if (cw < 720) s = [s[0] * 0.5 + 0.25, s[1] * 0.8, s[2] * 0.85, s[3], s[4] * 0.8];
|
||||
return s;
|
||||
}
|
||||
function draw(t) {
|
||||
ctx.clearRect(0, 0, cw, ch);
|
||||
const S = document.documentElement.getAttribute('data-season') || '';
|
||||
const [fx, fy, fr, spread, ga] = state();
|
||||
let cx = cw * fx, cy = ch * fy, R = Math.min(cw, ch) * fr;
|
||||
if (S) { R *= 0.88; cy += R * 0.17; } // saison : orbe un peu plus basse pour que l'attache de la boule de Noël sorte de sous la barre
|
||||
const H = SEASON[S], C = { LIME, INK, GOLD }, sol = ga * Math.max(0, 1 - spread * 2.2);
|
||||
if (H && H.pulse) R *= H.pulse(t);
|
||||
const kx = H && H.shape ? H.shape[0] : 1, ky = H && H.shape ? H.shape[1] : 1;
|
||||
// Orientation neutre tant que la souris/le doigt n'a pas bougé (chargement, mobile)
|
||||
const has = m.x > -9000, sx = has ? (m.x - cx) / cw : 0, sy = has ? (m.y - cy) / ch : 0;
|
||||
const rY = t * 0.35 + sx * 1.2, rX = 0.35 + sy * 0.8;
|
||||
const cY = Math.cos(rY), sY = Math.sin(rY), cX = Math.cos(rX), sX = Math.sin(rX);
|
||||
const g = ctx.createRadialGradient(cx, cy, R * 0.2, cx, cy, R * 1.6);
|
||||
g.addColorStop(0, 'rgba(' + LIME + ',' + 0.08 * ga * (1 - spread * 0.8) + ')');
|
||||
g.addColorStop(1, 'rgba(' + LIME + ',0)');
|
||||
ctx.fillStyle = g; ctx.fillRect(cx - R * 2, cy - R * 2, R * 4, R * 4);
|
||||
if (H && H.under && sol > 0.01) H.under(ctx, cx, cy, R, t, sol, C);
|
||||
for (let i = 0; i < N; i++) {
|
||||
const p = pts[i];
|
||||
let x = p[0] * cY + p[2] * sY, z = -p[0] * sY + p[2] * cY;
|
||||
const y = p[1] * cX - z * sX; z = p[1] * sX + z * cX;
|
||||
const f = 2.4 / (2.4 + z);
|
||||
let px = cx + x * kx * R * f + rx[i] * spread * cw * 0.42;
|
||||
let py = cy + y * ky * R * f + ry[i] * spread * ch * 0.42;
|
||||
let boost = 0;
|
||||
const dx = px - m.x, dy = py - m.y, d2 = dx * dx + dy * dy;
|
||||
if (d2 < 32400) { // répulsion souris, rayon 180px
|
||||
const d = Math.sqrt(d2) || 1, k = (1 - d / 180) * 46;
|
||||
px += dx / d * k; py += dy / d * k; boost = 1 - d / 180;
|
||||
}
|
||||
const s = Math.max(0.4, (1 - z) * 1.5) + boost * 2.4;
|
||||
const a = Math.max(0.05, (1 - z) * 0.5) * ga + boost * 0.55;
|
||||
const lime = boost > 0.15 || p[1] > 0.72 || x > 0.85;
|
||||
if (H && H.point) {
|
||||
const r = H.point(i, p, x, y, z, lime, a, t);
|
||||
if (r) { ctx.fillStyle = 'rgba(' + r[0] + ',' + Math.min(r[1], 1) + ')'; ctx.beginPath(); ctx.arc(px, py, s * (r[2] || 1), 0, 6.283); ctx.fill(); continue; }
|
||||
}
|
||||
if (S === 'halloween') {
|
||||
// citrouille : points orangés, le visage reste vide (la lueur est dessinée après)
|
||||
if (z < 0.1 && spread < 0.5 && inFace((px - cx) / R, (py - cy) / R)) continue;
|
||||
ctx.fillStyle = 'rgba(' + (lime ? LIME : (i % 3 ? LIME : INK)) + ',' + Math.min(a * (lime ? 1.3 : 0.85), 1) + ')';
|
||||
ctx.beginPath(); ctx.arc(px, py, s, 0, 6.283); ctx.fill();
|
||||
continue;
|
||||
}
|
||||
if (S === 'noel' && Math.abs(Math.abs(p[1]) - 0.3) < 0.075) {
|
||||
// boule de Noël : deux bandes dorées qui scintillent
|
||||
const tw = 0.6 + 0.4 * Math.sin(t * 3 + i);
|
||||
ctx.fillStyle = 'rgba(' + GOLD + ',' + Math.min((a + 0.3 * ga) * tw, 1) + ')';
|
||||
ctx.beginPath(); ctx.arc(px, py, s * 1.15, 0, 6.283); ctx.fill();
|
||||
continue;
|
||||
}
|
||||
ctx.fillStyle = lime ? 'rgba(' + LIME + ',' + Math.min(a + 0.25 * ga, 1) + ')' : 'rgba(' + INK + ',' + a + ')';
|
||||
ctx.beginPath(); ctx.arc(px, py, s, 0, 6.283); ctx.fill();
|
||||
}
|
||||
const solid = sol; // décor visible tant que l'orbe n'est pas éclatée
|
||||
if (H && H.over && solid > 0.01) H.over(ctx, cx, cy, R, t, solid, C);
|
||||
if (S === 'halloween' && solid > 0.01) {
|
||||
// lueur de bougie qui vacille
|
||||
const fl = 0.78 + 0.14 * Math.sin(t * 9) + 0.08 * Math.sin(t * 23 + 1.3);
|
||||
ctx.save();
|
||||
facePath(cx, cy, R);
|
||||
const fg = ctx.createRadialGradient(cx, cy + R * 0.1, R * 0.05, cx, cy + R * 0.1, R * 0.8);
|
||||
fg.addColorStop(0, 'rgba(255,236,150,' + solid * fl + ')');
|
||||
fg.addColorStop(1, 'rgba(' + LIME + ',' + solid * fl * 0.9 + ')');
|
||||
ctx.shadowColor = 'rgba(' + LIME + ',' + solid + ')'; ctx.shadowBlur = 28 * fl;
|
||||
ctx.fillStyle = fg; ctx.fill();
|
||||
ctx.restore();
|
||||
}
|
||||
if (S === 'noel' && solid > 0.01) {
|
||||
// attache dorée et fil qui pend depuis le haut de l'écran
|
||||
const w = R * 0.3, h = R * 0.15, top = cy - R * 1.02;
|
||||
ctx.strokeStyle = 'rgba(' + INK + ',' + 0.3 * solid + ')'; ctx.lineWidth = 1;
|
||||
ctx.beginPath(); ctx.moveTo(cx, 0); ctx.lineTo(cx, top - h - R * 0.08); ctx.stroke();
|
||||
ctx.strokeStyle = 'rgba(' + GOLD + ',' + solid + ')'; ctx.lineWidth = Math.max(1.5, R * 0.02);
|
||||
ctx.beginPath(); ctx.ellipse(cx, top - h - R * 0.04, R * 0.05, R * 0.06, 0, 0, 6.283); ctx.stroke();
|
||||
const cg = ctx.createLinearGradient(cx - w / 2, 0, cx + w / 2, 0);
|
||||
cg.addColorStop(0, 'rgba(' + GOLD + ',' + 0.55 * solid + ')'); cg.addColorStop(0.45, 'rgba(255,240,200,' + solid + ')'); cg.addColorStop(1, 'rgba(' + GOLD + ',' + 0.5 * solid + ')');
|
||||
ctx.fillStyle = cg; ctx.fillRect(cx - w / 2, top - h, w, h);
|
||||
}
|
||||
if (spread < 0.6) {
|
||||
ctx.strokeStyle = 'rgba(' + LIME + ',' + 0.22 * ga * (1 - spread) + ')';
|
||||
ctx.lineWidth = 1; ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, R * 1.3, R * 0.4, -0.3 + sx * 0.3, 0, 6.283); ctx.stroke();
|
||||
}
|
||||
}
|
||||
function loop(ms) { if (!run) return; try { draw(ms / 1000); } catch (e) {} requestAnimationFrame(loop); }
|
||||
return {
|
||||
init(sel, opts) {
|
||||
cv = document.querySelector(sel); if (!cv) return;
|
||||
ctx = cv.getContext('2d');
|
||||
const o = (opts && opts.sections) || {};
|
||||
secs = { works: document.querySelector(o.works), about: document.querySelector(o.about), cta: document.querySelector(o.cta) };
|
||||
readColors(); addEventListener('yb:theme', () => { readColors(); if (!run) draw(0); });
|
||||
size(); addEventListener('resize', size);
|
||||
addEventListener('mousemove', e => { m.x = e.clientX; m.y = e.clientY; });
|
||||
addEventListener('touchmove', e => { const t = e.touches[0]; m.x = t.clientX; m.y = t.clientY; }, { passive: true });
|
||||
addEventListener('touchend', () => { m.x = m.y = -9999; });
|
||||
if (document.documentElement.classList.contains('yb-static')) { draw(0); return; } // ?ybstatic=1 : image fixe
|
||||
run = true; requestAnimationFrame(loop);
|
||||
},
|
||||
destroy() { run = false; }
|
||||
};
|
||||
})();
|
||||
Loading…
Add table
Add a link
Reference in a new issue