Initial commit: RHINE LAB · ANALYSIS OS:三维界面与动效实验(本仓库不含个人归档索引数据)

This commit is contained in:
WpyQwq
2026-09-19 13:04:56 +08:00
commit 3f422218a5
1098 changed files with 39410 additions and 0 deletions
+147
View File
@@ -0,0 +1,147 @@
import assert from "node:assert/strict";
import {
archiveWave,
extraction,
selectionWave,
baselineSelectionWave,
rippleEnvelope,
settlingWave,
damp,
idleWave,
} from "../src/motion.ts";
import { selectionWave as historicalWave } from "../reference/baseline-motion.ts";
// The selected production motion is the original signed wave, including troughs.
let baselineTrough = false;
for (let frame = -1; frame <= 200; frame++) {
for (let distance = 0; distance <= 32; distance += 0.5) {
const restored = baselineSelectionWave(distance, frame / 60);
assert.equal(restored, historicalWave(distance, frame / 60));
baselineTrough ||= restored < -0.01;
}
}
assert.ok(baselineTrough, "The original negative trough is restored");
let idleRange = 0;
for (let lane = 0; lane < 5; lane++) {
for (let row = 0; row < 32; row++) {
for (let frame = 0; frame < 60 * 13; frame++) {
const a = idleWave(row, lane, frame / 60);
const b = idleWave(row, lane, (frame + 1) / 60);
idleRange = Math.max(idleRange, Math.abs(a));
assert.ok(
Math.abs(a) < 3.7 * 0.03,
"Idle lift stays below 3% of card height",
);
assert.ok(
Math.abs(b - a) * (1080 / 7.33) < 0.21,
"Idle motion remains subpixel per frame",
);
}
}
}
assert.ok(idleRange > 0.075, "Idle field remains perceptible without input");
const peak = (t) =>
Array.from({ length: 32 }, (_, row) => archiveWave(row, 2, t)).reduce(
(best, y, row, values) => (y > values[best] ? row : best),
0,
);
assert.ok(peak(23.3) > peak(22.7) + 6, "First crest must travel across rows");
assert.ok(peak(24.8) < peak(24.2) - 8, "Second crest must return across rows");
let maxFrameDelta = 0;
for (let frame = 550; frame < 800; frame++) {
for (let row = 0; row < 32; row++)
for (let lane = 0; lane < 5; lane++) {
const a = archiveWave(row, lane, frame / 25);
const b = archiveWave(row, lane, (frame + 1) / 25);
assert.ok(Number.isFinite(a));
maxFrameDelta = Math.max(maxFrameDelta, Math.abs(b - a));
}
}
assert.ok(maxFrameDelta < 0.7, "25 fps samples must not teleport");
assert.ok(
Math.abs(extraction(26.6) - extraction(27.2)) < 0.01,
"Pause between extraction phases",
);
assert.ok(extraction(29) > 3 && extraction(25.1) === 0);
assert.ok(
Math.abs(settlingWave(2, 26.1) - settlingWave(2, 26.5)) > 0.01,
"Neighbors keep moving during the first extraction hold",
);
assert.ok(selectionWave(8, 1) > 0.1, "Click ripple reaches neighboring rows");
for (let frame = 0; frame <= 200; frame++) {
for (let distance = 0; distance <= 32; distance += 0.5) {
const y = selectionWave(distance, frame / 60);
const age = frame / 60;
const ramp = Math.max(0, Math.min(1, age / 0.2));
const original =
0.8 *
ramp ** 3 *
(10 + ramp * (-15 + 6 * ramp)) *
Math.exp(-age * 1.15) *
Math.cos((distance - age * 8) * 0.58) *
Math.exp(-0.5 * ((distance - age * 8) / 3.4) ** 2);
assert.ok(
y >= 0 && y <= 0.8,
"Selection pulse cannot create a negative trough",
);
if (age <= 3.2 && original > 0)
assert.ok(
Math.abs(y - original) < 1e-12,
"Positive crests retain the baseline amplitude and timing",
);
}
}
const ripple = (distance, age) =>
selectionWave(distance, age) * rippleEnvelope(distance, age);
for (let frame = 0; frame <= 200; frame++) {
assert.equal(
ripple(0, frame / 60),
0,
"The selected source cannot bounce on its own ripple",
);
}
for (const distance of [3, 5, 8, 12]) {
const crestTime = distance / 8;
assert.equal(
ripple(distance, crestTime),
selectionWave(distance, crestTime),
"The outward crest keeps its strength away from the source",
);
const edge = (distance - Math.PI / (2 * 0.58)) / 8;
const epsilon = 1e-5;
const velocity =
(ripple(distance, edge + epsilon) - ripple(distance, edge - epsilon)) /
(2 * epsilon);
assert.ok(
Math.abs(velocity) < 0.001,
"Ripple edges approach rest without a velocity snap",
);
}
const coarse = { value: 5, velocity: -2 },
fine = { ...coarse };
for (let i = 0; i < 30; i++) damp(coarse, -3, 4, 1 / 30);
for (let i = 0; i < 120; i++) damp(fine, -3, 4, 1 / 120);
assert.ok(
Math.abs(coarse.value - fine.value) < 1e-9,
"Spring must be frame-rate independent",
);
const before = coarse.value;
damp(coarse, 6, 4, 1 / 120);
assert.ok(
Math.abs(coarse.value - before) < 0.05,
"Retargeting must preserve position continuity",
);
console.log(
JSON.stringify(
{
forwardPeaks: [peak(22.7), peak(23.3)],
returnPeaks: [peak(24.2), peak(24.8)],
maxFrameDelta,
checks: "passed",
},
null,
2,
),
);