updated site ( crazy )

This commit is contained in:
2026-08-27 21:26:05 -07:00
parent ddaa71dda8
commit df1897745a
19 changed files with 1345 additions and 591 deletions

View File

@@ -29,7 +29,7 @@
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
<meta name="msapplication-starturl" content="/">
<meta name="msapplication-window" content="width=device-width;height=device-height">
<meta name="msapplication-navbutton-color" content="#1a1a2e">
<meta name="msapplication-navbutton-color" content="#FFF">
<link rel="icon" href="/assets/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.ico">
@@ -73,13 +73,13 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a1a2e">
<meta name="theme-color" content="#FFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Vyla Home">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vyla Home">
<meta name="msapplication-TileColor" content="#1a1a2e">
<meta name="msapplication-TileColor" content="#FFF">
<meta name="msapplication-config" content="/browserconfig.xml">
<link rel="apple-touch-icon" href="favicon.ico">
@@ -302,15 +302,10 @@
</div>
</main>
<div class="cta-footer-wrap">
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
<section class="cta-banner" aria-labelledby="cta-heading">
<div class="cta-banner-media" aria-hidden="true">
<video class="cta-banner-video hero-atmosphere-video"
src="https://videos.pexels.com/video-files/26754965/12000494_3840_2160_30fps.mp4" autoplay loop muted
playsinline webkit-playsinline preload="auto" crossorigin="anonymous" aria-hidden="true">
</video>
<canvas id="cta-byte-overlay" aria-hidden="true"></canvas>
<div class="cta-banner-overlay"></div>
</div>
<div class="container cta-banner-content reveal">
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
@@ -320,25 +315,43 @@
<script>
async function setupLatestDownload() {
const btn = document.getElementById("hero-copy-script-btn");
const buttons = [
document.getElementById("hero-copy-script-btn"),
document.getElementById("cta-copy-script-btn")
];
try {
const project = "vyla-entertainment%2Fhome";
const response = await fetch(
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"
`https://gitlab.com/api/v4/projects/${project}/releases/permalink/latest`
);
if (!response.ok) throw new Error("Failed to fetch latest release");
if (!response.ok) {
throw new Error(`Failed to fetch latest release: ${response.status}`);
}
const release = await response.json();
const version = release.tag_name;
const fileVersion = version.replace(/^v/, "");
btn.href =
`https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
const asset = release.assets?.links?.find(link =>
link.name?.toLowerCase().endsWith(".exe")
);
btn.textContent = `Download Release ${version}`;
if (!asset?.url) {
throw new Error("No .exe asset found in latest release");
}
buttons.forEach(button => {
if (button) {
button.href = asset.url;
}
});
} catch (error) {
btn.textContent = "Download Latest Release";
buttons.forEach(button => {
if (button) {
button.textContent = "Download Latest Release";
}
});
}
}
@@ -349,27 +362,27 @@
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>Vyla</h4>
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h4>Backup Site:</h4>
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
<h4>Quick Links</h4>
<h2>Quick Links</h2>
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/about-us">About Us</a>
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
</div>
<div class="footer-section">
<h4>Our API</h4>
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
</div>
<div class="footer-section">
<h4>Join our Discord</h4>
<h2>Join our Discord</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
@@ -377,8 +390,8 @@
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc?nocache=true"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200px"
height="95px">
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
@@ -386,6 +399,7 @@
</a>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
</body>

BIN
assets/images/thumbnail.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@@ -29,7 +29,7 @@
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
<meta name="msapplication-starturl" content="/">
<meta name="msapplication-window" content="width=device-width;height=device-height">
<meta name="msapplication-navbutton-color" content="#1a1a2e">
<meta name="msapplication-navbutton-color" content="#FFF">
<link rel="icon" href="assets/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="favicon.ico">
@@ -50,7 +50,7 @@
<meta property="og:title" content="Vyla Home - Self-Hosted Desktop Media Center | Serverless Streaming">
<meta property="og:description"
content="Experience revolutionary self-hosted streaming with Vyla Home. Bypass datacenter blocking, ISP throttling, and cloud downtime. 4K movies, TV shows, anime with complete privacy and zero server dependencies.">
<meta property="og:image" content="https://vyla.cc/assets/images/thumbnail.png">
<meta property="og:image" content="/assets/images/thumbnail.png">
<meta property="og:image:width" content="1200">
<meta property="og:image:height" content="630">
<meta property="og:image:alt" content="Vyla Home Desktop Media Center Interface">
@@ -66,20 +66,20 @@
<meta name="twitter:title" content="Vyla Home - Self-Hosted Desktop Media Center">
<meta name="twitter:description"
content="Experience revolutionary self-hosted streaming. Bypass datacenter blocking, ISP throttling, and cloud downtime. 4K movies, TV shows, anime with complete privacy.">
<meta name="twitter:image" content="https://vyla.cc/assets/images/thumbnail.png">
<meta name="twitter:image" content="/assets/images/thumbnail.png">
<meta name="twitter:image:alt" content="Vyla Home Desktop Media Center Interface">
<link rel="icon" href="favicon.ico" type="image/x-icon">
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#1a1a2e">
<meta name="theme-color" content="#FFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Vyla Home">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vyla Home">
<meta name="msapplication-TileColor" content="#1a1a2e">
<meta name="msapplication-TileColor" content="#FFF">
<meta name="msapplication-config" content="/browserconfig.xml">
<link rel="apple-touch-icon" href="favicon.ico">
@@ -91,6 +91,7 @@
<link rel="stylesheet" href="src/styles/base/typography.css">
<link rel="stylesheet" href="src/styles/utilities/layout.css">
<link rel="stylesheet" href="src/styles/utilities/reveal.css">
<link rel="stylesheet" href="src/styles/components/glass-surface.css">
<link rel="stylesheet" href="src/styles/layout/nav.css">
<link rel="stylesheet" href="src/styles/sections/hero-atmosphere.css">
<link rel="stylesheet" href="src/styles/sections/hero-content.css">
@@ -266,14 +267,7 @@
<main role="main">
<section class="hero" aria-labelledby="hero-title">
<div class="hero-atmosphere" aria-hidden="true">
<video class="hero-atmosphere-video" autoplay loop muted playsinline webkit-playsinline preload="auto"
crossorigin="anonymous" aria-hidden="true">
<source src="https://videos.pexels.com/video-files/26754965/12000494_3840_2160_30fps.mp4"
type="video/mp4">
</video>
<div class="hero-atmosphere-sheen"></div>
<canvas id="hero-byte-overlay" aria-hidden="true"></canvas>
<div class="hero-atmosphere-vignette"></div>
<div id="hero-prism" class="hero-prism"></div>
</div>
<div class="container hero-container">
@@ -531,15 +525,10 @@
</section>
</main>
<div class="cta-footer-wrap">
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
<section class="cta-banner" aria-labelledby="cta-heading">
<div class="cta-banner-media" aria-hidden="true">
<video class="cta-banner-video hero-atmosphere-video"
src="https://videos.pexels.com/video-files/26754965/12000494_3840_2160_30fps.mp4" autoplay loop muted
playsinline webkit-playsinline preload="auto" crossorigin="anonymous" aria-hidden="true">
</video>
<canvas id="cta-byte-overlay" aria-hidden="true"></canvas>
<div class="cta-banner-overlay"></div>
</div>
<div class="container cta-banner-content reveal">
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
@@ -624,8 +613,8 @@
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc?nocache=true"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200" height="95"
loading="lazy" decoding="async">
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
@@ -633,6 +622,7 @@
</a>
</div>
</footer>
</div>
<script type="module" src="src/scripts/main.js"></script>
</body>

View File

@@ -29,7 +29,7 @@
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
<meta name="msapplication-starturl" content="/">
<meta name="msapplication-window" content="width=device-width;height=device-height">
<meta name="msapplication-navbutton-color" content="#1a1a2e">
<meta name="msapplication-navbutton-color" content="#FFF">
<link rel="icon" href="/assets/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.ico">
@@ -73,13 +73,13 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a1a2e">
<meta name="theme-color" content="#FFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Vyla Home">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vyla Home">
<meta name="msapplication-TileColor" content="#1a1a2e">
<meta name="msapplication-TileColor" content="#FFF">
<meta name="msapplication-config" content="/browserconfig.xml">
<link rel="apple-touch-icon" href="favicon.ico">
@@ -313,15 +313,10 @@
</div>
</main>
<div class="cta-footer-wrap">
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
<section class="cta-banner" aria-labelledby="cta-heading">
<div class="cta-banner-media" aria-hidden="true">
<video class="cta-banner-video hero-atmosphere-video"
src="https://videos.pexels.com/video-files/26754965/12000494_3840_2160_30fps.mp4" autoplay loop muted
playsinline webkit-playsinline preload="auto" crossorigin="anonymous" aria-hidden="true">
</video>
<canvas id="cta-byte-overlay" aria-hidden="true"></canvas>
<div class="cta-banner-overlay"></div>
</div>
<div class="container cta-banner-content reveal">
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
@@ -331,25 +326,43 @@
<script>
async function setupLatestDownload() {
const btn = document.getElementById("hero-copy-script-btn");
const buttons = [
document.getElementById("hero-copy-script-btn"),
document.getElementById("cta-copy-script-btn")
];
try {
const project = "vyla-entertainment%2Fhome";
const response = await fetch(
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"
`https://gitlab.com/api/v4/projects/${project}/releases/permalink/latest`
);
if (!response.ok) throw new Error("Failed to fetch latest release");
if (!response.ok) {
throw new Error(`Failed to fetch latest release: ${response.status}`);
}
const release = await response.json();
const version = release.tag_name;
const fileVersion = version.replace(/^v/, "");
btn.href =
`https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
const asset = release.assets?.links?.find(link =>
link.name?.toLowerCase().endsWith(".exe")
);
btn.textContent = `Download Release ${version}`;
if (!asset?.url) {
throw new Error("No .exe asset found in latest release");
}
buttons.forEach(button => {
if (button) {
button.href = asset.url;
}
});
} catch (error) {
btn.textContent = "Download Latest Release";
buttons.forEach(button => {
if (button) {
button.textContent = "Download Latest Release";
}
});
}
}
@@ -360,27 +373,27 @@
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>Vyla</h4>
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h4>Backup Site:</h4>
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
<h4>Quick Links</h4>
<h2>Quick Links</h2>
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/about-us">About Us</a>
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
</div>
<div class="footer-section">
<h4>Our API</h4>
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
</div>
<div class="footer-section">
<h4>Join our Discord</h4>
<h2>Join our Discord</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
@@ -388,8 +401,8 @@
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc?nocache=true"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200px"
height="95px">
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
@@ -397,6 +410,7 @@
</a>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
</body>

View File

@@ -0,0 +1,121 @@
let globalSvgDefs = null;
function ensureGlobalDefs() {
if (!globalSvgDefs) {
globalSvgDefs = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
globalSvgDefs.setAttribute('id', 'glass-surface-defs');
globalSvgDefs.setAttribute('aria-hidden', 'true');
globalSvgDefs.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;pointer-events:none;z-index:-9999;opacity:0;';
document.body.appendChild(globalSvgDefs);
}
return globalSvgDefs;
}
export function initGlassSurface(selector, options = {}) {
const elements = typeof selector === 'string' ? document.querySelectorAll(selector) : [selector];
const defsContainer = ensureGlobalDefs();
elements.forEach((el, index) => {
if (!el || el.dataset.glassInitialized) return;
el.dataset.glassInitialized = 'true';
const id = `glass-surf-${index}-${Math.random().toString(36).slice(2, 7)}`;
const filterId = `filter-${id}`;
const redGradId = `red-grad-${id}`;
const blueGradId = `blue-grad-${id}`;
const config = {
borderRadius: options.borderRadius !== undefined ? Number(options.borderRadius) : 50,
borderWidth: options.borderWidth !== undefined ? Number(options.borderWidth) : 0,
brightness: options.brightness !== undefined ? Number(options.brightness) : 100,
opacity: options.opacity !== undefined ? Number(options.opacity) : 0.93,
blur: options.blur !== undefined ? Number(options.blur) : 15,
displace: options.displace !== undefined ? Number(options.displace) : 0.5,
backgroundOpacity: options.backgroundOpacity !== undefined ? Number(options.backgroundOpacity) : 0,
saturation: options.saturation !== undefined ? Number(options.saturation) : 0,
distortionScale: options.distortionScale !== undefined ? Number(options.distortionScale) : -180,
redOffset: options.redOffset !== undefined ? Number(options.redOffset) : -50,
greenOffset: options.greenOffset !== undefined ? Number(options.greenOffset) : -50,
blueOffset: options.blueOffset !== undefined ? Number(options.blueOffset) : 20,
xChannel: options.xChannel || 'R',
yChannel: options.yChannel || 'G',
mixBlendMode: options.mixBlendMode || 'difference'
};
const filterGroup = document.createElementNS('http://www.w3.org/2000/svg', 'defs');
filterGroup.innerHTML = `
<filter id="${filterId}" color-interpolation-filters="sRGB" x="-50%" y="-50%" width="200%" height="200%">
<feImage id="fe-map-${id}" x="0" y="0" width="100%" height="100%" preserveAspectRatio="none" result="map" />
<feDisplacementMap id="disp-red-${id}" in="SourceGraphic" in2="map" scale="${config.distortionScale + config.redOffset}" xChannelSelector="${config.xChannel}" yChannelSelector="${config.yChannel}" result="dispRed" />
<feColorMatrix in="dispRed" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red" />
<feDisplacementMap id="disp-green-${id}" in="SourceGraphic" in2="map" scale="${config.distortionScale + config.greenOffset}" xChannelSelector="${config.xChannel}" yChannelSelector="${config.yChannel}" result="dispGreen" />
<feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green" />
<feDisplacementMap id="disp-blue-${id}" in="SourceGraphic" in2="map" scale="${config.distortionScale + config.blueOffset}" xChannelSelector="${config.xChannel}" yChannelSelector="${config.yChannel}" result="dispBlue" />
<feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue" />
<feBlend in="red" in2="green" mode="screen" result="rg" />
<feBlend in="rg" in2="blue" mode="screen" result="output" />
<feGaussianBlur id="blur-${id}" in="output" stdDeviation="${config.displace}" />
</filter>
`;
defsContainer.appendChild(filterGroup);
const rawContent = el.innerHTML;
el.innerHTML = '';
const backdropLayer = document.createElement('span');
backdropLayer.className = 'glass-surface__backdrop';
const contentLayer = document.createElement('span');
contentLayer.className = 'glass-surface__content';
contentLayer.innerHTML = rawContent;
el.appendChild(backdropLayer);
el.appendChild(contentLayer);
el.classList.add('glass-surface');
el.style.borderRadius = `${config.borderRadius}px`;
el.style.setProperty('--glass-radius', `${config.borderRadius}px`);
el.style.setProperty('--glass-saturation', config.saturation.toString());
const feImage = defsContainer.querySelector(`#fe-map-${id}`);
const updateMap = () => {
const rect = el.getBoundingClientRect();
const width = Math.max(Math.round(rect.width), 40);
const height = Math.max(Math.round(rect.height), 20);
const edgeSize = Math.min(width, height) * (config.borderWidth * 0.5);
const svgContent = `
<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg">
<defs>
<linearGradient id="${redGradId}" x1="100%" y1="0%" x2="0%" y2="0%">
<stop offset="0%" stop-color="#0000"/>
<stop offset="100%" stop-color="red"/>
</linearGradient>
<linearGradient id="${blueGradId}" x1="0%" y1="0%" x2="0%" y2="100%">
<stop offset="0%" stop-color="#0000"/>
<stop offset="100%" stop-color="blue"/>
</linearGradient>
</defs>
<rect x="0" y="0" width="${width}" height="${height}" fill="black"></rect>
<rect x="0" y="0" width="${width}" height="${height}" rx="${config.borderRadius}" fill="url(#${redGradId})" />
<rect x="0" y="0" width="${width}" height="${height}" rx="${config.borderRadius}" fill="url(#${blueGradId})" style="mix-blend-mode: ${config.mixBlendMode}" />
<rect x="${edgeSize}" y="${edgeSize}" width="${Math.max(0, width - edgeSize * 2)}" height="${Math.max(0, height - edgeSize * 2)}" rx="${config.borderRadius}" fill="hsl(0 0% ${config.brightness}% / ${config.opacity})" style="filter:blur(${config.blur}px)" />
</svg>
`;
if (feImage) {
feImage.setAttribute('href', `data:image/svg+xml;charset=utf-8,${encodeURIComponent(svgContent)}`);
}
el.style.setProperty('--glass-filter-id', `url(#${filterId})`);
el.style.setProperty('--glass-radius', `${config.borderRadius}px`);
el.style.setProperty('--glass-saturation', config.saturation.toString());
el.style.borderRadius = `${config.borderRadius}px`;
};
updateMap();
const ro = new ResizeObserver(() => requestAnimationFrame(updateMap));
ro.observe(el);
});
}

