feat: enhance resume interactions
This commit is contained in:
+317
-39
@@ -231,10 +231,6 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
<h2 id="rail-name">{displayNameFor(resume)}</h2>
|
<h2 id="rail-name">{displayNameFor(resume)}</h2>
|
||||||
<p id="rail-intent">{resume.intent}</p>
|
<p id="rail-intent">{resume.intent}</p>
|
||||||
<dl>
|
<dl>
|
||||||
<div>
|
|
||||||
<dt id="phone-label">{labels.zh.phone}</dt>
|
|
||||||
<dd><a id="rail-phone" href={`tel:${resume.contact.phone}`}>{resume.contact.phone}</a></dd>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<dt id="email-label">{labels.zh.email}</dt>
|
<dt id="email-label">{labels.zh.email}</dt>
|
||||||
<dd class="email-list" id="rail-emails">
|
<dd class="email-list" id="rail-emails">
|
||||||
@@ -385,7 +381,6 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
<div class="contact-actions">
|
<div class="contact-actions">
|
||||||
<a class="button button-primary" id="send-email" href={`mailto:${resume.contact.email}`}>{labels.zh.sendEmail}</a>
|
<a class="button button-primary" id="send-email" href={`mailto:${resume.contact.email}`}>{labels.zh.sendEmail}</a>
|
||||||
<a class="button" id="contact-email-alt" href={`mailto:${resume.contact.emailAlt}`}>{resume.contact.emailAlt}</a>
|
<a class="button" id="contact-email-alt" href={`mailto:${resume.contact.emailAlt}`}>{resume.contact.emailAlt}</a>
|
||||||
<a class="button" id="contact-phone" href={`tel:${resume.contact.phone}`}>{resume.contact.phone}</a>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
@@ -604,11 +599,8 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
setText("rail-label", text.candidate);
|
setText("rail-label", text.candidate);
|
||||||
setText("rail-name", name);
|
setText("rail-name", name);
|
||||||
setText("rail-intent", data.intent);
|
setText("rail-intent", data.intent);
|
||||||
setText("phone-label", text.phone);
|
|
||||||
setText("email-label", text.email);
|
setText("email-label", text.email);
|
||||||
setText("education-label", text.education);
|
setText("education-label", text.education);
|
||||||
setText("rail-phone", data.contact.phone);
|
|
||||||
$("rail-phone").href = `tel:${data.contact.phone}`;
|
|
||||||
setText("rail-email", data.contact.email);
|
setText("rail-email", data.contact.email);
|
||||||
$("rail-email").href = `mailto:${data.contact.email}`;
|
$("rail-email").href = `mailto:${data.contact.email}`;
|
||||||
setText("rail-email-alt", data.contact.emailAlt);
|
setText("rail-email-alt", data.contact.emailAlt);
|
||||||
@@ -632,8 +624,6 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
$("send-email").href = `mailto:${data.contact.email}`;
|
$("send-email").href = `mailto:${data.contact.email}`;
|
||||||
setText("contact-email-alt", data.contact.emailAlt);
|
setText("contact-email-alt", data.contact.emailAlt);
|
||||||
$("contact-email-alt").href = `mailto:${data.contact.emailAlt}`;
|
$("contact-email-alt").href = `mailto:${data.contact.emailAlt}`;
|
||||||
setText("contact-phone", data.contact.phone);
|
|
||||||
$("contact-phone").href = `tel:${data.contact.phone}`;
|
|
||||||
setText("footer-text", `© 2026 ${name}. ${text.footer}`);
|
setText("footer-text", `© 2026 ${name}. ${text.footer}`);
|
||||||
|
|
||||||
renderBasics(data);
|
renderBasics(data);
|
||||||
@@ -687,6 +677,10 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
ty: window.innerHeight * 0.42,
|
ty: window.innerHeight * 0.42,
|
||||||
speed: 0,
|
speed: 0,
|
||||||
};
|
};
|
||||||
|
const trail = [];
|
||||||
|
const bursts = [];
|
||||||
|
const tokens = [];
|
||||||
|
const tokenLexicon = ["AI", "SSE", "Agent", "LLM", "RAG", "Vision", "Next", "RN", "3D"];
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
@@ -698,19 +692,32 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
? {
|
? {
|
||||||
dot: "rgba(139, 180, 255, 0.55)",
|
dot: "rgba(139, 180, 255, 0.55)",
|
||||||
dotAlt: "rgba(99, 215, 155, 0.5)",
|
dotAlt: "rgba(99, 215, 155, 0.5)",
|
||||||
line: "rgba(244, 239, 230, 0.12)",
|
dotHot: "rgba(255, 138, 168, 0.64)",
|
||||||
|
line: "rgba(244, 239, 230, 0.11)",
|
||||||
|
lineActive: "rgba(139, 180, 255, 0.48)",
|
||||||
cursor: "rgba(240, 179, 90, 0.34)",
|
cursor: "rgba(240, 179, 90, 0.34)",
|
||||||
|
trail: "rgba(139, 180, 255, 0.58)",
|
||||||
|
trailFast: "rgba(255, 138, 168, 0.62)",
|
||||||
|
burst: "rgba(240, 179, 90, 0.68)",
|
||||||
|
token: "rgba(244, 239, 230, 0.7)",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
dot: "rgba(47, 95, 190, 0.5)",
|
dot: "rgba(47, 95, 190, 0.5)",
|
||||||
dotAlt: "rgba(22, 133, 94, 0.45)",
|
dotAlt: "rgba(22, 133, 94, 0.45)",
|
||||||
line: "rgba(23, 23, 23, 0.11)",
|
dotHot: "rgba(180, 60, 100, 0.55)",
|
||||||
|
line: "rgba(23, 23, 23, 0.1)",
|
||||||
|
lineActive: "rgba(47, 95, 190, 0.42)",
|
||||||
cursor: "rgba(167, 101, 22, 0.28)",
|
cursor: "rgba(167, 101, 22, 0.28)",
|
||||||
|
trail: "rgba(47, 95, 190, 0.5)",
|
||||||
|
trailFast: "rgba(180, 60, 100, 0.52)",
|
||||||
|
burst: "rgba(167, 101, 22, 0.52)",
|
||||||
|
token: "rgba(23, 23, 23, 0.58)",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function seedParticles() {
|
function seedParticles() {
|
||||||
const count = Math.min(86, Math.max(38, Math.floor((width * height) / 17000)));
|
const compact = width < 760;
|
||||||
|
const count = Math.min(compact ? 58 : 96, Math.max(compact ? 30 : 44, Math.floor((width * height) / (compact ? 23000 : 15500))));
|
||||||
particles = Array.from({ length: count }, (_, index) => ({
|
particles = Array.from({ length: count }, (_, index) => ({
|
||||||
x: Math.random() * width,
|
x: Math.random() * width,
|
||||||
y: Math.random() * height,
|
y: Math.random() * height,
|
||||||
@@ -718,6 +725,7 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
vy: (Math.random() - 0.5) * 0.24,
|
vy: (Math.random() - 0.5) * 0.24,
|
||||||
size: 0.8 + Math.random() * 1.8,
|
size: 0.8 + Math.random() * 1.8,
|
||||||
hue: index % 3,
|
hue: index % 3,
|
||||||
|
pulse: 0,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -734,10 +742,146 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
function onPointerMove(event) {
|
function onPointerMove(event) {
|
||||||
const dx = event.clientX - pointer.tx;
|
const dx = event.clientX - pointer.tx;
|
||||||
const dy = event.clientY - pointer.ty;
|
const dy = event.clientY - pointer.ty;
|
||||||
|
const speed = Math.min(1, Math.hypot(dx, dy) / 80);
|
||||||
pointer.tx = event.clientX;
|
pointer.tx = event.clientX;
|
||||||
pointer.ty = event.clientY;
|
pointer.ty = event.clientY;
|
||||||
pointer.speed = Math.min(1, Math.hypot(dx, dy) / 80);
|
pointer.speed = Math.max(pointer.speed, speed);
|
||||||
pointer.active = true;
|
pointer.active = true;
|
||||||
|
if (Math.hypot(dx, dy) > 2) {
|
||||||
|
trail.push({ x: pointer.tx, y: pointer.ty, speed, life: 1 });
|
||||||
|
if (trail.length > (width < 760 ? 18 : 28)) trail.shift();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function emitBurst(x, y, power = 1) {
|
||||||
|
const compact = width < 760;
|
||||||
|
bursts.push({ x, y, age: 0, life: compact ? 42 : 54, power });
|
||||||
|
if (bursts.length > 8) bursts.shift();
|
||||||
|
|
||||||
|
particles.forEach((particle) => {
|
||||||
|
const dx = particle.x - x;
|
||||||
|
const dy = particle.y - y;
|
||||||
|
const dist = Math.hypot(dx, dy);
|
||||||
|
if (dist < 260) {
|
||||||
|
const force = (1 - dist / 260) * (1.7 + power);
|
||||||
|
particle.vx += (dx / (dist || 1)) * force * 0.58;
|
||||||
|
particle.vy += (dy / (dist || 1)) * force * 0.58;
|
||||||
|
particle.pulse = 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tokenCount = compact ? 4 : 7;
|
||||||
|
for (let index = 0; index < tokenCount; index += 1) {
|
||||||
|
const angle = Math.random() * Math.PI * 2;
|
||||||
|
const velocity = 1.15 + Math.random() * 2.15;
|
||||||
|
tokens.push({
|
||||||
|
text: tokenLexicon[Math.floor(Math.random() * tokenLexicon.length)],
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
vx: Math.cos(angle) * velocity,
|
||||||
|
vy: Math.sin(angle) * velocity - 0.35,
|
||||||
|
age: 0,
|
||||||
|
life: 34 + Math.random() * 24,
|
||||||
|
size: 9 + Math.random() * 4,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (tokens.length > 42) tokens.splice(0, tokens.length - 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onPointerDown(event) {
|
||||||
|
onPointerMove(event);
|
||||||
|
emitBurst(event.clientX, event.clientY, 0.75 + pointer.speed);
|
||||||
|
if (event.target instanceof Element) {
|
||||||
|
const target = event.target.closest(
|
||||||
|
"a, button, .project-row, .map-node, .strength-grid article, .domain-strip article, .skill-list span, .tech-line span",
|
||||||
|
);
|
||||||
|
if (target) {
|
||||||
|
target.classList.add("is-pressed");
|
||||||
|
window.setTimeout(() => target.classList.remove("is-pressed"), 420);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTrail(colors) {
|
||||||
|
for (let index = trail.length - 1; index >= 0; index -= 1) {
|
||||||
|
trail[index].life *= 0.935;
|
||||||
|
if (trail[index].life < 0.035) trail.splice(index, 1);
|
||||||
|
}
|
||||||
|
if (trail.length < 2) return;
|
||||||
|
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
for (let index = 1; index < trail.length; index += 1) {
|
||||||
|
const previous = trail[index - 1];
|
||||||
|
const point = trail[index];
|
||||||
|
const strength = Math.min(previous.life, point.life) * (index / trail.length);
|
||||||
|
ctx.globalAlpha = strength * (0.34 + point.speed * 0.42);
|
||||||
|
ctx.strokeStyle = point.speed > 0.46 ? colors.trailFast : colors.trail;
|
||||||
|
ctx.lineWidth = 1.1 + point.speed * 2.4 + (index / trail.length) * 1.2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(previous.x, previous.y);
|
||||||
|
ctx.lineTo(point.x, point.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawBursts(colors) {
|
||||||
|
bursts.forEach((burst) => {
|
||||||
|
const t = burst.age / burst.life;
|
||||||
|
const fade = Math.max(0, 1 - t);
|
||||||
|
for (let ring = 0; ring < 3; ring += 1) {
|
||||||
|
const radius = 10 + t * (62 + ring * 28) * burst.power + ring * 9;
|
||||||
|
ctx.globalAlpha = fade * (0.38 - ring * 0.08);
|
||||||
|
ctx.strokeStyle = ring === 1 ? colors.trail : colors.burst;
|
||||||
|
ctx.lineWidth = 1.2 + ring * 0.35;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(burst.x, burst.y, radius, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = fade * 0.34;
|
||||||
|
ctx.strokeStyle = colors.burst;
|
||||||
|
ctx.lineWidth = 1;
|
||||||
|
for (let spoke = 0; spoke < 10; spoke += 1) {
|
||||||
|
const angle = (Math.PI * 2 * spoke) / 10 + t * 0.7;
|
||||||
|
const inner = 20 + t * 46;
|
||||||
|
const outer = inner + 12 + burst.power * 8;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(burst.x + Math.cos(angle) * inner, burst.y + Math.sin(angle) * inner);
|
||||||
|
ctx.lineTo(burst.x + Math.cos(angle) * outer, burst.y + Math.sin(angle) * outer);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
burst.age += 1;
|
||||||
|
});
|
||||||
|
for (let index = bursts.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (bursts[index].age > bursts[index].life) bursts.splice(index, 1);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function drawTokens(colors) {
|
||||||
|
ctx.textAlign = "center";
|
||||||
|
ctx.textBaseline = "middle";
|
||||||
|
tokens.forEach((token) => {
|
||||||
|
const t = token.age / token.life;
|
||||||
|
const fade = Math.max(0, 1 - t);
|
||||||
|
token.x += token.vx;
|
||||||
|
token.y += token.vy;
|
||||||
|
token.vx *= 0.985;
|
||||||
|
token.vy = token.vy * 0.985 - 0.004;
|
||||||
|
token.age += 1;
|
||||||
|
|
||||||
|
ctx.globalAlpha = fade * 0.72;
|
||||||
|
ctx.fillStyle = colors.token;
|
||||||
|
ctx.font = `700 ${token.size}px Inter, ui-sans-serif, system-ui, sans-serif`;
|
||||||
|
ctx.fillText(token.text, token.x, token.y);
|
||||||
|
});
|
||||||
|
for (let index = tokens.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (tokens[index].age > tokens[index].life) tokens.splice(index, 1);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
@@ -746,19 +890,21 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
pointer.x += (pointer.tx - pointer.x) * 0.08;
|
pointer.x += (pointer.tx - pointer.x) * 0.08;
|
||||||
pointer.y += (pointer.ty - pointer.y) * 0.08;
|
pointer.y += (pointer.ty - pointer.y) * 0.08;
|
||||||
pointer.speed *= 0.92;
|
pointer.speed *= 0.92;
|
||||||
|
drawTrail(colors);
|
||||||
|
|
||||||
particles.forEach((particle, index) => {
|
particles.forEach((particle, index) => {
|
||||||
const dx = particle.x - pointer.x;
|
const dx = particle.x - pointer.x;
|
||||||
const dy = particle.y - pointer.y;
|
const dy = particle.y - pointer.y;
|
||||||
const dist = Math.hypot(dx, dy);
|
const dist = Math.hypot(dx, dy);
|
||||||
if (pointer.active && dist < 150) {
|
if (pointer.active && dist < 172) {
|
||||||
const force = (1 - dist / 150) * (0.55 + pointer.speed);
|
const force = (1 - dist / 172) * (0.62 + pointer.speed);
|
||||||
particle.vx += (dx / (dist || 1)) * force * 0.018;
|
particle.vx += (dx / (dist || 1)) * force * 0.018;
|
||||||
particle.vy += (dy / (dist || 1)) * force * 0.018;
|
particle.vy += (dy / (dist || 1)) * force * 0.018;
|
||||||
}
|
}
|
||||||
|
|
||||||
particle.vx *= 0.988;
|
particle.vx *= 0.988;
|
||||||
particle.vy *= 0.988;
|
particle.vy *= 0.988;
|
||||||
|
particle.pulse *= 0.92;
|
||||||
particle.x += particle.vx;
|
particle.x += particle.vx;
|
||||||
particle.y += particle.vy;
|
particle.y += particle.vy;
|
||||||
|
|
||||||
@@ -770,10 +916,10 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
for (let next = index + 1; next < particles.length; next += 1) {
|
for (let next = index + 1; next < particles.length; next += 1) {
|
||||||
const other = particles[next];
|
const other = particles[next];
|
||||||
const lineDistance = Math.hypot(particle.x - other.x, particle.y - other.y);
|
const lineDistance = Math.hypot(particle.x - other.x, particle.y - other.y);
|
||||||
if (lineDistance < 112) {
|
if (lineDistance < 118) {
|
||||||
ctx.globalAlpha = (1 - lineDistance / 112) * 0.46;
|
ctx.globalAlpha = (1 - lineDistance / 118) * (0.36 + particle.pulse * 0.25 + other.pulse * 0.25);
|
||||||
ctx.strokeStyle = colors.line;
|
ctx.strokeStyle = particle.pulse + other.pulse > 0.2 ? colors.lineActive : colors.line;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 0.9 + Math.max(particle.pulse, other.pulse) * 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(particle.x, particle.y);
|
ctx.moveTo(particle.x, particle.y);
|
||||||
ctx.lineTo(other.x, other.y);
|
ctx.lineTo(other.x, other.y);
|
||||||
@@ -781,19 +927,38 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.globalAlpha = 0.58;
|
if (pointer.active && dist < 198) {
|
||||||
ctx.fillStyle = particle.hue === 1 ? colors.dotAlt : colors.dot;
|
ctx.globalAlpha = (1 - dist / 198) * (0.2 + pointer.speed * 0.38);
|
||||||
|
ctx.strokeStyle = particle.hue === 2 ? colors.trailFast : colors.lineActive;
|
||||||
|
ctx.lineWidth = 0.8 + pointer.speed * 1.2;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(pointer.x, pointer.y);
|
||||||
|
ctx.lineTo(particle.x, particle.y);
|
||||||
|
ctx.stroke();
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.globalAlpha = 0.5 + particle.pulse * 0.28;
|
||||||
|
ctx.fillStyle = particle.pulse > 0.35 ? colors.dotHot : particle.hue === 1 ? colors.dotAlt : colors.dot;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(particle.x, particle.y, particle.size, 0, Math.PI * 2);
|
ctx.arc(particle.x, particle.y, particle.size + particle.pulse * 1.4, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
drawBursts(colors);
|
||||||
|
drawTokens(colors);
|
||||||
|
|
||||||
if (pointer.active) {
|
if (pointer.active) {
|
||||||
ctx.globalAlpha = 0.45;
|
ctx.globalAlpha = 0.4 + pointer.speed * 0.16;
|
||||||
ctx.strokeStyle = colors.cursor;
|
ctx.strokeStyle = colors.cursor;
|
||||||
ctx.lineWidth = 1;
|
ctx.lineWidth = 1 + pointer.speed * 0.8;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(pointer.x, pointer.y, 26 + pointer.speed * 18, 0, Math.PI * 2);
|
ctx.arc(pointer.x, pointer.y, 24 + pointer.speed * 24, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
|
||||||
|
ctx.globalAlpha = 0.26 + pointer.speed * 0.2;
|
||||||
|
ctx.strokeStyle = colors.trail;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(pointer.x, pointer.y, 7 + pointer.speed * 7, 0.3, Math.PI * 1.45);
|
||||||
ctx.stroke();
|
ctx.stroke();
|
||||||
}
|
}
|
||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
@@ -802,7 +967,7 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
|
|
||||||
window.addEventListener("resize", resize);
|
window.addEventListener("resize", resize);
|
||||||
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
window.addEventListener("pointermove", onPointerMove, { passive: true });
|
||||||
window.addEventListener("pointerdown", onPointerMove, { passive: true });
|
window.addEventListener("pointerdown", onPointerDown, { passive: true });
|
||||||
document.addEventListener("pointerleave", () => {
|
document.addEventListener("pointerleave", () => {
|
||||||
pointer.active = false;
|
pointer.active = false;
|
||||||
});
|
});
|
||||||
@@ -820,11 +985,14 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
}
|
}
|
||||||
|
|
||||||
const pointer = { active: false, x: 0, y: 0 };
|
const pointer = { active: false, x: 0, y: 0 };
|
||||||
|
const pulses = [];
|
||||||
const travelers = Array.from({ length: 9 }, (_, index) => ({
|
const travelers = Array.from({ length: 9 }, (_, index) => ({
|
||||||
edge: index % 4,
|
edge: index % 4,
|
||||||
progress: Math.random(),
|
progress: Math.random(),
|
||||||
speed: 0.002 + Math.random() * 0.0025,
|
speed: 0.002 + Math.random() * 0.0025,
|
||||||
}));
|
}));
|
||||||
|
let routeFocus = -1;
|
||||||
|
let routePulse = 0;
|
||||||
let width = 0;
|
let width = 0;
|
||||||
let height = 0;
|
let height = 0;
|
||||||
let dpr = 1;
|
let dpr = 1;
|
||||||
@@ -835,12 +1003,16 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
? {
|
? {
|
||||||
line: "rgba(244, 239, 230, 0.22)",
|
line: "rgba(244, 239, 230, 0.22)",
|
||||||
active: "rgba(139, 180, 255, 0.55)",
|
active: "rgba(139, 180, 255, 0.55)",
|
||||||
|
route: "rgba(240, 179, 90, 0.66)",
|
||||||
pulse: "rgba(99, 215, 155, 0.86)",
|
pulse: "rgba(99, 215, 155, 0.86)",
|
||||||
|
glow: "rgba(139, 180, 255, 0.16)",
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
line: "rgba(23, 23, 23, 0.18)",
|
line: "rgba(23, 23, 23, 0.18)",
|
||||||
active: "rgba(47, 95, 190, 0.52)",
|
active: "rgba(47, 95, 190, 0.52)",
|
||||||
|
route: "rgba(167, 101, 22, 0.58)",
|
||||||
pulse: "rgba(22, 133, 94, 0.78)",
|
pulse: "rgba(22, 133, 94, 0.78)",
|
||||||
|
glow: "rgba(47, 95, 190, 0.14)",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -859,49 +1031,122 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
return [...map.querySelectorAll(".map-node")].map((node) => {
|
return [...map.querySelectorAll(".map-node")].map((node) => {
|
||||||
const rect = node.getBoundingClientRect();
|
const rect = node.getBoundingClientRect();
|
||||||
return {
|
return {
|
||||||
|
element: node,
|
||||||
main: node.classList.contains("map-node-main"),
|
main: node.classList.contains("map-node-main"),
|
||||||
|
route: node.dataset.mapNode === undefined ? -1 : Number(node.dataset.mapNode),
|
||||||
x: rect.left - mapRect.left + rect.width / 2,
|
x: rect.left - mapRect.left + rect.width / 2,
|
||||||
y: rect.top - mapRect.top + rect.height / 2,
|
y: rect.top - mapRect.top + rect.height / 2,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function pointOnEdge(start, end, progress) {
|
function curveControl(start, end) {
|
||||||
|
const horizontal = end.x < start.x ? -1 : 1;
|
||||||
|
const vertical = end.y < start.y ? -1 : 1;
|
||||||
return {
|
return {
|
||||||
x: start.x + (end.x - start.x) * progress,
|
x: (start.x + end.x) / 2 + horizontal * 18,
|
||||||
y: start.y + (end.y - start.y) * progress,
|
y: (start.y + end.y) / 2 + vertical * 12,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function pointOnEdge(start, end, progress) {
|
||||||
|
const control = curveControl(start, end);
|
||||||
|
const inverse = 1 - progress;
|
||||||
|
return {
|
||||||
|
x: inverse * inverse * start.x + 2 * inverse * progress * control.x + progress * progress * end.x,
|
||||||
|
y: inverse * inverse * start.y + 2 * inverse * progress * control.y + progress * progress * end.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function strokeRoute(start, end, color, widthValue, alpha) {
|
||||||
|
const control = curveControl(start, end);
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
|
ctx.strokeStyle = color;
|
||||||
|
ctx.lineWidth = widthValue;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.moveTo(start.x, start.y);
|
||||||
|
ctx.quadraticCurveTo(control.x, control.y, end.x, end.y);
|
||||||
|
ctx.stroke();
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
function setRouteFocus(route, node) {
|
||||||
|
routeFocus = route;
|
||||||
|
routePulse = 1;
|
||||||
|
map.querySelectorAll(".map-node.is-active-route").forEach((item) => item.classList.remove("is-active-route"));
|
||||||
|
map.querySelector(".map-node-main")?.classList.add("is-active-route");
|
||||||
|
node?.classList.add("is-active-route");
|
||||||
|
window.setTimeout(() => {
|
||||||
|
if (routeFocus === route) {
|
||||||
|
map.querySelectorAll(".map-node.is-active-route").forEach((item) => item.classList.remove("is-active-route"));
|
||||||
|
}
|
||||||
|
}, 760);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusNearestRoute() {
|
||||||
|
const nodes = centers();
|
||||||
|
const targets = nodes.filter((node) => !node.main);
|
||||||
|
let nearest = null;
|
||||||
|
let nearestDistance = Infinity;
|
||||||
|
targets.forEach((target) => {
|
||||||
|
const distance = Math.hypot(pointer.x - target.x, pointer.y - target.y);
|
||||||
|
if (distance < nearestDistance) {
|
||||||
|
nearest = target;
|
||||||
|
nearestDistance = distance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (nearest && nearestDistance < 180) setRouteFocus(nearest.route, nearest.element);
|
||||||
|
}
|
||||||
|
|
||||||
function draw() {
|
function draw() {
|
||||||
const colors = palette();
|
const colors = palette();
|
||||||
const nodes = centers();
|
const nodes = centers();
|
||||||
const main = nodes.find((node) => node.main) || nodes[0];
|
const main = nodes.find((node) => node.main) || nodes[0];
|
||||||
const targets = nodes.filter((node) => node !== main);
|
const targets = nodes.filter((node) => node !== main);
|
||||||
|
routePulse *= 0.94;
|
||||||
ctx.clearRect(0, 0, width, height);
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
ctx.lineCap = "round";
|
||||||
|
ctx.lineJoin = "round";
|
||||||
|
|
||||||
targets.forEach((target) => {
|
targets.forEach((target) => {
|
||||||
const distanceToPointer = pointer.active ? Math.hypot(pointer.x - target.x, pointer.y - target.y) : 999;
|
const distanceToPointer = pointer.active ? Math.hypot(pointer.x - target.x, pointer.y - target.y) : 999;
|
||||||
ctx.strokeStyle = distanceToPointer < 135 ? colors.active : colors.line;
|
const hoverIntensity = distanceToPointer < 135 ? 1 - distanceToPointer / 135 : 0;
|
||||||
ctx.lineWidth = distanceToPointer < 135 ? 1.6 : 1;
|
const focusIntensity = routeFocus === target.route ? routePulse : 0;
|
||||||
ctx.beginPath();
|
const intensity = Math.max(hoverIntensity, focusIntensity);
|
||||||
ctx.moveTo(main.x, main.y);
|
if (intensity > 0.08) {
|
||||||
ctx.lineTo(target.x, target.y);
|
strokeRoute(main, target, colors.glow, 6 + intensity * 5, intensity * 0.72);
|
||||||
ctx.stroke();
|
}
|
||||||
|
strokeRoute(
|
||||||
|
main,
|
||||||
|
target,
|
||||||
|
focusIntensity > hoverIntensity ? colors.route : intensity > 0 ? colors.active : colors.line,
|
||||||
|
1 + intensity * 1.4,
|
||||||
|
0.82 + intensity * 0.18,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
travelers.forEach((traveler) => {
|
travelers.forEach((traveler) => {
|
||||||
if (!targets.length) return;
|
if (!targets.length) return;
|
||||||
const target = targets[traveler.edge % targets.length];
|
const target = targets[traveler.edge % targets.length];
|
||||||
traveler.progress += traveler.speed;
|
const focusBoost = routeFocus === target.route ? 1 + routePulse * 3.2 : 1;
|
||||||
|
traveler.progress += traveler.speed * focusBoost;
|
||||||
if (traveler.progress > 1) {
|
if (traveler.progress > 1) {
|
||||||
traveler.progress = 0;
|
traveler.progress = 0;
|
||||||
traveler.edge = (traveler.edge + 1) % targets.length;
|
traveler.edge = (traveler.edge + 1) % targets.length;
|
||||||
}
|
}
|
||||||
const point = pointOnEdge(main, target, traveler.progress);
|
const point = pointOnEdge(main, target, traveler.progress);
|
||||||
|
const isFocused = routeFocus === target.route && routePulse > 0.08;
|
||||||
|
if (isFocused) {
|
||||||
|
ctx.globalAlpha = routePulse * 0.26;
|
||||||
|
ctx.fillStyle = colors.route;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(point.x, point.y, 7.5, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
ctx.fillStyle = colors.pulse;
|
ctx.fillStyle = colors.pulse;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.arc(point.x, point.y, 2.2, 0, Math.PI * 2);
|
ctx.arc(point.x, point.y, isFocused ? 3 : 2.2, 0, Math.PI * 2);
|
||||||
ctx.fill();
|
ctx.fill();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -909,8 +1154,10 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
targets.forEach((target) => {
|
targets.forEach((target) => {
|
||||||
const distance = Math.hypot(pointer.x - target.x, pointer.y - target.y);
|
const distance = Math.hypot(pointer.x - target.x, pointer.y - target.y);
|
||||||
if (distance < 170) {
|
if (distance < 170) {
|
||||||
ctx.globalAlpha = 1 - distance / 170;
|
const alpha = 1 - distance / 170;
|
||||||
|
ctx.globalAlpha = alpha;
|
||||||
ctx.strokeStyle = colors.active;
|
ctx.strokeStyle = colors.active;
|
||||||
|
ctx.lineWidth = 1 + alpha;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(pointer.x, pointer.y);
|
ctx.moveTo(pointer.x, pointer.y);
|
||||||
ctx.lineTo(target.x, target.y);
|
ctx.lineTo(target.x, target.y);
|
||||||
@@ -920,6 +1167,22 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
ctx.globalAlpha = 1;
|
ctx.globalAlpha = 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pulses.forEach((pulse) => {
|
||||||
|
const t = pulse.age / pulse.life;
|
||||||
|
const fade = Math.max(0, 1 - t);
|
||||||
|
ctx.globalAlpha = fade * 0.58;
|
||||||
|
ctx.strokeStyle = colors.route;
|
||||||
|
ctx.lineWidth = 1.4;
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(pulse.x, pulse.y, 10 + t * 42, 0, Math.PI * 2);
|
||||||
|
ctx.stroke();
|
||||||
|
pulse.age += 1;
|
||||||
|
});
|
||||||
|
for (let index = pulses.length - 1; index >= 0; index -= 1) {
|
||||||
|
if (pulses[index].age > pulses[index].life) pulses.splice(index, 1);
|
||||||
|
}
|
||||||
|
ctx.globalAlpha = 1;
|
||||||
|
|
||||||
requestAnimationFrame(draw);
|
requestAnimationFrame(draw);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -932,6 +1195,21 @@ const projectLogoFor = (id: string) => brandAssets.projects[id] ?? null;
|
|||||||
map.addEventListener("pointerleave", () => {
|
map.addEventListener("pointerleave", () => {
|
||||||
pointer.active = false;
|
pointer.active = false;
|
||||||
});
|
});
|
||||||
|
map.addEventListener("pointerdown", (event) => {
|
||||||
|
const rect = map.getBoundingClientRect();
|
||||||
|
pointer.x = event.clientX - rect.left;
|
||||||
|
pointer.y = event.clientY - rect.top;
|
||||||
|
pointer.active = true;
|
||||||
|
pulses.push({ x: pointer.x, y: pointer.y, age: 0, life: 34 });
|
||||||
|
if (pulses.length > 6) pulses.shift();
|
||||||
|
focusNearestRoute();
|
||||||
|
});
|
||||||
|
map.querySelectorAll(".map-node").forEach((node) => {
|
||||||
|
node.addEventListener("pointerenter", () => {
|
||||||
|
if (node.dataset.mapNode === undefined) return;
|
||||||
|
setRouteFocus(Number(node.dataset.mapNode), node);
|
||||||
|
});
|
||||||
|
});
|
||||||
window.addEventListener("resize", resize);
|
window.addEventListener("resize", resize);
|
||||||
if ("ResizeObserver" in window) new ResizeObserver(resize).observe(map);
|
if ("ResizeObserver" in window) new ResizeObserver(resize).observe(map);
|
||||||
resize();
|
resize();
|
||||||
|
|||||||
+126
-10
@@ -189,6 +189,7 @@ a {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.82rem;
|
font-size: 0.82rem;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
|
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbar-actions {
|
.topbar-actions {
|
||||||
@@ -213,6 +214,7 @@ a {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.github-link {
|
.github-link {
|
||||||
@@ -234,6 +236,7 @@ a {
|
|||||||
.github-link:hover,
|
.github-link:hover,
|
||||||
.control-button:hover,
|
.control-button:hover,
|
||||||
.topbar-cta:hover {
|
.topbar-cta:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
border-color: var(--ink);
|
border-color: var(--ink);
|
||||||
background: var(--button-hover);
|
background: var(--button-hover);
|
||||||
}
|
}
|
||||||
@@ -309,7 +312,7 @@ main {
|
|||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
font-weight: 750;
|
font-weight: 750;
|
||||||
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease;
|
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button:hover {
|
.button:hover {
|
||||||
@@ -468,6 +471,21 @@ main {
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
box-shadow: 0 10px 24px rgba(23, 23, 23, 0.06);
|
box-shadow: 0 10px 24px rgba(23, 23, 23, 0.06);
|
||||||
backdrop-filter: blur(8px);
|
backdrop-filter: blur(8px);
|
||||||
|
transition: border-color 180ms ease, box-shadow 180ms ease, filter 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node.is-active-route {
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 58%, var(--ink));
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--blue) 24%, transparent),
|
||||||
|
0 14px 34px color-mix(in srgb, var(--blue) 18%, transparent);
|
||||||
|
filter: saturate(1.16);
|
||||||
|
}
|
||||||
|
|
||||||
|
.map-node-main.is-active-route {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px color-mix(in srgb, var(--amber) 38%, transparent),
|
||||||
|
0 18px 42px color-mix(in srgb, var(--amber) 20%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
.map-node-main {
|
.map-node-main {
|
||||||
@@ -661,6 +679,13 @@ main {
|
|||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--surface);
|
background: var(--surface);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-grid article:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: color-mix(in srgb, var(--green) 42%, var(--line-strong));
|
||||||
|
box-shadow: 0 16px 34px rgba(23, 23, 23, 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
.strength-grid span {
|
.strength-grid span {
|
||||||
@@ -713,6 +738,14 @@ main {
|
|||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
|
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease, color 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.skill-list span:hover,
|
||||||
|
.tech-line span:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 42%, var(--line));
|
||||||
|
color: var(--ink);
|
||||||
}
|
}
|
||||||
|
|
||||||
.with-action {
|
.with-action {
|
||||||
@@ -735,10 +768,12 @@ main {
|
|||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 0.76rem;
|
font-size: 0.76rem;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
|
transition: transform 180ms ease, background 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filter-tabs button:hover,
|
.filter-tabs button:hover,
|
||||||
.filter-tabs button.active {
|
.filter-tabs button.active {
|
||||||
|
transform: translateY(-1px);
|
||||||
border-color: var(--ink);
|
border-color: var(--ink);
|
||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
color: var(--paper);
|
color: var(--paper);
|
||||||
@@ -758,6 +793,13 @@ main {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
box-shadow: 0 16px 36px rgba(23, 23, 23, 0.07);
|
box-shadow: 0 16px 36px rgba(23, 23, 23, 0.07);
|
||||||
backdrop-filter: blur(12px);
|
backdrop-filter: blur(12px);
|
||||||
|
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project-row:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: color-mix(in srgb, var(--blue) 42%, var(--line-strong));
|
||||||
|
box-shadow: 0 20px 42px rgba(23, 23, 23, 0.09);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project-row[hidden] {
|
.project-row[hidden] {
|
||||||
@@ -893,6 +935,13 @@ main {
|
|||||||
linear-gradient(135deg, rgba(31, 143, 95, 0.14), transparent 48%),
|
linear-gradient(135deg, rgba(31, 143, 95, 0.14), transparent 48%),
|
||||||
var(--surface-strong);
|
var(--surface-strong);
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
|
transition: transform 180ms ease, border-color 180ms ease, box-shadow 180ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.domain-strip article:hover {
|
||||||
|
transform: translateY(-2px);
|
||||||
|
border-color: color-mix(in srgb, var(--amber) 46%, var(--line-strong));
|
||||||
|
box-shadow: 0 16px 34px rgba(23, 23, 23, 0.07);
|
||||||
}
|
}
|
||||||
|
|
||||||
.domain-strip .brand-logo {
|
.domain-strip .brand-logo {
|
||||||
@@ -908,11 +957,11 @@ main {
|
|||||||
|
|
||||||
.contact-section {
|
.contact-section {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) auto;
|
grid-template-columns: minmax(0, 0.92fr) minmax(280px, 0.68fr);
|
||||||
gap: 1.2rem;
|
gap: clamp(1.2rem, 4vw, 2.2rem);
|
||||||
align-items: center;
|
align-items: stretch;
|
||||||
margin-top: 3.2rem;
|
margin-top: 3.2rem;
|
||||||
padding: clamp(1.1rem, 3.4vw, 1.6rem);
|
padding: clamp(1.2rem, 3.6vw, 2rem);
|
||||||
border: 1px solid rgba(23, 23, 23, 0.82);
|
border: 1px solid rgba(23, 23, 23, 0.82);
|
||||||
border-radius: var(--radius);
|
border-radius: var(--radius);
|
||||||
background: var(--ink);
|
background: var(--ink);
|
||||||
@@ -920,17 +969,46 @@ main {
|
|||||||
box-shadow: var(--shadow);
|
box-shadow: var(--shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-section > div:first-child {
|
||||||
|
display: grid;
|
||||||
|
align-content: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.contact-section h2 {
|
||||||
|
max-width: 440px;
|
||||||
|
font-size: clamp(1.65rem, 3.2vw, 2.55rem);
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
.contact-section span {
|
.contact-section span {
|
||||||
display: block;
|
display: block;
|
||||||
margin-top: 0.62rem;
|
margin-top: 0.62rem;
|
||||||
color: rgba(244, 241, 234, 0.64);
|
color: color-mix(in srgb, currentColor 62%, transparent);
|
||||||
font-size: 0.88rem;
|
font-size: 0.88rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-actions {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.6rem;
|
||||||
|
align-content: center;
|
||||||
|
min-width: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
padding: 0.78rem;
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, currentColor 5%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.contact-section .button {
|
.contact-section .button {
|
||||||
border-color: rgba(244, 241, 234, 0.3);
|
width: 100%;
|
||||||
background: transparent;
|
min-height: 40px;
|
||||||
color: var(--paper);
|
justify-content: flex-start;
|
||||||
|
padding-inline: 0.82rem;
|
||||||
|
border-color: color-mix(in srgb, currentColor 16%, transparent);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
background: color-mix(in srgb, currentColor 4%, transparent);
|
||||||
|
color: currentColor;
|
||||||
|
font-size: 0.82rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact-section .button-primary {
|
.contact-section .button-primary {
|
||||||
@@ -957,6 +1035,29 @@ main {
|
|||||||
transform: translateY(0);
|
transform: translateY(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-pressed {
|
||||||
|
animation: press-feedback 420ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes press-feedback {
|
||||||
|
0% {
|
||||||
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--amber) 36%, transparent);
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
44% {
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 5px color-mix(in srgb, var(--amber) 20%, transparent),
|
||||||
|
0 0 22px color-mix(in srgb, var(--blue) 18%, transparent);
|
||||||
|
filter: saturate(1.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
100% {
|
||||||
|
box-shadow: 0 0 0 0 color-mix(in srgb, var(--amber) 0%, transparent);
|
||||||
|
filter: saturate(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
@media (max-width: 1080px) {
|
||||||
.topbar {
|
.topbar {
|
||||||
grid-template-columns: auto 1fr auto;
|
grid-template-columns: auto 1fr auto;
|
||||||
@@ -1065,6 +1166,7 @@ main {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.contact-actions {
|
.contact-actions {
|
||||||
|
width: 100%;
|
||||||
margin-top: 0;
|
margin-top: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1106,10 +1208,24 @@ main {
|
|||||||
|
|
||||||
.reveal,
|
.reveal,
|
||||||
.button,
|
.button,
|
||||||
.nav a {
|
.nav a,
|
||||||
|
.topbar-cta,
|
||||||
|
.github-link,
|
||||||
|
.control-button,
|
||||||
|
.filter-tabs button,
|
||||||
|
.project-row,
|
||||||
|
.strength-grid article,
|
||||||
|
.domain-strip article,
|
||||||
|
.skill-list span,
|
||||||
|
.tech-line span,
|
||||||
|
.map-node {
|
||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-pressed {
|
||||||
|
animation: none;
|
||||||
|
}
|
||||||
|
|
||||||
.reveal {
|
.reveal {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transform: none;
|
transform: none;
|
||||||
|
|||||||
Reference in New Issue
Block a user