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>
1248 lines
41 KiB
HTML
1248 lines
41 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en">
|
|
<head>
|
|
<!--
|
|
Integration v5 — THE NIGHT MARKET — 2026-03-26 (final pass)
|
|
AUTHOR: Sonnet (acting as parent)
|
|
|
|
BOLD OVERHAUL. Parent-directed changes from v4:
|
|
|
|
REMOVED:
|
|
- All constellation lines (SVG element, CSS .constellation-line, JS drawConstellationLines,
|
|
CONSTELLATION_LINES array). The sky is open and vast.
|
|
|
|
NEW IN THIS VERSION:
|
|
- VAPORWAVE GRID: Perspective canvas in transition zone. Horizontal + converging
|
|
vertical lines, each cycling through the full palette. The ruins have geometry.
|
|
- PERSONALITY LAYER v2: 8 celestial character types via .star-visual pseudo-elements.
|
|
Beacon (crosshair), Binary (orbit), Pulsar (ring), Flora (bioluminescent rings),
|
|
Hearth (fire glow), Nebula Point (gas halo), Spark (prismatic '+'), Flow (comet dust).
|
|
- 6-LAYER NEBULAE: Pushed to 0.07-0.08 opacity. Orchid, Paradise, Toucan, Teal,
|
|
Fairy-Pink, Purple. Atmospheric pools of color in the night sky.
|
|
- STAR BLOOM: 20px amber (was 18px). The campfire fills your hands.
|
|
- STAR LABELS: clamp(12px, 2vw, 16px) desktop. Readable, present.
|
|
- BILLBOARD DIVERSITY: 3 size classes, varied font-weight. The city is polychromatic.
|
|
- SKYLINE GLOW: Ambient amber ground at 0.07 opacity (up from 0.06).
|
|
- WINDOW LIGHTS: Brighter dots (opacity raised to 0.08-0.18).
|
|
- STAR-VISUAL WRAPPER: Cleaner DOM structure for personality pseudo-elements.
|
|
|
|
ALL V4 MECHANICS LOCKED:
|
|
- Smooth scroll descent with cubic easing
|
|
- Parallax: star zone at 0.4x scroll speed
|
|
- Keyboard spatial navigation (arrow keys, Enter, Escape)
|
|
- Visited star tracking (localStorage)
|
|
- First-visit lightbox with focus trap
|
|
- aria-live announcements, role="dialog", prefers-reduced-motion
|
|
- Throttled RAF at 20fps, debounced resize, static nebula layer
|
|
- Mobile: reduced stars, enlarged touch targets
|
|
- Content zone with iframe, back button, JL link
|
|
-->
|
|
<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-void: #04060b;
|
|
--bg-deep: #060a14;
|
|
--bg-warm: #0d1320;
|
|
--fire-amber: #e8943a;
|
|
--fire-coral: #d4654a;
|
|
--neon-green: #32dc8c;
|
|
--neon-teal: #2ac4b3;
|
|
--deep-red: #8b2020;
|
|
--blue-magenta: #6b3fa0;
|
|
--cosmic-purple: #4a1d6e;
|
|
--star-blue: #a0c4ff;
|
|
--text-warm: #e8d5b8;
|
|
--text-muted: #6a7a8a;
|
|
--orchid: #c558d9;
|
|
--paradise: #ff7f3f;
|
|
--toucan: #ffcf40;
|
|
--mint-glow: #86efac;
|
|
--fairy-pink: #f472b6;
|
|
--waterfall: #3fbfaf;
|
|
--phosphor: #00ff41;
|
|
--warm-gold: #c4a24a;
|
|
}
|
|
|
|
*, *::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-void);
|
|
color: var(--text-warm);
|
|
overflow: hidden;
|
|
min-height: 100vh;
|
|
}
|
|
|
|
body.content-mode { overflow-y: auto; }
|
|
|
|
/* ── CRT Ghost — viewport scanline overlay, sits above everything ── */
|
|
body::after {
|
|
content: '';
|
|
position: fixed;
|
|
inset: 0;
|
|
background: repeating-linear-gradient(
|
|
0deg,
|
|
transparent,
|
|
transparent 2px,
|
|
rgba(0, 0, 0, 0.06) 2px,
|
|
rgba(0, 0, 0, 0.06) 4px
|
|
);
|
|
pointer-events: none;
|
|
z-index: 9999;
|
|
}
|
|
|
|
/* ── Zone 1: Star Map ── */
|
|
|
|
#star-zone {
|
|
position: relative;
|
|
width: 100%; height: 100vh;
|
|
overflow: hidden;
|
|
will-change: transform;
|
|
background: radial-gradient(ellipse at 50% 40%, #090d1c 0%, var(--bg-void) 100%);
|
|
}
|
|
|
|
/* Phosphor vignette */
|
|
#star-zone::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 0;
|
|
background: radial-gradient(ellipse at 50% 50%, transparent 50%, rgba(0,0,0,0.32) 100%);
|
|
pointer-events: none;
|
|
z-index: 4;
|
|
}
|
|
|
|
#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;
|
|
}
|
|
|
|
/* ── Star Nodes ── */
|
|
|
|
.star-node {
|
|
position: absolute;
|
|
z-index: 5;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8px;
|
|
cursor: pointer;
|
|
transform: translate(-50%, -50%);
|
|
outline: none;
|
|
border: none;
|
|
background: none;
|
|
padding: 0;
|
|
font-family: inherit;
|
|
}
|
|
|
|
.star-visual {
|
|
position: relative;
|
|
width: 40px; height: 40px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.star-dot {
|
|
width: 7px; height: 7px;
|
|
border-radius: 50%;
|
|
position: relative;
|
|
z-index: 2;
|
|
transition: width 150ms ease, height 150ms ease, background 150ms ease, box-shadow 150ms ease;
|
|
}
|
|
|
|
.star-label {
|
|
font-size: clamp(12px, 2vw, 16px);
|
|
font-weight: 500;
|
|
color: var(--text-muted);
|
|
letter-spacing: 0.03em;
|
|
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; color: var(--text-warm); }
|
|
|
|
/* Per-star accent colors */
|
|
.star-node[data-star="writings"] .star-dot { background: var(--star-blue); box-shadow: 0 0 8px var(--star-blue), 0 0 16px rgba(160,196,255,0.4); }
|
|
.star-node[data-star="videos"] .star-dot { background: var(--fire-coral); box-shadow: 0 0 8px var(--fire-coral), 0 0 16px rgba(212,101,74,0.4); }
|
|
.star-node[data-star="music"] .star-dot { background: var(--neon-teal); box-shadow: 0 0 8px var(--neon-teal), 0 0 16px rgba(42,196,179,0.4); }
|
|
.star-node[data-star="images"] .star-dot { background: var(--mint-glow); box-shadow: 0 0 8px var(--mint-glow), 0 0 16px rgba(134,239,172,0.4); }
|
|
.star-node[data-star="playlists"] .star-dot { background: var(--toucan); box-shadow: 0 0 8px var(--toucan), 0 0 16px rgba(255,207,64,0.4); }
|
|
.star-node[data-star="watchlists"] .star-dot { background: var(--orchid); box-shadow: 0 0 8px var(--orchid), 0 0 16px rgba(197,88,217,0.4); }
|
|
.star-node[data-star="toolsntoys"] .star-dot { background: var(--fairy-pink); box-shadow: 0 0 8px var(--fairy-pink), 0 0 16px rgba(244,114,182,0.4); }
|
|
.star-node[data-star="creatorlists"] .star-dot { background: var(--warm-gold); box-shadow: 0 0 8px var(--warm-gold), 0 0 16px rgba(196,162,74,0.4); }
|
|
|
|
/* BLOOM — hover/focus preview: 20px campfire */
|
|
.star-node:hover .star-dot,
|
|
.star-node:focus .star-dot {
|
|
width: 20px; height: 20px;
|
|
background: var(--fire-amber) !important;
|
|
box-shadow: 0 0 12px rgba(232, 148, 58, 0.95), 0 0 24px rgba(232, 148, 58, 0.5) !important;
|
|
}
|
|
|
|
/* SELECTED — current star is doubled: 40px, full campfire */
|
|
.star-node.current .star-dot {
|
|
width: 40px; height: 40px;
|
|
background: var(--fire-amber) !important;
|
|
box-shadow: 0 0 20px rgba(232, 148, 58, 1), 0 0 50px rgba(232, 148, 58, 0.65), 0 0 80px rgba(232, 148, 58, 0.2) !important;
|
|
}
|
|
.star-node.current .star-label {
|
|
font-size: clamp(22px, 4vw, 32px);
|
|
color: var(--text-warm);
|
|
opacity: 1;
|
|
}
|
|
|
|
/* Visited */
|
|
.star-node.visited .star-dot {
|
|
background: var(--neon-green) !important;
|
|
box-shadow: 0 0 8px rgba(50, 220, 140, 0.9), 0 0 16px rgba(50, 220, 140, 0.4) !important;
|
|
}
|
|
.star-node.visited .star-label { color: var(--neon-green); }
|
|
|
|
/* Visited + hover/focus bloom */
|
|
.star-node.visited:hover .star-dot,
|
|
.star-node.visited:focus .star-dot {
|
|
background: var(--fire-amber) !important;
|
|
box-shadow: 0 0 8px rgba(232, 148, 58, 0.9) !important;
|
|
}
|
|
/* Visited + selected */
|
|
.star-node.visited.current .star-dot {
|
|
width: 40px; height: 40px;
|
|
background: var(--fire-amber) !important;
|
|
box-shadow: 0 0 20px rgba(232, 148, 58, 1), 0 0 50px rgba(232, 148, 58, 0.65), 0 0 80px rgba(232, 148, 58, 0.2) !important;
|
|
}
|
|
.star-node.visited:hover .star-label,
|
|
.star-node.visited:focus .star-label,
|
|
.star-node.visited.current .star-label { color: var(--text-warm); opacity: 1; }
|
|
|
|
/* ── Personality 1: Writings — The Beacon (crosshair glow) ── */
|
|
.star-node[data-star="writings"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 1px; height: 100%;
|
|
background: linear-gradient(to bottom, transparent 0%, var(--star-blue) 50%, transparent 100%);
|
|
animation: beacon-v 4s ease-in-out infinite;
|
|
}
|
|
.star-node[data-star="writings"] .star-visual::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 100%; height: 1px;
|
|
background: linear-gradient(to right, transparent 0%, var(--star-blue) 50%, transparent 100%);
|
|
animation: beacon-h 4s ease-in-out infinite 0.4s;
|
|
}
|
|
@keyframes beacon-v { 0%, 100% { opacity: 0.08; } 50% { opacity: 0.4; } }
|
|
@keyframes beacon-h { 0%, 100% { opacity: 0.08; } 50% { opacity: 0.4; } }
|
|
|
|
/* ── Personality 2: Videos — The Binary (orbiting spark) ── */
|
|
.star-node[data-star="videos"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 3px; height: 3px;
|
|
border-radius: 50%;
|
|
background: rgba(255,255,255,0.9);
|
|
animation: binary-orbit 3.2s linear infinite;
|
|
}
|
|
@keyframes binary-orbit {
|
|
from { transform: rotate(0deg) translateX(14px); }
|
|
to { transform: rotate(360deg) translateX(14px); }
|
|
}
|
|
|
|
/* ── Personality 3: Music — The Pulsar (expanding ring) ── */
|
|
.star-node[data-star="music"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 32px; height: 32px;
|
|
border-radius: 50%;
|
|
border: 1px solid var(--neon-teal);
|
|
animation: pulsar-ring 2.2s ease-out infinite;
|
|
}
|
|
@keyframes pulsar-ring {
|
|
0% { transform: scale(0.2); opacity: 0.8; }
|
|
100% { transform: scale(1.1); opacity: 0; }
|
|
}
|
|
|
|
/* ── Personality 4: Images — The Flora (bioluminescent rings) ── */
|
|
.star-node[data-star="images"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 22px; height: 22px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(134, 239, 172, 0.45);
|
|
animation: flora-pulse 3.5s ease-in-out infinite;
|
|
}
|
|
.star-node[data-star="images"] .star-visual::after {
|
|
content: '';
|
|
position: absolute;
|
|
width: 36px; height: 36px;
|
|
border-radius: 50%;
|
|
border: 1px solid rgba(134, 239, 172, 0.22);
|
|
animation: flora-pulse 3.5s ease-in-out infinite 0.9s;
|
|
}
|
|
@keyframes flora-pulse {
|
|
0%, 100% { opacity: 0.85; transform: scale(0.9); }
|
|
50% { opacity: 0.3; transform: scale(1.05); }
|
|
}
|
|
|
|
/* ── Personality 5: Playlists — The Hearth (fire glow) ── */
|
|
.star-node[data-star="playlists"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 30px; height: 30px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(circle, rgba(232, 148, 58, 0.3) 0%, transparent 68%);
|
|
animation: hearth-breathe 2.8s ease-in-out infinite;
|
|
}
|
|
@keyframes hearth-breathe {
|
|
0%, 100% { opacity: 0.4; transform: scale(0.8); }
|
|
50% { opacity: 1.0; transform: scale(1.2); }
|
|
}
|
|
|
|
/* ── Personality 6: Watchlists — The Nebula Point (gas halo) ── */
|
|
.star-node[data-star="watchlists"] .star-visual::before {
|
|
content: '';
|
|
position: absolute;
|
|
width: 38px; height: 26px;
|
|
border-radius: 50%;
|
|
background: radial-gradient(ellipse, rgba(197, 88, 217, 0.22) 0%, transparent 70%);
|
|
animation: nebula-breathe 5.5s ease-in-out infinite;
|
|
}
|
|
@keyframes nebula-breathe {
|
|
0%, 100% { opacity: 0.5; transform: scale(0.85) rotate(-6deg); }
|
|
50% { opacity: 1.0; transform: scale(1.12) rotate(4deg); }
|
|
}
|
|
|
|
/* ── Personality 7: ToolsnToys — The Spark (prismatic '+') ── */
|
|
.star-node[data-star="toolsntoys"] .star-visual::before {
|
|
content: '+';
|
|
position: absolute;
|
|
color: var(--fairy-pink);
|
|
font-size: 24px;
|
|
font-weight: 300;
|
|
line-height: 1;
|
|
top: -11px;
|
|
opacity: 0.5;
|
|
animation: spark-flicker 1.6s ease-in-out infinite;
|
|
}
|
|
@keyframes spark-flicker {
|
|
0%, 100% { opacity: 0.3; transform: scale(0.88); }
|
|
40% { opacity: 0.75; transform: scale(1.06); }
|
|
70% { opacity: 0.4; transform: scale(0.94); }
|
|
}
|
|
|
|
/* ── Personality 8: Creatorlists — The Flow (comet dust) ── */
|
|
.star-node[data-star="creatorlists"] .star-visual::before {
|
|
content: '· · ·';
|
|
position: absolute;
|
|
left: 22px;
|
|
color: var(--waterfall);
|
|
font-size: 12px;
|
|
letter-spacing: 4px;
|
|
white-space: nowrap;
|
|
opacity: 0.5;
|
|
animation: flow-drift 4s ease-in-out infinite;
|
|
}
|
|
@keyframes flow-drift {
|
|
0%, 100% { opacity: 0.28; transform: translateX(0px); }
|
|
50% { opacity: 0.65; transform: translateX(4px); }
|
|
}
|
|
|
|
/* ── Transition Zone: Vaporwave Ruins ── */
|
|
|
|
#transition-zone {
|
|
position: relative;
|
|
width: 100%; height: 48vh;
|
|
background: linear-gradient(to bottom,
|
|
var(--bg-void) 0%,
|
|
#080a18 20%,
|
|
#0a0c1e 45%,
|
|
var(--bg-warm) 100%);
|
|
overflow: hidden;
|
|
}
|
|
|
|
#grid-canvas {
|
|
position: absolute;
|
|
inset: 0;
|
|
width: 100%; height: 100%;
|
|
z-index: 0;
|
|
pointer-events: none;
|
|
}
|
|
|
|
#skyline {
|
|
position: absolute;
|
|
bottom: 0; left: 0;
|
|
width: 100%; height: 72%;
|
|
display: flex;
|
|
align-items: flex-end;
|
|
justify-content: center;
|
|
z-index: 2;
|
|
}
|
|
|
|
/* Ambient city glow at ground */
|
|
#skyline::before {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 0; left: 0;
|
|
width: 100%; height: 90px;
|
|
background: linear-gradient(to top, rgba(232, 148, 58, 0.18), rgba(232, 148, 58, 0.04), transparent);
|
|
z-index: 1;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.building {
|
|
background: #0b1120;
|
|
flex-shrink: 0;
|
|
position: relative;
|
|
border-top: 1px solid rgba(232, 148, 58, 0.10);
|
|
}
|
|
|
|
.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.32), rgba(42, 196, 179, 0.04));
|
|
}
|
|
|
|
.building .bio-stripe {
|
|
position: absolute;
|
|
width: 2px;
|
|
background: var(--mint-glow);
|
|
opacity: 0.22;
|
|
box-shadow: 0 0 4px var(--mint-glow);
|
|
}
|
|
|
|
.building .bio-stripe.orchid {
|
|
background: var(--orchid);
|
|
opacity: 0.18;
|
|
box-shadow: 0 0 4px var(--orchid);
|
|
}
|
|
|
|
/* Billboard nav — each section gets a distinct identity */
|
|
.billboard-nav {
|
|
position: absolute;
|
|
top: -2px; left: 50%;
|
|
transform: translate(-50%, -100%);
|
|
font-family: inherit;
|
|
border: 1px solid;
|
|
background: rgba(4, 6, 11, 0.92);
|
|
white-space: nowrap;
|
|
text-decoration: none;
|
|
transition: color 150ms ease, border-color 150ms ease;
|
|
cursor: pointer;
|
|
}
|
|
|
|
/* Billboard size variants — single glyphs, so push sizes up */
|
|
.billboard-nav.bb-sm { font-size: 14px; font-weight: 400; padding: 3px 8px; font-family: 'Noto Emoji', 'Space Grotesk', system-ui, sans-serif; }
|
|
.billboard-nav.bb-md { font-size: 18px; font-weight: 500; padding: 4px 10px; font-family: 'Noto Emoji', 'Space Grotesk', system-ui, sans-serif; }
|
|
.billboard-nav.bb-lg { font-size: 22px; font-weight: 600; padding: 5px 12px; font-family: 'Noto Emoji', 'Space Grotesk', system-ui, sans-serif; }
|
|
|
|
/* Billboard color classes */
|
|
.billboard-nav.bb-teal { color: var(--neon-teal); border-color: rgba(42,196,179,0.28); }
|
|
.billboard-nav.bb-amber { color: var(--fire-amber); border-color: rgba(232,148,58,0.28); }
|
|
.billboard-nav.bb-green { color: var(--neon-green); border-color: rgba(50,220,140,0.28); }
|
|
.billboard-nav.bb-orchid { color: var(--orchid); border-color: rgba(197,88,217,0.28); }
|
|
.billboard-nav.bb-coral { color: var(--fire-coral); border-color: rgba(212,101,74,0.28); }
|
|
.billboard-nav.bb-pink { color: var(--fairy-pink); border-color: rgba(244,114,182,0.28); }
|
|
.billboard-nav.bb-gold { color: var(--toucan); border-color: rgba(255,207,64,0.28); }
|
|
.billboard-nav.bb-water { color: var(--waterfall); border-color: rgba(63,191,175,0.28); }
|
|
|
|
.billboard-nav:hover,
|
|
.billboard-nav:focus {
|
|
color: var(--fire-amber);
|
|
border-color: rgba(232, 148, 58, 0.45);
|
|
outline: none;
|
|
}
|
|
|
|
/* ── Zone 3: Content ── */
|
|
|
|
#content-zone {
|
|
width: 100%; min-height: 100vh;
|
|
background: var(--bg-warm);
|
|
}
|
|
|
|
#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; }
|
|
|
|
/* ── HUD — matrix terminal overlay ── */
|
|
|
|
#hud {
|
|
position: fixed;
|
|
top: 14px; right: 14px;
|
|
z-index: 100;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 5px;
|
|
padding: 9px 13px 11px;
|
|
background: rgba(4, 6, 11, 0.92);
|
|
border: 1px solid rgba(0, 255, 65, 0.2);
|
|
}
|
|
|
|
/* System header line */
|
|
#hud::before {
|
|
content: '[ SYS ]';
|
|
display: block;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 8px;
|
|
color: rgba(0, 255, 65, 0.3);
|
|
letter-spacing: 0.18em;
|
|
margin-bottom: 5px;
|
|
}
|
|
|
|
#back-btn {
|
|
background: none;
|
|
border: none;
|
|
border-radius: 0;
|
|
padding: 0;
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 12px;
|
|
font-weight: 400;
|
|
color: rgba(0, 255, 65, 0.75);
|
|
cursor: pointer;
|
|
text-align: left;
|
|
letter-spacing: 0.04em;
|
|
transition: color 100ms ease;
|
|
}
|
|
#back-btn::before { content: '> '; color: rgba(0, 255, 65, 0.4); }
|
|
#back-btn::after {
|
|
content: '_';
|
|
color: rgba(0, 255, 65, 0.65);
|
|
animation: cur-blink 1.1s step-end infinite;
|
|
}
|
|
@keyframes cur-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0; } }
|
|
#back-btn:hover, #back-btn:focus {
|
|
color: var(--phosphor);
|
|
outline: none;
|
|
}
|
|
|
|
#jl-link {
|
|
font-family: 'Courier New', Courier, monospace;
|
|
font-size: 10px;
|
|
color: rgba(0, 255, 65, 0.42);
|
|
text-decoration: none;
|
|
letter-spacing: 0.04em;
|
|
transition: color 100ms ease;
|
|
}
|
|
#jl-link::before { content: '>> '; color: rgba(0, 255, 65, 0.22); }
|
|
#jl-link:hover, #jl-link:focus {
|
|
color: var(--phosphor);
|
|
outline: none;
|
|
}
|
|
|
|
#sr-announce {
|
|
position: absolute;
|
|
width: 1px; height: 1px;
|
|
padding: 0; margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0,0,0,0);
|
|
border: 0;
|
|
}
|
|
|
|
/* ── Lightbox: Campfire Note ── */
|
|
|
|
#lightbox-overlay {
|
|
position: fixed; inset: 0;
|
|
background: rgba(0, 0, 0, 0.8);
|
|
z-index: 200;
|
|
display: flex; align-items: center; justify-content: center;
|
|
}
|
|
#lightbox-overlay.hidden { display: none; }
|
|
|
|
#lightbox {
|
|
background: var(--bg-warm);
|
|
border: 1px solid var(--fire-amber);
|
|
border-radius: 4px;
|
|
padding: 36px;
|
|
max-width: 480px; width: 90%;
|
|
text-align: center;
|
|
position: relative;
|
|
}
|
|
|
|
/* Phosphor cursor line — ghost of CRT at the top */
|
|
#lightbox::before {
|
|
content: '';
|
|
position: absolute;
|
|
top: 0; left: 8px; right: 8px;
|
|
height: 1px;
|
|
background: linear-gradient(to right, transparent, rgba(0, 255, 65, 0.15), transparent);
|
|
pointer-events: none;
|
|
}
|
|
|
|
/* Orchid inner border — fairy magic surrounding the campfire note */
|
|
#lightbox::after {
|
|
content: '';
|
|
position: absolute;
|
|
inset: 4px;
|
|
border: 1px solid rgba(197, 88, 217, 0.13);
|
|
pointer-events: none;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
#lightbox p {
|
|
font-size: 16px; line-height: 1.65;
|
|
color: var(--text-warm);
|
|
margin-bottom: 20px;
|
|
}
|
|
|
|
#lightbox .nav-hint {
|
|
font-size: 13px; color: var(--text-muted);
|
|
margin-bottom: 24px; line-height: 1.5;
|
|
display: block;
|
|
}
|
|
|
|
#lightbox-enter {
|
|
padding: 8px 28px;
|
|
font-family: 'Space Grotesk', system-ui, -apple-system, sans-serif;
|
|
font-size: 14px; font-weight: 500;
|
|
color: var(--bg-void);
|
|
background: var(--fire-amber);
|
|
border: none; border-radius: 4px;
|
|
cursor: pointer;
|
|
transition: opacity 150ms ease;
|
|
}
|
|
#lightbox-enter:hover, #lightbox-enter:focus { opacity: 0.85; outline: none; }
|
|
|
|
/* ── Reduced motion ── */
|
|
@media (prefers-reduced-motion: reduce) {
|
|
.star-node[data-star] .star-visual::before,
|
|
.star-node[data-star] .star-visual::after { animation: none; opacity: 0.06; }
|
|
#star-zone { will-change: auto; }
|
|
#content-frame { transition: none; }
|
|
.star-dot { transition: none; }
|
|
}
|
|
|
|
/* ── Responsive ── */
|
|
@media (max-width: 768px) {
|
|
.star-label { font-size: 12px; }
|
|
#lightbox { padding: 24px; max-width: 380px; }
|
|
.billboard-nav.bb-sm { font-size: 12px; }
|
|
.billboard-nav.bb-md { font-size: 15px; }
|
|
.billboard-nav.bb-lg { font-size: 18px; }
|
|
}
|
|
|
|
@media (max-width: 480px) {
|
|
.star-dot { width: 12px; height: 12px; }
|
|
.star-node:hover .star-dot,
|
|
.star-node:focus .star-dot { width: 22px; height: 22px; }
|
|
.star-node.current .star-dot { width: 36px; height: 36px; }
|
|
.star-node.current .star-label { font-size: clamp(18px, 6vw, 24px); }
|
|
.star-node:active .star-label { opacity: 1; }
|
|
#lightbox { padding: 20px 16px; }
|
|
#lightbox p { font-size: 15px; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div id="star-zone" role="navigation" aria-label="Star map navigation">
|
|
<canvas id="nebula-canvas"></canvas>
|
|
<canvas id="star-canvas"></canvas>
|
|
</div>
|
|
|
|
<div id="transition-zone">
|
|
<canvas id="grid-canvas"></canvas>
|
|
<div id="skyline"></div>
|
|
</div>
|
|
|
|
<div id="content-zone" role="region" aria-label="Section content">
|
|
<iframe id="content-frame" title="Section content"></iframe>
|
|
</div>
|
|
|
|
<div id="hud">
|
|
<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>
|
|
</div>
|
|
<div id="sr-announce" aria-live="polite" aria-atomic="true"></div>
|
|
|
|
<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>
|
|
<span class="nav-hint">click stars to explore · arrow keys to navigate · escape to return</span>
|
|
<button id="lightbox-enter">Enter</button>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
(function() {
|
|
'use strict';
|
|
|
|
var prefersReducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
|
|
var STARS = [
|
|
{ id: 'writings', label: 'Writings', sign: '文', x: 25, y: 18, href: '', accent: '#a0c4ff' },
|
|
{ id: 'videos', label: 'Videos', sign: '映', x: 68, y: 15, href: '', accent: '#d4654a' },
|
|
{ id: 'music', label: 'Music', sign: '♬', x: 12, y: 42, href: '', accent: '#2ac4b3' },
|
|
{ id: 'images', label: 'Images', sign: '絵', x: 55, y: 35, href: '', accent: '#86efac' },
|
|
{ id: 'playlists', label: 'Playlists', sign: '≡', x: 78, y: 48, href: '', accent: '#ffcf40' },
|
|
{ id: 'watchlists', label: 'Watchlists', sign: '視', x: 22, y: 65, href: '', accent: '#c558d9' },
|
|
{ id: 'toolsntoys', label: 'ToolsnToys', sign: '⚙', x: 50, y: 72, href: '', accent: '#f472b6' },
|
|
{ id: 'creatorlists', label: 'Creatorlists', sign: '創', x: 75, y: 70, href: '', accent: '#c4a24a' }
|
|
];
|
|
|
|
var currentStarIndex = 0;
|
|
var inContentMode = false;
|
|
var lightboxOpen = false;
|
|
var visited = JSON.parse(localStorage.getItem('sp-visited') || '{}');
|
|
var isScrolling = false;
|
|
|
|
// ── Star color palette — 8 colors, weighted per aesthetic brief ──
|
|
// 35% cool white, 15% warm white, 12% pale blue, 10% toucan gold,
|
|
// 8% fairy pink, 8% orchid, 7% mint green, 5% coral
|
|
var STAR_COLORS = [
|
|
{ r: 200, g: 210, b: 230, weight: 35 },
|
|
{ r: 240, g: 220, b: 190, weight: 15 },
|
|
{ r: 140, g: 180, b: 230, weight: 12 },
|
|
{ r: 255, g: 207, b: 64, weight: 10 },
|
|
{ r: 244, g: 114, b: 182, weight: 8 },
|
|
{ r: 197, g: 88, b: 217, weight: 8 },
|
|
{ r: 134, g: 239, b: 172, weight: 7 },
|
|
{ r: 212, g: 101, b: 74, weight: 5 }
|
|
];
|
|
|
|
function pickStarColor() {
|
|
var roll = Math.random() * 100, cum = 0;
|
|
for (var i = 0; i < STAR_COLORS.length; i++) {
|
|
cum += STAR_COLORS[i].weight;
|
|
if (roll < cum) return STAR_COLORS[i];
|
|
}
|
|
return STAR_COLORS[0];
|
|
}
|
|
|
|
function pickStarSize() {
|
|
var roll = Math.random() * 100;
|
|
if (roll < 42) return 0.3 + Math.random() * 0.2; // dim far
|
|
if (roll < 70) return 0.5 + Math.random() * 0.5; // mid
|
|
if (roll < 90) return 1.0 + Math.random() * 0.5; // bright
|
|
return 1.5 + Math.random() * 1.5; // mega (up to 3px)
|
|
}
|
|
|
|
// ── Nebula canvas (static, drawn once on resize) ──
|
|
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, h = nebulaCanvas.height;
|
|
var isMobile = w < 480;
|
|
|
|
// 6 bold washes — pushed to 0.07-0.08 opacity at center
|
|
|
|
// Wash 1: orchid — center-left
|
|
var g1 = nebulaCtx.createRadialGradient(w*0.25, h*0.35, 0, w*0.25, h*0.35, w*0.38);
|
|
g1.addColorStop(0, 'rgba(197, 88, 217, 0.08)');
|
|
g1.addColorStop(0.5, 'rgba(197, 88, 217, 0.03)');
|
|
g1.addColorStop(1, 'rgba(197, 88, 217, 0)');
|
|
nebulaCtx.fillStyle = g1;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
// Wash 2: teal — lower-left
|
|
var g2 = nebulaCtx.createRadialGradient(w*0.12, h*0.72, 0, w*0.12, h*0.72, w*0.38);
|
|
g2.addColorStop(0, 'rgba(42, 196, 179, 0.07)');
|
|
g2.addColorStop(0.5, 'rgba(42, 196, 179, 0.025)');
|
|
g2.addColorStop(1, 'rgba(42, 196, 179, 0)');
|
|
nebulaCtx.fillStyle = g2;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
// Wash 3: cosmic purple — upper-right
|
|
var g3 = nebulaCtx.createRadialGradient(w*0.78, h*0.18, 0, w*0.78, h*0.18, w*0.42);
|
|
g3.addColorStop(0, 'rgba(74, 29, 110, 0.08)');
|
|
g3.addColorStop(0.5, 'rgba(74, 29, 110, 0.03)');
|
|
g3.addColorStop(1, 'rgba(74, 29, 110, 0)');
|
|
nebulaCtx.fillStyle = g3;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
if (!isMobile) {
|
|
// Wash 4: fairy-pink — upper-center
|
|
var g4 = nebulaCtx.createRadialGradient(w*0.52, h*0.12, 0, w*0.52, h*0.12, w*0.3);
|
|
g4.addColorStop(0, 'rgba(244, 114, 182, 0.07)');
|
|
g4.addColorStop(0.5, 'rgba(244, 114, 182, 0.025)');
|
|
g4.addColorStop(1, 'rgba(244, 114, 182, 0)');
|
|
nebulaCtx.fillStyle = g4;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
// Wash 5: paradise orange — right-center
|
|
var g5 = nebulaCtx.createRadialGradient(w*0.85, h*0.55, 0, w*0.85, h*0.55, w*0.3);
|
|
g5.addColorStop(0, 'rgba(255, 127, 63, 0.06)');
|
|
g5.addColorStop(0.5, 'rgba(255, 127, 63, 0.02)');
|
|
g5.addColorStop(1, 'rgba(255, 127, 63, 0)');
|
|
nebulaCtx.fillStyle = g5;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
|
|
// Wash 6: toucan gold — lower-center
|
|
var g6 = nebulaCtx.createRadialGradient(w*0.52, h*0.78, 0, w*0.52, h*0.78, w*0.28);
|
|
g6.addColorStop(0, 'rgba(255, 207, 64, 0.05)');
|
|
g6.addColorStop(0.5, 'rgba(255, 207, 64, 0.018)');
|
|
g6.addColorStop(1, 'rgba(255, 207, 64, 0)');
|
|
nebulaCtx.fillStyle = g6;
|
|
nebulaCtx.fillRect(0, 0, w, h);
|
|
}
|
|
}
|
|
|
|
// ── Star field (animated, throttled at ~20fps) ──
|
|
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, h = starCanvas.height;
|
|
var isMobile = w < 480;
|
|
var count = isMobile ? 200 : (300 + Math.floor(Math.random() * 80));
|
|
bgStars = [];
|
|
|
|
for (var i = 0; i < count; i++) {
|
|
var color = pickStarColor();
|
|
var r = pickStarSize();
|
|
|
|
var steady = Math.random() < 0.12;
|
|
var speed = steady ? 0 :
|
|
(r > 1.5 ? 0.15 + Math.random() * 0.25 :
|
|
(r < 0.5 ? 0.6 + Math.random() * 0.6 :
|
|
0.3 + Math.random() * 0.5));
|
|
|
|
bgStars.push({
|
|
x: Math.random() * w,
|
|
y: Math.random() * h,
|
|
r: r,
|
|
base: 0.25 + Math.random() * 0.55,
|
|
phase: Math.random() * Math.PI * 2,
|
|
speed: speed,
|
|
color: color.r + ',' + color.g + ',' + color.b
|
|
});
|
|
}
|
|
}
|
|
|
|
function resizeCanvases() {
|
|
starCanvas.width = starCanvas.offsetWidth;
|
|
starCanvas.height = starCanvas.offsetHeight;
|
|
drawNebulae();
|
|
generateBgStars();
|
|
drawVaporwaveGrid();
|
|
}
|
|
|
|
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 = (s.speed === 0 || prefersReducedMotion)
|
|
? s.base
|
|
: s.base + Math.sin(t * s.speed + s.phase) * 0.3;
|
|
if (a < 0.05) a = 0.05;
|
|
if (a > 1) a = 1;
|
|
starCtx.beginPath();
|
|
starCtx.arc(s.x, s.y, s.r, 0, 6.2832);
|
|
starCtx.fillStyle = 'rgba(' + s.color + ',' + a + ')';
|
|
starCtx.fill();
|
|
}
|
|
requestAnimationFrame(drawStars);
|
|
}
|
|
|
|
// ── Vaporwave perspective grid ──
|
|
function drawVaporwaveGrid() {
|
|
var canvas = document.getElementById('grid-canvas');
|
|
var ctx = canvas.getContext('2d');
|
|
canvas.width = canvas.offsetWidth;
|
|
canvas.height = canvas.offsetHeight;
|
|
var w = canvas.width, h = canvas.height;
|
|
if (w === 0 || h === 0) return;
|
|
|
|
var vpX = w * 0.5; // horizontal center (spread origin)
|
|
var gridTop = 3; // ≤8px below star-zone base — grid aligns flush with sky
|
|
var spread = 2.4; // vertical lines fan this far beyond viewport at bottom
|
|
|
|
var pal = [
|
|
[42, 196, 179], // teal
|
|
[197, 88, 217], // orchid
|
|
[244, 114, 182], // fairy-pink
|
|
[255, 207, 64], // toucan gold
|
|
[134, 239, 172], // mint
|
|
[212, 101, 74], // coral
|
|
[232, 148, 58], // amber
|
|
[63, 191, 175], // waterfall
|
|
];
|
|
|
|
// Horizontal lines — quadratic spacing: dense near gridTop, sparse at bottom
|
|
// Full viewport width edge-to-edge at every y
|
|
var numH = 22;
|
|
for (var i = 0; i < numH; i++) {
|
|
var s = i / (numH - 1);
|
|
var y = gridTop + (h - gridTop) * (s * s);
|
|
var ci = i % pal.length;
|
|
var alpha = 0.045 + s * 0.19;
|
|
ctx.strokeStyle = 'rgba(' + pal[ci][0] + ',' + pal[ci][1] + ',' + pal[ci][2] + ',' + alpha + ')';
|
|
ctx.lineWidth = 1.3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(0, y);
|
|
ctx.lineTo(w, y);
|
|
ctx.stroke();
|
|
}
|
|
|
|
// Vertical lines:
|
|
// TOP (gridTop): evenly spread across FULL viewport width, 0 → w
|
|
// BOTTOM (h): fan outward beyond viewport by spread factor
|
|
// Lines end at the same gridTop as horizontal lines — perfect shared ceiling
|
|
var numV = 26;
|
|
for (var j = 0; j <= numV; j++) {
|
|
var topX = w * (j / numV); // full width at top
|
|
var bottomX = vpX + (topX - vpX) * spread; // spreads beyond edges at bottom
|
|
var ci = j % pal.length;
|
|
var dist = Math.abs(j / numV - 0.5) * 2; // 0 at center, 1 at edges
|
|
var alpha = 0.055 + dist * 0.10;
|
|
ctx.strokeStyle = 'rgba(' + pal[ci][0] + ',' + pal[ci][1] + ',' + pal[ci][2] + ',' + alpha + ')';
|
|
ctx.lineWidth = 1.3;
|
|
ctx.beginPath();
|
|
ctx.moveTo(topX, gridTop); // top: full width
|
|
ctx.lineTo(bottomX, h); // bottom: fans beyond viewport
|
|
ctx.stroke();
|
|
}
|
|
}
|
|
|
|
// ── Parallax ──
|
|
var starZone = document.getElementById('star-zone');
|
|
|
|
function updateParallax() {
|
|
if (prefersReducedMotion || !inContentMode) {
|
|
starZone.style.transform = '';
|
|
return;
|
|
}
|
|
var scrollY = window.scrollY || window.pageYOffset;
|
|
var offset = Math.min(scrollY * 0.10, window.innerHeight * 0.14);
|
|
starZone.style.transform = 'translateY(-' + offset + 'px)';
|
|
}
|
|
|
|
window.addEventListener('scroll', function() {
|
|
if (inContentMode) requestAnimationFrame(updateParallax);
|
|
}, { passive: true });
|
|
|
|
// ── Star DOM nodes ──
|
|
var starElements = [];
|
|
var srAnnounce = document.getElementById('sr-announce');
|
|
|
|
STARS.forEach(function(star, i) {
|
|
var btn = document.createElement('button');
|
|
btn.className = 'star-node';
|
|
btn.setAttribute('aria-label', star.label + ' section');
|
|
btn.setAttribute('data-star', star.id);
|
|
btn.style.left = star.x + '%';
|
|
btn.style.top = star.y + '%';
|
|
|
|
var visual = document.createElement('div');
|
|
visual.className = 'star-visual';
|
|
|
|
var dot = document.createElement('span');
|
|
dot.className = 'star-dot';
|
|
visual.appendChild(dot);
|
|
|
|
var label = document.createElement('span');
|
|
label.className = 'star-label';
|
|
label.textContent = star.label;
|
|
|
|
btn.appendChild(visual);
|
|
btn.appendChild(label);
|
|
starZone.appendChild(btn);
|
|
starElements.push(btn);
|
|
|
|
if (visited[star.id]) btn.classList.add('visited');
|
|
btn.addEventListener('click', function(e) { e.preventDefault(); selectStar(i); });
|
|
});
|
|
|
|
starElements[0].classList.add('current');
|
|
|
|
// ── Skyline — Lush Ruins ──
|
|
function generateSkyline() {
|
|
var skyline = document.getElementById('skyline');
|
|
skyline.innerHTML = '';
|
|
|
|
// Fit buildings to viewport — avg ~26px per building keeps all on-screen
|
|
var avgBuildingW = 26;
|
|
var totalBuildings = Math.max(20, Math.min(52, Math.floor(window.innerWidth / avgBuildingW)));
|
|
|
|
// Billboard positions spread evenly across the INNER 80% to avoid edge clipping
|
|
var edgePad = Math.max(1, Math.floor(totalBuildings * 0.10));
|
|
var usable = totalBuildings - edgePad * 2; // safe center band
|
|
var step = Math.max(1, Math.floor(usable / STARS.length));
|
|
|
|
// Each section gets a distinct billboard color and size
|
|
var bbColors = ['bb-teal', 'bb-coral', 'bb-orchid', 'bb-green', 'bb-gold', 'bb-water', 'bb-pink', 'bb-amber'];
|
|
var bbSizes = ['bb-sm', 'bb-md', 'bb-lg', 'bb-sm', 'bb-lg', 'bb-md', 'bb-sm', 'bb-lg'];
|
|
var billboardAssign = {};
|
|
STARS.forEach(function(s, i) {
|
|
billboardAssign[edgePad + i * step] = {
|
|
star: s,
|
|
colorClass: bbColors[i],
|
|
sizeClass: bbSizes[i]
|
|
};
|
|
});
|
|
|
|
for (var i = 0; i < totalBuildings; i++) {
|
|
var div = document.createElement('div');
|
|
div.className = 'building';
|
|
var bw = 12 + Math.random() * 50;
|
|
// ~15% of buildings are spires that pierce the grid
|
|
var bh = Math.random() < 0.15
|
|
? 340 + Math.random() * 180 // tall spires: 340-520px
|
|
: 20 + Math.random() * 270; // normal: 20-290px
|
|
div.style.width = bw + 'px';
|
|
div.style.height = bh + 'px';
|
|
div.style.marginLeft = (Math.random() * 2) + 'px';
|
|
|
|
// Edge-light on ~28%
|
|
if (Math.random() < 0.28) div.classList.add('edge-lit');
|
|
|
|
// Bioluminescent stripes — mint or orchid — on ~40%
|
|
if (Math.random() < 0.40 && bh > 55) {
|
|
var stripe = document.createElement('span');
|
|
stripe.className = 'bio-stripe' + (Math.random() < 0.55 ? '' : ' orchid');
|
|
var sTop = 8 + Math.random() * 28;
|
|
var sH = 14 + Math.random() * 30;
|
|
stripe.style.cssText = 'top:' + sTop + '%;height:' + sH + '%;left:0;';
|
|
div.appendChild(stripe);
|
|
}
|
|
|
|
// Polychromatic windows — brighter than before
|
|
if (Math.random() > 0.25 && bh > 45) {
|
|
var winCount = Math.floor(bh / 22);
|
|
for (var j = 0; j < winCount; j++) {
|
|
if (Math.random() > 0.52) continue;
|
|
var win = document.createElement('span');
|
|
// 50% amber, 20% teal, 15% orchid, 10% pink, 5% phosphor
|
|
var winRoll = Math.random() * 100;
|
|
var winColor;
|
|
if (winRoll < 50) winColor = '232,148,58';
|
|
else if (winRoll < 70) winColor = '42,196,179';
|
|
else if (winRoll < 85) winColor = '197,88,217';
|
|
else if (winRoll < 95) winColor = '244,114,182';
|
|
else winColor = '0,255,65';
|
|
var winAlpha = 0.18 + Math.random() * 0.20; // bright: 0.18-0.38
|
|
win.style.cssText = 'position:absolute;width:2px;height:2px;background:rgba(' +
|
|
winColor + ',' + winAlpha + ');left:' +
|
|
(3 + Math.random() * (bw - 6)) + 'px;top:' +
|
|
(6 + j * 20 + Math.random() * 8) + 'px;';
|
|
div.appendChild(win);
|
|
}
|
|
}
|
|
|
|
// Billboard
|
|
if (billboardAssign[i]) {
|
|
var info = billboardAssign[i];
|
|
var bb = document.createElement('a');
|
|
bb.className = 'billboard-nav ' + info.colorClass + ' ' + info.sizeClass;
|
|
bb.textContent = info.star.sign;
|
|
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 with cubic ease ──
|
|
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 progress = Math.min((time - startTime) / duration, 1);
|
|
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];
|
|
visited[star.id] = true;
|
|
localStorage.setItem('sp-visited', JSON.stringify(visited));
|
|
starElements[index].classList.add('visited');
|
|
starElements.forEach(function(el) { el.classList.remove('current'); });
|
|
starElements[index].classList.add('current');
|
|
currentStarIndex = index;
|
|
srAnnounce.textContent = 'Opening ' + star.label;
|
|
inContentMode = true;
|
|
document.body.classList.add('content-mode');
|
|
backBtn.textContent = 'back to stars';
|
|
|
|
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'); };
|
|
}
|
|
|
|
var contentTop = document.getElementById('content-zone').offsetTop;
|
|
smoothScrollTo(contentTop, 1100);
|
|
}
|
|
|
|
function returnToStars() {
|
|
if (isScrolling) return;
|
|
inContentMode = false;
|
|
backBtn.textContent = 'stars';
|
|
srAnnounce.textContent = 'Returning to star map';
|
|
smoothScrollTo(0, 700, 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 smoothScrollTo(0, 300);
|
|
});
|
|
|
|
// ── Keyboard spatial navigation ──
|
|
function findNearestStar(fromIndex, direction) {
|
|
var from = STARS[fromIndex];
|
|
var bestIndex = -1, bestScore = Infinity;
|
|
for (var i = 0; i < STARS.length; i++) {
|
|
if (i === fromIndex) continue;
|
|
var dx = STARS[i].x - from.x, dy = STARS[i].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 penalty = (direction === 'up' || direction === 'down') ? Math.abs(dx)*0.5 : Math.abs(dy)*0.5;
|
|
var score = dist + penalty;
|
|
if (score < bestScore) { bestScore = score; bestIndex = i; }
|
|
}
|
|
return bestIndex;
|
|
}
|
|
|
|
document.addEventListener('keydown', function(e) {
|
|
if (lightboxOpen) {
|
|
if (e.key === 'Escape' || e.key === 'Enter') { closeLightbox(); e.preventDefault(); }
|
|
return;
|
|
}
|
|
if (inContentMode) {
|
|
if (e.key === 'Escape') { returnToStars(); e.preventDefault(); }
|
|
return;
|
|
}
|
|
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();
|
|
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-v5-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(); });
|
|
overlay.addEventListener('keydown', function(e) {
|
|
if (e.key === 'Tab') { e.preventDefault(); enterBtn.focus(); }
|
|
});
|
|
|
|
// ── Resize ──
|
|
var resizeTimer;
|
|
window.addEventListener('resize', function() {
|
|
clearTimeout(resizeTimer);
|
|
resizeTimer = setTimeout(resizeCanvases, 150);
|
|
});
|
|
|
|
// ── Init ──
|
|
resizeCanvases();
|
|
requestAnimationFrame(drawStars);
|
|
|
|
if (!localStorage.getItem('sp-v5-welcomed')) showLightbox();
|
|
else starElements[currentStarIndex].focus();
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|