diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index d9b222d..a5cb7b0 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -33,7 +33,18 @@ jobs:
run: |
npm ci
npm run generate
-
+
+ - name: Prepare S3 uploads
+ run: |
+ # Ensure the dist directory exists
+ if [ -d "dist" ]; then
+ # Copy index.html to each route directory
+ mkdir -p dist/experience dist/projects dist/contact
+ cp dist/index.html dist/experience/index.html
+ cp dist/index.html dist/projects/index.html
+ cp dist/index.html dist/contact/index.html
+ fi
+
- name: Sync to S3
env:
AWS_REGION: us-east-1
diff --git a/assets/files/Mehmet_Deveci_Resume.pdf b/assets/files/Mehmet_Deveci_Resume.pdf
new file mode 100644
index 0000000..3b440b0
Binary files /dev/null and b/assets/files/Mehmet_Deveci_Resume.pdf differ
diff --git a/assets/files/resume.pdf b/assets/files/resume.pdf
deleted file mode 100644
index 7bb38c9..0000000
Binary files a/assets/files/resume.pdf and /dev/null differ
diff --git a/assets/styles/colors.scss b/assets/styles/colors.scss
index 58e6070..76a9dbe 100644
--- a/assets/styles/colors.scss
+++ b/assets/styles/colors.scss
@@ -22,7 +22,7 @@ $tooltipBg: #1f2131; // Tooltip background
$redButton: #da7164; // Red UI button
$yellowButton: #ebc063; // Yellow UI button
$greenButton: #3fdd78; // Green UI button
-
+$blueButton: #103cfc; // Blue UI button
// Status colors
$error: #b80000; // Error state
diff --git a/assets/styles/main.scss b/assets/styles/main.scss
index 1a987bc..34bd81b 100644
--- a/assets/styles/main.scss
+++ b/assets/styles/main.scss
@@ -677,4 +677,7 @@ p {
font-size: 1.25rem;
line-height: 1.75rem;
}
+ .modal-wrapper--full {
+ max-height: 260px;
+ }
}
diff --git a/components/AppHeader.vue b/components/AppHeader.vue
index fae88ff..d1c7798 100644
--- a/components/AppHeader.vue
+++ b/components/AppHeader.vue
@@ -1,11 +1,21 @@
@@ -54,7 +77,8 @@ const downloadFile = () => {
Projects
Contact
-
+
+
@@ -66,7 +90,8 @@ const downloadFile = () => {
#header {
box-sizing: border-box;
- box-shadow: 0px 30px 40px colors.$navyBlue;
+ box-shadow: 0px 30px 40px #01020d;
+ background: #01020d;
}
#header .container {
diff --git a/components/AppHero.vue b/components/AppHero.vue
index a0c52ab..2094e3c 100644
--- a/components/AppHero.vue
+++ b/components/AppHero.vue
@@ -61,27 +61,35 @@ const achievements = ref([
accomplishments: [
{
entity:
- 'Built a high-performance landing page with Vue.js for 2019 Local Elections at TRT World, delivering real-time results in 1300ms.*'
+ 'Built a high-performance landing page with Vue.js for 2019 Local Elections at TRT World, delivering real-time results in 1300ms.*'
},
{
entity:
- 'Migrated components and pages from Symfony to Vue.js at Homeday, improved page speeds up to %30.*'
+ 'Migrated components and pages from PHP to Vue.js at Homeday, improved page speeds up to 30%.*'
},
{
- entity: 'Implemented new user flows in Homeday\'s CMS: ensuring qualified buyers, and reducing costs for all parties.'
+ entity:
+ 'Built an embedded SVG-based map with Vue.js for Turkey\'s 2018 General Elections at TRT World, handling 75k+ concurrent users with 100% uptime.*'
+ },
+ {
+ entity:
+ 'Implemented new user flows in Homeday\'s CMS with Vue.js, ensuring users to be qualified, reducing costs for all parties.*'
}
]
},
{
- label: 'Data Visualization',
- hero: 'I built apps that transform complex information into interactive real-time interfaces.',
+ label: 'Development Infrastructure',
+ hero: 'I built, optimized CI/CD pipelines and infrastructure to streamline development workflows.',
image: 'data-bg.jpg',
accomplishments: [
{
- entity: 'Built an embedded SVG-based map with Vue.js for Turkey\'s 2018 General Elections at TRT World, handling 75k+ concurrent users.'
+ entity: 'Developed frontend infrastructure with AWS and GitHub Actions at Homeday, reducing costs by 15% and streamlining deployments.'
},
{
- entity: 'Upgraded frontend infrastructure with AWS and GitHub Actions, reducing costs by 15% and streamlining deployments.'
+ entity: 'Built Infrastructure as Code with Terraform & AWS, publishing static apps for no costs.*'
+ },
+ {
+ entity: 'Optimized CI/CD pipelines, with Testing Library at Homeday, reducing deployment loading times at least 20%.'
}
]
},
@@ -91,14 +99,14 @@ const achievements = ref([
image: 'engagement-bg.jpg',
accomplishments: [
{
- entity: "Enhanced Sdui's event management system, increasing user interaction at least 20% via better calendar & scheduling features."
+ entity: 'Enhanced the event management system at Sdui, increasing user interaction at least 20% via better calendar & scheduling features.'
},
{
entity:
'Built an asset management app from scratch, effectively using Node.js, and Vue.js at TRT World, reducing search times at least 50% and boosting editor efficiency.'
},
{
- entity: 'Conceptualized and implemented video conference functionality for a chat app at Sdui, allowing school teachers to set video calls.'
+ entity: 'Conceptualized and implemented video conference features for a chat app at Sdui, allowing school teachers to set video calls.'
}
]
},
@@ -121,13 +129,13 @@ const achievements = ref([
image: 'collaboration-bg.jpg',
accomplishments: [
{
- entity: 'Optimized test cases, reduced redundancies of the Design System at Homeday, reduced development build times by ~%20.'
+ entity: 'Optimized test cases, reduced redundancies of the Design System at Homeday, reduced development build times.'
},
{
- entity: 'Modernized an internal CMS at TRT World, improved search and creation user flows by 30%.'
+ entity: 'Implemented integration tests and reduced legacy unit and E2E tests at Homeday, accelerated build times.'
},
{
- entity: 'Implemented integration tests and reduced legacy unit and E2E tests at Homeday, accelerated build times by ~%25.'
+ entity: 'Moderated design system, D&I, and engineering workshops at Homeday, improving collaboration and design quality.'
}
]
}
@@ -171,407 +179,6 @@ const openVideoModal = () => {
isVideoModalOpen.value = true;
});
};
-
-// Canvas-based matrix animation
-const matrixCanvasRef = ref(null);
-const matrixAnimationStarted = ref(false);
-let animationFrameId = null;
-let matrixCanvas = null;
-let matrixCtx = null;
-
-// Matrix character set
-const matrixChars = '01'.split('');
-
-// Matrix character set
-const matrixWords = [
- // Frontend Frameworks & Libraries
- 'VUE',
- 'NUXT',
- 'NEXT',
- 'REACT',
- 'VITE',
- 'WEBPACK',
- 'BABEL',
- 'TYPESCRIPT',
- 'JAVASCRIPT',
- 'HTML',
- 'CSS',
- 'SCSS',
- 'TAILWIND',
- 'VUEX',
- 'GRAPHQL',
-
- // Backend & Servers
- 'NODE',
- 'EXPRESS',
- 'NEST',
- 'SYMFONY',
- 'LARAVEL',
- 'PHP',
- 'GO',
- 'POSTGRESQL',
- 'MYSQL',
- 'DOCKER',
- 'KUBERNETES',
-
- // Cloud & DevOps
- 'AWS',
- 'EC2',
- 'S3',
- 'LAMBDA',
- 'CLOUDFRONT',
- 'FIREBASE',
- 'NETLIFY',
- 'VERCEL',
- 'HEROKU',
- 'GITHUB',
- 'GITLAB',
- 'BITBUCKET',
- 'CI/CD',
- 'JENKINS',
- 'TERRAFORM',
- 'GRAFANA',
-
- // Design & UX
- 'UI',
- 'UX',
- 'FIGMA',
- 'SKETCH',
- 'ADOBE',
- 'PHOTOSHOP',
- 'WIREFRAME',
- 'PROTOTYPE',
- 'ACCESSIBILITY',
- 'A11Y',
- 'RESPONSIVE',
- 'MOBILE',
-
- // General Programming
- 'STAGING',
- 'BUILD',
- 'TEST',
- 'DEBUG',
- 'DEPLOY',
- 'RELEASE',
- 'AGILE',
- 'SCRUM',
- 'KANBAN',
- 'JIRA',
- 'API',
- 'REST',
- 'CLEAN',
- 'SOLID',
- 'PATTERNS',
- 'ALGORITHMS',
- 'DATA',
-
- // Performance & Optimization
- 'OPTIMIZE',
- 'CACHE',
- 'LAZY',
- 'BUNDLE',
- 'MINIFY',
- 'COMPRESS',
- 'PERFORMANCE',
- 'METRICS',
- 'LIGHTHOUSE',
- 'WEBVITALS',
- 'LCP',
- 'FID',
- 'CLS',
- 'SEO',
- 'PWA',
-
- // Data & Analytics
- 'ANALYTICS',
- 'AI',
- 'VISUALIZATION',
- 'D3',
- 'CHART',
- 'DASHBOARD',
- 'LOGGING',
- 'MONITOR',
- 'TRACK'
-];
-const drops = [];
-
-// Initialize the drops
-const initDrops = () => {
- if (!matrixCanvas) return;
-
- // Calculate how many drops to add based on width
- const fontSize = isMobile.value ? 10 : 14;
- const columns = Math.floor((matrixCanvas.width / fontSize) * 1.2); // Increase density of columns
-
- // Reset drops array
- drops.length = 0;
-
- // Create initial drops
- for (let i = 0; i < columns; i++) {
- // Random starting y position
- drops.push({
- x: i * fontSize * 2 + Math.random() * fontSize, // Reduce spacing between words
- y: Math.random() * matrixCanvas.height,
- speed: 0.5 + Math.random() * 1.5,
- opacity: 0.03 + Math.random() * 0.15, // Lower overall opacity
- word: matrixWords[Math.floor(Math.random() * matrixWords.length)],
- length: Math.floor(2 + Math.random() * 5), // Fewer trailing characters
- chars: []
- });
-
- // Generate trailing characters for this drop
- for (let j = 0; j < drops[i].length; j++) {
- drops[i].chars.push({
- char: drops[i].word[Math.floor(Math.random() * drops[i].word.length)],
- opacity: j === 0 ? 0.9 : (1 - j / drops[i].length) * 0.7
- });
- }
- }
-};
-
-// The animation loop
-const startMatrixAnimation = () => {
- if (!matrixCtx || !matrixCanvas) return;
-
- // More thorough clearing to prevent trails
- matrixCtx.globalCompositeOperation = 'source-over';
- matrixCtx.fillStyle = 'rgba(2, 6, 23, 0.15)'; // Increased opacity for better clearing
- matrixCtx.fillRect(0, 0, matrixCanvas.width, matrixCanvas.height);
-
- const fontSize = isMobile.value ? 10 : 14;
-
- // Draw each drop
- drops.forEach((drop, _) => {
- // Skip drops without proper initialization
- if (!drop || !drop.word || !drop.chars || !Array.isArray(drop.chars)) return;
-
- // Move drop down by its speed
- drop.y += drop.speed;
-
- // Draw the main word at the head of the drop with lighter green
- matrixCtx.font = `bold ${fontSize}px monospace`;
- // Clear compositing to prevent white halos
- matrixCtx.globalCompositeOperation = 'source-over';
- // Use a lighter shade of green with lower opacity
- matrixCtx.fillStyle = `rgba(120, 230, 160, ${drop.opacity * 0.8})`; // Lighter green with reduced opacity
- matrixCtx.fillText(drop.word, drop.x, drop.y);
-
- // Draw trailing characters
- drop.chars.forEach((charObj, j) => {
- // Skip undefined char objects
- if (!charObj || typeof charObj !== 'object') return;
-
- // Create trailing effect with fading CTA color
- const trailY = drop.y - (j + 1) * fontSize;
-
- if (trailY > 0 && trailY < matrixCanvas.height) {
- // Trailing characters have fading opacity and color transition from CTA to white
- const opacity = (1 - j / drop.length) * drop.opacity * 0.5; // Lower opacity for trails
-
- if (j < 2) {
- // First trailing characters with lighter green
- matrixCtx.globalCompositeOperation = 'source-over';
- matrixCtx.fillStyle = `rgba(140, 245, 180, ${opacity})`;
- } else {
- // Rest fade to very light cyan
- matrixCtx.globalCompositeOperation = 'source-over';
- matrixCtx.fillStyle = `rgba(160, 245, 200, ${opacity})`;
- }
-
- matrixCtx.font = `${fontSize}px monospace`;
- matrixCtx.fillText(charObj.char, drop.x + (Math.random() > 0.5 ? fontSize / 4 : 0), trailY);
-
- // Occasionally change character
- if (Math.random() > 0.92) {
- if (drop.word && drop.word.length > 0) {
- charObj.char = drop.word[Math.floor(Math.random() * drop.word.length)];
- }
- }
- }
- });
-
- // Reset drop when it goes off screen
- if (drop.y > matrixCanvas.height + fontSize) {
- // Move drop far above viewport to prevent trails
- drop.y = -fontSize * 4; // Position higher above the viewport
- drop.speed = 0.5 + Math.random() * 1.5;
- drop.opacity = 0.03 + Math.random() * 0.15; // Lower overall opacity
- drop.word = matrixWords[Math.floor(Math.random() * matrixWords.length)];
-
- // Ensure length is properly set
- if (!drop.length || drop.length < 1) {
- drop.length = Math.floor(2 + Math.random() * 5);
- }
-
- // Clear and regenerate trailing characters
- drop.chars = [];
- for (let j = 0; j < drop.length; j++) {
- if (drop.word && drop.word.length > 0) {
- drop.chars.push({
- char: drop.word[Math.floor(Math.random() * drop.word.length)],
- opacity: j === 0 ? 0.9 : (1 - j / drop.length) * 0.7
- });
- }
- }
- }
- });
-
- // Occasionally add a new fast "highlight" drop
- if (Math.random() > 0.99 && drops.length < (matrixCanvas.width / fontSize) * 1.2) {
- const fontSize = isMobile.value ? 10 : 14;
- const x = Math.floor((Math.random() * matrixCanvas.width) / fontSize) * fontSize;
-
- // Check if there's already a drop at this x position
- const existingDropIndex = drops.findIndex((d) => d.x === x);
-
- if (existingDropIndex === -1) {
- // Add a new bright drop
- const newDrop = {
- x,
- y: -5 * fontSize,
- speed: 2 + Math.random() * 3,
- opacity: 0.7 + Math.random() * 0.3,
- length: Math.floor(4 + Math.random() * 8),
- chars: []
- };
-
- // Generate characters for this drop
- for (let j = 0; j < newDrop.length; j++) {
- newDrop.chars.push({
- char: matrixChars[Math.floor(Math.random() * matrixChars.length)],
- opacity: j === 0 ? 1 : (1 - j / newDrop.length) * 0.9
- });
- }
-
- drops.push(newDrop);
- }
- }
-
- // Occasionally add a new fast "highlight" drop with a full word
- if (Math.random() > 0.97 && drops.length < matrixCanvas.width / fontSize) {
- // Increased probability and allowed density
- const x = Math.floor((Math.random() * matrixCanvas.width) / fontSize) * fontSize;
-
- // Check if there's already a drop near this x position (with smaller distance check)
- const existingDropIndex = drops.findIndex((d) => Math.abs(d.x - x) < fontSize * 2);
-
- if (existingDropIndex === -1) {
- // Add a new bright word drop
- const newWord = matrixWords[Math.floor(Math.random() * matrixWords.length)];
- const newDrop = {
- x,
- y: -5 * fontSize,
- speed: 2 + Math.random() * 3,
- opacity: 0.04 + Math.random() * 0.12, // Much lower opacity
- word: newWord,
- length: Math.floor(2 + Math.random() * 4),
- chars: []
- };
-
- // Generate trailing characters for this drop
- for (let j = 0; j < newDrop.length; j++) {
- newDrop.chars.push({
- char: newWord[Math.floor(Math.random() * newWord.length)],
- opacity: j === 0 ? 1 : (1 - j / newDrop.length) * 0.9
- });
- }
-
- drops.push(newDrop);
- }
- }
-
- // Continue animation loop
- animationFrameId = requestAnimationFrame(startMatrixAnimation);
-};
-
-// Matrix digital rain effect setup
-onMounted(() => {
- // Render boxes after LCP is done
- setTimeout(() => {
- isBoxesRendered.value = true;
- }, 1000);
-
- // Initialize matrix animation after boxes are rendered
- setTimeout(() => {
- matrixAnimationStarted.value = true;
- initMatrixAnimation();
- }, 1500);
-
- // Keyboard event listeners
- window.addEventListener('keydown', (e) => {
- if (e.key === 'Escape' && isModalOpen.value) {
- closeModal();
- }
- });
-});
-
-// Initialize the matrix animation
-const initMatrixAnimation = () => {
- if (!isBoxesRendered.value) return;
-
- // Only set up canvas once
- nextTick(() => {
- matrixCanvas = matrixCanvasRef.value;
- if (!matrixCanvas) return;
-
- // Create canvas context with settings to prevent white background artifacts
- matrixCtx = matrixCanvas.getContext('2d', {
- alpha: false,
- willReadFrequently: false,
- desynchronized: true
- });
-
- // Disable text anti-aliasing for sharper text
- matrixCtx.imageSmoothingEnabled = false;
-
- // Initial full clear of the canvas
- matrixCtx.fillStyle = 'rgb(2, 6, 23)'; // navyBlue
- matrixCtx.fillRect(0, 0, matrixCanvas.width, matrixCanvas.height);
-
- // Set canvas dimensions to match container
- const hero = document.getElementById('hero');
- if (hero) {
- matrixCanvas.width = hero.offsetWidth;
- matrixCanvas.height = hero.offsetHeight;
- } else {
- matrixCanvas.width = window.innerWidth;
- matrixCanvas.height = window.innerHeight;
- }
-
- // Add resize listener
- window.addEventListener('resize', () => {
- clearTimeout(resizeTimeout);
- resizeTimeout = setTimeout(() => {
- isMobile.value = window.innerWidth < 1024;
-
- // Resize canvas
- if (matrixCanvas) {
- if (hero) {
- matrixCanvas.width = hero.offsetWidth;
- matrixCanvas.height = hero.offsetHeight;
- } else {
- matrixCanvas.width = window.innerWidth;
- matrixCanvas.height = window.innerHeight;
- }
-
- // Reset drops when resizing
- initDrops();
- }
- }, 250);
- });
-
- // Start matrix animation
- initDrops();
- startMatrixAnimation();
- });
-};
-
-// Clean up animation on component unmount
-onBeforeUnmount(() => {
- if (animationFrameId !== null) {
- cancelAnimationFrame(animationFrameId);
- }
-});
@@ -622,7 +229,7 @@ onBeforeUnmount(() => {
-
+
Explore {{ nextAchievementLabel }} Accomplishments
@@ -762,6 +369,28 @@ onBeforeUnmount(() => {
margin-left: 4rem;
}
+.cta--lightBlue {
+ color: rgba(123, 199, 255, 0.2);
+ border-bottom: 2px solid rgba(123, 199, 255, 0.2);
+
+ &:after {
+ background-color: colors.$cta;
+ }
+
+ svg {
+ transform: rotate(135deg) translate(0, 0);
+ }
+
+ &:hover {
+ color: black;
+ border-color: colors.$ctaHover;
+
+ svg {
+ transform: rotate(135deg) translate(-0.9rem, -0.75rem);
+ }
+ }
+}
+
.scroll-indicator {
display: flex;
flex-direction: column;
@@ -903,7 +532,6 @@ onBeforeUnmount(() => {
.hero-sentence {
position: relative;
overflow: hidden;
- // border-radius: 8px;
margin-bottom: 2rem;
&__bg {
@@ -918,7 +546,7 @@ onBeforeUnmount(() => {
p {
position: relative;
z-index: 1;
- padding: 3rem;
+ padding: 4rem;
color: white;
text-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
}
@@ -1014,38 +642,4 @@ onBeforeUnmount(() => {
filter: brightness(1.2);
}
}
-// #hero {
-// *:not(.boxes > div):not(.boxes):not(.btn):not(.btn--video):not(.modal-button) {
-// pointer-events: none;
-// }
-// }
-
-/* Canvas-based Matrix animation for better performance */
-.matrix-container {
- position: absolute;
- top: 0;
- left: 0;
- width: 100%;
- height: 100vh;
- overflow: hidden;
- opacity: 0;
- animation: fadeIn 0.5s cubic-bezier(0.2, 0.57, 0.76, 0.79) forwards;
- animation-delay: 0.2s;
- box-sizing: border-box;
- z-index: -1;
- contain: layout style paint; /* Improve performance */
-}
-
-.matrix-canvas {
- display: block;
- width: 100%;
- height: 100%;
- object-fit: cover;
- transform: translateZ(0); /* Hardware acceleration */
- will-change: transform; /* Optimize for animations */
- opacity: 0.7; // Slightly reduced opacity
- filter: contrast(1.05) brightness(1.05); // Reduced filter intensity to minimize trails
- image-rendering: optimizeSpeed; // Improve rendering performance
- background-color: rgb(2, 6, 23); // Match canvas background to prevent white flashes
-}
diff --git a/components/MatrixBackground.vue b/components/MatrixBackground.vue
index 6082d6d..43bccce 100644
--- a/components/MatrixBackground.vue
+++ b/components/MatrixBackground.vue
@@ -16,6 +16,12 @@ let matrixCanvas = null;
let matrixCtx = null;
let resizeTimeout;
+// Mouse position for parallax effect
+const mouseX = ref(0);
+const mouseY = ref(0);
+const parallaxOffsetX = ref(0);
+const parallaxOffsetY = ref(0);
+
// Matrix words
const matrixWords = [
// Frontend Frameworks & Libraries
@@ -137,6 +143,70 @@ const matrixWords = [
'TRACK'
];
+// Secondary smaller words
+const secondaryWords = [
+ 'CODE',
+ 'DEVELOP',
+ 'RENDER',
+ 'STATE',
+ 'PROPS',
+ 'MODULE',
+ 'EXPORT',
+ 'IMPORT',
+ 'FUNCTION',
+ 'METHOD',
+ 'ASYNC',
+ 'AWAIT',
+ 'PROMISE',
+ 'CLASS',
+ 'INTERFACE',
+ 'TYPE',
+ 'UNION',
+ 'ANY',
+ 'NULL',
+ 'UNDEFINED',
+ 'STRING',
+ 'NUMBER',
+ 'BOOLEAN',
+ 'ARRAY',
+ 'OBJECT',
+ 'MAP',
+ 'SET',
+ 'SYMBOL',
+ 'DOM',
+ 'EVENT',
+ 'STORE',
+ 'VUEX',
+ 'PINIA',
+ 'REDUX',
+ 'HOOK',
+ 'REF',
+ 'REACTIVE',
+ 'COMPUTED',
+ 'WATCH',
+ 'EFFECT',
+ 'LIFECYCLE',
+ 'MOUNTED',
+ 'CREATED',
+ 'EMIT',
+ 'BINDING',
+ 'TEMPLATE',
+ 'STYLE',
+ 'SCOPE',
+ 'SHADOW',
+ 'SERVER',
+ 'CLIENT',
+ 'HYBRID',
+ 'ISOMORPHIC',
+ 'HEADLESS',
+ 'STATIC',
+ 'DYNAMIC',
+ 'NATIVE'
+];
+
+// Combined words list for more variety
+const allWords = [...matrixWords, ...secondaryWords];
+
// Array to hold falling word objects
const drops = [];
@@ -145,23 +215,31 @@ const initDrops = () => {
if (!matrixCanvas) return;
// Calculate how many drops to add based on width
- const fontSize = isMobile.value ? 10 : 14;
- const columns = Math.floor((matrixCanvas.width / fontSize) * 1.2); // Increase density of columns
+ const baseFontSize = isMobile.value ? 10 : 14;
+ const columns = Math.floor((matrixCanvas.width / baseFontSize) * 1.5); // Increase density further
// Reset drops array
drops.length = 0;
- // Create initial drops
+ // Create initial drops with different sizes and depths
for (let i = 0; i < columns; i++) {
+ // Determine size/depth layer (1-3, where 1 is largest, 3 is smallest)
+ const depthLayer = Math.ceil(Math.random() * 3);
+ const fontSize = depthLayer === 1 ? baseFontSize : depthLayer === 2 ? baseFontSize * 0.8 : baseFontSize * 0.6;
+ const wordsList = depthLayer === 1 ? matrixWords : allWords;
+
// Random starting y position
drops.push({
- x: i * fontSize * 2 + Math.random() * fontSize, // Reduce spacing between words
+ x: i * baseFontSize * 1.5 + Math.random() * baseFontSize, // Reduce spacing between words
y: Math.random() * matrixCanvas.height,
- speed: 0.5 + Math.random() * 1.5,
- opacity: 0.03 + Math.random() * 0.15, // Lower overall opacity
- word: matrixWords[Math.floor(Math.random() * matrixWords.length)],
+ speed: 0.3 + Math.random() * 1.5,
+ opacity: 0.03 + Math.random() * (depthLayer === 1 ? 0.15 : 0.1), // Different opacity per layer
+ word: wordsList[Math.floor(Math.random() * wordsList.length)],
length: Math.floor(2 + Math.random() * 5), // Fewer trailing characters
- chars: []
+ chars: [],
+ fontSize, // Store font size with the drop
+ depthLayer, // Store depth layer for parallax effect
+ parallaxFactor: depthLayer === 1 ? 0.05 : depthLayer === 2 ? 0.03 : 0.01 // Different parallax response
});
// Generate trailing characters for this drop
@@ -183,48 +261,52 @@ const startMatrixAnimation = () => {
matrixCtx.fillStyle = 'rgba(2, 6, 23, 0.85)'; // Semi-transparent navyBlue
matrixCtx.fillRect(0, 0, matrixCanvas.width, matrixCanvas.height);
- const fontSize = isMobile.value ? 10 : 14;
-
- // Draw each drop
- drops.forEach((drop, _) => {
+ // Draw each drop with parallax effect
+ drops.forEach((drop) => {
// Skip drops without proper initialization
if (!drop || !drop.word || !drop.chars || !Array.isArray(drop.chars)) return;
+ // Apply parallax offset based on depth layer
+ const parallaxX = drop.x + parallaxOffsetX.value * drop.parallaxFactor;
+ const parallaxY = drop.y + parallaxOffsetY.value * drop.parallaxFactor;
+
// Move drop down by its speed
drop.y += drop.speed;
// Draw the main word at the head of the drop with lighter green
- matrixCtx.font = `bold ${fontSize}px monospace`;
+ matrixCtx.font = `bold ${drop.fontSize}px monospace`;
// Clear compositing to prevent white halos
matrixCtx.globalCompositeOperation = 'source-over';
- // Use a lighter shade of green with lower opacity
- matrixCtx.fillStyle = `rgba(130, 255, 170, ${drop.opacity * 1.2})`; // Brighter green with increased opacity
- matrixCtx.fillText(drop.word, drop.x, drop.y);
- // Draw trailing characters
+ // Use a lighter shade of green with opacity based on depth
+ const opacity = drop.depthLayer === 1 ? drop.opacity * 1.3 : drop.depthLayer === 2 ? drop.opacity * 1.1 : drop.opacity * 0.9;
+ matrixCtx.fillStyle = `rgba(130, 255, 170, ${opacity})`;
+ matrixCtx.fillText(drop.word, parallaxX, parallaxY);
+
+ // Draw trailing characters with parallax
drop.chars.forEach((charObj, j) => {
// Skip undefined char objects
if (!charObj || typeof charObj !== 'object') return;
// Create trailing effect with fading CTA color
- const trailY = drop.y - (j + 1) * fontSize;
+ const trailY = parallaxY - (j + 1) * drop.fontSize;
if (trailY > 0 && trailY < matrixCanvas.height) {
// Trailing characters have fading opacity and color transition from CTA to white
- const opacity = (1 - j / drop.length) * drop.opacity * 0.7; // Increased opacity for trails
+ const trailOpacity = (1 - j / drop.length) * drop.opacity * 0.7; // Increased opacity for trails
if (j < 2) {
// First trailing characters with lighter green
matrixCtx.globalCompositeOperation = 'source-over';
- matrixCtx.fillStyle = `rgba(150, 255, 190, ${opacity * 1.3})`;
+ matrixCtx.fillStyle = `rgba(150, 255, 190, ${trailOpacity * 1.3})`;
} else {
// Rest fade to very light cyan
matrixCtx.globalCompositeOperation = 'source-over';
- matrixCtx.fillStyle = `rgba(170, 255, 210, ${opacity})`;
+ matrixCtx.fillStyle = `rgba(170, 255, 210, ${trailOpacity})`;
}
- matrixCtx.font = `${fontSize}px monospace`;
- matrixCtx.fillText(charObj.char, drop.x + (Math.random() > 0.5 ? fontSize / 4 : 0), trailY);
+ matrixCtx.font = `${drop.fontSize}px monospace`;
+ matrixCtx.fillText(charObj.char, parallaxX + (Math.random() > 0.5 ? drop.fontSize / 4 : 0), trailY);
// Occasionally change character
if (Math.random() > 0.92) {
@@ -236,12 +318,14 @@ const startMatrixAnimation = () => {
});
// Reset drop when it goes off screen
- if (drop.y > matrixCanvas.height + fontSize) {
+ if (drop.y > matrixCanvas.height + drop.fontSize) {
// Move drop far above viewport to prevent trails
- drop.y = -fontSize * 4; // Position higher above the viewport
- drop.speed = 0.5 + Math.random() * 1.5;
- drop.opacity = 0.03 + Math.random() * 0.15; // Lower overall opacity
- drop.word = matrixWords[Math.floor(Math.random() * matrixWords.length)];
+ drop.y = -drop.fontSize * 4; // Position higher above the viewport
+ drop.speed = 0.3 + Math.random() * 1.5;
+
+ // Refresh word based on depth layer
+ const wordsList = drop.depthLayer === 1 ? matrixWords : allWords;
+ drop.word = wordsList[Math.floor(Math.random() * wordsList.length)];
// Ensure length is properly set
if (!drop.length || drop.length < 1) {
@@ -262,24 +346,30 @@ const startMatrixAnimation = () => {
});
// Occasionally add a new fast "highlight" drop with a full word
- if (Math.random() > 0.97 && drops.length < matrixCanvas.width / fontSize) {
+ if (Math.random() > 0.97 && drops.length < matrixCanvas.width / 10) {
// Increased probability and allowed density
- const x = Math.floor((Math.random() * matrixCanvas.width) / fontSize) * fontSize;
+ const x = Math.floor(Math.random() * matrixCanvas.width);
+ const depthLayer = Math.ceil(Math.random() * 3);
+ const fontSize = depthLayer === 1 ? (isMobile.value ? 10 : 14) : depthLayer === 2 ? (isMobile.value ? 8 : 11) : isMobile.value ? 7 : 9;
+ const wordsList = depthLayer === 1 ? matrixWords : allWords;
// Check if there's already a drop near this x position (with smaller distance check)
- const existingDropIndex = drops.findIndex((d) => Math.abs(d.x - x) < fontSize * 2);
+ const existingDropIndex = drops.findIndex((d) => Math.abs(d.x - x) < fontSize * 1.5);
if (existingDropIndex === -1) {
// Add a new bright word drop
- const newWord = matrixWords[Math.floor(Math.random() * matrixWords.length)];
+ const newWord = wordsList[Math.floor(Math.random() * wordsList.length)];
const newDrop = {
x,
y: -5 * fontSize,
- speed: 2 + Math.random() * 3,
- opacity: 0.04 + Math.random() * 0.12, // Much lower opacity
+ speed: 1.5 + Math.random() * 3,
+ opacity: 0.04 + Math.random() * (depthLayer === 1 ? 0.12 : 0.09),
word: newWord,
length: Math.floor(2 + Math.random() * 4),
- chars: []
+ chars: [],
+ fontSize,
+ depthLayer,
+ parallaxFactor: depthLayer === 1 ? 0.05 : depthLayer === 2 ? 0.03 : 0.01
};
// Generate trailing characters for this drop
@@ -298,6 +388,42 @@ const startMatrixAnimation = () => {
animationFrameId = requestAnimationFrame(startMatrixAnimation);
};
+// Update parallax values based on mouse position
+const handleMouseMove = (e) => {
+ if (!matrixCanvas) return;
+
+ // Calculate mouse position relative to center of canvas
+ const rect = matrixCanvas.getBoundingClientRect();
+ const centerX = rect.left + rect.width / 2;
+ const centerY = rect.top + rect.height / 2;
+
+ // Calculate distance from center (normalized -1 to 1)
+ mouseX.value = (e.clientX - centerX) / (rect.width / 2);
+ mouseY.value = (e.clientY - centerY) / (rect.height / 2);
+
+ // Apply easing for smoother movement
+ parallaxOffsetX.value += (mouseX.value * 20 - parallaxOffsetX.value) * 0.05;
+ parallaxOffsetY.value += (mouseY.value * 20 - parallaxOffsetY.value) * 0.05;
+};
+
+// Gyroscope parallax for mobile
+const handleDeviceOrientation = (e) => {
+ if (!matrixCanvas || !e.gamma || !e.beta) return;
+
+ // Use device orientation data for parallax
+ // gamma is the left-to-right tilt in degrees, beta is front-to-back
+ const gammaRange = 20; // Limit the effect range
+ const betaRange = 20;
+
+ // Normalize and limit value range (-1 to 1)
+ const normalizedGamma = Math.max(Math.min(e.gamma / gammaRange, 1), -1);
+ const normalizedBeta = Math.max(Math.min((e.beta - 45) / betaRange, 1), -1);
+
+ // Apply easing for smoother movement
+ parallaxOffsetX.value += (normalizedGamma * 20 - parallaxOffsetX.value) * 0.05;
+ parallaxOffsetY.value += (normalizedBeta * 20 - parallaxOffsetY.value) * 0.05;
+};
+
// Initialize the matrix animation
const initMatrixAnimation = () => {
// Only set up canvas once
@@ -356,6 +482,13 @@ const initMatrixAnimation = () => {
}, 250);
});
+ // Add mouse/device movement listeners for parallax
+ if (isMobile.value) {
+ window.addEventListener('deviceorientation', handleDeviceOrientation);
+ } else {
+ window.addEventListener('mousemove', handleMouseMove);
+ }
+
// Start matrix animation
initDrops();
startMatrixAnimation();
@@ -381,6 +514,8 @@ onBeforeUnmount(() => {
// Remove event listeners
window.removeEventListener('resize', () => {});
+ window.removeEventListener('mousemove', handleMouseMove);
+ window.removeEventListener('deviceorientation', handleDeviceOrientation);
});
@@ -406,6 +541,7 @@ onBeforeUnmount(() => {
box-sizing: border-box;
z-index: -1;
contain: layout style paint; /* Improve performance */
+ perspective: 1000px; /* Add perspective for 3D effect */
}
.matrix-canvas {
@@ -416,9 +552,10 @@ onBeforeUnmount(() => {
transform: translateZ(0); /* Hardware acceleration */
will-change: transform; /* Optimize for animations */
opacity: 0.9; // Higher opacity for vivid effect
- filter: contrast(1.08) brightness(1.05) blur(0.3px); // Added slight blur for glow effect
+ filter: contrast(1.1) brightness(1.05) blur(0.3px); // Added slight blur for glow effect
image-rendering: optimizeSpeed; // Improve rendering performance
background: linear-gradient(to bottom, #01020d, #03050d); // Vertical gradient
+ transition: transform 0.05s ease-out; /* Smooth transition for parallax effect */
}
@keyframes fadeIn {
diff --git a/components/Modal.vue b/components/Modal.vue
index 39931a7..c63e364 100644
--- a/components/Modal.vue
+++ b/components/Modal.vue
@@ -24,7 +24,7 @@