Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ __pycache__/
# Keep only accepted GUI references. CI uploads comparison images separately.
**/tests/snapshots/**/*.diff.png
**/tests/snapshots/**/*.new.png

# /brag launch-video runs (rendered locally, never committed)
/brag-output*/
Binary file modified docs/assets/host-app.webp
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/launch-poster.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/launch.mp4
Binary file not shown.
20 changes: 20 additions & 0 deletions docs/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,26 @@ under the SIL Open Font License 1.1.
- iPad: `ios/EternalMonitor/App/DesignSystem.swift`.
- Website: `docs/style.css`.

## Motion

Movement explains state; it never decorates. Everything settles in well under
half a second, continuous effects run only while something is live or waiting,
and every surface honours the system's reduce-motion setting.

- Website: `docs/style.css` keeps the keyframes and the `.reveal` /
`.reveal-stagger` scroll reveals; `docs/script.js` adds `.visible`, the nav
scroll state and the card spotlight. The hero and showcase enter on load.
Nothing is hidden without JavaScript (`html.no-js`).
- iPad: `Motion` in `ios/EternalMonitor/App/DesignSystem.swift` holds the shared
springs. `entrance(_:delay:)` staggers the connect screen, `PulseRings` waits
for a PC, `StatusDot(pulses:)` breathes while live, `Shake` rejects a wrong
pairing code, and the HUD numbers roll with `contentTransition(.numericText())`.
- Windows: `widgets::appear` in `host/src/gui/widgets.rs` fades and settles
pages and notices in; hover, the sidebar's active bar, the segmented pill and
the slider knob interpolate with egui's `animate_bool` / `animate_value`. The
window still repaints only while something animates, so the encoder keeps the
GPU.

## Writing

Sentence case everywhere. Say what happens and what to do next, in plain
Expand Down
7 changes: 4 additions & 3 deletions docs/download.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8">
<script>document.documentElement.className = 'js';</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download EternalMonitor</title>
<meta name="description" content="Download EternalMonitor v0.3.0, the current stable Windows release, with the matching iPad TestFlight build 143.">
Expand Down Expand Up @@ -53,7 +54,7 @@ <h1>Download EternalMonitor</h1>
</section>

<section>
<div class="container download-grid">
<div class="container download-grid reveal reveal-stagger">
<!-- Windows -->
<article class="download-card download-card--primary card">
<div class="download-card-header">
Expand Down Expand Up @@ -99,7 +100,7 @@ <h2>iPad</h2>

<section class="build-section">
<div class="container">
<details class="build-details card">
<details class="build-details card reveal">
<summary>Build from source</summary>
<div class="build-content">
<h3>Requirements</h3>
Expand Down
38 changes: 30 additions & 8 deletions docs/index.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<!DOCTYPE html>
<html lang="en">
<html lang="en" class="no-js">
<head>
<meta charset="UTF-8">
<script>document.documentElement.className = 'js';</script>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>EternalMonitor | Your iPad as a second screen for Windows</title>
<meta name="description" content="Use your iPad as a second monitor for your Windows PC. Extend or mirror your desktop, and control it with touch and Apple Pencil. Free and open source.">
Expand Down Expand Up @@ -50,7 +51,7 @@
<section class="hero">
<div class="container">
<span class="eyebrow"><span class="eyebrow-dot" aria-hidden="true"></span>v0.3.0 · Current stable · Free and open source</span>
<h1>Turn your iPad into a <span class="accent">second screen</span> for Windows.</h1>
<h1><span class="w" style="--i:0">Turn</span> <span class="w" style="--i:1">your</span> <span class="w" style="--i:2">iPad</span> <span class="w" style="--i:3">into</span> <span class="w" style="--i:4">a</span> <span class="accent"><span class="w" style="--i:5">second</span> <span class="w" style="--i:6">screen</span></span> <span class="w" style="--i:7">for</span> <span class="w" style="--i:8">Windows.</span></h1>
<p class="hero-subtitle">Extend or mirror your desktop onto an iPad, then tap, drag and draw on it to control your PC. Free, with no account or subscription.</p>
<div class="hero-actions">
<a href="download.html" class="btn btn-primary">
Expand All @@ -66,7 +67,7 @@ <h1>Turn your iPad into a <span class="accent">second screen</span> for Windows.
</div>
</div>

