feat: Update About page content with new media, refactor BentoGrid layout, and optimize particle background performance and styling.

This commit is contained in:
2026-02-03 01:29:11 -06:00
parent c2f3b57837
commit 2f8b4e690e
25 changed files with 128 additions and 49 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 168 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 394 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 MiB

After

Width:  |  Height:  |  Size: 326 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 604 KiB

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 806 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 406 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 427 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 395 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 520 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 MiB

After

Width:  |  Height:  |  Size: 825 KiB

View File

@@ -8,39 +8,42 @@ interface BentoItem {
description: string; description: string;
className?: string; // For sizing: bento-large, bento-wide, bento-tall className?: string; // For sizing: bento-large, bento-wide, bento-tall
bgImage: string | string[]; bgImage: string | string[];
videoSrc?: string; // Optional video source
} }
const items: BentoItem[] = [ const items: BentoItem[] = [
{ {
id: 'gaming', id: 'gaming',
title: "Gaming & Tech", title: "Gaming",
description: "Wind down with some PC gaming or just appreciating good new gear.", description: "From Old Classic Fighting Game Tournaments to New PC Gaming Experiences. I enjoy it all",
className: "bento-large", className: "bento-large",
bgImage: "https://placehold.co/800x800/2a1a4a/FFF?text=Gaming+Setup" bgImage: "/images/about/slab.jpg"
}, },
{ {
id: 'snowboarding', id: 'snowboarding',
title: "Snowboarding", title: "Snowboarding",
description: "The rush of going down the mountains are unmatched", description: "Here is me teaching my girlfriend😅",
className: "bento-tall", className: "bento-tall",
bgImage: "https://placehold.co/400x800/a7ffeb/004d40?text=Snowboarding" bgImage: "https://placehold.co/400x800/a7ffeb/004d40?text=Snowboarding",
videoSrc: "/images/about/snowboard2.mp4"
}, },
{ {
id: 'cooking', id: 'cooking',
title: "Culinary Arts", title: "Cooking",
description: "Experimenting with new recipes and flavors in the kitchen.", description: "Everyone cooks sure, I'll give you that. But I want to master the art of cooking a excellent steak.",
className: "", // Standard 1x1 className: "",
bgImage: "https://placehold.co/400x400/3e2723/d7ccc8?text=Cooking" bgImage: "/images/about/steak.jpg"
}, },
{ {
id: 'social', id: 'social',
title: "Family & Friends", title: "Family & Friends",
description: "Good times with my favorite people. Including my Girlfriend, Friends and Family", description: "Good times with my favorite people. Including my Girlfriend, Friends and Family",
className: "", // Standard 1x1 className: "bento-tall",
bgImage: [ bgImage: [
"https://placehold.co/400x400/f8bbd0/880e4f?text=Social+1", "/images/about/girl1.jpg",
"https://placehold.co/400x400/d81b60/ffffff?text=Social+2", "/images/about/family1.jpg",
"https://placehold.co/400x400/880e4f/f8bbd0?text=Social+3" "/images/about/family2.jpg",
"/images/about/friend2.jpg",
] ]
}, },
{ {
@@ -49,6 +52,59 @@ const items: BentoItem[] = [
description: "I enjoy the challenge of self-hosting my own digital services at home. I also love tinkering with new hardware and software setups.", description: "I enjoy the challenge of self-hosting my own digital services at home. I also love tinkering with new hardware and software setups.",
className: "bento-wide", className: "bento-wide",
bgImage: "/images/projects/homelabber.png" bgImage: "/images/projects/homelabber.png"
},
{
id: 'controller-modding',
title: "Controller Modding",
description: "Restoring and modifying retro game controllers. Giving new life to old hardware.",
className: "", // Standard 1x1
bgImage: "/images/about/gamecube.jpg"
},
{
id: 'outdoor-exploring',
title: "Outdoor Exploring",
description: "Hiking mountains, exploring caves, beaches and more.",
className: "bento-tall",
bgImage: [
"/images/about/outdoor1.jpg",
"/images/about/outdoor2.jpg",
"/images/about/beach1.jpg",
]
},
{
id: 'smart-home',
title: "Smart Home",
description: "Automating my living space with smart devices and custom integrations.",
className: "", // Standard 1x1
bgImage: "https://placehold.co/400x400/0288d1/e1f5fe?text=Smart+Home"
},
{
id: 'pc-building',
title: "Computer Building",
description: "Designing and assembling custom PC builds for performance and aesthetics.",
className: "bento-wide",
bgImage: "/images/about/pcBuild.jpg"
},
{
id: 'small-town',
title: "Small Town Origin",
description: "I'm from Melfort, Saskatchewan. A small town with a big heart.",
className: "", // Standard 1x1
bgImage: [
"/images/about/melfort1.jpg",
"/images/about/melfort2.jpg",
"/images/about/melfort3.png",
"/images/about/melfort4.jpg",
"/images/about/melfort5.jpg",
]
},
{
id: 'cats',
title: "Cats",
description: "I see a cat, I pet a cat. Simple as that.",
className: "bento-large",
bgImage: "https://placehold.co/800x800/ffccbc/bf360c?text=Cats", // Fallback
videoSrc: "/images/about/kitty.mp4"
} }
]; ];
@@ -71,21 +127,33 @@ const BentoCard = ({ item, index, delay }: { item: BentoItem; index: number; del
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.5, delay: delay + index * 0.1 }} transition={{ duration: 0.5, delay: delay + index * 0.1 }}
> >
<AnimatePresence mode="popLayout" initial={false}> {item.videoSrc ? (
<motion.img <video
key={currentIndex} src={item.videoSrc}
src={images[currentIndex]} autoPlay
alt={item.title} loop
muted
playsInline
className="bento-bg" className="bento-bg"
initial={{ opacity: 0 }} style={{ objectFit: 'cover', opacity: 0.6 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8 }}
onError={(e) => {
e.currentTarget.src = "https://placehold.co/600x400?text=" + item.title;
}}
/> />
</AnimatePresence> ) : (
<AnimatePresence mode="popLayout" initial={false}>
<motion.img
key={currentIndex}
src={images[currentIndex]}
alt={item.title}
className="bento-bg"
initial={{ opacity: 0 }}
animate={{ opacity: 0.6 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.8 }}
onError={(e) => {
e.currentTarget.src = "https://placehold.co/600x400?text=" + item.title;
}}
/>
</AnimatePresence>
)}
<div className="bento-overlay" /> <div className="bento-overlay" />
<div className="bento-content"> <div className="bento-content">
<h3 className="bento-title">{item.title}</h3> <h3 className="bento-title">{item.title}</h3>

View File

@@ -79,6 +79,7 @@ const ParticlesBackground: React.FC = () => {
// Mobile (390x844) -> ~48 particles // Mobile (390x844) -> ~48 particles
// Mobile Landscape (844x390) -> ~48 particles (Same as portrait!) // Mobile Landscape (844x390) -> ~48 particles (Same as portrait!)
const area = canvas.width * canvas.height; const area = canvas.width * canvas.height;
// Reduce density further to specificially help the heavy About page
const particleCount = Math.floor(Math.sqrt(area) / 12); const particleCount = Math.floor(Math.sqrt(area) / 12);
particles = []; particles = [];
@@ -90,17 +91,7 @@ const ParticlesBackground: React.FC = () => {
const animate = () => { const animate = () => {
if (!ctx || !canvas) return; if (!ctx || !canvas) return;
ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw a subtle gradient background for the canvas itself if needed, or keep transparent // Gradient is now handled by CSS to avoid repainting every frame
// For now, let's keep it transparent so we can style the container behind it if we want,
// OR we can make this the definitive background.
// Let's add a deep gradient here to match the user's previous aesthetic but cooler.
const gradient = ctx.createLinearGradient(0, 0, canvas.width, canvas.height);
gradient.addColorStop(0, '#0f0c29');
gradient.addColorStop(0.5, '#302b63');
gradient.addColorStop(1, '#24243e');
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, canvas.width, canvas.height);
particles.forEach((particle) => { particles.forEach((particle) => {
particle.update(); particle.update();
@@ -119,18 +110,38 @@ const ParticlesBackground: React.FC = () => {
for (let b = a; b < particles.length; b++) { for (let b = a; b < particles.length; b++) {
const dx = particles[a].x - particles[b].x; const dx = particles[a].x - particles[b].x;
const dy = particles[a].y - particles[b].y; const dy = particles[a].y - particles[b].y;
const distance = Math.sqrt(dx * dx + dy * dy);
// Optimization 1: Strictly check max possible visible distance
// The opacity formula becomes <= 0 when distance > (2/3 * distanceThreshold).
// Max distanceThreshold is 300, so max visible distance is 200.
if (Math.abs(dx) > 200 || Math.abs(dy) > 200) continue;
const distSq = dx * dx + dy * dy;
const sizeAverage = (particles[a].size + particles[b].size) / 2; const sizeAverage = (particles[a].size + particles[b].size) / 2;
// Scale from 100 to 300 based on size average (2 to 6) // Scale from 100 to 300 based on size average (2 to 6)
const distanceThreshold = 100 + (sizeAverage - 2) * 50; const distanceThreshold = 100 + (sizeAverage - 2) * 50;
if (distance < distanceThreshold) { // Optimization 2: Only calculate if we are within the VISIBLE threshold (2/3 of logical threshold)
ctx.strokeStyle = `rgba(255, 255, 255, ${(distanceThreshold * (2 / 3) * .001) - distance / 1000})`; // Opacity = (Threshold * 2/3 * 0.001) - (dist / 1000)
ctx.lineWidth = sizeAverage; // 0 = (2/3 T)/1000 - d/1000 => d = 2/3 T
ctx.beginPath(); const distinctThreshold = distanceThreshold * (2 / 3);
ctx.moveTo(particles[a].x, particles[a].y); const distinctThresholdSq = distinctThreshold * distinctThreshold;
ctx.lineTo(particles[b].x, particles[b].y);
ctx.stroke(); if (distSq < distinctThresholdSq) {
const distance = Math.sqrt(distSq);
// Calculate opacity
const opacity = (distanceThreshold * (2 / 3) * .001) - distance / 1000;
if (opacity > 0) {
ctx.strokeStyle = `rgba(255, 255, 255, ${opacity})`;
ctx.lineWidth = sizeAverage;
ctx.beginPath();
ctx.moveTo(particles[a].x, particles[a].y);
ctx.lineTo(particles[b].x, particles[b].y);
ctx.stroke();
}
} }
} }
} }
@@ -166,6 +177,7 @@ const ParticlesBackground: React.FC = () => {
width: '100%', width: '100%',
height: '120vh', // Extend well below viewport to cover mobile browser bar retraction height: '120vh', // Extend well below viewport to cover mobile browser bar retraction
zIndex: -1, // Behind everything zIndex: -1, // Behind everything
background: 'linear-gradient(to bottom, #0f0c29 0%, #302b63 50%, #24243e 100%)'
}} }}
/> />
); );

View File

@@ -34,7 +34,7 @@ export default function About() {
transition={{ duration: 0.8 }} transition={{ duration: 0.8 }}
> >
<div style={{ width: "100%", maxWidth: "1200px" }}> <div style={{ width: "100%", maxWidth: "1200px" }}>
<motion.h1 {/* <motion.h1
className="about-title" className="about-title"
initial={{ y: -20, opacity: 0 }} initial={{ y: -20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }} animate={{ y: 0, opacity: 1 }}
@@ -42,7 +42,7 @@ export default function About() {
style={{ textAlign: "center" }} style={{ textAlign: "center" }}
> >
About Me About Me
</motion.h1> </motion.h1> */}
<BioSection <BioSection
imageSrc="/images/about/dapperSasha.jpg" imageSrc="/images/about/dapperSasha.jpg"
@@ -57,7 +57,6 @@ export default function About() {
animate={{ opacity: 1, y: 0 }} animate={{ opacity: 1, y: 0 }}
transition={{ duration: 0.6, delay: 0.8 }} transition={{ duration: 0.6, delay: 0.8 }}
> >
<h2 style={{ textAlign: "center", fontSize: "1.5rem", marginBottom: "30px", color: "rgba(255,255,255,0.9)" }}>Personal Interests</h2>
<BentoGrid delay={1.0} /> <BentoGrid delay={1.0} />
</motion.div> </motion.div>
<motion.div <motion.div

View File

@@ -1,7 +1,7 @@
.bento-grid-container { .bento-grid-container {
display: grid; display: grid;
grid-template-columns: repeat(4, 1fr); grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(2, 250px); grid-auto-rows: 250px;
gap: 20px; gap: 20px;
width: 100%; width: 100%;
max-width: 1200px; max-width: 1200px;