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
58 changes: 33 additions & 25 deletions site/src/components/GodRays.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@
//
// - Inlined GLSL (no third-party shader library) + raw WebGL (no Three.js) to
// keep the page lean.
// - DARK MODE ONLY: light shafts read on the black dark-theme background; on the
// light (warm paper) theme the canvas is hidden via CSS and the render loop
// stays paused.
// - BOTH THEMES: the canvas is transparent (premultiplied alpha), so the page
// background shows through the gaps between shafts. Dark mode glows on the
// near-black page; light mode shows softer, deeper shafts on the warm paper.
// - Reduced motion is a contract: under prefers-reduced-motion we draw a single
// static frame, never an animation loop.
// - Pauses when the tab is hidden. pointer-events:none, z-index 0 — it sits
Expand All @@ -21,11 +21,12 @@
function initGodRays(canvas: HTMLCanvasElement): void {
const gl = canvas.getContext("webgl", {
antialias: false,
alpha: false,
alpha: true,
premultipliedAlpha: true,
depth: false,
powerPreference: "low-power",
})
if (!gl) return // No WebGL → the black page background is a fine fallback.
if (!gl) return // No WebGL → the page background is a fine fallback.

const vert = `
attribute vec2 p;
Expand All @@ -43,6 +44,7 @@
uniform float uTime;
uniform float uIntensity;
uniform float uLightY;
uniform float uLight;
uniform vec3 uColorA;
uniform vec3 uColorB;
uniform vec3 uColorC;
Expand Down Expand Up @@ -77,12 +79,24 @@
// Cyclic blend across the brand jewel tones by ray angle, so adjacent
// shafts differ in hue; drifts slowly over time.
float g = fract(ang / 6.28318 * 1.2 + uTime * 0.06);
vec3 col;
if (g < 0.33333) col = mix(uColorA, uColorB, g * 3.0);
else if (g < 0.66667) col = mix(uColorB, uColorC, (g - 0.33333) * 3.0);
else col = mix(uColorC, uColorA, (g - 0.66667) * 3.0);
col *= v;
gl_FragColor = vec4(col, 1.0);
vec3 hue;
if (g < 0.33333) hue = mix(uColorA, uColorB, g * 3.0);
else if (g < 0.66667) hue = mix(uColorB, uColorC, (g - 0.33333) * 3.0);
else hue = mix(uColorC, uColorA, (g - 0.66667) * 3.0);

// Premultiplied-alpha output so the page background shows through the
// gaps in both themes. Dark: bright shafts glowing on the near-black
// page. Light: deeper, slightly stronger shafts that read on warm paper.
vec3 rgb;
float a;
if (uLight > 0.5) {
rgb = hue * 0.82;
a = clamp(v * 1.15, 0.0, 0.55);
} else {
rgb = hue;
a = clamp(v, 0.0, 1.0);
}
gl_FragColor = vec4(rgb * a, a);
}
`

Expand Down Expand Up @@ -131,6 +145,7 @@
const uColorB = gl.getUniformLocation(prog, "uColorB")
const uColorC = gl.getUniformLocation(prog, "uColorC")
const uLightY = gl.getUniformLocation(prog, "uLightY")
const uLight = gl.getUniformLocation(prog, "uLight")
gl.uniform1f(uIntensity, 0.62)
// Brand jewel tones (dark-mode tints): turquoise → indigo → magenta. No
// gold — these coordinate with the teal cards and complement the red hero.
Expand Down Expand Up @@ -176,6 +191,7 @@
function frame(now: number): void {
gl!.uniform1f(uTime, (now - start) / 1000)
gl!.uniform1f(uLightY, lightY())
gl!.uniform1f(uLight, darkMM.matches ? 0 : 1)
gl!.drawArrays(gl!.TRIANGLES, 0, 3)
if (running) rafId = requestAnimationFrame(frame)
}
Expand All @@ -192,18 +208,16 @@
const darkMM = window.matchMedia("(prefers-color-scheme: dark)")
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches

// Active only in dark mode. In light mode CSS hides the canvas and we keep
// the loop paused.
// Active in both themes (the shader adapts via uLight). Pauses only for
// reduced motion (single static frame) and hidden tabs.
function sync(): void {
if (!darkMM.matches) {
pause()
return
}
if (reduced) {
pause()
frame(performance.now()) // single static frame
} else if (document.visibilityState === "visible") {
play()
} else {
pause()
}
}

Expand All @@ -212,7 +226,7 @@
"resize",
() => {
resize()
if (!running && darkMM.matches) frame(performance.now())
if (!running) frame(performance.now())
},
{ passive: true },
)
Expand All @@ -229,7 +243,7 @@
window.addEventListener(
"scroll",
() => {
if (running || !reduced || !darkMM.matches || scrollPending) return
if (running || !reduced || scrollPending) return
scrollPending = true
requestAnimationFrame((now) => {
scrollPending = false
Expand All @@ -251,10 +265,4 @@
pointer-events: none;
display: block;
}
/* Light theme keeps its warm-paper background — no shafts there. */
@media (prefers-color-scheme: light) {
canvas.godrays {
display: none;
}
}
</style>
4 changes: 3 additions & 1 deletion site/src/components/Paint.astro
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,10 @@ const CFG = {
// the imaginary pointer's reflection follows the mouse. The canvas is
// pointer-events:none, so listen on the host card. rAF-throttled; skipped
// under reduced motion. Live frames already re-read the uniforms each tick.
// DISABLED FOR NOW — flip CURSOR_REFLECTION to re-enable.
const CURSOR_REFLECTION = false
const host = canvas.parentElement
if (cfg.coat === 1 && host && !reduced) {
if (CURSOR_REFLECTION && cfg.coat === 1 && host && !reduced) {
let queued = false
host.addEventListener(
"pointermove",
Expand Down
51 changes: 35 additions & 16 deletions site/src/components/markdoc/GetStarted.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ import Paint from "../Paint.astro";
import Spark from "../Spark.astro";
import { getDownloadInfo } from "../../lib/downloads";

const { macDmg, versionLabel, releasesUrl, repoUrl } = await getDownloadInfo();
const { macDmg, winSetup, hasWindows, versionLabel, releasesUrl, repoUrl } =
await getDownloadInfo();
const readmeUrl = `${repoUrl}#readme`;

const M = `<span class="brandword">maximal</span>`;
const steps = [
{
title: "Install maximal",
body: "Grab the macOS app. It lives quietly in your menu bar; Windows is coming soon.",
body: hasWindows
? "Grab the app for macOS or Windows; it sits quietly in your menu bar or system tray."
: "Grab the macOS app. It sits quietly in your menu bar; Windows is coming soon.",
},
{
title: "Sign in with GitHub",
Expand Down Expand Up @@ -63,20 +66,36 @@ const steps = [
</span>
{versionLabel && <span class="btn-meta">{versionLabel}</span>}
</a>
<div class="btn btn--soon" data-os="windows" aria-disabled="true">
<span class="btn-label">
<svg
class="btn-spark"
viewBox="0 0 22 22"
fill="currentColor"
aria-hidden="true"
>
<path d="M11 1 L13 9 L21 11 L13 13 L11 21 L9 13 L1 11 L9 9 Z" />
</svg>
<span>Windows</span>
</span>
<span class="btn-meta">coming soon</span>
</div>
{hasWindows ? (
<a
class="btn btn--primary"
data-os="windows"
href={winSetup}
rel="noopener"
>
<Paint base="#15656f" mode="clearcoat" class="btn-paint" />
<span class="btn-label">
<Spark class="btn-spark" />
<span>Windows</span>
</span>
{versionLabel && <span class="btn-meta">{versionLabel}</span>}
</a>
) : (
<div class="btn btn--soon" data-os="windows" aria-disabled="true">
<span class="btn-label">
<svg
class="btn-spark"
viewBox="0 0 22 22"
fill="currentColor"
aria-hidden="true"
>
<path d="M11 1 L13 9 L21 11 L13 13 L11 21 L9 13 L1 11 L9 9 Z" />
</svg>
<span>Windows</span>
</span>
<span class="btn-meta">coming soon</span>
</div>
)}
</div>
<p class="step__readme">
Homebrew and other options are in the{" "}
Expand Down
20 changes: 17 additions & 3 deletions site/src/components/markdoc/Hero.astro
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ interface Props {
const { tagline, downloadLabel = "Download for macOS" } = Astro.props;
// Markdoc doesn't forward $variables into tag attributes, so pull the resolved
// release straight from the shared (memoized) build-time module.
const { macDmg: downloadUrl, versionLabel } = await getDownloadInfo();
const { macDmg: downloadUrl, versionLabel, winSetup } = await getDownloadInfo();
---

<header class="card card--scarlet hero" aria-labelledby="title">
Expand All @@ -28,7 +28,12 @@ const { macDmg: downloadUrl, versionLabel } = await getDownloadInfo();
<div class="hero-cta">
{
downloadUrl && (
<a class="btn btn--primary hero-cta__primary" href={downloadUrl} rel="noopener">
<a
class="btn btn--primary hero-cta__primary"
href={downloadUrl}
rel="noopener"
data-win-setup={winSetup ?? ""}
>
<Paint base="#15656f" mode="clearcoat" class="btn-paint" />
<span class="btn-label">
<Spark class="btn-spark" />
Expand Down Expand Up @@ -149,13 +154,22 @@ const { macDmg: downloadUrl, versionLabel } = await getDownloadInfo();
if (!isWindows) return;
var btn = document.querySelector(".hero-cta__primary");
if (!btn) return;
var label = btn.querySelector(".btn-label span:last-child");
var winSetup = btn.getAttribute("data-win-setup");
if (winSetup) {
// A Windows installer exists in the release → keep the primary affordance
// lit and point it at the .exe.
btn.setAttribute("href", winSetup);
if (label) label.textContent = "Download for Windows";
return;
}
// No Windows build yet → an honest inert "coming soon" chip.
btn.classList.remove("btn--primary");
btn.classList.add("btn--soon");
btn.removeAttribute("href");
btn.setAttribute("aria-disabled", "true");
var paint = btn.querySelector(".btn-paint");
if (paint) paint.style.display = "none";
var label = btn.querySelector(".btn-label span:last-child");
if (label) label.textContent = "Windows";
var meta = btn.querySelector(".btn-meta");
if (meta) meta.textContent = "coming soon";
Expand Down
17 changes: 16 additions & 1 deletion site/src/lib/downloads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ export interface DownloadInfo {
/** Direct .dmg URL when a release exists, else the releases listing. */
macDmg: string;
macDmgFile: string;
/** Direct Windows installer URL when the release ships a *-setup.exe, else null. */
winSetup: string | null;
winSetupFile: string | null;
/** True when the release carries a Windows *-setup.exe artifact. */
hasWindows: boolean;
/** Human label, e.g. "v0.4.32" or "see /releases". */
versionLabel: string;
tag: string | null;
Expand All @@ -31,19 +36,29 @@ export function getDownloadInfo(): Promise<DownloadInfo> {
}

async function compute(): Promise<DownloadInfo> {
const { tag, hasRelease } = await resolveLatestRelease();
const { tag, hasRelease, assets } = await resolveLatestRelease();
const assetUrl = (filename: string): string =>
hasRelease && tag
? `${REPO_URL}/releases/download/${tag}/${filename}`
: RELEASES_URL;
const versionForAsset = tag ?? "latest";
const macDmgFile = `maximal-${versionForAsset}-darwin-arm64.dmg`;

// Windows: only advertise the installer when the release actually ships a
// *-setup.exe (the Tauri NSIS artifact). We pick it up from the resolved
// asset list rather than guessing a filename, so the button stays "coming
// soon" until a real Windows build is attached.
const winAsset = assets.find((a) => /-setup\.exe$/i.test(a.name)) ?? null;

return {
repo: REPO,
repoUrl: REPO_URL,
releasesUrl: RELEASES_URL,
macDmg: assetUrl(macDmgFile),
macDmgFile,
winSetup: winAsset?.url ?? null,
winSetupFile: winAsset?.name ?? null,
hasWindows: winAsset !== null,
versionLabel: hasRelease && tag ? tag : "see /releases",
tag,
hasRelease,
Expand Down
Loading
Loading