diff --git a/site/README.md b/site/README.md index c0003f9..1630774 100644 --- a/site/README.md +++ b/site/README.md @@ -20,19 +20,24 @@ bun site/tools/deploy.ts # build out/site (strict, see below), then wrangler ``` It is a Cloudflare Worker, `threert-site`: static assets from `out/site` on the custom domains threert.dev and -www.threert.dev; `site/worker.ts` redirects www to the apex and the old Showcase page to threert.app, serves the clients' disk images +www.threert.dev; `site/worker.ts` redirects www to the apex and the old Showcase page to threert.app, serves the clients' files from R2 and hands everything else to the assets. -Workers static assets take **25 MiB a file**, and the client's disk image is about 32 MiB (it carries the `threert` -command line). So the image is not a static asset: it lives in the R2 bucket `threert-releases` under its file name, -and `worker.ts` serves it at the URL the Download page links, `/releases/` (ranges, `ETag`, `HEAD`). R2 rather +Workers static assets take **25 MiB a file**, and the macOS disk image is about 34 MiB (it carries the `threert` +command line). So the clients' files are not static assets: the disk image (`.dmg`), the Linux tarball (`.tar.gz`) +and the Android APK (`.apk`) live in the R2 bucket `threert-releases` under their file names, and `worker.ts` serves +them at the URL the Download page links, `/releases/` (ranges, `ETag`, `HEAD`), each with its type +(`application/x-apple-diskimage`, `application/gzip`, `application/vnd.android.package-archive`) and +`content-disposition: attachment`, so a phone's browser saves the APK instead of trying to show it. R2 rather than a GitHub release because the client is closed source and has no public repository to carry one. Once: ```bash bunx wrangler@4 r2 bucket create threert-releases ``` -`deploy.ts` uploads the image `releases/latest.json` names, then deploys. `build.ts` leaves the image out of -`out/site` (`--with-image` copies it, for a plain file host). Locally, `serve.ts` serves it from `site/releases/`. +`deploy.ts` uploads every file `releases/latest.json` names, then deploys. A file the bucket already has is not sent +again: the script asks the live site (`HEAD /releases/`) and skips one with the same size (and the same MD5, +where the object's ETag is one). `build.ts` leaves the clients' files out of `out/site` (`--with-image` copies them, +for a plain file host). Locally, `serve.ts` serves them from `site/releases/`. To check the Worker itself without an account: `bunx wrangler@4 r2 object put threert-releases/ --file site/releases/ --local --config site/wrangler.jsonc`, then `bunx wrangler@4 dev --config site/wrangler.jsonc`. @@ -52,7 +57,7 @@ The markup is repeated in each page; change it in all of them (the privacy page | Page | What is on it | |---|---| | `index.html` | The landing page (below). | -| `download/` | ThreeRT for macOS (the build `releases/latest.json` names) and ThreeRT for iPhone (a TestFlight or App Store link once `ios.url` is set), then how a client loads a game: `threert dev`, scan, traced and live. | +| `download/` | The four clients, a card each (below), then how a client loads a game: `threert dev`, scan, traced and live. | | `docs/` | Getting started: install the client, open an example, write a scene, the dev loop with the `threert` CLI, the package format and publishing to threert.app, links to the reference documents in `docs/` on GitHub. | | `privacy/` | What the apps and the site do with data, including the requests the pages make to other services (below). | | `showcase/` | Gone as a page: what it listed is published on threert.app. The Worker answers `/showcase`, `/showcase/` and `/showcase/index.html` with a 301 to `https://threert.app/` (old links: the npm packages' READMEs, the pages of packages built before, earlier apps); `showcase/index.html` is what is left for a host without the Worker, a page that sends the reader on. `showcase/community.json` stays where it is, served as before: ThreeRT for macOS before 0.4.0 reads it (and `play/index.json`) for its Showcase. Nothing is added to it any more: people publish with `threert publish`. | @@ -60,8 +65,10 @@ The markup is repeated in each page; change it in all of them (the privacy page ### The landing page The buttons, in the hero and at the end, in this order of weight: **Explore apps** (filled, to threert.app), **Star on -GitHub** (outlined, with the count), and under them a line with **Download** as a link (named for the visitor's -platform by `js/site.js`). One filled button a row; three equal buttons were the thing to avoid. +GitHub** (outlined, with the count), and under them a line with **Download** as a link, named for the visitor's +platform by `js/site.js` (`a[data-dl]`), then the other clients in a word each (`[data-dl-others]`: "Download for +macOS · iPhone · Steam Deck · Android"). One filled button a row; three equal buttons were the thing to avoid, and +the clients do not get buttons here. 1. **Hero:** a reel behind the headline: a grid of short clips of this repository's demos (Bistro with a crowd, Tokyo at dusk, Kabukichō, the voxel level, the showcase cases) and of OpenStrike, all ThreeRT live, ray traced, @@ -91,6 +98,26 @@ platform by `js/site.js`). One filled button a row; three equal buttons were the `threert publish` does is what https://threert.app/publish says; check there before changing it. 12. **Just light.** +### The Download page + +Four cards, the same anatomy in each: a glyph, the name, one requirement line, three short lines, then one button +with the version and the size beside it, and under it at most a fine line and the file's SHA-256 behind a +disclosure. **Keep it that terse**: a card that grows a paragraph stops matching the others. + +| Card | From `latest.json` | Under the button | +|---|---|---| +| ThreeRT for macOS | `macos`: the disk image | SHA-256 | +| ThreeRT for iPhone | `ios.url`: TestFlight or the App Store | the link as a QR code in the card's corner | +| ThreeRT for Steam Deck | `linux`: the tarball (x86_64; Linux is in the requirement line) | the two commands that install it, in a block with a Copy button (`tar xzf ~/Downloads/`, `ThreeRT/install.sh`: what the tarball's README says, with the place a browser saves to), one line on what they do, SHA-256 | +| ThreeRT for Android | `android`: the APK (arm64) | one line on installing it, SHA-256; the APK's address as a QR code in the corner | + +Two cards a row from 1021 px up, one column below. The two cards of a row share their rows (`subgrid`), so the +buttons are level whatever the lines above them wrap to; the pairs stay together (macOS with iPhone, Steam Deck with +Android). The visitor's own client comes first and is the lit card (`platform()` in `js/site.js`: an Android browser +says Linux too, so it is asked first; `Linux x86_64` is what a Steam Deck's browsers say in Desktop Mode and in Game +Mode); the QR codes show from 1241 px up, and never for the device the visitor is on. A client `latest.json` has no +entry for gets a disabled button and "No … build is published on this host yet." + ### threert.app's apps on the landing page `js/platform.js` decides which apps are shown and builds a tile; it has no DOM and no imports, so the page and the @@ -126,27 +153,41 @@ in its Showcase and opens its packages. Without `releases/` the Download page sa `serve.ts` and `build.ts` warn; `build.ts --require-release` (what `deploy.ts` runs) stops instead, so a deploy never puts up a Download button with nothing behind it. -`releases/latest.json`, next to the file it names: +`releases/latest.json`, next to the files it names. An entry a client; each has its own `version` and `date`, +because clients are released on different days (an entry without them has the top-level ones, which are the macOS +release's). A client without an entry is not published on this host: ```jsonc { - "version": "0.1.0", "date": "2026-11-11", - "macos": { "file": "ThreeRT-0.1.0-macos-arm64.dmg", "size": 33981234, "sha256": "…", "minOS": "14.0", "arch": "arm64", + "version": "0.4.0", "date": "2026-11-13", + "macos": { "version": "0.4.0", "date": "2026-11-13", + "file": "ThreeRT-0.4.0-macos-arm64.dmg", "size": 35754483, "sha256": "…", "minOS": "14.0", "arch": "arm64", "url": null, // null: `releases/` on this host (R2 behind the Worker; site/releases/ locally); - // or the https URL of the image when it is hosted elsewhere (`release.ts --macos-url`) + // or the https URL of the file when it is hosted elsewhere. The same in every entry "signed": "developer-id", // or "adhoc" (a local preview) "notarized": true }, // build.ts --require-release (the deploy) refuses anything but a notarized Developer ID build - "ios": { "url": null, // the TestFlight / App Store link; null: "TestFlight beta not open yet" + "ios": { "version": "0.4.0", "date": "2026-11-13", + "url": null, // the TestFlight / App Store link; null: "TestFlight beta not open yet" "channel": "testflight", // or "appstore" - "minOS": "17.0" } + "minOS": "17.0" }, + "linux": { "version": "0.4.0", "date": "2026-11-13", + "file": "ThreeRT-0.4.0-linux-x86_64.tar.gz", "url": null, "size": 18871783, "sha256": "…", "arch": "x86_64", + "needs": "SteamOS 3 or a Linux with Vulkan ray queries" }, + "android": { "version": "0.4.0", "date": "2026-11-13", + "file": "ThreeRT-0.4.0-android-arm64.apk", "url": null, "size": 10012854, "sha256": "…", "arch": "arm64", + "minOS": "10", // the Android release + "signed": true } // with the release key; the deploy refuses anything else } ``` +`build.ts` checks that every file named is in `site/releases/` at the size named (`--require-release`: or stops). +The release script keeps the entries and files of the clients a release does not make. + `releases/appcast.xml`, written by the same release for a signed build, is the update feed installed copies of ThreeRT for macOS read (Sparkle, from 0.3.0; `https://threert.dev/releases/appcast.xml` is in the app's Info.plist): one item, the release, its image's URL and length and the EdDSA signature the apps check. It is a static asset next to `latest.json`; the image it names is the one `deploy.ts` puts in R2 first. `build.ts` stops on a feed that offers -another version, file or size than `latest.json`, or a non-https URL. +another version, file or size than `latest.json`'s macOS entry, or a non-https URL. `play/index.json`, next to a thumbnail and a package directory for each game: @@ -175,14 +216,14 @@ where it is served: | `showcase/` | The page that sends old links on to threert.app, and `community.json` for clients before 0.4.0 | | `css/threert.css` | The visual system: ink, bone and tungsten tokens, type, the nav and footer (with the Star button and the arrow of a link to threert.app), the comparison and player components, the cards, steps and QR tiles of the inner pages | | `js/threert.js` | The wipe comparison (and the clip that can play over its two stills), the reel behind the landing page's headline, the synced camera-move player, the code highlighter. A divider follows a finger only once it moves sideways, so scrolling past one does not move it | -| `js/site.js` | What the pages share: the visitor's platform, the release on this host, the Star buttons' count, QR codes | +| `js/site.js` | What the pages share: the visitor's platform (macOS, iPhone, Android, Linux x86_64), the clients released on this host, the Star buttons' count, QR codes | | `js/platform.js` | threert.app on the landing page: which apps are shown, and a tile's markup (shared with `tools/platform.ts`) | | `js/devices.js` | The 3D device rows (three.js), with a still fallback when WebGL is off | | `brand/card.jpg` | The share card (Open Graph, 1200 x 630): the hero's headline over the reel's still, a screenshot of the page with the nav's links, the lede and the buttons hidden | | `brand/` | The mark ("Three bounces": the three.js triangle, tilted as three.js draws it, and the closed light path inside it, which is three.js's own inner triangle) and its variants. `js/mark.js` draws it live in the lockup and on the landing page, and turns the path on hover. `avatar.svg` is the profile picture (X, Discord): the mark on black for a round crop, its triangle concentric with the circle, heavier strokes (4.2 against 3.2) so it holds at 32 px | | `media/` | The renders, WebP, and the clips (the reel, the Bistro comparison), H.264. Generated; never retouched | | `serve.ts`, `tools/` | Local server, static build, deploy, media and device-still generators; `tools/platform.ts` bakes threert.app's apps and OpenStrikeRT's pictures into the landing page; `tools/vendor.ts` names the npm files the pages import (three.js, qrcode-generator) | -| `worker.ts`, `wrangler.jsonc` | The Cloudflare Worker (www redirect, the old Showcase page to threert.app, disk images from R2, byte ranges for the clips, static assets) and its config (threert.dev) | +| `worker.ts`, `wrangler.jsonc` | The Cloudflare Worker (www redirect, the old Showcase page to threert.app, the clients' files from R2 as downloads, byte ranges for the clips, static assets) and its config (threert.dev) | | `releases/`, `play/` | Generated, not committed (above) | ## Media diff --git a/site/docs/index.html b/site/docs/index.html index 2dce013..42bcc71 100644 --- a/site/docs/index.html +++ b/site/docs/index.html @@ -4,7 +4,7 @@ ThreeRT Docs — getting started - + @@ -59,24 +72,23 @@

