mirror of
https://github.com/Youzini-afk/ST-Bionic-Memory-Ecology.git
synced 2026-06-13 18:31:16 +08:00
feat(graph-ui): pulse recall and extraction nodes
This commit is contained in:
@@ -36,6 +36,7 @@ const canvasMockStats = {
|
||||
linearGradientCalls: 0,
|
||||
strokeCalls: 0,
|
||||
shadowBlurValues: [],
|
||||
arcRadii: [],
|
||||
};
|
||||
|
||||
function flushNextRaf(ms = 16) {
|
||||
@@ -84,7 +85,9 @@ function createNoopContext() {
|
||||
translate: noop,
|
||||
scale: noop,
|
||||
beginPath: noop,
|
||||
arc: noop,
|
||||
arc: (_x, _y, radius) => {
|
||||
canvasMockStats.arcRadii.push(Number(radius) || 0);
|
||||
},
|
||||
arcTo: noop,
|
||||
fill: noop,
|
||||
stroke: () => {
|
||||
@@ -194,6 +197,7 @@ function resetCanvasStats() {
|
||||
canvasMockStats.linearGradientCalls = 0;
|
||||
canvasMockStats.strokeCalls = 0;
|
||||
canvasMockStats.shadowBlurValues = [];
|
||||
canvasMockStats.arcRadii = [];
|
||||
}
|
||||
|
||||
function assertRendererNodesInsideRegions(renderer) {
|
||||
@@ -419,6 +423,10 @@ const { GraphRenderer } = await import("../ui/graph-renderer.js");
|
||||
assert.ok(flushNextRaf());
|
||||
assert.ok(canvasMockStats.radialGradientCalls > 0);
|
||||
assert.ok(canvasMockStats.strokeCalls > 0);
|
||||
assert.ok(
|
||||
Math.max(0, ...canvasMockStats.arcRadii) <= 18,
|
||||
"transient recall/extraction highlights stay close to node body, not large crystal-ball rings",
|
||||
);
|
||||
diagnostics = renderer.getTransientHighlightDiagnostics();
|
||||
assert.equal(diagnostics.count, 3);
|
||||
mockNow += 120;
|
||||
|
||||
@@ -1947,8 +1947,9 @@ export class GraphRenderer {
|
||||
const activeRadius = isSelected
|
||||
? Math.min(10, baseRadius * 1.22, baseRadius + 1.8)
|
||||
: (isHovered ? Math.min(9, baseRadius * 1.12, baseRadius + 1.1) : baseRadius);
|
||||
const r = activeRadius * (isDimmed ? 0.62 : 1);
|
||||
const transientHighlight = this._transientHighlights.get(node.id) || null;
|
||||
const transientVisual = this._getTransientHighlightVisual(transientHighlight);
|
||||
const r = activeRadius * (isDimmed ? 0.62 : 1) * transientVisual.scale;
|
||||
const scope = normalizeMemoryScope(node.raw?.scope);
|
||||
const outlineColor = scope.layer === 'pov'
|
||||
? (scope.ownerType === 'user'
|
||||
@@ -1960,7 +1961,7 @@ export class GraphRenderer {
|
||||
if (isDimmed) ctx.globalAlpha = 0.2;
|
||||
|
||||
if (transientHighlight) {
|
||||
this._drawTransientHighlight(ctx, node, r, transientHighlight);
|
||||
this._drawTransientHighlight(ctx, node, r, transientHighlight, transientVisual);
|
||||
}
|
||||
|
||||
if (isSelected || isHovered) {
|
||||
@@ -1981,7 +1982,10 @@ export class GraphRenderer {
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, r, 0, Math.PI * 2);
|
||||
ctx.fillStyle = colorWithAlpha(color, isSelected ? 0.96 : (isHovered ? 0.9 : 0.82));
|
||||
ctx.fillStyle = colorWithAlpha(
|
||||
transientVisual.color || color,
|
||||
Math.min(1, (isSelected ? 0.96 : (isHovered ? 0.9 : 0.82)) * transientVisual.alpha),
|
||||
);
|
||||
ctx.fill();
|
||||
|
||||
ctx.strokeStyle = isSelected
|
||||
@@ -2031,8 +2035,10 @@ export class GraphRenderer {
|
||||
this._afterRenderTransientHighlights();
|
||||
}
|
||||
|
||||
_drawTransientHighlight(ctx, node, radius, highlight) {
|
||||
if (!highlight || !node) return;
|
||||
_getTransientHighlightVisual(highlight) {
|
||||
if (!highlight) {
|
||||
return { scale: 1, alpha: 1, phase: 0, progress: 1, fade: 0, color: null };
|
||||
}
|
||||
const now = this._nowMs();
|
||||
const ttl = Math.max(1, Number(highlight.ttlMs) || 1);
|
||||
const progress = Math.max(0, Math.min(1, (now - Number(highlight.startedAt || now)) / ttl));
|
||||
@@ -2040,25 +2046,69 @@ export class GraphRenderer {
|
||||
const phase = reducedMotion ? 0.55 : (Math.sin(progress * Math.PI * 4) + 1) / 2;
|
||||
const fade = Math.max(0, 1 - progress);
|
||||
const kind = highlight.kind || 'recall';
|
||||
const drawPulse = (color, offset, alphaScale = 1) => {
|
||||
const pulse = reducedMotion ? 0.35 : phase;
|
||||
const ringRadius = radius + offset + pulse * 5.5;
|
||||
if (kind === 'extracted') {
|
||||
const birth = reducedMotion ? 1 : Math.min(1, progress / 0.42);
|
||||
return {
|
||||
scale: 0.64 + birth * 0.5 + phase * 0.16 * fade,
|
||||
alpha: 0.72 + birth * 0.28,
|
||||
phase,
|
||||
progress,
|
||||
fade,
|
||||
color: '#b79cff',
|
||||
};
|
||||
}
|
||||
if (kind === 'mixed') {
|
||||
return {
|
||||
scale: 1.16 + phase * 0.32 * fade,
|
||||
alpha: 1,
|
||||
phase,
|
||||
progress,
|
||||
fade,
|
||||
color: phase > 0.5 ? '#7cf8ff' : '#b79cff',
|
||||
};
|
||||
}
|
||||
return {
|
||||
scale: 1.1 + phase * 0.28 * fade,
|
||||
alpha: 1,
|
||||
phase,
|
||||
progress,
|
||||
fade,
|
||||
color: '#7cf8ff',
|
||||
};
|
||||
}
|
||||
|
||||
_drawTransientHighlight(ctx, node, radius, highlight, visual = null) {
|
||||
if (!highlight || !node) return;
|
||||
const reducedMotion = this._isReducedMotion();
|
||||
const pulse = visual || this._getTransientHighlightVisual(highlight);
|
||||
const phase = pulse.phase;
|
||||
const fade = pulse.fade;
|
||||
const kind = highlight.kind || 'recall';
|
||||
const drawThinPulse = (color, offset, alphaScale = 1) => {
|
||||
const pulseAmount = reducedMotion ? 0.2 : phase;
|
||||
const ringRadius = radius + offset + pulseAmount * 2.4;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, ringRadius, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = colorWithAlpha(color, (0.18 + phase * 0.18) * fade * alphaScale);
|
||||
ctx.lineWidth = 0.65 + phase * 0.45;
|
||||
ctx.strokeStyle = colorWithAlpha(color, (0.09 + phase * 0.1) * fade * alphaScale);
|
||||
ctx.lineWidth = 0.45 + phase * 0.25;
|
||||
ctx.stroke();
|
||||
};
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(node.x, node.y, radius + 1.4 + phase * 1.2, 0, Math.PI * 2);
|
||||
ctx.strokeStyle = colorWithAlpha(pulse.color || '#7cf8ff', (0.2 + phase * 0.16) * fade);
|
||||
ctx.lineWidth = 0.8 + phase * 0.28;
|
||||
ctx.stroke();
|
||||
|
||||
if (kind === 'mixed') {
|
||||
drawPulse('#7cf8ff', 4.2, 0.92);
|
||||
drawPulse('#b79cff', 7.5, 0.72);
|
||||
drawThinPulse('#7cf8ff', 3.8, 0.55);
|
||||
drawThinPulse('#b79cff', 5.6, 0.42);
|
||||
} else if (kind === 'extracted') {
|
||||
drawPulse('#b79cff', 4.8, 0.82);
|
||||
drawPulse('#75ffb1', 7.8, 0.42);
|
||||
drawThinPulse('#b79cff', 3.6, 0.5);
|
||||
drawThinPulse('#75ffb1', 5.4, 0.28);
|
||||
} else {
|
||||
drawPulse('#7cf8ff', 4.5, 0.9);
|
||||
drawThinPulse('#7cf8ff', 4.0, 0.48);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user