feat: Update About page content with new media, refactor BentoGrid layout, and optimize particle background performance and styling.
BIN
public/images/about/aurora1.jpg
Normal file
|
After Width: | Height: | Size: 168 KiB |
BIN
public/images/about/beach1.jpg
Normal file
|
After Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 394 KiB |
BIN
public/images/about/family1.jpg
Normal file
|
After Width: | Height: | Size: 629 KiB |
BIN
public/images/about/family2.jpg
Normal file
|
After Width: | Height: | Size: 358 KiB |
BIN
public/images/about/friend2.jpg
Normal file
|
After Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 1.7 MiB After Width: | Height: | Size: 326 KiB |
BIN
public/images/about/girl1.jpg
Normal file
|
After Width: | Height: | Size: 604 KiB |
BIN
public/images/about/kitty.mp4
Normal file
BIN
public/images/about/melfort1.jpg
Normal file
|
After Width: | Height: | Size: 212 KiB |
BIN
public/images/about/melfort2.jpg
Normal file
|
After Width: | Height: | Size: 806 KiB |
BIN
public/images/about/melfort3.png
Normal file
|
After Width: | Height: | Size: 1.4 MiB |
BIN
public/images/about/melfort4.jpg
Normal file
|
After Width: | Height: | Size: 349 KiB |
BIN
public/images/about/melfort5.jpg
Normal file
|
After Width: | Height: | Size: 470 KiB |
BIN
public/images/about/outdoor1.jpg
Normal file
|
After Width: | Height: | Size: 406 KiB |
BIN
public/images/about/outdoor2.jpg
Normal file
|
After Width: | Height: | Size: 427 KiB |
BIN
public/images/about/pcBuild.jpg
Normal file
|
After Width: | Height: | Size: 395 KiB |
BIN
public/images/about/slab.jpg
Normal file
|
After Width: | Height: | Size: 520 KiB |
BIN
public/images/about/snowboard1.mp4
Normal file
BIN
public/images/about/snowboard2.mp4
Normal file
|
Before Width: | Height: | Size: 2.2 MiB After Width: | Height: | Size: 825 KiB |
@@ -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,6 +127,17 @@ 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 }}
|
||||||
>
|
>
|
||||||
|
{item.videoSrc ? (
|
||||||
|
<video
|
||||||
|
src={item.videoSrc}
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
className="bento-bg"
|
||||||
|
style={{ objectFit: 'cover', opacity: 0.6 }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
<motion.img
|
<motion.img
|
||||||
key={currentIndex}
|
key={currentIndex}
|
||||||
@@ -86,6 +153,7 @@ const BentoCard = ({ item, index, delay }: { item: BentoItem; index: number; del
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</AnimatePresence>
|
</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>
|
||||||
|
|||||||
@@ -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,13 +110,32 @@ 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)
|
||||||
|
// 0 = (2/3 T)/1000 - d/1000 => d = 2/3 T
|
||||||
|
const distinctThreshold = distanceThreshold * (2 / 3);
|
||||||
|
const distinctThresholdSq = distinctThreshold * distinctThreshold;
|
||||||
|
|
||||||
|
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.lineWidth = sizeAverage;
|
||||||
ctx.beginPath();
|
ctx.beginPath();
|
||||||
ctx.moveTo(particles[a].x, particles[a].y);
|
ctx.moveTo(particles[a].x, particles[a].y);
|
||||||
@@ -135,6 +145,7 @@ const ParticlesBackground: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
init();
|
init();
|
||||||
animate();
|
animate();
|
||||||
@@ -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%)'
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||