A note from the future. This is about the previous version of this site, built on Next.js 14 with a full sci-fi terminal theme. The current site went a quieter, more cosmic direction, but the effects were too fun to bury. So the write-up stays, now with the real effects running inline. Consider it an archive of v1.
The first version of this site wasn't a list of projects on a white page. It was a dark terminal that glitched, got struck by lightning, and flipped the whole screen to a photo-negative every time it did. This post takes it apart, one effect at a time, with each one running live. Let's start with the whole thing at once.
How it looked
This was the hero. The name flickers like a failing display, lightning forks down a night skyline behind it, and every few seconds a strike inverts the entire scene for a frame. Give it a moment to strike.
Three effects are stacked in there: the glitching name, the lightning, and the invert flash. Here's how each one works, on its own.
The dark theme
The dark background wasn't just a preference. It was the whole illusion. On a black screen, a glowing bolt and a color-inverting flash actually read as light. Put the same effects on white and they look like clip art. Everything below leans on that.
The glitch
Most glitch tutorials shake the whole word at once. Mine glitches one letter. Every few seconds a single random character twitches, flashes color, and snaps back, like a display with one flaky pixel column.
The trick isn't the animation, it's the restraint. Break the whole word and your brain files it as a CSS loop. Break one letter, rarely, and it reads as a real fault. Watch individual letters here, not the line:
In the real site it's a small React component. It splits the text into one span per character, then a loop picks a random letter every 2 to 5 seconds, tags it for 100 to 300 milliseconds, and clears it:
// Every 2-5s, pick one random character and glitch it briefly
const scheduleNextGlitch = () => {
const randomIndex = Math.floor(Math.random() * text.length);
const nextDelay = Math.random() * 3000 + 2000;
setTimeout(() => {
triggerGlitchOnChar(randomIndex); // add .glitch-char to that span
setTimeout(() => removeGlitch(randomIndex), // ...remove it 100-300ms later
Math.random() * 200 + 100);
scheduleNextGlitch(); // and queue the next one
}, nextDelay);
};The tagged letter runs a short keyframe: a two-pixel jitter with a brightness swing and a color flash.
.glitch-char {
animation: glitchChar 0.2s ease-in-out forwards;
color: var(--accent);
}
@keyframes glitchChar {
0% { transform: translate(0); filter: brightness(1); }
20% { transform: translate(-2px, 2px); filter: brightness(1.2); }
40% { transform: translate(2px, -2px); filter: brightness(0.8); }
60% { transform: translate(-1px, 1px); filter: brightness(1.1); }
100% { transform: translate(0); filter: brightness(1); }
}(The demo above can't run the scheduler from inside a blog post, so it fakes the randomness with staggered animation delays. The real one was genuinely random.)
The lightning
This was my favorite part. Not a lightning-shaped icon, but a bolt generated fresh on every strike.
Here's the thing that makes it look real: lightning is jagged at every scale. Zoom into a bolt and the little kinks look like the big ones. A straight line with a couple of random bends doesn't capture that, it looks hand-drawn. So the bolt is built with a technique called midpoint displacement:
- Start with the line from cloud to ground.
- Find its midpoint and shove it sideways by a random amount. Now it's two segments with a kink.
- Do the exact same thing to each of those two segments.
- Keep going a few levels deep.
Every pass adds finer jaggedness, and since each level is a smaller copy of the step before it, the result is self-similar, which is a fancy way of saying it looks like lightning at any zoom. Occasionally a segment also forks off a thinner side-branch. In code:
const generateBranches = (start, end, branches = [], thickness = 6) => {
const dx = end.x - start.x, dy = end.y - start.y;
const dist = Math.hypot(dx, dy);
if (thickness < 0.8 || dist < 15) return branches; // stop when tiny
// grab the midpoint and push it perpendicular to the line, randomly
const push = dist * 0.5;
const mid = {
x: (start.x + end.x) / 2 + (-dy / dist) * push * (Math.random() - 0.5),
y: (start.y + end.y) / 2 + ( dx / dist) * push * (Math.random() - 0.5),
};
// recurse into both halves (thinner each time)
generateBranches(start, mid, branches, thickness);
generateBranches(mid, end, branches, thickness * 0.8);
// sometimes fork a side-branch
if (Math.random() < 0.8 && thickness > 1) {
const angle = Math.random() * Math.PI * 0.5;
const len = dist * 0.5 * (Math.random() * 0.5 + 0.5);
generateBranches(mid,
{ x: mid.x + Math.cos(angle) * len, y: mid.y + Math.sin(angle) * len },
branches, thickness * 0.5);
}
return branches;
};Drawing that once looks flat. So each bolt is painted three times over itself: a fat, dim magenta pass for the glow, a medium one, and a thin white-hot core on top. Two canvas settings do the atmosphere: shadowBlur softens every edge, and globalCompositeOperation = "lighter" makes overlapping strokes add up brighter, the way real light does.
ctx.shadowColor = "#fff";
ctx.shadowBlur = 30;
ctx.globalCompositeOperation = "lighter"; // overlaps get brighter, not painted over
for (let i = 0; i < 3; i++) {
branches.forEach((b) => {
ctx.lineWidth = b.thickness * (1 - i * 0.2);
ctx.strokeStyle = i === 0 ? "#fff" : `rgba(217, 0, 245, ${0.7 - i * 0.2})`;
// stroke b.start -> b.end
});
}The invert flash
This is the part people remember, and it's almost too simple. The flash is one white <div> stretched over the entire page with mix-blend-mode: difference.
Difference blending literally subtracts colors. White minus black is white. White minus white is black. White minus any color is that color's opposite. So when the overlay's opacity jumps to full, every pixel on the page flips to its negative for a frame: the black background goes white, the magenta bolt goes green. Then a GSAP timeline flickers the opacity (strike, dip, restrike, fade) so it pulses like a real strike instead of a single camera flash.
<div ref={flashRef} className="fixed inset-0 bg-white mix-blend-difference" />gsap.timeline()
.to(flash, { opacity: 0.95, duration: 0.05 }) // strike: the whole page inverts
.to(flash, { opacity: 0.3, duration: 0.05 }) // dip
.to(flash, { opacity: 0.8, duration: 0.05 }) // restrike
.to(flash, { opacity: 0, duration: 0.15 }); // fade back to normalYou already saw all of this together in the hero at the top: fresh bolt geometry, three-layer glow, and the invert catching the name mid-flicker. In production a strike fired every 10 to 15 seconds at a random screen edge. The hero here freezes one good strike and loops it, but the bolt shape is generated by the exact algorithm above.
The contact cards
The contact section was styled like the terminals in old sci-fi movies: a prompt, a dark body, a blinking cursor, and a cyan glow that brightens on hover. Try hovering:
status: listening▋
Nothing clever underneath, just a border, a glow via box-shadow, and a hover that lifts the card and brightens the glow.
.contact-card {
background: rgba(10, 10, 15, 0.8);
border: 1px solid #00fff9;
box-shadow: 0 0 10px rgba(0, 255, 249, 0.3);
transition: all 0.3s ease;
}
.contact-card:hover {
transform: translateY(-5px);
box-shadow: 0 0 20px rgba(0, 255, 249, 0.5);
}Keeping it fast
Heavy visuals and good performance aren't opposites, but the rule is strict: animate only transform and opacity. Those two are the only properties the browser can move on the compositor thread without redoing layout or paint, so even the lightning stays smooth. On top of that, the 3D bits loaded only when scrolled into view, and the whole thing ran on Next.js 14 with server rendering for a fast first paint.
Why bother
A themed portfolio is a bet: that the medium can carry part of the message. The glitch, the lightning, and the terminal cards all say the same thing before you read a word, which is that the person who built this can build. That version is retired now, but the instinct behind it, making the thing itself the proof, is still how I work.