diff --git a/web/README.md b/web/README.md index 6a31a5c..f6d54ad 100644 --- a/web/README.md +++ b/web/README.md @@ -2,6 +2,8 @@ A two-page website for [LinuxPlayer](https://github.com/teamkrope/linuxplayer). It uses real application screenshots, the final bird mark shared with the desktop app, and an illustrative frame for the canvas demo. The site is self-contained and has no runtime or build dependencies beyond Node.js. +`index.html` presents the player as a product showcase. Shared components and the download page use `styles.css`; homepage presentation lives in `showcase.css`. `motion.js` handles blur reveals and subtle scene movement, with a reduced-motion fallback. Keep the real screenshots and interactive gallery/ratio demo intact when changing the layout. + ## Preview locally ```bash diff --git a/web/index.html b/web/index.html index 1a87322..a86947d 100644 --- a/web/index.html +++ b/web/index.html @@ -20,6 +20,7 @@ + @@ -33,33 +34,28 @@
-

Video playback,
refined for Linux.

-
Actual LinuxPlayer 1.0 window playing a local video with controls visible
+
LinuxPlayer

Get lost in the video.
Not the player.

Beautiful playback, built for Linux.

+
Actual LinuxPlayer 1.0 window playing a local video with controls visible
+
-
-

Everything within reach.

-
Actual LinuxPlayer screenshot with playback controls hidden
-

Focused playback

Seek, skip, loop, change speed, and save frames.

-

Floating mini-player

Keep the video visible while you work.

-

Audio & subtitles

Choose tracks or load subtitles.

-

Desktop integration

Open files from your desktop and set LinuxPlayer as default.

-

Responsive feedback

Subtle cues confirm every action.

-

Personal controls

Shortcuts, canvas ratios, and your system accent.

-
+
+
The viewing experience

Only the video,
when you want it.

+
Actual LinuxPlayer screenshot with playback controls hidden
+
01

Controls on demand.

They disappear as playback begins.

02

Stay in the picture.

Lock the interface to show only video.

03

Every action feels clear.

Subtle feedback for play, pause, and seeking.

-

Watch while you work.

-
Files
Project notes Media Downloads
Workspace
Today’s work
Actual LinuxPlayer mini-player screenshot floating over an illustrative desktop workspace
+
Mini-player

Your video
comes along.

Keep watching in a compact window while you work.

+
Files
Project notes Media Downloads
Workspace
Today’s work
Actual LinuxPlayer mini-player screenshot floating over an illustrative desktop workspace
-

Preview any shape.

-
+
Canvas preview

One video.
Every frame.

Try a new canvas ratio without changing the file.

+
Source video16:9 · unchanged
Illustrative widescreen frame showing a person on a coastal overlook and a lighthouse
Canvas preview16:9 · Fit
Same illustrative frame inside the selected canvas
Canvas ratio
Video in canvas
-

Preview only. Your video stays untouched. Feature guide

+

Your video stays untouched. Feature guide

-

See the player.

+
A closer look

See it in action.

Install LinuxPlayer.

64-bit x86 .deb package

Terminal install
curl -fsSLo /tmp/linuxplayer-install.sh \
   https://github.com/teamkrope/linuxplayer/releases/latest/download/install.sh
 bash /tmp/linuxplayer-install.sh
diff --git a/web/motion.js b/web/motion.js index 4a65dd8..67dfbc8 100644 --- a/web/motion.js +++ b/web/motion.js @@ -8,11 +8,10 @@ if (!('IntersectionObserver' in window) || window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; const targets = document.querySelectorAll([ - 'main .hero-copy', 'main .hero-shot', 'main .section-head', - 'main .feature-shot', 'main .feature-list article', - 'main .pip-copy', 'main .workspace-figure', 'main .ratio-demo', - 'main .gallery', 'main .download-grid > *', 'main .source-grid > *', - 'main .faq-grid > *', 'main .package-card', 'main .download-help-grid > *' + 'main [data-reveal]', 'main .packages-section .section-head', + 'main .download-grid > *', 'main .source-grid > *', + 'main .faq-grid > *', 'main .package-card', + 'main .download-help-grid > *' ].join(', ')); targets.forEach(target => { @@ -21,7 +20,7 @@ if (target.getBoundingClientRect().top < window.innerHeight * 0.94) { target.classList.add('is-visible'); } - if (target.matches('.feature-list article')) { + if (target.matches('.focus-points article')) { const index = [...target.parentElement.children].indexOf(target); target.style.setProperty('--reveal-delay', `${Math.min(index, 5) * 55}ms`); } @@ -36,4 +35,25 @@ }); }, { threshold: 0.08, rootMargin: '0px 0px -6% 0px' }); targets.forEach(target => observer.observe(target)); + + const scenes = [...document.querySelectorAll('[data-scroll-scene]')]; + if (!scenes.length) return; + let scheduled = false; + const updateScenes = () => { + const viewport = window.innerHeight; + scenes.forEach(scene => { + const rect = scene.getBoundingClientRect(); + const progress = Math.max(0, Math.min(1, (viewport - rect.top) / (viewport + rect.height))); + scene.style.setProperty('--scene-shift', `${(-18 * progress).toFixed(1)}px`); + }); + scheduled = false; + }; + const scheduleScenes = () => { + if (scheduled) return; + scheduled = true; + requestAnimationFrame(updateScenes); + }; + window.addEventListener('scroll', scheduleScenes, { passive: true }); + window.addEventListener('resize', scheduleScenes); + scheduleScenes(); })(); diff --git a/web/scripts/build.mjs b/web/scripts/build.mjs index 0de8228..4ed85a6 100644 --- a/web/scripts/build.mjs +++ b/web/scripts/build.mjs @@ -19,6 +19,7 @@ await rm(output, { recursive: true, force: true }); await mkdir(output, { recursive: true }); await cp(path.join(root, 'assets'), path.join(output, 'assets'), { recursive: true }); await cp(path.join(root, 'styles.css'), path.join(output, 'styles.css')); +await cp(path.join(root, 'showcase.css'), path.join(output, 'showcase.css')); await cp(path.join(root, 'script.js'), path.join(output, 'script.js')); await cp(path.join(root, 'motion.js'), path.join(output, 'motion.js')); await cp(path.join(root, 'download.js'), path.join(output, 'download.js')); diff --git a/web/scripts/check.mjs b/web/scripts/check.mjs index 082a677..d2cfed3 100644 --- a/web/scripts/check.mjs +++ b/web/scripts/check.mjs @@ -10,13 +10,15 @@ if ([html, download].some(page => page.includes('Download for Linux'))) throw ne if (!html.includes('playback-v1-coast.png') || !script.includes('options-v1-coast.png')) throw new Error('Current screenshots are not linked from the site'); if (html.includes('Start screen') || script.includes('empty-v1-coast.png')) throw new Error('Removed gallery tab is still present'); if (![html, download].every(page => page.includes('motion.js') && page.includes('footer-line'))) throw new Error('Shared motion or compact footer missing'); +if (!html.includes('showcase.css') || !html.includes('class="hero-shot"') || !html.includes('data-scroll-scene')) throw new Error('Product showcase is missing'); +if (!(html.indexOf('class="hero-copy"') < html.indexOf('class="hero-shot"') && html.indexOf('class="hero-shot"') < html.indexOf('class="hero-actions"'))) throw new Error('Hero must show title, product image, then actions'); for (const text of ['LinuxPlayer', 'id="download"', 'id="features"', 'id="preview"', 'id="open-source"', 'application/ld+json', 'releases/latest/download/install.sh']) { if (!html.includes(text)) throw new Error(`Missing required site content: ${text}`); } for (const asset of ['linuxplayer_amd64.deb', 'linuxplayer_arm64.deb']) { if (!download.includes(`/releases/latest/download/${asset}`)) throw new Error(`Missing latest direct download: ${asset}`); } -for (const file of ['styles.css', 'motion.js', 'script.js', 'download.js', 'robots.txt', 'LICENSE', 'assets/linuxplayer-bird.png', 'assets/external-link.svg', 'assets/ratio-source.webp', 'assets/screenshots/playback-v1-coast.png', 'assets/screenshots/options-v1-coast.png', 'assets/screenshots/hidden-v1-coast.png', 'assets/screenshots/mini-player-v1-coast.png']) { +for (const file of ['styles.css', 'showcase.css', 'motion.js', 'script.js', 'download.js', 'robots.txt', 'LICENSE', 'assets/linuxplayer-bird.png', 'assets/external-link.svg', 'assets/ratio-source.webp', 'assets/screenshots/playback-v1-coast.png', 'assets/screenshots/options-v1-coast.png', 'assets/screenshots/hidden-v1-coast.png', 'assets/screenshots/mini-player-v1-coast.png']) { if (!(await stat(path.join(root, file))).isFile()) throw new Error(`Missing asset: ${file}`); } console.log('Site content and required assets verified.'); diff --git a/web/showcase.css b/web/showcase.css new file mode 100644 index 0000000..285bd56 --- /dev/null +++ b/web/showcase.css @@ -0,0 +1,151 @@ +/* Homepage presentation. The shared styles and download page live in styles.css. */ +:root { --showcase-ease: cubic-bezier(.22, 1, .36, 1); } + +.site-header { background: rgba(8, 18, 27, .82); border-color: rgba(225, 248, 245, .1); } +.nav-links a { color: #becbca; } +.header-download { border-radius: 999px; padding-inline: 19px; } +.section-kicker { display: block; color: var(--accent); font-size: 12px; font-weight: 750; letter-spacing: .16em; text-transform: uppercase; } +.hero h1 em, .focus-intro h2 em, .canvas-head h2 em { font-style: normal; color: var(--accent); } + +.hero { position: relative; overflow: hidden; padding: 116px 0 126px; background: radial-gradient(ellipse 55% 35% at 50% 50%, rgba(62, 171, 160, .15), transparent 90%), #07131d; } +.hero-inner { display: flex; flex-direction: column; align-items: center; gap: 0; } +.hero-copy { padding: 0; text-align: center; max-width: 1050px; position: relative; z-index: 1; } +.hero-copy h1 { font-size: clamp(62px, 8.1vw, 120px); line-height: .99; letter-spacing: -.078em; max-width: none; margin: 24px 0 24px; font-weight: 650; } +.hero-copy p { max-width: none; color: #adc1c3; font-size: clamp(17px, 2vw, 21px); } +.hero-shot { width: min(100%, 1160px); margin: 76px auto 0; position: relative; isolation: isolate; } +.hero-shot::before { content: ""; position: absolute; inset: 12% 4% -8%; z-index: -1; border-radius: 45%; background: #48c6b2; filter: blur(95px); opacity: .17; } +.hero .shot-frame { padding: 9px; border: 1px solid #547b7b; border-radius: 23px; background: #0e202c; box-shadow: 0 48px 100px #0009, 0 0 0 1px #ffffff0d inset; overflow: hidden; } +.hero .shot-frame img { width: 100%; border-radius: 14px; transform: translateY(var(--scene-shift, 0px)) scale(1.025); will-change: transform; } +.hero-actions { justify-content: center; margin: 46px 0 0; gap: 14px; } +.hero-actions .button { min-height: 54px; padding: 15px 26px; border-radius: 999px; font-size: 14px; } +.hero-actions .button-secondary { background: rgba(25, 49, 61, .7); border-color: #496a6b; } + +.section { padding-block: clamp(112px, 12vw, 190px); } +.features-section { background: #0a1923; } +.focus-layout { display: grid; justify-items: center; } +.focus-intro { max-width: 920px; text-align: center; margin-bottom: 65px; } +.focus-intro h2 { font-size: clamp(53px, 7.1vw, 104px); line-height: 1.03; letter-spacing: -.07em; margin: 23px 0 0; } +.focus-visual { width: 100%; max-width: 1130px; overflow: hidden; position: relative; border: 1px solid #56716c; border-radius: 23px; box-shadow: 0 40px 95px #0008; background: #080f16; } +.focus-visual img { width: 100%; transform: translateY(var(--scene-shift, 0px)) scale(1.035); will-change: transform; } +.focus-points { display: grid; grid-template-columns: repeat(3, 1fr); gap: 42px; width: 100%; max-width: 1130px; margin-top: 66px; } +.focus-points article { padding-top: 24px; border-top: 1px solid #4f6668; } +.focus-points span { display: block; color: var(--accent); font-size: 11px; font-weight: 750; letter-spacing: .14em; margin-bottom: 19px; } +.focus-points h3 { font-size: clamp(20px, 2vw, 26px); letter-spacing: -.035em; line-height: 1.2; margin-bottom: 9px; } +.focus-points p { font-size: 14px; line-height: 1.55; } + +.pip-section { background: #10242e; border: 0; } +.pip-grid { grid-template-columns: .7fr 1.3fr; gap: clamp(36px, 6vw, 95px); } +.pip-copy h2 { font-size: clamp(55px, 6.4vw, 96px); line-height: 1.02; letter-spacing: -.068em; margin: 22px 0 25px; } +.pip-copy p { font-size: 17px; line-height: 1.55; } +.workspace-stage { height: 560px; border-radius: 24px; border-color: #5a7880; background: radial-gradient(circle at 70% 20%, #305467, #142b3b 48%, #0d1d2c); box-shadow: 0 36px 85px #0006; } +.pip-capture { transform: translateY(var(--scene-shift, 0px)) rotate(-2deg); width: min(62%, 470px); border-radius: 8px; box-shadow: 0 28px 70px #000d; will-change: transform; } + +.canvas-section { background: #0b1721; } +.canvas-head { text-align: center; max-width: 900px; margin: 0 auto 62px; } +.canvas-head h2 { font-size: clamp(55px, 7vw, 100px); line-height: 1.02; margin: 22px 0 20px; } +.canvas-head p { margin: auto; font-size: 17px; } +.ratio-demo { border-radius: 23px; border-color: #527174; background: #122733; box-shadow: 0 30px 85px #0006; } +.demo-source, .demo-preview { padding: 32px; } +.source-frame, .ratio-stage { border-radius: 12px; border-color: #405c66; height: 390px; } +.demo-controls { padding: 25px 32px; } +.demo-note { text-align: center; margin-top: 25px; } + +.preview-section { background: #10222d; } +.preview-section .section-head { max-width: none; text-align: center; margin-bottom: 57px; } +.preview-section .section-head h2 { font-size: clamp(55px, 7vw, 100px); margin: 21px 0 0; } +.gallery { border-radius: 23px; border-color: #506b70; box-shadow: 0 30px 85px #0007; } +.gallery-tabs { background: #142b39; padding: 17px; gap: 8px; } +.gallery-tabs button { padding: 10px 19px; border-radius: 999px; font-size: 13px; } +.gallery-tabs button.active { background: var(--accent); color: #10201b; } +.gallery-image-wrap { height: min(58vw, 655px); padding: 36px; background: #07131d; } +.gallery-image-wrap img { border-radius: 9px; border-color: #465e63; box-shadow: 0 20px 65px #0008; } + +.download-section { background: radial-gradient(circle at 50% 10%, #2a5d61, #15343f 48%, #0f2631 100%); border-block-color: #4d7775; } +.download-grid { display: flex; flex-direction: column; align-items: center; gap: 58px; } +.download-grid > div:first-child { text-align: center; } +.download-grid h2 { font-size: clamp(62px, 7.6vw, 108px); letter-spacing: -.07em; line-height: 1.02; margin-bottom: 22px; } +.download-grid p { max-width: none; font-size: 16px; } +.download-actions { justify-content: center; margin-top: 32px; } +.download-actions .button { min-height: 52px; padding: 14px 25px; border-radius: 999px; } +.download-grid .install-card { width: min(100%, 850px); background: rgba(7, 24, 34, .82); border-color: #6b9290; border-radius: 16px; box-shadow: 0 25px 60px #0004; } +.download-grid .install-card pre { padding-block: 30px; } + +.source-section { background: #0b1923; } +.source-grid { grid-template-columns: .9fr 1.1fr; gap: clamp(45px, 8vw, 140px); } +.source-grid h2 { font-size: clamp(55px, 6.5vw, 95px); letter-spacing: -.067em; } +.source-actions .button { border-radius: 999px; } +.repo-links a { padding-block: 26px; } +.faq-section { background: #0e202b; } +.faq-grid { grid-template-columns: .8fr 1.2fr; } +.faq-grid h2 { font-size: clamp(55px, 6vw, 86px); } + +.motion-ready [data-reveal] { opacity: 0; filter: blur(16px); transform: translate3d(0, 30px, 0); transition: opacity .8s var(--showcase-ease), filter 1s var(--showcase-ease), transform 1s var(--showcase-ease); transition-delay: var(--reveal-delay, 0ms); } +.motion-ready [data-reveal="visual"] { transform: translate3d(0, 48px, 0) scale(.94); } +.motion-ready [data-reveal].is-visible { opacity: 1; filter: blur(0); transform: translate3d(0, 0, 0) scale(1); } +.motion-ready .hero [data-reveal].is-visible { animation: hero-arrive 1.1s var(--showcase-ease) both; } +.motion-ready .hero .hero-shot.is-visible { animation-delay: .12s; } +.motion-ready .hero .hero-actions.is-visible { animation-delay: .22s; } +@keyframes hero-arrive { from { opacity: 0; filter: blur(18px); transform: translate3d(0, 34px, 0) scale(.98); } to { opacity: 1; filter: blur(0); transform: translate3d(0, 0, 0) scale(1); } } + +@media (max-width: 1050px) { + .hero { padding-top: 95px; } + .hero-inner { gap: 0; } + .pip-grid { gap: 45px; } + .workspace-stage { height: 470px; } +} +@media (max-width: 760px) { + .hero { padding: 90px 0 105px; } + .hero-copy h1 { font-size: clamp(54px, 11vw, 78px); } + .hero-shot { margin-top: 55px; } + .hero .shot-frame { border-radius: 15px; padding: 5px; } + .hero .shot-frame img { border-radius: 10px; } + .hero-actions { margin-top: 35px; } + .section { padding-block: 105px; } + .focus-intro { margin-bottom: 42px; } + .focus-intro h2, .canvas-head h2, .preview-section .section-head h2 { font-size: clamp(51px, 9vw, 72px); } + .focus-visual { border-radius: 15px; } + .focus-points { gap: 20px; margin-top: 48px; } + .focus-points h3 { font-size: 20px; } + .pip-grid { grid-template-columns: 1fr; } + .pip-copy h2 { font-size: 65px; } + .workspace-stage { height: 420px; } + .source-frame, .ratio-stage { height: 310px; } + .gallery-image-wrap { padding: 16px; } + .download-grid h2 { font-size: 65px; } + .source-grid { grid-template-columns: 1fr; } +} +@media (max-width: 560px) { + .hero { padding-top: 81px; } + .hero-copy h1 { font-size: clamp(47px, 12.5vw, 64px); } + .hero-copy p { font-size: 16px; } + .hero-shot { width: calc(100% + 16px); max-width: none; margin-top: 48px; } + .hero-actions { width: 100%; display: grid; grid-template-columns: 1fr; gap: 11px; } + .hero-actions .button { width: 100%; } + .section { padding-block: 88px; } + .focus-intro h2, .canvas-head h2, .preview-section .section-head h2 { font-size: 49px; } + .focus-points { grid-template-columns: 1fr; gap: 25px; } + .focus-points article { padding-top: 18px; } + .focus-points span { margin-bottom: 8px; } + .focus-points p { font-size: 13px; } + .pip-copy h2 { font-size: 57px; } + .pip-copy p { font-size: 16px; } + .workspace-stage { height: 315px; border-radius: 16px; } + .pip-capture { width: 66%; } + .canvas-head { margin-bottom: 36px; } + .demo-source, .demo-preview { padding: 17px; } + .source-frame, .ratio-stage { height: 260px; } + .demo-controls { padding: 20px 17px; } + .gallery { border-radius: 14px; } + .gallery-tabs { justify-content: center; gap: 4px; padding: 9px 7px; } + .gallery-tabs button { padding: 8px; font-size: 11px; } + .gallery-image-wrap { min-height: 240px; } + .download-grid { gap: 36px; } + .download-grid h2 { font-size: 54px; } + .download-actions { display: grid; gap: 14px; justify-items: center; } + .download-actions .button { min-width: 180px; } + .download-grid .install-card pre { padding: 19px 15px; } +} +@media (prefers-reduced-motion: reduce) { + .motion-ready [data-reveal], .motion-ready [data-reveal="visual"], .motion-ready [data-reveal].is-visible { opacity: 1; filter: none; transform: none; animation: none; } + .hero .shot-frame img, .focus-visual img, .pip-capture { transform: none; } +}