<div class="showcase reveal">
<div class="showcase reveal reveal-showcase">
<figure class="showcase-window">
<div class="showcase-titlebar" aria-hidden="true">
<span class="title"><img src="assets/icon.png" alt="" width="16" height="16">EternalMonitor</span>
Expand All @@ -86,6 +87,27 @@ <h1>Turn your iPad into a <span class="accent">second screen</span> for Windows.
</div>
</section>

<!-- Launch film -->
<section class="section section--watch" id="watch">
<div class="container">
<div class="section-header reveal">
<span class="kicker">Watch</span>
<h2>Twenty seconds, both apps</h2>
<p id="launch-description">Connect over Wi-Fi or USB, pair once, and stream.</p>
</div>
<figure class="launch reveal">
<div class="launch-frame">
<video id="launch-video" controls playsinline preload="metadata" poster="assets/launch-poster.jpg" aria-describedby="launch-description">
<source src="assets/launch.mp4" type="video/mp4">
</video>
<button type="button" class="launch-play" id="launch-play" aria-label="Play the film" hidden>
<svg viewBox="0 0 24 24" fill="currentColor" aria-hidden="true"><path d="M8 5.5v13l11-6.5z"/></svg>
</button>
</div>
</figure>
</div>
</section>

<!-- Features -->
<section class="section" id="features">
<div class="container">
Expand All @@ -94,7 +116,7 @@ <h1>Turn your iPad into a <span class="accent">second screen</span> for Windows.
<h2>What it does</h2>
<p>Mirror or extend, then work on the iPad as if it were part of your PC. If the Wi-Fi drops, the stream reconnects by itself.</p>
</div>
<div class="features-grid reveal">
<div class="features-grid reveal reveal-stagger">
<article class="feature card">
<div class="feature-icon"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="4" width="13" height="10" rx="2"/><rect x="16" y="7" width="6" height="11" rx="1.5"/><path d="M6 18h5M8.5 14v4"/></svg></div>
<h3>Extend or mirror</h3>
Expand Down Expand Up @@ -137,7 +159,7 @@ <h3>Pairing <span class="tag">v0.3</span></h3>
<h2>Three steps, about a minute</h2>
<p>One installer sets up everything on the PC. There's no account to create.</p>
</div>
<ol class="steps reveal">
<ol class="steps reveal reveal-stagger">
<li class="step card">
<div class="step-number" aria-hidden="true">1</div>
<h3>Install on your PC</h3>
Expand All @@ -164,7 +186,7 @@ <h3>Pair once, then stream</h3>
<span class="kicker">Requirements</span>
<h2>What you need</h2>
</div>
<div class="requirements-grid reveal">
<div class="requirements-grid reveal reveal-stagger">
<div class="req-card card">
<h3>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>
Expand Down Expand Up @@ -205,7 +227,7 @@ <h2>Current stable</h2>
<span>Use a network you trust. Pairing keeps other devices out, but the stream itself is not encrypted.</span>
</div>
</div>
<ol class="timeline" aria-label="Release progress">
<ol class="timeline reveal-stagger" aria-label="Release progress">
<li class="milestone milestone--done">
<span class="milestone-dot" aria-hidden="true"></span>
<span><span class="milestone-version">v0.1.0</span><span class="milestone-name">Mirror</span></span>
Expand Down Expand Up @@ -238,7 +260,7 @@ <h2>Current stable</h2>
<span class="kicker">FAQ</span>
<h2>Questions</h2>
</div>
<div class="faq reveal">
<div class="faq reveal reveal-stagger">
<details>
<summary>Is it really free?</summary>
<p>Yes. EternalMonitor is open source under the MIT license. There's no account, subscription or ads.</p>
Expand Down
71 changes: 69 additions & 2 deletions docs/script.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,12 @@
(function () {
'use strict';

var reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var nav = document.querySelector('.nav');

/* --- macOS Notice --- */
// iPadOS reports platform 'MacIntel' too, so require a non-touch device.
var isMac = /Mac/.test(navigator.platform) && navigator.maxTouchPoints <= 1;
var nav = document.querySelector('.nav');
if (isMac && nav && sessionStorage.getItem('mac-notice-dismissed') !== '1') {
var notice = document.createElement('aside');
notice.className = 'mac-notice';
Expand All @@ -25,6 +27,45 @@
nav.insertAdjacentElement('afterend', notice);
}

/* --- Nav: solid once the page scrolls, and the section in view --- */
if (nav) {
var navTick = false;
var updateNav = function () {
nav.classList.toggle('scrolled', window.scrollY > 8);
navTick = false;
};
window.addEventListener('scroll', function () {
if (!navTick) {
navTick = true;
window.requestAnimationFrame(updateNav);
}
}, { passive: true });
updateNav();
}

var sectionLinks = Array.prototype.slice.call(document.querySelectorAll('.nav-link[href^="#"]'));
if (sectionLinks.length && 'IntersectionObserver' in window) {
var linkFor = {};
sectionLinks.forEach(function (link) { linkFor[link.getAttribute('href').slice(1)] = link; });
// A thin band just above the middle of the viewport decides which
// section is "current".
var sectionObserver = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
var link = linkFor[entry.target.id];
if (entry.isIntersecting) {
sectionLinks.forEach(function (l) { l.classList.remove('active'); });
link.classList.add('active');
} else {
link.classList.remove('active');
}
});
}, { rootMargin: '-45% 0px -50% 0px' });
Object.keys(linkFor).forEach(function (id) {
var section = document.getElementById(id);
if (section) sectionObserver.observe(section);
});
}

