mirror of
https://github.com/Lissy93/web-check.git
synced 2025-04-29 11:54:32 +02:00
Adds interactive animated components for homepage
This commit is contained in:
parent
fa6ef6f929
commit
68f95d503c
94
src/components/homepage/AnimatedButton.astro
Normal file
94
src/components/homepage/AnimatedButton.astro
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
---
|
||||||
|
const buttonText = 'Analyze URL';
|
||||||
|
const buttonType = 'submit';
|
||||||
|
---
|
||||||
|
|
||||||
|
<button class="button" type={buttonType}>{buttonText}</button>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@property --angle {
|
||||||
|
syntax: '<angle>';
|
||||||
|
initial-value: 90deg;
|
||||||
|
inherits: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --gradX {
|
||||||
|
syntax: '<percentage>';
|
||||||
|
initial-value: 50%;
|
||||||
|
inherits: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@property --gradY {
|
||||||
|
syntax: '<percentage>';
|
||||||
|
initial-value: 0%;
|
||||||
|
inherits: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
.button {
|
||||||
|
padding: 1rem;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
border: 2px solid var(--text-color-thirdly);
|
||||||
|
border-radius: 2px;
|
||||||
|
font-weight: bold;
|
||||||
|
background: var(--background);
|
||||||
|
cursor: pointer;
|
||||||
|
overflow: hidden;
|
||||||
|
--angle: 90deg;
|
||||||
|
--gradX: 100%;
|
||||||
|
--gradY: 50%;
|
||||||
|
--c1: var(--primary);
|
||||||
|
--c2: var(--text-color-thirdly);
|
||||||
|
border-image: conic-gradient(from var(--angle), var(--c2), var(--c1) 0.5turn, var(--c1) 0.15turn, var(--c2) 0.25turn) 1;
|
||||||
|
animation: borderRotate 3500ms linear infinite forwards;
|
||||||
|
transition: border 0.3s ease-in-out;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: 2px solid var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes borderRotate {
|
||||||
|
0% { --angle: 90deg; }
|
||||||
|
25% { --angle: 180deg; }
|
||||||
|
50% { --angle: 270deg; }
|
||||||
|
75% { --angle: 360deg; }
|
||||||
|
100% { --angle: 450deg; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Workaround to add fancy border animation for users who use a REAL browser
|
||||||
|
// Since CSS @property attribute isn't supported in Firefox yet
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const isFirefox = typeof (window as any).InstallTrigger !== 'undefined';
|
||||||
|
if (isFirefox) {
|
||||||
|
const button = document.querySelector('.button') as HTMLElement | null;
|
||||||
|
if (button) {
|
||||||
|
const duration = 3500;
|
||||||
|
const startAngle = 90;
|
||||||
|
const endAngle = 420;
|
||||||
|
function animateAngle() {
|
||||||
|
let start: number | null = null;
|
||||||
|
function step(timestamp: number) {
|
||||||
|
if (!start) start = timestamp;
|
||||||
|
const progress = (timestamp - start) / duration;
|
||||||
|
const angle = startAngle + progress * (endAngle - startAngle);
|
||||||
|
|
||||||
|
button && button.style.setProperty('--angle', `${angle}deg`);
|
||||||
|
|
||||||
|
if (progress < 1) {
|
||||||
|
window.requestAnimationFrame(step);
|
||||||
|
} else {
|
||||||
|
start = null;
|
||||||
|
window.requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
window.requestAnimationFrame(step);
|
||||||
|
}
|
||||||
|
animateAngle();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
186
src/components/homepage/AnimatedInput.astro
Normal file
186
src/components/homepage/AnimatedInput.astro
Normal file
@ -0,0 +1,186 @@
|
|||||||
|
---
|
||||||
|
const placeholders = [
|
||||||
|
'duck.com',
|
||||||
|
'github.com',
|
||||||
|
'google.com',
|
||||||
|
'x.com',
|
||||||
|
'bbc.co.uk',
|
||||||
|
'wikipedia.org',
|
||||||
|
'openai.com',
|
||||||
|
];
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
|
<div class="input-container">
|
||||||
|
<input
|
||||||
|
required
|
||||||
|
id="url-input"
|
||||||
|
type="url"
|
||||||
|
name="url"
|
||||||
|
placeholder="E.g. duck.com"
|
||||||
|
/>
|
||||||
|
<div class="placeholder-container">
|
||||||
|
<span class="starter" aria-hidden="true">E.g.</span>
|
||||||
|
{placeholders.map((placeholder, index) => (
|
||||||
|
<span class={`placeholder ${index === 0 ? 'active' : ''}`} aria-hidden="true">{placeholder}</span>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Grab the DOM elements we need
|
||||||
|
const placeholders = document.querySelectorAll('.placeholder');
|
||||||
|
const starter = document.querySelector('.starter') as HTMLElement;
|
||||||
|
const inputField = document.getElementById('url-input') as HTMLInputElement;
|
||||||
|
|
||||||
|
// Variables for the configuring + tracking the placeholder animation
|
||||||
|
let currentIndex = 0;
|
||||||
|
const timeBetweenPlaceholders = 3000;
|
||||||
|
let interval: ReturnType<typeof setInterval>;
|
||||||
|
|
||||||
|
// Function to change which placeholder is currently visible
|
||||||
|
const changePlaceholder = () => {
|
||||||
|
placeholders.forEach((el, index) => {
|
||||||
|
starter.classList.remove('hide');
|
||||||
|
if (index === currentIndex) {
|
||||||
|
el.classList.add('active');
|
||||||
|
el.classList.remove('inactive');
|
||||||
|
el.classList.remove('still-inactive');
|
||||||
|
} else {
|
||||||
|
el.classList.add('inactive');
|
||||||
|
el.classList.remove('active');
|
||||||
|
setTimeout(() => {
|
||||||
|
el.classList.add('still-inactive');
|
||||||
|
}, timeBetweenPlaceholders / 5);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
currentIndex = (currentIndex + 1) % placeholders.length;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Begin the placeholder animation
|
||||||
|
const startPlaceholderAnimation = () => {
|
||||||
|
interval = setInterval(changePlaceholder, timeBetweenPlaceholders);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Stop the placeholder animation
|
||||||
|
const stopPlaceholderAnimation = () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
starter.classList.add('hide');
|
||||||
|
placeholders.forEach((el) => {
|
||||||
|
el.classList.remove('active');
|
||||||
|
el.classList.add('inactive');
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// When user focuses on the input field, stop the animation
|
||||||
|
inputField.addEventListener('focus', () => {
|
||||||
|
stopPlaceholderAnimation();
|
||||||
|
});
|
||||||
|
|
||||||
|
// And, when user un-focuses on input, resume the placeholder animation
|
||||||
|
inputField.addEventListener('blur', () => {
|
||||||
|
if (!inputField.value) {
|
||||||
|
startPlaceholderAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// If user types something, stop the animation
|
||||||
|
inputField.addEventListener('input', () => {
|
||||||
|
if (inputField.value) {
|
||||||
|
stopPlaceholderAnimation();
|
||||||
|
} else if (document.activeElement !== inputField) {
|
||||||
|
startPlaceholderAnimation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Disable the input's placeholder attribute
|
||||||
|
inputField.setAttribute('placeholder', '');
|
||||||
|
|
||||||
|
// Make visible the placeholder container
|
||||||
|
(document.querySelector('.placeholder-container') as HTMLElement).style.display = 'flex';
|
||||||
|
|
||||||
|
// And finally, start the animation!
|
||||||
|
startPlaceholderAnimation();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@styles/typography.scss';
|
||||||
|
@import '@styles/global.scss';
|
||||||
|
|
||||||
|
.input-container {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
input {
|
||||||
|
width: 100%;
|
||||||
|
padding: 1rem;
|
||||||
|
background: var(--background-50);
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--text-color-secondary);
|
||||||
|
border: 2px solid var(--text-color);
|
||||||
|
border-radius: 2px;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
&:focus {
|
||||||
|
border-color: var(--primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.input-container .placeholder-container {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: 16px;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.starter, .placeholder {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2rem;
|
||||||
|
color: var(--text-color-thirdly);
|
||||||
|
padding: 0.1rem 0;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.starter {
|
||||||
|
transition: opacity 0.6s, transform 0.5s;
|
||||||
|
&.hide {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
padding-left: 4rem;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
transition: transform 0.5s ease-in-out, opacity 0.6s;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-150%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder.active {
|
||||||
|
transform: translateY(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder.inactive {
|
||||||
|
transform: translateY(150%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder.still-inactive {
|
||||||
|
transform: translateY(-150%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
</style>
|
128
src/components/homepage/HeroForm.astro
Normal file
128
src/components/homepage/HeroForm.astro
Normal file
@ -0,0 +1,128 @@
|
|||||||
|
---
|
||||||
|
import AnimatedButton from "./AnimatedButton.astro"
|
||||||
|
import AnimatedInput from "./AnimatedInput.astro"
|
||||||
|
---
|
||||||
|
|
||||||
|
<div class="left">
|
||||||
|
<h1>
|
||||||
|
<img src="/favicon.svg" alt="Check Web" width="64" />
|
||||||
|
<span class="web">Web</span>
|
||||||
|
<span class="check">Check</span>
|
||||||
|
</h1>
|
||||||
|
<div class="homepage-action-content">
|
||||||
|
<h2>We give you X-Ray<br />Vision for your Website</h2>
|
||||||
|
<h3>
|
||||||
|
In just 20 seconds, you can see
|
||||||
|
<span>what attackers already know</span>
|
||||||
|
</h3>
|
||||||
|
<form name="live-start" autocomplete="off" action="/check" class="live-start" id="live-start">
|
||||||
|
<label for="url">Enter a URL to start 👇</label>
|
||||||
|
<AnimatedInput />
|
||||||
|
<AnimatedButton />
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div>X</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/**
|
||||||
|
* Form management actions (validation, submission, etc.)
|
||||||
|
* We just use normal, old school JavaScript for this
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Select the form and input elements from the DOM
|
||||||
|
const form = document.getElementById('live-start');
|
||||||
|
const urlInput = document.getElementById('url-input') as HTMLInputElement;
|
||||||
|
|
||||||
|
// Submit Event - called when user submits form with a valid URL
|
||||||
|
// Gets and checks the URL, then redirects user to /check/:url
|
||||||
|
form?.addEventListener('submit', (event) => {
|
||||||
|
event.preventDefault();
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
if (url) {
|
||||||
|
const encodedUrl = encodeURIComponent(url);
|
||||||
|
window.location.href = `/check/${encodedUrl}`;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// User presses enter, forgets to add protocol
|
||||||
|
// Will add https:// to the URL, and retry form submit
|
||||||
|
urlInput?.addEventListener('keydown', (event) => {
|
||||||
|
if (event.key === 'Enter') {
|
||||||
|
const url = urlInput.value.trim();
|
||||||
|
const urlWithoutProtocolRegex = /^[a-zA-Z0-9]+[a-zA-Z0-9.-]*\.[a-zA-Z]{2,}$/;
|
||||||
|
if (url && !/^https?:\/\//i.test(url) && urlWithoutProtocolRegex.test(url)) {
|
||||||
|
urlInput.value = 'https://' + url;
|
||||||
|
form?.dispatchEvent(new Event('submit'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '@styles/global.scss';
|
||||||
|
.left {
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 20vh;
|
||||||
|
@include tablet-landscape-down {
|
||||||
|
gap: 6rem;
|
||||||
|
}
|
||||||
|
@include mobile-down {
|
||||||
|
gap: 4rem;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin: 0;
|
||||||
|
fontis-size: 3em;
|
||||||
|
z-index: 5;
|
||||||
|
display: flex;
|
||||||
|
gap: 0.1rem;
|
||||||
|
align-items: center;
|
||||||
|
img {
|
||||||
|
vertical-align: middle;
|
||||||
|
width: 3rem;
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
}
|
||||||
|
.web {
|
||||||
|
color: var(--text-color);
|
||||||
|
}
|
||||||
|
.check {
|
||||||
|
color: var(--primary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.homepage-action-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
max-width: 700px;
|
||||||
|
z-index: 1;
|
||||||
|
@include tablet-landscape-down {
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
font-size: 3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-weight: normal;
|
||||||
|
font-size: 1.3rem;
|
||||||
|
span {
|
||||||
|
color: var(--primary);
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
form {
|
||||||
|
margin-top: 1.5rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 1rem;
|
||||||
|
label {
|
||||||
|
font-size: 1.3rem;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
174
src/components/homepage/HomeBackground.tsx
Normal file
174
src/components/homepage/HomeBackground.tsx
Normal file
@ -0,0 +1,174 @@
|
|||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
|
import styled from '@emotion/styled';
|
||||||
|
|
||||||
|
// Global Animation Configuration Constants
|
||||||
|
const dotSpacing = 32; // Number of px between each dot
|
||||||
|
const meteorCount = 4; // Number of meteors to display at any given time
|
||||||
|
const tailLength = 80; // Length of the meteor tail in px
|
||||||
|
const distanceBase = 5; // Base distance for meteor to travel in grid units
|
||||||
|
const distanceVariance = 5; // Variance for randomization to append to travel in grid units
|
||||||
|
const durationBase = 1.5; // Base duration for meteor to travel in seconds
|
||||||
|
const durationVariance = 1; // Variance for randomization to append to travel in seconds
|
||||||
|
const delayBase = 500; // Base delay for meteor to respawn in milliseconds
|
||||||
|
const delayVariance = 1500; // Variance for randomization to append to respawn in milliseconds
|
||||||
|
const tailDuration = 0.25; // Duration for meteor tail to retract in seconds
|
||||||
|
const headEasing = [0.8, 0.6, 1, 1]; // Easing for meteor head
|
||||||
|
const tailEasing = [0.5, 0.6, 0.6, 1]; // Easing for meteor tail
|
||||||
|
|
||||||
|
const MeteorContainer = styled(motion.div)`
|
||||||
|
position: absolute;
|
||||||
|
width: 4px;
|
||||||
|
height: 4px;
|
||||||
|
border-radius: 50%;
|
||||||
|
background-color: #9fef00;
|
||||||
|
top: 1px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Tail = styled(motion.div)`
|
||||||
|
position: absolute;
|
||||||
|
top: -80px;
|
||||||
|
left: 1px;
|
||||||
|
width: 2px;
|
||||||
|
height: 80px;
|
||||||
|
background: linear-gradient(to bottom, transparent, #9fef00);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledSvg = styled.svg`
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
inset: 0;
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
fill: rgba(100, 100, 100, 0.5);
|
||||||
|
defs pattern circle {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const StyledRect = styled.rect`
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
stroke-width: 0;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
pointer-events: none;
|
||||||
|
position: absolute;
|
||||||
|
height: 100vh;
|
||||||
|
width: 100vw;
|
||||||
|
z-index: 1;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
background: radial-gradient(circle at center top, transparent, transparent 60%, var(--background) 100%);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const generateMeteor = (id: number, gridSizeX: number, gridSizeY: number) => {
|
||||||
|
const column = Math.floor(Math.random() * gridSizeX);
|
||||||
|
const startRow = Math.floor(Math.random() * (gridSizeY - 12));
|
||||||
|
const travelDistance = distanceBase + Math.floor(Math.random() * distanceVariance);
|
||||||
|
const duration = durationBase + Math.floor(Math.random() * durationVariance);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id,
|
||||||
|
column,
|
||||||
|
startRow,
|
||||||
|
endRow: startRow + travelDistance,
|
||||||
|
duration,
|
||||||
|
tailVisible: true,
|
||||||
|
animationStage: 'traveling',
|
||||||
|
opacity: 1, // Initial opacity
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const generateInitialMeteors = (gridSizeX: number, gridSizeY: number) => {
|
||||||
|
const seen = new Set();
|
||||||
|
return Array.from({ length: meteorCount }, (_, index) => generateMeteor(index, gridSizeX, gridSizeY))
|
||||||
|
.filter(item => !seen.has(item.column) && seen.add(item.column));
|
||||||
|
};
|
||||||
|
|
||||||
|
const WebCheckHomeBackground = () => {
|
||||||
|
const [gridSizeX, setGridSizeX] = useState(Math.floor(window.innerWidth / dotSpacing));
|
||||||
|
const [gridSizeY, setGridSizeY] = useState(Math.floor(window.innerHeight / dotSpacing));
|
||||||
|
const [meteors, setMeteors] = useState(() => generateInitialMeteors(gridSizeX, gridSizeY));
|
||||||
|
|
||||||
|
const handleAnimationComplete = (id: number) => {
|
||||||
|
setMeteors(current =>
|
||||||
|
current.map(meteor => {
|
||||||
|
if (meteor.id === id) {
|
||||||
|
if (meteor.animationStage === 'traveling') {
|
||||||
|
// Transition to retracting tail
|
||||||
|
return { ...meteor, tailVisible: false, animationStage: 'retractingTail' };
|
||||||
|
} else if (meteor.animationStage === 'retractingTail') {
|
||||||
|
// Set to resetting and make invisible
|
||||||
|
return { ...meteor, animationStage: 'resetting', opacity: 0 };
|
||||||
|
} else if (meteor.animationStage === 'resetting') {
|
||||||
|
// Respawn the meteor after a delay
|
||||||
|
setTimeout(() => {
|
||||||
|
setMeteors(current =>
|
||||||
|
current.map(m => m.id === id ? generateMeteor(id, gridSizeX, gridSizeY) : m)
|
||||||
|
);
|
||||||
|
}, delayBase + Math.random() * delayVariance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return meteor;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleResize = () => {
|
||||||
|
setGridSizeX(Math.floor(window.innerWidth / dotSpacing));
|
||||||
|
setGridSizeY(Math.floor(window.innerHeight / dotSpacing));
|
||||||
|
};
|
||||||
|
window.addEventListener('resize', handleResize);
|
||||||
|
return () => window.removeEventListener('resize', handleResize);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Container />
|
||||||
|
<StyledSvg>
|
||||||
|
<defs>
|
||||||
|
<pattern id="dot-pattern" width={dotSpacing} height={dotSpacing} patternUnits="userSpaceOnUse">
|
||||||
|
<circle cx={1} cy={1} r={2} />
|
||||||
|
</pattern>
|
||||||
|
</defs>
|
||||||
|
<StyledRect fill="url(#dot-pattern)" />
|
||||||
|
</StyledSvg>
|
||||||
|
|
||||||
|
{meteors.map(({ id, column, startRow, endRow, duration, tailVisible, animationStage, opacity }) => {
|
||||||
|
return (
|
||||||
|
<MeteorContainer
|
||||||
|
key={id}
|
||||||
|
initial={{
|
||||||
|
x: column * dotSpacing,
|
||||||
|
y: startRow * dotSpacing,
|
||||||
|
opacity: 1,
|
||||||
|
}}
|
||||||
|
animate={{
|
||||||
|
opacity: tailVisible ? 1 : 0,
|
||||||
|
y: animationStage === 'resetting' ? startRow * dotSpacing : endRow * dotSpacing,
|
||||||
|
}}
|
||||||
|
transition={{
|
||||||
|
duration: animationStage === 'resetting' ? 0 : duration,
|
||||||
|
ease: headEasing,
|
||||||
|
}}
|
||||||
|
onAnimationComplete={() => handleAnimationComplete(id)}
|
||||||
|
>
|
||||||
|
<Tail
|
||||||
|
initial={{ top: `-${tailLength}px`, height: `${tailLength}px` }}
|
||||||
|
animate={{ top: tailVisible ? `-${tailLength}px` : 0, height: tailVisible ? `${tailLength}px` : 0 }}
|
||||||
|
transition={{
|
||||||
|
duration: tailDuration,
|
||||||
|
ease: tailEasing,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</MeteorContainer>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default WebCheckHomeBackground;
|
Loading…
Reference in New Issue
Block a user