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
5 changes: 5 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,11 @@ updates:
ignore:
- dependency-name: "@types/node"
update-types: ["version-update:semver-major"]
# Forced in package.json overrides (see CLAUDE.md → package.json overrides).
# Dependabot PRs would fight the pin / reopen known-accepted risk.
- dependency-name: "lodash"
- dependency-name: "three"
- dependency-name: "brace-expansion"
groups:
development-dependencies:
dependency-type: "development"
Expand Down
16 changes: 16 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,20 @@ Full convention: [Baton/ACCESSIBILITY.md](https://github.com/OpenPhysics/Baton/b
flat list of exports. Four screens' worth of names is more than one flat list keeps legible; Baton
CONVENTIONS §2 permits this variation, and the file still lives at `src/` root as required.


### `package.json` overrides

JSON cannot carry comments, so the rationale for forced transitive pins lives here. Prefer
**tilde (`~`) or exact** versions — caret (`^`) lets minors drift under what is meant to be a
hard pin. Dependabot ignores these three names (see `.github/dependabot.yml`) so it does not
open PRs that fight the overrides. Revisit when SceneryStack drops or re-pins them upstream.

| Override | Pin | Why |
|---|---|---|
| `lodash` | `~4.18.1` | SceneryStack declares `~4.17.12`. Bump clears Dependabot/npm advisories patched in 4.18.x (e.g. GHSA-r5fr-rjxr-66jc, GHSA-f23m-r3pf-42rh). |
| `three` | `~0.125.2` | SceneryStack declares `^0.104.0`. Floor is 0.125.0 for GHSA-fq6p-x6j3-cmmq (ReDoS). Staying on the 0.125 line avoids a larger API jump; **0.125.x still has open CVEs** (e.g. XSS GHSA-7vvq-7r29-5vg3, fixed only in ≥0.137.0). Remove this override if/when SceneryStack stops depending on `three` or pins a patched line itself. LightPropagation keeps a higher `three` pin — do not force 0.125 there. |
| `brace-expansion` | `~5.0.9` | Transitive via `vite-plugin-pwa` / Workbox. Clears npm audit (originally GHSA-mh99-v99m-4gvg; keep ≥5.0.9 for GHSA-rgw5-rvv9-x895). |

## Testing

`npm test` — Vitest, `happy-dom`, `--expose-gc`. 151 tests across seven files.
Expand All @@ -135,6 +149,8 @@ npm run test:fuzz:quick
bash ../Baton/scripts/check-repo-compliance.sh SpecialRelativity # from the workspace root
```

`npm run release` intentionally skips `npm test` in some sims — append `&& npm test` before the version bump so a release cannot ship a failing suite.

Query parameters: `?initialBeta=0.8`, `?showRapidity=true`, `?shadeLightCone=true`.

## PWA
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Special Relativity

[![CI](https://github.com/OpenPhysics/SpecialRelativity/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenPhysics/SpecialRelativity/actions/workflows/ci.yml)

An interactive simulation of flat-spacetime relativity — a moving light clock, a live Minkowski
diagram, the ladder-and-barn paradox, the twin paradox, and the relativistic Doppler effect — built
with
Expand Down
22 changes: 22 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,28 @@
<meta name="theme-color" content="#1a1a2e" />
<meta name="phet-sim-level" content="production" />

<meta
name="description"
content="Explore special relativity across five screens: a moving light clock, an interactive Minkowski spacetime diagram, the ladder-and-barn length contraction paradox, the twin paradox, and the relativistic Doppler effect."
/>

<!-- Open Graph (LMS / social share previews). Update og:url after deploy. -->
<meta property="og:type" content="website" />
<meta property="og:title" content="Special Relativity" />
<meta
property="og:description"
content="Explore special relativity across five screens: a moving light clock, an interactive Minkowski spacetime diagram, the ladder-and-barn length contraction paradox, the twin paradox, and the relativistic Doppler effect."
/>
<meta property="og:image" content="./icons/icon-512.png" />
<meta name="twitter:card" content="summary" />
<meta name="twitter:title" content="Special Relativity" />
<meta
name="twitter:description"
content="Explore special relativity across five screens: a moving light clock, an interactive Minkowski spacetime diagram, the ladder-and-barn length contraction paradox, the twin paradox, and the relativistic Doppler effect."
/>
<meta name="twitter:image" content="./icons/icon-512.png" />


<link rel="icon" href="./favicon.ico" sizes="any" />
<link rel="icon" href="./icons/icon.svg" type="image/svg+xml" />
<link rel="apple-touch-icon" href="./icons/apple-touch-icon.png" />
Expand Down
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,8 @@
"node": ">=24"
},
"overrides": {
"lodash": "^4.18.0",
"three": "^0.125.0",
"brace-expansion": "^5.0.8"
"lodash": "~4.18.1",
"three": "~0.125.2",
"brace-expansion": "~5.0.9"
}
}
Binary file added public/screenshots/narrow.png
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 public/screenshots/wide.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 30 additions & 3 deletions scripts/generate-icons.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
/**
* generate-icons.ts
*
* Rasterizes public/icons/icon.svg into the PNG icons and favicon.ico used by the PWA.
* Run with: npm run icons
* Rasterizes public/icons/icon.svg into the PNG icons, favicon.ico, and placeholder
* PWA install screenshots used by the manifest. Run with: npm run icons
*
* Replace public/screenshots/{wide,narrow}.png with real sim shots before shipping
* (e.g. Baton/scripts/generate-screenshots.sh → copy into public/screenshots/).
*/

import { readFileSync, writeFileSync } from "node:fs";
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, resolve } from "node:path";
import { fileURLToPath } from "node:url";
import pngToIco from "png-to-ico";
Expand All @@ -15,6 +18,9 @@ const here = dirname(fileURLToPath(import.meta.url));
const publicDir = resolve(here, "..", "public");
const svg = readFileSync(resolve(publicDir, "icons", "icon.svg"));

/** Theme background matching `theme_color` / icon.svg fill (`#1a1a2e`). */
const THEME_BG = { r: 26, g: 26, b: 46, alpha: 1 };

const density = 512;

const pngTargets = [
Expand All @@ -31,3 +37,24 @@ const icoBuffers = await Promise.all(
[16, 32, 48, 64].map((size) => sharp(svg, { density }).resize(size, size).png().toBuffer()),
);
writeFileSync(resolve(publicDir, "favicon.ico"), await pngToIco(icoBuffers));

/** Branded placeholder screenshots for the Web App Manifest `screenshots` member. */
async function writeScreenshot(width: number, height: number, file: string): Promise<void> {
const iconSize = Math.round(Math.min(width, height) * 0.4);
const icon = await sharp(svg, { density }).resize(iconSize, iconSize).png().toBuffer();
await sharp({
create: {
width,
height,
channels: 4,
background: THEME_BG,
},
})
.composite([{ input: icon, gravity: "center" }])
.png()
.toFile(resolve(publicDir, file));
}

mkdirSync(resolve(publicDir, "screenshots"), { recursive: true });
await writeScreenshot(1280, 720, "screenshots/wide.png");
await writeScreenshot(720, 1280, "screenshots/narrow.png");
46 changes: 43 additions & 3 deletions vite.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { VitePWA } from "vite-plugin-pwa";
* Security headers required for:
* - COOP/COEP: SharedArrayBuffer support
* - CSP: restrict resource loading to same-origin + known blob/data exceptions
* - Referrer / Permissions: tighten default browser leakage
* - X-Content-Type-Options: prevent MIME sniffing
* - X-Frame-Options: prevent clickjacking (belt-and-suspenders alongside frame-ancestors)
*/
Expand All @@ -14,9 +15,17 @@ const securityHeaders: Record<string, string> = {
"Cross-Origin-Embedder-Policy": "require-corp",
"Content-Security-Policy": [
"default-src 'self'",
// TODO(scenerystack): drop 'unsafe-eval' when SceneryStack no longer needs

// Function/eval for query-parameter parsing — reopen a CSP audit then.

// 'unsafe-eval' is required for SceneryStack query parameter parsing
"script-src 'self' 'unsafe-eval'",
"worker-src blob: 'self'",
// TODO(scenerystack): drop 'unsafe-inline' when SceneryStack stops setting

// element.style / cssText for theming (same CSP revisit as unsafe-eval).

// Inline styles are set via element.style / cssText throughout the UI layer
"style-src 'self' 'unsafe-inline'",
// data: for icons
Expand All @@ -29,10 +38,18 @@ const securityHeaders: Record<string, string> = {
"base-uri 'self'",
"frame-ancestors 'none'",
].join("; "),
"Referrer-Policy": "strict-origin-when-cross-origin",
"Permissions-Policy": "camera=(), microphone=(), geolocation=()",
"X-Content-Type-Options": "nosniff",
"X-Frame-Options": "DENY",
};

/** Single-file mode: inline every imported asset as base64 (effectively unlimited). */
const INLINE_LIMIT_BYTES = 100 * 1024 * 1024;

/** Workbox precache ceiling — SceneryStack bundles exceed the default 2 MB limit. */
const WORKBOX_MAX_FILE_BYTES = 12 * 1024 * 1024;

/** Escape a string for literal use inside a `RegExp`. */
function escapeRegExp(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
Expand Down Expand Up @@ -130,7 +147,7 @@ export default defineConfig(({ mode }) => {
chunkSizeWarningLimit: 5000,
...(single && {
// Inline every imported asset as a base64 data URI instead of emitting files.
assetsInlineLimit: 100_000_000,
assetsInlineLimit: INLINE_LIMIT_BYTES,
// Emit one CSS file (no per-chunk split) so there is a single tag to inline.
cssCodeSplit: false,
// Skip copying public/ (favicon, icons) — nothing external should remain.
Expand All @@ -154,16 +171,20 @@ export default defineConfig(({ mode }) => {
registerType: "autoUpdate",
includeAssets: ["favicon.ico", "icons/apple-touch-icon.png"],
manifest: {
id: "special-relativity",
name: "Special Relativity",
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
short_name: "SpecialRelativity",
description: "A SceneryStack simulation: Special Relativity",
categories: ["education", "science"],
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
theme_color: "#1a1a2e",
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
background_color: "#000000",
display: "standalone",
orientation: "landscape",
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
display_override: ["window-controls-overlay", "standalone"],
// No `orientation` — leave free so portrait-friendly sims are not forced landscape.
icons: [
{
src: "icons/icon-192.png",
Expand All @@ -182,9 +203,28 @@ export default defineConfig(({ mode }) => {
purpose: "maskable",
},
],
// Placeholder shots from `npm run icons`; replace with real sim screenshots before shipping.
screenshots: [
{
src: "screenshots/wide.png",
sizes: "1280x720",
type: "image/png",
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
form_factor: "wide",
label: "Special Relativity",
},
{
src: "screenshots/narrow.png",
sizes: "720x1280",
type: "image/png",
// biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys
form_factor: "narrow",
label: "Special Relativity",
},
],
},
workbox: {
maximumFileSizeToCacheInBytes: 12 * 1024 * 1024,
maximumFileSizeToCacheInBytes: WORKBOX_MAX_FILE_BYTES,
globPatterns: ["**/*.{js,css,html,svg,png,woff2}"],
},
}),
Expand Down