533 lines
16 KiB
HTML
533 lines
16 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||
<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 href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;600&display=swap" rel="stylesheet" />
|
||
|
||
<style>
|
||
/* --- RESET & BASE --- */
|
||
html { scroll-behavior: smooth; }
|
||
body {
|
||
margin: 0;
|
||
background-color: #0a0a0a;
|
||
color: #e5e5e5;
|
||
font-family: 'Space Grotesk', sans-serif;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* --- HEADER --- */
|
||
header {
|
||
backdrop-filter: blur(10px);
|
||
-webkit-backdrop-filter: blur(10px);
|
||
background-color: rgba(10, 10, 10, 0.85);
|
||
padding: 0.5em;
|
||
text-align: center;
|
||
position: sticky;
|
||
top: 0;
|
||
z-index: 1000;
|
||
border-bottom: 1px solid rgba(255,255,255,0.0);
|
||
}
|
||
|
||
header h1 {
|
||
margin: 0;
|
||
display: inline-block;
|
||
transition: letter-spacing 0.5s ease-out;
|
||
}
|
||
|
||
header h1 a {
|
||
text-decoration: none;
|
||
color: #fff;
|
||
font-size: 2rem;
|
||
font-weight: 600;
|
||
display: block;
|
||
}
|
||
|
||
header h1:hover {
|
||
letter-spacing: 0.5em;
|
||
transition: letter-spacing 2s ease;
|
||
}
|
||
|
||
header p {
|
||
font-size: 0.9rem;
|
||
color: #888;
|
||
margin-top: 0.3em;
|
||
letter-spacing: 0.05em;
|
||
}
|
||
|
||
/* --- TIMELINE CONTAINER --- */
|
||
main {
|
||
min-height: 80vh;
|
||
}
|
||
|
||
.timeline-container {
|
||
position: relative;
|
||
max-width: 1920px;
|
||
margin: 4em auto;
|
||
padding: 0 20px; /* Reduced padding for mobile safety */
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.timeline-container {
|
||
padding: 0 250px;
|
||
}
|
||
}
|
||
|
||
.timeline-line {
|
||
position: absolute;
|
||
left: 20px; /* Mobile position */
|
||
top: 0;
|
||
bottom: 0;
|
||
width: 1px;
|
||
background-color: #404040;
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.timeline-line {
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
}
|
||
}
|
||
|
||
/* --- YEAR SECTIONS --- */
|
||
.year-section {
|
||
margin-bottom: 3em;
|
||
position: relative;
|
||
}
|
||
|
||
.year-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center; /* Center on desktop */
|
||
margin-bottom: 3em;
|
||
position: relative;
|
||
}
|
||
|
||
.year-header::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 0;
|
||
width: 100%;
|
||
height: 1px;
|
||
background: linear-gradient(to right, transparent, #404040, transparent);
|
||
}
|
||
|
||
.year-header h2 {
|
||
position: relative;
|
||
background-color: #0a0a0a;
|
||
color: #fff;
|
||
font-size: 1.8em;
|
||
font-weight: 600;
|
||
padding: 0 1em;
|
||
z-index: 1;
|
||
margin: 0;
|
||
}
|
||
|
||
/* --- PROJECT ITEMS --- */
|
||
.project {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 3em;
|
||
opacity: 0;
|
||
transform: translateY(30px);
|
||
transition: opacity 0.6s ease-out, transform 0.6s ease-out;
|
||
}
|
||
|
||
.project.visible {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
|
||
.project .card {
|
||
background-color: #1a1a1a;
|
||
padding: 1.25em;
|
||
border: 0px solid rgba(255,255,255,0.0);
|
||
transition: background-color 0.3s, transform 0.2s;
|
||
width: fit-content;
|
||
max-width: 280px;
|
||
cursor: pointer;
|
||
position: relative;
|
||
margin-left: 40px; /* Space for line on mobile */
|
||
}
|
||
|
||
.project .card:hover {
|
||
background-color: #2a2a2a;
|
||
transform: scale(1.01);
|
||
border: 0px solid rgba(0,0,0,0);
|
||
border-color: rgba(255,255,255,0.0);
|
||
}
|
||
|
||
.project .card h3 {
|
||
color: #fff;
|
||
margin: 0 0 0.3em 0;
|
||
font-size: 1.1em;
|
||
}
|
||
|
||
.project .card p {
|
||
color: #999;
|
||
font-size: 0.9em;
|
||
margin: 0;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
/* Desktop Alternating Layout */
|
||
@media (min-width: 900px) {
|
||
.project .card {
|
||
margin: 0; /* Reset mobile margin */
|
||
}
|
||
|
||
.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 {
|
||
position: absolute;
|
||
left: 16px; /* Mobile position (line is at 20px, dot 8px width) */
|
||
top: 1.5em; /* Align with card top approximately */
|
||
width: 8px;
|
||
height: 8px;
|
||
background: #fff;
|
||
border: 4px solid #0a0a0a;
|
||
border-radius: 50%;
|
||
z-index: 10;
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.dot {
|
||
left: 50%;
|
||
top: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
}
|
||
|
||
/* --- HOVER PREVIEW (Desktop Only) --- */
|
||
.preview {
|
||
display: none; /* Hidden by default (mobile) */
|
||
position: absolute;
|
||
top: 50%;
|
||
width: 300px;
|
||
transform: translateY(-50%) scale(0.95);
|
||
opacity: 0;
|
||
transition: all 0.3s ease;
|
||
box-shadow: 0 25px 30px rgba(0, 0, 0, 0.5);
|
||
pointer-events: none;
|
||
background: #000;
|
||
z-index: 99;
|
||
border: 0px solid #333;
|
||
}
|
||
|
||
@media (min-width: 900px) {
|
||
.preview { display: block; }
|
||
|
||
.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 img {
|
||
width: 100%;
|
||
height: auto;
|
||
display: block;
|
||
}
|
||
|
||
/* --- FOOTER & SOCIALS --- */
|
||
footer {
|
||
text-align: center;
|
||
padding: 3em 1em 4em;
|
||
background: #111;
|
||
color: #555;
|
||
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 {
|
||
position: fixed;
|
||
bottom: 20px;
|
||
right: 20px;
|
||
background-color: rgba(255,255,255,0.1);
|
||
color: #fff;
|
||
font-size: 1.2rem;
|
||
width: 50px;
|
||
height: 50px;
|
||
border: none;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
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;
|
||
}
|
||
|
||
#scrollToTopBtn.show {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
pointer-events: auto;
|
||
}
|
||
|
||
#scrollToTopBtn:hover {
|
||
background-color: rgba(255,255,255,0.3);
|
||
}
|
||
</style>
|
||
</head>
|
||
|
||
<body>
|
||
<header>
|
||
<h1><a href="index.html">flat space</a></h1>
|
||
<p>a small space in a huge universe</p>
|
||
</header>
|
||
|
||
<main>
|
||
<div class="timeline-container">
|
||
<div class="timeline-line"></div>
|
||
<div id="timeline-content"></div>
|
||
<div id="loading" style="text-align: center; color: #555; margin-top: 2em;">loading history...</div>
|
||
</div>
|
||
</main>
|
||
|
||
<footer>
|
||
<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>
|
||
|
||
<button id="scrollToTopBtn" title="Up">⇧</button>
|
||
|
||
<script>
|
||
// === TIMELINE GENERATION ===
|
||
function generateTimeline(data) {
|
||
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();
|
||
|
||
years.forEach(year => {
|
||
const yearSection = document.createElement('div');
|
||
yearSection.className = 'year-section';
|
||
yearSection.id = `year-${year}`;
|
||
|
||
const yearHeader = document.createElement('div');
|
||
yearHeader.className = 'year-header';
|
||
yearHeader.innerHTML = `<h2>${year}</h2>`;
|
||
yearSection.appendChild(yearHeader);
|
||
|
||
data[year].forEach(project => {
|
||
const projectDiv = document.createElement('div');
|
||
projectDiv.className = `project ${project.side}`;
|
||
|
||
const dot = document.createElement('div');
|
||
dot.className = 'dot';
|
||
projectDiv.appendChild(dot);
|
||
|
||
const card = document.createElement('div');
|
||
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');
|
||
title.textContent = project.title;
|
||
card.appendChild(title);
|
||
|
||
const desc = document.createElement('p');
|
||
desc.textContent = project.description;
|
||
card.appendChild(desc);
|
||
|
||
if (project.preview) {
|
||
const preview = document.createElement('div');
|
||
preview.className = 'preview';
|
||
|
||
if (project.preview.type === 'video') {
|
||
const video = document.createElement('video');
|
||
video.muted = true;
|
||
video.loop = true;
|
||
video.playsInline = true; // Important for mobile
|
||
|
||
const source = document.createElement('source');
|
||
source.src = project.preview.url;
|
||
source.type = 'video/mp4';
|
||
|
||
video.appendChild(source);
|
||
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') {
|
||
const img = document.createElement('img');
|
||
img.src = project.preview.url;
|
||
img.alt = project.title;
|
||
img.loading = "lazy";
|
||
preview.appendChild(img);
|
||
}
|
||
|
||
card.appendChild(preview);
|
||
}
|
||
|
||
projectDiv.appendChild(card);
|
||
yearSection.appendChild(projectDiv);
|
||
});
|
||
|
||
container.appendChild(yearSection);
|
||
});
|
||
}
|
||
|
||
// === DATA FETCHING ===
|
||
fetch('projects.json')
|
||
.then(response => {
|
||
if (!response.ok) throw new Error("JSON not found");
|
||
return response.json();
|
||
})
|
||
.then(data => {
|
||
generateTimeline(data);
|
||
initObserver(); // Only observe after DOM is built
|
||
})
|
||
.catch(error => {
|
||
console.error('Error:', error);
|
||
document.getElementById('loading').textContent = "Error loading timeline data.";
|
||
});
|
||
|
||
// === OBSERVER (Fade-in) ===
|
||
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' });
|
||
});
|
||
|
||
// === PERFORMANCE OPTIMIZED SCROLL ===
|
||
const h1 = document.querySelector("header h1");
|
||
let ticking = false;
|
||
|
||
window.addEventListener("scroll", () => {
|
||
if (!ticking) {
|
||
window.requestAnimationFrame(() => {
|
||
handleScroll();
|
||
ticking = false;
|
||
});
|
||
ticking = true;
|
||
}
|
||
});
|
||
|
||
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>
|
||
</body>
|
||
</html>
|