View File

@@ -0,0 +1,245 @@
import { Renderer, Program, Triangle, Mesh } from 'https://esm.sh/ogl@1.0.11';
const hexToRgb = hex => {
const m = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return m ? [parseInt(m[1], 16) / 255, parseInt(m[2], 16) / 255, parseInt(m[3], 16) / 255] : [1, 1, 1];
};
const getAnchorAndDir = (origin, w, h) => {
const outside = 0.05;
switch (origin) {
case 'top-left':
return { anchor: [0, -outside * h], dir: [0.707, 0.707] };
case 'top-right':
return { anchor: [w, -outside * h], dir: [-0.707, 0.707] };
case 'left':
return { anchor: [-outside * w, 0.5 * h], dir: [1, 0] };
case 'right':
return { anchor: [(1 + outside) * w, 0.5 * h], dir: [-1, 0] };
case 'bottom-left':
return { anchor: [0, (1 + outside) * h], dir: [0.707, -0.707] };
case 'bottom-center':
return { anchor: [0.5 * w, (1 + outside) * h], dir: [0, -1] };
case 'bottom-right':
return { anchor: [w, (1 + outside) * h], dir: [-0.707, -0.707] };
default:
return { anchor: [0.5 * w, -outside * h], dir: [0, 1] };
}
};
export function initLightRays(container, options = {}) {
if (!container) return () => { };
const raysOrigin = options.raysOrigin ?? 'bottom-center';
const rayColor1 = options.rayColor1 ?? '#EAB308';
const rayColor2 = options.rayColor2 ?? '#96c8ff';
const blend = options.blend ?? 0.65;
const raysSpeed = options.raysSpeed ?? 1.8;
const lightSpread = options.lightSpread ?? 0.7;
const rayLength = options.rayLength ?? 2.5;
const pulsating = Boolean(options.pulsating);
const fadeDistance = options.fadeDistance ?? 2.0;
const saturation = options.saturation ?? 1.4;
const followMouse = options.followMouse !== false;
const mouseInfluence = options.mouseInfluence ?? 0.12;
const noiseAmount = options.noiseAmount ?? 0.08;
const distortion = options.distortion ?? 0.04;
const mouse = { x: 0.5, y: 0.5 };
const smoothMouse = { x: 0.5, y: 0.5 };
let animationId = null;
const renderer = new Renderer({
dpr: Math.min(window.devicePixelRatio, 2),
alpha: true
});
const gl = renderer.gl;
Object.assign(gl.canvas.style, {
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
display: 'block',
pointerEvents: 'none'
});
while (container.firstChild) {
container.removeChild(container.firstChild);
}
container.appendChild(gl.canvas);
const vert = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const frag = `
precision highp float;
uniform float iTime;
uniform vec2 iResolution;
uniform vec2 rayPos;
uniform vec2 rayDir;
uniform vec3 rayColor1;
uniform vec3 rayColor2;
uniform float blend;
uniform float raysSpeed;
uniform float lightSpread;
uniform float rayLength;
uniform float pulsating;
uniform float fadeDistance;
uniform float saturation;
uniform vec2 mousePos;
uniform float mouseInfluence;
uniform float noiseAmount;
uniform float distortion;
float noise(vec2 st) {
return fract(sin(dot(st.xy, vec2(12.9898,78.233))) * 43758.5453123);
}
float rayStrength(vec2 raySource, vec2 rayRefDirection, vec2 coord, float seedA, float seedB, float speed) {
vec2 sourceToCoord = coord - raySource;
vec2 dirNorm = normalize(sourceToCoord);
float cosAngle = dot(dirNorm, rayRefDirection);
float distortedAngle = cosAngle + distortion * sin(iTime * 2.0 + length(sourceToCoord) * 0.005) * 0.2;
float spreadFactor = pow(max(distortedAngle, 0.0), 1.0 / max(lightSpread, 0.001));
float maxDim = max(iResolution.x, iResolution.y);
float distance = length(sourceToCoord);
float maxDistance = maxDim * rayLength;
float lengthFalloff = clamp((maxDistance - distance) / maxDistance, 0.0, 1.0);
float fadeRadius = maxDim * fadeDistance;
float fadeFalloff = clamp((fadeRadius - distance) / fadeRadius, 0.0, 1.0);
float pulse = pulsating > 0.5 ? (0.85 + 0.15 * sin(iTime * speed * 3.0)) : 1.0;
float baseStrength = clamp(
(0.45 + 0.15 * sin(distortedAngle * seedA + iTime * speed)) +
(0.3 + 0.2 * cos(-distortedAngle * seedB + iTime * speed)),
0.0, 1.0
);
return baseStrength * lengthFalloff * fadeFalloff * spreadFactor * pulse;
}
void main() {
vec2 coord = vec2(gl_FragCoord.x, iResolution.y - gl_FragCoord.y);
vec2 finalRayDir = rayDir;
if (mouseInfluence > 0.0) {
vec2 mouseScreenPos = mousePos * iResolution.xy;
vec2 mouseDirection = normalize(mouseScreenPos - rayPos);
finalRayDir = normalize(mix(rayDir, mouseDirection, mouseInfluence));
}
vec4 rays1 = vec4(rayColor1, 1.0) * rayStrength(rayPos, finalRayDir, coord, 36.2214, 21.11349, 1.5 * raysSpeed);
vec4 rays2 = vec4(rayColor2, 1.0) * rayStrength(rayPos, finalRayDir, coord, 22.3991, 18.0234, 1.1 * raysSpeed);
vec4 color = rays1 * (1.0 - blend) * 1.2 + rays2 * blend * 1.2;
if (noiseAmount > 0.0) {
float n = noise(coord * 0.01 + iTime * 0.1);
color.rgb *= (1.0 - noiseAmount + noiseAmount * n);
}
float maxDim = max(iResolution.x, iResolution.y);
float distFromSource = length(coord - rayPos) / maxDim;
float glowFalloff = clamp(1.3 - distFromSource * 0.7, 0.3, 1.6);
color.rgb *= glowFalloff;
if (saturation != 1.0) {
float gray = dot(color.rgb, vec3(0.299, 0.587, 0.114));
color.rgb = mix(vec3(gray), color.rgb, saturation);
}
color.a = clamp(max(color.r, max(color.g, color.b)) * 1.4, 0.0, 1.0);
gl_FragColor = color;
}
`;
const uniforms = {
iTime: { value: 0 },
iResolution: { value: [1, 1] },
rayPos: { value: [0, 0] },
rayDir: { value: [0, -1] },
rayColor1: { value: hexToRgb(rayColor1) },
rayColor2: { value: hexToRgb(rayColor2) },
blend: { value: blend },
raysSpeed: { value: raysSpeed },
lightSpread: { value: lightSpread },
rayLength: { value: rayLength },
pulsating: { value: pulsating ? 1.0 : 0.0 },
fadeDistance: { value: fadeDistance },
saturation: { value: saturation },
mousePos: { value: [0.5, 0.5] },
mouseInfluence: { value: mouseInfluence },
noiseAmount: { value: noiseAmount },
distortion: { value: distortion }
};
const geometry = new Triangle(gl);
const program = new Program(gl, { vertex: vert, fragment: frag, uniforms });
const mesh = new Mesh(gl, { geometry, program });
const updatePlacement = () => {
if (!container || !renderer) return;
renderer.dpr = Math.min(window.devicePixelRatio, 2);
const wCSS = container.clientWidth || window.innerWidth || 1;
const hCSS = container.clientHeight || window.innerHeight || 1;
renderer.setSize(wCSS, hCSS);
const w = wCSS * renderer.dpr;
const h = hCSS * renderer.dpr;
uniforms.iResolution.value = [w, h];
const { anchor, dir } = getAnchorAndDir(raysOrigin, w, h);
uniforms.rayPos.value = anchor;
uniforms.rayDir.value = dir;
};
const handleMouseMove = e => {
const rect = container.getBoundingClientRect();
mouse.x = (e.clientX - rect.left) / (rect.width || 1);
mouse.y = (e.clientY - rect.top) / (rect.height || 1);
};
if (followMouse) {
window.addEventListener('mousemove', handleMouseMove, { passive: true });
}
const loop = t => {
uniforms.iTime.value = t * 0.001;
if (followMouse && mouseInfluence > 0.0) {
smoothMouse.x += (mouse.x - smoothMouse.x) * 0.08;
smoothMouse.y += (mouse.y - smoothMouse.y) * 0.08;
uniforms.mousePos.value = [smoothMouse.x, smoothMouse.y];
}
try {
renderer.render({ scene: mesh });
animationId = requestAnimationFrame(loop);
} catch (e) { }
};
const ro = new ResizeObserver(updatePlacement);
ro.observe(container);
window.addEventListener('resize', updatePlacement);
updatePlacement();
animationId = requestAnimationFrame(loop);
return () => {
if (animationId) cancelAnimationFrame(animationId);
ro.disconnect();
window.removeEventListener('resize', updatePlacement);
if (followMouse) window.removeEventListener('mousemove', handleMouseMove);
if (renderer && renderer.gl && renderer.gl.canvas && renderer.gl.canvas.parentNode) {
renderer.gl.canvas.parentNode.removeChild(renderer.gl.canvas);
}
};
}

