diff --git a/src/public/projects/darkbox/thumbnail.jpeg b/src/public/projects/darkbox/thumbnail.jpeg new file mode 100644 index 0000000..5f769c2 Binary files /dev/null and b/src/public/projects/darkbox/thumbnail.jpeg differ diff --git a/src/public/projects/discord-bot/thumbnail.jpeg b/src/public/projects/discord-bot/thumbnail.jpeg new file mode 100644 index 0000000..3d36da3 Binary files /dev/null and b/src/public/projects/discord-bot/thumbnail.jpeg differ diff --git a/src/public/projects/web/thumbnail.jpeg b/src/public/projects/web/thumbnail.jpeg new file mode 100644 index 0000000..1e6838f Binary files /dev/null and b/src/public/projects/web/thumbnail.jpeg differ diff --git a/src/src/components/header/index.tsx b/src/src/components/header/index.tsx index 6a0cfe2..f3d3b51 100644 --- a/src/src/components/header/index.tsx +++ b/src/src/components/header/index.tsx @@ -27,7 +27,7 @@ export default function Header() { )); return ( -
diff --git a/src/src/components/vines/index.tsx b/src/src/components/vines/index.tsx new file mode 100644 index 0000000..09d9869 --- /dev/null +++ b/src/src/components/vines/index.tsx @@ -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 ( + + ); + }; + + 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 ( + + {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 ( + + ); + })} + {branch.leaves.map((leaf, i) => { + const point = points[Math.floor(leaf.position)]; + if (!point) return null; + return ( + + {renderLeaf(point, leaf.size, leaf.side, parentOpacity)} + + ); + })} + + ); + }; + + // 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 ( +
+ + {vines.map(vine => ( + + {renderBranch(vine.mainBranch, vine.opacity)} + {vine.subBranches.map(branch => renderBranch(branch, vine.opacity))} + + ))} + +
+ ); +}; + +export default VineAnimation; diff --git a/src/src/content/projects/discord-bot.mdx b/src/src/content/projects/discord-bot.mdx index 9a13cec..5abca16 100644 --- a/src/src/content/projects/discord-bot.mdx +++ b/src/src/content/projects/discord-bot.mdx @@ -4,5 +4,5 @@ description: "A discord bot template" githubUrl: "https://github.com/timmypidashev/pycord-bot-template" techStack: ["Python", "SQlite", "Docker"] date: "2025-01-03" -image: "/projects/web/thumbnail.jpeg" +image: "/projects/discord-bot/thumbnail.jpeg" --- diff --git a/src/src/layouts/index.astro b/src/src/layouts/index.astro index d44e545..838a1b4 100644 --- a/src/src/layouts/index.astro +++ b/src/src/layouts/index.astro @@ -5,6 +5,7 @@ import "@/style/globals.css"; import Header from "@/components/header"; import Footer from "@/components/footer"; +import VineAnimation from "@/components/vines"; --- @@ -16,6 +17,8 @@ import Footer from "@/components/footer";
+ +
diff --git a/src/tailwind.config.cjs b/src/tailwind.config.cjs index 0152480..bea890b 100644 --- a/src/tailwind.config.cjs +++ b/src/tailwind.config.cjs @@ -34,146 +34,155 @@ module.exports = { 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) => ({ DEFAULT: { css: { - color: theme('colors.foreground'), - '--tw-prose-body': theme('colors.foreground'), - '--tw-prose-headings': theme('colors.yellow.bright'), - '--tw-prose-links': theme('colors.blue.bright'), - '--tw-prose-bold': theme('colors.orange.bright'), - '--tw-prose-quotes': theme('colors.green.bright'), - '--tw-prose-code': theme('colors.purple.bright'), - '--tw-prose-hr': theme('colors.foreground'), - '--tw-prose-bullets': theme('colors.foreground'), + color: theme("colors.foreground"), + "--tw-prose-body": theme("colors.foreground"), + "--tw-prose-headings": theme("colors.yellow.bright"), + "--tw-prose-links": theme("colors.blue.bright"), + "--tw-prose-bold": theme("colors.orange.bright"), + "--tw-prose-quotes": theme("colors.green.bright"), + "--tw-prose-code": theme("colors.purple.bright"), + "--tw-prose-hr": theme("colors.foreground"), + "--tw-prose-bullets": theme("colors.foreground"), // Base text color - color: theme('colors.foreground'), + color: theme("colors.foreground"), // Headings h1: { - color: theme('colors.yellow.bright'), - fontWeight: '700', + color: theme("colors.yellow.bright"), + fontWeight: "700", }, h2: { - color: theme('colors.yellow.bright'), - fontWeight: '600', + color: theme("colors.yellow.bright"), + fontWeight: "600", }, h3: { - color: theme('colors.yellow.bright'), - fontWeight: '600', + color: theme("colors.yellow.bright"), + fontWeight: "600", }, h4: { - color: theme('colors.yellow.bright'), - fontWeight: '600', + color: theme("colors.yellow.bright"), + fontWeight: "600", }, // Links a: { - color: theme('colors.blue.bright'), - '&:hover': { - color: theme('colors.blue.DEFAULT'), + color: theme("colors.blue.bright"), + "&:hover": { + color: theme("colors.blue.DEFAULT"), }, - textDecoration: 'none', - borderBottom: `1px solid ${theme('colors.blue.bright')}`, - transition: 'all 0.2s ease-in-out', + textDecoration: "none", + borderBottom: `1px solid ${theme("colors.blue.bright")}`, + transition: "all 0.2s ease-in-out", }, // Bold strong: { - color: theme('colors.orange.bright'), - fontWeight: '600', + color: theme("colors.orange.bright"), + fontWeight: "600", }, // Lists ul: { li: { - '&::before': { - backgroundColor: theme('colors.foreground'), + "&::before": { + backgroundColor: theme("colors.foreground"), }, }, }, // Blockquotes blockquote: { - borderLeftColor: theme('colors.green.bright'), - color: theme('colors.green.bright'), - fontStyle: 'italic', - quotes: '"\\201C""\\201D""\\2018""\\2019"', + borderLeftColor: theme("colors.green.bright"), + color: theme("colors.green.bright"), + fontStyle: "italic", + quotes: "\"\\201C\"\"\\201D\"\"\\2018\"\"\\2019\"", p: { - '&::before': { content: 'none' }, - '&::after': { content: 'none' }, + "&::before": { content: "none" }, + "&::after": { content: "none" }, }, }, // Code code: { - color: theme('colors.purple.bright'), - backgroundColor: '#282828', // A dark gray that works with black - padding: '0.2em 0.4em', - borderRadius: '0.25rem', - fontWeight: '400', - '&::before': { - content: '""', + color: theme("colors.purple.bright"), + backgroundColor: "#282828", // A dark gray that works with black + padding: "0.2em 0.4em", + borderRadius: "0.25rem", + fontWeight: "400", + "&::before": { + content: "\"\"", }, - '&::after': { - content: '""', + "&::after": { + content: "\"\"", }, }, // Inline code - 'code::before': { - content: '""', + "code::before": { + content: "\"\"", }, - 'code::after': { - content: '""', + "code::after": { + content: "\"\"", }, // Pre pre: { - backgroundColor: '#282828', - color: theme('colors.foreground'), + backgroundColor: "#282828", + color: theme("colors.foreground"), code: { - backgroundColor: 'transparent', - padding: '0', - color: 'inherit', - fontSize: 'inherit', - fontWeight: 'inherit', - '&::before': { content: 'none' }, - '&::after': { content: 'none' }, + backgroundColor: "transparent", + padding: "0", + color: "inherit", + fontSize: "inherit", + fontWeight: "inherit", + "&::before": { content: "none" }, + "&::after": { content: "none" }, }, }, // Horizontal rules hr: { - borderColor: theme('colors.foreground'), - opacity: '0.2', + borderColor: theme("colors.foreground"), + opacity: "0.2", }, // Table table: { thead: { - borderBottomColor: theme('colors.foreground'), + borderBottomColor: theme("colors.foreground"), th: { - color: theme('colors.yellow.bright'), + color: theme("colors.yellow.bright"), }, }, tbody: { tr: { - borderBottomColor: theme('colors.foreground'), + borderBottomColor: theme("colors.foreground"), }, }, }, // Images img: { - borderRadius: '0.375rem', + borderRadius: "0.375rem", }, // Figures figcaption: { - color: theme('colors.foreground'), - opacity: '0.8', + color: theme("colors.foreground"), + opacity: "0.8", }, }, },