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
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

Source repository for [oss-singularity.io](https://oss-singularity.io/).

Launch Pad v0 is a dependency-free static site shaped around the “Signal Observatory” visual direction: a precise cosmic shell, authentic project interfaces, and an intentionally human open-source voice. GitHub remains canonical; `dist/` is a reproducible, allowlisted production artifact.
Launch Pad is a dependency-free static site shaped around the “Signal Observatory” visual direction: a precise cosmic shell, an adaptive pointer-reactive signal field, authentic project interfaces, and an intentionally human open-source voice. GitHub remains canonical; `dist/` is a reproducible, allowlisted production artifact.

## Development

Expand All @@ -12,7 +12,7 @@ Build and validate the complete site with:
./scripts/check-repository.sh
```

The build uses authored HTML and CSS plus local optimized assets. It requires a POSIX shell and Python 3 for validation, installs no packages, executes no client-side JavaScript, and writes only to ignored `dist/`.
The build uses authored HTML and CSS, one small dependency-free Canvas enhancement, and local optimized assets. It requires a POSIX shell and Python 3 for validation, installs no packages, makes no runtime network requests, and writes only to ignored `dist/`.

To preview the production tree locally after a successful build:

Expand Down
10 changes: 5 additions & 5 deletions docs/product-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ Project documentation, releases, issue tracking, and installation instructions r
- Minimum 44×44 CSS-pixel primary interactive targets where practical
- Text contrast of at least 4.5:1 and non-text contrast of at least 3:1
- No information conveyed by color alone
- Motion is decorative, modest, and removed for `prefers-reduced-motion: reduce`
- Motion is decorative, modest, pointer-reactive, and removed for `prefers-reduced-motion: reduce`
- Layout remains usable at 320 CSS pixels, 200% zoom, and with long translated text even though v0 ships in English

## Performance budget
Expand All @@ -82,15 +82,15 @@ Measured against a clean production build before launch:

| Budget | Target |
| --- | --- |
| Executable JavaScript | 0 bytes |
| Executable JavaScript | under 8 KB, local and dependency-free |
| Initial HTML, uncompressed | under 35 KB |
| CSS, uncompressed | under 40 KB |
| Initial page transfer, excluding social preview | under 350 KB |
| Largest content image | under 180 KB in its delivered format |
| Lighthouse mobile | Performance, Accessibility, Best Practices and SEO each at least 95 |
| Core stability goals | LCP under 2.0 s on a fast 4G profile; CLS under 0.05 |

System fonts, responsive local images, explicit image dimensions, immutable asset caching, and zero client-side framework code support the budget.
System fonts, responsive local images, explicit image dimensions, immutable asset caching, and zero client-side framework code support the budget. The optional reactive signal field makes no network requests, stores no state, stops in background tabs, caps display density, and is disabled when reduced motion is requested.

## SEO and social sharing

Expand All @@ -110,9 +110,9 @@ System fonts, responsive local images, explicit image dimensions, immutable asse

## Technology decision

Use authored HTML and CSS with a small POSIX-shell build script that copies an explicit allowlist into `dist/` and verifies the required output. There are no package-manager dependencies.
Use authored HTML, CSS and one small progressive-enhancement script with a POSIX-shell build that copies an explicit allowlist into `dist/` and verifies the required output. There are no package-manager dependencies.

This is the simplest fit because the content is editorial, the interactions are links and anchors, and the verified default Stellar shell exposes no Node/npm runtime. PHP 8.2 is available but adds no value to v0. A static output is reproducible locally and in GitHub Actions, cheap to cache, easy to stage exactly, and easy to roll back.
This remains the simplest fit because the content is editorial, the primary interactions are links and anchors, and the verified default Stellar shell exposes no Node/npm runtime. A small local Canvas enhancement adds visual reactivity without a framework, runtime service, tracking or third-party request. PHP 8.2 is available but adds no value. The static output remains reproducible locally and in GitHub Actions, cheap to cache, easy to stage exactly, and easy to roll back.

Reconsider a static-site generator only when repeated content, localization, or a larger editorial surface makes hand-authored pages demonstrably harder to maintain. Reconsider a server runtime only for a concrete server-side requirement.

Expand Down
2 changes: 2 additions & 0 deletions scripts/build-site.sh
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ rm -rf -- "$output"
mkdir -p \
"$output/assets/brand" \
"$output/assets/projects" \
"$output/assets/scripts" \
"$output/assets/social" \
"$output/assets/styles"

Expand All @@ -44,6 +45,7 @@ copy_file assets/brand/oss-singularity-mark.svg assets/brand/oss-singularity-mar
copy_file assets/projects/pdrive-control-center-v080.webp assets/projects/pdrive-control-center-v080.webp
copy_file assets/projects/chatgpt-usage-v030.webp assets/projects/chatgpt-usage-v030.webp
copy_file assets/projects/nemo-action-bar.webp assets/projects/nemo-action-bar.webp
copy_file assets/scripts/reactive-field.js assets/scripts/reactive-field.js
copy_file assets/social/oss-singularity-social-preview.png assets/social/oss-singularity-social-preview.png
copy_file assets/styles/site-v0.css assets/styles/site-v0.css

Expand Down
17 changes: 11 additions & 6 deletions scripts/check-site.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,16 +77,14 @@ def main() -> int:
"assets/projects/chatgpt-usage-v030.webp",
"assets/projects/nemo-action-bar.webp",
"assets/projects/pdrive-control-center-v080.webp",
"assets/scripts/reactive-field.js",
"assets/social/oss-singularity-social-preview.png",
"assets/styles/site-v0.css",
}
actual = {str(path.relative_to(root)) for path in root.rglob("*") if path.is_file()}
if actual != required:
fail(f"build allowlist mismatch: missing={sorted(required - actual)} extra={sorted(actual - required)}")

if any(path.suffix == ".js" for path in root.rglob("*")):
fail("executable JavaScript is outside the v0 budget")

html_bytes = 0
for document in (root / "index.html", root / "404.html"):
parser = Document()
Expand All @@ -99,8 +97,9 @@ def main() -> int:
fail(f"{document.name} has no title")
if parser.h1_count != 1:
fail(f"{document.name} must contain exactly one h1")
if parser.scripts:
fail(f"{document.name} contains script elements")
expected_scripts = 1 if document.name == "index.html" else 0
if parser.scripts != expected_scripts:
fail(f"{document.name} must contain {expected_scripts} script elements")
if len(parser.ids) != len(set(parser.ids)):
fail(f"{document.name} contains duplicate ids")

Expand Down Expand Up @@ -147,6 +146,9 @@ def main() -> int:
css_bytes = (root / "assets/styles/site-v0.css").stat().st_size
if css_bytes > 40_000:
fail(f"CSS budget exceeded: {css_bytes} bytes")
script_bytes = (root / "assets/scripts/reactive-field.js").stat().st_size
if script_bytes > 8_000:
fail(f"JavaScript budget exceeded: {script_bytes} bytes")
for image in (root / "assets/projects").iterdir():
if image.stat().st_size > 180_000:
fail(f"project image budget exceeded: {image.name} is {image.stat().st_size} bytes")
Expand All @@ -163,7 +165,10 @@ def main() -> int:
ElementTree.parse(root / "sitemap.xml")
ElementTree.parse(root / "assets/brand/oss-singularity-mark.svg")

print(f"site checks passed: html={html_bytes} css={css_bytes} transfer={transfer}")
print(
f"site checks passed: html={html_bytes} css={css_bytes} "
f"js={script_bytes} transfer={transfer}"
)
return 0


Expand Down
8 changes: 4 additions & 4 deletions site/.htaccess
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ ErrorDocument 404 /404.html
</IfModule>

<IfModule mod_headers.c>
Header always set Content-Security-Policy "default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; media-src 'none'; object-src 'none'; script-src 'none'; style-src 'self'; upgrade-insecure-requests"
Header always set Content-Security-Policy "default-src 'self'; base-uri 'none'; connect-src 'none'; font-src 'self'; form-action 'none'; frame-ancestors 'none'; img-src 'self' data:; media-src 'none'; object-src 'none'; script-src 'self'; style-src 'self'; upgrade-insecure-requests"
Header always set Cross-Origin-Opener-Policy "same-origin"
Header always set Cross-Origin-Resource-Policy "same-origin"
Header always set Permissions-Policy "camera=(), geolocation=(), microphone=(), payment=(), usb=()"
Expand All @@ -22,7 +22,7 @@ ErrorDocument 404 /404.html
<FilesMatch "\.(?:html?)$">
Header set Cache-Control "no-cache"
</FilesMatch>
<FilesMatch "\.(?:css|svg|webp)$">
<FilesMatch "\.(?:css|js|svg|webp)$">
Header set Cache-Control "public, max-age=31536000, immutable"
</FilesMatch>
<FilesMatch "^(?:robots\.txt|sitemap\.xml|site\.webmanifest)$">
Expand All @@ -31,9 +31,9 @@ ErrorDocument 404 /404.html
</IfModule>

<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/css application/json application/manifest+json application/xml image/svg+xml
AddOutputFilterByType DEFLATE text/html text/plain text/css application/javascript application/json application/manifest+json application/xml image/svg+xml
</IfModule>

<IfModule mod_brotli.c>
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/json application/manifest+json application/xml image/svg+xml
AddOutputFilterByType BROTLI_COMPRESS text/html text/plain text/css application/javascript application/json application/manifest+json application/xml image/svg+xml
</IfModule>
182 changes: 182 additions & 0 deletions site/assets/scripts/reactive-field.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
(() => {
const canvas = document.querySelector("[data-signal-field]");
const reducedMotion = window.matchMedia("(prefers-reduced-motion: reduce)");

if (!(canvas instanceof HTMLCanvasElement) || reducedMotion.matches) {
return;
}

const context = canvas.getContext("2d", { alpha: true });

if (!context) {
return;
}

const pointer = { active: false, x: 0, y: 0, easedX: 0, easedY: 0 };
let particles = [];
let width = 0;
let height = 0;
let pixelRatio = 1;
let frame = 0;
let visible = true;

const random = (() => {
let seed = 0x51a1f13d;
return () => {
seed |= 0;
seed = seed + 0x6d2b79f5 | 0;
let value = Math.imul(seed ^ seed >>> 15, 1 | seed);
value = value + Math.imul(value ^ value >>> 7, 61 | value) ^ value;
return ((value ^ value >>> 14) >>> 0) / 4294967296;
};
})();

const createParticles = () => {
const density = Math.floor(width * height / 26000);
const count = Math.max(28, Math.min(72, density));
particles = Array.from({ length: count }, (_, index) => ({
x: random() * width,
y: random() * height,
depth: .28 + random() * .72,
size: .45 + random() * 1.15,
phase: random() * Math.PI * 2,
accent: index % 7 === 0,
}));
};

const resize = () => {
width = window.innerWidth;
height = window.innerHeight;
pixelRatio = Math.min(window.devicePixelRatio || 1, 1.5);
canvas.width = Math.round(width * pixelRatio);
canvas.height = Math.round(height * pixelRatio);
context.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
createParticles();
};

const positionParticle = (particle, time) => {
let x = particle.x + pointer.easedX * particle.depth * 15;
let y = particle.y + pointer.easedY * particle.depth * 10;
x += Math.sin(time * .00016 + particle.phase) * 2.5 * particle.depth;
y += Math.cos(time * .00012 + particle.phase) * 2 * particle.depth;

if (pointer.active) {
const offsetX = x - pointer.x;
const offsetY = y - pointer.y;
const distance = Math.hypot(offsetX, offsetY) || 1;
const reach = 190;

if (distance < reach) {
const lens = (1 - distance / reach) * 18 * particle.depth;
x += offsetX / distance * lens;
y += offsetY / distance * lens;
}
}

return { x, y };
};

const drawGlow = () => {
if (!pointer.active) {
return;
}

const glow = context.createRadialGradient(pointer.x, pointer.y, 0, pointer.x, pointer.y, 230);
glow.addColorStop(0, "rgba(101, 229, 255, .075)");
glow.addColorStop(.48, "rgba(255, 99, 200, .035)");
glow.addColorStop(1, "rgba(3, 8, 17, 0)");
context.fillStyle = glow;
context.fillRect(pointer.x - 230, pointer.y - 230, 460, 460);
};

const draw = (time) => {
if (!visible) {
return;
}

pointer.easedX += ((pointer.active ? pointer.x / width - .5 : 0) - pointer.easedX) * .045;
pointer.easedY += ((pointer.active ? pointer.y / height - .5 : 0) - pointer.easedY) * .045;

context.clearRect(0, 0, width, height);
drawGlow();

const positions = particles.map((particle) => positionParticle(particle, time));

if (pointer.active) {
positions.forEach((point) => {
const distance = Math.hypot(point.x - pointer.x, point.y - pointer.y);

if (distance < 165) {
context.beginPath();
context.moveTo(pointer.x, pointer.y);
context.lineTo(point.x, point.y);
context.strokeStyle = `rgba(255, 99, 200, ${(1 - distance / 165) * .16})`;
context.lineWidth = .7;
context.stroke();
}
});
}

for (let first = 0; first < positions.length; first += 1) {
for (let second = first + 1; second < positions.length; second += 1) {
const distance = Math.hypot(
positions[first].x - positions[second].x,
positions[first].y - positions[second].y,
);

if (distance < 108) {
context.beginPath();
context.moveTo(positions[first].x, positions[first].y);
context.lineTo(positions[second].x, positions[second].y);
context.strokeStyle = `rgba(101, 229, 255, ${(1 - distance / 108) * .085})`;
context.lineWidth = .6;
context.stroke();
}
}
}

particles.forEach((particle, index) => {
const point = positions[index];
const pulse = .58 + Math.sin(time * .001 + particle.phase) * .22;
context.beginPath();
context.arc(point.x, point.y, particle.size, 0, Math.PI * 2);
context.fillStyle = particle.accent
? `rgba(255, 99, 200, ${pulse})`
: `rgba(185, 243, 255, ${pulse * .72})`;
context.fill();
});

frame = window.requestAnimationFrame(draw);
};

const updatePointer = (event) => {
if (event.pointerType === "touch" && !event.isPrimary) {
return;
}
pointer.active = true;
pointer.x = event.clientX;
pointer.y = event.clientY;
};

const releasePointer = () => {
pointer.active = false;
};

const updateVisibility = () => {
visible = !document.hidden;
window.cancelAnimationFrame(frame);
if (visible) {
frame = window.requestAnimationFrame(draw);
}
};

window.addEventListener("resize", resize, { passive: true });
window.addEventListener("pointermove", updatePointer, { passive: true });
window.addEventListener("pointerdown", updatePointer, { passive: true });
window.addEventListener("pointerup", releasePointer, { passive: true });
document.documentElement.addEventListener("pointerleave", releasePointer, { passive: true });
document.addEventListener("visibilitychange", updateVisibility);

resize();
frame = window.requestAnimationFrame(draw);
})();
13 changes: 12 additions & 1 deletion site/assets/styles/site-v0.css
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,16 @@ body {
overflow-x: clip;
}

.signal-field {
height: 100%;
inset: 0;
opacity: .72;
pointer-events: none;
position: fixed;
width: 100%;
z-index: 0;
}

body::before {
background-image:
linear-gradient(var(--line-soft) 1px, transparent 1px),
Expand Down Expand Up @@ -76,7 +86,7 @@ a { color: inherit; }
outline-offset: 5px;
}

.site-shell { position: relative; }
.site-shell { position: relative; z-index: 1; }

.site-header,
.hero,
Expand Down Expand Up @@ -376,5 +386,6 @@ a { color: inherit; }

@media (prefers-reduced-motion: reduce) {
:root { scroll-behavior: auto; }
.signal-field { display: none; }
*, *::before, *::after { animation-duration: .01ms !important; animation-iteration-count: 1 !important; scroll-behavior: auto !important; transition-duration: .01ms !important; }
}
3 changes: 3 additions & 0 deletions site/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
<link rel="icon" href="/assets/brand/oss-singularity-mark.svg" type="image/svg+xml">
<link rel="manifest" href="/site.webmanifest">
<link rel="stylesheet" href="/assets/styles/site-v0.css">
<script src="/assets/scripts/reactive-field.js" defer></script>
<meta property="og:type" content="website">
<meta property="og:site_name" content="OSS Singularity">
<meta property="og:title" content="OSS Singularity — Engineering beyond the event horizon">
Expand All @@ -28,6 +29,8 @@
<body>
<a class="skip-link" href="#main">Skip to main content</a>

<canvas class="signal-field" data-signal-field aria-hidden="true"></canvas>

<div class="site-shell">
<header class="site-header" aria-label="Primary">
<a class="wordmark" href="/" aria-label="OSS Singularity home">
Expand Down