diff --git a/style.css b/style.css index 8a10fbe..a5e79d9 100644 --- a/style.css +++ b/style.css @@ -554,9 +554,9 @@ min-height: 0; position: relative; background: - radial-gradient(circle at 52% 34%, var(--bme-primary-dim, rgba(44, 92, 162, 0.16)), transparent 42%), - radial-gradient(circle at 86% 16%, rgba(150, 91, 255, 0.08), transparent 34%), - radial-gradient(circle at 12% 82%, rgba(0, 229, 255, 0.05), transparent 36%), + radial-gradient(circle at 52% 34%, rgba(44, 92, 162, 0.07), transparent 42%), + radial-gradient(circle at 86% 16%, rgba(150, 91, 255, 0.035), transparent 34%), + radial-gradient(circle at 12% 82%, rgba(0, 229, 255, 0.025), transparent 36%), var(--bme-surface-lowest, #0e0e11); } @@ -566,11 +566,11 @@ inset: 0; pointer-events: none; background-image: - radial-gradient(circle, rgba(255, 255, 255, 0.52) 0 1px, transparent 1.4px), - radial-gradient(circle, rgba(124, 248, 255, 0.28) 0 1px, transparent 1.6px); + radial-gradient(circle, rgba(255, 255, 255, 0.28) 0 1px, transparent 1.4px), + radial-gradient(circle, rgba(124, 248, 255, 0.13) 0 1px, transparent 1.6px); background-position: 18px 24px, 68px 86px; background-size: 134px 134px, 211px 211px; - opacity: 0.16; + opacity: 0.11; z-index: 0; } diff --git a/tests/graph-renderer-guardrails.mjs b/tests/graph-renderer-guardrails.mjs index 4a2e063..26f1fa6 100644 --- a/tests/graph-renderer-guardrails.mjs +++ b/tests/graph-renderer-guardrails.mjs @@ -35,6 +35,7 @@ const canvasMockStats = { radialGradientCalls: 0, linearGradientCalls: 0, strokeCalls: 0, + shadowBlurValues: [], }; function flushNextRaf(ms = 16) { @@ -102,7 +103,9 @@ function createNoopContext() { set lineCap(_value) {}, set lineJoin(_value) {}, set shadowColor(_value) {}, - set shadowBlur(_value) {}, + set shadowBlur(value) { + canvasMockStats.shadowBlurValues.push(Number(value) || 0); + }, }; } @@ -176,6 +179,7 @@ function resetCanvasStats() { canvasMockStats.radialGradientCalls = 0; canvasMockStats.linearGradientCalls = 0; canvasMockStats.strokeCalls = 0; + canvasMockStats.shadowBlurValues = []; } function assertRendererNodesInsideRegions(renderer) { @@ -275,12 +279,20 @@ const { GraphRenderer } = await import("../ui/graph-renderer.js"); }); const radius = renderer._nodeRadius({ type: "character", importance: 10 }); + const visualRadius = renderer._nodeVisualRadius({ type: "character", importance: 10 }); assert.equal(radius, 14); + assert.ok(visualRadius <= 8, "visual radius stays small and crisp"); + assert.ok(visualRadius < radius, "visual radius does not affect layout/collision radius"); renderer.loadGraph(graph, { userPovAliases: ["Host"] }); renderer.highlightNode("char-1"); assertInputUnchanged(graph, before); assert.ok(canvasMockStats.radialGradientCalls > 0); assert.ok(canvasMockStats.linearGradientCalls > 0); + assert.equal( + Math.max(0, ...canvasMockStats.shadowBlurValues), + 0, + "node visuals should not reintroduce heavy crystal-ball shadow blur", + ); renderer.destroy(); } diff --git a/ui/graph-renderer.js b/ui/graph-renderer.js index 78d88dc..e1bc41a 100644 --- a/ui/graph-renderer.js +++ b/ui/graph-renderer.js @@ -1569,25 +1569,25 @@ export class GraphRenderer { const cy = my + ny * bend; const relationColor = edgeColorForRelation(edge.relation); - const baseAlpha = sameZone ? 0.035 + strength * 0.09 : 0.026 + strength * 0.07; - const alpha = isDimmed ? 0.018 : (isConnectedToSelection ? 0.38 + strength * 0.28 : baseAlpha); + const baseAlpha = sameZone ? 0.026 + strength * 0.052 : 0.02 + strength * 0.045; + const alpha = isDimmed ? 0.012 : (isConnectedToSelection ? 0.28 + strength * 0.14 : baseAlpha); if (isConnectedToSelection) { ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.quadraticCurveTo(cx, cy, to.x, to.y); - ctx.strokeStyle = colorWithAlpha(relationColor, 0.12 + strength * 0.12); - ctx.lineWidth = 2.8 + strength * 2.2; + ctx.strokeStyle = colorWithAlpha(relationColor, 0.055 + strength * 0.055); + ctx.lineWidth = 1.35 + strength * 0.95; ctx.stroke(); } ctx.beginPath(); ctx.moveTo(from.x, from.y); ctx.quadraticCurveTo(cx, cy, to.x, to.y); - ctx.strokeStyle = colorWithAlpha(isConnectedToSelection ? relationColor : '#c9dcff', alpha); + ctx.strokeStyle = colorWithAlpha(isConnectedToSelection ? relationColor : '#9eb2cf', alpha); ctx.lineWidth = isConnectedToSelection - ? 1.35 + strength * 2.15 - : 0.35 + strength * 0.82; + ? 0.7 + strength * 0.72 + : 0.28 + strength * 0.44; ctx.stroke(); } @@ -1628,7 +1628,10 @@ export class GraphRenderer { const isSelected = node === this.selectedNode; const isHovered = node === this.hoveredNode; const isDimmed = focus.selectedNode && !focus.connectedNodes.has(node); - const r = (isSelected ? baseRadius * 1.12 : baseRadius) * (isDimmed ? 0.6 : 1); + 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 scope = normalizeMemoryScope(node.raw?.scope); const outlineColor = scope.layer === 'pov' @@ -1645,63 +1648,32 @@ export class GraphRenderer { } if (isSelected || isHovered) { - const glowRadius = r + (isSelected ? 20 : 13); ctx.beginPath(); - ctx.arc(node.x, node.y, glowRadius, 0, Math.PI * 2); - ctx.fillStyle = createCanvasGradient( - ctx, - 'createRadialGradient', - [node.x, node.y, Math.max(1, r * 0.45), node.x, node.y, glowRadius], - [ - [0, colorWithAlpha(color, isSelected ? 0.52 : 0.34)], - [0.55, colorWithAlpha(color, isSelected ? 0.22 : 0.14)], - [1, colorWithAlpha(color, 0)], - ], - colorWithAlpha(color, isSelected ? 0.24 : 0.14), - ); - ctx.fill(); - - ctx.beginPath(); - ctx.arc(node.x, node.y, r + (isSelected ? 6 : 4), 0, Math.PI * 2); - ctx.strokeStyle = colorWithAlpha(isSelected ? '#ffffff' : color, isSelected ? 0.72 : 0.5); - ctx.lineWidth = isSelected ? 2.4 : 1.6; + ctx.arc(node.x, node.y, r + (isSelected ? 5.2 : 3.8), 0, Math.PI * 2); + ctx.strokeStyle = colorWithAlpha(color, isSelected ? 0.54 : 0.36); + ctx.lineWidth = isSelected ? 1.05 : 0.85; ctx.stroke(); + + if (isSelected) { + ctx.beginPath(); + ctx.arc(node.x, node.y, r + 8.2, 0, Math.PI * 2); + ctx.strokeStyle = colorWithAlpha('#dbeafe', 0.22); + ctx.lineWidth = 0.75; + ctx.stroke(); + } } ctx.beginPath(); ctx.arc(node.x, node.y, r, 0, Math.PI * 2); - ctx.fillStyle = createCanvasGradient( - ctx, - 'createRadialGradient', - [ - node.x - r * 0.32, - node.y - r * 0.34, - Math.max(1, r * 0.16), - node.x, - node.y, - r, - ], - [ - [0, colorWithAlpha('#ffffff', isSelected ? 0.86 : 0.68)], - [0.18, colorWithAlpha(color, isSelected ? 1 : 0.94)], - [1, colorWithAlpha(color, isSelected ? 0.68 : 0.52)], - ], - colorWithAlpha(color, isSelected ? 1 : 0.86), - ); + ctx.fillStyle = colorWithAlpha(color, isSelected ? 0.96 : (isHovered ? 0.9 : 0.82)); ctx.fill(); - ctx.strokeStyle = isSelected ? '#fff' : colorWithAlpha(outlineColor, isHovered ? 0.9 : 0.64); - ctx.lineWidth = isSelected ? 2.8 : (isHovered ? 1.8 : 1.1); + ctx.strokeStyle = isSelected + ? colorWithAlpha('#eef6ff', 0.72) + : colorWithAlpha(outlineColor, isHovered ? 0.58 : 0.38); + ctx.lineWidth = isSelected ? 1.15 : (isHovered ? 0.95 : 0.65); ctx.stroke(); - ctx.shadowColor = colorWithAlpha(color, isSelected ? 0.5 : 0.2); - ctx.shadowBlur = isSelected ? 16 : 7; - ctx.beginPath(); - ctx.arc(node.x - r * 0.28, node.y - r * 0.34, Math.max(1.4, r * 0.16), 0, Math.PI * 2); - ctx.fillStyle = colorWithAlpha('#ffffff', isSelected ? 0.7 : 0.5); - ctx.fill(); - ctx.shadowBlur = 0; - ctx.font = `${this.config.labelFontSize}px Inter, sans-serif`; ctx.textAlign = 'center'; const rect = node.regionRect; @@ -1720,21 +1692,21 @@ export class GraphRenderer { ); if (isHovered || isSelected) { const metrics = ctx.measureText(labelDraw); - const pillW = Math.min(maxLabelW + 12, metrics.width + 14); - const pillH = 17; + const pillW = Math.min(maxLabelW + 10, metrics.width + 12); + const pillH = 16; const pillX = node.x - pillW / 2; - const pillY = node.y + r + 6; + const pillY = node.y + r + 6.5; ctx.beginPath(); - roundRectPath(ctx, pillX, pillY, pillW, pillH, 8); + roundRectPath(ctx, pillX, pillY, pillW, pillH, 5); ctx.fillStyle = isSelected - ? 'rgba(7, 14, 32, 0.88)' - : 'rgba(7, 14, 32, 0.72)'; + ? 'rgba(8, 10, 16, 0.64)' + : 'rgba(8, 10, 16, 0.52)'; ctx.fill(); - ctx.strokeStyle = colorWithAlpha(color, isSelected ? 0.48 : 0.32); + ctx.strokeStyle = 'rgba(238, 246, 255, 0.09)'; ctx.lineWidth = 1; ctx.stroke(); } - ctx.fillStyle = `rgba(238,247,255,${isHovered || isSelected ? 0.96 : 0.62})`; + ctx.fillStyle = `rgba(218,229,242,${isHovered || isSelected ? 0.88 : 0.52})`; ctx.fillText(labelDraw, node.x, node.y + r + 14); ctx.restore(); } @@ -1754,38 +1726,23 @@ export class GraphRenderer { const kind = highlight.kind || 'recall'; const drawPulse = (color, offset, alphaScale = 1) => { const pulse = reducedMotion ? 0.35 : phase; - const ringRadius = radius + offset + pulse * 11; - const glowRadius = ringRadius + 11; - ctx.beginPath(); - ctx.arc(node.x, node.y, glowRadius, 0, Math.PI * 2); - ctx.fillStyle = createCanvasGradient( - ctx, - 'createRadialGradient', - [node.x, node.y, Math.max(1, radius * 0.8), node.x, node.y, glowRadius], - [ - [0, colorWithAlpha(color, 0.10 * fade * alphaScale)], - [0.58, colorWithAlpha(color, 0.16 * fade * alphaScale)], - [1, colorWithAlpha(color, 0)], - ], - colorWithAlpha(color, 0.08 * fade * alphaScale), - ); - ctx.fill(); + const ringRadius = radius + offset + pulse * 5.5; ctx.beginPath(); ctx.arc(node.x, node.y, ringRadius, 0, Math.PI * 2); - ctx.strokeStyle = colorWithAlpha(color, (0.28 + phase * 0.34) * fade * alphaScale); - ctx.lineWidth = 1.2 + phase * 1.4; + ctx.strokeStyle = colorWithAlpha(color, (0.18 + phase * 0.18) * fade * alphaScale); + ctx.lineWidth = 0.65 + phase * 0.45; ctx.stroke(); }; if (kind === 'mixed') { - drawPulse('#7cf8ff', 7, 1); - drawPulse('#b79cff', 14, 0.88); + drawPulse('#7cf8ff', 4.2, 0.92); + drawPulse('#b79cff', 7.5, 0.72); } else if (kind === 'extracted') { - drawPulse('#b79cff', 9, 0.9); - drawPulse('#75ffb1', 15, 0.5); + drawPulse('#b79cff', 4.8, 0.82); + drawPulse('#75ffb1', 7.8, 0.42); } else { - drawPulse('#7cf8ff', 8, 1); + drawPulse('#7cf8ff', 4.5, 0.9); } } @@ -1866,9 +1823,9 @@ export class GraphRenderer { Math.max(width, height) * 0.82, ], [ - [0, colorWithAlpha(theme.primary || '#224b84', 0.2)], - [0.34, colorWithAlpha(theme.secondary || '#141c48', 0.12)], - [0.72, colorWithAlpha(theme.surfaceLow || '#070b1a', 0.5)], + [0, colorWithAlpha(theme.primary || '#224b84', 0.12)], + [0.34, colorWithAlpha(theme.secondary || '#141c48', 0.075)], + [0.72, colorWithAlpha(theme.surfaceLow || '#070b1a', 0.58)], [1, theme.surfaceLowest || 'rgba(2, 4, 12, 0.96)'], ], theme.surfaceLowest || 'rgba(2, 4, 12, 0.96)', @@ -1887,8 +1844,8 @@ export class GraphRenderer { Math.max(width, height) * 0.46, ], [ - [0, colorWithAlpha(theme.accent3 || '#b073ff', 0.12)], - [0.5, colorWithAlpha(theme.accent2 || '#57c7ff', 0.055)], + [0, colorWithAlpha(theme.accent3 || '#b073ff', 0.055)], + [0.5, colorWithAlpha(theme.accent2 || '#57c7ff', 0.032)], [1, 'rgba(0, 0, 0, 0)'], ], 'rgba(0, 0, 0, 0)', @@ -2053,12 +2010,12 @@ export class GraphRenderer { const importance = Number(node?.importance || 5); const type = String(node?.type || '').toLowerCase(); if (type === 'character' || importance >= 9) { - return Math.min(base * 1.18, base + 4); + return Math.min(8, Math.max(4.8, base * 0.58)); } if (type === 'event' || importance >= 6) { - return Math.min(base * 1.08, base + 2); + return Math.min(7, Math.max(4.2, base * 0.52)); } - return base; + return Math.min(6.2, Math.max(3.4, base * 0.46)); } _ellipsisLabel(ctx, text, maxW) {