mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-08 04:08:24 +00:00
Content Updates with projects and experience
This commit is contained in:
@@ -134,12 +134,12 @@ export default function AnimatedBackground({
|
||||
const cy = 0;
|
||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||
|
||||
// Dense starfield: ~3x the previous density for a richly star-packed sky.
|
||||
// One star per ~1100 px^2 of the disk, capped for perf (~15% anchors).
|
||||
// Ultra-dense starfield: ~5x the previous density for a richly packed sky.
|
||||
// One star per ~220 px^2 of the disk, capped for perf (~17% anchors).
|
||||
const diskArea = Math.PI * maxR * maxR;
|
||||
const count = Math.min(1500, Math.floor(diskArea / 1100));
|
||||
const count = Math.min(3000, Math.floor(diskArea / 220));
|
||||
stars = Array.from({ length: count }, (_, i) => {
|
||||
const anchor = i % 7 === 0; // ~15% anchors -> dense constellation backbone
|
||||
const anchor = i % 6 === 0; // ~17% anchors -> very dense constellation backbone
|
||||
// Uniform-ish distribution across the disk via sqrt on radius.
|
||||
const rr = maxR * Math.sqrt(Math.random());
|
||||
const a = Math.random() * Math.PI * 2;
|
||||
|
||||
@@ -41,12 +41,12 @@ const {
|
||||
} = Astro.props;
|
||||
---
|
||||
|
||||
<section class="relative min-h-[60vh] overflow-hidden pt-28 sm:pt-32">
|
||||
<section class="relative overflow-hidden pb-8 pt-28 sm:pt-32">
|
||||
<!-- Full animated Milky Way field + stars (matches the Home/Interests hero).
|
||||
Carries the per-page morph name (one .vt-sky per page snapshot). -->
|
||||
<AnimatedBackground client:idle className="vt-sky" />
|
||||
|
||||
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-16">
|
||||
<div class="container-prose relative z-10 flex flex-col items-start gap-6 py-8">
|
||||
<Reveal>
|
||||
<a
|
||||
href="/interests"
|
||||
|
||||
@@ -2,20 +2,15 @@
|
||||
/**
|
||||
* CelestialFrame.astro
|
||||
* ---------------------------------------------------------------------------
|
||||
* An ornamental frame for imagery: rounded image container with thin gold
|
||||
* hairline + small star-burst flourishes in the corners and a faint sacred-
|
||||
* geometry tint behind. Wraps an <img> (or any media) passed via the default
|
||||
* slot.
|
||||
* An ornamental frame for imagery: rounded image container with a gold border
|
||||
* + small star-burst flourishes in the corners and a faint sacred-geometry tint
|
||||
* behind. Wraps an <img> (or any media) passed via the default slot.
|
||||
*
|
||||
* The frame is structural (not just decorative), so it owns the rounded
|
||||
* corners + border + overflow. The corner sparkles are absolutely positioned
|
||||
* SVGs that inherit the accent color.
|
||||
*
|
||||
* Usage:
|
||||
* <CelestialFrame>
|
||||
* <img src="/images/hero.jpg" alt="..." class="h-full w-full object-cover" />
|
||||
* </CelestialFrame>
|
||||
* <CelestialFrame rounded="none"> ... </CelestialFrame>
|
||||
* CORNER FIT: the gold border lives on the outer <figure>. The media container
|
||||
* is INSET by exactly the border width (via --frame-bw) and its corner radius
|
||||
* is reduced by the same amount, so the image nests flush inside the border at
|
||||
* every corner — no gap, no mismatch. Adjust --frame-bw here in one place if
|
||||
* the border width changes.
|
||||
*/
|
||||
import StarBurst from "./StarBurst.astro";
|
||||
|
||||
@@ -32,30 +27,33 @@ const {
|
||||
emphasis = "subtle",
|
||||
} = Astro.props;
|
||||
|
||||
const radius = {
|
||||
none: "rounded-none",
|
||||
lg: "rounded-lg",
|
||||
xl: "rounded-xl",
|
||||
"2xl": "rounded-2xl",
|
||||
}[rounded];
|
||||
|
||||
const borderStyle = emphasis === "strong" ? "border-2" : "border";
|
||||
// Outer radius (px) per option. The inner media radius is outer - border width
|
||||
// so the image corner nests perfectly inside the border's corner.
|
||||
const outerPx = { none: 0, lg: 8, xl: 12, "2xl": 16 }[rounded];
|
||||
const bw = emphasis === "strong" ? 4 : 3; // border width in px (matches border-2 / 3px)
|
||||
const innerPx = Math.max(0, outerPx - bw);
|
||||
---
|
||||
|
||||
<figure
|
||||
class:list={["relative", radius, borderStyle, className]}
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 75%, transparent); border-width: 3px;"
|
||||
class:list={["relative", className]}
|
||||
style={`border-radius: ${outerPx}px; border: ${bw}px solid color-mix(in srgb, var(--color-accent) 75%, transparent); overflow: hidden;`}
|
||||
>
|
||||
{/* Faint sacred-geometry tint behind the image. */}
|
||||
{/* Faint sacred-geometry tint behind the image (fills the full figure). */}
|
||||
<div
|
||||
class:list={["absolute inset-0", radius]}
|
||||
style="background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));"
|
||||
class="absolute inset-0"
|
||||
style={`border-radius: ${outerPx}px; background: color-mix(in srgb, var(--color-violet, var(--color-accent)) 14%, var(--color-surface-sunken));`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
|
||||
{/* The media itself, clipped to the frame. */}
|
||||
<div class:list={["relative overflow-hidden", radius]}>
|
||||
{/* The media itself, in normal flow (defines the figure's height via the slot's
|
||||
aspect-ratio div). Its radius = outer radius - border width, so the image's
|
||||
rounded corner nests PERFECTLY inside the border's rounded corner. The
|
||||
figure's `overflow: hidden` clips the tint layer to the outer radius. */}
|
||||
<div
|
||||
class="relative overflow-hidden"
|
||||
style={`border-radius: ${innerPx}px;`}
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
@@ -79,3 +77,5 @@ const borderStyle = emphasis === "strong" ? "border-2" : "border";
|
||||
style="color: var(--color-accent); opacity:.7;"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
|
||||
|
||||
@@ -69,9 +69,9 @@ export default function CircuitField({
|
||||
canvas.height = Math.floor(height * dpr);
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
// Build an orthogonal circuit on a grid: place nodes at grid intersections
|
||||
// (with jitter + random omissions), then connect neighbors with right-angle
|
||||
// traces. This reads as a PCB / schematic, not a star map.
|
||||
// Build a clean, ALIGNED orthogonal circuit on a grid: pads sit exactly at
|
||||
// grid intersections (no jitter) and traces connect them in straight
|
||||
// right-angle lines. Reads as an orderly schematic / PCB layout.
|
||||
const step = 64;
|
||||
const cols = Math.ceil(width / step) + 1;
|
||||
const rows = Math.ceil(height / step) + 1;
|
||||
@@ -83,9 +83,8 @@ export default function CircuitField({
|
||||
// ~62% of intersections get a pad.
|
||||
present[r][c] = Math.random() < 0.62;
|
||||
if (present[r][c]) {
|
||||
const jx = (Math.random() - 0.5) * step * 0.25;
|
||||
const jy = (Math.random() - 0.5) * step * 0.25;
|
||||
nodes.push({ x: c * step + jx, y: r * step + jy });
|
||||
// Snapped to the grid — no jitter, so traces land flush on pads.
|
||||
nodes.push({ x: c * step, y: r * step });
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -236,21 +235,15 @@ export default function CircuitField({
|
||||
className={`pointer-events-none absolute inset-0 -z-10 overflow-hidden ${className}`}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{/* Base dark purple/blue gradient — the professional hero's atmosphere. */}
|
||||
{/* Base gradient — IDENTICAL colors to AnimatedBackground's night sky so
|
||||
the morph between star field and circuit field is color-consistent.
|
||||
No nebula radials here (the user asked to keep the circuit style only);
|
||||
just the clean deep-blue sky base. */}
|
||||
<div
|
||||
className="absolute inset-0"
|
||||
style={{
|
||||
background:
|
||||
'linear-gradient(160deg, #0a0e2a 0%, #14123a 38%, #1a1648 64%, #0e0c28 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* Animated violet/teal nebula clouds for depth + motion. */}
|
||||
<div
|
||||
className="animate-gradient-shift absolute inset-0"
|
||||
style={{
|
||||
backgroundImage:
|
||||
'radial-gradient(50% 60% at 18% 20%, rgba(139, 123, 216, 0.28), transparent 62%), radial-gradient(45% 55% at 85% 15%, rgba(111, 182, 173, 0.16), transparent 60%), radial-gradient(55% 70% at 70% 110%, rgba(139, 123, 216, 0.20), transparent 60%)',
|
||||
backgroundSize: '200% 200%',
|
||||
'linear-gradient(180deg, #04061a 0%, #070b24 32%, #0a1230 58%, #0e1a3e 82%, #12224a 100%)',
|
||||
}}
|
||||
/>
|
||||
{/* Circuit traces + nodes + signal pulses. */}
|
||||
|
||||
@@ -92,12 +92,11 @@ export default function ConstellationField({
|
||||
const cx = width;
|
||||
const cy = 0;
|
||||
const maxR = Math.sqrt(cx * cx + height * height) + 40;
|
||||
// Denser field for richer constellations. One star per ~4500 px^2, scaled
|
||||
// ~3x density for a richly star-packed sky. One star per ~1500 px^2,
|
||||
// scaled by density; ~15% anchors for a dense constellation backbone.
|
||||
// Ultra-dense: ~5x. One star per ~300 px^2, scaled by density; ~17%
|
||||
// anchors for a very dense constellation backbone.
|
||||
const count = Math.min(
|
||||
900,
|
||||
Math.floor(((Math.PI * maxR * maxR) / 1500) * density),
|
||||
3000,
|
||||
Math.floor(((Math.PI * maxR * maxR) / 300) * density),
|
||||
);
|
||||
stars = Array.from({ length: count }, (_, i) => {
|
||||
const anchor = i % 7 === 0;
|
||||
|
||||
@@ -2,14 +2,15 @@
|
||||
/**
|
||||
* SectionNav.astro
|
||||
* ---------------------------------------------------------------------------
|
||||
* Sticky in-page sub-navigation for the Professional page. Jumps between the
|
||||
* six anchor sections (Summary, Experience, Skills, Projects, Education,
|
||||
* Contact) via smooth scroll. Hidden on small screens where the page reads as a
|
||||
* simple scroll. Active state is computed client-side via IntersectionObserver.
|
||||
* Fixed LEFT-SIDE vertical rail for the Professional page. Shows the six
|
||||
* sections as a stacked icon+label list, highlights the section currently in
|
||||
* view, and lets the user click any item to smooth-scroll to it. A vertical
|
||||
* progress line beside the items fills as the user scrolls down the page.
|
||||
*
|
||||
* Sits BELOW the hero and is OPAQUE (a solid dark nebula surface) so it remains
|
||||
* readable while scrolling over content. Sticky so it tracks the viewport once
|
||||
* the user scrolls past it.
|
||||
* Stylistically it mirrors the page's `.card` language (same raised surface,
|
||||
* border, radius) so it reads as a sibling of the content cards rather than a
|
||||
* separate chrome element. Icons + labels use bright tokens so they pop on the
|
||||
* dark surface. Hidden below `lg`.
|
||||
*/
|
||||
import Icon from "@components/Icon.astro";
|
||||
const sections = [
|
||||
@@ -22,28 +23,79 @@ const sections = [
|
||||
];
|
||||
---
|
||||
|
||||
<nav aria-label="Professional sections" class="hidden lg:block">
|
||||
<nav
|
||||
aria-label="Professional sections"
|
||||
class="pro-section-nav fixed left-4 top-1/2 z-40 hidden -translate-y-1/2 flex-col lg:flex"
|
||||
data-pro-nav
|
||||
>
|
||||
{/* Rail container — matches the .card aesthetic: raised surface, hairline
|
||||
border, 1rem radius. Bright icons + labels inside. */}
|
||||
<div
|
||||
class="sticky top-20 z-30 flex flex-wrap gap-1 rounded-xl border p-1.5 shadow-lg"
|
||||
style="border-color: color-mix(in srgb, var(--color-accent) 28%, var(--color-border)); background: linear-gradient(180deg, rgba(12, 14, 40, 0.96), rgba(8, 10, 32, 0.96)); backdrop-filter: blur(8px); -webkit-backdrop-filter: blur(8px);"
|
||||
class="card relative flex flex-col gap-1 p-3"
|
||||
style="border-color: var(--color-border); background: var(--color-surface-raised); backdrop-filter: blur(10px); -webkit-backdrop-filter: blur(10px); box-shadow: 0 18px 40px -24px rgba(11, 16, 38, 0.6);"
|
||||
>
|
||||
{/* Vertical progress track behind the dots. */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-[1.55rem] top-6 bottom-6 w-px"
|
||||
style="background: color-mix(in srgb, var(--color-border) 80%, transparent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
{/* The fill bar — height set by JS as the user scrolls. */}
|
||||
<div
|
||||
class="pointer-events-none absolute left-[1.55rem] top-6 w-px origin-top"
|
||||
style="background: var(--color-accent); height: 0%; box-shadow: 0 0 8px color-mix(in srgb, var(--color-accent) 60%, transparent);"
|
||||
data-pro-fill
|
||||
aria-hidden="true"
|
||||
>
|
||||
</div>
|
||||
|
||||
{sections.map((s) => (
|
||||
<a
|
||||
href={`#${s.id}`}
|
||||
data-section-link={s.id}
|
||||
class="inline-flex items-center gap-1.5 rounded-lg px-3 py-1.5 font-sans text-sm font-medium transition-colors"
|
||||
style="color: var(--color-cream); opacity: 0.7;"
|
||||
class="group relative flex items-center gap-3 rounded-lg px-2.5 py-2 font-sans text-sm font-semibold transition-all"
|
||||
style="color: var(--color-cream); opacity: 0.85;"
|
||||
>
|
||||
<Icon name={s.icon} class="h-4 w-4" />
|
||||
{s.label}
|
||||
<span
|
||||
class="relative z-10 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-lg transition-colors"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 14%, transparent); color: var(--color-cream);"
|
||||
data-section-dot={s.id}
|
||||
>
|
||||
<Icon name={s.icon} class="h-[1.15rem] w-[1.15rem]" />
|
||||
</span>
|
||||
<span class="relative z-10 tracking-wide">{s.label}</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<style>
|
||||
/* Active link: full opacity + accent text + subtle accent wash. */
|
||||
.pro-section-nav a[data-active="true"] {
|
||||
opacity: 1 !important;
|
||||
color: var(--color-accent) !important;
|
||||
background: color-mix(in srgb, var(--color-accent) 14%, transparent);
|
||||
}
|
||||
/* Active dot: filled accent, bright icon, glow ring. */
|
||||
.pro-section-nav a[data-active="true"] [data-section-dot] {
|
||||
background: color-mix(in srgb, var(--color-accent) 80%, transparent) !important;
|
||||
color: var(--color-ink) !important;
|
||||
box-shadow: 0 0 12px color-mix(in srgb, var(--color-accent) 65%, transparent);
|
||||
}
|
||||
/* Hover lift on inactive items. */
|
||||
.pro-section-nav a:hover {
|
||||
opacity: 1 !important;
|
||||
background: color-mix(in srgb, var(--color-accent) 8%, transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Highlight the link for whichever section is currently in view.
|
||||
// Spy: highlight the link for whichever section is in view + fill the rail.
|
||||
function setupSectionSpy() {
|
||||
const nav = document.querySelector<HTMLElement>("[data-pro-nav]");
|
||||
if (!nav) return;
|
||||
|
||||
const links = Array.from(
|
||||
document.querySelectorAll<HTMLAnchorElement>("[data-section-link]"),
|
||||
);
|
||||
@@ -51,20 +103,16 @@ const sections = [
|
||||
const sections = links
|
||||
.map((l) => document.getElementById(l.dataset.sectionLink!))
|
||||
.filter((el): el is HTMLElement => Boolean(el));
|
||||
const fill = nav.querySelector<HTMLElement>("[data-pro-fill]");
|
||||
|
||||
const setActive = (id: string) => {
|
||||
for (const l of links) {
|
||||
const active = l.dataset.sectionLink === id;
|
||||
l.style.color = active
|
||||
? "var(--color-accent)"
|
||||
: "var(--color-cream)";
|
||||
l.style.opacity = active ? "1" : "0.7";
|
||||
l.style.background = active
|
||||
? "color-mix(in srgb, var(--color-accent) 16%, transparent)"
|
||||
: "transparent";
|
||||
l.setAttribute("data-active", active ? "true" : "false");
|
||||
}
|
||||
};
|
||||
|
||||
// Highlight the most-visible section.
|
||||
const io = new IntersectionObserver(
|
||||
(entries) => {
|
||||
const visible = entries
|
||||
@@ -75,6 +123,25 @@ const sections = [
|
||||
{ rootMargin: "-30% 0px -55% 0px", threshold: [0, 0.25, 0.5] },
|
||||
);
|
||||
sections.forEach((s) => io.observe(s));
|
||||
|
||||
// Progress fill: fraction of the page scrolled.
|
||||
const trackTop = 24; // px — matches top-6 on the track/fill
|
||||
const trackBottom = 24; // px — matches bottom-6
|
||||
const updateFill = () => {
|
||||
if (!fill) return;
|
||||
const doc = document.documentElement;
|
||||
const max = doc.scrollHeight - doc.clientHeight;
|
||||
const frac = max > 0 ? Math.max(0, Math.min(1, window.scrollY / max)) : 0;
|
||||
// Fill height as a percentage of the track length (which spans the card
|
||||
// minus top/bottom padding).
|
||||
const cardEl = fill.parentElement;
|
||||
if (!cardEl) return;
|
||||
const trackPx = cardEl.clientHeight - trackTop - trackBottom;
|
||||
fill.style.height = `${(frac * trackPx).toFixed(1)}px`;
|
||||
};
|
||||
updateFill();
|
||||
window.addEventListener("scroll", updateFill, { passive: true });
|
||||
window.addEventListener("resize", updateFill);
|
||||
}
|
||||
setupSectionSpy();
|
||||
document.addEventListener("astro:page-load", setupSectionSpy);
|
||||
|
||||
@@ -55,7 +55,8 @@ const techStack = [
|
||||
{
|
||||
icon: "git",
|
||||
label: "Tools",
|
||||
value: "Git, Gitea, CI/CD pipelines",
|
||||
value:
|
||||
"PostgreSQL, VSCode, Unity, XCode, Android Studio, Git, Gitea, CI/CD pipelines",
|
||||
hint: "Add the rest of your toolchain.",
|
||||
},
|
||||
{
|
||||
|
||||
@@ -43,23 +43,28 @@ const rendered = await Promise.all(
|
||||
{rendered.map(({ entry, Content }, i) => (
|
||||
<li class="relative pl-12">
|
||||
<Reveal delay={i * 80}>
|
||||
{/* Marker */}
|
||||
{/* Marker dot on the timeline line (kept small; the icon now lives inline
|
||||
with the company name inside the card so it never clips the card). */}
|
||||
<span
|
||||
class="absolute left-0 top-0 inline-flex h-9 w-9 items-center justify-center rounded-full border-4"
|
||||
style="background: var(--color-surface-raised); border-color: var(--color-surface);"
|
||||
class="absolute left-[1.15rem] top-7 h-3 w-3 -translate-x-1/2 rounded-full"
|
||||
style="background: var(--color-accent); box-shadow: 0 0 0 4px var(--color-surface);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<span
|
||||
class="inline-flex h-9 w-9 items-center justify-center rounded-full"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
|
||||
>
|
||||
<Icon name={entry.data.icon} class="h-4 w-4" />
|
||||
</span>
|
||||
</span>
|
||||
|
||||
<div class="card flex flex-col gap-3">
|
||||
<div class="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1">
|
||||
<h3 style="color: var(--color-text);">{entry.data.company}</h3>
|
||||
{/* Header row: icon + company name inline, period on the right. */}
|
||||
<div class="flex flex-wrap items-center justify-between gap-x-4 gap-y-1">
|
||||
<h3 class="flex items-center gap-3" style="color: var(--color-text);">
|
||||
<span
|
||||
class="inline-flex h-9 w-9 shrink-0 items-center justify-center rounded-full"
|
||||
style="background: color-mix(in srgb, var(--color-accent) 16%, transparent);"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<Icon name={entry.data.icon} class="h-5 w-5" style="color: var(--color-accent);" />
|
||||
</span>
|
||||
{entry.data.company}
|
||||
</h3>
|
||||
<span class="font-mono text-xs uppercase tracking-[0.18em]" style="color: var(--color-text-subtle);">
|
||||
{entry.data.period}
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user