View File

@@ -0,0 +1,361 @@
import { Renderer, Triangle, Program, Mesh } from 'https://esm.sh/ogl@1.0.11';
export function initPrism(container, options = {}) {
if (!container) return () => { };
const H = Math.max(0.001, options.height ?? 3.5);
const BW = Math.max(0.001, options.baseWidth ?? 5.5);
const BASE_HALF = BW * 0.5;
const GLOW = Math.max(0.0, options.glow ?? 1);
const NOISE = Math.max(0.0, options.noise ?? 0.5);
const offX = options.offset?.x ?? 0;
const offY = options.offset?.y ?? 0;
const SAT = options.transparent !== false ? 1.5 : 1;
const SCALE = Math.max(0.001, options.scale ?? 3.6);
const HUE = options.hueShift || 0;
const CFREQ = Math.max(0.0, options.colorFrequency ?? 1);
const BLOOM = Math.max(0.0, options.bloom ?? 1);
const RSX = 1;
const RSY = 1;
const RSZ = 1;
const TS = Math.max(0, options.timeScale ?? 0.5);
const HOVSTR = Math.max(0, options.hoverStrength ?? 2);
const INERT = Math.max(0, Math.min(1, options.inertia ?? 0.05));
const animationType = options.animationType || 'rotate';
const transparent = options.transparent !== false;
const suspendWhenOffscreen = Boolean(options.suspendWhenOffscreen);
const dpr = Math.min(2, window.devicePixelRatio || 1);
const renderer = new Renderer({
dpr,
alpha: transparent,
antialias: false
});
const gl = renderer.gl;
gl.disable(gl.DEPTH_TEST);
gl.disable(gl.CULL_FACE);
gl.disable(gl.BLEND);
Object.assign(gl.canvas.style, {
position: 'absolute',
inset: '0',
width: '100%',
height: '100%',
display: 'block'
});
container.appendChild(gl.canvas);
const vertex = `
attribute vec2 position;
void main() {
gl_Position = vec4(position, 0.0, 1.0);
}
`;
const fragment = `
precision highp float;
uniform vec2 iResolution;
uniform float iTime;
uniform float uHeight;
uniform float uBaseHalf;
uniform mat3 uRot;
uniform int uUseBaseWobble;
uniform float uGlow;
uniform vec2 uOffsetPx;
uniform float uNoise;
uniform float uSaturation;
uniform float uScale;
uniform float uHueShift;
uniform float uColorFreq;
uniform float uBloom;
uniform float uCenterShift;
uniform float uInvBaseHalf;
uniform float uInvHeight;
uniform float uMinAxis;
uniform float uPxScale;
uniform float uTimeScale;
vec4 tanh4(vec4 x){
vec4 e2x = exp(2.0*x);
return (e2x - 1.0) / (e2x + 1.0);
}
float rand(vec2 co){
return fract(sin(dot(co, vec2(12.9898, 78.233))) * 43758.5453123);
}
float sdOctaAnisoInv(vec3 p){
vec3 q = vec3(abs(p.x) * uInvBaseHalf, abs(p.y) * uInvHeight, abs(p.z) * uInvBaseHalf);
float m = q.x + q.y + q.z - 1.0;
return m * uMinAxis * 0.5773502691896258;
}
float sdPyramidUpInv(vec3 p){
float oct = sdOctaAnisoInv(p);
float halfSpace = -p.y;
return max(oct, halfSpace);
}
mat3 hueRotation(float a){
float c = cos(a), s = sin(a);
mat3 W = mat3(
0.299, 0.587, 0.114,
0.299, 0.587, 0.114,
0.299, 0.587, 0.114
);
mat3 U = mat3(
0.701, -0.587, -0.114,
-0.299, 0.413, -0.114,
-0.300, -0.588, 0.886
);
mat3 V = mat3(
0.168, -0.331, 0.500,
0.328, 0.035, -0.500,
-0.497, 0.296, 0.201
);
return W + U * c + V * s;
}
void main(){
vec2 f = (gl_FragCoord.xy - 0.5 * iResolution.xy - uOffsetPx) * uPxScale;
float z = 5.0;
float d = 0.0;
vec3 p;
vec4 o = vec4(0.0);
float centerShift = uCenterShift;
float cf = uColorFreq;
mat2 wob = mat2(1.0);
if (uUseBaseWobble == 1) {
float t = iTime * uTimeScale;
float c0 = cos(t + 0.0);
float c1 = cos(t + 33.0);
float c2 = cos(t + 11.0);
wob = mat2(c0, c1, c2, c0);
}
const int STEPS = 100;
for (int i = 0; i < STEPS; i++) {
p = vec3(f, z);
p.xz = p.xz * wob;
p = uRot * p;
vec3 q = p;
q.y += centerShift;
d = 0.1 + 0.2 * abs(sdPyramidUpInv(q));
z -= d;
o += (sin((p.y + z) * cf + vec4(0.0, 1.0, 2.0, 3.0)) + 1.0) / d;
}
o = tanh4(o * o * (uGlow * uBloom) / 1e5);
vec3 col = o.rgb;
float n = rand(gl_FragCoord.xy + vec2(iTime));
col += (n - 0.5) * uNoise;
col = clamp(col, 0.0, 1.0);
float L = dot(col, vec3(0.2126, 0.7152, 0.0722));
col = clamp(mix(vec3(L), col, uSaturation), 0.0, 1.0);
if(abs(uHueShift) > 0.0001){
col = clamp(hueRotation(uHueShift) * col, 0.0, 1.0);
}
gl_FragColor = vec4(col, o.a);
}
`;
const geometry = new Triangle(gl);
const iResBuf = new Float32Array(2);
const offsetPxBuf = new Float32Array(2);
const program = new Program(gl, {
vertex,
fragment,
uniforms: {
iResolution: { value: iResBuf },
iTime: { value: 0 },
uHeight: { value: H },
uBaseHalf: { value: BASE_HALF },
uUseBaseWobble: { value: 1 },
uRot: { value: new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]) },
uGlow: { value: GLOW },
uOffsetPx: { value: offsetPxBuf },
uNoise: { value: NOISE },
uSaturation: { value: SAT },
uScale: { value: SCALE },
uHueShift: { value: HUE },
uColorFreq: { value: CFREQ },
uBloom: { value: BLOOM },
uCenterShift: { value: H * 0.25 },
uInvBaseHalf: { value: 1 / BASE_HALF },
uInvHeight: { value: 1 / H },
uMinAxis: { value: Math.min(BASE_HALF, H) },
uPxScale: {
value: 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE)
},
uTimeScale: { value: TS }
}
});
const mesh = new Mesh(gl, { geometry, program });
const resize = () => {
const w = container.clientWidth || 1;
const h = container.clientHeight || 1;
renderer.setSize(w, h);
iResBuf[0] = gl.drawingBufferWidth;
iResBuf[1] = gl.drawingBufferHeight;
offsetPxBuf[0] = offX * dpr;
offsetPxBuf[1] = offY * dpr;
program.uniforms.uPxScale.value = 1 / ((gl.drawingBufferHeight || 1) * 0.1 * SCALE);
};
const ro = new ResizeObserver(resize);
ro.observe(container);
resize();
const rotBuf = new Float32Array(9);
const setMat3FromEuler = (yawY, pitchX, rollZ, out) => {
const cy = Math.cos(yawY), sy = Math.sin(yawY);
const cx = Math.cos(pitchX), sx = Math.sin(pitchX);
const cz = Math.cos(rollZ), sz = Math.sin(rollZ);
out[0] = cy * cz + sy * sx * sz;
out[1] = cx * sz;
out[2] = -sy * cz + cy * sx * sz;
out[3] = -cy * sz + sy * sx * cz;
out[4] = cx * cz;
out[5] = sy * sz + cy * sx * cz;
out[6] = sy * cx;
out[7] = -sx;
out[8] = cy * cx;
return out;
};
const NOISE_IS_ZERO = NOISE < 1e-6;
let raf = 0;
const t0 = performance.now();
const startRAF = () => {
if (raf) return;
raf = requestAnimationFrame(render);
};
const stopRAF = () => {
if (!raf) return;
cancelAnimationFrame(raf);
raf = 0;
};
const rnd = () => Math.random();
const wX = (0.3 + rnd() * 0.6) * RSX;
const wY = (0.2 + rnd() * 0.7) * RSY;
const wZ = (0.1 + rnd() * 0.5) * RSZ;
const phX = rnd() * Math.PI * 2;
const phZ = rnd() * Math.PI * 2;
let yaw = 0, pitch = 0, roll = 0;
let targetYaw = 0, targetPitch = 0;
const lerp = (a, b, t) => a + (b - a) * t;
const pointer = { x: 0, y: 0, inside: true };
const onMove = e => {
const ww = Math.max(1, window.innerWidth);
const wh = Math.max(1, window.innerHeight);
const cx = ww * 0.5;
const cy = wh * 0.5;
pointer.x = Math.max(-1, Math.min(1, (e.clientX - cx) / cx));
pointer.y = Math.max(-1, Math.min(1, (e.clientY - cy) / cy));
pointer.inside = true;
};
const onLeave = () => { pointer.inside = false; };
const onBlur = () => { pointer.inside = false; };
let onPointerMove = null;
if (animationType === 'hover') {
onPointerMove = e => {
onMove(e);
startRAF();
};
window.addEventListener('pointermove', onPointerMove, { passive: true });
window.addEventListener('mouseleave', onLeave);
window.addEventListener('blur', onBlur);
program.uniforms.uUseBaseWobble.value = 0;
} else if (animationType === '3drotate') {
program.uniforms.uUseBaseWobble.value = 0;
} else {
program.uniforms.uUseBaseWobble.value = 1;
}
const render = t => {
const time = (t - t0) * 0.001;
program.uniforms.iTime.value = time;
let continueRAF = true;
if (animationType === 'hover') {
const maxPitch = 0.6 * HOVSTR;
const maxYaw = 0.6 * HOVSTR;
targetYaw = (pointer.inside ? -pointer.x : 0) * maxYaw;
targetPitch = (pointer.inside ? pointer.y : 0) * maxPitch;
yaw = lerp(yaw, targetYaw, INERT);
pitch = lerp(pitch, targetPitch, INERT);
roll = lerp(roll, 0, 0.1);
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);
if (NOISE_IS_ZERO && Math.abs(yaw - targetYaw) < 1e-4 && Math.abs(pitch - targetPitch) < 1e-4 && Math.abs(roll) < 1e-4) {
continueRAF = false;
}
} else if (animationType === '3drotate') {
const tScaled = time * TS;
yaw = tScaled * wY;
pitch = Math.sin(tScaled * wX + phX) * 0.6;
roll = Math.sin(tScaled * wZ + phZ) * 0.5;
program.uniforms.uRot.value = setMat3FromEuler(yaw, pitch, roll, rotBuf);
if (TS < 1e-6) continueRAF = false;
} else {
rotBuf[0] = 1; rotBuf[1] = 0; rotBuf[2] = 0;
rotBuf[3] = 0; rotBuf[4] = 1; rotBuf[5] = 0;
rotBuf[6] = 0; rotBuf[7] = 0; rotBuf[8] = 1;
program.uniforms.uRot.value = rotBuf;
if (TS < 1e-6) continueRAF = false;
}
renderer.render({ scene: mesh });
if (continueRAF) {
raf = requestAnimationFrame(render);
} else {
raf = 0;
}
};
if (suspendWhenOffscreen) {
const io = new IntersectionObserver(entries => {
const vis = entries.some(e => e.isIntersecting);
if (vis) startRAF();
else stopRAF();
});
io.observe(container);
startRAF();
container.__prismIO = io;
} else {
startRAF();
}
return () => {
stopRAF();
ro.disconnect();
if (animationType === 'hover' && onPointerMove) {
window.removeEventListener('pointermove', onPointerMove);
window.removeEventListener('mouseleave', onLeave);
window.removeEventListener('blur', onBlur);
}
if (suspendWhenOffscreen && container.__prismIO) {
container.__prismIO.disconnect();
delete container.__prismIO;
}
if (gl.canvas.parentElement === container) {
container.removeChild(gl.canvas);
}
};
}

