Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
176 changes: 176 additions & 0 deletions css/coolness.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
/* Progressive visual layer: the portfolio remains complete without this file. */
body.coolness-ready { --pointer-x: 50vw; --pointer-y: 50vh; }
body.coolness-ready::after {
position: fixed;
inset: 0;
z-index: -1;
background: radial-gradient(circle 32rem at var(--pointer-x) var(--pointer-y), rgba(164, 79, 50, .09), transparent 70%);
content: "";
pointer-events: none;
}
::selection { background: var(--rust); color: var(--paper); }

.field-coordinates {
display: inline-flex;
min-width: 7.5rem;
align-items: center;
gap: .45rem;
color: var(--muted);
font: .58rem var(--mono);
letter-spacing: .06em;
white-space: nowrap;
}
.field-coordinates::before {
width: .42rem;
height: .42rem;
border-radius: 50%;
background: var(--rust);
box-shadow: 0 0 0 .22rem rgba(164, 79, 50, .14);
content: "";
animation: signal-pulse 2.4s ease-in-out infinite;
}

.hero-card { transition: transform 260ms cubic-bezier(.22, 1, .36, 1); transform: perspective(60rem) rotate(1deg) rotateX(var(--field-tilt-y, 0deg)) rotateY(var(--field-tilt-x, 0deg)); }
.field-map::before { animation: orbit-drift 12s linear infinite; }
.field-map::after { animation: signal-pulse 2s ease-in-out infinite; }
.field-map .map-route {
position: absolute;
inset: 13% 9%;
border: 1px solid var(--blue);
border-right-color: transparent;
border-radius: 46% 54% 58% 42%;
opacity: .55;
transform: rotate(24deg);
}
.field-map .map-route::after {
position: absolute;
top: 4%;
right: 15%;
width: .45rem;
height: .45rem;
border: 2px solid var(--blue);
border-radius: 50%;
background: var(--paper);
content: "";
}
.field-map .map-label { position: absolute; right: .55rem; bottom: .45rem; color: var(--muted); font: .53rem var(--mono); letter-spacing: .07em; }

.cool-reveal { opacity: 1; transform: none; }
.coolness-ready .cool-reveal:not(.is-revealed) { opacity: 0; transform: translateY(1.35rem) rotate(.3deg); }
.coolness-ready .cool-reveal { transition: opacity 620ms ease, transform 620ms cubic-bezier(.22, 1, .36, 1); }
.coolness-ready .cool-reveal:nth-child(2n) { transition-delay: 70ms; }

