Compare commits
No commits in common. "0e59cb390fb201225ab198453d129ad846731dea" and "2a035aa82c50f8ec5267ec2b5843635a8c497b27" have entirely different histories.
0e59cb390f
...
2a035aa82c
6 changed files with 18 additions and 418 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -1 +0,0 @@
|
||||||
node_modules/
|
|
||||||
82
index.html
82
index.html
|
|
@ -73,7 +73,6 @@
|
||||||
max-width: 1280px;
|
max-width: 1280px;
|
||||||
margin: 1em auto;
|
margin: 1em auto;
|
||||||
padding: 0 1em;
|
padding: 0 1em;
|
||||||
padding-bottom: 100px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
section {
|
section {
|
||||||
|
|
@ -117,7 +116,7 @@
|
||||||
display: grid;
|
display: grid;
|
||||||
/* Responsive grid: min 300px on mobile, larger on desktop */
|
/* Responsive grid: min 300px on mobile, larger on desktop */
|
||||||
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
|
||||||
gap: 0px; /* Small gap for aesthetic */
|
gap: 1px; /* Small gap for aesthetic */
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #000;
|
background: #000;
|
||||||
}
|
}
|
||||||
|
|
@ -204,7 +203,7 @@
|
||||||
@media (hover: none), (max-width: 768px) {
|
@media (hover: none), (max-width: 768px) {
|
||||||
.caption {
|
.caption {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
background: linear-gradient(to top, rgba(0,0,0,0.95) 10%, rgba(0,0,0,0.75) 75%, transparent 100%);
|
background: linear-gradient(to top, rgba(0,0,0,0.95) 10%, rgba(0,0,0,0.4) 60%, transparent 100%);
|
||||||
padding-bottom: 25px;
|
padding-bottom: 25px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -216,54 +215,28 @@
|
||||||
/* --- FOOTER & SOCIALS --- */
|
/* --- FOOTER & SOCIALS --- */
|
||||||
footer {
|
footer {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
padding: 0.5em 1em;
|
padding: 1em 1em 2em; /* Чуть больше отступа снизу */
|
||||||
background-color: rgba(0, 0, 0, 0.75);
|
background: #111;
|
||||||
backdrop-filter: blur(20px);
|
|
||||||
-webkit-backdrop-filter: blur(20px);
|
|
||||||
|
|
||||||
color: #555;
|
color: #555;
|
||||||
font-size: 0.9em;
|
font-size: 0.9em;
|
||||||
|
margin-top: 2em;
|
||||||
position: fixed;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 1000;
|
|
||||||
|
|
||||||
display: flex;
|
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;
|
flex-direction: column;
|
||||||
gap: 0.5em;
|
align-items: center;
|
||||||
padding: 0.8em;
|
gap: 1.5em; /* Расстояние между копирайтом и иконками */
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.social-links {
|
.social-links {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
gap: 15px; /* Расстояние между иконками */
|
gap: 20px; /* Расстояние между иконками */
|
||||||
margin: 0;
|
|
||||||
margin-top: 0.5em;
|
margin-top: 0.5em;
|
||||||
}
|
}
|
||||||
|
|
||||||
footer p {
|
|
||||||
margin: 0;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.social-icon {
|
.social-icon {
|
||||||
display: block;
|
display: block;
|
||||||
width: 22px;
|
width: 24px;
|
||||||
height: 22px;
|
height: 24px;
|
||||||
fill: #555; /* Цвет иконок в покое (как текст) */
|
fill: #555; /* Цвет иконок в покое (как текст) */
|
||||||
transition: fill 0.3s ease, transform 0.3s ease;
|
transition: fill 0.3s ease, transform 0.3s ease;
|
||||||
}
|
}
|
||||||
|
|
@ -273,32 +246,18 @@
|
||||||
transform: translateY(-3px); /* Легкое всплытие */
|
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 {
|
#scrollToTopBtn {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
bottom: 0px;
|
bottom: 20px;
|
||||||
right: 40px;
|
right: 20px;
|
||||||
z-index: 9999;
|
z-index: 9999;
|
||||||
background-color: rgba(55,55,55,0.5);
|
background-color: rgba(255,255,255,0.1);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: 1.2rem;
|
font-size: 1.2rem;
|
||||||
width: 40px;
|
width: 50px;
|
||||||
height: 40px;
|
height: 50px;
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 0%;
|
border-radius: 50%;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transform: translateY(20px);
|
transform: translateY(20px);
|
||||||
|
|
@ -325,7 +284,6 @@
|
||||||
font-size: 0.8rem;
|
font-size: 0.8rem;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.3s;
|
transition: opacity 0.3s;
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
}
|
||||||
#loading-indicator.active {
|
#loading-indicator.active {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
|
|
@ -362,13 +320,8 @@
|
||||||
<section>
|
<section>
|
||||||
<div class="video-grid" id="videoGrid"></div>
|
<div class="video-grid" id="videoGrid"></div>
|
||||||
<div id="loading-indicator">loading works...</div>
|
<div id="loading-indicator">loading works...</div>
|
||||||
|
|
||||||
<p class="footer-copyright">
|
|
||||||
© <span id="year"></span> Andrey Flat
|
|
||||||
</p>
|
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
<footer>
|
<footer>
|
||||||
<div class="social-links">
|
<div class="social-links">
|
||||||
|
|
||||||
|
|
@ -398,6 +351,7 @@
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<p>© <span id="year"></span> Andrey Flat</p>
|
||||||
</footer>
|
</footer>
|
||||||
|
|
||||||
<button id="scrollToTopBtn" title="Up">⇧</button>
|
<button id="scrollToTopBtn" title="Up">⇧</button>
|
||||||
|
|
@ -722,7 +676,7 @@ window.addEventListener("load", () => {
|
||||||
if (sessionStorage.getItem("af_logged")) return;
|
if (sessionStorage.getItem("af_logged")) return;
|
||||||
sessionStorage.setItem("af_logged", "1");
|
sessionStorage.setItem("af_logged", "1");
|
||||||
|
|
||||||
// WARNING: Exposing secret keys in frontend code is risky. commet
|
// WARNING: Exposing secret keys in frontend code is risky.
|
||||||
// Anyone can see this key. Use environment variables in Netlify if possible.
|
// Anyone can see this key. Use environment variables in Netlify if possible.
|
||||||
const secret = "flat_secret_123";
|
const secret = "flat_secret_123";
|
||||||
|
|
||||||
|
|
|
||||||
Binary file not shown.
Binary file not shown.
|
|
@ -1,172 +0,0 @@
|
||||||
// netlify/functions/privateViewLogger.js
|
|
||||||
// Требуется package.json с {"type":"module"}
|
|
||||||
|
|
||||||
import https from "https";
|
|
||||||
import { createClient } from "@supabase/supabase-js";
|
|
||||||
|
|
||||||
// node-fetch (ленивая подгрузка)
|
|
||||||
const fetch = (...args) => import("node-fetch").then(({ default: f }) => f(...args));
|
|
||||||
|
|
||||||
// --- ENV ---
|
|
||||||
const BOT_TOKEN = process.env.BOT_TOKEN; // опционально
|
|
||||||
const CHAT_ID = process.env.CHAT_ID; // опционально
|
|
||||||
const SECRET_KEY = process.env.SECRET_KEY; // обязателен
|
|
||||||
const SUPABASE_URL = process.env.SUPABASE_URL; // обязателен
|
|
||||||
const SUPABASE_KEY = process.env.SUPABASE_SERVICE_KEY; // обязателен
|
|
||||||
|
|
||||||
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY, { auth: { persistSession: false } });
|
|
||||||
|
|
||||||
// ---------- helpers ----------
|
|
||||||
function extractIp(headers = {}) {
|
|
||||||
return (
|
|
||||||
headers["x-nf-client-connection-ip"] ||
|
|
||||||
(headers["x-forwarded-for"] || "").split(",")[0]?.trim() ||
|
|
||||||
headers["client-ip"] ||
|
|
||||||
headers["x-real-ip"] ||
|
|
||||||
""
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isPrivateIp(ip) {
|
|
||||||
if (!ip) return true;
|
|
||||||
const cleaned = ip.replace(/^\[?([^\]]+)\]?(:\d+)?$/, "$1");
|
|
||||||
if (/^\d{1,3}(\.\d{1,3}){3}$/.test(cleaned)) {
|
|
||||||
const [a,b] = cleaned.split(".").map(Number);
|
|
||||||
if (a === 10 || a === 127) return true;
|
|
||||||
if (a === 100 && b >= 64 && b <= 127) return true;
|
|
||||||
if (a === 192 && b === 168) return true;
|
|
||||||
if (a === 172 && b >= 16 && b <= 31) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const lc = cleaned.toLowerCase();
|
|
||||||
if (lc === "::1" || lc.startsWith("fc") || lc.startsWith("fd")) return true;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function parseUA(ua = "") {
|
|
||||||
let os = "Unknown OS", browser = "Unknown Browser";
|
|
||||||
if (/\bWindows\b/i.test(ua)) os = "Windows";
|
|
||||||
else if (/\bMac OS X|Macintosh\b/i.test(ua)) os = "macOS";
|
|
||||||
else if (/\bAndroid\b/i.test(ua)) os = "Android";
|
|
||||||
else if (/\biPhone|iPad\b/i.test(ua)) os = "iOS";
|
|
||||||
else if (/\bX11|Linux\b/i.test(ua)) os = "Linux";
|
|
||||||
|
|
||||||
if (/Firefox\/\d+/i.test(ua)) browser = "Firefox";
|
|
||||||
else if (/\bEdg\/\d+/i.test(ua)) browser = "Edge";
|
|
||||||
else if (/Chrome\/\d+/i.test(ua) && !/Chromium/i.test(ua)) browser = "Chrome";
|
|
||||||
else if (/Safari\/\d+/i.test(ua) && !/Chrome/i.test(ua)) browser = "Safari";
|
|
||||||
else if (/Chromium\/\d+/i.test(ua)) browser = "Chromium";
|
|
||||||
|
|
||||||
return { os, browser };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function sendTelegram(text) {
|
|
||||||
if (!BOT_TOKEN || !CHAT_ID) return { ok: true };
|
|
||||||
const data = JSON.stringify({ chat_id: CHAT_ID, text, parse_mode: "Markdown" });
|
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const req = https.request(
|
|
||||||
{
|
|
||||||
hostname: "api.telegram.org",
|
|
||||||
path: `/bot${BOT_TOKEN}/sendMessage`,
|
|
||||||
method: "POST",
|
|
||||||
headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) },
|
|
||||||
},
|
|
||||||
(res) => {
|
|
||||||
res.on("data", () => {});
|
|
||||||
res.on("end", () => resolve({ ok: res.statusCode === 200, status: res.statusCode }));
|
|
||||||
}
|
|
||||||
);
|
|
||||||
req.on("error", () => resolve({ ok: false }));
|
|
||||||
req.write(data);
|
|
||||||
req.end();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------- handler ----------
|
|
||||||
export async function handler(event) {
|
|
||||||
// доступ: ключ ИЛИ тот же домен; блочим TelegramBot UA
|
|
||||||
const qs = event.queryStringParameters || {};
|
|
||||||
const { key } = qs;
|
|
||||||
|
|
||||||
const headers = event.headers || {};
|
|
||||||
const ua = headers["user-agent"] || "";
|
|
||||||
const referer = (headers["referer"] || "").toLowerCase();
|
|
||||||
|
|
||||||
const sameOrigin = referer.includes("andreyflat.space");
|
|
||||||
const keyOk = key && SECRET_KEY && key === SECRET_KEY;
|
|
||||||
|
|
||||||
if (ua.includes("TelegramBot") || (!keyOk && !sameOrigin)) {
|
|
||||||
return {
|
|
||||||
statusCode: 403,
|
|
||||||
headers: { "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0" },
|
|
||||||
body: JSON.stringify({ error: "Forbidden" }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- извлекаем данные для статистики ---
|
|
||||||
const ipRaw = extractIp(headers) || "unknown"; // используем ТОЛЬКО для гео, НЕ пишем в БД
|
|
||||||
const { os, browser } = parseUA(ua);
|
|
||||||
|
|
||||||
// 1) сначала пробуем заголовки Netlify
|
|
||||||
let country = headers["x-country"] || null; // ISO-2, например "ID"
|
|
||||||
let city = null;
|
|
||||||
try {
|
|
||||||
if (headers["x-nf-geo"]) {
|
|
||||||
const g = JSON.parse(headers["x-nf-geo"]);
|
|
||||||
if (g?.country?.name || g?.country?.code) country = g.country.name || g.country.code || country;
|
|
||||||
if (g?.city) city = g.city;
|
|
||||||
}
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// 2) если города нет и IP публичный — мягко добираем через ipapi.co (таймаут 3с)
|
|
||||||
if (!city && ipRaw !== "unknown" && !isPrivateIp(ipRaw)) {
|
|
||||||
try {
|
|
||||||
const ctrl = new AbortController();
|
|
||||||
const t = setTimeout(() => ctrl.abort(), 3000);
|
|
||||||
const r = await fetch(`https://ipapi.co/${ipRaw}/json/`, { signal: ctrl.signal });
|
|
||||||
clearTimeout(t);
|
|
||||||
if (r.ok) {
|
|
||||||
const j = await r.json();
|
|
||||||
city = j.city || city;
|
|
||||||
country = j.country_name || j.country || country;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// игнор
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- запись в БД (без IP и без path) ---
|
|
||||||
const insertPayload = {
|
|
||||||
referrer: headers["referer"] || null,
|
|
||||||
country: country || null,
|
|
||||||
city: city || null,
|
|
||||||
ua_os: os,
|
|
||||||
ua_browser: browser,
|
|
||||||
};
|
|
||||||
|
|
||||||
const { error } = await supabase.from("pageviews").insert([insertPayload]);
|
|
||||||
|
|
||||||
// --- уведомление в Телеграм (опционально) ---
|
|
||||||
const loc = country ? (city ? `${country}, ${city}` : country) : "Unknown";
|
|
||||||
const text =
|
|
||||||
`🔔 *New View*
|
|
||||||
📍 ${loc}
|
|
||||||
📱 ${os} / ${browser}`;
|
|
||||||
|
|
||||||
if (!error) await sendTelegram(text);
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return {
|
|
||||||
statusCode: 500,
|
|
||||||
headers: { "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0" },
|
|
||||||
body: JSON.stringify({ error: error.message }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
statusCode: 200,
|
|
||||||
headers: { "Cache-Control": "no-store, no-cache, must-revalidate, max-age=0" },
|
|
||||||
body: JSON.stringify({ ok: true }),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
@ -1,181 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
<title>Splat Viewer — Nebula</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>
|
|
||||||
body {
|
|
||||||
margin: 0;
|
|
||||||
padding: 0;
|
|
||||||
background-color: #000;
|
|
||||||
color: #eee;
|
|
||||||
font-family: 'Space Grotesk', sans-serif;
|
|
||||||
overflow: hidden;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
height: 100vh;
|
|
||||||
}
|
|
||||||
|
|
||||||
header {
|
|
||||||
backdrop-filter: blur(10px);
|
|
||||||
-webkit-backdrop-filter: blur(10px);
|
|
||||||
background-color: rgba(10, 10, 10, 0.8);
|
|
||||||
padding: 10px 20px;
|
|
||||||
text-align: center;
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
width: 100%;
|
|
||||||
z-index: 10;
|
|
||||||
box-sizing: border-box;
|
|
||||||
border-bottom: 1px solid rgba(255, 255, 255, 0.1);
|
|
||||||
}
|
|
||||||
|
|
||||||
header h1 {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 1.2rem;
|
|
||||||
letter-spacing: 0.1em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
#container {
|
|
||||||
flex-grow: 1;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#loading {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
text-align: center;
|
|
||||||
z-index: 5;
|
|
||||||
}
|
|
||||||
|
|
||||||
.spinner {
|
|
||||||
width: 40px;
|
|
||||||
height: 40px;
|
|
||||||
border: 3px solid rgba(255, 255, 255, 0.1);
|
|
||||||
border-top: 3px solid #fff;
|
|
||||||
border-radius: 50%;
|
|
||||||
animation: spin 1s linear infinite;
|
|
||||||
margin: 0 auto 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@keyframes spin {
|
|
||||||
0% { transform: rotate(0deg); }
|
|
||||||
100% { transform: rotate(360deg); }
|
|
||||||
}
|
|
||||||
|
|
||||||
.controls-hint {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 20px;
|
|
||||||
left: 50%;
|
|
||||||
transform: translateX(-50%);
|
|
||||||
background: rgba(0, 0, 0, 0.6);
|
|
||||||
padding: 8px 15px;
|
|
||||||
border-radius: 20px;
|
|
||||||
font-size: 0.8rem;
|
|
||||||
color: #888;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 10;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link {
|
|
||||||
position: absolute;
|
|
||||||
top: 15px;
|
|
||||||
left: 20px;
|
|
||||||
z-index: 20;
|
|
||||||
color: #fff;
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 0.9rem;
|
|
||||||
opacity: 0.7;
|
|
||||||
transition: opacity 0.3s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.back-link:hover {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<header>
|
|
||||||
<a href="index.html" class="back-link">← Back</a>
|
|
||||||
<h1>Nebula Splat Viewer</h1>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div id="loading">
|
|
||||||
<div class="spinner"></div>
|
|
||||||
<div id="status">Loading Gaussian Splat...</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div id="container"></div>
|
|
||||||
|
|
||||||
<div class="controls-hint">
|
|
||||||
Left Click: Rotate | Right Click: Pan | Scroll: Zoom
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="module">
|
|
||||||
// Using a pinned version for stability
|
|
||||||
import * as SPLAT from "https://cdn.jsdelivr.net/npm/gsplat@1.2.9/dist/index.es.min.js";
|
|
||||||
|
|
||||||
async function init() {
|
|
||||||
const container = document.getElementById('container');
|
|
||||||
const status = document.getElementById('status');
|
|
||||||
const loading = document.getElementById('loading');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const renderer = new SPLAT.WebGLRenderer();
|
|
||||||
container.appendChild(renderer.canvas);
|
|
||||||
|
|
||||||
const scene = new SPLAT.Scene();
|
|
||||||
const camera = new SPLAT.Camera();
|
|
||||||
const controls = new SPLAT.OrbitControls(camera, renderer.canvas);
|
|
||||||
|
|
||||||
// Resize handler
|
|
||||||
const resize = () => {
|
|
||||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
|
||||||
};
|
|
||||||
window.addEventListener('resize', resize);
|
|
||||||
resize();
|
|
||||||
|
|
||||||
// Load the Splat file
|
|
||||||
status.innerText = "Downloading Splat...";
|
|
||||||
|
|
||||||
await SPLAT.Loader.LoadAsync("/models/gs/nebula_rnd1.splat", scene, (progress) => {
|
|
||||||
status.innerText = `Loading: ${Math.round(progress * 100)}%`;
|
|
||||||
});
|
|
||||||
|
|
||||||
loading.style.display = 'none';
|
|
||||||
|
|
||||||
// Set initial camera position AFTER loading,
|
|
||||||
// because some models might be very far from origin
|
|
||||||
camera.position.z = 5;
|
|
||||||
camera.position.y = 1;
|
|
||||||
controls.update();
|
|
||||||
|
|
||||||
const frame = () => {
|
|
||||||
controls.update();
|
|
||||||
renderer.render(scene, camera);
|
|
||||||
requestAnimationFrame(frame);
|
|
||||||
};
|
|
||||||
requestAnimationFrame(frame);
|
|
||||||
|
|
||||||
console.log("Splat loaded successfully into scene", scene);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error in GSPLAT init:", error);
|
|
||||||
status.innerText = "Error: " + error.message;
|
|
||||||
status.style.color = "#ff4444";
|
|
||||||
document.querySelector('.spinner').style.display = 'none';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init();
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
Loading…
Reference in a new issue