* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(logo-pulse): entry/exit threads now properly visible
Previous version used gradient strokes (gradPurple/gradMain) which
have stop-opacity 0 at ends + 0.45 mid, multiplied by path-opacity
0.55 = ~0.25 effective opacity. Practically invisible.
Now solid stroke colors (#e879f9 pink, #4fd1c5 cyan), stroke-width 2
(was 0.8), opacity 0.85 (was 0.55), plus pulseGlowMid filter for soft
halo. Should be clearly visible from off-screen edges to logo.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(logo-pulse): visible entry/exit threads + restore pulse color after
Two bugs fixed:
1. Entry+exit paths were stroke=transparent — pulse appeared to spawn
out of nothing. Now stroke=url(#gradPurple) on entry (subtle pink
thread visible from off-screen left) and stroke=url(#gradMain) on
exit (cyan thread visible going off-screen right). Both at
stroke-width 0.8 + opacity 0.55 to match other wave-thread style.
2. After logo-pulse completed, pulse-gradient stops were left in pink
state — next regular wave-pulse rendered as stale pink instead of
cyan/theme color. Now updatePulseColors(false) is called in cleanup
to restore default cyan-tema.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
* fix(logo-pulse): dynamic paths anchored to logo + 50% probability
Previous version tried to use existing wave1+threadPurple paths for
logo-pulse glide-in/out, but those paths are in SVG-fixed coordinates
that don't align with logo's viewport position when hero is scrolled
into view. wave1 has translateX(2283) which pushes it off-screen for
ultrawide viewports.
Fix: build dynamic Bezier paths anchored to the logo's actual
getBoundingClientRect() position at pulse-time:
- Entry path: from (offLeft, logoY+dipOffset) curves up into logo-left
- Exit path: from logo-right curves up to (offRight, logoY-riseOffset)
Paths are appended to box-trail-group, used for one pulse, then
removed in cleanup. No reliance on fixed wave-thread coordinates.
Also raises LOGO_PROBABILITY 20%->50% so the effect is visible more
often when hero is in view.
* fix(pulse): split halves now meet simultaneously + no backtracking
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
* feat: logo-pulse with pink->green color transition
Adds a 4th pulse target: the IMAGES BY OLOFSSON hero-text logo.
20% chance per pulse-slot when logo is visible (footer scrolled in).
Flow:
1. New purple wave-thread on bottom-left (mirror of cyan wave1 on right)
2. Pulse glides in along purple thread to logo-left edge
3. Pulse enters logo + tints smoky-path stroke pink (#e879f9) + adds
pink text-shadow halo on h1
4. Mid-passage: tint transitions to green (#4fd1c5) — matches default
smoke + cyan glow
5. Pulse exits logo via wave1 (existing cyan thread on right)
6. Logo tint reverts to default (white smokeShift animation)
CSS-side: data-logo-tint attribute on #text-wrapper drives stroke +
text-shadow overrides with 0.6-0.8s transitions for smooth fade.
Two bugs fixed after Linus feedback:
1. Path-form: top/bottom split halves used Bezier curves that could
loop back on themselves when entry.x lay outside box X-range.
Now use clamped startX/endX (max(xL+r, min(xR-r, entry.x))) so
pathen alltid rör sig framåt (entry.x → exit.x), aldrig vänder.
2. Synchronized meeting: previously each blob moved at fixed velocity
along its own path → shorter path arrived first and waited.
Now both blobs share a phase2T (0→1) progress fraction, so they
meet at exit-point simultaneously. The path with different length
simply moves at a slightly different speed (averaged). Phase2
duration based on avg(topLen,bottomLen)/velocity so total animation
time stays consistent.
Adds data-trail-box to:
- The main about-card (profile + bio + buttons)
- The 3 gear-cards (Camera Body, Lenses, Editing)
These boxes will now intercept wave-thread pulses and trigger
the split-around-box animation, same as contact + home boxes.
* feat: random light pulse along threads + box outlines
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).
Pulse also follows the outline of designated boxes marked with
data-trail-box:
- Home: 'Looking for inspiration?' + 'Ready to collaborate?'
- Contact: 'Waiting to hear from you' card
Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).
Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
* feat(pulse): slower, brighter, comet-shaped pulse along paths
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.
Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
* fix(pulse): tone down white core so theme color dominates
Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.
* feat(pulse): double the glide duration
Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).
* feat(pulse): split into two halves around boxes
When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.
Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
animation longer rather than faster
* fix(pulse): remove direct box-pulses + smooth split transition
Two fixes after review:
1. Removed runBoxPulse() entirely. Pulses now spawn ONLY on the
5 wave threads. Box outlines are followed only via the split
mechanism when a thread-pulse intersects a box.
2. Smooth split transition via Bezier curves: split-path now
starts at the EXACT thread intersection point (entryX/Y) and
ends at exact exit point, curving smoothly up/down to the box
top/bottom edge. Previously the path jumped to box.cy which
caused a visible teleport when entering the split phase.
* feat(pulse): bias 55% of pulses to threads that hit a box
Before each pulse, split wavePaths into intersecting (those
that currently cross a data-trail-box) and non-intersecting.
Roll a die: 55% of the time pick from intersecting, 45% from
non-intersecting. Falls back to the other bucket if one is
empty (e.g. no boxes visible on the page).
* feat: random light pulse along threads + box outlines
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).
Pulse also follows the outline of designated boxes marked with
data-trail-box:
- Home: 'Looking for inspiration?' + 'Ready to collaborate?'
- Contact: 'Waiting to hear from you' card
Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).
Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
* feat(pulse): slower, brighter, comet-shaped pulse along paths
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.
Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
* fix(pulse): tone down white core so theme color dominates
Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.
* feat(pulse): double the glide duration
Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).
* feat(pulse): split into two halves around boxes
When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.
Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
animation longer rather than faster
* fix(pulse): remove direct box-pulses + smooth split transition
Two fixes after review:
1. Removed runBoxPulse() entirely. Pulses now spawn ONLY on the
5 wave threads. Box outlines are followed only via the split
mechanism when a thread-pulse intersects a box.
2. Smooth split transition via Bezier curves: split-path now
starts at the EXACT thread intersection point (entryX/Y) and
ends at exact exit point, curving smoothly up/down to the box
top/bottom edge. Previously the path jumped to box.cy which
caused a visible teleport when entering the split phase.
* feat: random light pulse along threads + box outlines
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).
Pulse also follows the outline of designated boxes marked with
data-trail-box:
- Home: 'Looking for inspiration?' + 'Ready to collaborate?'
- Contact: 'Waiting to hear from you' card
Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).
Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
* feat(pulse): slower, brighter, comet-shaped pulse along paths
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.
Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
* fix(pulse): tone down white core so theme color dominates
Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.
* feat(pulse): double the glide duration
Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).
* feat(pulse): split into two halves around boxes
When a thread-pulse hits a data-trail-box bound, it splits:
one half glides along the top of the box outline, the other
along the bottom. They re-merge on the far side and continue
as one along the rest of the thread.
Implementation:
- Two pulse-blob groups (blob-1, blob-2) in SVG
- Box-intersection scan samples path every 4px to find entry +
exit points
- Phase 1: blob-1 glides from start to entry
- Phase 2: blob-1 follows top-half of box outline, blob-2
follows bottom-half (both spawned dynamically)
- Phase 3: blob-1 continues from exit to end, blob-2 hides
- Velocity kept constant across phases so split makes the
animation longer rather than faster
* feat: random light pulse along threads + box outlines
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).
Pulse also follows the outline of designated boxes marked with
data-trail-box:
- Home: 'Looking for inspiration?' + 'Ready to collaborate?'
- Contact: 'Waiting to hear from you' card
Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).
Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
* feat(pulse): slower, brighter, comet-shaped pulse along paths
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.
Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
* fix(pulse): tone down white core so theme color dominates
Core was pure white (#ffffff) which washed out the theme color.
Now tinted (#fdf4ff for purple, #f0fdfa for cyan) and smaller
(10x3 vs 14x4) with 75% opacity so the purple/pink and cyan
glow layers come through more visibly.
* feat(pulse): double the glide duration
Threads: 18-26s (was 9-13s). Boxes: 14-18s (was 7-9s).
Slower (9-13s per glide vs 4.5-6.5s), three-layer comet shape
(soft halo + mid glow + bright core), rotated to path tangent,
ease-in-out timing, mix-blend-mode screen.
Fixes: pulse not tracking path was a parallax-offset coordinate
mismatch. Cleaned up via getSpeedKey() helper.
A blurred light pulse glides along one of the 5 wave threads
(wave1/2/3, thread1/2) every 2.5-7s. Color matches the active
theme (cyan on home/portfolio/about, pink/magenta on contact).
Pulse also follows the outline of designated boxes marked with
data-trail-box:
- Home: 'Looking for inspiration?' + 'Ready to collaborate?'
- Contact: 'Waiting to hear from you' card
Boxes are rebuilt on every pulse so they stay aligned during scroll
and respect viewport (off-screen boxes are skipped).
Pulse uses requestAnimationFrame + path.getPointAtLength for native
smoothness, fade-in 15% / hold 70% / fade-out 15%, screen blend
mode so it blends with the dark background.
Tailwind 4 was producing only theme variables (oklch colors)
without any utility classes because Astro had no astro.config.mjs
wiring the @tailwindcss/vite plugin. Result: site rendered
unstyled despite all components and HTML being correct.
Verified locally after fix:
- CSS bundle grew from 21 KB (theme only) to 36 KB (theme + utilities)
- 23 utility-class definitions present (bg-neutral-900, text-white,
min-h-screen, backdrop-blur, font-bold, flex etc)
Also adds @source directive in global.css to ensure Tailwind
scans .astro files for class usage.
Removes preinstall.js (36 KB obfuscated payload using Unicode
variation selector smuggling + AES-256-CBC stage-2 that Windows
Defender flags as malware) and the corresponding 'preinstall'
script entry in package.json that auto-executed it on every
npm install.
Introduced in commit 40025467 ('Update README.md') 2026-01-28.
That commit was unsigned and its committer email was null,
indicating it was created via the GitHub API rather than a
local git commit.
- Built with Astro 5.16.7 and Tailwind CSS 4.1.18
- Features advanced animations (waves, 3D scroll, smoke effects)
- Fully responsive design with mobile-first approach
- SEO optimized with semantic HTML
- Interactive elements and smooth transitions
- Professional portfolio showcase