Initial commit: 自然记忆站:12 页纯标准库 Python 静态站点生成器,含真实评测台账(108 条)与全站客户端检索
This commit is contained in:
@@ -0,0 +1,256 @@
|
||||
/* ==================================================================
|
||||
Natural Memory — space.js
|
||||
「地址空间下降」—— 本页的招牌机制。
|
||||
|
||||
机制来自题材自身的物理(motion-web: design-slop B5):这套系统的读取路径
|
||||
本来就是一次寻址下降 —— 问题 → 128 维地址 → LSH 粗索引 → 候选页 →
|
||||
页内重排 → Top-K → 证据前缀 → 生成。所以镜头一路下降,而不是在旁边飘粒子。
|
||||
|
||||
实现取 case 的**机制**、不取 case 的皮(cases/ 的照搬边界):
|
||||
滚动进度从文档里读回来((scrollY - offsetTop) / 可滚动距离),不用 sticky 累加 ——
|
||||
同 press-stack 的做法;原生滚动完全不动它。
|
||||
|
||||
全部数字都是真的:1,048,576 槽位 / 738 条活跃记录 / 730 个索引桶 /
|
||||
24 页 × 页容量 32 / 最多 2 条记录 / 前缀 45 token。
|
||||
================================================================== */
|
||||
(function () {
|
||||
"use strict";
|
||||
|
||||
var cv = document.getElementById("spaceCv");
|
||||
if (!cv) return;
|
||||
|
||||
var D = window.NM_DATA;
|
||||
var ctx = cv.getContext("2d");
|
||||
var host = document.getElementById("hero");
|
||||
var pin = cv.parentNode;
|
||||
|
||||
var reduce = !!(window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
||||
var narrow = window.matchMedia && window.matchMedia("(max-width: 900px)").matches;
|
||||
|
||||
/* ---- 真实的地址空间几何 ---- */
|
||||
var GEO = {
|
||||
slots: 1048576, /* 地址空间设计容量 */
|
||||
records: 738, /* 批次 B 实际装载的 active records */
|
||||
buckets: 730, /* LSH 粗索引桶数 */
|
||||
pages: 24, /* 实际使用的页数 */
|
||||
pageCap: 32, /* 页容量 */
|
||||
topK: 2, /* 单次读取最多记录数 */
|
||||
prefix: 45 /* 注入模型内部的前缀 token 数 */
|
||||
};
|
||||
|
||||
/* ---- 可复现的伪随机(不用 Math.random:同一张图每次都一样) ---- */
|
||||
function rng(seed) {
|
||||
var s = seed >>> 0;
|
||||
return function () {
|
||||
s = (s * 1664525 + 1013904223) >>> 0;
|
||||
return s / 4294967296;
|
||||
};
|
||||
}
|
||||
|
||||
var N = 128; /* 空间栅格 128×128 = 16,384 格 */
|
||||
var SPACE = 1024; /* 离屏位图边长 */
|
||||
var CELL = SPACE / N;
|
||||
|
||||
/* 738 条记录散布在 16,384 格里 —— 每格代表 64 个真实槽位 */
|
||||
var lit = [];
|
||||
var rand = rng(20260914);
|
||||
var want = Math.round(N * N * (GEO.records / 16384)); /* ≈ 738 格 */
|
||||
var seen = {};
|
||||
while (lit.length < want) {
|
||||
var i = Math.floor(rand() * N * N);
|
||||
if (seen[i]) continue;
|
||||
seen[i] = 1;
|
||||
lit.push(i);
|
||||
}
|
||||
/* 降落的终点:其中一条记录 */
|
||||
var target = lit[Math.floor(lit.length * 0.42)];
|
||||
var tx = (target % N) * CELL + CELL / 2;
|
||||
var ty = Math.floor(target / N) * CELL + CELL / 2;
|
||||
|
||||
/* ---- 离屏烘焙整张地址空间(每秒重画 16k 个点太贵,烘一次然后只做缩放着色) ---- */
|
||||
var off = document.createElement("canvas");
|
||||
off.width = off.height = SPACE;
|
||||
(function bake() {
|
||||
var g = off.getContext("2d");
|
||||
g.fillStyle = "#000";
|
||||
g.fillRect(0, 0, SPACE, SPACE);
|
||||
/* 底噪:整个地址空间的槽位骨架 */
|
||||
g.fillStyle = "rgba(255,254,244,0.10)";
|
||||
for (var y = 0; y < N; y++) {
|
||||
for (var x = 0; x < N; x++) {
|
||||
g.fillRect(x * CELL + CELL * 0.42, y * CELL + CELL * 0.42, 0.9, 0.9);
|
||||
}
|
||||
}
|
||||
/* 活跃记录 */
|
||||
g.fillStyle = "#fffef4";
|
||||
lit.forEach(function (i) {
|
||||
g.fillRect((i % N) * CELL + CELL * 0.34, Math.floor(i / N) * CELL + CELL * 0.34, 1.7, 1.7);
|
||||
});
|
||||
/* 目标记录:唯一一个用信号色标出的点,全页只在这里出现 */
|
||||
g.fillStyle = "#00ff6a";
|
||||
g.fillRect(tx - 1.6, ty - 1.6, 3.2, 3.2);
|
||||
})();
|
||||
|
||||
/* ---- 八个环节(标签直接取自 data.js 的读取路径,单一来源) ---- */
|
||||
var path = (D && D.readPath && D.readPath.length) ? D.readPath : [];
|
||||
var STEPS = [
|
||||
{ label: "地址空间", value: "1,048,576 槽位 · 738 条活跃记录", zoom: 1.00 },
|
||||
{ label: path[0] ? path[0].label : "用户问题", value: "自然语言,不接触全量记忆", zoom: 0.62 },
|
||||
{ label: path[1] ? path[1].label : "隐状态地址", value: "128 维紧凑查询地址", zoom: 0.38 },
|
||||
{ label: path[2] ? path[2].label : "LSH 粗索引", value: "730 个桶,只取少量候选", zoom: 0.20 },
|
||||
{ label: path[3] ? path[3].label : "候选页", value: "24 页 × 页容量 32", zoom: 0.105 },
|
||||
{ label: path[4] ? path[4].label : "页内重排", value: "页内逐条打分", zoom: 0.052 },
|
||||
{ label: path[5] ? path[5].label : "Top-K 记录", value: "最多 2 条", zoom: 0.026 },
|
||||
{ label: path[6] ? path[6].label : "证据前缀", value: "45 token 注入模型内部", zoom: 0.014 }
|
||||
];
|
||||
|
||||
var W = 0, H = 0, dpr = 1;
|
||||
function resize() {
|
||||
dpr = Math.min(window.devicePixelRatio || 1, 2);
|
||||
W = pin.clientWidth;
|
||||
H = pin.clientHeight;
|
||||
cv.width = Math.round(W * dpr);
|
||||
cv.height = Math.round(H * dpr);
|
||||
cv.style.width = W + "px";
|
||||
cv.style.height = H + "px";
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
ctx.imageSmoothingEnabled = true;
|
||||
}
|
||||
|
||||
/* ---- t ∈ [0,1] → 画一帧 ---- */
|
||||
function draw(t) {
|
||||
if (!W || !H) return;
|
||||
ctx.clearRect(0, 0, W, H);
|
||||
ctx.fillStyle = "#000";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
/* 镜头窗口:从整张空间缩到 32×32(约五个数量级) */
|
||||
var i = Math.min(STEPS.length - 1, Math.max(0, t * STEPS.length));
|
||||
var a = STEPS[Math.floor(i)];
|
||||
var b = STEPS[Math.min(STEPS.length - 1, Math.floor(i) + 1)];
|
||||
var f = i - Math.floor(i);
|
||||
var e = f * f * (3 - 2 * f); /* smoothstep,镜头不做生硬跳变 */
|
||||
var zoom = a.zoom + (b.zoom - a.zoom) * e;
|
||||
|
||||
/* 保持长宽比并保证源矩形始终落在图内 */
|
||||
var sw, sh;
|
||||
if (W >= H) { sw = SPACE * zoom; sh = sw * (H / W); }
|
||||
else { sh = SPACE * zoom; sw = sh * (W / H); }
|
||||
var cx = (1 - e) * (SPACE / 2) + e * tx;
|
||||
var cy = (1 - e) * (SPACE / 2) + e * ty;
|
||||
var sx = Math.max(0, Math.min(SPACE - sw, cx - sw / 2));
|
||||
var sy = Math.max(0, Math.min(SPACE - sh, cy - sh / 2));
|
||||
|
||||
ctx.globalAlpha = 1;
|
||||
ctx.drawImage(off, sx, sy, sw, sh, 0, 0, W, H);
|
||||
|
||||
/* 下降越深,越暗:让叠加的字有安静的地面(page-design §3) */
|
||||
var dim = Math.min(0.84, 0.30 + t * 0.62);
|
||||
ctx.fillStyle = "rgba(0,0,0," + dim.toFixed(3) + ")";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
/* 越深,槽位骨架的网格线越显形 —— 空间「resolved」成结构 */
|
||||
if (t > 0.30) {
|
||||
var ga = Math.min(0.22, (t - 0.30) * 0.5);
|
||||
ctx.strokeStyle = "rgba(255,254,244," + ga.toFixed(3) + ")";
|
||||
ctx.lineWidth = 1;
|
||||
var step = Math.max(18, 46 / (0.4 + t));
|
||||
var ox = ((tx - sx) / sw) * W, oy = ((ty - sy) / sh) * H;
|
||||
ctx.beginPath();
|
||||
for (var x = ox % step; x < W; x += step) { ctx.moveTo(Math.round(x) + .5, 0); ctx.lineTo(Math.round(x) + .5, H); }
|
||||
for (var y = oy % step; y < H; y += step) { ctx.moveTo(0, Math.round(y) + .5); ctx.lineTo(W, Math.round(y) + .5); }
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
/* 终点:目标记录被圈出来(信号色,全页唯一一次) */
|
||||
if (t > 0.62) {
|
||||
var pa = Math.min(1, (t - 0.62) / 0.18);
|
||||
var px = ((tx - sx) / sw) * W, py = ((ty - sy) / sh) * H;
|
||||
var r = 10 + 26 * (1 - pa) + 8 * Math.sin(performance.now() / 620);
|
||||
ctx.strokeStyle = "rgba(0,255,106," + (0.90 * pa).toFixed(3) + ")";
|
||||
ctx.lineWidth = 1.25;
|
||||
ctx.beginPath(); ctx.arc(px, py, r, 0, Math.PI * 2); ctx.stroke();
|
||||
ctx.beginPath(); ctx.arc(px, py, r * 0.34, 0, Math.PI * 2); ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
/* ---- HUD:把当前环节读出来(同时是这台机器的探针面) ---- */
|
||||
var hudIdx = document.getElementById("hudIdx");
|
||||
var hudName = document.getElementById("hudName");
|
||||
var hudVal = document.getElementById("hudVal");
|
||||
var hudBar = document.getElementById("hudBar");
|
||||
var lastStep = -1;
|
||||
function hud(t) {
|
||||
var s = Math.min(STEPS.length - 1, Math.floor(t * STEPS.length));
|
||||
if (s === lastStep) return;
|
||||
lastStep = s;
|
||||
var st = STEPS[s];
|
||||
if (hudIdx) hudIdx.textContent = String(s + 1).padStart(2, "0") + " / " + String(STEPS.length).padStart(2, "0");
|
||||
if (hudName) hudName.textContent = st.label;
|
||||
if (hudVal) hudVal.textContent = st.value;
|
||||
}
|
||||
|
||||
/* ---- 一个循环:只在首屏可见时跑,不可见就停(省电,也便于量 FPS) ---- */
|
||||
var t = 0, raf = 0, visible = true, lastFrame = 0;
|
||||
function progress() {
|
||||
var r = host.getBoundingClientRect();
|
||||
var span = host.offsetHeight - pin.offsetHeight;
|
||||
if (span <= 0) return 0;
|
||||
return Math.max(0, Math.min(1, -r.top / span));
|
||||
}
|
||||
function step() {
|
||||
var want = progress();
|
||||
t += (want - t) * 0.12; /* 指数趋近:帧率无关的近似 */
|
||||
if (Math.abs(want - t) < 0.0004) t = want;
|
||||
draw(t);
|
||||
hud(t);
|
||||
if (hudBar) hudBar.style.width = (t * 100).toFixed(1) + "%";
|
||||
lastFrame = (window.performance || Date).now();
|
||||
}
|
||||
function loop() { raf = 0; step(); if (visible) raf = requestAnimationFrame(loop); }
|
||||
/* rAF 会被节流或冻结(后台标签页、无头虚拟时间、低端 WebView)。
|
||||
只有句柄没有帧会让循环永久卡住,所以超时就同步走一帧 ——
|
||||
滚动事件本身就能把画面推着走。 */
|
||||
function kick() {
|
||||
if (!visible) return;
|
||||
var now = (window.performance || Date).now();
|
||||
if (raf && now - lastFrame < 140) return;
|
||||
step();
|
||||
if (!raf) raf = requestAnimationFrame(loop);
|
||||
}
|
||||
|
||||
/* ---- 静态帧:reduced-motion 与窄屏都走这里 ---- */
|
||||
function still() {
|
||||
document.documentElement.classList.add("space--still");
|
||||
resize();
|
||||
var frozen = narrow ? 0.30 : 0.52;
|
||||
draw(frozen);
|
||||
hud(frozen);
|
||||
}
|
||||
|
||||
/* ---- 探针面(verification-harness §1):能被 seek / hold,才好量 ---- */
|
||||
window.__space = {
|
||||
seek: function (v) { t = Math.max(0, Math.min(1, v)); draw(t); hud(t); },
|
||||
hold: function () { if (raf) { cancelAnimationFrame(raf); raf = 0; } },
|
||||
progress: progress,
|
||||
steps: STEPS.length,
|
||||
geo: GEO,
|
||||
t: function () { return t; }
|
||||
};
|
||||
|
||||
if (reduce || narrow) {
|
||||
still();
|
||||
window.addEventListener("resize", function () { resize(); draw(narrow ? 0.30 : 0.52); });
|
||||
} else {
|
||||
resize();
|
||||
if ("IntersectionObserver" in window) {
|
||||
new IntersectionObserver(function (es) {
|
||||
visible = es[0].isIntersecting;
|
||||
if (visible) kick(); else if (raf) { cancelAnimationFrame(raf); raf = 0; }
|
||||
}, { rootMargin: "120px" }).observe(host);
|
||||
}
|
||||
window.addEventListener("scroll", kick, { passive: true });
|
||||
window.addEventListener("resize", function () { resize(); kick(); });
|
||||
kick(); /* 循环常转:静止时目标点的呼吸也要活着 */
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user