View File

@@ -1,4 +1,5 @@
import { observeReveals } from './animations/reveal-observer.js';
import { initGlassSurface } from './animations/glass-surface.js';
import { initHeroSection } from './sections/hero.js';
import { initTestimonialsSection } from './sections/testimonials.js';
import { initPartnersSection } from './sections/partners.js';
@@ -8,6 +9,21 @@ import { initStatsSection } from './sections/stats.js';
function initApp() {
observeReveals();
initGlassSurface('.nav-links, .discord-copy-bar, .platform-pill, .hero-cta-light', {
borderRadius: 50,
borderWidth: 0,
backgroundOpacity: 0,
saturation: 1,
brightness: 0,
opacity: 1,
blur: 15,
displace: 0.5,
distortionScale: -180,
redOffset: -50,
greenOffset: -50,
blueOffset: 20
});
initHeroSection();
initStatsSection();
initTestimonialsSection();

View File

@@ -1,95 +1,48 @@
import { qs, isMobileViewport } from '../utils/dom.js';
import { qs } from '../utils/dom.js';
import { copyToClipboard, flashFeedback, flashClass } from '../utils/clipboard.js';
import { typewriterReveal } from '../animations/typewriter.js';
import { initFluidByteField } from '../animations/fluid-byte-field.js';
import { initPrism } from '../animations/prism.js';
import { initLightRays } from '../animations/light-rays.js';
import { DISCORD_LOADER_SCRIPT, COPY_FEEDBACK_DURATION_MS } from '../config/constants.js';
const HERO_GREETING = 'Vyla Home';
const HERO_GREETING = 'The Local-First Desktop Media Center';
function initVideoColorExtraction() {
const videos = document.querySelectorAll('.hero-atmosphere-video');
if (videos.length === 0) return;
function initHeroPrism() {
const prismContainer = document.getElementById('hero-prism');
if (!prismContainer) return;
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d', { willReadFrequently: true });
let animationFrameId;
let frameCount = 0;
let currentVideoColor = 'rgb(0, 0, 0)';
function extractColors() {
const video = videos[0];
if (video.readyState < 2) {
animationFrameId = requestAnimationFrame(extractColors);
return;
}
try {
canvas.width = 64;
canvas.height = 64;
ctx.drawImage(video, 0, 0, 64, 64);
const imageData = ctx.getImageData(0, 0, 64, 64);
const data = imageData.data;
let r = 0, g = 0, b = 0;
let count = 0;
for (let i = 0; i < data.length; i += 4) {
r += data[i];
g += data[i + 1];
b += data[i + 2];
count++;
}
r = Math.round(r / count);
g = Math.round(g / count);
b = Math.round(b / count);
currentVideoColor = `rgb(${r}, ${g}, ${b})`;
const dominantColorTransparent = `rgba(${r}, ${g}, ${b}, 0.5)`;
document.documentElement.style.setProperty('--video-dominant-color', currentVideoColor);
document.documentElement.style.setProperty('--video-dominant-color-transparent', dominantColorTransparent);
if (window.scrollY === 0) {
document.documentElement.style.setProperty('--color-bg', currentVideoColor);
}
frameCount++;
if (frameCount % 30 === 0) {
}
} catch (e) {
}
animationFrameId = requestAnimationFrame(extractColors);
}
function handleScroll() {
const scrollY = window.scrollY;
const heroHeight = window.innerHeight;
const scrollProgress = Math.min(scrollY / (heroHeight * 0.5), 1);
const videoColorRGB = currentVideoColor.match(/\d+/g);
if (videoColorRGB) {
const r = Math.round(parseInt(videoColorRGB[0]) * (1 - scrollProgress));
const g = Math.round(parseInt(videoColorRGB[1]) * (1 - scrollProgress));
const b = Math.round(parseInt(videoColorRGB[2]) * (1 - scrollProgress));
const fadedColor = `rgb(${r}, ${g}, ${b})`;
document.documentElement.style.setProperty('--color-bg', fadedColor);
}
}
animationFrameId = requestAnimationFrame(extractColors);
window.addEventListener('scroll', handleScroll, { passive: true });
handleScroll();
videos.forEach(video => {
video.addEventListener('pause', () => {
cancelAnimationFrame(animationFrameId);
initPrism(prismContainer, {
animationType: 'rotate',
timeScale: 0.5,
height: 3.5,
baseWidth: 5.5,
scale: 3.6,
hueShift: 0,
colorFrequency: 1,
noise: 0.5,
glow: 1,
transparent: true
});
}
video.addEventListener('play', () => {
animationFrameId = requestAnimationFrame(extractColors);
});
function initCtaRays() {
const raysContainer = document.getElementById('cta-rays');
if (!raysContainer) return;
initLightRays(raysContainer, {
raysOrigin: 'bottom-center',
rayColor1: '#EAB308',
rayColor2: '#96c8ff',
blend: 0.65,
raysSpeed: 1.8,
lightSpread: 0.7,
rayLength: 2.5,
followMouse: true,
mouseInfluence: 0.12,
noiseAmount: 0.08,
distortion: 0.04,
fadeDistance: 2.0,
saturation: 1.4
});
}
@@ -100,7 +53,6 @@ function initHeroTitle() {
function initDiscordCopyBar() {
const copyBtn = document.getElementById('copy-discord-btn');
const copyLabel = document.getElementById('copy-discord-label');
const copyIcon = document.getElementById('copy-discord-icon');
if (!copyBtn) return;
@@ -131,11 +83,10 @@ function initCtaCopyButton() {
}
export function initHeroSection() {
initHeroPrism();
initCtaRays();
initHeroTitle();
initDiscordCopyBar();
initHeroCopyButton();
initCtaCopyButton();
initVideoColorExtraction();
initFluidByteField('hero-byte-overlay');
initFluidByteField('cta-byte-overlay');
}

View File

@@ -27,8 +27,11 @@
.hero-cta-row {
display: flex;
align-items: center;
gap: 0.75rem;
margin-top: 1rem;
justify-content: center;
gap: 1rem;
margin-top: 1.5rem;
position: relative;
z-index: 10;
}
.hero-cta {
@@ -70,12 +73,19 @@
}
.hero-cta-light {
background: rgba(255, 255, 255, 0.92);
color: #0f0f0f;
position: relative;
background: transparent;
color: var(--color-fg);
border-radius: 50px;
padding: 0.75rem 1.75rem;
font-weight: 500;
font-size: 0.875rem;
text-decoration: none;
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.hero-cta-light:hover {
background: #fff;
transform: translateY(-1px);
}
@media (hover: none) and (pointer: coarse) {

View File

@@ -0,0 +1,52 @@
.glass-surface {
position: relative !important;
display: inline-flex !important;
align-items: center !important;
justify-content: center !important;
background: transparent !important;
border: none !important;
border-radius: var(--glass-radius, 50px) !important;
overflow: visible !important;
text-decoration: none !important;
box-sizing: border-box !important;
width: auto !important;
max-width: max-content !important;
}
.glass-surface__backdrop {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
border-radius: inherit !important;
background: transparent;
backdrop-filter: var(--glass-filter-id, none) saturate(var(--glass-saturation, 0));
-webkit-backdrop-filter: var(--glass-filter-id, none) saturate(var(--glass-saturation, 0));
pointer-events: none;
z-index: 0;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
}
.glass-surface__content {
position: relative;
z-index: 2;
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: inherit;
width: auto;
height: 100%;
color: inherit;
white-space: nowrap;
border-radius: inherit;
pointer-events: auto;
}
.glass-surface:hover .glass-surface__backdrop {
border-color: rgba(255, 255, 255, 0.45);
box-shadow:
inset 0 1px 2px 0 rgba(255, 255, 255, 0.6),
inset 0 -1px 1px 0 rgba(255, 255, 255, 0.15),
0 6px 20px 0 rgba(0, 0, 0, 0.35);
}

View File

@@ -1,31 +1,47 @@
.discord-copy-bar {
display: inline-flex;
flex-direction: row;
align-items: center;
gap: 0.75rem;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px) saturate(140%);
-webkit-backdrop-filter: blur(8px) saturate(140%);
border: none;
border-radius: var(--radius-pill);
padding: 0.5rem 0.5rem 0.5rem 0.9rem;
font-family: var(--font-body);
font-size: 0.8125rem;
font-weight: 600;
color: var(--color-fg);
justify-content: space-between;
gap: 1rem;
padding: 0.65rem 1.25rem;
border-radius: 50px;
cursor: pointer;
letter-spacing: -0.01em;
margin-top: 1.2rem;
transition: border-color var(--duration-fast) ease, background var(--duration-fast) ease;
margin-top: 1.5rem;
max-width: 90vw;
width: auto;
}
.copy-discord-label,
.discord-copy-actions {
position: relative;
z-index: 2;
}
.copy-discord-label {
position: relative;
max-width: 350px;
font-size: 0.85rem;
color: var(--color-fg-dim);
font-family: ui-monospace, SFMono-Regular, Consolas, monospace;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
color: var(--color-fg);
-webkit-mask-image: linear-gradient(to left, transparent, black 15%);
mask-image: linear-gradient(to left, transparent, black 15%);
}
.discord-copy-icon {
display: flex;
align-items: center;
}
.discord-copy-icon .check-icon {
display: none;
}
.discord-copy-icon.copied .copy-icon {
display: none;
}
.discord-copy-icon.copied .check-icon {
display: block;
}
.discord-copy-actions {

View File

@@ -1,7 +1,9 @@
.footer {
position: relative;
padding: 48px 24px 24px;
overflow: hidden
padding: 32px 24px 24px;
overflow: visible;
background: transparent;
z-index: 2;
}
.footer-content {
@@ -10,20 +12,23 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 40px;
font-weight: 400
font-weight: 400;
position: relative;
z-index: 2;
}
.footer-section h2 {
font-size: 1rem;
font-weight: 700;
margin-bottom: 7px
margin-bottom: 7px;
color: var(--color-fg);
}
.footer-section p {
color: var(--color-muted);
font-size: 14px;
line-height: 1.6;
font-weight: 400
font-weight: 400;
}
.footer-section a {
@@ -34,17 +39,17 @@
font-size: 14px;
margin-bottom: 10px;
transition: color .2s;
font-weight: 400
font-weight: 400;
}
.footer-section a:hover {
color: var(--color-muted)
color: var(--color-fg);
}
.social-links {
display: flex;
flex-direction: column;
font-weight: 400
font-weight: 400;
}
.footer-bottom {
@@ -52,10 +57,12 @@
font-weight: 400;
font-size: 0.8rem;
color: var(--color-muted);
position: relative;
z-index: 2;
}
.footer-bottom p {
color: var(--color-muted);
font-size: 0.8rem;
font-weight: 400
font-weight: 400;
}

View File

@@ -26,13 +26,8 @@
}
.nav-links {
display: flex;
gap: clamp(1.25rem, 3vw, 2.5rem);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(8px) saturate(140%);
-webkit-backdrop-filter: blur(8px) saturate(140%);
padding: 1rem 1rem;
gap: 1.25rem;
padding: 0.75rem 1.5rem;
border-radius: 100px;
}

View File

@@ -1,64 +1,45 @@
.cta-banner {
.cta-footer-wrap {
position: relative;
padding: clamp(4rem, 10vw, 7rem) 0;
overflow: hidden;
isolation: isolate;
background: #000000;
width: 100%;
min-height: 600px;
}
.cta-rays-container {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.cta-title-warp {
position: relative;
width: 100%;
max-width: 800px;
height: clamp(60px, 10vw, 110px);
margin: 0 auto 0.5rem;
overflow: hidden;
isolation: isolate;
}
.cta-banner {
position: relative;
padding: clamp(5rem, 10vw, 8rem) 0 3rem;
overflow: visible;
display: flex;
align-items: center;
justify-content: center;
height: 50vh;
}
.cta-banner-media {
position: absolute;
inset: 0;
z-index: -1;
overflow: hidden;
pointer-events: none;
background: var(--video-dominant-color, black);
-webkit-mask-image:
linear-gradient(to bottom, transparent, black 50%),
linear-gradient(to top, transparent, black 70%);
-webkit-mask-composite: source-in;
mask-composite: intersect;
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-image:
linear-gradient(to bottom, transparent, black 50%),
linear-gradient(to top, transparent, black 65%);
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
.cta-banner-video {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
object-position: center;
}
.cta-banner-overlay {
position: absolute;
inset: 0;
}
#cta-byte-overlay {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
@media (max-width: 768px) {
#cta-byte-overlay {
display: none;
}
background: transparent;
z-index: 2;
}
.cta-banner-content {
position: relative;
z-index: 2;
display: flex;
flex-direction: column;
align-items: center;

View File

@@ -2,18 +2,17 @@
position: relative;
padding: clamp(6rem, 14vh, 8.5rem) 0 clamp(3rem, 6vh, 5rem);
overflow: hidden;
isolation: isolate;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
.hero-atmosphere {
position: absolute;
inset: 0;
z-index: -1;
z-index: 0;
overflow: hidden;
pointer-events: none;
-webkit-mask-image: linear-gradient(to bottom,
var(--video-dominant-color, black) 0%,
var(--video-dominant-color, black) 48%,
@@ -24,78 +23,13 @@
var(--video-dominant-color, black) 48%,
var(--video-dominant-color-transparent, rgba(0, 0, 0, 0.5)) 80%,
transparent 96%);
-webkit-mask-repeat: no-repeat;
-webkit-mask-size: 100% 100%;
mask-repeat: no-repeat;
mask-size: 100% 100%;
}
.hero-atmosphere-video {
position: absolute;
inset: -6%;
width: 112%;
height: 112%;
object-fit: cover;
object-position: center top;
transform: translateZ(0) scale(1.04);
}
.hero-atmosphere-sheen {
position: absolute;
inset: 0;
background: radial-gradient(ellipse at 50% 4%, rgba(255, 255, 255, 0.18), transparent 46%),
linear-gradient(116deg, transparent 18%, rgba(255, 255, 255, 0.06) 37%, transparent 51%);
mix-blend-mode: soft-light;
opacity: 0.8;
animation: hero-sheen 16s ease-in-out infinite alternate;
}
#hero-byte-overlay {
.hero-prism {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
opacity: 0.9;
mix-blend-mode: screen;
pointer-events: none;
}
@keyframes hero-sheen {
0% {
transform: translate3d(-4%, 0, 0) scale(1.04);
}
to {
transform: translate3d(5%, 1%, 0) scale(1.1);
}
}
@media (max-width: 768px) {
#hero-byte-overlay {
display: none;
}
}
@media (prefers-reduced-motion: reduce) {
.hero-atmosphere-sheen {
animation: none;
}
.hero-atmosphere-video,
#hero-byte-overlay {
display: none;
}
}
@media (max-width: 480px) {
.hero {
padding-top: 8rem;
}
}
@media (orientation: landscape) and (max-height: 600px) {
.hero {
height: auto;
height: 100vh;
}
}

View File

@@ -19,6 +19,32 @@
object-fit: contain;
}
.hero-title-warp {
position: relative;
width: 100%;
max-width: 900px;
height: clamp(80px, 14vw, 150px);
margin: 0.5rem auto 1rem;
overflow: hidden;
isolation: isolate;
}
.warp-text {
position: relative;
display: block;
width: 100%;
overflow: hidden;
isolation: isolate;
}
.warp-text canvas {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
display: block;
}
.hero-title {
font-size: 2.3rem;
letter-spacing: -.03em;
@@ -92,24 +118,31 @@
.platform-pill {
display: inline-flex;
flex-direction: row;
align-items: center;
justify-content: center;
gap: 0.5rem;
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(15px) saturate(140%);
-webkit-backdrop-filter: blur(15px) saturate(140%);
border: none;
padding: 0.35rem 0.85rem;
border-radius: var(--radius-pill);
padding: 0.45rem 1.1rem;
border-radius: 50px;
font-size: 0.75rem;
font-weight: 500;
color: var(--color-fg-dim);
color: #ffffff;
margin-bottom: 1.25rem;
position: relative;
z-index: 10;
white-space: nowrap;
width: auto;
max-width: max-content;
}
.platform-pill svg {
color: var(--color-fg);
width: 13px;
height: 13px;
color: #ffffff;
fill: currentColor;
flex-shrink: 0;
}
.platform-pill span {
white-space: nowrap;
}
.hero-screen-mockup {

View File

@@ -3,12 +3,12 @@
transform: translate3d(0, 24px, 0);
transition: opacity var(--duration-slow) var(--ease-standard),
transform var(--duration-slow) var(--ease-standard);
will-change: opacity, transform;
}
.reveal.in-view {
opacity: 1;
transform: translate3d(0, 0, 0);
transform: none;
will-change: auto;
}
.reveal-stagger .reveal:nth-child(1) {

View File

@@ -29,7 +29,7 @@
<meta name="msapplication-tooltip" content="Self-hosted desktop media center for streaming entertainment">
<meta name="msapplication-starturl" content="/">
<meta name="msapplication-window" content="width=device-width;height=device-height">
<meta name="msapplication-navbutton-color" content="#1a1a2e">
<meta name="msapplication-navbutton-color" content="#FFF">
<link rel="icon" href="/assets/images/favicon.ico" type="image/x-icon">
<link rel="icon" type="image/png" sizes="32x32" href="/favicon.ico">
@@ -73,13 +73,13 @@
<link rel="manifest" href="/manifest.json">
<meta name="theme-color" content="#1a1a2e">
<meta name="theme-color" content="#FFF">
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="default">
<meta name="apple-mobile-web-app-title" content="Vyla Home">
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Vyla Home">
<meta name="msapplication-TileColor" content="#1a1a2e">
<meta name="msapplication-TileColor" content="#FFF">
<meta name="msapplication-config" content="/browserconfig.xml">
<link rel="apple-touch-icon" href="favicon.ico">
@@ -300,15 +300,10 @@
</div>
</main>
<div class="cta-footer-wrap">
<div id="cta-rays" class="cta-rays-container" aria-hidden="true"></div>
<section class="cta-banner" aria-labelledby="cta-heading">
<div class="cta-banner-media" aria-hidden="true">
<video class="cta-banner-video hero-atmosphere-video"
src="https://videos.pexels.com/video-files/26754965/12000494_3840_2160_30fps.mp4" autoplay loop muted
playsinline webkit-playsinline preload="auto" crossorigin="anonymous" aria-hidden="true">
</video>
<canvas id="cta-byte-overlay" aria-hidden="true"></canvas>
<div class="cta-banner-overlay"></div>
</div>
<div class="container cta-banner-content reveal">
<h2 id="cta-heading" class="cta-banner-title">Try Vyla Home today</h2>
<p class="cta-banner-subtitle">Your Entertainment. Zero Servers Attached.</p>
@@ -318,25 +313,43 @@
<script>
async function setupLatestDownload() {
const btn = document.getElementById("hero-copy-script-btn");
const buttons = [
document.getElementById("hero-copy-script-btn"),
document.getElementById("cta-copy-script-btn")
];
try {
const project = "vyla-entertainment%2Fhome";
const response = await fetch(
"https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/releases/permalink/latest"
`https://gitlab.com/api/v4/projects/${project}/releases/permalink/latest`
);
if (!response.ok) throw new Error("Failed to fetch latest release");
if (!response.ok) {
throw new Error(`Failed to fetch latest release: ${response.status}`);
}
const release = await response.json();
const version = release.tag_name;
const fileVersion = version.replace(/^v/, "");
btn.href =
`https://gitlab.com/api/v4/projects/vyla-entertainment%2Fhome/packages/generic/vyla-home/${version}/Vyla-Home_${fileVersion}_x64-setup.exe`;
const asset = release.assets?.links?.find(link =>
link.name?.toLowerCase().endsWith(".exe")
);
btn.textContent = `Download Release ${version}`;
if (!asset?.url) {
throw new Error("No .exe asset found in latest release");
}
buttons.forEach(button => {
if (button) {
button.href = asset.url;
}
});
} catch (error) {
btn.textContent = "Download Latest Release";
buttons.forEach(button => {
if (button) {
button.textContent = "Download Latest Release";
}
});
}
}
@@ -347,27 +360,27 @@
<footer class="footer">
<div class="footer-content">
<div class="footer-section">
<h4>Vyla</h4>
<h2>Vyla</h2>
<p>Vyla is a passion project born from a love for film and television.</p>
</div>
<div class="footer-section">
<h4>Backup Site:</h4>
<h2>Backup Site:</h2>
<p><a href="https://vyla.qzz.io" target="_blank" rel="noopener">https://vyla.qzz.io</a>
</p>
</div>
<div class="footer-section">
<h4>Quick Links</h4>
<h2>Quick Links</h2>
<a href="/terms-of-service">Terms of Service</a>
<a href="/privacy-policy">Privacy Policy</a>
<a href="/about-us">About Us</a>
<a href="https://docs.vyla.cc/misc/dmca" target="_blank" rel="noopener">DMCA</a>
</div>
<div class="footer-section">
<h4>Our API</h4>
<h2>Our API</h2>
<a href="https://docs.vyla.cc" target="_blank" rel="noopener">Documentation</a>
</div>
<div class="footer-section">
<h4>Join our Discord</h4>
<h2>Join our Discord</h2>
<div class="social-links">
<a href="/discord" target="_blank" rel="noopener">
<i class="fab fa-discord"></i> Discord
@@ -375,8 +388,8 @@
</div>
</div>
<img src="https://app.greenweb.org/api/v3/greencheckimage/vyla.cc?nocache=true"
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200px"
height="95px">
alt="This website runs on green hosting - verified by thegreenwebfoundation.org" width="200"
height="95" loading="lazy" decoding="async">
</div>
<div class="footer-bottom">
<a href="/fuckinggodihatethisprojectijustwantoenditall" class="secret-footer">
@@ -384,6 +397,7 @@
</a>
</div>
</footer>
</div>
<script type="module" src="/src/scripts/main.js"></script>
</body>