feat: binary glow effects, refined drawings and calls to action
Dark surfaces now carry light: frames glow along their edge and shed binary digits (BitAura), dark sections open with a lit seam (BitSeam), the career path is a line of light and "My work" became a dark stage. Buttons carry a travelling light and a spot that follows the pointer (BitGlow). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -91,6 +91,8 @@ export const BitHeadline: React.FC<{
|
||||
const reducedMotion = window.matchMedia(
|
||||
"(prefers-reduced-motion: reduce)",
|
||||
).matches;
|
||||
// Touch screens have no pointer to light the digits up, so a spot wanders on its own.
|
||||
const noHover = window.matchMedia("(hover: none)").matches;
|
||||
|
||||
let measured: Measured[] = [];
|
||||
let width = 0;
|
||||
@@ -306,6 +308,13 @@ export const BitHeadline: React.FC<{
|
||||
if (disposed) return;
|
||||
if (now - lastFrame >= FRAME_MS) {
|
||||
lastFrame = now;
|
||||
if (noHover) {
|
||||
const seconds = now / 1000;
|
||||
pointer = {
|
||||
x: width * (0.5 + 0.46 * Math.sin(seconds * 0.55)),
|
||||
y: height * (0.5 + 0.4 * Math.sin(seconds * 0.9 + 1)),
|
||||
};
|
||||
}
|
||||
paint(now);
|
||||
}
|
||||
raf = requestAnimationFrame(loop);
|
||||
@@ -339,7 +348,7 @@ export const BitHeadline: React.FC<{
|
||||
restart();
|
||||
setPainted(true);
|
||||
resizeObserver.observe(wrap);
|
||||
if (!reducedMotion) {
|
||||
if (!reducedMotion && !noHover) {
|
||||
window.addEventListener("pointermove", onPointerMove);
|
||||
document.addEventListener("pointerleave", onPointerLeave);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import * as React from "react";
|
||||
import { BinaryTicker } from "@/src/components/bits/BinaryTicker";
|
||||
import { BitSeam } from "@/src/components/bits/BitSeam";
|
||||
import { BitWipe } from "@/src/components/bits/BitWipe";
|
||||
import { DecodeText } from "@/src/components/bits/DecodeText";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
@@ -10,6 +11,8 @@ interface BlockProps {
|
||||
label: string;
|
||||
title: React.ReactNode;
|
||||
id?: string;
|
||||
/** `dark` turns the section into a dark stage. */
|
||||
tone?: "light" | "dark";
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
@@ -22,36 +25,61 @@ export const Block: React.FC<BlockProps> = ({
|
||||
label,
|
||||
title,
|
||||
id,
|
||||
tone = "light",
|
||||
children,
|
||||
}) => (
|
||||
<section
|
||||
id={id}
|
||||
className="relative scroll-mt-24 border-t border-slate-200 bg-white"
|
||||
>
|
||||
<div className="mx-auto w-full max-w-[1400px] px-5 py-20 md:px-12 md:py-36">
|
||||
<Reveal>
|
||||
<div className="mb-8 flex items-center gap-4 md:mb-12">
|
||||
<span className="font-mono text-[11px] font-bold tracking-[0.2em] text-slate-900">
|
||||
{index}
|
||||
</span>
|
||||
<span className="h-px w-10 bg-slate-300" />
|
||||
<DecodeText
|
||||
text={label}
|
||||
className="font-mono text-[11px] uppercase tracking-[0.25em] text-slate-500"
|
||||
/>
|
||||
<BinaryTicker
|
||||
value={parseInt(index, 10)}
|
||||
alive
|
||||
className="ml-auto hidden font-mono text-[10px] tracking-[0.3em] text-slate-300 md:block"
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
<BitWipe delay={0.1}>
|
||||
<h2 className="m-0 max-w-6xl text-4xl font-bold leading-[0.95] tracking-tighter text-slate-900 md:text-7xl lg:text-[6.5rem]">
|
||||
{title}
|
||||
</h2>
|
||||
</BitWipe>
|
||||
<div className="mt-14 md:mt-24">{children}</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}) => {
|
||||
const dark = tone === "dark";
|
||||
return (
|
||||
<section
|
||||
id={id}
|
||||
className={`relative scroll-mt-24 border-t ${
|
||||
dark
|
||||
? "overflow-clip border-slate-800 bg-slate-900"
|
||||
: "overflow-x-clip border-slate-200 bg-white"
|
||||
}`}
|
||||
>
|
||||
{dark && (
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-70"
|
||||
style={{
|
||||
backgroundImage: "radial-gradient(#1e293b 1px, transparent 1px)",
|
||||
backgroundSize: "20px 20px",
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
)}
|
||||
{dark && <BitSeam />}
|
||||
<div className="relative mx-auto w-full max-w-[1400px] px-5 py-20 md:px-12 md:py-36">
|
||||
<Reveal>
|
||||
<div className="mb-8 flex items-center gap-4 md:mb-12">
|
||||
<span
|
||||
className={`font-mono text-[11px] font-bold tracking-[0.2em] ${dark ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{index}
|
||||
</span>
|
||||
<span
|
||||
className={`h-px w-10 ${dark ? "bg-slate-700" : "bg-slate-300"}`}
|
||||
/>
|
||||
<DecodeText
|
||||
text={label}
|
||||
className={`font-mono text-[11px] uppercase tracking-[0.25em] ${dark ? "text-slate-400" : "text-slate-500"}`}
|
||||
/>
|
||||
<BinaryTicker
|
||||
value={parseInt(index, 10)}
|
||||
alive
|
||||
className={`ml-auto hidden font-mono text-[10px] tracking-[0.3em] md:block ${dark ? "text-slate-600" : "text-slate-300"}`}
|
||||
/>
|
||||
</div>
|
||||
</Reveal>
|
||||
<BitWipe delay={0.1} tone={dark ? "light" : "dark"}>
|
||||
<h2
|
||||
className={`m-0 max-w-6xl text-4xl font-bold leading-[0.95] tracking-tighter md:text-7xl lg:text-[6.5rem] ${dark ? "text-white" : "text-slate-900"}`}
|
||||
>
|
||||
{title}
|
||||
</h2>
|
||||
</BitWipe>
|
||||
<div className="mt-14 md:mt-24">{children}</div>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import * as React from "react";
|
||||
import { Reveal } from "@/src/components/Reveal";
|
||||
import { EmailLink } from "@/src/components/EmailLink";
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
import { BitSeam } from "@/src/components/bits/BitSeam";
|
||||
import { BitHeadline, type BitLine } from "./BitHeadline";
|
||||
import { ContactComposer } from "./ContactComposer";
|
||||
|
||||
@@ -13,7 +15,8 @@ const CLOSING: readonly BitLine[] = [
|
||||
export const ClosingCta: React.FC = () => (
|
||||
<section
|
||||
id="contact"
|
||||
className="relative border-t border-slate-200 bg-slate-900 text-white"
|
||||
data-glow
|
||||
className="relative overflow-clip border-t border-slate-200 bg-slate-900 text-white"
|
||||
>
|
||||
<div
|
||||
className="pointer-events-none absolute inset-0 opacity-70"
|
||||
@@ -23,6 +26,8 @@ export const ClosingCta: React.FC = () => (
|
||||
}}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<div className="bit-glow bit-glow-dots" aria-hidden="true" />
|
||||
<BitSeam />
|
||||
<div className="relative mx-auto grid w-full max-w-[1400px] grid-cols-1 gap-14 px-5 py-24 md:px-12 md:py-40 lg:grid-cols-12 lg:items-center lg:gap-12">
|
||||
<Reveal className="lg:col-span-6">
|
||||
<BitHeadline
|
||||
@@ -40,7 +45,9 @@ export const ClosingCta: React.FC = () => (
|
||||
</div>
|
||||
</Reveal>
|
||||
<Reveal delay={0.15} className="lg:col-span-6">
|
||||
<ContactComposer />
|
||||
<BitAura source="around" tone="dark">
|
||||
<ContactComposer />
|
||||
</BitAura>
|
||||
</Reveal>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@@ -122,7 +122,7 @@ export const ContactComposer: React.FC = () => {
|
||||
<dl className="m-0 border-b border-slate-800 px-5 py-4 font-mono text-[12px] leading-relaxed md:px-7 md:text-[13px]">
|
||||
<div className="flex gap-4">
|
||||
<dt className="w-16 shrink-0 text-slate-500">to</dt>
|
||||
<dd className="m-0 text-slate-200">
|
||||
<dd className="m-0 select-all text-slate-200">
|
||||
{address ?? spellOutEmail(CONTACT_EMAIL_PARTS)}
|
||||
</dd>
|
||||
</div>
|
||||
@@ -148,7 +148,7 @@ export const ContactComposer: React.FC = () => {
|
||||
href={
|
||||
address ? mailtoHref(CONTACT_EMAIL_PARTS, topic.draft) : "#contact"
|
||||
}
|
||||
className="group inline-flex flex-1 items-center justify-between gap-4 rounded-full bg-white px-7 py-4 font-sans text-[12px] font-bold uppercase tracking-[0.18em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
|
||||
className="bit-cta bit-cta-light group inline-flex flex-1 items-center justify-between gap-4 rounded-full bg-white px-7 py-4 font-sans text-[12px] font-bold uppercase tracking-[0.18em] text-slate-900 no-underline transition-transform duration-300 hover:-translate-y-0.5"
|
||||
>
|
||||
Open in my mail app
|
||||
<span
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import * as React from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { Button } from "@/src/components/Button";
|
||||
import { BitAura } from "@/src/components/bits/BitAura";
|
||||
import { DecodeText } from "@/src/components/bits/DecodeText";
|
||||
import { PathScene } from "@/src/components/illustrations/PathScenes";
|
||||
import { SERVICE_PATHS, type ServicePath } from "@/src/content/paths";
|
||||
@@ -49,25 +50,33 @@ const PathStory: React.FC<{ path: ServicePath; active: boolean }> = ({
|
||||
return (
|
||||
<div ref={ref} className="grid grid-cols-1 gap-8 md:grid-cols-12 md:gap-12">
|
||||
<div className="md:order-2 md:col-span-7">
|
||||
<div className="relative overflow-hidden rounded-3xl bg-slate-900 md:sticky md:top-28">
|
||||
<div className="flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<DecodeText
|
||||
key={current?.title}
|
||||
text={current?.title ?? ""}
|
||||
className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-300"
|
||||
/>
|
||||
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-500">
|
||||
{step + 1} / {path.steps.length}
|
||||
</span>
|
||||
<BitAura source="top" tone="light" className="md:sticky md:top-28">
|
||||
<div
|
||||
data-glow
|
||||
className="relative overflow-hidden rounded-3xl bg-slate-900"
|
||||
>
|
||||
<span className="bit-glow bit-glow-dots" aria-hidden="true" />
|
||||
<div className="relative flex items-center justify-between border-b border-slate-800 px-5 py-4 md:px-7">
|
||||
<DecodeText
|
||||
key={current?.title}
|
||||
text={current?.title ?? ""}
|
||||
className="font-mono text-[11px] uppercase tracking-[0.2em] text-slate-300"
|
||||
/>
|
||||
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-500">
|
||||
{step + 1} / {path.steps.length}
|
||||
</span>
|
||||
</div>
|
||||
<div className="relative px-2 py-4 md:px-10 md:py-8">
|
||||
<PathScene
|
||||
pathId={path.id}
|
||||
step={step}
|
||||
title={
|
||||
current ? `${current.title}. ${current.text}` : path.need
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-2 py-4 md:px-10 md:py-8">
|
||||
<PathScene
|
||||
pathId={path.id}
|
||||
step={step}
|
||||
title={current ? `${current.title}. ${current.text}` : path.need}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</BitAura>
|
||||
</div>
|
||||
|
||||
<ol className="m-0 list-none p-0 md:order-1 md:col-span-5">
|
||||
@@ -154,12 +163,14 @@ export const PathPicker: React.FC = () => {
|
||||
aria-selected={selected}
|
||||
aria-controls={`path-panel-${path.id}`}
|
||||
onClick={() => setActiveId(path.id)}
|
||||
data-glow
|
||||
className={`bit-card group flex h-full flex-col justify-between rounded-3xl border p-5 text-left transition-all duration-300 md:p-7 ${
|
||||
selected
|
||||
? "border-slate-900 bg-slate-900 text-white"
|
||||
: "border-slate-200 bg-white text-slate-900 hover:-translate-y-1 hover:border-slate-400"
|
||||
}`}
|
||||
>
|
||||
<span className="bit-glow bit-glow-edge" aria-hidden="true" />
|
||||
<span className="font-mono text-[11px] tracking-[0.2em] text-slate-400">
|
||||
0{index + 1}
|
||||
</span>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { BitSeam } from "@/src/components/bits/BitSeam";
|
||||
import * as React from "react";
|
||||
import Image from "next/image";
|
||||
import { motion } from "framer-motion";
|
||||
@@ -32,12 +33,14 @@ export const PortraitSection: React.FC<{
|
||||
>
|
||||
<Image
|
||||
src="/marc-mintel.png"
|
||||
alt="Portrait of Marc Mintel"
|
||||
alt="Marc Mintel, freelance web developer"
|
||||
fill
|
||||
sizes="(min-width: 768px) 62vw, 100vw"
|
||||
className="object-cover object-[50%_18%] grayscale"
|
||||
priority={false}
|
||||
/>
|
||||
{/* Slow light behind the fades, so the dark surface is not flat. */}
|
||||
<div className="bit-aurora" aria-hidden="true" />
|
||||
{/* Fades: into the surface on the left, and at top and bottom. */}
|
||||
<div className="absolute inset-0 hidden bg-gradient-to-r from-slate-900 via-slate-900/75 to-slate-900/10 md:block" />
|
||||
<div className="absolute inset-x-0 bottom-0 h-2/5 bg-gradient-to-t from-slate-900 to-transparent" />
|
||||
@@ -72,5 +75,6 @@ export const PortraitSection: React.FC<{
|
||||
<div className="mt-10 md:mt-14">{children}</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
<BitSeam density={1} />
|
||||
</section>
|
||||
);
|
||||
|
||||
@@ -37,7 +37,7 @@ export const StickyBook: React.FC = () => {
|
||||
<a
|
||||
href="#contact"
|
||||
tabIndex={visible ? 0 : -1}
|
||||
className="flex items-center gap-3 rounded-full bg-slate-900 px-7 py-4 font-sans text-[11px] font-bold uppercase tracking-[0.2em] text-white no-underline shadow-2xl shadow-slate-900/20 transition-transform duration-300 hover:-translate-y-0.5"
|
||||
className="bit-cta flex items-center gap-3 rounded-full bg-slate-900 px-7 py-4 font-sans text-[11px] font-bold uppercase tracking-[0.2em] text-white no-underline shadow-2xl shadow-slate-900/20 transition-transform duration-300 hover:-translate-y-0.5"
|
||||
>
|
||||
Get in touch <span aria-hidden="true">→</span>
|
||||
</a>
|
||||
|
||||
Reference in New Issue
Block a user