301 lines
9.3 KiB
Plaintext
301 lines
9.3 KiB
Plaintext
---
|
|
import { Icon } from 'astro-icon/components';
|
|
|
|
export interface Props {
|
|
isDark?: boolean;
|
|
showIcons?: boolean;
|
|
disableParallax?: boolean;
|
|
}
|
|
|
|
const { isDark = false, showIcons = true, disableParallax = false } = Astro.props;
|
|
|
|
// Define color palettes for light and dark modes with higher contrast
|
|
const lightModeColors = [
|
|
'text-blue-600/40',
|
|
'text-indigo-600/40',
|
|
'text-purple-600/40',
|
|
'text-cyan-600/40',
|
|
'text-teal-600/40',
|
|
'text-emerald-600/40',
|
|
'text-sky-600/40',
|
|
'text-violet-600/40',
|
|
];
|
|
|
|
const darkModeColors = [
|
|
'dark:text-blue-400/45',
|
|
'dark:text-indigo-400/45',
|
|
'dark:text-purple-400/45',
|
|
'dark:text-cyan-400/45',
|
|
'dark:text-teal-400/45',
|
|
'dark:text-emerald-400/45',
|
|
'dark:text-sky-400/45',
|
|
'dark:text-violet-400/45',
|
|
];
|
|
|
|
// Define interfaces for our icon objects
|
|
interface BaseIconObject {
|
|
icon: string;
|
|
x: string;
|
|
y: string;
|
|
size: string;
|
|
opacity: string;
|
|
rotate: string;
|
|
}
|
|
|
|
interface IconWithColors extends BaseIconObject {
|
|
lightColor: string;
|
|
darkColor: string;
|
|
}
|
|
|
|
// Function to get a random color from the palette
|
|
const getRandomColor = (palette: string[]): string => {
|
|
const randomIndex = Math.floor(Math.random() * palette.length);
|
|
return palette[randomIndex];
|
|
};
|
|
|
|
// List of icons to be used in the background
|
|
// Expanded to include a wider variety of symbols for more visual diversity
|
|
const iconNames: string[] = [
|
|
// Core IT/Tech icons
|
|
'tabler:settings-automation',
|
|
'tabler:brand-office',
|
|
'tabler:api',
|
|
'tabler:server',
|
|
'tabler:message-chatbot',
|
|
'tabler:share',
|
|
'tabler:code',
|
|
'tabler:cloud',
|
|
'tabler:device-laptop',
|
|
'tabler:database',
|
|
'tabler:brand-github',
|
|
'tabler:device-desktop',
|
|
'tabler:brand-azure',
|
|
|
|
// Additional tech-related icons
|
|
'tabler:cpu',
|
|
'tabler:device-mobile',
|
|
'tabler:wifi',
|
|
'tabler:network',
|
|
'tabler:shield',
|
|
'tabler:lock',
|
|
'tabler:key',
|
|
|
|
// Tangentially related icons for visual diversity
|
|
'tabler:bulb',
|
|
'tabler:compass',
|
|
'tabler:binary',
|
|
'tabler:infinity',
|
|
'tabler:brain',
|
|
];
|
|
|
|
// Function to get a random value within a range
|
|
const getRandomInRange = (min: number, max: number): number => {
|
|
return Math.random() * (max - min) + min;
|
|
};
|
|
|
|
// Function to get a random rotation with increased range
|
|
const getRandomRotation = (): string => {
|
|
return `${getRandomInRange(-30, 30)}deg`;
|
|
};
|
|
|
|
// Function to get a random size (restored to original dimensions)
|
|
const getRandomSize = (): string => {
|
|
return `${getRandomInRange(140, 180)}px`;
|
|
};
|
|
|
|
// Function to get a random opacity
|
|
const getRandomOpacity = (): string => {
|
|
return getRandomInRange(0.32, 0.38).toFixed(2);
|
|
};
|
|
|
|
// Create a spacious layout with well-separated icons
|
|
const createSpacedIcons = (): BaseIconObject[] => {
|
|
const icons: BaseIconObject[] = [];
|
|
const rows = 6; // Reduced from 8 to 6 for fewer potential positions
|
|
const cols = 6; // Reduced from 8 to 6 for fewer potential positions
|
|
|
|
// Define larger margins to keep icons away from edges (in percentage)
|
|
const marginX = 10; // 10% margin from left and right edges (increased from 5%)
|
|
const marginY = 10; // 10% margin from top and bottom edges (increased from 5%)
|
|
|
|
// Minimum distance between icons (in percentage points)
|
|
const minDistance = 20; // Ensure at least 20% distance between any two icons
|
|
|
|
// Create a base grid of positions
|
|
for (let row = 0; row < rows; row++) {
|
|
for (let col = 0; col < cols; col++) {
|
|
// Randomly skip more positions (75% chance) for fewer icons overall
|
|
if (Math.random() < 0.75) {
|
|
continue;
|
|
}
|
|
|
|
// Base position in the grid with margins applied
|
|
const baseX = marginX + ((col / cols) * (100 - 2 * marginX));
|
|
const baseY = marginY + ((row / rows) * (100 - 2 * marginY));
|
|
|
|
// Add limited randomness to the position (±5%) to maintain spacing
|
|
const randomOffsetX = getRandomInRange(-5, 5);
|
|
const randomOffsetY = getRandomInRange(-5, 5);
|
|
|
|
// Ensure positions stay within margins
|
|
const x = Math.max(marginX, Math.min(100 - marginX, baseX + randomOffsetX));
|
|
const y = Math.max(marginY, Math.min(100 - marginY, baseY + randomOffsetY));
|
|
|
|
// Check if this position is too close to any existing icon
|
|
let tooClose = false;
|
|
for (const existingIcon of icons) {
|
|
// Extract numeric values from percentage strings
|
|
const existingX = parseFloat(existingIcon.x);
|
|
const existingY = parseFloat(existingIcon.y);
|
|
|
|
// Calculate distance between points
|
|
const distance = Math.sqrt(
|
|
Math.pow(x - existingX, 2) +
|
|
Math.pow(y - existingY, 2)
|
|
);
|
|
|
|
// If too close to an existing icon, skip this position
|
|
if (distance < minDistance) {
|
|
tooClose = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (tooClose) {
|
|
continue;
|
|
}
|
|
|
|
// Randomly select an icon from the expanded set
|
|
const iconIndex = Math.floor(Math.random() * iconNames.length);
|
|
|
|
icons.push({
|
|
icon: iconNames[iconIndex],
|
|
x: `${x}%`,
|
|
y: `${y}%`,
|
|
size: getRandomSize(),
|
|
opacity: getRandomOpacity(),
|
|
rotate: getRandomRotation(),
|
|
});
|
|
}
|
|
}
|
|
|
|
return icons;
|
|
};
|
|
|
|
const icons: BaseIconObject[] = createSpacedIcons();
|
|
|
|
// Assign random colors to each icon
|
|
const iconsWithColors: IconWithColors[] = icons.map(icon => ({
|
|
icon: icon.icon,
|
|
x: icon.x,
|
|
y: icon.y,
|
|
size: icon.size,
|
|
opacity: icon.opacity,
|
|
rotate: icon.rotate,
|
|
lightColor: getRandomColor(lightModeColors),
|
|
darkColor: getRandomColor(darkModeColors),
|
|
}));
|
|
---
|
|
<div class:list={['absolute inset-0', { 'backdrop-blur-sm bg-white/5 dark:bg-gray-900/10': isDark }]}>
|
|
<slot />
|
|
|
|
{showIcons && (
|
|
/* Decorative background icons with parallax effect */
|
|
<div id="parallax-background" class="absolute inset-0 overflow-hidden pointer-events-none z-[-5]">
|
|
{iconsWithColors.map(({ icon, x, y, size, opacity, rotate, lightColor, darkColor }, index) => (
|
|
<div
|
|
class={`absolute ${lightColor} ${darkColor} parallax-icon`}
|
|
style={`left: ${x}; top: ${y}; opacity: ${opacity}; transform: rotate(${rotate}); will-change: transform; transition: transform 0.1s ease-out;`}
|
|
data-depth={`${0.5 + (index % 3) * 0.2}`}
|
|
data-initial-x={x}
|
|
data-initial-y={y}
|
|
>
|
|
<Icon name={icon} style={`width: ${size}; height: ${size};`} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
)}
|
|
</div>
|
|
|
|
{showIcons && (
|
|
<script define:vars={{ disableParallax }}>
|
|
// Parallax scrolling effect for background icons
|
|
document.addEventListener('DOMContentLoaded', () => {
|
|
// Get all parallax icons
|
|
const parallaxIcons = document.querySelectorAll('.parallax-icon');
|
|
|
|
// Skip parallax on mobile devices for better performance or if parallax is disabled
|
|
const isMobile = window.matchMedia('(max-width: 768px)').matches;
|
|
if (isMobile || disableParallax) return;
|
|
|
|
// Variables to track scroll position
|
|
let lastScrollY = window.scrollY;
|
|
let ticking = false;
|
|
|
|
// Function to update icon positions based on scroll
|
|
const updateParallax = () => {
|
|
parallaxIcons.forEach((icon) => {
|
|
const depth = parseFloat(icon.getAttribute('data-depth') || '0.5');
|
|
|
|
// Calculate parallax offset based on scroll position and depth
|
|
// Lower depth value means the icon moves slower (appears further away)
|
|
const yOffset = (lastScrollY * depth * 0.15);
|
|
|
|
// Get the original rotation
|
|
const transformValue = icon.style.transform;
|
|
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
|
|
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
|
|
|
|
// Apply transform with the original rotation plus the parallax offset
|
|
icon.style.transform = `${rotateValue} translate3d(0, ${yOffset}px, 0)`;
|
|
});
|
|
|
|
ticking = false;
|
|
};
|
|
|
|
// Throttle scroll events for better performance
|
|
const onScroll = () => {
|
|
lastScrollY = window.scrollY;
|
|
|
|
if (!ticking) {
|
|
window.requestAnimationFrame(() => {
|
|
updateParallax();
|
|
ticking = false;
|
|
});
|
|
|
|
ticking = true;
|
|
}
|
|
};
|
|
|
|
// Add scroll event listener
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
|
|
// Update on resize (debounced)
|
|
let resizeTimer;
|
|
window.addEventListener('resize', () => {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(() => {
|
|
// Check if device is now mobile and disable parallax if needed
|
|
const isMobileNow = window.matchMedia('(max-width: 768px)').matches;
|
|
|
|
if (isMobileNow) {
|
|
// Reset positions on mobile
|
|
parallaxIcons.forEach((icon) => {
|
|
const transformValue = icon.style.transform;
|
|
const rotateMatch = transformValue.match(/rotate\([^)]+\)/);
|
|
const rotateValue = rotateMatch ? rotateMatch[0] : 'rotate(0deg)';
|
|
icon.style.transform = rotateValue;
|
|
});
|
|
} else {
|
|
// Update parallax on desktop
|
|
updateParallax();
|
|
}
|
|
}, 200);
|
|
}, { passive: true });
|
|
|
|
// Initial update
|
|
updateParallax();
|
|
});
|
|
</script>
|
|
)}
|