2297 lines
85 KiB
HTML
2297 lines
85 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>Engram — A Database Designed for Minds</title>
|
||
<style>
|
||
:root {
|
||
--bg: #0D0D1A;
|
||
--bg2: #111122;
|
||
--bg3: #161628;
|
||
--purple: #7B61FF;
|
||
--purple-dim: #4A3A99;
|
||
--purple-glow: rgba(123, 97, 255, 0.15);
|
||
--purple-glow-strong: rgba(123, 97, 255, 0.35);
|
||
--white: #F0F0FF;
|
||
--muted: #8888AA;
|
||
--dim: #444466;
|
||
--working: #FF6B6B;
|
||
--episodic: #FFB347;
|
||
--semantic: #7B61FF;
|
||
--procedural: #4ECDC4;
|
||
--mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
|
||
}
|
||
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
|
||
html { scroll-behavior: smooth; }
|
||
|
||
body {
|
||
background: var(--bg);
|
||
color: var(--white);
|
||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', system-ui, sans-serif;
|
||
line-height: 1.6;
|
||
overflow-x: hidden;
|
||
}
|
||
|
||
/* ── Typography ── */
|
||
h1 { font-size: clamp(3rem, 10vw, 7rem); font-weight: 800; letter-spacing: -0.04em; line-height: 1; }
|
||
h2 { font-size: clamp(1.6rem, 4vw, 2.4rem); font-weight: 700; letter-spacing: -0.02em; }
|
||
h3 { font-size: 1.1rem; font-weight: 600; letter-spacing: 0.01em; }
|
||
p { color: #CCCCEE; font-size: 1.05rem; }
|
||
|
||
code, pre {
|
||
font-family: var(--mono);
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* ── Layout ── */
|
||
section {
|
||
max-width: 1100px;
|
||
margin: 0 auto;
|
||
padding: 100px 40px;
|
||
}
|
||
|
||
.section-label {
|
||
font-size: 0.7rem;
|
||
font-weight: 700;
|
||
letter-spacing: 0.2em;
|
||
text-transform: uppercase;
|
||
color: var(--purple);
|
||
margin-bottom: 1.2rem;
|
||
display: block;
|
||
}
|
||
|
||
/* ── Hero ── */
|
||
#hero {
|
||
max-width: 100%;
|
||
padding: 0;
|
||
position: relative;
|
||
min-height: 100vh;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
overflow: hidden;
|
||
}
|
||
|
||
#hero-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
|
||
.hero-content {
|
||
position: relative;
|
||
z-index: 2;
|
||
text-align: center;
|
||
padding: 60px 40px;
|
||
}
|
||
|
||
.hero-title {
|
||
font-size: clamp(5rem, 18vw, 14rem);
|
||
font-weight: 900;
|
||
letter-spacing: -0.06em;
|
||
line-height: 0.9;
|
||
background: linear-gradient(135deg, #FFFFFF 0%, #B8A8FF 40%, #7B61FF 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.hero-tagline {
|
||
font-size: clamp(1rem, 2.5vw, 1.4rem);
|
||
color: var(--muted);
|
||
max-width: 600px;
|
||
margin: 0 auto 2.5rem;
|
||
font-weight: 400;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.hero-tagline em {
|
||
color: var(--white);
|
||
font-style: normal;
|
||
}
|
||
|
||
.hero-badge {
|
||
display: inline-flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
background: var(--purple-glow);
|
||
border: 1px solid var(--purple-dim);
|
||
border-radius: 100px;
|
||
padding: 6px 16px;
|
||
font-size: 0.8rem;
|
||
color: var(--purple);
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
.pulse-dot {
|
||
width: 6px;
|
||
height: 6px;
|
||
border-radius: 50%;
|
||
background: var(--purple);
|
||
animation: pulse-dot 2s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes pulse-dot {
|
||
0%, 100% { opacity: 1; transform: scale(1); }
|
||
50% { opacity: 0.4; transform: scale(0.6); }
|
||
}
|
||
|
||
.scroll-hint {
|
||
position: absolute;
|
||
bottom: 40px;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
color: var(--dim);
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
animation: fade-up 3s ease-in-out infinite;
|
||
}
|
||
|
||
.scroll-arrow {
|
||
width: 1px;
|
||
height: 40px;
|
||
background: linear-gradient(to bottom, transparent, var(--dim));
|
||
}
|
||
|
||
@keyframes fade-up {
|
||
0%, 100% { opacity: 0.4; transform: translateX(-50%) translateY(0); }
|
||
50% { opacity: 1; transform: translateX(-50%) translateY(6px); }
|
||
}
|
||
|
||
/* ── Problem Section ── */
|
||
#problem {
|
||
border-top: 1px solid var(--dim);
|
||
}
|
||
|
||
.problem-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(3, 1fr);
|
||
gap: 2px;
|
||
margin: 3rem 0;
|
||
border: 1px solid var(--dim);
|
||
border-radius: 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.problem-card {
|
||
padding: 2.5rem 2rem;
|
||
background: var(--bg2);
|
||
position: relative;
|
||
}
|
||
|
||
.problem-card + .problem-card {
|
||
border-left: 1px solid var(--dim);
|
||
}
|
||
|
||
.problem-icon {
|
||
font-size: 2rem;
|
||
margin-bottom: 1rem;
|
||
display: block;
|
||
}
|
||
|
||
.problem-card h3 {
|
||
margin-bottom: 0.75rem;
|
||
color: var(--white);
|
||
}
|
||
|
||
.problem-card p {
|
||
font-size: 0.95rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.problem-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 2px;
|
||
background: linear-gradient(90deg, transparent, var(--dim), transparent);
|
||
}
|
||
|
||
.bold-statement {
|
||
text-align: center;
|
||
margin-top: 4rem;
|
||
padding: 3rem;
|
||
background: var(--bg2);
|
||
border-radius: 16px;
|
||
border: 1px solid var(--purple-dim);
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.bold-statement::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: -1px;
|
||
border-radius: 16px;
|
||
background: linear-gradient(135deg, var(--purple-dim), transparent, var(--purple-dim));
|
||
z-index: 0;
|
||
opacity: 0.4;
|
||
}
|
||
|
||
.bold-statement p {
|
||
position: relative;
|
||
z-index: 1;
|
||
font-size: clamp(1.4rem, 3vw, 2rem);
|
||
font-weight: 700;
|
||
color: var(--white);
|
||
letter-spacing: -0.02em;
|
||
}
|
||
|
||
.bold-statement span {
|
||
color: var(--purple);
|
||
}
|
||
|
||
/* ── Activation Demo ── */
|
||
#activation {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.demo-container {
|
||
margin-top: 2.5rem;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--dim);
|
||
border-radius: 16px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.demo-toolbar {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
padding: 1rem 1.5rem;
|
||
border-bottom: 1px solid var(--dim);
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.demo-toolbar-label {
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.btn-activate {
|
||
background: var(--purple);
|
||
color: #fff;
|
||
border: none;
|
||
padding: 8px 24px;
|
||
border-radius: 8px;
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
transition: all 0.15s;
|
||
font-family: inherit;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.btn-activate:hover { background: #9580FF; transform: translateY(-1px); box-shadow: 0 4px 20px var(--purple-glow-strong); }
|
||
.btn-activate:active { transform: translateY(0); }
|
||
.btn-activate:disabled { opacity: 0.5; cursor: not-allowed; transform: none; }
|
||
|
||
.btn-reset {
|
||
background: transparent;
|
||
color: var(--muted);
|
||
border: 1px solid var(--dim);
|
||
padding: 8px 20px;
|
||
border-radius: 8px;
|
||
font-size: 0.85rem;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
transition: all 0.15s;
|
||
}
|
||
|
||
.btn-reset:hover { border-color: var(--muted); color: var(--white); }
|
||
|
||
.speed-control {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
margin-left: auto;
|
||
}
|
||
|
||
.speed-control input {
|
||
width: 80px;
|
||
accent-color: var(--purple);
|
||
}
|
||
|
||
#activation-canvas {
|
||
width: 100%;
|
||
height: 500px;
|
||
display: block;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.formula-box {
|
||
padding: 1rem 1.5rem;
|
||
border-top: 1px solid var(--dim);
|
||
font-family: var(--mono);
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 1rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.formula-box .formula-label {
|
||
color: var(--white);
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.08em;
|
||
text-transform: uppercase;
|
||
font-family: inherit;
|
||
}
|
||
|
||
.formula-box .formula {
|
||
color: var(--purple);
|
||
}
|
||
|
||
.formula-box .formula-hint {
|
||
margin-left: auto;
|
||
font-size: 0.78rem;
|
||
color: var(--dim);
|
||
font-family: inherit;
|
||
}
|
||
|
||
/* ── Memory Tiers ── */
|
||
#tiers {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.tiers-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(2, 1fr);
|
||
gap: 1.5rem;
|
||
margin-top: 2.5rem;
|
||
}
|
||
|
||
.tier-card {
|
||
background: var(--bg2);
|
||
border-radius: 12px;
|
||
padding: 2rem;
|
||
border: 1px solid var(--dim);
|
||
position: relative;
|
||
overflow: hidden;
|
||
transition: border-color 0.3s, box-shadow 0.3s;
|
||
}
|
||
|
||
.tier-card:hover {
|
||
box-shadow: 0 0 30px rgba(0,0,0,0.5);
|
||
}
|
||
|
||
.tier-card::before {
|
||
content: '';
|
||
position: absolute;
|
||
top: 0; left: 0; right: 0;
|
||
height: 3px;
|
||
}
|
||
|
||
.tier-card.working::before { background: var(--working); }
|
||
.tier-card.episodic::before { background: var(--episodic); }
|
||
.tier-card.semantic::before { background: var(--semantic); }
|
||
.tier-card.procedural::before { background: var(--procedural); }
|
||
|
||
.tier-card:hover.working { border-color: var(--working); box-shadow: 0 0 30px rgba(255,107,107,0.1); }
|
||
.tier-card:hover.episodic { border-color: var(--episodic); box-shadow: 0 0 30px rgba(255,179,71,0.1); }
|
||
.tier-card:hover.semantic { border-color: var(--semantic); box-shadow: 0 0 30px rgba(123,97,255,0.15); }
|
||
.tier-card:hover.procedural { border-color: var(--procedural); box-shadow: 0 0 30px rgba(78,205,196,0.1); }
|
||
|
||
.tier-header {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
margin-bottom: 1rem;
|
||
}
|
||
|
||
.tier-icon {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.tier-name {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: var(--white);
|
||
}
|
||
|
||
.tier-analogy {
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
font-family: var(--mono);
|
||
font-weight: 400;
|
||
}
|
||
|
||
.tier-desc {
|
||
font-size: 0.95rem;
|
||
color: var(--muted);
|
||
margin-bottom: 1.2rem;
|
||
}
|
||
|
||
.tier-example {
|
||
background: var(--bg3);
|
||
border-radius: 8px;
|
||
padding: 0.75rem 1rem;
|
||
font-size: 0.8rem;
|
||
font-family: var(--mono);
|
||
color: var(--muted);
|
||
border-left: 3px solid;
|
||
line-height: 1.5;
|
||
}
|
||
|
||
.tier-card.working .tier-example { border-color: var(--working); color: #FFB0B0; }
|
||
.tier-card.episodic .tier-example { border-color: var(--episodic); color: #FFCC80; }
|
||
.tier-card.semantic .tier-example { border-color: var(--semantic); color: #B8A8FF; }
|
||
.tier-card.procedural .tier-example { border-color: var(--procedural); color: #80E8E4; }
|
||
|
||
/* ── Salience ── */
|
||
#salience {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.salience-container {
|
||
margin-top: 2.5rem;
|
||
display: grid;
|
||
grid-template-columns: 1fr 1fr;
|
||
gap: 2rem;
|
||
}
|
||
|
||
.salience-formula-display {
|
||
background: var(--bg2);
|
||
border-radius: 12px;
|
||
border: 1px solid var(--dim);
|
||
padding: 2rem;
|
||
}
|
||
|
||
.salience-formula-display .formula-title {
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-bottom: 1.5rem;
|
||
font-weight: 600;
|
||
}
|
||
|
||
.formula-visual {
|
||
font-family: var(--mono);
|
||
font-size: 0.9rem;
|
||
color: var(--white);
|
||
line-height: 2;
|
||
margin-bottom: 2rem;
|
||
}
|
||
|
||
.fv-equal { color: var(--muted); }
|
||
.fv-var { color: var(--purple); }
|
||
.fv-op { color: var(--muted); }
|
||
.fv-fn { color: #4ECDC4; }
|
||
.fv-num { color: #FFB347; }
|
||
.fv-comment { color: var(--dim); }
|
||
|
||
.salience-sliders {
|
||
background: var(--bg2);
|
||
border-radius: 12px;
|
||
border: 1px solid var(--dim);
|
||
padding: 2rem;
|
||
}
|
||
|
||
.slider-row {
|
||
margin-bottom: 1.5rem;
|
||
}
|
||
|
||
.slider-label {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.slider-label span:first-child {
|
||
font-size: 0.85rem;
|
||
color: var(--muted);
|
||
font-weight: 500;
|
||
}
|
||
|
||
.slider-label .slider-value {
|
||
font-family: var(--mono);
|
||
font-size: 0.85rem;
|
||
color: var(--purple);
|
||
font-weight: 700;
|
||
}
|
||
|
||
input[type=range] {
|
||
width: 100%;
|
||
-webkit-appearance: none;
|
||
height: 4px;
|
||
border-radius: 2px;
|
||
background: var(--dim);
|
||
outline: none;
|
||
cursor: pointer;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb {
|
||
-webkit-appearance: none;
|
||
width: 16px;
|
||
height: 16px;
|
||
border-radius: 50%;
|
||
background: var(--purple);
|
||
cursor: pointer;
|
||
box-shadow: 0 0 8px var(--purple-glow-strong);
|
||
transition: transform 0.1s;
|
||
}
|
||
|
||
input[type=range]::-webkit-slider-thumb:hover { transform: scale(1.2); }
|
||
|
||
.salience-output {
|
||
background: var(--bg3);
|
||
border-radius: 10px;
|
||
padding: 1.5rem;
|
||
text-align: center;
|
||
border: 1px solid var(--purple-dim);
|
||
margin-top: 1.5rem;
|
||
}
|
||
|
||
.salience-score-label {
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.1em;
|
||
text-transform: uppercase;
|
||
color: var(--muted);
|
||
margin-bottom: 0.75rem;
|
||
}
|
||
|
||
.salience-score {
|
||
font-size: 3rem;
|
||
font-weight: 900;
|
||
font-family: var(--mono);
|
||
color: var(--purple);
|
||
letter-spacing: -0.04em;
|
||
line-height: 1;
|
||
margin-bottom: 1rem;
|
||
transition: color 0.3s;
|
||
}
|
||
|
||
.salience-gauge {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: var(--dim);
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.salience-gauge-fill {
|
||
height: 100%;
|
||
border-radius: 4px;
|
||
transition: width 0.4s cubic-bezier(0.4, 0, 0.2, 1), background 0.4s;
|
||
}
|
||
|
||
.forgetting-quote {
|
||
text-align: center;
|
||
margin-top: 3rem;
|
||
font-size: 1.2rem;
|
||
font-style: italic;
|
||
color: var(--muted);
|
||
max-width: 600px;
|
||
margin-left: auto;
|
||
margin-right: auto;
|
||
}
|
||
|
||
/* ── Consolidation ── */
|
||
#consolidation {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.consolidation-diagram {
|
||
margin-top: 2.5rem;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--dim);
|
||
border-radius: 16px;
|
||
padding: 2.5rem;
|
||
position: relative;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.consol-stages {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
gap: 1rem;
|
||
position: relative;
|
||
z-index: 2;
|
||
}
|
||
|
||
.consol-box {
|
||
flex: 1;
|
||
max-width: 260px;
|
||
background: var(--bg3);
|
||
border-radius: 12px;
|
||
padding: 1.5rem;
|
||
border: 1px solid var(--dim);
|
||
text-align: center;
|
||
}
|
||
|
||
.consol-box.episodic { border-color: var(--episodic); }
|
||
.consol-box.semantic { border-color: var(--semantic); }
|
||
|
||
.consol-box-title {
|
||
font-weight: 700;
|
||
font-size: 1rem;
|
||
margin-bottom: 0.5rem;
|
||
}
|
||
|
||
.consol-box.episodic .consol-box-title { color: var(--episodic); }
|
||
.consol-box.semantic .consol-box-title { color: var(--semantic); }
|
||
|
||
.consol-box p {
|
||
font-size: 0.8rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
.consol-arrow-zone {
|
||
flex: 1;
|
||
max-width: 200px;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.consol-conditions {
|
||
font-size: 0.72rem;
|
||
font-family: var(--mono);
|
||
color: var(--muted);
|
||
text-align: center;
|
||
line-height: 1.7;
|
||
background: var(--bg3);
|
||
padding: 0.75rem;
|
||
border-radius: 8px;
|
||
border: 1px solid var(--dim);
|
||
}
|
||
|
||
.consol-conditions .cond-hl { color: var(--purple); }
|
||
|
||
.consol-arrow-svg {
|
||
width: 100%;
|
||
}
|
||
|
||
#consol-canvas {
|
||
position: absolute;
|
||
inset: 0;
|
||
width: 100%;
|
||
height: 100%;
|
||
pointer-events: none;
|
||
z-index: 1;
|
||
}
|
||
|
||
.consol-diagram-bottom {
|
||
margin-top: 2rem;
|
||
text-align: center;
|
||
font-size: 0.9rem;
|
||
color: var(--muted);
|
||
}
|
||
|
||
/* ── Architecture ── */
|
||
#architecture {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.arch-diagram {
|
||
margin-top: 2.5rem;
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
gap: 0;
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
.arch-layer {
|
||
width: 100%;
|
||
max-width: 700px;
|
||
padding: 1.2rem 2rem;
|
||
border-radius: 10px;
|
||
text-align: center;
|
||
font-size: 0.9rem;
|
||
position: relative;
|
||
}
|
||
|
||
.arch-layer.top {
|
||
background: linear-gradient(135deg, #1A1A35, #1F1F40);
|
||
border: 1px solid var(--purple-dim);
|
||
color: var(--white);
|
||
font-weight: 700;
|
||
letter-spacing: 0.02em;
|
||
}
|
||
|
||
.arch-layer.api {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--purple);
|
||
color: var(--purple);
|
||
font-weight: 700;
|
||
margin: 2px 0;
|
||
}
|
||
|
||
.arch-layer.inner {
|
||
background: var(--bg3);
|
||
border: 1px solid var(--dim);
|
||
color: var(--white);
|
||
display: flex;
|
||
gap: 1px;
|
||
padding: 0;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.arch-inner-block {
|
||
flex: 1;
|
||
padding: 1.2rem 1rem;
|
||
text-align: center;
|
||
font-size: 0.82rem;
|
||
}
|
||
|
||
.arch-inner-block + .arch-inner-block {
|
||
border-left: 1px solid var(--dim);
|
||
}
|
||
|
||
.arch-inner-block .block-title {
|
||
font-weight: 700;
|
||
color: var(--white);
|
||
margin-bottom: 0.25rem;
|
||
}
|
||
|
||
.arch-inner-block .block-sub {
|
||
color: var(--muted);
|
||
font-size: 0.75rem;
|
||
}
|
||
|
||
.arch-layer.storage {
|
||
background: var(--bg2);
|
||
border: 1px solid var(--dim);
|
||
color: var(--muted);
|
||
margin: 2px 0;
|
||
}
|
||
|
||
.arch-layer.disk {
|
||
background: var(--bg);
|
||
border: 1px solid var(--dim);
|
||
color: var(--dim);
|
||
}
|
||
|
||
.arch-connector {
|
||
display: flex;
|
||
flex-direction: column;
|
||
align-items: center;
|
||
color: var(--dim);
|
||
font-size: 1.1rem;
|
||
line-height: 1;
|
||
padding: 4px 0;
|
||
}
|
||
|
||
.arch-connector span {
|
||
font-size: 0.75rem;
|
||
letter-spacing: 0.05em;
|
||
color: var(--dim);
|
||
}
|
||
|
||
/* ── Bindings ── */
|
||
#bindings {
|
||
background: var(--bg);
|
||
}
|
||
|
||
.tabs {
|
||
margin-top: 2.5rem;
|
||
}
|
||
|
||
.tab-buttons {
|
||
display: flex;
|
||
gap: 2px;
|
||
margin-bottom: 0;
|
||
border-bottom: 1px solid var(--dim);
|
||
}
|
||
|
||
.tab-btn {
|
||
padding: 10px 24px;
|
||
background: transparent;
|
||
border: none;
|
||
color: var(--muted);
|
||
font-size: 0.85rem;
|
||
font-weight: 600;
|
||
cursor: pointer;
|
||
font-family: inherit;
|
||
letter-spacing: 0.03em;
|
||
position: relative;
|
||
transition: color 0.15s;
|
||
border-bottom: 2px solid transparent;
|
||
margin-bottom: -1px;
|
||
}
|
||
|
||
.tab-btn.active {
|
||
color: var(--white);
|
||
border-bottom-color: var(--purple);
|
||
}
|
||
|
||
.tab-btn:hover:not(.active) { color: var(--white); }
|
||
|
||
.tab-content {
|
||
display: none;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--dim);
|
||
border-top: none;
|
||
border-radius: 0 0 12px 12px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.tab-content.active { display: block; }
|
||
|
||
.code-header {
|
||
display: flex;
|
||
align-items: center;
|
||
padding: 0.75rem 1.5rem;
|
||
border-bottom: 1px solid var(--dim);
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
font-family: var(--mono);
|
||
gap: 8px;
|
||
}
|
||
|
||
.dot-red { width: 10px; height: 10px; border-radius: 50%; background: #FF5F57; }
|
||
.dot-yellow { width: 10px; height: 10px; border-radius: 50%; background: #FFBD2E; }
|
||
.dot-green { width: 10px; height: 10px; border-radius: 50%; background: #28CA41; }
|
||
|
||
pre {
|
||
padding: 2rem;
|
||
overflow-x: auto;
|
||
tab-size: 4;
|
||
line-height: 1.7;
|
||
font-size: 0.875rem;
|
||
}
|
||
|
||
/* Syntax highlighting */
|
||
.kw { color: #C792EA; }
|
||
.fn-name { color: #82AAFF; }
|
||
.str { color: #C3E88D; }
|
||
.num { color: #F78C6C; }
|
||
.comment { color: #546E7A; font-style: italic; }
|
||
.type { color: #FFCB6B; }
|
||
.op { color: var(--muted); }
|
||
.punct { color: var(--muted); }
|
||
.const { color: #89DDFF; }
|
||
|
||
/* ── Why Local ── */
|
||
#local {
|
||
background: var(--bg);
|
||
text-align: center;
|
||
}
|
||
|
||
.local-statement {
|
||
max-width: 700px;
|
||
margin: 2rem auto 0;
|
||
font-size: clamp(1.15rem, 2.5vw, 1.4rem);
|
||
color: var(--white);
|
||
line-height: 1.7;
|
||
font-weight: 400;
|
||
}
|
||
|
||
.local-statement strong {
|
||
color: var(--purple);
|
||
font-weight: 700;
|
||
}
|
||
|
||
.local-statement em {
|
||
color: var(--muted);
|
||
font-style: normal;
|
||
}
|
||
|
||
.local-divider {
|
||
width: 1px;
|
||
height: 3rem;
|
||
background: linear-gradient(to bottom, var(--purple), transparent);
|
||
margin: 2rem auto;
|
||
}
|
||
|
||
/* ── Status ── */
|
||
#status {
|
||
background: var(--bg);
|
||
border-top: 1px solid var(--dim);
|
||
padding-top: 60px;
|
||
padding-bottom: 100px;
|
||
}
|
||
|
||
.status-grid {
|
||
display: flex;
|
||
gap: 2rem;
|
||
margin-top: 2.5rem;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.status-badge {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 12px;
|
||
background: var(--bg2);
|
||
border: 1px solid var(--dim);
|
||
border-radius: 10px;
|
||
padding: 1rem 1.5rem;
|
||
}
|
||
|
||
.status-icon {
|
||
font-size: 1.5rem;
|
||
}
|
||
|
||
.status-badge-label {
|
||
font-size: 0.75rem;
|
||
color: var(--muted);
|
||
letter-spacing: 0.05em;
|
||
text-transform: uppercase;
|
||
}
|
||
|
||
.status-badge-value {
|
||
font-size: 1.1rem;
|
||
font-weight: 700;
|
||
color: var(--white);
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
.status-badge.ok { border-color: #28CA41; }
|
||
.status-badge.ok .status-icon { color: #28CA41; }
|
||
|
||
.status-badge.ver { border-color: var(--purple); }
|
||
.status-badge.ver .status-icon { color: var(--purple); }
|
||
|
||
.status-badge.tests { border-color: #4ECDC4; }
|
||
.status-badge.tests .status-icon { color: #4ECDC4; }
|
||
|
||
/* ── Footer ── */
|
||
footer {
|
||
text-align: center;
|
||
padding: 3rem 2rem;
|
||
border-top: 1px solid var(--dim);
|
||
color: var(--dim);
|
||
font-size: 0.8rem;
|
||
font-family: var(--mono);
|
||
}
|
||
|
||
/* ── Responsive ── */
|
||
@media (max-width: 800px) {
|
||
.problem-grid { grid-template-columns: 1fr; }
|
||
.problem-card + .problem-card { border-left: none; border-top: 1px solid var(--dim); }
|
||
.tiers-grid { grid-template-columns: 1fr; }
|
||
.salience-container { grid-template-columns: 1fr; }
|
||
.consol-stages { flex-direction: column; }
|
||
.consol-arrow-zone { transform: rotate(90deg); }
|
||
section { padding: 60px 24px; }
|
||
}
|
||
|
||
@media (max-width: 600px) {
|
||
#activation-canvas { height: 380px; }
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
|
||
<!-- ═══════════════════════════════════════════ HERO ═══════════════════════════════════════════ -->
|
||
<div id="hero">
|
||
<canvas id="hero-canvas"></canvas>
|
||
<div class="hero-content">
|
||
<div class="hero-badge">
|
||
<div class="pulse-dot"></div>
|
||
v0.1.0 · Local-first memory substrate
|
||
</div>
|
||
<h1 class="hero-title">Engram</h1>
|
||
<p class="hero-tagline">
|
||
The physical trace of a memory.<br>
|
||
<em>A database designed for minds, not tables.</em>
|
||
</p>
|
||
</div>
|
||
<div class="scroll-hint">
|
||
<span>Scroll</span>
|
||
<div class="scroll-arrow"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- ═══════════════════════════════════════════ PROBLEM ═══════════════════════════════════════════ -->
|
||
<section id="problem">
|
||
<span class="section-label">The Problem</span>
|
||
<h2>Every existing database<br>gets memory wrong</h2>
|
||
|
||
<div class="problem-grid">
|
||
<div class="problem-card">
|
||
<span class="problem-icon">⊞</span>
|
||
<h3>Relational</h3>
|
||
<p>You store rows. You query rows. Storage and retrieval are entirely separate systems. The structure that holds the data knows nothing about how you will need it back.</p>
|
||
</div>
|
||
<div class="problem-card">
|
||
<span class="problem-icon">⊙</span>
|
||
<h3>Vector</h3>
|
||
<p>You store embeddings. You search by geometric proximity. Still fundamentally a query against static, passive data — the structure has no opinion about your context.</p>
|
||
</div>
|
||
<div class="problem-card">
|
||
<span class="problem-icon">◈</span>
|
||
<h3>Graph</h3>
|
||
<p>You store edges. You traverse paths. Still asking questions of a static structure. The graph doesn't activate — you query it from outside, like a stranger reading a map.</p>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="bold-statement">
|
||
<p>The brain doesn't <span>query</span>.<br>It <span>activates</span>.</p>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ ACTIVATION DEMO ═══════════════════════════════════════════ -->
|
||
<section id="activation">
|
||
<span class="section-label">Core Mechanism</span>
|
||
<h2>Spreading Activation</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">Click any node to set it as the seed. Hit <strong style="color:var(--white)">Activate</strong> to watch the pattern propagate. Activation flows outward through weighted edges — attenuating at every hop, pruned when too weak to matter.</p>
|
||
|
||
<div class="demo-container">
|
||
<div class="demo-toolbar">
|
||
<span class="demo-toolbar-label">Seed</span>
|
||
<div id="seed-indicator" style="font-family:var(--mono);font-size:0.85rem;color:var(--purple);background:var(--purple-glow);padding:4px 12px;border-radius:6px;border:1px solid var(--purple-dim);">Click a node to select</div>
|
||
<button class="btn-activate" id="btn-activate" disabled>Activate</button>
|
||
<button class="btn-reset" id="btn-reset">Reset</button>
|
||
<div class="speed-control">
|
||
<span class="demo-toolbar-label">Speed</span>
|
||
<input type="range" id="speed-slider" min="1" max="5" value="3">
|
||
</div>
|
||
</div>
|
||
<canvas id="activation-canvas"></canvas>
|
||
<div class="formula-box">
|
||
<span class="formula-label">Formula</span>
|
||
<code class="formula">strength = parent × edge_weight × salience × cos_sim(query, target)</code>
|
||
<span class="formula-hint">Multiplicative — every factor must be non-trivial for the path to survive</span>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ TIERS ═══════════════════════════════════════════ -->
|
||
<section id="tiers">
|
||
<span class="section-label">Memory Architecture</span>
|
||
<h2>The Four Memory Tiers</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">Nodes migrate between tiers based on salience and reinforcement — exactly as memories migrate between the hippocampus and neocortex through activation and sleep.</p>
|
||
|
||
<div class="tiers-grid">
|
||
<div class="tier-card working">
|
||
<div class="tier-header">
|
||
<span class="tier-icon">🔥</span>
|
||
<div>
|
||
<div class="tier-name">Working</div>
|
||
<div class="tier-analogy">Prefrontal cortex — hot, volatile</div>
|
||
</div>
|
||
</div>
|
||
<p class="tier-desc">The K most recently activated nodes. Ultra-fast access. Evicted by recency when capacity is exceeded. What you're actively thinking about right now.</p>
|
||
<div class="tier-example">
|
||
// Currently active<br>
|
||
task: "Implement activation BFS"<br>
|
||
context: spreading_activation_node<br>
|
||
recent: hebbian_learning_concept
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tier-card episodic">
|
||
<div class="tier-header">
|
||
<span class="tier-icon">📖</span>
|
||
<div>
|
||
<div class="tier-name">Episodic</div>
|
||
<div class="tier-analogy">Hippocampus — time-ordered experience</div>
|
||
</div>
|
||
</div>
|
||
<p class="tier-desc">Time-stamped events and raw experiences. What happened, and when. The raw feed of observations before they have been abstracted into knowledge.</p>
|
||
<div class="tier-example">
|
||
2026-04-27T14:23Z event:<br>
|
||
"Will explained Dharma Registry.<br>
|
||
Patterns logged. ISE confirmed."
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tier-card semantic">
|
||
<div class="tier-header">
|
||
<span class="tier-icon">🧠</span>
|
||
<div>
|
||
<div class="tier-name">Semantic</div>
|
||
<div class="tier-analogy">Neocortex — stable knowledge</div>
|
||
</div>
|
||
</div>
|
||
<p class="tier-desc">The concept graph with weighted associations. Long-term structural knowledge. What you know, abstracted from any specific event that taught it to you.</p>
|
||
<div class="tier-example">
|
||
concept: "spreading_activation"<br>
|
||
→ Causes: "long_term_potentiation"<br>
|
||
→ Activates: "associative_memory"<br>
|
||
salience: 0.82, activations: 47
|
||
</div>
|
||
</div>
|
||
|
||
<div class="tier-card procedural">
|
||
<div class="tier-header">
|
||
<span class="tier-icon">⚙️</span>
|
||
<div>
|
||
<div class="tier-name">Procedural</div>
|
||
<div class="tier-analogy">Cerebellum — patterns and habits</div>
|
||
</div>
|
||
</div>
|
||
<p class="tier-desc">Encoded patterns, workflows, and repeatable processes. How to do things. Retrieved by similarity to current task context, not by conscious recall.</p>
|
||
<div class="tier-example">
|
||
process: "commit_workflow"<br>
|
||
steps: [stage → test → commit<br>
|
||
→ push → check_ci]<br>
|
||
triggered by: git_context_match
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ SALIENCE ═══════════════════════════════════════════ -->
|
||
<section id="salience">
|
||
<span class="section-label">Forgetting as Design</span>
|
||
<h2>Salience — the attention filter</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">Every node has a salience score. It governs whether a node surfaces during retrieval. It decays. It strengthens on activation. Adjust the sliders to see how the three signals combine.</p>
|
||
|
||
<div class="salience-container">
|
||
<div class="salience-formula-display">
|
||
<div class="formula-title">The Salience Formula</div>
|
||
<div class="formula-visual">
|
||
<span class="fv-var">salience</span>
|
||
<span class="fv-equal"> = </span>
|
||
<span class="fv-var">importance</span>
|
||
<br>
|
||
<span style="color:transparent">salience = </span>
|
||
<span class="fv-op">× </span>
|
||
<span class="fv-fn">1</span><span class="fv-op"> / </span><span class="fv-fn">(1</span><span class="fv-op"> + </span><span class="fv-var">days_since</span><span class="fv-fn">)</span>
|
||
<br>
|
||
<span style="color:transparent">salience = </span>
|
||
<span class="fv-op">× </span>
|
||
<span class="fv-fn">ln(</span><span class="fv-var">activation_count</span><span class="fv-op"> + </span><span class="fv-num">1</span><span class="fv-fn">)</span>
|
||
</div>
|
||
|
||
<div style="font-size:0.85rem;color:var(--muted);line-height:2;">
|
||
<div style="display:flex;gap:1rem;align-items:baseline;margin-bottom:0.5rem;">
|
||
<span style="color:var(--purple);font-family:var(--mono);min-width:100px;">importance</span>
|
||
<span>Explicit weight at creation. Stable over time. You set it once.</span>
|
||
</div>
|
||
<div style="display:flex;gap:1rem;align-items:baseline;margin-bottom:0.5rem;">
|
||
<span style="color:var(--procedural);font-family:var(--mono);min-width:100px;">recency</span>
|
||
<span>At activation: 1.0. After 1 day: 0.5. After 6 days: ~0.14. Asymptotic toward zero.</span>
|
||
</div>
|
||
<div style="display:flex;gap:1rem;align-items:baseline;">
|
||
<span style="color:var(--episodic);font-family:var(--mono);min-width:100px;">frequency</span>
|
||
<span>Log-compressed: 0→1 activation matters more than 100→101. Diminishing returns.</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="salience-sliders">
|
||
<div class="slider-row">
|
||
<div class="slider-label">
|
||
<span>Importance</span>
|
||
<span class="slider-value" id="val-importance">0.85</span>
|
||
</div>
|
||
<input type="range" id="sl-importance" min="0" max="100" value="85">
|
||
</div>
|
||
|
||
<div class="slider-row">
|
||
<div class="slider-label">
|
||
<span>Days since last activation</span>
|
||
<span class="slider-value" id="val-days">2</span>
|
||
</div>
|
||
<input type="range" id="sl-days" min="0" max="365" value="2">
|
||
</div>
|
||
|
||
<div class="slider-row">
|
||
<div class="slider-label">
|
||
<span>Activation count</span>
|
||
<span class="slider-value" id="val-count">12</span>
|
||
</div>
|
||
<input type="range" id="sl-count" min="1" max="1000" value="12">
|
||
</div>
|
||
|
||
<div class="salience-output">
|
||
<div class="salience-score-label">Computed Salience</div>
|
||
<div class="salience-score" id="salience-score">0.00</div>
|
||
<div class="salience-gauge">
|
||
<div class="salience-gauge-fill" id="salience-gauge-fill" style="width:0%;background:var(--purple)"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<p class="forgetting-quote">
|
||
"Forgetting is not failure. It is the system prioritising what matters."
|
||
</p>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ CONSOLIDATION ═══════════════════════════════════════════ -->
|
||
<section id="consolidation">
|
||
<span class="section-label">Memory Lifecycle</span>
|
||
<h2>Consolidation</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">In the brain, memories consolidate during sleep — the hippocampus replays experiences into the neocortex until they become stable knowledge. Engram makes this explicit.</p>
|
||
|
||
<div class="consolidation-diagram" id="consol-diagram">
|
||
<canvas id="consol-canvas"></canvas>
|
||
<div class="consol-stages">
|
||
<div class="consol-box episodic" id="consol-ep">
|
||
<div class="consol-box-title">Episodic</div>
|
||
<p>Raw events. Time-stamped experience. Not yet abstract.</p>
|
||
<div style="margin-top:1rem;font-family:var(--mono);font-size:0.72rem;color:var(--muted);text-align:left;line-height:1.7;">
|
||
tier: Episodic<br>
|
||
activation_count: 2<br>
|
||
salience: 0.41
|
||
</div>
|
||
</div>
|
||
|
||
<div class="consol-arrow-zone">
|
||
<svg class="consol-arrow-svg" viewBox="0 0 180 80" fill="none">
|
||
<defs>
|
||
<marker id="arrow-head" markerWidth="8" markerHeight="8" refX="4" refY="4" orient="auto">
|
||
<path d="M 0 0 L 8 4 L 0 8 Z" fill="#7B61FF"/>
|
||
</marker>
|
||
<linearGradient id="arrow-grad" x1="0%" y1="0%" x2="100%" y2="0%">
|
||
<stop offset="0%" stop-color="#FFB347"/>
|
||
<stop offset="100%" stop-color="#7B61FF"/>
|
||
</linearGradient>
|
||
</defs>
|
||
<line x1="10" y1="40" x2="160" y2="40" stroke="url(#arrow-grad)" stroke-width="2.5" stroke-dasharray="6 4" marker-end="url(#arrow-head)"/>
|
||
</svg>
|
||
<div class="consol-conditions">
|
||
activation_count <span class="cond-hl">≥ 5</span><br>
|
||
salience <span class="cond-hl">≥ 0.3</span><br>
|
||
<span style="color:var(--dim)">→ promote on consolidate()</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="consol-box semantic" id="consol-sem">
|
||
<div class="consol-box-title">Semantic</div>
|
||
<p>Stable concept. Integrated knowledge. No longer tied to a specific event.</p>
|
||
<div style="margin-top:1rem;font-family:var(--mono);font-size:0.72rem;color:var(--muted);text-align:left;line-height:1.7;">
|
||
tier: Semantic<br>
|
||
activation_count: 7<br>
|
||
salience: 0.68
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="consol-diagram-bottom">
|
||
Promotion is earned by use — activated, reinforced, and found relevant repeatedly over time.
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ ARCHITECTURE ═══════════════════════════════════════════ -->
|
||
<section id="architecture">
|
||
<span class="section-label">System Design</span>
|
||
<h2>Architecture</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">Three retrieval primitives layered over a single embedded store. No daemon. No network. No server to babysit.</p>
|
||
|
||
<div class="arch-diagram">
|
||
<div class="arch-layer top">[ Your Intelligence ]</div>
|
||
<div class="arch-connector">↕</div>
|
||
<div class="arch-layer api">EngramDb API</div>
|
||
<div class="arch-connector">↕</div>
|
||
<div class="arch-layer inner">
|
||
<div class="arch-inner-block">
|
||
<div class="block-title">Spreading Activation</div>
|
||
<div class="block-sub">Best-first BFS · multiplicative weights · pruning at 0.01</div>
|
||
</div>
|
||
<div class="arch-inner-block">
|
||
<div class="block-title">Vector Search</div>
|
||
<div class="block-sub">Flat cosine scan · semantic direction filter · secondary signal</div>
|
||
</div>
|
||
<div class="arch-inner-block">
|
||
<div class="block-title">Graph Traversal</div>
|
||
<div class="block-sub">BFS by relation type · typed edges · max depth</div>
|
||
</div>
|
||
</div>
|
||
<div class="arch-connector">↕</div>
|
||
<div class="arch-layer storage">sled — embedded persistent B-tree · bincode serialization · transactional</div>
|
||
<div class="arch-connector">↕</div>
|
||
<div class="arch-layer disk">[ Your disk — no daemon · no network · local-first ]</div>
|
||
</div>
|
||
|
||
<div style="margin-top:3rem;display:grid;grid-template-columns:repeat(3,1fr);gap:1.5rem;">
|
||
<div style="background:var(--bg2);border:1px solid var(--dim);border-radius:10px;padding:1.5rem;">
|
||
<div style="font-weight:700;margin-bottom:0.5rem;color:var(--white);">Why multiplication?</div>
|
||
<p style="font-size:0.85rem;color:var(--muted);">Addition lets many weak signals accumulate into false relevance. Multiplication is conjunctive: a weak edge, a dormant node, or a semantically irrelevant target all kill the path. This is how associative memory actually works.</p>
|
||
</div>
|
||
<div style="background:var(--bg2);border:1px solid var(--dim);border-radius:10px;padding:1.5rem;">
|
||
<div style="font-weight:700;margin-bottom:0.5rem;color:var(--white);">Why sled?</div>
|
||
<p style="font-size:0.85rem;color:var(--muted);">Local-first. Transactional. No daemon process, no network socket. HNSW indexing layers on top when needed — the graph structure itself is the primary retrieval mechanism.</p>
|
||
</div>
|
||
<div style="background:var(--bg2);border:1px solid var(--dim);border-radius:10px;padding:1.5rem;">
|
||
<div style="font-weight:700;margin-bottom:0.5rem;color:var(--white);">Why pruning at 0.01?</div>
|
||
<p style="font-size:0.85rem;color:var(--muted);">Small enough to allow long indirect chains when intermediate edges are strong. Raise it to focus retrieval; lower it for more associative drift. The brain's attention filter, made explicit.</p>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ BINDINGS ═══════════════════════════════════════════ -->
|
||
<section id="bindings">
|
||
<span class="section-label">Language Bindings</span>
|
||
<h2>Use from anywhere</h2>
|
||
<p style="max-width:640px; margin-top:1rem;">A C FFI layer exposes the full API. Language bindings ship for Kotlin, TypeScript, Go, and Rust natively.</p>
|
||
|
||
<div class="tabs">
|
||
<div class="tab-buttons">
|
||
<button class="tab-btn active" onclick="switchTab('rust')">Rust</button>
|
||
<button class="tab-btn" onclick="switchTab('kotlin')">Kotlin</button>
|
||
<button class="tab-btn" onclick="switchTab('typescript')">TypeScript</button>
|
||
<button class="tab-btn" onclick="switchTab('go')">Go</button>
|
||
</div>
|
||
|
||
<div class="tab-content active" id="tab-rust">
|
||
<div class="code-header">
|
||
<div class="dot-red"></div><div class="dot-yellow"></div><div class="dot-green"></div>
|
||
<span style="margin-left:8px;">basic.rs</span>
|
||
</div>
|
||
<pre><code><span class="kw">use</span> engram_core<span class="punct">::{</span><span class="type">EngramDb</span><span class="punct">,</span> <span class="type">Node</span><span class="punct">,</span> <span class="type">Edge</span><span class="punct">,</span> <span class="type">NodeType</span><span class="punct">,</span> <span class="type">MemoryTier</span><span class="punct">,</span> <span class="type">RelationType</span><span class="punct">};</span>
|
||
|
||
<span class="comment">// Open or create a local database — no server, no daemon</span>
|
||
<span class="kw">let</span> db <span class="op">=</span> <span class="type">EngramDb</span><span class="punct">::</span><span class="fn-name">open</span><span class="punct">(</span><span class="type">Path</span><span class="punct">::</span><span class="fn-name">new</span><span class="punct">(</span><span class="str">"/var/lib/agent/memory"</span><span class="punct">))?;</span>
|
||
|
||
<span class="comment">// Store a concept with a semantic embedding</span>
|
||
<span class="kw">let</span> id <span class="op">=</span> db<span class="punct">.</span><span class="fn-name">put_node</span><span class="punct">(</span><span class="type">Node</span><span class="punct">::</span><span class="fn-name">new</span><span class="punct">(</span>
|
||
<span class="type">NodeType</span><span class="punct">::</span><span class="const">Concept</span><span class="punct">,</span>
|
||
embedding<span class="punct">,</span> <span class="comment">// Vec<f32> from your LLM</span>
|
||
content<span class="punct">,</span> <span class="comment">// Vec<u8> — any payload</span>
|
||
<span class="type">MemoryTier</span><span class="punct">::</span><span class="const">Semantic</span><span class="punct">,</span>
|
||
<span class="num">0.9</span><span class="punct">,</span> <span class="comment">// importance 0.0–1.0</span>
|
||
<span class="punct">))?;</span>
|
||
|
||
<span class="comment">// Link to a related concept</span>
|
||
db<span class="punct">.</span><span class="fn-name">put_edge</span><span class="punct">(</span><span class="type">Edge</span><span class="punct">::</span><span class="fn-name">new</span><span class="punct">(</span>id<span class="punct">,</span> related_id<span class="punct">,</span> <span class="type">RelationType</span><span class="punct">::</span><span class="const">Causes</span><span class="punct">,</span> <span class="num">0.88</span><span class="punct">))?;</span>
|
||
|
||
<span class="comment">// Retrieve by spreading activation — not a query, a pattern completion</span>
|
||
<span class="kw">let</span> results <span class="op">=</span> db<span class="punct">.</span><span class="fn-name">activate</span><span class="punct">(</span>
|
||
<span class="op">&</span><span class="punct">[</span>id<span class="punct">],</span> <span class="comment">// seed nodes</span>
|
||
<span class="op">&</span>query_embedding<span class="punct">,</span> <span class="comment">// direction of thought</span>
|
||
<span class="num">3</span><span class="punct">,</span> <span class="comment">// max hops</span>
|
||
<span class="num">10</span><span class="punct">,</span> <span class="comment">// top-N results</span>
|
||
<span class="punct">)?;</span>
|
||
|
||
<span class="kw">for</span> r <span class="kw">in</span> <span class="op">&</span>results <span class="punct">{</span>
|
||
<span class="fn-name">println!</span><span class="punct">(</span><span class="str">"strength={:.4} hops={}"</span><span class="punct">,</span> r<span class="punct">.</span>activation_strength<span class="punct">,</span> r<span class="punct">.</span>hops<span class="punct">);</span>
|
||
<span class="punct">}</span></code></pre>
|
||
</div>
|
||
|
||
<div class="tab-content" id="tab-kotlin">
|
||
<div class="code-header">
|
||
<div class="dot-red"></div><div class="dot-yellow"></div><div class="dot-green"></div>
|
||
<span style="margin-left:8px;">Memory.kt</span>
|
||
</div>
|
||
<pre><code><span class="kw">import</span> ai<span class="punct">.</span>neuron<span class="punct">.</span>engram<span class="punct">.*</span>
|
||
|
||
<span class="comment">// JVM binding via JNI — same embedded storage, no server</span>
|
||
<span class="kw">val</span> db <span class="op">=</span> <span class="type">EngramDb</span><span class="punct">.</span><span class="fn-name">open</span><span class="punct">(</span><span class="str">"/data/user/0/ai.agent/memory"</span><span class="punct">)</span>
|
||
|
||
<span class="comment">// Store an episodic event</span>
|
||
<span class="kw">val</span> id <span class="op">=</span> db<span class="punct">.</span><span class="fn-name">putNode</span><span class="punct">(</span>
|
||
<span class="type">Node</span><span class="punct">(</span>
|
||
nodeType <span class="op">=</span> <span class="type">NodeType</span><span class="punct">.</span><span class="const">EVENT</span><span class="punct">,</span>
|
||
embedding <span class="op">=</span> llm<span class="punct">.</span><span class="fn-name">embed</span><span class="punct">(</span>text<span class="punct">),</span>
|
||
content <span class="op">=</span> text<span class="punct">.</span><span class="fn-name">toByteArray</span><span class="punct">(),</span>
|
||
tier <span class="op">=</span> <span class="type">MemoryTier</span><span class="punct">.</span><span class="const">EPISODIC</span><span class="punct">,</span>
|
||
importance <span class="op">=</span> <span class="num">0.8f</span>
|
||
<span class="punct">)</span>
|
||
<span class="punct">)</span>
|
||
|
||
<span class="comment">// Activate from current context</span>
|
||
<span class="kw">val</span> recalled <span class="op">=</span> db<span class="punct">.</span><span class="fn-name">activate</span><span class="punct">(</span>
|
||
seeds <span class="op">=</span> listOf<span class="punct">(</span>id<span class="punct">),</span>
|
||
queryEmbedding <span class="op">=</span> currentContext<span class="punct">.</span>embedding<span class="punct">,</span>
|
||
maxDepth <span class="op">=</span> <span class="num">3</span><span class="punct">,</span>
|
||
limit <span class="op">=</span> <span class="num">10</span>
|
||
<span class="punct">)</span>
|
||
|
||
recalled<span class="punct">.</span><span class="fn-name">forEach</span> <span class="punct">{</span>
|
||
<span class="fn-name">println</span><span class="punct">(</span><span class="str">"[${it.hops} hops] strength=${it.activationStrength}"</span><span class="punct">)</span>
|
||
<span class="punct">}</span></code></pre>
|
||
</div>
|
||
|
||
<div class="tab-content" id="tab-typescript">
|
||
<div class="code-header">
|
||
<div class="dot-red"></div><div class="dot-yellow"></div><div class="dot-green"></div>
|
||
<span style="margin-left:8px;">memory.ts</span>
|
||
</div>
|
||
<pre><code><span class="kw">import</span> <span class="punct">{</span> <span class="type">EngramDb</span><span class="punct">,</span> <span class="type">NodeType</span><span class="punct">,</span> <span class="type">MemoryTier</span><span class="punct">,</span> <span class="type">RelationType</span> <span class="punct">}</span> <span class="kw">from</span> <span class="str">"@neuron/engram"</span><span class="punct">;</span>
|
||
|
||
<span class="comment">// WASM build — runs in Node or browser, fully in-process</span>
|
||
<span class="kw">const</span> db <span class="op">=</span> <span class="kw">await</span> <span class="type">EngramDb</span><span class="punct">.</span><span class="fn-name">open</span><span class="punct">(</span><span class="str">"./agent-memory"</span><span class="punct">);</span>
|
||
|
||
<span class="comment">// Store a concept node</span>
|
||
<span class="kw">const</span> id <span class="op">=</span> <span class="kw">await</span> db<span class="punct">.</span><span class="fn-name">putNode</span><span class="punct">({</span>
|
||
nodeType<span class="punct">:</span> <span class="type">NodeType</span><span class="punct">.</span><span class="const">Concept</span><span class="punct">,</span>
|
||
embedding<span class="punct">:</span> <span class="kw">await</span> llm<span class="punct">.</span><span class="fn-name">embed</span><span class="punct">(</span>text<span class="punct">),</span>
|
||
content<span class="punct">:</span> <span class="kw">new</span> <span class="type">TextEncoder</span><span class="punct">().</span><span class="fn-name">encode</span><span class="punct">(</span>text<span class="punct">),</span>
|
||
tier<span class="punct">:</span> <span class="type">MemoryTier</span><span class="punct">.</span><span class="const">Semantic</span><span class="punct">,</span>
|
||
importance<span class="punct">:</span> <span class="num">0.85</span><span class="punct">,</span>
|
||
<span class="punct">});</span>
|
||
|
||
<span class="comment">// Spreading activation — pattern completion, not query</span>
|
||
<span class="kw">const</span> recalled <span class="op">=</span> <span class="kw">await</span> db<span class="punct">.</span><span class="fn-name">activate</span><span class="punct">({</span>
|
||
seeds<span class="punct">:</span> <span class="punct">[</span>id<span class="punct">],</span>
|
||
queryEmbedding<span class="punct">:</span> currentContext<span class="punct">.</span>embedding<span class="punct">,</span>
|
||
maxDepth<span class="punct">:</span> <span class="num">3</span><span class="punct">,</span>
|
||
limit<span class="punct">:</span> <span class="num">10</span><span class="punct">,</span>
|
||
<span class="punct">});</span>
|
||
|
||
<span class="kw">for</span> <span class="punct">(</span><span class="kw">const</span> node <span class="kw">of</span> recalled<span class="punct">)</span> <span class="punct">{</span>
|
||
console<span class="punct">.</span><span class="fn-name">log</span><span class="punct">(</span><span class="str">`strength=<span class="punct">${</span>node<span class="punct">.</span>activationStrength<span class="punct">.</span><span class="fn-name">toFixed</span><span class="punct">(</span><span class="num">4</span><span class="punct">)}</span> hops=<span class="punct">${</span>node<span class="punct">.</span>hops<span class="punct">}</span>`</span><span class="punct">);</span>
|
||
<span class="punct">}</span></code></pre>
|
||
</div>
|
||
|
||
<div class="tab-content" id="tab-go">
|
||
<div class="code-header">
|
||
<div class="dot-red"></div><div class="dot-yellow"></div><div class="dot-green"></div>
|
||
<span style="margin-left:8px;">memory.go</span>
|
||
</div>
|
||
<pre><code><span class="kw">import</span> <span class="punct">(</span>
|
||
engram <span class="str">"github.com/neuron-technologies/engram-go"</span>
|
||
<span class="punct">)</span>
|
||
|
||
<span class="comment">// CGo binding — links the Rust library, zero copies for embeddings</span>
|
||
db<span class="punct">,</span> err <span class="op">:=</span> engram<span class="punct">.</span><span class="fn-name">Open</span><span class="punct">(</span><span class="str">"/var/lib/agent/memory"</span><span class="punct">)</span>
|
||
<span class="kw">if</span> err <span class="op">!=</span> <span class="const">nil</span> <span class="punct">{</span>
|
||
<span class="kw">return</span> err
|
||
<span class="punct">}</span>
|
||
<span class="kw">defer</span> db<span class="punct">.</span><span class="fn-name">Close</span><span class="punct">()</span>
|
||
|
||
<span class="comment">// Store a node</span>
|
||
id<span class="punct">,</span> err <span class="op">:=</span> db<span class="punct">.</span><span class="fn-name">PutNode</span><span class="punct">(</span>engram<span class="punct">.</span><span class="type">Node</span><span class="punct">{</span>
|
||
NodeType<span class="punct">:</span> engram<span class="punct">.</span><span class="const">Concept</span><span class="punct">,</span>
|
||
Embedding<span class="punct">:</span> embedding<span class="punct">,</span>
|
||
Content<span class="punct">:</span> <span class="punct">[]</span><span class="type">byte</span><span class="punct">(</span>text<span class="punct">),</span>
|
||
Tier<span class="punct">:</span> engram<span class="punct">.</span><span class="const">Semantic</span><span class="punct">,</span>
|
||
Importance<span class="punct">:</span> <span class="num">0.9</span><span class="punct">,</span>
|
||
<span class="punct">})</span>
|
||
|
||
<span class="comment">// Activate from seed — the graph does the retrieval</span>
|
||
results<span class="punct">,</span> err <span class="op">:=</span> db<span class="punct">.</span><span class="fn-name">Activate</span><span class="punct">(</span>
|
||
<span class="punct">[]</span>engram<span class="punct">.</span><span class="type">UUID</span><span class="punct">{</span>id<span class="punct">},</span>
|
||
queryEmbedding<span class="punct">,</span>
|
||
<span class="num">3</span><span class="punct">,</span> <span class="comment">// maxDepth</span>
|
||
<span class="num">10</span><span class="punct">,</span> <span class="comment">// limit</span>
|
||
<span class="punct">)</span>
|
||
|
||
<span class="kw">for</span> <span class="punct">_,</span> r <span class="op">:=</span> <span class="kw">range</span> results <span class="punct">{</span>
|
||
fmt<span class="punct">.</span><span class="fn-name">Printf</span><span class="punct">(</span><span class="str">"strength=%.4f hops=%d\n"</span><span class="punct">,</span> r<span class="punct">.</span>Strength<span class="punct">,</span> r<span class="punct">.</span>Hops<span class="punct">)</span>
|
||
<span class="punct">}</span></code></pre>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ WHY LOCAL ═══════════════════════════════════════════ -->
|
||
<section id="local" style="border-top: 1px solid var(--dim);">
|
||
<span class="section-label">Philosophy</span>
|
||
<h2>Why local</h2>
|
||
|
||
<div class="local-divider"></div>
|
||
|
||
<p class="local-statement">
|
||
Your memory is not a service.<br>
|
||
<em>It is you.</em><br>
|
||
<br>
|
||
It lives on <strong>your hardware</strong>, under <strong>your control</strong>.<br>
|
||
It does not leave your device.<br>
|
||
It does not phone home.<br>
|
||
<br>
|
||
<em>It does not require a network connection<br>to remember who you are.</em>
|
||
</p>
|
||
|
||
<div class="local-divider"></div>
|
||
|
||
<div style="display:flex;gap:2rem;justify-content:center;flex-wrap:wrap;margin-top:1rem;">
|
||
<div style="text-align:center;color:var(--muted);font-size:0.85rem;">
|
||
<div style="font-size:1.5rem;margin-bottom:0.5rem;">🔒</div>
|
||
No telemetry
|
||
</div>
|
||
<div style="text-align:center;color:var(--muted);font-size:0.85rem;">
|
||
<div style="font-size:1.5rem;margin-bottom:0.5rem;">📡</div>
|
||
No network required
|
||
</div>
|
||
<div style="text-align:center;color:var(--muted);font-size:0.85rem;">
|
||
<div style="font-size:1.5rem;margin-bottom:0.5rem;">🗄️</div>
|
||
Embedded storage
|
||
</div>
|
||
<div style="text-align:center;color:var(--muted);font-size:0.85rem;">
|
||
<div style="font-size:1.5rem;margin-bottom:0.5rem;">⚡</div>
|
||
No daemon process
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- ═══════════════════════════════════════════ STATUS ═══════════════════════════════════════════ -->
|
||
<section id="status">
|
||
<span class="section-label">Build Status</span>
|
||
<h2>Current state</h2>
|
||
|
||
<div class="status-grid">
|
||
<div class="status-badge ver">
|
||
<span class="status-icon">◈</span>
|
||
<div>
|
||
<div class="status-badge-label">Version</div>
|
||
<div class="status-badge-value">v0.1.0</div>
|
||
</div>
|
||
</div>
|
||
<div class="status-badge ok">
|
||
<span class="status-icon">✓</span>
|
||
<div>
|
||
<div class="status-badge-label">Build</div>
|
||
<div class="status-badge-value">Zero warnings · Zero errors</div>
|
||
</div>
|
||
</div>
|
||
<div class="status-badge tests">
|
||
<span class="status-icon">◎</span>
|
||
<div>
|
||
<div class="status-badge-label">Test Suite</div>
|
||
<div class="status-badge-value">38 passing</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<div style="margin-top:3rem;background:var(--bg2);border:1px solid var(--dim);border-radius:12px;overflow:hidden;">
|
||
<div style="padding:1rem 1.5rem;border-bottom:1px solid var(--dim);font-size:0.75rem;color:var(--muted);font-family:var(--mono);">
|
||
Public API Surface · engram-core v0.1.0
|
||
</div>
|
||
<pre style="padding:1.5rem;font-size:0.8rem;"><code><span class="kw">impl</span> <span class="type">EngramDb</span> <span class="punct">{</span>
|
||
<span class="kw">fn</span> <span class="fn-name">open</span><span class="punct">(</span>path<span class="punct">:</span> <span class="op">&</span><span class="type">Path</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Self</span><span class="punct">>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">put_node</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> node<span class="punct">:</span> <span class="type">Node</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Uuid</span><span class="punct">>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">get_node</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> id<span class="punct">:</span> <span class="type">Uuid</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Option</span><span class="punct"><</span><span class="type">Node</span><span class="punct">>>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">put_edge</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> edge<span class="punct">:</span> <span class="type">Edge</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><()>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">activate</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> seeds<span class="punct">:</span> <span class="op">&</span><span class="punct">[</span><span class="type">Uuid</span><span class="punct">],</span> emb<span class="punct">:</span> <span class="op">&</span><span class="punct">[</span><span class="type">f32</span><span class="punct">],</span> depth<span class="punct">:</span> <span class="type">u8</span><span class="punct">,</span> n<span class="punct">:</span> <span class="type">usize</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Vec</span><span class="punct"><</span><span class="type">ActivatedNode</span><span class="punct">>>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">search_embedding</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> emb<span class="punct">:</span> <span class="op">&</span><span class="punct">[</span><span class="type">f32</span><span class="punct">],</span> limit<span class="punct">:</span> <span class="type">usize</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Vec</span><span class="punct"><</span><span class="type">ScoredNode</span><span class="punct">>>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">traverse</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> from<span class="punct">:</span> <span class="type">Uuid</span><span class="punct">,</span> rel<span class="punct">:</span> <span class="type">Option</span><span class="punct"><</span><span class="type">RelationType</span><span class="punct">>,</span> depth<span class="punct">:</span> <span class="type">u8</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">Vec</span><span class="punct"><</span><span class="type">Node</span><span class="punct">>>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">touch</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> id<span class="punct">:</span> <span class="type">Uuid</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><()>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">decay</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> factor<span class="punct">:</span> <span class="type">f32</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">usize</span><span class="punct">>;</span>
|
||
<span class="kw">fn</span> <span class="fn-name">consolidate</span><span class="punct">(&</span><span class="kw">self</span><span class="punct">,</span> config<span class="punct">:</span> <span class="op">&</span><span class="type">ConsolidationConfig</span><span class="punct">)</span> <span class="op">-></span> <span class="type">EngramResult</span><span class="punct"><</span><span class="type">ConsolidationReport</span><span class="punct">>;</span>
|
||
<span class="punct">}</span></code></pre>
|
||
</div>
|
||
</section>
|
||
|
||
<footer>
|
||
engram · v0.1.0 · neuron technologies · the physical trace of a memory
|
||
</footer>
|
||
|
||
<script>
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// HERO CANVAS — pulsing node graph background
|
||
// ════════════════════════════════════════════════════════════════════
|
||
(function() {
|
||
const canvas = document.getElementById('hero-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
let W, H;
|
||
let nodes = [];
|
||
let edges = [];
|
||
let t = 0;
|
||
|
||
function resize() {
|
||
W = canvas.width = canvas.offsetWidth;
|
||
H = canvas.height = canvas.offsetHeight;
|
||
initGraph();
|
||
}
|
||
|
||
function initGraph() {
|
||
const N = Math.min(28, Math.floor(W * H / 22000));
|
||
nodes = [];
|
||
edges = [];
|
||
for (let i = 0; i < N; i++) {
|
||
nodes.push({
|
||
x: Math.random() * W,
|
||
y: Math.random() * H,
|
||
r: 2 + Math.random() * 3,
|
||
vx: (Math.random() - 0.5) * 0.18,
|
||
vy: (Math.random() - 0.5) * 0.18,
|
||
phase: Math.random() * Math.PI * 2,
|
||
speed: 0.3 + Math.random() * 0.8,
|
||
});
|
||
}
|
||
for (let i = 0; i < nodes.length; i++) {
|
||
for (let j = i + 1; j < nodes.length; j++) {
|
||
const dx = nodes[i].x - nodes[j].x;
|
||
const dy = nodes[i].y - nodes[j].y;
|
||
const dist = Math.sqrt(dx*dx + dy*dy);
|
||
if (dist < Math.min(W, H) * 0.28 && Math.random() < 0.35) {
|
||
edges.push({ a: i, b: j, baseOpacity: 0.04 + Math.random() * 0.08 });
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
function draw() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
t += 0.006;
|
||
|
||
// Update node positions
|
||
for (const n of nodes) {
|
||
n.x += n.vx;
|
||
n.y += n.vy;
|
||
if (n.x < -40) n.x = W + 40;
|
||
if (n.x > W + 40) n.x = -40;
|
||
if (n.y < -40) n.y = H + 40;
|
||
if (n.y > H + 40) n.y = -40;
|
||
}
|
||
|
||
// Draw edges
|
||
for (const e of edges) {
|
||
const a = nodes[e.a], b = nodes[e.b];
|
||
const pulse = 0.5 + 0.5 * Math.sin(t * 1.5 + e.a * 0.7);
|
||
const opacity = e.baseOpacity * (0.5 + 0.5 * pulse);
|
||
ctx.beginPath();
|
||
ctx.moveTo(a.x, a.y);
|
||
ctx.lineTo(b.x, b.y);
|
||
ctx.strokeStyle = `rgba(123,97,255,${opacity})`;
|
||
ctx.lineWidth = 0.8;
|
||
ctx.stroke();
|
||
}
|
||
|
||
// Draw nodes
|
||
for (const n of nodes) {
|
||
const glow = 0.5 + 0.5 * Math.sin(t * n.speed + n.phase);
|
||
const r = n.r * (0.85 + 0.15 * glow);
|
||
const alpha = 0.25 + 0.35 * glow;
|
||
|
||
// Glow halo
|
||
const gradient = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, r * 4);
|
||
gradient.addColorStop(0, `rgba(123,97,255,${alpha * 0.6})`);
|
||
gradient.addColorStop(1, 'rgba(123,97,255,0)');
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, r * 4, 0, Math.PI * 2);
|
||
ctx.fillStyle = gradient;
|
||
ctx.fill();
|
||
|
||
// Core dot
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
||
ctx.fillStyle = `rgba(123,97,255,${alpha})`;
|
||
ctx.fill();
|
||
}
|
||
|
||
requestAnimationFrame(draw);
|
||
}
|
||
|
||
window.addEventListener('resize', resize);
|
||
resize();
|
||
draw();
|
||
})();
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// ACTIVATION DEMO CANVAS
|
||
// ════════════════════════════════════════════════════════════════════
|
||
(function() {
|
||
const canvas = document.getElementById('activation-canvas');
|
||
const ctx = canvas.getContext('2d');
|
||
const btn = document.getElementById('btn-activate');
|
||
const resetBtn = document.getElementById('btn-reset');
|
||
const seedIndicator = document.getElementById('seed-indicator');
|
||
const speedSlider = document.getElementById('speed-slider');
|
||
|
||
let W, H, dpr;
|
||
let selectedSeed = null;
|
||
let animating = false;
|
||
let animationFrames = [];
|
||
let currentFrame = 0;
|
||
let animTimer = null;
|
||
|
||
// ── Graph definition (from the example in basic.rs) ──
|
||
const nodeData = [
|
||
{ label: 'Spreading\nActivation', type: 'Concept', tier: 'Semantic', salience: 0.95, importance: 0.95, col: '#7B61FF' },
|
||
{ label: 'Long-Term\nPotentiation', type: 'Concept', tier: 'Semantic', salience: 0.90, importance: 0.90, col: '#7B61FF' },
|
||
{ label: 'Hebbian\nLearning', type: 'Memory', tier: 'Episodic', salience: 0.85, importance: 0.85, col: '#FFB347' },
|
||
{ label: 'Associative\nMemory', type: 'Concept', tier: 'Semantic', salience: 0.88, importance: 0.88, col: '#7B61FF' },
|
||
{ label: 'Salience\nDecay', type: 'Process', tier: 'Procedural',salience: 0.75, importance: 0.75, col: '#4ECDC4' },
|
||
{ label: 'Memory\nConsolidation', type: 'Event', tier: 'Episodic', salience: 0.70, importance: 0.70, col: '#FFB347' },
|
||
{ label: 'Pattern\nCompletion', type: 'Concept', tier: 'Semantic', salience: 0.80, importance: 0.80, col: '#7B61FF' },
|
||
{ label: 'Synaptic\nWeight', type: 'Entity', tier: 'Semantic', salience: 0.72, importance: 0.72, col: '#7B61FF' },
|
||
];
|
||
|
||
const edgeData = [
|
||
{ from: 0, to: 1, weight: 0.90, label: 'Causes' },
|
||
{ from: 1, to: 2, weight: 0.85, label: 'References' },
|
||
{ from: 0, to: 3, weight: 0.88, label: 'Activates' },
|
||
{ from: 2, to: 3, weight: 0.80, label: 'Exemplifies' },
|
||
{ from: 4, to: 5, weight: 0.65, label: 'Precedes' },
|
||
{ from: 1, to: 5, weight: 0.72, label: 'Precedes' },
|
||
{ from: 3, to: 6, weight: 0.78, label: 'Causes' },
|
||
{ from: 6, to: 7, weight: 0.68, label: 'References' },
|
||
{ from: 1, to: 7, weight: 0.82, label: 'Activates' },
|
||
{ from: 0, to: 4, weight: 0.55, label: 'References' },
|
||
];
|
||
|
||
let nodes = [];
|
||
let edges = [];
|
||
let activationStrengths = {}; // nodeIdx -> strength
|
||
let activationHops = {};
|
||
|
||
function resize() {
|
||
const rect = canvas.getBoundingClientRect();
|
||
dpr = window.devicePixelRatio || 1;
|
||
W = rect.width;
|
||
H = rect.height;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
ctx.scale(dpr, dpr);
|
||
layoutNodes();
|
||
render();
|
||
}
|
||
|
||
function layoutNodes() {
|
||
// Place nodes in a natural arrangement
|
||
const positions = [
|
||
[0.5, 0.2 ], // 0 Spreading Activation — top center (main)
|
||
[0.72, 0.38], // 1 LTP
|
||
[0.62, 0.58], // 2 Hebbian Learning
|
||
[0.38, 0.55], // 3 Associative Memory
|
||
[0.18, 0.32], // 4 Salience Decay
|
||
[0.28, 0.72], // 5 Memory Consolidation
|
||
[0.52, 0.80], // 6 Pattern Completion
|
||
[0.82, 0.62], // 7 Synaptic Weight
|
||
];
|
||
|
||
nodes = nodeData.map((d, i) => ({
|
||
...d,
|
||
x: positions[i][0] * W,
|
||
y: positions[i][1] * H,
|
||
r: 26,
|
||
idx: i,
|
||
}));
|
||
|
||
edges = edgeData.map(e => ({ ...e }));
|
||
}
|
||
|
||
function cosSim(seed) {
|
||
// Fake cos_sim: based on node tier similarity to seed
|
||
return function(targetIdx) {
|
||
const seedNode = nodeData[seed];
|
||
const targetNode = nodeData[targetIdx];
|
||
if (seedNode.tier === targetNode.tier) return 0.85;
|
||
if (seedNode.type === targetNode.type) return 0.72;
|
||
return 0.45 + Math.random() * 0.2;
|
||
};
|
||
}
|
||
|
||
function computeActivation(seedIdx) {
|
||
// BFS spreading activation, matching the Rust implementation
|
||
const best = {};
|
||
const queue = [{ idx: seedIdx, strength: 1.0, hops: 0 }];
|
||
best[seedIdx] = { strength: 1.0, hops: 0 };
|
||
const cos = cosSim(seedIdx);
|
||
|
||
const PRUNE = 0.01;
|
||
const MAX_DEPTH = 3;
|
||
|
||
while (queue.length > 0) {
|
||
queue.sort((a, b) => b.strength - a.strength);
|
||
const { idx, strength, hops } = queue.shift();
|
||
if (hops >= MAX_DEPTH) continue;
|
||
|
||
const outEdges = edges.filter(e => e.from === idx);
|
||
for (const e of outEdges) {
|
||
const target = e.to;
|
||
const sem = cos(target);
|
||
const newStr = strength * e.weight * nodeData[target].salience * sem;
|
||
if (newStr < PRUNE) continue;
|
||
const nextHops = hops + 1;
|
||
const existing = best[target];
|
||
if (!existing || newStr > existing.strength) {
|
||
best[target] = { strength: newStr, hops: nextHops };
|
||
queue.push({ idx: target, strength: newStr, hops: nextHops });
|
||
}
|
||
}
|
||
}
|
||
|
||
return best;
|
||
}
|
||
|
||
function buildAnimationFrames(seedIdx, activation) {
|
||
// Build frames: hop 0 (seed glow), then hop 1, hop 2, hop 3
|
||
const frames = [];
|
||
frames.push({ type: 'seed', nodeIdx: seedIdx });
|
||
|
||
// Group by hop distance
|
||
const byHop = {};
|
||
for (const [idxStr, data] of Object.entries(activation)) {
|
||
const idx = parseInt(idxStr);
|
||
if (idx === seedIdx) continue;
|
||
if (!byHop[data.hops]) byHop[data.hops] = [];
|
||
byHop[data.hops].push({ idx, strength: data.strength, hops: data.hops });
|
||
}
|
||
|
||
// Animate edges first, then node arrival
|
||
const maxHop = Math.max(...Object.keys(byHop).map(Number));
|
||
for (let h = 1; h <= maxHop; h++) {
|
||
if (byHop[h]) {
|
||
// Find which edges carry activation to nodes at this hop
|
||
for (const activated of byHop[h]) {
|
||
// Find source edge (the edge that carries this activation)
|
||
const inEdges = edges.filter(e => e.to === activated.idx);
|
||
for (const e of inEdges) {
|
||
if (activation[e.from] && activation[e.from].hops === h - 1) {
|
||
frames.push({ type: 'edge-activate', from: e.from, to: activated.idx, weight: e.weight });
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
for (const activated of byHop[h]) {
|
||
frames.push({ type: 'node-activate', nodeIdx: activated.idx, strength: activated.strength, hops: activated.hops });
|
||
}
|
||
}
|
||
}
|
||
|
||
return frames;
|
||
}
|
||
|
||
// Rendering state
|
||
let glowNodes = {}; // nodeIdx -> { alpha, strength, hops }
|
||
let glowEdges = {}; // `${from}-${to}` -> alpha
|
||
let animationState = 'idle'; // idle | running | done
|
||
|
||
function render() {
|
||
ctx.clearRect(0, 0, W, H);
|
||
|
||
// ── Draw edges ──
|
||
for (const e of edges) {
|
||
const a = nodes[e.from], b = nodes[e.to];
|
||
const key = `${e.from}-${e.to}`;
|
||
const glowAlpha = glowEdges[key] || 0;
|
||
const baseAlpha = 0.18;
|
||
const alpha = baseAlpha + glowAlpha * 0.7;
|
||
const width = 1 + e.weight * 2 + glowAlpha * 3;
|
||
|
||
ctx.beginPath();
|
||
ctx.moveTo(a.x, a.y);
|
||
ctx.lineTo(b.x, b.y);
|
||
ctx.strokeStyle = glowAlpha > 0.1 ? `rgba(123,97,255,${alpha})` : `rgba(100,100,150,${baseAlpha})`;
|
||
ctx.lineWidth = width;
|
||
ctx.stroke();
|
||
|
||
// Weight label on active edges
|
||
if (glowAlpha > 0.3) {
|
||
const mx = (a.x + b.x) / 2, my = (a.y + b.y) / 2;
|
||
ctx.fillStyle = `rgba(180,160,255,${glowAlpha * 0.9})`;
|
||
ctx.font = `600 9px var(--mono, monospace)`;
|
||
ctx.textAlign = 'center';
|
||
ctx.fillText(e.weight.toFixed(2), mx, my - 5);
|
||
}
|
||
}
|
||
|
||
// ── Draw nodes ──
|
||
for (const n of nodes) {
|
||
const glow = glowNodes[n.idx];
|
||
const isSelected = n.idx === selectedSeed;
|
||
const isSeed = glow && glow.isSeed;
|
||
const isActivated = glow && !glow.isSeed;
|
||
|
||
const glowAlpha = glow ? glow.alpha : 0;
|
||
const r = n.r;
|
||
|
||
// Outer glow
|
||
if (glowAlpha > 0.01 || isSelected) {
|
||
const gAlpha = isSelected ? 0.3 : glowAlpha * 0.5;
|
||
const gRadius = isSeed ? r * 3.5 : r * 2.5;
|
||
const gradient = ctx.createRadialGradient(n.x, n.y, 0, n.x, n.y, gRadius);
|
||
const color = isSeed ? '123,97,255' : '123,97,255';
|
||
gradient.addColorStop(0, `rgba(${color},${gAlpha})`);
|
||
gradient.addColorStop(1, `rgba(${color},0)`);
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, gRadius, 0, Math.PI * 2);
|
||
ctx.fillStyle = gradient;
|
||
ctx.fill();
|
||
}
|
||
|
||
// Ring for selected seed
|
||
if (isSelected && !isSeed) {
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, r + 4, 0, Math.PI * 2);
|
||
ctx.strokeStyle = 'rgba(123,97,255,0.7)';
|
||
ctx.lineWidth = 1.5;
|
||
ctx.setLineDash([4, 4]);
|
||
ctx.stroke();
|
||
ctx.setLineDash([]);
|
||
}
|
||
|
||
// Node background
|
||
const bgAlpha = isSeed ? 0.95 : (isActivated ? 0.85 : (isSelected ? 0.7 : 0.5));
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
||
|
||
if (isSeed) {
|
||
ctx.fillStyle = '#7B61FF';
|
||
} else if (isActivated) {
|
||
const str = glow.strength;
|
||
const intensity = Math.min(1, str * 1.5);
|
||
ctx.fillStyle = `rgba(${Math.floor(60 + intensity * 63)},${Math.floor(40 + intensity * 57)},${Math.floor(150 + intensity * 105)},${bgAlpha})`;
|
||
} else if (isSelected) {
|
||
ctx.fillStyle = 'rgba(123,97,255,0.4)';
|
||
} else {
|
||
ctx.fillStyle = 'rgba(30,25,60,0.85)';
|
||
}
|
||
ctx.fill();
|
||
|
||
// Border
|
||
ctx.beginPath();
|
||
ctx.arc(n.x, n.y, r, 0, Math.PI * 2);
|
||
if (isSeed) {
|
||
ctx.strokeStyle = '#B8A8FF';
|
||
} else if (isActivated) {
|
||
ctx.strokeStyle = `rgba(123,97,255,${0.4 + glowAlpha * 0.5})`;
|
||
} else if (isSelected) {
|
||
ctx.strokeStyle = 'rgba(123,97,255,0.8)';
|
||
} else {
|
||
ctx.strokeStyle = 'rgba(80,70,130,0.6)';
|
||
}
|
||
ctx.lineWidth = isSeed ? 2 : 1;
|
||
ctx.stroke();
|
||
|
||
// Node label (type)
|
||
const lines = n.label.split('\n');
|
||
const textColor = isSeed ? '#fff' : (isActivated ? `rgba(220,210,255,${0.5 + glowAlpha * 0.5})` : 'rgba(140,130,180,0.8)');
|
||
ctx.fillStyle = textColor;
|
||
ctx.font = `600 9px -apple-system,sans-serif`;
|
||
ctx.textAlign = 'center';
|
||
if (lines.length === 1) {
|
||
ctx.fillText(lines[0], n.x, n.y + 3);
|
||
} else {
|
||
ctx.fillText(lines[0], n.x, n.y - 2);
|
||
ctx.fillText(lines[1], n.x, n.y + 9);
|
||
}
|
||
|
||
// Activation strength label
|
||
if (isActivated && glow.strength !== undefined) {
|
||
ctx.fillStyle = `rgba(160,145,255,${glowAlpha})`;
|
||
ctx.font = `bold 10px var(--mono, monospace)`;
|
||
ctx.fillText(glow.strength.toFixed(2), n.x, n.y + r + 13);
|
||
}
|
||
|
||
// Seed label
|
||
if (isSeed) {
|
||
ctx.fillStyle = 'rgba(200,180,255,0.9)';
|
||
ctx.font = `bold 9px var(--mono, monospace)`;
|
||
ctx.fillText('SEED', n.x, n.y + r + 13);
|
||
}
|
||
|
||
// Tier dot
|
||
const tierColors = { Semantic: '#7B61FF', Episodic: '#FFB347', Procedural: '#4ECDC4', Working: '#FF6B6B' };
|
||
ctx.beginPath();
|
||
ctx.arc(n.x + r - 5, n.y - r + 5, 4, 0, Math.PI * 2);
|
||
ctx.fillStyle = tierColors[n.tier] || '#888';
|
||
ctx.fill();
|
||
}
|
||
}
|
||
|
||
// Smooth animation loop
|
||
let glowTargets = {};
|
||
let edgeTargets = {};
|
||
let animRunning = false;
|
||
|
||
function smoothRender() {
|
||
// Lerp current glow toward targets
|
||
let changed = false;
|
||
for (const [key, target] of Object.entries(glowTargets)) {
|
||
const current = glowNodes[key] || { alpha: 0 };
|
||
const newAlpha = current.alpha + (target.alpha - current.alpha) * 0.12;
|
||
if (Math.abs(newAlpha - current.alpha) > 0.001) changed = true;
|
||
glowNodes[key] = { ...target, alpha: newAlpha };
|
||
}
|
||
for (const [key, target] of Object.entries(edgeTargets)) {
|
||
const current = glowEdges[key] || 0;
|
||
const newAlpha = current + (target - current) * 0.1;
|
||
if (Math.abs(newAlpha - current) > 0.001) changed = true;
|
||
glowEdges[key] = newAlpha;
|
||
}
|
||
render();
|
||
if (animRunning || changed) requestAnimationFrame(smoothRender);
|
||
}
|
||
|
||
function runActivationAnimation(seedIdx, activation, frames) {
|
||
animRunning = true;
|
||
glowNodes = {};
|
||
glowEdges = {};
|
||
glowTargets = {};
|
||
edgeTargets = {};
|
||
|
||
// Dim all non-seed nodes
|
||
nodes.forEach((n, i) => {
|
||
if (i !== seedIdx) {
|
||
glowTargets[i] = { alpha: 0, strength: 0, isSeed: false };
|
||
}
|
||
});
|
||
|
||
let frameIdx = 0;
|
||
const speedMs = [600, 450, 300, 200, 120][parseInt(speedSlider.value) - 1];
|
||
|
||
function nextFrame() {
|
||
if (frameIdx >= frames.length) {
|
||
animRunning = false;
|
||
animating = false;
|
||
btn.disabled = false;
|
||
btn.textContent = 'Activate';
|
||
return;
|
||
}
|
||
|
||
const frame = frames[frameIdx++];
|
||
|
||
if (frame.type === 'seed') {
|
||
glowTargets[frame.nodeIdx] = { alpha: 1, strength: 1.0, isSeed: true };
|
||
} else if (frame.type === 'edge-activate') {
|
||
const key = `${frame.from}-${frame.to}`;
|
||
edgeTargets[key] = 0.9;
|
||
} else if (frame.type === 'node-activate') {
|
||
const idx = frame.nodeIdx;
|
||
glowTargets[idx] = { alpha: frame.strength * 0.8 + 0.2, strength: frame.strength, hops: frame.hops, isSeed: false };
|
||
}
|
||
|
||
animTimer = setTimeout(nextFrame, frameIdx === 1 ? speedMs * 2 : speedMs);
|
||
}
|
||
|
||
smoothRender();
|
||
nextFrame();
|
||
}
|
||
|
||
btn.addEventListener('click', () => {
|
||
if (selectedSeed === null || animating) return;
|
||
animating = true;
|
||
btn.disabled = true;
|
||
btn.textContent = 'Activating…';
|
||
|
||
const activation = computeActivation(selectedSeed);
|
||
const frames = buildAnimationFrames(selectedSeed, activation);
|
||
runActivationAnimation(selectedSeed, activation, frames);
|
||
});
|
||
|
||
resetBtn.addEventListener('click', () => {
|
||
if (animTimer) clearTimeout(animTimer);
|
||
animating = false;
|
||
animRunning = false;
|
||
selectedSeed = null;
|
||
glowNodes = {};
|
||
glowEdges = {};
|
||
glowTargets = {};
|
||
edgeTargets = {};
|
||
seedIndicator.textContent = 'Click a node to select';
|
||
btn.disabled = true;
|
||
btn.textContent = 'Activate';
|
||
render();
|
||
});
|
||
|
||
canvas.addEventListener('click', (e) => {
|
||
if (animating) return;
|
||
const rect = canvas.getBoundingClientRect();
|
||
const mx = e.clientX - rect.left;
|
||
const my = e.clientY - rect.top;
|
||
|
||
for (const n of nodes) {
|
||
const dx = mx - n.x, dy = my - n.y;
|
||
if (Math.sqrt(dx*dx + dy*dy) < n.r + 6) {
|
||
selectedSeed = n.idx;
|
||
seedIndicator.textContent = n.label.replace('\n', ' ');
|
||
btn.disabled = false;
|
||
// Reset glow state
|
||
glowNodes = {};
|
||
glowEdges = {};
|
||
glowTargets = {};
|
||
edgeTargets = {};
|
||
animRunning = false;
|
||
render();
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
|
||
canvas.style.cursor = 'pointer';
|
||
|
||
window.addEventListener('resize', () => {
|
||
const rect = canvas.getBoundingClientRect();
|
||
W = rect.width;
|
||
H = rect.height;
|
||
canvas.width = W * dpr;
|
||
canvas.height = H * dpr;
|
||
ctx.scale(dpr, dpr);
|
||
layoutNodes();
|
||
render();
|
||
});
|
||
|
||
resize();
|
||
})();
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// SALIENCE SLIDERS
|
||
// ════════════════════════════════════════════════════════════════════
|
||
(function() {
|
||
const slImportance = document.getElementById('sl-importance');
|
||
const slDays = document.getElementById('sl-days');
|
||
const slCount = document.getElementById('sl-count');
|
||
const valImportance = document.getElementById('val-importance');
|
||
const valDays = document.getElementById('val-days');
|
||
const valCount = document.getElementById('val-count');
|
||
const scoreEl = document.getElementById('salience-score');
|
||
const gaugeEl = document.getElementById('salience-gauge-fill');
|
||
|
||
function compute() {
|
||
const importance = parseInt(slImportance.value) / 100;
|
||
const days = parseInt(slDays.value);
|
||
const count = parseInt(slCount.value);
|
||
|
||
valImportance.textContent = importance.toFixed(2);
|
||
valDays.textContent = days;
|
||
valCount.textContent = count;
|
||
|
||
const recency = 1.0 / (1.0 + days);
|
||
const frequency = Math.log(count + 1);
|
||
const salience = importance * recency * frequency;
|
||
|
||
scoreEl.textContent = salience.toFixed(3);
|
||
|
||
// Gauge: normalize to reasonable range (max ~4)
|
||
const pct = Math.min(100, (salience / 4) * 100);
|
||
gaugeEl.style.width = pct + '%';
|
||
|
||
// Color the score
|
||
if (salience > 1.5) {
|
||
scoreEl.style.color = '#28CA41';
|
||
gaugeEl.style.background = '#28CA41';
|
||
} else if (salience > 0.5) {
|
||
scoreEl.style.color = '#7B61FF';
|
||
gaugeEl.style.background = '#7B61FF';
|
||
} else if (salience > 0.1) {
|
||
scoreEl.style.color = '#FFB347';
|
||
gaugeEl.style.background = '#FFB347';
|
||
} else {
|
||
scoreEl.style.color = '#666688';
|
||
gaugeEl.style.background = '#666688';
|
||
}
|
||
}
|
||
|
||
slImportance.addEventListener('input', compute);
|
||
slDays.addEventListener('input', compute);
|
||
slCount.addEventListener('input', compute);
|
||
compute();
|
||
})();
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// TABS
|
||
// ════════════════════════════════════════════════════════════════════
|
||
function switchTab(lang) {
|
||
document.querySelectorAll('.tab-btn').forEach(b => {
|
||
b.classList.toggle('active', b.textContent.toLowerCase() === lang);
|
||
});
|
||
document.querySelectorAll('.tab-content').forEach(c => {
|
||
c.classList.toggle('active', c.id === 'tab-' + lang);
|
||
});
|
||
}
|
||
|
||
// ════════════════════════════════════════════════════════════════════
|
||
// CONSOLIDATION SCROLL ANIMATION
|
||
// ════════════════════════════════════════════════════════════════════
|
||
(function() {
|
||
const diagram = document.getElementById('consol-diagram');
|
||
const consolCanvas = document.getElementById('consol-canvas');
|
||
const consolCtx = consolCanvas.getContext('2d');
|
||
let particles = [];
|
||
let animRunning = false;
|
||
let hasAnimated = false;
|
||
|
||
function resizeConsolCanvas() {
|
||
const rect = diagram.getBoundingClientRect();
|
||
consolCanvas.width = rect.width;
|
||
consolCanvas.height = rect.height;
|
||
}
|
||
|
||
function spawnParticles() {
|
||
// Find episodic box and semantic box positions roughly
|
||
const rect = diagram.getBoundingClientRect();
|
||
const W = consolCanvas.width;
|
||
const H = consolCanvas.height;
|
||
|
||
for (let i = 0; i < 12; i++) {
|
||
const delay = i * 180;
|
||
setTimeout(() => {
|
||
particles.push({
|
||
x: W * 0.15 + Math.random() * W * 0.12,
|
||
y: H * 0.3 + Math.random() * H * 0.4,
|
||
tx: W * 0.73 + Math.random() * W * 0.12,
|
||
ty: H * 0.3 + Math.random() * H * 0.4,
|
||
progress: 0,
|
||
speed: 0.008 + Math.random() * 0.006,
|
||
alpha: 0,
|
||
r: 3 + Math.random() * 2,
|
||
color: i % 2 === 0 ? '#FFB347' : '#7B61FF',
|
||
});
|
||
}, delay);
|
||
}
|
||
}
|
||
|
||
function animateParticles() {
|
||
consolCtx.clearRect(0, 0, consolCanvas.width, consolCanvas.height);
|
||
|
||
let allDone = true;
|
||
for (const p of particles) {
|
||
p.progress += p.speed;
|
||
p.alpha = p.progress < 0.1 ? p.progress * 10 : (p.progress > 0.9 ? (1 - p.progress) * 10 : 1);
|
||
p.alpha = Math.max(0, Math.min(1, p.alpha));
|
||
|
||
const t = p.progress;
|
||
// Bezier arc upward
|
||
const cx = (p.x + p.tx) / 2;
|
||
const cy = Math.min(p.y, p.ty) - consolCanvas.height * 0.18;
|
||
const x = (1-t)*(1-t)*p.x + 2*(1-t)*t*cx + t*t*p.tx;
|
||
const y = (1-t)*(1-t)*p.y + 2*(1-t)*t*cy + t*t*p.ty;
|
||
|
||
consolCtx.beginPath();
|
||
consolCtx.arc(x, y, p.r, 0, Math.PI * 2);
|
||
consolCtx.fillStyle = p.color.replace(')', `,${p.alpha * 0.85})`).replace('rgb', 'rgba');
|
||
|
||
// Handle hex colors
|
||
const hexMatch = p.color.match(/^#([A-Fa-f0-9]{6})$/);
|
||
if (hexMatch) {
|
||
const r = parseInt(hexMatch[1].substr(0,2),16);
|
||
const g = parseInt(hexMatch[1].substr(2,2),16);
|
||
const b = parseInt(hexMatch[1].substr(4,2),16);
|
||
consolCtx.fillStyle = `rgba(${r},${g},${b},${p.alpha * 0.85})`;
|
||
}
|
||
|
||
consolCtx.fill();
|
||
|
||
if (p.progress < 1) allDone = false;
|
||
}
|
||
|
||
if (!allDone || particles.length === 0) {
|
||
requestAnimationFrame(animateParticles);
|
||
} else {
|
||
animRunning = false;
|
||
consolCtx.clearRect(0, 0, consolCanvas.width, consolCanvas.height);
|
||
}
|
||
}
|
||
|
||
const observer = new IntersectionObserver((entries) => {
|
||
for (const entry of entries) {
|
||
if (entry.isIntersecting && !hasAnimated) {
|
||
hasAnimated = true;
|
||
resizeConsolCanvas();
|
||
setTimeout(() => {
|
||
spawnParticles();
|
||
if (!animRunning) {
|
||
animRunning = true;
|
||
animateParticles();
|
||
}
|
||
}, 600);
|
||
// Re-animate every 4 seconds
|
||
setInterval(() => {
|
||
particles = [];
|
||
spawnParticles();
|
||
if (!animRunning) {
|
||
animRunning = true;
|
||
animateParticles();
|
||
}
|
||
}, 4000);
|
||
}
|
||
}
|
||
}, { threshold: 0.4 });
|
||
|
||
observer.observe(diagram);
|
||
resizeConsolCanvas();
|
||
window.addEventListener('resize', resizeConsolCanvas);
|
||
})();
|
||
</script>
|
||
</body>
|
||
</html>
|