The client traces it.
On your own GPU.
-

ThreeRT for macOS

macOS 14 or later · Apple silicon
+

ThreeRT for macOS

macOS 14 or later · Apple silicon
  • Everything on threert.app, traced on your Mac
  • Hardware ray tracing on M3 and later
  • Includes the threert command line; updates itself
-

Looking for the build on this host…

- +
-

ThreeRT for iPhone

iOS 17 or later · iPhone and iPad
+

ThreeRT for iPhone

iOS 17 or later · iPhone and iPad
    @@ -84,11 +96,41 @@

    The client traces it.
    On your own GPU.Hardware ray tracing on iPhone 15 Pro and later
  • Reloads on every save of your project
-

Looking for the build on this host…

+ +
+
+ +

ThreeRT for Steam Deck

SteamOS 3 · Linux x86_64 · Vulkan ray queries
+
+
    +
  • Everything on threert.app, in Game Mode
  • +
  • Hardware ray tracing on the Deck’s own GPU
  • +
  • Tested on a Steam Deck; untested on desktop Linux
  • +
+
+

Looking for the build on this host…

+
+
+ +
+
+ +

ThreeRT for Android

Android 10 or later · arm64 · Vulkan ray queries
+ +
+
    +
  • Everything on threert.app, with a controller or touch
  • +
  • For flagship chips with hardware ray tracing
  • +
  • Verified on a Snapdragon 8 Gen 2 handheld
  • +
