projects timeline
This commit is contained in:
parent
fb35c0e188
commit
c678bdfd65
2 changed files with 442 additions and 0 deletions
79
projects.json
Normal file
79
projects.json
Normal file
|
|
@ -0,0 +1,79 @@
|
||||||
|
{
|
||||||
|
"2003": [
|
||||||
|
{
|
||||||
|
"title": "First Render",
|
||||||
|
"description": "Early 3D tests with 3ds max and something else",
|
||||||
|
"side": "left",
|
||||||
|
"preview": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://placehold.co/300x200?text=First+Render"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Second Render",
|
||||||
|
"description": "Tw 3D test",
|
||||||
|
"side": "left",
|
||||||
|
"preview": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://placehold.co/300x200?text=First+Render"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Tree Render",
|
||||||
|
"description": "Bla 3D test",
|
||||||
|
"side": "right",
|
||||||
|
"preview": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://placehold.co/300x200?text=First+Render"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2010": [
|
||||||
|
{
|
||||||
|
"title": "Commercial VFX",
|
||||||
|
"description": "First studio project",
|
||||||
|
"side": "left",
|
||||||
|
"preview": {
|
||||||
|
"type": "video",
|
||||||
|
"url": "https://filesamples.com/samples/video/mp4/sample_640x360.mp4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Title Sequence",
|
||||||
|
"description": "Motion design exploration",
|
||||||
|
"side": "right"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2020": [
|
||||||
|
{
|
||||||
|
"title": "Realtime Sim",
|
||||||
|
"description": "Procedural FX",
|
||||||
|
"side": "left",
|
||||||
|
"preview": {
|
||||||
|
"type": "image",
|
||||||
|
"url": "https://placehold.co/300x200?text=Realtime+Sim"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Ocean Test",
|
||||||
|
"description": "Water surface simulation",
|
||||||
|
"side": "right"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"2025": [
|
||||||
|
{
|
||||||
|
"title": "Bevy Shooter",
|
||||||
|
"description": "Procedural game prototype",
|
||||||
|
"side": "right",
|
||||||
|
"preview": {
|
||||||
|
"type": "video",
|
||||||
|
"url": "https://filesamples.com/samples/video/mp4/sample_960x400_ocean.mp4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Shader R&D",
|
||||||
|
"description": "Raytraced lighting test",
|
||||||
|
"side": "left"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
363
timeline.html
Normal file
363
timeline.html
Normal file
|
|
@ -0,0 +1,363 @@
|
||||||
|
<!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>
|
||||||
|
<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>
|
||||||
|
html { scroll-behavior: smooth; }
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color: #e5e5e5;
|
||||||
|
font-family: 'Space Grotesk', sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
-webkit-backdrop-filter: blur(10px);
|
||||||
|
background-color: rgba(10, 10, 10, 0.85);
|
||||||
|
padding: 1em;
|
||||||
|
text-align: center;
|
||||||
|
position: sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
header h1 {
|
||||||
|
display: inline-block;
|
||||||
|
font-size: 2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #fff;
|
||||||
|
margin: 0;
|
||||||
|
transition: letter-spacing 1s ease;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
header p {
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #888;
|
||||||
|
margin-top: 0.3em;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-container {
|
||||||
|
position: relative;
|
||||||
|
max-width: 1000px;
|
||||||
|
margin: 4em auto;
|
||||||
|
padding: 0 350px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.timeline-line {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 2px;
|
||||||
|
background-color: #404040;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-section {
|
||||||
|
margin-bottom: 6em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-header::before {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 100%;
|
||||||
|
height: 2px;
|
||||||
|
background-color: #404040;
|
||||||
|
}
|
||||||
|
|
||||||
|
.year-header h2 {
|
||||||
|
position: relative;
|
||||||
|
background-color: #0a0a0a;
|
||||||
|
color: #fff;
|
||||||
|
font-size: 1.8em;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 0 1em;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 3em;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px);
|
||||||
|
transition: opacity 0.8s ease-out, transform 0.8s ease-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.visible {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .card {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
padding: 1.5em;
|
||||||
|
border-radius: 0;
|
||||||
|
transition: background-color 0.3s, transform 0.2s;
|
||||||
|
max-width: 280px;
|
||||||
|
cursor: pointer;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project .card:hover {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
transform: scale(1.02);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.left .card {
|
||||||
|
margin-right: calc(50% + 2rem);
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.right .card {
|
||||||
|
margin-left: calc(50% + 2rem);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
.dot {
|
||||||
|
position: absolute;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
background: #fff;
|
||||||
|
border: 4px solid #0a0a0a;
|
||||||
|
border-radius: 50%;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
width: 300px;
|
||||||
|
transform: translateY(-50%) scale(0.98);
|
||||||
|
opacity: 0;
|
||||||
|
transition: all 0.4s ease;
|
||||||
|
border-radius: 0;
|
||||||
|
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4);
|
||||||
|
pointer-events: none;
|
||||||
|
background: #000;
|
||||||
|
z-index: 99;
|
||||||
|
}
|
||||||
|
|
||||||
|
.card:hover .preview {
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateY(-50%) scale(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview video,
|
||||||
|
.preview img {
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 0;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.left .preview {
|
||||||
|
right: calc(100% + 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.project.right .preview {
|
||||||
|
left: calc(100% + 1.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
text-align: center;
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #111;
|
||||||
|
color: #777;
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
#scrollToTopBtn {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
background-color: #111;
|
||||||
|
color: #666;
|
||||||
|
font-size: 1.4em;
|
||||||
|
padding: 0.4em 0.8em;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.4s;
|
||||||
|
z-index: 999;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.timeline-line { left: 8px; }
|
||||||
|
.project.left .card, .project.right .card { margin: 0 0 0 2em; }
|
||||||
|
.dot { left: 0; transform: none; }
|
||||||
|
.preview { display: none; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<header>
|
||||||
|
<h1 onclick="window.location.href='index.html'">flat space</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>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer>
|
||||||
|
<p>© 2025 Andrey Flat</p>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<button id="scrollToTopBtn" title="Rewind">⇧</button>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// === GENERATE TIMELINE ===
|
||||||
|
function generateTimeline(data) {
|
||||||
|
const container = document.getElementById('timeline-content');
|
||||||
|
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}`;
|
||||||
|
|
||||||
|
console.log(`Project: ${project.title}, side: ${project.side}, has preview:`, !!project.preview);
|
||||||
|
|
||||||
|
const dot = document.createElement('div');
|
||||||
|
dot.className = 'dot';
|
||||||
|
projectDiv.appendChild(dot);
|
||||||
|
|
||||||
|
const card = document.createElement('div');
|
||||||
|
card.className = 'card';
|
||||||
|
|
||||||
|
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.autoplay = true;
|
||||||
|
video.muted = true;
|
||||||
|
video.loop = true;
|
||||||
|
video.playsInline = true;
|
||||||
|
const source = document.createElement('source');
|
||||||
|
source.src = project.preview.url;
|
||||||
|
source.type = 'video/mp4';
|
||||||
|
video.appendChild(source);
|
||||||
|
preview.appendChild(video);
|
||||||
|
} else if (project.preview.type === 'image') {
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.src = project.preview.url;
|
||||||
|
img.alt = project.title;
|
||||||
|
preview.appendChild(img);
|
||||||
|
}
|
||||||
|
|
||||||
|
card.appendChild(preview);
|
||||||
|
}
|
||||||
|
|
||||||
|
projectDiv.appendChild(card);
|
||||||
|
yearSection.appendChild(projectDiv);
|
||||||
|
});
|
||||||
|
|
||||||
|
container.appendChild(yearSection);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Initialize
|
||||||
|
fetch('projects.json')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
generateTimeline(data);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
|
||||||
|
// Scroll to top
|
||||||
|
document.getElementById('scrollToTopBtn').addEventListener('click', () => {
|
||||||
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Header animation
|
||||||
|
const h1 = document.querySelector("header h1");
|
||||||
|
h1.addEventListener("mouseenter", () => {
|
||||||
|
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", () => {
|
||||||
|
const baseMaxSpacing = 0.0012;
|
||||||
|
const maxScroll = 3000;
|
||||||
|
const scrollY = window.scrollY;
|
||||||
|
const dynamicMaxSpacing = window.innerWidth * baseMaxSpacing;
|
||||||
|
const spacing = Math.min(scrollY / maxScroll * dynamicMaxSpacing, dynamicMaxSpacing);
|
||||||
|
if (!h1.matches(':hover')) {
|
||||||
|
h1.style.letterSpacing = spacing + "em";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Loading…
Reference in a new issue