.case-card {
transform: perspective(80rem) rotateX(var(--card-tilt-x, 0deg)) rotateY(var(--card-tilt-y, 0deg));
transform-style: preserve-3d;
transition: border-color 180ms, box-shadow 180ms, transform 220ms cubic-bezier(.22, 1, .36, 1);
}
.case-card.is-tilting { border-color: color-mix(in srgb, var(--accent), var(--text) 28%); box-shadow: 0 1.8rem 4.5rem rgba(55, 42, 25, .16); }
.case-card.is-tilting .case-body { transform: translateZ(1rem); }
.case-body { transition: transform 220ms cubic-bezier(.22, 1, .36, 1); }
.case-visual { isolation: isolate; }
.case-visual::before, .case-visual::after, .case-initials { transition: opacity 180ms; }
.case-visual.has-scene::before, .case-visual.has-scene::after, .case-visual.has-scene .case-initials { opacity: 0; }
.project-scene { position: relative; z-index: 2; width: min(92%, 25rem); color: var(--accent); filter: drop-shadow(0 .8rem 1.4rem rgba(49, 40, 27, .12)); }
.project-scene * { vector-effect: non-scaling-stroke; }
.scene-line { fill: none; stroke: currentColor; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
.scene-faint { fill: none; stroke: currentColor; stroke-width: 1; opacity: .35; }
.scene-fill { fill: currentColor; opacity: .12; stroke: currentColor; stroke-width: 1.5; }
.scene-dot { fill: var(--rust); }
.scene-paper { fill: var(--surface); stroke: currentColor; stroke-width: 1.5; }
.scene-orbit { transform-origin: center; animation: scene-orbit 9s linear infinite; }
.scene-dash { stroke-dasharray: 8 8; animation: dash-travel 8s linear infinite; }
.scene-scan { animation: scan-page 2.8s ease-in-out infinite; }
.scene-wave { transform-origin: center; animation: wave-breathe 1.9s ease-in-out infinite; }
.scene-blink { animation: scene-blink 2.4s step-end infinite; }
.scene-limb { transform-origin: center; animation: pose-shift 4s ease-in-out infinite; }

.cow-companion {
position: fixed;
right: 1rem;
bottom: 1rem;
z-index: 40;
display: grid;
width: 4.25rem;
height: 4.25rem;
place-items: center;
padding: .45rem;
border: 1px solid var(--line);
border-radius: 50%;
background: var(--paper-deep);
box-shadow: var(--shadow);
cursor: pointer;
transition: transform 180ms cubic-bezier(.22, 1, .36, 1);
}
.cow-companion:hover { transform: rotate(-5deg) scale(1.07); }
.cow-companion img { width: 100%; height: 100%; object-fit: contain; filter: grayscale(1) contrast(1.1); mix-blend-mode: multiply; }
.cow-companion.is-signaling { animation: cow-pop 520ms cubic-bezier(.2, .9, .2, 1); }
.cow-message {
position: fixed;
right: 1rem;
bottom: 6rem;
z-index: 39;
max-width: 15rem;
padding: .7rem .85rem;
border: 1px solid var(--line);
border-radius: var(--radius);
background: var(--surface);
box-shadow: var(--shadow);
color: var(--text);
font: .64rem/1.45 var(--mono);
opacity: 0;
transform: translateY(.5rem) rotate(1deg);
transition: opacity 180ms, transform 180ms;
pointer-events: none;
}
.cow-message.is-visible { opacity: 1; transform: translateY(0) rotate(-1deg); }

.cow-trail {
position: fixed;
top: 0;
left: 0;
z-index: 80;
width: 2rem;
height: 2rem;
opacity: 0;
transform: translate3d(-5rem, -5rem, 0) rotate(var(--trail-rotate, 0deg));
transition: opacity 140ms;
pointer-events: none;
will-change: transform;
}
.cow-trail.is-active { opacity: .62; }
.cow-trail img { width: 100%; height: 100%; object-fit: contain; filter: grayscale(1) sepia(.3) contrast(1.15); }

.page-wipe {
position: fixed;
inset: 0;
z-index: 90;
display: grid;
place-items: center;
background: var(--ink);
color: var(--paper);
clip-path: inset(0 100% 0 0);
pointer-events: none;
transition: clip-path 260ms cubic-bezier(.75, 0, .25, 1);
}
.page-wipe::before { width: 115%; height: .85rem; background: url("../assets/images/rope.png") center 48% / 5rem auto repeat-x; content: ""; filter: sepia(.4); transform: rotate(-2deg); }
.page-wipe.is-active { clip-path: inset(0 0 0 0); }

@keyframes signal-pulse { 0%, 100% { transform: scale(.8); opacity: .55; } 50% { transform: scale(1.16); opacity: 1; } }
@keyframes orbit-drift { to { transform: rotate(342deg); } }
@keyframes scene-orbit { to { transform: rotate(360deg); } }
@keyframes dash-travel { to { stroke-dashoffset: -64; } }
@keyframes scan-page { 0%, 100% { transform: translateY(-1.2rem); opacity: .35; } 50% { transform: translateY(1.5rem); opacity: 1; } }
@keyframes wave-breathe { 0%, 100% { transform: scaleY(.7); } 50% { transform: scaleY(1.15); } }
@keyframes scene-blink { 0%, 42% { opacity: 1; } 43%, 50% { opacity: .1; } 51%, 100% { opacity: 1; } }
@keyframes pose-shift { 0%, 100% { transform: rotate(-2deg); } 50% { transform: rotate(4deg); } }
@keyframes cow-pop { 0% { transform: scale(1) rotate(0); } 40% { transform: scale(1.23) rotate(-12deg); } 72% { transform: scale(.94) rotate(7deg); } 100% { transform: scale(1) rotate(0); } }

@media (max-width: 48rem) { .field-coordinates, .cow-trail { display: none; } .cow-companion { width: 3.5rem; height: 3.5rem; } .cow-message { bottom: 5.2rem; } }
@media (pointer: coarse) { .cow-trail { display: none; } .case-card { transform: none !important; } }
@media (prefers-reduced-motion: reduce) {
body.coolness-ready::after, .cow-trail, .page-wipe { display: none; }
.field-coordinates::before, .field-map::before, .field-map::after, .project-scene *, .cow-companion { animation: none !important; }
.coolness-ready .cool-reveal:not(.is-revealed) { opacity: 1; transform: none; }
.case-card, .hero-card { transform: none !important; }
}
177 changes: 177 additions & 0 deletions js/coolness.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
(() => {
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");
const finePointer = window.matchMedia("(pointer: fine)");
const script = document.currentScript;
const siteRoot = script ? new URL("../", script.src) : new URL("../", window.location.href);
const asset = (path) => new URL(path, siteRoot).href;

document.body.classList.add("coolness-ready");

const header = document.querySelector(".site-header");
if (header) {
const coordinates = document.createElement("span");
coordinates.className = "field-coordinates";
coordinates.textContent = "POINTER 000 / 000";
coordinates.setAttribute("aria-hidden", "true");
header.querySelector(".wordmark")?.after(coordinates);
document.addEventListener("pointermove", (event) => {
document.body.style.setProperty("--pointer-x", `${event.clientX}px`);
document.body.style.setProperty("--pointer-y", `${event.clientY}px`);
coordinates.textContent = `POINTER ${String(Math.round(event.clientX)).padStart(3, "0")} / ${String(Math.round(event.clientY)).padStart(3, "0")}`;
}, { passive: true });
}

const map = document.querySelector(".field-map");
if (map) {
const route = document.createElement("span");
route.className = "map-route";
const label = document.createElement("span");
label.className = "map-label";
label.textContent = "SIGNAL ACQUIRED";
map.append(route, label);
if (!reducedMotion.matches && finePointer.matches) {
map.closest(".hero-card")?.addEventListener("pointermove", (event) => {
const card = event.currentTarget;
const bounds = card.getBoundingClientRect();
card.style.setProperty("--field-tilt-x", `${((event.clientX - bounds.left) / bounds.width - .5) * 4}deg`);
card.style.setProperty("--field-tilt-y", `${((event.clientY - bounds.top) / bounds.height - .5) * -4}deg`);
});
}
}

const sceneMarkup = {
"Solar System Missions": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><circle class="scene-faint" cx="160" cy="110" r="72"/><ellipse class="scene-line scene-dash scene-orbit" cx="160" cy="110" rx="125" ry="50"/><circle class="scene-fill" cx="160" cy="110" r="25"/><circle class="scene-dot" cx="276" cy="92" r="6"/><path class="scene-line" d="M35 174C91 125 199 184 285 55"/></svg>`,
"Gesture-Reactive Avatar": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><path class="scene-line" d="M107 76l-24-29 42 14M213 76l24-29-42 14M96 95c0-44 128-44 128 0v45c0 52-128 52-128 0z"/><g class="scene-blink"><path class="scene-line" d="M119 113q14-12 28 0M173 113q14-12 28 0"/></g><path class="scene-line" d="M136 151q24 18 48 0"/><g class="scene-faint"><circle cx="119" cy="113" r="4"/><circle cx="201" cy="113" r="4"/><circle cx="160" cy="132" r="4"/></g></svg>`,
"Ambient Dashboard": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><rect class="scene-paper" x="38" y="38" width="244" height="144" rx="10"/><path class="scene-faint" d="M38 70h244M126 70v112"/><circle class="scene-fill scene-wave" cx="81" cy="123" r="27"/><path class="scene-line" d="M151 101h92M151 122h68M151 143h81"/><circle class="scene-dot" cx="263" cy="54" r="4"/></svg>`,
"HandwritingConverter": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><rect class="scene-paper" x="56" y="35" width="94" height="145" rx="4" transform="rotate(-5 103 107)"/><rect class="scene-paper" x="169" y="35" width="94" height="145" rx="4" transform="rotate(4 216 107)"/><path class="scene-line" d="M74 78q19-22 42 2t19-4M72 103q25 15 60-3M184 76h60M184 96h47M184 116h56M184 136h39"/><path class="scene-line scene-scan" d="M45 109h230"/></svg>`,
"VoxNav": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><g class="scene-wave"><path class="scene-line" d="M31 110h24l9-42 16 86 15-64 13 39 15-74 18 108 17-83 15 51 12-21h29"/></g><rect class="scene-paper" x="210" y="61" width="82" height="98" rx="5"/><path class="scene-line" d="M223 84h48M223 105h35M223 126h44"/><circle class="scene-dot scene-blink" cx="279" cy="144" r="5"/></svg>`,
"Yoga Pose Match": `<svg class="project-scene" viewBox="0 0 320 220" aria-hidden="true"><g class="scene-limb"><circle class="scene-fill" cx="160" cy="48" r="18"/><path class="scene-line" d="M160 66v55M160 82l-69 35M160 82l68 33M160 121l-48 66M160 121l48 66"/></g><path class="scene-faint scene-dash" d="M76 201h168M63 30v171M257 30v171"/><circle class="scene-dot" cx="91" cy="117" r="5"/><circle class="scene-dot" cx="228" cy="115" r="5"/></svg>`
};

function hydrateScene(card) {
if (card.dataset.sceneReady === "true") return;
const title = card.querySelector("h2")?.textContent?.trim();
const markup = sceneMarkup[title];
const visual = card.querySelector(".case-visual");
if (!markup || !visual) return;
const documentFragment = new DOMParser().parseFromString(markup, "image/svg+xml").documentElement;
visual.append(document.importNode(documentFragment, true));
visual.classList.add("has-scene");
card.dataset.sceneReady = "true";
}

function enhanceCard(card) {
hydrateScene(card);
card.classList.add("cool-reveal");
reveal(card);
if (reducedMotion.matches || !finePointer.matches) return;
card.addEventListener("pointermove", (event) => {
const bounds = card.getBoundingClientRect();
card.style.setProperty("--card-tilt-y", `${((event.clientX - bounds.left) / bounds.width - .5) * 2.6}deg`);
card.style.setProperty("--card-tilt-x", `${((event.clientY - bounds.top) / bounds.height - .5) * -2.6}deg`);
card.classList.add("is-tilting");
});
card.addEventListener("pointerleave", () => {
card.style.removeProperty("--card-tilt-x");
card.style.removeProperty("--card-tilt-y");
card.classList.remove("is-tilting");
});
}

let observer;
function reveal(element) {
if (reducedMotion.matches || !("IntersectionObserver" in window)) {
element.classList.add("is-revealed");
return;
}
observer ||= new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (!entry.isIntersecting) return;
entry.target.classList.add("is-revealed");
observer.unobserve(entry.target);
});
}, { threshold: .12, rootMargin: "0px 0px -5%" });
observer.observe(element);
}

document.querySelectorAll("main > section, .preview, .featured").forEach((element) => {
element.classList.add("cool-reveal");
reveal(element);
});
document.querySelectorAll(".case-card").forEach(enhanceCard);
const projectList = document.querySelector("[data-project-list]");
if (projectList) {
new MutationObserver((mutations) => mutations.forEach((mutation) => mutation.addedNodes.forEach((node) => {
if (node instanceof HTMLElement && node.matches(".case-card")) enhanceCard(node);
}))).observe(projectList, { childList: true });
}

const companion = document.createElement("button");
companion.className = "cow-companion";
companion.type = "button";
companion.setAttribute("aria-label", "Ask the field assistant for a signal");
const cow = document.createElement("img");
cow.src = asset("assets/images/cow.png");
cow.alt = "";
companion.append(cow);
const message = document.createElement("div");
message.className = "cow-message";
message.setAttribute("role", "status");
message.setAttribute("aria-live", "polite");
const messages = ["Signal acquired. Keep building.", "Cow-approved system boundary.", "Prototype honestly. Iterate loudly.", "Useful beats impressive.", "Moo is a valid debugging strategy."];
let messageIndex = -1;
let messageTimer;
companion.addEventListener("click", () => {
messageIndex = (messageIndex + 1) % messages.length;
message.textContent = messages[messageIndex];
message.classList.add("is-visible");
companion.classList.remove("is-signaling");
requestAnimationFrame(() => companion.classList.add("is-signaling"));
clearTimeout(messageTimer);
messageTimer = setTimeout(() => message.classList.remove("is-visible"), 3200);
});
document.body.append(message, companion);

if (!reducedMotion.matches && finePointer.matches) {
const trail = document.createElement("div");
trail.className = "cow-trail";
const image = document.createElement("img");
image.src = asset("assets/images/cow.png");
image.alt = "";
trail.append(image);
document.body.append(trail);
let targetX = -80, targetY = -80, x = targetX, y = targetY, rotation = 0, frame;
const tick = () => {
x += (targetX - x) * .18;
y += (targetY - y) * .18;
rotation += ((targetX - x) * .12 - rotation) * .12;
trail.style.transform = `translate3d(${x + 15}px, ${y + 18}px, 0) rotate(${rotation}deg)`;
if (Math.abs(targetX - x) + Math.abs(targetY - y) > .6) frame = requestAnimationFrame(tick);
else frame = null;
};
document.addEventListener("pointermove", (event) => {
targetX = event.clientX;
targetY = event.clientY;
trail.classList.add("is-active");
if (!frame) frame = requestAnimationFrame(tick);
}, { passive: true });
document.addEventListener("pointerleave", () => trail.classList.remove("is-active"));
document.addEventListener("visibilitychange", () => { if (document.hidden && frame) { cancelAnimationFrame(frame); frame = null; } });
}

if (!reducedMotion.matches) {
const wipe = document.createElement("div");
wipe.className = "page-wipe";
wipe.setAttribute("aria-hidden", "true");
document.body.append(wipe);
document.addEventListener("click", (event) => {
const link = event.target.closest("a[href]");
if (!link || event.defaultPrevented || event.button !== 0 || event.metaKey || event.ctrlKey || event.shiftKey || link.target === "_blank" || link.hasAttribute("download")) return;
const destination = new URL(link.href, window.location.href);
if (destination.origin !== window.location.origin || destination.pathname === window.location.pathname && destination.hash) return;
event.preventDefault();
wipe.classList.add("is-active");
setTimeout(() => { window.location.href = destination.href; }, 260);
});
}
})();
14 changes: 13 additions & 1 deletion js/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,16 @@ document.querySelectorAll("[data-current-year]").forEach((element) => {
document.querySelectorAll(".mobile-nav a").forEach((link) => {
link.addEventListener("click", () => link.closest("details")?.removeAttribute("open"));
});


const mainScript = document.currentScript;
if (mainScript) {
const style = document.createElement("link");
style.rel = "stylesheet";
style.href = new URL("../css/coolness.css", mainScript.src).href;
document.head.append(style);

const coolness = document.createElement("script");
coolness.src = new URL("coolness.js", mainScript.src).href;
coolness.defer = true;
document.head.append(coolness);
}
Loading