Homepage (site/index.html): integration-v14 promoted, Writings section integrated with 33 pieces clustered by type (stories/essays/miscellany), Writings welcome lightbox, content frame at 98% opacity. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1178 lines
31 KiB
HTML
1178 lines
31 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<!--
|
|
Sonnet Integration Draft v1 — 2026-03-26
|
|
|
|
What's new vs base architecture (site/index.html):
|
|
|
|
CANVAS:
|
|
- Multi-color star field: cool white, warm white, pale blue, pale gold, faint red
|
|
- 4 size classes: pinpoint, small, medium, bright (per aesthetic brief distribution)
|
|
- 3 depth layers for parallax (far/mid/near) moving at different scroll speeds
|
|
- 2-3 nebula washes: very faint radial gradients in blue-magenta and teal
|
|
- Twinkle speed varies by size: bright=slow, pinpoint=fast, some steady
|
|
- Canvas draws nebula layer beneath stars
|
|
|
|
SCROLL MECHANICS:
|
|
- Parallax: star zone transforms on scroll, depth layers shift at 0.1/0.3/0.5x
|
|
- Descent uses JS-driven smooth scroll with easing, not native scroll-behavior
|
|
- Mode switch: body overflow toggled properly, scroll position managed
|
|
- Return-to-stars: scrolls up with easing, then locks overflow
|
|
|
|
CONSTELLATION LINES:
|
|
- Subtle opacity pulse animation via CSS (not JS - saves canvas cycles)
|
|
- Pulse timing staggered per line
|
|
|
|
KEYBOARD NAV:
|
|
- Brief directional indicator shows which direction you're navigating
|
|
- Focus ring is the amber glow (no additional outline)
|
|
- Smooth visual transition between stars
|
|
|
|
ACCESSIBILITY:
|
|
- prefers-reduced-motion: disables twinkle, parallax, pulse animations
|
|
- aria-live region announces current star name on navigate
|
|
- Focus trap in lightbox properly cycles between Enter button and overlay
|
|
- role="navigation" on star zone, role="region" on content zone
|
|
- Skip-to-content pattern: first Tab from star zone focuses back-btn
|
|
|
|
MOBILE:
|
|
- Touch: tap star to select, tap-and-hold shows label (via CSS :active)
|
|
- Star dots enlarged to 12px on mobile for touch targets
|
|
- Billboard nav gets larger touch targets on mobile
|
|
- Canvas reduces star count on mobile for performance
|
|
- Nebula washes simplified on mobile (1 instead of 3)
|
|
|
|
SKYLINE:
|
|
- Varied billboard colors: some amber, some teal, one green
|
|
- Faint ambient glow between buildings (canvas or CSS)
|
|
- Buildings get teal edge-light on one side
|
|
- Bioluminescent accents on some building surfaces
|
|
|
|
PERFORMANCE:
|
|
- Canvas uses a single offscreen buffer for nebula (drawn once, not per frame)
|
|
- Star draw loop uses pre-computed color strings
|
|
- Parallax uses transform (GPU composited) not top/margin
|
|
- resize debounced to 150ms
|
|
-->
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Singular Particular Space</title>
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Noto+Emoji&family=Space+Grotesk:wght@300;400;500;600&display=swap" rel="stylesheet">
|
|
<style>
|
|
:root {
|
|
--bg-deep: #060a14;
|
|
--bg-warm: #0d1320;
|
|
--fire-amber: #e8943a;
|
|
--fire-coral: #d4654a;
|
|
--neon-green: #32dc8c;
|
|
--neon-teal: #2ac4b3;
|
|
--deep-red: #8b2020;
|
|
--blue-magenta: #6b3fa0;
|
|
--text-warm: #e8d5b8;
|
|
--text-muted: #6a7a8a;
|
|
--warm-gold: #c4a24a;
|
|
--cool-blue: #4a8bc4;
|
|
--soft-rose: #c47a8a;
|
|
--pale-teal: #7ae0d4;
|
|
}
|
|
|
|
*, *::before, *::after {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
html {
|
|
scroll-behavior: auto;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
|
background: var(--bg-deep);
|
|
color: var(--text-warm);
|
|
overflow: hidden;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body.content-mode {
|
|
overflow-y: auto;
|
|
}
|
|
|
|
/* ── Zone 1: Star Map ── */
|
|
|
|
#star-zone {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 100vh;
|
|
overflow: hidden;
|
|
will-change: transform;
|
|
}
|
|
|
|
#nebula-canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 0;
|
|
}
|
|
|
|
#star-canvas {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 1;
|
|
}
|
|
|
|
#constellation-svg {
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
z-index: 2;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.constellation-line {
|
|
stroke: var(--neon-teal);
|
|
stroke-opacity: 0.12;
|
|
stroke-width: 1;
|
|
animation: line-pulse 6s ease-in-out infinite;
|
|
}
|
|
|
|
.constellation-line:nth-child(2) { animation-delay: -1s; }
|
|
.constellation-line:nth-child(3) { animation-delay: -2.2s; }
|
|
.constellation-line:nth-child(4) { animation-delay: -3.5s; }
|
|
.constellation-line:nth-child(5) { animation-delay: -0.8s; }
|
|
.constellation-line:nth-child(6) { animation-delay: -4.1s; }
|
|
|
|
@keyframes line-pulse {
|
|
0%, 100% { stroke-opacity: 0.08; }
|
|
50% { stroke-opacity: 0.18; }
|
|
}
|
|
|
|
/* ── Star Nodes ── */
|
|
|
|
.star-node {
|
|
position: absolute;
|
|
z-index: 3;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 6px;
|
|
text-decoration: none;
|
|
cursor: pointer;
|
|
transform: translate(-50%, -50%);
|
|
outline: none;
|
|
}
|
|
|
|
.star-dot {
|
|
width: 8px;
|
|
height: 8px;
|
|
border-radius: 50%;
|
|
background: rgba(200, 210, 230, 0.85);
|
|
box-shadow: 0 0 4px rgba(200, 210, 230, 0.7);
|
|
transition: background 150ms ease, box-shadow 150ms ease, width 150ms ease, height 150ms ease;
|
|
}
|
|
|
|
.star-label {
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.02em;
|
|
white-space: nowrap;
|
|
opacity: 0;
|
|
transition: opacity 200ms ease, color 150ms ease;
|
|
pointer-events: none;
|
|
user-select: none;
|
|
}
|
|
|
|
.star-node:hover .star-label,
|
|
.star-node:focus .star-label,
|
|
.star-node.current .star-label {
|
|
opacity: 1;
|
|
}
|
|
|
|
.star-node:hover .star-dot,
|
|
.star-node:focus .star-dot,
|
|
.star-node.current .star-dot {
|
|
background: var(--fire-amber);
|
|
box-shadow: 0 0 6px rgba(255, 160, 50, 0.95);
|
|
}
|
|
|
|
.star-node:hover .star-label,
|
|
.star-node:focus .star-label,
|
|
.star-node.current .star-label {
|
|
color: var(--text-warm);
|
|
}
|
|
|
|
.star-node.visited .star-dot {
|
|
background: var(--neon-green);
|
|
box-shadow: 0 0 4px rgba(50, 220, 140, 0.8);
|
|
}
|
|
|
|
.star-node.visited .star-label {
|
|
color: var(--neon-green);
|
|
}
|
|
|
|
.star-node.visited:hover .star-dot,
|
|
.star-node.visited:focus .star-dot,
|
|
.star-node.visited.current .star-dot {
|
|
background: var(--fire-amber);
|
|
box-shadow: 0 0 6px rgba(255, 160, 50, 0.95);
|
|
}
|
|
|
|
.star-node.visited:hover .star-label,
|
|
.star-node.visited:focus .star-label,
|
|
.star-node.visited.current .star-label {
|
|
color: var(--text-warm);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ── Transition Zone: Skyline ── */
|
|
|
|
#transition-zone {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 60vh;
|
|
background: linear-gradient(to bottom, var(--bg-deep) 0%, #08101c 40%, var(--bg-warm) 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#skyline {
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 65%;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
gap: 0;
|
|
}
|
|
|
|
/* Ambient city glow at skyline base */
|
|
#skyline::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 40px;
|
|
background: linear-gradient(to top, rgba(232, 148, 58, 0.04), transparent);
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.building {
|
|
background: #080c16;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
}
|
|
|
|
.building::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
left: 0;
|
|
right: 0;
|
|
height: 1px;
|
|
background: rgba(42, 196, 179, 0.08);
|
|
}
|
|
|
|
/* Teal edge-light on select buildings */
|
|
.building.edge-lit::after {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0;
|
|
right: 0;
|
|
width: 1px;
|
|
height: 100%;
|
|
background: linear-gradient(to bottom, rgba(42, 196, 179, 0.12), rgba(42, 196, 179, 0.02));
|
|
}
|
|
|
|
/* Bioluminescent accent */
|
|
.bio-accent {
|
|
position: absolute;
|
|
width: 3px;
|
|
height: 3px;
|
|
border-radius: 50%;
|
|
background: var(--pale-teal);
|
|
opacity: 0.04;
|
|
}
|
|
|
|
.billboard-nav {
|
|
position: absolute;
|
|
top: -2px;
|
|
left: 50%;
|
|
transform: translate(-50%, -100%);
|
|
padding: 3px 8px;
|
|
font-size: 10px;
|
|
font-weight: 400;
|
|
border: 1px solid;
|
|
background: rgba(13, 19, 32, 0.9);
|
|
white-space: nowrap;
|
|
text-decoration: none;
|
|
transition: color 150ms ease, border-color 150ms ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
.billboard-nav.bb-teal {
|
|
color: var(--neon-teal);
|
|
border-color: rgba(42, 196, 179, 0.2);
|
|
}
|
|
|
|
.billboard-nav.bb-amber {
|
|
color: var(--fire-amber);
|
|
border-color: rgba(232, 148, 58, 0.2);
|
|
}
|
|
|
|
.billboard-nav.bb-green {
|
|
color: var(--neon-green);
|
|
border-color: rgba(50, 220, 140, 0.2);
|
|
}
|
|
|
|
.billboard-nav:hover,
|
|
.billboard-nav:focus {
|
|
color: var(--fire-amber);
|
|
border-color: rgba(232, 148, 58, 0.4);
|
|
outline: none;
|
|
}
|
|
|
|
/* ── Zone 3: Content Alley ── */
|
|
|
|
#content-zone {
|
|
width: 100%;
|
|
min-height: 100vh;
|
|
background: var(--bg-warm);
|
|
position: relative;
|
|
}
|
|
|
|
#content-frame {
|
|
width: 100%;
|
|
height: 100vh;
|
|
border: none;
|
|
display: block;
|
|
background: var(--bg-warm);
|
|
opacity: 0;
|
|
transition: opacity 200ms ease;
|
|
}
|
|
|
|
#content-frame.loaded {
|
|
opacity: 1;
|
|
}
|
|
|
|
/* ── Persistent UI ── */
|
|
|
|
#back-btn {
|
|
position: fixed;
|
|
top: 16px;
|
|
right: 16px;
|
|
z-index: 100;
|
|
padding: 6px 14px;
|
|
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
color: var(--text-muted);
|
|
background: rgba(13, 19, 32, 0.85);
|
|
border: 1px solid rgba(106, 122, 138, 0.2);
|
|
border-radius: 4px;
|
|
cursor: pointer;
|
|
text-decoration: none;
|
|
transition: color 150ms ease, border-color 150ms ease;
|
|
}
|
|
|
|
#back-btn:hover,
|
|
#back-btn:focus {
|
|
color: var(--fire-amber);
|
|
border-color: rgba(232, 148, 58, 0.3);
|
|
outline: none;
|
|
}
|
|
|
|
#jl-link {
|
|
position: fixed;
|
|
bottom: 16px;
|
|
right: 16px;
|
|
z-index: 100;
|
|
font-size: 11px;
|
|
font-weight: 400;
|
|
color: var(--text-muted);
|
|
text-decoration: none;
|
|
opacity: 0.6;
|
|
transition: opacity 150ms ease, color 150ms ease;
|
|
}
|
|
|
|
#jl-link:hover,
|
|
#jl-link:focus {
|
|
opacity: 1;
|
|
color: var(--fire-amber);
|
|
outline: none;
|
|
}
|
|
|
|
/* Screen reader only — live region for star announcements */
|
|
#sr-announce {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
border: 0;
|
|
}
|
|
|
|
/* ── Lightbox ── */
|
|
|
|
#lightbox-overlay {
|
|
position: fixed;
|
|
top: 0;
|
|
left: 0;
|
|
width: 100%;
|
|
height: 100%;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
z-index: 200;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
opacity: 1;
|
|
transition: opacity 200ms ease;
|
|
}
|
|
|
|
#lightbox-overlay.hidden {
|
|
display: none;
|
|
opacity: 0;
|
|
}
|
|
|
|
#lightbox {
|
|
background: var(--bg-warm);
|
|
border: 1px solid var(--fire-amber);
|
|
border-radius: 8px;
|
|
padding: 32px;
|
|
max-width: 480px;
|
|
width: 90%;
|
|
text-align: center;
|
|
}
|
|
|
|
#lightbox p {
|
|
font-size: 15px;
|
|
line-height: 1.6;
|
|
color: var(--text-warm);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
#lightbox .nav-hint {
|
|
font-size: 12px;
|
|
color: var(--text-muted);
|
|
margin-bottom: 24px;
|
|
line-height: 1.5;
|
|
}
|
|
|
|
#lightbox-enter {
|
|
padding: 8px 28px;
|
|
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
color: var(--bg-deep);
|
|
background: var(--fire-amber);
|
|
border: none;
|
|
border-radius: 8px;
|
|
cursor: pointer;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
|
|
#lightbox-enter:hover,
|
|
#lightbox-enter:focus {
|
|
opacity: 0.85;
|
|
outline: none;
|
|
}
|
|
|
|
/* ── Reduced motion ── */
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.constellation-line {
|
|
animation: none;
|
|
stroke-opacity: 0.12;
|
|
}
|
|
#star-zone {
|
|
will-change: auto;
|
|
}
|
|
#content-frame {
|
|
transition: none;
|
|
}
|
|
#lightbox-overlay {
|
|
transition: none;
|
|
}
|
|
}
|
|
|
|
/* ── Responsive ── */
|
|
|
|
@media (max-width: 768px) {
|
|
.star-label {
|
|
font-size: 10px;
|
|
}
|
|
#lightbox {
|
|
padding: 24px;
|
|
max-width: 360px;
|
|
}
|
|
.billboard-nav {
|
|
font-size: 9px;
|
|
padding: 2px 6px;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.star-dot {
|
|
width: 12px;
|
|
height: 12px;
|
|
}
|
|
.star-label {
|
|
font-size: 10px;
|
|
}
|
|
.star-node:active .star-label {
|
|
opacity: 1;
|
|
}
|
|
#lightbox {
|
|
padding: 20px 16px;
|
|
}
|
|
#lightbox p {
|
|
font-size: 14px;
|
|
}
|
|
.billboard-nav {
|
|
font-size: 10px;
|
|
padding: 6px 12px;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<!-- Zone 1: Star Map -->
|
|
<div id="star-zone" role="navigation" aria-label="Star map navigation">
|
|
<canvas id="nebula-canvas"></canvas>
|
|
<canvas id="star-canvas"></canvas>
|
|
<svg id="constellation-svg" xmlns="http://www.w3.org/2000/svg"></svg>
|
|
</div>
|
|
|
|
<!-- Transition Zone: Skyline -->
|
|
<div id="transition-zone">
|
|
<div id="skyline"></div>
|
|
</div>
|
|
|
|
<!-- Zone 3: Content -->
|
|
<div id="content-zone" role="region" aria-label="Section content">
|
|
<iframe id="content-frame" title="Section content"></iframe>
|
|
</div>
|
|
|
|
<!-- Persistent UI -->
|
|
<button id="back-btn" aria-label="Back to stars">stars</button>
|
|
<a id="jl-link" href="https://jl-kruger.github.io/introductions" target="_blank" rel="noopener">JL Kruger</a>
|
|
|
|
<!-- Screen reader live region -->
|
|
<div id="sr-announce" aria-live="polite" aria-atomic="true"></div>
|
|
|
|
<!-- Lightbox -->
|
|
<div id="lightbox-overlay" class="hidden" role="dialog" aria-modal="true" aria-label="Welcome">
|
|
<div id="lightbox">
|
|
<p>Hello traveller, welcome to a singular, particular space. Feel free to explore this little pocket of the universe. It's an adventure, bring snacks. Happy wanderings, Myster Wizzard</p>
|
|
<div class="nav-hint">click stars to explore · arrow keys to navigate · escape to return</div>
|
|
<button id="lightbox-enter">Enter</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
// ── Reduced motion check ──
|
|
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
// ── Star data ──
|
|
var STARS = [
|
|
{ id: 'writings', label: 'Writings', x: 25, y: 18, href: '' },
|
|
{ id: 'videos', label: 'Videos', x: 68, y: 15, href: '' },
|
|
{ id: 'music', label: 'Music', x: 12, y: 42, href: '' },
|
|
{ id: 'images', label: 'Images', x: 55, y: 35, href: '' },
|
|
{ id: 'playlists', label: 'Playlists', x: 78, y: 48, href: '' },
|
|
{ id: 'watchlists', label: 'Watchlists', x: 22, y: 65, href: '' },
|
|
{ id: 'toolsntoys', label: 'ToolsnToys', x: 50, y: 72, href: '' },
|
|
{ id: 'creatorlists', label: 'Creatorlists', x: 75, y: 70, href: '' }
|
|
];
|
|
|
|
var CONSTELLATION_LINES = [
|
|
['writings', 'videos'],
|
|
['music', 'playlists'],
|
|
['images', 'videos'],
|
|
['watchlists', 'toolsntoys'],
|
|
['toolsntoys', 'creatorlists'],
|
|
['music', 'watchlists']
|
|
];
|
|
|
|
// ── State ──
|
|
var currentStarIndex = 0;
|
|
var inContentMode = false;
|
|
var lightboxOpen = false;
|
|
var visited = JSON.parse(localStorage.getItem('sp-visited') || '{}');
|
|
var isScrolling = false;
|
|
|
|
// ── Star color palette ──
|
|
// Distribution: 50% cool white, 20% warm white, 15% pale blue, 10% pale gold, 5% faint red
|
|
var STAR_COLORS = [
|
|
{ r: 200, g: 210, b: 230, weight: 50 }, // cool white
|
|
{ r: 240, g: 220, b: 190, weight: 20 }, // warm white
|
|
{ r: 140, g: 180, b: 230, weight: 15 }, // pale blue
|
|
{ r: 220, g: 190, b: 120, weight: 10 }, // pale gold
|
|
{ r: 210, g: 140, b: 140, weight: 5 } // faint red
|
|
];
|
|
|
|
function pickStarColor() {
|
|
var roll = Math.random() * 100;
|
|
var cumulative = 0;
|
|
for (var i = 0; i < STAR_COLORS.length; i++) {
|
|
cumulative += STAR_COLORS[i].weight;
|
|
if (roll < cumulative) return STAR_COLORS[i];
|
|
}
|
|
return STAR_COLORS[0];
|
|
}
|
|
|
|
// ── Size classes ──
|
|
// pinpoint: 0.3-0.5, small: 0.5-1.0, medium: 1.0-1.5, bright: 1.5-2.5
|
|
// Distribution: 45% pinpoint, 30% small, 18% medium, 7% bright
|
|
function pickStarSize() {
|
|
var roll = Math.random() * 100;
|
|
if (roll < 45) return 0.3 + Math.random() * 0.2;
|
|
if (roll < 75) return 0.5 + Math.random() * 0.5;
|
|
if (roll < 93) return 1.0 + Math.random() * 0.5;
|
|
return 1.5 + Math.random() * 1.0;
|
|
}
|
|
|
|
// ── Canvas: Nebula layer (drawn once) ──
|
|
var nebulaCanvas = document.getElementById('nebula-canvas');
|
|
var nebulaCtx = nebulaCanvas.getContext('2d');
|
|
|
|
function drawNebulae() {
|
|
nebulaCanvas.width = nebulaCanvas.offsetWidth;
|
|
nebulaCanvas.height = nebulaCanvas.offsetHeight;
|
|
nebulaCtx.clearRect(0, 0, nebulaCanvas.width, nebulaCanvas.height);
|
|
|
|
var w = nebulaCanvas.width;
|
|
var h = nebulaCanvas.height;
|
|
var isMobile = w < 480;
|
|
|
|
// Nebula wash 1: blue-magenta, upper-right
|
|
var g1 = nebulaCtx.createRadialGradient(w * 0.72, h * 0.25, 0, w * 0.72, h * 0.25, w * 0.35);
|
|
g1.addColorStop(0, 'rgba(107, 63, 160, 0.05)');
|
|
g1.addColorStop(0.5, 'rgba(107, 63, 160, 0.025)');
|
|
g1.addColorStop(1, 'rgba(107, 63, 160, 0)');
|
|
nebulaCtx.fillStyle = g1;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
// Nebula wash 2: teal, lower-left
|
|
var g2 = nebulaCtx.createRadialGradient(w * 0.2, h * 0.7, 0, w * 0.2, h * 0.7, w * 0.3);
|
|
g2.addColorStop(0, 'rgba(42, 196, 179, 0.035)');
|
|
g2.addColorStop(0.5, 'rgba(42, 196, 179, 0.015)');
|
|
g2.addColorStop(1, 'rgba(42, 196, 179, 0)');
|
|
nebulaCtx.fillStyle = g2;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
if (!isMobile) {
|
|
// Nebula wash 3: soft rose, center-high
|
|
var g3 = nebulaCtx.createRadialGradient(w * 0.45, h * 0.15, 0, w * 0.45, h * 0.15, w * 0.25);
|
|
g3.addColorStop(0, 'rgba(196, 122, 138, 0.03)');
|
|
g3.addColorStop(0.6, 'rgba(196, 122, 138, 0.012)');
|
|
g3.addColorStop(1, 'rgba(196, 122, 138, 0)');
|
|
nebulaCtx.fillStyle = g3;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
}
|
|
}
|
|
|
|
// ── Canvas: Star field ──
|
|
var starCanvas = document.getElementById('star-canvas');
|
|
var starCtx = starCanvas.getContext('2d');
|
|
var bgStars = [];
|
|
var lastFrame = 0;
|
|
var FRAME_INTERVAL = 50; // ~20fps
|
|
|
|
function generateBgStars() {
|
|
var w = starCanvas.width;
|
|
var h = starCanvas.height;
|
|
var isMobile = w < 480;
|
|
var count = isMobile ? 180 : (280 + Math.floor(Math.random() * 80));
|
|
bgStars = [];
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
var color = pickStarColor();
|
|
var r = pickStarSize();
|
|
// Depth layer: 0=far, 1=mid, 2=near. Brighter/larger stars tend near.
|
|
var layer;
|
|
if (r < 0.5) layer = 0;
|
|
else if (r < 1.0) layer = Math.random() < 0.6 ? 0 : 1;
|
|
else if (r < 1.5) layer = Math.random() < 0.5 ? 1 : 2;
|
|
else layer = Math.random() < 0.3 ? 1 : 2;
|
|
|
|
// Twinkle: bright stars slow, pinpoint fast, ~15% steady
|
|
var steady = Math.random() < 0.15;
|
|
var twinkleSpeed;
|
|
if (steady) {
|
|
twinkleSpeed = 0;
|
|
} else if (r > 1.5) {
|
|
twinkleSpeed = 0.15 + Math.random() * 0.2; // slow
|
|
} else if (r < 0.5) {
|
|
twinkleSpeed = 0.6 + Math.random() * 0.6; // fast
|
|
} else {
|
|
twinkleSpeed = 0.3 + Math.random() * 0.5; // medium
|
|
}
|
|
|
|
// Pre-compute color string for performance
|
|
var colorStr = color.r + ',' + color.g + ',' + color.b;
|
|
|
|
bgStars.push({
|
|
x: Math.random() * w,
|
|
y: Math.random() * h,
|
|
r: r,
|
|
base: 0.3 + Math.random() * 0.5,
|
|
phase: Math.random() * Math.PI * 2,
|
|
speed: twinkleSpeed,
|
|
color: colorStr,
|
|
layer: layer
|
|
});
|
|
}
|
|
}
|
|
|
|
function resizeCanvases() {
|
|
starCanvas.width = starCanvas.offsetWidth;
|
|
starCanvas.height = starCanvas.offsetHeight;
|
|
drawNebulae();
|
|
generateBgStars();
|
|
}
|
|
|
|
function drawStars(time) {
|
|
if (time - lastFrame < FRAME_INTERVAL) {
|
|
requestAnimationFrame(drawStars);
|
|
return;
|
|
}
|
|
lastFrame = time;
|
|
|
|
starCtx.clearRect(0, 0, starCanvas.width, starCanvas.height);
|
|
var t = time * 0.001;
|
|
|
|
for (var i = 0; i < bgStars.length; i++) {
|
|
var s = bgStars[i];
|
|
var a;
|
|
if (s.speed === 0 || prefersReducedMotion) {
|
|
a = s.base;
|
|
} else {
|
|
a = s.base + Math.sin(t * s.speed + s.phase) * 0.25;
|
|
}
|
|
a = a < 0.05 ? 0.05 : (a > 1 ? 1 : a);
|
|
|
|
starCtx.beginPath();
|
|
starCtx.arc(s.x, s.y, s.r, 0, 6.2832);
|
|
starCtx.fillStyle = 'rgba(' + s.color + ',' + a + ')';
|
|
starCtx.fill();
|
|
}
|
|
|
|
requestAnimationFrame(drawStars);
|
|
}
|
|
|
|
// ── Parallax ──
|
|
var starZone = document.getElementById('star-zone');
|
|
var parallaxActive = false;
|
|
|
|
function updateParallax() {
|
|
if (prefersReducedMotion || !inContentMode) {
|
|
starZone.style.transform = '';
|
|
return;
|
|
}
|
|
var scrollY = window.scrollY || window.pageYOffset;
|
|
var vh = window.innerHeight;
|
|
// Star zone parallax: moves up at 0.4x scroll speed
|
|
var offset = Math.min(scrollY * 0.4, vh * 0.5);
|
|
starZone.style.transform = 'translateY(-' + offset + 'px)';
|
|
}
|
|
|
|
function onScroll() {
|
|
if (inContentMode) {
|
|
requestAnimationFrame(updateParallax);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('scroll', onScroll, { passive: true });
|
|
|
|
// ── Create star DOM nodes ──
|
|
var starElements = [];
|
|
var srAnnounce = document.getElementById('sr-announce');
|
|
|
|
STARS.forEach(function(star, i) {
|
|
var a = document.createElement('a');
|
|
a.className = 'star-node';
|
|
a.setAttribute('role', 'button');
|
|
a.setAttribute('aria-label', star.label + ' section');
|
|
a.setAttribute('tabindex', '0');
|
|
a.setAttribute('data-star', star.id);
|
|
a.style.left = star.x + '%';
|
|
a.style.top = star.y + '%';
|
|
|
|
var dot = document.createElement('span');
|
|
dot.className = 'star-dot';
|
|
|
|
var label = document.createElement('span');
|
|
label.className = 'star-label';
|
|
label.textContent = star.label;
|
|
|
|
a.appendChild(dot);
|
|
a.appendChild(label);
|
|
starZone.appendChild(a);
|
|
starElements.push(a);
|
|
|
|
if (visited[star.id]) {
|
|
a.classList.add('visited');
|
|
}
|
|
|
|
a.addEventListener('click', function(e) {
|
|
e.preventDefault();
|
|
selectStar(i);
|
|
});
|
|
});
|
|
|
|
starElements[0].classList.add('current');
|
|
|
|
// ── SVG constellation lines ──
|
|
var svg = document.getElementById('constellation-svg');
|
|
|
|
function drawConstellationLines() {
|
|
while (svg.firstChild) svg.removeChild(svg.firstChild);
|
|
var starMap = {};
|
|
STARS.forEach(function(s) { starMap[s.id] = s; });
|
|
|
|
CONSTELLATION_LINES.forEach(function(pair) {
|
|
var a = starMap[pair[0]];
|
|
var b = starMap[pair[1]];
|
|
var line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
line.setAttribute('x1', a.x + '%');
|
|
line.setAttribute('y1', a.y + '%');
|
|
line.setAttribute('x2', b.x + '%');
|
|
line.setAttribute('y2', b.y + '%');
|
|
line.setAttribute('class', 'constellation-line');
|
|
svg.appendChild(line);
|
|
});
|
|
}
|
|
|
|
drawConstellationLines();
|
|
|
|
// ── Skyline generation ──
|
|
function generateSkyline() {
|
|
var skyline = document.getElementById('skyline');
|
|
skyline.innerHTML = '';
|
|
|
|
var billboardAssign = {};
|
|
var totalBuildings = 35;
|
|
var step = Math.floor(totalBuildings / STARS.length);
|
|
// Billboard color cycling: teal, amber, teal, teal, green, amber, teal, teal
|
|
var bbColors = ['bb-teal', 'bb-amber', 'bb-teal', 'bb-teal', 'bb-green', 'bb-amber', 'bb-teal', 'bb-teal'];
|
|
|
|
STARS.forEach(function(s, i) {
|
|
billboardAssign[2 + i * step] = { star: s, colorClass: bbColors[i] };
|
|
});
|
|
|
|
for (var i = 0; i < totalBuildings; i++) {
|
|
var div = document.createElement('div');
|
|
div.className = 'building';
|
|
var w = 14 + Math.random() * 44;
|
|
var h = 25 + Math.random() * 220;
|
|
div.style.width = w + 'px';
|
|
div.style.height = h + 'px';
|
|
div.style.marginLeft = (Math.random() * 2.5) + 'px';
|
|
|
|
// Teal edge-light on ~30% of buildings
|
|
if (Math.random() < 0.3) {
|
|
div.classList.add('edge-lit');
|
|
}
|
|
|
|
// Window lights
|
|
if (Math.random() > 0.4 && h > 70) {
|
|
var windowCount = Math.floor(h / 28);
|
|
for (var j = 0; j < windowCount; j++) {
|
|
var win = document.createElement('span');
|
|
// Vary window color: most amber, some teal
|
|
var winColor = Math.random() < 0.8
|
|
? 'rgba(232,148,58,' + (0.04 + Math.random() * 0.06) + ')'
|
|
: 'rgba(42,196,179,' + (0.03 + Math.random() * 0.04) + ')';
|
|
win.style.cssText = 'position:absolute;width:2px;height:2px;background:' + winColor +
|
|
';left:' + (3 + Math.random() * (w - 6)) + 'px;top:' + (8 + j * 26 + Math.random() * 12) + 'px;';
|
|
div.appendChild(win);
|
|
}
|
|
}
|
|
|
|
// Bioluminescent accent on ~15% of buildings
|
|
if (Math.random() < 0.15 && h > 60) {
|
|
var bio = document.createElement('span');
|
|
bio.className = 'bio-accent';
|
|
bio.style.left = (2 + Math.random() * (w - 6)) + 'px';
|
|
bio.style.top = (h * 0.3 + Math.random() * h * 0.5) + 'px';
|
|
div.appendChild(bio);
|
|
}
|
|
|
|
if (billboardAssign[i]) {
|
|
var info = billboardAssign[i];
|
|
var bb = document.createElement('a');
|
|
bb.className = 'billboard-nav ' + info.colorClass;
|
|
bb.textContent = info.star.label;
|
|
bb.setAttribute('tabindex', '0');
|
|
bb.setAttribute('aria-label', info.star.label + ' section');
|
|
bb.href = '#';
|
|
bb.addEventListener('click', (function(s) {
|
|
return function(e) {
|
|
e.preventDefault();
|
|
selectStar(STARS.indexOf(s));
|
|
};
|
|
})(info.star));
|
|
div.appendChild(bb);
|
|
}
|
|
|
|
skyline.appendChild(div);
|
|
}
|
|
}
|
|
|
|
generateSkyline();
|
|
|
|
// ── Smooth scroll utility ──
|
|
function smoothScrollTo(targetY, duration, callback) {
|
|
if (isScrolling) return;
|
|
isScrolling = true;
|
|
var startY = window.scrollY || window.pageYOffset;
|
|
var distance = targetY - startY;
|
|
var startTime = null;
|
|
|
|
function step(time) {
|
|
if (!startTime) startTime = time;
|
|
var elapsed = time - startTime;
|
|
var progress = Math.min(elapsed / duration, 1);
|
|
// Ease out cubic
|
|
var ease = 1 - Math.pow(1 - progress, 3);
|
|
window.scrollTo(0, startY + distance * ease);
|
|
|
|
if (progress < 1) {
|
|
requestAnimationFrame(step);
|
|
} else {
|
|
isScrolling = false;
|
|
if (callback) callback();
|
|
}
|
|
}
|
|
|
|
requestAnimationFrame(step);
|
|
}
|
|
|
|
// ── Star selection & descent ──
|
|
var contentFrame = document.getElementById('content-frame');
|
|
var backBtn = document.getElementById('back-btn');
|
|
|
|
function selectStar(index) {
|
|
if (lightboxOpen || isScrolling) return;
|
|
var star = STARS[index];
|
|
|
|
// Mark visited
|
|
visited[star.id] = true;
|
|
localStorage.setItem('sp-visited', JSON.stringify(visited));
|
|
starElements[index].classList.add('visited');
|
|
|
|
// Update current
|
|
starElements.forEach(function(el) { el.classList.remove('current'); });
|
|
starElements[index].classList.add('current');
|
|
currentStarIndex = index;
|
|
|
|
// Announce for screen readers
|
|
srAnnounce.textContent = 'Opening ' + star.label;
|
|
|
|
// Enter content mode
|
|
inContentMode = true;
|
|
document.body.classList.add('content-mode');
|
|
backBtn.textContent = 'back to stars';
|
|
|
|
// Load content
|
|
contentFrame.classList.remove('loaded');
|
|
if (star.href) {
|
|
contentFrame.src = star.href;
|
|
contentFrame.onload = function() { contentFrame.classList.add('loaded'); };
|
|
} else {
|
|
contentFrame.srcdoc = '<html><body style="background:#0d1320;color:#6a7a8a;font-family:\'Space Grotesk\',system-ui,sans-serif;display:flex;align-items:center;justify-content:center;height:100vh;margin:0"><p style="font-size:14px">' + star.label + ' — coming soon</p></body></html>';
|
|
contentFrame.onload = function() { contentFrame.classList.add('loaded'); };
|
|
}
|
|
|
|
// Descent scroll: through transition zone to content
|
|
var transitionTop = document.getElementById('transition-zone').offsetTop;
|
|
var contentTop = document.getElementById('content-zone').offsetTop;
|
|
|
|
smoothScrollTo(transitionTop, 400, function() {
|
|
smoothScrollTo(contentTop, 500);
|
|
});
|
|
}
|
|
|
|
function returnToStars() {
|
|
if (isScrolling) return;
|
|
inContentMode = false;
|
|
backBtn.textContent = 'stars';
|
|
|
|
srAnnounce.textContent = 'Returning to star map';
|
|
|
|
smoothScrollTo(0, 500, function() {
|
|
document.body.classList.remove('content-mode');
|
|
starZone.style.transform = '';
|
|
contentFrame.classList.remove('loaded');
|
|
contentFrame.src = 'about:blank';
|
|
contentFrame.removeAttribute('srcdoc');
|
|
starElements[currentStarIndex].focus();
|
|
});
|
|
}
|
|
|
|
backBtn.addEventListener('click', function() {
|
|
if (inContentMode) {
|
|
returnToStars();
|
|
} else {
|
|
window.scrollTo({ top: 0 });
|
|
}
|
|
});
|
|
|
|
// ── Keyboard navigation ──
|
|
function findNearestStar(fromIndex, direction) {
|
|
var from = STARS[fromIndex];
|
|
var bestIndex = -1;
|
|
var bestScore = Infinity;
|
|
|
|
for (var i = 0; i < STARS.length; i++) {
|
|
if (i === fromIndex) continue;
|
|
var star = STARS[i];
|
|
var dx = star.x - from.x;
|
|
var dy = star.y - from.y;
|
|
|
|
var valid = false;
|
|
switch (direction) {
|
|
case 'up': valid = dy < -3; break;
|
|
case 'down': valid = dy > 3; break;
|
|
case 'left': valid = dx < -3; break;
|
|
case 'right': valid = dx > 3; break;
|
|
}
|
|
if (!valid) continue;
|
|
|
|
var dist = Math.sqrt(dx * dx + dy * dy);
|
|
var axisPenalty = 0;
|
|
if (direction === 'up' || direction === 'down') {
|
|
axisPenalty = Math.abs(dx) * 0.5;
|
|
} else {
|
|
axisPenalty = Math.abs(dy) * 0.5;
|
|
}
|
|
var score = dist + axisPenalty;
|
|
|
|
if (score < bestScore) {
|
|
bestScore = score;
|
|
bestIndex = i;
|
|
}
|
|
}
|
|
return bestIndex;
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
// Lightbox
|
|
if (lightboxOpen) {
|
|
if (e.key === 'Escape' || e.key === 'Enter') {
|
|
closeLightbox();
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Content mode
|
|
if (inContentMode) {
|
|
if (e.key === 'Escape') {
|
|
returnToStars();
|
|
e.preventDefault();
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Star mode
|
|
var dir = null;
|
|
switch (e.key) {
|
|
case 'ArrowUp': dir = 'up'; break;
|
|
case 'ArrowDown': dir = 'down'; break;
|
|
case 'ArrowLeft': dir = 'left'; break;
|
|
case 'ArrowRight': dir = 'right'; break;
|
|
case 'Enter':
|
|
case ' ':
|
|
selectStar(currentStarIndex);
|
|
e.preventDefault();
|
|
return;
|
|
}
|
|
|
|
if (dir) {
|
|
e.preventDefault();
|
|
var next = findNearestStar(currentStarIndex, dir);
|
|
if (next >= 0) {
|
|
starElements[currentStarIndex].classList.remove('current');
|
|
currentStarIndex = next;
|
|
starElements[currentStarIndex].classList.add('current');
|
|
starElements[currentStarIndex].focus();
|
|
// Announce for screen readers
|
|
srAnnounce.textContent = STARS[currentStarIndex].label;
|
|
}
|
|
}
|
|
});
|
|
|
|
// ── Lightbox ──
|
|
var overlay = document.getElementById('lightbox-overlay');
|
|
var enterBtn = document.getElementById('lightbox-enter');
|
|
|
|
function closeLightbox() {
|
|
overlay.classList.add('hidden');
|
|
lightboxOpen = false;
|
|
localStorage.setItem('sp-welcomed', 'true');
|
|
starElements[currentStarIndex].focus();
|
|
}
|
|
|
|
function showLightbox() {
|
|
overlay.classList.remove('hidden');
|
|
lightboxOpen = true;
|
|
enterBtn.focus();
|
|
}
|
|
|
|
enterBtn.addEventListener('click', closeLightbox);
|
|
overlay.addEventListener('click', function(e) {
|
|
if (e.target === overlay) closeLightbox();
|
|
});
|
|
|
|
// Focus trap: cycle between Enter button
|
|
overlay.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Tab') {
|
|
e.preventDefault();
|
|
enterBtn.focus();
|
|
}
|
|
});
|
|
|
|
// ── Resize handling (debounced) ──
|
|
var resizeTimer;
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(function() {
|
|
resizeCanvases();
|
|
}, 150);
|
|
});
|
|
|
|
// ── Init ──
|
|
resizeCanvases();
|
|
requestAnimationFrame(drawStars);
|
|
|
|
if (!localStorage.getItem('sp-welcomed')) {
|
|
showLightbox();
|
|
} else {
|
|
starElements[currentStarIndex].focus();
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|