Add preliminary vines animation; more work on content

This commit is contained in:
Timothy Pidashev
2025-01-06 11:55:34 -08:00
parent efe0b9713f
commit 21772ae6cb
8 changed files with 370 additions and 66 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

View File

@@ -27,7 +27,7 @@ export default function Header() {
)); ));
return ( return (
<header className={`fixed z-50 top-0 left-0 right-0 bg-black font-bold transition-transform duration-300 ${ <header className={`fixed z-50 top-0 left-0 right-0 font-bold transition-transform duration-300 ${
visible ? "translate-y-0" : "-translate-y-full" visible ? "translate-y-0" : "-translate-y-full"
}`}> }`}>
<div className="flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20"> <div className="flex flex-row pt-1 px-2 text-lg lg:pt-2 lg:text-3xl md:text-2xl items-center justify-between md:justify-center space-x-2 md:space-x-10 lg:space-x-20">

View File

@@ -0,0 +1,292 @@
import React, { useState, useEffect, useCallback } from 'react';
const VineAnimation = ({ side }) => {
const [vines, setVines] = useState([]);
const VINE_COLOR = '#b8bb26'; // Gruvbox green
const VINE_LIFETIME = 8000; // Time before fade starts
const FADE_DURATION = 3000; // How long the fade takes
const MAX_VINES = Math.max(3, Math.floor(window.innerWidth / 400)); // Adjust to screen width
const isMobile = window.innerWidth <= 768;
const getDistance = (pointA, pointB) => {
return Math.sqrt(
Math.pow(pointA.x - pointB.x, 2) + Math.pow(pointA.y - pointB.y, 2)
);
};
// Function to create a new branch
const createBranch = (startX, startY, baseRotation) => ({
id: Date.now() + Math.random(),
points: [{
x: startX,
y: startY,
rotation: baseRotation
}],
leaves: [],
growing: true,
phase: Math.random() * Math.PI * 2,
amplitude: Math.random() * 0.5 + 1.2
});
// Function to create a new vine
const createNewVine = useCallback(() => ({
id: Date.now() + Math.random(),
mainBranch: createBranch(
side === 'left' ? 0 : window.innerWidth,
Math.random() * (window.innerHeight * 0.8) + (window.innerHeight * 0.1),
side === 'left' ? 0 : Math.PI
),
subBranches: [],
growing: true,
createdAt: Date.now(),
fadingOut: false,
opacity: 1
}), [side]);
// Update branch function
const updateBranch = (branch, isSubBranch = false) => {
if (!branch.growing) return branch;
const lastPoint = branch.points[branch.points.length - 1];
const progress = branch.points.length * 0.15;
const safetyMargin = window.innerWidth * 0.2;
const minX = safetyMargin;
const maxX = window.innerWidth - safetyMargin;
const baseAngle = side === 'left' ? 0 : Math.PI;
let curve = Math.sin(progress + branch.phase) * branch.amplitude;
const distanceFromCenter = Math.abs(window.innerWidth/2 - lastPoint.x);
const centerRepulsion = Math.max(0, 1 - (distanceFromCenter / (window.innerWidth/4)));
curve += (side === 'left' ? -1 : 1) * centerRepulsion * 0.5;
if (side === 'left' && lastPoint.x > minX) {
curve -= Math.pow((lastPoint.x - minX) / safetyMargin, 2);
} else if (side === 'right' && lastPoint.x < maxX) {
curve += Math.pow((maxX - lastPoint.x) / safetyMargin, 2);
}
const currentAngle = baseAngle + curve;
const distance = isSubBranch ? 12 : 18;
const newX = lastPoint.x + Math.cos(currentAngle) * distance;
const newY = lastPoint.y + Math.sin(currentAngle) * distance;
const newPoint = {
x: newX,
y: newY,
rotation: currentAngle
};
let newLeaves = [...branch.leaves];
if (Math.random() < 0.2 && branch.points.length > 2) {
const maxLength = isSubBranch ? 15 : 30;
const progress = branch.points.length / maxLength;
const baseSize = isSubBranch ? 20 : 35;
const sizeGradient = Math.pow(1 - progress, 2);
const leafSize = Math.max(8, baseSize * sizeGradient);
// Ensure leaves don't grow on the last point
const leafPosition = Math.min(branch.points.length - 2, Math.floor(Math.random() * branch.points.length));
newLeaves.push({
position: leafPosition, // Use the selected point
size: leafSize,
side: Math.random() > 0.5 ? 'left' : 'right'
});
}
return {
...branch,
points: [...branch.points, newPoint],
leaves: newLeaves,
growing: branch.points.length < (isSubBranch ? 15 : 30)
};
};
// Update vine function
const updateVine = useCallback((vine) => {
const now = Date.now();
const age = now - vine.createdAt;
// Calculate opacity based on age
let newOpacity = vine.opacity;
if (age > VINE_LIFETIME) {
const fadeProgress = (age - VINE_LIFETIME) / FADE_DURATION;
newOpacity = Math.max(0, 1 - fadeProgress);
}
// Update main branch
const newMainBranch = updateBranch(vine.mainBranch);
let newSubBranches = [...vine.subBranches];
// Add new branches with random probability
if (
!vine.fadingOut &&
age < VINE_LIFETIME &&
Math.random() < 0.05 &&
newMainBranch.points.length > 4
) {
// Choose a random point, excluding the last point
const allBranches = [newMainBranch, ...newSubBranches];
const sourceBranch = allBranches[Math.floor(Math.random() * allBranches.length)];
const branchPointIndex = Math.floor(Math.random() * (sourceBranch.points.length - 1)); // Exclude the last point
const branchPoint = sourceBranch.points[branchPointIndex];
const rotationOffset = Math.random() * 0.8 - 0.4;
newSubBranches.push(
createBranch(
branchPoint.x,
branchPoint.y,
branchPoint.rotation + rotationOffset
)
);
}
// Update existing branches
newSubBranches = newSubBranches.map(branch => updateBranch(branch, true));
return {
...vine,
mainBranch: newMainBranch,
subBranches: newSubBranches,
growing: newMainBranch.growing || newSubBranches.some(b => b.growing),
opacity: newOpacity,
fadingOut: age > VINE_LIFETIME
};
}, [side]);
// Render functions for leaves and branches
const renderLeaf = (point, size, leafSide, parentOpacity = 1) => {
const sideMultiplier = leafSide === 'left' ? -1 : 1;
const angle = point.rotation + (Math.PI / 3) * sideMultiplier;
const tipX = point.x + Math.cos(angle) * size * 2;
const tipY = point.y + Math.sin(angle) * size * 2;
const ctrl1X = point.x + Math.cos(angle - Math.PI/8) * size * 1.8;
const ctrl1Y = point.y + Math.sin(angle - Math.PI/8) * size * 1.8;
const ctrl2X = point.x + Math.cos(angle + Math.PI/8) * size * 1.8;
const ctrl2Y = point.y + Math.sin(angle + Math.PI/8) * size * 1.8;
const baseCtrl1X = point.x + Math.cos(angle - Math.PI/4) * size * 0.5;
const baseCtrl1Y = point.y + Math.sin(angle - Math.PI/4) * size * 0.5;
const baseCtrl2X = point.x + Math.cos(angle + Math.PI/4) * size * 0.5;
const baseCtrl2Y = point.y + Math.sin(angle + Math.PI/4) * size * 0.5;
return (
<path
d={`
M ${point.x} ${point.y}
C ${baseCtrl1X} ${baseCtrl1Y} ${ctrl1X} ${ctrl1Y} ${tipX} ${tipY}
C ${ctrl2X} ${ctrl2Y} ${baseCtrl2X} ${baseCtrl2Y} ${point.x} ${point.y}
`}
fill={VINE_COLOR}
opacity={parentOpacity * 0.8}
/>
);
};
const renderBranch = (branch, parentOpacity = 1) => {
if (branch.points.length < 2) return null;
const points = branch.points;
const getStrokeWidth = (index) => {
const maxWidth = 5;
const progress = index / (points.length - 1);
const startTaper = Math.min(1, index / 3);
const endTaper = Math.pow(1 - progress, 1.5);
return maxWidth * startTaper * endTaper;
};
return (
<g key={branch.id}>
{points.map((point, i) => {
if (i === 0) return null;
const prev = points[i - 1];
const dx = point.x - prev.x;
const dy = point.y - prev.y;
const controlX = prev.x + dx * 0.7;
const controlY = prev.y + dy * 0.7;
return (
<path
key={`segment-${i}`}
d={`M ${prev.x} ${prev.y} Q ${controlX} ${controlY} ${point.x} ${point.y}`}
fill="none"
stroke={VINE_COLOR}
strokeWidth={getStrokeWidth(i)}
strokeLinecap="round"
strokeLinejoin="round"
opacity={parentOpacity * 0.9}
/>
);
})}
{branch.leaves.map((leaf, i) => {
const point = points[Math.floor(leaf.position)];
if (!point) return null;
return (
<g key={`${branch.id}-leaf-${i}`}>
{renderLeaf(point, leaf.size, leaf.side, parentOpacity)}
</g>
);
})}
</g>
);
};
// Animation loop effect
useEffect(() => {
if (isMobile) return;
// Initialize with staggered vines
if (vines.length === 0) {
setVines([
createNewVine(),
{ ...createNewVine(), createdAt: Date.now() - 2000 },
{ ...createNewVine(), createdAt: Date.now() - 4000 }
]);
}
const interval = setInterval(() => {
setVines(currentVines => {
// Update all vines
const updatedVines = currentVines
.map(vine => updateVine(vine))
.filter(vine => vine.opacity > 0.01);
// Add new vines to maintain constant activity
if (updatedVines.length < 3) {
return [...updatedVines, createNewVine()];
}
return updatedVines;
});
}, 40);
return () => clearInterval(interval);
}, [createNewVine, updateVine]);
return (
<div className="fixed top-0 left-0 w-full h-full pointer-events-none">
<svg
className="w-full h-full"
viewBox={`0 0 ${window.innerWidth} ${window.innerHeight}`}
preserveAspectRatio="xMidYMid meet"
>
{vines.map(vine => (
<g key={vine.id}>
{renderBranch(vine.mainBranch, vine.opacity)}
{vine.subBranches.map(branch => renderBranch(branch, vine.opacity))}
</g>
))}
</svg>
</div>
);
};
export default VineAnimation;

View File

@@ -4,5 +4,5 @@ description: "A discord bot template"
githubUrl: "https://github.com/timmypidashev/pycord-bot-template" githubUrl: "https://github.com/timmypidashev/pycord-bot-template"
techStack: ["Python", "SQlite", "Docker"] techStack: ["Python", "SQlite", "Docker"]
date: "2025-01-03" date: "2025-01-03"
image: "/projects/web/thumbnail.jpeg" image: "/projects/discord-bot/thumbnail.jpeg"
--- ---

View File

@@ -5,6 +5,7 @@ import "@/style/globals.css";
import Header from "@/components/header"; import Header from "@/components/header";
import Footer from "@/components/footer"; import Footer from "@/components/footer";
import VineAnimation from "@/components/vines";
--- ---
<html lang="en"> <html lang="en">
@@ -16,6 +17,8 @@ import Footer from "@/components/footer";
</head> </head>
<body class="bg-background text-foreground"> <body class="bg-background text-foreground">
<Header client:load /> <Header client:load />
<VineAnimation side="left" client:only="react" />
<VineAnimation side="right" client:only="react" />
<main> <main>
<slot /> <slot />
</main> </main>

View File

@@ -34,146 +34,155 @@ module.exports = {
bright: "#8ec07c" bright: "#8ec07c"
} }
}, },
keyframes: {
"draw-line": {
"0%": { "stroke-dashoffset": "100" },
"100%": { "stroke-dashoffset": "0" }
}
},
animation: {
"draw-line": "draw-line 0.6s ease-out forwards"
},
typography: (theme) => ({ typography: (theme) => ({
DEFAULT: { DEFAULT: {
css: { css: {
color: theme('colors.foreground'), color: theme("colors.foreground"),
'--tw-prose-body': theme('colors.foreground'), "--tw-prose-body": theme("colors.foreground"),
'--tw-prose-headings': theme('colors.yellow.bright'), "--tw-prose-headings": theme("colors.yellow.bright"),
'--tw-prose-links': theme('colors.blue.bright'), "--tw-prose-links": theme("colors.blue.bright"),
'--tw-prose-bold': theme('colors.orange.bright'), "--tw-prose-bold": theme("colors.orange.bright"),
'--tw-prose-quotes': theme('colors.green.bright'), "--tw-prose-quotes": theme("colors.green.bright"),
'--tw-prose-code': theme('colors.purple.bright'), "--tw-prose-code": theme("colors.purple.bright"),
'--tw-prose-hr': theme('colors.foreground'), "--tw-prose-hr": theme("colors.foreground"),
'--tw-prose-bullets': theme('colors.foreground'), "--tw-prose-bullets": theme("colors.foreground"),
// Base text color // Base text color
color: theme('colors.foreground'), color: theme("colors.foreground"),
// Headings // Headings
h1: { h1: {
color: theme('colors.yellow.bright'), color: theme("colors.yellow.bright"),
fontWeight: '700', fontWeight: "700",
}, },
h2: { h2: {
color: theme('colors.yellow.bright'), color: theme("colors.yellow.bright"),
fontWeight: '600', fontWeight: "600",
}, },
h3: { h3: {
color: theme('colors.yellow.bright'), color: theme("colors.yellow.bright"),
fontWeight: '600', fontWeight: "600",
}, },
h4: { h4: {
color: theme('colors.yellow.bright'), color: theme("colors.yellow.bright"),
fontWeight: '600', fontWeight: "600",
}, },
// Links // Links
a: { a: {
color: theme('colors.blue.bright'), color: theme("colors.blue.bright"),
'&:hover': { "&:hover": {
color: theme('colors.blue.DEFAULT'), color: theme("colors.blue.DEFAULT"),
}, },
textDecoration: 'none', textDecoration: "none",
borderBottom: `1px solid ${theme('colors.blue.bright')}`, borderBottom: `1px solid ${theme("colors.blue.bright")}`,
transition: 'all 0.2s ease-in-out', transition: "all 0.2s ease-in-out",
}, },
// Bold // Bold
strong: { strong: {
color: theme('colors.orange.bright'), color: theme("colors.orange.bright"),
fontWeight: '600', fontWeight: "600",
}, },
// Lists // Lists
ul: { ul: {
li: { li: {
'&::before': { "&::before": {
backgroundColor: theme('colors.foreground'), backgroundColor: theme("colors.foreground"),
}, },
}, },
}, },
// Blockquotes // Blockquotes
blockquote: { blockquote: {
borderLeftColor: theme('colors.green.bright'), borderLeftColor: theme("colors.green.bright"),
color: theme('colors.green.bright'), color: theme("colors.green.bright"),
fontStyle: 'italic', fontStyle: "italic",
quotes: '"\\201C""\\201D""\\2018""\\2019"', quotes: "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\"",
p: { p: {
'&::before': { content: 'none' }, "&::before": { content: "none" },
'&::after': { content: 'none' }, "&::after": { content: "none" },
}, },
}, },
// Code // Code
code: { code: {
color: theme('colors.purple.bright'), color: theme("colors.purple.bright"),
backgroundColor: '#282828', // A dark gray that works with black backgroundColor: "#282828", // A dark gray that works with black
padding: '0.2em 0.4em', padding: "0.2em 0.4em",
borderRadius: '0.25rem', borderRadius: "0.25rem",
fontWeight: '400', fontWeight: "400",
'&::before': { "&::before": {
content: '""', content: "\"\"",
}, },
'&::after': { "&::after": {
content: '""', content: "\"\"",
}, },
}, },
// Inline code // Inline code
'code::before': { "code::before": {
content: '""', content: "\"\"",
}, },
'code::after': { "code::after": {
content: '""', content: "\"\"",
}, },
// Pre // Pre
pre: { pre: {
backgroundColor: '#282828', backgroundColor: "#282828",
color: theme('colors.foreground'), color: theme("colors.foreground"),
code: { code: {
backgroundColor: 'transparent', backgroundColor: "transparent",
padding: '0', padding: "0",
color: 'inherit', color: "inherit",
fontSize: 'inherit', fontSize: "inherit",
fontWeight: 'inherit', fontWeight: "inherit",
'&::before': { content: 'none' }, "&::before": { content: "none" },
'&::after': { content: 'none' }, "&::after": { content: "none" },
}, },
}, },
// Horizontal rules // Horizontal rules
hr: { hr: {
borderColor: theme('colors.foreground'), borderColor: theme("colors.foreground"),
opacity: '0.2', opacity: "0.2",
}, },
// Table // Table
table: { table: {
thead: { thead: {
borderBottomColor: theme('colors.foreground'), borderBottomColor: theme("colors.foreground"),
th: { th: {
color: theme('colors.yellow.bright'), color: theme("colors.yellow.bright"),
}, },
}, },
tbody: { tbody: {
tr: { tr: {
borderBottomColor: theme('colors.foreground'), borderBottomColor: theme("colors.foreground"),
}, },
}, },
}, },
// Images // Images
img: { img: {
borderRadius: '0.375rem', borderRadius: "0.375rem",
}, },
// Figures // Figures
figcaption: { figcaption: {
color: theme('colors.foreground'), color: theme("colors.foreground"),
opacity: '0.8', opacity: "0.8",
}, },
}, },
}, },