+
+

Looking for the build on this host…

+
+
@@ -139,55 +181,91 @@

No ray tracing? Still the same game.

-

ThreeRT for macOS and ThreeRT for iPhone are free to use. The engine they run is open source: meshy-dev/ThreeRT.

-

Mac, macOS, iPhone, iPad and TestFlight are trademarks of Apple Inc.

+

The ThreeRT clients are free to use. The engine they run is open source: meshy-dev/ThreeRT.

+

Mac, macOS, iPhone, iPad and TestFlight are trademarks of Apple Inc. Steam Deck and SteamOS are trademarks of Valve Corporation. Android is a trademark of Google LLC.

diff --git a/site/index.html b/site/index.html index e691032..a8c30ca 100644 --- a/site/index.html +++ b/site/index.html @@ -4,7 +4,7 @@ ThreeRT — Hardware ray-traced three.js - + @@ -19,7 +19,7 @@ - + @@ -243,7 +243,7 @@

Hardwareray-tracedthree.js.

ThreeRT is the first three.js-style engine to fully path-trace industry-standard scenes at 60 fps.1

-

Download ThreeRT · macOS 14+ on Apple silicon · iPhone and iPad, iOS 17+

+

Download ThreeRT · macOS · iPhone · Steam Deck · Android

@@ -371,7 +371,7 @@

