695 lines
23 KiB
HTML
695 lines
23 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="en" dir="ltr">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Edut — Deterministic Governance Infrastructure</title>
|
|
<meta name="description" content="Edut LLC. Development and licensing of deterministic governance systems. A local-first framework for governed business operations.">
|
|
<meta name="theme-color" content="#f0f4f8">
|
|
|
|
<!-- Open Graph: Math Layer -->
|
|
<meta property="og:title" content="Edut — Deterministic Governance Systems">
|
|
<meta property="og:description" content="Development and licensing of deterministic governance systems. Local-first infrastructure with deterministic controls, evidence trails, and optional intelligence layers.">
|
|
<meta property="og:type" content="website">
|
|
<meta property="og:url" content="https://edut.ai">
|
|
<meta property="og:site_name" content="Edut">
|
|
|
|
<!-- Math Layer: Structured Data -->
|
|
<script type="application/ld+json">
|
|
{
|
|
"@context": "https://schema.org",
|
|
"@type": "Organization",
|
|
"name": "Edut LLC",
|
|
"url": "https://edut.ai",
|
|
"description": "Development and licensing of deterministic governance systems",
|
|
"foundingDate": "2026",
|
|
"address": {
|
|
"@type": "PostalAddress",
|
|
"addressLocality": "Herald",
|
|
"addressRegion": "CA",
|
|
"postalCode": "95638",
|
|
"addressCountry": "US"
|
|
},
|
|
"sameAs": [
|
|
"https://edut.dev"
|
|
]
|
|
}
|
|
</script>
|
|
|
|
<!-- AI + Accessibility Context: visible to screen readers and AI, hidden visually -->
|
|
<style>
|
|
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500&display=swap');
|
|
|
|
*, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
|
|
html, body { height: 100%; overflow: hidden; }
|
|
|
|
body {
|
|
font-family: 'IBM Plex Mono', 'Courier New', Courier, monospace;
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
min-height: 100vh;
|
|
position: relative;
|
|
cursor: pointer;
|
|
-webkit-font-smoothing: antialiased;
|
|
-moz-osx-font-smoothing: grayscale;
|
|
|
|
/* Clean white background */
|
|
background: #f0f4f8;
|
|
}
|
|
|
|
/* Ambient glow handled by Three.js instead */
|
|
|
|
.skip-link {
|
|
position: absolute;
|
|
top: -100px;
|
|
left: 8px;
|
|
background: #2c2c2c;
|
|
color: #f0f4f8;
|
|
padding: 8px 16px;
|
|
font-size: 13px;
|
|
z-index: 1000;
|
|
text-decoration: none;
|
|
border-radius: 2px;
|
|
}
|
|
.skip-link:focus {
|
|
top: 8px;
|
|
outline: 2px solid #2c2c2c;
|
|
outline-offset: 2px;
|
|
}
|
|
|
|
.wrapper {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
pointer-events: none;
|
|
position: relative;
|
|
z-index: 1;
|
|
}
|
|
|
|
#globe-container {
|
|
width: 420px;
|
|
height: 420px;
|
|
pointer-events: none;
|
|
}
|
|
|
|
.identity {
|
|
text-align: center;
|
|
margin-top: 28px;
|
|
}
|
|
|
|
/* Identity name: 16px, needs 4.5:1 for AA normal text */
|
|
.identity-name {
|
|
font-size: 16px;
|
|
letter-spacing: 0.5em;
|
|
text-transform: uppercase;
|
|
color: #3a3d42;
|
|
font-weight: 400;
|
|
transition: opacity 0.6s ease, letter-spacing 0.6s ease;
|
|
cursor: pointer;
|
|
}
|
|
.identity-name:hover {
|
|
color: #2c2c2c;
|
|
letter-spacing: 0.6em;
|
|
}
|
|
|
|
/* Definition: needs to be readable */
|
|
.identity-definition {
|
|
font-size: 13px;
|
|
letter-spacing: 0.25em;
|
|
color: #555a60;
|
|
font-weight: 300;
|
|
margin-top: 10px;
|
|
transition: color 0.6s ease;
|
|
cursor: pointer;
|
|
}
|
|
.identity-definition:hover { color: #3a3d42; }
|
|
|
|
/* Descriptor */
|
|
.identity-descriptor {
|
|
font-size: 12px;
|
|
letter-spacing: 0.15em;
|
|
color: #606264;
|
|
font-weight: 300;
|
|
margin-top: 8px;
|
|
transition: color 0.6s ease;
|
|
cursor: pointer;
|
|
}
|
|
.identity-descriptor:hover { color: #555a60; }
|
|
|
|
/* Acknowledged token */
|
|
.acknowledged {
|
|
font-size: 12px;
|
|
letter-spacing: 0.3em;
|
|
color: #606264;
|
|
font-weight: 300;
|
|
margin-top: 24px;
|
|
opacity: 0;
|
|
transition: opacity 1.5s ease;
|
|
cursor: default;
|
|
}
|
|
.acknowledged.visible { opacity: 1; }
|
|
.acknowledged.visible:hover { color: #555a60; }
|
|
|
|
/* Footer */
|
|
footer {
|
|
position: fixed;
|
|
bottom: 0; left: 0; right: 0;
|
|
padding: 16px 24px;
|
|
display: flex;
|
|
justify-content: center;
|
|
gap: 16px;
|
|
align-items: center;
|
|
font-size: 11px;
|
|
letter-spacing: 0.1em;
|
|
font-weight: 300;
|
|
pointer-events: auto;
|
|
z-index: 10;
|
|
}
|
|
footer span {
|
|
color: #606264;
|
|
}
|
|
footer a {
|
|
color: #555a60;
|
|
text-decoration: none;
|
|
text-transform: uppercase;
|
|
transition: color 0.4s ease;
|
|
pointer-events: auto;
|
|
}
|
|
footer a:hover { color: #2c2c2c; }
|
|
footer a:focus-visible {
|
|
outline: 2px solid #2c2c2c;
|
|
outline-offset: 3px;
|
|
border-radius: 2px;
|
|
}
|
|
|
|
/* Screen reader only text */
|
|
.sr-only {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: nowrap;
|
|
border: 0;
|
|
}
|
|
|
|
/* Context Layer: accessible to screen readers + AI, hidden visually
|
|
Uses same technique as sr-only but as a full section.
|
|
Future: toggling .context-layer--visible reveals it on screen. */
|
|
.context-layer {
|
|
position: absolute;
|
|
width: 1px;
|
|
height: 1px;
|
|
padding: 0;
|
|
margin: -1px;
|
|
overflow: hidden;
|
|
clip: rect(0, 0, 0, 0);
|
|
white-space: normal;
|
|
border: 0;
|
|
}
|
|
.context-layer h2,
|
|
.context-layer h3 {
|
|
font-size: 14px;
|
|
font-weight: 500;
|
|
margin-top: 16px;
|
|
margin-bottom: 8px;
|
|
}
|
|
.context-layer p {
|
|
font-size: 14px;
|
|
font-weight: 300;
|
|
line-height: 1.8;
|
|
color: #3a3a3a;
|
|
margin-bottom: 12px;
|
|
}
|
|
|
|
.localizable[dir="rtl"] {
|
|
direction: rtl;
|
|
unicode-bidi: plaintext;
|
|
}
|
|
|
|
/* Future: when activated, reveals context layer as readable overlay */
|
|
.context-layer--visible {
|
|
position: fixed;
|
|
top: 0; left: 0; right: 0; bottom: 0;
|
|
width: auto;
|
|
height: auto;
|
|
margin: 0;
|
|
clip: auto;
|
|
overflow-y: auto;
|
|
background: #f0f4f8;
|
|
padding: 60px 24px;
|
|
z-index: 100;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
.context-layer--visible article {
|
|
max-width: 640px;
|
|
width: 100%;
|
|
}
|
|
|
|
@media (max-width: 600px) {
|
|
#globe-container { width: 300px; height: 300px; }
|
|
.identity-name { font-size: 14px; letter-spacing: 0.4em; }
|
|
.identity-definition { font-size: 12px; }
|
|
.identity-descriptor { font-size: 11px; }
|
|
body::before { width: 350px; height: 350px; }
|
|
}
|
|
|
|
@media (prefers-reduced-motion: reduce) {
|
|
*, *::before, *::after {
|
|
transition: none !important;
|
|
animation: none !important;
|
|
}
|
|
body::before { animation: none; opacity: 0.8; }
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<a href="#main-content" class="skip-link localizable" data-i18n="skip_to_content">Skip to content</a>
|
|
|
|
<div class="wrapper" role="main" id="main-content">
|
|
<div id="globe-container" aria-hidden="true"></div>
|
|
<div class="identity">
|
|
<h1 class="identity-name">edut · עֵדוּת</h1>
|
|
<p class="identity-definition localizable" data-i18n="definition">testimony · witness · evidence</p>
|
|
<p class="identity-descriptor localizable" data-i18n="descriptor">Development and licensing of deterministic governance systems</p>
|
|
</div>
|
|
<div class="acknowledged" id="acknowledged" aria-live="polite"></div>
|
|
<span class="sr-only localizable" id="interaction-hint" data-i18n="interaction_hint">Click anywhere on the page to begin your access request.</span>
|
|
</div>
|
|
|
|
<!-- Context Layer: accessible to screen readers, AI systems, and future audio playback -->
|
|
<section id="context-layer" class="context-layer" aria-label="About Edut">
|
|
<article>
|
|
<h2 class="localizable" data-i18n="context.heading">Research Abstract</h2>
|
|
<p class="localizable" data-i18n="context.intro">Edut (Hebrew: עֵדוּת — testimony, witness, evidence) is a systems research and licensing organization focused on deterministic governance infrastructure for real-world operations.</p>
|
|
<p class="localizable" data-i18n="context.thesis">The central thesis is simple: operational dependence should not require permanent dependence on outside control planes. Infrastructure should remain useful under constrained connectivity, changing vendor economics, and long operating horizons.</p>
|
|
<p class="localizable" data-i18n="context.approach">Edut addresses this through a math-first architecture: deterministic data modeling, verifiable decision paths, and structured evidence before optimization layers are introduced.</p>
|
|
<h3 class="localizable" data-i18n="context.principles_heading">Framework Principles</h3>
|
|
<p class="localizable" data-i18n="context.deterministic"><strong>Deterministic Foundations.</strong> Core policy and safety decisions are computed from explicit rules and typed inputs. Outputs are testable, reproducible, and auditable.</p>
|
|
<p class="localizable" data-i18n="context.ownership"><strong>Operational Ownership.</strong> Deployments are designed for client-controlled operation on client-chosen infrastructure, with clear contractual boundaries around rights and responsibilities.</p>
|
|
<p class="localizable" data-i18n="context.layered"><strong>Layered Architecture.</strong> The human layer presents simple surfaces. The mathematical layer assembles and scores structured context. The intelligence layer is optional augmentation and never the sole dependency.</p>
|
|
<p class="localizable" data-i18n="context.permanence"><strong>Permanence by Design.</strong> Systems are engineered for long service life with upgrade paths, compatibility controls, and explicit rollback discipline.</p>
|
|
<p class="localizable" data-i18n="context.constitutional"><strong>Constitutional Governance.</strong> Mechanical invariants enforce safety boundaries consistently across modes, users, and modules.</p>
|
|
<h3 class="localizable" data-i18n="context.domains_heading">Research Domains</h3>
|
|
<p class="localizable" data-i18n="context.domains">Current research spans deterministic orchestration, workspace-isolated data graphs, connector contracts, evidence integrity, module governance, and long-horizon operational resilience.</p>
|
|
<h3 class="localizable" data-i18n="context.licensing_heading">Licensing Model</h3>
|
|
<p class="localizable" data-i18n="context.licensing">Edut uses a build-and-license model for governed software systems. Licensing is designed for durable runtime rights with explicit policy, support, and deployment boundaries.</p>
|
|
<h3 class="localizable" data-i18n="context.status_heading">Current Status</h3>
|
|
<p class="localizable" data-i18n="context.status">The framework is in active implementation and validation. Early designations are being issued for staged launch readiness and controlled deployment onboarding.</p>
|
|
<p class="localizable" data-i18n="context.note"><strong>Note:</strong> This page is intentionally minimal. It reflects a product philosophy where computation is complex and interfaces remain quiet.</p>
|
|
</article>
|
|
|
|
<!-- Future: read-aloud button
|
|
<button id="read-context" aria-label="Read about Edut">Listen</button>
|
|
-->
|
|
</section>
|
|
|
|
<footer role="contentinfo">
|
|
<span>© Edut LLC</span>
|
|
<a href="/privacy" id="privacyLink" class="localizable" data-i18n="privacy">Privacy</a>
|
|
<a href="/terms" id="termsLink" class="localizable" data-i18n="terms">Terms</a>
|
|
</footer>
|
|
|
|
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
<script>
|
|
const container = document.getElementById('globe-container');
|
|
const supportedLanguages = ['en', 'zh', 'es', 'ar', 'fr', 'pt', 'de', 'ja', 'ru', 'ko', 'hi', 'he'];
|
|
let activeLocale = null;
|
|
|
|
function getByPath(obj, path) {
|
|
return path.split('.').reduce((value, part) => (value && value[part] !== undefined ? value[part] : undefined), obj);
|
|
}
|
|
|
|
function resolveLanguage(input) {
|
|
if (!input) return 'en';
|
|
const normalized = String(input).toLowerCase();
|
|
if (normalized.startsWith('zh')) return 'zh';
|
|
const base = normalized.split('-')[0];
|
|
return supportedLanguages.includes(base) ? base : 'en';
|
|
}
|
|
|
|
async function loadLocaleBundle(lang) {
|
|
const candidates = [
|
|
'/translations/' + lang + '.json',
|
|
'../translations/' + lang + '.json',
|
|
'./translations/' + lang + '.json',
|
|
];
|
|
for (const url of candidates) {
|
|
try {
|
|
const res = await fetch(url, { cache: 'no-cache' });
|
|
if (!res.ok) continue;
|
|
return await res.json();
|
|
} catch (err) {
|
|
// Try next path.
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function renderAcknowledged(code) {
|
|
if (!code) return;
|
|
const el = document.getElementById('acknowledged');
|
|
const token = code.slice(0,4) + '-' + code.slice(4,8) + '-' + code.slice(8,12) + '-' + code.slice(12);
|
|
const label = activeLocale && activeLocale.acknowledged ? activeLocale.acknowledged : 'acknowledged';
|
|
el.textContent = label + ' · ' + token;
|
|
el.classList.add('visible');
|
|
}
|
|
|
|
function applyLocale(bundle, lang) {
|
|
activeLocale = bundle;
|
|
localStorage.setItem('edut_lang', lang);
|
|
document.documentElement.setAttribute('lang', lang);
|
|
document.documentElement.setAttribute('dir', 'ltr');
|
|
|
|
const direction = bundle.dir === 'rtl' ? 'rtl' : 'ltr';
|
|
const nodes = document.querySelectorAll('[data-i18n]');
|
|
nodes.forEach((node) => {
|
|
const key = node.getAttribute('data-i18n');
|
|
const value = getByPath(bundle, key);
|
|
if (typeof value === 'string') {
|
|
node.textContent = value;
|
|
}
|
|
node.setAttribute('dir', direction);
|
|
});
|
|
|
|
const savedCode = localStorage.getItem('edut_acknowledged');
|
|
if (savedCode) {
|
|
renderAcknowledged(savedCode);
|
|
}
|
|
}
|
|
|
|
async function initializeLocale() {
|
|
const saved = localStorage.getItem('edut_lang');
|
|
const requested = resolveLanguage(saved || navigator.language || navigator.userLanguage);
|
|
const primaryBundle = await loadLocaleBundle(requested);
|
|
if (primaryBundle) {
|
|
applyLocale(primaryBundle, requested);
|
|
return;
|
|
}
|
|
const fallbackBundle = await loadLocaleBundle('en');
|
|
if (fallbackBundle) {
|
|
applyLocale(fallbackBundle, 'en');
|
|
}
|
|
}
|
|
|
|
initializeLocale();
|
|
|
|
// Acknowledged check
|
|
const savedCode = localStorage.getItem('edut_acknowledged');
|
|
if (savedCode) {
|
|
renderAcknowledged(savedCode);
|
|
}
|
|
|
|
// Privacy/Terms don't trigger mailto
|
|
document.getElementById('privacyLink').addEventListener('click', (e) => e.stopPropagation());
|
|
document.getElementById('termsLink').addEventListener('click', (e) => e.stopPropagation());
|
|
|
|
// Keyboard support: Enter/Space triggers same as click
|
|
document.body.addEventListener('keydown', (e) => {
|
|
if (e.key === 'Enter' || e.key === ' ') {
|
|
if (e.target.tagName !== 'A') {
|
|
e.preventDefault();
|
|
document.body.click();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Interaction state
|
|
let spinSpeed = 0.15;
|
|
let targetSpin = 0.15;
|
|
let clickBoost = 0;
|
|
|
|
// Full page click
|
|
document.body.addEventListener('click', () => {
|
|
clickBoost = 2.5;
|
|
targetSpin = 2.0;
|
|
|
|
if (!localStorage.getItem('edut_acknowledged')) {
|
|
setTimeout(() => {
|
|
const now = new Date();
|
|
const mm = String(now.getMonth() + 1).padStart(2, '0');
|
|
const dd = String(now.getDate()).padStart(2, '0');
|
|
const hh = String(now.getHours()).padStart(2, '0');
|
|
const mi = String(now.getMinutes()).padStart(2, '0');
|
|
const ss = String(now.getSeconds()).padStart(2, '0');
|
|
const ms = String(now.getMilliseconds()).padStart(3, '0');
|
|
const code = mm + dd + hh + mi + ss + ms;
|
|
const token = code.slice(0,4) + '-' + code.slice(4,8) + '-' + code.slice(8,12) + '-' + code.slice(12);
|
|
const ts = now.toISOString().split('.')[0] + 'Z';
|
|
const addr = code + '@secret.edut.ai';
|
|
const subject = 'EDUT-' + code;
|
|
const body = [
|
|
'━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
'EDUT GOVERNANCE PROTOCOL',
|
|
'',
|
|
'Access Request: ' + token,
|
|
'Timestamp: ' + ts,
|
|
'━━━━━━━━━━━━━━━━━━━━━━━━━━',
|
|
].join('\n');
|
|
window.location.href = 'mailto:' + addr + '?subject=' + encodeURIComponent(subject) + '&body=' + encodeURIComponent(body);
|
|
localStorage.setItem('edut_acknowledged', code);
|
|
renderAcknowledged(code);
|
|
}, 600);
|
|
}
|
|
});
|
|
|
|
// ============================================================
|
|
// THREE.JS — FLOATING GLOBE
|
|
// ============================================================
|
|
const width = container.clientWidth;
|
|
const height = container.clientHeight;
|
|
|
|
const scene = new THREE.Scene();
|
|
const camera = new THREE.PerspectiveCamera(42, width / height, 0.1, 100);
|
|
camera.position.set(0, 0.3, 4.2);
|
|
camera.lookAt(0, 0, 0);
|
|
|
|
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
|
|
renderer.setSize(width, height);
|
|
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
renderer.setClearColor(0x000000, 0);
|
|
container.appendChild(renderer.domElement);
|
|
|
|
// Globe group — tilted, spins
|
|
const globeGroup = new THREE.Group();
|
|
globeGroup.rotation.z = -0.3;
|
|
globeGroup.rotation.x = 0.15;
|
|
scene.add(globeGroup);
|
|
|
|
// Sphere shell — slightly more visible
|
|
const sphereGeo = new THREE.SphereGeometry(1, 64, 64);
|
|
const sphereMat = new THREE.MeshBasicMaterial({
|
|
color: 0x1a4090,
|
|
transparent: true,
|
|
opacity: 0.12,
|
|
});
|
|
const sphere = new THREE.Mesh(sphereGeo, sphereMat);
|
|
globeGroup.add(sphere);
|
|
|
|
// Latitude/Longitude wireframe — stronger
|
|
const wireGeo = new THREE.SphereGeometry(1.002, 36, 24);
|
|
const wireMat = new THREE.MeshBasicMaterial({
|
|
color: 0x1a4590,
|
|
transparent: true,
|
|
opacity: 0.14,
|
|
wireframe: true,
|
|
});
|
|
const wireframe = new THREE.Mesh(wireGeo, wireMat);
|
|
globeGroup.add(wireframe);
|
|
|
|
// Core — brighter
|
|
const coreGeo = new THREE.SphereGeometry(0.08, 16, 16);
|
|
const coreMat = new THREE.MeshBasicMaterial({ color: 0xffffff, transparent: true, opacity: 0.9 });
|
|
const core = new THREE.Mesh(coreGeo, coreMat);
|
|
globeGroup.add(core);
|
|
|
|
const coreGlowGeo = new THREE.SphereGeometry(0.25, 16, 16);
|
|
const coreGlowMat = new THREE.MeshBasicMaterial({ color: 0x3060c0, transparent: true, opacity: 0.25 });
|
|
const coreGlow = new THREE.Mesh(coreGlowGeo, coreGlowMat);
|
|
globeGroup.add(coreGlow);
|
|
|
|
// Rays — more visible
|
|
const NUM_RAYS = 80;
|
|
const rayGroup = new THREE.Group();
|
|
globeGroup.add(rayGroup);
|
|
const rayData = [];
|
|
|
|
for (let i = 0; i < NUM_RAYS; i++) {
|
|
const phi = Math.acos(2 * Math.random() - 1);
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const len = 0.5 + Math.random() * 0.45;
|
|
const endX = len * Math.sin(phi) * Math.cos(theta);
|
|
const endY = len * Math.cos(phi);
|
|
const endZ = len * Math.sin(phi) * Math.sin(theta);
|
|
|
|
const geo = new THREE.BufferGeometry().setFromPoints([
|
|
new THREE.Vector3(0, 0, 0),
|
|
new THREE.Vector3(endX, endY, endZ)
|
|
]);
|
|
const mat = new THREE.LineBasicMaterial({
|
|
color: 0x2060b0,
|
|
transparent: true,
|
|
opacity: 0.14 + Math.random() * 0.1,
|
|
});
|
|
const line = new THREE.Line(geo, mat);
|
|
rayGroup.add(line);
|
|
|
|
const tipGeo = new THREE.SphereGeometry(0.014, 6, 6);
|
|
const tipMat = new THREE.MeshBasicMaterial({
|
|
color: 0x3080e0,
|
|
transparent: true,
|
|
opacity: 0.3 + Math.random() * 0.25,
|
|
});
|
|
const tip = new THREE.Mesh(tipGeo, tipMat);
|
|
tip.position.set(endX, endY, endZ);
|
|
rayGroup.add(tip);
|
|
|
|
rayData.push({ line, tip, phase: Math.random() * Math.PI * 2, speed: 0.3 + Math.random() * 0.5 });
|
|
}
|
|
|
|
// Swarm particles — more visible
|
|
const NUM_PARTICLES = 1200;
|
|
const particlePositions = new Float32Array(NUM_PARTICLES * 3);
|
|
const particleData = [];
|
|
|
|
for (let i = 0; i < NUM_PARTICLES; i++) {
|
|
const phi = Math.acos(2 * Math.random() - 1);
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const r = 0.7 + Math.random() * 0.3;
|
|
particlePositions[i * 3] = r * Math.sin(phi) * Math.cos(theta);
|
|
particlePositions[i * 3 + 1] = r * Math.cos(phi);
|
|
particlePositions[i * 3 + 2] = r * Math.sin(phi) * Math.sin(theta);
|
|
particleData.push({
|
|
phi, theta, r,
|
|
dTheta: (0.1 + Math.random() * 0.3) * (Math.random() > 0.5 ? 1 : -1),
|
|
dPhi: (Math.random() - 0.5) * 0.05,
|
|
wobblePhase: Math.random() * Math.PI * 2,
|
|
wobbleSpeed: 0.3 + Math.random() * 0.8,
|
|
});
|
|
}
|
|
|
|
const particleGeo = new THREE.BufferGeometry();
|
|
particleGeo.setAttribute('position', new THREE.BufferAttribute(particlePositions, 3));
|
|
|
|
const particleMat = new THREE.PointsMaterial({
|
|
color: 0x1540a0,
|
|
size: 0.025,
|
|
transparent: true,
|
|
opacity: 0.7,
|
|
sizeAttenuation: true,
|
|
blending: THREE.AdditiveBlending,
|
|
depthWrite: false,
|
|
});
|
|
const particles = new THREE.Points(particleGeo, particleMat);
|
|
globeGroup.add(particles);
|
|
|
|
// Nodes — stronger
|
|
const NUM_NODES = 40;
|
|
const nodeData = [];
|
|
|
|
for (let i = 0; i < NUM_NODES; i++) {
|
|
const phi = Math.acos(2 * Math.random() - 1);
|
|
const theta = Math.random() * Math.PI * 2;
|
|
const r = 0.35 + Math.random() * 0.45;
|
|
const geo = new THREE.SphereGeometry(0.018 + Math.random() * 0.015, 8, 8);
|
|
const mat = new THREE.MeshBasicMaterial({
|
|
color: 0x1a60c0,
|
|
transparent: true,
|
|
opacity: 0.4 + Math.random() * 0.25,
|
|
});
|
|
const mesh = new THREE.Mesh(geo, mat);
|
|
mesh.position.set(
|
|
r * Math.sin(phi) * Math.cos(theta),
|
|
r * Math.cos(phi),
|
|
r * Math.sin(phi) * Math.sin(theta)
|
|
);
|
|
globeGroup.add(mesh);
|
|
nodeData.push({
|
|
mesh, phi, theta, r,
|
|
speed: (0.15 + Math.random() * 0.3) * (Math.random() > 0.5 ? 1 : -1),
|
|
pulsePhase: Math.random() * Math.PI * 2,
|
|
});
|
|
}
|
|
|
|
// (ambient glow removed — clean background)
|
|
|
|
// ============================================================
|
|
// ANIMATION
|
|
// ============================================================
|
|
const clock = new THREE.Clock();
|
|
|
|
function animate() {
|
|
requestAnimationFrame(animate);
|
|
const dt = Math.min(clock.getDelta(), 0.05);
|
|
const t = clock.elapsedTime;
|
|
|
|
if (clickBoost > 0) {
|
|
clickBoost = Math.max(0, clickBoost - dt * 1.5);
|
|
targetSpin = 0.15 + clickBoost;
|
|
}
|
|
spinSpeed += (targetSpin - spinSpeed) * dt * 3;
|
|
|
|
globeGroup.rotation.y += spinSpeed * dt;
|
|
|
|
// Breathing core
|
|
const breath = Math.sin(t * 0.5) * 0.1 + 0.9;
|
|
core.scale.setScalar(breath);
|
|
coreGlow.scale.setScalar(breath * 1.2);
|
|
coreMat.opacity = 0.8 + Math.sin(t * 0.5) * 0.1;
|
|
|
|
// Swarm
|
|
const positions = particles.geometry.attributes.position.array;
|
|
for (let i = 0; i < NUM_PARTICLES; i++) {
|
|
const pd = particleData[i];
|
|
pd.theta += pd.dTheta * dt;
|
|
pd.phi += (pd.dPhi + Math.sin(t * pd.wobbleSpeed + pd.wobblePhase) * 0.02) * dt;
|
|
pd.phi = Math.max(0.1, Math.min(Math.PI - 0.1, pd.phi));
|
|
const r = pd.r + Math.sin(t * 0.3 + pd.wobblePhase) * 0.02;
|
|
positions[i * 3] = r * Math.sin(pd.phi) * Math.cos(pd.theta);
|
|
positions[i * 3 + 1] = r * Math.cos(pd.phi);
|
|
positions[i * 3 + 2] = r * Math.sin(pd.phi) * Math.sin(pd.theta);
|
|
}
|
|
particles.geometry.attributes.position.needsUpdate = true;
|
|
|
|
// Nodes
|
|
for (const nd of nodeData) {
|
|
nd.theta += nd.speed * dt;
|
|
const pulse = Math.sin(t * 1.5 + nd.pulsePhase) * 0.1 + 0.9;
|
|
nd.mesh.position.set(
|
|
nd.r * Math.sin(nd.phi) * Math.cos(nd.theta),
|
|
nd.r * Math.cos(nd.phi),
|
|
nd.r * Math.sin(nd.phi) * Math.sin(nd.theta)
|
|
);
|
|
nd.mesh.scale.setScalar(pulse);
|
|
nd.mesh.material.opacity = 0.2 + pulse * 0.2;
|
|
}
|
|
|
|
// Rays
|
|
for (const rd of rayData) {
|
|
const pulse = Math.sin(t * rd.speed + rd.phase) * 0.15 + 0.85;
|
|
rd.line.material.opacity = (0.06 + Math.sin(t * rd.speed + rd.phase) * 0.06) * pulse;
|
|
rd.tip.material.opacity = (0.15 + Math.sin(t * rd.speed + rd.phase) * 0.12) * pulse;
|
|
}
|
|
|
|
renderer.render(scene, camera);
|
|
}
|
|
|
|
animate();
|
|
|
|
window.addEventListener('resize', () => {
|
|
const w = container.clientWidth;
|
|
const h = container.clientHeight;
|
|
camera.aspect = w / h;
|
|
camera.updateProjectionMatrix();
|
|
renderer.setSize(w, h);
|
|
});
|
|
</script>
|
|
</body>
|
|
</html>
|