flatspace/index.html
2026-10-28 01:43:39 +08:00

750 lines
23 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 — Space</title>
<meta name="description" content="Personal Space of Digital Media Artist Andrey Flat. Art Direction, VFX, Motion Design." />
<meta name="keywords" content="Andrey Flat, Art Director, CG Artist, VFX, Houdini Artist, CGI , 3D Generalist, Motion Design, AI, Supervisor" />
<meta name="author" content="Andrey Flat" />
<meta property="og:type" content="website" />
<meta property="og:title" content="Andrey Flat — Digital Media Artist" />
<meta property="og:description" content="Personal Space. A curated selection of VFX and Generative Art." />
<meta property="og:image" content="https://andreyflat.space/cover.jpg" />
<meta property="og:url" content="https://andreyflat.space/" />
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:image" content="https://andreyflat.space/cover.jpg" />
<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>
/* --- CSS RESET & BASE --- */
body {
background: #000;
color: #eee;
font-family: 'Space Grotesk', sans-serif;
margin: 0;
padding: 0;
overflow-x: hidden; /* Prevent horizontal scroll */
}
/* --- HEADER --- */
header {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
background-color: rgba(10, 10, 10, 0.8);
padding: 0.5em;
text-align: center;
position: sticky;
top: 0;
z-index: 1000;
border-bottom: 0px solid rgba(255,255,255,0.05);
}
header h1 {
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-align: center;
max-width: 100vw;
text-overflow: clip;
margin: 0.2em 0;
cursor: pointer;
/* Default transition */
transition: letter-spacing 0.5s ease-out;
}
header h1:hover {
letter-spacing: 0.5em; /* Reduced excessive spacing for better UX */
transition: letter-spacing 2s ease;
}
header p {
margin: 0 0 0.5em 0;
color: #888;
font-size: 0.9rem;
}
/* --- MAIN CONTENT --- */
main {
max-width: 1280px;
margin: 1em auto;
padding: 0 1em;
padding-bottom: 100px;
}
section {
margin-bottom: 2em;
}
p {
line-height: 1.6;
}
a {
color: #fff;
text-decoration: underline;
text-underline-offset: 4px;
transition: color 0.3s;
}
a:hover {
color: #aaa;
}
/* --- VIDEO LAYOUT --- */
.video-container {
position: relative;
width: 100%;
max-width: 1280px;
margin: 0 auto 2em;
aspect-ratio: 16 / 9;
background: #111;
}
.video-container iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
border: none;
}
.video-grid {
display: grid;
/* Responsive grid: min 300px on mobile, larger on desktop */
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
gap: 0px; /* Small gap for aesthetic */
justify-content: center;
background: #000;
}
@media (max-width: 600px) {
.video-grid {
grid-template-columns: 1fr; /* 1 column on mobile */
}
}
.video-tile {
position: relative;
width: 100%;
aspect-ratio: 1 / 1;
overflow: hidden;
background: #050505;
display: block;
text-decoration: none;
}
.video-tile video {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
opacity: 0.8;
transition: opacity 0.3s ease, transform 0.5s ease;
}
.video-tile:hover video {
opacity: 1;
transform: scale(1.02);
}
/* Animation classes */
.video-tile.fade-in {
opacity: 0;
transform: translateY(30px);
transition: opacity 0.6s ease, transform 0.6s ease;
}
.video-tile.fade-in.visible {
opacity: 1;
transform: translateY(0);
}
/* --- CAPTIONS --- */
.caption {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
padding: 20px;
box-sizing: border-box;
background: linear-gradient(to top, rgba(0,0,0,0.9) 0%, rgba(0,0,0,0) 100%);
color: #fff;
opacity: 0; /* Hidden by default on desktop */
transition: opacity 0.3s ease;
pointer-events: none; /* Let clicks pass through to the link */
z-index: 2;
}
.caption h3 {
margin: 0;
font-size: 1.1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 1px;
}
.caption p {
margin: 4px 0 0;
font-size: 0.85rem;
color: #ccc;
line-height: 1.4;
}
/* Desktop Hover */
.video-tile:hover .caption {
opacity: 1;
}
/* --- MOBILE FIX: Always show caption on touch devices/small screens --- */
@media (hover: none), (max-width: 768px) {
.caption {
opacity: 1;
background: linear-gradient(to top, rgba(0,0,0,0.95) 10%, rgba(0,0,0,0.75) 75%, transparent 100%);
padding-bottom: 25px;
}
/* Reset video transform on mobile to save battery/performance */
.video-tile:hover video {
transform: none;
}
}
/* --- FOOTER & SOCIALS --- */
footer {
text-align: center;
padding: 0.5em 1em;
background-color: rgba(0, 0, 0, 0.75);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
color: #555;
font-size: 0.9em;
position: fixed;
bottom: 0;
left: 0;
width: 100%;
z-index: 1000;
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
gap: 40px;
border-top: 0px solid rgba(0,0,0,0);
}
@media (max-width: 768px) {
footer {
flex-direction: column;
gap: 0.5em;
padding: 0.8em;
}
}
.social-links {
display: flex;
align-items: center;
justify-content: center;
gap: 15px; /* Расстояние между иконками */
margin: 0;
margin-top: 0.5em;
}
footer p {
margin: 0;
line-height: 1;
}
.social-icon {
display: block;
width: 22px;
height: 22px;
fill: #555; /* Цвет иконок в покое (как текст) */
transition: fill 0.3s ease, transform 0.3s ease;
}
.social-icon:hover {
fill: #fff; /* Цвет при наведении */
transform: translateY(-3px); /* Легкое всплытие */
}
.footer-copyright {
text-align: center;
color: #555;
font-size: 0.8rem;
margin-top: 50px; /* Минимальный отступ от плитки/индикатора */
margin-bottom: 0;
padding-bottom: 10px; /* Зазор до края страницы, чтобы не прилипало к футеру */
}
main {
/* Уменьшаем этот отступ, так как копирайт теперь "съедает" часть места */
padding-bottom: 30px;
}
#scrollToTopBtn {
position: fixed;
bottom: 0px;
right: 40px;
z-index: 9999;
background-color: rgba(55,55,55,0.5);
color: #fff;
font-size: 1.2rem;
width: 40px;
height: 40px;
border: none;
border-radius: 0%;
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);
}
#scrollToTopBtn.show {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
#scrollToTopBtn:hover {
background-color: rgba(255,255,255,0.3);
}
/* Loading Spinner for infinite scroll */
#loading-indicator {
text-align: center;
padding: 20px;
color: #555;
font-size: 0.8rem;
opacity: 0;
transition: opacity 0.3s;
margin-bottom: 20px;
}
#loading-indicator.active {
opacity: 1;
}
</style>
</head>
<body>
<header>
<h1 id="mainTitle" onclick="window.scrollTo({ top: 0, behavior: 'smooth' })">
flat space
</h1>
<p>a small space in a huge universe</p>
</header>
<main>
<section style="text-align: justify; max-width: 400px; margin: 0 auto;">
<p>My name is <strong>Andrey Flat</strong>. I'm a media artist specializing in procedural systems, generative effects and simulations.</p>
</section>
<section>
<div class="video-container">
<iframe
src="https://www.youtube.com/embed/GwuMJhSXcj0"
title="Showreel 2023"
allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share"
allowfullscreen
loading="lazy">
</iframe>
</div>
</section>
<section>
<div class="video-grid" id="videoGrid"></div>
<div id="loading-indicator">loading works...</div>
<p class="footer-copyright">
&copy; <span id="year"></span> Andrey Flat
</p>
</section>
</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="M8.228 15.01c0 1.341-.453 2.146-1.503 2.146H3.34V7.848h3.146c1.013 0 1.5.58 1.5 1.545 0 .848-.34 1.353-1.015 1.63v.068c.848.2 1.257.854 1.257 1.919zm-3.235-5.613v2.106h1.094c.54 0 .807-.34.807-.98 0-.693-.247-1.126-.82-1.126H4.993zm0 3.824v2.305h1.22c.626 0 .893-.413.893-1.086 0-.766-.286-1.22-1.026-1.22h-1.087zM20.613 13.04c.054-2.585-1.545-4.324-4.104-4.324-2.58 0-4.185 1.959-4.185 4.417 0 2.593 1.626 4.385 4.385 4.385 2.14 0 3.651-1.12 3.984-2.825h-1.745c-.247.786-.96 1.246-2.186 1.246-1.393 0-2.305-.733-2.433-2.113h6.432c0-.28-.013-.533-.047-.786zm-6.284-.913c.12-1.166.86-1.92 2.173-1.92 1.246 0 1.98.754 2.066 1.92h-4.24zm.447-4.43h4.484v-1.16h-4.484v1.16z"/>
</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="https://youtube.com/@andreyflat" target="_blank" aria-label="YouTube">
<svg class="social-icon" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<path d="M23.498 6.186a3.016 3.016 0 0 0-2.122-2.136C19.505 3.545 12 3.545 12 3.545s-7.505 0-9.377.505A3.017 3.017 0 0 0 .502 6.186C0 8.07 0 12 0 12s0 3.93.502 5.814a3.016 3.016 0 0 0 2.122 2.136c1.871.505 9.376.505 9.376.505s7.505 0 9.377-.505a3.015 3.015 0 0 0 2.122-2.136C24 15.93 24 12 24 12s0-3.93-.502-5.814zM9.545 15.568V8.432L15.818 12l-6.273 3.568z"/>
</svg>
</a>
<a href="mailto:me@andreyflat.space" aria-label="e-mail">
<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>
</footer>
<button id="scrollToTopBtn" title="Up">&#8679;</button>
<script>
// --- DATA SOURCE ---
const videoSources = [
{
src: "videos/project_01.mp4",
title: "EUCARIOTE (2021)",
description: "Personal project. Consciousness of a living organism.",
link: "https://www.behance.net/gallery/100474835/Eucariote"
},
{
src: "videos/project_02.mp4",
title: "SALUKI (2023)",
description: "Music video. Large water FLIP simulation.",
link: "https://www.behance.net/gallery/181128113/Large-Water"
},
{
src: "videos/project_03.mp4",
title: "MACROPLASTIC (2024)",
description: "Personal project. Trapped by generated waste.",
link: "https://www.behance.net/gallery/214236927/MACROPLASTIC"
},
{
src: "videos/project_04.mp4",
title: "Water Drop (2020)",
description: "Personal project. Unit-M5 Empire-V tribute.",
link: "https://www.instagram.com/p/Cye_mjiSt31/"
},
{
src: "videos/project_05.mp4",
title: "Experiment №80/26 (2021)",
description: "Personal project. Iron Kettlebell in Mercury.",
link: "https://www.behance.net/gallery/130228053/Experiment-8026"
},
{
src: "videos/project_06.mp4",
title: "Waterfall (2022)",
description: "Exhibition. Led screen visual.",
link: "https://www.behance.net/gallery/146506521/WATERFALL"
},
{
src: "videos/project_07.mp4",
title: "Dry Season (2023)",
description: "Simulation and visualisation experiment.",
link: "https://www.instagram.com/p/Cy0Cg8OytWN/"
},
{
src: "videos/project_08.mp4",
title: "HiPhi Z Rollout (2022)",
description: "AR Electric car factory launch show.",
link: "https://www.behance.net/gallery/148168593/HiPhi-Z-Rollout-Show"
},
{
src: "videos/project_09.mp4",
title: "Infinix HOT 10 (2021)",
description: "Advertising video for phone.",
link: "https://www.behance.net/gallery/116302519/Infinix-Hot-X-Play"
},
{
src: "videos/project_10.mp4",
title: "2000x2036 (2021)",
description: "Personal project. Destructive behavior.",
link: "https://www.behance.net/gallery/119929513/2000x2036"
},
{
src: "videos/project_11.mp4",
title: "EX vs REX (2020)",
description: "Collaboration. What came first?",
link: "https://www.behance.net/gallery/104782579/EX-vs-REX"
},
{
src: "videos/project_12.mp4",
title: "Bloemstilleven (2021)",
description: "Confrontation between humans and AI.",
link: "https://www.behance.net/gallery/198703355/Bloemstilleven"
},
{
src: "videos/project_13.mp4",
title: "cohærens (2024)",
description: "Personal project. A state of united whole.",
link: "https://youtu.be/tZphaJ_MWXI"
},
{
src: "videos/project_14.mp4",
title: "Magic Animals (2024)",
description: "Exposition. Festival of light. Holographic screens.",
link: "https://www.behance.net/gallery/208881221/Magic-Animals"
},
{
src: "videos/project_15.mp4",
title: "ELEMENTS (2024)",
description: "LED screens. Water, emotions, intuition.",
link: "https://www.behance.net/gallery/209820423/ELEMENTS"
},
{
src: "videos/project_16.mp4",
title: "MURAQQA - Experiment (2023)",
description: "Experimental visuals.",
link: "https://youtu.be/oEwYm7WJNL0"
},
{
src: "videos/project_17.mp4",
title: "MURAQQA - Genesis (2023)",
description: "Generative art piece.",
link: "https://youtu.be/ZyfbAwPa-5Q"
},
{
src: "videos/project_18.mp4",
title: "Scene Change (2018)",
description: "Venice Biennale 2017. Motion Designer.",
link: "https://www.behance.net/gallery/202101993/Scene-change-Prologue"
},
{
src: "videos/project_19.mp4",
title: "7 AV-Performance (2021)",
description: "Art project. Scene: Prologue, Spectrum",
link: "https://www.behance.net/gallery/128538543/7-AV-Performance"
},
{
src: "videos/project_20.mp4",
title: "Pre-match show (2025)",
description: "Multimedia performance. Particles workflow.",
link: "https://www.behance.net/gallery/224229141/Winline-Match-Tour"
},
{
src: "videos/project_21.mp4",
title: "Black Hole",
description: "timeline test.",
link: "https://andreyflat.space/timeline"
},
];
// --- LOGIC ---
// 1. Dynamic Year
document.getElementById('year').textContent = new Date().getFullYear();
// 2. Improved Shuffling (Fisher-Yates)
// Replaces Math.random() - 0.5 which is biased
function shuffleArray(array) {
for (let i = array.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[array[i], array[j]] = [array[j], array[i]];
}
}
shuffleArray(videoSources);
// 3. Grid & Loading Logic
const videoGrid = document.getElementById('videoGrid');
const loadingIndicator = document.getElementById('loading-indicator');
let loadedCount = 0;
const BATCH_SIZE = 4; // Increased batch size slightly
let isLoading = false;
function loadVideos(startIndex, count) {
isLoading = true;
loadingIndicator.classList.add('active');
// Simulate small delay for UI feel or remove setTimeOut for instant
setTimeout(() => {
const limit = Math.min(startIndex + count, videoSources.length);
for (let i = startIndex; i < limit; i++) {
const { src, title, description, link } = videoSources[i];
const tile = document.createElement('a');
tile.href = link || `https://andreyflat.space/projects/${i + 1}`;
tile.target = '_blank';
tile.className = 'video-tile fade-in';
tile.rel = "noopener noreferrer"; // Security best practice
const video = document.createElement('video');
// Fallback to jpg if poster exists, otherwise browser shows black
video.poster = src.replace(/\.mp4$/, '.jpg');
video.muted = true;
video.loop = true;
video.playsInline = true; // Critical for iOS
video.dataset.src = src;
const caption = document.createElement('div');
caption.className = 'caption';
caption.innerHTML = `<h3>${title}</h3><p>${description}</p>`;
tile.appendChild(video);
tile.appendChild(caption);
videoGrid.appendChild(tile);
// Observe for animation and playback
fadeObserver.observe(tile);
videoObserver.observe(video);
// Desktop Hover Logic (Mouse)
tile.addEventListener('mouseenter', () => {
if (!video.src) {
video.src = video.dataset.src;
video.preload = 'auto';
}
video.play().catch(() => { /* Auto-play prevention handler */ });
});
tile.addEventListener('mouseleave', () => {
video.pause();
});
}
loadedCount = limit;
isLoading = false;
loadingIndicator.classList.remove('active');
if (loadedCount >= videoSources.length) {
loadingIndicator.style.display = 'none';
}
}, 300);
}
// 4. Observers
const fadeObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
entry.target.classList.add('visible');
fadeObserver.unobserve(entry.target);
}
});
}, { threshold: 0.1 });
const videoObserver = new IntersectionObserver((entries, observer) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const video = entry.target;
// Lazy load src
if (!video.src) {
video.src = video.dataset.src;
video.preload = 'auto';
}
// Mobile/Touch Autoplay logic:
// If it's a touch device, we might want to auto-play when visible
// combined with the CSS caption fix.
if (window.matchMedia("(hover: none)").matches) {
video.play().catch(()=>{});
}
observer.unobserve(video);
}
});
}, { rootMargin: '200px 0px' });
// 5. Scroll Performance (requestAnimationFrame)
const h1 = document.querySelector("header h1");
const scrollToTopBtn = document.getElementById("scrollToTopBtn");
let ticking = false;
window.addEventListener("scroll", () => {
if (!ticking) {
window.requestAnimationFrame(() => {
performScrollLogic();
ticking = false;
});
ticking = true;
}
});
function performScrollLogic() {
const scrollY = window.scrollY;
// Header Animation
if (!h1.matches(':hover') && window.innerWidth > 768) {
// Only animate letter spacing on desktop to avoid breakage on mobile
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";
}
// Scroll To Top Button
if (scrollY > 500) {
scrollToTopBtn.classList.add("show");
} else {
scrollToTopBtn.classList.remove("show");
}
// Infinite Scroll
if (!isLoading && loadedCount < videoSources.length) {
const gridBottom = videoGrid.offsetTop + videoGrid.offsetHeight;
const viewBottom = scrollY + window.innerHeight;
if (viewBottom > gridBottom - 300) {
loadVideos(loadedCount, BATCH_SIZE);
}
}
}
// Scroll Button Click
scrollToTopBtn.addEventListener("click", () => {
window.scrollTo({ top: 0, behavior: "smooth" });
});
// Init
window.addEventListener('load', () => {
loadVideos(loadedCount, BATCH_SIZE);
});
// Header interaction for desktop
h1.addEventListener("mouseenter", () => {
if (window.innerWidth > 768) {
h1.style.letterSpacing = "0.8em";
h1.style.transition = "letter-spacing 4s ease";
}
});
h1.addEventListener("mouseleave", () => {
h1.style.letterSpacing = "";
h1.style.transition = "letter-spacing 1s ease";
});
</script>
<script>
window.addEventListener("load", () => {
try {
if (sessionStorage.getItem("af_logged")) return;
sessionStorage.setItem("af_logged", "1");
// WARNING: Exposing secret keys in frontend code is risky. commet
// Anyone can see this key. Use environment variables in Netlify if possible.
const secret = "flat_secret_123";
const url = "/.netlify/functions/privateViewLogger"
+ "?key=" + encodeURIComponent(secret)
+ "&path=" + encodeURIComponent(location.pathname)
+ "&t=" + Date.now();
if (navigator.sendBeacon) {
const body = JSON.stringify({ path: location.pathname });
navigator.sendBeacon(url, body);
} else {
fetch(url, {
method: "POST",
keepalive: true,
headers: { "content-type": "application/json" },
body: JSON.stringify({ path: location.pathname })
}).catch(() => {});
}
} catch {}
});
</script>
</body>
</html>