/* orb.js : Orbe de particules persistante (vanilla JS) 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; } }; })();