Traced where it can be.
three.js everywher

Native client

hardware ray tracing

Every light path traced, at 60 fps.

-
  • Dynamic resolution holds the frame rate
  • Installs like any game client: macOS and iPhone
+
@@ -476,7 +476,7 @@

One command
puts yours online.

Shadow maps. Light probes. Lightmaps.
Screen-space GI. Baking.
Just light.
-

Download ThreeRT · macOS 14+ on Apple silicon · iPhone and iPad, iOS 17+

+

Download ThreeRT · macOS · iPhone · Steam Deck · Android

diff --git a/site/js/site.js b/site/js/site.js index 9b7e9f3..9c0249c 100644 --- a/site/js/site.js +++ b/site/js/site.js @@ -8,21 +8,37 @@ import { mark } from "./mark.js"; export const ROOT = new URL("..", import.meta.url); export const GITHUB = "https://github.com/meshy-dev/ThreeRT"; -/** "macos" | "ios" | "other". An iPad asks for the desktop site as a Mac with a touch screen. */ -export function platform() { - const ua = navigator.userAgent; - if (/iPhone|iPod|iPad/.test(ua) || (/Macintosh/.test(ua) && navigator.maxTouchPoints > 1)) return "ios"; +/** The clients there are, as a visitor knows the device each is for. */ +export const CLIENTS = { macos: "macOS", ios: "iPhone", linux: "Steam Deck", android: "Android" }; + +/** + * "macos" | "ios" | "android" | "linux" | "other": the client a visitor's device takes. An iPad asks for the desktop + * site as a Mac with a touch screen. An Android browser says Linux too, so it is asked first. "linux" is x86_64 + * alone, what the tarball is built for: a Steam Deck's browsers say "X11; Linux x86_64", in Desktop Mode and in Game + * Mode alike. + */ +export function platform(ua = navigator.userAgent, touchPoints = navigator.maxTouchPoints) { + if (/iPhone|iPod|iPad/.test(ua) || (/Macintosh/.test(ua) && touchPoints > 1)) return "ios"; + if (/Android/.test(ua)) return "android"; if (/Macintosh|Mac OS X/.test(ua)) return "macos"; + if (/Linux x86_64/.test(ua)) return "linux"; return "other"; } -/** The download links (`a[data-dl]`, leading to the download page) name the visitor's platform. */ +/** + * The download links (`a[data-dl]`, leading to the download page) name the visitor's platform, and the list beside + * one (`[data-dl-others]`) the other clients. + */ export function downloadButtons(root = document) { - const text = { macos: "Download for macOS", ios: "Get ThreeRT for iPhone", other: "Download" }[platform()]; + const p = platform(); + const text = { macos: "Download for macOS", ios: "Get ThreeRT for iPhone", linux: "Download for Steam Deck", android: "Download for Android", other: "Download" }[p]; for (const a of root.querySelectorAll("a[data-dl]")) { const label = a.querySelector("span") ?? a; label.textContent = text; } + // (A name stays whole: the line breaks at a dot.) + const others = Object.entries(CLIENTS).filter(([k]) => k !== p).map(([, name]) => name.replaceAll(" ", "\u00a0")).join(" · "); + for (const el of root.querySelectorAll("[data-dl-others]")) el.textContent = others; } async function json(rel) { @@ -34,12 +50,19 @@ async function json(rel) { } } -/** The client release on this host: {version, date, macos: {file, url, size, sha256, minOS, arch, signed, notarized}, ios: {url, channel, minOS}}, or null. */ +/** + * The clients released on this host, or null: {version, date, macos, ios, linux, android}, an entry a client, each + * with its own `version` (clients are released on different days; an entry without one has the top-level version): + * macos {file, url, size, sha256, minOS, arch, signed, notarized}, ios {url, channel, minOS}, + * linux {file, url, size, sha256, arch, needs}, android {file, url, size, sha256, arch, minOS, signed}. + * A client without an entry is not published here. + */ export const release = () => json("releases/latest.json"); export const fileUrl = (file) => new URL(`releases/${file}`, ROOT).href; -/** Where a release's macOS disk image is: `macos.url` when it is hosted elsewhere (it is over this host's size limit), else next to latest.json. */ -export const macUrl = (m) => (m.url ? m.url : fileUrl(m.file)); +/** Where a client's file is: its `url` when it is hosted elsewhere, else next to latest.json. */ +export const clientUrl = (entry) => (entry.url ? entry.url : fileUrl(entry.file)); +export const macUrl = clientUrl; export const megabytes = (bytes) => `${(bytes / 1e6).toFixed(bytes < 1e7 ? 1 : 0)} MB`; let qrcode; diff --git a/site/privacy/index.html b/site/privacy/index.html index fb1b2b8..da66ded 100644 --- a/site/privacy/index.html +++ b/site/privacy/index.html @@ -4,7 +4,7 @@ Privacy — ThreeRT - +