feat(timeline): enhance layout and animations
This commit is contained in:
parent
b6a68b6e0d
commit
d13cc94a2c
1 changed files with 265 additions and 101 deletions
362
timeline.html
362
timeline.html
|
|
@ -4,37 +4,52 @@
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Andrey Flat – Timeline</title>
|
<title>Andrey Flat – Timeline</title>
|
||||||
|
<meta name="description" content="A chronological journey through the works and projects of Andrey Flat." />
|
||||||
|
|
||||||
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
<link rel="icon" href="favicon.ico" type="image/x-icon" />
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet" />
|
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet" />
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
/* --- RESET & BASE --- */
|
||||||
html { scroll-behavior: smooth; }
|
html { scroll-behavior: smooth; }
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
background-color: #0a0a0a;
|
background-color: #0a0a0a;
|
||||||
color: #e5e5e5;
|
color: #e5e5e5;
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- HEADER --- */
|
||||||
header {
|
header {
|
||||||
backdrop-filter: blur(10px);
|
backdrop-filter: blur(10px);
|
||||||
-webkit-backdrop-filter: blur(10px);
|
-webkit-backdrop-filter: blur(10px);
|
||||||
background-color: rgba(10, 10, 10, 0.85);
|
background-color: rgba(10, 10, 10, 0.85);
|
||||||
padding: 1em;
|
padding: 0.5em;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
|
border-bottom: 1px solid rgba(255,255,255,0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
header h1 {
|
header h1 {
|
||||||
|
margin: 0;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
transition: letter-spacing 0.5s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: #fff;
|
||||||
font-size: 2rem;
|
font-size: 2rem;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: #fff;
|
display: block;
|
||||||
margin: 0;
|
}
|
||||||
transition: letter-spacing 1s ease;
|
|
||||||
cursor: pointer;
|
header h1:hover {
|
||||||
|
letter-spacing: 0.5em;
|
||||||
|
transition: letter-spacing 2s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
header p {
|
header p {
|
||||||
|
|
@ -44,23 +59,41 @@
|
||||||
letter-spacing: 0.05em;
|
letter-spacing: 0.05em;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- TIMELINE CONTAINER --- */
|
||||||
|
main {
|
||||||
|
min-height: 80vh;
|
||||||
|
}
|
||||||
|
|
||||||
.timeline-container {
|
.timeline-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
max-width: 1920px;
|
max-width: 1920px;
|
||||||
margin: 4em auto;
|
margin: 4em auto;
|
||||||
padding: 0 250px;
|
padding: 0 20px; /* Reduced padding for mobile safety */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.timeline-container {
|
||||||
|
padding: 0 250px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.timeline-line {
|
.timeline-line {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 20px; /* Mobile position */
|
||||||
top: 0;
|
top: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
background-color: #404040;
|
background-color: #404040;
|
||||||
transform: translateX(-50%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.timeline-line {
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- YEAR SECTIONS --- */
|
||||||
.year-section {
|
.year-section {
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
@ -69,7 +102,7 @@
|
||||||
.year-header {
|
.year-header {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center; /* Center on desktop */
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
@ -81,12 +114,7 @@
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 1px;
|
height: 1px;
|
||||||
background: linear-gradient(
|
background: linear-gradient(to right, transparent, #404040, transparent);
|
||||||
to right,
|
|
||||||
transparent,
|
|
||||||
#404040,
|
|
||||||
transparent
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.year-header h2 {
|
.year-header h2 {
|
||||||
|
|
@ -97,16 +125,18 @@
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- PROJECT ITEMS --- */
|
||||||
.project {
|
.project {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin-bottom: 3em;
|
margin-bottom: 3em;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(50px);
|
transform: translateY(30px);
|
||||||
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
|
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project.visible {
|
.project.visible {
|
||||||
|
|
@ -117,16 +147,20 @@
|
||||||
.project .card {
|
.project .card {
|
||||||
background-color: #1a1a1a;
|
background-color: #1a1a1a;
|
||||||
padding: 1.25em;
|
padding: 1.25em;
|
||||||
border-radius: 0;
|
border: 0px solid rgba(255,255,255,0.0);
|
||||||
transition: background-color 0.3s, transform 0.2s;
|
transition: background-color 0.3s, transform 0.2s;
|
||||||
|
width: fit-content;
|
||||||
max-width: 280px;
|
max-width: 280px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin-left: 40px; /* Space for line on mobile */
|
||||||
}
|
}
|
||||||
|
|
||||||
.project .card:hover {
|
.project .card:hover {
|
||||||
background-color: #2a2a2a;
|
background-color: #2a2a2a;
|
||||||
transform: scale(1.02);
|
transform: scale(1.01);
|
||||||
|
border: 0px solid rgba(0,0,0,0);
|
||||||
|
border-color: rgba(255,255,255,0.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
.project .card h3 {
|
.project .card h3 {
|
||||||
|
|
@ -139,22 +173,41 @@
|
||||||
color: #999;
|
color: #999;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
line-height: 1.5;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project.left .card {
|
/* Desktop Alternating Layout */
|
||||||
margin-right: calc(50% + 2rem);
|
@media (min-width: 900px) {
|
||||||
margin-left: auto;
|
.project .card {
|
||||||
}
|
margin: 0; /* Reset mobile margin */
|
||||||
|
}
|
||||||
.project.right .card {
|
|
||||||
margin-left: calc(50% + 2rem);
|
.project.left {
|
||||||
|
justify-content: flex-end;
|
||||||
|
padding-right: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.right {
|
||||||
|
justify-content: flex-start;
|
||||||
|
padding-left: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.left .card {
|
||||||
|
margin-right: 2rem;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.right .card {
|
||||||
|
margin-left: 2rem;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- DOT --- */
|
||||||
.dot {
|
.dot {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 50%;
|
left: 16px; /* Mobile position (line is at 20px, dot 8px width) */
|
||||||
transform: translateX(-50%);
|
top: 1.5em; /* Align with card top approximately */
|
||||||
width: 8px;
|
width: 8px;
|
||||||
height: 8px;
|
height: 8px;
|
||||||
background: #fff;
|
background: #fff;
|
||||||
|
|
@ -163,75 +216,122 @@
|
||||||
z-index: 10;
|
z-index: 10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@media (min-width: 900px) {
|
||||||
|
.dot {
|
||||||
|
left: 50%;
|
||||||
|
top: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* --- HOVER PREVIEW (Desktop Only) --- */
|
||||||
.preview {
|
.preview {
|
||||||
|
display: none; /* Hidden by default (mobile) */
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
width: 300px;
|
width: 300px;
|
||||||
transform: translateY(-50%) scale(0.98);
|
transform: translateY(-50%) scale(0.95);
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: all 0.4s ease;
|
transition: all 0.3s ease;
|
||||||
border-radius: 0;
|
box-shadow: 0 25px 30px rgba(0, 0, 0, 0.5);
|
||||||
box-shadow: 0 25px 25px rgba(0, 0, 0, 0.25);
|
|
||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
background: #000;
|
background: #000;
|
||||||
z-index: 99;
|
z-index: 99;
|
||||||
|
border: 0px solid #333;
|
||||||
}
|
}
|
||||||
|
|
||||||
.card:hover .preview {
|
@media (min-width: 900px) {
|
||||||
opacity: 1;
|
.preview { display: block; }
|
||||||
transform: translateY(-50%) scale(1);
|
|
||||||
|
.card:hover .preview {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.left .preview {
|
||||||
|
right: calc(100% + 2rem); /* Show to the left of the card */
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.right .preview {
|
||||||
|
left: calc(100% + 2rem); /* Show to the right of the card */
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.preview video,
|
.preview video,
|
||||||
.preview img {
|
.preview img {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: auto;
|
height: auto;
|
||||||
border-radius: 0;
|
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
.project.left .preview {
|
/* --- FOOTER & SOCIALS --- */
|
||||||
right: calc(100% + 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
.project.right .preview {
|
|
||||||
left: calc(100% + 1.5rem);
|
|
||||||
}
|
|
||||||
|
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 1em;
|
padding: 3em 1em 4em;
|
||||||
background-color: #111;
|
background: #111;
|
||||||
color: #777;
|
color: #555;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
margin-top: 4em;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-links {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon {
|
||||||
|
display: block;
|
||||||
|
width: 24px;
|
||||||
|
height: 24px;
|
||||||
|
fill: #555;
|
||||||
|
transition: fill 0.3s ease, transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.social-icon:hover {
|
||||||
|
fill: #fff;
|
||||||
|
transform: translateY(-3px);
|
||||||
}
|
}
|
||||||
|
|
||||||
#scrollToTopBtn {
|
#scrollToTopBtn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 20px;
|
bottom: 20px;
|
||||||
right: 20px;
|
right: 20px;
|
||||||
background-color: #111;
|
background-color: rgba(255,255,255,0.1);
|
||||||
color: #666;
|
color: #fff;
|
||||||
font-size: 1.4em;
|
font-size: 1.2rem;
|
||||||
padding: 0.4em 0.8em;
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
border: none;
|
border: none;
|
||||||
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
transition: opacity 0.4s;
|
opacity: 0;
|
||||||
|
transform: translateY(20px);
|
||||||
|
transition: opacity 0.4s ease, transform 0.4s ease, background 0.3s;
|
||||||
|
pointer-events: none;
|
||||||
|
backdrop-filter: blur(5px);
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
#scrollToTopBtn.show {
|
||||||
.timeline-line { left: 8px; }
|
opacity: 1;
|
||||||
.project.left .card, .project.right .card { margin: 0 0 0 2em; }
|
transform: translateY(0);
|
||||||
.dot { left: 0; transform: none; }
|
pointer-events: auto;
|
||||||
.preview { display: none; }
|
}
|
||||||
|
|
||||||
|
#scrollToTopBtn:hover {
|
||||||
|
background-color: rgba(255,255,255,0.3);
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
<header>
|
<header>
|
||||||
<h1 onclick="window.location.href='index.html'">flat space</h1>
|
<h1><a href="index.html">flat space</a></h1>
|
||||||
<p>a small space in a huge universe</p>
|
<p>a small space in a huge universe</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
|
|
@ -239,19 +339,36 @@
|
||||||
<div class="timeline-container">
|
<div class="timeline-container">
|
||||||
<div class="timeline-line"></div>
|
<div class="timeline-line"></div>
|
||||||
<div id="timeline-content"></div>
|
<div id="timeline-content"></div>
|
||||||
|
<div id="loading" style="text-align: center; color: #555; margin-top: 2em;">loading history...</div>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<p>© 2025 Andrey Flat</p>
|
<div class="social-links">
|
||||||
|
<a href="https://www.behance.net/andrey_flat" target="_blank" aria-label="Behance">
|
||||||
|
<svg class="social-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M22 7h-5v2h5V7zm0 10h-5v2h5v-2zm-5-6h5v2h-5v-2zM4.02 12.06c.6.09 1.13.3 1.56.62.43.32.74.74.93 1.25.19.51.29 1.1.29 1.77 0 .84-.15 1.57-.45 2.19-.3.62-.73 1.11-1.29 1.48-.56.37-1.24.56-2.04.56H.5v-8h3.52zM3.48 9.32h-3v-5h3c.72 0 1.32.17 1.79.51.48.34.72.82.72 1.44 0 .39-.1.74-.31 1.05-.2.31-.5.56-.88.74.52.19.92.5 1.2.94.28.44.42.98.42 1.62h2.24c0-1.07-.26-1.96-.79-2.69-.53-.73-1.26-1.1-2.19-1.12.72-.25 1.27-.63 1.64-1.13.37-.5.56-1.12.56-1.87 0-.96-.28-1.74-.83-2.32C6.49 1.11 5.71.82 4.7.82H.5v8.5h2.98zM3.1 2.89h1.34c.48 0 .84.1.98.31.2.29.3.62.3.99 0 .43-.11.78-.34 1.04-.23.26-.62.39-1.17.39H3.1V2.89zm.1 9.87v3.29h1.12c.56 0 .98-.16 1.26-.47.28-.31.42-.74.42-1.29 0-.48-.13-.88-.38-1.18-.25-.3-.64-.45-1.17-.45H3.2z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="https://www.instagram.com/andrey.flat" target="_blank" aria-label="Instagram">
|
||||||
|
<svg class="social-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M12 2.163c3.204 0 3.584.012 4.85.07 3.252.148 4.771 1.691 4.919 4.919.058 1.265.069 1.645.069 4.849 0 3.205-.012 3.584-.069 4.849-.149 3.225-1.664 4.771-4.919 4.919-1.266.058-1.644.069-4.85.069-3.204 0-3.584-.011-4.849-.069-3.26-.149-4.771-1.699-4.919-4.92-.058-1.265-.07-1.644-.07-4.849 0-3.204.013-3.583.07-4.849.149-3.227 1.664-4.771 4.919-4.919 1.266-.057 1.645-.069 4.849-.069zm0-2.163c-3.259 0-3.667.014-4.947.072-4.358.2-6.78 2.618-6.98 6.98-.059 1.281-.073 1.689-.073 4.948 0 3.259.014 3.668.072 4.948.2 4.358 2.618 6.78 6.98 6.98 1.281.058 1.689.072 4.948.072 3.259 0 3.668-.014 4.948-.072 4.354-.2 6.782-2.618 6.979-6.98.059-1.28.073-1.689.073-4.948 0-3.259-.014-3.667-.072-4.947-.196-4.354-2.617-6.78-6.979-6.98-1.281-.059-1.69-.073-4.949-.073zm0 5.838c-3.403 0-6.162 2.759-6.162 6.162s2.759 6.163 6.162 6.163 6.162-2.759 6.162-6.163c0-3.403-2.759-6.162-6.162-6.162zm0 10.162c-2.209 0-4-1.79-4-4 0-2.209 1.791-4 4-4s4 1.791 4 4c0 2.21-1.791 4-4 4zm6.406-11.845c-.796 0-1.441.645-1.441 1.44s.645 1.44 1.441 1.44c.795 0 1.439-.645 1.439-1.44s-.644-1.44-1.439-1.44z"/></svg>
|
||||||
|
</a>
|
||||||
|
<a href="mailto:andrey.flat.post@gmail.com" aria-label="Email">
|
||||||
|
<svg class="social-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z"/></svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>© <span id="year"></span> Andrey Flat</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<button id="scrollToTopBtn" title="Rewind">⇧</button>
|
<button id="scrollToTopBtn" title="Up">⇧</button>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
// === GENERATE TIMELINE ===
|
// === TIMELINE GENERATION ===
|
||||||
function generateTimeline(data) {
|
function generateTimeline(data) {
|
||||||
const container = document.getElementById('timeline-content');
|
const container = document.getElementById('timeline-content');
|
||||||
|
document.getElementById('loading').style.display = 'none';
|
||||||
|
|
||||||
|
// Sort years: Ascending (History mode).
|
||||||
|
// If you want newest first, change to: Object.keys(data).sort((a,b) => b - a)
|
||||||
const years = Object.keys(data).sort();
|
const years = Object.keys(data).sort();
|
||||||
|
|
||||||
years.forEach(year => {
|
years.forEach(year => {
|
||||||
|
|
@ -268,14 +385,14 @@
|
||||||
const projectDiv = document.createElement('div');
|
const projectDiv = document.createElement('div');
|
||||||
projectDiv.className = `project ${project.side}`;
|
projectDiv.className = `project ${project.side}`;
|
||||||
|
|
||||||
console.log(`Project: ${project.title}, side: ${project.side}, has preview:`, !!project.preview);
|
|
||||||
|
|
||||||
const dot = document.createElement('div');
|
const dot = document.createElement('div');
|
||||||
dot.className = 'dot';
|
dot.className = 'dot';
|
||||||
projectDiv.appendChild(dot);
|
projectDiv.appendChild(dot);
|
||||||
|
|
||||||
const card = document.createElement('div');
|
const card = document.createElement('div');
|
||||||
card.className = 'card';
|
card.className = 'card';
|
||||||
|
// Make card clickable if there is a link in your JSON?
|
||||||
|
// Currently assuming no link in JSON per original code.
|
||||||
|
|
||||||
const title = document.createElement('h3');
|
const title = document.createElement('h3');
|
||||||
title.textContent = project.title;
|
title.textContent = project.title;
|
||||||
|
|
@ -291,19 +408,26 @@
|
||||||
|
|
||||||
if (project.preview.type === 'video') {
|
if (project.preview.type === 'video') {
|
||||||
const video = document.createElement('video');
|
const video = document.createElement('video');
|
||||||
video.autoplay = true;
|
|
||||||
video.muted = true;
|
video.muted = true;
|
||||||
video.loop = true;
|
video.loop = true;
|
||||||
video.playsInline = true;
|
video.playsInline = true; // Important for mobile
|
||||||
|
|
||||||
const source = document.createElement('source');
|
const source = document.createElement('source');
|
||||||
source.src = project.preview.url;
|
source.src = project.preview.url;
|
||||||
source.type = 'video/mp4';
|
source.type = 'video/mp4';
|
||||||
|
|
||||||
video.appendChild(source);
|
video.appendChild(source);
|
||||||
preview.appendChild(video);
|
preview.appendChild(video);
|
||||||
|
|
||||||
|
// Play video only on hover to save resources
|
||||||
|
card.addEventListener('mouseenter', () => video.play().catch(()=>{}));
|
||||||
|
card.addEventListener('mouseleave', () => video.pause());
|
||||||
|
|
||||||
} else if (project.preview.type === 'image') {
|
} else if (project.preview.type === 'image') {
|
||||||
const img = document.createElement('img');
|
const img = document.createElement('img');
|
||||||
img.src = project.preview.url;
|
img.src = project.preview.url;
|
||||||
img.alt = project.title;
|
img.alt = project.title;
|
||||||
|
img.loading = "lazy";
|
||||||
preview.appendChild(img);
|
preview.appendChild(img);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -318,52 +442,92 @@
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initialize
|
// === DATA FETCHING ===
|
||||||
fetch('projects.json')
|
fetch('projects.json')
|
||||||
.then(response => response.json())
|
.then(response => {
|
||||||
|
if (!response.ok) throw new Error("JSON not found");
|
||||||
|
return response.json();
|
||||||
|
})
|
||||||
.then(data => {
|
.then(data => {
|
||||||
generateTimeline(data);
|
generateTimeline(data);
|
||||||
|
initObserver(); // Only observe after DOM is built
|
||||||
// Fade-in animation (needs to run after timeline is generated)
|
|
||||||
const projects = document.querySelectorAll('.project');
|
|
||||||
const observer = new IntersectionObserver(entries => {
|
|
||||||
entries.forEach(entry => {
|
|
||||||
if (entry.isIntersecting) entry.target.classList.add('visible');
|
|
||||||
});
|
|
||||||
}, { threshold: 0.15 });
|
|
||||||
projects.forEach(p => observer.observe(p));
|
|
||||||
})
|
})
|
||||||
.catch(error => console.error('Error loading projects:', error));
|
.catch(error => {
|
||||||
|
console.error('Error:', error);
|
||||||
|
document.getElementById('loading').textContent = "Error loading timeline data.";
|
||||||
|
});
|
||||||
|
|
||||||
// Scroll to top
|
// === OBSERVER (Fade-in) ===
|
||||||
document.getElementById('scrollToTopBtn').addEventListener('click', () => {
|
function initObserver() {
|
||||||
|
const projects = document.querySelectorAll('.project');
|
||||||
|
const observer = new IntersectionObserver(entries => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (entry.isIntersecting) {
|
||||||
|
entry.target.classList.add('visible');
|
||||||
|
observer.unobserve(entry.target); // Run once per item
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}, { threshold: 0.1 });
|
||||||
|
|
||||||
|
projects.forEach(p => observer.observe(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
// === UTILS ===
|
||||||
|
// Dynamic Year
|
||||||
|
document.getElementById('year').textContent = new Date().getFullYear();
|
||||||
|
|
||||||
|
// Scroll To Top
|
||||||
|
const scrollToTopBtn = document.getElementById('scrollToTopBtn');
|
||||||
|
scrollToTopBtn.addEventListener('click', () => {
|
||||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
});
|
});
|
||||||
|
|
||||||
// Header animation
|
// === PERFORMANCE OPTIMIZED SCROLL ===
|
||||||
const h1 = document.querySelector("header h1");
|
const h1 = document.querySelector("header h1");
|
||||||
h1.addEventListener("mouseenter", () => {
|
let ticking = false;
|
||||||
const maxSpacing = 12.0;
|
|
||||||
const spacing = Math.min(window.innerWidth / 500, maxSpacing);
|
|
||||||
h1.style.letterSpacing = spacing + "em";
|
|
||||||
h1.style.transition = "letter-spacing 5s ease";
|
|
||||||
});
|
|
||||||
|
|
||||||
h1.addEventListener("mouseleave", () => {
|
|
||||||
h1.style.letterSpacing = "";
|
|
||||||
h1.style.transition = "letter-spacing 1s ease";
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener("scroll", () => {
|
window.addEventListener("scroll", () => {
|
||||||
const baseMaxSpacing = 0.0012;
|
if (!ticking) {
|
||||||
const maxScroll = 3000;
|
window.requestAnimationFrame(() => {
|
||||||
const scrollY = window.scrollY;
|
handleScroll();
|
||||||
const dynamicMaxSpacing = window.innerWidth * baseMaxSpacing;
|
ticking = false;
|
||||||
const spacing = Math.min(scrollY / maxScroll * dynamicMaxSpacing, dynamicMaxSpacing);
|
});
|
||||||
if (!h1.matches(':hover')) {
|
ticking = true;
|
||||||
h1.style.letterSpacing = spacing + "em";
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function handleScroll() {
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
|
||||||
|
// Header Animation (Only on Desktop)
|
||||||
|
if (window.innerWidth > 768 && !h1.matches(':hover')) {
|
||||||
|
const baseMaxSpacing = 0.0012;
|
||||||
|
const maxScroll = 3000;
|
||||||
|
const dynamicMaxSpacing = window.innerWidth * baseMaxSpacing;
|
||||||
|
const spacing = Math.min(scrollY / maxScroll * dynamicMaxSpacing, dynamicMaxSpacing);
|
||||||
|
h1.style.letterSpacing = spacing + "em";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show/Hide Top Button
|
||||||
|
if (scrollY > 500) {
|
||||||
|
scrollToTopBtn.classList.add("show");
|
||||||
|
} else {
|
||||||
|
scrollToTopBtn.classList.remove("show");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Header Desktop Hover
|
||||||
|
h1.addEventListener("mouseenter", () => {
|
||||||
|
if (window.innerWidth > 768) {
|
||||||
|
h1.style.letterSpacing = "0.5em";
|
||||||
|
h1.style.transition = "letter-spacing 2s ease";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
h1.addEventListener("mouseleave", () => {
|
||||||
|
h1.style.letterSpacing = "";
|
||||||
|
h1.style.transition = "letter-spacing 0.5s ease-out";
|
||||||
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue