feat(logo-threads): bracket-anchored paths with x^2 / -x^2 curves (#24)

Threads now lock to logo bracket pin endpoints (where vertical legs
of ::before / ::after bracket-corners end):
- Rosa tråd: från (0, viewport.height) → (logoLeft, logoTop + 40% height)
  Quadratic Bezier with cp=(endX, startY) = x^2 curve, går rakt först
  och accelererar upp i slutet
- Cyan tråd: från (logoRight, logoBottom - 40% height) → (viewport.width, 0)
  Quadratic Bezier with cp=(startX, endY) = -x^2 curve, accelererar upp
  i början och decelererar mot slutet

Permanent paths som uppdateras vid resize + scroll + 500ms/1500ms
post-load. runLogoPulse triggar update före varje pulse.
This commit is contained in:
Linus Olofsson
2026-05-21 05:34:27 +02:00
committed by GitHub
parent 336ac26e64
commit fb80bd0f2b
+46 -2
View File
@@ -72,8 +72,8 @@ const pulseCore = theme === 'purple' ? '#fdf4ff' : '#f0fdfa';
<path id="thread1" d="M-1000 200 Q 400 400 900 200 T 2600 200" fill="none" stroke="url(#gradSec)" stroke-width="0.5" class="opacity-50" data-pulse-path="true" /> <path id="thread1" d="M-1000 200 Q 400 400 900 200 T 2600 200" fill="none" stroke="url(#gradSec)" stroke-width="0.5" class="opacity-50" data-pulse-path="true" />
<path id="thread2" d="M-1000 800 Q 600 600 1100 800 T 2600 800" fill="none" stroke="url(#gradSec)" stroke-width="0.8" class="opacity-60" data-pulse-path="true" /> <path id="thread2" d="M-1000 800 Q 600 600 1100 800 T 2600 800" fill="none" stroke="url(#gradSec)" stroke-width="0.8" class="opacity-60" data-pulse-path="true" />
<path id="threadLogoIn" d="M-1000 600 C -300 600, 400 287, 900 287 S 1900 287, 2600 100" fill="none" stroke="url(#gradPurple)" stroke-width="0.7" class="opacity-55" /> <path id="threadLogoIn" d="M 0 0 L 0 0" fill="none" stroke="url(#gradPurple)" stroke-width="0.6" class="opacity-50" />
<path id="threadLogoOut" d="M-1000 100 C -300 100, 400 287, 900 287 S 1900 287, 2600 600" fill="none" stroke="url(#gradMain)" stroke-width="0.7" class="opacity-50" /> <path id="threadLogoOut" d="M 0 0 L 0 0" fill="none" stroke="url(#gradMain)" stroke-width="0.6" class="opacity-45" />
</g> </g>
<g id="box-trail-group"></g> <g id="box-trail-group"></g>
@@ -422,6 +422,47 @@ const pulseCore = theme === 'purple' ? '#fdf4ff' : '#f0fdfa';
return el; return el;
} }
function updateLogoThreadPaths() {
const logo = document.querySelector<HTMLElement>('[data-logo-target]');
const threadIn = document.getElementById('threadLogoIn') as SVGPathElement | null;
const threadOut = document.getElementById('threadLogoOut') as SVGPathElement | null;
if (!logo || !threadIn || !threadOut) return;
const rect = logo.getBoundingClientRect();
const vw = window.innerWidth;
const vh = window.innerHeight;
// Bracket-pinnarnas SLUT (där vertikala benen tar slut)
// ::before: top-left bracket, height=40% av text-wrapper. Vänstra benet går nedåt 40% av höjden
// Lås-punkt: (rect.left, rect.top + 0.40 * rect.height) — pinnens nedre spets
const leftPinX = rect.left;
const leftPinY = rect.top + rect.height * 0.40;
// ::after: bottom-right bracket. Höger ben börjar 40% från botten
// Lås-punkt: (rect.right, rect.bottom - 0.40 * rect.height) — pinnens övre spets
const rightPinX = rect.right;
const rightPinY = rect.bottom - rect.height * 0.40;
// Rosa tråd: från (0, vh) → (leftPinX, leftPinY)
// x² curve: kontrollpunkt (endX, startY) = rakt fram, sedan upp = accelererande lutning
const inStartX = 0, inStartY = vh;
const inEndX = leftPinX, inEndY = leftPinY;
const inCpX = inEndX, inCpY = inStartY;
threadIn.setAttribute('d', `M ${inStartX} ${inStartY} Q ${inCpX} ${inCpY}, ${inEndX} ${inEndY}`);
// Cyan tråd: från (rightPinX, rightPinY) → (vw, 0)
// -x² curve: kontrollpunkt (startX, endY) = upp först, sedan rakt = decelererande lutning
const outStartX = rightPinX, outStartY = rightPinY;
const outEndX = vw, outEndY = 0;
const outCpX = outStartX, outCpY = outEndY;
threadOut.setAttribute('d', `M ${outStartX} ${outStartY} Q ${outCpX} ${outCpY}, ${outEndX} ${outEndY}`);
}
// Uppdatera vid load + resize + scroll (logo-position beror på alla tre)
updateLogoThreadPaths();
window.addEventListener('resize', updateLogoThreadPaths);
window.addEventListener('scroll', updateLogoThreadPaths, { passive: true });
// Re-run efter en kort delay för att Astro layout ska vara klar
setTimeout(updateLogoThreadPaths, 500);
setTimeout(updateLogoThreadPaths, 1500);
function runLogoPulse(durationMs: number) { function runLogoPulse(durationMs: number) {
if (!blob1) return; if (!blob1) return;
const logo = isLogoTargetVisible(); const logo = isLogoTargetVisible();
@@ -434,6 +475,9 @@ const pulseCore = theme === 'purple' ? '#fdf4ff' : '#f0fdfa';
isPulsing = true; isPulsing = true;
updatePulseColors(false); updatePulseColors(false);
// Uppdatera path-d till EXAKT nuvarande logo-position innan pulse börjar
updateLogoThreadPaths();
const rect = logo.getBoundingClientRect(); const rect = logo.getBoundingClientRect();
const logoLeftX = rect.left + 8; const logoLeftX = rect.left + 8;
const logoRightX = rect.right - 8; const logoRightX = rect.right - 8;