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;
|
||||
className?: string; // For sizing: bento-large, bento-wide, bento-tall
|
||||
bgImage: string | string[];
|
||||
videoSrc?: string; // Optional video source
|
||||
}
|
||||
|
||||
const items: BentoItem[] = [
|
||||
{
|
||||
id: 'gaming',
|
||||
title: "Gaming & Tech",
|
||||
description: "Wind down with some PC gaming or just appreciating good new gear.",
|
||||
title: "Gaming",
|
||||
description: "From Old Classic Fighting Game Tournaments to New PC Gaming Experiences. I enjoy it all",
|
||||
className: "bento-large",
|
||||
bgImage: "https://placehold.co/800x800/2a1a4a/FFF?text=Gaming+Setup"
|
||||
bgImage: "/images/about/slab.jpg"
|
||||
},
|
||||
{
|
||||
id: 'snowboarding',
|
||||
title: "Snowboarding",
|
||||
description: "The rush of going down the mountains are unmatched",
|
||||
description: "Here is me teaching my girlfriend😅",
|
||||
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',
|
||||
title: "Culinary Arts",
|
||||
description: "Experimenting with new recipes and flavors in the kitchen.",
|
||||
className: "", // Standard 1x1
|
||||
bgImage: "https://placehold.co/400x400/3e2723/d7ccc8?text=Cooking"
|
||||
title: "Cooking",
|
||||
description: "Everyone cooks sure, I'll give you that. But I want to master the art of cooking a excellent steak.",
|
||||
className: "",
|
||||
bgImage: "/images/about/steak.jpg"
|
||||
},
|
||||
{
|
||||
id: 'social',
|
||||
title: "Family & Friends",
|
||||
description: "Good times with my favorite people. Including my Girlfriend, Friends and Family",
|
||||
className: "", // Standard 1x1
|
||||
className: "bento-tall",
|
||||
bgImage: [
|
||||
"https://placehold.co/400x400/f8bbd0/880e4f?text=Social+1",
|
||||
"https://placehold.co/400x400/d81b60/ffffff?text=Social+2",
|
||||
"https://placehold.co/400x400/880e4f/f8bbd0?text=Social+3"
|
||||
"/images/about/girl1.jpg",
|
||||
"/images/about/family1.jpg",
|
||||
"/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.",
|
||||
className: "bento-wide",
|
||||
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 }}
|
||||
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}>
|
||||
<motion.img
|
||||
key={currentIndex}
|
||||
@@ -86,6 +153,7 @@ const BentoCard = ({ item, index, delay }: { item: BentoItem; index: number; del
|
||||
}}
|
||||
/>
|
||||
</AnimatePresence>
|
||||
)}
|
||||
<div className="bento-overlay" />
|
||||
<div className="bento-content">
|
||||
<h3 className="bento-title">{item.title}</h3>
|
||||
|
||||
@@ -79,6 +79,7 @@ const ParticlesBackground: React.FC = () => {
|
||||
// Mobile (390x844) -> ~48 particles
|
||||
// Mobile Landscape (844x390) -> ~48 particles (Same as portrait!)
|
||||
const area = canvas.width * canvas.height;
|
||||
// Reduce density further to specificially help the heavy About page
|
||||
const particleCount = Math.floor(Math.sqrt(area) / 12);
|
||||
|
||||
particles = [];
|
||||
@@ -90,17 +91,7 @@ const ParticlesBackground: React.FC = () => {
|
||||
const animate = () => {
|
||||
if (!ctx || !canvas) return;
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
// Draw a subtle gradient background for the canvas itself if needed, or keep transparent
|
||||
// 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);
|
||||
|
||||
// Gradient is now handled by CSS to avoid repainting every frame
|
||||
|
||||
particles.forEach((particle) => {
|
||||
particle.update();
|
||||
@@ -119,13 +110,32 @@ const ParticlesBackground: React.FC = () => {
|
||||
for (let b = a; b < particles.length; b++) {
|
||||
const dx = particles[a].x - particles[b].x;
|
||||
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;
|
||||
// Scale from 100 to 300 based on size average (2 to 6)
|
||||
const distanceThreshold = 100 + (sizeAverage - 2) * 50;
|
||||
|
||||
if (distance < distanceThreshold) {
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${(distanceThreshold * (2 / 3) * .001) - distance / 1000})`;
|
||||
// Optimization 2: Only calculate if we are within the VISIBLE threshold (2/3 of logical threshold)
|
||||
// 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.beginPath();
|
||||
ctx.moveTo(particles[a].x, particles[a].y);
|
||||
@@ -135,6 +145,7 @@ const ParticlesBackground: React.FC = () => {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
animate();
|
||||
@@ -166,6 +177,7 @@ const ParticlesBackground: React.FC = () => {
|
||||
width: '100%',
|
||||
height: '120vh', // Extend well below viewport to cover mobile browser bar retraction
|
||||
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 }}
|
||||
>
|
||||
<div style={{ width: "100%", maxWidth: "1200px" }}>
|
||||
<motion.h1
|
||||
{/* <motion.h1
|
||||
className="about-title"
|
||||
initial={{ y: -20, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
@@ -42,7 +42,7 @@ export default function About() {
|
||||
style={{ textAlign: "center" }}
|
||||
>
|
||||
About Me
|
||||
</motion.h1>
|
||||
</motion.h1> */}
|
||||
|
||||
<BioSection
|
||||
imageSrc="/images/about/dapperSasha.jpg"
|
||||
@@ -57,7 +57,6 @@ export default function About() {
|
||||
animate={{ opacity: 1, y: 0 }}
|
||||
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} />
|
||||
</motion.div>
|
||||
<motion.div
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.bento-grid-container {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, 1fr);
|
||||
grid-template-rows: repeat(2, 250px);
|
||||
grid-auto-rows: 250px;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
max-width: 1200px;
|
||||
|
||||