/* --- Scroll Reveal (IntersectionObserver) --- */
var reveals = document.querySelectorAll('.reveal');
if (reveals.length && 'IntersectionObserver' in window) {
Expand All @@ -43,13 +84,39 @@
reveals.forEach(function (el) { el.classList.add('visible'); });
}

/* --- Launch film: one lime button, native controls once it runs --- */
var launch = document.getElementById('launch-video');
var launchPlay = document.getElementById('launch-play');
if (launch && launchPlay) {
launch.removeAttribute('controls');
launchPlay.hidden = false;
launchPlay.addEventListener('click', function () {
launch.setAttribute('controls', '');
var playing = launch.play();
if (playing && playing.catch) playing.catch(function () {});
});
launch.addEventListener('play', function () { launchPlay.hidden = true; });
launch.addEventListener('ended', function () { launchPlay.hidden = false; });
}

/* --- Card spotlight follows the pointer --- */
if (window.matchMedia('(hover: hover)').matches) {
document.querySelectorAll('.feature, .step, .req-card, .download-card').forEach(function (card) {
card.addEventListener('pointermove', function (e) {
var rect = card.getBoundingClientRect();
card.style.setProperty('--mx', (e.clientX - rect.left) + 'px');
card.style.setProperty('--my', (e.clientY - rect.top) + 'px');
});
});
}

/* --- Smooth Scroll for Anchor Links --- */
document.querySelectorAll('a[href^="#"]').forEach(function (link) {
link.addEventListener('click', function (e) {
var target = document.querySelector(this.getAttribute('href'));
if (target) {
e.preventDefault();
target.scrollIntoView({ behavior: 'smooth' });
target.scrollIntoView({ behavior: reducedMotion ? 'auto' : 'smooth' });
}
});
});
Expand Down
Loading
Loading