diff --git a/assets/images/features/4.png b/assets/images/features/4.png index 22d4d05..1c43c2b 100644 Binary files a/assets/images/features/4.png and b/assets/images/features/4.png differ diff --git a/assets/images/site_preview.png b/assets/images/site_preview.png new file mode 100644 index 0000000..011fc34 Binary files /dev/null and b/assets/images/site_preview.png differ diff --git a/index.html b/index.html index 9bafae9..9a530c4 100644 --- a/index.html +++ b/index.html @@ -265,8 +265,8 @@
-
- Vyla Home Desktop Media Player -
@@ -552,8 +548,9 @@
diff --git a/src/scripts/animations/fluid-byte-field.js b/src/scripts/animations/fluid-byte-field.js index 682afd4..29d3903 100644 --- a/src/scripts/animations/fluid-byte-field.js +++ b/src/scripts/animations/fluid-byte-field.js @@ -13,14 +13,12 @@ const DIFFUSION_ITERATIONS = 2; const PROJECT_ITERATIONS = 3; const VELOCITY_DAMP = 0.1; const DYE_DAMP = 0.1; -const TRAIL_LIFETIME_MS = 184; +const TRAIL_LIFETIME_MS = 2000; const TRAIL_RADIUS_PX = 100; const TRAIL_EASE = 2.5; const TRAIL_STRENGTH = 3; const TRAIL_FORCE_DECAY = 1.2; const TRAIL_WOBBLE = 0.14; -const DRAG_THRESHOLD_PX = 12; -const MAX_TRAIL_POINTS = 24; export function initFluidByteField(canvasId) { const canvas = document.getElementById(canvasId); @@ -57,11 +55,6 @@ export function initFluidByteField(canvasId) { let accumulator = 0; let running = true; - const trails = []; - let dragStartX = 0; - let dragStartY = 0; - let dragging = false; - function resize() { const rect = container.getBoundingClientRect(); const dpr = Math.min(window.devicePixelRatio || 1, 2); @@ -111,8 +104,16 @@ export function initFluidByteField(canvasId) { function onPointerMove(e) { const rect = container.getBoundingClientRect(); - const px = e.clientX - rect.left; - const py = e.clientY - rect.top; + let px, py; + + if (e.touches && e.touches.length > 0) { + px = e.touches[0].clientX - rect.left; + py = e.touches[0].clientY - rect.top; + } else { + px = e.clientX - rect.left; + py = e.clientY - rect.top; + } + if (cols > 0 && lastPointerX > -9000 && px >= 0 && py >= 0 && px <= rect.width && py <= rect.height) { const elapsed = Math.min(0.05, Math.max(0.001, (e.timeStamp - lastPointerTime) / 1000)); const fx = (px - lastPointerX) / CELL_SIZE / elapsed * ADVECT_SCALE; @@ -129,78 +130,7 @@ export function initFluidByteField(canvasId) { lastPointerY = -9999; } - function onPointerDown(e) { - const rect = container.getBoundingClientRect(); - const px = e.clientX - rect.left; - const py = e.clientY - rect.top; - dragStartX = px; - dragStartY = py; - dragging = px >= 0 && py >= 0 && px <= rect.width && py <= rect.height; - } - - function onPointerUp(e) { - if (!dragging) return; - dragging = false; - const rect = container.getBoundingClientRect(); - const px = e.clientX - rect.left; - const py = e.clientY - rect.top; - if (Math.hypot(px - dragStartX, py - dragStartY) > DRAG_THRESHOLD_PX) return; - trails.push({ x: px, y: py, age: 0, seed: Math.random() * Math.PI * 2 }); - if (trails.length > MAX_TRAIL_POINTS) trails.shift(); - } - - function applyTrails() { - if (!trails.length) return; - const lifetimeSec = TRAIL_LIFETIME_MS / 1000; - const cellsPerRadius = TRAIL_RADIUS_PX / CELL_SIZE; - for (let i = trails.length - 1; i >= 0; i--) { - const trail = trails[i]; - trail.age += DT; - const progress = trail.age / lifetimeSec; - if (progress >= 1) { - trails.splice(i, 1); - continue; - } - const eased = 1 - Math.pow(1 - progress, TRAIL_EASE); - const ringRadius = eased * TRAIL_RADIUS_PX / CELL_SIZE; - const remaining = 1 - progress; - const intensity = 0.35 + 0.65 * eased; - const force = TRAIL_STRENGTH * Math.pow(remaining, TRAIL_FORCE_DECAY); - const dyeAmount = 0.2 * Math.pow(remaining, 2); - const cx = trail.x / CELL_SIZE; - const cy = trail.y / CELL_SIZE; - const seed = trail.seed; - const band = ringRadius * 1.3 + cellsPerRadius; - const xMin = Math.max(1, Math.floor(cx - band)); - const xMax = Math.min(cols - 2, Math.ceil(cx + band)); - const yMin = Math.max(1, Math.floor(cy - band)); - const yMax = Math.min(rows - 2, Math.ceil(cy + band)); - for (let y = yMin; y <= yMax; y++) { - for (let x = xMin; x <= xMax; x++) { - const dx = x - cx; - const dy = y - cy; - const dist = Math.sqrt(dx * dx + dy * dy); - const angle = Math.atan2(dy, dx); - const wobble = 0.5 * Math.sin(6 * angle + seed) + 0.25 * Math.sin(3 * angle + 0.05 * dist + 0.3 * seed) + 0.5; - const wobbleAmount = TRAIL_WOBBLE * (2 * wobble - 1) * intensity; - const targetRadius = ringRadius * Math.min(1.6, Math.max(0.6, 1 + wobbleAmount)); - const ringDist = Math.abs(dist - targetRadius) / cellsPerRadius; - if (ringDist >= 1) continue; - const falloff = smoothstep(1 - ringDist); - const forceMul = Math.min(2.2, Math.max(0.4, 1 + wobbleAmount)); - const invDist = dist > 0.001 ? 1 / dist : 0; - const idx = x + y * cols; - velX[idx] += dx * invDist * force * forceMul * falloff; - velY[idx] += dy * invDist * force * forceMul * falloff; - dye[idx] = Math.min(1.5, dye[idx] + dyeAmount * forceMul * falloff); - if (Math.random() < 0.03 * falloff) glyphs[idx] = Math.floor(Math.random() * 256); - } - } - } - } - function step() { - applyTrails(); const visc = 1.5 * DT; velX0.set(velX); diffuse(velX, velX0, visc, cols, rows, DT, DIFFUSION_ITERATIONS); @@ -255,18 +185,18 @@ export function initFluidByteField(canvasId) { videoLuminance = (r * 0.299 + g * 0.587 + b * 0.114) / 255; } - const baseAlpha = 0.04 + videoLuminance * 0.22; + const baseAlpha = 0; if (d <= 0.02) { ctx.shadowBlur = 0; ctx.globalAlpha = baseAlpha; - ctx.fillStyle = 'rgba(0, 0, 0, 0.45)'; + ctx.fillStyle = 'rgba(255, 255, 255)'; ctx.fillText(hex, x * CELL_SIZE + half, y * CELL_SIZE + half); } else { - const alpha = d * FADE_RATE; - const activeAlpha = Math.min(alpha + baseAlpha, MAX_ALPHA); + const alpha = d * 1.5; + const activeAlpha = Math.min(alpha + baseAlpha, 1.0); ctx.globalAlpha = activeAlpha; ctx.fillStyle = '#ffffff'; - ctx.shadowColor = '#ffffff'; + ctx.shadowBlur = 0; ctx.fillText(hex, x * CELL_SIZE + half, y * CELL_SIZE + half); } } @@ -304,8 +234,8 @@ export function initFluidByteField(canvasId) { new ResizeObserver(resize).observe(container); window.addEventListener('pointermove', onPointerMove, { passive: true }); window.addEventListener('pointerleave', onPointerLeave); - window.addEventListener('pointerdown', onPointerDown, { passive: true }); - window.addEventListener('pointerup', onPointerUp, { passive: true }); + window.addEventListener('touchmove', onPointerMove, { passive: true }); + window.addEventListener('touchend', onPointerLeave); document.addEventListener('visibilitychange', onVisibilityChange); resize(); diff --git a/src/scripts/sections/hero.js b/src/scripts/sections/hero.js index 2ee46ae..034c06e 100644 --- a/src/scripts/sections/hero.js +++ b/src/scripts/sections/hero.js @@ -6,6 +6,101 @@ import { DISCORD_LOADER_SCRIPT, COPY_FEEDBACK_DURATION_MS } from '../config/cons const HERO_GREETING = 'Vyla Home'; +function initVideoColorExtraction() { + const videos = document.querySelectorAll('.hero-atmosphere-video'); + if (videos.length === 0) return; + + console.log('Video color extraction initialized, found', videos.length, 'videos'); + + 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) { + console.log('Video not ready, readyState:', video.readyState); + 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) { + console.log('Frame', frameCount, '- Extracted color:', currentVideoColor); + console.log('CSS variable set:', getComputedStyle(document.documentElement).getPropertyValue('--video-dominant-color')); + } + } catch (e) { + console.error('Error extracting video colors:', 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', () => { + console.log('Video paused, stopping color extraction'); + cancelAnimationFrame(animationFrameId); + }); + + video.addEventListener('play', () => { + console.log('Video playing, starting color extraction'); + animationFrameId = requestAnimationFrame(extractColors); + }); + }); +} + function initHeroTitle() { const heroTitle = qs('.hero-title'); if (heroTitle) typewriterReveal(heroTitle, HERO_GREETING); @@ -48,8 +143,7 @@ export function initHeroSection() { initDiscordCopyBar(); initHeroCopyButton(); initCtaCopyButton(); - if (!isMobileViewport()) { - initFluidByteField('hero-byte-overlay'); - initFluidByteField('cta-byte-overlay'); - } + initVideoColorExtraction(); + initFluidByteField('hero-byte-overlay'); + initFluidByteField('cta-byte-overlay'); } \ No newline at end of file diff --git a/src/styles/sections/cta-banner.css b/src/styles/sections/cta-banner.css index 7981915..20042d0 100644 --- a/src/styles/sections/cta-banner.css +++ b/src/styles/sections/cta-banner.css @@ -15,6 +15,7 @@ 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%); diff --git a/src/styles/sections/hero-atmosphere.css b/src/styles/sections/hero-atmosphere.css index e864635..c648a24 100644 --- a/src/styles/sections/hero-atmosphere.css +++ b/src/styles/sections/hero-atmosphere.css @@ -15,14 +15,14 @@ overflow: hidden; pointer-events: none; -webkit-mask-image: linear-gradient(to bottom, - black 0%, - black 48%, - rgba(0, 0, 0, 0.5) 80%, + var(--video-dominant-color, black) 0%, + var(--video-dominant-color, black) 48%, + var(--video-dominant-color-transparent, rgba(0, 0, 0, 0.5)) 80%, transparent 96%); mask-image: linear-gradient(to bottom, - black 0%, - black 48%, - rgba(0, 0, 0, 0.5) 80%, + var(--video-dominant-color, black) 0%, + 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%; @@ -50,13 +50,6 @@ animation: hero-sheen 16s ease-in-out infinite alternate; } -.hero-atmosphere-vignette { - position: absolute; - inset: 0; - background: linear-gradient(to bottom, transparent 15%, rgba(0, 0, 0, 0.15) 55%, rgba(0, 0, 0, 0.95) 100%), - radial-gradient(ellipse at center, transparent 40%, rgba(0, 0, 0, 0.2) 100%); -} - #hero-byte-overlay { position: absolute; inset: 0; @@ -103,7 +96,6 @@ @media (orientation: landscape) and (max-height: 600px) { .hero { height: auto; - min-height: 100svh; - padding-top: 5rem; + height: 100vh; } } \ No newline at end of file diff --git a/src/styles/sections/hero-content.css b/src/styles/sections/hero-content.css index 8a56faa..8dba204 100644 --- a/src/styles/sections/hero-content.css +++ b/src/styles/sections/hero-content.css @@ -95,8 +95,8 @@ align-items: center; gap: 0.5rem; background: rgba(255, 255, 255, 0.1); - backdrop-filter: blur(8px) saturate(140%); - -webkit-backdrop-filter: blur(8px) saturate(140%); + 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); @@ -104,6 +104,8 @@ font-weight: 500; color: var(--color-fg-dim); margin-bottom: 1.25rem; + position: relative; + z-index: 10; } .platform-pill svg { diff --git a/src/styles/utilities/responsive.css b/src/styles/utilities/responsive.css index 969764c..8fa8a78 100644 --- a/src/styles/utilities/responsive.css +++ b/src/styles/utilities/responsive.css @@ -16,7 +16,7 @@ @media (orientation: landscape) and (min-width: 600px) and (max-height: 500px) { .container { - width: 95%; + width: 70%; padding-inline: 0; }