flatspace/timeline.html

533 lines
16 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>&copy; <span id="year"></span> Andrey Flat</p>
</footer>
<button id="scrollToTopBtn" title="Up">&#8679;</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>